UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

236 lines (235 loc) 9.09 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); 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 { TYPE_UINT32, SEMANTIC_ATTR15, ADDRESS_CLAMP_TO_EDGE, FILTER_NEAREST, PIXELFORMAT_RGBA16F, PIXELFORMAT_RGBA32F, PIXELFORMAT_RGBA16U, isIntegerPixelFormat } from "../platform/graphics/constants.js"; import { Texture } from "../platform/graphics/texture.js"; import { VertexBuffer } from "../platform/graphics/vertex-buffer.js"; import { VertexFormat } from "../platform/graphics/vertex-format.js"; class Morph extends RefCountedObject { /** * Create a new Morph instance. * * @param {MorphTarget[]} targets - A list of morph targets. * @param {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 * preferred. 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 */ __publicField(this, "_aabb"); /** @type {boolean} */ __publicField(this, "preferHighPrecision"); Debug.assert(graphicsDevice, "Morph constructor takes a GraphicsDevice as a parameter, and it was not provided."); this.device = graphicsDevice; const device = graphicsDevice; this.preferHighPrecision = preferHighPrecision; 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(); const renderableHalf = device.textureHalfFloatRenderable ? PIXELFORMAT_RGBA16F : void 0; const renderableFloat = device.textureFloatRenderable ? PIXELFORMAT_RGBA32F : void 0; this._renderTextureFormat = this.preferHighPrecision ? renderableFloat ?? renderableHalf : renderableHalf ?? renderableFloat; this._renderTextureFormat = this._renderTextureFormat ?? PIXELFORMAT_RGBA16U; this.intRenderFormat = isIntegerPixelFormat(this._renderTextureFormat); this._textureFormat = this.preferHighPrecision ? PIXELFORMAT_RGBA32F : PIXELFORMAT_RGBA16F; this._init(); this._updateMorphFlags(); } /** * Frees video memory allocated by this object. */ destroy() { this.vertexBufferIds?.destroy(); this.vertexBufferIds = null; this.targetsTexturePositions?.destroy(); this.targetsTexturePositions = null; this.targetsTextureNormals?.destroy(); this.targetsTextureNormals = null; } get aabb() { if (!this._aabb) { 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; } _init() { this._initTextureBased(); for (let i = 0; i < this._targets.length; i++) { this._targets[i]._postInit(); } } _findSparseSet(deltaArrays, ids, usedDataIndices) { let freeIndex = 1; const dataCount = deltaArrays[0].length; for (let v = 0; v < dataCount; v += 3) { let vertexUsed = false; for (let i = 0; i < deltaArrays.length; i++) { const data = deltaArrays[i]; if (data[v] !== 0 || data[v + 1] !== 0 || data[v + 2] !== 0) { vertexUsed = true; break; } } if (vertexUsed) { ids.push(freeIndex); usedDataIndices.push(v / 3); freeIndex++; } else { ids.push(0); } } return freeIndex; } _initTextureBased() { const deltaArrays = [], deltaInfos = []; const targets = this._targets; for (let i = 0; i < targets.length; i++) { const target = targets[i]; if (target.options.deltaPositions) { deltaArrays.push(target.options.deltaPositions); deltaInfos.push(true); } if (target.options.deltaNormals) { deltaArrays.push(target.options.deltaNormals); deltaInfos.push(false); } } const ids = [], usedDataIndices = []; const freeIndex = this._findSparseSet(deltaArrays, ids, usedDataIndices); const maxTextureSize = this.device.maxTextureSize; let morphTextureWidth = Math.ceil(Math.sqrt(freeIndex)); morphTextureWidth = Math.min(morphTextureWidth, maxTextureSize); const morphTextureHeight = Math.ceil(freeIndex / morphTextureWidth); if (morphTextureHeight > maxTextureSize) { Debug.warnOnce(`Morph target data is too large to fit into a texture array. Required texture size: ${morphTextureWidth}x${morphTextureHeight}, max texture size: ${maxTextureSize}x${maxTextureSize}.`); return; } this.morphTextureWidth = morphTextureWidth; this.morphTextureHeight = morphTextureHeight; let halfFloat = false; const float2Half = FloatPacking.float2Half; if (this._textureFormat === PIXELFORMAT_RGBA16F) { halfFloat = true; } const texturesDataPositions = []; const texturesDataNormals = []; const textureDataSize = morphTextureWidth * morphTextureHeight * 4; for (let i = 0; i < deltaArrays.length; i++) { const data = deltaArrays[i]; const textureData = this._textureFormat === PIXELFORMAT_RGBA16F ? new Uint16Array(textureDataSize) : new Float32Array(textureDataSize); (deltaInfos[i] ? texturesDataPositions : texturesDataNormals).push(textureData); if (halfFloat) { for (let v = 0; v < usedDataIndices.length; v++) { const index = usedDataIndices[v] * 3; const dstIndex = v * 4 + 4; 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 * 4 + 4; textureData[dstIndex] = data[index]; textureData[dstIndex + 1] = data[index + 1]; textureData[dstIndex + 2] = data[index + 2]; } } } if (texturesDataPositions.length > 0) { this.targetsTexturePositions = this._createTexture("MorphPositionsTexture", this._textureFormat, targets.length, [texturesDataPositions]); } if (texturesDataNormals.length > 0) { this.targetsTextureNormals = this._createTexture("MorphNormalsTexture", this._textureFormat, targets.length, [texturesDataNormals]); } const formatDesc = [{ semantic: SEMANTIC_ATTR15, components: 1, type: TYPE_UINT32, asInt: true }]; this.vertexBufferIds = new VertexBuffer(this.device, new VertexFormat(this.device, formatDesc, ids.length), ids.length, { data: new Uint32Array(ids) }); return true; } /** * Gets the array of morph targets. * * @type {MorphTarget[]} */ get targets() { return this._targets; } _updateMorphFlags() { 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 a texture / texture array. Used to create both source morph target data, as well as * render target used to morph these into, positions and normals. * * @param {string} name - The name of the texture. * @param {number} format - The format of the texture. * @param {Array} [levels] - The levels of the texture. * @param {number} [arrayLength] - The length of the texture array. * @returns {Texture} The created texture. * @private */ _createTexture(name, format, arrayLength, levels) { return new Texture(this.device, { levels, arrayLength, width: this.morphTextureWidth, height: this.morphTextureHeight, format, cubemap: false, mipmaps: false, minFilter: FILTER_NEAREST, magFilter: FILTER_NEAREST, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE, name }); } } export { Morph };