@zephyr3d/scene 0.9.27 → 0.10.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 (502) hide show
  1. package/README.md +60 -27
  2. package/dist/animation/animation_bank.js +634 -0
  3. package/dist/animation/animation_bank.js.map +1 -0
  4. package/dist/animation/animation_bank_gpu.js +256 -0
  5. package/dist/animation/animation_bank_gpu.js.map +1 -0
  6. package/dist/animation/animationset.js +54 -0
  7. package/dist/animation/animationset.js.map +1 -1
  8. package/dist/animation/cloth/gpu_cloth_component.js +406 -0
  9. package/dist/animation/cloth/gpu_cloth_component.js.map +1 -0
  10. package/dist/animation/cloth/gpu_cloth_system.js +332 -202
  11. package/dist/animation/cloth/gpu_cloth_system.js.map +1 -1
  12. package/dist/animation/gpu_animation_driver.js +289 -0
  13. package/dist/animation/gpu_animation_driver.js.map +1 -0
  14. package/dist/animation/joint_dynamics/collision.js +112 -1
  15. package/dist/animation/joint_dynamics/collision.js.map +1 -1
  16. package/dist/animation/joint_dynamics/controller.js +6 -0
  17. package/dist/animation/joint_dynamics/controller.js.map +1 -1
  18. package/dist/animation/joint_dynamics/joint_dynamics_system.js +11 -0
  19. package/dist/animation/joint_dynamics/joint_dynamics_system.js.map +1 -1
  20. package/dist/animation/joint_dynamics/solver.js +50 -12
  21. package/dist/animation/joint_dynamics/solver.js.map +1 -1
  22. package/dist/animation/joint_dynamics/types.js.map +1 -1
  23. package/dist/animation/skeleton.js +279 -55
  24. package/dist/animation/skeleton.js.map +1 -1
  25. package/dist/animation/skin_palette_atlas.js +252 -0
  26. package/dist/animation/skin_palette_atlas.js.map +1 -0
  27. package/dist/animation/spring/multi_chain_spring_system.js +298 -38
  28. package/dist/animation/spring/multi_chain_spring_system.js.map +1 -1
  29. package/dist/animation/spring/spring_chain.js +2 -0
  30. package/dist/animation/spring/spring_chain.js.map +1 -1
  31. package/dist/animation/spring/spring_collider.js +159 -21
  32. package/dist/animation/spring/spring_collider.js.map +1 -1
  33. package/dist/animation/spring/spring_particle.js +1 -0
  34. package/dist/animation/spring/spring_particle.js.map +1 -1
  35. package/dist/animation/spring/spring_solver.js +173 -0
  36. package/dist/animation/spring/spring_solver.js.map +1 -0
  37. package/dist/animation/spring/spring_system.js +249 -24
  38. package/dist/animation/spring/spring_system.js.map +1 -1
  39. package/dist/animation/spring_modifier.js +32 -1
  40. package/dist/animation/spring_modifier.js.map +1 -1
  41. package/dist/app/app.js +26 -0
  42. package/dist/app/app.js.map +1 -1
  43. package/dist/app/engine.js +86 -15
  44. package/dist/app/engine.js.map +1 -1
  45. package/dist/app/gamepad.js +191 -0
  46. package/dist/app/gamepad.js.map +1 -0
  47. package/dist/app/inputactions.js +271 -0
  48. package/dist/app/inputactions.js.map +1 -0
  49. package/dist/app/inputmgr.js +331 -22
  50. package/dist/app/inputmgr.js.map +1 -1
  51. package/dist/app/loading.js +138 -0
  52. package/dist/app/loading.js.map +1 -0
  53. package/dist/app/pipelinecache.js +209 -0
  54. package/dist/app/pipelinecache.js.map +1 -0
  55. package/dist/app/runtimescript.js +25 -0
  56. package/dist/app/runtimescript.js.map +1 -1
  57. package/dist/app/scriptingsystem.js +54 -0
  58. package/dist/app/scriptingsystem.js.map +1 -1
  59. package/dist/app/scriptregistry.js +7 -2
  60. package/dist/app/scriptregistry.js.map +1 -1
  61. package/dist/asset/asset_manifest.js +59 -0
  62. package/dist/asset/asset_manifest.js.map +1 -0
  63. package/dist/asset/asset_preload.js +258 -0
  64. package/dist/asset/asset_preload.js.map +1 -0
  65. package/dist/asset/assetmanager.js +117 -65
  66. package/dist/asset/assetmanager.js.map +1 -1
  67. package/dist/asset/loaders/ktx2/basis/LICENSE +201 -0
  68. package/dist/asset/loaders/ktx2/basis/basis_transcoder.js +2 -0
  69. package/dist/asset/loaders/ktx2/basis/basis_transcoder.wasm +0 -0
  70. package/dist/asset/loaders/ktx2/basis_location.js +17 -0
  71. package/dist/asset/loaders/ktx2/basis_location.js.map +1 -0
  72. package/dist/asset/loaders/ktx2/ktx2_loader.js +88 -0
  73. package/dist/asset/loaders/ktx2/ktx2_loader.js.map +1 -0
  74. package/dist/asset/loaders/ktx2/ktx2_targets.js +118 -0
  75. package/dist/asset/loaders/ktx2/ktx2_targets.js.map +1 -0
  76. package/dist/asset/loaders/ktx2/transcoder_pool.js +259 -0
  77. package/dist/asset/loaders/ktx2/transcoder_pool.js.map +1 -0
  78. package/dist/asset/mesh_lod_builder.js +104 -0
  79. package/dist/asset/mesh_lod_builder.js.map +1 -0
  80. package/dist/asset/mesh_settings.js +111 -0
  81. package/dist/asset/mesh_settings.js.map +1 -0
  82. package/dist/asset/meshopt_decoder.js +18 -0
  83. package/dist/asset/meshopt_decoder.js.map +1 -0
  84. package/dist/asset/meshopt_simplifier.js +26 -0
  85. package/dist/asset/meshopt_simplifier.js.map +1 -0
  86. package/dist/asset/model.js +171 -1
  87. package/dist/asset/model.js.map +1 -1
  88. package/dist/asset/texture_settings.js +231 -0
  89. package/dist/asset/texture_settings.js.map +1 -0
  90. package/dist/asset/zmsh_binary.js +131 -0
  91. package/dist/asset/zmsh_binary.js.map +1 -0
  92. package/dist/asset/zsdf_binary.js +111 -0
  93. package/dist/asset/zsdf_binary.js.map +1 -0
  94. package/dist/blitter/bilateralblur.js +1 -2
  95. package/dist/blitter/bilateralblur.js.map +1 -1
  96. package/dist/blitter/gaussianblur.js +1 -6
  97. package/dist/blitter/gaussianblur.js.map +1 -1
  98. package/dist/camera/base.js +31 -10
  99. package/dist/camera/base.js.map +1 -1
  100. package/dist/camera/camera.js +104 -178
  101. package/dist/camera/camera.js.map +1 -1
  102. package/dist/camera/follow.js +342 -0
  103. package/dist/camera/follow.js.map +1 -0
  104. package/dist/camera/fps.js +9 -7
  105. package/dist/camera/fps.js.map +1 -1
  106. package/dist/camera/orbit.js +46 -19
  107. package/dist/camera/orbit.js.map +1 -1
  108. package/dist/index.d.ts +25436 -15990
  109. package/dist/index.js +65 -11
  110. package/dist/index.js.map +1 -1
  111. package/dist/material/blinn.js +3 -0
  112. package/dist/material/blinn.js.map +1 -1
  113. package/dist/material/eye.js +1 -1
  114. package/dist/material/eye.js.map +1 -1
  115. package/dist/material/hair.js +2 -3
  116. package/dist/material/hair.js.map +1 -1
  117. package/dist/material/lambert.js +4 -1
  118. package/dist/material/lambert.js.map +1 -1
  119. package/dist/material/material.js +66 -4
  120. package/dist/material/material.js.map +1 -1
  121. package/dist/material/meshmaterial.js +118 -45
  122. package/dist/material/meshmaterial.js.map +1 -1
  123. package/dist/material/mixins/lightmodel/blinnphong.js +1 -1
  124. package/dist/material/mixins/lightmodel/blinnphong.js.map +1 -1
  125. package/dist/material/mixins/lightmodel/lambert.js +1 -1
  126. package/dist/material/mixins/lightmodel/lambert.js.map +1 -1
  127. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js +34 -61
  128. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js.map +1 -1
  129. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js +16 -2
  130. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js.map +1 -1
  131. package/dist/material/mixins/lightmodel/pbrspecularglossness.js +2 -1
  132. package/dist/material/mixins/lightmodel/pbrspecularglossness.js.map +1 -1
  133. package/dist/material/mixins/lit.js +191 -83
  134. package/dist/material/mixins/lit.js.map +1 -1
  135. package/dist/material/mixins/pbr/common.js +31 -206
  136. package/dist/material/mixins/pbr/common.js.map +1 -1
  137. package/dist/material/mtoon.js +1 -1
  138. package/dist/material/mtoon.js.map +1 -1
  139. package/dist/material/pbrmr.js +3 -0
  140. package/dist/material/pbrmr.js.map +1 -1
  141. package/dist/material/pbrsg.js +3 -0
  142. package/dist/material/pbrsg.js.map +1 -1
  143. package/dist/material/sdf_debug.js +78 -0
  144. package/dist/material/sdf_debug.js.map +1 -0
  145. package/dist/material/shader/helper.js +333 -191
  146. package/dist/material/shader/helper.js.map +1 -1
  147. package/dist/material/shader/terrain_blend.js +204 -43
  148. package/dist/material/shader/terrain_blend.js.map +1 -1
  149. package/dist/material/skin.js +331 -297
  150. package/dist/material/skin.js.map +1 -1
  151. package/dist/material/skinprofile.js +1058 -0
  152. package/dist/material/skinprofile.js.map +1 -0
  153. package/dist/material/sss.js +353 -0
  154. package/dist/material/sss.js.map +1 -0
  155. package/dist/material/sss_kernel.js +265 -0
  156. package/dist/material/sss_kernel.js.map +1 -0
  157. package/dist/material/sssprofile.js +879 -0
  158. package/dist/material/sssprofile.js.map +1 -0
  159. package/dist/material/subsurfaceprofile.js +4 -8
  160. package/dist/material/subsurfaceprofile.js.map +1 -1
  161. package/dist/material/terrain-cm.js +464 -188
  162. package/dist/material/terrain-cm.js.map +1 -1
  163. package/dist/material/unlit.js +3 -0
  164. package/dist/material/unlit.js.map +1 -1
  165. package/dist/material/water.js +1261 -1438
  166. package/dist/material/water.js.map +1 -1
  167. package/dist/node_modules/meshoptimizer/LICENSE +21 -0
  168. package/dist/node_modules/meshoptimizer/meshopt_decoder.js +197 -0
  169. package/dist/node_modules/meshoptimizer/meshopt_decoder.js.map +1 -0
  170. package/dist/node_modules/meshoptimizer/meshopt_simplifier.js +677 -0
  171. package/dist/node_modules/meshoptimizer/meshopt_simplifier.js.map +1 -0
  172. package/dist/physics/character.js +226 -0
  173. package/dist/physics/character.js.map +1 -0
  174. package/dist/physics/collider.js +209 -0
  175. package/dist/physics/collider.js.map +1 -0
  176. package/dist/physics/events.js +79 -0
  177. package/dist/physics/events.js.map +1 -0
  178. package/dist/physics/joint.js +272 -0
  179. package/dist/physics/joint.js.map +1 -0
  180. package/dist/physics/node_physics.js +234 -0
  181. package/dist/physics/node_physics.js.map +1 -0
  182. package/dist/physics/part.js +91 -0
  183. package/dist/physics/part.js.map +1 -0
  184. package/dist/physics/rigid_body.js +379 -0
  185. package/dist/physics/rigid_body.js.map +1 -0
  186. package/dist/physics/vehicle.js +335 -0
  187. package/dist/physics/vehicle.js.map +1 -0
  188. package/dist/physics/world.js +25 -0
  189. package/dist/physics/world.js.map +1 -0
  190. package/dist/posteffect/bloom.js +49 -14
  191. package/dist/posteffect/bloom.js.map +1 -1
  192. package/dist/posteffect/motionblur.js +106 -106
  193. package/dist/posteffect/motionblur.js.map +1 -1
  194. package/dist/posteffect/postsss.js +572 -0
  195. package/dist/posteffect/postsss.js.map +1 -0
  196. package/dist/posteffect/sao.js +2 -6
  197. package/dist/posteffect/sao.js.map +1 -1
  198. package/dist/posteffect/skinsss.js +605 -355
  199. package/dist/posteffect/skinsss.js.map +1 -1
  200. package/dist/posteffect/ssgi.js +91 -162
  201. package/dist/posteffect/ssgi.js.map +1 -1
  202. package/dist/posteffect/ssr.js +3 -11
  203. package/dist/posteffect/ssr.js.map +1 -1
  204. package/dist/posteffect/sss.js +4 -13
  205. package/dist/posteffect/sss.js.map +1 -1
  206. package/dist/posteffect/taa.js +1 -0
  207. package/dist/posteffect/taa.js.map +1 -1
  208. package/dist/render/abuffer_oit.js +3 -2
  209. package/dist/render/abuffer_oit.js.map +1 -1
  210. package/dist/render/clipmap.js +179 -64
  211. package/dist/render/clipmap.js.map +1 -1
  212. package/dist/render/cluster_light.js +958 -224
  213. package/dist/render/cluster_light.js.map +1 -1
  214. package/dist/render/cull_visitor.js +25 -5
  215. package/dist/render/cull_visitor.js.map +1 -1
  216. package/dist/render/debug_views.js +37 -0
  217. package/dist/render/debug_views.js.map +1 -0
  218. package/dist/render/depthpass.js +5 -1
  219. package/dist/render/depthpass.js.map +1 -1
  220. package/dist/render/drawable_mixin.js +53 -6
  221. package/dist/render/drawable_mixin.js.map +1 -1
  222. package/dist/render/dualdepthpeeling_oit.js +5 -5
  223. package/dist/render/dualdepthpeeling_oit.js.map +1 -1
  224. package/dist/render/envlight.js +196 -113
  225. package/dist/render/envlight.js.map +1 -1
  226. package/dist/render/fbm_wavegenerator.js +5 -10
  227. package/dist/render/fbm_wavegenerator.js.map +1 -1
  228. package/dist/render/fft_wavegenerator.js +247 -157
  229. package/dist/render/fft_wavegenerator.js.map +1 -1
  230. package/dist/render/gerstner_wavegenerator.js +16 -38
  231. package/dist/render/gerstner_wavegenerator.js.map +1 -1
  232. package/dist/render/gi/probe_volume.js +1334 -0
  233. package/dist/render/gi/probe_volume.js.map +1 -0
  234. package/dist/render/gi/scene_gi.js +282 -0
  235. package/dist/render/gi/scene_gi.js.map +1 -0
  236. package/dist/render/gi/voxel_atlas.js +198 -0
  237. package/dist/render/gi/voxel_atlas.js.map +1 -0
  238. package/dist/render/gi/voxel_lighting.js +652 -0
  239. package/dist/render/gi/voxel_lighting.js.map +1 -0
  240. package/dist/render/gi/voxel_scene.js +642 -0
  241. package/dist/render/gi/voxel_scene.js.map +1 -0
  242. package/dist/render/gi/voxelize_pass.js +223 -0
  243. package/dist/render/gi/voxelize_pass.js.map +1 -0
  244. package/dist/render/globalbindgroup_allocator.js +3 -2
  245. package/dist/render/globalbindgroup_allocator.js.map +1 -1
  246. package/dist/render/gpu_picking.js +9 -36
  247. package/dist/render/gpu_picking.js.map +1 -1
  248. package/dist/render/instance_culling.js +499 -0
  249. package/dist/render/instance_culling.js.map +1 -0
  250. package/dist/render/lightpass.js +19 -8
  251. package/dist/render/lightpass.js.map +1 -1
  252. package/dist/render/lod.js +46 -0
  253. package/dist/render/lod.js.map +1 -0
  254. package/dist/render/primitive.js +301 -9
  255. package/dist/render/primitive.js.map +1 -1
  256. package/dist/render/primitive_readback.js +209 -0
  257. package/dist/render/primitive_readback.js.map +1 -0
  258. package/dist/render/render_queue.js +162 -11
  259. package/dist/render/render_queue.js.map +1 -1
  260. package/dist/render/renderer.js +10 -7
  261. package/dist/render/renderer.js.map +1 -1
  262. package/dist/render/rendergraph/blackboard.js +14 -1
  263. package/dist/render/rendergraph/blackboard.js.map +1 -1
  264. package/dist/render/rendergraph/executor.js +136 -0
  265. package/dist/render/rendergraph/executor.js.map +1 -1
  266. package/dist/render/rendergraph/forward_plus_builder.js +272 -55
  267. package/dist/render/rendergraph/forward_plus_builder.js.map +1 -1
  268. package/dist/render/rendergraph/frame_graph_context.js +41 -0
  269. package/dist/render/rendergraph/frame_graph_context.js.map +1 -0
  270. package/dist/render/rendergraph/scene_render_context.js +31 -2
  271. package/dist/render/rendergraph/scene_render_context.js.map +1 -1
  272. package/dist/render/rendergraph/types.js.map +1 -1
  273. package/dist/render/renderpass.js +28 -5
  274. package/dist/render/renderpass.js.map +1 -1
  275. package/dist/render/sdf/box_allocator.js +98 -0
  276. package/dist/render/sdf/box_allocator.js.map +1 -0
  277. package/dist/render/sdf/global_sdf.js +865 -0
  278. package/dist/render/sdf/global_sdf.js.map +1 -0
  279. package/dist/render/sdf/scene_sdf.js +562 -0
  280. package/dist/render/sdf/scene_sdf.js.map +1 -0
  281. package/dist/render/sdf/sdf_atlas.js +227 -0
  282. package/dist/render/sdf/sdf_atlas.js.map +1 -0
  283. package/dist/render/sdf/sdf_programs.js +42 -0
  284. package/dist/render/sdf/sdf_programs.js.map +1 -0
  285. package/dist/render/sdf/sdf_view.js +199 -0
  286. package/dist/render/sdf/sdf_view.js.map +1 -0
  287. package/dist/render/sdf/waterline.js +610 -0
  288. package/dist/render/sdf/waterline.js.map +1 -0
  289. package/dist/render/shadow_mask_pass.js +142 -25
  290. package/dist/render/shadow_mask_pass.js.map +1 -1
  291. package/dist/render/shore_wave_generator.js +1193 -0
  292. package/dist/render/shore_wave_generator.js.map +1 -0
  293. package/dist/render/sky.js +516 -86
  294. package/dist/render/sky.js.map +1 -1
  295. package/dist/render/transmission_thickness_pass.js +477 -0
  296. package/dist/render/transmission_thickness_pass.js.map +1 -0
  297. package/dist/render/underwater.js +58 -158
  298. package/dist/render/underwater.js.map +1 -1
  299. package/dist/render/virtualtexture/virtual_texture.js +1214 -0
  300. package/dist/render/virtualtexture/virtual_texture.js.map +1 -0
  301. package/dist/render/volumetric_cloud/cloud_common.js +472 -0
  302. package/dist/render/volumetric_cloud/cloud_common.js.map +1 -0
  303. package/dist/render/volumetric_cloud/cloud_noise.js +189 -0
  304. package/dist/render/volumetric_cloud/cloud_noise.js.map +1 -0
  305. package/dist/render/volumetric_cloud/debug_view.js +131 -0
  306. package/dist/render/volumetric_cloud/debug_view.js.map +1 -0
  307. package/dist/render/volumetric_cloud/resources.js +380 -0
  308. package/dist/render/volumetric_cloud/resources.js.map +1 -0
  309. package/dist/render/volumetric_cloud/sky_cache.js +380 -0
  310. package/dist/render/volumetric_cloud/sky_cache.js.map +1 -0
  311. package/dist/render/volumetric_cloud/volumetric_cloud.js +318 -0
  312. package/dist/render/volumetric_cloud/volumetric_cloud.js.map +1 -0
  313. package/dist/render/water_caustics.js +134 -319
  314. package/dist/render/water_caustics.js.map +1 -1
  315. package/dist/render/water_foam.js +627 -0
  316. package/dist/render/water_foam.js.map +1 -0
  317. package/dist/render/water_interaction.js +1795 -0
  318. package/dist/render/water_interaction.js.map +1 -0
  319. package/dist/render/weightedblended_oit.js +3 -3
  320. package/dist/render/weightedblended_oit.js.map +1 -1
  321. package/dist/scene/basesprite.js +9 -4
  322. package/dist/scene/basesprite.js.map +1 -1
  323. package/dist/scene/batchgroup.js +108 -12
  324. package/dist/scene/batchgroup.js.map +1 -1
  325. package/dist/scene/environment.js +28 -7
  326. package/dist/scene/environment.js.map +1 -1
  327. package/dist/scene/foliage.js +1020 -0
  328. package/dist/scene/foliage.js.map +1 -0
  329. package/dist/scene/foliage_buckets.js +147 -0
  330. package/dist/scene/foliage_buckets.js.map +1 -0
  331. package/dist/scene/foliage_mask.js +358 -0
  332. package/dist/scene/foliage_mask.js.map +1 -0
  333. package/dist/scene/hair.js +15 -4
  334. package/dist/scene/hair.js.map +1 -1
  335. package/dist/scene/light.js +179 -13
  336. package/dist/scene/light.js.map +1 -1
  337. package/dist/scene/mesh.js +327 -45
  338. package/dist/scene/mesh.js.map +1 -1
  339. package/dist/scene/meshdrawable.js +6 -0
  340. package/dist/scene/meshdrawable.js.map +1 -1
  341. package/dist/scene/msdftext.js +9 -3
  342. package/dist/scene/msdftext.js.map +1 -1
  343. package/dist/scene/msdftextsprite.js +9 -3
  344. package/dist/scene/msdftextsprite.js.map +1 -1
  345. package/dist/scene/particlesys.js +8 -2
  346. package/dist/scene/particlesys.js.map +1 -1
  347. package/dist/scene/physics_settings.js +157 -0
  348. package/dist/scene/physics_settings.js.map +1 -0
  349. package/dist/scene/scene.js +190 -44
  350. package/dist/scene/scene.js.map +1 -1
  351. package/dist/scene/scene_node.js +147 -6
  352. package/dist/scene/scene_node.js.map +1 -1
  353. package/dist/scene/terrain-cm/bladegrassmaterial.js +735 -0
  354. package/dist/scene/terrain-cm/bladegrassmaterial.js.map +1 -0
  355. package/dist/scene/terrain-cm/grass.js +881 -42
  356. package/dist/scene/terrain-cm/grass.js.map +1 -1
  357. package/dist/scene/terrain-cm/grass_gpu.js +929 -0
  358. package/dist/scene/terrain-cm/grass_gpu.js.map +1 -0
  359. package/dist/scene/terrain-cm/grassmaterial.js +180 -9
  360. package/dist/scene/terrain-cm/grassmaterial.js.map +1 -1
  361. package/dist/scene/terrain-cm/terrain-cm.js +465 -36
  362. package/dist/scene/terrain-cm/terrain-cm.js.map +1 -1
  363. package/dist/scene/terrain-cm/terrain_gi_albedo.js +257 -0
  364. package/dist/scene/terrain-cm/terrain_gi_albedo.js.map +1 -0
  365. package/dist/scene/terrain-cm/terrain_rvt.js +395 -0
  366. package/dist/scene/terrain-cm/terrain_rvt.js.map +1 -0
  367. package/dist/scene/update_queue.js +54 -0
  368. package/dist/scene/update_queue.js.map +1 -0
  369. package/dist/scene/water.js +633 -268
  370. package/dist/scene/water.js.map +1 -1
  371. package/dist/scene/water_surface_sampler.js +324 -0
  372. package/dist/scene/water_surface_sampler.js.map +1 -0
  373. package/dist/scene/wind.js +317 -0
  374. package/dist/scene/wind.js.map +1 -0
  375. package/dist/shaders/atmosphere.js +388 -166
  376. package/dist/shaders/atmosphere.js.map +1 -1
  377. package/dist/shaders/fog.js +100 -32
  378. package/dist/shaders/fog.js.map +1 -1
  379. package/dist/shaders/gi.js +365 -0
  380. package/dist/shaders/gi.js.map +1 -0
  381. package/dist/shaders/ltc_rect.js +202 -0
  382. package/dist/shaders/ltc_rect.js.map +1 -0
  383. package/dist/shaders/misc.js +1 -10
  384. package/dist/shaders/misc.js.map +1 -1
  385. package/dist/shaders/noise.js +5 -1
  386. package/dist/shaders/noise.js.map +1 -1
  387. package/dist/shaders/probes.js +309 -0
  388. package/dist/shaders/probes.js.map +1 -0
  389. package/dist/shaders/sdf.js +537 -0
  390. package/dist/shaders/sdf.js.map +1 -0
  391. package/dist/shaders/shadow.js +104 -70
  392. package/dist/shaders/shadow.js.map +1 -1
  393. package/dist/shaders/skin_brdf.js +243 -0
  394. package/dist/shaders/skin_brdf.js.map +1 -0
  395. package/dist/shaders/ssgi.js +63 -0
  396. package/dist/shaders/ssgi.js.map +1 -0
  397. package/dist/shaders/ssgi_reprojection.js +118 -0
  398. package/dist/shaders/ssgi_reprojection.js.map +1 -0
  399. package/dist/shaders/ssr.js +6 -159
  400. package/dist/shaders/ssr.js.map +1 -1
  401. package/dist/shaders/temporal.js +38 -27
  402. package/dist/shaders/temporal.js.map +1 -1
  403. package/dist/shaders/water.js +32 -82
  404. package/dist/shaders/water.js.map +1 -1
  405. package/dist/shaders/water_medium.js +7 -21
  406. package/dist/shaders/water_medium.js.map +1 -1
  407. package/dist/shadow/dom.js +110 -19
  408. package/dist/shadow/dom.js.map +1 -1
  409. package/dist/shadow/esm.js +3 -3
  410. package/dist/shadow/esm.js.map +1 -1
  411. package/dist/shadow/pcf_opt.js +15 -48
  412. package/dist/shadow/pcf_opt.js.map +1 -1
  413. package/dist/shadow/pcf_pd.js +15 -53
  414. package/dist/shadow/pcf_pd.js.map +1 -1
  415. package/dist/shadow/pcss.js +34 -9
  416. package/dist/shadow/pcss.js.map +1 -1
  417. package/dist/shadow/shadow_impl.js +3 -8
  418. package/dist/shadow/shadow_impl.js.map +1 -1
  419. package/dist/shadow/shadow_region.js +40 -2
  420. package/dist/shadow/shadow_region.js.map +1 -1
  421. package/dist/shadow/shadowmapper.js +93 -53
  422. package/dist/shadow/shadowmapper.js.map +1 -1
  423. package/dist/shadow/ssm.js +18 -72
  424. package/dist/shadow/ssm.js.map +1 -1
  425. package/dist/shadow/vsm.js +3 -3
  426. package/dist/shadow/vsm.js.map +1 -1
  427. package/dist/shapes/shape.js +44 -1
  428. package/dist/shapes/shape.js.map +1 -1
  429. package/dist/utility/buoyancy.js +287 -0
  430. package/dist/utility/buoyancy.js.map +1 -0
  431. package/dist/utility/physical.js +10 -1
  432. package/dist/utility/physical.js.map +1 -1
  433. package/dist/utility/pmrem.js +17 -43
  434. package/dist/utility/pmrem.js.map +1 -1
  435. package/dist/utility/sdf/bake.js +435 -0
  436. package/dist/utility/sdf/bake.js.map +1 -0
  437. package/dist/utility/sdf/bvh.js +576 -0
  438. package/dist/utility/sdf/bvh.js.map +1 -0
  439. package/dist/utility/serialization/manager.js +34 -6
  440. package/dist/utility/serialization/manager.js.map +1 -1
  441. package/dist/utility/serialization/scene/animation.js +46 -5
  442. package/dist/utility/serialization/scene/animation.js.map +1 -1
  443. package/dist/utility/serialization/scene/batch.js +11 -4
  444. package/dist/utility/serialization/scene/batch.js.map +1 -1
  445. package/dist/utility/serialization/scene/camera.js +87 -254
  446. package/dist/utility/serialization/scene/camera.js.map +1 -1
  447. package/dist/utility/serialization/scene/cloth.js +25 -0
  448. package/dist/utility/serialization/scene/cloth.js.map +1 -0
  449. package/dist/utility/serialization/scene/common.js +14 -6
  450. package/dist/utility/serialization/scene/common.js.map +1 -1
  451. package/dist/utility/serialization/scene/foliage.js +821 -0
  452. package/dist/utility/serialization/scene/foliage.js.map +1 -0
  453. package/dist/utility/serialization/scene/hair.js +11 -4
  454. package/dist/utility/serialization/scene/hair.js.map +1 -1
  455. package/dist/utility/serialization/scene/light.js +115 -7
  456. package/dist/utility/serialization/scene/light.js.map +1 -1
  457. package/dist/utility/serialization/scene/material.js +388 -220
  458. package/dist/utility/serialization/scene/material.js.map +1 -1
  459. package/dist/utility/serialization/scene/mesh.js +69 -4
  460. package/dist/utility/serialization/scene/mesh.js.map +1 -1
  461. package/dist/utility/serialization/scene/node.js +140 -5
  462. package/dist/utility/serialization/scene/node.js.map +1 -1
  463. package/dist/utility/serialization/scene/node_physics.js +1318 -0
  464. package/dist/utility/serialization/scene/node_physics.js.map +1 -0
  465. package/dist/utility/serialization/scene/particle.js +11 -4
  466. package/dist/utility/serialization/scene/particle.js.map +1 -1
  467. package/dist/utility/serialization/scene/physics.js +120 -0
  468. package/dist/utility/serialization/scene/physics.js.map +1 -0
  469. package/dist/utility/serialization/scene/primitive.js +124 -10
  470. package/dist/utility/serialization/scene/primitive.js.map +1 -1
  471. package/dist/utility/serialization/scene/scene.js +1687 -51
  472. package/dist/utility/serialization/scene/scene.js.map +1 -1
  473. package/dist/utility/serialization/scene/sprite.js +11 -4
  474. package/dist/utility/serialization/scene/sprite.js.map +1 -1
  475. package/dist/utility/serialization/scene/terrain.js +163 -33
  476. package/dist/utility/serialization/scene/terrain.js.map +1 -1
  477. package/dist/utility/serialization/scene/text.js +11 -4
  478. package/dist/utility/serialization/scene/text.js.map +1 -1
  479. package/dist/utility/serialization/scene/water.js +1543 -264
  480. package/dist/utility/serialization/scene/water.js.map +1 -1
  481. package/dist/utility/serialization/types.js.map +1 -1
  482. package/dist/utility/shprojector.js +10 -22
  483. package/dist/utility/shprojector.js.map +1 -1
  484. package/dist/utility/textures/foam.js +170 -0
  485. package/dist/utility/textures/foam.js.map +1 -0
  486. package/dist/utility/textures/ggxlut.js +17 -43
  487. package/dist/utility/textures/ggxlut.js.map +1 -1
  488. package/dist/utility/textures/ltcdata.js +4 -4
  489. package/dist/utility/textures/ltcdata.js.map +1 -1
  490. package/dist/utility/textures/ltclut.js +16 -6
  491. package/dist/utility/textures/ltclut.js.map +1 -1
  492. package/dist/utility/unsupported.js +24 -0
  493. package/dist/utility/unsupported.js.map +1 -0
  494. package/dist/values.js +13 -1
  495. package/dist/values.js.map +1 -1
  496. package/package.json +25 -12
  497. package/src/asset/loaders/ktx2/basis/LICENSE +201 -0
  498. package/src/asset/loaders/ktx2/basis/basis_transcoder.js +2 -0
  499. package/src/asset/loaders/ktx2/basis/basis_transcoder.wasm +0 -0
  500. package/src/asset/meshoptimizer/LICENSE +21 -0
  501. package/dist/animation/joint_dynamics/convex_collider.js +0 -320
  502. package/dist/animation/joint_dynamics/convex_collider.js.map +0 -1
@@ -1,14 +1,20 @@
1
1
  import { applyMaterialMixins, MeshMaterial } from './meshmaterial.js';
2
- import { MaterialVaryingFlags, LIGHT_TYPE_DIRECTIONAL } from '../values.js';
2
+ import { InteractiveWaveGenerator } from '../render/water_interaction.js';
3
+ import { ShoreWaveGenerator } from '../render/shore_wave_generator.js';
4
+ import { WaterFoamField, FoamFieldWaveGenerator, WaveFoamSource } from '../render/water_foam.js';
5
+ import { MaterialVaryingFlags, LIGHT_TYPE_RECT, LIGHT_TYPE_DIRECTIONAL } from '../values.js';
3
6
  import { ShaderHelper } from './shader/helper.js';
4
- import { Interpolator, DWeakRef, Vector4, Vector3, DRef } from '@zephyr3d/base';
7
+ import { Vector4, Vector3, DRef } from '@zephyr3d/base';
5
8
  import { screenSpaceRayTracing_HiZ, screenSpaceRayTracing_Linear2D } from '../shaders/ssr.js';
6
9
  import { fetchSampler } from '../utility/misc.js';
10
+ import { PHYSICAL_BAKE_EXPOSURE } from '../utility/physical.js';
7
11
  import { mixinLight } from './mixins/lit.js';
8
12
  import { fresnelSchlick, distributionGGX, visGGX } from '../shaders/pbr.js';
9
- import { valueNoise, interleavedGradientNoise } from '../shaders/noise.js';
10
13
  import { waterScatterPhase } from '../shaders/water_medium.js';
11
- import { getDevice } from '../app/api.js';
14
+ import { evaluateLTCRectLightTerms } from '../shaders/ltc_rect.js';
15
+ import { getLTCMatLUT, getLTCAmpLUT } from '../utility/textures/ltclut.js';
16
+ import { sampleFoamTexture, foamTexelSize, getDefaultFoamTexture } from '../utility/textures/foam.js';
17
+ import { RenderGraphExecutor } from '../render/rendergraph/executor.js';
12
18
 
13
19
  /** Label/value pairs for {@link WaterDebugOutput}, for editor enumerations. */ const WATER_DEBUG_OUTPUTS = [
14
20
  {
@@ -19,26 +25,10 @@ import { getDevice } from '../app/api.js';
19
25
  label: 'Normal',
20
26
  value: 'normal'
21
27
  },
22
- {
23
- label: 'Diffuse normal',
24
- value: 'diffuseNormal'
25
- },
26
- {
27
- label: 'View facing',
28
- value: 'viewFacing'
29
- },
30
- {
31
- label: 'Front facing',
32
- value: 'frontFacing'
33
- },
34
28
  {
35
29
  label: 'Foam',
36
30
  value: 'foam'
37
31
  },
38
- {
39
- label: 'Shore foam',
40
- value: 'shoreFoam'
41
- },
42
32
  {
43
33
  label: 'Water depth',
44
34
  value: 'waterDepth'
@@ -67,18 +57,6 @@ import { getDevice } from '../app/api.js';
67
57
  label: 'Sun scattering',
68
58
  value: 'sunScattering'
69
59
  },
70
- {
71
- label: 'Sun phase',
72
- value: 'sunPhase'
73
- },
74
- {
75
- label: 'Sun integral',
76
- value: 'sunIntegral'
77
- },
78
- {
79
- label: 'Sun NoL',
80
- value: 'sunNoL'
81
- },
82
60
  {
83
61
  label: 'Shadow',
84
62
  value: 'shadow'
@@ -87,6 +65,10 @@ import { getDevice } from '../app/api.js';
87
65
  label: 'Subsurface',
88
66
  value: 'subsurface'
89
67
  },
68
+ {
69
+ label: 'Subsurface thickness',
70
+ value: 'subsurfaceThickness'
71
+ },
90
72
  {
91
73
  label: 'Specular',
92
74
  value: 'specular'
@@ -96,221 +78,188 @@ import { getDevice } from '../app/api.js';
96
78
  value: 'depth'
97
79
  },
98
80
  {
99
- label: 'Refract UV',
100
- value: 'refractUV'
81
+ label: 'Interaction',
82
+ value: 'interaction'
83
+ },
84
+ {
85
+ label: 'Foam field',
86
+ value: 'foamField'
101
87
  },
102
88
  {
103
- label: 'NaN',
104
- value: 'nan'
89
+ label: 'Shore regions (geometry)',
90
+ value: 'shoreRegions'
91
+ },
92
+ {
93
+ label: 'Shore field levels',
94
+ value: 'shoreLevels'
95
+ },
96
+ {
97
+ label: 'Swash handover (RGB)',
98
+ value: 'swashHandover'
99
+ },
100
+ {
101
+ label: 'Swash foam sources (RGB)',
102
+ value: 'swashFoamSources'
103
+ },
104
+ {
105
+ label: 'Swash foam cuts (RGB)',
106
+ value: 'swashFoamCuts'
107
+ },
108
+ {
109
+ label: 'Swash visibility (RGB)',
110
+ value: 'swashVisibility'
111
+ },
112
+ {
113
+ label: 'Swash ground support (RGB)',
114
+ value: 'swashGroundSupport'
105
115
  }
106
116
  ];
107
- /** Fresnel reflectance of water at normal incidence, for n = 1.333. */ const WATER_F0 = 0.02;
108
- /** Specular roughness of water close enough that the waves are resolved. */ const WATER_BASE_ROUGHNESS = 0.04;
109
- /**
110
- * Specular roughness once the distance fade has flattened the waves away.
111
- * Widened by the same amount the slope was cut, so the remaining mirror does
112
- * not alias.
113
- */ const WATER_DISTANT_ROUGHNESS = 0.35;
114
- /** Index of refraction of water. Matches the caustics pass. */ const WATER_IOR = 1.333;
115
- /** Ratio for a ray entering the water from air, as `refract` wants it. */ const AIR_TO_WATER_ETA = 1 / WATER_IOR;
116
- /** Ratio for a ray leaving the water into air. */ const WATER_TO_AIR_ETA = WATER_IOR;
117
- /**
118
- * Cap on the refracted path length, as a multiple of the straight-line distance
119
- * to what is behind the water. A guard against nearly tangent rays; Snell bounds
120
- * the true ratio near 1.5 above water, so this leaves headroom.
121
- */ const REFRACT_MAX_PATH_RATIO = 4;
122
- /**
123
- * Number of steps the refracted-ray march takes over {@link REFRACT_MAX_PATH_RATIO}.
124
- *
125
- * Sets how finely a scene discontinuity is caught. Fixed count keeps the shader
126
- * uniform across backends; 24 steps resolve a crest-to-bed exchange to well
127
- * under a water pixel at typical camera distances.
128
- */ const REFRACT_MARCH_STEPS = 24;
129
- /** Depth tolerance, in meters, for the march to treat a step as a hit. */ const REFRACT_MARCH_THICKNESS = 0.05;
130
- /**
131
- * Fraction of the far plane an infinite water surface is pulled in to.
132
- *
133
- * Short of 1 so the result is never the vertex the hardware clips, and never
134
- * lands on the cleared depth value that `ShaderHelper.isFarthestDepth` tests for
135
- * by equality.
117
+ /** Fresnel reflectance of water at normal incidence, n = 1.333. */ const WATER_F0 = 0.02;
118
+ /** Specular roughness with the waves resolved. */ const WATER_BASE_ROUGHNESS = 0.04;
119
+ /** Specular roughness once distance has flattened the wave normals. */ const WATER_DISTANT_ROUGHNESS = 0.35;
120
+ /**
121
+ * Slope variance of a wind-driven sea for the mean Fresnel of the slopes
122
+ * distance flattens out of the normals, from the clean-surface fit of Cox &
123
+ * Munk, "Measurement of the roughness of the sea surface from photographs of
124
+ * the sun's glitter" (JOSA 44(11), 1954): upwind sigma_u^2 = 3.16e-3 U and
125
+ * crosswind sigma_c^2 = 0.003 + 1.92e-3 U, U the wind speed in m/s.
126
+ */ const COX_MUNK_UPWIND = 3.16e-3;
127
+ const COX_MUNK_CROSSWIND_BASE = 0.003;
128
+ const COX_MUNK_CROSSWIND = 1.92e-3;
129
+ /**
130
+ * Roughness over which the sky reflection hands over from the sharp baked sky
131
+ * to the environment's GGX-prefiltered radiance map, as Crest picks its
132
+ * reflection mip by roughness.
133
+ */ const SKY_PREFILTER_START = 0.06;
134
+ const SKY_PREFILTER_FULL = 0.15;
135
+ /** Wrap of the foam's diffuse lighting, `(N.L + w) / (1 + w)`. At 0 a lee slope is unlit. */ const FOAM_LIGHT_WRAP = 0.5;
136
+ /**
137
+ * Share of the edge fade width over which the foam on a thin sheet fades: the
138
+ * foam outlasts the water it floats on, down to the last centimetre or so.
139
+ */ const FOAM_EDGE_FADE_SHARE = 0.2;
140
+ /** Index of refraction of water. Must match the caustics pass. */ const WATER_IOR = 1.333;
141
+ const AIR_TO_WATER_ETA = 1 / WATER_IOR;
142
+ const WATER_TO_AIR_ETA = WATER_IOR;
143
+ /** Cap on the refracted path, as a multiple of the straight-line distance. */ const REFRACT_MAX_PATH_RATIO = 4;
144
+ /** Steps of the refracted-ray march over {@link REFRACT_MAX_PATH_RATIO}. */ const REFRACT_MARCH_STEPS = 24;
145
+ /** Depth tolerance, in meters, for a march step to count as a hit. */ const REFRACT_MARCH_THICKNESS = 0.05;
146
+ /**
147
+ * Fraction of the far plane an infinite surface is pulled in to. Must stay
148
+ * below 1: the cleared depth value is tested for by equality.
136
149
  */ const INFINITE_DEPTH_PULLIN = 0.99;
137
- /**
138
- * Minimum reflection-direction y a water surface samples the sky bake with.
139
- *
140
- * The bake's lower hemisphere is near-black, so grazing reflections would read
141
- * the dark terminator. Re-normalised after the floor, so only the few pixels
142
- * near the horizon change.
143
- */ const HORIZON_REFLECT_BIAS = 0.08;
144
- /**
145
- * Width in UV of the band the refraction offset fades out over at the screen
146
- * border. Off-screen there is no scene colour to refract, and a clamped sample
147
- * smears the border pixel across the water.
148
- */ const REFRACT_EDGE_FADE = 0.06;
149
- /**
150
- * Normalized linear depth at or above which what is behind the water is taken to
151
- * be the sky, and the refraction can skip its march.
152
- */ const SCENE_SKY_DEPTH01 = 0.999;
153
- /**
154
- * How far the surface normal bends the transmitted direction in the subsurface
155
- * term. This is what lets the wave itself modulate the glow.
156
- */ const SSS_DISTORTION = 0.25;
157
- /** Falloff of the subsurface lobe. Higher keeps the glow closer to the sun. */ const SSS_POWER = 4;
158
- /**
159
- * Mean cosine of one scattering event in the water body.
160
- *
161
- * Real sea water measures near 0.9, but at that value almost nothing comes back
162
- * towards a camera looking down at the water. The default gives up some of that
163
- * peak for a term that reads from above; the multiple-scattering blend in
164
- * {@link WaterMaterial.waterSunScattering} restores the isotropy.
150
+ /** Minimum reflection y for the sky bake lookup; its lower hemisphere is black. */ const HORIZON_REFLECT_BIAS = 0.08;
151
+ /** UV width of the band the refraction offset fades over at the screen border. */ const REFRACT_EDGE_FADE = 0.06;
152
+ /** Normalized depth at or above which the scene behind the water is sky. */ const SCENE_SKY_DEPTH01 = 0.999;
153
+ /** Medium path, in meters, through water with only sky behind it: deep enough to absorb everything. */ const SKY_MEDIUM_PATH = 1e4;
154
+ /** Floor on the subsurface term's sun-alignment factor. */ const SSS_SUN_ALIGN_FLOOR = 0.15;
155
+ /** Floor on the subsurface term's grazing-view factor. */ const SSS_GRAZING_FLOOR = 0.1;
156
+ /** Softplus width of the crest gate, as a fraction of the crest height. */ const SSS_HEIGHT_SOFTNESS = 0.6;
157
+ /** Least crest height the gate is worked out over, in metres. See {@link WaterMaterial.subsurfaceCrestHeight}. */ const SSS_CREST_HEIGHT_MIN = 0.05;
158
+ /** Floor on the crest gate's path length. */ const SSS_CREST_PATH_FLOOR = 0.2;
159
+ /** Extinction multiplier of the crest gate over a unit path. */ const SSS_ABSORPTION_SCALE = 3;
160
+ /**
161
+ * Mean cosine of one scattering event. Real sea water is near 0.9, at which
162
+ * almost nothing returns to a camera looking down.
165
163
  */ const DEFAULT_SCATTER_ANISOTROPY = 0.7;
166
- /**
167
- * Floor on how fast the refracted sun ray descends, used by the in-scattering
168
- * integral, so a sun on the horizon cannot make the term diverge. Snell refracts
169
- * even a grazing sun to about 41 degrees below the surface, so it is unreachable
170
- * for any sun actually above the horizon.
171
- */ const MIN_SUN_SLOPE = 0.05;
172
- /**
173
- * Blur per meter of path that even a non-scattering medium produces, in mip
174
- * widths. The wave normal varies across a texel's footprint, so the background
175
- * is slightly defocused by the geometry alone.
176
- */ const REFRACT_BLUR_GEOMETRIC = 0.02;
177
- /**
178
- * How strongly the scattering coefficient drives the refraction blur, mapping
179
- * 1/m onto mip widths per meter. Sized so sigma_s near 0.4 reaches the top of
180
- * the mip chain over a few meters while clear water stays sharp.
181
- */ const REFRACT_BLUR_DENSITY = 1.5;
182
- /**
183
- * Cap on the refraction blur LOD.
184
- *
185
- * Past this the sample stops being "what is behind the water" and becomes the
186
- * average of the frame. Six levels is a 64x footprint, already far wider than
187
- * any real forward-scattering kernel.
188
- */ const REFRACT_BLUR_MAX_LOD = 6;
189
- /**
190
- * Share of the shoreline ramp the foam pattern is allowed to eat into.
191
- *
192
- * The pattern raises the threshold the ramp has to clear, which breaks its outer
193
- * edge into clumps. Below 1 so the foam at the contact line survives it.
194
- */ const SHORE_FOAM_NOISE_BITE = 0.6;
195
- /** World-space drift of the foam pattern, m/s. Off-axis so it has no grain. */ const SHORE_FOAM_DRIFT_X = 0.11;
196
- /** @see SHORE_FOAM_DRIFT_X */ const SHORE_FOAM_DRIFT_Z = -0.07;
197
- /** Frequency ratio between octaves. Not 2, which would align the lattices. */ const SHORE_FOAM_OCTAVE_RATIO = 2.17;
198
- /**
199
- * Weight of each foam pattern octave, coarsest first. Three octaves span about
200
- * five times in feature size, so one authored clump size serves both a wide
201
- * shoreline and a narrow collar.
202
- */ const SHORE_FOAM_OCTAVE_WEIGHTS = [
203
- 0.5,
204
- 0.3,
205
- 0.2
206
- ];
207
- /**
208
- * Width of the band's gradient, as a fraction of the distance it reaches,
209
- * running inward from the reach. 1 fades across the whole band, 0 makes it a
210
- * hard-edged slab. The pattern can only break up what is still in the gradient.
211
- */ const SHORE_FOAM_EDGE_SOFTNESS = 0.85;
212
- /**
213
- * Hi-Z cells the proximity query's window spreads over its radius, which sets
214
- * the pyramid level. Higher locates surfaces more precisely but stops covering
215
- * the whole footprint, and geometry in the uncovered corners is missed.
216
- */ const SHORE_QUERY_TAP_SPREAD = 1.5;
217
- /**
218
- * Softness of the proximity query's minimum, as a fraction of its radius. A hard
219
- * minimum leaves the band in cell-shaped squares; the trade is that the band
220
- * sits a little tighter than the authored reach.
221
- */ const SHORE_QUERY_SOFTMIN = 0.25;
222
- /**
223
- * Distance, in radii, that an empty neighbourhood resolves to. Distances are
224
- * clamped to it before the exponential, so nothing can underflow every term and
225
- * leave the logarithm with nothing to take.
226
- */ const SHORE_QUERY_MISS_REACH = 2;
227
- /** Advance of the dither's hash input per frame, for TAA to average over. */ const SHORE_QUERY_DITHER_STRIDE = 5.588238;
228
- /**
229
- * How thick a cell's frustum block may be, in multiples of its own width. A cell
230
- * spanning an object's edge would otherwise stretch down the view ray and grow
231
- * foam on water far behind the object.
232
- */ const SHORE_QUERY_CELL_DEPTH_SPAN = 2;
164
+ /** Floor on the refracted sun ray's descent; only reached by a sun below the horizon. */ const MIN_SUN_SLOPE = 0.05;
165
+ /** Refraction blur per meter of path from geometry alone, in mip widths. */ const REFRACT_BLUR_GEOMETRIC = 0.02;
166
+ /** Maps the scattering coefficient (1/m) onto refraction blur mip widths per meter. */ const REFRACT_BLUR_DENSITY = 1.5;
167
+ /** Cap on the refraction blur LOD. */ const REFRACT_BLUR_MAX_LOD = 6;
168
+ /**
169
+ * The foam's look follows Crest's OceanFoam.hlsl (Wave Harmonic, MIT): a
170
+ * white foam layer cut from the foam texture by the foam density with a
171
+ * feathered black point, lit through a normal taken from the slope of that
172
+ * cut over a step of a quarter texel per metre of view depth, with a narrow
173
+ * glint; and beneath it bubbles, the same texture blurred three mips and
174
+ * shifted by parallax as if under the surface, tinted and lit by the sky.
175
+ * The texture drifts at FOAM_DRIFT metres per second along x and z, Crest's
176
+ * time / 10.
177
+ */ const FOAM_DRIFT = 0.1;
178
+ const FOAM_BUBBLE_MIP_SCALE = 8;
179
+ const FOAM_SPECULAR_POWER = 293;
180
+ const FOAM_SPECULAR_BOOST = 0.15;
233
181
  class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
234
- static FEATURE_MEDIUM_MODE = this.defineFeature();
235
182
  static FEATURE_REFRACTION_MODE = this.defineFeature();
236
183
  static FEATURE_INFINITE = this.defineFeature();
237
- static FEATURE_SHORE_FOAM = this.defineFeature();
238
184
  static FEATURE_DEBUG_OUTPUT = this.defineFeature();
239
- static _absorptionGrad = new Interpolator('linear', 'vec3', new Float32Array([
240
- 0,
241
- 0.082,
242
- 0.318,
243
- 0.665,
244
- 1
245
- ]), new Float32Array([
246
- 1,
247
- 1,
248
- 1,
249
- 0.22,
250
- 0.87,
251
- 0.87,
252
- 0,
253
- 0.47,
254
- 0.49,
255
- 0,
256
- 0.275,
257
- 0.44,
258
- 0,
259
- 0,
260
- 0
261
- ]));
262
- static _scatterGrad = new Interpolator('linear', 'vec3', new Float32Array([
263
- 0,
264
- 0.15,
265
- 0.42,
266
- 1
267
- ]), new Float32Array([
268
- 0,
269
- 0,
270
- 0,
271
- 0.08,
272
- 0.41,
273
- 0.34,
274
- 0.13,
275
- 0.4,
276
- 0.45,
277
- 0.21,
278
- 0.5,
279
- 0.6
280
- ]));
281
- static _defaultScatterRampTexture = new DWeakRef();
282
- static _defaultAbsorptionRampTexture = new DWeakRef();
283
185
  static _waveUpdateState = new WeakMap();
186
+ static _interactionUpdateState = new WeakMap();
284
187
  _region;
285
188
  _refractionScale;
286
- _depthMulti;
287
189
  _reflectionStrength;
288
- _scatterRampTexture;
289
- _absorptionRampTexture;
290
190
  _waveGenerator;
291
- /**
292
- * Per-bind-group wave uniform sync, keyed by the bind group object.
293
- *
294
- * The material owns one bind group per pass/render-variant hash, so a water
295
- * surface drawn for two different cameras touches two distinct bind groups
296
- * and each must upload once per wave generator version.
297
- */ _waveVersionByBindGroup;
191
+ _interaction;
192
+ /**
193
+ * The generator the shaders and every other consumer of the surface use: the
194
+ * base generator wrapped with the interaction field while one is attached,
195
+ * the base generator itself otherwise.
196
+ */ _effectiveWaveGenerator;
197
+ /** Settings of the shore waves, shared by reference with their generator. */ _shoreWaves = {
198
+ height: 0,
199
+ wavelength: 8,
200
+ period: 6,
201
+ zone: 20,
202
+ sharpness: 0.7,
203
+ lean: 0.5,
204
+ irregularity: 0.6,
205
+ waveFoamStrength: 1,
206
+ waveFoamCoverage: 0.55,
207
+ shorelineFoamMaxDepth: 0.65,
208
+ shorelineFoamStrength: 2,
209
+ wallFoamStrength: 1,
210
+ wallFoamMinSpeed: 0.2,
211
+ wallFoamMaxSpeed: 1.5,
212
+ wallFoamDrift: 0.3,
213
+ surfFoamDuration: 1.5,
214
+ foamSettleDepth: 0.02,
215
+ foamDriftDepth: 0.3,
216
+ settledFoamFade: 0.25
217
+ };
218
+ /** The shore wave layer of the effective generator, while the waves are on. */ _shoreWaveGenerator = null;
219
+ /** Foam with memory, carried by the water; kept across rebuilds of the generators. */ _foamField = new WaterFoamField();
220
+ /** The foam field layered over the base generator, innermost in the effective generator. */ _foamFieldGenerator = null;
221
+ /**
222
+ * The interaction layer of the effective generator while a field is attached,
223
+ * wherever it sits in the chain (the shore waves wrap it when they are on).
224
+ */ _interactiveWaveGenerator = null;
225
+ /** What feeds the foam field: the shore waves while they are on, else the open water. */ _foamSource = null;
226
+ /** Frame the foam field was last stepped in. */ _foamFrameId = -1;
227
+ /** Frame of the last update. */ _frameId = 0;
228
+ _edgeFadeWidth = 0.05;
229
+ /**
230
+ * Thickness in metres over which a thin sheet of water fades in from the
231
+ * ground under it, so swash running up the sand shows the sand through it.
232
+ * 0 disables it.
233
+ */ get edgeFadeWidth() {
234
+ return this._edgeFadeWidth;
235
+ }
236
+ set edgeFadeWidth(val) {
237
+ val = Math.max(0, val);
238
+ if (val !== this._edgeFadeWidth) {
239
+ this._edgeFadeWidth = val;
240
+ this.uniformChanged();
241
+ }
242
+ }
243
+ /** Seconds of water clock at the last update, for the shore waves. */ _waveClock = 0;
244
+ /** Wave uniform version last written, per bind group. */ _waveVersionByBindGroup;
298
245
  _clipmapInfo;
299
246
  _clipmapGridInfo;
300
247
  _skirtDistance;
301
248
  _ssrParams;
302
- /** Absorption coefficient sigma_a, per meter, per RGB channel. */ _absorption;
303
- /** Scattering coefficient sigma_s, per meter, per RGB channel. */ _scattering;
304
- /** sigma_a + sigma_s, recomputed whenever either coefficient changes. */ _extinction;
305
- /** sigma_s / sigma_t, the single-scattering albedo. */ _scatterAlbedo;
306
- /** Scale for the absorption coefficient. */ _absorptionScale;
307
- /** Scale for the scattering coefficient. */ _scatteringScale;
249
+ /** (wind direction x, z, Cox-Munk upwind slope variance, crosswind slope variance) */ _unresolvedSlope;
250
+ /** sigma_a, 1/m, per RGB channel. */ _absorption;
251
+ /** sigma_s, 1/m, per RGB channel. */ _scattering;
252
+ /** sigma_t = sigma_a + sigma_s. */ _extinction;
253
+ /** sigma_s / sigma_t. */ _scatterAlbedo;
254
+ _absorptionScale;
255
+ _scatteringScale;
308
256
  _causticsEnabled;
309
257
  _causticsIntensity;
310
258
  _causticsDepth;
311
259
  _causticsRange;
312
260
  _causticsFadeDistance;
313
261
  _causticsWarp;
262
+ _causticsMinDepth;
314
263
  _causticsSceneDepth;
315
264
  _causticsDefocus;
316
265
  _causticsResolution;
@@ -325,7 +274,8 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
325
274
  _underwaterGodRayShadow;
326
275
  _underwaterHysteresis;
327
276
  _subsurfaceIntensity;
328
- _subsurfaceSteepness;
277
+ _subsurfaceCrestHeight;
278
+ _subsurfaceTint;
329
279
  _subsurfaceParams;
330
280
  _sunScatteringIntensity;
331
281
  _scatterAnisotropy;
@@ -336,20 +286,17 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
336
286
  _foamFalloff;
337
287
  _foamColor;
338
288
  _foamParams;
339
- _shoreFoamAmount;
340
- _shoreFoamDepth;
341
- _shoreFoamFalloff;
342
- _shoreFoamScale;
343
- _shoreFoamWashAmount;
344
- _shoreFoamWashSpeed;
345
- _shoreFoamWashScale;
346
- _shoreFoamParams;
347
- _shoreFoamWashParams;
289
+ _foamFeather;
290
+ _foamNormalStrength;
291
+ _foamBubbleColor;
292
+ _foamBubbleCoverage;
293
+ _foamBubbleDepth;
294
+ _foamLookParams;
295
+ _foamTexture;
296
+ _foamTextureSize = 4;
348
297
  constructor(){
349
298
  super();
350
299
  this._region = new Vector4(-99999, -99999, 99999, 99999);
351
- // Fitted to the legacy absorption ramp at depthMulti = 0.1, so switching the
352
- // medium to physical does not change the out-of-box look much.
353
300
  this._absorption = new Vector3(1.0, 0.25, 0.15);
354
301
  this._scattering = new Vector3(0.05, 0.12, 0.18);
355
302
  this._extinction = new Vector3();
@@ -361,12 +308,12 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
361
308
  this._clipmapGridInfo = new Vector4();
362
309
  this._skirtDistance = 0;
363
310
  this._waveGenerator = new DRef();
311
+ this._interaction = new DRef();
312
+ this._effectiveWaveGenerator = null;
364
313
  this._waveVersionByBindGroup = new WeakMap();
365
314
  this._ssrParams = new Vector4(1000, 160, 0.5, 2);
366
- this._scatterRampTexture = new DRef();
367
- this._absorptionRampTexture = new DRef();
315
+ this._unresolvedSlope = new Vector4();
368
316
  this._refractionScale = 1;
369
- this._depthMulti = 0.1;
370
317
  this._reflectionStrength = 1;
371
318
  this._causticsEnabled = true;
372
319
  this._causticsIntensity = 1;
@@ -374,6 +321,7 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
374
321
  this._causticsRange = 60;
375
322
  this._causticsFadeDistance = 0;
376
323
  this._causticsWarp = 1.5;
324
+ this._causticsMinDepth = 0.15;
377
325
  this._causticsSceneDepth = true;
378
326
  this._causticsDefocus = 0.12;
379
327
  this._causticsResolution = 1024;
@@ -385,76 +333,47 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
385
333
  this._underwaterGodRays = true;
386
334
  this._underwaterGodRayIntensity = 1;
387
335
  this._underwaterGodRaySteps = 24;
388
- // Off by default: it is a shadow map lookup per march step on top of the
389
- // caustic lookup already there, which roughly doubles the cost of the
390
- // shafts. Worth turning on for a scene with something large standing in the
391
- // water, where a shaft running through it unbroken is the obvious artefact.
392
336
  this._underwaterGodRayShadow = false;
393
- // A few centimetres. Wide enough that floating-point noise on the camera
394
- // height cannot flip the state, narrow enough that the transition still
395
- // happens where the eye expects it.
396
337
  this._underwaterHysteresis = 0.05;
397
- // Sized so a fully lit crest contributes about as much as the ambient
398
- // scattering term already does.
399
- this._subsurfaceIntensity = 1.5;
400
- // At 4 the wave flanks carry the glow and the troughs stay dark; 20 turns
401
- // the whole sea into a lamp.
402
- this._subsurfaceSteepness = 4;
338
+ this._subsurfaceIntensity = 0.5;
339
+ this._subsurfaceCrestHeight = 1.5;
340
+ this._subsurfaceTint = new Vector3(0.86, 0.98, 0.71);
403
341
  this._subsurfaceParams = new Vector4();
404
- // The integral is derived, not fitted, so 1 is what the medium coefficients
405
- // already imply.
406
342
  this._sunScatteringIntensity = 1;
407
343
  this._scatterAnisotropy = DEFAULT_SCATTER_ANISOTROPY;
408
344
  this._sunScatterParams = new Vector4();
409
345
  this._refractionBlur = 1;
410
- // A shallow-pool depth: the offset stays plausible on thin water and cannot
411
- // reach across a large object's silhouette.
412
346
  this._cheapRefractionDepth = 1;
413
- // Coverage from the generator is a folded-surface measure, not an area
414
- // fraction; these map it onto one. The falloff above 1 keeps light folding
415
- // from reading as foam.
416
347
  this._foamAmount = 1;
417
348
  this._foamFalloff = 1.5;
418
- // Slightly off-white and slightly blue: sea foam is water and air, and a
419
- // pure white one reads as snow.
420
349
  this._foamColor = new Vector3(0.92, 0.95, 0.97);
421
350
  this._foamParams = new Vector4();
422
- // Off by default. The shoreline estimate is a property of whatever happens
423
- // to be behind the surface, so it would grow a white rim around every
424
- // submerged object in a scene authored without it.
425
- this._shoreFoamAmount = 0;
426
- // A waterline band of a foot or two: a wave over a gently shelving bed moves
427
- // it visibly, but it stays a band and not a white lagoon.
428
- this._shoreFoamDepth = 0.5;
429
- // Above 1, so coverage falls off towards the outer edge rather than filling
430
- // the whole band.
431
- this._shoreFoamFalloff = 1.5;
432
- // A couple of clumps across the band's width, whatever that width is.
433
- this._shoreFoamScale = 2.5;
434
- // Half the band's depth, so the waterline visibly advances and retreats
435
- // without the band ever closing completely.
436
- this._shoreFoamWashAmount = 0.5;
437
- // One wash every eight seconds or so - the band is what a set of waves does,
438
- // not what one wave does.
439
- this._shoreFoamWashSpeed = 0.12;
440
- // Wash phase decorrelates over ~30 m, so a long shoreline breaks in sections
441
- // instead of pulsing as one.
442
- this._shoreFoamWashScale = 0.03;
443
- this._shoreFoamParams = new Vector4();
444
- this._shoreFoamWashParams = new Vector4();
351
+ // Crest's defaults
352
+ this._foamFeather = 0.4;
353
+ this._foamNormalStrength = 3.5;
354
+ this._foamBubbleColor = new Vector3(0.64, 0.83, 0.82);
355
+ this._foamBubbleCoverage = 1.68;
356
+ this._foamBubbleDepth = 0.14;
357
+ this._foamLookParams = new Vector4();
358
+ this._foamTexture = new DRef();
445
359
  this.cullMode = 'none';
446
- this.useFeature(WaterMaterial.FEATURE_MEDIUM_MODE, 'physical');
447
360
  this.useFeature(WaterMaterial.FEATURE_REFRACTION_MODE, 'march');
448
361
  this.useFeature(WaterMaterial.FEATURE_INFINITE, false);
449
- this.useFeature(WaterMaterial.FEATURE_SHORE_FOAM, false);
450
362
  this.useFeature(WaterMaterial.FEATURE_DEBUG_OUTPUT, 'none');
451
- //this.TAADisabled = true;
452
363
  }
453
364
  /** {@inheritDoc Material.onDispose} */ onDispose() {
454
365
  super.onDispose();
455
366
  this._waveGenerator.dispose();
456
- this._scatterRampTexture.dispose();
457
- this._absorptionRampTexture.dispose();
367
+ this._interaction.dispose();
368
+ this._shoreWaveGenerator?.dispose();
369
+ this._shoreWaveGenerator = null;
370
+ this._foamFieldGenerator?.dispose();
371
+ this._foamFieldGenerator = null;
372
+ this._foamSource = null;
373
+ this._interactiveWaveGenerator = null;
374
+ this._foamField.dispose();
375
+ this._effectiveWaveGenerator = null;
376
+ this._foamTexture.dispose();
458
377
  }
459
378
  /** @internal */ get region() {
460
379
  return this._region;
@@ -465,58 +384,86 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
465
384
  this.uniformChanged();
466
385
  }
467
386
  }
468
- get waveGenerator() {
469
- return this._waveGenerator.get();
387
+ /**
388
+ * The wave generator the surface is evaluated with.
389
+ *
390
+ * While an {@link interaction} is attached this is a wrapper that layers the
391
+ * interaction field over {@link baseWaveGenerator}; every consumer of the
392
+ * surface (the shaders, the surface-point query, the caustics) goes through
393
+ * it and sees one displaced surface. Setting it sets the base generator.
394
+ */ get waveGenerator() {
395
+ return this._effectiveWaveGenerator;
470
396
  }
471
397
  set waveGenerator(waveGenerator) {
472
398
  if (this._waveGenerator.get() !== waveGenerator) {
473
399
  this._waveGenerator.set(waveGenerator);
474
- // Bind groups are pooled and reused across frames, so an old entry must
475
- // not suppress the upload for a new generator.
476
- this._waveVersionByBindGroup = new WeakMap();
477
- this.optionChanged(true);
400
+ this._rebuildEffectiveWaveGenerator();
478
401
  }
479
402
  }
480
- get scatterRampTexture() {
481
- const tex = this._getScatterRampTexture(getDevice());
482
- return tex === WaterMaterial._defaultScatterRampTexture.get() ? null : tex;
403
+ /** The generator supplying the ambient surface, without any interaction field. */ get baseWaveGenerator() {
404
+ return this._waveGenerator.get();
483
405
  }
484
- set scatterRampTexture(tex) {
485
- if (tex !== this.scatterRampTexture) {
486
- this._scatterRampTexture.set(tex);
487
- this.uniformChanged();
406
+ /**
407
+ * Dynamic height field layered over the surface, or null for none. See
408
+ * {@link WaterInteraction}.
409
+ *
410
+ * Ignored, with the base generator used on its own, on a device that cannot
411
+ * host the field.
412
+ */ get interaction() {
413
+ return this._interaction.get();
414
+ }
415
+ set interaction(val) {
416
+ if (this._interaction.get() !== val) {
417
+ this._interaction.set(val);
418
+ this._rebuildEffectiveWaveGenerator();
488
419
  }
489
420
  }
490
- get absorptionRampTexture() {
491
- const tex = this._getAbsorptionRampTexture(getDevice());
492
- return tex === WaterMaterial._defaultAbsorptionRampTexture.get() ? null : tex;
493
- }
494
- set absorptionRampTexture(tex) {
495
- if (tex !== this.absorptionRampTexture) {
496
- this._absorptionRampTexture.set(tex);
497
- this.uniformChanged();
421
+ /** @internal */ _rebuildEffectiveWaveGenerator() {
422
+ const base = this._waveGenerator.get();
423
+ const interaction = this._interaction.get();
424
+ this._shoreWaveGenerator?.dispose();
425
+ this._shoreWaveGenerator = null;
426
+ this._foamFieldGenerator?.dispose();
427
+ this._foamFieldGenerator = null;
428
+ this._foamSource = null;
429
+ // A field left behind must not hold on to the generators disposed here
430
+ this._interactiveWaveGenerator?.interaction.setAmbientSurface(null);
431
+ this._interactiveWaveGenerator = null;
432
+ let effective = null;
433
+ if (base) {
434
+ // Innermost the foam field, whose foam the shore waves and the
435
+ // interaction field compose theirs over
436
+ const that = this;
437
+ this._foamFieldGenerator = new FoamFieldWaveGenerator(base, this._foamField, {
438
+ getFoamTexture: ()=>that.foamTexture,
439
+ getFoamTextureSize: ()=>that._foamTextureSize
440
+ });
441
+ effective = this._foamFieldGenerator;
442
+ this._foamSource = new WaveFoamSource(base, this._foamField);
498
443
  }
499
- }
500
- /**
501
- * How the medium turns a path length into transmittance and in-scattering.
502
- *
503
- * Defaults to `physical`. `ramp` restores the legacy ramp-texture lookup for
504
- * scenes that were tuned against it.
505
- */ get mediumMode() {
506
- return this.featureUsed(WaterMaterial.FEATURE_MEDIUM_MODE) ?? 'physical';
507
- }
508
- set mediumMode(val) {
509
- if (val !== this.mediumMode) {
510
- this.useFeature(WaterMaterial.FEATURE_MEDIUM_MODE, val);
444
+ if (effective && this._shoreWaves.height > 0) {
445
+ this._shoreWaveGenerator = new ShoreWaveGenerator(effective, this._shoreWaves, this._foamSource);
446
+ this._foamFieldGenerator.flowSource = this._shoreWaveGenerator;
447
+ this._foamSource = this._shoreWaveGenerator;
448
+ effective = this._shoreWaveGenerator;
511
449
  }
450
+ // Outermost the interaction field: its ripples ride the shore waves rather
451
+ // than being faded with the open water's chop near the shore, and a query
452
+ // of the surface without the disturbers finds it on top to rebuild
453
+ if (effective && interaction && interaction.isOk()) {
454
+ // The field reads the surface beneath it under each disturber, so a hull
455
+ // riding the waves is measured against the water it floats on
456
+ interaction.setAmbientSurface(effective);
457
+ effective = new InteractiveWaveGenerator(effective, interaction);
458
+ this._interactiveWaveGenerator = effective;
459
+ interaction.ensureResources();
460
+ }
461
+ this._effectiveWaveGenerator = effective;
462
+ // Bind groups are pooled; a stale version must not suppress the upload.
463
+ this._waveVersionByBindGroup = new WeakMap();
464
+ this.optionChanged(true);
512
465
  }
513
- /**
514
- * How the refracted view sample is located. Defaults to `march`.
515
- *
516
- * Set `offset` on hardware that cannot afford the depth-buffer search - the
517
- * water still refracts, it just refracts to the wrong place. A compile-time
518
- * feature, so the cheap variant carries no trace of the march.
519
- */ get refractionMode() {
466
+ /** How the refracted view sample is located. Defaults to `march`. Compile-time feature. */ get refractionMode() {
520
467
  return this.featureUsed(WaterMaterial.FEATURE_REFRACTION_MODE) ?? 'march';
521
468
  }
522
469
  set refractionMode(val) {
@@ -524,9 +471,9 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
524
471
  this.useFeature(WaterMaterial.FEATURE_REFRACTION_MODE, val);
525
472
  }
526
473
  }
527
- /**
528
- * Which intermediate shading term to display instead of the final colour.
529
- * Defaults to `none`. See {@link WaterDebugOutput}.
474
+ /**
475
+ * Which intermediate shading term to display instead of the final colour.
476
+ * Defaults to `none`. See {@link WaterDebugOutput}.
530
477
  */ get debugOutput() {
531
478
  return this.featureUsed(WaterMaterial.FEATURE_DEBUG_OUTPUT) ?? 'none';
532
479
  }
@@ -535,13 +482,9 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
535
482
  this.useFeature(WaterMaterial.FEATURE_DEBUG_OUTPUT, val);
536
483
  }
537
484
  }
538
- /**
539
- * Whether the surface reads as unbounded, reaching the horizon rather than
540
- * ending at {@link region}. Off by default.
541
- *
542
- * The region test is not emitted at all, so the surface has no edge, and the
543
- * clipmap's outermost ring becomes a horizon skirt that survives clipping past
544
- * the camera's far plane. Use it for an ocean, not for a pond or a pool.
485
+ /**
486
+ * Whether the surface reaches the horizon rather than ending at {@link region}.
487
+ * Off by default. The clipmap's outermost ring becomes a horizon skirt.
545
488
  */ get infinite() {
546
489
  return this.featureUsed(WaterMaterial.FEATURE_INFINITE) ?? false;
547
490
  }
@@ -590,50 +533,30 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
590
533
  this.uniformChanged();
591
534
  }
592
535
  }
593
- /**
594
- * Extinction coefficient sigma_t = sigma_a + sigma_s in 1/m.
595
- *
596
- * Read by the caustics pass so the light attenuated along the refracted path
597
- * uses the same medium as the surface shading. Do not mutate the result.
598
- */ get extinction() {
536
+ /** sigma_t = sigma_a + sigma_s in 1/m. Shared with the caustics pass. Do not mutate. */ get extinction() {
599
537
  return this._extinction;
600
538
  }
601
- /** Single-scattering albedo sigma_s / sigma_t. Do not mutate the result. */ get scatterAlbedo() {
539
+ /** Single-scattering albedo sigma_s / sigma_t. Do not mutate. */ get scatterAlbedo() {
602
540
  return this._scatterAlbedo;
603
541
  }
604
- /**
605
- * Whether a camera inside this body of water sees the water from the inside:
606
- * the medium applied to the whole scene, the sky replaced by water, and the
607
- * surface read from below.
608
- *
609
- * The medium coefficients are the ones the surface already uses
610
- * ({@link WaterMaterial.absorption}, {@link WaterMaterial.scattering} and
611
- * their scales), so the volume and the surface never disagree.
542
+ /**
543
+ * Whether a camera inside this body of water sees the medium applied to the
544
+ * whole scene. Uses the same coefficients as the surface shading.
612
545
  */ get underwaterEnabled() {
613
546
  return this._underwaterEnabled;
614
547
  }
615
548
  set underwaterEnabled(val) {
616
549
  this._underwaterEnabled = !!val;
617
550
  }
618
- /**
619
- * Scale on the downwelling sky radiance that lights the water column from
620
- * inside, 1 for the value the sky implies.
621
- *
622
- * This is what the water fades to in the distance, so it is also what decides
623
- * how bright the underwater "fog" reads.
624
- */ get underwaterAmbientIntensity() {
551
+ /** Scale on the downwelling sky radiance lighting the column from inside, 1 for physical. */ get underwaterAmbientIntensity() {
625
552
  return this._underwaterAmbientIntensity;
626
553
  }
627
554
  set underwaterAmbientIntensity(val) {
628
555
  this._underwaterAmbientIntensity = Math.max(0, val);
629
556
  }
630
- /**
631
- * Whether shafts of sunlight are marched through the water column.
632
- *
633
- * Reads the caustic map as the surface's transmittance, so the shafts carry
634
- * the same focusing pattern that lands on the sea bed. Switches itself off
635
- * wherever the caustic map is unavailable - WebGL1, no shadow-casting
636
- * directional light, or a sun too close to the horizon.
557
+ /**
558
+ * Whether sunlight shafts are marched through the water column. Reads the
559
+ * caustic map, so it is off wherever that is unavailable.
637
560
  */ get underwaterGodRays() {
638
561
  return this._underwaterGodRays;
639
562
  }
@@ -646,50 +569,33 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
646
569
  set underwaterGodRayIntensity(val) {
647
570
  this._underwaterGodRayIntensity = Math.max(0, val);
648
571
  }
649
- /**
650
- * Samples taken along each view ray for the light shafts.
651
- *
652
- * The march is dithered, so a low count reads as noise rather than as banding;
653
- * below about 8 the noise stops resolving into shafts at all.
654
- */ get underwaterGodRaySteps() {
572
+ /** Samples per view ray for the light shafts. Dithered; below about 8 reads as noise. */ get underwaterGodRaySteps() {
655
573
  return this._underwaterGodRaySteps;
656
574
  }
657
575
  set underwaterGodRaySteps(val) {
658
576
  this._underwaterGodRaySteps = Math.max(1, Math.floor(val));
659
577
  }
660
- /**
661
- * Whether geometry standing in the water breaks the light shafts.
662
- *
663
- * Off by default. The shafts are built from the caustic map, which knows what
664
- * the *surface* did to the sunlight and nothing about what is under it, so
665
- * without this a shaft runs straight through a piling or a hull. Turning it on
666
- * samples the sun's shadow map once per march step, which roughly doubles what
667
- * the shafts cost; the sea bed's own shadows are unaffected either way, since
668
- * those come from the ordinary lighting path.
669
- *
670
- * Needs the caustic light to be casting shadows, which it already must be for
671
- * the caustics themselves to exist.
578
+ /**
579
+ * Whether geometry in the water breaks the light shafts. Off by default; one
580
+ * shadow map lookup per march step, roughly doubling the shafts' cost.
672
581
  */ get underwaterGodRayShadow() {
673
582
  return this._underwaterGodRayShadow;
674
583
  }
675
584
  set underwaterGodRayShadow(val) {
676
585
  this._underwaterGodRayShadow = !!val;
677
586
  }
678
- /**
679
- * Half-width in meters of the dead band around the surface the submerged test
680
- * uses, so a camera sitting exactly at water level does not flip state every
681
- * frame. The test is against the rest plane, not the displaced surface.
587
+ /**
588
+ * Half-width in meters of the dead band the submerged test uses around the
589
+ * rest plane, so a camera at water level does not flip state every frame.
682
590
  */ get underwaterHysteresis() {
683
591
  return this._underwaterHysteresis;
684
592
  }
685
593
  set underwaterHysteresis(val) {
686
594
  this._underwaterHysteresis = Math.max(0, val);
687
595
  }
688
- /**
689
- * Whether this water projects caustics onto the geometry below it.
690
- *
691
- * Requires a shadow-casting directional light and a non-WebGL1 device; the
692
- * caustics pass disables itself when either is missing.
596
+ /**
597
+ * Whether this water projects caustics onto the geometry below it. Requires a
598
+ * shadow-casting directional light.
693
599
  */ get causticsEnabled() {
694
600
  return this._causticsEnabled;
695
601
  }
@@ -702,79 +608,56 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
702
608
  set causticsIntensity(val) {
703
609
  this._causticsIntensity = val;
704
610
  }
705
- /**
706
- * Depth in meters below the surface where the caustics are in focus.
707
- *
708
- * Receivers away from this depth are progressively defocused rather than
709
- * displaced, so set it near the depth of the sea bed that should show the
710
- * sharpest pattern.
711
- */ get causticsDepth() {
611
+ /** Depth in meters below the surface where the caustics are in focus. */ get causticsDepth() {
712
612
  return this._causticsDepth;
713
613
  }
714
614
  set causticsDepth(val) {
715
615
  this._causticsDepth = Math.max(0.01, val);
716
616
  }
717
- /**
718
- * Furthest distance in meters from the camera the caustic map reaches.
719
- *
720
- * A cap rather than a fixed extent: the map is fitted to the part of the water
721
- * within this distance. Raise it to light more of the scene, at the cost of
722
- * resolution wherever the water is large enough to fill it.
723
- */ get causticsRange() {
617
+ /** Furthest distance in meters from the camera the caustic map reaches. */ get causticsRange() {
724
618
  return this._causticsRange;
725
619
  }
726
620
  set causticsRange(val) {
727
621
  this._causticsRange = Math.max(1, val);
728
622
  }
729
- /**
730
- * Width in meters of the band the pattern fades out over at the edge of the
731
- * map, or 0 to derive it from {@link causticsRange}.
732
- *
733
- * Too narrow a band makes the map boundary read as a hard line across the sea
734
- * bed. Auto scales with the range but puts a floor under it in meters. Capped
735
- * at 90% of the range, so a core of the map always survives.
623
+ /**
624
+ * Width in meters of the fade band at the edge of the caustic map, or 0 to
625
+ * derive it from {@link causticsRange}. Capped at 90% of the range.
736
626
  */ get causticsFadeDistance() {
737
627
  return this._causticsFadeDistance;
738
628
  }
739
629
  set causticsFadeDistance(val) {
740
630
  this._causticsFadeDistance = Math.max(0, val);
741
631
  }
742
- /**
743
- * Whether photons land on the scene instead of on a plane at
744
- * {@link causticsDepth}.
745
- *
746
- * On by default. The plane displaces the pattern sideways on any receiver not
747
- * at that depth, which is badly wrong under a low sun and on a sea bed with
748
- * relief. Resolving the real depth reuses the sun's shadow cascade, so it
749
- * costs no extra geometry pass, and falls back to the plane wherever that
750
- * cascade has nothing to say.
751
- *
752
- * {@link causticsDepth} still sets where the iteration starts and what the
753
- * defocus is measured against.
632
+ /**
633
+ * Whether photons land on the scene (via the sun's shadow cascade) instead of
634
+ * on a plane at {@link causticsDepth}. On by default.
754
635
  */ get causticsSceneDepth() {
755
636
  return this._causticsSceneDepth;
756
637
  }
757
638
  set causticsSceneDepth(val) {
758
639
  this._causticsSceneDepth = !!val;
759
640
  }
760
- /**
761
- * How strongly caustic map texels are concentrated near the camera, 0 to
762
- * spread them evenly.
763
- *
764
- * Texel density is `1 + causticsWarp` times uniform at the centre and
765
- * `1 / (1 + causticsWarp)` times it at the border. Raise it when a
766
- * {@link causticsRange} large enough to light the scene is too coarse to
767
- * resolve a caustic cell.
768
- *
769
- * Ignored while the map already fits the water within range, which is the case
770
- * a bounded pool inside {@link causticsRange} always lands in.
641
+ /**
642
+ * How strongly caustic map texels are concentrated near the camera, 0 for
643
+ * uniform. Density is `1 + warp` times uniform at the centre and
644
+ * `1 / (1 + warp)` at the border. Ignored while the map fits the water.
771
645
  */ get causticsWarp() {
772
646
  return this._causticsWarp;
773
647
  }
774
648
  set causticsWarp(val) {
775
649
  this._causticsWarp = Math.max(0, Math.min(8, val));
776
650
  }
777
- /** How fast the caustic contrast falls off per meter away from {@link causticsDepth}. */ get causticsDefocus() {
651
+ /**
652
+ * Water depth in metres below which no caustics show, faded in over half
653
+ * of it either side; 0 shows them right up to the water's edge.
654
+ */ get causticsMinDepth() {
655
+ return this._causticsMinDepth;
656
+ }
657
+ set causticsMinDepth(val) {
658
+ this._causticsMinDepth = Math.max(0, val);
659
+ }
660
+ /** Caustic contrast falloff per meter away from {@link causticsDepth}. */ get causticsDefocus() {
778
661
  return this._causticsDefocus;
779
662
  }
780
663
  set causticsDefocus(val) {
@@ -786,51 +669,29 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
786
669
  set causticsResolution(val) {
787
670
  this._causticsResolution = Math.max(16, Math.min(2048, val | 0));
788
671
  }
789
- /**
790
- * Edge length of the photon grid, or 0 to size it from the map.
791
- *
792
- * Auto solves for photons per map texel, which is the density that governs
793
- * quality; a fixed grid over-spends on a small pool and under-spends on open
794
- * water. Set a value to pin the grid explicitly; the cost is the square of it.
795
- */ get causticsPhotonResolution() {
672
+ /** Edge length of the photon grid, or 0 to size it from the map. Cost is quadratic. */ get causticsPhotonResolution() {
796
673
  return this._causticsPhotonResolution;
797
674
  }
798
675
  set causticsPhotonResolution(val) {
799
676
  const n = val | 0;
800
677
  this._causticsPhotonResolution = n <= 0 ? 0 : Math.max(16, Math.min(4096, n));
801
678
  }
802
- /**
803
- * Number of 2x2 blur iterations applied to the accumulated map. Rounded up to
804
- * an even count.
805
- */ get causticsBlurPasses() {
679
+ /** Number of 2x2 blur iterations on the caustic map. Rounded up to even. */ get causticsBlurPasses() {
806
680
  return this._causticsBlurPasses;
807
681
  }
808
682
  set causticsBlurPasses(val) {
809
683
  const clamped = Math.max(0, Math.min(4, val | 0));
810
684
  this._causticsBlurPasses = clamped + (clamped & 1);
811
685
  }
812
- /**
813
- * Weight the previous frame's caustic map keeps in the current one, 0 to
814
- * disable.
815
- *
816
- * Averages away the scintillation a moving photon lattice produces, in effect
817
- * multiplying the photon count for free. The resolve clamps the reprojected
818
- * value to its own 3x3 neighbourhood, so still regions accumulate while moving
819
- * ones fall back to the current frame. Raising this past the default buys
820
- * diminishing stability and starts to smear the animation.
821
- */ get causticsTemporalStrength() {
686
+ /** Weight of the previous frame's caustic map in the current one, 0 to disable. */ get causticsTemporalStrength() {
822
687
  return this._causticsTemporalStrength;
823
688
  }
824
689
  set causticsTemporalStrength(val) {
825
690
  this._causticsTemporalStrength = Math.max(0, Math.min(0.95, val));
826
691
  }
827
- /**
828
- * Strength of the sunlight scattered forward through a wave crest, 0 to
829
- * disable.
830
- *
831
- * This is the term that makes a backlit crest glow. Authored rather than
832
- * derived; the medium's albedo supplies the colour, so raising this brightens
833
- * the glow without shifting its hue.
692
+ /**
693
+ * Strength of the glow of a backlit wave crest, 0 to disable. Authored, not
694
+ * scaled by the light's intensity; {@link subsurfaceTint} supplies the hue.
834
695
  */ get subsurfaceIntensity() {
835
696
  return this._subsurfaceIntensity;
836
697
  }
@@ -840,31 +701,34 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
840
701
  this.uniformChanged();
841
702
  }
842
703
  }
843
- /**
844
- * How sharply surface tilt gates the subsurface glow.
845
- *
846
- * The glow is scaled by `(1 - normal.y) * subsurfaceSteepness`, clamped to 1,
847
- * so this is the reciprocal of the tilt at which it saturates. Unitless, and
848
- * larger than it looks like it should be: an ocean surface is nearly flat in
849
- * these terms.
850
- */ get subsurfaceSteepness() {
851
- return this._subsurfaceSteepness;
852
- }
853
- set subsurfaceSteepness(val) {
854
- if (val !== this._subsurfaceSteepness) {
855
- this._subsurfaceSteepness = Math.max(0, val);
704
+ /**
705
+ * Height above the still-water level, in meters, over which the lit wall of
706
+ * a crest thins by a factor of `e`. Lower for a calm sea, higher so only the
707
+ * tallest crests glow.
708
+ */ get subsurfaceCrestHeight() {
709
+ return this._subsurfaceCrestHeight;
710
+ }
711
+ set subsurfaceCrestHeight(val) {
712
+ // The gate softens over SSS_HEIGHT_SOFTNESS of this; too small a height
713
+ // makes it a hard step at the still-water level.
714
+ const clamped = Math.max(SSS_CREST_HEIGHT_MIN, val);
715
+ if (clamped !== this._subsurfaceCrestHeight) {
716
+ this._subsurfaceCrestHeight = clamped;
856
717
  this.uniformChanged();
857
718
  }
858
719
  }
859
- /**
860
- * Strength of the sunlight scattered out of the water column towards the eye,
861
- * 1 for the value the medium coefficients imply. 0 disables the term.
862
- *
863
- * This is what gives the water body a direction-dependent colour: a shadow
864
- * falling on the water darkens the water itself, and a low sun tints the
865
- * column. Unlike {@link subsurfaceIntensity} this is not an authored
866
- * magnitude - 1 is the physical answer and anything else is a deliberate
867
- * exaggeration.
720
+ /** Colour of the crest glow, before the crest gate tints it with the medium's extinction. */ get subsurfaceTint() {
721
+ return this._subsurfaceTint;
722
+ }
723
+ set subsurfaceTint(val) {
724
+ if (!val.equalsTo(this._subsurfaceTint)) {
725
+ this._subsurfaceTint.set(val);
726
+ this.uniformChanged();
727
+ }
728
+ }
729
+ /**
730
+ * Strength of the sunlight scattered out of the water column towards the eye,
731
+ * 1 for the physical value. 0 disables the term.
868
732
  */ get sunScatteringIntensity() {
869
733
  return this._sunScatteringIntensity;
870
734
  }
@@ -875,14 +739,10 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
875
739
  this.uniformChanged();
876
740
  }
877
741
  }
878
- /**
879
- * Depth in meters the cheap refraction mode assumes the water is, in
880
- * {@link refractionMode} `offset`. Ignored by `march`.
881
- *
882
- * Sets how strong the distortion looks: raise it for water that should read as
883
- * deep, lower it for a shallow film. A constant rather than the measured scene
884
- * distance, which would double every submerged object across its silhouette;
885
- * the price is that shallow and deep water distort equally.
742
+ /**
743
+ * Fixed depth in meters the `offset` refraction mode steps along the refracted
744
+ * ray. Ignored by `march`. Deliberately not the scene distance, which would
745
+ * double submerged objects across their silhouette.
886
746
  */ get cheapRefractionDepth() {
887
747
  return this._cheapRefractionDepth;
888
748
  }
@@ -893,14 +753,7 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
893
753
  this.uniformChanged();
894
754
  }
895
755
  }
896
- /**
897
- * Scale on how much the medium blurs what is seen through it. 0 keeps the
898
- * background perfectly sharp at any depth, 1 is what the medium implies.
899
- *
900
- * The width is derived from the scattering coefficient and the path length, so
901
- * this is a stylisation knob rather than the magnitude itself. Costs nothing -
902
- * it selects a mip of a chain that is generated regardless.
903
- */ get refractionBlur() {
756
+ /** Scale on the medium's refraction blur. 0 is sharp, 1 is what the medium implies. */ get refractionBlur() {
904
757
  return this._refractionBlur;
905
758
  }
906
759
  set refractionBlur(val) {
@@ -910,15 +763,7 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
910
763
  this.uniformChanged();
911
764
  }
912
765
  }
913
- /**
914
- * Mean cosine of a single scattering event, in `[0, 0.95]`.
915
- *
916
- * 0 scatters equally in all directions; higher values push light forward, so
917
- * the water brightens when looking towards the sun through it and darkens
918
- * when looking away. The term blends towards isotropic as the column gets
919
- * optically thick, so the visible anisotropy is always less than this number
920
- * alone suggests.
921
- */ get scatterAnisotropy() {
766
+ /** Mean cosine of a single scattering event, in `[0, 0.95]`. 0 is isotropic. */ get scatterAnisotropy() {
922
767
  return this._scatterAnisotropy;
923
768
  }
924
769
  set scatterAnisotropy(val) {
@@ -928,10 +773,7 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
928
773
  this.uniformChanged();
929
774
  }
930
775
  }
931
- /**
932
- * How much of a folded texel reads as foam, 0 to disable. Scales the wave
933
- * generator's fold measure into a coverage fraction.
934
- */ get foamAmount() {
776
+ /** Scale from the generator's fold measure to foam coverage, 0 to disable. */ get foamAmount() {
935
777
  return this._foamAmount;
936
778
  }
937
779
  set foamAmount(val) {
@@ -940,11 +782,7 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
940
782
  this.uniformChanged();
941
783
  }
942
784
  }
943
- /**
944
- * Falloff applied to foam coverage before it is scaled. Above 1 only a crest
945
- * that has genuinely broken shows any foam, which keeps a windy sea from
946
- * turning uniformly white.
947
- */ get foamFalloff() {
785
+ /** Power applied to foam coverage before scaling. Above 1 restricts foam to broken crests. */ get foamFalloff() {
948
786
  return this._foamFalloff;
949
787
  }
950
788
  set foamFalloff(val) {
@@ -962,172 +800,88 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
962
800
  this.uniformChanged();
963
801
  }
964
802
  }
965
- /**
966
- * Coverage of the foam that collects where the water meets a surface, 0 to disable.
967
- *
968
- * A second foam source independent of the crest foam, keyed on how far the
969
- * surface is from the nearest solid around it - the depth of water over a bed,
970
- * the horizontal distance to a hull or a piling. One band covers both the
971
- * shoreline and the contact line around anything standing in the water.
972
- *
973
- * Estimated in screen space, with the limits that implies: geometry that is
974
- * off screen, hidden behind something else, or thinner than a few pixels
975
- * produces no foam. Requires the Hi-Z pyramid, so it is unavailable on WebGL1.
976
- *
977
- * Drawn with {@link foamColor} and folded into the same coverage the crest foam
978
- * feeds, so it suppresses the specular lobe and the water body underneath it
979
- * exactly as that one does.
980
- */ get shoreFoamAmount() {
981
- return this._shoreFoamAmount;
982
- }
983
- set shoreFoamAmount(val) {
984
- const clamped = Math.max(0, val);
985
- if (clamped !== this._shoreFoamAmount) {
986
- // A compile-time feature, so crossing zero rebuilds the shader while
987
- // changing the value either side of it does not. Forced off on WebGL1,
988
- // which has no Hi-Z pyramid for the query to read.
989
- const wasEnabled = this._shoreFoamAmount > 0;
990
- this._shoreFoamAmount = clamped;
991
- if (wasEnabled !== clamped > 0) {
992
- this.useFeature(WaterMaterial.FEATURE_SHORE_FOAM, clamped > 0 && getDevice().type !== 'webgl');
993
- } else {
994
- this.uniformChanged();
995
- }
803
+ /**
804
+ * How gradually the white foam fades out at its edges, from a crisp cut
805
+ * (near 0) to a soft blend into the bubbles beneath (1).
806
+ */ get foamFeather() {
807
+ return this._foamFeather;
808
+ }
809
+ set foamFeather(val) {
810
+ const clamped = Math.min(Math.max(val, 0.001), 1);
811
+ if (clamped !== this._foamFeather) {
812
+ this._foamFeather = clamped;
813
+ this.uniformChanged();
996
814
  }
997
815
  }
998
- /**
999
- * How far, in meters, the foam band reaches from the surface behind it.
1000
- *
1001
- * Over a bed this is a depth of water; against anything vertical it is a
1002
- * horizontal distance, and the same value serves both. Coverage is solid
1003
- * against the contact itself and fades to nothing by this distance, so it is
1004
- * the outer edge of the band rather than its midpoint. What it means on screen
1005
- * depends on the geometry: on a steep drop-off the band is a thin line however
1006
- * large this is, and on a flat shelf a small value already covers a wide
1007
- * stretch.
1008
- */ get shoreFoamDepth() {
1009
- return this._shoreFoamDepth;
1010
- }
1011
- set shoreFoamDepth(val) {
1012
- const clamped = Math.max(0.001, val);
1013
- if (clamped !== this._shoreFoamDepth) {
1014
- this._shoreFoamDepth = clamped;
816
+ /** How much the white foam is lit as a lumpy relief rather than a flat sheet, 0 for flat. */ get foamNormalStrength() {
817
+ return this._foamNormalStrength;
818
+ }
819
+ set foamNormalStrength(val) {
820
+ const clamped = Math.max(0, val);
821
+ if (clamped !== this._foamNormalStrength) {
822
+ this._foamNormalStrength = clamped;
1015
823
  this.uniformChanged();
1016
824
  }
1017
825
  }
1018
- /**
1019
- * Falloff applied across the band before it is scaled. Above 1 the coverage is
1020
- * pushed towards the near end, keeping the outer edge thin and broken instead
1021
- * of washing evenly over the whole range.
1022
- */ get shoreFoamFalloff() {
1023
- return this._shoreFoamFalloff;
826
+ /** Tint of the bubbles under the white foam, lit by the sky. */ get foamBubbleColor() {
827
+ return this._foamBubbleColor;
1024
828
  }
1025
- set shoreFoamFalloff(val) {
1026
- const clamped = Math.max(0.01, val);
1027
- if (clamped !== this._shoreFoamFalloff) {
1028
- this._shoreFoamFalloff = clamped;
829
+ set foamBubbleColor(val) {
830
+ if (!val.equalsTo(this._foamBubbleColor)) {
831
+ this._foamBubbleColor.set(val);
1029
832
  this.uniformChanged();
1030
833
  }
1031
834
  }
1032
- /**
1033
- * Size of the clumps the band's edge breaks into, as cycles across the band.
1034
- *
1035
- * Relative to {@link shoreFoamDepth} rather than an absolute frequency, so one
1036
- * setting serves both a shoreline metres across and a collar around a piling,
1037
- * and widening the band widens its clumps with it.
1038
- *
1039
- * Evaluated in world space, so the pattern stays put as the camera moves.
1040
- */ get shoreFoamScale() {
1041
- return this._shoreFoamScale;
1042
- }
1043
- set shoreFoamScale(val) {
835
+ /** How far the bubbles spread around the white foam and how strongly they show, 0 for none. */ get foamBubbleCoverage() {
836
+ return this._foamBubbleCoverage;
837
+ }
838
+ set foamBubbleCoverage(val) {
1044
839
  const clamped = Math.max(0, val);
1045
- if (clamped !== this._shoreFoamScale) {
1046
- this._shoreFoamScale = clamped;
840
+ if (clamped !== this._foamBubbleCoverage) {
841
+ this._foamBubbleCoverage = clamped;
1047
842
  this.uniformChanged();
1048
843
  }
1049
844
  }
1050
- /**
1051
- * How far the band's edge runs back and forth, as a fraction of
1052
- * {@link shoreFoamDepth}.
1053
- *
1054
- * This is what makes the band read as surf rather than as a painted rim. 0
1055
- * leaves a static band. Above 1 the band closes completely at the bottom of
1056
- * the cycle, which looks like the foam blinking out rather than retreating.
1057
- */ get shoreFoamWashAmount() {
1058
- return this._shoreFoamWashAmount;
1059
- }
1060
- set shoreFoamWashAmount(val) {
1061
- const clamped = Math.max(0, val);
1062
- if (clamped !== this._shoreFoamWashAmount) {
1063
- this._shoreFoamWashAmount = clamped;
845
+ /**
846
+ * Texture the foam is cut from, its value in the red channel: the foam
847
+ * density uncovers it from its brightest parts down, so bright clumps show
848
+ * first and dark holes last. Null for the built-in bubble pattern (see
849
+ * getDefaultFoamTexture), which reads as a texture of one's own would.
850
+ * Tiles every {@link foamTextureSize} metres.
851
+ */ get foamTexture() {
852
+ return this._foamTexture.get() ?? null;
853
+ }
854
+ set foamTexture(tex) {
855
+ if (tex !== this.foamTexture) {
856
+ this._foamTexture.set(tex);
1064
857
  this.uniformChanged();
1065
858
  }
1066
859
  }
1067
- /** Run-up cycles per second. Swell rather than wind waves, so well under 1. */ get shoreFoamWashSpeed() {
1068
- return this._shoreFoamWashSpeed;
860
+ /** Metres of water one repeat of the {@link foamTexture} covers. */ get foamTextureSize() {
861
+ return this._foamTextureSize;
1069
862
  }
1070
- set shoreFoamWashSpeed(val) {
1071
- const clamped = Math.max(0, val);
1072
- if (clamped !== this._shoreFoamWashSpeed) {
1073
- this._shoreFoamWashSpeed = clamped;
863
+ set foamTextureSize(val) {
864
+ const clamped = Math.max(0.01, val);
865
+ if (clamped !== this._foamTextureSize) {
866
+ this._foamTextureSize = clamped;
1074
867
  this.uniformChanged();
1075
868
  }
1076
869
  }
1077
- /**
1078
- * Spatial frequency of the run-up phase, in cycles per meter.
1079
- *
1080
- * Decorrelates the wash along the shore: at 0 the entire waterline advances
1081
- * and retreats in lockstep, which reads as the water level itself rising and
1082
- * falling; a cycle every few tens of meters breaks a long shoreline into
1083
- * sections that run up out of step with one another.
1084
- */ get shoreFoamWashScale() {
1085
- return this._shoreFoamWashScale;
1086
- }
1087
- set shoreFoamWashScale(val) {
870
+ /** How deep under the surface the bubbles seem to float, in metres; they shift against the foam as the view moves. */ get foamBubbleDepth() {
871
+ return this._foamBubbleDepth;
872
+ }
873
+ set foamBubbleDepth(val) {
1088
874
  const clamped = Math.max(0, val);
1089
- if (clamped !== this._shoreFoamWashScale) {
1090
- this._shoreFoamWashScale = clamped;
875
+ if (clamped !== this._foamBubbleDepth) {
876
+ this._foamBubbleDepth = clamped;
1091
877
  this.uniformChanged();
1092
878
  }
1093
879
  }
1094
- /** Whether the shoreline foam contributes at all. @internal */ get _shoreFoamEnabled() {
1095
- return this.featureUsed(WaterMaterial.FEATURE_SHORE_FOAM) ?? false;
1096
- }
1097
- /**
1098
- * Whether this shader build can run the band: the feature is on *and* the
1099
- * pyramid it queries is bound for this pass.
1100
- *
1101
- * A pass outside the main graph can shade water with no Hi-Z pass having run
1102
- * for it, and compiling a fetch against an unbound texture is a build failure.
1103
- * @internal
1104
- */ _shoreFoamAvailable(scope) {
1105
- return this._shoreFoamEnabled && !!ShaderHelper.getHiZDepthTexture(scope);
1106
- }
1107
880
  /** @internal */ _updateMediumCoefficients() {
1108
881
  this._extinction.setXYZ(this._absorption.x * this._absorptionScale + this._scattering.x * this._scatteringScale, this._absorption.y * this._absorptionScale + this._scattering.y * this._scatteringScale, this._absorption.z * this._absorptionScale + this._scattering.z * this._scatteringScale);
1109
- // A channel with no interaction at all transmits fully and scatters nothing;
1110
- // its albedo is arbitrary, so pick 0 rather than divide.
1111
882
  this._scatterAlbedo.setXYZ(this._extinction.x > 0 ? this._scattering.x * this._scatteringScale / this._extinction.x : 0, this._extinction.y > 0 ? this._scattering.y * this._scatteringScale / this._extinction.y : 0, this._extinction.z > 0 ? this._scattering.z * this._scatteringScale / this._extinction.z : 0);
1112
883
  }
1113
- get depthMulti() {
1114
- return this._depthMulti;
1115
- }
1116
- set depthMulti(val) {
1117
- if (val !== this._depthMulti) {
1118
- this._depthMulti = val;
1119
- this.uniformChanged();
1120
- }
1121
- }
1122
- /**
1123
- * Artistic scale on the refracted view offset. 1 is physical, 0 disables it.
1124
- *
1125
- * The offset is already derived from Snell's law and accounts for the
1126
- * incidence angle, the receiver depth and the perspective foreshortening, so
1127
- * this exists only to dial the result back for a stylised look. Values above 1
1128
- * exaggerate; the surface stays continuous, but the sample can wander far
1129
- * enough that the medium tint stops matching what is visible through it.
1130
- */ get refractionScale() {
884
+ /** Artistic scale on the refracted view offset. 1 is physical, 0 disables it. */ get refractionScale() {
1131
885
  return this._refractionScale;
1132
886
  }
1133
887
  set refractionScale(val) {
@@ -1137,14 +891,7 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
1137
891
  this.uniformChanged();
1138
892
  }
1139
893
  }
1140
- /**
1141
- * Scale on the Fresnel reflectance, 1 for the physical value.
1142
- *
1143
- * Below 1 the surface reflects less than it should and shows more of what is
1144
- * beneath it - useful when a physically correct grazing reflection buries a
1145
- * sea bed the shot is about. The F0 floor is scaled with it, so 0 gives a
1146
- * surface with no specular response at all.
1147
- */ get reflectionStrength() {
894
+ /** Scale on the Fresnel reflectance, 1 for physical. Scales F0 too, so 0 kills specular. */ get reflectionStrength() {
1148
895
  return this._reflectionStrength;
1149
896
  }
1150
897
  set reflectionStrength(val) {
@@ -1160,12 +907,6 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
1160
907
  needSceneDepth() {
1161
908
  return true;
1162
909
  }
1163
- /**
1164
- * The shoreline foam reads the Hi-Z pyramid's nearest-depth channel. WebGL1
1165
- * has no pyramid at all.
1166
- */ needHiZNearest() {
1167
- return this._shoreFoamEnabled && getDevice().type !== 'webgl';
1168
- }
1169
910
  _createHash() {
1170
911
  return `${super._createHash()}:${this.waveGenerator?.getHash() ?? ''}`;
1171
912
  }
@@ -1173,16 +914,16 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
1173
914
  this._clipmapInfo.setXYZW(rotation, scale, offsetX, offsetY);
1174
915
  this.uniformChanged();
1175
916
  }
1176
- setClipmapGridInfo(gridScale, gridOffsetX, gridOffsetY) {
1177
- if (this._clipmapGridInfo.x !== gridScale || this._clipmapGridInfo.y !== gridOffsetX || this._clipmapGridInfo.z !== gridOffsetY) {
1178
- this._clipmapGridInfo.setXYZW(gridScale, gridOffsetX, gridOffsetY, 0);
917
+ /**
918
+ * @param tileResolution - Cells along a clipmap tile, for the spacing of the
919
+ * mesh vertices the wave generator band-limits to; 0 for no limit.
920
+ */ setClipmapGridInfo(gridScale, gridOffsetX, gridOffsetY, tileResolution = 0) {
921
+ if (this._clipmapGridInfo.x !== gridScale || this._clipmapGridInfo.y !== gridOffsetX || this._clipmapGridInfo.z !== gridOffsetY || this._clipmapGridInfo.w !== tileResolution) {
922
+ this._clipmapGridInfo.setXYZW(gridScale, gridOffsetX, gridOffsetY, tileResolution);
1179
923
  this.uniformChanged();
1180
924
  }
1181
925
  }
1182
- /**
1183
- * How far from the camera the horizon skirt's outer edge is placed, in world
1184
- * units. Only read while {@link infinite} is on.
1185
- */ setSkirtDistance(distance) {
926
+ /** Distance from the camera to the horizon skirt's outer edge. Only read while {@link infinite}. */ setSkirtDistance(distance) {
1186
927
  if (this._skirtDistance !== distance) {
1187
928
  this._skirtDistance = distance;
1188
929
  this.uniformChanged();
@@ -1194,14 +935,11 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
1194
935
  supportLighting() {
1195
936
  return true;
1196
937
  }
1197
- /**
1198
- * {@inheritDoc IMixinLight.receivesWaterCaustics}
1199
- *
1200
- * Always false. Caustics belong on whatever the water is above: the submersion
1201
- * test compares a fragment's height against the surface height sampled on its
1202
- * sun ray, so unless the sun is overhead every water fragment has some other
1203
- * part of the same surface up-sun of it and reads as submerged wherever that
1204
- * part rides a crest.
938
+ /**
939
+ * {@inheritDoc IMixinLight.receivesWaterCaustics}
940
+ *
941
+ * Always false: the submersion test would read a water fragment as submerged
942
+ * wherever the surface up-sun of it rides a crest.
1205
943
  */ receivesWaterCaustics() {
1206
944
  return false;
1207
945
  }
@@ -1220,54 +958,41 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
1220
958
  scope.$l.scale2 = pb.mul(scope.$inputs.clipmapInfo.y, scope.clipmapGridInfo.x);
1221
959
  scope.$l.clipmapMatrix = pb.mat4(pb.mul(scope.c, scope.scale2), pb.mul(scope.s, scope.scale2), 0, 0, pb.neg(pb.mul(scope.s, scope.scale2)), pb.mul(scope.c, scope.scale2), 0, 0, 0, 0, 1, 0, pb.sub(pb.mul(scope.$inputs.clipmapInfo.z, scope.clipmapGridInfo.x), scope.clipmapGridInfo.y), pb.sub(pb.mul(scope.$inputs.clipmapInfo.w, scope.clipmapGridInfo.x), scope.clipmapGridInfo.z), 0, 1);
1222
960
  scope.$l.clipmapPos = pb.mul(scope.clipmapMatrix, pb.vec4(scope.$inputs.position, 1)).xy;
1223
- //scope.$l.level = pb.mul(ShaderHelper.getWorldMatrix(scope), pb.vec4(0, 0, 0, 1)).y;
1224
- scope.clipmapWorldPos = pb.mul(ShaderHelper.getWorldMatrix(scope), pb.vec4(scope.clipmapPos.x, 0, scope.clipmapPos.y, 1)).xyz; // pb.vec3(scope.clipmapPos.x, scope.level, scope.clipmapPos.y);
961
+ scope.clipmapWorldPos = pb.mul(ShaderHelper.getWorldMatrix(scope), pb.vec4(scope.clipmapPos.x, 0, scope.clipmapPos.y, 1)).xyz;
1225
962
  scope.worldNormal = pb.vec3(0, 1, 0);
1226
963
  scope.worldPos = scope.clipmapWorldPos;
964
+ // Mesh vertex spacing: clipmap level L spans Chebyshev distances T 2^L to
965
+ // 2T 2^L cells with cells 2^L across, so distance / T bounds it. Taken from
966
+ // the distance, not the level, so both sides of a seam agree.
967
+ scope.$l.camOffset = pb.abs(pb.sub(scope.clipmapWorldPos.xz, ShaderHelper.getCameraPosition(scope).xz));
968
+ scope.$l.vertexSpacing = pb.mul(pb.max(scope.clipmapGridInfo.x, pb.div(pb.max(scope.camOffset.x, scope.camOffset.y), pb.max(scope.clipmapGridInfo.w, 1))), pb.step(0.5, scope.clipmapGridInfo.w));
1227
969
  if (this.infinite) {
1228
970
  const that = this;
1229
- // The clipmap meshes all leave position.z at 0; the skirt ring sets it to
1230
- // 1 on its outer edge. Its inner edge is welded to the outermost tiles and
1231
- // is displaced with them, so the join stays closed.
971
+ // position.z is 1 on the skirt ring's outer edge, 0 everywhere else.
1232
972
  scope.$l.skirt = pb.step(0.5, scope.$inputs.position.z);
1233
973
  scope.$if(pb.greaterThan(scope.skirt, 0), function() {
1234
- // Radially outwards from the camera rather than from the clipmap
1235
- // origin: the ring has to read as a horizon from where it is being
1236
- // looked at, and the clipmap is snapped to a grid the camera wanders
1237
- // within.
974
+ // Pushed radially out from the camera, undisplaced, at the still-water level.
1238
975
  this.$l.camXZ = ShaderHelper.getCameraPosition(this).xz;
1239
976
  this.$l.outDir = pb.normalize(pb.sub(this.clipmapWorldPos.xz, this.camXZ));
1240
977
  this.$l.outXZ = pb.add(this.camXZ, pb.mul(this.outDir, this.skirtDistance));
1241
- // Flat, at the still-water level. A displaced vertex out here would be
1242
- // several kilometres from its neighbours, so any wave it sampled would
1243
- // read as a jagged silhouette rather than a swell.
1244
978
  this.worldPos = pb.vec3(this.outXZ.x, this.clipmapWorldPos.y, this.outXZ.y);
1245
979
  }).$else(function() {
1246
- that.waveGenerator?.calcVertexPositionAndNormal(this, this.clipmapWorldPos, this.worldPos, this.worldNormal);
980
+ that.waveGenerator?.calcVertexPositionAndNormal(this, this.clipmapWorldPos, this.worldPos, this.worldNormal, this.vertexSpacing);
1247
981
  });
1248
982
  scope.$outputs.skirt = scope.skirt;
1249
983
  } else {
1250
- this.waveGenerator?.calcVertexPositionAndNormal(scope, scope.clipmapWorldPos, scope.worldPos, scope.worldNormal);
984
+ this.waveGenerator?.calcVertexPositionAndNormal(scope, scope.clipmapWorldPos, scope.worldPos, scope.worldNormal, scope.vertexSpacing);
1251
985
  }
1252
986
  scope.$outputs.worldPos = scope.worldPos;
1253
987
  scope.$outputs.clipmapPos = scope.clipmapWorldPos;
1254
988
  scope.$outputs.worldNormal = scope.worldNormal;
1255
989
  if (this.infinite) {
1256
- // Pulled in along the view ray rather than depth-clamped.
1257
- //
1258
- // The surface has to reach past the far plane, but the depth buffer cannot
1259
- // encode anything past it. A perspective projection is invariant under
1260
- // scaling about the eye, so moving a vertex along its own view ray leaves
1261
- // its screen position untouched while bringing its depth back inside the
1262
- // frustum - every vertex stays unclipped, so there is no seam to tear.
1263
- //
1264
- // Shading keeps the true world position, so only the written depth is
1265
- // affected, and only past the far plane.
990
+ // Vertices past the far plane are pulled in along their own view ray:
991
+ // screen position is unchanged, only the written depth moves. Shading
992
+ // keeps the true world position.
1266
993
  scope.$l.camPos = ShaderHelper.getCameraPosition(scope).xyz;
1267
994
  scope.$l.viewVec = pb.sub(scope.$outputs.worldPos, scope.camPos);
1268
995
  scope.$l.viewDist = pb.max(pb.length(scope.viewVec), 1e-6);
1269
- // Just inside the far plane, so the vertex is never the one the hardware
1270
- // decides to clip and never lands on the cleared depth value.
1271
996
  scope.$l.maxDist = pb.mul(ShaderHelper.getCameraParams(scope).y, INFINITE_DEPTH_PULLIN);
1272
997
  scope.$l.pullIn = pb.min(pb.div(scope.maxDist, scope.viewDist), 1);
1273
998
  scope.$l.projPos = pb.add(scope.camPos, pb.mul(scope.viewVec, scope.pullIn));
@@ -1275,7 +1000,18 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
1275
1000
  } else {
1276
1001
  ShaderHelper.setClipSpacePosition(scope, pb.mul(ShaderHelper.getViewProjectionMatrix(scope), pb.vec4(scope.$outputs.worldPos, 1)));
1277
1002
  }
1278
- ShaderHelper.resolveMotionVector(scope, scope.$outputs.worldPos, scope.$outputs.worldPos);
1003
+ const gen = this.waveGenerator;
1004
+ if (gen?.calcPrevVertexPosition && ShaderHelper.getPrevUnjitteredViewProjectionMatrix(scope)) {
1005
+ scope.$l.prevWorldPos = pb.vec3();
1006
+ gen.calcPrevVertexPosition(scope, scope.clipmapWorldPos, scope.prevWorldPos, scope.vertexSpacing);
1007
+ if (this.infinite) {
1008
+ // The skirt does not move
1009
+ scope.prevWorldPos = pb.mix(scope.prevWorldPos, scope.$outputs.worldPos, scope.skirt);
1010
+ }
1011
+ ShaderHelper.resolveMotionVector(scope, scope.$outputs.worldPos, scope.prevWorldPos);
1012
+ } else {
1013
+ ShaderHelper.resolveMotionVector(scope, scope.$outputs.worldPos, scope.$outputs.worldPos);
1014
+ }
1279
1015
  }
1280
1016
  fragmentShader(scope) {
1281
1017
  super.fragmentShader(scope);
@@ -1287,129 +1023,292 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
1287
1023
  if (this.needFragmentColor()) {
1288
1024
  scope.refractionScale = pb.float().uniform(2);
1289
1025
  if (this.refractionMode === 'offset') {
1290
- // Only the cheap path has a fixed depth scale to step along; the march
1291
- // derives its own from the scene.
1292
1026
  scope.cheapRefractionDepth = pb.float().uniform(2);
1293
1027
  }
1294
1028
  scope.reflectionStrength = pb.float().uniform(2);
1029
+ // (wind direction x, z, upwind slope variance, crosswind slope variance)
1030
+ scope.unresolvedSlope = pb.vec4().uniform(2);
1031
+ // Sky bake to the live pre-exposed space, as the visible sky is drawn
1032
+ scope.skyReflectScale = pb.float().uniform(2);
1295
1033
  scope.ssrParams = pb.vec4().uniform(2);
1296
- // (intensity, 1 / full-scatter crest height, 0, 0)
1034
+ // Eye vs. rest plane from the CPU submersion test: 1 above, -1 below,
1035
+ // 0 unknown (compare in the shader).
1036
+ scope.eyeSide = pb.float().uniform(2);
1037
+ // (intensity, crest absorption height in m, 0, 0)
1297
1038
  scope.subsurfaceParams = pb.vec4().uniform(2);
1039
+ scope.subsurfaceTint = pb.vec3().uniform(2);
1298
1040
  // (coverage scale, coverage falloff, 0, 0)
1299
1041
  scope.foamShadingParams = pb.vec4().uniform(2);
1300
1042
  scope.foamColor = pb.vec3().uniform(2);
1301
- if (this._shoreFoamAvailable(scope)) {
1302
- // (coverage scale, band depth in m, coverage falloff, pattern cycles/m)
1303
- scope.shoreFoamParams = pb.vec4().uniform(2);
1304
- // (run-up amount, run-up cycles/s, run-up cycles/m, query radius in m)
1305
- scope.shoreFoamWashParams = pb.vec4().uniform(2);
1306
- }
1043
+ // (white foam feather, foam normal strength, bubble coverage, bubble depth in m)
1044
+ scope.foamLookParams = pb.vec4().uniform(2);
1045
+ scope.foamBubbleColor = pb.vec3().uniform(2);
1046
+ scope.foamTex = pb.tex2D().uniform(2);
1047
+ // uv of the foam texture per metre
1048
+ scope.foamUVScale = pb.float().uniform(2);
1049
+ // Thickness in metres a sheet of water fades in over, 0 for none
1050
+ scope.edgeFadeWidth = pb.float().uniform(2);
1307
1051
  // (intensity, anisotropy, 0, 0)
1308
1052
  scope.sunScatterParams = pb.vec4().uniform(2);
1309
- // Mip widths per meter of path the medium's scattering contributes to the
1310
- // refraction blur. Resolved on the CPU from the per-channel scattering
1311
- // coefficient, since the blur is one LOD for all three.
1053
+ // Mip widths per meter of path; one scalar LOD for all three channels.
1312
1054
  scope.refractBlurDensity = pb.float().uniform(2);
1313
- // Declared in both medium modes: the ramp only replaces the depth-driven
1314
- // absorption and scattering, while the subsurface and sun-scattering terms
1315
- // need the medium's hue and thickness regardless.
1316
1055
  scope.mediumAlbedo = pb.vec3().uniform(2);
1317
1056
  scope.mediumExtinction = pb.vec3().uniform(2);
1318
- if (this.mediumMode === 'ramp') {
1319
- scope.depthMulti = pb.float().uniform(2);
1320
- scope.scatterRampTex = pb.tex2D().uniform(2);
1321
- scope.absorptionRampTex = pb.tex2D().uniform(2);
1322
- }
1057
+ // Rect light specular (evaluateLTCRectLightTerms).
1058
+ scope.zLTCMatLut = pb.tex2D().uniform(2);
1059
+ scope.zLTCAmpLut = pb.tex2D().uniform(2);
1323
1060
  }
1324
- // An unbounded surface has no edge to clip against.
1325
1061
  if (!this.infinite) {
1326
1062
  scope.$l.discardable = pb.or(pb.any(pb.lessThan(scope.$inputs.worldPos.xz, scope.region.xy)), pb.any(pb.greaterThan(scope.$inputs.worldPos.xz, scope.region.zw)));
1327
1063
  scope.$if(scope.discardable, function() {
1328
1064
  pb.discard();
1329
1065
  });
1330
1066
  }
1067
+ if (this.debugOutput === 'shoreRegions' || this.debugOutput === 'shoreLevels') {
1068
+ // Show the actual displaced carrier in every pass. Wet-edge discard and
1069
+ // ground-depth projection would otherwise conceal its geometry or create
1070
+ // an apparent boundary unrelated to rasterized mesh coverage.
1071
+ if (this.needFragmentColor()) {
1072
+ if (this.debugOutput === 'shoreLevels') {
1073
+ scope.$l.outColor = pb.vec4(this._shoreWaveGenerator ? this._shoreWaveGenerator.calcDebugLevels(scope, scope.$inputs.clipmapPos.xz) : pb.vec3(0.1), 1);
1074
+ } else {
1075
+ scope.$l.shoreRegion = this._shoreWaveGenerator ? this._shoreWaveGenerator.calcDebugRegion(scope, scope.$inputs.clipmapPos.xz) : pb.vec3(0, 0, 1);
1076
+ scope.$l.shoreColor = scope.$choice(pb.greaterThan(scope.shoreRegion.y, 0.5), scope.$choice(pb.greaterThan(scope.shoreRegion.z, 1e-3), pb.vec3(1, 0.3, 0.02), pb.vec3(0.85, 0.02, 0.8)), pb.vec3(0, 0.85, 1));
1077
+ scope.$l.outColor = pb.vec4(pb.mix(pb.vec3(0.02, 0.12, 0.9), scope.shoreColor, scope.shoreRegion.x), 1);
1078
+ }
1079
+ if (this.drawContext.materialFlags & (MaterialVaryingFlags.SCENE_STORE_ROUGHNESS | MaterialVaryingFlags.SCENE_STORE_NORMAL)) {
1080
+ this.outputFragmentColor(scope, scope.$inputs.worldPos, scope.outColor, pb.vec4(0), scope.outColor);
1081
+ } else {
1082
+ this.outputFragmentColor(scope, scope.$inputs.worldPos, scope.outColor);
1083
+ }
1084
+ } else {
1085
+ this.outputFragmentColor(scope, scope.$inputs.worldPos, null);
1086
+ }
1087
+ return;
1088
+ }
1331
1089
  if (this.needFragmentColor()) {
1332
- scope.$l.normal = this.waveGenerator ? this.waveGenerator.calcFragmentNormalAndFoam(scope, scope.$inputs.clipmapPos.xz, scope.$inputs.worldNormal) : pb.vec4(scope.$inputs.worldNormal, 0);
1090
+ // Screen derivatives of the undisplaced position, taken here in uniform
1091
+ // control flow; the foam textures are sampled with them under branches.
1092
+ scope.$l.restXZGrad = pb.vec4(pb.dpdx(scope.$inputs.clipmapPos.xz), pb.dpdy(scope.$inputs.clipmapPos.xz));
1093
+ // The foam texture here, drifting; a generator carrying foam of its own
1094
+ // blends in where its foam is
1095
+ scope.$l.foamSample = sampleFoamTexture(scope, scope.foamTex, pb.mul(pb.add(scope.$inputs.clipmapPos.xz, pb.vec2(pb.mul(ShaderHelper.getElapsedTime(scope), FOAM_DRIFT))), scope.foamUVScale), scope.restXZGrad, scope.foamUVScale);
1096
+ scope.$l.normal = this.waveGenerator ? this.waveGenerator.calcFragmentNormalAndFoam(scope, scope.$inputs.clipmapPos.xz, scope.$inputs.worldNormal, scope.foamSample, this.waveGenerator.calcUnresolvedSlopeVariance ? scope.restXZGrad : undefined) : pb.vec4(scope.$inputs.worldNormal, 0);
1333
1097
  if (this.infinite) {
1334
- // Flat and foamless across the horizon band. The skirt's world position
1335
- // was never displaced, so evaluating waves from it would paint detail
1336
- // the geometry does not have - and at kilometres per pixel that detail
1337
- // is under-sampled into crawling speckle. The screen-space fog pass is
1338
- // what should be visible here.
1098
+ // Flat and foamless on the skirt: waves evaluated at kilometres per
1099
+ // pixel alias into speckle.
1339
1100
  scope.normal = pb.mix(scope.normal, pb.vec4(0, 1, 0, 0), scope.$inputs.skirt);
1340
1101
  }
1341
- scope.$l.outColor = pb.vec4(this.waterShading(scope, scope.$inputs.worldPos, scope.normal.xyz, scope.normal.w), 1);
1102
+ // Vertical wave displacement, interpolated from the vertices. Zero on the skirt.
1103
+ scope.$l.crestHeight = pb.sub(scope.$inputs.worldPos.y, scope.$inputs.clipmapPos.y);
1104
+ const surfaceCoverage = this.waveGenerator?.calcSurfaceCoverage;
1105
+ scope.$l.surfaceCoverage = surfaceCoverage ? surfaceCoverage.call(this.waveGenerator, scope, scope.$inputs.clipmapPos.xz) : pb.float(1);
1106
+ const sheetThickness = this.waveGenerator?.calcSheetThickness;
1107
+ scope.$l.sheetThickness = sheetThickness ? sheetThickness.call(this.waveGenerator, scope, scope.$inputs.clipmapPos.xz) : pb.float(-1);
1108
+ if (this._shoreWaveGenerator) {
1109
+ // Near the shore the foam is at least the near-shore density, joining
1110
+ // the breaking waves' foam to the beach's (see nearShore).
1111
+ scope.normal.w = this._shoreWaveGenerator.calcNearShoreFoam(scope, scope.$inputs.clipmapPos.xz, scope.normal.w);
1112
+ }
1113
+ // Reconstruct the opaque surface before the swash's discard. Deposited
1114
+ // foam is projected onto this ground, independently of the raised mesh.
1115
+ scope.$l.scenePos = ShaderHelper.samplePositionFromDepth(scope, ShaderHelper.getLinearDepthTexture(scope), pb.div(scope.$builtins.fragCoord.xy, ShaderHelper.getRenderSize(scope)), ShaderHelper.getJitteredInvVPMatrix(scope), ShaderHelper.getCameraParams(scope).xy);
1116
+ scope.$l.groundFoamDensity = pb.float(0);
1117
+ scope.$l.groundFoamSample = pb.vec3(0);
1118
+ scope.$l.groundFoamWeight = pb.float(0);
1119
+ scope.$l.groundNormal = pb.vec3(0, 1, 0);
1120
+ if (this.debugOutput === 'swashVisibility') {
1121
+ scope.$l.visibilityDepthFail = pb.float(0);
1122
+ }
1123
+ if (this.debugOutput === 'swashGroundSupport') {
1124
+ scope.$l.groundFieldCoverage = pb.float(0);
1125
+ scope.$l.groundWetCoverage = pb.float(0);
1126
+ } else if (this.debugOutput === 'swashHandover') {
1127
+ scope.$l.groundWetCoverage = pb.float(0);
1128
+ } else if (this.debugOutput === 'swashFoamSources') {
1129
+ scope.$l.groundFieldDensity = pb.float(0);
1130
+ scope.$l.groundSurfDensity = pb.float(0);
1131
+ }
1132
+ if (this._shoreWaveGenerator && this._foamFieldGenerator) {
1133
+ // Derivatives precede all coverage branches; pattern coordinates stay
1134
+ // fixed to the sand even while the water and camera move.
1135
+ scope.$l.groundGrad = pb.vec4(pb.dpdx(scope.scenePos.xz), pb.dpdy(scope.scenePos.xz));
1136
+ scope.$l.groundCross = pb.cross(pb.dpdx(scope.scenePos.xyz), pb.dpdy(scope.scenePos.xyz));
1137
+ scope.groundNormal = pb.normalize(pb.add(pb.mul(scope.groundCross, pb.sign(scope.groundCross.y)), pb.vec3(0, 1e-8, 0)));
1138
+ scope.$l.groundMask = this._shoreWaveGenerator.calcFoamGroundMask(scope, scope.scenePos.xyz);
1139
+ const field = this._foamField;
1140
+ const shore = this._shoreWaveGenerator;
1141
+ const debugOutput = this.debugOutput;
1142
+ scope.$if(pb.and(pb.lessThan(scope.scenePos.w, SCENE_SKY_DEPTH01), pb.greaterThan(scope.groundMask, 0)), function() {
1143
+ // The field's foam at the actual ground position: on a thin sheet and
1144
+ // on the sand it drained from alike, the field holding it there
1145
+ // (FOAM_SETTLE of the shore waves)
1146
+ this.$l.groundMobile = field.sample(this, this.scenePos.xz);
1147
+ this.$l.groundWet = shore.calcSurfaceCoverage(this, this.scenePos.xz);
1148
+ this.$l.groundThickness = shore.calcSheetThickness(this, this.scenePos.xz);
1149
+ // Coverage and handover describe the same carrier point. Using the
1150
+ // ground point's thickness here leaves a gap at grazing angles:
1151
+ // the carrier can be dry while its projected ground is still wet.
1152
+ this.$l.groundHandoverThickness = this.$choice(pb.greaterThanEqual(this.sheetThickness, 0), this.sheetThickness, this.groundThickness);
1153
+ if (debugOutput === 'swashGroundSupport') {
1154
+ this.groundWetCoverage = this.groundWet;
1155
+ this.groundFieldCoverage = this.groundMobile.y;
1156
+ } else if (debugOutput === 'swashHandover') {
1157
+ this.groundWetCoverage = this.groundWet;
1158
+ } else if (debugOutput === 'swashFoamSources') {
1159
+ this.groundFieldDensity = this.groundMobile.x;
1160
+ this.groundSurfDensity = pb.mul(shore.calcNearShoreFoam(this, this.scenePos.xz, pb.float(0)), this.groundWet);
1161
+ }
1162
+ this.groundFoamWeight = pb.mul(this.groundMask, this.groundMobile.y, // Unknown thickness must retain the water surface. In particular,
1163
+ // a valid sand bed outside the shore-wave zone is not a dry sheet.
1164
+ this.$choice(pb.greaterThanEqual(this.groundHandoverThickness, 0), pb.sub(1, pb.smoothStep(0.01, 0.06, this.groundHandoverThickness)), pb.float(0)));
1165
+ // The surf's foam only where the sheet now is; the field's wherever it holds it
1166
+ this.groundFoamDensity = pb.clamp(pb.max(this.groundMobile.x, pb.mul(shore.calcNearShoreFoam(this, this.scenePos.xz, pb.float(0)), this.groundWet)), 0, 1);
1167
+ this.$if(pb.greaterThan(this.groundFoamDensity, 1e-3), function() {
1168
+ // The water's foam pattern at the ground: it stands still near
1169
+ // the shore (see nearShore), so the beach's foam and the water's
1170
+ // are one pattern through the handover.
1171
+ this.groundFoamSample = WaterFoamField.flowPattern(this, this.ffFoamTex, this.ffFoamUVScale, this.ffClock.x, this.scenePos.xz, this.groundGrad, shore.calcFoamFlow(this, this.scenePos.xz, this.ffDrift));
1172
+ });
1173
+ });
1174
+ // The thin-film foam and its depth use the same ground transition.
1175
+ // Otherwise a wet carrier can still clip the projected foam against
1176
+ // the terrain right where the two layers meet.
1177
+ scope.$builtins.fragDepth = pb.mix(scope.$builtins.fragCoord.z, ShaderHelper.linearNormalizedToNonLinearDepth(scope, pb.max(pb.sub(scope.scenePos.w, pb.div(0.001, ShaderHelper.getCameraParams(scope).y)), 0), ShaderHelper.getCameraParams(scope).xy), scope.$choice(pb.lessThanEqual(scope.surfaceCoverage, 1e-3), pb.float(1), scope.groundFoamWeight));
1178
+ }
1179
+ const revealSwash = this.debugOutput === 'foamField' || this.debugOutput === 'swashVisibility' || this.debugOutput === 'swashGroundSupport';
1180
+ if (revealSwash && this._shoreWaveGenerator && this._foamFieldGenerator) {
1181
+ if (this.debugOutput === 'swashVisibility') {
1182
+ scope.visibilityDepthFail = scope.$choice(pb.greaterThan(pb.mul(pb.sub(ShaderHelper.nonLinearDepthToLinearNormalized(scope, scope.$builtins.fragDepth), scope.scenePos.w), ShaderHelper.getCameraParams(scope).y), 0.0001), pb.float(1), pb.float(0));
1183
+ }
1184
+ scope.$if(pb.and(pb.lessThan(scope.scenePos.w, SCENE_SKY_DEPTH01), pb.greaterThan(scope.groundMask, 0)), function() {
1185
+ this.$builtins.fragDepth = ShaderHelper.linearNormalizedToNonLinearDepth(this, pb.max(pb.sub(this.scenePos.w, pb.div(0.001, ShaderHelper.getCameraParams(this).y)), 0), ShaderHelper.getCameraParams(this).xy);
1186
+ });
1187
+ }
1188
+ scope.$l.swashDiscard = pb.and(pb.lessThanEqual(scope.surfaceCoverage, 1e-3), pb.lessThanEqual(pb.mul(scope.groundFoamDensity, scope.groundFoamWeight), 1e-3));
1189
+ // Past the wet edge, only the foam actually left on the ground draws.
1190
+ if (surfaceCoverage && !revealSwash) {
1191
+ scope.$if(scope.swashDiscard, function() {
1192
+ pb.discard();
1193
+ });
1194
+ }
1195
+ if (this.debugOutput === 'swashGroundSupport') {
1196
+ scope.$l.outColor = pb.vec4(this._shoreWaveGenerator && this._foamFieldGenerator ? scope.groundMask : pb.float(0), scope.groundFieldCoverage, scope.groundWetCoverage, 1);
1197
+ } else if (this.debugOutput === 'swashVisibility') {
1198
+ scope.$l.outColor = pb.vec4(scope.$choice(scope.swashDiscard, pb.float(1), pb.float(0)), scope.visibilityDepthFail, scope.groundFoamDensity, 1);
1199
+ } else if (this.debugOutput === 'swashHandover') {
1200
+ scope.$l.outColor = pb.vec4(scope.surfaceCoverage, scope.groundWetCoverage, scope.groundFoamWeight, 1);
1201
+ } else if (this.debugOutput === 'swashFoamSources') {
1202
+ scope.$l.outColor = pb.vec4(scope.normal.w, scope.groundFieldDensity, scope.groundSurfDensity, 1);
1203
+ } else if (this.debugOutput === 'interaction') {
1204
+ // The field on its own, grey at rest, scaled so its amplitude clamp
1205
+ // reaches black and white; dimmed outside the window so its reach shows.
1206
+ if (this._interactiveWaveGenerator) {
1207
+ const interaction = this._interactiveWaveGenerator.interaction;
1208
+ scope.$l.wiDebugHeight = pb.div(interaction.sampleHeight(scope, scope.$inputs.clipmapPos.xz), scope.wiParams2.y);
1209
+ scope.$l.wiDebugMask = pb.mix(pb.float(0.55), pb.float(1), interaction.windowMask(scope, scope.$inputs.clipmapPos.xz));
1210
+ } else {
1211
+ scope.$l.wiDebugHeight = pb.float(0);
1212
+ scope.$l.wiDebugMask = pb.float(1);
1213
+ }
1214
+ scope.$l.outColor = pb.vec4(pb.vec3(pb.mul(pb.add(0.5, pb.mul(scope.wiDebugHeight, 0.5)), scope.wiDebugMask)), 1);
1215
+ } else if (this.debugOutput === 'foamField') {
1216
+ // The field's density; mid grey off the field, where coverage is 0
1217
+ if (this._foamFieldGenerator) {
1218
+ scope.$l.ffDebug = this._foamField.sample(scope, scope.$inputs.clipmapPos.xz);
1219
+ } else {
1220
+ scope.$l.ffDebug = pb.vec3(0);
1221
+ }
1222
+ scope.$l.outColor = pb.vec4(pb.vec3(pb.mix(pb.float(0.5), pb.max(scope.ffDebug.x, scope.ffDebug.z), scope.ffDebug.y)), 1);
1223
+ } else {
1224
+ // The foam field's wall foam layer, see FOAM_WALL
1225
+ scope.$l.wallFoam = this._foamFieldGenerator ? pb.mul(this._foamField.sample(scope, scope.$inputs.clipmapPos.xz).z, this.infinite ? pb.sub(1, scope.$inputs.skirt) : pb.float(1)) : pb.float(0);
1226
+ scope.$l.outColor = pb.vec4(this.waterShading(scope, scope.$inputs.worldPos, scope.normal.xyz, scope.normal.w, scope.crestHeight, scope.$inputs.clipmapPos.xz, scope.restXZGrad, scope.foamSample, scope.surfaceCoverage, scope.sheetThickness, scope.wallFoam, scope.scenePos, scope.groundFoamDensity, scope.groundFoamSample, scope.groundNormal, scope.groundFoamWeight), 1);
1227
+ }
1342
1228
  if (this.drawContext.materialFlags & (MaterialVaryingFlags.SCENE_STORE_ROUGHNESS | MaterialVaryingFlags.SCENE_STORE_NORMAL)) {
1343
- // The real roughness, not a constant 1, so SSR on another surface does
1344
- // not treat a near-mirror sea as fully diffuse.
1345
- scope.$l.outRoughness = pb.vec4(pb.vec3(this.waterRoughness(scope, scope.$inputs.worldPos)), 0);
1229
+ scope.$l.outRoughness = pb.vec4(pb.vec3(this.waterRoughness(scope, scope.$inputs.worldPos, scope.restXZGrad)), 0);
1346
1230
  this.outputFragmentColor(scope, scope.$inputs.worldPos, scope.outColor, scope.outRoughness, scope.outColor);
1347
1231
  } else {
1348
1232
  this.outputFragmentColor(scope, scope.$inputs.worldPos, scope.outColor);
1349
1233
  }
1350
1234
  } else {
1235
+ // The support mesh over dry sand is only a carrier for the colour pass.
1236
+ // It must not replace terrain depth or its motion vectors in other passes.
1237
+ const coverage = this.waveGenerator?.calcSurfaceCoverage;
1238
+ if (coverage) {
1239
+ scope.$l.surfaceCoverage = coverage.call(this.waveGenerator, scope, scope.$inputs.clipmapPos.xz);
1240
+ scope.$if(pb.lessThanEqual(scope.surfaceCoverage, 1e-3), function() {
1241
+ pb.discard();
1242
+ });
1243
+ }
1351
1244
  this.outputFragmentColor(scope, scope.$inputs.worldPos, null);
1352
1245
  }
1353
1246
  }
1354
- /**
1355
- * Specular roughness of the surface at a world position.
1356
- *
1357
- * Distance fades the wave normals flat, and this hands that lost slope to the
1358
- * specular lobe instead of dropping it. Shared with the scene roughness buffer
1359
- * so a surface reflecting the water sees the same value.
1360
- */ waterRoughness(scope, worldPos) {
1247
+ /**
1248
+ * Covariance (x, z, xz) of the wave slopes the shading normal no longer shows
1249
+ * at this pixel, or null when the wave generator cannot tell.
1250
+ */ unresolvedSlopeVariance(scope, restXZGrad) {
1251
+ const gen = this.waveGenerator;
1252
+ return gen?.calcUnresolvedSlopeVariance ? gen.calcUnresolvedSlopeVariance(scope, restXZGrad) : null;
1253
+ }
1254
+ /**
1255
+ * Specular roughness at a world position. The slopes too fine for the pixel
1256
+ * leave the normal and are handed to the lobe, GGX alpha^2 taking their
1257
+ * variance (Beckmann m^2 = 2 sigma^2 per axis). Without the generator's
1258
+ * account of them, distance flattens the normals and the lobe widens with it.
1259
+ * Also written to the scene roughness buffer.
1260
+ */ waterRoughness(scope, worldPos, restXZGrad) {
1361
1261
  const pb = scope.$builder;
1262
+ const that = this;
1362
1263
  pb.func('waterRoughness', [
1363
- pb.vec3('worldPos')
1264
+ pb.vec3('worldPos'),
1265
+ pb.vec4('restXZGrad')
1364
1266
  ], function() {
1365
- this.$l.dist = pb.length(pb.sub(this.worldPos, ShaderHelper.getCameraPosition(this)));
1366
- this.$l.normalScale = pb.clamp(pb.div(100, this.dist), 0, 1);
1367
- this.$return(pb.mix(WATER_DISTANT_ROUGHNESS, WATER_BASE_ROUGHNESS, this.normalScale));
1267
+ const slopeVar = that.unresolvedSlopeVariance(this, this.restXZGrad);
1268
+ if (slopeVar) {
1269
+ this.$l.slopeVar = slopeVar;
1270
+ this.$l.alpha2 = pb.add(WATER_BASE_ROUGHNESS ** 4, pb.max(pb.add(this.slopeVar.x, this.slopeVar.y), 0));
1271
+ this.$return(pb.sqrt(pb.sqrt(this.alpha2)));
1272
+ } else {
1273
+ this.$l.dist = pb.length(pb.sub(this.worldPos, ShaderHelper.getCameraPosition(this)));
1274
+ this.$l.normalScale = pb.clamp(pb.div(100, this.dist), 0, 1);
1275
+ this.$return(pb.mix(WATER_DISTANT_ROUGHNESS, WATER_BASE_ROUGHNESS, this.normalScale));
1276
+ }
1368
1277
  });
1369
- return scope.waterRoughness(worldPos);
1370
- }
1371
- /**
1372
- * Where to sample the scene behind the water, and how far the light travelled
1373
- * through the medium to get there.
1374
- *
1375
- * Refracts the view ray by Snell's law, then marches that ray against the scene
1376
- * depth: at each step it projects the current point back to the screen, reads
1377
- * the depth there, and stops at the first point where the ray has passed into
1378
- * the geometry that pixel shows. The incidence angle, the depth of the receiver
1379
- * and the perspective foreshortening all fall out of that.
1380
- *
1381
- * Independent of the engine's depth convention: the projection only ever reads
1382
- * `clip.xy / clip.w`, every depth comparison happens in view space, and
1383
- * decoding the depth texture lives behind {@link ShaderHelper.sampleLinearDepth}.
1384
- *
1385
- * @param scope - Current shader scope
1386
- * @param worldPos - Surface point being shaded
1387
- * @param normal - Wave normal at that point, pointing up out of the water
1388
- * @param eyeVecNorm - Normalized direction from the camera to the surface
1389
- * @param screenUV - Screen UV of the surface point
1390
- * @param surfaceViewZ - View-space z of the surface point
1391
- * @param straightDepth01 - Normalized linear depth of the scene behind it
1392
- * @param straightWorldPos - Unrefracted scene world position behind the surface
1393
- * @param straightDist - Straight-line distance from the surface to that scene
1394
- * @returns `vec3(uv, pathLength)` - where to sample, and the medium path in meters
1395
- */ waterRefraction(scope, worldPos, normal, eyeVecNorm, screenUV, surfaceViewZ, straightDepth01, straightWorldPos, straightDist) {
1278
+ return scope.waterRoughness(worldPos, restXZGrad);
1279
+ }
1280
+ /**
1281
+ * Where to sample the scene behind the water, and the medium path length to it.
1282
+ *
1283
+ * Refracts the view ray by Snell's law and marches it against the scene depth.
1284
+ * Depth-convention independent: projection only reads `clip.xy / clip.w` and
1285
+ * all depth comparisons are in view space.
1286
+ *
1287
+ * @param scope - Current shader scope
1288
+ * @param worldPos - Surface point being shaded
1289
+ * @param normal - Wave normal at that point, pointing up out of the water
1290
+ * @param eyeVecNorm - Normalized direction from the camera to the surface
1291
+ * @param screenUV - Screen UV of the surface point
1292
+ * @param surfaceViewZ - View-space z of the surface point
1293
+ * @param straightDepth01 - Normalized linear depth of the scene behind it
1294
+ * @param straightWorldPos - Unrefracted scene world position behind the surface
1295
+ * @param straightDist - Straight-line distance from the surface to that scene
1296
+ * @param underwater - Whether the eye is below the surface
1297
+ * @returns `vec3(uv, pathLength)` - where to sample, and the medium path in meters,
1298
+ * {@link BOTTOMLESS_PATH} with only sky behind the surface
1299
+ */ waterRefraction(scope, worldPos, normal, eyeVecNorm, screenUV, surfaceViewZ, straightDepth01, straightWorldPos, straightDist, underwater) {
1396
1300
  const pb = scope.$builder;
1397
1301
  const march = this.refractionMode === 'march';
1398
- // Screen UV of a world position, through the same matrix the surface itself
1399
- // was rasterised with.
1400
- //
1401
- // Only ever used as a difference of two such UVs, which is what makes the
1402
- // offset independent of the conventions baked into that matrix: the TAA
1403
- // jitter and the clip-space Y orientation cancel between the two ends. The
1404
- // 0.5/0.5 mapping is the inverse of ShaderHelper.samplePositionFromDepth's.
1302
+ // Only ever used as a difference of two UVs, so TAA jitter and clip-space Y
1303
+ // orientation cancel. Inverse of ShaderHelper.samplePositionFromDepth's mapping.
1405
1304
  pb.func('waterRefractProjectUV', [
1406
1305
  pb.vec3('worldPos')
1407
1306
  ], function() {
1408
1307
  this.$l.h = pb.mul(ShaderHelper.getViewProjectionMatrix(this), pb.vec4(this.worldPos, 1));
1409
1308
  this.$return(pb.add(pb.mul(pb.div(this.h.xy, pb.max(this.h.w, 1e-6)), 0.5), pb.vec2(0.5)));
1410
1309
  });
1411
- // Where to sample the scene colour for a refracted ray that ended at
1412
- // `hitPos`, faded back to the straight-through sample at the screen border.
1310
+ // Sample UV for a refracted ray ending at `hitPos`, faded to the straight
1311
+ // sample at the screen border.
1413
1312
  pb.func('waterRefractUV', [
1414
1313
  pb.vec2('screenUV'),
1415
1314
  pb.vec2('uvBase'),
@@ -1417,22 +1316,14 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
1417
1316
  pb.float('scale')
1418
1317
  ], function() {
1419
1318
  this.$l.uv = pb.add(this.screenUV, pb.mul(pb.sub(this.waterRefractProjectUV(this.hitPos), this.uvBase), this.scale));
1420
- // Off screen there is no scene colour to refract, and a clamped sample
1421
- // would smear one border pixel along the whole edge of the water.
1422
1319
  this.$l.edge = pb.min(pb.min(this.uv.x, pb.sub(1, this.uv.x)), pb.min(this.uv.y, pb.sub(1, this.uv.y)));
1423
1320
  this.$return(pb.mix(this.screenUV, this.uv, pb.clamp(pb.div(this.edge, REFRACT_EDGE_FADE), 0, 1)));
1424
1321
  });
1425
- // One step of the refracted-ray march.
1426
- //
1427
- // Projects the point `t` meters along the refracted ray to the screen, reads
1428
- // the depth there, and reports the signed gap between the ray and that pixel
1429
- // along the view axis: positive means the ray is still in front of the scene,
1430
- // negative that it has passed through it. The hit test itself belongs to the
1431
- // march.
1432
- //
1433
1322
  // Declared only in `march` mode: a dead function still forces the depth
1434
- // texture and the projection matrices into the bind group.
1323
+ // texture and projection matrices into the bind group.
1435
1324
  if (march) {
1325
+ // Returns (uv, rayViewZ - sceneViewZ) at `t` meters along the ray.
1326
+ // Positive: ray still in front of the scene. Negative: passed through it.
1436
1327
  pb.func('waterRefractStep', [
1437
1328
  pb.vec3('worldPos'),
1438
1329
  pb.vec3('refractDir'),
@@ -1444,22 +1335,12 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
1444
1335
  ], function() {
1445
1336
  this.$l.uv = this.waterRefractUV(this.screenUV, this.uvBase, pb.add(this.worldPos, pb.mul(this.refractDir, this.t)), this.refractionScale);
1446
1337
  this.$l.linearDepth = ShaderHelper.sampleLinearDepth(this, ShaderHelper.getLinearDepthTexture(this), this.uv, 0);
1447
- // View-space z of whatever that pixel shows. The comparison below is in
1448
- // view space, which the depth convention does not touch.
1449
1338
  this.$l.sceneViewZ = pb.mul(pb.neg(this.linearDepth), this.cameraFar);
1450
- // Ray depth minus scene depth. Sky sits at the far plane, so over empty
1451
- // water the gap stays large and positive and the march simply runs on.
1452
1339
  this.$return(pb.vec3(this.uv, pb.sub(this.rayViewZ, this.sceneViewZ)));
1453
1340
  });
1454
- // March the refracted ray and take the first crossing with the depth buffer.
1455
- //
1456
- // A hit is a step that lands inside a thickness band around the surface -
1457
- // the ray is neither clearly in front of the scene nor already out the far
1458
- // side. That two-sided test is what lets something poking out of the water
1459
- // fall through: its depth is *nearest* the camera, so the gap is negative
1460
- // from the first step and never enters the band. Stopping at the *first*
1461
- // hit keeps a submerged object solid; a later one belongs to whatever is
1462
- // behind it.
1341
+ // First crossing of the refracted ray with the depth buffer. A hit is a
1342
+ // step inside the thickness band or a sign change of the gap; an object
1343
+ // poking out of the water is negative from the first step and never hits.
1463
1344
  pb.func('waterRefractMarch', [
1464
1345
  pb.vec3('worldPos'),
1465
1346
  pb.vec3('refractDir'),
@@ -1482,9 +1363,7 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
1482
1363
  this.$l.s = this.waterRefractStep(this.worldPos, this.refractDir, this.screenUV, this.uvBase, this.rayViewZ, this.cameraFar, this.t);
1483
1364
  this.$l.gap = this.s.z;
1484
1365
  this.$if(pb.or(pb.lessThan(pb.abs(this.gap), REFRACT_MARCH_THICKNESS), pb.lessThan(pb.mul(this.gapPrev, this.gap), 0)), function() {
1485
- // Within the band, or the gap flipped sign this step: the ray is at
1486
- // the surface. Interpolate down to the exact crossing so the sample
1487
- // sits on the object instead of a step behind it.
1366
+ // Interpolate to the exact crossing.
1488
1367
  this.$l.k = pb.div(this.gapPrev, pb.sub(this.gapPrev, this.gap));
1489
1368
  this.hitUV = this.s.xy;
1490
1369
  this.hitPath = pb.mix(this.tPrev, this.t, this.k);
@@ -1494,8 +1373,6 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
1494
1373
  this.gapPrev = this.gap;
1495
1374
  });
1496
1375
  });
1497
- // No hit on any step leaves the straight-through sample at the
1498
- // straight-line path, which is what a ray over empty water should show.
1499
1376
  this.$return(pb.vec3(this.hitUV, this.hitPath));
1500
1377
  });
1501
1378
  }
@@ -1507,44 +1384,30 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
1507
1384
  pb.float('surfaceViewZ'),
1508
1385
  pb.float('straightDepth01'),
1509
1386
  pb.vec3('straightWorldPos'),
1510
- pb.float('straightDist')
1387
+ pb.float('straightDist'),
1388
+ pb.bool('underwater')
1511
1389
  ], function() {
1512
- // Both faces are handled: looking down at the water the ray enters the
1513
- // medium, looking up from inside it the ray leaves. The surface is drawn
1514
- // with cullMode 'none' and the wave normal always points up, so the
1515
- // normal is faced back towards the eye and the ratio picked to match.
1516
- // Taken from `frontFacing`, since a flipped wave normal cannot tell an
1517
- // underside from an eye below the surface.
1518
- this.$l.underwater = pb.not(this.$builtins.frontFacing);
1519
1390
  this.$l.faceNormal = this.$choice(this.underwater, pb.neg(this.normal), this.normal);
1520
1391
  this.$l.eta = this.$choice(this.underwater, pb.float(WATER_TO_AIR_ETA), pb.float(AIR_TO_WATER_ETA));
1521
- // The unrefracted hit: the view line through the surface to whatever is
1522
- // behind it, which is what keeps the sample tracking the object rather
1523
- // than wandering across a silhouette.
1524
1392
  this.$l.uvBase = this.waterRefractProjectUV(this.worldPos);
1525
- // Sky behind the water (depth at the far plane) has no surface to refract
1526
- // towards, so the unrefracted sample is the correct one.
1393
+ // Sky behind the water has no bottom. Its point lies on the far plane,
1394
+ // so the straight distance falls to 0 where the surface crosses that
1395
+ // plane and the water there would show the sky unabsorbed.
1527
1396
  this.$if(pb.greaterThanEqual(this.straightDepth01, SCENE_SKY_DEPTH01), function() {
1528
- this.$return(pb.vec3(this.screenUV, this.straightDist));
1397
+ this.$return(pb.vec3(this.screenUV, SKY_MEDIUM_PATH));
1529
1398
  });
1530
1399
  this.$l.waterCrossDir = pb.normalize(pb.sub(this.straightWorldPos, this.worldPos));
1531
- // Refract the view ray twice - once through the wave normal, once through
1532
- // a flat surface of the same facing - and use only the difference, added
1533
- // to the view line. The result is the view line plus a wave-normal
1534
- // perturbation, so on calm water the sample stays put and only the wave
1535
- // tilt moves it. The flat normal carries the same facing flip as the wave
1536
- // one so the two cancel exactly on calm water from either side.
1400
+ // refractDir = view line + (refract by wave normal - refract by flat
1401
+ // normal), so calm water samples exactly the straight hit. The flat
1402
+ // normal carries the same facing flip so the two cancel from either side.
1537
1403
  this.$l.refractWave = pb.refract(this.eyeVecNorm, this.faceNormal, this.eta);
1538
1404
  this.$l.refractFlatNormal = pb.vec3(0, pb.sign(this.faceNormal.y), 0);
1539
1405
  this.$l.refractFlat = pb.refract(this.eyeVecNorm, this.refractFlatNormal, this.eta);
1540
- // Total internal reflection - only reachable from under the surface -
1541
- // leaves refract returning zero, so degenerate to the view line.
1406
+ // Total internal reflection makes refract return zero.
1542
1407
  this.$l.refractDir = pb.add(this.waterCrossDir, pb.sub(this.refractWave, this.refractFlat));
1543
1408
  this.$if(pb.lessThan(pb.length(pb.sub(this.refractWave, this.refractFlat)), 1e-4), function() {
1544
1409
  this.refractDir = this.waterCrossDir;
1545
1410
  });
1546
- // A degenerate view line would push the direction below; clamp it to stay
1547
- // on the line if the difference vanished.
1548
1411
  this.$if(pb.lessThan(pb.dot(this.refractDir, this.refractDir), 1e-6), function() {
1549
1412
  this.refractDir = this.waterCrossDir;
1550
1413
  });
@@ -1552,39 +1415,20 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
1552
1415
  this.$l.refractUV = this.screenUV;
1553
1416
  this.$l.refractPath = this.straightDist;
1554
1417
  if (!march) {
1555
- // Cheap mode: step a fixed distance along the refracted direction and
1556
- // sample where that projects, with no search. `refractDir` is the view
1557
- // line plus a wave-normal perturbation, so with no waves this lands on
1558
- // the point behind the surface.
1559
- //
1560
- // The step length is a *fixed* depth scale, deliberately not the
1561
- // distance to what is behind the water: that distance jumps across an
1562
- // object's silhouette, and the larger offset outside the outline lands
1563
- // back on the object and paints a second copy of it. The price is that
1564
- // shallow and deep water refract by the same amount.
1565
- //
1566
- // The edge fade inside waterRefractUV still applies, so the sample
1567
- // cannot walk off screen.
1418
+ // Fixed step, not the scene distance: that jumps across silhouettes
1419
+ // and paints a second copy of the object.
1568
1420
  this.refractUV = this.waterRefractUV(this.screenUV, this.uvBase, pb.add(this.worldPos, pb.mul(this.refractDir, this.cheapRefractionDepth)), this.refractionScale);
1569
1421
  this.$return(pb.vec3(this.refractUV, this.refractPath));
1570
1422
  } else {
1571
1423
  this.$l.refractDirView = pb.mul(ShaderHelper.getViewMatrix(this), pb.vec4(this.refractDir, 0)).xyz;
1572
- // Away from the camera the ray must travel, so a tangent one is clamped
1573
- // rather than allowed to shoot off, and the straight-line distance bounds
1574
- // the refracted path to a sane multiple of itself.
1575
1424
  this.$l.refractStepZ = pb.min(this.refractDirView.z, -1e-4);
1576
1425
  this.$l.maxPath = pb.mul(this.straightDist, REFRACT_MAX_PATH_RATIO);
1577
1426
  this.$l.marchResult = this.waterRefractMarch(this.worldPos, this.refractDir, this.screenUV, this.uvBase, this.surfaceViewZ, this.refractStepZ, ShaderHelper.getCameraParams(this).y, this.maxPath, this.straightDist);
1578
1427
  this.refractUV = this.marchResult.xy;
1579
1428
  this.refractPath = pb.clamp(this.marchResult.z, 0, this.maxPath);
1580
- // Something sticking out of the water may occlude the refracted sample
1581
- // even though the march kept going. Reconstruct the scene point at the
1582
- // refracted UV and, if it sits above the surface, drop back to the
1583
- // straight-through sample rather than refracting the object.
1584
- this.$l.sceneHit = ShaderHelper.samplePositionFromDepth(this, ShaderHelper.getLinearDepthTexture(this), this.refractUV, ShaderHelper.getInvViewProjectionMatrix(this), ShaderHelper.getCameraParams(this).xy);
1585
- // Only a *finite* scene point above the surface is an object poking
1586
- // through - the sky's reconstructed point also sits high, but there is
1587
- // nothing to reject then.
1429
+ // A finite scene point above the surface at the refracted UV is an
1430
+ // object poking through: fall back to the straight sample.
1431
+ this.$l.sceneHit = ShaderHelper.samplePositionFromDepth(this, ShaderHelper.getLinearDepthTexture(this), this.refractUV, ShaderHelper.getJitteredInvVPMatrix(this), ShaderHelper.getCameraParams(this).xy);
1588
1432
  this.$if(pb.and(pb.lessThan(this.sceneHit.w, 0.999), pb.greaterThan(this.sceneHit.y, this.worldPos.y)), function() {
1589
1433
  this.refractUV = this.screenUV;
1590
1434
  this.refractPath = this.straightDist;
@@ -1592,46 +1436,32 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
1592
1436
  this.$return(pb.vec3(this.refractUV, this.refractPath));
1593
1437
  }
1594
1438
  });
1595
- return scope.waterRefraction(worldPos, normal, eyeVecNorm, screenUV, surfaceViewZ, straightDepth01, straightWorldPos, straightDist);
1596
- }
1597
- /**
1598
- * Sunlight scattered out of the water column towards the eye.
1599
- *
1600
- * Closed-form single scattering, not a ray march. The medium is homogeneous
1601
- * and the two paths through it are straight lines of fixed slope, so the
1602
- * integral along the view ray has an elementary solution and costs one `exp`.
1603
- *
1604
- * Take `t` as distance along the refracted view ray from the surface, `d` as
1605
- * its total length in the water, and `vy`/`sy` as how fast the view and sun
1606
- * rays descend. A scattering event at `t` sits `t * vy` below the surface, so
1607
- * the sun reached it over `t * vy / sy` and the scattered light returns over
1608
- * `t`. Writing `r = vy / sy`:
1609
- *
1610
- * ```
1611
- * S = integral(0..d) sigma_s * p * E * exp(-sigma_t * t * (1 + r)) dt
1612
- * = albedo * p * E * (1 - exp(-sigma_t * (1 + r) * d)) / (1 + r)
1613
- * ```
1614
- *
1615
- * `sigma_t` cancels out of everything but the exponent, which leaves the
1616
- * result finite for an unbounded column: looking straight down at deep water
1617
- * under an overhead sun gives `albedo * p * E / 2`, the textbook value.
1618
- *
1619
- * `E` is the sun's irradiance perpendicular to its own beam; there is no `NoL`
1620
- * here, because the geometry the cosine would describe is already in `r`. Both
1621
- * directions are refracted through a flat surface first.
1622
- *
1623
- * Only the entry Fresnel is applied here. The exit transmission is common to
1624
- * every term leaving the medium and is applied once by the caller.
1625
- *
1626
- * Assumes the eye is above the surface; the caller gates on that.
1627
- *
1628
- * @param scope - Current shader scope.
1629
- * @param lightEnergy - Sun irradiance perpendicular to the beam, after shadowing.
1630
- * @param lightDir - Unit vector from the surface towards the light.
1631
- * @param eyeVecNorm - Unit vector from the camera towards the surface.
1632
- * @param NoL - Cosine of the sun's incidence on the wave normal.
1633
- * @param depth - Refracted path length through the medium, in meters.
1634
- * @returns Radiance scattered towards the eye, before the exit Fresnel.
1439
+ return scope.waterRefraction(worldPos, normal, eyeVecNorm, screenUV, surfaceViewZ, straightDepth01, straightWorldPos, straightDist, underwater);
1440
+ }
1441
+ /**
1442
+ * Sunlight scattered out of the water column towards the eye. Closed-form
1443
+ * single scattering in a homogeneous medium.
1444
+ *
1445
+ * With `t` the distance along the refracted view ray, `d` its length in the
1446
+ * water, `vy`/`sy` the descent rates of the view and sun rays, and
1447
+ * `r = vy / sy`:
1448
+ *
1449
+ * ```
1450
+ * S = integral(0..d) sigma_s * p * E * exp(-sigma_t * t * (1 + r)) dt
1451
+ * = albedo * p * E * (1 - exp(-sigma_t * (1 + r) * d)) / (1 + r)
1452
+ * ```
1453
+ *
1454
+ * No `NoL` factor: that geometry is already in `r`. Only the entry Fresnel is
1455
+ * applied; the exit transmission is applied once by the caller. Assumes the
1456
+ * eye is above the surface.
1457
+ *
1458
+ * @param scope - Current shader scope.
1459
+ * @param lightEnergy - Sun irradiance perpendicular to the beam, after shadowing.
1460
+ * @param lightDir - Unit vector from the surface towards the light.
1461
+ * @param eyeVecNorm - Unit vector from the camera towards the surface.
1462
+ * @param NoL - Cosine of the sun's incidence on the wave normal.
1463
+ * @param depth - Refracted path length through the medium, in meters.
1464
+ * @returns Radiance scattered towards the eye, before the exit Fresnel.
1635
1465
  */ waterSunScattering(scope, lightEnergy, lightDir, eyeVecNorm, NoL, depth, foam) {
1636
1466
  const pb = scope.$builder;
1637
1467
  pb.func('waterSunScattering', [
@@ -1643,117 +1473,85 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
1643
1473
  pb.float('foam')
1644
1474
  ], function() {
1645
1475
  this.$l.up = pb.vec3(0, 1, 0);
1646
- // Both directions as they travel inside the water. The sun's incident
1647
- // direction is where it travels to, the opposite of `lightDir`.
1476
+ // Directions inside the water. The sun travels opposite to `lightDir`.
1648
1477
  this.$l.Lw = pb.refract(pb.neg(this.lightDir), this.up, AIR_TO_WATER_ETA);
1649
1478
  this.$l.Vw = pb.refract(this.eyeVecNorm, this.up, AIR_TO_WATER_ETA);
1650
- // How fast each descends. The sun is floored rather than allowed to
1651
- // reach zero: a sun on the horizon lights the column over an unbounded
1652
- // path, which single scattering cannot represent.
1653
1479
  this.$l.sy = pb.max(pb.neg(this.Lw.y), MIN_SUN_SLOPE);
1654
1480
  this.$l.vy = pb.max(pb.neg(this.Vw.y), 1e-3);
1655
1481
  this.$l.r = pb.div(this.vy, this.sy);
1656
1482
  this.$l.rr = pb.add(1, this.r);
1657
- // Angle between the sun's travel and the direction the light has to
1658
- // leave in to reach the eye, which is back along the view ray.
1659
1483
  this.$l.cosTheta = pb.neg(pb.dot(this.Lw, this.Vw));
1660
1484
  this.$l.hg = waterScatterPhase(this, this.cosTheta, this.sunScatterParams.y);
1661
- // Multiple scattering washes the lobe out: an optically thick column has
1662
- // scattered the light enough times that the direction it entered by no
1663
- // longer matters. Weighted on luminance and shared across channels,
1664
- // because the phase does not depend on wavelength - only how far the
1665
- // light got does.
1485
+ // Blend towards isotropic with optical thickness (multiple scattering).
1486
+ // Luminance-weighted: the phase is wavelength independent.
1666
1487
  this.$l.lumWeights = pb.vec3(0.2126, 0.7152, 0.0722);
1667
1488
  this.$l.extLum = pb.dot(this.mediumExtinction, this.lumWeights);
1668
1489
  this.$l.albedoLum = pb.dot(this.mediumAlbedo, this.lumWeights);
1669
1490
  this.$l.thickness = pb.mul(this.albedoLum, pb.sub(1, pb.exp(pb.neg(pb.mul(this.extLum, this.depth)))));
1670
1491
  this.$l.phase = pb.mix(this.hg, 1 / (4 * Math.PI), pb.smoothStep(0, 0.5, pb.clamp(this.thickness, 0, 1)));
1671
- // The integral itself. Per channel, since sigma_t is.
1672
1492
  this.$l.k = pb.mul(this.mediumExtinction, this.rr);
1673
1493
  this.$l.integral = pb.div(pb.sub(pb.vec3(1), pb.exp(pb.neg(pb.mul(this.k, this.depth)))), this.rr);
1674
- // Entry Fresnel: what the surface let through on the way in. This is
1675
- // also what keeps the term off a backlit crest - at NoL <= 0 no light
1676
- // enters the top face at all, and the subsurface term owns that case.
1494
+ // Entry Fresnel. Zero at NoL <= 0, where the subsurface term takes over.
1495
+ // Foam is a mass of bubbles, not a mirror, so it blends towards full
1496
+ // transmission.
1677
1497
  this.$l.entry = pb.sub(1, pb.add(WATER_F0, pb.mul(1 - WATER_F0, pb.pow(pb.sub(1, this.NoL), 5))));
1678
- // The flat-interface Fresnel goes to zero at grazing incidence, which is
1679
- // right for smooth water and wrong for a fold - a mass of bubbles, not a
1680
- // mirror. Blended towards full transmission by the foam coverage, so a
1681
- // breaking crest lights its own column. Weighted, not replaced: a thin
1682
- // scattering layer still attenuates.
1683
1498
  this.$l.entry = pb.mix(this.entry, pb.float(1), this.foam);
1684
1499
  this.$return(pb.mul(this.mediumAlbedo, this.lightEnergy, this.integral, pb.mul(this.phase, this.entry, this.sunScatterParams.x)));
1685
1500
  });
1686
1501
  return scope.waterSunScattering(lightEnergy, lightDir, eyeVecNorm, NoL, depth, foam);
1687
1502
  }
1688
- waterShading(scope, worldPos, worldNormal, foamFactor) {
1503
+ waterShading(scope, worldPos, worldNormal, foamFactor, crestHeight, restXZ, restXZGrad, foamSample, coverage, sheetThickness, wallFoam, scenePos, groundFoamDensity, groundFoamSample, groundNormal, groundFoamWeight) {
1689
1504
  const pb = scope.$builder;
1690
1505
  const that = this;
1691
- const ramp = this.mediumMode === 'ramp';
1692
- const shoreFoam = this._shoreFoamAvailable(scope);
1693
1506
  const debugOutput = this.debugOutput;
1694
- // Transmittance of the medium over `depth` meters of path.
1507
+ const hasGroundFoam = !!(this._shoreWaveGenerator && this._foamFieldGenerator);
1508
+ // The environment's prefiltered radiance, for the distant sky reflection
1509
+ const envLight = this.needCalculateEnvLight() ? this.drawContext.env?.light.envLight ?? null : null;
1510
+ // Transmittance over `depth` meters of path.
1695
1511
  pb.func('getAbsorption', [
1696
1512
  pb.float('depth')
1697
1513
  ], function() {
1698
- if (ramp) {
1699
- this.$l.c = pb.textureSampleLevel(this.absorptionRampTex, pb.vec2(pb.mul(this.depth, this.depthMulti), 0.5), 0).rgb;
1700
- this.$return(pb.mul(this.c, this.c));
1701
- } else {
1702
- this.$return(pb.exp(pb.neg(pb.mul(this.mediumExtinction, this.depth))));
1703
- }
1514
+ this.$return(pb.exp(pb.neg(pb.mul(this.mediumExtinction, this.depth))));
1704
1515
  });
1705
- // Radiance scattered back out of the medium over `depth` meters of path,
1706
- // as a fraction of the incident irradiance.
1516
+ // Fraction of incident irradiance scattered back over `depth` meters of path.
1707
1517
  pb.func('getScattering', [
1708
1518
  pb.float('depth')
1709
1519
  ], function() {
1710
- if (ramp) {
1711
- this.$l.c = pb.textureSampleLevel(this.scatterRampTex, pb.vec2(pb.mul(this.depth, this.depthMulti), 0.5), 0).rgb;
1712
- this.$return(pb.mul(this.c, this.c));
1713
- } else {
1714
- // Single-scattering: the albedo weighs how much of the extinguished
1715
- // energy comes back rather than being absorbed.
1716
- this.$return(pb.mul(this.mediumAlbedo, pb.sub(pb.vec3(1), pb.exp(pb.neg(pb.mul(this.mediumExtinction, this.depth))))));
1717
- }
1520
+ this.$return(pb.mul(this.mediumAlbedo, pb.sub(pb.vec3(1), pb.exp(pb.neg(pb.mul(this.mediumExtinction, this.depth))))));
1521
+ });
1522
+ // Mean Fresnel over a Gaussian distribution of unresolved slopes of standard
1523
+ // deviation `sigmaV` along the view direction, Bruneton, Neyret & Holzschuch,
1524
+ // "Real-time Realistic Ocean Lighting using Seamless Transitions from Geometry
1525
+ // to BRDF" (Eurographics 2010), as their ocean demo fits it:
1526
+ // (1 - cos)^(5 exp(-2.69 sigma)) / (1 + 22.7 sigma^1.5), Schlick at sigma = 0.
1527
+ pb.func('meanFresnel', [
1528
+ pb.float('cosThetaV'),
1529
+ pb.float('sigmaV')
1530
+ ], function() {
1531
+ this.$return(pb.div(pb.pow(pb.sub(1, this.cosThetaV), pb.mul(5, pb.exp(pb.mul(-2.69, this.sigmaV)))), pb.add(1, pb.mul(22.7, pb.pow(this.sigmaV, 1.5)))));
1718
1532
  });
1719
1533
  pb.func('fresnel', [
1720
1534
  pb.vec3('normal'),
1721
1535
  pb.vec3('eyeVec'),
1722
- pb.bool('underwater')
1536
+ pb.bool('underwater'),
1537
+ pb.float('sigmaV')
1723
1538
  ], function() {
1724
- // The magnitude of the cosine: it changes sign when the view direction
1725
- // crosses the normal, which a folded crest makes happen inside a single
1726
- // wave. Clamping at zero instead drops the reflectance to F0 exactly where
1727
- // the geometry is at its most grazing, giving a black band across the
1728
- // crest.
1539
+ // abs, not clamp: a folded crest flips the sign inside one wave, and a
1540
+ // clamp would drop to F0 exactly at the most grazing geometry.
1729
1541
  this.$l.NoV = pb.clamp(pb.abs(pb.dot(this.normal, this.eyeVec)), 0, 1);
1730
- // Looking up at the underside, the interface behaves quite differently.
1731
- // Past the critical angle - about 48.6 degrees off vertical for n = 1.333 -
1732
- // Snell's law has no solution and nothing gets out at all: the surface is
1733
- // a perfect mirror there, and the circle of sky inside that angle is the
1734
- // Snell window.
1735
1542
  this.$if(this.underwater, function() {
1736
1543
  this.$l.sinT2 = pb.mul(WATER_TO_AIR_ETA * WATER_TO_AIR_ETA, pb.sub(1, pb.mul(this.NoV, this.NoV)));
1737
- // Total internal reflection, and deliberately not scaled by
1738
- // reflectionStrength. That knob exists to trade an above-water
1739
- // reflection away for a view of the sea bed beneath it; from below there
1740
- // is no transmitted ray to reveal, and dialling the mirror down there
1741
- // just bleeds the straight-through sample - the world above the water -
1742
- // across the whole surface outside the window, where physically nothing
1743
- // is visible at all.
1544
+ // Total internal reflection outside the Snell window. Not scaled by
1545
+ // reflectionStrength: there is no transmitted ray to reveal.
1744
1546
  this.$l.f = pb.float(1);
1745
1547
  this.$if(pb.lessThan(this.sinT2, 1), function() {
1746
- // Schlick is written against the angle on the less dense side of the
1747
- // interface, so the transmitted cosine is what goes in rather than the
1748
- // incident one. F0 itself is unchanged: ((n1-n2)/(n1+n2))^2 is
1749
- // symmetric in the two media.
1548
+ // Schlick takes the cosine on the less dense side. F0 is symmetric.
1750
1549
  this.$l.cosT = pb.sqrt(pb.max(pb.sub(1, this.sinT2), 0));
1751
1550
  this.f = pb.clamp(pb.mul(pb.add(WATER_F0, pb.mul(1 - WATER_F0, pb.pow(pb.sub(1, this.cosT), 5))), this.reflectionStrength), 0, 1);
1752
1551
  });
1753
1552
  this.$return(this.f);
1754
1553
  });
1755
- this.$l.f = pb.add(WATER_F0, pb.mul(1 - WATER_F0, pb.pow(pb.sub(1, this.NoV), 5)));
1756
- // Scaling keeps the term in [0,1] for any authored reflectionStrength.
1554
+ this.$l.f = pb.add(WATER_F0, pb.mul(1 - WATER_F0, this.meanFresnel(this.NoV, this.sigmaV)));
1757
1555
  this.$return(pb.clamp(pb.mul(this.f, this.reflectionStrength), 0, 1));
1758
1556
  });
1759
1557
  pb.func('lightSpecular', [
@@ -1782,252 +1580,79 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
1782
1580
  });
1783
1581
  this.$return(this.specular);
1784
1582
  });
1785
- if (shoreFoam) {
1786
- // Three octaves of value noise over the world XZ plane, drifting slowly.
1787
- // World space rather than screen or surface space, so the pattern does not
1788
- // slide over the band as the camera moves.
1789
- pb.func('waterShoreFoamPattern', [
1790
- pb.vec2('xz'),
1791
- pb.float('time')
1792
- ], function() {
1793
- // Drift applied in meters before the frequency, so changing the clump
1794
- // size does not change how fast they travel.
1795
- this.$l.p = pb.mul(pb.add(this.xz, pb.mul(pb.vec2(SHORE_FOAM_DRIFT_X, SHORE_FOAM_DRIFT_Z), this.time)), this.shoreFoamParams.w);
1796
- this.$return(pb.add(pb.mul(valueNoise(this, this.p), SHORE_FOAM_OCTAVE_WEIGHTS[0]), // Offset as well as scaled: octaves sharing a lattice line their
1797
- // features up at the cell corners.
1798
- pb.mul(valueNoise(this, pb.add(pb.mul(this.p, SHORE_FOAM_OCTAVE_RATIO), pb.vec2(13.7, 7.3))), SHORE_FOAM_OCTAVE_WEIGHTS[1]), pb.mul(valueNoise(this, pb.add(pb.mul(this.p, SHORE_FOAM_OCTAVE_RATIO * SHORE_FOAM_OCTAVE_RATIO), pb.vec2(5.1, 29.3))), SHORE_FOAM_OCTAVE_WEIGHTS[2])));
1799
- });
1800
- // Distance from the water surface to the nearest opaque surface anywhere
1801
- // around it, through the Hi-Z pyramid's nearest-depth channel.
1802
- //
1803
- // A *neighbourhood* query rather than a lookup along the view ray. Beside
1804
- // a piling the ray hits its side just under the waterline, so a pixel far
1805
- // from the piling reads as touching it while the closer pixels to its left
1806
- // and right see past it entirely - the geometry those pixels need is not
1807
- // on their ray at all.
1808
- //
1809
- // The pyramid is what makes the neighbourhood affordable: one cell at the
1810
- // right level summarises the whole footprint, so the sample count does not
1811
- // grow with the radius, and the nearest-depth reduction means a cell the
1812
- // query covers cannot hide geometry from it.
1813
- //
1814
- // Reports a distance past the band's reach when the neighbourhood is
1815
- // empty, so the caller's ramp reads it as "outside the band" without a
1816
- // separate miss flag.
1817
- pb.func('waterShoreProximity', [
1818
- pb.vec3('viewPos'),
1819
- pb.vec2('screenUV'),
1820
- pb.float('radius')
1821
- ], function() {
1822
- this.$l.renderSize = ShaderHelper.getRenderSize(this);
1823
- this.$l.projMatrix = ShaderHelper.getProjectionMatrix(this);
1824
- // Screen radius, in pixels, of a sphere of `radius` metres at this
1825
- // depth. proj[0].x is the horizontal projection scale, and the NDC
1826
- // half-extent maps onto half the render width.
1827
- this.$l.viewDist = pb.max(pb.neg(this.viewPos.z), 1e-4);
1828
- this.$l.pixelRadius = pb.mul(pb.div(pb.mul(this.radius, this.projMatrix[0].x), this.viewDist), 0.5, this.renderSize.x);
1829
- // Level whose texel spans the query footprint divided by the tap
1830
- // spread, so the 3x3 grid covers roughly the whole sphere. Floored to
1831
- // an integer: the pyramid is sampled with a nearest mip filter, and a
1832
- // fractional level would show up as a seam across the band.
1833
- this.$l.level = pb.clamp(pb.floor(pb.log2(pb.max(pb.div(this.pixelRadius, SHORE_QUERY_TAP_SPREAD), 1))), 0, pb.sub(pb.float(ShaderHelper.getHiZDepthTextureMipLevelCount(this)), 1));
1834
- this.$l.mipSize = pb.max(pb.floor(pb.div(this.renderSize, pb.exp2(this.level))), pb.vec2(1));
1835
- this.$l.texelStep = pb.div(pb.vec2(1), this.mipSize);
1836
- // Every cell in a 4x4 window contributes, weighted by a tent reaching
1837
- // zero at the window's edge, and each is read as a point dithered
1838
- // somewhere inside it.
1839
- //
1840
- // The tent makes the window's own stepping invisible: the column of
1841
- // cells leaving and the one arriving both have zero weight there. The
1842
- // pyramid is bound with a nearest sampler - SSR's traversal needs exact
1843
- // cell values - so this smoothing has to happen here.
1844
- //
1845
- // The dither keeps the contours round. A cell is a box, and the
1846
- // distance to a box has square contours; at these levels a cell is most
1847
- // of the radius across, so what is left is a grid of squares. Picking a
1848
- // point inside the cell is what the cell's own statement licenses.
1849
- //
1850
- // Hashed from the pixel *and* the frame counter, and applied to the
1851
- // interpretation of the samples rather than to which samples are
1852
- // taken: the fetches stay identical frame to frame while the guess
1853
- // moves, which is what lets TAA average the residual grain away.
1854
- this.$l.dither = pb.mul(pb.float(ShaderHelper.getFramestamp(this)), SHORE_QUERY_DITHER_STRIDE);
1855
- this.$l.gridCoord = pb.sub(pb.mul(this.screenUV, this.mipSize), pb.vec2(0.5));
1856
- this.$l.gridBase = pb.floor(this.gridCoord);
1857
- this.$l.gridFrac = pb.sub(this.gridCoord, this.gridBase);
1858
- // Softness of the minimum below, and the distance a neighbourhood with
1859
- // nothing in it resolves to. Both scale with the radius, so the query
1860
- // behaves the same whatever the band is set to.
1861
- this.$l.softK = pb.mul(this.radius, SHORE_QUERY_SOFTMIN);
1862
- this.$l.dMax = pb.mul(this.radius, SHORE_QUERY_MISS_REACH);
1863
- this.$l.esum = pb.float(0);
1864
- this.$l.wsum = pb.float(0);
1865
- for(let gy = 0; gy < 4; gy++){
1866
- for(let gx = 0; gx < 4; gx++){
1867
- this.$l[`uv${gx}${gy}`] = pb.mul(pb.add(this.gridBase, pb.vec2(gx - 1 + 0.5, gy - 1 + 0.5)), this.texelStep);
1868
- this.$l[`hiz${gx}${gy}`] = pb.textureSampleLevel(ShaderHelper.getHiZDepthTexture(this), this[`uv${gx}${gy}`], this.level);
1869
- this.$l[`z${gx}${gy}`] = ShaderHelper.nonLinearDepthToLinear(this, this[`hiz${gx}${gy}`].g);
1870
- this.$l[`zFar${gx}${gy}`] = ShaderHelper.nonLinearDepthToLinear(this, this[`hiz${gx}${gy}`].r);
1871
- // A cell says "some surface lies inside this screen rectangle, at
1872
- // a depth between near and far" - a frustum block. Collapsing that
1873
- // to the point on the centre ray at the near depth puts a cell
1874
- // straddling a silhouette beside the object in empty space, nearer
1875
- // to the water than the object itself.
1876
- //
1877
- // Laterally the point is dithered across the cell instead, which
1878
- // invents nothing: any spot in the cell is somewhere the surface
1879
- // could be. In depth it is clamped, because the reduction
1880
- // guarantees a surface on the near face, and the span is clipped by
1881
- // SHORE_QUERY_CELL_DEPTH_SPAN.
1882
- this.$l[`cellW${gx}${gy}`] = pb.max(pb.div(pb.mul(this.texelStep.x, 2, this[`z${gx}${gy}`]), this.projMatrix[0].x), 1e-6);
1883
- this.$l[`zBack${gx}${gy}`] = pb.min(this[`zFar${gx}${gy}`], pb.add(this[`z${gx}${gy}`], pb.mul(this[`cellW${gx}${gy}`], SHORE_QUERY_CELL_DEPTH_SPAN)));
1884
- // How far outside the block's depth range the water pixel sits.
1885
- // Zero while it is between the two faces.
1886
- this.$l[`zClamp${gx}${gy}`] = pb.clamp(pb.neg(this.viewPos.z), this[`z${gx}${gy}`], this[`zBack${gx}${gy}`]);
1887
- this.$l[`dz${gx}${gy}`] = pb.add(this.viewPos.z, this[`zClamp${gx}${gy}`]);
1888
- // Seeded per cell as well as per pixel: one offset shared by all
1889
- // sixteen would move the whole set together.
1890
- this.$l[`jit${gx}${gy}`] = pb.sub(pb.vec2(interleavedGradientNoise(this, pb.add(this.$builtins.fragCoord.xy, pb.vec2(pb.add(this.dither, gx * 7.13 + 0.5), pb.add(this.dither, gy * 3.71 + 0.5)))), interleavedGradientNoise(this, pb.add(this.$builtins.fragCoord.xy, pb.vec2(pb.add(this.dither, gy * 3.71 + 37), pb.add(this.dither, gx * 7.13 + 17))))), pb.vec2(0.5));
1891
- this.$l[`guessNDC${gx}${gy}`] = pb.sub(pb.mul(pb.add(this[`uv${gx}${gy}`], pb.mul(this[`jit${gx}${gy}`], this.texelStep)), 2), pb.vec2(1));
1892
- this.$l[`guess${gx}${gy}`] = pb.div(pb.mul(this[`guessNDC${gx}${gy}`], this[`zClamp${gx}${gy}`]), pb.vec2(this.projMatrix[0].x, this.projMatrix[1].y));
1893
- this.$l[`dxy${gx}${gy}`] = pb.sub(this.viewPos.xy, this[`guess${gx}${gy}`]);
1894
- this.$l[`d${gx}${gy}`] = pb.length(pb.vec3(this[`dxy${gx}${gy}`], this[`dz${gx}${gy}`]));
1895
- // Tent weight, from the cell's offset to the pixel in cell units.
1896
- // Zero at two cells out, which is the edge of the 4x4 window, so a
1897
- // cell entering or leaving the window does so from nothing.
1898
- this.$l[`w${gx}${gy}`] = pb.mul(pb.max(pb.sub(1, pb.abs(pb.div(pb.sub(pb.float(gx - 1), this.gridFrac.x), 2))), 0), pb.max(pb.sub(1, pb.abs(pb.div(pb.sub(pb.float(gy - 1), this.gridFrac.y), 2))), 0));
1899
- this.wsum = pb.add(this.wsum, this[`w${gx}${gy}`]);
1900
- this.esum = pb.add(this.esum, pb.mul(this[`w${gx}${gy}`], pb.exp(pb.neg(pb.div(pb.min(this[`d${gx}${gy}`], this.dMax), this.softK)))));
1901
- }
1902
- }
1903
- // Weighted soft minimum, not a hard one. A hard `min` lets one cell
1904
- // decide the answer outright, so the band comes out in squares that
1905
- // step on cell boundaries. Averaging lets a cell losing its claim cost
1906
- // only its share and lets the dither average down. It still reads as a
1907
- // distance: below a few times `softK` it tracks the true minimum
1908
- // closely.
1909
- this.$return(pb.mul(pb.neg(this.softK), pb.log(pb.div(this.esum, this.wsum))));
1910
- });
1911
- // Coverage of the foam that collects where the water meets a surface, and
1912
- // how far that surface is, as (coverage, distance).
1913
- pb.func('waterShoreFoam', [
1914
- pb.vec3('worldPos'),
1915
- pb.vec3('viewPos'),
1916
- pb.vec2('screenUV')
1917
- ], function() {
1918
- this.$l.time = ShaderHelper.getElapsedTime(this);
1919
- // Queried at the band's widest reach rather than its current one, so the
1920
- // run-up below moves the edge without moving the mip level under it -
1921
- // which would step the whole band's resolution once a cycle.
1922
- this.$l.surfaceDist = this.waterShoreProximity(this.viewPos, this.screenUV, this.shoreFoamWashParams.w);
1923
- // Where the band currently reaches out to, modulated over time by the
1924
- // run-up. The phase offset comes from a noise field over the world plane
1925
- // rather than from a direction: the query does not know where the shore
1926
- // runs, and a guessed axis reads as a diagonal swell crossing the beach.
1927
- this.$l.washOffset = valueNoise(this, pb.mul(this.worldPos.xz, this.shoreFoamWashParams.z));
1928
- this.$l.wash = pb.sin(pb.mul(pb.add(pb.mul(this.time, this.shoreFoamWashParams.y), this.washOffset), 2 * Math.PI));
1929
- this.$l.edge = pb.max(pb.mul(this.shoreFoamParams.y, pb.add(1, pb.mul(this.shoreFoamWashParams.x, this.wash))), 0);
1930
- this.$l.soft = pb.max(pb.mul(this.edge, SHORE_FOAM_EDGE_SOFTNESS), 1e-4);
1931
- // The gradient runs *up to* the reach, not across it: `edge` is where the
1932
- // band ends, so coverage must be zero there. Straddling `edge` would put
1933
- // a total miss at the middle of the ramp, which is half coverage over the
1934
- // entire open sea.
1935
- this.$l.ramp = pb.sub(1, pb.smoothStep(pb.sub(this.edge, this.soft), this.edge, this.surfaceDist));
1936
- // Open water, which is almost every water pixel in almost every frame,
1937
- // is done here and does not pay for the pattern.
1938
- this.$if(pb.lessThanEqual(this.ramp, 0), function() {
1939
- this.$return(pb.vec2(0, this.surfaceDist));
1940
- });
1941
- this.$l.pattern = this.waterShoreFoamPattern(this.worldPos.xz, this.time);
1942
- this.$l.coverage = pb.clamp(pb.div(pb.sub(this.ramp, pb.mul(this.pattern, SHORE_FOAM_NOISE_BITE)), 1 - SHORE_FOAM_NOISE_BITE), 0, 1);
1943
- this.$return(pb.vec2(pb.clamp(pb.mul(pb.pow(this.coverage, this.shoreFoamParams.z), this.shoreFoamParams.x), 0, 1), this.surfaceDist));
1944
- });
1945
- }
1583
+ // White foam revealed from a foam texture sample (value, slopes along x
1584
+ // and z) by a density, Crest's feathered black point; as (white foam, its
1585
+ // slope along x, along z).
1586
+ pb.func('waterWhiteFoam', [
1587
+ pb.float('density'),
1588
+ pb.vec3('tex'),
1589
+ pb.float('feather')
1590
+ ], function() {
1591
+ this.$l.t = pb.clamp(pb.div(pb.sub(this.tex.x, pb.sub(1, pb.clamp(this.density, 0, 1))), this.feather), 0, 1);
1592
+ this.$return(pb.vec3(pb.mul(this.t, this.t, pb.sub(3, pb.mul(this.t, 2))), pb.mul(this.tex.yz, pb.div(pb.mul(6, this.t, pb.sub(1, this.t)), this.feather))));
1593
+ });
1946
1594
  pb.func('waterShading', [
1947
1595
  pb.vec3('worldPos'),
1948
1596
  pb.vec3('worldNormal'),
1949
- pb.float('foamFactor')
1597
+ pb.float('foamFactor'),
1598
+ pb.float('crestHeight'),
1599
+ pb.vec2('restXZ'),
1600
+ pb.vec4('restXZGrad'),
1601
+ pb.vec3('foamSample'),
1602
+ pb.float('coverage'),
1603
+ pb.float('sheetThickness'),
1604
+ pb.float('wallFoam'),
1605
+ pb.vec4('scenePos'),
1606
+ pb.float('groundFoamDensity'),
1607
+ pb.vec3('groundFoamSample'),
1608
+ pb.vec3('groundNormal'),
1609
+ pb.float('groundFoamWeight')
1950
1610
  ], function() {
1951
1611
  this.$l.screenUV = pb.div(pb.vec2(this.$builtins.fragCoord.xy), ShaderHelper.getRenderSize(this));
1952
1612
  this.$l.dist = pb.length(pb.sub(this.worldPos, ShaderHelper.getCameraPosition(this)));
1953
- this.$l.normalScale = pb.clamp(pb.div(100, this.dist), 0, 1);
1954
- this.$l.normal = pb.normalize(pb.mul(this.worldNormal, pb.vec3(this.normalScale, 1, this.normalScale)));
1955
- this.$l.roughness = that.waterRoughness(this, this.worldPos);
1956
- this.$l.wPos = ShaderHelper.samplePositionFromDepth(this, ShaderHelper.getLinearDepthTexture(this), this.screenUV, ShaderHelper.getInvViewProjectionMatrix(this), ShaderHelper.getCameraParams(this).xy);
1613
+ const slopeVar = that.unresolvedSlopeVariance(this, this.restXZGrad);
1614
+ if (slopeVar) {
1615
+ // The generator has already faded the slopes too fine for the pixel
1616
+ this.$l.slopeVar = slopeVar;
1617
+ this.$l.normal = pb.normalize(this.worldNormal);
1618
+ } else {
1619
+ this.$l.normalScale = pb.clamp(pb.div(100, this.dist), 0, 1);
1620
+ this.$l.normal = pb.normalize(pb.mul(this.worldNormal, pb.vec3(this.normalScale, 1, this.normalScale)));
1621
+ }
1622
+ this.$l.roughness = that.waterRoughness(this, this.worldPos, this.restXZGrad);
1623
+ this.$l.wPos = this.scenePos;
1957
1624
  this.$l.eyeVec = pb.sub(this.worldPos.xyz, ShaderHelper.getCameraPosition(this));
1958
1625
  this.$l.eyeVecNorm = pb.normalize(this.eyeVec);
1959
- // The normal the mirror-like terms use - Fresnel, the reflection vector,
1960
- // the specular lobe - turned towards the eye where the surface has
1961
- // folded far enough to show its underside.
1962
- //
1963
- // Flipped on the sign of the cosine against the view direction, not on
1964
- // `frontFacing`: the rasteriser's answer changes discontinuously along
1965
- // the triangle edge where the fold happens, so the flipped normal jumps
1966
- // by 2*N there and every term built on it steps with it. The cosine
1967
- // crosses zero continuously.
1968
- //
1969
- // `eyeVecNorm` runs from the camera to the surface, so a face turned
1970
- // towards the eye has a *negative* dot against it and the flip is the
1971
- // positive case.
1626
+ // Mirror-term normal, turned towards the eye on a folded underside.
1627
+ // Flipped on the cosine, not on `frontFacing`, which is discontinuous
1628
+ // along the fold's triangle edge. `eyeVecNorm` points camera -> surface,
1629
+ // so the flip case is the positive dot.
1972
1630
  this.$l.shadingNormal = this.$choice(pb.greaterThan(pb.dot(this.normal, this.eyeVecNorm), 0), pb.neg(this.normal), this.normal);
1973
- // The normal the diffuse terms use, which is not the wave normal.
1974
- //
1975
- // Where a choppy displacement folds the surface the cross-product normal
1976
- // is genuinely close to horizontal. That is the right normal for the
1977
- // mirror - the specular lobe and the refraction go by the face's true
1978
- // orientation - and the wrong one for anything lit diffusely: the sun is
1979
- // nearly perpendicular to a folded wall, so a diffuse term built on it
1980
- // loses the sun completely.
1981
- //
1982
- // Built on the eye-facing normal and then mixed towards up, so the two
1983
- // corrections compose. The weight rises with |normal.y|: at |y| = 1 the
1984
- // wave normal is kept exactly, and as the face tips over the up vector
1985
- // takes it back. A smoothstep rather than |normal.y| itself, which would
1986
- // stay dominated by the wall normal exactly where it is least wanted.
1987
- this.$l.diffuseWeight = pb.smoothStep(0, 0.5, pb.abs(this.normal.y));
1988
- this.$l.diffuseNormal = pb.normalize(pb.mix(pb.vec3(0, 1, 0), this.shadingNormal, this.diffuseWeight));
1989
- // Which face of the surface the camera is on. Taken from the rasteriser,
1990
- // not from the wave normal: the choppy displacement folds the sheet, so
1991
- // the pixels showing its underside also carry an upward normal, and a
1992
- // test built on the normal would put every term on the wrong side of the
1993
- // interface there.
1994
- this.$l.frontFace = this.$builtins.frontFacing;
1995
- this.$l.backFace = pb.not(this.frontFace);
1996
- this.$l.underwaterEye = this.backFace;
1631
+ // Diffuse-term normal, from the unflipped wave normal (the eye-facing
1632
+ // flip would point it into the lower hemisphere on any steep back
1633
+ // slope), mixed towards up as normal.y falls.
1634
+ this.$l.diffuseWeight = pb.smoothStep(0, 0.5, this.normal.y);
1635
+ this.$l.diffuseNormal = pb.normalize(pb.mix(pb.vec3(0, 1, 0), this.normal, this.diffuseWeight));
1636
+ // Eye below the rest plane, not `!frontFacing`: a choppy fold (J < 0)
1637
+ // flips the winding and would read as seen from below.
1638
+ this.$l.restLevel = pb.sub(this.worldPos.y, this.crestHeight);
1639
+ this.$l.underwaterEye = pb.or(pb.lessThan(this.eyeSide, 0), pb.and(pb.equal(this.eyeSide, 0), pb.lessThan(ShaderHelper.getCameraPosition(this).y, this.restLevel)));
1997
1640
  this.$l.depth = pb.length(pb.sub(this.wPos.xyz, this.worldPos));
1998
1641
  this.$l.viewPos = pb.mul(ShaderHelper.getViewMatrix(this), pb.vec4(this.worldPos, 1)).xyz;
1999
1642
  this.incidentVec = pb.normalize(pb.sub(this.worldPos, ShaderHelper.getCameraPosition(this)));
2000
1643
  this.reflectVecW = pb.reflect(this.incidentVec, this.shadingNormal);
2001
1644
  this.$l.reflectance = pb.vec3();
2002
1645
  this.$l.hitInfo = pb.vec4(0);
2003
- // Which way a reflection has to go to have anything to hit. From above
2004
- // that is upwards, off the surface into the sky; from below it is
2005
- // downwards, back into the water. Testing for "up" regardless meant a
2006
- // submerged pixel never traced at all and fell through to the sky bake,
2007
- // which is the one thing that cannot be behind a surface seen from
2008
- // underneath.
1646
+ // Trace only rays leaving the surface: up from above, down from below.
2009
1647
  this.$if(this.$choice(this.underwaterEye, pb.lessThan(this.reflectVecW.y, 0), pb.greaterThan(this.reflectVecW.y, 0)), function() {
2010
1648
  this.reflectVec = pb.mul(ShaderHelper.getViewMatrix(this), pb.vec4(this.reflectVecW, 0)).xyz;
2011
1649
  this.hitInfo = ShaderHelper.getHiZDepthTexture(this) ? screenSpaceRayTracing_HiZ(this, this.viewPos, this.reflectVec, ShaderHelper.getViewMatrix(this), ShaderHelper.getProjectionMatrix(this), ShaderHelper.getInvProjectionMatrix(this), ShaderHelper.getCameraParams(this).xy, pb.int(ShaderHelper.getHiZDepthTextureMipLevelCount(this)), this.ssrParams.y, this.ssrParams.x, this.ssrParams.z, pb.vec4(ShaderHelper.getRenderSize(this), ShaderHelper.getHiZDepthTextureSize(this)), ShaderHelper.getHiZDepthTexture(this)) : screenSpaceRayTracing_Linear2D(this, this.viewPos, this.reflectVec, ShaderHelper.getViewMatrix(this), ShaderHelper.getProjectionMatrix(this), ShaderHelper.getInvProjectionMatrix(this), ShaderHelper.getCameraParams(this).xy, this.ssrParams.x, this.ssrParams.y, this.ssrParams.z, this.ssrParams.w, pb.vec4(ShaderHelper.getRenderSize(this), ShaderHelper.getLinearDepthTextureSize(this)), ShaderHelper.getLinearDepthTexture(this));
2012
1650
  });
2013
1651
  this.$l.refl = pb.reflect(pb.normalize(pb.sub(this.worldPos, ShaderHelper.getCameraPosition(this))), this.shadingNormal);
2014
- // What the mirror shows where the screen-space trace found nothing.
2015
- //
2016
- // From above that is the sky. Rays reflecting downwards, and grazing
2017
- // ones, land on the bake's lower hemisphere, which the atmosphere
2018
- // renders nearly black, so the far ocean reads as a dark band. Flooring
2019
- // the y and re-normalising pins those directions just above the horizon,
2020
- // which is roughly what a grazing reflection does see; a reflection
2021
- // already pointing into the sky is untouched. A floor rather than a soft
2022
- // lift, so only the degenerate directions are nudged.
2023
- //
2024
- // From below there is no sky in that direction at all - the ray is
2025
- // heading back down into the water - and what it eventually reaches is
2026
- // the water itself. That is the medium's asymptotic colour, the same
2027
- // limit the underwater pass converges to as the path length grows:
2028
- // albedo * (1 - exp(-sigma_t * d)) * ambient with the exponential gone.
2029
- // Sampling the sky bake there instead put a bright sky outside the Snell
2030
- // window, which is exactly where nothing above the surface is visible.
1652
+ // Fallback where the trace misses. From above: the sky, with the
1653
+ // reflection y floored so grazing rays do not read the bake's black
1654
+ // lower hemisphere. From below: the medium's asymptotic colour
1655
+ // albedo * ambient, the d -> inf limit of the underwater pass.
2031
1656
  this.$l.reflMiss = pb.vec3();
2032
1657
  this.$if(this.underwaterEye, function() {
2033
1658
  this.$l.downwelling = ShaderHelper.sampleBakedSkyPreExposed(this, pb.vec3(0, 1, 0));
@@ -2035,177 +1660,258 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
2035
1660
  }).$else(function() {
2036
1661
  this.refl.y = pb.max(this.refl.y, HORIZON_REFLECT_BIAS);
2037
1662
  this.refl = pb.normalize(this.refl);
2038
- // Blended against the pre-exposed scene color, so the exposure-independent sky bake has
2039
- // to be lifted into the same space.
2040
- this.reflMiss = ShaderHelper.sampleBakedSkyPreExposed(this, this.refl);
1663
+ // The mirror image of the visible sky, at its on-screen scale: the bake
1664
+ // brought to the live exposure without the environment light's
1665
+ // strength, which dims the IBL and not the sky itself.
1666
+ this.reflMiss = pb.mul(pb.textureSampleLevel(ShaderHelper.getBakedSkyTexture(this), this.refl, 0).rgb, this.skyReflectScale);
1667
+ if (envLight?.hasRadiance()) {
1668
+ // Blurred as the distant normals are: the prefiltered radiance at
1669
+ // the surface's roughness, in the same pre-exposed space
1670
+ this.$l.skyBlur = pb.smoothStep(SKY_PREFILTER_START, SKY_PREFILTER_FULL, this.roughness);
1671
+ this.$if(pb.greaterThan(this.skyBlur, 0), function() {
1672
+ this.reflMiss = pb.mix(this.reflMiss, pb.mul(envLight.getRadiance(this, this.refl, this.roughness).rgb, this.skyReflectScale), this.skyBlur);
1673
+ });
1674
+ }
2041
1675
  });
2042
- this.reflectance = pb.mix(this.reflMiss, pb.textureSampleLevel(ShaderHelper.getSceneColorTexture(this), this.hitInfo.xy, 0).rgb, this.hitInfo.w);
2043
- this.$l.refractInfo = that.waterRefraction(this, this.worldPos, this.normal, this.eyeVecNorm, this.screenUV, this.viewPos.z, this.wPos.w, this.wPos.xyz, this.depth);
1676
+ // The traced reflection blurred by roughness as Uludag's cone-traced
1677
+ // reflections blur it ("Hi-Z Screen-Space Cone-Traced Reflections",
1678
+ // GPU Pro 5, 2014): the lobe as a cone of half angle
1679
+ // acos(0.244^(1 / (n + 1))) for a Phong power n, here
1680
+ // n = 2 / alpha^2 - 2 for the GGX alpha (Walter et al., "Microfacet
1681
+ // Models for Refraction through Rough Surfaces", EGSR 2007); its
1682
+ // footprint where the ray hits the radius of the circle inscribed in
1683
+ // the isosceles triangle of the cone over the screen-space ray, in
1684
+ // pixels; and the scene colour read at the level of that radius
1685
+ this.$l.alpha = pb.mul(this.roughness, this.roughness);
1686
+ this.$l.phongPower = pb.max(pb.sub(pb.div(2, pb.max(pb.mul(this.alpha, this.alpha), 1e-6)), 2), 0);
1687
+ this.$l.coneCos = pb.pow(0.244, pb.div(1, pb.add(this.phongPower, 1)));
1688
+ this.$l.coneTan = pb.div(pb.sqrt(pb.max(pb.sub(1, pb.mul(this.coneCos, this.coneCos)), 0)), pb.max(this.coneCos, 1e-4));
1689
+ this.$l.coneBase = pb.mul(2, this.coneTan, this.hitInfo.z);
1690
+ this.$l.coneRadius = pb.div(pb.mul(this.coneBase, pb.sub(pb.sqrt(pb.add(pb.mul(this.coneBase, this.coneBase), pb.mul(4, this.hitInfo.z, this.hitInfo.z))), this.coneBase)), pb.max(pb.mul(4, this.hitInfo.z), 1e-4));
1691
+ this.$l.reflectLod = pb.clamp(pb.log2(pb.max(this.coneRadius, 1)), 0, REFRACT_BLUR_MAX_LOD);
1692
+ this.reflectance = pb.mix(this.reflMiss, pb.textureSampleLevel(ShaderHelper.getSceneColorTexture(this), this.hitInfo.xy, this.reflectLod).rgb, this.hitInfo.w);
1693
+ this.$l.refractInfo = that.waterRefraction(this, this.worldPos, this.normal, this.eyeVecNorm, this.screenUV, this.viewPos.z, this.wPos.w, this.wPos.xyz, this.depth, this.underwaterEye);
2044
1694
  this.$l.refractUV = this.refractInfo.xy;
2045
- // The medium is walked along the refracted path, not along the straight
2046
- // line to the bed. A refracted ray reaches the same depth over a shorter
2047
- // distance, and at a grazing view the two differ by a lot.
1695
+ // Medium path is the refracted length, not the straight line.
2048
1696
  this.depth = this.refractInfo.z;
2049
- // How blurred what is behind the water reads. Scattering in the column
2050
- // deflects the transmitted ray at every event, so the image arriving at
2051
- // the surface is a convolution whose width grows with the optical depth;
2052
- // sampling a coarser mip is the cheap stand-in for that.
2053
- //
2054
- // Log in the path length because each mip is a doubling of the filter
2055
- // width, and driven by the *scattering* coefficient rather than the
2056
- // extinction: absorption removes light without redirecting it, so it
2057
- // darkens the background without blurring it.
2058
- //
2059
- // Zero from below. The ray leaving the underside travels through air to
2060
- // whatever it reaches, so there is no medium along it to blur or absorb
2061
- // with - what the eye already looked through to get here is the volume
2062
- // the Underwater pass owns, and charging the same column twice turned
2063
- // the world seen through the Snell window into a dark smear.
1697
+ // Zero from below: the column the eye is inside belongs to the
1698
+ // Underwater pass, and charging it twice smears the Snell window.
2064
1699
  this.$l.mediumPath = this.$choice(this.underwaterEye, pb.float(0), this.depth);
1700
+ // Blur LOD: log2 in path length (each mip doubles the width), driven by
1701
+ // scattering rather than extinction (absorption darkens without blurring).
2065
1702
  this.$l.refractBlur = pb.log2(pb.add(1, pb.mul(this.mediumPath, pb.add(REFRACT_BLUR_GEOMETRIC, this.refractBlurDensity))));
2066
1703
  this.$l.refraction = pb.textureSampleLevel(ShaderHelper.getSceneColorTexture(this), this.refractUV, pb.clamp(this.refractBlur, 0, REFRACT_BLUR_MAX_LOD)).rgb;
2067
1704
  this.$l.refractionRaw = this.refraction;
2068
1705
  this.$l.absorption = this.getAbsorption(this.mediumPath);
2069
1706
  this.refraction = pb.mul(this.refraction, this.absorption);
2070
- this.$l.fresnelTerm = this.fresnel(this.shadingNormal, pb.neg(this.eyeVecNorm), this.underwaterEye);
2071
- // Foam coverage. The generator reports where the surface has folded over
2072
- // on itself; the ramp turns that into how much of the texel is covered.
2073
- this.$l.crestFoam = pb.clamp(pb.mul(pb.pow(pb.clamp(this.foamFactor, 0, 1), this.foamShadingParams.y), this.foamShadingParams.x), 0, 1);
2074
- // (coverage, distance to the nearest surface around the pixel). The
2075
- // distance is carried out for the debug view; the shading only wants x.
2076
- this.$l.shoreFoamInfo = shoreFoam ? this.waterShoreFoam(this.worldPos, this.viewPos, this.screenUV) : pb.vec2(0, 0);
2077
- this.$l.shoreFoam = this.shoreFoamInfo.x;
2078
- // Two independent coverages of the same texel, so they compose as
2079
- // overlapping area: `max` would ignore the smaller source entirely and
2080
- // adding would run past 1.
2081
- this.$l.foam = pb.sub(1, pb.mul(pb.sub(1, this.crestFoam), pb.sub(1, this.shoreFoam)));
2082
- // Foam suppresses the specular lobe rather than adding to it: where it is
2083
- // thick the mirror underneath stops being visible at all.
2084
- this.fresnelTerm = pb.mul(this.fresnelTerm, pb.sub(1, this.foam));
2085
- this.$l.finalColor = pb.mix(this.refraction, this.reflectance, this.fresnelTerm);
2086
- // Per-term accumulators for the debug views. Dead in the `none` variant.
2087
- this.$l.dbgSpecular = pb.vec3(0);
2088
- this.$l.dbgNoL = pb.float(0);
2089
- this.$l.dbgShadow = pb.vec3(1);
1707
+ this.$l.viewXZ = pb.neg(this.eyeVecNorm.xz);
1708
+ if (slopeVar) {
1709
+ // Variance of the slopes the normal no longer shows along the view
1710
+ // direction, as Bruneton's MeanFresnel takes it
1711
+ this.$l.sigmaV = pb.sqrt(pb.max(pb.div(pb.add(pb.mul(this.viewXZ.x, this.viewXZ.x, this.slopeVar.x), pb.mul(this.viewXZ.y, this.viewXZ.y, this.slopeVar.y), pb.mul(this.viewXZ.x, this.viewXZ.y, this.slopeVar.z, 2)), pb.max(pb.dot(this.viewXZ, this.viewXZ), 1e-6)), 0));
1712
+ } else {
1713
+ // Variance of the slopes the flattened normal no longer shows, (1 - k^2)
1714
+ // of the sea's for a slope scaled by k, along the view direction in wind
1715
+ // space as Bruneton's MeanFresnel takes it
1716
+ this.$l.viewWind = pb.vec2(pb.dot(this.viewXZ, this.unresolvedSlope.xy), pb.dot(this.viewXZ, pb.vec2(pb.neg(this.unresolvedSlope.y), this.unresolvedSlope.x)));
1717
+ this.$l.viewWindT = pb.div(pb.mul(this.viewWind, this.viewWind), pb.max(pb.dot(this.viewXZ, this.viewXZ), 1e-6));
1718
+ this.$l.sigmaV = pb.sqrt(pb.mul(pb.dot(this.viewWindT, this.unresolvedSlope.zw), pb.sub(1, pb.mul(this.normalScale, this.normalScale))));
1719
+ }
1720
+ this.$l.fresnelTerm = this.fresnel(this.shadingNormal, pb.neg(this.eyeVecNorm), this.underwaterEye, this.sigmaV);
1721
+ // Foam density from the wave generator: crests, wakes, breaking shore waves
1722
+ this.$l.crestDensity = pb.clamp(pb.mul(pb.pow(pb.clamp(this.foamFactor, 0, 1), this.foamShadingParams.y), this.foamShadingParams.x), 0, 1);
1723
+ this.$l.crestWhite = this.waterWhiteFoam(this.crestDensity, this.foamSample, this.foamLookParams.x);
1724
+ // Wall foam, cut from the same texture by its own density: whole against
1725
+ // the wall, fraying as it thins. Its strength scales opacity after the cut.
1726
+ this.$l.wallDensity = pb.clamp(this.wallFoam, 0, 1);
1727
+ this.$l.wallCut = this.waterWhiteFoam(this.wallDensity, this.foamSample, this.foamLookParams.x);
1728
+ this.$l.wallWhite = pb.mul(this.wallCut.x, this.foamShadingParams.z);
1729
+ // The two layers combine as independent covers.
1730
+ this.$l.foam = pb.sub(1, pb.mul(pb.sub(1, this.crestWhite.x), pb.sub(1, this.wallWhite)));
1731
+ this.$l.foamGrad = pb.add(pb.mul(this.crestWhite.yz, pb.sub(1, this.wallWhite)), pb.mul(this.wallCut.yz, this.foamShadingParams.z, pb.sub(1, this.crestWhite.x)));
1732
+ this.$l.bubbleDensity = pb.sub(1, pb.mul(pb.sub(1, this.crestDensity), pb.sub(1, pb.mul(this.wallDensity, this.foamShadingParams.z))));
1733
+ // Normal of the white foam's relief: Crest differences the cut over a
1734
+ // quarter texel per metre of view depth, which keeps the relief at a
1735
+ // distance; the slope times that step here
1736
+ this.$l.foamStep = pb.mul(this.dist, 0.25, foamTexelSize(this, this.foamTex, this.foamUVScale));
1737
+ this.$l.foamNormal = pb.normalize(pb.add(this.diffuseNormal, pb.mul(pb.vec3(pb.neg(this.foamGrad.x), 0, pb.neg(this.foamGrad.y)), this.foamLookParams.y, this.foamStep)));
1738
+ // The white foam covers the water, its mirror and what is seen through
1739
+ // it alike, as Crest lays it over the finished water colour
1740
+ // (lerp(col, whiteFoam, a)): both only on the share without foam.
1741
+ // `bodyWeight`, the exit transmission on that share, weighs the
1742
+ // refraction and every body term below.
1743
+ this.$l.waterShare = pb.sub(1, this.foam);
1744
+ this.$l.bodyWeight = pb.mul(pb.sub(1, this.fresnelTerm), this.waterShare);
1745
+ this.fresnelTerm = pb.mul(this.fresnelTerm, this.waterShare);
1746
+ this.$l.finalColor = pb.add(pb.mul(this.refraction, this.bodyWeight), pb.mul(this.reflectance, this.fresnelTerm));
1747
+ // Prefer the wave generator's own ground for the thickness of thin swash.
1748
+ this.$l.edgeThickness = this.$choice(pb.greaterThanEqual(this.sheetThickness, 0), this.sheetThickness, // Sky reconstructs to the far plane, which can lie above an infinite
1749
+ // surface. With no bottom there, keep the water and foam fully visible.
1750
+ this.$choice(pb.greaterThanEqual(this.wPos.w, SCENE_SKY_DEPTH01), pb.max(this.edgeFadeWidth, 1e-4), pb.sub(this.worldPos.y, this.wPos.y)));
1751
+ this.$l.foamFade = pb.mul(this.$choice(pb.greaterThanEqual(this.sheetThickness, 0), pb.float(1), this.$choice(pb.greaterThan(this.edgeFadeWidth, 0), pb.smoothStep(0, pb.max(pb.mul(this.edgeFadeWidth, FOAM_EDGE_FADE_SHARE), 1e-4), this.edgeThickness), pb.float(1))), this.coverage);
1752
+ // Light the foam sends back, kept apart so the edge fade below can keep
1753
+ // the foam on a sheet too thin to show the water
1754
+ this.$l.foamRadiance = pb.vec3(0);
1755
+ this.$l.groundFoamOpacity = pb.float(0);
1756
+ this.$l.groundFoamRadiance = pb.vec3(0);
1757
+ if (hasGroundFoam) {
1758
+ this.$if(pb.greaterThan(this.groundFoamDensity, 1e-3), function() {
1759
+ this.$l.groundFoamWhite = this.waterWhiteFoam(pb.clamp(pb.mul(pb.pow(this.groundFoamDensity, this.foamShadingParams.y), this.foamShadingParams.x), 0, 1), this.groundFoamSample, this.foamLookParams.x);
1760
+ this.groundFoamOpacity = this.groundFoamWhite.x;
1761
+ });
1762
+ // The carrier P can still foam while its projected ground Q is dry.
1763
+ // Keep that visible cover through the handoff; once P drains, only
1764
+ // the ground foam remains. Set opacity before lighting both covers.
1765
+ this.groundFoamOpacity = pb.max(this.groundFoamOpacity, pb.mul(this.foam, this.foamFade));
1766
+ }
1767
+ // Debug accumulators. Dead in the `none` variant.
2090
1768
  this.$l.dbgSubsurface = pb.vec3(0);
2091
1769
  this.$l.dbgSunScatter = pb.vec3(0);
2092
- this.$l.dbgScatter = pb.vec3(0);
2093
- // What anything leaving the water body keeps on its way out, and the
2094
- // share of the surface that is water rather than foam. The refraction
2095
- // above already carries the first factor via the `mix`; every scattering
2096
- // term below has to carry it too.
2097
- this.$l.bodyWeight = pb.mul(pb.sub(1, this.fresnelTerm), pb.sub(1, this.foam));
2098
- // The sun-scattering debug views below rebuild that term's factors, and
2099
- // the light direction is only known inside this loop. Initialised so the
2100
- // views are well defined even with no directional light in the scene.
1770
+ this.$l.dbgShadow = pb.vec3(1);
1771
+ this.$l.dbgSpecular = pb.vec3(0);
1772
+ // Subsurface crest gate, after Ceto: the lit wall thins with height,
1773
+ // path = max(floor, exp(-softplus(h / H))), gate = exp(-sigma_t *
1774
+ // scale * path). On the vertex displacement, not the normal tilt,
1775
+ // which is zero at the crest and carries every ripple.
1776
+ this.$l.sssT = pb.div(this.crestHeight, this.subsurfaceParams.y);
1777
+ // softplus in the overflow-safe form max(t,0) + w*log(1+exp(-|t|/w)).
1778
+ this.$l.sssRise = pb.add(pb.max(this.sssT, 0), pb.mul(SSS_HEIGHT_SOFTNESS, pb.log(pb.add(1, pb.exp(pb.div(pb.neg(pb.abs(this.sssT)), SSS_HEIGHT_SOFTNESS))))));
1779
+ this.$l.sssPath = pb.max(pb.exp(pb.neg(this.sssRise)), SSS_CREST_PATH_FLOOR);
1780
+ this.$l.sssThickness = pb.exp(pb.neg(pb.mul(this.mediumExtinction, pb.mul(this.sssPath, SSS_ABSORPTION_SCALE))));
1781
+ // For the debug views that rebuild the sun terms outside the loop.
2101
1782
  this.dbgLightDir = pb.vec3(0, 1, 0);
2102
1783
  that.forEachLight(this, function(type, posRange, dirCutoff, colorIntensity, extra, shadow) {
2103
1784
  this.$l.lightAtten = that.calculateLightAttenuation(this, type, this.worldPos, posRange, dirCutoff, extra);
2104
- this.$l.lightDir = that.calculateLightDirection(this, type, this.worldPos, posRange, dirCutoff);
2105
- // Left behind for the sun-scattering debug views, which rebuild that
2106
- // term's factors outside this loop.
1785
+ this.$l.lightDir = that.calculateLightDirection(this, type, this.worldPos, posRange, dirCutoff, extra);
2107
1786
  this.dbgLightDir = this.lightDir;
2108
1787
  this.$l.NoL = pb.clamp(pb.dot(this.normal, this.lightDir), 0, 1);
2109
- // Diffuse incidence, off the diffuse normal. Used by the foam and the
2110
- // sun in-scattering below; NoL keeps the wave face for the mirror.
2111
1788
  this.$l.NoLdiffuse = pb.clamp(pb.dot(this.diffuseNormal, this.lightDir), 0, 1);
2112
- this.dbgNoL = this.NoLdiffuse;
1789
+ // From the raw cosine, so a lee slope below zero still wraps up.
1790
+ this.$l.NoLfoam = pb.clamp(pb.div(pb.add(pb.dot(this.foamNormal, this.lightDir), FOAM_LIGHT_WRAP), 1 + FOAM_LIGHT_WRAP), 0, 1);
2113
1791
  this.$l.lightEnergy = pb.mul(colorIntensity.rgb, colorIntensity.a, this.lightAtten);
2114
- this.$l.specularTerm = this.lightSpecular(this.lightDir, this.eyeVecNorm, this.shadingNormal, this.lightEnergy, this.roughness);
1792
+ this.$l.specularTerm = pb.vec3(0);
1793
+ // A rect light's mirror image is the rect itself: integrate the GGX
1794
+ // lobe over it with LTC, as the PBR materials do. One direction would
1795
+ // shrink it to a point highlight on a surface this smooth.
1796
+ this.$if(pb.equal(type, LIGHT_TYPE_RECT), function() {
1797
+ this.$l.rectTerms = evaluateLTCRectLightTerms(this, this.worldPos, this.shadingNormal, pb.neg(this.eyeVecNorm), this.roughness, posRange, dirCutoff.xyz, extra.xyz);
1798
+ this.specularTerm = pb.mul(colorIntensity.rgb, colorIntensity.a, this.rectTerms.x, pb.add(pb.mul(this.rectTerms.y, WATER_F0), pb.mul(this.rectTerms.z, 1 - WATER_F0)));
1799
+ }).$else(function() {
1800
+ this.specularTerm = this.lightSpecular(this.lightDir, this.eyeVecNorm, this.shadingNormal, this.lightEnergy, this.roughness);
1801
+ });
2115
1802
  this.$l.lightContrib = this.specularTerm;
2116
- // Sunlight that entered the far side of a wave and scattered back out
2117
- // towards the eye - what makes a backlit crest glow. It has to come
2118
- // from the light loop: the ambient scattering term below is built from
2119
- // the environment irradiance, which has no direction.
2120
- //
2121
- // Standard translucency approximation - the transmitted direction is
2122
- // the light continuing through the surface, bent by the normal, and
2123
- // the term peaks when the eye looks back along it.
2124
- this.$l.sssDir = pb.normalize(pb.add(pb.neg(this.lightDir), pb.mul(this.diffuseNormal, SSS_DISTORTION)));
2125
- this.$l.sssFacing = pb.pow(pb.clamp(pb.dot(pb.neg(this.eyeVecNorm), this.sssDir), 0, 1), SSS_POWER);
2126
- // Crests glow and troughs do not: height above the undisplaced surface
2127
- // stands in for how much lit water the ray passed through. The medium's
2128
- // albedo carries the hue; the magnitude is authored.
2129
- this.$l.sssThickness = pb.clamp(pb.mul(pb.sub(1, this.diffuseNormal.y), this.subsurfaceParams.y), 0, 1);
2130
- this.$l.subsurfaceTerm = pb.mul(this.lightEnergy, this.mediumAlbedo, pb.mul(this.sssFacing, this.sssThickness, this.subsurfaceParams.x));
2131
- this.lightContrib = pb.add(this.lightContrib, this.subsurfaceTerm);
2132
- // Sunlight scattered back out of the water column, which is what gives
2133
- // the body a direction-dependent colour at all.
2134
- //
2135
- // Directional only, and only with the eye above the surface. The
2136
- // integral assumes a parallel beam of fixed slope; a point light's
2137
- // falloff along the column and the underwater geometry both need a
2138
- // different solution. The refraction and the ambient scattering still
2139
- // carry the water colour in those cases.
1803
+ // Backlit-crest subsurface, after Ceto: grazing^2 * (view ray mirrored
1804
+ // in the rest plane, against the sun)^4 * sun-above-horizon fade.
1805
+ // Both cosines are floored.
1806
+ this.$l.sssToEye = pb.neg(this.eyeVecNorm);
1807
+ this.$l.sssGrazing = pb.sub(1, pb.clamp(this.sssToEye.y, 0, 1));
1808
+ this.sssGrazing = pb.mix(SSS_GRAZING_FLOOR, 1, pb.mul(this.sssGrazing, this.sssGrazing));
1809
+ this.$l.sssMirror = pb.reflect(this.sssToEye, pb.vec3(0, 1, 0));
1810
+ this.$l.sssAlign = pb.clamp(pb.dot(pb.neg(this.sssMirror), this.lightDir), 0, 1);
1811
+ this.sssAlign = pb.mul(this.sssAlign, this.sssAlign);
1812
+ this.sssAlign = pb.mix(SSS_SUN_ALIGN_FLOOR, 1, pb.mul(this.sssAlign, this.sssAlign));
1813
+ this.$l.sssSunUp = pb.smoothStep(0, 0.1, this.lightDir.y);
1814
+ this.$l.sssFacing = pb.mul(this.sssGrazing, this.sssAlign, this.sssSunUp);
1815
+ // Scaled by the light colour as in Crest, and by 1 / pi like the other
1816
+ // diffuse terms. Directional lights only; carries `bodyWeight`.
1817
+ this.$l.subsurfaceTerm = pb.vec3(0);
1818
+ this.$if(pb.equal(type, LIGHT_TYPE_DIRECTIONAL), function() {
1819
+ this.subsurfaceTerm = pb.mul(this.subsurfaceTint, this.lightEnergy, pb.mul(this.sssFacing, this.sssThickness, this.subsurfaceParams.x, 1 / Math.PI), this.bodyWeight);
1820
+ this.lightContrib = pb.add(this.lightContrib, this.subsurfaceTerm);
1821
+ });
1822
+ // Directional only (the integral assumes a parallel beam) and from
1823
+ // above only.
2140
1824
  this.$l.sunScatterTerm = pb.vec3(0);
2141
1825
  this.$if(pb.and(pb.equal(type, LIGHT_TYPE_DIRECTIONAL), pb.not(this.underwaterEye)), function() {
2142
- // Weighted by what the surface transmits on the way out and by how
2143
- // much of it is still water rather than foam, like every other
2144
- // term that comes from inside the body.
2145
1826
  this.sunScatterTerm = pb.mul(that.waterSunScattering(this, this.lightEnergy, this.lightDir, this.eyeVecNorm, this.NoLdiffuse, this.depth, this.foam), this.bodyWeight);
2146
1827
  this.lightContrib = pb.add(this.lightContrib, this.sunScatterTerm);
2147
1828
  });
2148
- // Foam is a rough dielectric layer, so it takes the light the way any
2149
- // matte surface does.
2150
- this.lightContrib = pb.add(this.lightContrib, pb.mul(this.lightEnergy, this.foamColor, this.foam, this.NoLdiffuse, 1 / Math.PI));
1829
+ // Foam as a matte layer, with Crest's narrow glint off its relief.
1830
+ this.$l.foamGlint = pb.mul(pb.pow(pb.max(pb.dot(pb.reflect(this.eyeVecNorm, this.foamNormal), this.lightDir), 0), FOAM_SPECULAR_POWER), FOAM_SPECULAR_BOOST);
1831
+ this.$l.foamLight = pb.mul(this.lightEnergy, this.foam, pb.add(pb.mul(this.foamColor, this.NoLfoam, 1 / Math.PI), pb.vec3(this.foamGlint)));
1832
+ this.lightContrib = pb.add(this.lightContrib, this.foamLight);
2151
1833
  this.$l.shadow = pb.vec3(1);
2152
1834
  if (shadow) {
2153
- // Water is a horizontal clipmap, so +Y is the geometric normal. The
2154
- // wave normal would jitter the shadow lookup per-pixel.
1835
+ // +Y, not the wave normal, which would jitter the lookup per pixel.
2155
1836
  this.shadow = pb.vec3(that.calculateShadow(this, this.worldPos, pb.vec3(0, 1, 0), this.NoL));
2156
- this.dbgShadow = this.shadow;
2157
1837
  this.lightContrib = pb.mul(this.lightContrib, this.shadow);
2158
1838
  }
1839
+ this.foamRadiance = pb.add(this.foamRadiance, pb.mul(this.foamLight, this.shadow));
1840
+ if (hasGroundFoam) {
1841
+ this.$if(pb.greaterThan(this.groundFoamOpacity, 0), function() {
1842
+ this.$l.groundNoL = pb.clamp(pb.div(pb.add(pb.dot(this.groundNormal, this.lightDir), FOAM_LIGHT_WRAP), 1 + FOAM_LIGHT_WRAP), 0, 1);
1843
+ this.$l.groundShadow = pb.vec3(1);
1844
+ if (shadow) {
1845
+ this.groundShadow = pb.vec3(that.calculateShadow(this, this.wPos.xyz, this.groundNormal, this.groundNoL));
1846
+ }
1847
+ this.groundFoamRadiance = pb.add(this.groundFoamRadiance, pb.mul(this.lightEnergy, this.foamColor, this.groundNoL, this.groundShadow, this.groundFoamOpacity, 1 / Math.PI));
1848
+ });
1849
+ }
2159
1850
  this.finalColor = pb.add(this.finalColor, this.lightContrib);
2160
- this.dbgSpecular = pb.add(this.dbgSpecular, pb.mul(this.specularTerm, this.shadow));
2161
1851
  this.dbgSubsurface = pb.add(this.dbgSubsurface, pb.mul(this.subsurfaceTerm, this.shadow));
2162
1852
  this.dbgSunScatter = pb.add(this.dbgSunScatter, pb.mul(this.sunScatterTerm, this.shadow));
1853
+ this.dbgShadow = pb.min(this.dbgShadow, this.shadow);
1854
+ this.dbgSpecular = pb.add(this.dbgSpecular, pb.mul(this.specularTerm, this.shadow));
2163
1855
  });
2164
1856
  if (that.needCalculateEnvLight()) {
2165
1857
  this.$l.irradiance = that.getEnvLightIrradiance(this, this.diffuseNormal);
2166
- // Scattering from the water body itself, and from the foam sitting on
2167
- // it. `bodyWeight` is what the surface transmits on the way out times
2168
- // the share of it that is still water.
2169
- //
2170
- // On the same path length the absorption uses, so this vanishes from
2171
- // below: the column the eye is already inside belongs to the
2172
- // Underwater pass, and adding the surface's own estimate of it on top
2173
- // paints the water colour twice. The foam term below is unconditional
2174
- // - a crest that has broken is white from underneath too.
1858
+ // On `mediumPath`, so zero from below. The foam term is unconditional.
2175
1859
  this.$l.sss = pb.mul(this.getScattering(this.mediumPath), this.irradiance, this.bodyWeight, 1 / Math.PI);
2176
1860
  this.finalColor = pb.add(this.finalColor, this.sss);
2177
- this.dbgScatter = this.sss;
2178
- this.finalColor = pb.add(this.finalColor, pb.mul(this.irradiance, this.foamColor, this.foam, 1 / Math.PI));
1861
+ this.$l.foamAmbient = pb.mul(this.irradiance, this.foamColor, this.foam, 1 / Math.PI);
1862
+ this.finalColor = pb.add(this.finalColor, this.foamAmbient);
1863
+ this.foamRadiance = pb.add(this.foamRadiance, this.foamAmbient);
1864
+ if (hasGroundFoam) {
1865
+ this.groundFoamRadiance = pb.add(this.groundFoamRadiance, pb.mul(that.getEnvLightIrradiance(this, this.groundNormal), this.foamColor, this.groundFoamOpacity, 1 / Math.PI));
1866
+ }
1867
+ // Bubbles under the white foam, part of the water body as in Crest,
1868
+ // so under the white foam and the mirror: the foam texture blurred and
1869
+ // shifted by parallax, as seen at a depth under the surface
1870
+ this.$if(pb.greaterThan(this.bubbleDensity, 0), function() {
1871
+ this.$l.toEye = pb.neg(this.eyeVecNorm);
1872
+ this.$l.bubbleShift = pb.div(pb.mul(this.toEye.xz, pb.neg(this.foamLookParams.w)), pb.max(pb.dot(this.diffuseNormal, this.toEye), 0.1));
1873
+ this.$l.bubbleTex = sampleFoamTexture(this, this.foamTex, pb.mul(pb.add(this.restXZ, this.bubbleShift, pb.vec2(pb.mul(ShaderHelper.getElapsedTime(this), FOAM_DRIFT))), this.foamUVScale), this.restXZGrad, this.foamUVScale, FOAM_BUBBLE_MIP_SCALE, false).x;
1874
+ this.finalColor = pb.add(this.finalColor, pb.mul(this.irradiance, this.foamBubbleColor, this.bubbleTex, pb.clamp(pb.mul(this.bubbleDensity, this.foamLookParams.z), 0, 1), this.bodyWeight, 1 / Math.PI));
1875
+ });
2179
1876
  }
1877
+ // A thin sheet fades into the scene behind it. Where the generator
1878
+ // knows the wet edge, its coverage keeps foam visible on even a
1879
+ // millimetre-thick sheet; elsewhere foam fades by the measured depth.
1880
+ this.$if(pb.not(this.underwaterEye), function() {
1881
+ this.$l.edgeFade = pb.mul(this.$choice(pb.greaterThan(this.edgeFadeWidth, 0), pb.smoothStep(0, pb.max(this.edgeFadeWidth, 1e-4), this.edgeThickness), pb.float(1)), this.coverage);
1882
+ this.$if(pb.lessThan(this.edgeFade, 1), function() {
1883
+ this.$l.behind = pb.textureSampleLevel(ShaderHelper.getSceneColorTexture(this), this.screenUV, 0).rgb;
1884
+ // finalColor = water (on the share without foam) + foam light: the
1885
+ // water fades into what is behind, under the foam
1886
+ this.finalColor = pb.add(pb.mix(pb.mul(this.behind, pb.sub(1, pb.mul(this.foam, this.foamFade))), pb.sub(this.finalColor, this.foamRadiance), this.edgeFade), pb.mul(this.foamRadiance, this.foamFade));
1887
+ });
1888
+ if (hasGroundFoam) {
1889
+ this.$if(pb.greaterThan(this.groundFoamWeight, 0), function() {
1890
+ this.$l.groundBehind = pb.textureSampleLevel(ShaderHelper.getSceneColorTexture(this), this.screenUV, 0).rgb;
1891
+ // One crossfade between complete covers. Attenuating the mobile
1892
+ // foam before overlaying ground foam applies this weight twice
1893
+ // and exposes sand in the middle even if both covers are white.
1894
+ this.$l.groundColor = pb.add(pb.mul(this.groundBehind, pb.sub(1, this.groundFoamOpacity)), this.groundFoamRadiance);
1895
+ this.finalColor = pb.mix(this.finalColor, this.groundColor, this.groundFoamWeight);
1896
+ });
1897
+ }
1898
+ });
2180
1899
  switch(debugOutput){
2181
1900
  case 'normal':
2182
- this.$return(pb.add(pb.mul(this.normal, 0.5), pb.vec3(0.5)));
2183
- break;
2184
- case 'diffuseNormal':
2185
- this.$return(pb.add(pb.mul(this.diffuseNormal, 0.5), pb.vec3(0.5)));
2186
- break;
2187
- case 'frontFacing':
2188
- this.$return(this.$choice(this.$builtins.frontFacing, pb.vec3(1), pb.vec3(0)));
1901
+ this.$if(pb.lessThan(this.diffuseNormal.y, 0.01), function() {
1902
+ this.$return(pb.vec3(1, 0, 0));
1903
+ }).$else(function() {
1904
+ this.$return(pb.add(pb.mul(this.diffuseNormal, 0.5), pb.vec3(0.5)));
1905
+ });
2189
1906
  break;
2190
- case 'viewFacing':
2191
- {
2192
- // Raw, not clamped: a negative value means the wave normal faces
2193
- // away from the eye at that pixel.
2194
- this.$l.viewFacing = pb.dot(this.normal, pb.neg(this.eyeVecNorm));
2195
- this.$return(pb.vec3(this.viewFacing));
2196
- break;
2197
- }
2198
1907
  case 'foam':
2199
- this.$return(pb.vec3(this.foam));
1908
+ this.$return(pb.vec3(pb.mix(this.foam, this.groundFoamOpacity, this.groundFoamWeight)));
2200
1909
  break;
2201
- case 'shoreFoam':
2202
- this.$return(pb.vec3(this.shoreFoam));
1910
+ case 'swashFoamCuts':
1911
+ this.$return(pb.vec3(this.foam, this.groundFoamOpacity, this.groundFoamWeight));
2203
1912
  break;
2204
1913
  case 'waterDepth':
2205
- // Distance from the surface to whatever is behind it, metres / 10.
2206
- // With the band switched off there is no such estimate, so the plain
2207
- // height difference stands in.
2208
- this.$return(pb.vec3(pb.mul(shoreFoam ? this.shoreFoamInfo.y : pb.max(pb.sub(this.worldPos.y, this.wPos.y), 0), 0.1)));
1914
+ this.$return(pb.vec3(pb.mul(pb.max(pb.sub(this.worldPos.y, this.wPos.y), 0), 0.1)));
2209
1915
  break;
2210
1916
  case 'fresnel':
2211
1917
  this.$return(pb.vec3(this.fresnelTerm));
@@ -2220,76 +1926,36 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
2220
1926
  this.$return(this.absorption);
2221
1927
  break;
2222
1928
  case 'scattering':
2223
- this.$return(this.dbgScatter);
1929
+ this.$return(this.sss);
2224
1930
  break;
2225
1931
  case 'sunScattering':
2226
1932
  this.$return(this.dbgSunScatter);
2227
1933
  break;
2228
- case 'sunNoL':
2229
- this.$return(pb.vec3(pb.clamp(this.dbgNoL, 0, 1)));
1934
+ case 'subsurface':
1935
+ this.$return(this.dbgSubsurface);
1936
+ break;
1937
+ case 'subsurfaceThickness':
1938
+ this.$return(pb.vec3(this.sssThickness));
2230
1939
  break;
2231
1940
  case 'shadow':
2232
1941
  this.$return(this.dbgShadow);
2233
1942
  break;
2234
- case 'sunPhase':
2235
- {
2236
- // Recomputed from the same inputs rather than read back out of
2237
- // waterSunScattering: the value belongs to that function's scope, and
2238
- // the variant that wants it is the one that never calls it. Keep the
2239
- // two expressions in step.
2240
- this.$l.dbgLw = pb.refract(pb.neg(this.dbgLightDir), pb.vec3(0, 1, 0), 1 / 1.333);
2241
- this.$l.dbgVw = pb.refract(this.eyeVecNorm, pb.vec3(0, 1, 0), 1 / 1.333);
2242
- this.$l.dbgCosTheta = pb.neg(pb.dot(this.dbgLw, this.dbgVw));
2243
- this.$l.dbgHg = waterScatterPhase(this, this.dbgCosTheta, this.sunScatterParams.y);
2244
- this.$l.dbgLumWeights = pb.vec3(0.2126, 0.7152, 0.0722);
2245
- this.$l.dbgThickness = pb.mul(pb.dot(this.mediumAlbedo, this.dbgLumWeights), pb.sub(1, pb.exp(pb.neg(pb.mul(pb.dot(this.mediumExtinction, this.dbgLumWeights), this.depth)))));
2246
- this.$return(pb.vec3(pb.clamp(pb.mix(this.dbgHg, 1 / (4 * Math.PI), pb.smoothStep(0, 0.5, pb.clamp(this.dbgThickness, 0, 1))), 0, 1)));
2247
- break;
2248
- }
2249
- case 'sunIntegral':
2250
- {
2251
- this.$l.dbgVy = pb.max(pb.neg(pb.refract(this.eyeVecNorm, pb.vec3(0, 1, 0), 1 / 1.333).y), 1e-3);
2252
- this.$l.dbgSy = pb.max(pb.neg(pb.refract(pb.neg(this.dbgLightDir), pb.vec3(0, 1, 0), 1 / 1.333).y), 0.05);
2253
- this.$l.dbgRr = pb.add(1, pb.div(this.dbgVy, this.dbgSy));
2254
- this.$l.dbgIntegral = pb.div(pb.sub(pb.vec3(1), pb.exp(pb.neg(pb.mul(pb.mul(this.mediumExtinction, this.dbgRr), this.depth)))), this.dbgRr);
2255
- this.$return(pb.vec3(pb.clamp(pb.length(this.dbgIntegral), 0, 1)));
2256
- break;
2257
- }
2258
- case 'subsurface':
2259
- this.$return(this.dbgSubsurface);
2260
- break;
2261
1943
  case 'specular':
2262
1944
  this.$return(this.dbgSpecular);
2263
1945
  break;
2264
1946
  case 'depth':
2265
1947
  this.$return(pb.vec3(pb.mul(this.depth, 0.1)));
2266
1948
  break;
2267
- case 'refractUV':
2268
- this.$return(pb.vec3(pb.add(pb.mul(pb.sub(this.refractUV, this.screenUV), 4), pb.vec2(0.5)), 0.5));
2269
- break;
2270
- case 'nan':
2271
- {
2272
- // NaN is the only value that fails x == x. The normal is checked
2273
- // separately from the colour so a NaN that a clamp downstream
2274
- // swallowed still shows.
2275
- this.$l.sum = pb.add(pb.dot(this.finalColor, pb.vec3(1)), pb.dot(this.normal, pb.vec3(1)));
2276
- this.$l.bad = pb.or(pb.notEqual(this.sum, this.sum), pb.greaterThan(pb.abs(this.sum), 1e30));
2277
- this.$return(this.$choice(this.bad, pb.vec3(1, 0, 0), pb.vec3(0)));
2278
- break;
2279
- }
2280
1949
  default:
2281
1950
  this.$return(this.finalColor);
2282
1951
  }
2283
1952
  });
2284
- return scope.waterShading(worldPos, worldNormal, foamFactor);
1953
+ return scope.waterShading(worldPos, worldNormal, foamFactor, crestHeight, restXZ, restXZGrad, foamSample, coverage, sheetThickness, wallFoam, scenePos, groundFoamDensity, groundFoamSample, groundNormal, groundFoamWeight);
2285
1954
  }
2286
1955
  applyUniforms(bindGroup, ctx, needUpdate, pass) {
2287
1956
  super.applyUniforms(bindGroup, ctx, needUpdate, pass);
2288
- const waveGenerator = this._waveGenerator.get();
2289
- // Synced per bind group, not per material: the material owns one bind group
2290
- // per pass/render-variant hash, so drawing for a second camera reuses a
2291
- // different group whose wave uniforms were never written. WeakMap so a
2292
- // released group is garbage collected with its entry.
1957
+ const waveGenerator = this._effectiveWaveGenerator;
1958
+ // Per bind group: the material owns one per pass/variant hash.
2293
1959
  const lastWritten = this._waveVersionByBindGroup.get(bindGroup) ?? -1;
2294
1960
  if (waveGenerator && lastWritten !== waveGenerator.version) {
2295
1961
  waveGenerator.applyWaterBindGroup(bindGroup);
@@ -2305,98 +1971,255 @@ class WaterMaterial extends applyMaterialMixins(MeshMaterial, mixinLight) {
2305
1971
  bindGroup.setValue('region', this._region);
2306
1972
  }
2307
1973
  if (this.needFragmentColor(ctx)) {
2308
- // Dimensionless: the offset is derived in world space and projected, so
2309
- // there is nothing here for the render size to scale.
2310
1974
  bindGroup.setValue('refractionScale', this._refractionScale);
2311
1975
  if (this.refractionMode === 'offset') {
2312
1976
  bindGroup.setValue('cheapRefractionDepth', this._cheapRefractionDepth);
2313
1977
  }
2314
1978
  bindGroup.setValue('reflectionStrength', this._reflectionStrength);
1979
+ const wind = this._waveGenerator.get()?.wind;
1980
+ const windSpeed = wind ? Math.hypot(wind.x, wind.y) : 0;
1981
+ this._unresolvedSlope.setXYZW(windSpeed > 0 ? wind.x / windSpeed : 1, windSpeed > 0 ? wind.y / windSpeed : 0, COX_MUNK_UPWIND * windSpeed, COX_MUNK_CROSSWIND_BASE + COX_MUNK_CROSSWIND * windSpeed);
1982
+ bindGroup.setValue('unresolvedSlope', this._unresolvedSlope);
1983
+ bindGroup.setValue('skyReflectScale', ctx.scene.lightingMode === 'physical' ? ShaderHelper.getPreExposure(ctx) / PHYSICAL_BAKE_EXPOSURE : 1);
2315
1984
  bindGroup.setValue('ssrParams', this._ssrParams);
2316
- this._subsurfaceParams.setXYZW(this._subsurfaceIntensity, this._subsurfaceSteepness, 0, 0);
1985
+ const eyeUnder = ctx.underwater;
1986
+ bindGroup.setValue('eyeSide', eyeUnder ? eyeUnder.material === this ? -1 : 0 : 1);
1987
+ this._subsurfaceParams.setXYZW(this._subsurfaceIntensity, this._subsurfaceCrestHeight, 0, 0);
2317
1988
  bindGroup.setValue('subsurfaceParams', this._subsurfaceParams);
2318
- this._foamParams.setXYZW(this._foamAmount, this._foamFalloff, 0, 0);
1989
+ bindGroup.setValue('subsurfaceTint', this._subsurfaceTint);
1990
+ this._foamParams.setXYZW(this._foamAmount, this._foamFalloff, this._shoreWaves.height > 0 ? this._shoreWaves.wallFoamStrength : 0, 0);
2319
1991
  bindGroup.setValue('foamShadingParams', this._foamParams);
2320
1992
  bindGroup.setValue('foamColor', this._foamColor);
2321
- if (this._shoreFoamEnabled) {
2322
- this._shoreFoamParams.setXYZW(this._shoreFoamAmount, this._shoreFoamDepth, this._shoreFoamFalloff, // Cycles per metre, resolved from cycles per band width.
2323
- this._shoreFoamScale / this._shoreFoamDepth);
2324
- bindGroup.setValue('shoreFoamParams', this._shoreFoamParams);
2325
- this._shoreFoamWashParams.setXYZW(this._shoreFoamWashAmount, this._shoreFoamWashSpeed, this._shoreFoamWashScale, // Query radius: the band's widest reach over the run-up cycle. Fixed
2326
- // over the cycle because it sets the pyramid level the query samples,
2327
- // and a level moving with the wash would step the band's resolution.
2328
- this._shoreFoamDepth * (1 + this._shoreFoamWashAmount));
2329
- bindGroup.setValue('shoreFoamWashParams', this._shoreFoamWashParams);
2330
- }
1993
+ this._foamLookParams.setXYZW(this._foamFeather, this._foamNormalStrength, this._foamBubbleCoverage, this._foamBubbleDepth);
1994
+ bindGroup.setValue('foamLookParams', this._foamLookParams);
1995
+ bindGroup.setValue('foamBubbleColor', this._foamBubbleColor);
1996
+ bindGroup.setTexture('foamTex', this.foamTexture ?? getDefaultFoamTexture(), fetchSampler('repeat_linear'));
1997
+ bindGroup.setValue('foamUVScale', 1 / this._foamTextureSize);
1998
+ bindGroup.setValue('edgeFadeWidth', this._edgeFadeWidth);
2331
1999
  bindGroup.setValue('mediumAlbedo', this._scatterAlbedo);
2332
- // Needed in both medium modes: the sun-scattering integral is always
2333
- // physical, even where the ramp overrides the depth-driven absorption.
2334
2000
  bindGroup.setValue('mediumExtinction', this._extinction);
2001
+ bindGroup.setTexture('zLTCMatLut', getLTCMatLUT());
2002
+ bindGroup.setTexture('zLTCAmpLut', getLTCAmpLUT());
2335
2003
  this._sunScatterParams.setXYZW(this._sunScatteringIntensity, this._scatterAnisotropy, 0, 0);
2336
2004
  bindGroup.setValue('sunScatterParams', this._sunScatterParams);
2337
- // One LOD serves all three channels, so the per-channel scattering has to
2338
- // collapse to a scalar. Luminance-weighted rather than a plain mean: the
2339
- // blur is a perceptual effect and green carries most of what is seen.
2005
+ // Luminance-weighted scalar: one blur LOD serves all three channels.
2340
2006
  bindGroup.setValue('refractBlurDensity', (0.2126 * this._scattering.x + 0.7152 * this._scattering.y + 0.0722 * this._scattering.z) * this._scatteringScale * REFRACT_BLUR_DENSITY * this._refractionBlur);
2341
- if (this.mediumMode === 'ramp') {
2342
- bindGroup.setValue('depthMulti', this._depthMulti);
2343
- bindGroup.setTexture('scatterRampTex', this._getScatterRampTexture(ctx.device), fetchSampler('clamp_linear_nomip'));
2344
- bindGroup.setTexture('absorptionRampTex', this._getAbsorptionRampTexture(ctx.device), fetchSampler('clamp_linear_nomip'));
2345
- }
2346
2007
  }
2347
2008
  if (this.waveGenerator) {
2348
2009
  this.waveGenerator.applyWaterBindGroup(bindGroup);
2349
2010
  }
2350
2011
  }
2351
2012
  needUpdate() {
2352
- return !!this._waveGenerator.get()?.needUpdate();
2013
+ return !!this._waveGenerator.get()?.needUpdate() || !!this._interaction.get() || !!this._shoreWaveGenerator;
2014
+ }
2015
+ /**
2016
+ * Crest to trough height in metres of the waves rolling in to the shore and
2017
+ * running up the beach, 0 for none. See {@link ShoreWaveGenerator}. Needs
2018
+ * the scene signed distance field (WebGPU); without it the waves are off.
2019
+ */ get shoreWaveHeight() {
2020
+ return this._shoreWaves.height;
2021
+ }
2022
+ set shoreWaveHeight(val) {
2023
+ val = Math.max(0, val);
2024
+ const wasOn = this._shoreWaves.height > 0;
2025
+ this._shoreWaves.height = val;
2026
+ if (wasOn !== val > 0) {
2027
+ this._rebuildEffectiveWaveGenerator();
2028
+ }
2029
+ }
2030
+ /** Distance in metres between shore wave crests at the outer edge of their zone; they bunch up towards the shore. */ get shoreWaveLength() {
2031
+ return this._shoreWaves.wavelength;
2032
+ }
2033
+ set shoreWaveLength(val) {
2034
+ this._shoreWaves.wavelength = Math.max(0.5, val);
2035
+ }
2036
+ /** Seconds between successive shore waves. */ get shoreWavePeriod() {
2037
+ return this._shoreWaves.period;
2038
+ }
2039
+ set shoreWavePeriod(val) {
2040
+ this._shoreWaves.period = Math.max(0.5, val);
2041
+ }
2042
+ /** How far out from the waterline the shore waves rise, in metres. */ get shoreWaveZone() {
2043
+ return this._shoreWaves.zone;
2044
+ }
2045
+ set shoreWaveZone(val) {
2046
+ this._shoreWaves.zone = Math.max(1, val);
2047
+ }
2048
+ /** How sharp the shore wave crests become at the shore, 0 for a sine and towards 1 for a cusp. */ get shoreWaveSharpness() {
2049
+ return this._shoreWaves.sharpness;
2050
+ }
2051
+ set shoreWaveSharpness(val) {
2052
+ this._shoreWaves.sharpness = Math.min(Math.max(val, 0), 0.95);
2053
+ }
2054
+ /** How far the shore wave crests lean towards the shore there, 0 for upright, 1 for a steep front face. */ get shoreWaveLean() {
2055
+ return this._shoreWaves.lean;
2056
+ }
2057
+ set shoreWaveLean(val) {
2058
+ this._shoreWaves.lean = Math.min(Math.max(val, 0), 1);
2059
+ }
2060
+ /** How irregular the shore waves are, 0..1: 0 for parallel crests a fixed wavelength apart, higher for each wave with a period and a bend of its own. */ get shoreWaveIrregularity() {
2061
+ return this._shoreWaves.irregularity;
2062
+ }
2063
+ set shoreWaveIrregularity(val) {
2064
+ this._shoreWaves.irregularity = Math.min(Math.max(val, 0), 1);
2065
+ }
2066
+ /** How fast the foam fades, per second (Crest's Foam Fade Rate): lower leaves longer trails behind breaking waves. */ get foamFadeRate() {
2067
+ return this._foamField.fadeRate;
2068
+ }
2069
+ set foamFadeRate(val) {
2070
+ this._foamField.fadeRate = val;
2071
+ }
2072
+ /** How much foam the open water's breaking crests leave behind to drift and fade; 0 keeps foam only while a crest breaks. */ get foamTrailStrength() {
2073
+ return this._foamField.baseStrength;
2074
+ }
2075
+ set foamTrailStrength(val) {
2076
+ this._foamField.baseStrength = val;
2077
+ }
2078
+ /** Share of the wind speed the foam drifts at, about 0.03 in nature. */ get foamDrift() {
2079
+ return this._foamField.drift;
2080
+ }
2081
+ set foamDrift(val) {
2082
+ this._foamField.drift = val;
2083
+ }
2084
+ /** Side in metres of the finest square of water around the view whose foam lingers, in the most detail; coarser squares reach eight times as far, beyond which foam shows only while crests break. */ get foamFieldSize() {
2085
+ return this._foamField.size;
2086
+ }
2087
+ set foamFieldSize(val) {
2088
+ this._foamField.size = val;
2089
+ }
2090
+ /** How much foam the breaking shore waves make (Crest's Wave Foam Strength). */ get shoreWaveFoamStrength() {
2091
+ return this._shoreWaves.waveFoamStrength;
2092
+ }
2093
+ set shoreWaveFoamStrength(val) {
2094
+ this._shoreWaves.waveFoamStrength = Math.max(0, val);
2095
+ }
2096
+ /** How much of the shore waves makes foam, 0..1, higher for more (Crest's Wave Foam Coverage). */ get shoreWaveFoamCoverage() {
2097
+ return this._shoreWaves.waveFoamCoverage;
2098
+ }
2099
+ set shoreWaveFoamCoverage(val) {
2100
+ this._shoreWaves.waveFoamCoverage = Math.min(Math.max(val, 0), 1);
2101
+ }
2102
+ /** Water shallower than this many metres makes foam (Crest's Shoreline Foam Max Depth). */ get shoreWaveShallowFoamDepth() {
2103
+ return this._shoreWaves.shorelineFoamMaxDepth;
2104
+ }
2105
+ set shoreWaveShallowFoamDepth(val) {
2106
+ this._shoreWaves.shorelineFoamMaxDepth = Math.max(0.01, val);
2107
+ }
2108
+ /** How much foam shallow water makes (Crest's Shoreline Foam Strength). */ get shoreWaveShallowFoamStrength() {
2109
+ return this._shoreWaves.shorelineFoamStrength;
2110
+ }
2111
+ set shoreWaveShallowFoamStrength(val) {
2112
+ this._shoreWaves.shorelineFoamStrength = Math.max(0, val);
2113
+ }
2114
+ /** How opaque the foam is that the water makes against walls, rocks and piers standing in it, 0..1; it always rings them whole. */ get wallFoamStrength() {
2115
+ return this._shoreWaves.wallFoamStrength;
2116
+ }
2117
+ set wallFoamStrength(val) {
2118
+ this._shoreWaves.wallFoamStrength = Math.min(Math.max(val, 0), 1);
2119
+ this.uniformChanged();
2120
+ }
2121
+ /** Speed in metres per second the water has to hit a wall at before it makes any foam there. */ get wallFoamMinSpeed() {
2122
+ return this._shoreWaves.wallFoamMinSpeed;
2123
+ }
2124
+ set wallFoamMinSpeed(val) {
2125
+ this._shoreWaves.wallFoamMinSpeed = Math.max(0, val);
2126
+ }
2127
+ /** Speed in metres per second at which water hitting a wall makes the most foam. */ get wallFoamMaxSpeed() {
2128
+ return this._shoreWaves.wallFoamMaxSpeed;
2129
+ }
2130
+ set wallFoamMaxSpeed(val) {
2131
+ this._shoreWaves.wallFoamMaxSpeed = Math.max(0.01, val);
2132
+ }
2133
+ /** Speed in metres per second the foam drifts out from walls, rocks and piers at: higher for a wider ring of foam around them. */ get wallFoamDrift() {
2134
+ return this._shoreWaves.wallFoamDrift;
2135
+ }
2136
+ set wallFoamDrift(val) {
2137
+ this._shoreWaves.wallFoamDrift = Math.max(0, val);
2138
+ }
2139
+ /**
2140
+ * Seconds the foam each breaking wave brings in lasts: higher for longer
2141
+ * white trails behind the crests and foam that stays on the swash all the
2142
+ * way up the beach, lower for brief flashes of white at the crests.
2143
+ */ get surfFoamDuration() {
2144
+ return this._shoreWaves.surfFoamDuration;
2145
+ }
2146
+ set surfFoamDuration(val) {
2147
+ this._shoreWaves.surfFoamDuration = Math.max(0.05, val);
2148
+ }
2149
+ /**
2150
+ * Water depth in metres at and below which foam stops drifting and stays
2151
+ * where it is: higher to leave foam lying over a wider band of the shallows
2152
+ * and the beach, lower to let it drift until the water is almost gone.
2153
+ */ get foamSettleDepth() {
2154
+ return this._shoreWaves.foamSettleDepth;
2155
+ }
2156
+ set foamSettleDepth(val) {
2157
+ this._shoreWaves.foamSettleDepth = Math.max(0, val);
2158
+ }
2159
+ /**
2160
+ * Water depth in metres at and beyond which foam drifts freely: higher to
2161
+ * slow the foam over a wider band of shallow water, lower to keep it moving
2162
+ * until close in.
2163
+ */ get foamDriftDepth() {
2164
+ return this._shoreWaves.foamDriftDepth;
2165
+ }
2166
+ set foamDriftDepth(val) {
2167
+ this._shoreWaves.foamDriftDepth = Math.max(0, val);
2168
+ }
2169
+ /**
2170
+ * How fast foam that has settled in shallow water and on the sand fades, as
2171
+ * a share of the foam's fade: lower to leave it lying on the beach longer
2172
+ * after the wave has drained, 1 to fade it as fast as foam on open water.
2173
+ */ get settledFoamFade() {
2174
+ return this._shoreWaves.settledFoamFade;
2175
+ }
2176
+ set settledFoamFade(val) {
2177
+ this._shoreWaves.settledFoamFade = Math.min(Math.max(val, 0.01), 1);
2178
+ }
2179
+ /**
2180
+ * Hands the shore waves this frame's waterline fields: the shore field in
2181
+ * levels, null when there is none, which turns them off; and the wall field,
2182
+ * null for no foam against walls. Called by the water every frame.
2183
+ * @internal
2184
+ */ setShoreWaveFrame(shore, shoreRegions, wall, wallRegion, level) {
2185
+ this._shoreWaveGenerator?.setFrame(shore, shoreRegions, wall, wallRegion, level, this._waveClock);
2186
+ }
2187
+ /**
2188
+ * Steps the foam field around the water the view looks at, once a frame
2189
+ * however many views there are. Called by the water after
2190
+ * {@link setShoreWaveFrame}, which its sources read.
2191
+ * @param focusX - World x the field is centred on.
2192
+ * @param focusZ - World z the field is centred on.
2193
+ * @internal
2194
+ */ updateFoamField(focusX, focusZ) {
2195
+ if (!this._foamSource || this._foamFrameId === this._frameId) {
2196
+ return;
2197
+ }
2198
+ this._foamFrameId = this._frameId;
2199
+ this._foamField.stepAround(focusX, focusZ, this._waveClock, this._waveGenerator.get()?.wind ?? null, this._foamSource);
2353
2200
  }
2354
2201
  update(frameId, elapsed) {
2202
+ this._frameId = frameId;
2203
+ this._waveClock = elapsed;
2204
+ // The base generator and the field are stepped separately, each guarded by
2205
+ // its own per-frame mark, so a generator shared between two bodies of water
2206
+ // is still stepped once even when each body wraps it with its own field.
2355
2207
  const waveGenerator = this._waveGenerator.get();
2356
2208
  if (waveGenerator) {
2357
2209
  const updateFrameId = WaterMaterial._waveUpdateState.get(waveGenerator);
2358
2210
  if (updateFrameId !== frameId) {
2359
- waveGenerator.update(elapsed);
2211
+ RenderGraphExecutor.profileScope('WaterWaves', ()=>waveGenerator.update(elapsed));
2360
2212
  WaterMaterial._waveUpdateState.set(waveGenerator, frameId);
2361
2213
  }
2362
2214
  }
2363
- }
2364
- _getRampTexture(device, grad) {
2365
- const width = 128;
2366
- const height = 1;
2367
- const texture = device.createTexture2D('rgba8unorm', width, height, {
2368
- mipmapping: false
2369
- });
2370
- const numTexels = width * height;
2371
- const data = new Uint8Array(numTexels * 4);
2372
- const tmpcolor = new Vector3();
2373
- for(let i = 0; i < numTexels; i++){
2374
- grad.interpolate(i % width / width, tmpcolor);
2375
- data[i * 4 + 0] = tmpcolor.x * 255 >> 0;
2376
- data[i * 4 + 1] = tmpcolor.y * 255 >> 0;
2377
- data[i * 4 + 2] = tmpcolor.z * 255 >> 0;
2378
- data[i * 4 + 3] = 255;
2379
- }
2380
- texture.update(data, 0, 0, width, height);
2381
- return texture;
2382
- }
2383
- _getScatterRampTexture(device) {
2384
- if (!this._scatterRampTexture.get()) {
2385
- if (!WaterMaterial._defaultScatterRampTexture.get()) {
2386
- WaterMaterial._defaultScatterRampTexture.set(this._getRampTexture(device, WaterMaterial._scatterGrad));
2387
- }
2388
- this._scatterRampTexture.set(WaterMaterial._defaultScatterRampTexture.get());
2389
- }
2390
- return this._scatterRampTexture.get();
2391
- }
2392
- _getAbsorptionRampTexture(device) {
2393
- if (!this._absorptionRampTexture.get()) {
2394
- if (!WaterMaterial._defaultAbsorptionRampTexture.get()) {
2395
- WaterMaterial._defaultAbsorptionRampTexture.set(this._getRampTexture(device, WaterMaterial._absorptionGrad));
2215
+ const interaction = this._interaction.get();
2216
+ if (interaction && this._interactiveWaveGenerator) {
2217
+ const updateFrameId = WaterMaterial._interactionUpdateState.get(interaction);
2218
+ if (updateFrameId !== frameId) {
2219
+ RenderGraphExecutor.profileScope('WaterInteraction', ()=>interaction.update(elapsed));
2220
+ WaterMaterial._interactionUpdateState.set(interaction, frameId);
2396
2221
  }
2397
- this._absorptionRampTexture.set(WaterMaterial._defaultAbsorptionRampTexture.get());
2398
2222
  }
2399
- return this._absorptionRampTexture.get();
2400
2223
  }
2401
2224
  }
2402
2225