@cesium/engine
Version:
CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.
649 lines (586 loc) • 17.5 kB
JavaScript
import Cartesian3 from "../../Core/Cartesian3.js";
import Check from "../../Core/Check.js";
import Frozen from "../../Core/Frozen.js";
import defined from "../../Core/defined.js";
import DeveloperError from "../../Core/DeveloperError.js";
import Matrix4 from "../../Core/Matrix4.js";
import TranslationRotationScale from "../../Core/TranslationRotationScale.js";
import Quaternion from "../../Core/Quaternion.js";
import InstancingPipelineStage from "./InstancingPipelineStage.js";
import ModelMatrixUpdateStage from "./ModelMatrixUpdateStage.js";
import NodeStatisticsPipelineStage from "./NodeStatisticsPipelineStage.js";
/**
* An in-memory representation of a node as part of the {@link ModelSceneGraph}.
*
* @param {object} options An object containing the following options:
* @param {ModelComponents.Node} options.node The corresponding node components from the 3D model.
* @param {Matrix4} options.transform The transform of this node, excluding transforms from the node's ancestors or children.
* @param {Matrix4} options.transformToRoot The product of the transforms of all the node's ancestors, excluding the node's own transform.
* @param {ModelSceneGraph} options.sceneGraph The scene graph this node belongs to.
* @param {number[]} options.children The indices of the children of this node in the runtime nodes array of the scene graph.
*
* @alias ModelRuntimeNode
* @constructor
*
* @private
*/
function ModelRuntimeNode(options) {
options = options ?? Frozen.EMPTY_OBJECT;
const node = options.node;
const transform = options.transform;
const transformToRoot = options.transformToRoot;
const sceneGraph = options.sceneGraph;
const children = options.children;
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("options.node", node);
Check.typeOf.object("options.transform", transform);
Check.typeOf.object("options.transformToRoot", transformToRoot);
Check.typeOf.object("options.sceneGraph", sceneGraph);
Check.typeOf.object("options.children", children);
//>>includeEnd('debug');
this._node = node;
this._name = node.name;
this._id = node.index;
this._sceneGraph = sceneGraph;
this._children = children;
this._originalTransform = Matrix4.clone(transform, this._originalTransform);
this._transform = Matrix4.clone(transform, this._transform);
this._transformToRoot = Matrix4.clone(transformToRoot, this._transformToRoot);
this._computedTransform = new Matrix4(); // Computed in initialize()
this._transformDirty = false;
// Used for animation
this._transformParameters = undefined;
this._morphWeights = [];
// Will be set by the scene graph after the skins have been created
this._runtimeSkin = undefined;
this._computedJointMatrices = [];
/**
* Whether or not to show this node and its children. This can be toggled
* by the user through {@link ModelNode}.
*
* @type {boolean}
*
* @default true
*
* @private
*/
this.show = true;
/**
* Whether or not this node is animated by the user. This is set by the
* corresponding {@link ModelNode} when the user supplies their
* own transform. If this is true, the node will ignore animations in the
* model's asset.
*
* @type {boolean}
*
* @private
*/
this.userAnimated = false;
/**
* Pipeline stages to apply across all the mesh primitives of this node.
* This is an array of classes, each with a static method called
* <code>process()</code>.
*
* @type {Object[]}
* @readonly
*
* @private
*/
this.pipelineStages = [];
/**
* The mesh primitives that belong to this node.
*
* @type {ModelRuntimePrimitive[]}
* @readonly
*
* @private
*/
this.runtimePrimitives = [];
/**
* Update stages to apply to this node.
*
* @type {Object[]}
* @readonly
*
* @private
*/
this.updateStages = [];
/**
* The component-wise minimum value of the translations of the instances.
* This value is set by InstancingPipelineStage.
*
* @type {Cartesian3}
*
* @private
*/
this.instancingTranslationMin = undefined;
/**
* The component-wise maximum value of the translations of the instances.
* This value is set by InstancingPipelineStage.
*
* @type {Cartesian3}
*
* @private
*/
this.instancingTranslationMax = undefined;
/**
* A buffer containing the instanced transforms. The memory is managed
* by Model; this is just a reference.
*
* @type {Buffer}
*
* @private
*/
this.instancingTransformsBuffer = undefined;
/**
* A buffer containing the instanced transforms projected to 2D world
* coordinates. Used for rendering in 2D / CV mode. The memory is managed
* by Model; this is just a reference.
*
* @type {Buffer}
*
* @private
*/
this.instancingTransformsBuffer2D = undefined;
/**
* A buffer containing the instanced translation values for the node if
* it is instanced. Used for rendering in 2D / CV mode. The memory is
* managed by Model; this is just a reference.
*
* @type {Buffer}
*
* @private
*/
this.instancingTranslationBuffer2D = undefined;
/**
* If the model is instanced and projected to 2D, the reference point is the
* average of the instancing translation max and min. The 2D translations are
* defined relative to this point to avoid precision issues on the GPU.
* <p>
* This value is set by InstancingPipelineStage.
* </p>
*
* @type {Cartesian3}
*
* @private
*/
this.instancingReferencePoint2D = undefined;
initialize(this);
}
Object.defineProperties(ModelRuntimeNode.prototype, {
/**
* The internal node this runtime node represents.
*
* @memberof ModelRuntimeNode.prototype
* @type {ModelComponents.Node}
* @readonly
*
* @private
*/
node: {
get: function () {
return this._node;
},
},
/**
* The scene graph this node belongs to.
*
* @memberof ModelRuntimeNode.prototype
* @type {ModelSceneGraph}
* @readonly
*
* @private
*/
sceneGraph: {
get: function () {
return this._sceneGraph;
},
},
/**
* The indices of the children of this node in the scene graph.
*
* @memberof ModelRuntimeNode.prototype
* @type {number[]}
* @readonly
*
* @private
*/
children: {
get: function () {
return this._children;
},
},
/**
* The node's local space transform. This can be changed externally via
* the corresponding {@link ModelNode}, such that animation can be
* driven by another source, not just an animation in the model's asset.
*
* @memberof ModelRuntimeNode.prototype
* @type {Matrix4}
*
* @private
*/
transform: {
get: function () {
return this._transform;
},
set: function (value) {
this._transformDirty = true;
this._transform = Matrix4.clone(value, this._transform);
},
},
/**
* The transforms of all the node's ancestors, not including this node's
* transform.
*
* @see ModelRuntimeNode#computedTransform
*
* @memberof ModelRuntimeNode.prototype
* @type {Matrix4}
* @readonly
*
* @private
*/
transformToRoot: {
get: function () {
return this._transformToRoot;
},
},
/**
* A transform from the node's local space to the model's scene graph space.
* This is the product of transformToRoot * transform.
*
* @memberof ModelRuntimeNode.prototype
* @type {Matrix4}
* @readonly
*
* @private
*/
computedTransform: {
get: function () {
return this._computedTransform;
},
},
/**
* The node's original transform, as specified in the model.
* Does not include transformations from the node's ancestors.
*
* @memberof ModelRuntimeNode.prototype
* @type {Matrix4}
* @readonly
*
* @private
*/
originalTransform: {
get: function () {
return this._originalTransform;
},
},
/**
* The node's local space translation. This is used internally to allow
* animations in the model's asset to affect the node's properties.
*
* If the node's transformation was originally described using a matrix
* in the model, then this will return undefined.
*
* @memberof ModelRuntimeNode.prototype
* @type {Cartesian3}
*
* @exception {DeveloperError} The translation of a node cannot be set if it was defined using a matrix in the model's asset.
*
* @private
*/
translation: {
get: function () {
return defined(this._transformParameters)
? this._transformParameters.translation
: undefined;
},
set: function (value) {
const transformParameters = this._transformParameters;
//>>includeStart('debug', pragmas.debug);
if (!defined(transformParameters)) {
throw new DeveloperError(
"The translation of a node cannot be set if it was defined using a matrix in the model.",
);
}
//>>includeEnd('debug');
const currentTranslation = transformParameters.translation;
if (Cartesian3.equals(currentTranslation, value)) {
return;
}
transformParameters.translation = Cartesian3.clone(
value,
transformParameters.translation,
);
updateTransformFromParameters(this, transformParameters);
},
},
/**
* The node's local space rotation. This is used internally to allow
* animations in the model's asset to affect the node's properties.
*
* If the node's transformation was originally described using a matrix
* in the model, then this will return undefined.
*
* @memberof ModelRuntimeNode.prototype
* @type {Quaternion}
*
* @exception {DeveloperError} The rotation of a node cannot be set if it was defined using a matrix in the model's asset.
*
* @private
*/
rotation: {
get: function () {
return defined(this._transformParameters)
? this._transformParameters.rotation
: undefined;
},
set: function (value) {
const transformParameters = this._transformParameters;
//>>includeStart('debug', pragmas.debug);
if (!defined(transformParameters)) {
throw new DeveloperError(
"The rotation of a node cannot be set if it was defined using a matrix in the model.",
);
}
//>>includeEnd('debug');
const currentRotation = transformParameters.rotation;
if (Quaternion.equals(currentRotation, value)) {
return;
}
transformParameters.rotation = Quaternion.clone(
value,
transformParameters.rotation,
);
updateTransformFromParameters(this, transformParameters);
},
},
/**
* The node's local space scale. This is used internally to allow
* animations in the model's asset to affect the node's properties.
*
* If the node's transformation was originally described using a matrix
* in the model, then this will return undefined.
*
* @memberof ModelRuntimeNode.prototype
* @type {Cartesian3}
*
* @exception {DeveloperError} The scale of a node cannot be set if it was defined using a matrix in the model's asset.
* @private
*/
scale: {
get: function () {
return defined(this._transformParameters)
? this._transformParameters.scale
: undefined;
},
set: function (value) {
const transformParameters = this._transformParameters;
//>>includeStart('debug', pragmas.debug);
if (!defined(transformParameters)) {
throw new DeveloperError(
"The scale of a node cannot be set if it was defined using a matrix in the model.",
);
}
//>>includeEnd('debug');
const currentScale = transformParameters.scale;
if (Cartesian3.equals(currentScale, value)) {
return;
}
transformParameters.scale = Cartesian3.clone(
value,
transformParameters.scale,
);
updateTransformFromParameters(this, transformParameters);
},
},
/**
* The node's morph weights. This is used internally to allow animations
* in the model's asset to affect the node's properties.
*
* @memberof ModelRuntimeNode.prototype
* @type {number[]}
*
* @private
*/
morphWeights: {
get: function () {
return this._morphWeights;
},
set: function (value) {
const valueLength = value.length;
//>>includeStart('debug', pragmas.debug);
if (this._morphWeights.length !== valueLength) {
throw new DeveloperError(
"value must have the same length as the original weights array.",
);
}
//>>includeEnd('debug');
for (let i = 0; i < valueLength; i++) {
this._morphWeights[i] = value[i];
}
},
},
/**
* The skin applied to this node, if it exists.
*
* @memberof ModelRuntimeNode.prototype
* @type {ModelSkin}
* @readonly
*
* @private
*/
runtimeSkin: {
get: function () {
return this._runtimeSkin;
},
},
/**
* The computed joint matrices of this node, derived from its skin.
*
* @memberof ModelRuntimeNode.prototype
* @type {Matrix4[]}
* @readonly
*
* @private
*/
computedJointMatrices: {
get: function () {
return this._computedJointMatrices;
},
},
});
function initialize(runtimeNode) {
const transform = runtimeNode.transform;
const transformToRoot = runtimeNode.transformToRoot;
const computedTransform = runtimeNode._computedTransform;
runtimeNode._computedTransform = Matrix4.multiply(
transformToRoot,
transform,
computedTransform,
);
const node = runtimeNode.node;
if (!defined(node.matrix)) {
runtimeNode._transformParameters = new TranslationRotationScale(
node.translation,
node.rotation,
node.scale,
);
}
if (defined(node.morphWeights)) {
runtimeNode._morphWeights = node.morphWeights.slice();
}
// If this node is affected by an articulation from the AGI_articulations
// extension, add this node to its list of affected nodes.
const articulationName = node.articulationName;
if (defined(articulationName)) {
const sceneGraph = runtimeNode.sceneGraph;
const runtimeArticulations = sceneGraph._runtimeArticulations;
const runtimeArticulation = runtimeArticulations[articulationName];
if (defined(runtimeArticulation)) {
runtimeArticulation.runtimeNodes.push(runtimeNode);
}
}
}
function updateTransformFromParameters(runtimeNode, transformParameters) {
runtimeNode._transformDirty = true;
runtimeNode._transform = Matrix4.fromTranslationRotationScale(
transformParameters,
runtimeNode._transform,
);
}
/**
* Returns the child with the given index.
*
* @param {number} index The index of the child.
*
* @returns {ModelRuntimeNode}
*
* @example
* // Iterate through all children of a runtime node.
* for (let i = 0; i < runtimeNode.children.length; i++)
* {
* const childNode = runtimeNode.getChild(i);
* }
*
* @private
*/
ModelRuntimeNode.prototype.getChild = function (index) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.number("index", index);
if (index < 0 || index >= this.children.length) {
throw new DeveloperError(
"index must be greater than or equal to 0 and less than the number of children.",
);
}
//>>includeEnd('debug');
return this.sceneGraph._runtimeNodes[this.children[index]];
};
/**
* Configure the node pipeline stages. If the pipeline needs to be re-run, call
* this method again to ensure the correct sequence of pipeline stages are
* used.
*
* @private
*/
ModelRuntimeNode.prototype.configurePipeline = function () {
const node = this.node;
const pipelineStages = this.pipelineStages;
pipelineStages.length = 0;
const updateStages = this.updateStages;
updateStages.length = 0;
if (defined(node.instances)) {
pipelineStages.push(InstancingPipelineStage);
}
pipelineStages.push(NodeStatisticsPipelineStage);
updateStages.push(ModelMatrixUpdateStage);
};
/**
* Updates the computed transform used for rendering and instancing.
*
* @private
*/
ModelRuntimeNode.prototype.updateComputedTransform = function () {
this._computedTransform = Matrix4.multiply(
this._transformToRoot,
this._transform,
this._computedTransform,
);
};
/**
* Updates the joint matrices for this node, where each matrix is computed as
* computedJointMatrix = nodeWorldTransform^(-1) * skinJointMatrix.
*
* @private
*/
ModelRuntimeNode.prototype.updateJointMatrices = function () {
const runtimeSkin = this._runtimeSkin;
if (!defined(runtimeSkin)) {
return;
}
runtimeSkin.updateJointMatrices();
const computedJointMatrices = this._computedJointMatrices;
const skinJointMatrices = runtimeSkin.jointMatrices;
const length = skinJointMatrices.length;
for (let i = 0; i < length; i++) {
if (!defined(computedJointMatrices[i])) {
computedJointMatrices[i] = new Matrix4();
}
const nodeWorldTransform = Matrix4.multiplyTransformation(
this.transformToRoot,
this.transform,
computedJointMatrices[i],
);
const inverseNodeWorldTransform = Matrix4.inverseTransformation(
nodeWorldTransform,
computedJointMatrices[i],
);
computedJointMatrices[i] = Matrix4.multiplyTransformation(
inverseNodeWorldTransform,
skinJointMatrices[i],
computedJointMatrices[i],
);
}
};
export default ModelRuntimeNode;