@babylonjs/core
Version:
Getting started? Play directly with the Babylon.js API using our [playground](https://playground.babylonjs.com/). It also contains a lot of samples to learn how to use it.
577 lines (576 loc) • 23.2 kB
JavaScript
import { SmartArray } from "../Misc/smartArray.js";
import { Logger } from "../Misc/logger.js";
import { EngineStore } from "../Engines/engineStore.js";
import { MorphTarget } from "./morphTarget.js";
import { RawTexture2DArray } from "../Materials/Textures/rawTexture2DArray.js";
/**
* This class is used to deform meshes using morphing between different targets
* @see https://doc.babylonjs.com/features/featuresDeepDive/mesh/morphTargets
*/
export class MorphTargetManager {
/**
* Sets a boolean indicating that adding new target or updating an existing target will not update the underlying data buffers
*/
set areUpdatesFrozen(block) {
if (block) {
this._blockCounter++;
}
else {
this._blockCounter--;
if (this._blockCounter <= 0) {
this._blockCounter = 0;
this._syncActiveTargets(this._forceUpdateWhenUnfrozen);
this._forceUpdateWhenUnfrozen = false;
}
}
}
get areUpdatesFrozen() {
return this._blockCounter > 0;
}
/**
* Creates a new MorphTargetManager
* @param scene defines the current scene
*/
constructor(scene = null) {
this._targets = new Array();
this._targetInfluenceChangedObservers = new Array();
this._targetDataLayoutChangedObservers = new Array();
this._activeTargets = new SmartArray(16);
this._supportsPositions = false;
this._supportsNormals = false;
this._supportsTangents = false;
this._supportsUVs = false;
this._supportsUV2s = false;
this._supportsColors = false;
this._vertexCount = 0;
this._uniqueId = 0;
this._tempInfluences = new Array();
this._canUseTextureForTargets = false;
this._blockCounter = 0;
this._mustSynchronize = true;
this._forceUpdateWhenUnfrozen = false;
/** @internal */
this._textureVertexStride = 0;
/** @internal */
this._textureWidth = 0;
/** @internal */
this._textureHeight = 1;
/** @internal */
this._parentContainer = null;
/**
* Gets or sets a boolean indicating if influencers must be optimized (eg. recompiling the shader if less influencers are used)
*/
this.optimizeInfluencers = true;
/**
* Gets or sets a boolean indicating if positions must be morphed
*/
this.enablePositionMorphing = true;
/**
* Gets or sets a boolean indicating if normals must be morphed
*/
this.enableNormalMorphing = true;
/**
* Gets or sets a boolean indicating if tangents must be morphed
*/
this.enableTangentMorphing = true;
/**
* Gets or sets a boolean indicating if UV must be morphed
*/
this.enableUVMorphing = true;
/**
* Gets or sets a boolean indicating if UV2 must be morphed
*/
this.enableUV2Morphing = true;
/**
* Gets or sets a boolean indicating if colors must be morphed
*/
this.enableColorMorphing = true;
this._numMaxInfluencers = 0;
this._useTextureToStoreTargets = true;
/**
* Gets or sets an object used to store user defined information for the MorphTargetManager
*/
this.metadata = null;
if (!scene) {
scene = EngineStore.LastCreatedScene;
}
this._scene = scene;
if (this._scene) {
this._scene.addMorphTargetManager(this);
this._uniqueId = this._scene.getUniqueId();
const engineCaps = this._scene.getEngine().getCaps();
this._canUseTextureForTargets =
engineCaps.canUseGLVertexID && engineCaps.textureFloat && engineCaps.maxVertexTextureImageUnits > 0 && engineCaps.texture2DArrayMaxLayerCount > 1;
}
}
/**
* Gets or sets the maximum number of influencers (targets) (default value: 0).
* Setting a value for this property can lead to a smoother experience, as only one shader will be compiled, which will use this value as the maximum number of influencers.
* If you leave the value at 0 (default), a new shader will be compiled every time the number of active influencers changes. This can cause problems, as compiling a shader takes time.
* If you assign a non-zero value to this property, you need to ensure that this value is greater than the maximum number of (active) influencers you'll need for this morph manager.
* Otherwise, the number of active influencers will be truncated at the value you set for this property, which can lead to unexpected results.
* Note that this property has no effect if "useTextureToStoreTargets" is false.
* Note as well that if MorphTargetManager.ConstantTargetCountForTextureMode is greater than 0, this property will be ignored and the constant value will be used instead.
*/
get numMaxInfluencers() {
if (MorphTargetManager.ConstantTargetCountForTextureMode > 0 && this.isUsingTextureForTargets) {
return MorphTargetManager.ConstantTargetCountForTextureMode;
}
return this._numMaxInfluencers;
}
set numMaxInfluencers(value) {
if (this._numMaxInfluencers === value) {
return;
}
this._numMaxInfluencers = value;
this._mustSynchronize = true;
this._syncActiveTargets();
}
/**
* Gets the unique ID of this manager
*/
get uniqueId() {
return this._uniqueId;
}
/**
* Gets the number of vertices handled by this manager
*/
get vertexCount() {
return this._vertexCount;
}
/**
* Gets a boolean indicating if this manager supports morphing of positions
*/
get supportsPositions() {
return this._supportsPositions && this.enablePositionMorphing;
}
/**
* Gets a boolean indicating if this manager supports morphing of normals
*/
get supportsNormals() {
return this._supportsNormals && this.enableNormalMorphing;
}
/**
* Gets a boolean indicating if this manager supports morphing of tangents
*/
get supportsTangents() {
return this._supportsTangents && this.enableTangentMorphing;
}
/**
* Gets a boolean indicating if this manager supports morphing of texture coordinates
*/
get supportsUVs() {
return this._supportsUVs && this.enableUVMorphing;
}
/**
* Gets a boolean indicating if this manager supports morphing of texture coordinates 2
*/
get supportsUV2s() {
return this._supportsUV2s && this.enableUV2Morphing;
}
/**
* Gets a boolean indicating if this manager supports morphing of colors
*/
get supportsColors() {
return this._supportsColors && this.enableColorMorphing;
}
/**
* Gets a boolean indicating if this manager has data for morphing positions
*/
get hasPositions() {
return this._supportsPositions;
}
/**
* Gets a boolean indicating if this manager has data for morphing normals
*/
get hasNormals() {
return this._supportsNormals;
}
/**
* Gets a boolean indicating if this manager has data for morphing tangents
*/
get hasTangents() {
return this._supportsTangents;
}
/**
* Gets a boolean indicating if this manager has data for morphing texture coordinates
*/
get hasUVs() {
return this._supportsUVs;
}
/**
* Gets a boolean indicating if this manager has data for morphing texture coordinates 2
*/
get hasUV2s() {
return this._supportsUV2s;
}
/**
* Gets a boolean indicating if this manager has data for morphing colors
*/
get hasColors() {
return this._supportsColors;
}
/**
* Gets the number of targets stored in this manager
*/
get numTargets() {
return this._targets.length;
}
/**
* Gets the number of influencers (ie. the number of targets with influences > 0)
*/
get numInfluencers() {
return this._activeTargets.length;
}
/**
* Gets the list of influences (one per target)
*/
get influences() {
return this._influences;
}
/**
* Gets or sets a boolean indicating that targets should be stored as a texture instead of using vertex attributes (default is true).
* Please note that this option is not available if the hardware does not support it
*/
get useTextureToStoreTargets() {
return this._useTextureToStoreTargets;
}
set useTextureToStoreTargets(value) {
if (this._useTextureToStoreTargets === value) {
return;
}
this._useTextureToStoreTargets = value;
this._mustSynchronize = true;
this._syncActiveTargets();
}
/**
* Gets a boolean indicating that the targets are stored into a texture (instead of as attributes)
*/
get isUsingTextureForTargets() {
return (MorphTargetManager.EnableTextureStorage &&
this.useTextureToStoreTargets &&
this._canUseTextureForTargets &&
!this._scene?.getEngine().getCaps().disableMorphTargetTexture);
}
/**
* Gets the active target at specified index. An active target is a target with an influence > 0
* @param index defines the index to check
* @returns the requested target
*/
getActiveTarget(index) {
return this._activeTargets.data[index];
}
/**
* Gets the target at specified index
* @param index defines the index to check
* @returns the requested target
*/
getTarget(index) {
return this._targets[index];
}
/**
* Gets the first target with the specified name
* @param name defines the name to check
* @returns the requested target
*/
getTargetByName(name) {
for (const target of this._targets) {
if (target.name === name) {
return target;
}
}
return null;
}
/**
* Add a new target to this manager
* @param target defines the target to add
*/
addTarget(target) {
this._targets.push(target);
this._targetInfluenceChangedObservers.push(target.onInfluenceChanged.add((needUpdate) => {
if (this.areUpdatesFrozen && needUpdate) {
this._forceUpdateWhenUnfrozen = true;
}
this._syncActiveTargets(needUpdate);
}));
this._targetDataLayoutChangedObservers.push(target._onDataLayoutChanged.add(() => {
this._mustSynchronize = true;
this._syncActiveTargets();
}));
this._mustSynchronize = true;
this._syncActiveTargets();
}
/**
* Removes a target from the manager
* @param target defines the target to remove
*/
removeTarget(target) {
const index = this._targets.indexOf(target);
if (index >= 0) {
this._targets.splice(index, 1);
target.onInfluenceChanged.remove(this._targetInfluenceChangedObservers.splice(index, 1)[0]);
target._onDataLayoutChanged.remove(this._targetDataLayoutChangedObservers.splice(index, 1)[0]);
this._mustSynchronize = true;
this._syncActiveTargets();
}
if (this._scene) {
this._scene.stopAnimation(target);
}
}
/**
* @internal
*/
_bind(effect) {
effect.setFloat3("morphTargetTextureInfo", this._textureVertexStride, this._textureWidth, this._textureHeight);
effect.setFloatArray("morphTargetTextureIndices", this._morphTargetTextureIndices);
effect.setTexture("morphTargets", this._targetStoreTexture);
effect.setFloat("morphTargetCount", this.numInfluencers);
}
/**
* Clone the current manager
* @returns a new MorphTargetManager
*/
clone() {
const copy = new MorphTargetManager(this._scene);
copy.areUpdatesFrozen = true;
for (const target of this._targets) {
copy.addTarget(target.clone());
}
copy.areUpdatesFrozen = false;
copy.enablePositionMorphing = this.enablePositionMorphing;
copy.enableNormalMorphing = this.enableNormalMorphing;
copy.enableTangentMorphing = this.enableTangentMorphing;
copy.enableUVMorphing = this.enableUVMorphing;
copy.enableUV2Morphing = this.enableUV2Morphing;
copy.enableColorMorphing = this.enableColorMorphing;
copy.metadata = this.metadata;
return copy;
}
/**
* Serializes the current manager into a Serialization object
* @returns the serialized object
*/
serialize() {
const serializationObject = {};
serializationObject.id = this.uniqueId;
serializationObject.targets = [];
for (const target of this._targets) {
serializationObject.targets.push(target.serialize());
}
if (this.metadata) {
serializationObject.metadata = this.metadata;
}
return serializationObject;
}
_syncActiveTargets(needUpdate = false) {
if (this.areUpdatesFrozen) {
return;
}
const wasUsingTextureForTargets = !!this._targetStoreTexture;
const isUsingTextureForTargets = this.isUsingTextureForTargets;
if (this._mustSynchronize || wasUsingTextureForTargets !== isUsingTextureForTargets) {
this._mustSynchronize = false;
this.synchronize();
}
let influenceCount = 0;
this._activeTargets.reset();
if (!this._morphTargetTextureIndices || this._morphTargetTextureIndices.length !== this._targets.length) {
this._morphTargetTextureIndices = new Float32Array(this._targets.length);
}
let targetIndex = -1;
for (const target of this._targets) {
targetIndex++;
if (target.influence === 0 && this.optimizeInfluencers) {
continue;
}
if (this._activeTargets.length >= MorphTargetManager.MaxActiveMorphTargetsInVertexAttributeMode && !this.isUsingTextureForTargets) {
break;
}
this._activeTargets.push(target);
this._morphTargetTextureIndices[influenceCount] = targetIndex;
this._tempInfluences[influenceCount++] = target.influence;
}
if (this._morphTargetTextureIndices.length !== influenceCount) {
this._morphTargetTextureIndices = this._morphTargetTextureIndices.slice(0, influenceCount);
}
if (!this._influences || this._influences.length !== influenceCount) {
this._influences = new Float32Array(influenceCount);
}
for (let index = 0; index < influenceCount; index++) {
this._influences[index] = this._tempInfluences[index];
}
if (needUpdate && this._scene) {
for (const mesh of this._scene.meshes) {
if (mesh.morphTargetManager === this) {
if (isUsingTextureForTargets) {
mesh._markSubMeshesAsAttributesDirty();
}
else {
mesh._syncGeometryWithMorphTargetManager();
}
}
}
}
}
/**
* Synchronize the targets with all the meshes using this morph target manager
*/
synchronize() {
if (!this._scene || this.areUpdatesFrozen) {
return;
}
const engine = this._scene.getEngine();
this._supportsPositions = true;
this._supportsNormals = true;
this._supportsTangents = true;
this._supportsUVs = true;
this._supportsUV2s = true;
this._supportsColors = true;
this._vertexCount = 0;
this._targetStoreTexture?.dispose();
this._targetStoreTexture = null;
if (this.isUsingTextureForTargets && this._targets.length > engine.getCaps().texture2DArrayMaxLayerCount) {
this.useTextureToStoreTargets = false;
}
for (const target of this._targets) {
this._supportsPositions = this._supportsPositions && target.hasPositions;
this._supportsNormals = this._supportsNormals && target.hasNormals;
this._supportsTangents = this._supportsTangents && target.hasTangents;
this._supportsUVs = this._supportsUVs && target.hasUVs;
this._supportsUV2s = this._supportsUV2s && target.hasUV2s;
this._supportsColors = this._supportsColors && target.hasColors;
const vertexCount = target.vertexCount;
if (this._vertexCount === 0) {
this._vertexCount = vertexCount;
}
else if (this._vertexCount !== vertexCount) {
Logger.Error(`Incompatible target. Targets must all have the same vertices count. Current vertex count: ${this._vertexCount}, vertex count for target "${target.name}": ${vertexCount}`);
return;
}
}
if (this.isUsingTextureForTargets) {
this._textureVertexStride = 0;
this._supportsPositions && this._textureVertexStride++;
this._supportsNormals && this._textureVertexStride++;
this._supportsTangents && this._textureVertexStride++;
this._supportsUVs && this._textureVertexStride++;
this._supportsUV2s && this._textureVertexStride++;
this._supportsColors && this._textureVertexStride++;
this._textureWidth = this._vertexCount * this._textureVertexStride || 1;
this._textureHeight = 1;
const maxTextureSize = engine.getCaps().maxTextureSize;
if (this._textureWidth > maxTextureSize) {
this._textureHeight = Math.ceil(this._textureWidth / maxTextureSize);
this._textureWidth = maxTextureSize;
}
const targetCount = this._targets.length;
const data = new Float32Array(targetCount * this._textureWidth * this._textureHeight * 4);
let offset = 0;
for (let index = 0; index < targetCount; index++) {
const target = this._targets[index];
const positions = target.getPositions();
const normals = target.getNormals();
const uvs = target.getUVs();
const tangents = target.getTangents();
const uv2s = target.getUV2s();
const colors = target.getColors();
offset = index * this._textureWidth * this._textureHeight * 4;
for (let vertex = 0; vertex < this._vertexCount; vertex++) {
if (this._supportsPositions && positions) {
data[offset] = positions[vertex * 3];
data[offset + 1] = positions[vertex * 3 + 1];
data[offset + 2] = positions[vertex * 3 + 2];
offset += 4;
}
if (this._supportsNormals && normals) {
data[offset] = normals[vertex * 3];
data[offset + 1] = normals[vertex * 3 + 1];
data[offset + 2] = normals[vertex * 3 + 2];
offset += 4;
}
if (this._supportsUVs && uvs) {
data[offset] = uvs[vertex * 2];
data[offset + 1] = uvs[vertex * 2 + 1];
offset += 4;
}
if (this._supportsTangents && tangents) {
data[offset] = tangents[vertex * 3];
data[offset + 1] = tangents[vertex * 3 + 1];
data[offset + 2] = tangents[vertex * 3 + 2];
offset += 4;
}
if (this._supportsUV2s && uv2s) {
data[offset] = uv2s[vertex * 2];
data[offset + 1] = uv2s[vertex * 2 + 1];
offset += 4;
}
if (this._supportsColors && colors) {
data[offset] = colors[vertex * 4];
data[offset + 1] = colors[vertex * 4 + 1];
data[offset + 2] = colors[vertex * 4 + 2];
data[offset + 3] = colors[vertex * 4 + 3];
offset += 4;
}
}
}
this._targetStoreTexture = RawTexture2DArray.CreateRGBATexture(data, this._textureWidth, this._textureHeight, targetCount, this._scene, false, false, 1, 1);
this._targetStoreTexture.name = `Morph texture_${this.uniqueId}`;
}
// Flag meshes as dirty to resync with the active targets
for (const mesh of this._scene.meshes) {
if (mesh.morphTargetManager === this) {
mesh._syncGeometryWithMorphTargetManager();
}
}
}
/**
* Release all resources
*/
dispose() {
if (this._targetStoreTexture) {
this._targetStoreTexture.dispose();
}
this._targetStoreTexture = null;
this.metadata = null;
// Remove from scene
if (this._scene) {
this._scene.removeMorphTargetManager(this);
if (this._parentContainer) {
const index = this._parentContainer.morphTargetManagers.indexOf(this);
if (index > -1) {
this._parentContainer.morphTargetManagers.splice(index, 1);
}
this._parentContainer = null;
}
for (const morph of this._targets) {
this._scene.stopAnimation(morph);
}
}
}
// Statics
/**
* Creates a new MorphTargetManager from serialized data
* @param serializationObject defines the serialized data
* @param scene defines the hosting scene
* @returns the new MorphTargetManager
*/
static Parse(serializationObject, scene) {
const result = new MorphTargetManager(scene);
for (const targetData of serializationObject.targets) {
result.addTarget(MorphTarget.Parse(targetData, scene));
}
if (serializationObject.metadata) {
result.metadata = serializationObject.metadata;
}
return result;
}
}
/** Enable storing morph target data into textures when set to true (true by default) */
MorphTargetManager.EnableTextureStorage = true;
/** Maximum number of active morph targets supported in the "vertex attribute" mode (i.e., not the "texture" mode) */
MorphTargetManager.MaxActiveMorphTargetsInVertexAttributeMode = 8;
/**
* When used in texture mode, if greather than 0, this will override the the morph manager numMaxInfluencers value.
*/
MorphTargetManager.ConstantTargetCountForTextureMode = 0;
//# sourceMappingURL=morphTargetManager.js.map