babylonjs-editor-tools 5.5.0 → 5.5.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/build/index.node.js +2330 -1
  2. package/build/src/index.js +4 -0
  3. package/build/src/index.js.map +1 -1
  4. package/build/src/loading/loader.js +7 -2
  5. package/build/src/loading/loader.js.map +1 -1
  6. package/build/src/loading/mesh.js +22 -0
  7. package/build/src/loading/mesh.js.map +1 -0
  8. package/build/src/loading/rendering.js +4 -0
  9. package/build/src/loading/rendering.js.map +1 -1
  10. package/build/src/loading/transform-node.js +4 -1
  11. package/build/src/loading/transform-node.js.map +1 -1
  12. package/build/src/rendering/tools.js +7 -0
  13. package/build/src/rendering/tools.js.map +1 -1
  14. package/build/src/rendering/volumetric/pipeline.js +915 -0
  15. package/build/src/rendering/volumetric/pipeline.js.map +1 -0
  16. package/build/src/rendering/volumetric/selector.js +525 -0
  17. package/build/src/rendering/volumetric/selector.js.map +1 -0
  18. package/build/src/rendering/volumetric/shaders-wgsl.js +1207 -0
  19. package/build/src/rendering/volumetric/shaders-wgsl.js.map +1 -0
  20. package/build/src/rendering/volumetric/shaders.js +1293 -0
  21. package/build/src/rendering/volumetric/shaders.js.map +1 -0
  22. package/build/src/rendering/volumetric/types.js +301 -0
  23. package/build/src/rendering/volumetric/types.js.map +1 -0
  24. package/build/src/rendering/volumetric-lighting.js +72 -0
  25. package/build/src/rendering/volumetric-lighting.js.map +1 -0
  26. package/build/src/tools/guards.js +14 -0
  27. package/build/src/tools/guards.js.map +1 -1
  28. package/declaration/src/index.d.ts +4 -0
  29. package/declaration/src/loading/mesh.d.ts +1 -0
  30. package/declaration/src/rendering/tools.d.ts +1 -0
  31. package/declaration/src/rendering/volumetric/pipeline.d.ts +231 -0
  32. package/declaration/src/rendering/volumetric/selector.d.ts +263 -0
  33. package/declaration/src/rendering/volumetric/shaders-wgsl.d.ts +21 -0
  34. package/declaration/src/rendering/volumetric/shaders.d.ts +60 -0
  35. package/declaration/src/rendering/volumetric/types.d.ts +362 -0
  36. package/declaration/src/rendering/volumetric-lighting.d.ts +28 -0
  37. package/declaration/src/tools/guards.d.ts +12 -0
  38. package/package.json +1 -1
@@ -0,0 +1,915 @@
1
+ import { Camera } from "@babylonjs/core/Cameras/camera";
2
+ import { Color3 } from "@babylonjs/core/Maths/math.color";
3
+ import { Constants } from "@babylonjs/core/Engines/constants";
4
+ import { Vector3, Matrix } from "@babylonjs/core/Maths/math.vector";
5
+ import { Texture } from "@babylonjs/core/Materials/Textures/texture";
6
+ import { PostProcess } from "@babylonjs/core/PostProcesses/postProcess";
7
+ import { ShaderLanguage } from "@babylonjs/core/Materials/shaderLanguage";
8
+ import { PostProcessRenderEffect } from "@babylonjs/core/PostProcesses/RenderPipeline/postProcessRenderEffect";
9
+ import { PostProcessRenderPipeline } from "@babylonjs/core/PostProcesses/RenderPipeline/postProcessRenderPipeline";
10
+ import { isDirectionalLight, isSpotLight } from "../../tools/guards";
11
+ import { VolumetricDitherMode, VolumetricFogMode, getDefaultVolumetricLightingConfiguration, getVolumetricLightSteps, maxVolumetricArrayLights, maxVolumetricShadowSlots, normalizeVolumetricLightingConfiguration, } from "./types";
12
+ import { VolumetricShadowKind, VolumetricTransmittanceMode, computeVolumetricBudget, getVolumetricLightRange, getVolumetricLightWorldDirection, getVolumetricLightWorldPosition, selectVolumetricLights, } from "./selector";
13
+ import { registerVolumetricLightingShaders, volumetricLightingBlurShaderName, volumetricLightingComposeShaderName, volumetricLightingLinearDepthShaderName, volumetricLightingScatteringShaderName, } from "./shaders";
14
+ const leftHandedForward = new Vector3(0, 0, 1);
15
+ const rightHandedForward = new Vector3(0, 0, -1);
16
+ /**
17
+ * Defines the keys of the configuration that change the shape of the chain of post-processes and, as a
18
+ * result, require the render effects of the pipeline to be rebuilt instead of simply rebound.
19
+ */
20
+ const structuralConfigurationKeys = ["resolutionScale", "blurPasses"];
21
+ /**
22
+ * Defines the number of samples of the optical depth integrated per pixel when the medium has no closed form.
23
+ */
24
+ const transmittanceSamples = 32;
25
+ /**
26
+ * Defines the list of every uniform the raymarching shader may declare. Listing a uniform that the compiled
27
+ * program optimized away is safe, Babylon.js resolves it to a null location and the setters become no-ops.
28
+ */
29
+ const scatteringUniforms = [
30
+ "volInverseViewProjection",
31
+ "volViewProjection",
32
+ "volCameraPosition",
33
+ "volCameraForward",
34
+ "volCameraMinMaxZ",
35
+ "volDepthRatio",
36
+ "volFrameIndex",
37
+ "volFogInfos",
38
+ "volFogColor",
39
+ "volLinearFogEps",
40
+ "volMedium",
41
+ "volScreenShadowParams",
42
+ "volAmbient",
43
+ "volParams",
44
+ "volLightData",
45
+ "volLightDiffuse",
46
+ "volLightDirection",
47
+ "volLightFalloff",
48
+ "volShadowLightData",
49
+ "volShadowLightDiffuse",
50
+ "volShadowLightDirection",
51
+ "volShadowLightFalloff",
52
+ "volShadowInfo",
53
+ "volShadowMatrix",
54
+ "volCsmLightData",
55
+ "volCsmLightDiffuse",
56
+ "volCsmLightDirection",
57
+ "volCsmLightFalloff",
58
+ "volCsmInfo",
59
+ "volCsmMatrices",
60
+ ];
61
+ const scatteringSamplers = ["volCsmSampler"];
62
+ for (let i = 0; i < maxVolumetricShadowSlots; ++i) {
63
+ scatteringSamplers.push(`volShadowSampler${i}`);
64
+ }
65
+ /**
66
+ * Defines a rendering pipeline computing single-scattering volumetric lighting: the light shafts are
67
+ * integrated through a participating medium the pipeline describes on its own, and are occluded either by
68
+ * the shadow map a light already renders or, when it has none, by the depth buffer of the scene.
69
+ *
70
+ * Every light of the scene can take part in the effect, including the lights that live inside a clustered
71
+ * light container, and each one is configured individually through "light.metadata.volumetricLighting".
72
+ *
73
+ * The cost of the effect scales with what is on screen rather than with the number of lights: a directional
74
+ * light is marched along the whole view ray, but a point or a spot light is only integrated over the part
75
+ * of the view ray crossing its volume, so a light costs nothing to the pixels whose view ray misses it.
76
+ *
77
+ * The pipeline is composed of four passes:
78
+ * - a pass keeping the color of the scene untouched and extracting its linear depth, which every later pass
79
+ * reads instead of the depth map,
80
+ * - the raymarching pass, rendered at a fraction of the resolution of the canvas,
81
+ * - a separable depth aware blur denoising the result of the raymarching,
82
+ * - a composition pass upsampling the result and adding it to the color of the scene.
83
+ */
84
+ export class VolumetricLightingRenderingPipeline extends PostProcessRenderPipeline {
85
+ /**
86
+ * Defines the name of the render effect keeping the color of the scene and extracting its linear depth.
87
+ */
88
+ static SceneColorEffectName = "VolumetricLightingSceneColorEffect";
89
+ /**
90
+ * Defines the name of the render effect raymarching the participating medium.
91
+ */
92
+ static ScatteringEffectName = "VolumetricLightingScatteringEffect";
93
+ /**
94
+ * Defines the name of the render effect denoising the result of the raymarching.
95
+ */
96
+ static BlurEffectName = "VolumetricLightingBlurEffect";
97
+ /**
98
+ * Defines the name of the render effect composing the light shafts over the color of the scene.
99
+ */
100
+ static ComposeEffectName = "VolumetricLightingComposeEffect";
101
+ /**
102
+ * Returns the distance at which the medium becomes fully opaque to use as a default for the linear mode.
103
+ * @param scene defines the reference to the scene to measure.
104
+ * @param camera defines the reference to the camera, used when the scene has no geometry yet.
105
+ */
106
+ static GetDefaultFogEnd(scene, camera) {
107
+ return 1 / VolumetricLightingRenderingPipeline.GetDefaultFogDensity(scene, camera);
108
+ }
109
+ /**
110
+ * Returns the extinction coefficient to use as a default for the medium of the pipeline.
111
+ *
112
+ * There is no unit agnostic constant that works here: an extinction of 1 per unit is a thick smoke in a
113
+ * scene authored in metres and a vacuum in one authored in centimetres. Deriving it from the size of the
114
+ * scene gives an optical depth of about 1 from one end of the content to the other, which reads well
115
+ * whatever the project is authored in.
116
+ * @param scene defines the reference to the scene to measure.
117
+ * @param camera defines the reference to the camera, used when the scene has no geometry yet.
118
+ */
119
+ static GetDefaultFogDensity(scene, camera) {
120
+ let size = 0;
121
+ if (scene.meshes.length) {
122
+ const extends_ = scene.getWorldExtends();
123
+ const diagonal = extends_.max.subtract(extends_.min).length();
124
+ if (isFinite(diagonal)) {
125
+ size = diagonal;
126
+ }
127
+ }
128
+ // An empty scene, or one made only of infinitely small meshes, falls back on how far the camera is
129
+ // from the origin. The far plane is useless here: the default camera of the editor sets it to
130
+ // 1 500 000, which would describe a medium thinner than the cutoff of the raymarching shader.
131
+ if (size < 1e-3) {
132
+ size = Math.max(camera.globalPosition.length(), camera.maxZ * 0.001, 1);
133
+ }
134
+ // Kept comfortably above the "sigmaT > 1e-6" cutoff of the shader, under which nothing scatters.
135
+ return Math.min(1, Math.max(1e-5, 1 / Math.max(1e-3, size)));
136
+ }
137
+ /**
138
+ * Returns wether or not the volumetric lighting rendering pipeline is supported by the given engine.
139
+ * The raymarching shader relies on shadow samplers, texture arrays and texel fetches, which need WebGL 2,
140
+ * and ships as both GLSL and hand written WGSL so WebGPU is supported without an external transpiler.
141
+ * @param engine defines the reference to the engine to check.
142
+ */
143
+ static IsSupported(engine) {
144
+ if (engine.isWebGPU) {
145
+ return true;
146
+ }
147
+ return (engine.webGLVersion ?? 0) >= 2;
148
+ }
149
+ /**
150
+ * Returns the language the shaders of the pipeline must be written in for the given engine.
151
+ * @param engine defines the reference to the engine to check.
152
+ */
153
+ static GetShaderLanguage(engine) {
154
+ return engine.isWebGPU ? ShaderLanguage.WGSL : ShaderLanguage.GLSL;
155
+ }
156
+ _scene;
157
+ _camera;
158
+ _configuration;
159
+ _configurationProxy;
160
+ _textureType;
161
+ _shaderLanguage;
162
+ _ldrEncode;
163
+ _linearDepthPacked;
164
+ _depthRenderer = null;
165
+ _ownsDepthRenderer = false;
166
+ _depthMode = 0;
167
+ _linearDepthPostProcess = null;
168
+ _scatteringPostProcess = null;
169
+ _blurPostProcesses = [];
170
+ _composePostProcess = null;
171
+ _selection = null;
172
+ _budget;
173
+ _linearDepthDefines = "";
174
+ _scatteringDefines = "";
175
+ _blurDefines = "";
176
+ _composeDefines = "";
177
+ _frameCount = 0;
178
+ _lightsDirty = true;
179
+ _disposed = false;
180
+ _beforeCameraRenderObserver = null;
181
+ _lightAddedObserver = null;
182
+ _lightRemovedObserver = null;
183
+ _inverseViewProjection = Matrix.Identity();
184
+ _temporaryVector = new Vector3();
185
+ _temporaryColor = new Color3();
186
+ _lightData = new Float32Array(maxVolumetricArrayLights * 4);
187
+ _lightDiffuse = new Float32Array(maxVolumetricArrayLights * 4);
188
+ _lightDirection = new Float32Array(maxVolumetricArrayLights * 4);
189
+ _lightFalloff = new Float32Array(maxVolumetricArrayLights * 4);
190
+ _shadowLightData = new Float32Array(maxVolumetricShadowSlots * 4);
191
+ _shadowLightDiffuse = new Float32Array(maxVolumetricShadowSlots * 4);
192
+ _shadowLightDirection = new Float32Array(maxVolumetricShadowSlots * 4);
193
+ _shadowLightFalloff = new Float32Array(maxVolumetricShadowSlots * 4);
194
+ _shadowInfo = new Float32Array(maxVolumetricShadowSlots * 4);
195
+ _shadowMatrices = new Float32Array(maxVolumetricShadowSlots * 16);
196
+ _csmLightData = new Float32Array(4);
197
+ _csmLightDiffuse = new Float32Array(4);
198
+ _csmLightDirection = new Float32Array(4);
199
+ _csmLightFalloff = new Float32Array(4);
200
+ _csmMatrices = new Float32Array(4 * 16);
201
+ /**
202
+ * Constructor.
203
+ * @param name defines the name of the pipeline.
204
+ * @param scene defines the reference to the scene the pipeline belongs to.
205
+ * @param camera defines the reference to the camera the pipeline is attached to.
206
+ * @param configuration defines the optional configuration to apply on the pipeline.
207
+ */
208
+ constructor(name, scene, camera, configuration) {
209
+ super(scene.getEngine(), name);
210
+ this._shaderLanguage = VolumetricLightingRenderingPipeline.GetShaderLanguage(scene.getEngine());
211
+ registerVolumetricLightingShaders(this._shaderLanguage);
212
+ this._scene = scene;
213
+ this._camera = camera;
214
+ this._configuration = normalizeVolumetricLightingConfiguration({
215
+ ...getDefaultVolumetricLightingConfiguration(),
216
+ fogDensity: configuration?.fogDensity ?? VolumetricLightingRenderingPipeline.GetDefaultFogDensity(scene, camera),
217
+ fogEnd: configuration?.fogEnd ?? VolumetricLightingRenderingPipeline.GetDefaultFogEnd(scene, camera),
218
+ screenSpaceShadowMaxDistance: configuration?.screenSpaceShadowMaxDistance ?? VolumetricLightingRenderingPipeline.GetDefaultFogEnd(scene, camera),
219
+ ...configuration,
220
+ });
221
+ this._configurationProxy = new Proxy(this._configuration, {
222
+ set: (target, property, value) => {
223
+ const key = property;
224
+ if (target[key] === value) {
225
+ return true;
226
+ }
227
+ target[key] = value;
228
+ this._onConfigurationChanged(key);
229
+ return true;
230
+ },
231
+ });
232
+ const engine = scene.getEngine();
233
+ const caps = engine.getCaps();
234
+ const supportsHalfFloat = caps.textureHalfFloatRender && caps.textureHalfFloatLinearFiltering;
235
+ this._textureType = supportsHalfFloat ? Constants.TEXTURETYPE_HALF_FLOAT : Constants.TEXTURETYPE_UNSIGNED_BYTE;
236
+ this._ldrEncode = !supportsHalfFloat;
237
+ // The linear depth is read with texel fetches only, so a 32 bits float texture needs no filtering
238
+ // support. Without float render targets it is packed in an 8 bits RGBA texture instead.
239
+ this._linearDepthPacked = !caps.textureFloatRender;
240
+ this._budget = computeVolumetricBudget(engine, this._configuration);
241
+ this._setupDepthRenderer();
242
+ this._buildRenderEffects();
243
+ scene.postProcessRenderPipelineManager.addPipeline(this);
244
+ scene.postProcessRenderPipelineManager.attachCamerasToRenderPipeline(this.name, camera);
245
+ this._beforeCameraRenderObserver = scene.onBeforeCameraRenderObservable.add((renderedCamera) => {
246
+ if (renderedCamera === this._camera) {
247
+ this._updateSelection();
248
+ }
249
+ });
250
+ this._lightAddedObserver = scene.onNewLightAddedObservable.add(() => this.markLightsDirty());
251
+ this._lightRemovedObserver = scene.onLightRemovedObservable.add(() => this.markLightsDirty());
252
+ }
253
+ /**
254
+ * Gets wether or not the pipeline was disposed. Babylon.js disposes a rendering pipeline on its own when
255
+ * one of its post-processes fails to compile, so this is not always the result of an explicit call.
256
+ */
257
+ get isDisposed() {
258
+ return this._disposed;
259
+ }
260
+ /**
261
+ * Gets the reference to the scene the pipeline belongs to.
262
+ */
263
+ get scene() {
264
+ return this._scene;
265
+ }
266
+ /**
267
+ * Gets the reference to the camera the pipeline is attached to.
268
+ */
269
+ get camera() {
270
+ return this._camera;
271
+ }
272
+ /**
273
+ * Gets the live configuration of the pipeline. Assigning a property of the returned object applies the
274
+ * change immediately, rebuilding the chain of post-processes when the change requires it.
275
+ */
276
+ get configuration() {
277
+ return this._configurationProxy;
278
+ }
279
+ /**
280
+ * Gets the statistics of the last selection pass, used by the inspector to explain what each light does.
281
+ */
282
+ getStats() {
283
+ return this._selection?.stats ?? null;
284
+ }
285
+ /**
286
+ * Gets the number of lights the raymarching shader can be compiled for on the current engine.
287
+ */
288
+ getBudget() {
289
+ return this._budget;
290
+ }
291
+ /**
292
+ * Gets a string identifying the name of the class.
293
+ */
294
+ getClassName() {
295
+ return "VolumetricLightingRenderingPipeline";
296
+ }
297
+ /**
298
+ * Marks the list of lights contributing to the effect as dirty, forcing a new selection pass on the
299
+ * next frame. Called when the volumetric configuration of a light is edited in the inspector.
300
+ */
301
+ markLightsDirty() {
302
+ this._lightsDirty = true;
303
+ }
304
+ /**
305
+ * Applies the given configuration on the pipeline.
306
+ * @param data defines the configuration to apply.
307
+ */
308
+ applyConfiguration(data) {
309
+ const normalized = normalizeVolumetricLightingConfiguration({
310
+ ...this._configuration,
311
+ ...data,
312
+ });
313
+ const requiresRebuild = structuralConfigurationKeys.some((key) => normalized[key] !== this._configuration[key]);
314
+ Object.assign(this._configuration, normalized);
315
+ this._budget = computeVolumetricBudget(this._scene.getEngine(), this._configuration);
316
+ this._lightsDirty = true;
317
+ if (requiresRebuild) {
318
+ this._rebuildRenderEffects();
319
+ }
320
+ }
321
+ /**
322
+ * Serializes the configuration of the pipeline.
323
+ */
324
+ serializeConfiguration() {
325
+ return {
326
+ ...this._configuration,
327
+ fogColor: this._configuration.fogColor.slice(),
328
+ ambientColor: this._configuration.ambientColor.slice(),
329
+ };
330
+ }
331
+ /**
332
+ * Disposes the pipeline and all the resources it owns.
333
+ */
334
+ dispose() {
335
+ if (this._disposed) {
336
+ return;
337
+ }
338
+ this._disposed = true;
339
+ this._scene.onBeforeCameraRenderObservable.remove(this._beforeCameraRenderObserver);
340
+ this._scene.onNewLightAddedObservable.remove(this._lightAddedObserver);
341
+ this._scene.onLightRemovedObservable.remove(this._lightRemovedObserver);
342
+ this._beforeCameraRenderObserver = null;
343
+ this._lightAddedObserver = null;
344
+ this._lightRemovedObserver = null;
345
+ const cameras = this._cameras.slice();
346
+ this._scene.postProcessRenderPipelineManager.detachCamerasFromRenderPipeline(this.name, cameras);
347
+ this._disposePostProcesses(cameras);
348
+ if (this._ownsDepthRenderer) {
349
+ this._scene.disableDepthRenderer(this._camera);
350
+ }
351
+ this._depthRenderer = null;
352
+ this._selection = null;
353
+ // Removing the pipeline explicitly, "dispose" alone can leave a stale entry in the manager.
354
+ this._scene.postProcessRenderPipelineManager.removePipeline(this.name);
355
+ super.dispose();
356
+ }
357
+ _onConfigurationChanged(key) {
358
+ if (key === "maxLights" || key === "maxShadowedLights") {
359
+ this._budget = computeVolumetricBudget(this._scene.getEngine(), this._configuration);
360
+ }
361
+ this._lightsDirty = true;
362
+ if (structuralConfigurationKeys.includes(key)) {
363
+ this._rebuildRenderEffects();
364
+ }
365
+ }
366
+ /**
367
+ * Adopts the depth renderer of the camera when one already exists (the SSAO and SSR pipelines create
368
+ * one) instead of paying for a second full depth pass, and detects the format it stores so the shader
369
+ * can be compiled for it.
370
+ */
371
+ _setupDepthRenderer() {
372
+ const existing = this._scene._depthRenderer?.[this._camera.uniqueId];
373
+ this._ownsDepthRenderer = !existing;
374
+ this._depthRenderer = existing ?? this._scene.enableDepthRenderer(this._camera, false, false, Constants.TEXTURE_NEAREST_SAMPLINGMODE, false);
375
+ if (this._depthRenderer.isPacked) {
376
+ this._depthMode = 2;
377
+ }
378
+ else if (this._depthRenderer.clearColor.r === 0) {
379
+ // The depth renderer stores the view space Z, the sky is cleared to 0 instead of 1.
380
+ this._depthMode = 1;
381
+ }
382
+ else {
383
+ this._depthMode = 0;
384
+ }
385
+ }
386
+ /**
387
+ * Binds the two values needed to turn the content of the depth map back into a distance along the view
388
+ * axis. The depth renderer stores "(clipZ + minZ) / (minZ + maxZ)", where "clipZ" is affine in the view
389
+ * space Z for both the perspective and the orthographic projections, so the inverse is affine too and
390
+ * can be derived exactly from the projection matrix instead of being approximated.
391
+ */
392
+ _bindDepthUniforms(effect) {
393
+ const camera = this._camera;
394
+ const engine = this._scene.getEngine();
395
+ effect.setFloat2("volCameraMinMaxZ", camera.minZ, camera.maxZ);
396
+ if (this._depthMode === 1) {
397
+ // The view space Z is stored as is, there is nothing to invert.
398
+ effect.setFloat2("volDepthUnpack", 1, 0);
399
+ return;
400
+ }
401
+ // Same values as "DepthRenderer" binds in its own "depthValues" uniform, including the orthographic
402
+ // branch where it uses constants instead of the near and far planes of the camera.
403
+ const isOrthographic = camera.mode === Camera.ORTHOGRAPHIC_CAMERA;
404
+ const minZ = isOrthographic
405
+ ? !engine.useReverseDepthBuffer && engine.isNDCHalfZRange
406
+ ? 0
407
+ : 1
408
+ : engine.useReverseDepthBuffer && engine.isNDCHalfZRange
409
+ ? camera.minZ
410
+ : engine.isNDCHalfZRange
411
+ ? 0
412
+ : camera.minZ;
413
+ const maxZ = isOrthographic ? (engine.useReverseDepthBuffer && engine.isNDCHalfZRange ? 0 : 1) : engine.useReverseDepthBuffer && engine.isNDCHalfZRange ? 0 : camera.maxZ;
414
+ const projection = this._scene.getProjectionMatrix().m;
415
+ const scaleZ = projection[10];
416
+ const offsetZ = projection[14];
417
+ if (!scaleZ) {
418
+ effect.setFloat2("volDepthUnpack", camera.maxZ, 0);
419
+ return;
420
+ }
421
+ const sign = engine.useReverseDepthBuffer ? -1 : 1;
422
+ effect.setFloat2("volDepthUnpack", (sign * (minZ + maxZ)) / scaleZ, (-sign * minZ - offsetZ) / scaleZ);
423
+ }
424
+ _getEnvironment() {
425
+ const configuration = this._configuration;
426
+ // The medium of the pipeline has a closed form transmittance, except when the height falloff is
427
+ // combined with a fog mode whose density already varies along the ray.
428
+ let transmittance = VolumetricTransmittanceMode.Analytic;
429
+ if (configuration.heightFogEnabled) {
430
+ transmittance = configuration.fogMode === VolumetricFogMode.Exponential ? VolumetricTransmittanceMode.AnalyticHeight : VolumetricTransmittanceMode.Numeric;
431
+ }
432
+ return {
433
+ depthMode: this._depthMode,
434
+ ldrEncode: this._ldrEncode,
435
+ reverseDepth: this._scene.getEngine().useReverseDepthBuffer,
436
+ ndcHalfZ: this._scene.getEngine().isNDCHalfZRange,
437
+ fogMode: configuration.fogMode,
438
+ transmittance,
439
+ linearDepthPacked: this._linearDepthPacked,
440
+ arrayLightCapacity: this._budget.maxArrayLights,
441
+ lightSteps: getVolumetricLightSteps(configuration),
442
+ };
443
+ }
444
+ /**
445
+ * Returns the defines of the passes reading the linear depth, which only depend on how it is stored.
446
+ */
447
+ _buildLinearDepthReadDefines(environment) {
448
+ return environment.linearDepthPacked ? "#define VOL_LINEAR_DEPTH_PACKED\n" : "";
449
+ }
450
+ /**
451
+ * Returns the defines of the linear depth pass, the only one reading the depth map of the depth renderer.
452
+ */
453
+ _buildLinearDepthDefines(environment) {
454
+ let defines = this._buildLinearDepthReadDefines(environment);
455
+ if (environment.depthMode === 2) {
456
+ defines += "#define VOL_DEPTH_PACKED\n";
457
+ }
458
+ else if (environment.depthMode === 1) {
459
+ defines += "#define VOL_DEPTH_VIEWZ\n";
460
+ }
461
+ return defines;
462
+ }
463
+ _buildScatteringDefines(selection, environment) {
464
+ const configuration = this._configuration;
465
+ const shadowed = selection?.shadowed ?? [];
466
+ const directionalCount = selection?.directionalCount ?? 0;
467
+ const csm = selection?.csm ?? null;
468
+ const globalSlotCount = shadowed.filter((entry) => !entry.shadow.local).length;
469
+ let defines = this._buildLinearDepthReadDefines(environment);
470
+ defines += `#define VOL_STEPS ${configuration.steps}\n`;
471
+ defines += `#define VOL_LIGHT_STEPS ${environment.lightSteps}\n`;
472
+ defines += `#define VOL_LOCAL_MAX_STEPS ${Math.max(environment.lightSteps, configuration.steps)}\n`;
473
+ defines += `#define VOL_DISTRIBUTION ${configuration.stepDistribution}\n`;
474
+ defines += `#define VOL_DITHER ${configuration.ditherMode}\n`;
475
+ defines += `#define VOL_PCF_TAPS ${configuration.pcfTaps}\n`;
476
+ defines += `#define VOL_FOG_MODE ${environment.fogMode}\n`;
477
+ defines += `#define VOL_TRANSMITTANCE ${environment.transmittance}\n`;
478
+ // Every medium but the linear fog alone has its transmittance written as the exponential of an optical depth.
479
+ if (environment.transmittance !== VolumetricTransmittanceMode.Analytic || environment.fogMode !== VolumetricFogMode.Linear) {
480
+ defines += "#define VOL_OPTICAL_DEPTH\n";
481
+ }
482
+ defines += `#define VOL_TRANSMITTANCE_SAMPLES ${transmittanceSamples}\n`;
483
+ defines += `#define VOL_DEBUG ${configuration.debugMode}\n`;
484
+ // The arrays are sized by the budget and the number of point and spot lights they hold is a uniform, so
485
+ // the lights entering and leaving the frustum of the camera never recompile the shader.
486
+ defines += `#define VOL_MAX_ARRAY_LIGHTS ${environment.arrayLightCapacity}\n`;
487
+ defines += `#define VOL_DIRECTIONAL_LIGHT_COUNT ${directionalCount}\n`;
488
+ defines += `#define VOL_LOCAL_ARRAY_LIGHTS ${environment.arrayLightCapacity > directionalCount ? 1 : 0}\n`;
489
+ defines += `#define VOL_SHADOW_SLOT_COUNT ${shadowed.length}\n`;
490
+ defines += `#define VOL_GLOBAL_LIGHT_COUNT ${directionalCount + globalSlotCount + (csm ? 1 : 0)}\n`;
491
+ if (configuration.ditherMode !== VolumetricDitherMode.None && configuration.temporalJitter) {
492
+ defines += "#define VOL_TEMPORAL_JITTER\n";
493
+ }
494
+ if (configuration.heightFogEnabled) {
495
+ defines += "#define VOL_HEIGHT_FOG\n";
496
+ }
497
+ if (configuration.lightExtinctionEnabled) {
498
+ defines += "#define VOL_LIGHT_EXTINCTION\n";
499
+ }
500
+ if (configuration.screenSpaceShadows) {
501
+ defines += "#define VOL_SCREEN_SHADOWS\n";
502
+ defines += `#define VOL_SCREEN_SHADOW_STEPS ${configuration.screenSpaceShadowSteps}\n`;
503
+ }
504
+ if (environment.ldrEncode) {
505
+ defines += "#define VOL_LDR_ENCODE\n";
506
+ }
507
+ if (environment.reverseDepth) {
508
+ defines += "#define VOL_REVERSE_DEPTH\n";
509
+ }
510
+ if (environment.ndcHalfZ) {
511
+ defines += "#define VOL_NDC_HALF_Z\n";
512
+ }
513
+ shadowed.forEach((entry, index) => {
514
+ defines += `#define VOL_SHADOW_KIND${index} ${entry.shadow.kind}\n`;
515
+ defines += `#define VOL_SHADOW_MODE${index} ${entry.shadow.mode}\n`;
516
+ defines += `#define VOL_SHADOW_LOCAL${index} ${entry.shadow.local ? 1 : 0}\n`;
517
+ if (entry.shadow.packed) {
518
+ defines += `#define VOL_SHADOW_PACKED${index}\n`;
519
+ }
520
+ });
521
+ if (csm) {
522
+ defines += "#define VOL_CSM\n";
523
+ defines += `#define VOL_CSM_CASCADES ${csm.cascades}\n`;
524
+ defines += `#define VOL_CSM_KIND ${csm.kind === VolumetricShadowKind.Sampler2DShadow ? 1 : 0}\n`;
525
+ if (csm.packed) {
526
+ defines += "#define VOL_CSM_PACKED\n";
527
+ }
528
+ }
529
+ return defines;
530
+ }
531
+ _buildBlurDefines(environment) {
532
+ return `${this._buildLinearDepthReadDefines(environment)}#define VOL_BLUR_RADIUS ${this._configuration.blurRadius}\n`;
533
+ }
534
+ _buildComposeDefines(environment) {
535
+ let defines = this._buildLinearDepthReadDefines(environment);
536
+ defines += `#define VOL_DEBUG ${this._configuration.debugMode}\n`;
537
+ if (environment.ldrEncode) {
538
+ defines += "#define VOL_LDR_ENCODE\n";
539
+ }
540
+ return defines;
541
+ }
542
+ _createPostProcess(options) {
543
+ const postProcess = new PostProcess(options.name, options.shaderName, options.uniforms, options.samplers, options.ratio, null, options.samplingMode, this._scene.getEngine(), false, options.defines, options.textureType, undefined, undefined, false, options.textureFormat, this._shaderLanguage);
544
+ postProcess.autoClear = false;
545
+ postProcess.alphaMode = Constants.ALPHA_DISABLE;
546
+ return postProcess;
547
+ }
548
+ _buildRenderEffects() {
549
+ const engine = this._scene.getEngine();
550
+ const configuration = this._configuration;
551
+ const environment = this._getEnvironment();
552
+ this._linearDepthDefines = this._buildLinearDepthDefines(environment);
553
+ this._scatteringDefines = this._buildScatteringDefines(this._selection, environment);
554
+ this._blurDefines = this._buildBlurDefines(environment);
555
+ this._composeDefines = this._buildComposeDefines(environment);
556
+ // First pass of the chain: its input is the color of the scene, kept untouched for the composition, and
557
+ // it writes the linear depth of the scene. The ratio of a post-process sizes the texture it READS, the
558
+ // size of what it writes is set by the pass that follows.
559
+ this._linearDepthPostProcess = this._createPostProcess({
560
+ name: "VolumetricLightingLinearDepth",
561
+ shaderName: volumetricLightingLinearDepthShaderName,
562
+ uniforms: ["volCameraMinMaxZ", "volDepthUnpack"],
563
+ samplers: ["depthSampler"],
564
+ ratio: 1.0,
565
+ samplingMode: Texture.BILINEAR_SAMPLINGMODE,
566
+ textureType: this._textureType,
567
+ defines: this._linearDepthDefines,
568
+ });
569
+ this._linearDepthPostProcess.onApplyObservable.add((effect) => this._bindLinearDepth(effect));
570
+ // The input of the raymarching pass is the linear depth, kept at the full resolution so the occlusion by
571
+ // the depth buffer sees the geometry as the scene draws it: the pass itself still rasterizes at the
572
+ // reduced resolution, which is set by the ratio of the pass that follows. The linear depth is only ever
573
+ // read with texel fetches, and a 32 bits float texture can't be filtered on every engine anyway.
574
+ this._scatteringPostProcess = this._createPostProcess({
575
+ name: "VolumetricLightingScattering",
576
+ shaderName: volumetricLightingScatteringShaderName,
577
+ uniforms: scatteringUniforms,
578
+ samplers: scatteringSamplers,
579
+ ratio: 1.0,
580
+ samplingMode: Texture.NEAREST_SAMPLINGMODE,
581
+ textureType: this._linearDepthPacked ? Constants.TEXTURETYPE_UNSIGNED_BYTE : Constants.TEXTURETYPE_FLOAT,
582
+ textureFormat: this._linearDepthPacked ? Constants.TEXTUREFORMAT_RGBA : Constants.TEXTUREFORMAT_R,
583
+ defines: this._scatteringDefines,
584
+ });
585
+ this._scatteringPostProcess.onApplyObservable.add((effect) => this._bindScattering(effect));
586
+ this._blurPostProcesses = [];
587
+ for (let i = 0; i < configuration.blurPasses; ++i) {
588
+ const blurPostProcess = this._createPostProcess({
589
+ name: `VolumetricLightingBlur${i}`,
590
+ shaderName: volumetricLightingBlurShaderName,
591
+ uniforms: ["volCameraMinMaxZ", "volDepthRatio", "volBlurDirection", "volBlurParams"],
592
+ samplers: ["volLinearDepthSampler"],
593
+ ratio: configuration.resolutionScale,
594
+ samplingMode: Texture.BILINEAR_SAMPLINGMODE,
595
+ textureType: this._textureType,
596
+ defines: this._blurDefines,
597
+ });
598
+ blurPostProcess.onApplyObservable.add((effect) => this._bindBlur(effect, i));
599
+ this._blurPostProcesses.push(blurPostProcess);
600
+ }
601
+ // Keeping the ratio at "resolutionScale" is what makes the pass before it rasterize at the reduced
602
+ // resolution; the composition itself still draws at the size of whatever follows it.
603
+ this._composePostProcess = this._createPostProcess({
604
+ name: "VolumetricLightingCompose",
605
+ shaderName: volumetricLightingComposeShaderName,
606
+ uniforms: ["volCameraMinMaxZ", "volDepthRatio", "volComposeParams"],
607
+ samplers: ["volSceneSampler", "volLinearDepthSampler"],
608
+ ratio: configuration.resolutionScale,
609
+ samplingMode: Texture.BILINEAR_SAMPLINGMODE,
610
+ textureType: this._textureType,
611
+ defines: this._composeDefines,
612
+ });
613
+ this._composePostProcess.onApplyObservable.add((effect) => this._bindCompose(effect));
614
+ this.addEffect(new PostProcessRenderEffect(engine, VolumetricLightingRenderingPipeline.SceneColorEffectName, () => this._linearDepthPostProcess, true));
615
+ this.addEffect(new PostProcessRenderEffect(engine, VolumetricLightingRenderingPipeline.ScatteringEffectName, () => this._scatteringPostProcess, true));
616
+ if (this._blurPostProcesses.length) {
617
+ this.addEffect(new PostProcessRenderEffect(engine, VolumetricLightingRenderingPipeline.BlurEffectName, () => this._blurPostProcesses, true));
618
+ }
619
+ this.addEffect(new PostProcessRenderEffect(engine, VolumetricLightingRenderingPipeline.ComposeEffectName, () => this._composePostProcess, true));
620
+ }
621
+ _disposePostProcesses(cameras) {
622
+ const postProcesses = [this._linearDepthPostProcess, this._scatteringPostProcess, ...this._blurPostProcesses, this._composePostProcess];
623
+ postProcesses.forEach((postProcess) => {
624
+ if (!postProcess) {
625
+ return;
626
+ }
627
+ cameras.forEach((camera) => postProcess.dispose(camera));
628
+ postProcess.dispose();
629
+ });
630
+ this._linearDepthPostProcess = null;
631
+ this._scatteringPostProcess = null;
632
+ this._blurPostProcesses = [];
633
+ this._composePostProcess = null;
634
+ }
635
+ /**
636
+ * Rebuilds the chain of post-processes. Changing the resolution of the scattering buffer or the number
637
+ * of blur passes changes the shape of the chain, which a post-process can't do in place.
638
+ */
639
+ _rebuildRenderEffects() {
640
+ if (this._disposed) {
641
+ return;
642
+ }
643
+ const manager = this._scene.postProcessRenderPipelineManager;
644
+ const cameras = this._cameras.slice();
645
+ try {
646
+ manager.detachCamerasFromRenderPipeline(this.name, cameras);
647
+ this._disposePostProcesses(cameras);
648
+ Object.keys(this._renderEffects).forEach((key) => {
649
+ delete this._renderEffects[key];
650
+ });
651
+ this._buildRenderEffects();
652
+ }
653
+ catch (e) {
654
+ console.error("Failed to rebuild the volumetric lighting rendering pipeline.", e);
655
+ }
656
+ finally {
657
+ // Always re-attach, even after a failure: leaving the cameras detached would make the pipeline
658
+ // impossible to recover from, including for the disposal path.
659
+ try {
660
+ manager.attachCamerasToRenderPipeline(this.name, cameras);
661
+ }
662
+ catch (e) {
663
+ console.error("Failed to re-attach the cameras to the volumetric lighting rendering pipeline.", e);
664
+ }
665
+ }
666
+ this._lightsDirty = true;
667
+ }
668
+ /**
669
+ * Runs the selection pass and recompiles the shaders when, and only when, their defines changed. The
670
+ * identity, the number and the values of the point and spot lights change every frame and only cost a
671
+ * uniform update: only the directional lights and the kinds of the shadow maps shape the shader.
672
+ */
673
+ _updateSelection() {
674
+ if (this._disposed || !this._scatteringPostProcess) {
675
+ return;
676
+ }
677
+ ++this._frameCount;
678
+ const shouldSelect = this._lightsDirty || !this._selection || this._frameCount % this._configuration.selectionRefreshRate === 0;
679
+ if (!shouldSelect) {
680
+ return;
681
+ }
682
+ this._lightsDirty = false;
683
+ const environment = this._getEnvironment();
684
+ const selection = selectVolumetricLights(this._scene, this._camera, this._configuration, this._budget, this._selection);
685
+ this._selection = selection;
686
+ // Each pass is only recompiled when its own defines changed: recompiling a pass makes it skip its
687
+ // draws until the new program is linked.
688
+ const scatteringDefines = this._buildScatteringDefines(selection, environment);
689
+ if (scatteringDefines !== this._scatteringDefines) {
690
+ this._scatteringDefines = scatteringDefines;
691
+ this._scatteringPostProcess.updateEffect(scatteringDefines);
692
+ }
693
+ const linearDepthDefines = this._buildLinearDepthDefines(environment);
694
+ if (linearDepthDefines !== this._linearDepthDefines) {
695
+ this._linearDepthDefines = linearDepthDefines;
696
+ this._linearDepthPostProcess?.updateEffect(linearDepthDefines);
697
+ }
698
+ const blurDefines = this._buildBlurDefines(environment);
699
+ if (blurDefines !== this._blurDefines) {
700
+ this._blurDefines = blurDefines;
701
+ this._blurPostProcesses.forEach((blurPostProcess) => blurPostProcess.updateEffect(blurDefines));
702
+ }
703
+ const composeDefines = this._buildComposeDefines(environment);
704
+ if (composeDefines !== this._composeDefines) {
705
+ this._composeDefines = composeDefines;
706
+ this._composePostProcess?.updateEffect(composeDefines);
707
+ }
708
+ }
709
+ /**
710
+ * Fills the four vectors describing a light in the raymarching shader. The packing follows what
711
+ * "Light.transferToEffect" writes for the surfaces of the scene so the shafts agree with them, except for
712
+ * the position of the point and spot lights, which is written relative to the camera: the raymarching
713
+ * works in that space, which keeps its precision whatever the distance of the scene to the origin.
714
+ */
715
+ _writeLight(candidate, index, data, diffuse, direction, falloff) {
716
+ const { light, config } = candidate;
717
+ const offset = index * 4;
718
+ if (isDirectionalLight(light)) {
719
+ getVolumetricLightWorldDirection(light, this._temporaryVector);
720
+ // The shader expects the direction pointing to the light, not the one the light travels along.
721
+ data[offset] = -this._temporaryVector.x;
722
+ data[offset + 1] = -this._temporaryVector.y;
723
+ data[offset + 2] = -this._temporaryVector.z;
724
+ data[offset + 3] = 1;
725
+ }
726
+ else {
727
+ getVolumetricLightWorldPosition(light, this._temporaryVector).subtractInPlace(this._camera.globalPosition);
728
+ data[offset] = this._temporaryVector.x;
729
+ data[offset + 1] = this._temporaryVector.y;
730
+ data[offset + 2] = this._temporaryVector.z;
731
+ data[offset + 3] = isSpotLight(light) ? 2 : 0;
732
+ }
733
+ if (config.useCustomColor) {
734
+ this._temporaryColor.set(config.color[0], config.color[1], config.color[2]);
735
+ }
736
+ else {
737
+ this._temporaryColor.copyFrom(light.diffuse);
738
+ }
739
+ const scale = light.getScaledIntensity() * config.volumeIntensity * 0.00001;
740
+ diffuse[offset] = this._temporaryColor.r * scale;
741
+ diffuse[offset + 1] = this._temporaryColor.g * scale;
742
+ diffuse[offset + 2] = this._temporaryColor.b * scale;
743
+ diffuse[offset + 3] = config.anisotropy;
744
+ if (isSpotLight(light)) {
745
+ getVolumetricLightWorldDirection(light, this._temporaryVector);
746
+ direction[offset] = this._temporaryVector.x;
747
+ direction[offset + 1] = this._temporaryVector.y;
748
+ direction[offset + 2] = this._temporaryVector.z;
749
+ direction[offset + 3] = Math.cos(light.angle * 0.5);
750
+ falloff[offset + 1] = light.exponent;
751
+ }
752
+ else {
753
+ direction[offset] = 0;
754
+ direction[offset + 1] = 0;
755
+ direction[offset + 2] = 1;
756
+ direction[offset + 3] = -1;
757
+ falloff[offset + 1] = 1;
758
+ }
759
+ falloff[offset] = getVolumetricLightRange(light, config, this._configuration);
760
+ // Only read by the unshadowed tier, where it asks for the depth buffer occlusion. The shadowed tier
761
+ // overwrites it with the frustum edge falloff of its generator, and uses its shadow map instead.
762
+ falloff[offset + 2] = config.castVolumetricShadows ? 1 : 0;
763
+ falloff[offset + 3] = config.shadowDarkness;
764
+ }
765
+ _bindLinearDepth(effect) {
766
+ if (!this._depthRenderer) {
767
+ return;
768
+ }
769
+ effect.setTexture("depthSampler", this._depthRenderer.getDepthMap());
770
+ this._bindDepthUniforms(effect);
771
+ }
772
+ _bindScattering(effect) {
773
+ const scene = this._scene;
774
+ const camera = this._camera;
775
+ const configuration = this._configuration;
776
+ const selection = this._selection;
777
+ if (!selection) {
778
+ return;
779
+ }
780
+ this._inverseViewProjection.copyFrom(scene.getTransformMatrix());
781
+ this._inverseViewProjection.invert();
782
+ effect.setMatrix("volInverseViewProjection", this._inverseViewProjection);
783
+ effect.setVector3("volCameraPosition", camera.globalPosition);
784
+ camera.getDirectionToRef(scene.useRightHandedSystem ? rightHandedForward : leftHandedForward, this._temporaryVector);
785
+ effect.setVector3("volCameraForward", this._temporaryVector.normalize());
786
+ effect.setFloat2("volCameraMinMaxZ", camera.minZ, camera.maxZ);
787
+ effect.setFloat("volFrameIndex", scene.getRenderId() % 64);
788
+ this._bindDepthRatio(effect);
789
+ effect.setFloat4("volFogInfos", configuration.fogMode, configuration.fogStart, configuration.fogEnd, configuration.fogDensity);
790
+ this._temporaryColor.set(configuration.fogColor[0], configuration.fogColor[1], configuration.fogColor[2]);
791
+ effect.setColor3("volFogColor", this._temporaryColor);
792
+ effect.setFloat("volLinearFogEps", Math.max((configuration.fogEnd - configuration.fogStart) * 0.02, 1e-4));
793
+ effect.setFloat4("volMedium", configuration.albedo, configuration.heightFogBaseHeight, configuration.heightFogFalloff, 0);
794
+ if (configuration.screenSpaceShadows) {
795
+ effect.setMatrix("volViewProjection", scene.getTransformMatrix());
796
+ effect.setFloat4("volScreenShadowParams", configuration.screenSpaceShadowMaxDistance, configuration.screenSpaceShadowBias, configuration.screenSpaceShadowThickness, 0);
797
+ }
798
+ this._temporaryColor.set(configuration.ambientColor[0], configuration.ambientColor[1], configuration.ambientColor[2]).scaleInPlace(configuration.ambientIntensity);
799
+ effect.setColor3("volAmbient", this._temporaryColor);
800
+ // The directional lights come first in the arrays, the shader walks the point and spot lights after them.
801
+ const unshadowedCount = selection.unshadowed.length;
802
+ const localCount = unshadowedCount - selection.directionalCount;
803
+ effect.setFloat4("volParams", configuration.maxDistance, configuration.ditherStrength, configuration.lightExtinctionClamp, localCount);
804
+ // Unshadowed lights.
805
+ for (let i = 0; i < unshadowedCount; ++i) {
806
+ this._writeLight(selection.unshadowed[i], i, this._lightData, this._lightDiffuse, this._lightDirection, this._lightFalloff);
807
+ }
808
+ if (unshadowedCount) {
809
+ const count = unshadowedCount * 4;
810
+ effect.setFloatArray4("volLightData", this._lightData.subarray(0, count));
811
+ effect.setFloatArray4("volLightDiffuse", this._lightDiffuse.subarray(0, count));
812
+ effect.setFloatArray4("volLightDirection", this._lightDirection.subarray(0, count));
813
+ effect.setFloatArray4("volLightFalloff", this._lightFalloff.subarray(0, count));
814
+ }
815
+ // Shadowed lights.
816
+ selection.shadowed.forEach((entry, index) => {
817
+ this._writeLight(entry.candidate, index, this._shadowLightData, this._shadowLightDiffuse, this._shadowLightDirection, this._shadowLightFalloff);
818
+ const { generator, kind, mode, mapSize } = entry.shadow;
819
+ const light = entry.candidate.light;
820
+ const offset = index * 4;
821
+ this._shadowLightFalloff[offset + 2] = generator.frustumEdgeFalloff;
822
+ const depthMinZ = light.getDepthMinZ?.(scene.activeCamera) ?? 0;
823
+ const depthMaxZ = light.getDepthMaxZ?.(scene.activeCamera) ?? 1;
824
+ this._shadowInfo[offset] = generator.getDarkness();
825
+ this._shadowInfo[offset + 1] = kind === VolumetricShadowKind.Sampler2DShadow ? 1 / mapSize : mode === 0 ? 0 : generator.depthScale;
826
+ this._shadowInfo[offset + 2] = depthMinZ;
827
+ this._shadowInfo[offset + 3] = depthMinZ + depthMaxZ;
828
+ generator.getTransformMatrix().copyToArray(this._shadowMatrices, index * 16);
829
+ const shadowMap = generator.getShadowMapForRendering();
830
+ if (kind === VolumetricShadowKind.Sampler2DShadow) {
831
+ effect.setDepthStencilTexture(`volShadowSampler${index}`, shadowMap);
832
+ }
833
+ else {
834
+ effect.setTexture(`volShadowSampler${index}`, shadowMap);
835
+ }
836
+ });
837
+ if (selection.shadowed.length) {
838
+ const count = selection.shadowed.length * 4;
839
+ effect.setFloatArray4("volShadowLightData", this._shadowLightData.subarray(0, count));
840
+ effect.setFloatArray4("volShadowLightDiffuse", this._shadowLightDiffuse.subarray(0, count));
841
+ effect.setFloatArray4("volShadowLightDirection", this._shadowLightDirection.subarray(0, count));
842
+ effect.setFloatArray4("volShadowLightFalloff", this._shadowLightFalloff.subarray(0, count));
843
+ effect.setFloatArray4("volShadowInfo", this._shadowInfo.subarray(0, count));
844
+ effect.setMatrices("volShadowMatrix", this._shadowMatrices.subarray(0, selection.shadowed.length * 16));
845
+ }
846
+ // Cascaded shadow map of the directional light, when there is one.
847
+ if (selection.csm) {
848
+ const { candidate, generator, cascades, kind } = selection.csm;
849
+ const light = candidate.light;
850
+ this._writeLight(candidate, 0, this._csmLightData, this._csmLightDiffuse, this._csmLightDirection, this._csmLightFalloff);
851
+ effect.setFloat4("volCsmLightData", this._csmLightData[0], this._csmLightData[1], this._csmLightData[2], this._csmLightData[3]);
852
+ effect.setFloat4("volCsmLightDiffuse", this._csmLightDiffuse[0], this._csmLightDiffuse[1], this._csmLightDiffuse[2], this._csmLightDiffuse[3]);
853
+ effect.setFloat4("volCsmLightDirection", this._csmLightDirection[0], this._csmLightDirection[1], this._csmLightDirection[2], this._csmLightDirection[3]);
854
+ effect.setFloat4("volCsmLightFalloff", this._csmLightFalloff[0], this._csmLightFalloff[1], generator.frustumEdgeFalloff, candidate.config.shadowDarkness);
855
+ const depthMinZ = light.getDepthMinZ?.(scene.activeCamera) ?? 1;
856
+ const depthMaxZ = light.getDepthMaxZ?.(scene.activeCamera) ?? 1;
857
+ const mapSize = generator.getShadowMap()?.getSize().width ?? 1024;
858
+ effect.setFloat4("volCsmInfo", generator.getDarkness(), 1 / mapSize, depthMinZ, depthMinZ + depthMaxZ);
859
+ for (let i = 0; i < cascades; ++i) {
860
+ generator.getCascadeTransformMatrix(i)?.copyToArray(this._csmMatrices, i * 16);
861
+ }
862
+ effect.setMatrices("volCsmMatrices", this._csmMatrices.subarray(0, cascades * 16));
863
+ const shadowMap = generator.getShadowMapForRendering();
864
+ if (kind === VolumetricShadowKind.Sampler2DShadow) {
865
+ effect.setDepthStencilTexture("volCsmSampler", shadowMap);
866
+ }
867
+ else {
868
+ effect.setTexture("volCsmSampler", shadowMap);
869
+ }
870
+ }
871
+ }
872
+ /**
873
+ * Binds the ratio between the resolution of the linear depth and the resolution of the scattering buffer.
874
+ * It is computed here rather than in the shaders so every pass rounds it exactly the same way, which is what
875
+ * makes them all associate each texel of the scattering buffer with the same texel of the linear depth.
876
+ */
877
+ _bindDepthRatio(effect) {
878
+ // The ratio of a post-process sizes its input texture: the raymarching pass reads the linear depth, and
879
+ // the pass that follows it reads the scattering buffer. Both are activated by the time a pass applies.
880
+ const depth = this._scatteringPostProcess;
881
+ const scattering = this._blurPostProcesses[0] ?? this._composePostProcess;
882
+ const depthWidth = depth && depth.width > 0 ? depth.width : 1;
883
+ const depthHeight = depth && depth.height > 0 ? depth.height : 1;
884
+ const scatteringWidth = scattering && scattering.width > 0 ? scattering.width : depthWidth;
885
+ const scatteringHeight = scattering && scattering.height > 0 ? scattering.height : depthHeight;
886
+ effect.setFloat2("volDepthRatio", depthWidth / scatteringWidth, depthHeight / scatteringHeight);
887
+ }
888
+ _bindBlur(effect, index) {
889
+ if (!this._scatteringPostProcess) {
890
+ return;
891
+ }
892
+ // The input of the raymarching pass is the linear depth.
893
+ effect.setTextureFromPostProcess("volLinearDepthSampler", this._scatteringPostProcess);
894
+ effect.setFloat2("volCameraMinMaxZ", this._camera.minZ, this._camera.maxZ);
895
+ this._bindDepthRatio(effect);
896
+ effect.setFloat2("volBlurDirection", index % 2 === 0 ? 1 : 0, index % 2 === 0 ? 0 : 1);
897
+ effect.setFloat2("volBlurParams", Math.max(this._configuration.blurRadius * 0.5, 0.5), this._configuration.blurDepthThreshold);
898
+ }
899
+ _bindCompose(effect) {
900
+ if (!this._linearDepthPostProcess || !this._scatteringPostProcess) {
901
+ return;
902
+ }
903
+ const configuration = this._configuration;
904
+ // The input texture of the first pass is the untouched, full resolution, color of the scene, and the
905
+ // input texture of the raymarching pass is the linear depth of the scene.
906
+ effect.setTextureFromPostProcess("volSceneSampler", this._linearDepthPostProcess);
907
+ effect.setTextureFromPostProcess("volLinearDepthSampler", this._scatteringPostProcess);
908
+ effect.setFloat2("volCameraMinMaxZ", this._camera.minZ, this._camera.maxZ);
909
+ this._bindDepthRatio(effect);
910
+ // The dithering of the output is only useful when the chain ends up in an 8 bits buffer.
911
+ const outputDither = this._ldrEncode ? 1 / 255 : 0;
912
+ effect.setFloat4("volComposeParams", configuration.intensity, configuration.extinctionAmount, configuration.upsampleDepthThreshold, outputDither);
913
+ }
914
+ }
915
+ //# sourceMappingURL=pipeline.js.map