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,1207 @@
1
+ import { maxVolumetricShadowSlots } from "./types";
2
+ /**
3
+ * WGSL version of the four passes of the volumetric lighting rendering pipeline, for WebGPU.
4
+ *
5
+ * This is a hand written port of the GLSL in "shaders.ts" rather than a transpilation: Babylon.js can only
6
+ * turn GLSL into WGSL by downloading twgsl from its CDN, which an offline Electron application can't rely on.
7
+ * Both versions are driven by exactly the same "#define" set, so the pipeline, the light selection and the
8
+ * shader shape key are shared and only the source of the passes differs.
9
+ *
10
+ * Babylon.js emulates the conventions of WebGL on WebGPU: render targets are stored bottom-up and
11
+ * "fragmentInputs.position" is flipped like "gl_FragCoord" when needed, so the integer texel fetches below
12
+ * address the textures exactly like their GLSL counterparts.
13
+ */
14
+ /**
15
+ * Unpacks a depth packed in the four channels of an 8 bits RGBA texture, the same way Babylon.js does.
16
+ */
17
+ const packingHelpers = /* wgsl */ `
18
+ fn volUnpack(color: vec4f) -> f32 {
19
+ let bitShift = vec4f(1.0 / (255.0 * 255.0 * 255.0), 1.0 / (255.0 * 255.0), 1.0 / 255.0, 1.0);
20
+ return dot(color, bitShift);
21
+ }
22
+ `;
23
+ /**
24
+ * Reads the full resolution depth map of the depth renderer, bound as "depthSampler". Requires the
25
+ * "volCameraMinMaxZ" and "volDepthUnpack" uniforms.
26
+ */
27
+ const depthMapHelpers = /* wgsl */ `
28
+ fn volSampleDepth(uv: vec2f) -> f32 {
29
+ #ifdef VOL_DEPTH_PACKED
30
+ return volUnpack(textureSampleLevel(depthSampler, depthSamplerSampler, uv, 0.0));
31
+ #else
32
+ return textureSampleLevel(depthSampler, depthSamplerSampler, uv, 0.0).r;
33
+ #endif
34
+ }
35
+
36
+ // Converts the raw value stored in the depth map into a distance along the view axis, in scene units.
37
+ fn volLinearDepth(d: f32) -> f32 {
38
+ #ifdef VOL_DEPTH_VIEWZ
39
+ // The depth renderer stores the view space Z directly. The sky is cleared to 0.
40
+ return select(d, uniforms.volCameraMinMaxZ.y, d <= 0.0);
41
+ #else
42
+ // The depth renderer stores "(clipZ + minZ) / (minZ + maxZ)", which is affine in the view space Z
43
+ // for both the perspective and the orthographic projections. "volDepthUnpack" holds the two
44
+ // coefficients of the exact inverse, computed on the CPU from the projection matrix of the scene.
45
+ return uniforms.volDepthUnpack.x * d + uniforms.volDepthUnpack.y;
46
+ #endif
47
+ }
48
+ `;
49
+ /**
50
+ * Reads the linear depth written by the linear depth pass. Requires the "volCameraMinMaxZ" uniform.
51
+ * @param texture defines the name of the texture holding the linear depth.
52
+ */
53
+ function buildLinearDepthHelpers(texture) {
54
+ return /* wgsl */ `
55
+ // Returns the distance along the view axis, in scene units, of the texel at the given coordinates.
56
+ fn volReadLinearDepth(coordinates: vec2i) -> f32 {
57
+ #ifdef VOL_LINEAR_DEPTH_PACKED
58
+ return volUnpack(textureLoad(${texture}, coordinates, 0)) * uniforms.volCameraMinMaxZ.y;
59
+ #else
60
+ return textureLoad(${texture}, coordinates, 0).r;
61
+ #endif
62
+ }
63
+
64
+ // Returns the texel of the linear depth under the center of the given texel of the scattering buffer.
65
+ // @see the GLSL version.
66
+ fn volScatterToDepthTexel(scatterTexel: vec2i, depthSize: vec2i) -> vec2i {
67
+ return min(vec2i((vec2f(scatterTexel) + 0.5) * uniforms.volDepthRatio), depthSize - 1);
68
+ }
69
+ `;
70
+ }
71
+ /**
72
+ * The linear depth pass. @see buildVolumetricLightingLinearDepthShader.
73
+ */
74
+ export function buildVolumetricLightingLinearDepthShaderWGSL() {
75
+ return /* wgsl */ `
76
+ varying vUV: vec2f;
77
+
78
+ var textureSamplerSampler: sampler;
79
+ var textureSampler: texture_2d<f32>;
80
+ var depthSamplerSampler: sampler;
81
+ var depthSampler: texture_2d<f32>;
82
+
83
+ uniform volCameraMinMaxZ: vec2f;
84
+ uniform volDepthUnpack: vec2f;
85
+
86
+ ${packingHelpers}
87
+ ${depthMapHelpers}
88
+
89
+ fn volPack(depth: f32) -> vec4f {
90
+ let bitShift = vec4f(255.0 * 255.0 * 255.0, 255.0 * 255.0, 255.0, 1.0);
91
+ let bitMask = vec4f(0.0, 1.0 / 255.0, 1.0 / 255.0, 1.0 / 255.0);
92
+
93
+ var result = fract(depth * bitShift);
94
+ result -= result.xxyz * bitMask;
95
+
96
+ return result;
97
+ }
98
+
99
+ @fragment
100
+ fn main(input: FragmentInputs) -> FragmentOutputs {
101
+ let viewZ = volLinearDepth(volSampleDepth(input.vUV));
102
+
103
+ #ifdef VOL_LINEAR_DEPTH_PACKED
104
+ fragmentOutputs.color = volPack(clamp(viewZ / uniforms.volCameraMinMaxZ.y, 0.0, 0.9999999));
105
+ #else
106
+ fragmentOutputs.color = vec4f(viewZ, 0.0, 0.0, 1.0);
107
+ #endif
108
+ }
109
+ `;
110
+ }
111
+ /**
112
+ * Generates the loop integrating the in-scattering of a point or a spot light over the part of the view ray
113
+ * crossing its volume. @see the GLSL version in "shaders.ts".
114
+ * @param seed defines the expression decorrelating the samples of this light from the ones of the other lights.
115
+ * @param occlusion defines the statement applying the occlusion of the light to "contribution".
116
+ */
117
+ function buildLocalLightIntegrationWGSL(seed, occlusion) {
118
+ return /* wgsl */ `
119
+ let chordLength = t1 - t0;
120
+
121
+ // The point of the view ray closest to the light splits the chord in two halves over which both the
122
+ // attenuation and the phase function are monotonic, which is where stratified samples do best.
123
+ var closest = clamp(dot(data.xyz, volRayDir), t0, t1);
124
+ if (closest - t0 < 0.02 * chordLength) {
125
+ closest = t0;
126
+ } else if (t1 - closest < 0.02 * chordLength) {
127
+ closest = t1;
128
+ }
129
+
130
+ let count = volLocalSampleCount(chordLength, falloff.x, tEnd - tStart);
131
+
132
+ var firstCount = clamp(i32(f32(count) * (closest - t0) / chordLength + 0.5), 1, count - 1);
133
+ if (closest <= t0) {
134
+ firstCount = 0;
135
+ } else if (closest >= t1) {
136
+ firstCount = count;
137
+ }
138
+
139
+ let firstWidth = (closest - t0) / f32(max(firstCount, 1));
140
+ let secondWidth = (t1 - closest) / f32(max(count - firstCount, 1));
141
+ let stratum = volStratumOffset(${seed});
142
+
143
+ var result = vec3f(0.0);
144
+
145
+ for (var k: i32 = 0; k < VOL_LOCAL_MAX_STEPS; k++) {
146
+ if (k >= count) {
147
+ break;
148
+ }
149
+
150
+ let inFirstHalf = k < firstCount;
151
+ let width = select(secondWidth, firstWidth, inFirstHalf);
152
+ let t = select(closest + width * (f32(k - firstCount) + stratum), t0 + width * (f32(k) + stratum), inFirstHalf);
153
+
154
+ let position = volRayDir * t;
155
+ let sigmaT = volExtinctionAt(uniforms.volCameraPosition.y + position.y, t);
156
+
157
+ let lightSample = volEvalLocalLight(data, diffuse, direction, falloff, position);
158
+ var contribution = lightSample.xyz;
159
+ let distanceToLight = lightSample.w;
160
+
161
+ if (contribution.r + contribution.g + contribution.b > 0.0) {
162
+ ${occlusion}
163
+ result += contribution * (volCombinedTransmittance(t, sigmaT, distanceToLight) * sigmaT * width);
164
+ }
165
+ }
166
+
167
+ return vec4f(result, 1.0);
168
+ `;
169
+ }
170
+ /**
171
+ * The raymarching pass. @see buildVolumetricLightingScatteringShader for what it computes.
172
+ * @param shadowSlotCount defines the number of shadowed light slots the shader is generated for.
173
+ */
174
+ export function buildVolumetricLightingScatteringShaderWGSL(shadowSlotCount) {
175
+ const slots = [];
176
+ const globalSlotContributions = [];
177
+ const localSlotContributions = [];
178
+ for (let i = 0; i < shadowSlotCount; ++i) {
179
+ slots.push(
180
+ /* wgsl */ `
181
+ #if VOL_SHADOW_SLOT_COUNT > {X}
182
+ #if VOL_SHADOW_KIND{X} == 2
183
+ var volShadowSampler{X}Sampler: sampler;
184
+ var volShadowSampler{X}: texture_cube<f32>;
185
+ #elif VOL_SHADOW_KIND{X} == 1
186
+ var volShadowSampler{X}Sampler: sampler_comparison;
187
+ var volShadowSampler{X}: texture_depth_2d;
188
+ #else
189
+ var volShadowSampler{X}Sampler: sampler;
190
+ var volShadowSampler{X}: texture_2d<f32>;
191
+ #endif
192
+
193
+ // Returns the shadowing of a point given its position relative to the camera and, for the 2d shadow maps, its
194
+ // position in the clip space of the light. @see the GLSL version.
195
+ fn volShadow{X}(position: vec3f, clip: vec4f) -> f32 {
196
+ let info = uniforms.volShadowInfo[{X}];
197
+ let edge = uniforms.volShadowLightFalloff[{X}].z;
198
+
199
+ #if VOL_SHADOW_KIND{X} == 2
200
+ // Cube shadow map of a point light. It stores the radial distance to the light.
201
+ var toFragment = position - uniforms.volShadowLightData[{X}].xyz;
202
+ let depth = clamp((length(toFragment) + info.z) / info.w, 0.0, 1.0);
203
+
204
+ toFragment = normalize(toFragment);
205
+ toFragment.y = -toFragment.y;
206
+
207
+ #ifdef VOL_SHADOW_PACKED{X}
208
+ let shadowMapSample = volUnpack(textureSampleLevel(volShadowSampler{X}, volShadowSampler{X}Sampler, toFragment, 0.0));
209
+ #else
210
+ let shadowMapSample = textureSampleLevel(volShadowSampler{X}, volShadowSampler{X}Sampler, toFragment, 0.0).x;
211
+ #endif
212
+
213
+ #if VOL_SHADOW_MODE{X} == 1
214
+ // Exponential shadow map, same encoding as the 2d case. "info.y" holds the depth scale.
215
+ return 1.0 - clamp(exp(min(87.0, info.y * depth)) * shadowMapSample, 0.0, 1.0 - info.x);
216
+ #elif VOL_SHADOW_MODE{X} == 2
217
+ return clamp(exp(min(87.0, -info.y * (depth - shadowMapSample))), info.x, 1.0);
218
+ #else
219
+ return select(1.0, info.x, depth > shadowMapSample);
220
+ #endif
221
+ #else
222
+ let clipSpace = clip.xyz / clip.w;
223
+
224
+ // "getTransformMatrix" gives the world to light clip space matrix without the [-1, 1] -> [0, 1] bias.
225
+ #if VOL_SHADOW_KIND{X} == 1
226
+ var uvDepth = 0.5 * clipSpace + vec3f(0.5);
227
+ #ifdef VOL_NDC_HALF_Z
228
+ uvDepth.z = clipSpace.z;
229
+ #endif
230
+
231
+ // Same guard as "computeShadowWithPCF1": past the far plane of the shadow map the hardware
232
+ // comparison would fail against every texel a caster wrote, reporting the sample as occluded
233
+ // while the surfaces at the same distance are lit by the material path.
234
+ let pcfDepthMetric = volDepthMetric(clip, info.zw);
235
+ if (pcfDepthMetric < 0.0 || pcfDepthMetric > 1.0 || uvDepth.x < 0.0 || uvDepth.x > 1.0 || uvDepth.y < 0.0 || uvDepth.y > 1.0) {
236
+ return 1.0;
237
+ }
238
+
239
+ // Hardware comparison sampling. "info.y" holds the inverse of the size of the shadow map.
240
+ #if VOL_PCF_TAPS == 4
241
+ let shadow = 0.25 * (
242
+ textureSampleCompareLevel(volShadowSampler{X}, volShadowSampler{X}Sampler, uvDepth.xy + vec2f(-0.5 * info.y, -0.5 * info.y), uvDepth.z) +
243
+ textureSampleCompareLevel(volShadowSampler{X}, volShadowSampler{X}Sampler, uvDepth.xy + vec2f( 0.5 * info.y, -0.5 * info.y), uvDepth.z) +
244
+ textureSampleCompareLevel(volShadowSampler{X}, volShadowSampler{X}Sampler, uvDepth.xy + vec2f(-0.5 * info.y, 0.5 * info.y), uvDepth.z) +
245
+ textureSampleCompareLevel(volShadowSampler{X}, volShadowSampler{X}Sampler, uvDepth.xy + vec2f( 0.5 * info.y, 0.5 * info.y), uvDepth.z)
246
+ );
247
+ #else
248
+ let shadow = textureSampleCompareLevel(volShadowSampler{X}, volShadowSampler{X}Sampler, uvDepth.xy, uvDepth.z);
249
+ #endif
250
+
251
+ return volFallOff(mix(info.x, 1.0, shadow), clipSpace.xy, edge);
252
+ #else
253
+ let uv = 0.5 * clipSpace.xy + vec2f(0.5);
254
+ if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {
255
+ return 1.0;
256
+ }
257
+
258
+ let depthMetric = clamp(volDepthMetric(clip, info.zw), 0.0, 1.0);
259
+
260
+ #ifdef VOL_SHADOW_PACKED{X}
261
+ let shadowMapSample = volUnpack(textureSampleLevel(volShadowSampler{X}, volShadowSampler{X}Sampler, uv, 0.0));
262
+ #else
263
+ let shadowMapSample = textureSampleLevel(volShadowSampler{X}, volShadowSampler{X}Sampler, uv, 0.0).x;
264
+ #endif
265
+
266
+ #if VOL_SHADOW_MODE{X} == 1
267
+ // Exponential shadow map. "info.y" holds the depth scale of the generator.
268
+ return volFallOff(1.0 - clamp(exp(min(87.0, info.y * depthMetric)) * shadowMapSample, 0.0, 1.0 - info.x), clipSpace.xy, edge);
269
+ #elif VOL_SHADOW_MODE{X} == 2
270
+ // Close exponential shadow map.
271
+ return volFallOff(clamp(exp(min(87.0, -info.y * (depthMetric - shadowMapSample))), info.x, 1.0), clipSpace.xy, edge);
272
+ #else
273
+ return select(1.0, volFallOff(info.x, clipSpace.xy, edge), depthMetric > shadowMapSample);
274
+ #endif
275
+ #endif
276
+ #endif
277
+ }
278
+
279
+ #if VOL_SHADOW_LOCAL{X} == 1
280
+ // Point or spot light casting volumetric shadows: integrated over its own volume, like the others.
281
+ fn volIntegrateShadowSlot{X}(tStart: f32, tEnd: f32) -> vec4f {
282
+ let data = uniforms.volShadowLightData[{X}];
283
+ let direction = uniforms.volShadowLightDirection[{X}];
284
+ let falloff = uniforms.volShadowLightFalloff[{X}];
285
+
286
+ let chord = volLightChord(data, direction, falloff.x);
287
+ let t0 = max(chord.x, tStart);
288
+ let t1 = min(chord.y, tEnd);
289
+
290
+ if (t1 <= t0) {
291
+ return vec4f(0.0);
292
+ }
293
+
294
+ let diffuse = uniforms.volShadowLightDiffuse[{X}];
295
+
296
+ // The light clip space position of the points of the view ray is affine in their distance to the camera.
297
+ let clipOrigin = uniforms.volShadowMatrix[{X}] * vec4f(uniforms.volCameraPosition, 1.0);
298
+ let clipDirection = uniforms.volShadowMatrix[{X}] * vec4f(volRayDir, 0.0);
299
+ ${buildLocalLightIntegrationWGSL("f32({X}) + 0.5", "contribution *= mix(1.0, volShadow{X}(position, clipOrigin + clipDirection * t), falloff.w);")}
300
+ }
301
+ #endif
302
+ #endif
303
+ `.replace(/\{X\}/g, i.toString()));
304
+ globalSlotContributions.push(
305
+ /* wgsl */ `
306
+ #if VOL_SHADOW_SLOT_COUNT > {X}
307
+ #if VOL_SHADOW_LOCAL{X} == 0
308
+ {
309
+ // Recomputed rather than kept affine to keep fewer registers alive across the march. @see the GLSL version.
310
+ let shadow = volShadow{X}(position, uniforms.volShadowMatrix[{X}] * vec4f(uniforms.volCameraPosition + position, 1.0));
311
+ scattering += volEvalDirectionalLight(uniforms.volShadowLightData[{X}], uniforms.volShadowLightDiffuse[{X}]) * mix(1.0, shadow, uniforms.volShadowLightFalloff[{X}].w);
312
+ stepLights += 1.0;
313
+ }
314
+ #endif
315
+ #endif
316
+ `.replace(/\{X\}/g, i.toString()));
317
+ localSlotContributions.push(
318
+ /* wgsl */ `
319
+ #if VOL_SHADOW_SLOT_COUNT > {X}
320
+ #if VOL_SHADOW_LOCAL{X} == 1
321
+ lightResult = volIntegrateShadowSlot{X}(tStart, tEnd);
322
+ accumulated += lightResult.xyz;
323
+ evaluatedLights += lightResult.w;
324
+ #endif
325
+ #endif
326
+ `.replace(/\{X\}/g, i.toString()));
327
+ }
328
+ return /* wgsl */ `
329
+ varying vUV: vec2f;
330
+
331
+ // Full resolution linear depth of the scene, written by the linear depth pass.
332
+ var textureSampler: texture_2d<f32>;
333
+
334
+ // Ratio between the resolution of the linear depth and the resolution of the scattering buffer.
335
+ uniform volDepthRatio: vec2f;
336
+
337
+ uniform volInverseViewProjection: mat4x4f;
338
+ uniform volCameraPosition: vec3f;
339
+ uniform volCameraForward: vec3f;
340
+ uniform volCameraMinMaxZ: vec2f;
341
+ uniform volFrameIndex: f32;
342
+
343
+ uniform volFogInfos: vec4f;
344
+ uniform volFogColor: vec3f;
345
+ uniform volLinearFogEps: f32;
346
+ uniform volMedium: vec4f;
347
+
348
+ #ifdef VOL_SCREEN_SHADOWS
349
+ uniform volViewProjection: mat4x4f;
350
+ uniform volScreenShadowParams: vec4f;
351
+ #endif
352
+ uniform volAmbient: vec3f;
353
+
354
+ // (maximum distance, dithering strength, light extinction clamp, number of point and spot lights in the arrays)
355
+ uniform volParams: vec4f;
356
+
357
+ #if VOL_MAX_ARRAY_LIGHTS > 0
358
+ uniform volLightData: array<vec4f, VOL_MAX_ARRAY_LIGHTS>;
359
+ uniform volLightDiffuse: array<vec4f, VOL_MAX_ARRAY_LIGHTS>;
360
+ uniform volLightDirection: array<vec4f, VOL_MAX_ARRAY_LIGHTS>;
361
+ uniform volLightFalloff: array<vec4f, VOL_MAX_ARRAY_LIGHTS>;
362
+ #endif
363
+
364
+ #if VOL_SHADOW_SLOT_COUNT > 0
365
+ uniform volShadowLightData: array<vec4f, VOL_SHADOW_SLOT_COUNT>;
366
+ uniform volShadowLightDiffuse: array<vec4f, VOL_SHADOW_SLOT_COUNT>;
367
+ uniform volShadowLightDirection: array<vec4f, VOL_SHADOW_SLOT_COUNT>;
368
+ uniform volShadowLightFalloff: array<vec4f, VOL_SHADOW_SLOT_COUNT>;
369
+ uniform volShadowInfo: array<vec4f, VOL_SHADOW_SLOT_COUNT>;
370
+ uniform volShadowMatrix: array<mat4x4f, VOL_SHADOW_SLOT_COUNT>;
371
+ #endif
372
+
373
+ #ifdef VOL_CSM
374
+ uniform volCsmLightData: vec4f;
375
+ uniform volCsmLightDiffuse: vec4f;
376
+ uniform volCsmLightDirection: vec4f;
377
+ uniform volCsmLightFalloff: vec4f;
378
+ uniform volCsmInfo: vec4f;
379
+ uniform volCsmMatrices: array<mat4x4f, VOL_CSM_CASCADES>;
380
+
381
+ #if VOL_CSM_KIND == 1
382
+ var volCsmSamplerSampler: sampler_comparison;
383
+ var volCsmSampler: texture_depth_2d_array;
384
+ #else
385
+ var volCsmSamplerSampler: sampler;
386
+ var volCsmSampler: texture_2d_array<f32>;
387
+ #endif
388
+ #endif
389
+
390
+ // Values shared by every function of the pass, computed once per pixel. Every position the raymarching
391
+ // manipulates is relative to the camera, which keeps the precision of the lights far from the origin.
392
+ var<private> volRayDir: vec3f;
393
+ var<private> volCosForward: f32;
394
+ var<private> volNoise: f32;
395
+ var<private> volJitter: f32;
396
+
397
+ #ifdef VOL_SCREEN_SHADOWS
398
+ var<private> volDepthSize: vec2i;
399
+ var<private> volClipOrigin: vec4f;
400
+ var<private> volClipDirection: vec4f;
401
+ #endif
402
+
403
+ ${packingHelpers}
404
+ ${buildLinearDepthHelpers("textureSampler")}
405
+
406
+ fn volMod(x: f32, y: f32) -> f32 {
407
+ return x - y * floor(x / y);
408
+ }
409
+
410
+ fn volFallOff(value: f32, clipSpaceXY: vec2f, frustumEdgeFalloff: f32) -> f32 {
411
+ let mask = smoothstep(1.0 - frustumEdgeFalloff, 1.00000012, clamp(dot(clipSpaceXY, clipSpaceXY), 0.0, 1.0));
412
+ return mix(value, 1.0, mask);
413
+ }
414
+
415
+ // Reproduces the depth metric computed by the "shadowsVertex" include of Babylon.js: the raw clip space
416
+ // Z is used, without the perspective divide, so the comparison matches what the shadow map stores.
417
+ fn volDepthMetric(clip: vec4f, depthValues: vec2f) -> f32 {
418
+ #ifdef VOL_REVERSE_DEPTH
419
+ return (-clip.z + depthValues.x) / depthValues.y;
420
+ #else
421
+ return (clip.z + depthValues.x) / depthValues.y;
422
+ #endif
423
+ }
424
+
425
+ // Henyey-Greenstein phase function, normalised to 1 for an isotropic medium. @see the GLSL version.
426
+ fn volPhaseHG(c: f32, g: f32) -> f32 {
427
+ let g2 = g * g;
428
+ let d = max(1.0 + g2 - 2.0 * g * c, 1e-4);
429
+ return (1.0 - g2) / (d * sqrt(d));
430
+ }
431
+
432
+ fn volBayer4(p: vec2f) -> f32 {
433
+ let p1 = vec2f(volMod(floor(p.x), 2.0), volMod(floor(p.y), 2.0));
434
+ let p2 = vec2f(volMod(floor(p.x * 0.5), 2.0), volMod(floor(p.y * 0.5), 2.0));
435
+
436
+ let b1 = 2.0 * p1.x + 3.0 * p1.y - 4.0 * p1.x * p1.y;
437
+ let b2 = 2.0 * p2.x + 3.0 * p2.y - 4.0 * p2.x * p2.y;
438
+
439
+ return (b1 * 4.0 + b2) / 16.0;
440
+ }
441
+
442
+ fn volInterleavedGradientNoise(p: vec2f) -> f32 {
443
+ return fract(52.9829189 * fract(dot(p, vec2f(0.06711056, 0.00583715))));
444
+ }
445
+
446
+ // Returns the offset of the samples of a light inside their strata. @see the GLSL version.
447
+ fn volStratumOffset(seed: f32) -> f32 {
448
+ #if VOL_DITHER == 0
449
+ return 0.5;
450
+ #else
451
+ return mix(0.5, fract(volNoise + seed * 0.618034), uniforms.volParams.y);
452
+ #endif
453
+ }
454
+
455
+ // sigma(t) = -d/dt ln(T(t)), the exact analytic derivative of the optical depth. @see the GLSL version.
456
+ fn volFogExtinction(t: f32) -> f32 {
457
+ #if VOL_FOG_MODE == 2
458
+ return select(1.0 / max(uniforms.volFogInfos.z - t, uniforms.volLinearFogEps), 0.0, t < uniforms.volFogInfos.y);
459
+ #elif VOL_FOG_MODE == 1
460
+ return 2.0 * uniforms.volFogInfos.w * uniforms.volFogInfos.w * t;
461
+ #else
462
+ return uniforms.volFogInfos.w;
463
+ #endif
464
+ }
465
+
466
+ // "height" is the absolute altitude of the sample, "t" its distance to the camera.
467
+ fn volExtinctionAt(height: f32, t: f32) -> f32 {
468
+ var sigma = volFogExtinction(t);
469
+
470
+ #ifdef VOL_HEIGHT_FOG
471
+ sigma *= exp(-max(0.0, height - uniforms.volMedium.y) * uniforms.volMedium.z);
472
+ #endif
473
+
474
+ return sigma;
475
+ }
476
+
477
+ // Transmittance of the medium between the camera and the point of the view ray at the distance "t".
478
+ // @see the GLSL version.
479
+ #if VOL_TRANSMITTANCE == 1
480
+ fn volHeightSegment(s1: f32, s2: f32) -> f32 {
481
+ let y0 = uniforms.volCameraPosition.y - uniforms.volMedium.y;
482
+ let h1 = y0 + s1 * volRayDir.y;
483
+ let h2 = y0 + s2 * volRayDir.y;
484
+
485
+ if (h1 + h2 <= 0.0) {
486
+ return s2 - s1;
487
+ }
488
+
489
+ let e1 = exp(-uniforms.volMedium.z * max(h1, 0.0));
490
+ let e2 = exp(-uniforms.volMedium.z * max(h2, 0.0));
491
+ let slope = uniforms.volMedium.z * volRayDir.y;
492
+
493
+ return select((e1 - e2) / slope, 0.5 * (e1 + e2) * (s2 - s1), abs(slope * (s2 - s1)) < 1e-4);
494
+ }
495
+
496
+ fn volOpticalDepth(t: f32) -> f32 {
497
+ let crossing = select(-1.0, (uniforms.volMedium.y - uniforms.volCameraPosition.y) / volRayDir.y, abs(volRayDir.y) > 1e-6);
498
+
499
+ var opticalLength = 0.0;
500
+ if (crossing > 0.0 && crossing < t) {
501
+ opticalLength = volHeightSegment(0.0, crossing) + volHeightSegment(crossing, t);
502
+ } else {
503
+ opticalLength = volHeightSegment(0.0, t);
504
+ }
505
+
506
+ return uniforms.volFogInfos.w * opticalLength;
507
+ }
508
+
509
+ fn volTransmittance(t: f32) -> f32 {
510
+ return exp(-volOpticalDepth(t));
511
+ }
512
+ #elif VOL_TRANSMITTANCE == 2
513
+ var<private> volOpticalDepths: array<f32, VOL_TRANSMITTANCE_SAMPLES + 1>;
514
+ var<private> volOpticalDepthStart: f32;
515
+ var<private> volOpticalDepthScale: f32;
516
+
517
+ fn volBuildOpticalDepths(tStart: f32, tEnd: f32) {
518
+ let stepSize = (tEnd - tStart) / f32(VOL_TRANSMITTANCE_SAMPLES);
519
+
520
+ volOpticalDepthStart = tStart;
521
+ volOpticalDepthScale = 1.0 / stepSize;
522
+
523
+ var previous = volExtinctionAt(uniforms.volCameraPosition.y + volRayDir.y * tStart, tStart);
524
+ var opticalDepth = previous * tStart;
525
+
526
+ volOpticalDepths[0] = opticalDepth;
527
+
528
+ for (var i: i32 = 1; i <= VOL_TRANSMITTANCE_SAMPLES; i++) {
529
+ let t = tStart + stepSize * f32(i);
530
+ let sigma = volExtinctionAt(uniforms.volCameraPosition.y + volRayDir.y * t, t);
531
+
532
+ opticalDepth += 0.5 * (previous + sigma) * stepSize;
533
+ volOpticalDepths[i] = opticalDepth;
534
+ previous = sigma;
535
+ }
536
+ }
537
+
538
+ fn volOpticalDepth(t: f32) -> f32 {
539
+ let x = clamp((t - volOpticalDepthStart) * volOpticalDepthScale, 0.0, f32(VOL_TRANSMITTANCE_SAMPLES));
540
+ let i = min(i32(x), VOL_TRANSMITTANCE_SAMPLES - 1);
541
+
542
+ return mix(volOpticalDepths[i], volOpticalDepths[i + 1], x - f32(i));
543
+ }
544
+
545
+ fn volTransmittance(t: f32) -> f32 {
546
+ return exp(-volOpticalDepth(t));
547
+ }
548
+ #elif VOL_FOG_MODE == 2
549
+ fn volTransmittance(t: f32) -> f32 {
550
+ return clamp((uniforms.volFogInfos.z - t) / max(uniforms.volFogInfos.z - uniforms.volFogInfos.y, uniforms.volLinearFogEps), 0.0, 1.0);
551
+ }
552
+ #else
553
+ fn volOpticalDepth(t: f32) -> f32 {
554
+ #if VOL_FOG_MODE == 1
555
+ let x = t * uniforms.volFogInfos.w;
556
+ return x * x;
557
+ #else
558
+ return t * uniforms.volFogInfos.w;
559
+ #endif
560
+ }
561
+
562
+ fn volTransmittance(t: f32) -> f32 {
563
+ return exp(-volOpticalDepth(t));
564
+ }
565
+ #endif
566
+
567
+ // Transmittance of the medium to the point at the distance "t", combined with the attenuation of the light on
568
+ // its way to that point. @see the GLSL version.
569
+ fn volCombinedTransmittance(t: f32, sigmaT: f32, distanceToLight: f32) -> f32 {
570
+ #ifdef VOL_LIGHT_EXTINCTION
571
+ let lightOpticalDepth = sigmaT * min(distanceToLight, uniforms.volParams.z);
572
+
573
+ #ifdef VOL_OPTICAL_DEPTH
574
+ return exp(-(volOpticalDepth(t) + lightOpticalDepth));
575
+ #else
576
+ return volTransmittance(t) * exp(-lightOpticalDepth);
577
+ #endif
578
+ #else
579
+ return volTransmittance(t);
580
+ #endif
581
+ }
582
+
583
+ #ifdef VOL_SCREEN_SHADOWS
584
+ // Walks a segment going from a point of the medium towards a light through the depth buffer. The segment is
585
+ // given in clip space, where it is affine. @see the GLSL version.
586
+ fn volScreenShadow(clipStart: vec4f, clipDelta: vec4f, depthStart: f32, depthDelta: f32) -> f32 {
587
+ for (var i: i32 = 1; i <= VOL_SCREEN_SHADOW_STEPS; i++) {
588
+ let u = (f32(i) + volJitter) * (1.0 / f32(VOL_SCREEN_SHADOW_STEPS + 1));
589
+
590
+ let clip = clipStart + clipDelta * u;
591
+ if (clip.w <= 0.0) {
592
+ continue;
593
+ }
594
+
595
+ let uv = (clip.xy / clip.w) * 0.5 + 0.5;
596
+ if (uv.x < 0.0 || uv.x >= 1.0 || uv.y < 0.0 || uv.y >= 1.0) {
597
+ continue;
598
+ }
599
+
600
+ let sceneDepth = volReadLinearDepth(vec2i(uv * vec2f(volDepthSize)));
601
+
602
+ let depthDifference = depthStart + depthDelta * u - sceneDepth;
603
+ let bias = max(uniforms.volScreenShadowParams.y * sceneDepth, uniforms.volCameraMinMaxZ.x);
604
+
605
+ if (depthDifference > bias && depthDifference < uniforms.volScreenShadowParams.z * sceneDepth) {
606
+ return 0.0;
607
+ }
608
+ }
609
+
610
+ return 1.0;
611
+ }
612
+
613
+ // Occlusion of a point or a spot light, seen from the point of the view ray at the distance "t".
614
+ fn volScreenShadowToLight(t: f32, lightClip: vec4f, lightDepth: f32, distanceToLight: f32) -> f32 {
615
+ let clipStart = volClipOrigin + volClipDirection * t;
616
+ let depthStart = t * volCosForward;
617
+ let fraction = min(1.0, uniforms.volScreenShadowParams.x / max(distanceToLight, 1e-4));
618
+
619
+ return volScreenShadow(clipStart, (lightClip - clipStart) * fraction, depthStart, (lightDepth - depthStart) * fraction);
620
+ }
621
+ #endif
622
+
623
+ // Evaluates the in-scattered radiance coming from a directional light, without any shadowing.
624
+ fn volEvalDirectionalLight(data: vec4f, diffuse: vec4f) -> vec3f {
625
+ return diffuse.rgb * volPhaseHG(dot(volRayDir, data.xyz), diffuse.a);
626
+ }
627
+
628
+ // Evaluates the in-scattered radiance coming from a point or a spot light at the given position, relative to
629
+ // the camera, without any shadowing nor extinction. Returns the radiance in "xyz" and the distance to the light in "w".
630
+ fn volEvalLocalLight(data: vec4f, diffuse: vec4f, direction: vec4f, falloff: vec4f, position: vec3f) -> vec4f {
631
+ let toLight = data.xyz - position;
632
+ let distanceToLight = max(length(toLight), 1e-4);
633
+ let lightVector = toLight / distanceToLight;
634
+
635
+ // Same linear attenuation as "computeLighting" in the "lightsFragmentFunctions" include.
636
+ var attenuation = max(0.0, 1.0 - distanceToLight / falloff.x);
637
+
638
+ if (data.w == 2.0) {
639
+ let cosAngle = max(0.0, dot(direction.xyz, -lightVector));
640
+ attenuation *= select(0.0, max(0.0, pow(cosAngle, falloff.y)), cosAngle >= direction.w);
641
+ }
642
+
643
+ if (attenuation <= 0.0) {
644
+ return vec4f(0.0, 0.0, 0.0, distanceToLight);
645
+ }
646
+
647
+ return vec4f(diffuse.rgb * (attenuation * volPhaseHG(dot(volRayDir, lightVector), diffuse.a)), distanceToLight);
648
+ }
649
+
650
+ // Returns the part of the view ray crossing the cone of a spot light, cut by the sphere of its range.
651
+ // @see the GLSL version.
652
+ fn volConeChord(apex: vec3f, axis: vec3f, cosAngle: f32, range: f32, sphere: vec2f) -> vec2f {
653
+ if (cosAngle <= 1e-3) {
654
+ return sphere;
655
+ }
656
+
657
+ let origin = apex * (-1.0 / range);
658
+ let dv = dot(volRayDir, axis);
659
+ let ov = dot(origin, axis);
660
+ let cos2 = cosAngle * cosAngle;
661
+
662
+ let a = dv * dv - cos2;
663
+ let b = dv * ov - cos2 * dot(volRayDir, origin);
664
+ let c = ov * ov - cos2 * dot(origin, origin);
665
+
666
+ if (abs(a) < 1e-4) {
667
+ return sphere;
668
+ }
669
+
670
+ let discriminant = b * b - a * c;
671
+ var cone = vec2f(0.0);
672
+
673
+ if (a < 0.0) {
674
+ if (discriminant <= 0.0) {
675
+ return vec2f(1.0, -1.0);
676
+ }
677
+
678
+ let s = sqrt(discriminant);
679
+ cone = vec2f((-b + s) / a, (-b - s) / a);
680
+
681
+ if (dv * 0.5 * (cone.x + cone.y) + ov < 0.0) {
682
+ return vec2f(1.0, -1.0);
683
+ }
684
+ } else {
685
+ let s = sqrt(max(discriminant, 0.0));
686
+ cone = select(vec2f(-1e30, (-b - s) / a), vec2f((-b + s) / a, 1e30), dv > 0.0);
687
+ }
688
+
689
+ return vec2f(max(sphere.x, (cone.x - 0.005) * range), min(sphere.y, (cone.y + 0.005) * range));
690
+ }
691
+
692
+ // Returns the part of the view ray, as distances to the camera, crossing the volume a point or a spot light
693
+ // reaches. @see the GLSL version.
694
+ fn volLightChord(data: vec4f, direction: vec4f, range: f32) -> vec2f {
695
+ let closest = dot(data.xyz, volRayDir);
696
+ let offset = data.xyz - volRayDir * closest;
697
+ let halfChord2 = range * range - dot(offset, offset);
698
+
699
+ if (halfChord2 <= 0.0) {
700
+ return vec2f(1.0, -1.0);
701
+ }
702
+
703
+ let halfChord = sqrt(halfChord2);
704
+ var chord = vec2f(closest - halfChord, closest + halfChord);
705
+
706
+ if (data.w == 2.0) {
707
+ chord = volConeChord(data.xyz, direction.xyz, direction.w, range, chord);
708
+ }
709
+
710
+ return chord;
711
+ }
712
+
713
+ // Returns the number of samples taken over the given chord of a light. @see the GLSL version.
714
+ fn volLocalSampleCount(chordLength: f32, range: f32, rayLength: f32) -> i32 {
715
+ let count = max(f32(VOL_LIGHT_STEPS) * chordLength / (2.0 * range), f32(VOL_STEPS) * chordLength / rayLength);
716
+ return clamp(i32(ceil(count)), 2, VOL_LOCAL_MAX_STEPS);
717
+ }
718
+
719
+ ${slots.join("\n")}
720
+
721
+ #ifdef VOL_CSM
722
+ // The parts of the view ray inside the box of each cascade, and the clip space position of the view ray in the
723
+ // current cascade. @see the GLSL version.
724
+ var<private> volCsmEntries: vec4f;
725
+ var<private> volCsmExits: vec4f;
726
+ var<private> volCsmCurrent: i32;
727
+ var<private> volCsmClipOrigin: vec4f;
728
+ var<private> volCsmClipDirection: vec4f;
729
+
730
+ // Returns the part of the view ray inside the box of a cascade. @see the GLSL version.
731
+ fn volCsmBoxInterval(origin: vec4f, direction: vec4f) -> vec2f {
732
+ #ifdef VOL_NDC_HALF_Z
733
+ let boxMin = vec3f(-0.96, -0.96, 0.0);
734
+ #else
735
+ let boxMin = vec3f(-0.96, -0.96, -1.0);
736
+ #endif
737
+ let boxMax = vec3f(0.96, 0.96, 1.0);
738
+
739
+ let safeDirection = select(vec3f(1e-9), direction.xyz, abs(direction.xyz) > vec3f(1e-9));
740
+ let inverseDirection = 1.0 / safeDirection;
741
+ let t0 = (boxMin - origin.xyz) * inverseDirection;
742
+ let t1 = (boxMax - origin.xyz) * inverseDirection;
743
+ let tMin = min(t0, t1);
744
+ let tMax = max(t0, t1);
745
+
746
+ return vec2f(max(max(tMin.x, tMin.y), tMin.z), min(min(tMax.x, tMax.y), tMax.z));
747
+ }
748
+
749
+ fn volPrepareCsm() {
750
+ volCsmEntries = vec4f(1e30);
751
+ volCsmExits = vec4f(-1e30);
752
+
753
+ for (var cascade: i32 = 0; cascade < VOL_CSM_CASCADES; cascade++) {
754
+ let interval = volCsmBoxInterval(uniforms.volCsmMatrices[cascade] * vec4f(uniforms.volCameraPosition, 1.0), uniforms.volCsmMatrices[cascade] * vec4f(volRayDir, 0.0));
755
+ volCsmEntries[cascade] = interval.x;
756
+ volCsmExits[cascade] = interval.y;
757
+ }
758
+
759
+ volCsmCurrent = -1;
760
+ }
761
+
762
+ // Returns the shadowing of the point of the view ray at the distance "t" from the camera.
763
+ fn volCsmShadow(t: f32) -> f32 {
764
+ // The first cascade containing the sample is the most detailed one.
765
+ var cascade: i32 = -1;
766
+ for (var i: i32 = VOL_CSM_CASCADES - 1; i >= 0; i--) {
767
+ if (t >= volCsmEntries[i] && t <= volCsmExits[i]) {
768
+ cascade = i;
769
+ }
770
+ }
771
+
772
+ if (cascade < 0) {
773
+ return 1.0;
774
+ }
775
+
776
+ if (cascade != volCsmCurrent) {
777
+ volCsmCurrent = cascade;
778
+ volCsmClipOrigin = uniforms.volCsmMatrices[cascade] * vec4f(uniforms.volCameraPosition, 1.0);
779
+ volCsmClipDirection = uniforms.volCsmMatrices[cascade] * vec4f(volRayDir, 0.0);
780
+ }
781
+
782
+ // The projection of a directional light is orthographic, "w" is always 1.
783
+ let clip = volCsmClipOrigin + volCsmClipDirection * t;
784
+ let clipSpace = clip.xyz;
785
+
786
+ #if VOL_CSM_KIND == 1
787
+ var uvDepth = 0.5 * clipSpace + vec3f(0.5);
788
+ #ifdef VOL_NDC_HALF_Z
789
+ uvDepth.z = clipSpace.z;
790
+ #endif
791
+
792
+ let shadow = textureSampleCompareLevel(volCsmSampler, volCsmSamplerSampler, uvDepth.xy, cascade, uvDepth.z);
793
+ return volFallOff(mix(uniforms.volCsmInfo.x, 1.0, shadow), clipSpace.xy, uniforms.volCsmLightFalloff.z);
794
+ #else
795
+ let uv = 0.5 * clipSpace.xy + vec2f(0.5);
796
+ let depthMetric = clamp(volDepthMetric(clip, uniforms.volCsmInfo.zw), 0.0, 1.0);
797
+
798
+ #ifdef VOL_CSM_PACKED
799
+ let shadowMapSample = volUnpack(textureSampleLevel(volCsmSampler, volCsmSamplerSampler, uv, cascade, 0.0));
800
+ #else
801
+ let shadowMapSample = textureSampleLevel(volCsmSampler, volCsmSamplerSampler, uv, cascade, 0.0).x;
802
+ #endif
803
+
804
+ return select(1.0, volFallOff(uniforms.volCsmInfo.x, clipSpace.xy, uniforms.volCsmLightFalloff.z), depthMetric > shadowMapSample);
805
+ #endif
806
+ }
807
+ #endif
808
+
809
+ #if VOL_GLOBAL_LIGHT_COUNT > 0
810
+ // Marches the whole view ray for the lights that reach all of it: the directional lights. Returns the
811
+ // in-scattered radiance in "xyz" and the number of lights evaluated in "w".
812
+ fn volMarchGlobalLights(tStart: f32, tEnd: f32) -> vec4f {
813
+ #if VOL_DIRECTIONAL_LIGHT_COUNT > 0
814
+ #ifdef VOL_SCREEN_SHADOWS
815
+ // The occlusion search of a directional light always walks the same direction over the same distance,
816
+ // only its starting point moves along the view ray.
817
+ var directionClip: array<vec4f, VOL_DIRECTIONAL_LIGHT_COUNT>;
818
+ var directionDepth: array<f32, VOL_DIRECTIONAL_LIGHT_COUNT>;
819
+
820
+ for (var li: i32 = 0; li < VOL_DIRECTIONAL_LIGHT_COUNT; li++) {
821
+ let traced = uniforms.volLightData[li].xyz * uniforms.volScreenShadowParams.x;
822
+ directionClip[li] = uniforms.volViewProjection * vec4f(traced, 0.0);
823
+ directionDepth[li] = dot(traced, uniforms.volCameraForward);
824
+ }
825
+ #endif
826
+ #endif
827
+
828
+ #if VOL_DISTRIBUTION == 1
829
+ let ratio = pow(tEnd / tStart, 1.0 / f32(VOL_STEPS));
830
+ #else
831
+ let uniformStep = (tEnd - tStart) / f32(VOL_STEPS);
832
+ #endif
833
+
834
+ var result = vec3f(0.0);
835
+ var evaluatedLights = 0.0;
836
+ var segmentStart = tStart;
837
+
838
+ for (var i: i32 = 0; i < VOL_STEPS; i++) {
839
+ #if VOL_DISTRIBUTION == 1
840
+ let segmentEnd = segmentStart * ratio;
841
+ #else
842
+ let segmentEnd = segmentStart + uniformStep;
843
+ #endif
844
+
845
+ let segmentBegin = segmentStart;
846
+ let dt = segmentEnd - segmentBegin;
847
+
848
+ // The jitter picks the sample inside its own segment, so the marched interval stays exactly
849
+ // [tStart, tEnd] whatever the dithering does.
850
+ let t = segmentBegin + dt * volJitter;
851
+ segmentStart = segmentEnd;
852
+
853
+ let position = volRayDir * t;
854
+ let sigmaT = volExtinctionAt(uniforms.volCameraPosition.y + position.y, t);
855
+
856
+ if (sigmaT <= 1e-6) {
857
+ continue;
858
+ }
859
+
860
+ // The transmittance at the START of the segment: the factor below accounts for the extinction
861
+ // across the segment itself.
862
+ let viewTransmittance = volTransmittance(segmentBegin);
863
+ if (viewTransmittance < 0.003) {
864
+ break;
865
+ }
866
+
867
+ var scattering = vec3f(0.0);
868
+ var stepLights = 0.0;
869
+
870
+ ${globalSlotContributions.join("")}
871
+ #ifdef VOL_CSM
872
+ {
873
+ scattering += volEvalDirectionalLight(uniforms.volCsmLightData, uniforms.volCsmLightDiffuse) * mix(1.0, volCsmShadow(t), uniforms.volCsmLightFalloff.w);
874
+ stepLights += 1.0;
875
+ }
876
+ #endif
877
+
878
+ #if VOL_DIRECTIONAL_LIGHT_COUNT > 0
879
+ for (var li: i32 = 0; li < VOL_DIRECTIONAL_LIGHT_COUNT; li++) {
880
+ var contribution = volEvalDirectionalLight(uniforms.volLightData[li], uniforms.volLightDiffuse[li]);
881
+
882
+ #ifdef VOL_SCREEN_SHADOWS
883
+ // "volLightFalloff[li].z" carries whether this light asked to be occluded by the geometry.
884
+ if (uniforms.volLightFalloff[li].z > 0.5) {
885
+ contribution *= volScreenShadow(volClipOrigin + volClipDirection * t, directionClip[li], t * volCosForward, directionDepth[li]);
886
+ }
887
+ #endif
888
+
889
+ scattering += contribution;
890
+ stepLights += 1.0;
891
+ }
892
+ #endif
893
+
894
+ evaluatedLights = max(evaluatedLights, stepLights);
895
+
896
+ // Analytic integral of the in-scattering over the segment. @see the GLSL version.
897
+ result += scattering * (viewTransmittance * (1.0 - exp(-sigmaT * dt)));
898
+ }
899
+
900
+ return vec4f(result, evaluatedLights);
901
+ }
902
+ #endif
903
+
904
+ #if VOL_LOCAL_ARRAY_LIGHTS == 1
905
+ // Integrates a point or a spot light of the arrays over the part of the view ray crossing its volume.
906
+ // Returns the in-scattered radiance in "xyz" and whether the light was evaluated in "w".
907
+ fn volIntegrateArrayLight(index: i32, tStart: f32, tEnd: f32) -> vec4f {
908
+ let data = uniforms.volLightData[index];
909
+ let direction = uniforms.volLightDirection[index];
910
+ let falloff = uniforms.volLightFalloff[index];
911
+
912
+ let chord = volLightChord(data, direction, falloff.x);
913
+ let t0 = max(chord.x, tStart);
914
+ let t1 = min(chord.y, tEnd);
915
+
916
+ if (t1 <= t0) {
917
+ return vec4f(0.0);
918
+ }
919
+
920
+ let diffuse = uniforms.volLightDiffuse[index];
921
+
922
+ #ifdef VOL_SCREEN_SHADOWS
923
+ // "falloff.z" carries whether this light asked to be occluded by the geometry.
924
+ let occluded = falloff.z > 0.5;
925
+ let lightClip = volClipOrigin + uniforms.volViewProjection * vec4f(data.xyz, 0.0);
926
+ let lightDepth = dot(data.xyz, uniforms.volCameraForward);
927
+ #endif
928
+
929
+ ${buildLocalLightIntegrationWGSL("f32(index)", `#ifdef VOL_SCREEN_SHADOWS
930
+ if (occluded) {
931
+ contribution *= volScreenShadowToLight(t, lightClip, lightDepth, distanceToLight);
932
+ }
933
+ #endif`)}
934
+ }
935
+ #endif
936
+
937
+ @fragment
938
+ fn main(input: FragmentInputs) -> FragmentOutputs {
939
+ // Reconstruct the world space direction of the view ray from the UV of the pixel.
940
+ let farPoint = uniforms.volInverseViewProjection * vec4f(input.vUV * 2.0 - 1.0, 1.0, 1.0);
941
+ volRayDir = normalize(farPoint.xyz / farPoint.w - uniforms.volCameraPosition);
942
+
943
+ // The depth map stores the distance along the view axis, the ray is not aligned with it.
944
+ volCosForward = max(dot(volRayDir, uniforms.volCameraForward), 1e-4);
945
+ let pixel = fragmentInputs.position.xy;
946
+ let depthSize = vec2i(textureDimensions(textureSampler));
947
+ let viewZ = volReadLinearDepth(volScatterToDepthTexel(vec2i(pixel), depthSize));
948
+
949
+ let tEnd = min(viewZ / volCosForward, uniforms.volParams.x);
950
+ let tStart = max(uniforms.volCameraMinMaxZ.x / volCosForward, 1e-3);
951
+
952
+ var result = vec3f(0.0);
953
+ var finalTransmittance = 1.0;
954
+ var evaluatedLights = 0.0;
955
+
956
+ // WGSL has no early return from the fragment entry point, the whole integration is guarded instead.
957
+ if (tEnd > tStart) {
958
+ // Offsets the samples inside their segments to trade the banding produced by a low sample count for noise.
959
+ volNoise = 0.0;
960
+ #if VOL_DITHER == 1
961
+ volNoise = volBayer4(pixel);
962
+ #elif VOL_DITHER == 2
963
+ var ditherPosition = pixel;
964
+ #ifdef VOL_TEMPORAL_JITTER
965
+ ditherPosition += uniforms.volFrameIndex * 5.588238;
966
+ #endif
967
+ volNoise = volInterleavedGradientNoise(ditherPosition);
968
+ #endif
969
+ volJitter = volNoise * uniforms.volParams.y;
970
+
971
+ #ifdef VOL_SCREEN_SHADOWS
972
+ volDepthSize = depthSize;
973
+ volClipOrigin = uniforms.volViewProjection * vec4f(uniforms.volCameraPosition, 1.0);
974
+ volClipDirection = uniforms.volViewProjection * vec4f(volRayDir, 0.0);
975
+ #endif
976
+
977
+ #if VOL_TRANSMITTANCE == 2
978
+ volBuildOpticalDepths(tStart, tEnd);
979
+ #endif
980
+
981
+ #ifdef VOL_CSM
982
+ volPrepareCsm();
983
+ #endif
984
+
985
+ // The in-scattering of the constant ambient light has a closed form whatever the medium. @see the GLSL version.
986
+ var accumulated = uniforms.volAmbient * (volTransmittance(tStart) - volTransmittance(tEnd));
987
+ var lightResult = vec4f(0.0);
988
+
989
+ #if VOL_GLOBAL_LIGHT_COUNT > 0
990
+ lightResult = volMarchGlobalLights(tStart, tEnd);
991
+ accumulated += lightResult.xyz;
992
+ evaluatedLights += lightResult.w;
993
+ #endif
994
+
995
+ ${localSlotContributions.join("")}
996
+
997
+ #if VOL_LOCAL_ARRAY_LIGHTS == 1
998
+ let localEnd = VOL_DIRECTIONAL_LIGHT_COUNT + i32(uniforms.volParams.w + 0.5);
999
+
1000
+ for (var i: i32 = VOL_DIRECTIONAL_LIGHT_COUNT; i < VOL_MAX_ARRAY_LIGHTS; i++) {
1001
+ if (i >= localEnd) {
1002
+ break;
1003
+ }
1004
+
1005
+ lightResult = volIntegrateArrayLight(i, tStart, tEnd);
1006
+ accumulated += lightResult.xyz;
1007
+ evaluatedLights += lightResult.w;
1008
+ }
1009
+ #endif
1010
+
1011
+ // The scattering coefficient is a fraction of the extinction, which guarantees energy conservation.
1012
+ result = accumulated * uniforms.volMedium.x * uniforms.volFogColor;
1013
+ finalTransmittance = volTransmittance(tEnd);
1014
+ }
1015
+
1016
+ #if VOL_DEBUG == 3
1017
+ // From blue for no light to red for 16 lights or more evaluated by the pixel.
1018
+ let ratioOfBudget = clamp(evaluatedLights / 16.0, 0.0, 1.0);
1019
+ result = vec3f(ratioOfBudget, 1.0 - abs(ratioOfBudget * 2.0 - 1.0), 1.0 - ratioOfBudget);
1020
+ #endif
1021
+
1022
+ #ifdef VOL_LDR_ENCODE
1023
+ result = result / (1.0 + result);
1024
+ #endif
1025
+
1026
+ fragmentOutputs.color = vec4f(result, finalTransmittance);
1027
+ }
1028
+ `;
1029
+ }
1030
+ /**
1031
+ * The separable bilateral blur pass. @see buildVolumetricLightingBlurShader.
1032
+ */
1033
+ export function buildVolumetricLightingBlurShaderWGSL() {
1034
+ return /* wgsl */ `
1035
+ varying vUV: vec2f;
1036
+
1037
+ var textureSampler: texture_2d<f32>;
1038
+ var volLinearDepthSampler: texture_2d<f32>;
1039
+
1040
+ uniform volCameraMinMaxZ: vec2f;
1041
+ uniform volDepthRatio: vec2f;
1042
+ uniform volBlurDirection: vec2f;
1043
+ uniform volBlurParams: vec2f;
1044
+
1045
+ ${packingHelpers}
1046
+ ${buildLinearDepthHelpers("volLinearDepthSampler")}
1047
+
1048
+ @fragment
1049
+ fn main(input: FragmentInputs) -> FragmentOutputs {
1050
+ // Every tap is an exact texel compared using the depth the raymarching used for it. @see the GLSL version.
1051
+ let size = vec2i(textureDimensions(textureSampler));
1052
+ let depthSize = vec2i(textureDimensions(volLinearDepthSampler));
1053
+ let center = vec2i(fragmentInputs.position.xy);
1054
+ let direction = vec2i(uniforms.volBlurDirection);
1055
+
1056
+ let centerDepth = volReadLinearDepth(volScatterToDepthTexel(center, depthSize));
1057
+ let inverseDepthThreshold = 1.0 / max(uniforms.volBlurParams.y * max(centerDepth, uniforms.volCameraMinMaxZ.x), 1e-4);
1058
+ let sigma = max(uniforms.volBlurParams.x, 1e-4);
1059
+ let inverseTwoSigma2 = 1.0 / (2.0 * sigma * sigma);
1060
+
1061
+ var result = textureLoad(textureSampler, center, 0);
1062
+ var totalWeight = 1.0;
1063
+
1064
+ for (var i: i32 = 1; i <= VOL_BLUR_RADIUS; i++) {
1065
+ let spatialWeight = exp(-f32(i * i) * inverseTwoSigma2);
1066
+
1067
+ let positive = center + direction * i;
1068
+ let negative = center - direction * i;
1069
+
1070
+ if (positive.x < size.x && positive.y < size.y) {
1071
+ let weight = spatialWeight * exp(-abs(volReadLinearDepth(volScatterToDepthTexel(positive, depthSize)) - centerDepth) * inverseDepthThreshold);
1072
+ result += textureLoad(textureSampler, positive, 0) * weight;
1073
+ totalWeight += weight;
1074
+ }
1075
+
1076
+ if (negative.x >= 0 && negative.y >= 0) {
1077
+ let weight = spatialWeight * exp(-abs(volReadLinearDepth(volScatterToDepthTexel(negative, depthSize)) - centerDepth) * inverseDepthThreshold);
1078
+ result += textureLoad(textureSampler, negative, 0) * weight;
1079
+ totalWeight += weight;
1080
+ }
1081
+ }
1082
+
1083
+ fragmentOutputs.color = result / totalWeight;
1084
+ }
1085
+ `;
1086
+ }
1087
+ /**
1088
+ * The composition pass. @see buildVolumetricLightingComposeShader.
1089
+ */
1090
+ export function buildVolumetricLightingComposeShaderWGSL() {
1091
+ return /* wgsl */ `
1092
+ varying vUV: vec2f;
1093
+
1094
+ var textureSampler: texture_2d<f32>;
1095
+ var volSceneSamplerSampler: sampler;
1096
+ var volSceneSampler: texture_2d<f32>;
1097
+ var volLinearDepthSampler: texture_2d<f32>;
1098
+
1099
+ uniform volCameraMinMaxZ: vec2f;
1100
+ uniform volDepthRatio: vec2f;
1101
+ uniform volComposeParams: vec4f;
1102
+
1103
+ ${packingHelpers}
1104
+ ${buildLinearDepthHelpers("volLinearDepthSampler")}
1105
+
1106
+ fn volMod(x: f32, y: f32) -> f32 {
1107
+ return x - y * floor(x / y);
1108
+ }
1109
+
1110
+ fn volBayer8(p: vec2f) -> f32 {
1111
+ let p1 = vec2f(volMod(floor(p.x), 2.0), volMod(floor(p.y), 2.0));
1112
+ let p2 = vec2f(volMod(floor(p.x * 0.5), 2.0), volMod(floor(p.y * 0.5), 2.0));
1113
+ let p3 = vec2f(volMod(floor(p.x * 0.25), 2.0), volMod(floor(p.y * 0.25), 2.0));
1114
+
1115
+ let b1 = 2.0 * p1.x + 3.0 * p1.y - 4.0 * p1.x * p1.y;
1116
+ let b2 = 2.0 * p2.x + 3.0 * p2.y - 4.0 * p2.x * p2.y;
1117
+ let b3 = 2.0 * p3.x + 3.0 * p3.y - 4.0 * p3.x * p3.y;
1118
+
1119
+ return (b1 * 16.0 + b2 * 4.0 + b3) / 64.0;
1120
+ }
1121
+
1122
+ // Nearest depth upsampling. @see the GLSL version.
1123
+ fn volUpsample(uv0: vec2f, centerDepth: f32, depthSize: vec2i) -> vec4f {
1124
+ let size = vec2i(textureDimensions(textureSampler));
1125
+ let texelCoordinate = uv0 * vec2f(size) - 0.5;
1126
+ let baseCoordinate = floor(texelCoordinate);
1127
+ let fraction = texelCoordinate - baseCoordinate;
1128
+
1129
+ let base = vec2i(baseCoordinate);
1130
+ let maximum = size - vec2i(1);
1131
+
1132
+ let inverseDepthThreshold = 1.0 / max(uniforms.volComposeParams.z * max(centerDepth, uniforms.volCameraMinMaxZ.x), 1e-4);
1133
+
1134
+ var result = vec4f(0.0);
1135
+ var totalWeight = 0.0;
1136
+
1137
+ var nearestSample = vec4f(0.0);
1138
+ var nearestDistance = 1e20;
1139
+
1140
+ for (var y: i32 = 0; y < 2; y++) {
1141
+ for (var x: i32 = 0; x < 2; x++) {
1142
+ let coordinates = clamp(base + vec2i(x, y), vec2i(0), maximum);
1143
+
1144
+ let bilinearWeight = select(fraction.x, 1.0 - fraction.x, x == 0) * select(fraction.y, 1.0 - fraction.y, y == 0);
1145
+ let depthDistance = abs(volReadLinearDepth(volScatterToDepthTexel(coordinates, depthSize)) - centerDepth);
1146
+
1147
+ let tap = textureLoad(textureSampler, coordinates, 0);
1148
+
1149
+ if (depthDistance < nearestDistance) {
1150
+ nearestDistance = depthDistance;
1151
+ nearestSample = tap;
1152
+ }
1153
+
1154
+ let weight = bilinearWeight / (1e-4 + depthDistance * inverseDepthThreshold);
1155
+ result += tap * weight;
1156
+ totalWeight += weight;
1157
+ }
1158
+ }
1159
+
1160
+ // Every tap sits on the other side of a silhouette, fall back to the closest one instead of averaging.
1161
+ return select(nearestSample, result / totalWeight, totalWeight > 1e-4);
1162
+ }
1163
+
1164
+ @fragment
1165
+ fn main(input: FragmentInputs) -> FragmentOutputs {
1166
+ // The linear depth has the full resolution of the canvas. @see the GLSL version.
1167
+ let depthSize = vec2i(textureDimensions(volLinearDepthSampler));
1168
+ let centerDepth = volReadLinearDepth(min(vec2i(input.vUV * vec2f(depthSize)), depthSize - 1));
1169
+
1170
+ var volumetric = volUpsample(input.vUV, centerDepth, depthSize);
1171
+
1172
+ #ifdef VOL_LDR_ENCODE
1173
+ volumetric = vec4f(volumetric.rgb / max(vec3f(1e-4), 1.0 - volumetric.rgb), volumetric.a);
1174
+ #endif
1175
+
1176
+ // WGSL has no early return from the fragment entry point, so the debug outputs are branches instead.
1177
+ #if VOL_DEBUG == 1 || VOL_DEBUG == 3
1178
+ fragmentOutputs.color = vec4f(volumetric.rgb * uniforms.volComposeParams.x, 1.0);
1179
+ #elif VOL_DEBUG == 2
1180
+ fragmentOutputs.color = vec4f(vec3f(volumetric.a), 1.0);
1181
+ #else
1182
+ var sceneColor = textureSampleLevel(volSceneSampler, volSceneSamplerSampler, input.vUV, 0.0).rgb;
1183
+
1184
+ sceneColor *= mix(1.0, volumetric.a, uniforms.volComposeParams.y);
1185
+
1186
+ var result = sceneColor + volumetric.rgb * uniforms.volComposeParams.x;
1187
+
1188
+ // Breaks up the banding an 8 bits output would otherwise show on the smooth gradients of the shafts.
1189
+ result += (volBayer8(fragmentInputs.position.xy) - 0.5) * uniforms.volComposeParams.w;
1190
+
1191
+ fragmentOutputs.color = vec4f(result, 1.0);
1192
+ #endif
1193
+ }
1194
+ `;
1195
+ }
1196
+ /**
1197
+ * Returns the four WGSL sources of the pipeline, keyed by the name they are registered under.
1198
+ */
1199
+ export function getVolumetricLightingShadersWGSL() {
1200
+ return {
1201
+ linearDepth: buildVolumetricLightingLinearDepthShaderWGSL(),
1202
+ scattering: buildVolumetricLightingScatteringShaderWGSL(maxVolumetricShadowSlots),
1203
+ blur: buildVolumetricLightingBlurShaderWGSL(),
1204
+ compose: buildVolumetricLightingComposeShaderWGSL(),
1205
+ };
1206
+ }
1207
+ //# sourceMappingURL=shaders-wgsl.js.map