UNPKG

@animech-public/playcanvas

Version:
308 lines (283 loc) 11.6 kB
import { Debug } from '../core/debug.js'; import { RefCountedObject } from '../core/ref-counted-object.js'; import { Vec3 } from '../core/math/vec3.js'; import { FloatPacking } from '../core/math/float-packing.js'; import { BoundingBox } from '../core/shape/bounding-box.js'; import { Texture } from '../platform/graphics/texture.js'; import { VertexBuffer } from '../platform/graphics/vertex-buffer.js'; import { VertexFormat } from '../platform/graphics/vertex-format.js'; import { PIXELFORMAT_RGBA16F, PIXELFORMAT_RGBA32F, PIXELFORMAT_RGB32F, FILTER_NEAREST, ADDRESS_CLAMP_TO_EDGE, SEMANTIC_ATTR15, TYPE_UINT32, TYPE_FLOAT32 } from '../platform/graphics/constants.js'; /** * Contains a list of {@link MorphTarget}, a combined delta AABB and some associated data. * * @category Graphics */ class Morph extends RefCountedObject { /** * Create a new Morph instance. * * @param {import('./morph-target.js').MorphTarget[]} targets - A list of morph targets. * @param {import('../platform/graphics/graphics-device.js').GraphicsDevice} graphicsDevice - * The graphics device used to manage this morph target. * @param {object} [options] - Object for passing optional arguments. * @param {boolean} [options.preferHighPrecision] - True if high precision storage should be * prefered. This is faster to create and allows higher precision, but takes more memory and * might be slower to render. Defaults to false. */ constructor(targets, graphicsDevice, { preferHighPrecision = false } = {}) { super(); /** * @type {BoundingBox} * @private */ this._aabb = void 0; /** @type {boolean} */ this.preferHighPrecision = void 0; Debug.assert(graphicsDevice, 'Morph constructor takes a GraphicsDevice as a parameter, and it was not provided.'); this.device = graphicsDevice; this.preferHighPrecision = preferHighPrecision; // validation Debug.assert(targets.every(target => !target.used), 'A specified target has already been used to create a Morph, use its clone instead.'); this._targets = targets.slice(); // default to texture based morphing if available const device = this.device; if (device.supportsMorphTargetTexturesCore) { // renderable format const renderableHalf = device.extTextureHalfFloat && device.textureHalfFloatRenderable ? PIXELFORMAT_RGBA16F : undefined; const renderableFloat = device.extTextureFloat && device.textureFloatRenderable ? PIXELFORMAT_RGBA32F : undefined; this._renderTextureFormat = this.preferHighPrecision ? renderableFloat != null ? renderableFloat : renderableHalf : renderableHalf != null ? renderableHalf : renderableFloat; // texture format const textureHalf = device.extTextureHalfFloat && device.textureHalfFloatUpdatable ? PIXELFORMAT_RGBA16F : undefined; const textureFloat = device.extTextureFloat ? PIXELFORMAT_RGB32F : undefined; this._textureFormat = this.preferHighPrecision ? textureFloat != null ? textureFloat : textureHalf : textureHalf != null ? textureHalf : textureFloat; // if both available, enable texture morphing if (this._renderTextureFormat !== undefined && this._textureFormat !== undefined) { this._useTextureMorph = true; } } this._init(); this._updateMorphFlags(); } get aabb() { // lazy evaluation, which allows us to skip this completely if customAABB is used if (!this._aabb) { // calculate min and max expansion size // Note: This represents average case, where most morph targets expand the mesh within the same area. It does not // represent the stacked worst case scenario where all morphs could be enabled at the same time, as this can result // in a very large aabb. In cases like this, the users should specify customAabb for Model/Render component. const min = new Vec3(); const max = new Vec3(); for (let i = 0; i < this._targets.length; i++) { const targetAabb = this._targets[i].aabb; min.min(targetAabb.getMin()); max.max(targetAabb.getMax()); } this._aabb = new BoundingBox(); this._aabb.setMinMax(min, max); } return this._aabb; } get morphPositions() { return this._morphPositions; } get morphNormals() { return this._morphNormals; } get maxActiveTargets() { // no limit when texture morph based if (this._useTextureMorph) { return this._targets.length; } return this._morphPositions && this._morphNormals ? 4 : 8; } get useTextureMorph() { return this._useTextureMorph; } _init() { // try to init texture based morphing if (this._useTextureMorph) { this._useTextureMorph = this._initTextureBased(); } // if texture morphing is not set up, use attribute based morphing if (!this._useTextureMorph) { for (let i = 0; i < this._targets.length; i++) { this._targets[i]._initVertexBuffers(this.device); } } // finalize init for (let i = 0; i < this._targets.length; i++) { this._targets[i]._postInit(); } } _findSparseSet(deltaArrays, ids, usedDataIndices, floatRounding) { let freeIndex = 1; // reserve slot 0 for zero delta const dataCount = deltaArrays[0].length; for (let v = 0; v < dataCount; v += 3) { // find if vertex is morphed by any target let vertexUsed = false; for (let i = 0; i < deltaArrays.length; i++) { const data = deltaArrays[i]; // if non-zero delta if (data[v] !== 0 || data[v + 1] !== 0 || data[v + 2] !== 0) { vertexUsed = true; break; } } if (vertexUsed) { ids.push(freeIndex + floatRounding); usedDataIndices.push(v / 3); freeIndex++; } else { // non morphed vertices would be all mapped to pixel 0 of texture ids.push(0 + floatRounding); } } return freeIndex; } _initTextureBased() { // use uint32 for vertex Ids instead of float32 const useUintIds = this.device.isWebGPU; // value added to floats which are used as ints on the shader side to avoid values being rounded to one less occasionally const floatRounding = useUintIds ? 0 : 0.2; // collect all source delta arrays to find sparse set of vertices const deltaArrays = [], deltaInfos = []; for (let i = 0; i < this._targets.length; i++) { const target = this._targets[i]; if (target.options.deltaPositions) { deltaArrays.push(target.options.deltaPositions); deltaInfos.push({ target: target, name: 'texturePositions' }); } if (target.options.deltaNormals) { deltaArrays.push(target.options.deltaNormals); deltaInfos.push({ target: target, name: 'textureNormals' }); } } // find sparse set for all target deltas into usedDataIndices and build vertex id buffer const ids = [], usedDataIndices = []; const freeIndex = this._findSparseSet(deltaArrays, ids, usedDataIndices, floatRounding); // max texture size: vertexBufferIds is stored in float32 format, giving us 2^24 range, so can address 4096 texture at maximum // TODO: on webgl2 we could store this in uint32 format and remove this limit const maxTextureSize = Math.min(this.device.maxTextureSize, 4096); // texture size for freeIndex pixels - roughly square let morphTextureWidth = Math.ceil(Math.sqrt(freeIndex)); morphTextureWidth = Math.min(morphTextureWidth, maxTextureSize); const morphTextureHeight = Math.ceil(freeIndex / morphTextureWidth); // if data cannot fit into max size texture, fail this set up if (morphTextureHeight > maxTextureSize) { return false; } this.morphTextureWidth = morphTextureWidth; this.morphTextureHeight = morphTextureHeight; // texture format based vars let halfFloat = false; let numComponents = 3; // RGB32 is used const float2Half = FloatPacking.float2Half; if (this._textureFormat === PIXELFORMAT_RGBA16F) { halfFloat = true; numComponents = 4; // RGBA16 is used, RGB16 does not work } // create textures const textures = []; for (let i = 0; i < deltaArrays.length; i++) { textures.push(this._createTexture('MorphTarget', this._textureFormat)); } // build texture for each delta array, all textures are the same size for (let i = 0; i < deltaArrays.length; i++) { const data = deltaArrays[i]; const texture = textures[i]; const textureData = texture.lock(); // copy full arrays into sparse arrays and convert format (skip 0th pixel - used by non-morphed vertices) if (halfFloat) { for (let v = 0; v < usedDataIndices.length; v++) { const index = usedDataIndices[v] * 3; const dstIndex = v * numComponents + numComponents; textureData[dstIndex] = float2Half(data[index]); textureData[dstIndex + 1] = float2Half(data[index + 1]); textureData[dstIndex + 2] = float2Half(data[index + 2]); } } else { for (let v = 0; v < usedDataIndices.length; v++) { const index = usedDataIndices[v] * 3; const dstIndex = v * numComponents + numComponents; textureData[dstIndex] = data[index]; textureData[dstIndex + 1] = data[index + 1]; textureData[dstIndex + 2] = data[index + 2]; } } // assign texture to target texture.unlock(); const target = deltaInfos[i].target; target._setTexture(deltaInfos[i].name, texture); } // create vertex stream with vertex_id used to map vertex to texture const formatDesc = [{ semantic: SEMANTIC_ATTR15, components: 1, type: useUintIds ? TYPE_UINT32 : TYPE_FLOAT32 }]; this.vertexBufferIds = new VertexBuffer(this.device, new VertexFormat(this.device, formatDesc, ids.length), ids.length, { data: useUintIds ? new Uint32Array(ids) : new Float32Array(ids) }); return true; } /** * Frees video memory allocated by this object. */ destroy() { var _this$vertexBufferIds; (_this$vertexBufferIds = this.vertexBufferIds) == null || _this$vertexBufferIds.destroy(); this.vertexBufferIds = null; for (let i = 0; i < this._targets.length; i++) { this._targets[i].destroy(); } this._targets.length = 0; } /** * Gets the array of morph targets. * * @type {import('./morph-target.js').MorphTarget[]} */ get targets() { return this._targets; } _updateMorphFlags() { // find out if this morph needs to morph positions and normals this._morphPositions = false; this._morphNormals = false; for (let i = 0; i < this._targets.length; i++) { const target = this._targets[i]; if (target.morphPositions) { this._morphPositions = true; } if (target.morphNormals) { this._morphNormals = true; } } } // creates texture. Used to create both source morph target data, as well as render target used to morph these into, positions and normals _createTexture(name, format) { return new Texture(this.device, { width: this.morphTextureWidth, height: this.morphTextureHeight, format: format, cubemap: false, mipmaps: false, minFilter: FILTER_NEAREST, magFilter: FILTER_NEAREST, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE, name: name }); } } export { Morph };