UNPKG

@babylonjs/viewer

Version:

The Babylon Viewer aims to simplify a specific but common Babylon.js use case: loading, viewing, and interacting with a 3D model.

1,100 lines (1,098 loc) 136 kB
import { b1 as Color4, O as Observable, M as Matrix, y as Logger, c6 as EffectRenderer, c5 as EffectWrapper, aA as RenderTargetTexture, C as Constants, m as Texture, be as MultiRenderTarget, c as ShaderMaterial, bd as IsGaussianSplattingClassName, V as Vector3, aP as PostProcess, bc as Vector4, z as __classPrivateFieldGet, D as __classPrivateFieldSet, l as __runInitializers, q as __esDecorate, aw as MaterialDefines, s as serialize, H as expandToProperty, a as EngineStore, aD as Quaternion, aH as RawTexture, aY as Tools } from './index-MZPybX0H.esm.js'; import { P as ProceduralTexture } from './proceduralTexture.pure-BnRujZUW.esm.js'; import { G as GeometryBufferRenderer, R as RegisterGeometryBufferRendererSceneComponent } from './geometryBufferRenderer.pure-BFEtStOF.esm.js'; import { a as PostProcessRenderPipeline, P as PostProcessRenderEffect } from './postProcessRenderEffect-LuJHEk0q.esm.js'; import { R as RegisterIblCdfGeneratorSceneComponent, I as IblCdfGenerator } from './iblCdfGenerator-gclpoAcj.esm.js'; import { M as MaterialPluginBase } from './material.detailMapConfiguration-CHgbrJ-3.esm.js'; import { P as PBRBaseMaterial } from './pbrBaseMaterial.pure-CYpho1Q0.esm.js'; import { S as StandardMaterial } from './standardMaterial.pure-D4Vk5EeI.esm.js'; import { OpenPBRMaterial } from './openpbrMaterial.pure-DxPL0OR1.esm.js'; import './postProcessRenderPipelineManager-Ve0zUYex.esm.js'; import './cubeTexture.pure-D6LcyeP8.esm.js'; import './abstractEngine.cubeTexture.pure-D_XmNIcO.esm.js'; import './textureLoaderManager-l5zAgBPe.esm.js'; import './brdfTextureTools-DrHgFJ85.esm.js'; import './prepass.defines-D50C_zO6.esm.js'; /** This file must only contain pure code and pure imports */ // Max frames _renderVoxelGrid waits for splat depth sorts to settle before voxelizing anyway // (~3s at 60fps). Bounds the wait so a continuously re-sorting (orbiting) splat can't block it. const _MaxSortSettleWaitFrames = 180; /** * Voxel-based shadow rendering for IBL's. * This should not be instanciated directly, as it is part of a scene component * @internal * @see https://playground.babylonjs.com/#8R5SSE#222 */ class _IblShadowsVoxelRenderer { /** * Return the voxel grid texture. * @returns The voxel grid texture. */ getVoxelGrid() { if (this._engine.isWebGPU) { return this._voxelGrid; } else if (this._triPlanarVoxelization) { return this._combinedVoxelGridPT; } else { return this._voxelGridZaxis; } } /** * Return the voxel render target used during voxelization. * @returns The voxel render target. */ getRT() { if (this._engine.isWebGPU) { return this._voxelGridRT; } else if (this._triPlanarVoxelization) { return this._combinedVoxelGridPT; } else { return this._voxelGridZaxis; } } /** * Whether to use tri-planar voxelization. More expensive, but can help with artifacts. */ get triPlanarVoxelization() { return this._triPlanarVoxelization; } /** * Whether to use tri-planar voxelization. More expensive, but can help with artifacts. */ set triPlanarVoxelization(enabled) { if (this._engine.isWebGPU) { // WebGPU only supports tri-planar voxelization. this._triPlanarVoxelization = true; return; } if (this._triPlanarVoxelization === enabled) { return; } this._triPlanarVoxelization = enabled; this._disposeVoxelTextures(); this._createTextures(); } /** * Set the matrix to use for scaling the world space to voxel space * @param matrix The matrix to use for scaling the world space to voxel space */ setWorldScaleMatrix(matrix) { this._invWorldScaleMatrix = matrix; } /** * @returns Whether voxelization is currently happening. */ isVoxelizationInProgress() { return this._voxelizationInProgress; } /** * Resolution of the voxel grid. The final resolution will be 2^resolutionExp. */ get voxelResolutionExp() { return this._voxelResolutionExp; } /** * Resolution of the voxel grid. The final resolution will be 2^resolutionExp. */ set voxelResolutionExp(resolutionExp) { if (this._voxelResolutionExp === resolutionExp && this._voxelGridZaxis) { return; } this._voxelResolutionExp = Math.round(Math.min(Math.max(resolutionExp, 3), 9)); this._voxelResolution = Math.pow(2.0, this._voxelResolutionExp); this._disposeVoxelTextures(); this._createTextures(); } /** * Instanciates the voxel renderer * @param scene Scene to attach to * @param iblShadowsRenderPipeline The render pipeline this pass is associated with * @param resolutionExp Resolution of the voxel grid. The final resolution will be 2^resolutionExp. * @param triPlanarVoxelization Whether to use tri-planar voxelization. Only applies to WebGL. Voxelization will take longer but will reduce missing geometry. * @returns The voxel renderer */ constructor(scene, iblShadowsRenderPipeline, resolutionExp = 6, triPlanarVoxelization = true) { this._voxelMrtsXaxis = []; this._voxelMrtsYaxis = []; this._voxelMrtsZaxis = []; this._voxelClearColor = new Color4(0, 0, 0, 1); /** * Observable that triggers when the voxelization is complete */ this.onVoxelizationCompleteObservable = new Observable(); this._renderTargets = []; /** Per-mesh voxel ShaderMaterials for GaussianSplattingMesh, keyed by mesh uniqueId. */ this._gsVoxelMaterialCache = new Map(); this._triPlanarVoxelization = true; this._voxelizationInProgress = false; // Frames spent waiting for shadow-casting splats' depth sort to settle before voxelizing; see // _renderVoxelGrid. Capped so a continuously re-sorting (e.g. orbiting) splat can't starve it. this._sortSettleWaitFrames = 0; this._invWorldScaleMatrix = Matrix.Identity(); this._voxelResolution = 64; this._voxelResolutionExp = 6; this._copyMipLayer = 0; this._mipArray = []; this._scene = scene; this._engine = scene.getEngine(); this._triPlanarVoxelization = this._engine.isWebGPU || triPlanarVoxelization; if (!this._engine.getCaps().drawBuffersExtension) { Logger.Error("Can't do voxel rendering without the draw buffers extension."); } const isWebGPU = this._engine.isWebGPU; // Round down to a power of 2 so it evenly divides the power-of-2 voxel resolution, // preventing out-of-bounds layer indices in the last MRT slab. // This shader implementation writes up to 16 MRT outputs, so clamp to 16 to keep // active draw buffers aligned with declared/written fragment outputs. const rawMaxDrawBuffers = this._engine.getCaps().maxDrawBuffers || 0; const cappedMaxDrawBuffers = Math.min(rawMaxDrawBuffers, 16); this._maxDrawBuffers = cappedMaxDrawBuffers >= 1 ? 1 << Math.floor(Math.log2(cappedMaxDrawBuffers)) : 0; this._copyMipEffectRenderer = new EffectRenderer(this._engine); this._copyMipEffectWrapper = new EffectWrapper({ engine: this._engine, fragmentShader: "copyTexture3DLayerToTexture", useShaderStore: true, uniformNames: ["layerNum"], samplerNames: ["textureSampler"], shaderLanguage: isWebGPU ? 1 /* ShaderLanguage.WGSL */ : 0 /* ShaderLanguage.GLSL */, extraInitializationsAsync: async () => { if (isWebGPU) { await import('./copyTexture3DLayerToTexture.fragment-Cl1VOb4y.esm.js'); } else { await import('./copyTexture3DLayerToTexture.fragment-CaYXTDus.esm.js'); } }, }); this._copyMipEffectWrapper.onApplyObservable.add(() => { const effect = this._copyMipEffectWrapper.effect; if (!effect || !this._copyMipSourceTexture) { return; } effect.setTexture("textureSampler", this._copyMipSourceTexture); effect.setInt("layerNum", this._copyMipLayer); }); this.voxelResolutionExp = resolutionExp; } _generateMipMaps() { const iterations = Math.ceil(Math.log2(this._voxelResolution)); for (let i = 1; i < iterations + 1; i++) { this._generateMipMap(i); } } _generateMipMap(lodLevel) { // Generate a mip map for the given level by triggering the render of the procedural mip texture. const mipTarget = this._mipArray[lodLevel - 1]; if (!mipTarget) { return; } mipTarget.setTexture("srcMip", lodLevel === 1 ? this.getVoxelGrid() : this._mipArray[lodLevel - 2]); mipTarget.render(); } _copyMipMaps() { const iterations = Math.ceil(Math.log2(this._voxelResolution)); for (let i = 1; i < iterations + 1; i++) { this._copyMipMap(i); } } _copyMipMap(lodLevel) { // Now, copy this mip into the mip chain of the voxel grid. const mipTarget = this._mipArray[lodLevel - 1]; if (!mipTarget) { return; } const voxelGrid = this.getVoxelGrid(); let rt; if (voxelGrid instanceof RenderTargetTexture && voxelGrid.renderTarget) { rt = voxelGrid.renderTarget; } else { rt = voxelGrid._rtWrapper; } if (rt) { this._copyMipEffectRenderer.saveStates(); const previousColorWrite = this._engine.getColorWrite(); const previousDepthBuffer = this._engine.getDepthBuffer(); const previousDepthWrite = this._engine.getDepthWrite(); const previousAlphaMode = this._engine.getAlphaMode(); this._engine.setColorWrite(true); this._engine.setDepthBuffer(false); this._engine.setDepthWrite(false); this._engine.setAlphaMode(Constants.ALPHA_DISABLE); const bindSize = mipTarget.getSize().width; let sourceDepth = mipTarget.getInternalTexture()?.depth; sourceDepth = Math.max(1, sourceDepth || bindSize); const destinationMipDepth = Math.max(1, this._voxelResolution >> lodLevel); const layersToCopy = Math.min(sourceDepth, destinationMipDepth); const destinationTexture = rt.texture; const previousGenerateMipMaps = destinationTexture?.generateMipMaps; if (destinationTexture) { destinationTexture.generateMipMaps = false; } try { // Render to each layer of the voxel grid. for (let layer = 0; layer < layersToCopy; layer++) { this._engine.bindFramebuffer(rt, 0, bindSize, bindSize, true, lodLevel, layer); this._copyMipSourceTexture = mipTarget; this._copyMipLayer = layer; this._copyMipEffectRenderer.applyEffectWrapper(this._copyMipEffectWrapper); this._copyMipEffectRenderer.draw(); this._engine.unBindFramebuffer(rt, true); } if (!this._engine.isWebGPU) { this._engine.unbindAllTextures(); } } finally { if (destinationTexture && previousGenerateMipMaps !== undefined) { destinationTexture.generateMipMaps = previousGenerateMipMaps; } this._engine.setAlphaMode(previousAlphaMode); this._engine.setDepthWrite(previousDepthWrite); this._engine.setDepthBuffer(previousDepthBuffer); this._engine.setColorWrite(previousColorWrite); } this._copyMipSourceTexture = undefined; this._copyMipEffectRenderer.restoreStates(); } } _computeNumberOfSlabs() { return Math.ceil(this._voxelResolution / this._maxDrawBuffers); } _createTextures() { const isWebGPU = this._engine.isWebGPU; const size = { width: this._voxelResolution, height: this._voxelResolution, depth: this._voxelResolution, }; const voxelAxisOptions = { generateDepthBuffer: false, generateMipMaps: false, type: Constants.TEXTURETYPE_UNSIGNED_BYTE, format: Constants.TEXTUREFORMAT_R, samplingMode: Constants.TEXTURE_NEAREST_SAMPLINGMODE, }; // We can render up to maxDrawBuffers voxel slices of the grid per render. // We call this a slab. const numSlabs = this._computeNumberOfSlabs(); const voxelCombinedOptions = { generateDepthBuffer: false, generateMipMaps: true, type: Constants.TEXTURETYPE_UNSIGNED_BYTE, format: Constants.TEXTUREFORMAT_R, samplingMode: Constants.TEXTURE_NEAREST_NEAREST_MIPNEAREST, shaderLanguage: isWebGPU ? 1 /* ShaderLanguage.WGSL */ : 0 /* ShaderLanguage.GLSL */, extraInitializationsAsync: async () => { if (isWebGPU) { await import('./iblCombineVoxelGrids.fragment-CqoCbFlp.esm.js'); } else { await import('./iblCombineVoxelGrids.fragment-Bw7qmU8z.esm.js'); } }, }; if (this._engine.isWebGPU) { this._voxelGrid = new RenderTargetTexture("voxelGrid", size, this._scene, { ...voxelCombinedOptions, format: Constants.TEXTUREFORMAT_R, creationFlags: Constants.TEXTURE_CREATIONFLAG_STORAGE, }); this._voxelGridRT = new RenderTargetTexture("voxelGridRT", { width: Math.min(size.width * 2.0, 2048), height: Math.min(size.height * 2.0, 2048) }, this._scene, voxelAxisOptions); } else if (this._triPlanarVoxelization) { this._voxelGridXaxis = new RenderTargetTexture("voxelGridXaxis", size, this._scene, voxelAxisOptions); this._voxelGridYaxis = new RenderTargetTexture("voxelGridYaxis", size, this._scene, voxelAxisOptions); this._voxelGridZaxis = new RenderTargetTexture("voxelGridZaxis", size, this._scene, voxelAxisOptions); this._voxelMrtsXaxis = this._createVoxelMRTs("x_axis_", this._voxelGridXaxis, numSlabs); this._voxelMrtsYaxis = this._createVoxelMRTs("y_axis_", this._voxelGridYaxis, numSlabs); this._voxelMrtsZaxis = this._createVoxelMRTs("z_axis_", this._voxelGridZaxis, numSlabs); this._combinedVoxelGridPT = new ProceduralTexture("combinedVoxelGrid", size, "iblCombineVoxelGrids", this._scene, voxelCombinedOptions, false); this._scene.proceduralTextures.splice(this._scene.proceduralTextures.indexOf(this._combinedVoxelGridPT), 1); this._combinedVoxelGridPT.setFloat("layer", 0.0); this._combinedVoxelGridPT.setTexture("voxelXaxisSampler", this._voxelGridXaxis); this._combinedVoxelGridPT.setTexture("voxelYaxisSampler", this._voxelGridYaxis); this._combinedVoxelGridPT.setTexture("voxelZaxisSampler", this._voxelGridZaxis); // We will render this only after voxelization is completed for the 3 axes. this._combinedVoxelGridPT.autoClear = false; this._combinedVoxelGridPT.wrapU = Texture.CLAMP_ADDRESSMODE; this._combinedVoxelGridPT.wrapV = Texture.CLAMP_ADDRESSMODE; } else { this._voxelGridZaxis = new RenderTargetTexture("voxelGridZaxis", size, this._scene, voxelCombinedOptions); this._voxelMrtsZaxis = this._createVoxelMRTs("z_axis_", this._voxelGridZaxis, numSlabs); } const generateVoxelMipOptions = { generateDepthBuffer: false, generateMipMaps: false, type: Constants.TEXTURETYPE_UNSIGNED_BYTE, format: Constants.TEXTUREFORMAT_R, samplingMode: Constants.TEXTURE_NEAREST_SAMPLINGMODE, shaderLanguage: isWebGPU ? 1 /* ShaderLanguage.WGSL */ : 0 /* ShaderLanguage.GLSL */, extraInitializationsAsync: async () => { if (isWebGPU) { await import('./iblGenerateVoxelMip.fragment-BNQ1_JBg.esm.js'); } else { await import('./iblGenerateVoxelMip.fragment-FIpEKDTR.esm.js'); } }, }; this._mipArray = new Array(Math.ceil(Math.log2(this._voxelResolution))); for (let mipIdx = 1; mipIdx <= this._mipArray.length; mipIdx++) { const mipDim = this._voxelResolution >> mipIdx; const mipSize = { width: mipDim, height: mipDim, depth: mipDim }; this._mipArray[mipIdx - 1] = new ProceduralTexture("voxelMip" + mipIdx, mipSize, "iblGenerateVoxelMip", this._scene, generateVoxelMipOptions, false); this._scene.proceduralTextures.splice(this._scene.proceduralTextures.indexOf(this._mipArray[mipIdx - 1]), 1); const mipTarget = this._mipArray[mipIdx - 1]; mipTarget.autoClear = false; mipTarget.wrapU = Texture.CLAMP_ADDRESSMODE; mipTarget.wrapV = Texture.CLAMP_ADDRESSMODE; mipTarget.setTexture("srcMip", mipIdx > 1 ? this._mipArray[mipIdx - 2] : this.getVoxelGrid()); mipTarget.setInt("layerNum", 0); } this._createVoxelMaterials(); } _createVoxelMRTs(name, voxelRT, numSlabs) { voxelRT.wrapU = Texture.CLAMP_ADDRESSMODE; voxelRT.wrapV = Texture.CLAMP_ADDRESSMODE; voxelRT.noPrePassRenderer = true; const mrtArray = []; const targetTypes = new Array(this._maxDrawBuffers).fill(Constants.TEXTURE_3D); for (let mrtIndex = 0; mrtIndex < numSlabs; mrtIndex++) { let layerIndices = new Array(this._maxDrawBuffers).fill(0); layerIndices = layerIndices.map((value, index) => mrtIndex * this._maxDrawBuffers + index); let textureNames = new Array(this._maxDrawBuffers).fill(""); textureNames = textureNames.map((value, index) => "voxel_grid_" + name + (mrtIndex * this._maxDrawBuffers + index)); const mrt = new MultiRenderTarget("mrt_" + name + mrtIndex, { width: this._voxelResolution, height: this._voxelResolution, depth: this._voxelResolution }, this._maxDrawBuffers, // number of draw buffers this._scene, { types: new Array(this._maxDrawBuffers).fill(Constants.TEXTURETYPE_UNSIGNED_BYTE), samplingModes: new Array(this._maxDrawBuffers).fill(Constants.TEXTURE_TRILINEAR_SAMPLINGMODE), generateMipMaps: false, targetTypes, formats: new Array(this._maxDrawBuffers).fill(Constants.TEXTUREFORMAT_R), faceIndex: new Array(this._maxDrawBuffers).fill(0), layerIndex: layerIndices, layerCounts: new Array(this._maxDrawBuffers).fill(this._voxelResolution), generateDepthBuffer: false, generateStencilBuffer: false, }, textureNames); mrt.clearColor = new Color4(0, 0, 0, 1); mrt.noPrePassRenderer = true; for (let i = 0; i < this._maxDrawBuffers; i++) { mrt.setInternalTexture(voxelRT.getInternalTexture(), i); } mrtArray.push(mrt); } return mrtArray; } _disposeVoxelTextures() { this._stopVoxelization(); for (let i = 0; i < this._voxelMrtsZaxis.length; i++) { if (this._triPlanarVoxelization) { this._voxelMrtsXaxis[i].dispose(true); this._voxelMrtsYaxis[i].dispose(true); } this._voxelMrtsZaxis[i].dispose(true); } if (this._triPlanarVoxelization) { this._voxelGridXaxis?.dispose(); this._voxelGridYaxis?.dispose(); this._combinedVoxelGridPT?.dispose(); } this._voxelGridZaxis?.dispose(); for (const mip of this._mipArray) { mip.dispose(); } this._voxelMaterial?.dispose(); this._mipArray = []; this._voxelMrtsXaxis = []; this._voxelMrtsYaxis = []; this._voxelMrtsZaxis = []; } _createVoxelMaterials() { const isWebGPU = this._engine.isWebGPU; this._voxelMaterial = new ShaderMaterial("voxelization", this._scene, "iblVoxelGrid", { uniforms: ["world", "viewMatrix", "invTransWorld", "invWorldScale", "nearPlane", "farPlane", "stepSize"], defines: ["MAX_DRAW_BUFFERS " + this._maxDrawBuffers], shaderLanguage: isWebGPU ? 1 /* ShaderLanguage.WGSL */ : 0 /* ShaderLanguage.GLSL */, extraInitializationsAsync: async () => { if (isWebGPU) { await Promise.all([import('./iblVoxelGrid.fragment-CgH9RRO7.esm.js'), import('./iblVoxelGrid.vertex-C7QKbJg2.esm.js')]); } else { await Promise.all([import('./iblVoxelGrid.fragment-t94-_O0A.esm.js'), import('./iblVoxelGrid.vertex-KY-XdLON.esm.js')]); } }, }); this._voxelMaterial.cullBackFaces = false; this._voxelMaterial.backFaceCulling = false; this._voxelMaterial.depthFunction = Constants.ALWAYS; } /** * Checks if the voxel renderer is ready to voxelize scene * @returns true if the voxel renderer is ready to voxelize scene */ isReady() { let allReady = this.getVoxelGrid().isReady(); for (let i = 0; i < this._mipArray.length; i++) { const mipReady = this._mipArray[i].isReady(); allReady &&= mipReady; } if (!allReady || this._voxelizationInProgress) { return false; } return true; } /** * If the MRT's are already in the list of render targets, this will * remove them so that they don't get rendered again. */ _stopVoxelization() { // If the MRT's are already in the list of render targets, remove them. this._removeVoxelRTs(this._voxelMrtsXaxis); this._removeVoxelRTs(this._voxelMrtsYaxis); this._removeVoxelRTs(this._voxelMrtsZaxis); this._removeVoxelRTs([this._voxelGridRT]); } _removeVoxelRTs(rts) { // const currentRTs = this._scene.customRenderTargets; const rtIdx = this._renderTargets.findIndex((rt) => { if (rt === rts[0]) { return true; } return false; }); if (rtIdx >= 0) { this._renderTargets.splice(rtIdx, rts.length); } else { const rtIdx = this._scene.customRenderTargets.findIndex((rt) => { if (rt === rts[0]) { return true; } return false; }); if (rtIdx >= 0) { this._scene.customRenderTargets.splice(rtIdx, rts.length); } } } /** * Renders voxel grid of scene for IBL shadows * @param includedMeshes * @param registerAfterRenderObservable Whether to register scene onAfterRender callback (legacy path). */ updateVoxelGrid(includedMeshes, registerAfterRenderObservable = true) { if (this._voxelizationInProgress) { return; } this._stopVoxelization(); this._voxelizationInProgress = true; if (this._engine.isWebGPU) { this._voxelGridRT.renderList = includedMeshes; this._addRTsForRender([this._voxelGridRT], includedMeshes, 0); } else if (this._triPlanarVoxelization) { this._addRTsForRender(this._voxelMrtsXaxis, includedMeshes, 0); this._addRTsForRender(this._voxelMrtsYaxis, includedMeshes, 1); this._addRTsForRender(this._voxelMrtsZaxis, includedMeshes, 2); } else { this._addRTsForRender(this._voxelMrtsZaxis, includedMeshes, 2); } if (registerAfterRenderObservable) { this._renderVoxelGridBound = this._renderVoxelGrid.bind(this); this._scene.onAfterRenderObservable.add(this._renderVoxelGridBound); } } /** * Advances voxelization work when running in custom render loops (for example FrameGraph tasks) * where scene onAfterRender timing may differ from classic pipeline flow. */ processVoxelization() { this._renderVoxelGrid(); } _renderVoxelGrid() { if (this._voxelizationInProgress) { // Wait for shadow-casting GaussianSplatting meshes' depth sort to settle before // rasterizing. Voxelization is order-independent — the order splats are drawn never // affects the grid — but the GS voxel draw still *indexes* each splat through the // `splatIndex` thin-instance buffer, and that buffer is (re)sized, filled and rebound by // the sort worker's callback (the same callback that flips `_isDepthSortSettled`). After // addPart() merges a splat, `forcedInstanceCount` jumps to the new total synchronously // but the index buffer is only finalized when the sort lands; rasterizing in between // indexes a stale/mismatched buffer and writes an empty grid (no IBL shadow until a // later refresh). Frame-capped so a continuously re-sorting (orbiting) splat still // voxelizes eventually rather than blocking forever. let gsSortPending = false; for (let i = 0; i < this._renderTargets.length && !gsSortPending; i++) { const renderList = this._renderTargets[i].renderList; if (!renderList) { continue; } for (const mesh of renderList) { if (IsGaussianSplattingClassName(mesh.getClassName()) && !mesh._isDepthSortSettled) { gsSortPending = true; break; } } } if (gsSortPending && this._sortSettleWaitFrames < _MaxSortSettleWaitFrames) { this._sortSettleWaitFrames++; return; } this._sortSettleWaitFrames = 0; let allReady = this.getVoxelGrid().isReady(); for (let i = 0; i < this._mipArray.length; i++) { const mipReady = this._mipArray[i].isReady(); allReady &&= mipReady; } for (let i = 0; i < this._renderTargets.length; i++) { const rttReady = this._renderTargets[i].isReadyForRendering(); allReady &&= rttReady; } for (const gsVoxelMat of Array.from(this._gsVoxelMaterialCache.values())) { allReady &&= gsVoxelMat.isReady(); } if (!allReady) { return; } const copyMipEffect = this._copyMipEffectWrapper.effect; if (!copyMipEffect.isReady()) { return; } if (this._engine.isWebGPU) { // Clear the voxel grid storage texture. // Need to clear each layer individually. // Would a compute shader be faster here to clear all layers in one go? if (this._voxelGrid && this._voxelGrid.renderTarget) { for (let layer = 0; layer < this._voxelResolution; layer++) { this._engine.bindFramebuffer(this._voxelGrid.renderTarget, 0, undefined, undefined, true, 0, layer); this._engine.clear(this._voxelClearColor, true, false, false); this._engine.unBindFramebuffer(this._voxelGrid.renderTarget, true); } } } for (const rt of this._renderTargets) { rt.render(); } this._stopVoxelization(); if (this._triPlanarVoxelization && !this._engine.isWebGPU) { this._combinedVoxelGridPT.render(); } this._generateMipMaps(); this._copyMipMaps(); this._scene.onAfterRenderObservable.removeCallback(this._renderVoxelGridBound); this._voxelizationInProgress = false; this.onVoxelizationCompleteObservable.notifyObservers(); } } /** * Splits rendering for every voxel RT: non–Gaussian splatting meshes use subMesh.render * (material override from setMaterialForRendering); GaussianSplattingMesh uses a custom draw path with its cached voxel ShaderMaterial. * @param rtt - the render target texture to install the custom render function on */ _installVoxelMixedCustomRender(rtt) { const scene = this._scene; const engine = scene.getEngine(); const renderGsSplat = (sm) => { const renderingMesh = sm.getRenderingMesh(); const effectiveMesh = sm.getEffectiveMesh(); const gsVoxelMaterial = this._gsVoxelMaterialCache.get(effectiveMesh.uniqueId); if (!gsVoxelMaterial || !gsVoxelMaterial.isReady()) { return; } const drawWrapper = gsVoxelMaterial._getDrawWrapper(); if (!drawWrapper?.effect) { return; } const effect = drawWrapper.effect; const batch = renderingMesh._getInstancesRenderList(sm._id, !!sm.getReplacementMesh()); if (batch.mustReturn) { return; } const hardwareInstancedRendering = engine.getCaps().instancedArrays && ((batch.visibleInstances[sm._id] !== null && batch.visibleInstances[sm._id] !== undefined) || renderingMesh.hasThinInstances); const fillMode = sm.getMaterial()?.fillMode ?? Constants.MATERIAL_TriangleFillMode; engine.enableEffect(drawWrapper); renderingMesh._bind(sm, effect, fillMode); gsVoxelMaterial._preBind(drawWrapper); gsVoxelMaterial.bind(effectiveMesh.getWorldMatrix(), effectiveMesh, effect); // If rotation/scale textures are missing, bind() logged the warning; skip the draw to avoid GPU errors. if (!effectiveMesh.rotationsATexture) { gsVoxelMaterial.unbind(); return; } if (engine.isWebGPU) { // A GSplat is a 3D Gaussian ellipsoid. To approximate its volume in the voxel grid // we rasterize three planar cross-section quads — one per principal axis — each // spanning the ellipsoid cross-section perpendicular to that axis. A quad rendered // edge-on produces zero fragments, so we draw once per world axis: each draw lets // computeVoxelSplatWorldPos pick the quad whose normal best aligns with that view, // guaranteeing every splat is captured face-on from at least one direction. const viewMatrices = _IblShadowsVoxelRenderer._VOXEL_VIEW_MATRICES; for (let axisIdx = 0; axisIdx < 3; axisIdx++) { effect.setMatrix("viewMatrix", viewMatrices[axisIdx]); renderingMesh._processRendering(effectiveMesh, sm, effect, fillMode, batch, hardwareInstancedRendering, (_isInstance, world) => { effect.setMatrix("world", world); }); } } else { renderingMesh._processRendering(effectiveMesh, sm, effect, fillMode, batch, hardwareInstancedRendering, (_isInstance, world) => effect.setMatrix("world", world)); } gsVoxelMaterial.unbind(); }; const processBucket = (subMeshes, enableAlphaMode) => { for (let i = 0; i < subMeshes.length; i++) { const sm = subMeshes.data[i]; const effective = sm.getEffectiveMesh(); if (IsGaussianSplattingClassName(effective.getClassName())) { renderGsSplat(sm); } else { sm.render(enableAlphaMode); } } }; rtt.customRenderFunction = (opaqueSubMeshes, alphaTestSubMeshes, transparentSubMeshes, depthOnlySubMeshes) => { if (depthOnlySubMeshes.length) { engine.setColorWrite(false); processBucket(depthOnlySubMeshes, false); engine.setColorWrite(true); } processBucket(opaqueSubMeshes, false); processBucket(alphaTestSubMeshes, false); processBucket(transparentSubMeshes, true); }; } _addGsMeshToVoxelRT(mrt, mesh) { let gsVoxelMaterial = this._gsVoxelMaterialCache.get(mesh.uniqueId); if (!gsVoxelMaterial) { const gsMaterial = mesh.material; if (!gsMaterial) { return; } const shaderLanguage = this._engine.isWebGPU ? 1 /* ShaderLanguage.WGSL */ : 0 /* ShaderLanguage.GLSL */; gsVoxelMaterial = gsMaterial.makeVoxelRenderingMaterial(this._scene, shaderLanguage, this._maxDrawBuffers, mesh.isCompound); this._gsVoxelMaterialCache.set(mesh.uniqueId, gsVoxelMaterial); } mrt.renderList?.push(mesh); mrt.setMaterialForRendering(mesh, gsVoxelMaterial); } _addRTsForRender(mrts, includedMeshes, axis) { const slabSize = 1.0 / this._computeNumberOfSlabs(); const voxelMaterial = this._voxelMaterial; // We need to update the world scale uniform for every mesh being rendered to the voxel grid. for (let mrtIndex = 0; mrtIndex < mrts.length; mrtIndex++) { const mrt = mrts[mrtIndex]; mrt._disableEngineStages = true; mrt.useCameraPostProcesses = false; mrt.renderParticles = false; mrt.renderSprites = false; mrt.enableOutlineRendering = false; const renderBucket = (bucket) => { for (let index = 0; index < bucket.length; index++) { const subMesh = bucket.data[index]; if (subMesh.getMaterial() !== voxelMaterial) { continue; } subMesh.render(false); } }; mrt.customRenderFunction = (opaqueSubMeshes, alphaTestSubMeshes, transparentSubMeshes, depthOnlySubMeshes) => { renderBucket(depthOnlySubMeshes); renderBucket(opaqueSubMeshes); renderBucket(alphaTestSubMeshes); renderBucket(transparentSubMeshes); }; mrt.renderList = []; const nearPlane = mrtIndex * slabSize; const farPlane = (mrtIndex + 1) * slabSize; const stepSize = slabSize / this._maxDrawBuffers; const viewMatrix = _IblShadowsVoxelRenderer._VOXEL_VIEW_MATRICES[axis]; mrt.onBeforeRenderObservable.clear(); mrt.onBeforeRenderObservable.add(() => { voxelMaterial.setMatrix("viewMatrix", viewMatrix); voxelMaterial.setMatrix("invWorldScale", this._invWorldScaleMatrix); voxelMaterial.setFloat("nearPlane", nearPlane); voxelMaterial.setFloat("farPlane", farPlane); voxelMaterial.setFloat("stepSize", stepSize); if (this._engine.isWebGPU) { this._voxelMaterial.useVertexPulling = true; this._voxelMaterial.setTexture("voxel_storage", this.getVoxelGrid()); } // Push per-slab uniforms to each GS voxel material in this MRT's render list. for (const m of mrt.renderList ?? []) { if (IsGaussianSplattingClassName(m.getClassName())) { const gsVoxelMat = this._gsVoxelMaterialCache.get(m.uniqueId); if (gsVoxelMat) { gsVoxelMat.setMatrix("invWorldScale", this._invWorldScaleMatrix); if (this._engine.isWebGPU) { // WGSL GS voxel shader uses the same viewMatrix approach as WebGL; the per-axis viewMatrix is set per-draw in renderGsSplat. gsVoxelMat.setTexture("voxel_storage", this.getVoxelGrid()); } else { gsVoxelMat.setMatrix("viewMatrix", viewMatrix); gsVoxelMat.setFloat("nearPlane", nearPlane); gsVoxelMat.setFloat("farPlane", farPlane); gsVoxelMat.setFloat("stepSize", stepSize); } } } } }); // Set this material on every mesh in the scene (for this RT) if (includedMeshes.length === 0) { return; } for (const mesh of includedMeshes) { if (!mesh) { continue; } if (IsGaussianSplattingClassName(mesh.getClassName())) { this._addGsMeshToVoxelRT(mrt, mesh); } else if (mesh.subMeshes && mesh.subMeshes.length > 0) { mrt.renderList?.push(mesh); mrt.setMaterialForRendering(mesh, voxelMaterial); } const meshes = mesh.getChildMeshes(); for (const childMesh of meshes) { if (IsGaussianSplattingClassName(childMesh.getClassName())) { this._addGsMeshToVoxelRT(mrt, childMesh); } else if (childMesh.subMeshes && childMesh.subMeshes.length > 0) { mrt.renderList?.push(childMesh); mrt.setMaterialForRendering(childMesh, voxelMaterial); } } } this._installVoxelMixedCustomRender(mrt); } this._renderTargets = this._renderTargets.concat(mrts); } /** * Called by the pipeline to resize resources. */ resize() { } /** * Disposes the voxel renderer and associated resources */ dispose() { this._disposeVoxelTextures(); for (const mat of Array.from(this._gsVoxelMaterialCache.values())) { mat.dispose(); } this._gsVoxelMaterialCache.clear(); } } // View matrices for the three voxelization axes. _IblShadowsVoxelRenderer._VOXEL_VIEW_MATRICES = [ Matrix.LookAtLH(Vector3.Zero(), new Vector3(1, 0, 0), Vector3.Up()), Matrix.LookAtLH(Vector3.Zero(), new Vector3(0, 1, 0), new Vector3(1, 0, 0)), Matrix.LookAtLH(Vector3.Zero(), new Vector3(0, 0, 1), Vector3.Up()), ]; /** * Build cdf maps for IBL importance sampling during IBL shadow computation. * This should not be instantiated directly, as it is part of a scene component * @internal */ class _IblShadowsVoxelTracingPass { /** * The opacity of the shadow cast from the voxel grid */ get voxelShadowOpacity() { return this._voxelShadowOpacity; } /** * The opacity of the shadow cast from the voxel grid */ set voxelShadowOpacity(value) { this._voxelShadowOpacity = value; } /** * The opacity of the screen-space shadow */ get ssShadowOpacity() { return this._ssShadowOpacity; } /** * The opacity of the screen-space shadow */ set ssShadowOpacity(value) { this._ssShadowOpacity = value; } /** * The number of samples used in the screen space shadow pass. */ get sssSamples() { return this._sssSamples; } /** * The number of samples used in the screen space shadow pass. */ set sssSamples(value) { this._sssSamples = value; } /** * The stride used in the screen space shadow pass. This controls the distance between samples. */ get sssStride() { return this._sssStride; } /** * The stride used in the screen space shadow pass. This controls the distance between samples. */ set sssStride(value) { this._sssStride = value; } /** * The maximum distance that the screen-space shadow will be able to occlude. */ get sssMaxDist() { return this._sssMaxDist; } /** * The maximum distance that the screen-space shadow will be able to occlude. */ set sssMaxDist(value) { this._sssMaxDist = value; } /** * The thickness of the screen-space shadow */ get sssThickness() { return this._sssThickness; } /** * The thickness of the screen-space shadow */ set sssThickness(value) { this._sssThickness = value; } /** * The bias to apply to the voxel sampling in the direction of the surface normal of the geometry. */ get voxelNormalBias() { return this._voxelNormalBias; } set voxelNormalBias(value) { this._voxelNormalBias = value; } /** * The bias to apply to the voxel sampling in the direction of the light. */ get voxelDirectionBias() { return this._voxelDirectionBias; } set voxelDirectionBias(value) { this._voxelDirectionBias = value; } /** * Is the effect enabled */ get enabled() { return this._enabled; } set enabled(value) { this._enabled = value; // _render() already gates on `enabled`, but also disable the underlying ProceduralTexture // directly so it can't be rendered through any other path (e.g. Babylon's generic // per-frame ProceduralTexture refresh loop) while the pass is toggled off. if (this._outputTexture) { this._outputTexture.isEnabled = value; } } /** * The number of directions to sample for the voxel tracing. */ get sampleDirections() { return this._sampleDirections; } /** * The number of directions to sample for the voxel tracing. */ set sampleDirections(value) { this._sampleDirections = value; } /** * The current rotation of the environment map, in radians. */ get envRotation() { return this._envRotation; } /** * The current rotation of the environment map, in radians. */ set envRotation(value) { this._envRotation = value; } /** * Returns the output texture of the pass. * @returns The output texture. */ getOutputTexture() { return this._outputTexture; } /** * Gets the debug pass post process. This will create the resources for the pass * if they don't already exist. * @returns The post process */ getDebugPassPP() { if (!this._debugPassPP) { this._createDebugPass(); } return this._debugPassPP; } /** * The name of the debug pass */ get debugPassName() { return this._debugPassName; } /** * Set the matrix to use for scaling the world space to voxel space * @param matrix The matrix to use for scaling the world space to voxel space */ setWorldScaleMatrix(matrix) { this._invWorldScaleMatrix = matrix; } /** * Render the shadows in color rather than black and white. * This is slightly more expensive than black and white shadows but can be much * more accurate when the strongest lights in the IBL are non-white. */ set coloredShadows(value) { this._coloredShadows = value; } get coloredShadows() { return this._coloredShadows; } /** * Sets params that control the position and scaling of the debug display on the screen. * @param x Screen X offset of the debug display (0-1) * @param y Screen Y offset of the debug display (0-1) * @param widthScale X scale of the debug display (0-1) * @param heightScale Y scale of the debug display (0-1) */ setDebugDisplayParams(x, y, widthScale, heightScale) { this._debugSizeParams.set(x, y, widthScale, heightScale); } /** * Creates the debug post process effect for this pass */ _createDebugPass() { const isWebGPU = this._engine.isWebGPU; if (!this._debugPassPP) { const debugOptions = { width: this._engine.getRenderWidth(), height: this._engine.getRenderHeight(), uniforms: ["sizeParams"], samplers: ["debugSampler"], engine: this._engine, reusable: true, shaderLanguage: isWebGPU ? 1 /* ShaderLanguage.WGSL */ : 0 /* ShaderLanguage.GLSL */, extraInitializations: (useWebGPU, list) => { if (useWebGPU) { list.push(import('./iblShadowDebug.fragment-Bfs5GsuM.esm.js')); } else { list.push(import('./iblShadowDebug.fragment-DC4W5nFQ.esm.js')); } }, }; this._debugPassPP = new PostProcess(this.debugPassName, "iblShadowDebug", debugOptions); this._debugPassPP.autoClear = false; this._debugPassPP.onApplyObservable.add((effect) => { // update the caustic texture with what we just rendered. effect.setTexture("debugSampler", this._outputTexture); effect.setVector4("sizeParams", this._debugSizeParams); }); } } /** * Instantiates the shadow voxel-tracing pass * @param scene Scene to attach to * @param iblShadowsRenderPipeline The IBL shadows render pipeline * @returns The shadow voxel-tracing pass */ constructor(scene, iblShadowsRenderPipeline) { this._voxelShadowOpacity = 1.0; this._sssSamples = 16; this._sssStride = 8; this._sssMaxDist = 0.05; this._sssThickness = 0.5; this._ssShadowOpacity = 1.0; this._cameraInvView = Matrix.Identity(); this._cameraInvProj = Matrix.Identity(); this._invWorldScaleMatrix = Matrix.Identity(); this._frameId = 0; this._sampleDirections = 4; this._shadowParameters = new Vector4(0.0, 0.0, 0.0, 0.0); this._sssParameters = new Vector4(0.0, 0.0, 0.0, 0.0); this._opacityParameters = new Vector4(0.0, 0.0, 0.0, 0.0); this._voxelBiasParameters = new Vector4(0.0, 0.0, 0.0, 0.0); this._voxelNormalBias = 1.4; this._voxelDirectionBias = 1.75; this._enabled = true; /** Enable the debug view for this pass */ this.debugEnabled = false; this._debugPassName = "Voxel Tracing Debug Pass"; /** The default rotation of the environment map will align the shadows with the default lighting orientation */ this._envRotation = 0.0; this._coloredShadows = false; this._debugVoxelMarchEnabled = false; this._debugSizeParams = new Vector4(0.0, 0.0, 0.0, 0.0); this._renderWhenGBufferReady = null; this._scene = scene; this._engine = scene.getEngine(); this._renderPipeline = iblShadowsRenderPipeline; this._createTextures(); } _createTextures() { const defines = this._createDefines(); const isWebGPU = this._engine.isWebGPU; const textureOptions = { type: Constants.TEXTURETYPE_UNSIGNED_BYTE, format: Constants.TEXTUREFORMAT_RGBA, samplingMode: Constants.TEXTURE_NEAREST_SAMPLINGMODE, generateDepthBuffer: false, shaderLanguage: isWebGPU ? 1 /* ShaderLanguage.WGSL */ : 0 /* ShaderLanguage.GLSL */, extraInitializationsAsync: async () => { if (isWebGPU) { await Promise.all([import('./iblShadowVoxelTracing.fragment-D39QfF7h.esm.js')]); } else { await Promise.all([import('./iblShadowVoxelTracing.fragment-pB9suz3I.esm.js')]); } }, }; this._outputTexture = new ProceduralTexture("voxelTracingPass", { width: this._engine.getRenderWidth(), height: this._engine.getRenderHeight(), }, "iblShadowVoxelTracing", this._scene, textureOptions); this._outputTexture.refreshRate = -1; this._outputTexture.autoClear = false; this._outputTexture.defines = defines; // Need to set all the textures first so that the effect gets created with the proper uniforms. this._setBindings(this._scene.activeCamera); this._renderWhenGBufferReady = this._render.bind(this); // Don't start rendering until the first vozelization is done. this._renderPipeline.onVoxelizationCompleteObservable.addOnce(() => { if (this._scene.geometryBufferRenderer) { this._scene.geometryBufferRenderer.getGBuffer().onAfterRenderObservable.add(this._renderWhenGBufferReady); } }); } _createDefines() { let defines = ""; if (this._scene.useRightHandedSystem) { defines += "#define RIGHT_HANDED\n"; } i