playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
236 lines (235 loc) • 9.09 kB
JavaScript
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
};