Skip to content

Commit

Permalink
Add support for explicit control over model animations
Browse files Browse the repository at this point in the history
  • Loading branch information
markw65 committed Feb 9, 2021
1 parent 4c7fb7c commit d7297b9
Show file tree
Hide file tree
Showing 7 changed files with 251 additions and 39 deletions.
Binary file added Apps/SampleData/models/Elliptigo/elliptigo.glb
Binary file not shown.
172 changes: 172 additions & 0 deletions Apps/Sandcastle/gallery/Manually Controlled Animation.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
<!DOCTYPE html>
<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
var viewer = new Cesium.Viewer("cesiumContainer", {
shouldAnimate: true,
});

//Make sure viewer is at the desired time.
var start = Cesium.JulianDate.fromDate(new Date(2018, 11, 12, 15));
var totalSeconds = 30;
var 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);

var wheelRadius = 0.52; //in meters.
var circumference = Math.PI * wheelRadius * 2;
// Create a path for our vehicle by lerping between two positions.
var position = new Cesium.SampledPositionProperty();
var distance = new Cesium.SampledProperty(Number);
var startPosition = new Cesium.Cartesian3(
-2379556.799372864,
-4665528.205030263,
3628013.106599678
);
var 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.
var velocityVectorProperty = new Cesium.VelocityVectorProperty(
position,
false
);
var velocityVector = new Cesium.Cartesian3();

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

// Lerp using a non-linear factor so that the vehicle accelerates.
var locationFactor = Math.pow(factor, 2);
var 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);
var metersPerSecond = Cesium.Cartesian3.magnitude(velocityVector);
var kmPerHour = Math.round(metersPerSecond * 3.6);

return kmPerHour + " km/hr";
}

// Add our vehicle model.
var vehiclePrimitive = viewer.scene.primitives.add(
Cesium.Model.fromGltf({
url: "../../SampleData/models/Elliptigo/elliptigo.glb",
cull: false,
})
);
var vehicleLabel = viewer.entities.add({
position: position,
orientation: new Cesium.VelocityOrientationProperty(position), // Automatically set the vehicle'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, 2.3, 0),
},
});

Cesium.when(vehiclePrimitive.readyPromise).then(function (model) {
model.activeAnimations.addAll({
loop: Cesium.ModelAnimationLoop.REPEAT,
});
model.activeAnimations.manualAnimation = true;
var rot = new Cesium.Matrix3();
viewer.scene.preUpdate.addEventListener(function () {
var time = viewer.clock.currentTime;
var pos = position.getValue(time);
var 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);
var dist = distance.getValue(time);
var animations = model.activeAnimations;
var length = animations.length;
for (var i = 0; i < length; ++i) {
animations.get(i).animationTime = dist;
}
});
});
viewer.trackedEntity = vehicleLabel;
vehicleLabel.viewFrom = new Cesium.Cartesian3(-10.0, 7.0, 4.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.
4 changes: 4 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

### 1.79 - 2021-03-01

##### Additions :tada:

- Added `ModelAnimationCollection.manualAnimation` and `ModelAnimation.animationTime` to allow explicit control over a model's animations.

##### Fixes :wrench:

- Fixed an issue where certain inputs to EllipsoidGeodesic would result in a surfaceDistance of NaN. [#9316](https://github.com/CesiumGS/cesium/pull/9316)
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,3 +284,4 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute to Cesiu
- [Will Jadkowski](https://github.com/willjad)
- [Mac Clayton](https://github.com/mclayton7)
- [Ben Murphy](https://github.com/littlemurph)
- [Mark Williams](https://github.com/markw65)
8 changes: 8 additions & 0 deletions Source/Scene/ModelAnimation.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ function ModelAnimation(options, model, runtimeAnimation) {
this._reverse = defaultValue(options.reverse, false);
this._loop = defaultValue(options.loop, ModelAnimationLoop.NONE);

/**
* If this is defined, it will be used as the local animation time,
* rather than deriving the local animation time from the current time
* and the above parameters.
*/
this.animationTime = undefined;
this._prevAnimationTime = undefined;

/**
* The event fired when this animation is started. This can be used, for
* example, to play a sound or start a particle system, when the animation starts.
Expand Down
105 changes: 66 additions & 39 deletions Source/Scene/ModelAnimationCollection.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ function ModelAnimationCollection(model) {
*/
this.animationRemoved = new Event();

/**
* When false, all the animations are time based. When true, each animation can choose to
* override the local animation time.
*/
this.manualAnimation = false;

this._model = model;
this._scheduledAnimations = [];
this._previousTime = undefined;
Expand Down Expand Up @@ -366,8 +372,11 @@ 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.manualAnimation &&
JulianDate.equals(frameState.time, this._previousTime)
) {
// Animations are all time-dependent so do not animate when paused or picking
return false;
}
this._previousTime = JulianDate.clone(frameState.time, this._previousTime);
Expand All @@ -380,54 +389,72 @@ ModelAnimationCollection.prototype.update = function (frameState) {
var scheduledAnimation = scheduledAnimations[i];
var runtimeAnimation = scheduledAnimation._runtimeAnimation;

if (!defined(scheduledAnimation._computedStartTime)) {
scheduledAnimation._computedStartTime = JulianDate.addSeconds(
defaultValue(scheduledAnimation.startTime, sceneTime),
scheduledAnimation.delay,
new JulianDate()
);
}

if (!defined(scheduledAnimation._duration)) {
scheduledAnimation._duration =
runtimeAnimation.stopTime * (1.0 / scheduledAnimation.multiplier);
}

var startTime = scheduledAnimation._computedStartTime;
var duration = scheduledAnimation._duration;
var stopTime = scheduledAnimation.stopTime;

// [0.0, 1.0] normalized local animation time
var 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;
}
var delta;
var play;
if (this.manualAnimation && defined(scheduledAnimation.animationTime)) {
if (
scheduledAnimation.animationTime ===
scheduledAnimation._prevAnimationTime
) {
continue;
}
scheduledAnimation._prevAnimationTime = scheduledAnimation.animationTime;
if (duration !== 0.0) {
delta = scheduledAnimation.animationTime / duration;
} else {
delta = 0.0;
}
play = true;
} else {
if (!defined(scheduledAnimation._computedStartTime)) {
scheduledAnimation._computedStartTime = JulianDate.addSeconds(
defaultValue(scheduledAnimation.startTime, sceneTime),
scheduledAnimation.delay,
new JulianDate()
);
}

var pastStartTime = delta >= 0.0;
var startTime = scheduledAnimation._computedStartTime;
var stopTime = scheduledAnimation.stopTime;

// 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
// * we did not reach a user-provided stop time.
// [0.0, 1.0] normalized local animation time
delta =
duration !== 0.0
? JulianDate.secondsDifference(sceneTime, startTime) / duration
: 0.0;

var repeat =
scheduledAnimation.loop === ModelAnimationLoop.REPEAT ||
scheduledAnimation.loop === ModelAnimationLoop.MIRRORED_REPEAT;
// Clamp delta to stop time, if defined.
if (
duration !== 0.0 &&
defined(stopTime) &&
JulianDate.greaterThan(sceneTime, stopTime)
) {
delta = JulianDate.secondsDifference(stopTime, startTime) / duration;
}

var pastStartTime = delta >= 0.0;

var play =
(pastStartTime || (repeat && !defined(scheduledAnimation.startTime))) &&
(delta <= 1.0 || repeat) &&
(!defined(stopTime) || JulianDate.lessThanOrEquals(sceneTime, stopTime));
// 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
// * we did not reach a user-provided stop time.

var repeat =
scheduledAnimation.loop === ModelAnimationLoop.REPEAT ||
scheduledAnimation.loop === ModelAnimationLoop.MIRRORED_REPEAT;

play =
(pastStartTime || (repeat && !defined(scheduledAnimation.startTime))) &&
(delta <= 1.0 || repeat) &&
(!defined(stopTime) ||
JulianDate.lessThanOrEquals(sceneTime, stopTime));
}
// If it IS, or WAS, animating...
if (play || scheduledAnimation._state === ModelAnimationState.ANIMATING) {
// STOPPED -> ANIMATING state transition?
Expand Down

0 comments on commit d7297b9

Please sign in to comment.