@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.
3,066 lines • 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-HyNDfLMI.esm.js';
import { P as ProceduralTexture } from './proceduralTexture.pure-Csq5aHN9.esm.js';
import { G as GeometryBufferRenderer, R as RegisterGeometryBufferRendererSceneComponent } from './geometryBufferRenderer.pure-CaK79VQE.esm.js';
import { a as PostProcessRenderPipeline, P as PostProcessRenderEffect } from './postProcessRenderEffect-D3mUm7-6.esm.js';
import { R as RegisterIblCdfGeneratorSceneComponent, I as IblCdfGenerator } from './iblCdfGenerator-C0XgwuZV.esm.js';
import { M as MaterialPluginBase } from './material.detailMapConfiguration-C33hA3Hm.esm.js';
import { P as PBRBaseMaterial } from './pbrBaseMaterial.pure-5Eirh_Zs.esm.js';
import { S as StandardMaterial } from './standardMaterial.pure-B45jJn5x.esm.js';
import { OpenPBRMaterial } from './openpbrMaterial.pure-ChcQZB2S.esm.js';
import './postProcessRenderPipelineManager-DPqTdabq.esm.js';
import './cubeTexture.pure-0fDNg7hw.esm.js';
import './abstractEngine.cubeTexture.pure-CoMRqdbN.esm.js';
import './textureLoaderManager-GZAmSsDa.esm.js';
import './brdfTextureTools-CrS8Z4G9.esm.js';
import './prepass.defines-NGwPAvhm.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._disposeVoxelTextures();
this._triPlanarVoxelization = enabled;
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-B8JTk03Y.esm.js');
}
else {
await import('./copyTexture3DLayerToTexture.fragment-CifDzCTr.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-CSkA84bA.esm.js');
}
else {
await import('./iblCombineVoxelGrids.fragment-CDBHHq70.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-C3f2_BRC.esm.js');
}
else {
await import('./iblGenerateVoxelMip.fragment-Bj5_XXuq.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-D4DhzdOP.esm.js'), import('./iblVoxelGrid.vertex-DNTUIWV3.esm.js')]);
}
else {
await Promise.all([import('./iblVoxelGrid.fragment-CDasKFAt.esm.js'), import('./iblVoxelGrid.vertex-B2pLv1Qv.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-f5MbNzgY.esm.js'));
}
else {
list.push(import('./iblShadowDebug.fragment-CAvtv_Yy.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-DUXh6c9U.esm.js')]);
}
else {
await Promise.all([import('./iblShadowVoxelTracing.fragment-_94Wdqp2.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";
}
if (this._debugVoxelMarchEnabled) {
defines += "#define VOXEL_MARCH_DIAGNOSTIC_INFO_OPTION 1u\n";
}
if (this._coloredShadows) {
defines += "#define COLOR_SHADOWS 1u\n";
}
if (this._scene.geometryBufferRenderer?.normalsAreUnsigned) {
defines += "#define WORLD_NORMAL_UNSIGNED\n";
}
return defines;
}
_setBindings(camera) {
this._outputTexture.defines = this._createDefines();
this._outputTexture.setMatrix("viewMtx", camera.getViewMatrix());
this._outputTexture.setMatrix("projMtx", camera.getProjectionMatrix());
camera.getProjectionMatrix().invertToRef(this._cameraInvProj);
camera.getViewMatrix().invertToRef(this._cameraInvView);
this._outputTexture.setMatrix("invProjMtx", this._cameraInvProj);
this._outputTexture.setMatrix("invViewMtx", this._cameraInvView);
this._outputTexture.setMatrix("wsNormalizationMtx", this._invWorldScaleMatrix);
this._frameId++;
let rotation = 0.0;
if (this._scene.environmentTexture) {
rotation = this._scene.environmentTexture.rotationY ?? 0;
}
rotation = this._scene.useRightHandedSystem ? -(rotation + 0.5 * Math.PI) : rotation - 0.5 * Math.PI;
rotation = rotation % (2.0 * Math.PI);
this._shadowParameters.set(this._sampleDirections, this._frameId, 1.0, rotation);
this._outputTexture.setVector4("shadowParameters", this._shadowParameters);
const voxelGrid = this._renderPipeline._getVoxelGridTexture();
const highestMip = Math.floor(Math.log2(voxelGrid.getSize().width));
this._voxelBiasParameters.set(this._voxelNormalBias, this._voxelDirectionBias, highestMip, 0.0);
this._outputTexture.setVector4("voxelBiasParameters", this._voxelBiasParameters);
// SSS Options.
this._sssParameters.set(this._sssSamples, this._sssStride, this._sssMaxDist, this._sssThickness);
this._outputTexture.setVector4("sssParameters", this._sssParameters);
this._opacityParameters.set(this._voxelShadowOpacity, this._ssShadowOpacity, 0.0, 0.0);
this._outputTexture.setVector4("shadowOpacity", this._opacityParameters);
this._outputTexture.setTexture("voxelGridSampler", voxelGrid);
this._outputTexture.setTexture("blueNoiseSampler", this._renderPipeline._getNoiseTexture());
const cdfGenerator = this._scene.iblCdfGenerator;
if (!cdfGenerator) {
Logger.Warn("IBLShadowsVoxelTracingPass: Can't bind for render because iblCdfGenerator is not enabled.");
return false;
}
this._outputTexture.setTexture("icdfSampler", cdfGenerator.getIcdfTexture());
if (this._coloredShadows && this._scene.environmentTexture) {
this._outputTexture.setTexture("iblSampler", this._scene.environmentTexture);
}
const geometryBufferRenderer = this._scene.geometryBufferRenderer;
if (!geometryBufferRenderer) {
Logger.Warn("IBLShadowsVoxelTracingPass: Can't bind for render because GeometryBufferRenderer is not enabled.");
return false;
}
const depthIndex = geometryBufferRenderer.getTextureIndex(GeometryBufferRenderer.SCREENSPACE_DEPTH_TEXTURE_TYPE);
this._outputTexture.setTexture("depthSampler", geometryBufferRenderer.getGBuffer().textures[depthIndex]);
const wnormalIndex = geometryBufferRenderer.getTextureIndex(GeometryBufferRenderer.NORMAL_TEXTURE_TYPE);
this._outputTexture.setTexture("worldNormalSampler", geometryBufferRenderer.getGBuffer().textures[wnormalIndex]);
return true;
}
_render() {
if (this.enabled && this._outputTexture.isReady() && this._outputTexture.getEffect()?.isReady()) {
if (this._setBindings(this._scene.activeCamera)) {
this._outputTexture.render();
}
}
}
/**
* Called by render pipeline when canvas resized.
* @param scaleFactor The factor by which to scale the canvas size.
*/
resize(scaleFactor = 1.0) {
const newSize = {
width: Math.max(1.0, Math.floor(this._engine.getRenderWidth() * scaleFactor)),
height: Math.max(1.0, Math.floor(this._engine.getRenderHeight() * scaleFactor)),
};
// Don't resize if the size is the same as the current size.
if (this._outputTexture.getSize().width === newSize.width && this._outputTexture.getSize().height === newSize.height) {
return;
}
this._outputTexture.resize(newSize, false);
}
/**
* Checks if the pass is ready
* @returns true if the pass is ready
*/
isReady() {
return (this._outputTexture.isReady() &&
!(this._debugPassPP && !this._debugPassPP.isReady()) &&
this._scene.iblCdfGenerator &&
this._scene.iblCdfGenerator.getIcdfTexture().isReady() &&
this._renderPipeline._getVoxelGridTexture().isReady());
}
/**
* Disposes the associated resources
*/
dispose() {
if (this._scene.geometryBufferRenderer && this._renderWhenGBufferReady) {
const gBuffer = this._scene.geometryBufferRenderer.getGBuffer();
gBuffer.onAfterRenderObservable.removeCallback(this._renderWhenGBufferReady);
}
this._outputTexture.dispose();
if (this._debugPassPP) {
this._debugPassPP.dispose();
}
}
}
/**
* This should not be instanciated directly, as it is part of a scene component
* @internal
*/
class _IblShadowsSpatialBlurPass {
/**
* 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;
}
}
/**
* Returns the output texture of the pass.
* @returns The output texture.
*/
getOutputTexture() {
return this._outputTexture;
}
/**
* Gets the debug pass post process
* @returns The post process
*/
getDebugPassPP() {
if (!this._debugPassPP) {
this._createDebugPass();
}
return this._debugPassPP;
}
/**
* Sets the name of the debug pass
*/
get debugPassName() {
return this._debugPassName;
}
/**
* The scale of the voxel grid in world space. This is used to scale the blur radius in world space.
* @param scale The scale of the voxel grid in world space.
*/
setWorldScale(scale) {
this._worldScale = scale;
}
/**
* 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() {
if (!this._debugPassPP) {
const isWebGPU = this._engine.isWebGPU;
const debugOptions = {
width: this._engine.getRenderWidth(),
height: this._engine.getRenderHeight(),
textureFormat: Constants.TEXTUREFORMAT_RGBA,
textureType: Constants.TEXTURETYPE_UNSIGNED_BYTE,
samplingMode: Constants.TEXTURE_NEAREST_SAMPLINGMODE,
uniforms: ["sizeParams"],
samplers: ["debugSampler"],
engine: this._engine,
reusable: false,
shaderLanguage: isWebGPU ? 1 /* ShaderLanguage.WGSL */ : 0 /* ShaderLanguage.GLSL */,
extraInitializations: (useWebGPU, list) => {
if (useWebGPU) {
list.push(import('./iblShadowDebug.fragment-f5MbNzgY.esm.js'));
}
else {
list.push(import('./iblShadowDebug.fragment-CAvtv_Yy.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);
});
}
}
/**
* Instanciates the importance sampling renderer
* @param scene Scene to attach to
* @param iblShadowsRenderPipeline The IBL shadows render pipeline
* @returns The importance sampling renderer
*/
constructor(scene, iblShadowsRenderPipeline) {
this._worldScale = 1.0;
this._blurParameters = new Vector4(0.0, 0.0, 0.0, 0.0);
this._enabled = true;
this._debugPassName = "Spatial Blur Debug Pass";
/** Enable the debug view for this pass */
this.debugEnabled = 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 isWebGPU = this._engine.isWebGPU;
const textureOptions = {
type: Constants.TEXTURETYPE_UNSIGNED_BYTE,
format: Constants.TEXTUREFORMAT_RGBA,
samplingMode: Constants.TEXTURE_NEAREST_SAMPLINGMODE,
generateDepthBuffer: false,
generateMipMaps: false,
shaderLanguage: isWebGPU ? 1 /* ShaderLanguage.WGSL */ : 0 /* ShaderLanguage.GLSL */,
extraInitializationsAsync: async () => {
if (isWebGPU) {
await Promise.all([import('./iblShadowSpatialBlur.fragment-B2VRNC1_.esm.js')]);
}
else {
await Promise.all([import('./iblShadowSpatialBlur.fragment-Bm2uWJvs.esm.js')]);
}
},
};
this._outputTexture = new ProceduralTexture("spatialBlurPass", {
width: this._engine.getRenderWidth(),
height: this._engine.getRenderHeight(),
}, "iblShadowSpatialBlur", this._scene, textureOptions, false, false, Constants.TEXTURETYPE_UNSIGNED_BYTE);
this._outputTexture.refreshRate = -1;
this._outputTexture.autoClear = false;
// Need to set all the textures first so that the effect gets created with the proper uniforms.
this._setBindings();
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);
}
});
}
_setBindings() {
this._outputTexture.setTexture("voxelTracingSampler", this._renderPipeline._getVoxelTracingTexture());
const iterationCount = 1;
this._blurParameters.set(iterationCount, this._worldScale, 0.0, 0.0);
this._outputTexture.setVector4("blurParameters", this._blurParameters);
const geometryBufferRenderer = this._scene.geometryBufferRenderer;
if (!geometryBufferRenderer) {
return false;
}
const depthIndex = geometryBufferRenderer.getTextureIndex(GeometryBufferRenderer.SCREENSPACE_DEPTH_TEXTURE_TYPE);
this._outputTexture.setTexture("depthSampler", geometryBufferRenderer.getGBuffer().textures[depthIndex]);
const wnormalIndex = geometryBufferRenderer.getTextureIndex(GeometryBufferRenderer.NORMAL_TEXTURE_TYPE);
this._outputTexture.setTexture("worldNormalSampler", geometryBufferRenderer.getGBuffer().textures[wnormalIndex]);
return true;
}
_render() {
if (this.enabled && this._outputTexture.isReady() && this._outputTexture.getEffect()?.isReady()) {
if (this._setBindings()) {
this._outputTexture.render();
}
}
}
/**
* Called by render pipeline when canvas resized.
* @param scaleFactor The factor by which to scale the canvas size.
*/
resize(scaleFactor = 1.0) {
const newSize = {
width: Math.max(1.0, Math.floor(this._engine.getRenderWidth() * scaleFactor)),
height: Math.max(1.0, Math.floor(this._engine.getRenderHeight() * scaleFactor)),
};
// Don't resize if the size is the same as the current size.
if (this._outputTexture.getSize().width === newSize.width && this._outputTexture.getSize().height === newSize.height) {
return;
}
this._outputTexture.resize(newSize, false);
}
/**
* Checks if the pass is ready
* @returns true if the pass is ready
*/
isReady() {
return this._outputTexture.isReady() && !(this._debugPassPP && !this._debugPassPP.isReady());
}
/**
* Disposes the associated resources
*/
dispose() {
if (this._scene.geometryBufferRenderer && this._renderWhenGBufferReady) {
const gBuffer = this._scene.geometryBufferRenderer.getGBuffer();
gBuffer.onAfterRenderObservable.removeCallback(this._renderWhenGBufferReady);
}
this._outputTexture.dispose();
if (this._debugPassPP) {
this._debugPassPP.dispose();
}
}
}
/**
* This should not be instantiated directly, as it is part of a scene component
* @internal
*/
class _IblShadowsAccumulationPass {
/**
* Is the effect enabled
*/
get enabled() {
return this._enabled;
}
set enabled(value) {
this._enabled = value;
// _render() already gates on `enabled` for _outputTexture, but _oldAccumulationCopy and
// _oldPositionCopy have refreshRate = 1 and are rendered by Babylon's generic per-frame
// ProceduralTexture refresh loop, entirely independent of `enabled` / `_render()`. Without
// disabling them directly here, they keep rendering (and paying a real per-frame texture-view
// creation cost) every frame forever, even when the whole pass — and the rest of the IBL
// shadows pipeline — has been toggled off (e.g. no shadow casters or receivers in the scene).
if (this._outputTexture) {
this._outputTexture.isEnabled = value;
}
if (this._oldAccumulationCopy) {
this._oldAccumulationCopy.isEnabled = value;
}
if (this._oldPositionCopy) {
this._oldPositionCopy.isEnabled = value;
}
}
/**
* Returns the output texture of the pass.
* @returns The output texture.
*/
getOutputTexture() {
return this._outputTexture;
}
/**
* Gets the debug pass post process
* @returns The post process
*/
getDebugPassPP() {
if (!this._debugPassPP) {
this._createDebugPass();
}
return this._debugPassPP;
}
/**
* Gets the name of the debug pass
* @returns The name of the debug pass
*/
get debugPassName() {
return this._debugPassName;
}
/**
* A value that controls how much of the previous frame's accumulation to keep.
* The higher the value, the faster the shadows accumulate but the more potential ghosting you'll see.
*/
get remanence() {
return this._remanence;
}
/**
* A value that controls how much of the previous frame's accumulation to keep.
* The higher the value, the faster the shadows accumulate but the more potential ghosting you'll see.
*/
set remanence(value) {
this._remanence = value;
}
/**
* Reset the accumulation.
*/
get reset() {
return this._reset;
}
/**
* Reset the accumulation.
*/
set reset(value) {
this._reset = value;
}
/**
* Tell the pass that the camera is moving. This will cause the accumulation
* rate to change.
*/
set isMoving(value) {
this._isMoving = value;
}
/**
* 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() {
if (!this._debugPassPP) {
const isWebGPU = this._engine.isWebGPU;
const debugOptions = {
width: this._engine.getRenderWidth(),
height: this._engine.getRenderHeight(),
textureFormat: Constants.TEXTUREFORMAT_RGBA,
textureType: Constants.TEXTURETYPE_UNSIGNED_BYTE,
samplingMode: Constants.TEXTURE_NEAREST_SAMPLINGMODE,
uniforms: ["sizeParams"],
samplers: ["debugSampler"],
engine: this._engine,
reusable: false,
shaderLanguage: isWebGPU ? 1 /* ShaderLanguage.WGSL */ : 0 /* ShaderLanguage.GLSL */,
extraInitializations: (useWebGPU, list) => {
if (useWebGPU) {
list.push(import('./iblShadowDebug.fragment-f5MbNzgY.esm.js'));
}
else {
list.push(import('./iblShadowDebug.fragment-CAvtv_Yy.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 accumulation pass
* @param scene Scene to attach to
* @param iblShadowsRenderPipeline The IBL shadows render pipeline
* @returns The accumulation pass
*/
constructor(scene, iblShadowsRenderPipeline) {
this._accumulationParams = new Vector4(0.0, 0.0, 0.0, 0.0);
/** Enable the debug view for this pass */
this.debugEnabled = false;
this._enabled = true;
/**
* Observable that triggers when the accumulation texture is ready
*/
this.onReadyObservable = new Observable();
this._debugPassName = "Shadow Accumulation Debug Pass";
this._remanence = 0.9;
this._reset = true;
this._isMoving = 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 isWebGPU = this._engine.isWebGPU;
const outputTextureOptions = {
type: Constants.TEXTURETYPE_HALF_FLOAT,
format: Constants.TEXTUREFORMAT_RGBA,
samplingMode: Constants.TEXTURE_NEAREST_SAMPLINGMODE,
generateDepthBuffer: false,
generateMipMaps: false,
shaderLanguage: isWebGPU ? 1 /* ShaderLanguage.WGSL */ : 0 /* ShaderLanguage.GLSL */,
extraInitializationsAsync: async () => {
if (isWebGPU) {
await Promise.all([import('./iblShadowAccumulation.fragment-BEDpECEq.esm.js')]);
}
else {
await Promise.all([import('./iblShadowAccumulation.fragment-CTXh-kaj.esm.js')]);
}
},
};
this._outputTexture = new ProceduralTexture("shadowAccumulationPass", {
width: this._engine.getRenderWidth(),
height: this._engine.getRenderHeight(),
}, "iblShadowAccumulation", this._scene, outputTextureOptions);
this._outputTexture.refreshRate = -1;
this._outputTexture.autoClear = false;
this._outputTexture.onGeneratedObservable.addOnce(() => {
this.onReadyObservable.notifyObservers();
});
// Need to set all the textures first so that the effect gets created with the proper uniforms.
this._setOutputTextureBindings();
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);
}
});
// Create the accumulation texture for the previous frame.
// We'll copy the output of the accumulation pass to this texture at the start of every frame.
const accumulationOptions = {
type: Constants.TEXTURETYPE_HALF_FLOAT,
format: Constants.TEXTUREFORMAT_RGBA,
samplingMode: Constants.TEXTURE_NEAREST_SAMPLINGMODE,
generateDepthBuffer: false,
generateMipMaps: false,
shaderLanguage: isWebGPU ? 1 /* ShaderLanguage.WGSL */ : 0 /* ShaderLanguage.GLSL */,
extraInitializationsAsync: async () => {
if (isWebGPU) {
await Promise.all([import('./pass.fragment-DS6uQAkG.esm.js')]);
}
else {
await Promise.all([import('./pass.fragment-Bo-QegcY.esm.js')]);
}
},
};
this._oldAccumulationCopy = new ProceduralTexture("oldAccumulationRT", { width: this._engine.getRenderWidth(), height: this._engine.getRenderHeight() }, "pass", this._scene, accumulationOptions, false);
this._oldAccumulationCopy.autoClear = false;
this._oldAccumulationCopy.refreshRate = 1;
this._oldAccumulationCopy.onBeforeGenerationObservable.add(this._setAccumulationCopyBindings.bind(this));
this._setAccumulationCopyBindings();
// Create the local position texture for the previous frame.
// We'll copy the previous local position texture to this texture at the start of every frame.
const localPositionOptions = {
type: Constants.TEXTURETYPE_HALF_FLOAT,
format: Constants.TEXTUREFORMAT_RGBA,
samplingMode: Constants.TEXTURE_NEAREST_SAMPLINGMODE,
generateDepthBuffer: false,
generateMipMaps: false,
shaderLanguage: isWebGPU ? 1 /* ShaderLanguage.WGSL */ : 0 /* ShaderLanguage.GLSL */,
extraInitializationsAsync: async () => {
if (isWebGPU) {
await Promise.all([import('./pass.fragment-DS6uQAkG.esm.js')]);
}
else {
await Promise.all([import('./pass.fragment-Bo-QegcY.esm.js')]);
}
},
};
this._oldPositionCopy = new ProceduralTexture("oldLocalPositionRT", { width: this._engine.getRenderWidth(), height: this._engine.getRenderHeight() }, "pass", this._scene, localPositionOptions, false);
this._updatePositionCopy();
this._oldPositionCopy.autoClear = false;
this._oldPositionCopy.refreshRate = 1;
this._oldPositionCopy.onBeforeGenerationObservable.add(this._updatePositionCopy.bind(this));
}
_setOutputTextureBindings() {
const remanence = this._isMoving ? this.remanence : 0.99;
this._accumulationParams.set(remanence, this.reset ? 1.0 : 0.0, this._renderPipeline.voxelGridSize, 0.0);
this._outputTexture.setTexture("spatialBlurSampler", this._renderPipeline._getSpatialBlurTexture());
this._outputTexture.setVector4("accumulationParameters", this._accumulationParams);
this._outputTexture.setTexture("oldAccumulationSampler", this._oldAccumulationCopy ? this._oldAccumulationCopy : this._renderPipeline._dummyTexture2d);
this._outputTexture.setTexture("prevPositionSampler", this._oldPositionCopy ? this._oldPositionCopy : this._renderPipeline._dummyTexture2d);
const geometryBufferRenderer = this._scene.geometryBufferRenderer;
if (!geometryBufferRenderer) {
return false;
}
const velocityIndex = geometryBufferRenderer.getTextureIndex(GeometryBufferRenderer.VELOCITY_LINEAR_TEXTURE_TYPE);
this._outputTexture.setTexture("motionSampler", geometryBufferRenderer.getGBuffer().textures[velocityIndex]);
const wPositionIndex = geometryBufferRenderer.getTextureIndex(GeometryBufferRenderer.POSITION_TEXTURE_TYPE);
this._outputTexture.setTexture("positionSampler", geometryBufferRenderer.getGBuffer().textures[wPositionIndex]);
this.reset = false;
this._isMoving = false;
return true;
}
_updatePositionCopy() {
const geometryBufferRenderer = this._scene.geometryBufferRenderer;
if (geometryBufferRenderer) {
const index = geometryBufferRenderer.getTextureIndex(GeometryBufferRenderer.POSITION_TEXTURE_TYPE);
this._oldPositionCopy.setTexture("textureSampler", geometryBufferRenderer.getGBuffer().textures[index]);
}
}
_setAccumulationCopyBindings() {
this._oldAccumulationCopy.setTexture("textureSampler", this._outputTexture);
}
_render() {
if (this.enabled && this._outputTexture.isReady() && this._outputTexture.getEffect()?.isReady()) {
if (this._setOutputTextureBindings()) {
this._outputTexture.render();
}
}
}
/**
* Called by render pipeline when canvas resized.
* @param scaleFactor The factor by which to scale the canvas size.
*/
resize(scaleFactor = 1.0) {
const newSize = {
width: Math.max(1.0, Math.floor(this._engine.getRenderWidth() * scaleFactor)),
height: Math.max(1.0, Math.floor(this._engine.getRenderHeight() * scaleFactor)),
};
// Don't resize if the size is the same as the current size.
if (this._outputTexture.getSize().width === newSize.width && this._outputTexture.getSize().height === newSize.height) {
return;
}
this._outputTexture.resize(newSize, false);
this._oldAccumulationCopy.resize(newSize, false);
this._oldPositionCopy.resize({ width: this._engine.getRenderWidth(), height: this._engine.getRenderHeight() }, false);
this.reset = true;
}
_disposeTextures() {
this._oldAccumulationCopy.dispose();
this._oldPositionCopy.dispose();
this._outputTexture.dispose();
}
/**
* Checks if the pass is ready
* @returns true if the pass is ready
*/
isReady() {
return (this._oldAccumulationCopy &&
this._oldAccumulationCopy.isReady() &&
this._oldPositionCopy &&
this._oldPositionCopy.isReady() &&
this._outputTexture.isReady() &&
!(this._debugPassPP && !this._debugPassPP.isReady()));
}
/**
* Disposes the associated resources
*/
dispose() {
if (this._scene.geometryBufferRenderer && this._renderWhenGBufferReady) {
const gBuffer = this._scene.geometryBufferRenderer.getGBuffer();
gBuffer.onAfterRenderObservable.removeCallback(this._renderWhenGBufferReady);
}
this._disposeTextures();
if (this._debugPassPP) {
this._debugPassPP.dispose();
}
this.onReadyObservable.clear();
}
}
/**
* Class used to store 3D textures containing user data
*/
class RawTexture3D extends Texture {
/**
* Gets the width of the texture
*/
get width() {
return this._texture ? this._texture.width : 0;
}
/**
* Gets the height of the texture
*/
get height() {
return this._texture ? this._texture.height : 0;
}
/**
* Gets the depth of the texture
*/
get depth() {
return this._texture ? this._texture.depth : 0;
}
/**
* Create a new RawTexture3D
* @param data defines the data of the texture
* @param width defines the width of the texture
* @param height defines the height of the texture
* @param depth defines the depth of the texture
* @param format defines the texture format to use
* @param scene defines the hosting scene
* @param generateMipMaps defines a boolean indicating if mip levels should be generated (true by default)
* @param invertY defines if texture must be stored with Y axis inverted
* @param samplingMode defines the sampling mode to use (Texture.TRILINEAR_SAMPLINGMODE by default)
* @param textureType defines the texture Type (Engine.TEXTURETYPE_UNSIGNED_BYTE, Engine.TEXTURETYPE_FLOAT...)
* @param creationFlags specific flags to use when creating the texture (Constants.TEXTURE_CREATIONFLAG_STORAGE for storage textures, for eg)
*/
constructor(data, width, height, depth,
/** Gets or sets the texture format to use */
format, scene, generateMipMaps = true, invertY = false, samplingMode = Texture.TRILINEAR_SAMPLINGMODE, textureType = Constants.TEXTURETYPE_UNSIGNED_BYTE, creationFlags) {
super(null, scene, !generateMipMaps, invertY);
this.format = format;
this._texture = scene.getEngine().createRawTexture3D(data, width, height, depth, format, generateMipMaps, invertY, samplingMode, null, textureType, creationFlags);
this.is3D = true;
}
/**
* Update the texture with new data
* @param data defines the data to store in the texture
*/
update(data) {
if (!this._texture) {
return;
}
this._getEngine().updateRawTexture3D(this._texture, data, this._texture.format, this._texture.invertY, null, this._texture.type);
}
}
/** This file must only contain pure code and pure imports */
/**
* @internal
*/
class MaterialIBLShadowsRenderDefines extends MaterialDefines {
constructor() {
super(...arguments);
this.RENDER_WITH_IBL_SHADOWS = false;
this.COLORED_IBL_SHADOWS = false;
}
}
/**
* Plugin used to render the contribution from IBL shadows.
*/
let IBLShadowsPluginMaterial = (() => {
var _a, _IBLShadowsPluginMaterial_isEnabled_accessor_storage;
let _classSuper = MaterialPluginBase;
let _shadowOpacity_decorators;
let _shadowOpacity_initializers = [];
let _shadowOpacity_extraInitializers = [];
let _isEnabled_decorators;
let _isEnabled_initializers = [];
let _isEnabled_extraInitializers = [];
return _a = class IBLShadowsPluginMaterial extends _classSuper {
get iblShadowsTexture() {
return this._iblShadowsTexture;
}
set iblShadowsTexture(value) {
if (this._iblShadowsTexture === value) {
return;
}
this._iblShadowsTexture = value;
this._markAllSubMeshesAsTexturesDirty();
}
get isColored() {
return this._isColored;
}
set isColored(value) {
if (this._isColored === value) {
return;
}
this._isColored = value;
this._markAllSubMeshesAsTexturesDirty();
}
/**
* Defines if the plugin is enabled in the material.
*/
get isEnabled() { return __classPrivateFieldGet(this, _IBLShadowsPluginMaterial_isEnabled_accessor_storage, "f"); }
set isEnabled(value) { __classPrivateFieldSet(this, _IBLShadowsPluginMaterial_isEnabled_accessor_storage, value, "f"); }
_markAllSubMeshesAsTexturesDirty() {
this._enable(this._isEnabled);
this._internalMarkAllSubMeshesAsTexturesDirty();
}
/**
* Gets a boolean indicating that the plugin is compatible with a give shader language.
* @returns true if the plugin is compatible with the shader language
*/
isCompatible() {
return true;
}
constructor(material) {
super(material, _a.Name, 310, new MaterialIBLShadowsRenderDefines());
/**
* The opacity of the shadows.
*/
this.shadowOpacity = __runInitializers(this, _shadowOpacity_initializers, 1.0);
this._isEnabled = (__runInitializers(this, _shadowOpacity_extraInitializers), false);
this._isColored = false;
_IBLShadowsPluginMaterial_isEnabled_accessor_storage.set(this, __runInitializers(this, _isEnabled_initializers, false));
this._internalMarkAllSubMeshesAsTexturesDirty = __runInitializers(this, _isEnabled_extraInitializers);
this._internalMarkAllSubMeshesAsTexturesDirty = material._dirtyCallbacks[Constants.MATERIAL_TextureDirtyFlag];
}
_isOpenPBRMaterial() {
return this._material.getClassName() === "OpenPBRMaterial";
}
prepareDefines(defines) {
defines.RENDER_WITH_IBL_SHADOWS = this._isEnabled;
defines.COLORED_IBL_SHADOWS = this.isColored;
}
getClassName() {
return "IBLShadowsPluginMaterial";
}
getUniforms(_shaderLanguage) {
const result = {};
result.ubo = [];
if (this._isOpenPBRMaterial()) {
if (_shaderLanguage === 1 /* ShaderLanguage.WGSL */) {
result.fragment = `#ifdef RENDER_WITH_IBL_SHADOWS
var shadowOpacity: f32;
#endif`;
}
else {
result.fragment = `#ifdef RENDER_WITH_IBL_SHADOWS
uniform float shadowOpacity;
#endif`;
}
}
else {
result.ubo.push({ name: "renderTargetSize", size: 2, type: "vec2" });
if (_shaderLanguage === 1 /* ShaderLanguage.WGSL */) {
result.fragment = `#ifdef RENDER_WITH_IBL_SHADOWS
var renderTargetSize: vec2f;
var shadowOpacity: f32;
#endif`;
}
else {
result.fragment = `#ifdef RENDER_WITH_IBL_SHADOWS
uniform vec2 renderTargetSize;
uniform float shadowOpacity;
#endif`;
}
}
result.ubo.push({ name: "shadowOpacity", size: 1, type: "float" });
return result;
}
getSamplers(samplers) {
samplers.push("iblShadowsTexture");
}
bindForSubMesh(uniformBuffer) {
if (this._isEnabled && this.iblShadowsTexture) {
uniformBuffer.bindTexture("iblShadowsTexture", this.iblShadowsTexture);
uniformBuffer.updateFloat2("renderTargetSize", this._material.getScene().getEngine().getRenderWidth(), this._material.getScene().getEngine().getRenderHeight());
uniformBuffer.updateFloat("shadowOpacity", this.shadowOpacity);
}
}
getCustomCode(shaderType, shaderLanguage) {
let frag;
if (shaderLanguage === 1 /* ShaderLanguage.WGSL */) {
frag = {
// eslint-disable-next-line @typescript-eslint/naming-convention
CUSTOM_FRAGMENT_DEFINITIONS: `
#ifdef RENDER_WITH_IBL_SHADOWS
var iblShadowsTextureSampler: sampler;
var iblShadowsTexture: texture_2d<f32>;
#ifdef COLORED_IBL_SHADOWS
fn computeIndirectShadow() -> vec3f {
var uv = fragmentInputs.position.xy / uniforms.renderTargetSize;
var shadowValue: vec3f = textureSample(iblShadowsTexture, iblShadowsTextureSampler, uv).rgb;
return mix(shadowValue, vec3f(1.0), 1.0 - uniforms.shadowOpacity);
}
#else
fn computeIndirectShadow() -> vec2f {
var uv = fragmentInputs.position.xy / uniforms.renderTargetSize;
var shadowValue: vec2f = textureSample(iblShadowsTexture, iblShadowsTextureSampler, uv).rg;
return mix(shadowValue, vec2f(1.0), 1.0 - uniforms.shadowOpacity);
}
#endif
#endif
`,
};
if (this._material instanceof PBRBaseMaterial) {
// eslint-disable-next-line @typescript-eslint/naming-convention
frag["CUSTOM_FRAGMENT_BEFORE_FINALCOLORCOMPOSITION"] = `
#ifdef RENDER_WITH_IBL_SHADOWS
#ifndef UNLIT
#ifdef REFLECTION
#ifdef COLORED_IBL_SHADOWS
var shadowValue: vec3f = computeIndirectShadow();
finalIrradiance *= shadowValue;
finalRadianceScaled *= mix(vec3f(1.0), shadowValue, roughness);
#else
var shadowValue: vec2f = computeIndirectShadow();
finalIrradiance *= vec3f(shadowValue.x);
finalRadianceScaled *= vec3f(mix(pow(shadowValue.y, 4.0), shadowValue.x, roughness));
#endif
#endif
#else
finalDiffuse *= computeIndirectShadow().x;
#endif
#endif
`;
}
else if (this._isOpenPBRMaterial()) {
// eslint-disable-next-line @typescript-eslint/naming-convention
frag["CUSTOM_FRAGMENT_BEFORE_IBLLAYERCOMPOSITION"] = `
#ifdef RENDER_WITH_IBL_SHADOWS
#ifndef UNLIT
#ifdef REFLECTION
#ifdef COLORED_IBL_SHADOWS
var shadowValue: vec3f = computeIndirectShadow();
ambient_occlusion = min(ambient_occlusion, shadowValue);
#else
var shadowValue: vec2f = computeIndirectShadow();
ambient_occlusion = min(ambient_occlusion, vec3f(shadowValue.x));
specular_ambient_occlusion = min(specular_ambient_occlusion, pow(shadowValue.y, 4.0));
#endif
#endif
#else
ambient_occlusion = min(ambient_occlusion, vec3f(computeIndirectShadow().x));
#endif
#endif
`;
}
else {
frag["CUSTOM_FRAGMENT_BEFORE_FRAGCOLOR"] = `
#ifdef RENDER_WITH_IBL_SHADOWS
#ifdef COLORED_IBL_SHADOWS
var shadowValue: vec3f = computeIndirectShadow();
color *= toGammaSpace(vec4f(shadowValue, 1.0f));
#else
var shadowValue: vec2f = computeIndirectShadow();
color *= toGammaSpace(vec4f(shadowValue.x, shadowValue.x, shadowValue.x, 1.0f));
#endif
#endif
`;
}
}
else {
frag = {
// eslint-disable-next-line @typescript-eslint/naming-convention
CUSTOM_FRAGMENT_DEFINITIONS: `
#ifdef RENDER_WITH_IBL_SHADOWS
uniform sampler2D iblShadowsTexture;
#ifdef COLORED_IBL_SHADOWS
vec3 computeIndirectShadow() {
vec2 uv = gl_FragCoord.xy / renderTargetSize;
vec3 shadowValue = texture2D(iblShadowsTexture, uv).rgb;
return mix(shadowValue.rgb, vec3(1.0), 1.0 - shadowOpacity);
}
#else
vec2 computeIndirectShadow() {
vec2 uv = gl_FragCoord.xy / renderTargetSize;
vec2 shadowValue = texture2D(iblShadowsTexture, uv).rg;
return mix(shadowValue.rg, vec2(1.0), 1.0 - shadowOpacity);
}
#endif
#endif
`,
};
if (this._material instanceof PBRBaseMaterial) {
// eslint-disable-next-line @typescript-eslint/naming-convention
frag["CUSTOM_FRAGMENT_BEFORE_FINALCOLORCOMPOSITION"] = `
#ifdef RENDER_WITH_IBL_SHADOWS
#ifndef UNLIT
#ifdef REFLECTION
#ifdef COLORED_IBL_SHADOWS
vec3 shadowValue = computeIndirectShadow();
finalIrradiance.rgb *= shadowValue.rgb;
finalRadianceScaled *= mix(vec3(1.0), shadowValue.rgb, roughness);
#else
vec2 shadowValue = computeIndirectShadow();
finalIrradiance *= shadowValue.x;
finalRadianceScaled *= mix(pow(shadowValue.y, 4.0), shadowValue.x, roughness);
#endif
#endif
#else
finalDiffuse *= computeIndirectShadow().x;
#endif
#endif
`;
}
else if (this._isOpenPBRMaterial()) {
// eslint-disable-next-line @typescript-eslint/naming-convention
frag["CUSTOM_FRAGMENT_BEFORE_IBLLAYERCOMPOSITION"] = `
#ifdef RENDER_WITH_IBL_SHADOWS
#ifndef UNLIT
#ifdef REFLECTION
#ifdef COLORED_IBL_SHADOWS
vec3 shadowValue = computeIndirectShadow();
ambient_occlusion = min(ambient_occlusion, shadowValue);
#else
vec2 shadowValue = computeIndirectShadow();
ambient_occlusion = min(ambient_occlusion, vec3(shadowValue.x));
specular_ambient_occlusion = min(specular_ambient_occlusion, pow(shadowValue.y, 4.0));
#endif
#endif
#else
ambient_occlusion = min(ambient_occlusion, vec3(computeIndirectShadow().x));
#endif
#endif
`;
}
else {
frag["CUSTOM_FRAGMENT_BEFORE_FRAGCOLOR"] = `
#ifdef RENDER_WITH_IBL_SHADOWS
#ifdef COLORED_IBL_SHADOWS
vec3 shadowValue = computeIndirectShadow();
color.rgb *= toGammaSpace(shadowValue.rgb);
#else
vec2 shadowValue = computeIndirectShadow();
color.rgb *= toGammaSpace(shadowValue.x);
#endif
#endif
`;
}
}
return shaderType === "vertex" ? null : frag;
}
},
_IBLShadowsPluginMaterial_isEnabled_accessor_storage = new WeakMap(),
(() => {
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
_shadowOpacity_decorators = [serialize()];
_isEnabled_decorators = [serialize(), expandToProperty("_markAllSubMeshesAsTexturesDirty")];
__esDecorate(_a, null, _isEnabled_decorators, { kind: "accessor", name: "isEnabled", static: false, private: false, access: { has: obj => "isEnabled" in obj, get: obj => obj.isEnabled, set: (obj, value) => { obj.isEnabled = value; } }, metadata: _metadata }, _isEnabled_initializers, _isEnabled_extraInitializers);
__esDecorate(null, null, _shadowOpacity_decorators, { kind: "field", name: "shadowOpacity", static: false, private: false, access: { has: obj => "shadowOpacity" in obj, get: obj => obj.shadowOpacity, set: (obj, value) => { obj.shadowOpacity = value; } }, metadata: _metadata }, _shadowOpacity_initializers, _shadowOpacity_extraInitializers);
if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
})(),
/**
* Defines the name of the plugin.
*/
_a.Name = "IBLShadowsPluginMaterial",
_a;
})();
/** This file must only contain pure code and pure imports */
/**
* Voxel-based shadow rendering for IBL's.
* This should not be instanciated directly, as it is part of a scene component
*/
class IblShadowsRenderPipeline extends PostProcessRenderPipeline {
/**
* Reset the shadow accumulation. This has a similar affect to lowering the remanence for a single frame.
* This is useful when making a sudden change to the IBL.
*/
resetAccumulation() {
this._accumulationPass.reset = true;
}
/**
* How dark the shadows appear. 1.0 is full opacity, 0.0 is no shadows.
*/
get shadowOpacity() {
return this._shadowOpacity;
}
set shadowOpacity(value) {
this._shadowOpacity = value;
this._setPluginParameters();
}
/**
* 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.
*/
get coloredShadows() {
return this._coloredShadows;
}
set coloredShadows(value) {
this._coloredShadows = value;
this._voxelTracingPass.coloredShadows = value;
this._setPluginParameters();
}
/**
* A multiplier for the render size of the shadows. Used for rendering lower-resolution shadows.
*/
get shadowRenderSizeFactor() {
return this._renderSizeFactor;
}
set shadowRenderSizeFactor(value) {
this._renderSizeFactor = Math.max(Math.min(value, 1.0), 0.0);
this._voxelTracingPass.resize(value);
this._spatialBlurPass.resize(value);
this._accumulationPass.resize(value);
this._setPluginParameters();
}
/**
* How dark the voxel shadows appear. 1.0 is full opacity, 0.0 is no shadows.
*/
get voxelShadowOpacity() {
return this._voxelTracingPass?.voxelShadowOpacity;
}
set voxelShadowOpacity(value) {
if (!this._voxelTracingPass) {
return;
}
this._voxelTracingPass.voxelShadowOpacity = value;
}
/**
* How dark the screen-space shadows appear. 1.0 is full opacity, 0.0 is no shadows.
*/
get ssShadowOpacity() {
return this._voxelTracingPass?.ssShadowOpacity;
}
set ssShadowOpacity(value) {
if (!this._voxelTracingPass) {
return;
}
this._voxelTracingPass.ssShadowOpacity = value;
}
/**
* The number of samples used in the screen space shadow pass.
*/
get ssShadowSampleCount() {
return this._voxelTracingPass?.sssSamples;
}
set ssShadowSampleCount(value) {
if (!this._voxelTracingPass) {
return;
}
this._voxelTracingPass.sssSamples = value;
}
/**
* The stride of the screen-space shadow pass. This controls the distance between samples
* in pixels.
*/
get ssShadowStride() {
return this._voxelTracingPass?.sssStride;
}
set ssShadowStride(value) {
if (!this._voxelTracingPass) {
return;
}
this._voxelTracingPass.sssStride = value;
}
/**
* A scale for the maximum distance a screen-space shadow can be cast in world-space.
* The maximum distance that screen-space shadows cast is derived from the voxel size
* and this value so shouldn't need to change if you scale your scene
*/
get ssShadowDistanceScale() {
return this._sssMaxDistScale;
}
set ssShadowDistanceScale(value) {
this._sssMaxDistScale = value;
this._updateSsShadowParams();
}
/**
* Screen-space shadow thickness scale. This value controls the assumed thickness of
* on-screen surfaces in world-space. It scales with the size of the shadow-casting
* region so shouldn't need to change if you scale your scene.
*/
get ssShadowThicknessScale() {
return this._sssThicknessScale;
}
set ssShadowThicknessScale(value) {
this._sssThicknessScale = value;
this._updateSsShadowParams();
}
/**
* Returns the texture containing the voxel grid data
* @returns The texture containing the voxel grid data
* @internal
*/
_getVoxelGridTexture() {
const tex = this._voxelRenderer?.getVoxelGrid();
if (tex && tex.isReady()) {
return tex;
}
return this._dummyTexture3d;
}
/**
* Returns the noise texture.
* @returns The noise texture.
* @internal
*/
_getNoiseTexture() {
const tex = this._noiseTexture;
if (tex && tex.isReady()) {
return tex;
}
return this._dummyTexture2d;
}
/**
* Returns the voxel-tracing texture.
* @returns The voxel-tracing texture.
* @internal
*/
_getVoxelTracingTexture() {
const tex = this._voxelTracingPass?.getOutputTexture();
if (tex && tex.isReady()) {
return tex;
}
return this._dummyTexture2d;
}
/**
* Returns the spatial blur texture.
* @returns The spatial blur texture.
* @internal
*/
_getSpatialBlurTexture() {
const tex = this._spatialBlurPass.getOutputTexture();
if (tex && tex.isReady()) {
return tex;
}
return this._dummyTexture2d;
}
/**
* Returns the accumulated shadow texture.
* @returns The accumulated shadow texture.
* @internal
*/
_getAccumulatedTexture() {
const tex = this._accumulationPass?.getOutputTexture();
if (tex && tex.isReady()) {
return tex;
}
return this._dummyTexture2d;
}
/**
* Turn on or off the debug view of the G-Buffer. This will display only the targets
* of the g-buffer that are used by the shadow pipeline.
*/
get gbufferDebugEnabled() {
return this._gbufferDebugEnabled;
}
set gbufferDebugEnabled(enabled) {
if (enabled && !this.allowDebugPasses) {
Logger.Warn("Can't enable G-Buffer debug view without setting allowDebugPasses to true.");
return;
}
this._gbufferDebugEnabled = enabled;
if (enabled) {
this._enableEffect(this._getGBufferDebugPass().name, this.cameras);
}
else {
this._disableEffect(this._getGBufferDebugPass().name, this.cameras);
}
}
/**
* Turn on or off the debug view of the CDF importance sampling data
*/
get cdfDebugEnabled() {
return this.scene.iblCdfGenerator ? this.scene.iblCdfGenerator.debugEnabled : false;
}
/**
* Turn on or off the debug view of the CDF importance sampling data
*/
set cdfDebugEnabled(enabled) {
if (!this.scene.iblCdfGenerator) {
return;
}
if (enabled && !this.allowDebugPasses) {
Logger.Warn("Can't enable importance sampling debug view without setting allowDebugPasses to true.");
return;
}
if (enabled === this.scene.iblCdfGenerator.debugEnabled) {
return;
}
this.scene.iblCdfGenerator.debugEnabled = enabled;
if (enabled) {
this._enableEffect(this.scene.iblCdfGenerator.debugPassName, this.cameras);
}
else {
this._disableEffect(this.scene.iblCdfGenerator.debugPassName, this.cameras);
}
}
/**
* Display the debug view for just the shadow samples taken this frame.
*/
get voxelTracingDebugEnabled() {
return this._voxelTracingPass?.debugEnabled;
}
set voxelTracingDebugEnabled(enabled) {
if (!this._voxelTracingPass) {
return;
}
if (enabled && !this.allowDebugPasses) {
Logger.Warn("Can't enable voxel tracing debug view without setting allowDebugPasses to true.");
return;
}
if (enabled === this._voxelTracingPass.debugEnabled) {
return;
}
this._voxelTracingPass.debugEnabled = enabled;
if (enabled) {
this._enableEffect(this._voxelTracingPass.debugPassName, this.cameras);
}
else {
this._disableEffect(this._voxelTracingPass.debugPassName, this.cameras);
}
}
/**
* Display the debug view for the spatial blur pass
*/
get spatialBlurPassDebugEnabled() {
return this._spatialBlurPass.debugEnabled;
}
set spatialBlurPassDebugEnabled(enabled) {
if (!this._spatialBlurPass) {
return;
}
if (enabled && !this.allowDebugPasses) {
Logger.Warn("Can't enable spatial blur debug view without setting allowDebugPasses to true.");
return;
}
if (enabled === this._spatialBlurPass.debugEnabled) {
return;
}
this._spatialBlurPass.debugEnabled = enabled;
if (enabled) {
this._enableEffect(this._spatialBlurPass.debugPassName, this.cameras);
}
else {
this._disableEffect(this._spatialBlurPass.debugPassName, this.cameras);
}
}
/**
* Display the debug view for the shadows accumulated over time.
*/
get accumulationPassDebugEnabled() {
return this._accumulationPass?.debugEnabled;
}
set accumulationPassDebugEnabled(enabled) {
if (!this._accumulationPass) {
return;
}
if (enabled && !this.allowDebugPasses) {
Logger.Warn("Can't enable accumulation pass debug view without setting allowDebugPasses to true.");
return;
}
if (enabled === this._accumulationPass.debugEnabled) {
return;
}
this._accumulationPass.debugEnabled = enabled;
if (enabled) {
this._enableEffect(this._accumulationPass.debugPassName, this.cameras);
}
else {
this._disableEffect(this._accumulationPass.debugPassName, this.cameras);
}
}
/**
* Add a mesh to be used for shadow-casting in the IBL shadow pipeline.
* These meshes will be written to the voxel grid.
* @param mesh A mesh or list of meshes that you want to cast shadows
*/
addShadowCastingMesh(mesh) {
if (Array.isArray(mesh)) {
for (const m of mesh) {
if (m && this._shadowCastingMeshes.indexOf(m) === -1) {
this._shadowCastingMeshes.push(m);
if (IsGaussianSplattingClassName(m.getClassName())) {
m.needsRotationScaleTextures = true;
}
}
}
}
else {
if (mesh && this._shadowCastingMeshes.indexOf(mesh) === -1) {
this._shadowCastingMeshes.push(mesh);
if (IsGaussianSplattingClassName(mesh.getClassName())) {
mesh.needsRotationScaleTextures = true;
}
}
}
}
/**
* Remove a mesh from the shadow-casting list. The mesh will no longer be written
* to the voxel grid and will not cast shadows.
* @param mesh The mesh or list of meshes that you don't want to cast shadows.
*/
removeShadowCastingMesh(mesh) {
if (Array.isArray(mesh)) {
for (const m of mesh) {
const index = this._shadowCastingMeshes.indexOf(m);
if (index !== -1) {
this._shadowCastingMeshes.splice(index, 1);
if (IsGaussianSplattingClassName(m.getClassName())) {
m.needsRotationScaleTextures = false;
}
}
}
}
else {
const index = this._shadowCastingMeshes.indexOf(mesh);
if (index !== -1) {
this._shadowCastingMeshes.splice(index, 1);
if (IsGaussianSplattingClassName(mesh.getClassName())) {
mesh.needsRotationScaleTextures = false;
}
}
}
}
/**
* Clear the list of shadow-casting meshes. This will remove all meshes from the list
*/
clearShadowCastingMeshes() {
for (const m of this._shadowCastingMeshes) {
if (IsGaussianSplattingClassName(m.getClassName())) {
m.needsRotationScaleTextures = false;
}
}
this._shadowCastingMeshes.length = 0;
}
/**
* The exponent of the resolution of the voxel shadow grid. Higher resolutions will result in sharper
* shadows but are more expensive to compute and require more memory.
* The resolution is calculated as 2 to the power of this number.
*/
get resolutionExp() {
return this._voxelRenderer.voxelResolutionExp;
}
set resolutionExp(newResolution) {
if (newResolution === this._voxelRenderer.voxelResolutionExp) {
return;
}
if (this._voxelRenderer.isVoxelizationInProgress()) {
Logger.Warn("Can't change the resolution of the voxel grid while voxelization is in progress.");
return;
}
this._voxelRenderer.voxelResolutionExp = Math.max(1, Math.min(newResolution, 8));
this._accumulationPass.reset = true;
}
/**
* The number of different directions to sample during the voxel tracing pass
*/
get sampleDirections() {
return this._voxelTracingPass?.sampleDirections;
}
/**
* The number of different directions to sample during the voxel tracing pass
*/
set sampleDirections(value) {
if (!this._voxelTracingPass) {
return;
}
this._voxelTracingPass.sampleDirections = value;
}
/**
* The decree to which the shadows persist between frames. 0.0 is no persistence, 1.0 is full persistence.
**/
get shadowRemanence() {
return this._accumulationPass?.remanence;
}
/**
* The decree to which the shadows persist between frames. 0.0 is no persistence, 1.0 is full persistence.
**/
set shadowRemanence(value) {
if (!this._accumulationPass) {
return;
}
this._accumulationPass.remanence = value;
}
/**
* The global Y-axis rotation of the IBL for shadows. This should match the Y-rotation of the environment map applied to materials, skybox, etc.
*/
get envRotation() {
return this._voxelTracingPass?.envRotation;
}
/**
* The global Y-axis rotation of the IBL for shadows. This should match the Y-rotation of the environment map applied to materials, skybox, etc.
*/
set envRotation(value) {
if (!this._voxelTracingPass) {
return;
}
this._voxelTracingPass.envRotation = value;
this._accumulationPass.reset = true;
}
/**
* Allow debug passes to be enabled. Default is false.
*/
get allowDebugPasses() {
return this._allowDebugPasses;
}
/**
* Allow debug passes to be enabled. Default is false.
*/
set allowDebugPasses(value) {
if (this._allowDebugPasses === value) {
return;
}
this._allowDebugPasses = value;
if (value && this.scene.iblCdfGenerator) {
if (this.scene.iblCdfGenerator.isReady()) {
this._createDebugPasses();
}
else {
this.scene.iblCdfGenerator.onGeneratedObservable.addOnce(() => {
this._createDebugPasses();
});
}
}
else {
this._disposeDebugPasses();
}
}
/**
* Support test.
*/
static get IsSupported() {
const engine = EngineStore.LastCreatedEngine;
if (!engine) {
return false;
}
return engine._features.supportIBLShadows;
}
/**
* Toggle the shadow tracing on or off
* @param enabled Toggle the shadow tracing on or off
*/
toggleShadow(enabled) {
this._enabled = enabled;
this._voxelTracingPass.enabled = enabled;
this._spatialBlurPass.enabled = enabled;
this._accumulationPass.enabled = enabled;
for (const mat of this._materialsWithRenderPlugin) {
if (mat.pluginManager) {
const plugin = mat.pluginManager.getPlugin(IBLShadowsPluginMaterial.Name);
plugin.isEnabled = enabled;
}
}
this._setPluginParameters();
}
/**
* Trigger the scene to be re-voxelized. This should be run when any shadow-casters have been added, removed or moved.
*/
updateVoxelization() {
if (this._shadowCastingMeshes.length === 0) {
Logger.Warn("IBL Shadows: updateVoxelization called with no shadow-casting meshes to voxelize.");
return;
}
this._voxelRenderer.updateVoxelGrid(this._shadowCastingMeshes);
this._voxelRenderer.onVoxelizationCompleteObservable.addOnce(() => {
this.onVoxelizationCompleteObservable.notifyObservers();
});
this._updateSsShadowParams();
}
/**
* Trigger the scene bounds of shadow-casters to be calculated. This is the world size that the voxel grid will cover and will always be a cube.
*/
updateSceneBounds() {
const bounds = {
min: new Vector3(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE),
max: new Vector3(-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE),
};
for (const mesh of this._shadowCastingMeshes) {
const localBounds = mesh.getHierarchyBoundingVectors(true);
bounds.min = Vector3.Minimize(bounds.min, localBounds.min);
bounds.max = Vector3.Maximize(bounds.max, localBounds.max);
}
if (this._shadowCastingMeshes.length === 0) {
Logger.Warn("IBL Shadows: updateSceneBounds called with no shadow-casting meshes.");
this.voxelGridSize = 1.0;
return;
}
// If no visible geometry contributed, bounds stay at sentinel values. Keep the
// last valid invWorldScale and return silently — this is expected when all parts
// are temporarily hidden (e.g. while switching between displayed models).
if (bounds.min.x > bounds.max.x) {
return;
}
const size = bounds.max.subtract(bounds.min);
this.voxelGridSize = Math.max(size.x, size.y, size.z);
if (!isFinite(this.voxelGridSize) || this.voxelGridSize === 0) {
Logger.Warn("IBL Shadows: Scene size is invalid. Can't update bounds.");
this.voxelGridSize = 1.0;
return;
}
const halfSize = this.voxelGridSize / 2.0;
const centre = bounds.max.add(bounds.min).multiplyByFloats(-0.5, -0.5, -0.5);
const invWorldScaleMatrix = Matrix.Compose(new Vector3(1.0 / halfSize, 1.0 / halfSize, 1.0 / halfSize), new Quaternion(), new Vector3(0, 0, 0));
const invTranslationMatrix = Matrix.Compose(new Vector3(1.0, 1.0, 1.0), new Quaternion(), centre);
invTranslationMatrix.multiplyToRef(invWorldScaleMatrix, invWorldScaleMatrix);
this._voxelTracingPass.setWorldScaleMatrix(invWorldScaleMatrix);
this._voxelRenderer.setWorldScaleMatrix(invWorldScaleMatrix);
// Set world scale for spatial blur.
this._spatialBlurPass.setWorldScale(halfSize * 2.0);
this._updateSsShadowParams();
}
/**
* @param name The rendering pipeline name
* @param scene The scene linked to this pipeline
* @param options Options to configure the pipeline
* @param cameras Cameras to apply the pipeline to.
*/
constructor(name, scene, options = {}, cameras) {
super(scene.getEngine(), name);
this._allowDebugPasses = false;
this._debugPasses = [];
this._shadowCastingMeshes = [];
this._shadowOpacity = 0.8;
this._enabled = true;
this._coloredShadows = false;
this._materialsWithRenderPlugin = [];
/**
* Observable that triggers when the shadow renderer is ready
*/
this.onShadowTextureReadyObservable = new Observable();
/**
* Observable that triggers when a new IBL is set and the importance sampling is ready
*/
this.onNewIblReadyObservable = new Observable();
/**
* Observable that triggers when the voxelization is complete
*/
this.onVoxelizationCompleteObservable = new Observable();
/**
* The current world-space size of that the voxel grid covers in the scene.
*/
this.voxelGridSize = 1.0;
this._renderSizeFactor = 1.0;
this._gbufferDebugEnabled = false;
this._gBufferDebugSizeParams = new Vector4(0.0, 0.0, 0.0, 0.0);
this.scene = scene;
RegisterGeometryBufferRendererSceneComponent(GeometryBufferRenderer);
RegisterIblCdfGeneratorSceneComponent(IblCdfGenerator);
this._cameras = cameras || [scene.activeCamera];
// Create the dummy textures to be used when the pipeline is not ready
const blackPixels = new Uint8Array([0, 0, 0, 255]);
this._dummyTexture2d = new RawTexture(blackPixels, 1, 1, Constants.TEXTUREFORMAT_RGBA, scene, false);
this._dummyTexture3d = new RawTexture3D(blackPixels, 1, 1, 1, Constants.TEXTUREFORMAT_RGBA, scene, false);
// Setup the geometry buffer target formats
const textureTypesAndFormats = {};
textureTypesAndFormats[GeometryBufferRenderer.SCREENSPACE_DEPTH_TEXTURE_TYPE] = {
textureFormat: Constants.TEXTUREFORMAT_R,
textureType: Constants.TEXTURETYPE_FLOAT,
samplingMode: Constants.TEXTURE_NEAREST_SAMPLINGMODE,
};
textureTypesAndFormats[GeometryBufferRenderer.VELOCITY_LINEAR_TEXTURE_TYPE] = {
textureFormat: Constants.TEXTUREFORMAT_RG,
textureType: Constants.TEXTURETYPE_HALF_FLOAT,
samplingMode: Constants.TEXTURE_NEAREST_SAMPLINGMODE,
};
textureTypesAndFormats[GeometryBufferRenderer.POSITION_TEXTURE_TYPE] = {
textureFormat: Constants.TEXTUREFORMAT_RGBA,
textureType: Constants.TEXTURETYPE_HALF_FLOAT,
samplingMode: Constants.TEXTURE_NEAREST_SAMPLINGMODE,
};
textureTypesAndFormats[GeometryBufferRenderer.NORMAL_TEXTURE_TYPE] = {
textureFormat: Constants.TEXTUREFORMAT_RGBA,
textureType: Constants.TEXTURETYPE_HALF_FLOAT,
samplingMode: Constants.TEXTURE_NEAREST_SAMPLINGMODE,
};
const geometryBufferRenderer = scene.enableGeometryBufferRenderer(undefined, Constants.TEXTUREFORMAT_DEPTH32_FLOAT, textureTypesAndFormats);
if (!geometryBufferRenderer) {
Logger.Error("Geometry buffer renderer is required for IBL shadows to work.");
return;
}
this._geometryBufferRenderer = geometryBufferRenderer;
this._geometryBufferRenderer.enableScreenspaceDepth = true;
this._geometryBufferRenderer.enableVelocityLinear = true;
this._geometryBufferRenderer.enablePosition = true;
this._geometryBufferRenderer.enableNormal = true;
this._geometryBufferRenderer.generateNormalsInWorldSpace = true;
this.scene.enableIblCdfGenerator();
this.shadowOpacity = options.shadowOpacity ?? 0.8;
this._voxelRenderer = new _IblShadowsVoxelRenderer(this.scene, this, options ? options.resolutionExp : 6, options.triPlanarVoxelization !== undefined ? options.triPlanarVoxelization : true);
this._voxelTracingPass = new _IblShadowsVoxelTracingPass(this.scene, this);
this._spatialBlurPass = new _IblShadowsSpatialBlurPass(this.scene, this);
this._accumulationPass = new _IblShadowsAccumulationPass(this.scene, this);
this._accumulationPass.onReadyObservable.addOnce(() => {
this.onShadowTextureReadyObservable.notifyObservers();
});
this.sampleDirections = options.sampleDirections || 2;
this.voxelShadowOpacity = options.voxelShadowOpacity ?? 1.0;
this.envRotation = options.envRotation ?? 0.0;
this.shadowRenderSizeFactor = options.shadowRenderSizeFactor || 1.0;
this.ssShadowOpacity = options.ssShadowsEnabled === undefined || options.ssShadowsEnabled ? 1.0 : 0.0;
this.ssShadowDistanceScale = options.ssShadowDistanceScale || 1.25;
this.ssShadowSampleCount = options.ssShadowSampleCount || 16;
this.ssShadowStride = options.ssShadowStride || 8;
this.ssShadowThicknessScale = options.ssShadowThicknessScale || 1.0;
this.shadowRemanence = options.shadowRemanence ?? 0.75;
this._noiseTexture = new Texture(Tools.GetAssetUrl("https://assets.babylonjs.com/core/blue_noise/blue_noise_rgb.png"), this.scene, false, true, Constants.TEXTURE_NEAREST_SAMPLINGMODE);
scene.postProcessRenderPipelineManager.addPipeline(this);
this.scene.onActiveCameraChanged.add(this._listenForCameraChanges.bind(this));
this.scene.onBeforeRenderObservable.add(this._updateBeforeRender.bind(this));
this._listenForCameraChanges();
this.scene.getEngine().onResizeObservable.add(this._handleResize.bind(this));
// Assigning the shadow texture to the materials needs to be done after the RT's are created.
if (this.scene.iblCdfGenerator) {
this.scene.iblCdfGenerator.onGeneratedObservable.add(() => {
this._setPluginParameters();
this.onNewIblReadyObservable.notifyObservers();
});
}
}
_handleResize() {
this._voxelRenderer.resize();
this._voxelTracingPass.resize(this.shadowRenderSizeFactor);
this._spatialBlurPass.resize(this.shadowRenderSizeFactor);
this._accumulationPass.resize(this.shadowRenderSizeFactor);
this._setPluginParameters();
}
_getGBufferDebugPass() {
if (this._gbufferDebugPass) {
return this._gbufferDebugPass;
}
const isWebGPU = this.engine.isWebGPU;
const textureNames = ["depthSampler", "normalSampler", "positionSampler", "velocitySampler"];
const options = {
width: this.scene.getEngine().getRenderWidth(),
height: this.scene.getEngine().getRenderHeight(),
samplingMode: Constants.TEXTURE_NEAREST_SAMPLINGMODE,
engine: this.scene.getEngine(),
textureType: Constants.TEXTURETYPE_UNSIGNED_BYTE,
textureFormat: Constants.TEXTUREFORMAT_RGBA,
uniforms: ["sizeParams"],
samplers: textureNames,
reusable: false,
shaderLanguage: isWebGPU ? 1 /* ShaderLanguage.WGSL */ : 0 /* ShaderLanguage.GLSL */,
extraInitializations: (useWebGPU, list) => {
if (useWebGPU) {
list.push(import('./iblShadowGBufferDebug.fragment-Bx1F4RDG.esm.js'));
}
else {
list.push(import('./iblShadowGBufferDebug.fragment-CiINH-c7.esm.js'));
}
},
};
this._gbufferDebugPass = new PostProcess("iblShadowGBufferDebug", "iblShadowGBufferDebug", options);
if (this.engine.isWebGPU) {
this._gbufferDebugPass.samples = this.engine.currentSampleCount ?? 1;
}
this._gbufferDebugPass.autoClear = false;
this._gbufferDebugPass.onApplyObservable.add((effect) => {
const depthIndex = this._geometryBufferRenderer.getTextureIndex(GeometryBufferRenderer.SCREENSPACE_DEPTH_TEXTURE_TYPE);
effect.setTexture("depthSampler", this._geometryBufferRenderer.getGBuffer().textures[depthIndex]);
const normalIndex = this._geometryBufferRenderer.getTextureIndex(GeometryBufferRenderer.NORMAL_TEXTURE_TYPE);
effect.setTexture("normalSampler", this._geometryBufferRenderer.getGBuffer().textures[normalIndex]);
const positionIndex = this._geometryBufferRenderer.getTextureIndex(GeometryBufferRenderer.POSITION_TEXTURE_TYPE);
effect.setTexture("positionSampler", this._geometryBufferRenderer.getGBuffer().textures[positionIndex]);
const velocityIndex = this._geometryBufferRenderer.getTextureIndex(GeometryBufferRenderer.VELOCITY_LINEAR_TEXTURE_TYPE);
effect.setTexture("velocitySampler", this._geometryBufferRenderer.getGBuffer().textures[velocityIndex]);
effect.setVector4("sizeParams", this._gBufferDebugSizeParams);
if (this.scene.activeCamera) {
effect.setFloat("maxDepth", this.scene.activeCamera.maxZ);
}
});
return this._gbufferDebugPass;
}
_createDebugPasses() {
if (this.scene.iblCdfGenerator) {
this._debugPasses = [{ pass: this.scene.iblCdfGenerator.getDebugPassPP(), enabled: this.cdfDebugEnabled }];
}
else {
this._debugPasses = [];
}
this._debugPasses.push({ pass: this._voxelTracingPass.getDebugPassPP(), enabled: this.voxelTracingDebugEnabled }, { pass: this._spatialBlurPass.getDebugPassPP(), enabled: this.spatialBlurPassDebugEnabled }, { pass: this._accumulationPass.getDebugPassPP(), enabled: this.accumulationPassDebugEnabled }, { pass: this._getGBufferDebugPass(), enabled: this.gbufferDebugEnabled });
for (let i = 0; i < this._debugPasses.length; i++) {
if (!this._debugPasses[i].pass) {
continue;
}
this.addEffect(new PostProcessRenderEffect(this.scene.getEngine(), this._debugPasses[i].pass.name, () => {
return this._debugPasses[i].pass;
}, true));
}
const cameras = this.cameras.slice();
this.scene.postProcessRenderPipelineManager.detachCamerasFromRenderPipeline(this.name, this.cameras);
this.scene.postProcessRenderPipelineManager.attachCamerasToRenderPipeline(this.name, cameras);
for (let i = 0; i < this._debugPasses.length; i++) {
if (!this._debugPasses[i].pass) {
continue;
}
if (this._debugPasses[i].enabled) {
this._enableEffect(this._debugPasses[i].pass.name, this.cameras);
}
else {
this._disableEffect(this._debugPasses[i].pass.name, this.cameras);
}
}
}
_disposeEffectPasses() {
this.scene.postProcessRenderPipelineManager.detachCamerasFromRenderPipeline(this.name, this.cameras);
this._disposeDebugPasses();
this._reset();
}
_disposeDebugPasses() {
for (let i = 0; i < this._debugPasses.length; i++) {
this._disableEffect(this._debugPasses[i].pass.name, this.cameras);
this._debugPasses[i].pass.dispose();
}
this._debugPasses = [];
}
_updateDebugPasses() {
let count = 0;
if (this._gbufferDebugEnabled) {
count++;
}
if (this.cdfDebugEnabled) {
count++;
}
if (this.voxelTracingDebugEnabled) {
count++;
}
if (this.spatialBlurPassDebugEnabled) {
count++;
}
if (this.accumulationPassDebugEnabled) {
count++;
}
const rows = Math.ceil(Math.sqrt(count));
const cols = Math.ceil(count / rows);
const width = 1.0 / cols;
const height = 1.0 / rows;
let x = 0;
let y = 0;
if (this.gbufferDebugEnabled) {
this._gBufferDebugSizeParams.set(x, y, cols, rows);
x -= width;
if (x <= -1) {
x = 0;
y -= height;
}
}
if (this.cdfDebugEnabled && this.scene.iblCdfGenerator) {
this.scene.iblCdfGenerator.setDebugDisplayParams(x, y, cols, rows);
x -= width;
if (x <= -1) {
x = 0;
y -= height;
}
}
if (this.voxelTracingDebugEnabled) {
this._voxelTracingPass.setDebugDisplayParams(x, y, cols, rows);
x -= width;
if (x <= -1) {
x = 0;
y -= height;
}
}
if (this.spatialBlurPassDebugEnabled) {
this._spatialBlurPass.setDebugDisplayParams(x, y, cols, rows);
x -= width;
if (x <= -1) {
x = 0;
y -= height;
}
}
if (this.accumulationPassDebugEnabled) {
this._accumulationPass.setDebugDisplayParams(x, y, cols, rows);
}
}
/**
* Update the SS shadow max distance and thickness based on the voxel grid size and resolution.
* The max distance should be just a little larger than the world size of a single voxel.
*/
_updateSsShadowParams() {
this._voxelTracingPass.sssMaxDist = (this._sssMaxDistScale * this.voxelGridSize) / (1 << this.resolutionExp);
this._voxelTracingPass.sssThickness = this._sssThicknessScale * 0.005 * this.voxelGridSize;
}
/**
* Apply the shadows to a material or array of materials. If no material is provided, all
* materials in the scene will be added.
* @param material Material that will be affected by the shadows. If not provided, all materials of the scene will be affected.
*/
addShadowReceivingMaterial(material) {
if (material) {
if (Array.isArray(material)) {
for (const m of material) {
this._addShadowSupportToMaterial(m);
}
}
else {
this._addShadowSupportToMaterial(material);
}
}
else {
for (const mat of this.scene.materials) {
this._addShadowSupportToMaterial(mat);
}
}
}
/**
* Remove a material from the list of materials that receive shadows. If no material
* is provided, all materials in the scene will be removed.
* @param material The material or array of materials that will no longer receive shadows
*/
removeShadowReceivingMaterial(material) {
if (Array.isArray(material)) {
for (const m of material) {
const matIndex = this._materialsWithRenderPlugin.indexOf(m);
if (matIndex !== -1) {
this._materialsWithRenderPlugin.splice(matIndex, 1);
const plugin = m.pluginManager?.getPlugin(IBLShadowsPluginMaterial.Name);
plugin.isEnabled = false;
}
}
}
else {
const matIndex = this._materialsWithRenderPlugin.indexOf(material);
if (matIndex !== -1) {
this._materialsWithRenderPlugin.splice(matIndex, 1);
const plugin = material.pluginManager.getPlugin(IBLShadowsPluginMaterial.Name);
plugin.isEnabled = false;
}
}
}
/**
* Clear the list of materials that receive shadows. This will remove all materials from the list
*/
clearShadowReceivingMaterials() {
for (const mat of this._materialsWithRenderPlugin) {
const plugin = mat.pluginManager?.getPlugin(IBLShadowsPluginMaterial.Name);
if (plugin) {
plugin.isEnabled = false;
}
}
this._materialsWithRenderPlugin.length = 0;
}
_addShadowSupportToMaterial(material) {
if (!(material instanceof PBRBaseMaterial) && !(material instanceof StandardMaterial) && !(material instanceof OpenPBRMaterial)) {
return;
}
let plugin = material.pluginManager?.getPlugin(IBLShadowsPluginMaterial.Name);
if (!plugin) {
plugin = new IBLShadowsPluginMaterial(material);
}
if (this._materialsWithRenderPlugin.indexOf(material) !== -1) {
return;
}
if (this._enabled) {
plugin.iblShadowsTexture = this._getAccumulatedTexture().getInternalTexture();
plugin.shadowOpacity = this.shadowOpacity;
}
plugin.isEnabled = this._enabled;
plugin.isColored = this._coloredShadows;
this._materialsWithRenderPlugin.push(material);
}
_setPluginParameters() {
if (!this._enabled) {
return;
}
for (const mat of this._materialsWithRenderPlugin) {
if (mat.pluginManager) {
const plugin = mat.pluginManager.getPlugin(IBLShadowsPluginMaterial.Name);
plugin.iblShadowsTexture = this._getAccumulatedTexture().getInternalTexture();
plugin.shadowOpacity = this.shadowOpacity;
plugin.isColored = this._coloredShadows;
}
}
}
_updateBeforeRender() {
this._updateDebugPasses();
}
_listenForCameraChanges() {
// We want to listen for camera changes and change settings while the camera is moving.
this.scene.activeCamera?.onViewMatrixChangedObservable.add(() => {
this._accumulationPass.isMoving = true;
});
}
/**
* Checks if the IBL shadow pipeline is ready to render shadows
* @returns true if the IBL shadow pipeline is ready to render the shadows
*/
isReady() {
return (this._noiseTexture.isReady() &&
this._voxelRenderer.isReady() &&
this.scene.iblCdfGenerator &&
this.scene.iblCdfGenerator.isReady() &&
(!this._voxelTracingPass || this._voxelTracingPass.isReady()) &&
(!this._spatialBlurPass || this._spatialBlurPass.isReady()) &&
(!this._accumulationPass || this._accumulationPass.isReady()));
}
/**
* Get the class name
* @returns "IBLShadowsRenderPipeline"
*/
getClassName() {
return "IBLShadowsRenderPipeline";
}
/**
* Disposes the IBL shadow pipeline and associated resources
*/
dispose() {
const materials = this._materialsWithRenderPlugin.splice(0);
for (const mat of materials) {
this.removeShadowReceivingMaterial(mat);
}
this._disposeEffectPasses();
this._noiseTexture.dispose();
this._voxelRenderer.dispose();
this._voxelTracingPass.dispose();
this._spatialBlurPass.dispose();
this._accumulationPass.dispose();
this._dummyTexture2d.dispose();
this._dummyTexture3d.dispose();
this.onNewIblReadyObservable.clear();
this.onShadowTextureReadyObservable.clear();
this.onVoxelizationCompleteObservable.clear();
super.dispose();
}
}
export { IblShadowsRenderPipeline };
//# sourceMappingURL=iblShadowsRenderPipeline-CG-YhfMG.esm.js.map