diff --git a/src/deprecated/deprecated.js b/src/deprecated/deprecated.js index 79358fc03fa..ca7e1ba038f 100644 --- a/src/deprecated/deprecated.js +++ b/src/deprecated/deprecated.js @@ -868,12 +868,6 @@ ForwardRenderer.prototype.renderComposition = function (comp) { getApplication().renderComposition(comp); }; -ForwardRenderer.prototype.updateShader = function (meshInstance, objDefs, unused, pass, sortedLights) { - Debug.deprecated('pc.ForwardRenderer#updateShader is deprecated, use pc.MeshInstance#updatePassShader.'); - const scene = meshInstance.material._scene || getApplication().scene; - return meshInstance.updatePassShader(scene, pass, sortedLights); -}; - MeshInstance.prototype.syncAabb = function () { Debug.deprecated('pc.MeshInstance#syncAabb is deprecated.'); }; diff --git a/src/scene/materials/basic-material.js b/src/scene/materials/basic-material.js index 8a5770cc78f..553b9da1e71 100644 --- a/src/scene/materials/basic-material.js +++ b/src/scene/materials/basic-material.js @@ -1,4 +1,3 @@ -import { Debug } from '../../core/debug.js'; import { Color } from '../../core/math/color.js'; import { ShaderProcessorOptions } from '../../platform/graphics/shader-processor-options.js'; @@ -88,14 +87,6 @@ class BasicMaterial extends Material { getShaderVariant(device, scene, objDefs, unused, pass, sortedLights, viewUniformFormat, viewBindGroupFormat, vertexFormat) { - // Note: this is deprecated function Editor and possibly other projects use: they define - // updateShader callback on their BasicMaterial, so we handle it here. - if (this.updateShader) { - Debug.deprecated('pc.BasicMaterial.updateShader is deprecated'); - this.updateShader(device, scene, objDefs, null, pass, sortedLights); - return this.shader; - } - const options = { skin: objDefs && (objDefs & SHADERDEF_SKIN) !== 0, screenSpace: objDefs && (objDefs & SHADERDEF_SCREENSPACE) !== 0, diff --git a/src/scene/materials/material.js b/src/scene/materials/material.js index 4f653def456..df4bc6da4fc 100644 --- a/src/scene/materials/material.js +++ b/src/scene/materials/material.js @@ -79,7 +79,14 @@ class Material { id = id++; - variants = {}; + /** + * The cache of shader variants generated for this material. The key represents the unique + * variant, the value is the shader. + * + * @type {Map} + * @ignore + */ + variants = new Map(); parameters = {}; @@ -472,7 +479,7 @@ class Material { clearVariants() { // clear variants on the material - this.variants = {}; + this.variants.clear(); // but also clear them from all materials that reference them const meshInstances = this.meshInstances; @@ -556,7 +563,7 @@ class Material { * are no other materials using it). */ destroy() { - this.variants = {}; + this.variants.clear(); this._shader = null; for (let i = 0; i < this.meshInstances.length; i++) { diff --git a/src/scene/mesh-instance.js b/src/scene/mesh-instance.js index bd9ff040db3..59bfbc084a3 100644 --- a/src/scene/mesh-instance.js +++ b/src/scene/mesh-instance.js @@ -59,6 +59,85 @@ class Command { } } +/** + * Internal helper class for storing the shader and related mesh bind group in the shader cache. + * + * @ignore + */ +class ShaderInstance { + /** + * A shader. + * + * @type {import('../platform/graphics/shader.js').Shader|undefined} + */ + shader; + + /** + * A bind group storing mesh uniforms for the shader. + * + * @type {BindGroup|null} + */ + bindGroup = null; + + /** + * Returns the mesh bind group for the shader. + * + * @param {import('../platform/graphics/graphics-device.js').GraphicsDevice} device - The + * graphics device. + * @returns {BindGroup} - The mesh bind group. + */ + getBindGroup(device) { + + // create bind group + if (!this.bindGroup) { + const shader = this.shader; + Debug.assert(shader); + + // mesh uniform buffer + const ubFormat = shader.meshUniformBufferFormat; + Debug.assert(ubFormat); + const uniformBuffer = new UniformBuffer(device, ubFormat, false); + + // mesh bind group + const bindGroupFormat = shader.meshBindGroupFormat; + Debug.assert(bindGroupFormat); + this.bindGroup = new BindGroup(device, bindGroupFormat, uniformBuffer); + DebugHelper.setName(this.bindGroup, `MeshBindGroup_${this.bindGroup.id}`); + } + + return this.bindGroup; + } + + destroy() { + const group = this.bindGroup; + if (group) { + group.defaultUniformBuffer?.destroy(); + group.destroy(); + this.bindGroup = null; + } + } +} + +/** + * An entry in the shader cache, representing shaders for this mesh instance and a specific shader + * pass. + * + * @ignore + */ +class ShaderCacheEntry { + /** + * The shader instances. Looked up by lightHash, which represents an ordered set of lights. + * + * @type {Map} + */ + shaderInstances = new Map(); + + destroy() { + this.shaderInstances.forEach(instance => instance.destroy()); + this.shaderInstances.clear(); + } +} + /** * Callback used by {@link Layer} to calculate the "sort distance" for a {@link MeshInstance}, * which determines its place in the render order. @@ -105,27 +184,18 @@ class MeshInstance { transparent = false; /** - * @type {import('./materials/material.js').Material} + * @type {import('./materials/material.js').Material|null} * @private */ - _material; + _material = null; /** - * An array of shaders used by the mesh instance, indexed by the shader pass constant (SHADER_FORWARD..) + * An array of shader cache entries, indexed by the shader pass constant (SHADER_FORWARD..). The + * value stores all shaders and bind groups for the shader pass for various light combinations. * - * @type {Array} - * @ignore + * @type {Array} */ - _shader = []; - - /** - * An array of bind groups, storing uniforms per pass. This has 1:1 relation with the _shades array, - * and is indexed by the shader pass constant as well. - * - * @type {Array} - * @ignore - */ - _bindGroups = []; + _shaderCache = []; /** * Create a new MeshInstance instance. @@ -178,8 +248,6 @@ class MeshInstance { this._shaderDefs |= mesh.vertexBuffer.format.hasColor ? SHADERDEF_VCOLOR : 0; this._shaderDefs |= mesh.vertexBuffer.format.hasTangents ? SHADERDEF_TANGENTS : 0; - this._lightHash = 0; - // Render options this.layer = LAYER_WORLD; // legacy /** @private */ @@ -212,12 +280,13 @@ class MeshInstance { this.updateKey(); /** - * @type {import('./skin-instance.js').SkinInstance} + * @type {import('./skin-instance.js').SkinInstance|null} * @private */ this._skinInstance = null; + /** - * @type {import('./morph-instance.js').MorphInstance} + * @type {import('./morph-instance.js').MorphInstance|null} * @private */ this._morphInstance = null; @@ -225,7 +294,7 @@ class MeshInstance { this.instancingData = null; /** - * @type {BoundingBox} + * @type {BoundingBox|null} * @private */ this._customAabb = null; @@ -400,65 +469,71 @@ class MeshInstance { } /** - * Clear the internal shader array. + * Clear the internal shader cache. * * @ignore */ clearShaders() { - const shaders = this._shader; - for (let i = 0; i < shaders.length; i++) { - shaders[i] = null; + const shaderCache = this._shaderCache; + for (let i = 0; i < shaderCache.length; i++) { + shaderCache[i]?.destroy(); + shaderCache[i] = null; } - - this.destroyBindGroups(); - } - - destroyBindGroups() { - - const groups = this._bindGroups; - for (let i = 0; i < groups.length; i++) { - const group = groups[i]; - if (group) { - const uniformBuffer = group.defaultUniformBuffer; - if (uniformBuffer) { - uniformBuffer.destroy(); - } - group.destroy(); - } - } - groups.length = 0; } /** - * @param {import('../platform/graphics/graphics-device.js').GraphicsDevice} device - The - * graphics device. - * @param {number} pass - Shader pass number. - * @returns {BindGroup} - The mesh bind group. + * Returns the shader instance for the specified shader pass and light hash that is compatible + * with this mesh instance. + * + * @param {number} shaderPass - The shader pass index. + * @param {number} lightHash - The hash value of the lights that are affecting this mesh instance. + * @param {import('./scene.js').Scene} scene - The scene. + * @param {import('../platform/graphics/uniform-buffer-format.js').UniformBufferFormat} [viewUniformFormat] - The + * format of the view uniform buffer. + * @param {import('../platform/graphics/bind-group-format.js').BindGroupFormat} [viewBindGroupFormat] - The + * format of the view bind group. + * @param {any} [sortedLights] - Array of arrays of lights. + * @returns {ShaderInstance} - the shader instance. * @ignore */ - getBindGroup(device, pass) { + getShaderInstance(shaderPass, lightHash, scene, viewUniformFormat, viewBindGroupFormat, sortedLights) { - // create bind group - let bindGroup = this._bindGroups[pass]; - if (!bindGroup) { - const shader = this._shader[pass]; - Debug.assert(shader); + let shaderInstance; + let passEntry = this._shaderCache[shaderPass]; + if (passEntry) { + shaderInstance = passEntry.shaderInstances.get(lightHash); + } else { + passEntry = new ShaderCacheEntry(); + this._shaderCache[shaderPass] = passEntry; + } - // mesh uniform buffer - const ubFormat = shader.meshUniformBufferFormat; - Debug.assert(ubFormat); - const uniformBuffer = new UniformBuffer(device, ubFormat, false); + // cache miss in the shader cache of the mesh instance + if (!shaderInstance) { - // mesh bind group - const bindGroupFormat = shader.meshBindGroupFormat; - Debug.assert(bindGroupFormat); - bindGroup = new BindGroup(device, bindGroupFormat, uniformBuffer); - DebugHelper.setName(bindGroup, `MeshBindGroup_${bindGroup.id}`); + // get the shader from the material + const mat = this._material; + const shaderDefs = this._shaderDefs; + const variantKey = shaderPass + '_' + shaderDefs + '_' + lightHash; + shaderInstance = new ShaderInstance(); + shaderInstance.shader = mat.variants.get(variantKey); + + // cache miss in the material variants + if (!shaderInstance.shader) { + + const shader = mat.getShaderVariant(this.mesh.device, scene, shaderDefs, null, shaderPass, sortedLights, + viewUniformFormat, viewBindGroupFormat, this._mesh.vertexBuffer.format); - this._bindGroups[pass] = bindGroup; + // add it to the material variants cache + mat.variants.set(variantKey, shader); + + shaderInstance.shader = shader; + } + + // add it to the mesh instance cache + passEntry.shaderInstances.set(lightHash, shaderInstance); } - return bindGroup; + return shaderInstance; } /** @@ -731,23 +806,6 @@ class MeshInstance { this._updateShaderDefs(vertexBuffer ? (this._shaderDefs | SHADERDEF_INSTANCING) : (this._shaderDefs & ~SHADERDEF_INSTANCING)); } - /** - * Obtain a shader variant required to render the mesh instance within specified pass. - * - * @param {import('./scene.js').Scene} scene - The scene. - * @param {number} pass - The render pass. - * @param {any} sortedLights - Array of arrays of lights. - * @param {import('../platform/graphics/uniform-buffer-format.js').UniformBufferFormat} viewUniformFormat - The - * format of the view uniform buffer. - * @param {import('../platform/graphics/bind-group-format.js').BindGroupFormat} viewBindGroupFormat - The - * format of the view bind group. - * @ignore - */ - updatePassShader(scene, pass, sortedLights, viewUniformFormat, viewBindGroupFormat) { - this._shader[pass] = this.material.getShaderVariant(this.mesh.device, scene, this._shaderDefs, null, pass, sortedLights, - viewUniformFormat, viewBindGroupFormat, this._mesh.vertexBuffer.format); - } - ensureMaterial(device) { if (!this.material) { Debug.warn(`Mesh attached to entity '${this.node.name}' does not have a material, using a default one.`); diff --git a/src/scene/renderer/forward-renderer.js b/src/scene/renderer/forward-renderer.js index fb8fd593997..b1599c8e4c3 100644 --- a/src/scene/renderer/forward-renderer.js +++ b/src/scene/renderer/forward-renderer.js @@ -26,11 +26,13 @@ const webgl1DepthClearColor = new Color(254.0 / 255, 254.0 / 255, 254.0 / 255, 2 const _drawCallList = { drawCalls: [], + shaderInstances: [], isNewMaterial: [], lightMaskChanged: [], clear: function () { this.drawCalls.length = 0; + this.shaderInstances.length = 0; this.isNewMaterial.length = 0; this.lightMaskChanged.length = 0; } @@ -463,8 +465,9 @@ class ForwardRenderer extends Renderer { // execute first pass over draw calls, in order to update materials / shaders renderForwardPrepareMaterials(camera, drawCalls, sortedLights, layer, pass) { - const addCall = (drawCall, isNewMaterial, lightMaskChanged) => { + const addCall = (drawCall, shaderInstance, isNewMaterial, lightMaskChanged) => { _drawCallList.drawCalls.push(drawCall); + _drawCallList.shaderInstances.push(shaderInstance); _drawCallList.isNewMaterial.push(isNewMaterial); _drawCallList.lightMaskChanged.push(lightMaskChanged); }; @@ -486,7 +489,7 @@ class ForwardRenderer extends Renderer { if (drawCall.command) { - addCall(drawCall, false, false); + addCall(drawCall, null, false, false); } else { @@ -523,27 +526,14 @@ class ForwardRenderer extends Renderer { } } - if (!drawCall._shader[pass] || drawCall._shaderDefs !== objDefs || drawCall._lightHash !== lightHash) { - - // marker to allow us to see the source node for shader alloc - DebugGraphics.pushGpuMarker(device, `Node: ${drawCall.node.name}`); + // marker to allow us to see the source node for shader alloc + DebugGraphics.pushGpuMarker(device, `Node: ${drawCall.node.name}`); - // use variants cache on material to quickly find the shader, as they are all - // the same for the same pass, using all lights of the scene - const variantKey = pass + '_' + objDefs + '_' + lightHash; - drawCall._shader[pass] = material.variants[variantKey]; - if (!drawCall._shader[pass]) { - drawCall.updatePassShader(scene, pass, sortedLights, this.viewUniformFormat, this.viewBindGroupFormat); - material.variants[variantKey] = drawCall._shader[pass]; - } - drawCall._lightHash = lightHash; + const shaderInstance = drawCall.getShaderInstance(pass, lightHash, scene, this.viewUniformFormat, this.viewBindGroupFormat, sortedLights); - DebugGraphics.popGpuMarker(device); - } - - Debug.assert(drawCall._shader[pass], "no shader for pass", material); + DebugGraphics.popGpuMarker(device); - addCall(drawCall, material !== prevMaterial, !prevMaterial || lightMask !== prevLightMask); + addCall(drawCall, shaderInstance, material !== prevMaterial, !prevMaterial || lightMask !== prevLightMask); prevMaterial = material; prevObjDefs = objDefs; @@ -581,13 +571,14 @@ class ForwardRenderer extends Renderer { // We have a mesh instance const newMaterial = preparedCalls.isNewMaterial[i]; const lightMaskChanged = preparedCalls.lightMaskChanged[i]; + const shaderInstance = preparedCalls.shaderInstances[i]; const material = drawCall.material; const objDefs = drawCall._shaderDefs; const lightMask = drawCall.mask; if (newMaterial) { - const shader = drawCall._shader[pass]; + const shader = shaderInstance.shader; if (!shader.failed && !device.setShader(shader)) { Debug.error(`Error compiling shader [${shader.label}] for material=${material.name} pass=${pass} objDefs=${objDefs}`, material); } @@ -644,7 +635,7 @@ class ForwardRenderer extends Renderer { this.setMorphing(device, drawCall.morphInstance); this.setSkinning(device, drawCall); - this.setupMeshUniformBuffers(drawCall, pass); + this.setupMeshUniformBuffers(shaderInstance, drawCall); const style = drawCall.renderStyle; device.setIndexBuffer(mesh.indexBuffer[style]); diff --git a/src/scene/renderer/renderer.js b/src/scene/renderer/renderer.js index b2f637c56a7..2acfea19026 100644 --- a/src/scene/renderer/renderer.js +++ b/src/scene/renderer/renderer.js @@ -760,7 +760,7 @@ class Renderer { device.setBindGroup(BINDGROUP_VIEW, viewBindGroup); } - setupMeshUniformBuffers(meshInstance, pass) { + setupMeshUniformBuffers(shaderInstance, meshInstance) { const device = this.device; if (device.supportsUniformBuffers) { @@ -771,7 +771,8 @@ class Renderer { this.normalMatrixId.setValue(meshInstance.node.normalMatrix.data); // update mesh bind group / uniform buffer - const meshBindGroup = meshInstance.getBindGroup(device, pass); + const meshBindGroup = shaderInstance.getBindGroup(device); + meshBindGroup.defaultUniformBuffer.update(); meshBindGroup.update(); device.setBindGroup(BINDGROUP_MESH, meshBindGroup); diff --git a/src/scene/renderer/shadow-renderer.js b/src/scene/renderer/shadow-renderer.js index f1a42fa0a9a..18e9596b63c 100644 --- a/src/scene/renderer/shadow-renderer.js +++ b/src/scene/renderer/shadow-renderer.js @@ -52,22 +52,8 @@ const shadowCamView = new Mat4(); const shadowCamViewProj = new Mat4(); const pixelOffset = new Float32Array(2); const blurScissorRect = new Vec4(1, 1, 0, 0); -const opChanId = { r: 1, g: 2, b: 3, a: 4 }; const viewportMatrix = new Mat4(); -function getDepthKey(meshInstance) { - const material = meshInstance.material; - const x = meshInstance.skinInstance ? 10 : 0; - let y = 0; - if (material.opacityMap) { - const opChan = material.opacityMapChannel; - if (opChan) { - y = opChanId[opChan]; - } - } - return x + y; -} - /** * @ignore */ @@ -214,7 +200,7 @@ class ShadowRenderer { tempSet.clear(); } - // TODO: we should probably sort shadow meshes by shader and not depth + // this sorts the shadow casters by the shader id visible.sort(this.renderer.sortCompareDepth); } @@ -362,13 +348,13 @@ class ShadowRenderer { meshInstance.setParameters(device, passFlags); } - // set shader - let shadowShader = meshInstance._shader[shadowPass]; - if (!shadowShader) { - meshInstance.updatePassShader(scene, shadowPass, null, this.viewUniformFormat, this.viewBindGroupFormat); - shadowShader = meshInstance._shader[shadowPass]; - meshInstance._key[SORTKEY_DEPTH] = getDepthKey(meshInstance); - } + const shaderInstance = meshInstance.getShaderInstance(shadowPass, 0, scene, this.viewUniformFormat, this.viewBindGroupFormat); + const shadowShader = shaderInstance.shader; + Debug.assert(shadowShader, `no shader for pass ${shadowPass}`, material); + + // sort shadow casters by shader + meshInstance._key[SORTKEY_DEPTH] = shadowShader.id; + if (!shadowShader.failed && !device.setShader(shadowShader)) { Debug.error(`Error compiling shadow shader for material=${material.name} pass=${shadowPass}`, material); } @@ -377,7 +363,7 @@ class ShadowRenderer { renderer.setVertexBuffers(device, mesh); renderer.setMorphing(device, meshInstance.morphInstance); - this.renderer.setupMeshUniformBuffers(meshInstance, shadowPass); + this.renderer.setupMeshUniformBuffers(shaderInstance, meshInstance); const style = meshInstance.renderStyle; device.setIndexBuffer(mesh.indexBuffer[style]);