@zephyr3d/scene 0.9.26 → 0.9.27

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 (95) hide show
  1. package/dist/animation/animationset.js +92 -30
  2. package/dist/animation/animationset.js.map +1 -1
  3. package/dist/animation/joint_dynamics/convex_collider.js +320 -0
  4. package/dist/animation/joint_dynamics/convex_collider.js.map +1 -0
  5. package/dist/animation/skeleton.js +21 -1
  6. package/dist/animation/skeleton.js.map +1 -1
  7. package/dist/asset/model.js +6 -2
  8. package/dist/asset/model.js.map +1 -1
  9. package/dist/camera/camera.js +75 -23
  10. package/dist/camera/camera.js.map +1 -1
  11. package/dist/index.d.ts +4764 -3600
  12. package/dist/index.js +2 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/material/hairstrand.js +235 -235
  15. package/dist/material/hairstrand.js.map +1 -1
  16. package/dist/material/material.js +296 -296
  17. package/dist/material/material.js.map +1 -1
  18. package/dist/material/mixins/lit.js +33 -1
  19. package/dist/material/mixins/lit.js.map +1 -1
  20. package/dist/material/shader/helper.js +404 -3
  21. package/dist/material/shader/helper.js.map +1 -1
  22. package/dist/material/water.js +2057 -72
  23. package/dist/material/water.js.map +1 -1
  24. package/dist/posteffect/motionblur.js +663 -58
  25. package/dist/posteffect/motionblur.js.map +1 -1
  26. package/dist/posteffect/posteffect.js +12 -1
  27. package/dist/posteffect/posteffect.js.map +1 -1
  28. package/dist/posteffect/sao.js +46 -44
  29. package/dist/posteffect/sao.js.map +1 -1
  30. package/dist/posteffect/sss.js +30 -2
  31. package/dist/posteffect/sss.js.map +1 -1
  32. package/dist/render/abuffer_oit.js +37 -37
  33. package/dist/render/abuffer_oit.js.map +1 -1
  34. package/dist/render/clipmap.js +156 -5
  35. package/dist/render/clipmap.js.map +1 -1
  36. package/dist/render/cull_visitor.js +1 -0
  37. package/dist/render/cull_visitor.js.map +1 -1
  38. package/dist/render/dualdepthpeeling_oit.js +49 -49
  39. package/dist/render/dualdepthpeeling_oit.js.map +1 -1
  40. package/dist/render/fbm_wavegenerator.js +3 -3
  41. package/dist/render/fbm_wavegenerator.js.map +1 -1
  42. package/dist/render/fft_wavegenerator.js +57 -15
  43. package/dist/render/fft_wavegenerator.js.map +1 -1
  44. package/dist/render/gerstner_wavegenerator.js +127 -20
  45. package/dist/render/gerstner_wavegenerator.js.map +1 -1
  46. package/dist/render/hzb.js +80 -22
  47. package/dist/render/hzb.js.map +1 -1
  48. package/dist/render/lightpass.js +6 -3
  49. package/dist/render/lightpass.js.map +1 -1
  50. package/dist/render/oit.js +11 -11
  51. package/dist/render/oit.js.map +1 -1
  52. package/dist/render/render_queue.js +36 -0
  53. package/dist/render/render_queue.js.map +1 -1
  54. package/dist/render/renderer.js +4 -0
  55. package/dist/render/renderer.js.map +1 -1
  56. package/dist/render/rendergraph/blackboard.js +1 -0
  57. package/dist/render/rendergraph/blackboard.js.map +1 -1
  58. package/dist/render/rendergraph/forward_plus_builder.js +318 -9
  59. package/dist/render/rendergraph/forward_plus_builder.js.map +1 -1
  60. package/dist/render/rendergraph/frame_resource_requirements.js +1 -0
  61. package/dist/render/rendergraph/frame_resource_requirements.js.map +1 -1
  62. package/dist/render/rendergraph/history_resources.js +8 -1
  63. package/dist/render/rendergraph/history_resources.js.map +1 -1
  64. package/dist/render/underwater.js +624 -0
  65. package/dist/render/underwater.js.map +1 -0
  66. package/dist/render/water_caustics.js +1380 -0
  67. package/dist/render/water_caustics.js.map +1 -0
  68. package/dist/render/weightedblended_oit.js +33 -33
  69. package/dist/render/weightedblended_oit.js.map +1 -1
  70. package/dist/scene/water.js +540 -4
  71. package/dist/scene/water.js.map +1 -1
  72. package/dist/shaders/water.js +22 -19
  73. package/dist/shaders/water.js.map +1 -1
  74. package/dist/shaders/water_medium.js +56 -0
  75. package/dist/shaders/water_medium.js.map +1 -0
  76. package/dist/shadow/shadow_region.js +49 -49
  77. package/dist/shadow/shadow_region.js.map +1 -1
  78. package/dist/utility/serialization/manager.js +3 -1
  79. package/dist/utility/serialization/manager.js.map +1 -1
  80. package/dist/utility/serialization/scene/animation.js +31 -2
  81. package/dist/utility/serialization/scene/animation.js.map +1 -1
  82. package/dist/utility/serialization/scene/camera.js +53 -25
  83. package/dist/utility/serialization/scene/camera.js.map +1 -1
  84. package/dist/utility/serialization/scene/light.js.map +1 -1
  85. package/dist/utility/serialization/scene/water.js +1101 -21
  86. package/dist/utility/serialization/scene/water.js.map +1 -1
  87. package/dist/utility/shprojector.js +87 -87
  88. package/dist/utility/shprojector.js.map +1 -1
  89. package/package.json +1 -1
  90. package/dist/render/rendergraph/frame_graph_context.js +0 -41
  91. package/dist/render/rendergraph/frame_graph_context.js.map +0 -1
  92. package/dist/shaders/ssgi.js +0 -63
  93. package/dist/shaders/ssgi.js.map +0 -1
  94. package/dist/shaders/ssgi_reprojection.js +0 -118
  95. package/dist/shaders/ssgi_reprojection.js.map +0 -1
@@ -17,6 +17,28 @@ const UNIFORM_NAME_SKYDISTANTLIGHT_LUT = 'Z_UniformSkyDistantLightLUT';
17
17
  const UNIFORM_NAME_SHADOW_MAP = 'Z_UniformShadowMap';
18
18
  const UNIFORM_NAME_SHADOW_MASK = 'Z_UniformShadowMask';
19
19
  const UNIFORM_NAME_SHADOW_MASK_MODE = 'Z_UniformShadowMaskMode';
20
+ const UNIFORM_NAME_CAUSTIC_MAP = 'Z_UniformCausticMap';
21
+ const UNIFORM_NAME_CAUSTIC_HEIGHT_MAP = 'Z_UniformCausticHeightMap';
22
+ /**
23
+ * Offset added to a surface height before it is stored in the caustic height
24
+ * map, so that a cleared texel - no surface on that ray - is distinguishable
25
+ * from a surface at the rest level. A ray with no water on it must read as
26
+ * dry, not as submerged under a flat surface: on the down-sun side of a
27
+ * footprint edge, a receiver below the rest level but above a trough would
28
+ * otherwise be lit through water that is not there. Eight keeps troughs down
29
+ * to seven metres positive and leaves half floats millimetre precision.
30
+ * @internal
31
+ */ const CAUSTIC_HEIGHT_BIAS = 8;
32
+ /**
33
+ * Water bodies that can cast caustics into one map.
34
+ *
35
+ * They share the map and the slice, so this is not a limit on how much water a
36
+ * scene may hold - only on how many separate surfaces the receiver will test
37
+ * itself against, which it does per fragment. Four covers a lake with a couple
38
+ * of pools beside it; beyond that the renderer keeps the largest four, which
39
+ * are the ones a viewer is most likely to be standing over.
40
+ */ const MAX_CAUSTIC_WATERS = 4;
41
+ const UNIFORM_NAME_CAUSTIC_PARAMS = 'Z_UniformCausticParams';
20
42
  const UNIFORM_NAME_LINEAR_DEPTH_MAP = 'Z_UniformLinearDepth';
21
43
  const UNIFORM_NAME_LINEAR_DEPTH_MAP_SIZE = 'Z_UniformLinearDepthSize';
22
44
  const UNIFORM_NAME_SCENE_COLOR_MAP = 'Z_UniformSceneColor';
@@ -294,6 +316,9 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
294
316
  }
295
317
  scope[UNIFORM_NAME_SHADOW_MAP] = tex.uniform(0);
296
318
  }
319
+ if (this.usesWaterCaustics(ctx)) {
320
+ this.declareWaterCausticUniforms(pb);
321
+ }
297
322
  if (ctx.drawEnvLight) {
298
323
  ctx.env.light.envLight.initShaderBindings(pb, ctx);
299
324
  }
@@ -304,8 +329,18 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
304
329
  scope[UNIFORM_NAME_LINEAR_DEPTH_MAP_SIZE] = pb.vec2().uniform(0);
305
330
  }
306
331
  if (ctx.sceneColorTexture) {
307
- // Scene color copy: single mip level
308
- scope[UNIFORM_NAME_SCENE_COLOR_MAP] = pb.tex2D().uniform(0).withSampler(getSamplerOptions('clamp_linear_nomip'));
332
+ // Mip-filtered, because the refraction background is read at a LOD and
333
+ // not only at mip 0: the water picks one from how deep and how turbid
334
+ // the column it is looking through is. A `nomip` sampler pins mip 0
335
+ // whatever LOD the shader passes, so this and the copy's level count
336
+ // have to change together - and each backend only honours one of them.
337
+ // WebGPU binds the sampler declared here as a static binding the runtime
338
+ // cannot overwrite; WebGL falls back to the texture's own sampler, whose
339
+ // mip filter follows its level count.
340
+ //
341
+ // Every other reader of this texture passes an explicit LOD 0 (SSR hits,
342
+ // the sky blend), so none of them changes behaviour.
343
+ scope[UNIFORM_NAME_SCENE_COLOR_MAP] = pb.tex2D().uniform(0).withSampler(getSamplerOptions('clamp_linear'));
309
344
  scope[UNIFORM_NAME_SCENE_COLOR_MAP_SIZE] = pb.vec2().uniform(0);
310
345
  }
311
346
  if (ctx.HiZTexture) {
@@ -1091,10 +1126,329 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
1091
1126
  bindGroup.setTexture(UNIFORM_NAME_SHADOW_MASK, ctx.shadowMaskTexture ?? this.getDummyShadowMask(ctx.device), fetchSampler('clamp_nearest'));
1092
1127
  }
1093
1128
  bindGroup.setTexture(UNIFORM_NAME_BAKED_SKY_MAP, ctx.scene.env.sky.getBakedSkyTexture(ctx));
1129
+ if (this.usesWaterCaustics(ctx)) {
1130
+ this.setWaterCausticUniforms(bindGroup, ctx);
1131
+ }
1094
1132
  if (ctx.drawEnvLight) {
1095
1133
  ctx.env.light.envLight.updateBindGroup(bindGroup, ctx);
1096
1134
  }
1097
1135
  }
1136
+ /**
1137
+ * Whether the pass being built modulates its light with the water caustic map.
1138
+ *
1139
+ * True for every light pass of a frame that produced a map; which light the
1140
+ * caustics actually attach to is decided per-fragment by
1141
+ * {@link ShaderHelper.calculateWaterCaustic}. Both the shader variant hash and
1142
+ * the global bind group hash fold this in, so a program that declares the
1143
+ * caustic uniforms is always paired with a bind group that supplies them.
1144
+ *
1145
+ * Deliberately a function of the draw context alone, with no material input.
1146
+ * The caustic uniforms sit in the global bind group, whose layout is built
1147
+ * once per {@link DrawContext.renderPassHash} and shared by every material in
1148
+ * the pass, so the declarations have to be uniform across the pass. A material
1149
+ * that must not receive caustics - water itself - opts out of the sampling
1150
+ * instead, through `receivesWaterCaustics()`; the uniforms stay declared and
1151
+ * simply go unread.
1152
+ *
1153
+ * @param ctx - Current draw context.
1154
+ * @returns True when the caustic uniforms take part in this pass.
1155
+ * @internal
1156
+ */ static usesWaterCaustics(ctx) {
1157
+ return !!ctx.waterCaustics && !!ctx.waterCausticLight && ctx.renderPass.type === RENDER_PASS_TYPE_LIGHT;
1158
+ }
1159
+ /**
1160
+ * Declares the global uniforms {@link ShaderHelper.calculateWaterCaustic} reads.
1161
+ *
1162
+ * Split out of the global uniform setup so a test can build a program that
1163
+ * exercises the sampling code without standing up a whole light pass.
1164
+ *
1165
+ * @param pb - Program builder whose global scope receives the uniforms.
1166
+ * @internal
1167
+ */ /**
1168
+ * Warps a linear caustic-map coordinate so texels concentrate near the centre.
1169
+ *
1170
+ * The map covers a fixed footprint, so its world texel size is the range
1171
+ * divided by the resolution and a range large enough to light the scene is
1172
+ * already too coarse to resolve a caustic cell. This trades the edge of the
1173
+ * map, which is far from the camera, for the middle, which is not:
1174
+ *
1175
+ * ```
1176
+ * warp(u) = u (1 + k) / (1 + k |u|)
1177
+ * unwarp(w) = w / ((1 + k) - k |w|)
1178
+ * ```
1179
+ *
1180
+ * Texel density is `1 + k` times uniform at the centre and `1 / (1 + k)` times
1181
+ * it at the border, and `k = 0` is exactly the identity, so the pass can apply
1182
+ * this unconditionally. Both ends of `[-1, 1]` are fixed points, which is what
1183
+ * lets the warp sit inside the existing parameterisation without moving the
1184
+ * footprint the receiver gates on. The first derivative is continuous at the
1185
+ * centre; the second flips sign there, which is not visible in a pattern this
1186
+ * noisy.
1187
+ *
1188
+ * Applied per axis rather than radially: the photon grid, the map and the blur
1189
+ * are all axis-aligned, so a separable warp keeps photon `(i,j)` on texel
1190
+ * `(i,j)` for calm water and leaves the map's normalisation to 1.0 intact.
1191
+ *
1192
+ * @param scope - Current shader scope.
1193
+ * @param ndc - Linear map coordinate in `[-1, 1]`.
1194
+ * @param strength - Warp strength `k`; 0 is the identity.
1195
+ * @returns The warped coordinate, also in `[-1, 1]`.
1196
+ * @internal
1197
+ */ static warpCausticNDC(scope, ndc, strength) {
1198
+ const pb = scope.$builder;
1199
+ const funcName = 'Z_warpCausticNDC';
1200
+ pb.func(funcName, [
1201
+ pb.vec2('ndc'),
1202
+ pb.float('k')
1203
+ ], function() {
1204
+ this.$return(pb.div(pb.mul(this.ndc, pb.add(this.k, 1)), pb.add(pb.vec2(1), pb.mul(pb.abs(this.ndc), this.k))));
1205
+ });
1206
+ return pb.getGlobalScope()[funcName](ndc, strength);
1207
+ }
1208
+ /**
1209
+ * Inverse of {@link ShaderHelper.warpCausticNDC}.
1210
+ *
1211
+ * The denominator is `1` at the border and `1 + k` at the centre, so it stays
1212
+ * positive for any input the passes feed it - all of which come from `[-1, 1]`.
1213
+ *
1214
+ * @param scope - Current shader scope.
1215
+ * @param warped - Warped map coordinate in `[-1, 1]`.
1216
+ * @param strength - Warp strength `k`; 0 is the identity.
1217
+ * @returns The linear coordinate the warp came from.
1218
+ * @internal
1219
+ */ static unwarpCausticNDC(scope, warped, strength) {
1220
+ const pb = scope.$builder;
1221
+ const funcName = 'Z_unwarpCausticNDC';
1222
+ pb.func(funcName, [
1223
+ pb.vec2('warped'),
1224
+ pb.float('k')
1225
+ ], function() {
1226
+ this.$return(pb.div(this.warped, pb.sub(pb.vec2(pb.add(this.k, 1)), pb.mul(pb.abs(this.warped), this.k))));
1227
+ });
1228
+ return pb.getGlobalScope()[funcName](warped, strength);
1229
+ }
1230
+ /** @internal */ static declareWaterCausticUniforms(pb) {
1231
+ const scope = pb.getGlobalScope();
1232
+ const causticStruct = pb.defineStruct([
1233
+ pb.vec4('frameX'),
1234
+ pb.vec4('frameY'),
1235
+ pb.vec4('center'),
1236
+ pb.vec4('lightDir'),
1237
+ pb.vec4('params'),
1238
+ pb.vec4('extinction'),
1239
+ // Per water body, because the slice is shared but the water is not: each
1240
+ // has its own surface height, footprint and medium, and none of the three
1241
+ // means anything until a receiver has resolved which water is above it.
1242
+ pb.int('waterCount'),
1243
+ pb.vec4[MAX_CAUSTIC_WATERS]('waterRegions'),
1244
+ // (sigma_t.xyz, surface height)
1245
+ pb.vec4[MAX_CAUSTIC_WATERS]('waterMedia')
1246
+ ]);
1247
+ scope[UNIFORM_NAME_CAUSTIC_PARAMS] = causticStruct().uniform(0);
1248
+ // The map is single mip and read bilinearly; clamping keeps receivers
1249
+ // outside the covered area reading the border rather than wrapping.
1250
+ scope[UNIFORM_NAME_CAUSTIC_MAP] = pb.tex2D().uniform(0).withSampler(getSamplerOptions('clamp_linear_nomip'));
1251
+ // Displaced surface height per water slot, on the same slice as the map.
1252
+ // Read with the map's uv, so it shares the map's sampling.
1253
+ scope[UNIFORM_NAME_CAUSTIC_HEIGHT_MAP] = pb.tex2D().uniform(0).withSampler(getSamplerOptions('clamp_linear_nomip'));
1254
+ }
1255
+ /**
1256
+ * Attenuation the water caustics apply to one light at a world position.
1257
+ *
1258
+ * Returns the product of two effects: the caustic pattern focused by the waves,
1259
+ * and the Beer-Lambert transmittance of the water column the light travelled
1260
+ * through to reach the point. The result is 1 for any light that is not the
1261
+ * caustic light, above the surface, and outside the water region, so the caller
1262
+ * can multiply unconditionally.
1263
+ *
1264
+ * The caustic light is identified at runtime by comparing directions rather
1265
+ * than by which pass is running. Shadow-casting lights reach the opaque queue
1266
+ * through the clustered path when the screen-space shadow mask is on (the
1267
+ * default) and through a per-light additive pass when it is off; only a runtime
1268
+ * test covers both. Two directional lights pointing the same way would both
1269
+ * pick up the caustics, but they are physically the same light.
1270
+ *
1271
+ * @param scope - Current shader scope.
1272
+ * @param worldPos - World position of the shaded fragment.
1273
+ * @param lightType - The light's type constant.
1274
+ * @param lightDirection - Direction the light travels, i.e. `directionAndCutoff.xyz`.
1275
+ * @param ctx - Current draw context.
1276
+ * @returns A `vec3` multiplier, or null when this pass has no caustics.
1277
+ */ static calculateWaterCaustic(scope, worldPos, lightType, lightDirection, ctx) {
1278
+ if (!this.usesWaterCaustics(ctx)) {
1279
+ return null;
1280
+ }
1281
+ return this.sampleWaterCaustic(scope, worldPos, lightType, lightDirection);
1282
+ }
1283
+ /**
1284
+ * {@link ShaderHelper.calculateWaterCaustic} without the pass gate.
1285
+ *
1286
+ * The gate asks whether the *light pass* has caustics, which is the right
1287
+ * question for a material and the wrong one for a standalone full-screen pass
1288
+ * that declared the same uniforms itself. Such a pass has already decided it
1289
+ * has a map; this is the sampling half on its own.
1290
+ *
1291
+ * @param scope - Current shader scope.
1292
+ * @param worldPos - World position of the shaded fragment.
1293
+ * @param lightType - The light's type constant.
1294
+ * @param lightDirection - Direction the light travels, i.e. `directionAndCutoff.xyz`.
1295
+ * @param defocus - Whether to fade the pattern's contrast by how far the point
1296
+ * is from the map's focal depth. True for a receiver surface, which is what
1297
+ * that fade describes. False for a point inside the water column: see the
1298
+ * comment on the term itself.
1299
+ * @returns A `vec3` multiplier.
1300
+ * @internal
1301
+ */ static sampleWaterCaustic(scope, worldPos, lightType, lightDirection, defocus = true) {
1302
+ const pb = scope.$builder;
1303
+ const funcName = defocus ? 'Z_calculateWaterCaustic' : 'Z_calculateWaterCausticVolume';
1304
+ pb.func(funcName, [
1305
+ pb.vec3('worldPos'),
1306
+ pb.int('lightType'),
1307
+ pb.vec3('lightDirection')
1308
+ ], function() {
1309
+ this.$l.cu = this[UNIFORM_NAME_CAUSTIC_PARAMS];
1310
+ // Only the one light the map was built for casts caustics.
1311
+ this.$if(pb.or(pb.notEqual(this.lightType, LIGHT_TYPE_DIRECTIONAL), pb.lessThan(pb.dot(this.lightDirection, this.cu.lightDir.xyz), 0.999)), function() {
1312
+ this.$return(pb.vec3(1));
1313
+ });
1314
+ // Project into the map: frameX/frameY are orthonormal and perpendicular to
1315
+ // the light, so the same two dot products the splat pass used invert it.
1316
+ this.$l.rel = pb.sub(this.worldPos, this.cu.center.xyz);
1317
+ // Per-axis inverse extents: the slice is fitted to the water, so it is
1318
+ // only square in texels, not in meters.
1319
+ this.$l.mapNDC = pb.mul(pb.vec2(pb.dot(this.rel, this.cu.frameX.xyz), pb.dot(this.rel, this.cu.frameY.xyz)), pb.vec2(this.cu.frameX.w, this.cu.frameY.w));
1320
+ // Texels are concentrated towards the middle of the map, so the lookup
1321
+ // goes through the same warp the splat laid the photons out under.
1322
+ this.$l.warpedNDC = ShaderHelper.warpCausticNDC(this, this.mapNDC, this.cu.extinction.w);
1323
+ this.$l.uv = pb.add(pb.mul(this.warpedNDC, 0.5), pb.vec2(0.5));
1324
+ // The displaced surface height of each water slot on this fragment's own
1325
+ // sun ray, biased by CAUSTIC_HEIGHT_BIAS. The height map rasterises the
1326
+ // displaced surface into the same light-space slice as the pattern, so
1327
+ // the texel under this uv holds the surface the ray through this fragment
1328
+ // passed through - no entry point has to be estimated. Beyond the map the
1329
+ // sampler would repeat its border, which is some other ray's wave; the
1330
+ // surface is taken as flat there.
1331
+ this.$l.insideMap = pb.all(pb.lessThanEqual(pb.abs(this.mapNDC), pb.vec2(1)));
1332
+ // How far this fragment is towards the map border, and how much the map is
1333
+ // therefore trusted. Hoisted above the water loop because the wave height
1334
+ // is faded by it too, not just the pattern.
1335
+ //
1336
+ // Measured on the linear coordinate, not the warped one. The warp fixes
1337
+ // both ends of [-1, 1], so the border is in the same world place either
1338
+ // way, but the band between them is not: warping it would make its world
1339
+ // width depend on the warp strength, and the CPU sized it in meters.
1340
+ //
1341
+ // Distance is measured under an L4 norm rather than per-axis. A per-axis
1342
+ // fade is a rectangle in light space, and the sun tilts that rectangle, so
1343
+ // its border lands on the sea bed as straight lines - which the eye picks
1344
+ // out of a caustic pattern however wide the gradient is. An L2 circle has
1345
+ // no straight part anywhere but discards the 21% of the map outside its
1346
+ // inscribed disc; L4 gives up 7% instead and still curves everywhere.
1347
+ this.$l.ndcSq = pb.mul(this.mapNDC, this.mapNDC);
1348
+ this.$l.edgeDist = pb.sqrt(pb.sqrt(pb.dot(this.ndcSq, this.ndcSq)));
1349
+ this.$l.edge = pb.sub(1, pb.smoothStep(this.cu.params.w, 1, this.edgeDist));
1350
+ this.$l.waveHeights = pb.textureSampleLevel(this[UNIFORM_NAME_CAUSTIC_HEIGHT_MAP], this.uv, 0);
1351
+ // Find the water above this point. Several can qualify - a pool inside a
1352
+ // lake, water on terraces - and the one that matters is the lowest of
1353
+ // them, because that is the last surface the sun crossed on its way here.
1354
+ this.$l.depth = pb.float(-1);
1355
+ this.$l.medium = pb.vec3(0);
1356
+ this.$for(pb.int('i'), 0, MAX_CAUSTIC_WATERS, function() {
1357
+ this.$if(pb.greaterThanEqual(this.i, this.cu.waterCount), function() {
1358
+ this.$break();
1359
+ });
1360
+ this.$l.slotMedium = this.cu.waterMedia.at(this.i);
1361
+ // Where the sun ray that lights this point crossed that water's rest
1362
+ // plane, for the footprint test. The splat pass clips photons by that
1363
+ // same crossing, so the receiver must ask the same question: a point
1364
+ // under the footprint that the sun reaches from outside it saw no water
1365
+ // at all, and a point beyond the footprint that the sun reaches through
1366
+ // it did. Testing the point's own xz gets both wrong by the offset
1367
+ // depth / tan(elevation). Only the footprint is decided here; the depth
1368
+ // itself comes from the height map, which needs no crossing point.
1369
+ this.$l.slotFlat = pb.sub(this.slotMedium.w, this.worldPos.y);
1370
+ this.$l.slotPath = pb.mul(this.slotFlat, this.cu.lightDir.w);
1371
+ this.$l.slotEntry = pb.sub(this.worldPos.xz, pb.mul(this.cu.lightDir.xz, this.slotPath));
1372
+ this.$l.slotRegion = this.cu.waterRegions.at(this.i);
1373
+ this.$if(pb.and(pb.all(pb.greaterThanEqual(this.slotEntry, this.slotRegion.xy)), pb.all(pb.lessThanEqual(this.slotEntry, this.slotRegion.zw))), function() {
1374
+ // Depth under the *displaced* surface on this ray, which is what
1375
+ // the splat refracted the photons through. Gating on the rest plane
1376
+ // instead cut the caustics off at a flat line across any receiver
1377
+ // that pokes above it under a crest. Slot i lives in channel i.
1378
+ this.$l.slotMask = pb.vec4(pb.float(pb.equal(this.i, 0)), pb.float(pb.equal(this.i, 1)), pb.float(pb.equal(this.i, 2)), pb.float(pb.equal(this.i, 3)));
1379
+ this.$l.slotSample = pb.dot(this.waveHeights, this.slotMask);
1380
+ // A cleared texel inside the map means this ray never meets that
1381
+ // body's surface - it crosses the rest plane beyond the footprint,
1382
+ // or the surface is on the far side of the receiver - and the
1383
+ // receiver is dry as far as this body is concerned. Outside the map
1384
+ // nothing is known and the surface is taken as flat.
1385
+ this.$l.slotCovered = pb.or(pb.not(this.insideMap), pb.greaterThan(this.slotSample, 0.5));
1386
+ this.$if(this.slotCovered, function() {
1387
+ // Faded into the rest plane over the same band the pattern fades
1388
+ // on, rather than switched off at the border. Beyond the map the
1389
+ // surface is taken as flat, and exp(-sigma * depth) is convex, so
1390
+ // a hard switch makes the wavy side systematically brighter than
1391
+ // the flat side - E[exp(-sigma*w)] > 1 - and the step lands as a
1392
+ // mean brightness seam that grows with both the swell and the
1393
+ // turbidity. Ramping the wave to zero puts the two sides on the
1394
+ // same value at the border.
1395
+ this.$l.slotWave = pb.mul(pb.sub(this.slotSample, CAUSTIC_HEIGHT_BIAS), pb.float(this.insideMap), this.edge);
1396
+ this.$l.slotDepth = pb.sub(pb.add(this.slotMedium.w, this.slotWave), this.worldPos.y);
1397
+ this.$if(pb.greaterThan(this.slotDepth, 0), function() {
1398
+ this.$if(pb.or(pb.lessThan(this.depth, 0), pb.lessThan(this.slotDepth, this.depth)), function() {
1399
+ this.depth = this.slotDepth;
1400
+ this.medium = this.slotMedium.xyz;
1401
+ });
1402
+ });
1403
+ });
1404
+ });
1405
+ });
1406
+ this.$if(pb.lessThanEqual(this.depth, 0), function() {
1407
+ this.$return(pb.vec3(1));
1408
+ });
1409
+ this.$l.pathLength = pb.mul(this.depth, this.cu.lightDir.w);
1410
+ this.$l.transmittance = pb.exp(pb.neg(pb.mul(this.medium, this.pathLength)));
1411
+ this.$l.pattern = pb.textureSampleLevel(this[UNIFORM_NAME_CAUSTIC_MAP], this.uv, 0).x;
1412
+ if (defocus) {
1413
+ // Receivers away from the focal plane see a defocused, lower-contrast
1414
+ // pattern rather than a displaced one.
1415
+ this.$l.defocus = pb.div(1, pb.add(1, pb.mul(pb.abs(pb.sub(this.depth, this.cu.params.y)), this.cu.params.z)));
1416
+ } else {
1417
+ // A point inside the water column is not a receiver, and has no depth of
1418
+ // its own to be out of focus at - the volumetric march visits every
1419
+ // depth along the ray. Charging it the receiver's fade makes the light
1420
+ // shafts fade according to where the *sea bed* was told to be sharpest:
1421
+ // the open-ocean preset focuses the map at 26m for its bed, which at the
1422
+ // default defocus leaves a point 2m down at 26% contrast, and the shafts
1423
+ // dissolve into grain. The shafts instead take the pattern as the map
1424
+ // holds it and fade with the edge and the transmittance like everything
1425
+ // else in the column.
1426
+ this.$l.defocus = pb.float(1);
1427
+ }
1428
+ this.$l.weight = pb.mul(this.cu.params.x, this.defocus, this.edge);
1429
+ this.$l.caustic = pb.add(1, pb.mul(pb.sub(this.pattern, 1), this.weight));
1430
+ // Soften the waterline so the transmittance does not switch on abruptly.
1431
+ this.$l.submerged = pb.smoothStep(0, 0.05, this.depth);
1432
+ this.$return(pb.mix(pb.vec3(1), pb.mul(pb.vec3(this.caustic), this.transmittance), this.submerged));
1433
+ });
1434
+ return pb.getGlobalScope()[funcName](worldPos, lightType, lightDirection);
1435
+ }
1436
+ /** @internal */ static setWaterCausticUniforms(bindGroup, ctx) {
1437
+ const uniforms = ctx.waterCausticUniforms;
1438
+ bindGroup.setValue(UNIFORM_NAME_CAUSTIC_PARAMS, {
1439
+ frameX: uniforms.frameX,
1440
+ frameY: uniforms.frameY,
1441
+ center: uniforms.center,
1442
+ lightDir: uniforms.lightDir,
1443
+ params: uniforms.params,
1444
+ extinction: uniforms.extinction,
1445
+ waterCount: uniforms.waterCount,
1446
+ waterRegions: uniforms.waterRegions,
1447
+ waterMedia: uniforms.waterMedia
1448
+ });
1449
+ bindGroup.setTexture(UNIFORM_NAME_CAUSTIC_MAP, ctx.waterCausticTexture, fetchSampler('clamp_linear_nomip'));
1450
+ bindGroup.setTexture(UNIFORM_NAME_CAUSTIC_HEIGHT_MAP, ctx.waterCausticHeightTexture, fetchSampler('clamp_linear_nomip'));
1451
+ }
1098
1452
  /** @internal */ static setLightUniformsShadow(bindGroup, ctx, light) {
1099
1453
  const shadowMapParams = ctx.shadowMapInfo.get(light);
1100
1454
  this._lightUniformShadow.sunDir = ctx.sunLight ? ctx.sunLight.directionAndCutoff.xyz().scaleBy(-1) : this.defaultSunDir;
@@ -1114,6 +1468,9 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
1114
1468
  this._lightUniformShadow.envLightSpecularStrength = ctx.env?.light.specularStrength ?? 1;
1115
1469
  bindGroup.setValue('light', this._lightUniformShadow);
1116
1470
  bindGroup.setTexture(UNIFORM_NAME_SHADOW_MAP, shadowMapParams.shadowMap, shadowMapParams.shadowMapSampler);
1471
+ if (this.usesWaterCaustics(ctx)) {
1472
+ this.setWaterCausticUniforms(bindGroup, ctx);
1473
+ }
1117
1474
  bindGroup.setTexture(UNIFORM_NAME_BAKED_SKY_MAP, ctx.scene.env.sky.getBakedSkyTexture(ctx));
1118
1475
  if (ctx.drawEnvLight) {
1119
1476
  ctx.env.light.envLight.updateBindGroup(bindGroup, ctx);
@@ -1235,6 +1592,50 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
1235
1592
  return scope.camera.elapsedTime;
1236
1593
  }
1237
1594
  /**
1595
+ * Declares the one camera field {@link ShaderHelper.getElapsedTime} reads.
1596
+ *
1597
+ * Wave generators animate off `camera.elapsedTime`, which the light and depth
1598
+ * passes provide as part of the full camera struct. A standalone program that
1599
+ * drives a wave generator - the caustics splat pass - has no such struct, so it
1600
+ * declares that single field under the same name. Any generator that starts
1601
+ * reading a second camera field will need it added here.
1602
+ *
1603
+ * The three padding floats round the struct up to 16 bytes. std140 aligns a
1604
+ * struct to a vec4 boundary, so a lone float leaves the GLSL block larger than
1605
+ * the buffer the engine sizes from the struct's own members - WebGL2 then
1606
+ * rejects every draw with "uniform buffer that is too small" and rasterises
1607
+ * nothing, while WebGPU is unaffected.
1608
+ *
1609
+ * @param scope - Global scope of the standalone program.
1610
+ * @param uniformGroup - Bind group the uniform belongs to.
1611
+ * @internal
1612
+ */ static declareStandaloneCameraTime(scope, uniformGroup) {
1613
+ const pb = scope.$builder;
1614
+ scope.camera = pb.defineStruct([
1615
+ pb.float('elapsedTime'),
1616
+ pb.float('_pad0'),
1617
+ pb.float('_pad1'),
1618
+ pb.float('_pad2')
1619
+ ])().uniform(uniformGroup);
1620
+ }
1621
+ /**
1622
+ * Uploads the value declared by {@link ShaderHelper.declareStandaloneCameraTime}.
1623
+ *
1624
+ * Must be the same clock the light pass uses, or a wave generator's caustics
1625
+ * would animate out of step with the surface they are refracted through.
1626
+ *
1627
+ * @param bindGroup - Bind group of the standalone program.
1628
+ * @param ctx - Current draw context.
1629
+ * @internal
1630
+ */ static setStandaloneCameraTime(bindGroup, ctx) {
1631
+ bindGroup.setValue('camera', {
1632
+ elapsedTime: ctx.device.frameInfo.elapsedOverall * 0.001,
1633
+ _pad0: 0,
1634
+ _pad1: 0,
1635
+ _pad2: 0
1636
+ });
1637
+ }
1638
+ /**
1238
1639
  * Gets the elapsed time since last frame in seconds
1239
1640
  * @param scope - Current shader scope
1240
1641
  * @returns The elapsed time since last frame in seconds
@@ -1865,5 +2266,5 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
1865
2266
  }
1866
2267
  }
1867
2268
 
1868
- export { ShaderHelper };
2269
+ export { CAUSTIC_HEIGHT_BIAS, MAX_CAUSTIC_WATERS, ShaderHelper };
1869
2270
  //# sourceMappingURL=helper.js.map