@cesium/engine
Version:
CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.
295 lines (262 loc) • 8.74 kB
JavaScript
import Cartesian3 from "../../Core/Cartesian3.js";
import Check from "../../Core/Check.js";
import ConstantSpline from "../../Core/ConstantSpline.js";
import Frozen from "../../Core/Frozen.js";
import defined from "../../Core/defined.js";
import HermiteSpline from "../../Core/HermiteSpline.js";
import InterpolationType from "../../Core/InterpolationType.js";
import LinearSpline from "../../Core/LinearSpline.js";
import ModelComponents from "../ModelComponents.js";
import SteppedSpline from "../../Core/SteppedSpline.js";
import Quaternion from "../../Core/Quaternion.js";
import QuaternionSpline from "../../Core/QuaternionSpline.js";
const AnimatedPropertyType = ModelComponents.AnimatedPropertyType;
/**
* A runtime animation channel for a {@link ModelAnimation}. An animation
* channel is responsible for interpolating between the keyframe values of an animated
* property, then applying the change to the target node.
*
* @param {object} options An object containing the following options:
* @param {ModelComponents.AnimationChannel} options.channel The corresponding animation channel components from the 3D model.
* @param {ModelAnimation} options.runtimeAnimation The runtime animation containing this channel.
* @param {ModelRuntimeNode} options.runtimeNode The runtime node that this channel will animate.
*
* @alias ModelAnimationChannel
* @constructor
*
* @private
*/
function ModelAnimationChannel(options) {
options = options ?? Frozen.EMPTY_OBJECT;
const channel = options.channel;
const runtimeAnimation = options.runtimeAnimation;
const runtimeNode = options.runtimeNode;
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("options.channel", channel);
Check.typeOf.object("options.runtimeAnimation", runtimeAnimation);
Check.typeOf.object("options.runtimeNode", runtimeNode);
//>>includeEnd('debug');
this._channel = channel;
this._runtimeAnimation = runtimeAnimation;
this._runtimeNode = runtimeNode;
// An animation channel can have multiple splines if it animates
// a node's morph weights, which will involve multiple morph targets.
this._splines = [];
this._path = undefined;
initialize(this);
}
Object.defineProperties(ModelAnimationChannel.prototype, {
/**
* The glTF animation channel.
*
* @memberof ModelAnimationChannel.prototype
*
* @type {ModelComponents.AnimationChannel}
* @readonly
*
* @private
*/
channel: {
get: function () {
return this._channel;
},
},
/**
* The runtime animation that owns this channel.
*
* @memberof ModelAnimationChannel.prototype
*
* @type {ModelAnimation}
* @readonly
*
* @private
*/
runtimeAnimation: {
get: function () {
return this._runtimeAnimation;
},
},
/**
* The runtime node that this channel animates.
*
* @memberof ModelAnimationChannel.prototype
*
* @type {ModelRuntimeNode}
* @readonly
*
* @private
*/
runtimeNode: {
get: function () {
return this._runtimeNode;
},
},
/**
* The splines used to evaluate this animation channel.
*
* @memberof ModelAnimationChannel.prototype
*
* @type {Spline[]}
* @readonly
*
* @private
*/
splines: {
get: function () {
return this._splines;
},
},
});
function createCubicSpline(times, points) {
const cubicPoints = [];
const inTangents = [];
const outTangents = [];
const length = points.length;
for (let i = 0; i < length; i += 3) {
inTangents.push(points[i]);
cubicPoints.push(points[i + 1]);
outTangents.push(points[i + 2]);
}
// Remove the first in-tangent and last out-tangent, since they
// are not used in the spline calculations
inTangents.splice(0, 1);
outTangents.length = outTangents.length - 1;
return new HermiteSpline({
times: times,
points: cubicPoints,
inTangents: inTangents,
outTangents: outTangents,
});
}
function createSpline(times, points, interpolation, path) {
if (times.length === 1 && points.length === 1) {
return new ConstantSpline(points[0]);
}
switch (interpolation) {
case InterpolationType.STEP:
return new SteppedSpline({
times: times,
points: points,
});
case InterpolationType.CUBICSPLINE:
return createCubicSpline(times, points);
case InterpolationType.LINEAR:
if (path === AnimatedPropertyType.ROTATION) {
return new QuaternionSpline({
times: times,
points: points,
});
}
return new LinearSpline({
times: times,
points: points,
});
}
}
function createSplines(times, points, interpolation, path, count) {
const splines = [];
if (path === AnimatedPropertyType.WEIGHTS) {
const pointsLength = points.length;
// Get the number of keyframes in each weight's output.
const outputLength = pointsLength / count;
// Iterate over the array using the number of morph targets in the model.
let targetIndex, i;
for (targetIndex = 0; targetIndex < count; targetIndex++) {
const output = new Array(outputLength);
// Weights are ordered such that they are keyframed in the order in which
// their targets appear the glTF. For example, the weights of three targets
// may appear as [w(0,0), w(0,1), w(0,2), w(1,0), w(1,1), w(1,2) ...],
// where i and j in w(i,j) are the time indices and target indices, respectively.
// However, for morph targets with cubic interpolation, the data is stored per
// keyframe in the order [a1, a2, ..., an, v1, v2, ... vn, b1, b2, ..., bn],
// where ai, vi, and bi are the in-tangent, property, and out-tangents of
// the ith morph target respectively.
let pointsIndex = targetIndex;
if (interpolation === InterpolationType.CUBICSPLINE) {
for (i = 0; i < outputLength; i += 3) {
output[i] = points[pointsIndex];
output[i + 1] = points[pointsIndex + count];
output[i + 2] = points[pointsIndex + 2 * count];
pointsIndex += count * 3;
}
} else {
for (i = 0; i < outputLength; i++) {
output[i] = points[pointsIndex];
pointsIndex += count;
}
}
splines.push(createSpline(times, output, interpolation, path));
}
} else {
splines.push(createSpline(times, points, interpolation, path));
}
return splines;
}
const scratchCartesian3 = new Cartesian3();
const scratchQuaternion = new Quaternion();
function initialize(runtimeChannel) {
const channel = runtimeChannel._channel;
const sampler = channel.sampler;
const times = sampler.input;
const points = sampler.output;
const interpolation = sampler.interpolation;
const target = channel.target;
const path = target.path;
const runtimeNode = runtimeChannel._runtimeNode;
const count = defined(runtimeNode.morphWeights)
? runtimeNode.morphWeights.length
: 1;
const splines = createSplines(times, points, interpolation, path, count);
runtimeChannel._splines = splines;
runtimeChannel._path = path;
}
/**
* Animates the target node property based on its spline.
*
* @param {number} time The local animation time.
*
* @private
*/
ModelAnimationChannel.prototype.animate = function (time) {
const splines = this._splines;
const path = this._path;
const model = this._runtimeAnimation.model;
const runtimeNode = this._runtimeNode;
// Weights are handled differently than the other properties because
// they need to be updated in place.
if (path === AnimatedPropertyType.WEIGHTS) {
const morphWeights = runtimeNode.morphWeights;
const length = morphWeights.length;
for (let i = 0; i < length; i++) {
const spline = splines[i];
const localAnimationTime = model.clampAnimations
? spline.clampTime(time)
: spline.wrapTime(time);
morphWeights[i] = spline.evaluate(localAnimationTime);
}
} else if (runtimeNode.userAnimated) {
// If the node is being animated externally, ignore the glTF animation.
return;
} else {
const spline = splines[0];
const localAnimationTime = model.clampAnimations
? spline.clampTime(time)
: spline.wrapTime(time);
// This sets the translate, rotate, and scale properties.
if (
path === AnimatedPropertyType.TRANSLATION ||
path === AnimatedPropertyType.SCALE
) {
runtimeNode[path] = spline.evaluate(
localAnimationTime,
scratchCartesian3,
);
} else if (path === AnimatedPropertyType.ROTATION) {
runtimeNode[path] = spline.evaluate(
localAnimationTime,
scratchQuaternion,
);
}
}
};
export default ModelAnimationChannel;