replay-viewer
Version:
Rocket League replay viewer React component and tooling
126 lines • 6.58 kB
JavaScript
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