diff --git a/Apps/Sandcastle/gallery/CZML Model Articulations.html b/Apps/Sandcastle/gallery/CZML Model Articulations.html new file mode 100644 index 000000000000..f5b81c38baea --- /dev/null +++ b/Apps/Sandcastle/gallery/CZML Model Articulations.html @@ -0,0 +1,107 @@ + + + + + + + + + Cesium Demo + + + + + + +
+

Loading...

+
+ + + + diff --git a/Apps/Sandcastle/gallery/CZML Model Articulations.jpg b/Apps/Sandcastle/gallery/CZML Model Articulations.jpg new file mode 100644 index 000000000000..c85729c2e907 Binary files /dev/null and b/Apps/Sandcastle/gallery/CZML Model Articulations.jpg differ diff --git a/CHANGES.md b/CHANGES.md index 29654e7163e3..36f1df04d5ed 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,9 @@ Change Log ### 1.59 - 2019-07-01 +##### Additions :tada: +* Added support for the [AGI_articulations](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/AGI_articulations) vendor extension of glTF 2.0 to the Entity API and CZML. [#7907](https://github.com/AnalyticalGraphicsInc/cesium/pull/7907) + ##### Fixes :wrench: * Fixed a bug that caused missing segments for ground polylines with coplanar points over large distances and problems with polylines containing duplicate points. [#7885](https://github.com/AnalyticalGraphicsInc/cesium//pull/7885) * Fixed a bug where billboards were not pickable when zoomed out completely in 2D View. [#7908](https://github.com/AnalyticalGraphicsInc/cesium/pull/7908) diff --git a/Source/DataSources/CzmlDataSource.js b/Source/DataSources/CzmlDataSource.js index a61b8b68662b..2132ef20cf00 100644 --- a/Source/DataSources/CzmlDataSource.js +++ b/Source/DataSources/CzmlDataSource.js @@ -1713,16 +1713,28 @@ define([ processPacketData(Number, model, 'colorBlendAmount', modelData.colorBlendAmount, interval, sourceUri, entityCollection); processPacketData(DistanceDisplayCondition, model, 'distanceDisplayCondition', modelData.distanceDisplayCondition, interval, sourceUri, entityCollection); + var i, len; var nodeTransformationsData = modelData.nodeTransformations; if (defined(nodeTransformationsData)) { if (isArray(nodeTransformationsData)) { - for (var i = 0, len = nodeTransformationsData.length; i < len; i++) { + for (i = 0, len = nodeTransformationsData.length; i < len; i++) { processNodeTransformations(model, nodeTransformationsData[i], interval, sourceUri, entityCollection); } } else { processNodeTransformations(model, nodeTransformationsData, interval, sourceUri, entityCollection); } } + + var articulationsData = modelData.articulations; + if (defined(articulationsData)) { + if (isArray(articulationsData)) { + for (i = 0, len = articulationsData.length; i < len; i++) { + processArticulations(model, articulationsData[i], interval, sourceUri, entityCollection); + } + } else { + processArticulations(model, articulationsData, interval, sourceUri, entityCollection); + } + } } function processNodeTransformations(model, nodeTransformationsData, constrainedInterval, sourceUri, entityCollection) { @@ -1772,6 +1784,46 @@ define([ } } + function processArticulations(model, articulationsData, constrainedInterval, sourceUri, entityCollection) { + var combinedInterval; + var packetInterval = articulationsData.interval; + if (defined(packetInterval)) { + iso8601Scratch.iso8601 = packetInterval; + combinedInterval = TimeInterval.fromIso8601(iso8601Scratch); + if (defined(constrainedInterval)) { + combinedInterval = TimeInterval.intersect(combinedInterval, constrainedInterval, scratchTimeInterval); + } + } else if (defined(constrainedInterval)) { + combinedInterval = constrainedInterval; + } + + var articulations = model.articulations; + var keys = Object.keys(articulationsData); + for (var i = 0, len = keys.length; i < len; ++i) { + var key = keys[i]; + + if (key === 'interval') { + continue; + } + + var articulationStageData = articulationsData[key]; + + if (!defined(articulationStageData)) { + continue; + } + + if (!defined(articulations)) { + model.articulations = articulations = new PropertyBag(); + } + + if (!articulations.hasProperty(key)) { + articulations.addProperty(key); + } + + processPacketData(Number, articulations, key, articulationStageData, combinedInterval, sourceUri, entityCollection); + } + } + function processPath(entity, packet, entityCollection, sourceUri) { var pathData = packet.path; if (!defined(pathData)) { diff --git a/Source/DataSources/ModelGraphics.js b/Source/DataSources/ModelGraphics.js index 2039fa13944e..afe99a157dd3 100644 --- a/Source/DataSources/ModelGraphics.js +++ b/Source/DataSources/ModelGraphics.js @@ -26,6 +26,10 @@ define([ return new PropertyBag(value, createNodeTransformationProperty); } + function createArticulationStagePropertyBag(value) { + return new PropertyBag(value); + } + /** * A 3D model based on {@link https://github.com/KhronosGroup/glTF|glTF}, the runtime asset format for WebGL, OpenGL ES, and OpenGL. * The position and orientation of the model is determined by the containing {@link Entity}. @@ -47,6 +51,7 @@ define([ * @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 {PropertyBag} [options.nodeTransformations] An object, where keys are names of nodes, and values are {@link TranslationRotationScale} Properties describing the transformation to apply to that node. The transformation is applied after the node's existing transformation as specified in the glTF, and does not replace the node's existing transformation. + * @param {PropertyBag} [options.articulations] An object, where keys are composed of an articulation name, a single space, and a stage name, and the values are numeric properties. * @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. * @param {Property} [options.distanceDisplayCondition] A Property specifying at what distance from the camera that this model will be displayed. @@ -82,6 +87,8 @@ define([ this._runAnimationsSubscription = undefined; this._nodeTransformations = undefined; this._nodeTransformationsSubscription = undefined; + this._articulations = undefined; + this._articulationsSubscription = undefined; this._heightReference = undefined; this._heightReferenceSubscription = undefined; this._distanceDisplayCondition = undefined; @@ -207,6 +214,14 @@ define([ */ nodeTransformations : createPropertyDescriptor('nodeTransformations', undefined, createNodeTransformationPropertyBag), + /** + * Gets or sets the set of articulation values to apply to this model. This is represented as an {@link PropertyBag}, where keys are + * composed as the name of the articulation, a single space, and the name of the stage. + * @memberof ModelGraphics.prototype + * @type {PropertyBag} + */ + articulations : createPropertyDescriptor('articulations', undefined, createArticulationStagePropertyBag), + /** * Gets or sets the Property specifying the {@link HeightReference}. * @memberof ModelGraphics.prototype @@ -306,6 +321,7 @@ define([ result.runAnimations = this.runAnimations; result.clampAnimations = this.clampAnimations; result.nodeTransformations = this.nodeTransformations; + result.articulations = this.articulations; result.heightReference = this._heightReference; result.distanceDisplayCondition = this.distanceDisplayCondition; result.silhouetteColor = this.silhouetteColor; @@ -362,6 +378,16 @@ define([ this.nodeTransformations = new PropertyBag(sourceNodeTransformations, createNodeTransformationProperty); } } + + var sourceArticulations = source.articulations; + if (defined(sourceArticulations)) { + var targetArticulations = this.articulations; + if (defined(targetArticulations)) { + targetArticulations.merge(sourceArticulations); + } else { + this.articulations = new PropertyBag(sourceArticulations); + } + } }; return ModelGraphics; diff --git a/Source/DataSources/ModelVisualizer.js b/Source/DataSources/ModelVisualizer.js index 06469ee82796..0c0132632161 100644 --- a/Source/DataSources/ModelVisualizer.js +++ b/Source/DataSources/ModelVisualizer.js @@ -137,6 +137,7 @@ define([ url : resource.url, animationsRunning : false, nodeTransformationsScratch : {}, + articulationsScratch : {}, loadFail : false }; modelHash[entity.id] = modelData; @@ -196,6 +197,28 @@ define([ modelNode.matrix = Matrix4.multiply(modelNode.originalMatrix, transformationMatrix, transformationMatrix); } } + + // Apply articulations + var anyArticulationUpdated = false; + var articulations = Property.getValueOrUndefined(modelGraphics._articulations, time, modelData.articulationsScratch); + if (defined(articulations)) { + var articulationStageKeys = Object.keys(articulations); + for (var s = 0, numKeys = articulationStageKeys.length; s < numKeys; ++s) { + var key = articulationStageKeys[s]; + + var articulationStageValue = articulations[key]; + if (!defined(articulationStageValue)) { + continue; + } + + anyArticulationUpdated = true; + model.setArticulationStage(key, articulationStageValue); + } + } + + if (anyArticulationUpdated) { + model.applyArticulations(); + } } } @@ -291,7 +314,7 @@ define([ for (i = changed.length - 1; i > -1; i--) { entity = changed[i]; if (defined(entity._model) && defined(entity._position)) { - clearNodeTransformationsScratch(entity, modelHash); + clearNodeTransformationsArticulationsScratch(entity, modelHash); entities.set(entity.id, entity); } else { removeModel(this, entity, modelHash, primitives); @@ -314,10 +337,11 @@ define([ } } - function clearNodeTransformationsScratch(entity, modelHash) { + function clearNodeTransformationsArticulationsScratch(entity, modelHash) { var modelData = modelHash[entity.id]; if (defined(modelData)) { modelData.nodeTransformationsScratch = {}; + modelData.articulationsScratch = {}; } } diff --git a/Specs/Data/Models/Box-Articulations/Box-Articulations.gltf b/Specs/Data/Models/Box-Articulations/Box-Articulations.gltf index d5f906c215d0..b727cf9e9395 100644 --- a/Specs/Data/Models/Box-Articulations/Box-Articulations.gltf +++ b/Specs/Data/Models/Box-Articulations/Box-Articulations.gltf @@ -42,6 +42,7 @@ } }, { + "name": "Mesh", "mesh": 0 } ], diff --git a/Specs/DataSources/CzmlDataSourceSpec.js b/Specs/DataSources/CzmlDataSourceSpec.js index eff0e7abc1d3..5b11f38ea90a 100644 --- a/Specs/DataSources/CzmlDataSourceSpec.js +++ b/Specs/DataSources/CzmlDataSourceSpec.js @@ -3196,7 +3196,7 @@ defineSuite([ scale : 3.0, minimumPixelSize : 5.0, maximumScale : 4.0, - gltf : './Data/Models/Box/CesiumBoxTest.gltf', + gltf : './Data/Models/Box-Articulations/Box-Articulations.gltf', incrementallyLoadTextures : true, shadows : 'ENABLED', heightReference : 'CLAMP_TO_GROUND', @@ -3221,6 +3221,11 @@ defineSuite([ unitQuaternion : [0.0, 0.707, 0.0, 0.707] } } + }, + articulations : { + 'SampleArticulation Yaw' : 30, + 'SampleArticulation Pitch' : 45, + 'SampleArticulation Roll' : 60 } } }; @@ -3234,7 +3239,7 @@ defineSuite([ expect(entity.model.scale.getValue(Iso8601.MINIMUM_VALUE)).toEqual(3.0); expect(entity.model.minimumPixelSize.getValue(Iso8601.MINIMUM_VALUE)).toEqual(5.0); expect(entity.model.maximumScale.getValue(Iso8601.MINIMUM_VALUE)).toEqual(4.0); - expect(entity.model.uri.getValue(Iso8601.MINIMUM_VALUE).url).toEqual('./Data/Models/Box/CesiumBoxTest.gltf'); + expect(entity.model.uri.getValue(Iso8601.MINIMUM_VALUE).url).toEqual('./Data/Models/Box-Articulations/Box-Articulations.gltf'); expect(entity.model.incrementallyLoadTextures.getValue(Iso8601.MINIMUM_VALUE)).toEqual(true); expect(entity.model.shadows.getValue(Iso8601.MINIMUM_VALUE)).toEqual(ShadowMode.ENABLED); expect(entity.model.heightReference.getValue(Iso8601.MINIMUM_VALUE)).toEqual(HeightReference.CLAMP_TO_GROUND); @@ -3256,6 +3261,12 @@ defineSuite([ expect(entity.model.nodeTransformations.Mesh.scale.getValue(Iso8601.MINIMUM_VALUE)).toEqual(new Cartesian3(1.0, 2.0, 3.0)); expect(entity.model.nodeTransformations.Mesh.translation.getValue(Iso8601.MINIMUM_VALUE)).toEqual(new Cartesian3(4.0, 5.0, 6.0)); expect(entity.model.nodeTransformations.Mesh.rotation.getValue(Iso8601.MINIMUM_VALUE)).toEqual(expectedRotation); + + var articulations = entity.model.articulations.getValue(Iso8601.MINIMUM_VALUE); + expect(articulations).toBeDefined(); + expect(articulations['SampleArticulation Yaw']).toEqual(30); + expect(articulations['SampleArticulation Pitch']).toEqual(45); + expect(articulations['SampleArticulation Roll']).toEqual(60); }); }); @@ -3266,7 +3277,7 @@ defineSuite([ show : true, scale : 3.0, minimumPixelSize : 5.0, - gltf : './Data/Models/Box/CesiumBoxTest.gltf', + gltf : './Data/Models/Box-Articulations/Box-Articulations.gltf', incrementallyLoadTextures : true, shadows : 'ENABLED', heightReference: 'CLAMP_TO_GROUND', @@ -3291,6 +3302,11 @@ defineSuite([ unitQuaternion : [0.0, 0.707, 0.0, 0.707] } } + }, + articulations : { + 'SampleArticulation Yaw' : 30, + 'SampleArticulation Pitch' : 45, + 'SampleArticulation Roll' : 60 } } }; @@ -3308,7 +3324,7 @@ defineSuite([ expect(entity.model.show.getValue(validTime)).toEqual(true); expect(entity.model.scale.getValue(validTime)).toEqual(3.0); expect(entity.model.minimumPixelSize.getValue(validTime)).toEqual(5.0); - expect(entity.model.uri.getValue(validTime).url).toEqual('./Data/Models/Box/CesiumBoxTest.gltf'); + expect(entity.model.uri.getValue(validTime).url).toEqual('./Data/Models/Box-Articulations/Box-Articulations.gltf'); expect(entity.model.incrementallyLoadTextures.getValue(validTime)).toEqual(true); expect(entity.model.shadows.getValue(validTime)).toEqual(ShadowMode.ENABLED); expect(entity.model.heightReference.getValue(validTime)).toEqual(HeightReference.CLAMP_TO_GROUND); @@ -3331,6 +3347,12 @@ defineSuite([ expect(entity.model.nodeTransformations.Mesh.translation.getValue(validTime)).toEqual(new Cartesian3(4.0, 5.0, 6.0)); expect(entity.model.nodeTransformations.Mesh.rotation.getValue(validTime)).toEqual(expectedRotation); + var articulations = entity.model.articulations.getValue(validTime); + expect(articulations).toBeDefined(); + expect(articulations['SampleArticulation Yaw']).toEqual(30); + expect(articulations['SampleArticulation Pitch']).toEqual(45); + expect(articulations['SampleArticulation Roll']).toEqual(60); + expect(entity.model.show.getValue(invalidTime)).toBeUndefined(); expect(entity.model.scale.getValue(invalidTime)).toBeUndefined(); expect(entity.model.minimumPixelSize.getValue(invalidTime)).toBeUndefined(); @@ -3348,6 +3370,12 @@ defineSuite([ expect(entity.model.nodeTransformations.Mesh.scale.getValue(invalidTime)).toBeUndefined(); expect(entity.model.nodeTransformations.Mesh.translation.getValue(invalidTime)).toBeUndefined(); expect(entity.model.nodeTransformations.Mesh.rotation.getValue(invalidTime)).toBeUndefined(); + + var invalidArticulations = entity.model.articulations.getValue(invalidTime); + expect(invalidArticulations).toBeDefined(); + expect(invalidArticulations['SampleArticulation Yaw']).toBeUndefined(); + expect(invalidArticulations['SampleArticulation Pitch']).toBeUndefined(); + expect(invalidArticulations['SampleArticulation Roll']).toBeUndefined(); }); }); diff --git a/Specs/DataSources/ModelGraphicsSpec.js b/Specs/DataSources/ModelGraphicsSpec.js index 9d521fc14600..9e194bb108a7 100644 --- a/Specs/DataSources/ModelGraphicsSpec.js +++ b/Specs/DataSources/ModelGraphicsSpec.js @@ -57,6 +57,9 @@ defineSuite([ rotation : new Quaternion(0.5, 0.5, 0.5, 0.5), scale : Cartesian3.UNIT_X } + }, + articulations : { + 'articulation1 stage1' : 45 } }; @@ -82,6 +85,7 @@ defineSuite([ expect(model.clampAnimations).toBeInstanceOf(ConstantProperty); expect(model.nodeTransformations).toBeInstanceOf(PropertyBag); + expect(model.articulations).toBeInstanceOf(PropertyBag); expect(model.uri.getValue()).toEqual(options.uri); expect(model.scale.getValue()).toEqual(options.scale); @@ -110,6 +114,14 @@ defineSuite([ actualNodeTransformations = JSON.parse(JSON.stringify(actualNodeTransformations)); expectedNodeTransformations = JSON.parse(JSON.stringify(expectedNodeTransformations)); expect(actualNodeTransformations).toEqual(expectedNodeTransformations); + + var actualArticulations = model.articulations.getValue(new JulianDate()); + var expectedArticulations = options.articulations; + + // by default toEqual requires constructors to match. for the purposes of this test, we only care about the structure. + actualArticulations = JSON.parse(JSON.stringify(actualArticulations)); + expectedArticulations = JSON.parse(JSON.stringify(expectedArticulations)); + expect(actualArticulations).toEqual(expectedArticulations); }); it('merge assigns unassigned properties', function() { @@ -143,6 +155,10 @@ defineSuite([ scale : Cartesian3.UNIT_Z }) }; + source.articulations = { + 'a1 s1' : 10, + 'a2 s2' : 20 + }; var target = new ModelGraphics(); target.merge(source); @@ -167,6 +183,7 @@ defineSuite([ expect(target.runAnimations).toBe(source.runAnimations); expect(target.clampAnimations).toBe(source.clampAnimations); expect(target.nodeTransformations).toEqual(source.nodeTransformations); + expect(target.articulations).toEqual(source.articulations); }); it('merge does not assign assigned properties', function() { @@ -193,6 +210,10 @@ defineSuite([ source.nodeTransformations = { transform : new NodeTransformationProperty() }; + source.articulations = { + 'a1 s1' : 10, + 'a2 s2' : 20 + }; var uri = new ConstantProperty(''); var show = new ConstantProperty(true); @@ -216,6 +237,10 @@ defineSuite([ var nodeTransformations = new PropertyBag({ transform : new NodeTransformationProperty() }); + var articulations = new PropertyBag({ + 'a1 s1' : 10, + 'a2 s2' : 20 + }); var target = new ModelGraphics(); target.uri = uri; @@ -238,6 +263,7 @@ defineSuite([ target.runAnimations = runAnimations; target.clampAnimations = clampAnimations; target.nodeTransformations = nodeTransformations; + target.articulations = articulations; target.merge(source); @@ -261,6 +287,7 @@ defineSuite([ expect(target.runAnimations).toBe(runAnimations); expect(target.clampAnimations).toBe(clampAnimations); expect(target.nodeTransformations).toBe(nodeTransformations); + expect(target.articulations).toBe(articulations); }); it('clone works', function() { @@ -288,6 +315,10 @@ defineSuite([ node1 : new NodeTransformationProperty(), node2 : new NodeTransformationProperty() }; + source.articulations = { + 'a1 s1' : 10, + 'a2 s2' : 20 + }; var result = source.clone(); expect(result.uri).toBe(source.uri); @@ -310,6 +341,7 @@ defineSuite([ expect(result.runAnimations).toBe(source.runAnimations); expect(result.clampAnimations).toBe(source.clampAnimations); expect(result.nodeTransformations).toEqual(source.nodeTransformations); + expect(result.articulations).toEqual(source.articulations); }); it('merge throws if source undefined', function() { diff --git a/Specs/DataSources/ModelVisualizerSpec.js b/Specs/DataSources/ModelVisualizerSpec.js index 34034c24f0d6..e3cf35d5a183 100644 --- a/Specs/DataSources/ModelVisualizerSpec.js +++ b/Specs/DataSources/ModelVisualizerSpec.js @@ -7,6 +7,7 @@ defineSuite([ 'Core/defined', 'Core/DistanceDisplayCondition', 'Core/JulianDate', + 'Core/Math', 'Core/Matrix4', 'Core/Quaternion', 'Core/Resource', @@ -31,6 +32,7 @@ defineSuite([ defined, DistanceDisplayCondition, JulianDate, + CesiumMath, Matrix4, Quaternion, Resource, @@ -49,6 +51,7 @@ defineSuite([ 'use strict'; var boxUrl = './Data/Models/Box/CesiumBoxTest.gltf'; + var boxArticulationsUrl = './Data/Models/Box-Articulations/Box-Articulations.gltf'; var scene; var visualizer; @@ -192,6 +195,59 @@ defineSuite([ }); }); + it('can apply model articulations', function() { + var time = JulianDate.now(); + var entityCollection = new EntityCollection(); + visualizer = new ModelVisualizer(scene, entityCollection); + + var model = new ModelGraphics(); + model.uri = new ConstantProperty(boxArticulationsUrl); + + var articulations = { + 'SampleArticulation MoveX' : 1.0, + 'SampleArticulation MoveY' : 2.0, + 'SampleArticulation MoveZ' : 3.0, + 'SampleArticulation Yaw' : 4.0, + 'SampleArticulation Pitch' : 5.0, + 'SampleArticulation Roll' : 6.0, + 'SampleArticulation Size' : 0.9, + 'SampleArticulation SizeX' : 0.8, + 'SampleArticulation SizeY' : 0.7, + 'SampleArticulation SizeZ' : 0.6 + }; + model.articulations = articulations; + + var testObject = entityCollection.getOrCreateEntity('test'); + testObject.position = new ConstantPositionProperty(Cartesian3.fromDegrees(1, 2, 3)); + testObject.model = model; + + visualizer.update(time); + + expect(scene.primitives.length).toEqual(1); + + var primitive = scene.primitives.get(0); + + // wait till the model is loaded before we can check articulations + return pollToPromise(function() { + scene.render(); + return primitive.ready; + }).then(function() { + visualizer.update(time); + + var node = primitive.getNode('Root'); + expect(node.useMatrix).toBe(true); + + var expected = [ + 0.7147690483240505, -0.04340611926232735, -0.0749741046529782, 0, + -0.06188330295778636, 0.05906797312763484, -0.6241645867602773, 0, + 0.03752515582279579, 0.5366347296529127, 0.04706410108373541, 0, + 1, 3, -2, 1 + ]; + + expect(node.matrix).toEqualEpsilon(expected, CesiumMath.EPSILON14); + }); + }); + it('A ModelGraphics with a Resource causes a primitive to be created.', function() { var time = JulianDate.now(); var entityCollection = new EntityCollection();