UNPKG

replay-viewer

Version:

Rocket League replay viewer React component and tooling

126 lines 6.58 kB
import { AnimationClip, AnimationMixer, Euler, Quaternion, QuaternionKeyframeTrack, Vector3, VectorKeyframeTrack, } from "three"; import { BALL } from "../constants/gameObjectNames"; import AnimationManager from "../managers/AnimationManager"; import { getActionClipName, getPositionName, getRotationName, } from "./utils/animationNameGetters"; import { getCarName, getGroupName } from "./utils/playerNameGetters"; /** * Class is responsible for all position and rotation-based updating of models that occur inside * the three.js scene. Builds animation mixers that are used to display animations but do not * directly interact with the models themselves outside of the required naming conventions that * keyframe tracks provide. */ var defaultAnimationBuilder = function (replayData, playerModels, ballModel, useBallRotation) { if (useBallRotation === void 0) { useBallRotation = true; } /** * Replay data is of this form: * [posX, posZ, posY, rotX, rotZ, royY] * * Three is RH as opposed to Unreal/Unity's LH axes and uses y as the up axis. All angles * are in the range -PI to PI. * * For parsed data information, see: * https://github.com/SaltieRL/carball/blob/master/carball/json_parser/actor_parsing.py#L107 * */ var dataToVector = function (data) { var x = data[0]; var y = data[2]; var z = data[1]; return new Vector3(x, y, z); }; var dataToQuaternion = function (data) { var q = new Quaternion(); var x = -data[3]; var y = -data[5]; var z = -data[4]; q.setFromEuler(new Euler(y, z, x, "YZX")); return q; }; var generateKeyframeData = function (posRotData) { var positions = []; var rotations = []; /** * We are calculating vector and quaternion times independently because there are often * cases where the data of a frame might coincide with the data of the next frame. The * replays may not contain data for every frame, so carball inserts the previous frame * data into the following frame as to avoid any missing frames. Here, we assume the * entire duration of that missing frame must be animated. * * For example, if a car's position at 1.1 seconds reads (45, 100, 500), at 1.2 seconds * reads (45, 100, 500), and at 1.3 seconds reads (50, 110, 400), we can assume the * middle frame was skipped and so we need to perform animation between 1.1 seconds and * 1.3 seconds instead of including the jitter of the car remaining in position from 1.1 * seconds to 1.2 seconds. * * We perform the second duration vs. last frame calculation check because we consider * kickoffs as a valid reason to not treat the first position traveled to as a valid * three second animation. Otherwise, the car will slowly creep forward during kickoff. * A kickoff occurs for about three seconds, so 2.9 ensures we break off just before a * kickoff occurs. */ var totalDuration = 0; var positionTimes = []; var rotationTimes = []; var prevVector = new Vector3(0, 0, 0); var prevQuat = new Quaternion(0, 0, 0, 0); posRotData.forEach(function (data, index) { // Apply position frame var newVector = dataToVector(data); var lastVectorFrame = positionTimes.length ? positionTimes[positionTimes.length - 1] : 0; if (!newVector.equals(prevVector) || totalDuration - lastVectorFrame > 2.9) { newVector.toArray(positions, positions.length); positionTimes.push(totalDuration); prevVector = newVector; } // Apply rotation frame var newQuat = dataToQuaternion(data); var lastQuatFrame = rotationTimes.length ? rotationTimes[rotationTimes.length - 1] : 0; if (!newQuat.equals(prevQuat) || totalDuration - lastQuatFrame > 2.9) { newQuat.toArray(rotations, rotations.length); rotationTimes.push(totalDuration); prevQuat = newQuat; } // Add the delta totalDuration += replayData.frames[index][0]; }); return { duration: totalDuration, positionTimes: positionTimes, positionValues: positions, rotationTimes: rotationTimes, rotationValues: rotations, }; }; var playerClips = []; // First, generate player clips for (var player = 0; player < replayData.players.length; player++) { var playerData = replayData.players[player]; var playerName = "".concat(replayData.names[player]); var playerKeyframeData = generateKeyframeData(playerData); // Note that Three.JS requires this .position/.quaternion naming convention, and that // the object we wish to modify must have this associated name. var playerPosKeyframes = new VectorKeyframeTrack(getPositionName(getGroupName(playerName)), playerKeyframeData.positionTimes, playerKeyframeData.positionValues); var playerRotKeyframes = new QuaternionKeyframeTrack(getRotationName(getCarName(playerName)), playerKeyframeData.rotationTimes, playerKeyframeData.rotationValues); var playerClip = new AnimationClip(getActionClipName(playerName), playerKeyframeData.duration, [playerPosKeyframes, playerRotKeyframes]); playerClips.push(playerClip); } // Then, generate the ball clip var ballData = replayData.ball; var ballKeyframeData = generateKeyframeData(ballData); var ballPosKeyframes = new VectorKeyframeTrack(getPositionName(BALL), ballKeyframeData.positionTimes, ballKeyframeData.positionValues); var ballRotKeyframes = new QuaternionKeyframeTrack(getRotationName(BALL), ballKeyframeData.rotationTimes, ballKeyframeData.rotationValues); var ballClip = new AnimationClip(getActionClipName(BALL), ballKeyframeData.duration, useBallRotation ? [ballPosKeyframes, ballRotKeyframes] : [ballPosKeyframes]); return AnimationManager.init({ playerClips: playerClips, ballClip: ballClip, playerMixers: playerModels.map(function (model) { return new AnimationMixer(model.carGroup); }), ballMixer: new AnimationMixer(ballModel.ball), }); }; export default defaultAnimationBuilder; //# sourceMappingURL=AnimationBuilder.js.map