@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
JavaScript
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