UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

249 lines (246 loc) 9.57 kB
import { Debug } from '../core/debug.js'; import { SEMANTIC_POSITION } from '../platform/graphics/constants.js'; import { drawQuadWithShader } from './graphics/quad-render-utils.js'; import { RenderTarget } from '../platform/graphics/render-target.js'; import { DebugGraphics } from '../platform/graphics/debug-graphics.js'; import { ShaderUtils } from './shader-lib/shader-utils.js'; import { BlendState } from '../platform/graphics/blend-state.js'; /** * @import { Morph } from './morph.js' * @import { Shader } from '../platform/graphics/shader.js' */ /** * An instance of {@link Morph}. Contains weights to assign to every {@link MorphTarget}, manages * selection of active morph targets. * * @category Graphics */ class MorphInstance { /** * Create a new MorphInstance instance. * * @param {Morph} morph - The {@link Morph} to instance. */ constructor(morph){ /** * The morph with its targets, which is being instanced. * * @type {Morph} */ this.morph = morph; morph.incRefCount(); this.device = morph.device; // shader to blend a required number of morph targets const maxNumTargets = morph._targets.length; this.shader = this._createShader(maxNumTargets); // weights this._weights = []; this._weightMap = new Map(); for(let v = 0; v < morph._targets.length; v++){ const target = morph._targets[v]; if (target.name) { this._weightMap.set(target.name, v); } this.setWeight(v, target.defaultWeight); } // array for max number of weights this._shaderMorphWeights = new Float32Array(maxNumTargets); // array for target indices this._shaderMorphIndex = new Uint32Array(maxNumTargets); // create render targets to morph targets into const createRT = (name, textureVar)=>{ // render to appropriate, RGBA formats this[textureVar] = morph._createTexture(name, morph._renderTextureFormat); return new RenderTarget({ colorBuffer: this[textureVar], depth: false }); }; if (morph.morphPositions) { this.rtPositions = createRT('MorphRTPos', 'texturePositions'); } if (morph.morphNormals) { this.rtNormals = createRT('MorphRTNrm', 'textureNormals'); } this._textureParams = new Float32Array([ morph.morphTextureWidth, morph.morphTextureHeight ]); // position aabb data - expand it 2x on each side to handle the expected worse range. Note // that this is only needed for the fallback solution using integer textures to encode positions const halfSize = morph.aabb.halfExtents; this._aabbSize = new Float32Array([ halfSize.x * 4, halfSize.y * 4, halfSize.z * 4 ]); const min = morph.aabb.getMin(); this._aabbMin = new Float32Array([ min.x * 2, min.y * 2, min.z * 2 ]); // aabb size and min factors for normal rendering, where the range is -1..1 this._aabbNrmSize = new Float32Array([ 2, 2, 2 ]); this._aabbNrmMin = new Float32Array([ -1, -1, -1 ]); this.aabbSizeId = this.device.scope.resolve('aabbSize'); this.aabbMinId = this.device.scope.resolve('aabbMin'); // resolve shader inputs this.morphTextureId = this.device.scope.resolve('morphTexture'); this.morphFactor = this.device.scope.resolve('morphFactor[0]'); this.morphIndex = this.device.scope.resolve('morphIndex[0]'); this.countId = this.device.scope.resolve('count'); // true indicates render target textures are full of zeros to avoid rendering to them when all weights are zero this.zeroTextures = false; } /** * Frees video memory allocated by this object. */ destroy() { // don't destroy shader as it's in the cache and can be used by other materials this.shader = null; const morph = this.morph; if (morph) { // decrease ref count this.morph = null; morph.decRefCount(); // destroy morph if (morph.refCount < 1) { morph.destroy(); } } this.rtPositions?.destroy(); this.rtPositions = null; this.texturePositions?.destroy(); this.texturePositions = null; this.rtNormals?.destroy(); this.rtNormals = null; this.textureNormals?.destroy(); this.textureNormals = null; } /** * Clones a MorphInstance. The returned clone uses the same {@link Morph} and weights are set * to defaults. * * @returns {MorphInstance} A clone of the specified MorphInstance. */ clone() { return new MorphInstance(this.morph); } _getWeightIndex(key) { if (typeof key === 'string') { const index = this._weightMap.get(key); if (index === undefined) { Debug.errorOnce(`Cannot find morph target with name: ${key}.`); } return index; } return key; } /** * Gets current weight of the specified morph target. * * @param {string|number} key - An identifier for the morph target. Either the weight index or * the weight name. * @returns {number} Weight. */ getWeight(key) { const index = this._getWeightIndex(key); return this._weights[index]; } /** * Sets weight of the specified morph target. * * @param {string|number} key - An identifier for the morph target. Either the weight index or * the weight name. * @param {number} weight - Weight. */ setWeight(key, weight) { const index = this._getWeightIndex(key); Debug.assert(index >= 0 && index < this.morph._targets.length); this._weights[index] = weight; this._dirty = true; } /** * Create the shader for texture based morphing. * * @param {number} maxCount - Maximum bumber of textures to blend. * @returns {Shader} Shader. * @private */ _createShader(maxCount) { const defines = new Map(); defines.set('{MORPH_TEXTURE_MAX_COUNT}', maxCount); if (this.morph.intRenderFormat) defines.set('MORPH_INT', ''); const outputType = this.morph.intRenderFormat ? 'uvec4' : 'vec4'; return ShaderUtils.createShader(this.device, { uniqueName: `TextureMorphShader_${maxCount}-${this.morph.intRenderFormat ? 'int' : 'float'}`, attributes: { vertex_position: SEMANTIC_POSITION }, vertexChunk: 'morphVS', fragmentChunk: 'morphPS', fragmentDefines: defines, fragmentOutputTypes: [ outputType ] }); } _updateTextureRenderTarget(renderTarget, activeCount, isPos) { const { morph, device } = this; this.setAabbUniforms(isPos); this.morphTextureId.setValue(isPos ? morph.targetsTexturePositions : morph.targetsTextureNormals); device.setBlendState(BlendState.NOBLEND); // set up parameters for active blend targets this.countId.setValue(activeCount); this.morphFactor.setValue(this._shaderMorphWeights); this.morphIndex.setValue(this._shaderMorphIndex); // render quad with shader drawQuadWithShader(device, renderTarget, this.shader); } _updateTextureMorph(activeCount) { const device = this.device; DebugGraphics.pushGpuMarker(device, 'MorphUpdate'); // update textures if active targets, or no active targets and textures need to be cleared if (activeCount > 0 || !this.zeroTextures) { // blend morph targets into render targets if (this.rtPositions) { this._updateTextureRenderTarget(this.rtPositions, activeCount, true); } if (this.rtNormals) { this._updateTextureRenderTarget(this.rtNormals, activeCount, false); } // textures were cleared if no active targets this.zeroTextures = activeCount === 0; } DebugGraphics.popGpuMarker(device); } setAabbUniforms(isPos = true) { this.aabbSizeId.setValue(isPos ? this._aabbSize : this._aabbNrmSize); this.aabbMinId.setValue(isPos ? this._aabbMin : this._aabbNrmMin); } prepareRendering(device) { this.setAabbUniforms(); } /** * Selects active morph targets and prepares morph for rendering. Called automatically by * renderer. */ update() { this._dirty = false; const targets = this.morph._targets; // collect weights for active targets const epsilon = 0.00001; const weights = this._shaderMorphWeights; const indices = this._shaderMorphIndex; let activeCount = 0; for(let i = 0; i < targets.length; i++){ if (Math.abs(this.getWeight(i)) > epsilon) { weights[activeCount] = this.getWeight(i); indices[activeCount] = i; activeCount++; } } // prepare for rendering this._updateTextureMorph(activeCount); } } export { MorphInstance };