diff --git a/Apps/Sandcastle/CesiumSandcastle.css b/Apps/Sandcastle/CesiumSandcastle.css index 2c87c1799c4e..5013fc8d8d93 100644 --- a/Apps/Sandcastle/CesiumSandcastle.css +++ b/Apps/Sandcastle/CesiumSandcastle.css @@ -138,7 +138,7 @@ html, body { height: 100%; transition-property: transform; transition-duration: 0.5s; - transform-origin: 200px 150px; + transform-origin: 200px 152px; /* These numbers should be divisible by 4 because of scaling in .makeThumbnail */ } .makeThumbnail { diff --git a/Apps/Sandcastle/gallery/development/3D Models Articulations.html b/Apps/Sandcastle/gallery/development/3D Models Articulations.html new file mode 100644 index 000000000000..3393d1b4a3b1 --- /dev/null +++ b/Apps/Sandcastle/gallery/development/3D Models Articulations.html @@ -0,0 +1,152 @@ + + +
+ + + + + ++ | + + + | +
result
parameter, in that the incoming value of result
is
+ * meaningful. Various stages of an articulation can be multiplied together, so their
+ * transformations are all merged into a composite Matrix4 representing them all.
+ *
+ * @param {object} stage The stage of an articulation that is being evaluated.
+ * @param {Matrix4} result The matrix to be modified.
+ * @returns {Matrix4} A matrix transformed as requested by the articulation stage.
+ *
+ * @private
+ */
+ function applyArticulationStageMatrix(stage, result) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.object('stage', stage);
+ Check.typeOf.object('result', result);
+ //>>includeEnd('debug');
+
+ var value = stage.currentValue;
+ var cartesian = scratchArticulationCartesian;
+ var rotation;
+ switch (stage.type) {
+ case 'xRotate':
+ rotation = Matrix3.fromRotationX(CesiumMath.toRadians(value), scratchArticulationRotation);
+ Matrix4.multiplyByMatrix3(result, rotation, result);
+ break;
+ case 'yRotate':
+ rotation = Matrix3.fromRotationY(CesiumMath.toRadians(value), scratchArticulationRotation);
+ Matrix4.multiplyByMatrix3(result, rotation, result);
+ break;
+ case 'zRotate':
+ rotation = Matrix3.fromRotationZ(CesiumMath.toRadians(value), scratchArticulationRotation);
+ Matrix4.multiplyByMatrix3(result, rotation, result);
+ break;
+ case 'xTranslate':
+ cartesian.x = value;
+ cartesian.y = 0.0;
+ cartesian.z = 0.0;
+ Matrix4.multiplyByTranslation(result, cartesian, result);
+ break;
+ case 'yTranslate':
+ cartesian.x = 0.0;
+ cartesian.y = value;
+ cartesian.z = 0.0;
+ Matrix4.multiplyByTranslation(result, cartesian, result);
+ break;
+ case 'zTranslate':
+ cartesian.x = 0.0;
+ cartesian.y = 0.0;
+ cartesian.z = value;
+ Matrix4.multiplyByTranslation(result, cartesian, result);
+ break;
+ case 'xScale':
+ cartesian.x = value;
+ cartesian.y = 1.0;
+ cartesian.z = 1.0;
+ Matrix4.multiplyByScale(result, cartesian, result);
+ break;
+ case 'yScale':
+ cartesian.x = 1.0;
+ cartesian.y = value;
+ cartesian.z = 1.0;
+ Matrix4.multiplyByScale(result, cartesian, result);
+ break;
+ case 'zScale':
+ cartesian.x = 1.0;
+ cartesian.y = 1.0;
+ cartesian.z = value;
+ Matrix4.multiplyByScale(result, cartesian, result);
+ break;
+ case 'uniformScale':
+ Matrix4.multiplyByUniformScale(result, value, result);
+ break;
+ default:
+ break;
+ }
+ return result;
+ }
+
+ var scratchApplyArticulationTransform = new Matrix4();
+
+ /**
+ * Applies any modified articulation stages to the matrix of each node that participates
+ * in any articulation. Note that this will overwrite any nodeTransformations on participating nodes.
+ *
+ * @exception {DeveloperError} The model is not loaded. Use Model.readyPromise or wait for Model.ready to be true.
+ */
+ Model.prototype.applyArticulations = function() {
+ var articulationsByName = this._runtime.articulationsByName;
+ for (var articulationName in articulationsByName) {
+ if (articulationsByName.hasOwnProperty(articulationName)) {
+ var articulation = articulationsByName[articulationName];
+ if (articulation.isDirty) {
+ articulation.isDirty = false;
+ var numNodes = articulation.nodes.length;
+ for (var n = 0; n < numNodes; ++n) {
+ var node = articulation.nodes[n];
+ var transform = Matrix4.clone(node.originalMatrix, scratchApplyArticulationTransform);
+
+ var numStages = articulation.stages.length;
+ for (var s = 0; s < numStages; ++s) {
+ var stage = articulation.stages[s];
+ transform = applyArticulationStageMatrix(stage, transform);
+ }
+ node.matrix = transform;
+ }
+ }
+ }
+ }
+ };
+
///////////////////////////////////////////////////////////////////////////
function addBuffersToLoadResources(model) {
@@ -1631,6 +1781,44 @@ define([
}
}
+ function parseArticulations(model) {
+ var articulationsByName = {};
+ var articulationsByStageKey = {};
+ var runtimeStagesByKey = {};
+
+ model._runtime.articulationsByName = articulationsByName;
+ model._runtime.articulationsByStageKey = articulationsByStageKey;
+ model._runtime.stagesByKey = runtimeStagesByKey;
+
+ var gltf = model.gltf;
+ if (!hasExtension(gltf, 'AGI_articulations') || !defined(gltf.extensions) || !defined(gltf.extensions.AGI_articulations)) {
+ return;
+ }
+
+ var gltfArticulations = gltf.extensions.AGI_articulations.articulations;
+ if (!defined(gltfArticulations)) {
+ return;
+ }
+
+ var numArticulations = gltfArticulations.length;
+ for (var i = 0; i < numArticulations; ++i) {
+ var articulation = clone(gltfArticulations[i]);
+ articulation.nodes = [];
+ articulation.isDirty = true;
+ articulationsByName[articulation.name] = articulation;
+
+ var numStages = articulation.stages.length;
+ for (var s = 0; s < numStages; ++s) {
+ var stage = articulation.stages[s];
+ stage.currentValue = stage.initialValue;
+
+ var stageKey = articulation.name + ' ' + stage.name;
+ articulationsByStageKey[stageKey] = articulation;
+ runtimeStagesByKey[stageKey] = stage;
+ }
+ }
+ }
+
function imageLoad(model, textureId) {
return function(image) {
var loadResources = model._loadResources;
@@ -1733,12 +1921,15 @@ define([
});
}
+ var scratchArticulationStageInitialTransform = new Matrix4();
+
function parseNodes(model) {
var runtimeNodes = {};
var runtimeNodesByName = {};
var skinnedNodes = [];
var skinnedNodesIds = model._loadResources.skinnedNodesIds;
+ var articulationsByName = model._runtime.articulationsByName;
ForEach.node(model.gltf, function(node, id) {
var runtimeNode = {
@@ -1786,6 +1977,22 @@ define([
skinnedNodesIds.push(id);
skinnedNodes.push(runtimeNode);
}
+
+ if (defined(node.extensions) && defined(node.extensions.AGI_articulations)) {
+ var articulationName = node.extensions.AGI_articulations.articulationName;
+ if (defined(articulationName)) {
+ var transform = Matrix4.clone(runtimeNode.publicNode.originalMatrix, scratchArticulationStageInitialTransform);
+ var articulation = articulationsByName[articulationName];
+ articulation.nodes.push(runtimeNode.publicNode);
+
+ var numStages = articulation.stages.length;
+ for (var s = 0; s < numStages; ++s) {
+ var stage = articulation.stages[s];
+ transform = applyArticulationStageMatrix(stage, transform);
+ }
+ runtimeNode.publicNode.matrix = transform;
+ }
+ }
});
model._runtime.nodes = runtimeNodes;
@@ -4465,6 +4672,7 @@ define([
// We do this after to make sure that the ids don't change
addBuffersToLoadResources(this);
+ parseArticulations(this);
parseTechniques(this);
if (!this._loadRendererResourcesFromCache) {
parseBufferViews(this);
diff --git a/Source/Scene/ModelNode.js b/Source/Scene/ModelNode.js
index 517dde68305a..8bbfb727b77a 100644
--- a/Source/Scene/ModelNode.js
+++ b/Source/Scene/ModelNode.js
@@ -38,6 +38,7 @@ define([
this._show = true;
this._matrix = Matrix4.clone(matrix);
+ this._originalMatrix = Matrix4.clone(matrix);
}
defineProperties(ModelNode.prototype, {
@@ -112,6 +113,19 @@ define([
model._cesiumAnimationsDirty = true;
this._runtimeNode.dirtyNumber = model._maxDirtyNumber;
}
+ },
+
+ /**
+ * Gets the node's original 4x4 matrix transform from its local coordinates to
+ * its parent's, without any node transformations or articulations applied.
+ *
+ * @memberof ModelNode.prototype
+ * @type {Matrix4}
+ */
+ originalMatrix : {
+ get : function() {
+ return this._originalMatrix;
+ }
}
});
diff --git a/Source/Scene/ModelUtility.js b/Source/Scene/ModelUtility.js
index 53a517e95c67..f8fe7ff0eda4 100644
--- a/Source/Scene/ModelUtility.js
+++ b/Source/Scene/ModelUtility.js
@@ -485,7 +485,9 @@ define([
};
ModelUtility.supportedExtensions = {
+ 'AGI_articulations' : true,
'CESIUM_RTC' : true,
+ 'EXT_texture_webp' : true,
'KHR_blend' : true,
'KHR_binary_glTF' : true,
'KHR_draco_mesh_compression' : true,
@@ -494,8 +496,7 @@ define([
'KHR_materials_unlit' : true,
'KHR_materials_pbrSpecularGlossiness' : true,
'KHR_texture_transform' : true,
- 'WEB3D_quantized_attributes' : true,
- 'EXT_texture_webp' : true
+ 'WEB3D_quantized_attributes' : true
};
ModelUtility.checkSupportedExtensions = function(extensionsRequired, browserSupportsWebp) {
diff --git a/Specs/Data/Models/Box-Articulations/Box-Articulations.gltf b/Specs/Data/Models/Box-Articulations/Box-Articulations.gltf
new file mode 100644
index 000000000000..d5f906c215d0
--- /dev/null
+++ b/Specs/Data/Models/Box-Articulations/Box-Articulations.gltf
@@ -0,0 +1,241 @@
+{
+ "asset": {
+ "generator": "COLLADA2GLTF",
+ "version": "2.0"
+ },
+ "scene": 0,
+ "scenes": [
+ {
+ "nodes": [
+ 0
+ ]
+ }
+ ],
+ "nodes": [
+ {
+ "name": "Root",
+ "children": [
+ 1
+ ],
+ "matrix": [
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ -1,
+ 0,
+ 0,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 1
+ ],
+ "extensions": {
+ "AGI_articulations": {
+ "articulationName": "SampleArticulation"
+ }
+ }
+ },
+ {
+ "mesh": 0
+ }
+ ],
+ "meshes": [
+ {
+ "primitives": [
+ {
+ "attributes": {
+ "NORMAL": 1,
+ "POSITION": 2
+ },
+ "indices": 0,
+ "mode": 4,
+ "material": 0
+ }
+ ],
+ "name": "Mesh"
+ }
+ ],
+ "accessors": [
+ {
+ "bufferView": 0,
+ "byteOffset": 0,
+ "componentType": 5123,
+ "count": 36,
+ "max": [
+ 23
+ ],
+ "min": [
+ 0
+ ],
+ "type": "SCALAR"
+ },
+ {
+ "bufferView": 1,
+ "byteOffset": 0,
+ "componentType": 5126,
+ "count": 24,
+ "max": [
+ 1,
+ 1,
+ 1
+ ],
+ "min": [
+ -1,
+ -1,
+ -1
+ ],
+ "type": "VEC3"
+ },
+ {
+ "bufferView": 1,
+ "byteOffset": 288,
+ "componentType": 5126,
+ "count": 24,
+ "max": [
+ 0.5,
+ 0.5,
+ 0.5
+ ],
+ "min": [
+ -0.5,
+ -0.5,
+ -0.5
+ ],
+ "type": "VEC3"
+ }
+ ],
+ "materials": [
+ {
+ "pbrMetallicRoughness": {
+ "baseColorFactor": [
+ 0.800000011920929,
+ 0,
+ 0,
+ 1
+ ],
+ "metallicFactor": 0,
+ "roughnessFactor": 1
+ },
+ "name": "Red",
+ "emissiveFactor": [
+ 0,
+ 0,
+ 0
+ ],
+ "alphaMode": "OPAQUE",
+ "doubleSided": false
+ }
+ ],
+ "bufferViews": [
+ {
+ "buffer": 0,
+ "byteOffset": 0,
+ "byteLength": 72,
+ "target": 34963
+ },
+ {
+ "buffer": 0,
+ "byteOffset": 72,
+ "byteLength": 576,
+ "byteStride": 12,
+ "target": 34962
+ }
+ ],
+ "buffers": [
+ {
+ "name": "Box0",
+ "byteLength": 648,
+ "uri": "data:application/octet-stream;base64,AAABAAIAAwACAAEABAAFAAYABwAGAAUACAAJAAoACwAKAAkADAANAA4ADwAOAA0AEAARABIAEwASABEAFAAVABYAFwAWABUAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAvwAAAL8AAAA/AAAAPwAAAL8AAAA/AAAAvwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAPwAAAL8AAAA/AAAAvwAAAL8AAAA/AAAAPwAAAL8AAAC/AAAAvwAAAL8AAAC/AAAAPwAAAD8AAAA/AAAAPwAAAL8AAAA/AAAAPwAAAD8AAAC/AAAAPwAAAL8AAAC/AAAAvwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAvwAAAD8AAAC/AAAAPwAAAD8AAAC/AAAAvwAAAL8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAPwAAAL8AAAC/AAAAPwAAAD8AAAC/"
+ }
+ ],
+ "extensionsUsed": [
+ "AGI_articulations"
+ ],
+ "extensions": {
+ "AGI_articulations": {
+ "articulations": [
+ {
+ "name": "SampleArticulation",
+ "stages": [
+ {
+ "name": "MoveX",
+ "type": "xTranslate",
+ "minimumValue": -1000.0,
+ "maximumValue": 1000.0,
+ "initialValue": 0.0
+ },
+ {
+ "name": "MoveY",
+ "type": "yTranslate",
+ "minimumValue": -1000.0,
+ "maximumValue": 1000.0,
+ "initialValue": 0.0
+ },
+ {
+ "name": "MoveZ",
+ "type": "zTranslate",
+ "minimumValue": -1000.0,
+ "maximumValue": 1000.0,
+ "initialValue": 0.0
+ },
+ {
+ "name": "Yaw",
+ "type": "yRotate",
+ "minimumValue": -360.0,
+ "maximumValue": 360.0,
+ "initialValue": 0.0
+ },
+ {
+ "name": "Pitch",
+ "type": "xRotate",
+ "minimumValue": -360.0,
+ "maximumValue": 360.0,
+ "initialValue": 0.0
+ },
+ {
+ "name": "Roll",
+ "type": "zRotate",
+ "minimumValue": -360.0,
+ "maximumValue": 360.0,
+ "initialValue": 0.0
+ },
+ {
+ "name": "Size",
+ "type": "uniformScale",
+ "minimumValue": 0.0,
+ "maximumValue": 1.0,
+ "initialValue": 1.0
+ },
+ {
+ "name": "SizeX",
+ "type": "xScale",
+ "minimumValue": 0.0,
+ "maximumValue": 1.0,
+ "initialValue": 1.0
+ },
+ {
+ "name": "SizeY",
+ "type": "yScale",
+ "minimumValue": 0.0,
+ "maximumValue": 1.0,
+ "initialValue": 1.0
+ },
+ {
+ "name": "SizeZ",
+ "type": "zScale",
+ "minimumValue": 0.0,
+ "maximumValue": 1.0,
+ "initialValue": 1.0
+ }
+ ]
+ }
+ ]
+ }
+ }
+}
diff --git a/Specs/Scene/ModelSpec.js b/Specs/Scene/ModelSpec.js
index 7beafda1a432..0dce256ffe87 100644
--- a/Specs/Scene/ModelSpec.js
+++ b/Specs/Scene/ModelSpec.js
@@ -95,6 +95,7 @@ defineSuite([
var boxRtcUrl = './Data/Models/Box-RTC/Box.gltf';
var boxEcefUrl = './Data/Models/Box-ECEF/ecef.gltf';
var boxWithUnusedMaterial = './Data/Models/BoxWithUnusedMaterial/Box.gltf';
+ var boxArticulationsUrl = './Data/Models/Box-Articulations/Box-Articulations.gltf';
var cesiumAirUrl = './Data/Models/CesiumAir/Cesium_Air.gltf';
var cesiumAir_0_8Url = './Data/Models/CesiumAir/Cesium_Air_0_8.gltf';
@@ -1041,6 +1042,38 @@ defineSuite([
});
});
+ it('loads a glTF 2.0 model with AGI_articulations extension', function() {
+ return loadModel(boxArticulationsUrl).then(function(m) {
+ verifyRender(m);
+
+ m.setArticulationStage('SampleArticulation MoveX', 1.0);
+ m.setArticulationStage('SampleArticulation MoveY', 2.0);
+ m.setArticulationStage('SampleArticulation MoveZ', 3.0);
+ m.setArticulationStage('SampleArticulation Yaw', 4.0);
+ m.setArticulationStage('SampleArticulation Pitch', 5.0);
+ m.setArticulationStage('SampleArticulation Roll', 6.0);
+ m.setArticulationStage('SampleArticulation Size', 0.9);
+ m.setArticulationStage('SampleArticulation SizeX', 0.8);
+ m.setArticulationStage('SampleArticulation SizeY', 0.7);
+ m.setArticulationStage('SampleArticulation SizeZ', 0.6);
+ m.applyArticulations();
+
+ var node = m.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);
+
+ primitives.remove(m);
+ });
+ });
+
it('loads a glTF model with unused material', function() {
return loadModel(boxWithUnusedMaterial).then(function(m) {
verifyRender(m);