Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for explicit control over model animations #9339

Merged
merged 6 commits into from
May 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions Apps/Sandcastle/gallery/Manually Controlled Animation.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<!DOCTYPE html>
j9liu marked this conversation as resolved.
Show resolved Hide resolved
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"
/>
<meta name="description" content="Manually control a model's animations." />
<meta name="cesium-sandcastle-labels" content="Showcases" />
<title>Cesium Demo</title>
<script type="text/javascript" src="../Sandcastle-header.js"></script>
<script
type="text/javascript"
src="../../../Build/CesiumUnminified/Cesium.js"
nomodule
></script>
<script type="module" src="../load-cesium-es6.js"></script>
</head>
<body
class="sandcastle-loading"
data-sandcastle-bucket="bucket-requirejs.html"
>
<style>
@import url(../templates/bucket.css);
</style>
<div id="cesiumContainer" class="fullSize"></div>
<div id="loadingOverlay"><h1>Loading...</h1></div>
<div id="toolbar"></div>
<script id="cesium_sandcastle_script">
function startup(Cesium) {
"use strict";
//Sandcastle_Begin
const viewer = new Cesium.Viewer("cesiumContainer", {
shouldAnimate: true,
});

//Make sure viewer is at the desired time.
const start = Cesium.JulianDate.fromDate(new Date(2018, 11, 12, 15));
const totalSeconds = 30;
const stop = Cesium.JulianDate.addSeconds(
start,
totalSeconds,
new Cesium.JulianDate()
);
viewer.clock.startTime = start.clone();
viewer.clock.stopTime = stop.clone();
viewer.clock.currentTime = start.clone();
viewer.clock.clockRange = Cesium.ClockRange.LOOP_STOP;
viewer.timeline.zoomTo(start, stop);

// Create a path for our model by lerping between two positions.
const position = new Cesium.SampledPositionProperty();
const distance = new Cesium.SampledProperty(Number);
const startPosition = new Cesium.Cartesian3(
-2379556.799372864,
-4665528.205030263,
3628013.106599678
);
const endPosition = new Cesium.Cartesian3(
-2379603.7074103747,
-4665623.48990283,
3627860.82704567
);
// A velocity vector property will give us the entity's speed and direction at any given time.
const velocityVectorProperty = new Cesium.VelocityVectorProperty(
position,
false
);
const velocityVector = new Cesium.Cartesian3();

const numberOfSamples = 100;
let prevLocation = startPosition;
let totalDistance = 0;
for (let i = 0; i <= numberOfSamples; ++i) {
const factor = i / numberOfSamples;
const time = Cesium.JulianDate.addSeconds(
start,
factor * totalSeconds,
new Cesium.JulianDate()
);

// Lerp using a non-linear factor so that the model accelerates.
const locationFactor = Math.pow(factor, 2);
const location = Cesium.Cartesian3.lerp(
startPosition,
endPosition,
locationFactor,
new Cesium.Cartesian3()
);
position.addSample(time, location);
distance.addSample(
time,
(totalDistance += Cesium.Cartesian3.distance(
location,
prevLocation
))
);
prevLocation = location;
}

function updateSpeedLabel(time, result) {
velocityVectorProperty.getValue(time, velocityVector);
const metersPerSecond = Cesium.Cartesian3.magnitude(velocityVector);
const kmPerHour = Math.round(metersPerSecond * 3.6);

return `${kmPerHour} km/hr`;
}

// Add our model.
const modelPrimitive = viewer.scene.primitives.add(
Cesium.Model.fromGltf({
url: "../../SampleData/models/CesiumMan/Cesium_Man.glb",
scale: 4,
})
);
const modelLabel = viewer.entities.add({
position: position,
orientation: new Cesium.VelocityOrientationProperty(position), // Automatically set the model's orientation to the direction it's facing.
label: {
text: new Cesium.CallbackProperty(updateSpeedLabel, false),
font: "20px sans-serif",
showBackground: true,
distanceDisplayCondition: new Cesium.DistanceDisplayCondition(
0.0,
100.0
),
eyeOffset: new Cesium.Cartesian3(0, 7.2, 0),
},
});

modelPrimitive.readyPromise.then(function (model) {
model.activeAnimations.addAll({
loop: Cesium.ModelAnimationLoop.REPEAT,
animationTime: function (duration) {
return distance.getValue(viewer.clock.currentTime) / duration;
},
multiplier: 0.25,
});
const rot = new Cesium.Matrix3();
viewer.scene.preUpdate.addEventListener(function () {
const time = viewer.clock.currentTime;
const pos = position.getValue(time);
const vel = velocityVectorProperty.getValue(time);
Cesium.Cartesian3.normalize(vel, vel);
Cesium.Transforms.rotationMatrixFromPositionVelocity(
pos,
vel,
viewer.scene.globe.ellipsoid,
rot
);
Cesium.Matrix4.fromRotationTranslation(rot, pos, model.modelMatrix);
});
});
viewer.trackedEntity = modelLabel;
modelLabel.viewFrom = new Cesium.Cartesian3(-30.0, -10.0, 10.0);
//Sandcastle_End
Sandcastle.finishedLoading();
}
if (typeof Cesium !== "undefined") {
window.startupCalled = true;
startup(Cesium);
}
</script>
</body>
</html>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

- Added `Cesium3DTileStyle.fromUrl` for loading a style from a url. [#10348](https://github.com/CesiumGS/cesium/pull/10348)
- Added `IndexDatatype.fromTypedArray`. [#10350](https://github.com/CesiumGS/cesium/pull/10350)
- Added `ModelAnimationCollection.animateWhilePaused` and `ModelAnimation.animationTime` to allow explicit control over a model's animations. [#9339](https://github.com/CesiumGS/cesium/pull/9339)

##### Fixes :wrench:

Expand Down
36 changes: 36 additions & 0 deletions Source/Scene/ModelAnimation.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ function ModelAnimation(options, model, runtimeAnimation) {
this._multiplier = defaultValue(options.multiplier, 1.0);
this._reverse = defaultValue(options.reverse, false);
this._loop = defaultValue(options.loop, ModelAnimationLoop.NONE);
this._animationTime = options.animationTime;
this._prevAnimationDelta = undefined;

/**
* The event fired when this animation is started. This can be used, for
Expand Down Expand Up @@ -229,5 +231,39 @@ Object.defineProperties(ModelAnimation.prototype, {
return this._loop;
},
},

/**
* If this is defined, it will be used to compute the local animation time
* instead of the scene's time.
*
* @type {ModelAnimation.AnimationTimeCallback}
* @default undefined
*/
animationTime: {
get: function () {
return this._animationTime;
},
},
});
/**
* A function used to compute the local animation time for a ModelAnimation.
j9liu marked this conversation as resolved.
Show resolved Hide resolved
* @callback ModelAnimation.AnimationTimeCallback
*
* @param {Number} duration The animation's original duration in seconds.
* @param {Number} seconds The seconds since the animation started, in scene time.
* @returns {Number} Returns the local animation time.
*
* @example
* // Use real time for model animation (assuming animateWhilePaused was set to true)
* function animationTime(duration) {
j9liu marked this conversation as resolved.
Show resolved Hide resolved
* return Date.now() / 1000 / duration;
* }
*
* @example
* // Offset the phase of the animation, so it starts halfway
* // through its cycle.
* function animationTime(duration, seconds) {
* return seconds / duration + 0.5;
* }
*/
export default ModelAnimation;
61 changes: 43 additions & 18 deletions Source/Scene/ModelAnimationCollection.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@ function ModelAnimationCollection(model) {
*/
this.animationRemoved = new Event();

/**
j9liu marked this conversation as resolved.
Show resolved Hide resolved
* When true, the animation will play even when the scene time is paused. However,
* whether animation takes place will depend on the animationTime functions assigned
* to the model's animations. By default, this is based on scene time, so models using
* the default will not animate regardless of this setting.
*
* @type {Boolean}
* @default false
*/
this.animateWhilePaused = false;

this._model = model;
this._scheduledAnimations = [];
this._previousTime = undefined;
j9liu marked this conversation as resolved.
Show resolved Hide resolved
Expand Down Expand Up @@ -93,6 +104,7 @@ function add(collection, index, options) {
* @param {Number} [options.multiplier=1.0] Values greater than <code>1.0</code> increase the speed that the animation is played relative to the scene clock speed; values less than <code>1.0</code> decrease the speed.
* @param {Boolean} [options.reverse=false] When <code>true</code>, the animation is played in reverse.
* @param {ModelAnimationLoop} [options.loop=ModelAnimationLoop.NONE] Determines if and how the animation is looped.
* @param {ModelAnimation.AnimationTimeCallback} [options.animationTime=undefined] If defined, computes the local animation time for this animation.
* @returns {ModelAnimation} The animation that was added to the collection.
*
* @exception {DeveloperError} Animations are not loaded. Wait for the {@link Model#readyPromise} to resolve.
Expand Down Expand Up @@ -204,6 +216,7 @@ ModelAnimationCollection.prototype.add = function (options) {
* @param {Number} [options.multiplier=1.0] Values greater than <code>1.0</code> increase the speed that the animations play relative to the scene clock speed; values less than <code>1.0</code> decrease the speed.
* @param {Boolean} [options.reverse=false] When <code>true</code>, the animations are played in reverse.
* @param {ModelAnimationLoop} [options.loop=ModelAnimationLoop.NONE] Determines if and how the animations are looped.
* @param {ModelAnimation.AnimationTimeCallback} [options.animationTime=undefined] If defined, computes the local animation time for all of the animations.
* @returns {ModelAnimation[]} An array of {@link ModelAnimation} objects, one for each animation added to the collection. If there are no glTF animations, the array is empty.
*
* @exception {DeveloperError} Animations are not loaded. Wait for the {@link Model#readyPromise} to resolve.
Expand Down Expand Up @@ -366,8 +379,10 @@ ModelAnimationCollection.prototype.update = function (frameState) {
return false;
}

if (JulianDate.equals(frameState.time, this._previousTime)) {
// Animations are currently only time-dependent so do not animate when paused or picking
if (
!this.animateWhilePaused &&
JulianDate.equals(frameState.time, this._previousTime)
) {
return false;
}
this._previousTime = JulianDate.clone(frameState.time, this._previousTime);
Expand Down Expand Up @@ -397,23 +412,22 @@ ModelAnimationCollection.prototype.update = function (frameState) {
const duration = scheduledAnimation._duration;
const stopTime = scheduledAnimation.stopTime;

const pastStartTime = JulianDate.lessThanOrEquals(startTime, sceneTime);
const pastStopTime =
defined(stopTime) && JulianDate.greaterThan(sceneTime, stopTime);

// [0.0, 1.0] normalized local animation time
let delta =
duration !== 0.0
? JulianDate.secondsDifference(sceneTime, startTime) / duration
: 0.0;

// Clamp delta to stop time, if defined.
if (
duration !== 0.0 &&
defined(stopTime) &&
JulianDate.greaterThan(sceneTime, stopTime)
) {
delta = JulianDate.secondsDifference(stopTime, startTime) / duration;
let delta = 0.0;
if (duration !== 0.0) {
const seconds = JulianDate.secondsDifference(
pastStopTime ? stopTime : sceneTime,
startTime
);
delta = defined(scheduledAnimation._animationTime)
? scheduledAnimation._animationTime(duration, seconds)
: seconds / duration;
}

const pastStartTime = delta >= 0.0;

// Play animation if
// * we are after the start time or the animation is being repeated, and
// * before the end of the animation's duration or the animation is being repeated, and
Expand All @@ -426,7 +440,18 @@ ModelAnimationCollection.prototype.update = function (frameState) {
const play =
(pastStartTime || (repeat && !defined(scheduledAnimation.startTime))) &&
(delta <= 1.0 || repeat) &&
(!defined(stopTime) || JulianDate.lessThanOrEquals(sceneTime, stopTime));
!pastStopTime;

if (delta === scheduledAnimation._prevAnimationDelta) {
const animationStopped =
scheduledAnimation._state === ModelAnimationState.STOPPED;
// no change to delta, and no change to the animation state means we can
// skip the update this time around.
if (play !== animationStopped) {
continue;
}
}
scheduledAnimation._prevAnimationDelta = delta;

// If it IS, or WAS, animating...
if (play || scheduledAnimation._state === ModelAnimationState.ANIMATING) {
Expand All @@ -446,7 +471,7 @@ ModelAnimationCollection.prototype.update = function (frameState) {
) {
const floor = Math.floor(delta);
const fract = delta - floor;
// When even use (1.0 - fract) to mirror repeat
// When odd use (1.0 - fract) to mirror repeat
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!

delta = floor % 2 === 1.0 ? 1.0 - fract : fract;
}

Expand Down
36 changes: 36 additions & 0 deletions Source/Scene/ModelExperimental/ModelExperimentalAnimation.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ function ModelExperimentalAnimation(model, animation, options) {
this._multiplier = defaultValue(options.multiplier, 1.0);
this._reverse = defaultValue(options.reverse, false);
this._loop = defaultValue(options.loop, ModelAnimationLoop.NONE);
this._animationTime = options.animationTime;
this._prevAnimationDelta = undefined;

/**
* The event fired when this animation is started. This can be used, for
Expand Down Expand Up @@ -323,6 +325,19 @@ Object.defineProperties(ModelExperimentalAnimation.prototype, {
return this._loop;
},
},

/**
* If this is defined, it will be used to compute the local animation time
* instead of the scene's time.
*
* @type {ModelExperimentalAnimation.AnimationTimeCallback}
* @default undefined
*/
animationTime: {
get: function () {
return this._animationTime;
},
},
});

function initialize(runtimeAnimation) {
Expand Down Expand Up @@ -381,4 +396,25 @@ ModelExperimentalAnimation.prototype.animate = function (time) {
}
};

/**
* A function used to compute the local animation time for a ModelExperimentalAnimation.
* @callback ModelExperimentalAnimation.AnimationTimeCallback
*
* @param {Number} duration The animation's original duration in seconds.
* @param {Number} seconds The seconds since the animation started, in scene time.
* @returns {Number} Returns the local animation time.
*
* @example
* // Use real time for model animation (assuming animateWhilePaused was set to true)
* function animationTime(duration) {
* return Date.now() / 1000 / duration;
* }
*
* @example
* // Offset the phase of the animation, so it starts halfway
* // through its cycle.
* function animationTime(duration, seconds) {
* return seconds / duration + 0.5;
* }
*/
export default ModelExperimentalAnimation;
Loading