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);