@zephyr3d/scene 0.9.25 → 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 (299) hide show
  1. package/dist/animation/animationset.js +92 -30
  2. package/dist/animation/animationset.js.map +1 -1
  3. package/dist/animation/hair/gpu_hair_simulation.js +1090 -0
  4. package/dist/animation/hair/gpu_hair_simulation.js.map +1 -0
  5. package/dist/animation/joint_dynamics/controller.js +7 -5
  6. package/dist/animation/joint_dynamics/controller.js.map +1 -1
  7. package/dist/animation/joint_dynamics/convex_collider.js +320 -0
  8. package/dist/animation/joint_dynamics/convex_collider.js.map +1 -0
  9. package/dist/animation/joint_dynamics/joint_dynamics_system.js +81 -5
  10. package/dist/animation/joint_dynamics/joint_dynamics_system.js.map +1 -1
  11. package/dist/animation/morphtrack.js +3 -4
  12. package/dist/animation/morphtrack.js.map +1 -1
  13. package/dist/animation/skeleton.js +79 -25
  14. package/dist/animation/skeleton.js.map +1 -1
  15. package/dist/animation/spring/multi_chain_spring_system.js +577 -179
  16. package/dist/animation/spring/multi_chain_spring_system.js.map +1 -1
  17. package/dist/animation/spring/spring_collider.js +4 -3
  18. package/dist/animation/spring/spring_collider.js.map +1 -1
  19. package/dist/animation/spring/spring_node_pose_tracker.js +40 -0
  20. package/dist/animation/spring/spring_node_pose_tracker.js.map +1 -0
  21. package/dist/animation/spring/spring_particle.js +2 -0
  22. package/dist/animation/spring/spring_particle.js.map +1 -1
  23. package/dist/animation/spring/spring_system.js +11 -3
  24. package/dist/animation/spring/spring_system.js.map +1 -1
  25. package/dist/animation/spring_modifier.js +10 -0
  26. package/dist/animation/spring_modifier.js.map +1 -1
  27. package/dist/app/engine.js +53 -43
  28. package/dist/app/engine.js.map +1 -1
  29. package/dist/app/scriptingsystem.js +119 -6
  30. package/dist/app/scriptingsystem.js.map +1 -1
  31. package/dist/app/scriptregistry.js +3 -2
  32. package/dist/app/scriptregistry.js.map +1 -1
  33. package/dist/asset/assetmanager.js +284 -67
  34. package/dist/asset/assetmanager.js.map +1 -1
  35. package/dist/asset/loaders/zhair/zhair_format.js +288 -0
  36. package/dist/asset/loaders/zhair/zhair_format.js.map +1 -0
  37. package/dist/asset/loaders/zhair/zhair_loader.js +221 -0
  38. package/dist/asset/loaders/zhair/zhair_loader.js.map +1 -0
  39. package/dist/asset/model.js +184 -69
  40. package/dist/asset/model.js.map +1 -1
  41. package/dist/camera/camera.js +268 -44
  42. package/dist/camera/camera.js.map +1 -1
  43. package/dist/camera/perspectivecamera.js +3 -1
  44. package/dist/camera/perspectivecamera.js.map +1 -1
  45. package/dist/index.d.ts +9044 -5139
  46. package/dist/index.js +17 -6
  47. package/dist/index.js.map +1 -1
  48. package/dist/material/eye.js +932 -0
  49. package/dist/material/eye.js.map +1 -0
  50. package/dist/material/hair.js +917 -0
  51. package/dist/material/hair.js.map +1 -0
  52. package/dist/material/hairstrand.js +842 -0
  53. package/dist/material/hairstrand.js.map +1 -0
  54. package/dist/material/hairstrand_data.js +162 -0
  55. package/dist/material/hairstrand_data.js.map +1 -0
  56. package/dist/material/lambert.js +5 -2
  57. package/dist/material/lambert.js.map +1 -1
  58. package/dist/material/material.js +48 -10
  59. package/dist/material/material.js.map +1 -1
  60. package/dist/material/meshmaterial.js +120 -19
  61. package/dist/material/meshmaterial.js.map +1 -1
  62. package/dist/material/mixins/albedocolor.js +1 -1
  63. package/dist/material/mixins/albedocolor.js.map +1 -1
  64. package/dist/material/mixins/lightmodel/blinnphong.js +5 -3
  65. package/dist/material/mixins/lightmodel/blinnphong.js.map +1 -1
  66. package/dist/material/mixins/lightmodel/lambert.js +5 -4
  67. package/dist/material/mixins/lightmodel/lambert.js.map +1 -1
  68. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js +3 -3
  69. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js.map +1 -1
  70. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js +5 -5
  71. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js.map +1 -1
  72. package/dist/material/mixins/lightmodel/pbrspecularglossness.js +3 -3
  73. package/dist/material/mixins/lightmodel/pbrspecularglossness.js.map +1 -1
  74. package/dist/material/mixins/lit.js +106 -19
  75. package/dist/material/mixins/lit.js.map +1 -1
  76. package/dist/material/mixins/pbr/common.js +4 -4
  77. package/dist/material/mixins/pbr/common.js.map +1 -1
  78. package/dist/material/mixins/texture.js +48 -11
  79. package/dist/material/mixins/texture.js.map +1 -1
  80. package/dist/material/mtoon.js +6 -5
  81. package/dist/material/mtoon.js.map +1 -1
  82. package/dist/material/pbrblueprint.js +146 -28
  83. package/dist/material/pbrblueprint.js.map +1 -1
  84. package/dist/material/pbrblueprint_instance.js +501 -0
  85. package/dist/material/pbrblueprint_instance.js.map +1 -0
  86. package/dist/material/pbrmr.js +15 -16
  87. package/dist/material/pbrmr.js.map +1 -1
  88. package/dist/material/shader/helper.js +734 -23
  89. package/dist/material/shader/helper.js.map +1 -1
  90. package/dist/material/shader/terrain_blend.js +55 -0
  91. package/dist/material/shader/terrain_blend.js.map +1 -0
  92. package/dist/material/skin.js +192 -32
  93. package/dist/material/skin.js.map +1 -1
  94. package/dist/material/spriteblueprint.js +16 -1
  95. package/dist/material/spriteblueprint.js.map +1 -1
  96. package/dist/material/subsurfaceprofile.js +86 -23
  97. package/dist/material/subsurfaceprofile.js.map +1 -1
  98. package/dist/material/water.js +2059 -72
  99. package/dist/material/water.js.map +1 -1
  100. package/dist/posteffect/bloom.js +167 -7
  101. package/dist/posteffect/bloom.js.map +1 -1
  102. package/dist/posteffect/coloradjust.js +2 -1
  103. package/dist/posteffect/coloradjust.js.map +1 -1
  104. package/dist/posteffect/compositor.js.map +1 -1
  105. package/dist/posteffect/fxaa.js +2 -1
  106. package/dist/posteffect/fxaa.js.map +1 -1
  107. package/dist/posteffect/grayscale.js +3 -1
  108. package/dist/posteffect/grayscale.js.map +1 -1
  109. package/dist/posteffect/motionblur.js +664 -56
  110. package/dist/posteffect/motionblur.js.map +1 -1
  111. package/dist/posteffect/posteffect.js +12 -1
  112. package/dist/posteffect/posteffect.js.map +1 -1
  113. package/dist/posteffect/sao.js +46 -44
  114. package/dist/posteffect/sao.js.map +1 -1
  115. package/dist/posteffect/skinsss.js +368 -69
  116. package/dist/posteffect/skinsss.js.map +1 -1
  117. package/dist/posteffect/ssgi.js +16 -9
  118. package/dist/posteffect/ssgi.js.map +1 -1
  119. package/dist/posteffect/ssr.js +3 -0
  120. package/dist/posteffect/ssr.js.map +1 -1
  121. package/dist/posteffect/sss.js +121 -36
  122. package/dist/posteffect/sss.js.map +1 -1
  123. package/dist/posteffect/taa.js.map +1 -1
  124. package/dist/posteffect/tonemap.js +44 -1
  125. package/dist/posteffect/tonemap.js.map +1 -1
  126. package/dist/render/abuffer_oit.js +8 -2
  127. package/dist/render/abuffer_oit.js.map +1 -1
  128. package/dist/render/clipmap.js +156 -5
  129. package/dist/render/clipmap.js.map +1 -1
  130. package/dist/render/cull_visitor.js +20 -0
  131. package/dist/render/cull_visitor.js.map +1 -1
  132. package/dist/render/depthpass.js +36 -2
  133. package/dist/render/depthpass.js.map +1 -1
  134. package/dist/render/drawable_mixin.js +26 -1
  135. package/dist/render/drawable_mixin.js.map +1 -1
  136. package/dist/render/dualdepthpeeling_oit.js +3 -2
  137. package/dist/render/dualdepthpeeling_oit.js.map +1 -1
  138. package/dist/render/envlight.js +1 -1
  139. package/dist/render/envlight.js.map +1 -1
  140. package/dist/render/fbm_wavegenerator.js +6 -3
  141. package/dist/render/fbm_wavegenerator.js.map +1 -1
  142. package/dist/render/fft_wavegenerator.js +57 -15
  143. package/dist/render/fft_wavegenerator.js.map +1 -1
  144. package/dist/render/gerstner_wavegenerator.js +130 -20
  145. package/dist/render/gerstner_wavegenerator.js.map +1 -1
  146. package/dist/render/globalbindgroup_allocator.js +3 -0
  147. package/dist/render/globalbindgroup_allocator.js.map +1 -1
  148. package/dist/render/hzb.js +80 -22
  149. package/dist/render/hzb.js.map +1 -1
  150. package/dist/render/lightpass.js +6 -3
  151. package/dist/render/lightpass.js.map +1 -1
  152. package/dist/render/oit.js +29 -0
  153. package/dist/render/oit.js.map +1 -0
  154. package/dist/render/render_queue.js +39 -0
  155. package/dist/render/render_queue.js.map +1 -1
  156. package/dist/render/renderer.js +4 -0
  157. package/dist/render/renderer.js.map +1 -1
  158. package/dist/render/rendergraph/blackboard.js +1 -0
  159. package/dist/render/rendergraph/blackboard.js.map +1 -1
  160. package/dist/render/rendergraph/device_pool_allocator.js.map +1 -1
  161. package/dist/render/rendergraph/executor.js +2 -0
  162. package/dist/render/rendergraph/executor.js.map +1 -1
  163. package/dist/render/rendergraph/forward_plus_builder.js +379 -10
  164. package/dist/render/rendergraph/forward_plus_builder.js.map +1 -1
  165. package/dist/render/rendergraph/frame_resource_requirements.js +1 -0
  166. package/dist/render/rendergraph/frame_resource_requirements.js.map +1 -1
  167. package/dist/render/rendergraph/history_resources.js +8 -1
  168. package/dist/render/rendergraph/history_resources.js.map +1 -1
  169. package/dist/render/rendergraph/rendergraph.js.map +1 -1
  170. package/dist/render/rendergraph/texture_affinity_cache.js +1 -1
  171. package/dist/render/rendergraph/texture_affinity_cache.js.map +1 -1
  172. package/dist/render/rendergraph/types.js.map +1 -1
  173. package/dist/render/shadow_mask_pass.js +73 -8
  174. package/dist/render/shadow_mask_pass.js.map +1 -1
  175. package/dist/render/sky.js +166 -126
  176. package/dist/render/sky.js.map +1 -1
  177. package/dist/render/underwater.js +624 -0
  178. package/dist/render/underwater.js.map +1 -0
  179. package/dist/render/water_caustics.js +1380 -0
  180. package/dist/render/water_caustics.js.map +1 -0
  181. package/dist/render/weightedblended_oit.js +3 -2
  182. package/dist/render/weightedblended_oit.js.map +1 -1
  183. package/dist/scene/basesprite.js +8 -0
  184. package/dist/scene/basesprite.js.map +1 -1
  185. package/dist/scene/batchgroup.js +3 -0
  186. package/dist/scene/batchgroup.js.map +1 -1
  187. package/dist/scene/environment.js +4 -1
  188. package/dist/scene/environment.js.map +1 -1
  189. package/dist/scene/hair.js +963 -0
  190. package/dist/scene/hair.js.map +1 -0
  191. package/dist/scene/light.js +5 -1
  192. package/dist/scene/light.js.map +1 -1
  193. package/dist/scene/mesh.js +481 -47
  194. package/dist/scene/mesh.js.map +1 -1
  195. package/dist/scene/meshdrawable.js +6 -0
  196. package/dist/scene/meshdrawable.js.map +1 -1
  197. package/dist/scene/msdftext.js +3 -0
  198. package/dist/scene/msdftext.js.map +1 -1
  199. package/dist/scene/msdftextsprite.js +3 -0
  200. package/dist/scene/msdftextsprite.js.map +1 -1
  201. package/dist/scene/particlesys.js +6 -0
  202. package/dist/scene/particlesys.js.map +1 -1
  203. package/dist/scene/scene.js +3 -0
  204. package/dist/scene/scene.js.map +1 -1
  205. package/dist/scene/scene_node.js +6 -1
  206. package/dist/scene/scene_node.js.map +1 -1
  207. package/dist/scene/terrain-cm/grass.js +3 -0
  208. package/dist/scene/terrain-cm/grass.js.map +1 -1
  209. package/dist/scene/terrain-cm/grassmaterial.js +3 -0
  210. package/dist/scene/terrain-cm/grassmaterial.js.map +1 -1
  211. package/dist/scene/terrain-cm/terrain-cm.js +6 -0
  212. package/dist/scene/terrain-cm/terrain-cm.js.map +1 -1
  213. package/dist/scene/terrain-cm/texture_cache.js +309 -0
  214. package/dist/scene/terrain-cm/texture_cache.js.map +1 -0
  215. package/dist/scene/water.js +546 -4
  216. package/dist/scene/water.js.map +1 -1
  217. package/dist/shaders/atmosphere.js.map +1 -1
  218. package/dist/shaders/diffusion.js +54 -0
  219. package/dist/shaders/diffusion.js.map +1 -0
  220. package/dist/shaders/shadow.js +33 -21
  221. package/dist/shaders/shadow.js.map +1 -1
  222. package/dist/shaders/temporal.js +71 -33
  223. package/dist/shaders/temporal.js.map +1 -1
  224. package/dist/shaders/water.js +22 -19
  225. package/dist/shaders/water.js.map +1 -1
  226. package/dist/shaders/water_medium.js +56 -0
  227. package/dist/shaders/water_medium.js.map +1 -0
  228. package/dist/shadow/dom.js +546 -0
  229. package/dist/shadow/dom.js.map +1 -0
  230. package/dist/shadow/pcf_opt.js +3 -5
  231. package/dist/shadow/pcf_opt.js.map +1 -1
  232. package/dist/shadow/pcf_pd.js +1 -1
  233. package/dist/shadow/pcf_pd.js.map +1 -1
  234. package/dist/shadow/receiver_bias.js +41 -0
  235. package/dist/shadow/receiver_bias.js.map +1 -0
  236. package/dist/shadow/shader.js +20 -60
  237. package/dist/shadow/shader.js.map +1 -1
  238. package/dist/shadow/shadow_impl.js +89 -0
  239. package/dist/shadow/shadow_impl.js.map +1 -1
  240. package/dist/shadow/shadow_region.js +1 -1
  241. package/dist/shadow/shadow_region.js.map +1 -1
  242. package/dist/shadow/shadowmapper.js +150 -37
  243. package/dist/shadow/shadowmapper.js.map +1 -1
  244. package/dist/shapes/shape.js +11 -2
  245. package/dist/shapes/shape.js.map +1 -1
  246. package/dist/shapes/sphere.js +50 -9
  247. package/dist/shapes/sphere.js.map +1 -1
  248. package/dist/utility/blueprint/common/constants.js +112 -2
  249. package/dist/utility/blueprint/common/constants.js.map +1 -1
  250. package/dist/utility/blueprint/material/func.js +142 -49
  251. package/dist/utility/blueprint/material/func.js.map +1 -1
  252. package/dist/utility/blueprint/material/ir.js +490 -25
  253. package/dist/utility/blueprint/material/ir.js.map +1 -1
  254. package/dist/utility/blueprint/material/texture.js +898 -4
  255. package/dist/utility/blueprint/material/texture.js.map +1 -1
  256. package/dist/utility/serialization/manager.js +434 -101
  257. package/dist/utility/serialization/manager.js.map +1 -1
  258. package/dist/utility/serialization/scene/animation.js +401 -5
  259. package/dist/utility/serialization/scene/animation.js.map +1 -1
  260. package/dist/utility/serialization/scene/batch.js +4 -0
  261. package/dist/utility/serialization/scene/batch.js.map +1 -1
  262. package/dist/utility/serialization/scene/camera.js +186 -26
  263. package/dist/utility/serialization/scene/camera.js.map +1 -1
  264. package/dist/utility/serialization/scene/common.js +26 -5
  265. package/dist/utility/serialization/scene/common.js.map +1 -1
  266. package/dist/utility/serialization/scene/hair.js +1141 -0
  267. package/dist/utility/serialization/scene/hair.js.map +1 -0
  268. package/dist/utility/serialization/scene/light.js +84 -4
  269. package/dist/utility/serialization/scene/light.js.map +1 -1
  270. package/dist/utility/serialization/scene/material.js +1867 -166
  271. package/dist/utility/serialization/scene/material.js.map +1 -1
  272. package/dist/utility/serialization/scene/mesh.js +231 -14
  273. package/dist/utility/serialization/scene/mesh.js.map +1 -1
  274. package/dist/utility/serialization/scene/node.js +141 -7
  275. package/dist/utility/serialization/scene/node.js.map +1 -1
  276. package/dist/utility/serialization/scene/particle.js +4 -0
  277. package/dist/utility/serialization/scene/particle.js.map +1 -1
  278. package/dist/utility/serialization/scene/primitive.js +18 -0
  279. package/dist/utility/serialization/scene/primitive.js.map +1 -1
  280. package/dist/utility/serialization/scene/sprite.js +4 -0
  281. package/dist/utility/serialization/scene/sprite.js.map +1 -1
  282. package/dist/utility/serialization/scene/terrain.js +4 -0
  283. package/dist/utility/serialization/scene/terrain.js.map +1 -1
  284. package/dist/utility/serialization/scene/text.js +4 -0
  285. package/dist/utility/serialization/scene/text.js.map +1 -1
  286. package/dist/utility/serialization/scene/water.js +1105 -21
  287. package/dist/utility/serialization/scene/water.js.map +1 -1
  288. package/dist/utility/serialization/types.js.map +1 -1
  289. package/dist/utility/shprojector.js +9 -8
  290. package/dist/utility/shprojector.js.map +1 -1
  291. package/dist/values.js +49 -2
  292. package/dist/values.js.map +1 -1
  293. package/package.json +3 -3
  294. package/dist/render/rendergraph/frame_graph_context.js +0 -41
  295. package/dist/render/rendergraph/frame_graph_context.js.map +0 -1
  296. package/dist/shaders/ssgi.js +0 -63
  297. package/dist/shaders/ssgi.js.map +0 -1
  298. package/dist/shaders/ssgi_reprojection.js +0 -118
  299. package/dist/shaders/ssgi_reprojection.js.map +0 -1
@@ -0,0 +1,624 @@
1
+ import { Vector4, Vector3, DEPTH_FARTHEST } from '@zephyr3d/base';
2
+ import { ShaderHelper } from '../material/shader/helper.js';
3
+ import { waterScatterPhase } from '../shaders/water_medium.js';
4
+ import { LIGHT_TYPE_DIRECTIONAL } from '../values.js';
5
+ import { interleavedGradientNoise } from '../shaders/noise.js';
6
+ import { SkyRenderer } from './sky.js';
7
+ import { fetchSampler } from '../utility/misc.js';
8
+ import { drawFullscreenQuad } from './fullscreenquad.js';
9
+
10
+ /**
11
+ * Which waters each camera is currently inside, and the rest plane of each.
12
+ *
13
+ * Written by {@link Water.updatePerCamera}, which runs for every water every
14
+ * frame whether or not the surface survived culling. Reading the render queue
15
+ * instead would lose the state the moment the surface left the frustum - which
16
+ * is exactly what happens to a camera deep underwater looking down.
17
+ *
18
+ * Keyed on the camera so multiple views stay independent. The inner map holds a
19
+ * strong reference to the water, which a disposed node would keep alive; entries
20
+ * are pruned on read rather than on dispose, since a disposed water simply stops
21
+ * updating its own entry.
22
+ *
23
+ * @internal
24
+ */ const _submergence = new WeakMap();
25
+ /**
26
+ * Record whether `camera` is inside `water` this frame.
27
+ *
28
+ * @param camera - Camera being updated.
29
+ * @param water - Water reporting its own state.
30
+ * @param submerged - Whether the camera is inside this body.
31
+ * @param surfaceY - World Y of the water's rest plane.
32
+ * @internal
33
+ */ function setWaterSubmergence(camera, water, submerged, surfaceY) {
34
+ let bodies = _submergence.get(camera);
35
+ if (submerged) {
36
+ if (!bodies) {
37
+ bodies = new Map();
38
+ _submergence.set(camera, bodies);
39
+ }
40
+ bodies.set(water, surfaceY);
41
+ } else if (bodies) {
42
+ bodies.delete(water);
43
+ }
44
+ }
45
+ /**
46
+ * The water the camera is currently inside, or null when it is above all of them.
47
+ *
48
+ * Several can qualify - a pool inside a lake, water on terraces - and the one
49
+ * that matters is the highest of them: that is the last surface the eye crossed
50
+ * on its way down, so it owns the column the eye is looking through.
51
+ *
52
+ * @param ctx - Frame draw context.
53
+ * @returns The submerged state, or null.
54
+ * @internal
55
+ */ function selectUnderwaterSource(ctx) {
56
+ const bodies = _submergence.get(ctx.camera);
57
+ if (!bodies || bodies.size === 0) {
58
+ return null;
59
+ }
60
+ let best = null;
61
+ let bestY = 0;
62
+ for (const [water, surfaceY] of bodies){
63
+ // A node disposed since its last update never clears its own entry.
64
+ if (water.disposed) {
65
+ bodies.delete(water);
66
+ continue;
67
+ }
68
+ if (!best || surfaceY > bestY) {
69
+ best = water;
70
+ bestY = surfaceY;
71
+ }
72
+ }
73
+ const material = best?.material;
74
+ if (!best || !material || !material.underwaterEnabled) {
75
+ return null;
76
+ }
77
+ return {
78
+ water: best,
79
+ material,
80
+ surfaceY: bestY,
81
+ eyeDepth: Math.max(0, bestY - ctx.camera.getWorldPosition().y)
82
+ };
83
+ }
84
+ /**
85
+ * Optical depth at which a channel is taken to have died completely.
86
+ *
87
+ * exp(-30) is about 1e-13, which is zero in any format the scene color uses. The
88
+ * path length fed to the medium is capped at whatever distance reaches this on
89
+ * the least attenuated channel, so a huge far plane cannot push the exponent
90
+ * anywhere unpleasant.
91
+ * @internal
92
+ */ const OPTICAL_DEPTH_CAP = 30;
93
+ /**
94
+ * Global name of the shadow map the receiver helpers sample.
95
+ *
96
+ * Fixed by `ShaderHelper`, which declares it under this name for materials and
97
+ * reads it from the implementation's `computeShadow`; a standalone pass sampling
98
+ * shadows has to use the same one.
99
+ * @internal
100
+ */ const SHADOW_MAP_UNIFORM = 'Z_UniformShadowMap';
101
+ /**
102
+ * Composites the water body a submerged camera is looking through.
103
+ *
104
+ * Two draws over the opaque scene, both in place on the scene color target:
105
+ *
106
+ * 1. multiply by the medium's transmittance, then
107
+ * 2. add what the medium scatters back towards the eye.
108
+ *
109
+ * Two rather than one because the transmittance is per channel - red dies within
110
+ * a metre or two of clear water while blue carries tens of metres, which is the
111
+ * entire reason water reads as water - and a single blend equation only has a
112
+ * scalar `src-alpha` to attenuate the destination with. Splitting them lets the
113
+ * first draw use `src-color` as the destination factor and keep the colour.
114
+ *
115
+ * The pass runs after the sky, and treats a sky pixel as a ray that never left
116
+ * the water: its path length is the cap above, so its transmittance is zero and
117
+ * the horizon is pure medium. That is what replaces the sky underwater, rather
118
+ * than any separate handling of it.
119
+ *
120
+ * @internal
121
+ */ class UnderwaterRenderer {
122
+ /** Multiplies the scene by the medium transmittance. */ _extinctionProgram;
123
+ _extinctionBindGroup;
124
+ _extinctionStates;
125
+ /**
126
+ * Adds what the medium scatters towards the eye. Two variants: ambient alone,
127
+ * and ambient plus a march for the sun shafts. Keyed on the step count as well
128
+ * as on whether the shafts are on, because the loop is unrolled at that count.
129
+ */ _inscatterPrograms;
130
+ _inscatterStates;
131
+ /** (ambientScale, surfaceY, maxPathLength, godRayIntensity) */ _params;
132
+ /** (scatterAnisotropy, unused, unused, unused) */ _phaseParams;
133
+ _extinction;
134
+ _albedo;
135
+ _sunDir;
136
+ _sunColor;
137
+ /** Shadow receiver uniforms, only touched while the shafts are shadowed. */ _cameraPosition;
138
+ _cameraParams;
139
+ _implParams;
140
+ _cameraForward;
141
+ /**
142
+ * Scratch for the cascade matrices. Copied rather than passed through, because
143
+ * the uniform setter wants an array it owns the buffer type of; reused so the
144
+ * copy does not allocate every frame.
145
+ */ _shadowMatrices;
146
+ constructor(){
147
+ this._extinctionProgram = null;
148
+ this._extinctionBindGroup = null;
149
+ this._extinctionStates = null;
150
+ this._inscatterPrograms = new Map();
151
+ this._inscatterStates = null;
152
+ this._params = new Vector4();
153
+ this._phaseParams = new Vector4();
154
+ this._extinction = new Vector3();
155
+ this._albedo = new Vector3();
156
+ this._sunDir = new Vector3();
157
+ this._sunColor = new Vector3();
158
+ this._cameraPosition = new Vector4();
159
+ this._cameraParams = new Vector4();
160
+ this._implParams = new Vector4();
161
+ this._cameraForward = new Vector4();
162
+ this._shadowMatrices = null;
163
+ }
164
+ /**
165
+ * Composite the medium over the opaque scene.
166
+ *
167
+ * @param ctx - Frame draw context.
168
+ * @param state - The water the camera is inside.
169
+ * @param colorFramebuffer - Color-only target holding the opaque scene and the
170
+ * sky. The depth attachment is deliberately absent: this pass samples the
171
+ * scene depth, which it could not do while that texture is still attached.
172
+ * @param depthTexture - Scene depth attachment, sampled for the path length.
173
+ */ render(ctx, state, colorFramebuffer, depthTexture) {
174
+ const device = ctx.device;
175
+ const material = state.material;
176
+ // Shafts need the caustic map as the surface's transmittance, and the sun
177
+ // that map was built for. Without one the march has nothing to modulate and
178
+ // would paint a uniform cone, so it degrades to the ambient term alone.
179
+ const sun = ctx.waterCausticLight;
180
+ const godRays = material.underwaterGodRays && !!ctx.waterCaustics && !!sun && material.underwaterGodRayIntensity > 0;
181
+ const steps = godRays ? material.underwaterGodRaySteps : 0;
182
+ // The caustic light is required to cast shadows for the map to exist at all,
183
+ // so its params are normally there; the lookup stays defensive because this
184
+ // runs a module later and nothing re-checks in between.
185
+ const shadowMapParams = godRays && material.underwaterGodRayShadow ? ctx.shadowMapInfo?.get(sun) ?? null : null;
186
+ const shadow = shadowMapParams?.shadowMap ? shadowMapParams : null;
187
+ const { extinctionPass, inscatterPass } = this._prepare(device, steps, shadow);
188
+ const extinction = material.extinction;
189
+ this._extinction.set(extinction);
190
+ this._albedo.set(material.scatterAlbedo);
191
+ // Far enough that the least attenuated channel has died. A channel with no
192
+ // extinction at all has no such distance, and the far plane stands in.
193
+ const minExtinction = Math.min(extinction.x, extinction.y, extinction.z);
194
+ const far = ctx.camera.getFarPlane();
195
+ this._params.setXYZW(// The distant-sky LUT is baked exposure-independently, as everywhere else
196
+ // it is read from.
197
+ SkyRenderer.getBakeToPreExposedScale(ctx) * material.underwaterAmbientIntensity, state.surfaceY, minExtinction > 0 ? Math.min(far, OPTICAL_DEPTH_CAP / minExtinction) : far, godRays ? material.underwaterGodRayIntensity : 0);
198
+ this._phaseParams.setXYZW(material.scatterAnisotropy, 0, 0, 0);
199
+ if (sun) {
200
+ // Direction the light travels, matching what calculateWaterCaustic
201
+ // compares its own against.
202
+ const dir = sun.directionAndCutoff;
203
+ this._sunDir.setXYZ(dir.x, dir.y, dir.z);
204
+ const color = sun.diffuseAndIntensity;
205
+ // Pre-exposed, like every other radiance this pass adds to the scene.
206
+ const intensity = color.w * ShaderHelper.getPreExposure(ctx);
207
+ this._sunColor.setXYZ(color.x * intensity, color.y * intensity, color.z * intensity);
208
+ } else {
209
+ this._sunDir.setXYZ(0, -1, 0);
210
+ this._sunColor.setXYZ(0, 0, 0);
211
+ }
212
+ // Same rule the post effects use (AbstractPostEffect.needFlip): WebGPU's
213
+ // render target has the opposite vertical convention to the screen.
214
+ const flip = device.type === 'webgpu' && colorFramebuffer ? 1 : 0;
215
+ const nearestSampler = fetchSampler('clamp_nearest_nomip');
216
+ const skyLut = ctx.scene.env.sky.getSkyDistantLightLUT(ctx);
217
+ const invProjView = ctx.camera.invViewProjectionMatrix;
218
+ const cameraPos = ctx.camera.getWorldPosition();
219
+ const passes = [
220
+ extinctionPass,
221
+ inscatterPass
222
+ ];
223
+ device.pushDeviceStates();
224
+ try {
225
+ device.setFramebuffer(colorFramebuffer);
226
+ device.setViewport(null);
227
+ device.setScissor(null);
228
+ for (const pass of passes){
229
+ const bindGroup = pass.bindGroup;
230
+ bindGroup.setTexture('depthTex', depthTexture, nearestSampler);
231
+ bindGroup.setTexture('skyLut', skyLut, nearestSampler);
232
+ bindGroup.setValue('invProjViewMatrix', invProjView);
233
+ bindGroup.setValue('cameraPosition', cameraPos);
234
+ bindGroup.setValue('mediumExtinction', this._extinction);
235
+ bindGroup.setValue('mediumAlbedo', this._albedo);
236
+ bindGroup.setValue('params', this._params);
237
+ bindGroup.setValue('flip', flip);
238
+ if (pass === inscatterPass && godRays) {
239
+ bindGroup.setValue('sunDirection', this._sunDir);
240
+ bindGroup.setValue('sunColor', this._sunColor);
241
+ bindGroup.setValue('phaseParams', this._phaseParams);
242
+ // Same uniforms the lit materials bind, so the shafts and the caustics
243
+ // on the sea bed are reading one map through one projection.
244
+ ShaderHelper.setWaterCausticUniforms(bindGroup, ctx);
245
+ if (shadow) {
246
+ this._setShadowUniforms(bindGroup, ctx, sun, shadow);
247
+ }
248
+ }
249
+ device.setProgram(pass.program);
250
+ device.setBindGroup(0, bindGroup);
251
+ drawFullscreenQuad(pass.states);
252
+ }
253
+ } finally{
254
+ device.popDeviceStates();
255
+ }
256
+ }
257
+ /**
258
+ * Upload the shadow receiver uniforms the march samples.
259
+ *
260
+ * The names and the struct layouts are dictated by the helpers that read them
261
+ * (`ShaderHelper.calculateShadowSpaceVertex`, the shadow implementation's
262
+ * `computeShadow`), which expect the material light pass's globals. This is
263
+ * the same arrangement the shadow mask pass makes for the same reason - see
264
+ * `ShadowMaskRenderer.setUniforms`.
265
+ * @internal
266
+ */ _setShadowUniforms(bindGroup, ctx, light, shadowMapParams) {
267
+ const camera = ctx.camera;
268
+ const near = camera.getNearPlane();
269
+ const far = camera.getFarPlane();
270
+ const cameraPos = camera.getWorldPosition();
271
+ this._cameraPosition.setXYZW(cameraPos.x, cameraPos.y, cameraPos.z, 0);
272
+ this._cameraParams.setXYZW(near, far, 1, 1);
273
+ bindGroup.setValue('camera', {
274
+ position: this._cameraPosition,
275
+ params: this._cameraParams,
276
+ shadowDebugCascades: camera.shadowDebugCascades ? 1 : 0,
277
+ framestamp: ctx.device.frameInfo.frameCounter
278
+ });
279
+ shadowMapParams.impl.getParams(this._implParams);
280
+ const matrices = shadowMapParams.shadowMatrices;
281
+ if (!this._shadowMatrices || this._shadowMatrices.length !== matrices.length) {
282
+ this._shadowMatrices = new Float32Array(matrices.length);
283
+ }
284
+ this._shadowMatrices.set(matrices);
285
+ bindGroup.setValue('light', {
286
+ sunDir: ctx.sunLight ? ctx.sunLight.directionAndCutoff.xyz().scaleBy(-1) : Vector3.axisPY(),
287
+ shadowCascades: shadowMapParams.numShadowCascades,
288
+ positionAndRange: light.positionAndRange,
289
+ directionAndCutoff: light.directionAndCutoff,
290
+ diffuseAndIntensity: ShaderHelper.getPreExposedColorIntensity(light, ctx),
291
+ extraParams: light.extraParams,
292
+ cascadeDistances: shadowMapParams.cascadeDistances,
293
+ depthBiasValues: shadowMapParams.depthBiasValues[0],
294
+ shadowCameraParams: shadowMapParams.cameraParams,
295
+ depthBiasScales: shadowMapParams.depthBiasScales,
296
+ implParams: this._implParams,
297
+ shadowMatrices: this._shadowMatrices,
298
+ shadowStrength: light.shadow.shadowStrength,
299
+ envLightStrength: ShaderHelper.getEnvLightLuminance(ctx),
300
+ envLightSpecularStrength: ctx.env?.light.specularStrength ?? 1
301
+ });
302
+ // Which way the camera looks, for the cascade split test. The march knows
303
+ // how far along its ray a sample is, and the splits are measured in view
304
+ // depth; using the radial distance instead would pick a coarser cascade
305
+ // towards the edges of a wide frustum and bend the cascade seam.
306
+ camera.worldMatrix.getRow(2, this._cameraForward);
307
+ this._cameraForward.scaleBy(-1);
308
+ bindGroup.setValue('cameraForward', this._cameraForward.xyz());
309
+ bindGroup.setTexture(SHADOW_MAP_UNIFORM, shadowMapParams.shadowMap, shadowMapParams.shadowMapSampler);
310
+ }
311
+ /**
312
+ * Build or fetch the two programs this frame needs.
313
+ *
314
+ * @param device - Rendering device.
315
+ * @param steps - Ray-march steps for the shafts, 0 for the ambient-only variant.
316
+ * @param shadow - Shadow params for the caustic light when the shafts should be
317
+ * occluded by geometry, null when they should not.
318
+ * @internal
319
+ */ _prepare(device, steps, shadow) {
320
+ if (!this._extinctionProgram) {
321
+ this._extinctionProgram = UnderwaterRenderer._createProgram(device, false, 0, null);
322
+ this._extinctionBindGroup = device.createBindGroup(this._extinctionProgram.bindGroupLayouts[0]);
323
+ this._extinctionStates = device.createRenderStateSet();
324
+ this._extinctionStates.useRasterizerState().setCullMode('none');
325
+ this._extinctionStates.useDepthState().enableTest(false).enableWrite(false);
326
+ // dst.rgb *= src.rgb. The whole reason this is its own draw: the
327
+ // destination factor has to be the per-channel transmittance.
328
+ this._extinctionStates.useBlendingState().enable(true).setBlendFuncRGB('zero', 'src-color').setBlendFuncAlpha('zero', 'one');
329
+ }
330
+ if (!this._inscatterStates) {
331
+ this._inscatterStates = device.createRenderStateSet();
332
+ this._inscatterStates.useRasterizerState().setCullMode('none');
333
+ this._inscatterStates.useDepthState().enableTest(false).enableWrite(false);
334
+ // dst.rgb += src.rgb. Alpha is left alone, as in the fog pass: folding a
335
+ // transmittance into the framebuffer coverage makes a dense medium show
336
+ // the page through the canvas.
337
+ this._inscatterStates.useBlendingState().enable(true).setBlendFuncRGB('one', 'one').setBlendFuncAlpha('zero', 'one');
338
+ }
339
+ // The march is unrolled, so the step count is part of the program identity.
340
+ // Authoring it as a uniform loop bound instead would cost a dynamic loop in
341
+ // every variant including the one with no shafts at all. The shadow hash
342
+ // joins it because the receiver code is generated from the implementation,
343
+ // the cascade count and the shadow map's own type.
344
+ const key = `${steps}|${shadow ? shadow.shaderHash : ''}`;
345
+ let inscatter = this._inscatterPrograms.get(key);
346
+ if (!inscatter) {
347
+ const program = UnderwaterRenderer._createProgram(device, true, steps, shadow);
348
+ inscatter = {
349
+ program,
350
+ bindGroup: device.createBindGroup(program.bindGroupLayouts[0])
351
+ };
352
+ this._inscatterPrograms.set(key, inscatter);
353
+ }
354
+ return {
355
+ extinctionPass: {
356
+ program: this._extinctionProgram,
357
+ bindGroup: this._extinctionBindGroup,
358
+ states: this._extinctionStates
359
+ },
360
+ inscatterPass: {
361
+ program: inscatter.program,
362
+ bindGroup: inscatter.bindGroup,
363
+ states: this._inscatterStates
364
+ }
365
+ };
366
+ }
367
+ /**
368
+ * @param device - Rendering device.
369
+ * @param inscatter - Build the in-scattering half rather than the extinction one.
370
+ * @param steps - Ray-march steps for the sun shafts; 0 leaves them out entirely.
371
+ * @param shadowMapParams - Shadow params when the shafts should be occluded by
372
+ * geometry, null when they should not.
373
+ * @internal
374
+ */ static _createProgram(device, inscatter, steps, shadowMapParams) {
375
+ const godRays = inscatter && steps > 0;
376
+ const shadow = godRays ? shadowMapParams : null;
377
+ const numCascades = shadow?.numShadowCascades ?? 0;
378
+ const program = device.buildRenderProgram({
379
+ label: inscatter ? 'UnderwaterInscatter' : 'UnderwaterExtinction',
380
+ vertex (pb) {
381
+ this.flip = pb.int().uniform(0);
382
+ this.$inputs.pos = pb.vec2().attrib('position');
383
+ this.$outputs.uv = pb.vec2();
384
+ pb.main(function() {
385
+ this.$builtins.position = pb.vec4(this.$inputs.pos, DEPTH_FARTHEST, 1);
386
+ this.$outputs.uv = pb.add(pb.mul(this.$inputs.pos.xy, 0.5), pb.vec2(0.5));
387
+ this.$if(pb.notEqual(this.flip, 0), function() {
388
+ this.$builtins.position.y = pb.neg(this.$builtins.position.y);
389
+ });
390
+ });
391
+ },
392
+ fragment (pb) {
393
+ this.depthTex = pb.tex2D().sampleType('unfilterable-float').uniform(0);
394
+ this.skyLut = pb.tex2D().uniform(0);
395
+ this.invProjViewMatrix = pb.mat4().uniform(0);
396
+ this.cameraPosition = pb.vec3().uniform(0);
397
+ this.mediumExtinction = pb.vec3().uniform(0);
398
+ this.mediumAlbedo = pb.vec3().uniform(0);
399
+ /** (ambientScale, surfaceY, maxPathLength, godRayIntensity) */ this.params = pb.vec4().uniform(0);
400
+ if (godRays) {
401
+ /** Direction the sun travels, as `directionAndCutoff.xyz`. */ this.sunDirection = pb.vec3().uniform(0);
402
+ /** Pre-exposed sun radiance. */ this.sunColor = pb.vec3().uniform(0);
403
+ /** (scatterAnisotropy, unused, unused, unused) */ this.phaseParams = pb.vec4().uniform(0);
404
+ // The same caustic uniforms every lit material declares, so the shafts
405
+ // read the map through the exact projection the sea bed does.
406
+ ShaderHelper.declareWaterCausticUniforms(pb);
407
+ }
408
+ if (shadow) {
409
+ // The shadow receiver helpers read the material light pass's globals by
410
+ // name, so a standalone pass has to reproduce them field for field.
411
+ // Same arrangement, and same reason, as ShadowMaskRenderer.
412
+ const cameraStruct = pb.defineStruct([
413
+ pb.vec4('position'),
414
+ pb.vec4('params'),
415
+ pb.float('shadowDebugCascades'),
416
+ // Read by the PCSS implementation's temporal jitter.
417
+ pb.int('framestamp')
418
+ ]);
419
+ const lightStruct = pb.defineStruct([
420
+ pb.vec3('sunDir'),
421
+ pb.int('shadowCascades'),
422
+ pb.vec4('positionAndRange'),
423
+ pb.vec4('directionAndCutoff'),
424
+ pb.vec4('diffuseAndIntensity'),
425
+ pb.vec4('extraParams'),
426
+ pb.vec4('cascadeDistances'),
427
+ pb.vec4('depthBiasValues'),
428
+ pb.vec4('shadowCameraParams'),
429
+ pb.vec4('depthBiasScales'),
430
+ pb.vec4('implParams'),
431
+ pb.vec4[16]('shadowMatrices'),
432
+ pb.float('shadowStrength'),
433
+ pb.float('envLightStrength'),
434
+ pb.float('envLightSpecularStrength')
435
+ ]);
436
+ this.camera = cameraStruct().uniform(0);
437
+ this.light = lightStruct().uniform(0);
438
+ this.cameraForward = pb.vec3().uniform(0);
439
+ const shadowMap = shadow.shadowMap;
440
+ const shadowTex = shadowMap.isTextureCube() ? shadowMap.isDepth() ? pb.texCubeShadow() : pb.texCube() : shadowMap.isTexture2D() ? shadowMap.isDepth() ? pb.tex2DShadow() : pb.tex2D() : shadowMap.isDepth() ? pb.tex2DArrayShadow() : pb.tex2DArray();
441
+ if (!shadowMap.isDepth() && !device.getDeviceCaps().textureCaps.getTextureFormatInfo(shadowMap.format).filterable) {
442
+ shadowTex.sampleType('unfilterable-float');
443
+ }
444
+ this[SHADOW_MAP_UNIFORM] = shadowTex.uniform(0);
445
+ // How much sun reaches a point inside the water column.
446
+ //
447
+ // No normal offset bias: that exists to stop a surface shadowing
448
+ // itself at grazing incidence, and a point floating in the medium has
449
+ // no surface to do it with. NoL is passed as 1 for the same reason -
450
+ // there is no receiver orientation, and 1 is the value that asks the
451
+ // depth bias for its smallest correction.
452
+ pb.func('zUnderwaterShadow', [
453
+ pb.vec3('worldPos'),
454
+ pb.float('viewDepth')
455
+ ], function() {
456
+ if (numCascades > 1) {
457
+ this.$l.comparison = pb.vec4(pb.greaterThan(pb.vec4(this.viewDepth), this.light.cascadeDistances));
458
+ this.$l.cascadeFlags = pb.vec4(pb.float(pb.greaterThan(this.light.shadowCascades, 0)), pb.float(pb.greaterThan(this.light.shadowCascades, 1)), pb.float(pb.greaterThan(this.light.shadowCascades, 2)), pb.float(pb.greaterThan(this.light.shadowCascades, 3)));
459
+ this.$l.split = pb.int(pb.dot(this.comparison, this.cascadeFlags));
460
+ if (device.type === 'webgl') {
461
+ // WebGL1 cannot index the matrix array dynamically.
462
+ this.$l.shadowVertex = pb.vec4();
463
+ this.$for(pb.int('cascade'), 0, 4, function() {
464
+ this.$if(pb.equal(this.cascade, this.split), function() {
465
+ this.shadowVertex = ShaderHelper.calculateShadowSpaceVertex(this, pb.vec4(this.worldPos, 1), this.cascade);
466
+ this.$break();
467
+ });
468
+ });
469
+ } else {
470
+ this.$l.shadowVertex = ShaderHelper.calculateShadowSpaceVertex(this, pb.vec4(this.worldPos, 1), this.split);
471
+ }
472
+ this.$l.shadow = shadow.impl.computeShadowCSM(shadow, this, this.shadowVertex, pb.float(1), this.split);
473
+ } else {
474
+ this.$l.shadowVertex = ShaderHelper.calculateShadowSpaceVertex(this, pb.vec4(this.worldPos, 1));
475
+ this.$l.shadow = shadow.impl.computeShadow(shadow, this, this.shadowVertex, pb.float(1));
476
+ }
477
+ // Past the shadow distance there is no map left to read, so the
478
+ // shafts have to come back to unshadowed rather than to whatever the
479
+ // last cascade's border holds. Same fade the surfaces use, so a shaft
480
+ // and the sea bed under it stop being shadowed together.
481
+ this.$l.shadowDistance = this.light.shadowCameraParams.w;
482
+ this.shadow = pb.mix(this.shadow, 1, pb.smoothStep(pb.mul(this.shadowDistance, 0.8), this.shadowDistance, pb.distance(this.camera.position.xyz, this.worldPos)));
483
+ this.shadow = pb.mix(1, this.shadow, this.light.shadowStrength);
484
+ this.$return(pb.clamp(this.shadow, 0, 1));
485
+ });
486
+ }
487
+ this.$outputs.outColor = pb.vec4();
488
+ pb.main(function() {
489
+ this.$l.depthValue = pb.textureSample(this.depthTex, this.$inputs.uv).r;
490
+ this.$l.clipSpacePos = pb.vec4(pb.sub(pb.mul(this.$inputs.uv, 2), pb.vec2(1)), ShaderHelper.deviceDepthToClipZ(this, this.depthValue), 1);
491
+ this.$l.hPos = pb.mul(this.invProjViewMatrix, this.clipSpacePos);
492
+ this.$l.worldPos = pb.div(this.hPos, this.hPos.w).xyz;
493
+ this.$l.isSky = ShaderHelper.isFarthestDepth(this, this.depthValue);
494
+ // Direction the eye is looking, as a unit vector. Normalised from the
495
+ // raw difference rather than divided by the capped path length below:
496
+ // the cap is a limit on how far the medium is integrated, not on where
497
+ // the geometry is, so dividing by it scaled this to |worldPos - eye| /
498
+ // cap - up to 2.4 in the turbid ocean preset. That put cosTheta outside
499
+ // [-1, 1], drove the Henyey-Greenstein denominator negative, and the
500
+ // clamp guarding pow() then returned a phase of some 40000 in a flat
501
+ // band across the distance.
502
+ this.$l.viewDir = pb.normalize(pb.sub(this.worldPos, this.cameraPosition));
503
+ // A ray that reached the sky never left the water, so it takes the
504
+ // whole cap rather than the far plane's world position. That is the
505
+ // one thing that makes the horizon converge to the medium instead of
506
+ // showing the sky through it.
507
+ this.$l.pathLength = this.$choice(this.isSky, this.params.z, pb.min(pb.distance(this.worldPos, this.cameraPosition), this.params.z));
508
+ this.$l.transmittance = pb.exp(pb.neg(pb.mul(this.mediumExtinction, this.pathLength)));
509
+ if (!inscatter) {
510
+ this.$outputs.outColor = pb.vec4(this.transmittance, 1);
511
+ } else {
512
+ // Downwelling sky light reaching the column, attenuated over the
513
+ // vertical depth rather than along the view ray: what lights the
514
+ // water is the sun and sky above it, and how far down this stretch
515
+ // of column sits is what decides how much of that is left. Averaged
516
+ // over the two ends of the path, which is exact for a column at
517
+ // constant depth and a fair stand-in when it is not.
518
+ this.$l.endY = this.$choice(this.isSky, this.cameraPosition.y, this.worldPos.y);
519
+ this.$l.columnDepth = pb.mul(0.5, pb.add(pb.max(pb.sub(this.params.y, this.cameraPosition.y), 0), pb.max(pb.sub(this.params.y, this.endY), 0)));
520
+ this.$l.ambient = pb.mul(pb.textureSampleLevel(this.skyLut, pb.vec2(0.5), 0).rgb, this.params.x, pb.exp(pb.neg(pb.mul(this.mediumExtinction, this.columnDepth))));
521
+ // Single scattering: the albedo is the share of the extinguished
522
+ // energy that comes back rather than being absorbed, the same form
523
+ // getScattering() uses on the surface.
524
+ this.$l.inscatter = pb.mul(this.mediumAlbedo, pb.sub(pb.vec3(1), this.transmittance), this.ambient);
525
+ if (godRays) {
526
+ // Shafts of sunlight through the column. The ambient term above is
527
+ // directionless and cannot produce them; this is the same single
528
+ // scattering integral evaluated against the sun instead, and the
529
+ // only way to get it is to walk the ray, because how much sun
530
+ // reaches a point depends on the surface overhead at that point.
531
+ //
532
+ // Angle between the sun's travel and the direction the scattered
533
+ // light has to leave in to reach the eye, which is back along the
534
+ // view ray. Constant along a ray from a directional light, so it
535
+ // is hoisted out of the march. Clamped because both inputs are
536
+ // unit vectors only up to rounding, and the phase function below
537
+ // is not defined outside the range.
538
+ this.$l.cosTheta = pb.clamp(pb.neg(pb.dot(this.sunDirection, this.viewDir)), -1, 1);
539
+ this.$l.phase = waterScatterPhase(this, this.cosTheta, this.phaseParams.x);
540
+ // Dithered sample offset. A fixed one puts every ray's samples on
541
+ // the same set of distances, and the shafts then show as concentric
542
+ // bands rather than as beams; the noise turns that structure into
543
+ // grain the eye reads as the water itself.
544
+ this.$l.jitter = interleavedGradientNoise(this, this.$builtins.fragCoord.xy);
545
+ // The integral is sigma_s * integral(sunAtP(t) * exp(-sigma_t * t) dt),
546
+ // and only `sunAtP` needs sampling - the exponential is analytic.
547
+ // So the march estimates the transmittance-weighted *mean* of
548
+ // sunAtP and the closed form below supplies the rest.
549
+ //
550
+ // Samples are placed by inverting that exponential rather than
551
+ // spread evenly along the ray. Evenly spaced ones broke the turbid
552
+ // ocean preset: at sigma_t = 0.46/m over a 125m cap, 24 steps sit
553
+ // 5.2m apart against a 2.2m mean free path, so the first sample
554
+ // outweighed the second tenfold and the red channel was decided by
555
+ // a single jittered position - noise per pixel. Importance
556
+ // sampling packs them into the few metres that carry the weight,
557
+ // where consecutive samples land close enough that the jitter
558
+ // barely moves them.
559
+ //
560
+ // Placement goes by the *least* attenuated channel, not by the
561
+ // luminance-weighted mean. The per-channel correction is
562
+ // exp(-sigma_c * t) / pdf, which grows like
563
+ // exp((sigma_place - sigma_c) * t): any channel thinner than the
564
+ // placement rate gets an exponentially growing weight, and its
565
+ // mean ends up decided by the single furthest sample. The
566
+ // luminance mean did exactly that here - sigma_lum = 0.30 against
567
+ // a blue 0.24 put a factor of 2000 on the last sample at 125m,
568
+ // which is both far outside the caustic map (so no pattern, hence
569
+ // no visible shafts) and violently jittered (hence the grain).
570
+ // Taking the minimum makes every exponent non-positive, so no
571
+ // weight can ever run away.
572
+ this.$l.sigmaPlace = pb.max(pb.min(pb.min(this.mediumExtinction.x, this.mediumExtinction.y), this.mediumExtinction.z), 1e-3);
573
+ // Renormalises the exponential to the finite path, so the samples
574
+ // span exactly the visible stretch however short it is.
575
+ this.$l.cdfMax = pb.sub(1, pb.exp(pb.neg(pb.mul(this.sigmaPlace, this.pathLength))));
576
+ this.$l.shafts = pb.vec3(0);
577
+ this.$l.weightSum = pb.vec3(0);
578
+ this.$for(pb.int('i'), 0, steps, function() {
579
+ this.$l.u = pb.div(pb.add(pb.float(this.i), this.jitter), steps);
580
+ this.$l.t = pb.min(pb.div(pb.neg(pb.log(pb.max(pb.sub(1, pb.mul(this.u, this.cdfMax)), 1e-6))), this.sigmaPlace), this.pathLength);
581
+ this.$l.samplePos = pb.add(this.cameraPosition, pb.mul(this.viewDir, this.t));
582
+ // How much sun survives to this point: the caustic map's own
583
+ // answer, which already carries both the Beer-Lambert
584
+ // transmittance down the sun ray and the focusing pattern the
585
+ // waves impose on it. Sharing it is what makes a shaft line up
586
+ // with the caustic cell it lands in.
587
+ this.$l.sunAtP = ShaderHelper.sampleWaterCaustic(this, this.samplePos, pb.int(LIGHT_TYPE_DIRECTIONAL), this.sunDirection, false);
588
+ if (shadow) {
589
+ // What the caustic map cannot know: it describes what the
590
+ // *surface* did to the sunlight and nothing about what stands
591
+ // under it, so without this a shaft runs straight through a
592
+ // piling. The sea bed's own shadow is unaffected either way -
593
+ // that comes from the ordinary lighting path.
594
+ this.$l.viewDepth = pb.dot(pb.sub(this.samplePos, this.cameraPosition), this.cameraForward);
595
+ this.sunAtP = pb.mul(this.sunAtP, this.zUnderwaterShadow(this.samplePos, this.viewDepth));
596
+ }
597
+ // Transmittance back to the eye over the density the sample was
598
+ // drawn from. The constant factors of the pdf - sigmaPlace and
599
+ // cdfMax - are the same for every sample, so they cancel in the
600
+ // ratio below and only the exponentials are kept.
601
+ this.$l.weight = pb.exp(pb.mul(pb.sub(pb.vec3(this.sigmaPlace), this.mediumExtinction), this.t));
602
+ this.shafts = pb.add(this.shafts, pb.mul(this.sunAtP, this.weight));
603
+ this.weightSum = pb.add(this.weightSum, this.weight);
604
+ });
605
+ // The closed form of the same integral with sunAtP held at its
606
+ // sampled mean: sigma_s/sigma_t * (1 - exp(-sigma_t * path)) is
607
+ // exactly the (1 - transmittance) factor the ambient term uses, so
608
+ // the two halves of the in-scattering share one normalisation and
609
+ // a shaft can never exceed the light available to it.
610
+ this.$l.meanSun = pb.div(this.shafts, pb.max(this.weightSum, pb.vec3(1e-6)));
611
+ this.inscatter = pb.add(this.inscatter, pb.mul(this.meanSun, this.mediumAlbedo, pb.sub(pb.vec3(1), this.transmittance), this.sunColor, pb.mul(this.phase, this.params.w)));
612
+ }
613
+ this.$outputs.outColor = pb.vec4(this.inscatter, 1);
614
+ }
615
+ });
616
+ }
617
+ });
618
+ program.name = inscatter ? '@UnderwaterInscatter' : '@UnderwaterExtinction';
619
+ return program;
620
+ }
621
+ }
622
+
623
+ export { UnderwaterRenderer, selectUnderwaterSource, setWaterSubmergence };
624
+ //# sourceMappingURL=underwater.js.map