diff --git a/CHANGES.md b/CHANGES.md
index 71008b438582..127634c13ccb 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -7,6 +7,8 @@ Change Log
* Added ability to support touch event in Imagery Layers Split demo application. [#5948](https://github.com/AnalyticalGraphicsInc/cesium/pull/5948)
* Fixed `Invalid asm.js: Invalid member of stdlib` console error by recompiling crunch.js with latest emscripten toolchain. [#5847](https://github.com/AnalyticalGraphicsInc/cesium/issues/5847)
* Added CZML support for `polyline.depthFailMaterial`, `label.scaleByDistance`, `distanceDisplayCondition`, and `disableDepthTestDistance`. [#5986](https://github.com/AnalyticalGraphicsInc/cesium/pull/5986)
+* Fixed a bug where models with animations of different lengths would cause an error. [#5694](https://github.com/AnalyticalGraphicsInc/cesium/issues/5694)
+* Added a `clampAnimations` parameter to `Model` and `Entity.model`. Setting this to `false` allows different length animations to loop asynchronously over the duration of the longest animation.
### 1.39 - 2017-11-01
diff --git a/Source/Core/CatmullRomSpline.js b/Source/Core/CatmullRomSpline.js
index fc417ce4a202..279f2437ddcf 100644
--- a/Source/Core/CatmullRomSpline.js
+++ b/Source/Core/CatmullRomSpline.js
@@ -267,6 +267,24 @@ define([
*/
CatmullRomSpline.prototype.findTimeInterval = Spline.prototype.findTimeInterval;
+ /**
+ * Wraps the given time to the period covered by the spline.
+ * @function
+ *
+ * @param {Number} time The time.
+ * @return {Number} The time, wrapped around to the updated animation.
+ */
+ CatmullRomSpline.prototype.wrapTime = Spline.prototype.wrapTime;
+
+ /**
+ * Clamps the given time to the period covered by the spline.
+ * @function
+ *
+ * @param {Number} time The time.
+ * @return {Number} The time, clamped to the animation period.
+ */
+ CatmullRomSpline.prototype.clampTime = Spline.prototype.clampTime;
+
/**
* Evaluates the curve at a given time.
*
diff --git a/Source/Core/HermiteSpline.js b/Source/Core/HermiteSpline.js
index e4d6209e8ca6..35eff8ad24d8 100644
--- a/Source/Core/HermiteSpline.js
+++ b/Source/Core/HermiteSpline.js
@@ -490,6 +490,24 @@ define([
var scratchTimeVec = new Cartesian4();
var scratchTemp = new Cartesian3();
+ /**
+ * Wraps the given time to the period covered by the spline.
+ * @function
+ *
+ * @param {Number} time The time.
+ * @return {Number} The time, wrapped around to the updated animation.
+ */
+ HermiteSpline.prototype.wrapTime = Spline.prototype.wrapTime;
+
+ /**
+ * Clamps the given time to the period covered by the spline.
+ * @function
+ *
+ * @param {Number} time The time.
+ * @return {Number} The time, clamped to the animation period.
+ */
+ HermiteSpline.prototype.clampTime = Spline.prototype.clampTime;
+
/**
* Evaluates the curve at a given time.
*
diff --git a/Source/Core/LinearSpline.js b/Source/Core/LinearSpline.js
index ad8b387727bc..399b3cd3185c 100644
--- a/Source/Core/LinearSpline.js
+++ b/Source/Core/LinearSpline.js
@@ -117,6 +117,24 @@ define([
*/
LinearSpline.prototype.findTimeInterval = Spline.prototype.findTimeInterval;
+ /**
+ * Wraps the given time to the period covered by the spline.
+ * @function
+ *
+ * @param {Number} time The time.
+ * @return {Number} The time, wrapped around to the updated animation.
+ */
+ LinearSpline.prototype.wrapTime = Spline.prototype.wrapTime;
+
+ /**
+ * Clamps the given time to the period covered by the spline.
+ * @function
+ *
+ * @param {Number} time The time.
+ * @return {Number} The time, clamped to the animation period.
+ */
+ LinearSpline.prototype.clampTime = Spline.prototype.clampTime;
+
/**
* Evaluates the curve at a given time.
*
diff --git a/Source/Core/QuaternionSpline.js b/Source/Core/QuaternionSpline.js
index bfe4c792821c..7165efc2b60e 100644
--- a/Source/Core/QuaternionSpline.js
+++ b/Source/Core/QuaternionSpline.js
@@ -179,6 +179,24 @@ define([
*/
QuaternionSpline.prototype.findTimeInterval = Spline.prototype.findTimeInterval;
+ /**
+ * Wraps the given time to the period covered by the spline.
+ * @function
+ *
+ * @param {Number} time The time.
+ * @return {Number} The time, wrapped around to the updated animation.
+ */
+ QuaternionSpline.prototype.wrapTime = Spline.prototype.wrapTime;
+
+ /**
+ * Clamps the given time to the period covered by the spline.
+ * @function
+ *
+ * @param {Number} time The time.
+ * @return {Number} The time, clamped to the animation period.
+ */
+ QuaternionSpline.prototype.clampTime = Spline.prototype.clampTime;
+
/**
* Evaluates the curve at a given time.
*
diff --git a/Source/Core/Spline.js b/Source/Core/Spline.js
index cbc7e6940ceb..49ccd8f593b3 100644
--- a/Source/Core/Spline.js
+++ b/Source/Core/Spline.js
@@ -1,11 +1,15 @@
define([
+ './Check',
'./defaultValue',
'./defined',
- './DeveloperError'
- ], function(
+ './DeveloperError',
+ './Math'
+], function(
+ Check,
defaultValue,
defined,
- DeveloperError) {
+ DeveloperError,
+ CesiumMath) {
'use strict';
/**
@@ -117,5 +121,49 @@ define([
return i;
};
+ /**
+ * Wraps the given time to the period covered by the spline.
+ * @function
+ *
+ * @param {Number} time The time.
+ * @return {Number} The time, wrapped around the animation period.
+ */
+ Spline.prototype.wrapTime = function(time) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.number('time', time);
+ //>>includeEnd('debug');
+
+ var times = this.times;
+ var timeEnd = times[times.length - 1];
+ var timeStart = times[0];
+ var timeStretch = timeEnd - timeStart;
+ var divs;
+ if (time < timeStart) {
+ divs = Math.floor((timeStart - time) / timeStretch) + 1;
+ time += divs * timeStretch;
+ }
+ if (time > timeEnd) {
+ divs = Math.floor((time - timeEnd) / timeStretch) + 1;
+ time -= divs * timeStretch;
+ }
+ return time;
+ };
+
+ /**
+ * Clamps the given time to the period covered by the spline.
+ * @function
+ *
+ * @param {Number} time The time.
+ * @return {Number} The time, clamped to the animation period.
+ */
+ Spline.prototype.clampTime = function(time) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.number('time', time);
+ //>>includeEnd('debug');
+
+ var times = this.times;
+ return CesiumMath.clamp(time, times[0], times[times.length - 1]);
+ };
+
return Spline;
});
diff --git a/Source/Core/WeightSpline.js b/Source/Core/WeightSpline.js
index 3ef4f9ce82cc..4a8ba66f84d8 100644
--- a/Source/Core/WeightSpline.js
+++ b/Source/Core/WeightSpline.js
@@ -113,6 +113,24 @@ define([
*/
WeightSpline.prototype.findTimeInterval = Spline.prototype.findTimeInterval;
+ /**
+ * Wraps the given time to the period covered by the spline.
+ * @function
+ *
+ * @param {Number} time The time.
+ * @return {Number} The time, wrapped around to the updated animation.
+ */
+ WeightSpline.prototype.wrapTime = Spline.prototype.wrapTime;
+
+ /**
+ * Clamps the given time to the period covered by the spline.
+ * @function
+ *
+ * @param {Number} time The time.
+ * @return {Number} The time, clamped to the animation period.
+ */
+ WeightSpline.prototype.clampTime = Spline.prototype.clampTime;
+
/**
* Evaluates the curve at a given time.
*
diff --git a/Source/DataSources/CzmlDataSource.js b/Source/DataSources/CzmlDataSource.js
index d7af454b453a..6109becefb10 100644
--- a/Source/DataSources/CzmlDataSource.js
+++ b/Source/DataSources/CzmlDataSource.js
@@ -1612,6 +1612,7 @@ define([
processPacketData(Number, model, 'maximumScale', modelData.maximumScale, interval, sourceUri, entityCollection, query);
processPacketData(Boolean, model, 'incrementallyLoadTextures', modelData.incrementallyLoadTextures, interval, sourceUri, entityCollection, query);
processPacketData(Boolean, model, 'runAnimations', modelData.runAnimations, interval, sourceUri, entityCollection, query);
+ processPacketData(Boolean, model, 'clampAnimations', modelData.clampAnimations, interval, sourceUri, entityCollection, query);
processPacketData(ShadowMode, model, 'shadows', modelData.shadows, interval, sourceUri, entityCollection, query);
processPacketData(HeightReference, model, 'heightReference', modelData.heightReference, interval, sourceUri, entityCollection, query);
processPacketData(Color, model, 'silhouetteColor', modelData.silhouetteColor, interval, sourceUri, entityCollection, query);
diff --git a/Source/DataSources/ModelGraphics.js b/Source/DataSources/ModelGraphics.js
index db69b5d8df4f..78fe8ab6b949 100644
--- a/Source/DataSources/ModelGraphics.js
+++ b/Source/DataSources/ModelGraphics.js
@@ -45,6 +45,7 @@ define([
* @param {Property} [options.maximumScale] The maximum scale size of a model. An upper limit for minimumPixelSize.
* @param {Property} [options.incrementallyLoadTextures=true] Determine if textures may continue to stream in after the model is loaded.
* @param {Property} [options.runAnimations=true] A boolean Property specifying if glTF animations specified in the model should be started.
+ * @param {Property} [options.clampAnimations=true] A boolean Property specifying if glTF animations should hold the last pose for time durations with no keyframes.
* @param {Property} [options.nodeTransformations] An object, where keys are names of nodes, and values are {@link TranslationRotationScale} Properties describing the transformation to apply to that node.
* @param {Property} [options.shadows=ShadowMode.ENABLED] An enum Property specifying whether the model casts or receives shadows from each light source.
* @param {Property} [options.heightReference=HeightReference.NONE] A Property specifying what the height is relative to.
@@ -74,6 +75,7 @@ define([
this._uri = undefined;
this._uriSubscription = undefined;
this._runAnimations = undefined;
+ this._clampAnimations = undefined;
this._runAnimationsSubscription = undefined;
this._nodeTransformations = undefined;
this._nodeTransformationsSubscription = undefined;
@@ -179,6 +181,14 @@ define([
*/
runAnimations : createPropertyDescriptor('runAnimations'),
+ /**
+ * Gets or sets the boolean Property specifying if glTF animations should hold the last pose for time durations with no keyframes.
+ * @memberof ModelGraphics.prototype
+ * @type {Property}
+ * @default true
+ */
+ clampAnimations : createPropertyDescriptor('clampAnimations'),
+
/**
* Gets or sets the set of node transformations to apply to this model. This is represented as an {@link PropertyBag}, where keys are
* names of nodes, and values are {@link TranslationRotationScale} Properties describing the transformation to apply to that node.
@@ -263,6 +273,7 @@ define([
result.shadows = this.shadows;
result.uri = this.uri;
result.runAnimations = this.runAnimations;
+ result.clampAnimations = this.clampAnimations;
result.nodeTransformations = this.nodeTransformations;
result.heightReference = this._heightReference;
result.distanceDisplayCondition = this.distanceDisplayCondition;
@@ -296,6 +307,7 @@ define([
this.shadows = defaultValue(this.shadows, source.shadows);
this.uri = defaultValue(this.uri, source.uri);
this.runAnimations = defaultValue(this.runAnimations, source.runAnimations);
+ this.clampAnimations = defaultValue(this.clampAnimations, source.clampAnimations);
this.heightReference = defaultValue(this.heightReference, source.heightReference);
this.distanceDisplayCondition = defaultValue(this.distanceDisplayCondition, source.distanceDisplayCondition);
this.silhouetteColor = defaultValue(this.silhouetteColor, source.silhouetteColor);
diff --git a/Source/DataSources/ModelVisualizer.js b/Source/DataSources/ModelVisualizer.js
index bf6e48d2dd2f..2c941d3381cd 100644
--- a/Source/DataSources/ModelVisualizer.js
+++ b/Source/DataSources/ModelVisualizer.js
@@ -33,6 +33,7 @@ define([
var defaultScale = 1.0;
var defaultMinimumPixelSize = 0.0;
var defaultIncrementallyLoadTextures = true;
+ var defaultClampAnimations = true;
var defaultShadows = ShadowMode.ENABLED;
var defaultHeightReference = HeightReference.NONE;
var defaultSilhouetteColor = Color.RED;
@@ -152,6 +153,7 @@ define([
model.color = Property.getValueOrDefault(modelGraphics._color, time, defaultColor, model._color);
model.colorBlendMode = Property.getValueOrDefault(modelGraphics._colorBlendMode, time, defaultColorBlendMode);
model.colorBlendAmount = Property.getValueOrDefault(modelGraphics._colorBlendAmount, time, defaultColorBlendAmount);
+ model.clampAnimations = Property.getValueOrDefault(modelGraphics._clampAnimations, time, defaultClampAnimations);
if (model.ready) {
var runAnimations = Property.getValueOrDefault(modelGraphics._runAnimations, time, true);
diff --git a/Source/Scene/Model.js b/Source/Scene/Model.js
index 2254975ebf00..c7cca1194ef6 100644
--- a/Source/Scene/Model.js
+++ b/Source/Scene/Model.js
@@ -326,6 +326,7 @@ define([
* @param {Boolean} [options.allowPicking=true] When true
, each glTF mesh and primitive is pickable with {@link Scene#pick}.
* @param {Boolean} [options.incrementallyLoadTextures=true] Determine if textures may continue to stream in after the model is loaded.
* @param {Boolean} [options.asynchronous=true] Determines if model WebGL resource creation will be spread out over several frames or block until completion once all glTF files are loaded.
+ * @param {Boolean} [options.clampAnimations=true] Determines if the model's animations should hold a pose over frames where no keyframes are specified.
* @param {ShadowMode} [options.shadows=ShadowMode.ENABLED] Determines whether the model casts or receives shadows from each light source.
* @param {Boolean} [options.debugShowBoundingVolume=false] For debugging only. Draws the bounding sphere for each draw command in the model.
* @param {Boolean} [options.debugWireframe=false] For debugging only. Draws the model in wireframe.
@@ -529,6 +530,13 @@ define([
*/
this.activeAnimations = new ModelAnimationCollection(this);
+ /**
+ * Determines if the model's animations should hold a pose over frames where no keyframes are specified.
+ *
+ * @type {Boolean}
+ */
+ this.clampAnimations = defaultValue(options.clampAnimations, true);
+
this._defaultTexture = undefined;
this._incrementallyLoadTextures = defaultValue(options.incrementallyLoadTextures, true);
this._asynchronous = defaultValue(options.asynchronous, true);
@@ -1110,6 +1118,7 @@ define([
* @param {Boolean} [options.allowPicking=true] When true
, each glTF mesh and primitive is pickable with {@link Scene#pick}.
* @param {Boolean} [options.incrementallyLoadTextures=true] Determine if textures may continue to stream in after the model is loaded.
* @param {Boolean} [options.asynchronous=true] Determines if model WebGL resource creation will be spread out over several frames or block until completion once all glTF files are loaded.
+ * @param {Boolean} [options.clampAnimations=true] Determines if the model's animations should hold a pose over frames where no keyframes are specified.
* @param {ShadowMode} [options.shadows=ShadowMode.ENABLED] Determines whether the model casts or receives shadows from each light source.
* @param {Boolean} [options.debugShowBoundingVolume=false] For debugging only. Draws the bounding sphere for each {@link DrawCommand} in the model.
* @param {Boolean} [options.debugWireframe=false] For debugging only. Draws the model in wireframe.
@@ -2488,6 +2497,7 @@ define([
// return;
//}
if (defined(spline)) {
+ localAnimationTime = model.clampAnimations ? spline.clampTime(localAnimationTime) : spline.wrapTime(localAnimationTime);
runtimeNode[targetPath] = spline.evaluate(localAnimationTime, runtimeNode[targetPath]);
runtimeNode.dirtyNumber = model._maxDirtyNumber;
}
diff --git a/Specs/Core/SplineSpec.js b/Specs/Core/SplineSpec.js
index 8073d53dcf63..415531f062fd 100644
--- a/Specs/Core/SplineSpec.js
+++ b/Specs/Core/SplineSpec.js
@@ -14,6 +14,48 @@ defineSuite([
}).toThrowDeveloperError();
});
+ it('wraps time that is out-of-bounds', function() {
+ var spline = HermiteSpline.createNaturalCubic({
+ points : [Cartesian3.ZERO, Cartesian3.UNIT_X, Cartesian3.UNIT_Y],
+ times : [0.0, 1.0, 2.0]
+ });
+
+ expect(spline.wrapTime(-0.5)).toEqual(1.5);
+ expect(spline.wrapTime(2.5)).toEqual(0.5);
+ });
+
+ it('clamps time that is out-of-bounds', function() {
+ var spline = HermiteSpline.createNaturalCubic({
+ points : [Cartesian3.ZERO, Cartesian3.UNIT_X, Cartesian3.UNIT_Y],
+ times : [0.0, 1.0, 2.0]
+ });
+
+ expect(spline.clampTime(-0.5)).toEqual(0.0);
+ expect(spline.clampTime(2.5)).toEqual(2.0);
+ });
+
+ it('wrapTime throws without a time', function() {
+ var spline = HermiteSpline.createNaturalCubic({
+ points : [Cartesian3.ZERO, Cartesian3.UNIT_X, Cartesian3.UNIT_Y],
+ times : [0.0, 1.0, 2.0]
+ });
+
+ expect(function() {
+ spline.wrapTime();
+ }).toThrowDeveloperError();
+ });
+
+ it('clampTime throws without a time', function() {
+ var spline = HermiteSpline.createNaturalCubic({
+ points : [Cartesian3.ZERO, Cartesian3.UNIT_X, Cartesian3.UNIT_Y],
+ times : [0.0, 1.0, 2.0]
+ });
+
+ expect(function() {
+ spline.clampTime();
+ }).toThrowDeveloperError();
+ });
+
it('findTimeInterval throws without a time', function() {
var spline = HermiteSpline.createNaturalCubic({
points : [Cartesian3.ZERO, Cartesian3.UNIT_X, Cartesian3.UNIT_Y],
diff --git a/Specs/DataSources/ModelGraphicsSpec.js b/Specs/DataSources/ModelGraphicsSpec.js
index c5db38a7d99c..b2e9a23fab2e 100644
--- a/Specs/DataSources/ModelGraphicsSpec.js
+++ b/Specs/DataSources/ModelGraphicsSpec.js
@@ -35,6 +35,7 @@ defineSuite([
maximumScale : 200,
incrementallyLoadTextures : false,
runAnimations : false,
+ clampAnimations : false,
shadows : ShadowMode.DISABLED,
heightReference : HeightReference.CLAMP_TO_GROUND,
distanceDisplayCondition : new DistanceDisplayCondition(),
@@ -68,6 +69,7 @@ defineSuite([
expect(model.colorBlendMode).toBeInstanceOf(ConstantProperty);
expect(model.colorBlendAmount).toBeInstanceOf(ConstantProperty);
expect(model.runAnimations).toBeInstanceOf(ConstantProperty);
+ expect(model.clampAnimations).toBeInstanceOf(ConstantProperty);
expect(model.nodeTransformations).toBeInstanceOf(PropertyBag);
@@ -86,6 +88,7 @@ defineSuite([
expect(model.colorBlendMode.getValue()).toEqual(options.colorBlendMode);
expect(model.colorBlendAmount.getValue()).toEqual(options.colorBlendAmount);
expect(model.runAnimations.getValue()).toEqual(options.runAnimations);
+ expect(model.clampAnimations.getValue()).toEqual(options.clampAnimations);
var actualNodeTransformations = model.nodeTransformations.getValue(new JulianDate());
var expectedNodeTransformations = options.nodeTransformations;
@@ -113,6 +116,7 @@ defineSuite([
source.colorBlendMode = new ConstantProperty(ColorBlendMode.HIGHLIGHT);
source.colorBlendAmount = new ConstantProperty(0.5);
source.runAnimations = new ConstantProperty(true);
+ source.clampAnimations = new ConstantProperty(true);
source.nodeTransformations = {
node1 : new NodeTransformationProperty({
translation : Cartesian3.UNIT_Y,
@@ -142,6 +146,7 @@ defineSuite([
expect(target.colorBlendMode).toBe(source.colorBlendMode);
expect(target.colorBlendAmount).toBe(source.colorBlendAmount);
expect(target.runAnimations).toBe(source.runAnimations);
+ expect(target.clampAnimations).toBe(source.clampAnimations);
expect(target.nodeTransformations).toEqual(source.nodeTransformations);
});
@@ -162,6 +167,7 @@ defineSuite([
source.colorBlendMode = new ConstantProperty(ColorBlendMode.HIGHLIGHT);
source.colorBlendAmount = new ConstantProperty(0.5);
source.runAnimations = new ConstantProperty(true);
+ source.clampAnimations = new ConstantProperty(true);
source.nodeTransformations = {
transform : new NodeTransformationProperty()
};
@@ -181,6 +187,7 @@ defineSuite([
var colorBlendMode = new ConstantProperty(ColorBlendMode.HIGHLIGHT);
var colorBlendAmount = new ConstantProperty(0.5);
var runAnimations = new ConstantProperty(true);
+ var clampAnimations = new ConstantProperty(true);
var nodeTransformations = new PropertyBag({
transform : new NodeTransformationProperty()
});
@@ -201,6 +208,7 @@ defineSuite([
target.colorBlendMode = colorBlendMode;
target.colorBlendAmount = colorBlendAmount;
target.runAnimations = runAnimations;
+ target.clampAnimations = clampAnimations;
target.nodeTransformations = nodeTransformations;
target.merge(source);
@@ -220,6 +228,7 @@ defineSuite([
expect(target.colorBlendMode).toBe(colorBlendMode);
expect(target.colorBlendAmount).toBe(colorBlendAmount);
expect(target.runAnimations).toBe(runAnimations);
+ expect(target.clampAnimations).toBe(clampAnimations);
expect(target.nodeTransformations).toBe(nodeTransformations);
});
@@ -240,6 +249,7 @@ defineSuite([
source.colorBlendMode = new ConstantProperty(ColorBlendMode.HIGHLIGHT);
source.colorBlendAmount = new ConstantProperty(0.5);
source.runAnimations = new ConstantProperty(true);
+ source.clampAnimations = new ConstantProperty(true);
source.nodeTransformations = {
node1 : new NodeTransformationProperty(),
node2 : new NodeTransformationProperty()
@@ -261,6 +271,7 @@ defineSuite([
expect(result.colorBlendMode).toBe(source.colorBlendMode);
expect(result.colorBlendAmount).toBe(source.colorBlendAmount);
expect(result.runAnimations).toBe(source.runAnimations);
+ expect(result.clampAnimations).toBe(source.clampAnimations);
expect(result.nodeTransformations).toEqual(source.nodeTransformations);
});
diff --git a/Specs/Scene/ModelSpec.js b/Specs/Scene/ModelSpec.js
index cf4acdda89e2..b30f2774d50c 100644
--- a/Specs/Scene/ModelSpec.js
+++ b/Specs/Scene/ModelSpec.js
@@ -2000,6 +2000,15 @@ defineSuite([
});
});
+ it('loads a glTF 2.0 with node animations set to unclamped', function() {
+ return loadModel(boxAnimatedPbrUrl, {
+ clampAnimations : false
+ }).then(function(m) {
+ verifyRender(m);
+ primitives.remove(m);
+ });
+ });
+
it('loads a glTF 2.0 with skinning', function() {
return loadModel(riggedSimplePbrUrl).then(function(m) {
verifyRender(m);