@zephyr3d/scene 0.9.26 → 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 (506) 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 +146 -30
  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 +300 -56
  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 +177 -3
  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 +178 -200
  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 +30475 -19865
  109. package/dist/index.js +67 -12
  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/hairstrand.js +235 -235
  118. package/dist/material/hairstrand.js.map +1 -1
  119. package/dist/material/lambert.js +4 -1
  120. package/dist/material/lambert.js.map +1 -1
  121. package/dist/material/material.js +362 -300
  122. package/dist/material/material.js.map +1 -1
  123. package/dist/material/meshmaterial.js +118 -45
  124. package/dist/material/meshmaterial.js.map +1 -1
  125. package/dist/material/mixins/lightmodel/blinnphong.js +1 -1
  126. package/dist/material/mixins/lightmodel/blinnphong.js.map +1 -1
  127. package/dist/material/mixins/lightmodel/lambert.js +1 -1
  128. package/dist/material/mixins/lightmodel/lambert.js.map +1 -1
  129. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js +34 -61
  130. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js.map +1 -1
  131. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js +16 -2
  132. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js.map +1 -1
  133. package/dist/material/mixins/lightmodel/pbrspecularglossness.js +2 -1
  134. package/dist/material/mixins/lightmodel/pbrspecularglossness.js.map +1 -1
  135. package/dist/material/mixins/lit.js +223 -83
  136. package/dist/material/mixins/lit.js.map +1 -1
  137. package/dist/material/mixins/pbr/common.js +31 -206
  138. package/dist/material/mixins/pbr/common.js.map +1 -1
  139. package/dist/material/mtoon.js +1 -1
  140. package/dist/material/mtoon.js.map +1 -1
  141. package/dist/material/pbrmr.js +3 -0
  142. package/dist/material/pbrmr.js.map +1 -1
  143. package/dist/material/pbrsg.js +3 -0
  144. package/dist/material/pbrsg.js.map +1 -1
  145. package/dist/material/sdf_debug.js +78 -0
  146. package/dist/material/sdf_debug.js.map +1 -0
  147. package/dist/material/shader/helper.js +733 -190
  148. package/dist/material/shader/helper.js.map +1 -1
  149. package/dist/material/shader/terrain_blend.js +204 -43
  150. package/dist/material/shader/terrain_blend.js.map +1 -1
  151. package/dist/material/skin.js +331 -297
  152. package/dist/material/skin.js.map +1 -1
  153. package/dist/material/skinprofile.js +1058 -0
  154. package/dist/material/skinprofile.js.map +1 -0
  155. package/dist/material/sss.js +353 -0
  156. package/dist/material/sss.js.map +1 -0
  157. package/dist/material/sss_kernel.js +265 -0
  158. package/dist/material/sss_kernel.js.map +1 -0
  159. package/dist/material/sssprofile.js +879 -0
  160. package/dist/material/sssprofile.js.map +1 -0
  161. package/dist/material/subsurfaceprofile.js +4 -8
  162. package/dist/material/subsurfaceprofile.js.map +1 -1
  163. package/dist/material/terrain-cm.js +464 -188
  164. package/dist/material/terrain-cm.js.map +1 -1
  165. package/dist/material/unlit.js +3 -0
  166. package/dist/material/unlit.js.map +1 -1
  167. package/dist/material/water.js +2009 -201
  168. package/dist/material/water.js.map +1 -1
  169. package/dist/node_modules/meshoptimizer/LICENSE +21 -0
  170. package/dist/node_modules/meshoptimizer/meshopt_decoder.js +197 -0
  171. package/dist/node_modules/meshoptimizer/meshopt_decoder.js.map +1 -0
  172. package/dist/node_modules/meshoptimizer/meshopt_simplifier.js +677 -0
  173. package/dist/node_modules/meshoptimizer/meshopt_simplifier.js.map +1 -0
  174. package/dist/physics/character.js +226 -0
  175. package/dist/physics/character.js.map +1 -0
  176. package/dist/physics/collider.js +209 -0
  177. package/dist/physics/collider.js.map +1 -0
  178. package/dist/physics/events.js +79 -0
  179. package/dist/physics/events.js.map +1 -0
  180. package/dist/physics/joint.js +272 -0
  181. package/dist/physics/joint.js.map +1 -0
  182. package/dist/physics/node_physics.js +234 -0
  183. package/dist/physics/node_physics.js.map +1 -0
  184. package/dist/physics/part.js +91 -0
  185. package/dist/physics/part.js.map +1 -0
  186. package/dist/physics/rigid_body.js +379 -0
  187. package/dist/physics/rigid_body.js.map +1 -0
  188. package/dist/physics/vehicle.js +335 -0
  189. package/dist/physics/vehicle.js.map +1 -0
  190. package/dist/physics/world.js +25 -0
  191. package/dist/physics/world.js.map +1 -0
  192. package/dist/posteffect/bloom.js +49 -14
  193. package/dist/posteffect/bloom.js.map +1 -1
  194. package/dist/posteffect/motionblur.js +668 -63
  195. package/dist/posteffect/motionblur.js.map +1 -1
  196. package/dist/posteffect/posteffect.js +12 -1
  197. package/dist/posteffect/posteffect.js.map +1 -1
  198. package/dist/posteffect/postsss.js +572 -0
  199. package/dist/posteffect/postsss.js.map +1 -0
  200. package/dist/posteffect/sao.js +48 -50
  201. package/dist/posteffect/sao.js.map +1 -1
  202. package/dist/posteffect/skinsss.js +605 -355
  203. package/dist/posteffect/skinsss.js.map +1 -1
  204. package/dist/posteffect/ssgi.js +91 -162
  205. package/dist/posteffect/ssgi.js.map +1 -1
  206. package/dist/posteffect/ssr.js +3 -11
  207. package/dist/posteffect/ssr.js.map +1 -1
  208. package/dist/posteffect/sss.js +34 -15
  209. package/dist/posteffect/sss.js.map +1 -1
  210. package/dist/posteffect/taa.js +1 -0
  211. package/dist/posteffect/taa.js.map +1 -1
  212. package/dist/render/abuffer_oit.js +40 -39
  213. package/dist/render/abuffer_oit.js.map +1 -1
  214. package/dist/render/clipmap.js +328 -62
  215. package/dist/render/clipmap.js.map +1 -1
  216. package/dist/render/cluster_light.js +958 -224
  217. package/dist/render/cluster_light.js.map +1 -1
  218. package/dist/render/cull_visitor.js +26 -5
  219. package/dist/render/cull_visitor.js.map +1 -1
  220. package/dist/render/debug_views.js +37 -0
  221. package/dist/render/debug_views.js.map +1 -0
  222. package/dist/render/depthpass.js +5 -1
  223. package/dist/render/depthpass.js.map +1 -1
  224. package/dist/render/drawable_mixin.js +53 -6
  225. package/dist/render/drawable_mixin.js.map +1 -1
  226. package/dist/render/dualdepthpeeling_oit.js +53 -53
  227. package/dist/render/dualdepthpeeling_oit.js.map +1 -1
  228. package/dist/render/envlight.js +196 -113
  229. package/dist/render/envlight.js.map +1 -1
  230. package/dist/render/fbm_wavegenerator.js +8 -13
  231. package/dist/render/fbm_wavegenerator.js.map +1 -1
  232. package/dist/render/fft_wavegenerator.js +268 -136
  233. package/dist/render/fft_wavegenerator.js.map +1 -1
  234. package/dist/render/gerstner_wavegenerator.js +113 -28
  235. package/dist/render/gerstner_wavegenerator.js.map +1 -1
  236. package/dist/render/gi/probe_volume.js +1334 -0
  237. package/dist/render/gi/probe_volume.js.map +1 -0
  238. package/dist/render/gi/scene_gi.js +282 -0
  239. package/dist/render/gi/scene_gi.js.map +1 -0
  240. package/dist/render/gi/voxel_atlas.js +198 -0
  241. package/dist/render/gi/voxel_atlas.js.map +1 -0
  242. package/dist/render/gi/voxel_lighting.js +652 -0
  243. package/dist/render/gi/voxel_lighting.js.map +1 -0
  244. package/dist/render/gi/voxel_scene.js +642 -0
  245. package/dist/render/gi/voxel_scene.js.map +1 -0
  246. package/dist/render/gi/voxelize_pass.js +223 -0
  247. package/dist/render/gi/voxelize_pass.js.map +1 -0
  248. package/dist/render/globalbindgroup_allocator.js +3 -2
  249. package/dist/render/globalbindgroup_allocator.js.map +1 -1
  250. package/dist/render/gpu_picking.js +9 -36
  251. package/dist/render/gpu_picking.js.map +1 -1
  252. package/dist/render/hzb.js +80 -22
  253. package/dist/render/hzb.js.map +1 -1
  254. package/dist/render/instance_culling.js +499 -0
  255. package/dist/render/instance_culling.js.map +1 -0
  256. package/dist/render/lightpass.js +23 -9
  257. package/dist/render/lightpass.js.map +1 -1
  258. package/dist/render/lod.js +46 -0
  259. package/dist/render/lod.js.map +1 -0
  260. package/dist/render/oit.js +11 -11
  261. package/dist/render/oit.js.map +1 -1
  262. package/dist/render/primitive.js +301 -9
  263. package/dist/render/primitive.js.map +1 -1
  264. package/dist/render/primitive_readback.js +209 -0
  265. package/dist/render/primitive_readback.js.map +1 -0
  266. package/dist/render/render_queue.js +198 -11
  267. package/dist/render/render_queue.js.map +1 -1
  268. package/dist/render/renderer.js +14 -7
  269. package/dist/render/renderer.js.map +1 -1
  270. package/dist/render/rendergraph/blackboard.js +15 -1
  271. package/dist/render/rendergraph/blackboard.js.map +1 -1
  272. package/dist/render/rendergraph/executor.js +136 -0
  273. package/dist/render/rendergraph/executor.js.map +1 -1
  274. package/dist/render/rendergraph/forward_plus_builder.js +585 -59
  275. package/dist/render/rendergraph/forward_plus_builder.js.map +1 -1
  276. package/dist/render/rendergraph/frame_resource_requirements.js +1 -0
  277. package/dist/render/rendergraph/frame_resource_requirements.js.map +1 -1
  278. package/dist/render/rendergraph/history_resources.js +8 -1
  279. package/dist/render/rendergraph/history_resources.js.map +1 -1
  280. package/dist/render/rendergraph/scene_render_context.js +31 -2
  281. package/dist/render/rendergraph/scene_render_context.js.map +1 -1
  282. package/dist/render/rendergraph/types.js.map +1 -1
  283. package/dist/render/renderpass.js +28 -5
  284. package/dist/render/renderpass.js.map +1 -1
  285. package/dist/render/sdf/box_allocator.js +98 -0
  286. package/dist/render/sdf/box_allocator.js.map +1 -0
  287. package/dist/render/sdf/global_sdf.js +865 -0
  288. package/dist/render/sdf/global_sdf.js.map +1 -0
  289. package/dist/render/sdf/scene_sdf.js +562 -0
  290. package/dist/render/sdf/scene_sdf.js.map +1 -0
  291. package/dist/render/sdf/sdf_atlas.js +227 -0
  292. package/dist/render/sdf/sdf_atlas.js.map +1 -0
  293. package/dist/render/sdf/sdf_programs.js +42 -0
  294. package/dist/render/sdf/sdf_programs.js.map +1 -0
  295. package/dist/render/sdf/sdf_view.js +199 -0
  296. package/dist/render/sdf/sdf_view.js.map +1 -0
  297. package/dist/render/sdf/waterline.js +610 -0
  298. package/dist/render/sdf/waterline.js.map +1 -0
  299. package/dist/render/shadow_mask_pass.js +142 -25
  300. package/dist/render/shadow_mask_pass.js.map +1 -1
  301. package/dist/render/shore_wave_generator.js +1193 -0
  302. package/dist/render/shore_wave_generator.js.map +1 -0
  303. package/dist/render/sky.js +516 -86
  304. package/dist/render/sky.js.map +1 -1
  305. package/dist/render/transmission_thickness_pass.js +477 -0
  306. package/dist/render/transmission_thickness_pass.js.map +1 -0
  307. package/dist/render/underwater.js +524 -0
  308. package/dist/render/underwater.js.map +1 -0
  309. package/dist/render/virtualtexture/virtual_texture.js +1214 -0
  310. package/dist/render/virtualtexture/virtual_texture.js.map +1 -0
  311. package/dist/render/volumetric_cloud/cloud_common.js +472 -0
  312. package/dist/render/volumetric_cloud/cloud_common.js.map +1 -0
  313. package/dist/render/volumetric_cloud/cloud_noise.js +189 -0
  314. package/dist/render/volumetric_cloud/cloud_noise.js.map +1 -0
  315. package/dist/render/volumetric_cloud/debug_view.js +131 -0
  316. package/dist/render/volumetric_cloud/debug_view.js.map +1 -0
  317. package/dist/render/volumetric_cloud/resources.js +380 -0
  318. package/dist/render/volumetric_cloud/resources.js.map +1 -0
  319. package/dist/render/volumetric_cloud/sky_cache.js +380 -0
  320. package/dist/render/volumetric_cloud/sky_cache.js.map +1 -0
  321. package/dist/render/volumetric_cloud/volumetric_cloud.js +318 -0
  322. package/dist/render/volumetric_cloud/volumetric_cloud.js.map +1 -0
  323. package/dist/render/water_caustics.js +1195 -0
  324. package/dist/render/water_caustics.js.map +1 -0
  325. package/dist/render/water_foam.js +627 -0
  326. package/dist/render/water_foam.js.map +1 -0
  327. package/dist/render/water_interaction.js +1795 -0
  328. package/dist/render/water_interaction.js.map +1 -0
  329. package/dist/render/weightedblended_oit.js +36 -36
  330. package/dist/render/weightedblended_oit.js.map +1 -1
  331. package/dist/scene/basesprite.js +9 -4
  332. package/dist/scene/basesprite.js.map +1 -1
  333. package/dist/scene/batchgroup.js +108 -12
  334. package/dist/scene/batchgroup.js.map +1 -1
  335. package/dist/scene/environment.js +28 -7
  336. package/dist/scene/environment.js.map +1 -1
  337. package/dist/scene/foliage.js +1020 -0
  338. package/dist/scene/foliage.js.map +1 -0
  339. package/dist/scene/foliage_buckets.js +147 -0
  340. package/dist/scene/foliage_buckets.js.map +1 -0
  341. package/dist/scene/foliage_mask.js +358 -0
  342. package/dist/scene/foliage_mask.js.map +1 -0
  343. package/dist/scene/hair.js +15 -4
  344. package/dist/scene/hair.js.map +1 -1
  345. package/dist/scene/light.js +179 -13
  346. package/dist/scene/light.js.map +1 -1
  347. package/dist/scene/mesh.js +327 -45
  348. package/dist/scene/mesh.js.map +1 -1
  349. package/dist/scene/meshdrawable.js +6 -0
  350. package/dist/scene/meshdrawable.js.map +1 -1
  351. package/dist/scene/msdftext.js +9 -3
  352. package/dist/scene/msdftext.js.map +1 -1
  353. package/dist/scene/msdftextsprite.js +9 -3
  354. package/dist/scene/msdftextsprite.js.map +1 -1
  355. package/dist/scene/particlesys.js +8 -2
  356. package/dist/scene/particlesys.js.map +1 -1
  357. package/dist/scene/physics_settings.js +157 -0
  358. package/dist/scene/physics_settings.js.map +1 -0
  359. package/dist/scene/scene.js +190 -44
  360. package/dist/scene/scene.js.map +1 -1
  361. package/dist/scene/scene_node.js +147 -6
  362. package/dist/scene/scene_node.js.map +1 -1
  363. package/dist/scene/terrain-cm/bladegrassmaterial.js +735 -0
  364. package/dist/scene/terrain-cm/bladegrassmaterial.js.map +1 -0
  365. package/dist/scene/terrain-cm/grass.js +881 -42
  366. package/dist/scene/terrain-cm/grass.js.map +1 -1
  367. package/dist/scene/terrain-cm/grass_gpu.js +929 -0
  368. package/dist/scene/terrain-cm/grass_gpu.js.map +1 -0
  369. package/dist/scene/terrain-cm/grassmaterial.js +180 -9
  370. package/dist/scene/terrain-cm/grassmaterial.js.map +1 -1
  371. package/dist/scene/terrain-cm/terrain-cm.js +465 -36
  372. package/dist/scene/terrain-cm/terrain-cm.js.map +1 -1
  373. package/dist/scene/terrain-cm/terrain_gi_albedo.js +257 -0
  374. package/dist/scene/terrain-cm/terrain_gi_albedo.js.map +1 -0
  375. package/dist/scene/terrain-cm/terrain_rvt.js +395 -0
  376. package/dist/scene/terrain-cm/terrain_rvt.js.map +1 -0
  377. package/dist/scene/update_queue.js +54 -0
  378. package/dist/scene/update_queue.js.map +1 -0
  379. package/dist/scene/water.js +997 -96
  380. package/dist/scene/water.js.map +1 -1
  381. package/dist/scene/water_surface_sampler.js +324 -0
  382. package/dist/scene/water_surface_sampler.js.map +1 -0
  383. package/dist/scene/wind.js +317 -0
  384. package/dist/scene/wind.js.map +1 -0
  385. package/dist/shaders/atmosphere.js +388 -166
  386. package/dist/shaders/atmosphere.js.map +1 -1
  387. package/dist/shaders/fog.js +100 -32
  388. package/dist/shaders/fog.js.map +1 -1
  389. package/dist/shaders/gi.js +365 -0
  390. package/dist/shaders/gi.js.map +1 -0
  391. package/dist/shaders/ltc_rect.js +202 -0
  392. package/dist/shaders/ltc_rect.js.map +1 -0
  393. package/dist/shaders/misc.js +1 -10
  394. package/dist/shaders/misc.js.map +1 -1
  395. package/dist/shaders/noise.js +5 -1
  396. package/dist/shaders/noise.js.map +1 -1
  397. package/dist/shaders/probes.js +309 -0
  398. package/dist/shaders/probes.js.map +1 -0
  399. package/dist/shaders/sdf.js +537 -0
  400. package/dist/shaders/sdf.js.map +1 -0
  401. package/dist/shaders/shadow.js +104 -70
  402. package/dist/shaders/shadow.js.map +1 -1
  403. package/dist/shaders/skin_brdf.js +243 -0
  404. package/dist/shaders/skin_brdf.js.map +1 -0
  405. package/dist/shaders/ssr.js +6 -159
  406. package/dist/shaders/ssr.js.map +1 -1
  407. package/dist/shaders/temporal.js +38 -27
  408. package/dist/shaders/temporal.js.map +1 -1
  409. package/dist/shaders/water.js +40 -87
  410. package/dist/shaders/water.js.map +1 -1
  411. package/dist/shaders/water_medium.js +42 -0
  412. package/dist/shaders/water_medium.js.map +1 -0
  413. package/dist/shadow/dom.js +110 -19
  414. package/dist/shadow/dom.js.map +1 -1
  415. package/dist/shadow/esm.js +3 -3
  416. package/dist/shadow/esm.js.map +1 -1
  417. package/dist/shadow/pcf_opt.js +15 -48
  418. package/dist/shadow/pcf_opt.js.map +1 -1
  419. package/dist/shadow/pcf_pd.js +15 -53
  420. package/dist/shadow/pcf_pd.js.map +1 -1
  421. package/dist/shadow/pcss.js +34 -9
  422. package/dist/shadow/pcss.js.map +1 -1
  423. package/dist/shadow/shadow_impl.js +3 -8
  424. package/dist/shadow/shadow_impl.js.map +1 -1
  425. package/dist/shadow/shadow_region.js +87 -49
  426. package/dist/shadow/shadow_region.js.map +1 -1
  427. package/dist/shadow/shadowmapper.js +93 -53
  428. package/dist/shadow/shadowmapper.js.map +1 -1
  429. package/dist/shadow/ssm.js +18 -72
  430. package/dist/shadow/ssm.js.map +1 -1
  431. package/dist/shadow/vsm.js +3 -3
  432. package/dist/shadow/vsm.js.map +1 -1
  433. package/dist/shapes/shape.js +44 -1
  434. package/dist/shapes/shape.js.map +1 -1
  435. package/dist/utility/buoyancy.js +287 -0
  436. package/dist/utility/buoyancy.js.map +1 -0
  437. package/dist/utility/physical.js +10 -1
  438. package/dist/utility/physical.js.map +1 -1
  439. package/dist/utility/pmrem.js +17 -43
  440. package/dist/utility/pmrem.js.map +1 -1
  441. package/dist/utility/sdf/bake.js +435 -0
  442. package/dist/utility/sdf/bake.js.map +1 -0
  443. package/dist/utility/sdf/bvh.js +576 -0
  444. package/dist/utility/sdf/bvh.js.map +1 -0
  445. package/dist/utility/serialization/manager.js +36 -6
  446. package/dist/utility/serialization/manager.js.map +1 -1
  447. package/dist/utility/serialization/scene/animation.js +77 -7
  448. package/dist/utility/serialization/scene/animation.js.map +1 -1
  449. package/dist/utility/serialization/scene/batch.js +11 -4
  450. package/dist/utility/serialization/scene/batch.js.map +1 -1
  451. package/dist/utility/serialization/scene/camera.js +140 -279
  452. package/dist/utility/serialization/scene/camera.js.map +1 -1
  453. package/dist/utility/serialization/scene/cloth.js +25 -0
  454. package/dist/utility/serialization/scene/cloth.js.map +1 -0
  455. package/dist/utility/serialization/scene/common.js +14 -6
  456. package/dist/utility/serialization/scene/common.js.map +1 -1
  457. package/dist/utility/serialization/scene/foliage.js +821 -0
  458. package/dist/utility/serialization/scene/foliage.js.map +1 -0
  459. package/dist/utility/serialization/scene/hair.js +11 -4
  460. package/dist/utility/serialization/scene/hair.js.map +1 -1
  461. package/dist/utility/serialization/scene/light.js +115 -7
  462. package/dist/utility/serialization/scene/light.js.map +1 -1
  463. package/dist/utility/serialization/scene/material.js +388 -220
  464. package/dist/utility/serialization/scene/material.js.map +1 -1
  465. package/dist/utility/serialization/scene/mesh.js +69 -4
  466. package/dist/utility/serialization/scene/mesh.js.map +1 -1
  467. package/dist/utility/serialization/scene/node.js +140 -5
  468. package/dist/utility/serialization/scene/node.js.map +1 -1
  469. package/dist/utility/serialization/scene/node_physics.js +1318 -0
  470. package/dist/utility/serialization/scene/node_physics.js.map +1 -0
  471. package/dist/utility/serialization/scene/particle.js +11 -4
  472. package/dist/utility/serialization/scene/particle.js.map +1 -1
  473. package/dist/utility/serialization/scene/physics.js +120 -0
  474. package/dist/utility/serialization/scene/physics.js.map +1 -0
  475. package/dist/utility/serialization/scene/primitive.js +124 -10
  476. package/dist/utility/serialization/scene/primitive.js.map +1 -1
  477. package/dist/utility/serialization/scene/scene.js +1687 -51
  478. package/dist/utility/serialization/scene/scene.js.map +1 -1
  479. package/dist/utility/serialization/scene/sprite.js +11 -4
  480. package/dist/utility/serialization/scene/sprite.js.map +1 -1
  481. package/dist/utility/serialization/scene/terrain.js +163 -33
  482. package/dist/utility/serialization/scene/terrain.js.map +1 -1
  483. package/dist/utility/serialization/scene/text.js +11 -4
  484. package/dist/utility/serialization/scene/text.js.map +1 -1
  485. package/dist/utility/serialization/scene/water.js +2473 -114
  486. package/dist/utility/serialization/scene/water.js.map +1 -1
  487. package/dist/utility/serialization/types.js.map +1 -1
  488. package/dist/utility/shprojector.js +97 -109
  489. package/dist/utility/shprojector.js.map +1 -1
  490. package/dist/utility/textures/foam.js +170 -0
  491. package/dist/utility/textures/foam.js.map +1 -0
  492. package/dist/utility/textures/ggxlut.js +17 -43
  493. package/dist/utility/textures/ggxlut.js.map +1 -1
  494. package/dist/utility/textures/ltcdata.js +4 -4
  495. package/dist/utility/textures/ltcdata.js.map +1 -1
  496. package/dist/utility/textures/ltclut.js +16 -6
  497. package/dist/utility/textures/ltclut.js.map +1 -1
  498. package/dist/utility/unsupported.js +24 -0
  499. package/dist/utility/unsupported.js.map +1 -0
  500. package/dist/values.js +13 -1
  501. package/dist/values.js.map +1 -1
  502. package/package.json +25 -12
  503. package/src/asset/loaders/ktx2/basis/LICENSE +201 -0
  504. package/src/asset/loaders/ktx2/basis/basis_transcoder.js +2 -0
  505. package/src/asset/loaders/ktx2/basis/basis_transcoder.wasm +0 -0
  506. package/src/asset/meshoptimizer/LICENSE +21 -0
@@ -1,17 +1,19 @@
1
- import { REVERSE_Z, Vector4, DEPTH_CLEAR_VALUE, DEPTH_COMPARE_DEFAULT } from '@zephyr3d/base';
1
+ import { Vector4, DEPTH_CLEAR_VALUE, DEPTH_COMPARE_DEFAULT } from '@zephyr3d/base';
2
2
  import { RenderQueue } from '../render_queue.js';
3
3
  import { LightPass } from '../lightpass.js';
4
4
  import { ShadowMapPass } from '../shadowmap_pass.js';
5
5
  import { DepthPass } from '../depthpass.js';
6
6
  import { ClusteredLight } from '../cluster_light.js';
7
7
  import { ShadowMaskRenderer } from '../shadow_mask_pass.js';
8
- import { buildHiZ } from '../hzb.js';
8
+ import { TransmissionThicknessRenderer } from '../transmission_thickness_pass.js';
9
+ import { getHiZFormat, buildHiZ } from '../hzb.js';
9
10
  import '../primitive.js';
10
11
  import '@zephyr3d/device';
11
12
  import { CopyBlitter } from '../../blitter/copy.js';
12
13
  import { fetchSampler } from '../../utility/misc.js';
13
14
  import { MaterialVaryingFlags } from '../../values.js';
14
- import { ShaderHelper } from '../../material/shader/helper.js';
15
+ import { ShaderHelper, MAX_CAUSTIC_WATERS } from '../../material/shader/helper.js';
16
+ import { REFRACT_BLUR_MAX_LOD } from '../../material/water.js';
15
17
  import { PostEffectLayer, AbstractPostEffect } from '../../posteffect/posteffect.js';
16
18
  import { RenderGraph } from './rendergraph.js';
17
19
  import { RenderGraphExecutor } from './executor.js';
@@ -24,6 +26,7 @@ import { OrderingScope } from './render_context.js';
24
26
  import { RenderPipeline } from './render_pipeline.js';
25
27
  import { mergeFrameResourceRequirements } from './frame_resource_requirements.js';
26
28
  import { renderObjectColors } from '../gpu_picking.js';
29
+ import '../../node_modules/meshoptimizer/meshopt_simplifier.js';
27
30
  import { BoxShape } from '../../shapes/box.js';
28
31
  import '../../shapes/capsule.js';
29
32
  import '../../shapes/cylinder.js';
@@ -31,13 +34,24 @@ import '../../shapes/torus.js';
31
34
  import '../../shapes/plane.js';
32
35
  import '../../shapes/sphere.js';
33
36
  import '../../shapes/tetrahedron.js';
37
+ import { WaterCausticsRenderer } from '../water_caustics.js';
38
+ import { UnderwaterRenderer, selectUnderwaterSource } from '../underwater.js';
34
39
 
35
40
  const _scenePass = new LightPass();
36
41
  const _depthPass = new DepthPass();
37
42
  const _shadowMapPass = new ShadowMapPass();
38
- const _clusters = [];
39
43
  const _shadowMaskRenderer = new ShadowMaskRenderer();
44
+ const _transmissionThicknessRenderer = new TransmissionThicknessRenderer();
45
+ const _waterCausticsRenderer = new WaterCausticsRenderer();
46
+ const _underwaterRenderer = new UnderwaterRenderer();
40
47
  const _devicePoolAllocator = new DevicePoolAllocator();
48
+ /**
49
+ * Clear value for the depth prepass's skin profile id target.
50
+ *
51
+ * @remarks
52
+ * 0 is "not skin". It has to differ from the depth target's clear, which is the
53
+ * far plane rather than an id.
54
+ */ const SKIN_PROFILE_ID_CLEAR = new Vector4(0, 0, 0, 0);
41
55
  const _textureAffinityCaches = new WeakMap();
42
56
  function getTextureAffinityCache(camera, device) {
43
57
  const current = _textureAffinityCaches.get(camera);
@@ -54,20 +68,14 @@ function getTextureAffinityCache(camera, device) {
54
68
  let _backDepthColorState = null;
55
69
  let _frontDepthColorState = null;
56
70
  const SURFACE_MRT_FLAGS = MaterialVaryingFlags.SCENE_STORE_ROUGHNESS | MaterialVaryingFlags.SSS_STORE_PROFILE | MaterialVaryingFlags.SSS_STORE_DIFFUSE | MaterialVaryingFlags.SCENE_STORE_NORMAL | MaterialVaryingFlags.SSS_STORE_TRANSMISSION | MaterialVaryingFlags.SKIN_SSS_STORE;
57
- function getClusteredLight() {
58
- return _clusters.length > 0 ? _clusters.pop() : new ClusteredLight();
59
- }
60
- function freeClusteredLight(cl) {
61
- _clusters.push(cl);
62
- }
63
71
  function getCoreMaterial(material) {
64
72
  return material?.coreMaterial ?? material ?? null;
65
73
  }
66
74
  function hasSSSMaterialCore(material) {
67
75
  return !!getCoreMaterial(material)?.subsurfaceProfile;
68
76
  }
69
- function hasSkinSSSMaterialCore(material) {
70
- return !!getCoreMaterial(material)?.skinSSS;
77
+ function hasPostSSSMaterialCore(material) {
78
+ return !!getCoreMaterial(material)?.postSSS;
71
79
  }
72
80
  function renderQueueHasActiveSSS(renderQueue) {
73
81
  const itemList = renderQueue.itemList;
@@ -87,7 +95,7 @@ function renderQueueHasActiveSSS(renderQueue) {
87
95
  }
88
96
  return false;
89
97
  }
90
- function renderQueueHasActiveSkinSSS(renderQueue) {
98
+ function renderQueueHasActivePostSSS(renderQueue) {
91
99
  const itemList = renderQueue.itemList;
92
100
  if (!itemList) {
93
101
  return false;
@@ -98,7 +106,7 @@ function renderQueueHasActiveSkinSSS(renderQueue) {
98
106
  ];
99
107
  for (const list of lists){
100
108
  for (const material of list.materialList){
101
- if (hasSkinSSSMaterialCore(material)) {
109
+ if (hasPostSSSMaterialCore(material)) {
102
110
  return true;
103
111
  }
104
112
  }
@@ -124,8 +132,10 @@ function cloneActualSSSListInfo(source, _targetQueue) {
124
132
  morphItemList: filterActualSSSItemList(source.morphItemList),
125
133
  skinAndMorphItemList: filterActualSSSItemList(source.skinAndMorphItemList),
126
134
  instanceItemList: filterActualSSSItemList(source.instanceItemList),
135
+ skinInstanceItemList: filterActualSSSItemList(source.skinInstanceItemList),
127
136
  materialList: filterActualSSSMaterialList(source.materialList),
128
137
  instanceList: {},
138
+ instanceLods: {},
129
139
  renderQueue: source.renderQueue
130
140
  };
131
141
  }
@@ -136,7 +146,7 @@ function cloneActualSSSBundle(source, targetQueue) {
136
146
  };
137
147
  }
138
148
  function hasAnyActualSSSItems(renderItems) {
139
- return renderItems.some((info)=>info.itemList.length > 0 || info.skinItemList.length > 0 || info.morphItemList.length > 0 || info.skinAndMorphItemList.length > 0 || info.instanceItemList.length > 0);
149
+ return renderItems.some((info)=>info.itemList.length > 0 || info.skinItemList.length > 0 || info.morphItemList.length > 0 || info.skinAndMorphItemList.length > 0 || info.instanceItemList.length > 0 || info.skinInstanceItemList.length > 0);
140
150
  }
141
151
  function createActualSSSRenderQueue(renderQueue) {
142
152
  const itemList = renderQueue.itemList;
@@ -210,6 +220,21 @@ function getSSSLightingTextureFormat(ctx, attachmentCount, surfaceAttachmentCoun
210
220
  function getFullMipLevelCount(width, height) {
211
221
  return Math.max(1, Math.floor(Math.log2(Math.max(1, width, height))) + 1);
212
222
  }
223
+ /**
224
+ * Levels the scene colour copy is allocated with.
225
+ *
226
+ * Deep enough to serve the refraction blur, which clamps its own LOD, and no
227
+ * deeper: the coarsest levels of a full chain are averages of the whole frame,
228
+ * which is not a plausible answer for "what is behind this water", and every
229
+ * level costs bandwidth to generate on each grab. Capped against the full chain
230
+ * so a small render target stays legal.
231
+ *
232
+ * Derived from the water material's clamp rather than restated, so the two
233
+ * cannot drift: a level count one short of it would leave the sampler asking for
234
+ * a level that does not exist.
235
+ */ function getSceneColorMipLevelCount(width, height) {
236
+ return Math.min(REFRACT_BLUR_MAX_LOD + 1, getFullMipLevelCount(width, height));
237
+ }
213
238
  function hasSurfaceMRT(ctx) {
214
239
  return !!(ctx.materialFlags & SURFACE_MRT_FLAGS);
215
240
  }
@@ -233,21 +258,27 @@ function getLightPassColorAttachments(ctx, colorAttachment) {
233
258
  attachments.push(ctx.SSSTransmissionTexture);
234
259
  }
235
260
  if (ctx.materialFlags & MaterialVaryingFlags.SKIN_SSS_STORE) {
236
- attachments.push(ctx.SkinSSSTexture);
261
+ attachments.push(ctx.SSSMaskTexture);
237
262
  }
238
263
  return attachments.length === 1 ? attachments[0] : attachments;
239
264
  }
240
- /** Derive Forward+ options from scene and camera state. @internal */ function deriveForwardPlusOptions(scene, camera, deviceType, renderQueue) {
265
+ /** Derive Forward+ options from scene and camera state. @internal */ function deriveForwardPlusOptions(scene, camera, _deviceType, renderQueue) {
241
266
  const ssr = camera.SSR && scene.env.light.envLight && scene.env.light.envLight.hasRadiance();
242
267
  const ssgi = camera.SSGI && camera.HDR && camera.ssgiIntensity > 0 && scene.env.light.type === 'ibl' && !!scene.env.light.envLight?.hasRadiance() && !!scene.env.light.envLight?.hasIrradiance();
243
268
  const sss = camera.SSS && renderQueueHasActiveSSS(renderQueue);
244
- const skinSSS = camera.skinSSS && renderQueueHasActiveSkinSSS(renderQueue);
269
+ // No camera switch for this one: a skin material always wants its diffusion,
270
+ // and the queue already says whether the frame draws any.
271
+ const postSSS = renderQueueHasActivePostSSS(renderQueue);
245
272
  const needSceneColor = renderQueue.needSceneColor();
246
273
  const needSceneColorWithDepth = renderQueue.needSceneColorWithDepth();
274
+ // Requested by materials rather than by a post effect, so it is resolved here
275
+ // instead of in resolveFrameResourceRequirements.
276
+ const needHiZNearest = renderQueue.needHiZNearest() || camera.HiZNearest;
247
277
  return {
248
278
  depthPrepass: true,
249
279
  motionVectors: false,
250
- hiZ: false,
280
+ hiZ: needHiZNearest,
281
+ hiZNearest: needHiZNearest,
251
282
  sceneNormal: false,
252
283
  sceneRoughness: false,
253
284
  shadowMask: false,
@@ -259,26 +290,22 @@ function getLightPassColorAttachments(ctx, colorAttachment) {
259
290
  needSceneColorWithDepth,
260
291
  needsTransmissionDepthForSSR: !!ssr && needSceneColor && !needSceneColorWithDepth,
261
292
  sss: !!sss,
262
- skinSSS: !!skinSSS,
293
+ postSSS: !!postSSS,
294
+ // Driven entirely by postSSS: the id exists to serve the diffusion and the
295
+ // transmission thickness pass, and both are gated on the same condition.
296
+ sssProfileId: !!postSSS,
263
297
  fogPresents: !!scene.env.sky?.fogPresents
264
298
  };
265
299
  }
266
300
  function resolveFrameResourceRequirements(ctx, options, requirements) {
267
- const deviceType = ctx.device.type;
268
301
  options.motionVectors ||= !!requirements.motionVector;
269
- options.hiZ ||= !!requirements.hiZ;
302
+ options.hiZNearest ||= !!requirements.hiZNearest;
303
+ // The nearest channel lives on the same pyramid, so asking for it is asking
304
+ // for the pyramid.
305
+ options.hiZ ||= !!requirements.hiZ || options.hiZNearest;
270
306
  options.sceneNormal ||= !!requirements.sceneNormal;
271
307
  options.sceneRoughness ||= !!requirements.sceneRoughness;
272
308
  options.shadowMask ||= !!requirements.shadowMask;
273
- if (options.motionVectors && deviceType === 'webgl') {
274
- throw new Error('Forward+: MotionVector was requested but is not supported by the WebGL backend.');
275
- }
276
- if (options.hiZ && deviceType === 'webgl') {
277
- throw new Error('Forward+: HiZ was requested but is not supported by the WebGL backend.');
278
- }
279
- if (options.shadowMask && deviceType === 'webgl') {
280
- throw new Error('Forward+: ShadowMask was requested but is not supported by the WebGL backend.');
281
- }
282
309
  const surfaceAttachmentCount = Number(options.sceneNormal) + Number(options.sceneRoughness);
283
310
  if (surfaceAttachmentCount > 0) {
284
311
  const caps = ctx.device.getDeviceCaps();
@@ -292,6 +319,7 @@ function resolveFrameResourceRequirements(ctx, options, requirements) {
292
319
  }
293
320
  }
294
321
  ctx.motionVectors = options.motionVectors;
322
+ ctx.sssProfileId = options.sssProfileId;
295
323
  ctx.HiZ = options.hiZ;
296
324
  ctx.SSGI = options.ssgi;
297
325
  ctx.screenSpaceShadowMask = options.shadowMask;
@@ -359,8 +387,9 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
359
387
  ordering.emit(builder, 'ClusterLightsDone');
360
388
  builder.sideEffect();
361
389
  builder.setExecute(()=>{
362
- ctx.clusteredLight = getClusteredLight();
363
- ctx.clusteredLight.calculateLightIndex(ctx.camera, renderQueue, ctx.screenSpaceShadowMask, ShaderHelper.getPreExposure(ctx));
390
+ ctx.clusteredLight = ClusteredLight.acquire(ctx.renderWidth, ctx.renderHeight);
391
+ // The light passes draw into targets of the render size (see renderOpaqueScenePass).
392
+ ctx.clusteredLight.calculateLightIndex(ctx.camera, renderQueue, ctx.renderWidth, ctx.renderHeight, ctx.screenSpaceShadowMask, ShaderHelper.getPreExposure(ctx));
364
393
  });
365
394
  });
366
395
  }
@@ -401,12 +430,159 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
401
430
  });
402
431
  }
403
432
  };
433
+ /**
434
+ * Picks the water surface and light the caustic map is built from.
435
+ *
436
+ * Only one pair is handled per frame: the caustics ride on a single light's
437
+ * additive pass, and a second water body would need a second map. The largest
438
+ * visible water surface wins, which is the one a camera near several of them is
439
+ * most likely to be looking through.
440
+ * @internal
441
+ */ function selectWaterCausticSource(ctx, renderQueue) {
442
+ if (renderQueue.waters.length === 0) {
443
+ return null;
444
+ }
445
+ // The caustic light must own an additive shadow pass, because that is the pass
446
+ // the caustic uniforms are declared and bound on.
447
+ const candidate = renderQueue.sunLight ?? renderQueue.primaryDirectionalLight;
448
+ const light = candidate && candidate.castShadow && renderQueue.shadowedLights.indexOf(candidate) >= 0 ? candidate : null;
449
+ if (!light) {
450
+ return null;
451
+ }
452
+ const waters = renderQueue.waters.filter((water)=>WaterCausticsRenderer.canRender(water, light));
453
+ if (waters.length === 0) {
454
+ return null;
455
+ }
456
+ // Largest first, which decides both what survives the cap and which body's
457
+ // settings describe the shared map. Area is a stand-in for "the one the viewer
458
+ // is most likely to be over"; a camera-distance sort would swap the map's
459
+ // whole configuration as the viewer walks between two pools, and swapping it
460
+ // discards the temporal history every time.
461
+ waters.sort((a, b)=>{
462
+ const ra = a.material.region;
463
+ const rb = b.material.region;
464
+ const areaA = Math.max(0, ra.z - ra.x) * Math.max(0, ra.w - ra.y);
465
+ const areaB = Math.max(0, rb.z - rb.x) * Math.max(0, rb.w - rb.y);
466
+ return areaB - areaA;
467
+ });
468
+ return {
469
+ waters: waters.slice(0, MAX_CAUSTIC_WATERS),
470
+ light
471
+ };
472
+ }
473
+ /** @internal */ const WaterCausticsModule = {
474
+ type: 'WaterCaustics',
475
+ writes: [
476
+ FrameResources.WaterCaustics
477
+ ],
478
+ prepare: ({ ctx, renderQueue })=>({
479
+ enabled: !!selectWaterCausticSource(ctx, renderQueue)
480
+ }),
481
+ setup (fg) {
482
+ const { graph, ctx, renderQueue, ordering, blackboard } = fg;
483
+ const source = selectWaterCausticSource(ctx, renderQueue);
484
+ // Map-wide settings come from the largest body; see selectWaterCausticSource.
485
+ const material = source.waters[0].material;
486
+ const size = material.causticsResolution;
487
+ const format = WaterCausticsRenderer.getMapFormat(ctx.device);
488
+ const history = fg.history;
489
+ const temporal = material.causticsTemporalStrength > 0;
490
+ const mapDesc = {
491
+ format,
492
+ sizeMode: 'absolute',
493
+ width: size,
494
+ height: size
495
+ };
496
+ const mapSize = {
497
+ width: size,
498
+ height: size
499
+ };
500
+ // Null on the first frame, after a resize, and whenever the map format
501
+ // changes; the renderer falls back to the current frame alone.
502
+ const previousHandle = temporal && history ? history.importPreviousIfCompatible(graph, RGHistoryResources.WATER_CAUSTICS, mapDesc, mapSize) : null;
503
+ const result = graph.addPass('WaterCaustics', (builder)=>{
504
+ // The wave textures the splat pass samples are updated by the scene, not by
505
+ // the graph, so chain after the passes that have already run rather than
506
+ // declaring a resource dependency on them.
507
+ ordering.chainInto(builder);
508
+ ordering.emit(builder, 'WaterCausticsDone');
509
+ // The map leaves the graph through the draw context rather than through a
510
+ // handle a later pass samples, so nothing would otherwise keep this alive.
511
+ builder.sideEffect();
512
+ // Fixed-size, unlike almost every other graph texture: the map covers a
513
+ // world-space footprint, not the screen.
514
+ const mapHandle = builder.createTexture({
515
+ format,
516
+ sizeMode: 'absolute',
517
+ width: size,
518
+ height: size,
519
+ label: 'waterCaustics',
520
+ allocationKey: 'ForwardPlus.WaterCaustics'
521
+ });
522
+ const scratchHandle = builder.createTexture({
523
+ format,
524
+ sizeMode: 'absolute',
525
+ width: size,
526
+ height: size,
527
+ label: 'waterCausticsScratch',
528
+ allocationKey: 'ForwardPlus.WaterCausticsScratch'
529
+ });
530
+ // The displaced surface height along each map ray, which the receiver
531
+ // gates its depth on. Same slice and size as the map so it shares the
532
+ // map's uv; four channels, one per water slot.
533
+ const heightHandle = builder.createTexture({
534
+ format: WaterCausticsRenderer.getHeightMapFormat(),
535
+ sizeMode: 'absolute',
536
+ width: size,
537
+ height: size,
538
+ label: 'waterCausticHeights',
539
+ allocationKey: 'ForwardPlus.WaterCausticHeights'
540
+ });
541
+ if (previousHandle) {
542
+ builder.read(previousHandle);
543
+ }
544
+ builder.setExecute((rgCtx)=>{
545
+ const map = rgCtx.getTexture(mapHandle);
546
+ const scratch = rgCtx.getTexture(scratchHandle);
547
+ const heights = rgCtx.getTexture(heightHandle);
548
+ const previous = previousHandle ? rgCtx.getTexture(previousHandle) : null;
549
+ const resolved = _waterCausticsRenderer.render(ctx, source.waters, source.light, map, scratch, heights, previous, (texture)=>rgCtx.createFramebuffer({
550
+ width: texture.width,
551
+ height: texture.height,
552
+ colorAttachments: texture,
553
+ depthAttachment: null
554
+ }));
555
+ if (temporal && history) {
556
+ // Commit whichever texture the renderer settled on, so the next frame
557
+ // reprojects the map that was actually shown rather than the
558
+ // pre-resolve one.
559
+ history.queueRetainedCommit(RGHistoryResources.WATER_CAUSTICS, mapDesc, mapSize, resolved);
560
+ }
561
+ // Publish only after the map exists: the light pass keys its shader
562
+ // variant on this flag and binds the texture in the same step.
563
+ ctx.waterCaustics = true;
564
+ ctx.waterCausticLight = source.light;
565
+ ctx.waterCausticTexture = resolved;
566
+ ctx.waterCausticHeightTexture = heights;
567
+ ctx.waterCausticUniforms = _waterCausticsRenderer.uniforms;
568
+ });
569
+ return {
570
+ mapHandle,
571
+ scratchHandle
572
+ };
573
+ });
574
+ // Which target holds the result depends on whether the resolve ran. Both are
575
+ // declared by this pass, so either is a valid handle for consumers to read.
576
+ blackboard.set(FrameResources.WaterCaustics, temporal && previousHandle ? result.scratchHandle : result.mapHandle);
577
+ }
578
+ };
404
579
  /** @internal */ const DepthPrepassModule = {
405
580
  type: 'DepthPrepass',
406
581
  writes: [
407
582
  FrameResources.LinearDepth,
408
583
  FrameResources.MotionVector,
409
- FrameResources.SceneDepthAttachment
584
+ FrameResources.SceneDepthAttachment,
585
+ FrameResources.SSSProfileId
410
586
  ],
411
587
  prepare: ()=>({
412
588
  enabled: true
@@ -415,7 +591,7 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
415
591
  const { graph, ctx, frame, ordering, blackboard, options } = fg;
416
592
  const result = graph.addPass('DepthPrepass', (builder)=>{
417
593
  ordering.chainInto(builder);
418
- const format = ctx.device.type === 'webgl' ? ctx.SSRCalcThickness ? 'rgba16f' : 'rgba8unorm' : ctx.SSRCalcThickness ? 'rg32f' : 'r32f';
594
+ const format = ctx.SSRCalcThickness ? 'rg32f' : 'r32f';
419
595
  const mvFormat = 'rgba16f';
420
596
  const depthHandle = builder.createTexture({
421
597
  format,
@@ -427,6 +603,14 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
427
603
  label: 'motionVector',
428
604
  allocationKey: 'ForwardPlus.MotionVector'
429
605
  }) : undefined;
606
+ // r8unorm is exact for this: the material writes `SSSProfile.encodedId`,
607
+ // i.e. `id / 255`, and the table holds 256 rows, so every representable id
608
+ // survives the round trip. 0 means "not skin".
609
+ const sssProfileIdHandle = options.sssProfileId ? builder.createTexture({
610
+ format: 'r8unorm',
611
+ label: 'sssProfileId',
612
+ allocationKey: 'ForwardPlus.SSSProfileId'
613
+ }) : undefined;
430
614
  const finalDepthAttachment = ctx.finalFramebuffer?.getDepthAttachment();
431
615
  const externalDepthAttachment = finalDepthAttachment?.isTexture2D() ? finalDepthAttachment : null;
432
616
  const externalDepthAttachmentHandle = externalDepthAttachment ? graph.importTexture('externalSceneDepth') : undefined;
@@ -436,14 +620,23 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
436
620
  allocationKey: 'ForwardPlus.SceneDepth'
437
621
  });
438
622
  const depthAttachmentOrFormat = externalDepthAttachmentHandle ?? graphDepthAttachmentHandle ?? ctx.depthFormat;
623
+ // Order matters and has to agree with the fragment output declaration
624
+ // order in MeshMaterial._createProgram: linear depth, then the motion
625
+ // vector, then the profile id.
626
+ const prepassColorAttachments = [
627
+ depthHandle
628
+ ];
629
+ if (motionVectorHandle) {
630
+ prepassColorAttachments.push(motionVectorHandle);
631
+ }
632
+ if (sssProfileIdHandle) {
633
+ prepassColorAttachments.push(sssProfileIdHandle);
634
+ }
439
635
  const depthFramebufferHandle = builder.createFramebuffer({
440
636
  label: 'DepthPrepassFramebuffer',
441
637
  width: ctx.renderWidth,
442
638
  height: ctx.renderHeight,
443
- colorAttachments: motionVectorHandle ? [
444
- depthHandle,
445
- motionVectorHandle
446
- ] : depthHandle,
639
+ colorAttachments: prepassColorAttachments.length === 1 ? depthHandle : prepassColorAttachments,
447
640
  depthAttachment: depthAttachmentOrFormat,
448
641
  ignoreDepthStencil: false
449
642
  });
@@ -471,6 +664,7 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
471
664
  return {
472
665
  depthHandle,
473
666
  motionVectorHandle,
667
+ sssProfileIdHandle: sssProfileIdHandle,
474
668
  graphDepthAttachmentHandle,
475
669
  externalDepthAttachmentHandle,
476
670
  externalDepthAttachment,
@@ -483,6 +677,9 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
483
677
  if (result.motionVectorHandle) {
484
678
  blackboard.set(FrameResources.MotionVector, result.motionVectorHandle);
485
679
  }
680
+ if (result.sssProfileIdHandle) {
681
+ blackboard.set(FrameResources.SSSProfileId, result.sssProfileIdHandle);
682
+ }
486
683
  blackboard.set(FrameResources.SceneDepthAttachment, result.graphDepthAttachmentHandle ?? result.externalDepthAttachmentHandle);
487
684
  // Later modules share this attachment without owning it.
488
685
  fg.state.renderDepthAttachment = result.graphDepthAttachmentHandle ?? result.externalDepthAttachmentHandle ?? null;
@@ -500,7 +697,7 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
500
697
  prepare: ({ ctx, options, renderQueue })=>({
501
698
  enabled: options.shadowMask && renderQueue.shadowedLights.length > 0,
502
699
  requirements: {
503
- shadowMask: ctx.device.type !== 'webgl' && ctx.camera.screenSpaceShadowMask && renderQueue.shadowedLights.length > 0
700
+ shadowMask: ctx.camera.screenSpaceShadowMask && renderQueue.shadowedLights.length > 0
504
701
  }
505
702
  }),
506
703
  setup (fg) {
@@ -539,6 +736,107 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
539
736
  blackboard.set(FrameResources.ShadowMask, maskPassResult.maskHandle);
540
737
  }
541
738
  };
739
+ /** Orders a pass drawing scene materials after this camera's virtual texture update. @internal */ function readVirtualTextureToken(builder, blackboard) {
740
+ const token = blackboard.get(FrameResources.VirtualTexture);
741
+ if (token) {
742
+ builder.read(token);
743
+ }
744
+ }
745
+ /** Scene virtual texture clients active for this camera's frame. @internal */ function activeVirtualTextureClients(ctx, renderQueue) {
746
+ if (ctx.device.type !== 'webgpu') {
747
+ return [];
748
+ }
749
+ return ctx.scene.virtualTextureClients.filter((c)=>c.isActive?.(ctx, renderQueue) ?? true);
750
+ }
751
+ /**
752
+ * Updates the scene's virtual textures for this camera: mark from the depth prepass, map, fill.
753
+ */ const VirtualTextureModule = {
754
+ type: 'VirtualTextureUpdate',
755
+ reads: [
756
+ {
757
+ resource: FrameResources.LinearDepth
758
+ }
759
+ ],
760
+ writes: [
761
+ FrameResources.VirtualTexture
762
+ ],
763
+ prepare: ({ ctx, renderQueue })=>({
764
+ enabled: activeVirtualTextureClients(ctx, renderQueue).length > 0
765
+ }),
766
+ setup (fg) {
767
+ const { graph, ctx, renderQueue, blackboard } = fg;
768
+ const clients = activeVirtualTextureClients(ctx, renderQueue);
769
+ const token = graph.addPass('VirtualTextureUpdate', (builder)=>{
770
+ const depthHandle = blackboard.expect(FrameResources.LinearDepth);
771
+ builder.read(depthHandle);
772
+ // Page tables and pools are scene state the graph does not track
773
+ builder.sideEffect();
774
+ const token = builder.createToken('VirtualTextureDone');
775
+ builder.setExecute((rgCtx)=>{
776
+ const depthTex = rgCtx.getTexture(depthHandle);
777
+ const stamp = ctx.device.frameInfo.frameCounter;
778
+ for (const client of clients){
779
+ client.markFromDepth?.(ctx, depthTex);
780
+ }
781
+ for (const client of clients){
782
+ client.virtualTexture.update(stamp);
783
+ client.fill(ctx);
784
+ }
785
+ });
786
+ return token;
787
+ });
788
+ blackboard.set(FrameResources.VirtualTexture, token);
789
+ }
790
+ };
791
+ /** @internal */ const TransmissionThicknessModule = {
792
+ type: 'TransmissionThicknessPass',
793
+ writes: [
794
+ FrameResources.TransmissionThickness
795
+ ],
796
+ prepare: ({ options, renderQueue })=>({
797
+ // Not gated on the device: transmission lights the subject directly in the
798
+ // base pass, so it does not depend on the WebGPU-only PostSSS diffusion.
799
+ enabled: options.postSSS && renderQueue.shadowedLights.some((light)=>light.transmission)
800
+ }),
801
+ setup (fg) {
802
+ const { graph, ctx, renderQueue, blackboard } = fg;
803
+ const depthPassResult = requireBuildState(fg, 'depth', 'DepthPrepass', 'TransmissionThicknessPass');
804
+ // The ordinal space is shared with the shadow mask so that a light's
805
+ // thickness and its shadow factor are found with the same arithmetic; that
806
+ // means the layer count follows the shadow light count, not the (smaller)
807
+ // transmission light count.
808
+ const numShadowLights = renderQueue.shadowedLights.length;
809
+ const numLayers = ShadowMaskRenderer.getLayerCount(numShadowLights);
810
+ const passResult = graph.addPass('TransmissionThicknessPass', (builder)=>{
811
+ const depthHandle = blackboard.expect(FrameResources.LinearDepth);
812
+ const sssProfileIdHandle = blackboard.expect(FrameResources.SSSProfileId);
813
+ builder.read(depthHandle);
814
+ builder.read(sssProfileIdHandle);
815
+ builder.read(depthPassResult.depthFramebufferHandle);
816
+ const thicknessHandle = builder.createTexture({
817
+ format: 'rgba8unorm',
818
+ label: 'transmissionThickness',
819
+ arrayLayers: numLayers,
820
+ allocationKey: 'ForwardPlus.TransmissionThickness'
821
+ });
822
+ builder.setExecute((rgCtx)=>{
823
+ const depthTex = rgCtx.getTexture(depthHandle);
824
+ const thicknessTex = rgCtx.getTexture(thicknessHandle);
825
+ _transmissionThicknessRenderer.render(ctx, depthTex, rgCtx.getTexture(sssProfileIdHandle), renderQueue.shadowedLights, (layer)=>rgCtx.createFramebuffer({
826
+ width: thicknessTex.width,
827
+ height: thicknessTex.height,
828
+ colorAttachments: thicknessTex,
829
+ depthAttachment: null,
830
+ attachmentLayer: layer
831
+ }));
832
+ });
833
+ return {
834
+ thicknessHandle
835
+ };
836
+ });
837
+ blackboard.set(FrameResources.TransmissionThickness, passResult.thicknessHandle);
838
+ }
839
+ };
542
840
  /** @internal */ const TransmissionDepthForSSRModule = {
543
841
  type: 'TransmissionDepthForSSR',
544
842
  writes: [
@@ -580,7 +878,7 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
580
878
  enabled: options.hiZ
581
879
  }),
582
880
  setup (fg) {
583
- const { graph, ctx, frame, blackboard } = fg;
881
+ const { graph, ctx, frame, blackboard, options } = fg;
584
882
  const depthPassResult = requireBuildState(fg, 'depth', 'DepthPrepass', 'HiZ');
585
883
  const preLightTransmissionDepthToken = fg.state.preLightTransmissionDepthToken;
586
884
  //const hiZWidth = nextPowerOf2(ctx.renderWidth);
@@ -593,11 +891,14 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
593
891
  builder.read(preLightTransmissionDepthToken);
594
892
  }
595
893
  hiZHandle = builder.createTexture({
596
- format: REVERSE_Z ? 'r16f' : 'r32f',
894
+ format: getHiZFormat(options.hiZNearest),
597
895
  label: 'hiZ',
598
896
  sizeMode: 'backbuffer-relative',
599
897
  mipLevels: getFullMipLevelCount(ctx.renderWidth, ctx.renderHeight),
600
- allocationKey: 'ForwardPlus.HiZ'
898
+ // Keyed on the channel count: a one- and a two-channel pyramid are
899
+ // different textures, and pooling them under one key would hand a
900
+ // consumer a pyramid with no nearest channel in it.
901
+ allocationKey: `ForwardPlus.HiZ${options.hiZNearest ? '.MinMax' : ''}`
601
902
  });
602
903
  const hiZFramebufferHandle = builder.createFramebuffer({
603
904
  label: 'HiZFramebuffer',
@@ -633,9 +934,12 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
633
934
  const depthPassResult = requireBuildState(fg, 'depth', 'DepthPrepass', 'SSSProfile');
634
935
  const preLightTransmissionDepthToken = fg.state.preLightTransmissionDepthToken;
635
936
  const shadowMaskHandle = blackboard.get(FrameResources.ShadowMask);
937
+ const thicknessHandle = blackboard.get(FrameResources.TransmissionThickness);
938
+ const waterCausticsHandle = blackboard.get(FrameResources.WaterCaustics);
636
939
  const renderDepthAttachment = fg.state.renderDepthAttachment;
637
940
  const sssProfileResult = graph.addPass('SSSProfile', (builder)=>{
638
941
  builder.read(blackboard.expect(FrameResources.LinearDepth));
942
+ readVirtualTextureToken(builder, blackboard);
639
943
  builder.read(depthPassResult.depthFramebufferHandle);
640
944
  if (preLightTransmissionDepthToken) {
641
945
  builder.read(preLightTransmissionDepthToken);
@@ -643,6 +947,15 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
643
947
  if (shadowMaskHandle) {
644
948
  builder.read(shadowMaskHandle);
645
949
  }
950
+ if (thicknessHandle) {
951
+ builder.read(thicknessHandle);
952
+ }
953
+ if (waterCausticsHandle) {
954
+ // Same reason as the shadow mask: the map reaches the shader through
955
+ // ctx.waterCausticTexture, so only a declared read stops the executor
956
+ // from recycling it while this pass is still sampling it.
957
+ builder.read(waterCausticsHandle);
958
+ }
646
959
  const profileHandle = builder.createTexture({
647
960
  format: 'rgba16f',
648
961
  label: 'sssProfile',
@@ -668,6 +981,7 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
668
981
  });
669
982
  builder.setExecute((rgCtx)=>{
670
983
  ctx.shadowMaskTexture = shadowMaskHandle ? rgCtx.getTexture(shadowMaskHandle) : null;
984
+ ctx.transmissionThicknessTexture = thicknessHandle ? rgCtx.getTexture(thicknessHandle) : null;
671
985
  renderForwardSSSProfile(frame, rgCtx.getFramebuffer(framebufferHandle), rgCtx.getTexture(profileHandle), rgCtx.getTexture(paramHandle), null);
672
986
  });
673
987
  return {
@@ -691,9 +1005,12 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
691
1005
  const depthPassResult = requireBuildState(fg, 'depth', 'DepthPrepass', 'SceneColorGrab');
692
1006
  const preLightTransmissionDepthToken = fg.state.preLightTransmissionDepthToken;
693
1007
  const shadowMaskHandle = blackboard.get(FrameResources.ShadowMask);
1008
+ const thicknessHandle = blackboard.get(FrameResources.TransmissionThickness);
1009
+ const waterCausticsHandle = blackboard.get(FrameResources.WaterCaustics);
694
1010
  const renderDepthAttachment = fg.state.renderDepthAttachment;
695
1011
  const grabResult = graph.addPass('SceneColorGrab', (builder)=>{
696
1012
  builder.read(blackboard.expect(FrameResources.LinearDepth));
1013
+ readVirtualTextureToken(builder, blackboard);
697
1014
  builder.read(depthPassResult.depthFramebufferHandle);
698
1015
  if (preLightTransmissionDepthToken) {
699
1016
  builder.read(preLightTransmissionDepthToken);
@@ -701,9 +1018,37 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
701
1018
  if (shadowMaskHandle) {
702
1019
  builder.read(shadowMaskHandle);
703
1020
  }
1021
+ if (thicknessHandle) {
1022
+ builder.read(thicknessHandle);
1023
+ }
1024
+ if (waterCausticsHandle) {
1025
+ // Same reason as the shadow mask: the map reaches the shader through
1026
+ // ctx.waterCausticTexture, so only a declared read stops the executor
1027
+ // from recycling it while this pass is still sampling it.
1028
+ builder.read(waterCausticsHandle);
1029
+ }
1030
+ // A few mip levels, because the refraction background is sampled at a LOD
1031
+ // and not only at mip 0: water picks one from how deep and how turbid the
1032
+ // column is, so a stone under clear shallows stays sharp while the same
1033
+ // stone under metres of silt smears. With a single-level texture the
1034
+ // sampler returned mip 0 whatever LOD was asked for, and the blur did
1035
+ // nothing.
1036
+ //
1037
+ // Nothing here generates the chain - both backends do it when the
1038
+ // framebuffer is unbound, for any colour attachment with more than one
1039
+ // level (WebGL `framebuffer_webgl.ts` unbind, WebGPU `renderpass_webgpu.ts`
1040
+ // end), and `generateMipmaps` defaults to true on colour attachments. The
1041
+ // level count is the whole precondition.
1042
+ //
1043
+ // Not the full chain down to 1x1. The consumer clamps its LOD, and the
1044
+ // coarsest levels of a full chain average across the whole frame - a
1045
+ // sample from one stops being "what is behind the water" and becomes the
1046
+ // mean of the image. Generating them would cost bandwidth every frame to
1047
+ // produce levels nothing may read.
704
1048
  const copyHandle = builder.createTexture({
705
1049
  format: ctx.colorFormat,
706
1050
  label: 'sceneColorCopy',
1051
+ mipLevels: getSceneColorMipLevelCount(ctx.renderWidth, ctx.renderHeight),
707
1052
  allocationKey: 'ForwardPlus.SceneColorCopy'
708
1053
  });
709
1054
  // Isolate depth when SSR inserts transmission depth before LightPass.
@@ -717,6 +1062,7 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
717
1062
  }) : undefined;
718
1063
  builder.setExecute((rgCtx)=>{
719
1064
  ctx.shadowMaskTexture = shadowMaskHandle ? rgCtx.getTexture(shadowMaskHandle) : null;
1065
+ ctx.transmissionThicknessTexture = thicknessHandle ? rgCtx.getTexture(thicknessHandle) : null;
720
1066
  renderSceneColorGrab(frame, rgCtx, copyHandle, copyFramebufferHandle);
721
1067
  });
722
1068
  return {
@@ -735,7 +1081,7 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
735
1081
  FrameResources.SceneNormal,
736
1082
  FrameResources.SSSDiffuse,
737
1083
  FrameResources.SSSTransmission,
738
- FrameResources.SkinSSS
1084
+ FrameResources.PostSSS
739
1085
  ],
740
1086
  prepare: ()=>({
741
1087
  enabled: true
@@ -744,10 +1090,13 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
744
1090
  const { graph, ctx, frame, blackboard, options, backbuffer } = fg;
745
1091
  const depthPassResult = requireBuildState(fg, 'depth', 'DepthPrepass', 'LightPass');
746
1092
  const shadowMaskHandle = blackboard.get(FrameResources.ShadowMask);
1093
+ const thicknessHandle = blackboard.get(FrameResources.TransmissionThickness);
1094
+ const waterCausticsHandle = blackboard.get(FrameResources.WaterCaustics);
747
1095
  const preLightTransmissionDepthToken = fg.state.preLightTransmissionDepthToken;
748
1096
  const hiZHandle = blackboard.get(FrameResources.HiZ);
749
1097
  const sssProfileHandle = blackboard.get(FrameResources.SSSProfile);
750
1098
  const sssParamHandle = blackboard.get(FrameResources.SSSParam);
1099
+ const sssProfileIdHandle = blackboard.get(FrameResources.SSSProfileId);
751
1100
  const sceneColorCopyHandle = blackboard.get(FrameResources.SceneColorCopy);
752
1101
  const useFinalFramebufferAsIntermediate = fg.state.useFinalFramebufferAsIntermediate;
753
1102
  const renderDepthAttachment = fg.state.renderDepthAttachment;
@@ -810,10 +1159,20 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
810
1159
  }
811
1160
  const opaquePassResult = graph.addPass('LightPass', (builder)=>{
812
1161
  builder.read(blackboard.expect(FrameResources.LinearDepth));
1162
+ readVirtualTextureToken(builder, blackboard);
813
1163
  builder.read(depthPassResult.depthFramebufferHandle);
814
1164
  if (shadowMaskHandle) {
815
1165
  builder.read(shadowMaskHandle);
816
1166
  }
1167
+ if (thicknessHandle) {
1168
+ builder.read(thicknessHandle);
1169
+ }
1170
+ if (waterCausticsHandle) {
1171
+ // Same reason as the shadow mask: the map reaches the shader through
1172
+ // ctx.waterCausticTexture, so only a declared read stops the executor
1173
+ // from recycling it while this pass is still sampling it.
1174
+ builder.read(waterCausticsHandle);
1175
+ }
817
1176
  if (preLightTransmissionDepthToken) {
818
1177
  builder.read(preLightTransmissionDepthToken);
819
1178
  }
@@ -838,6 +1197,11 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
838
1197
  if (sssParamHandle) {
839
1198
  builder.read(sssParamHandle);
840
1199
  }
1200
+ // Reached through ctx.SSSProfileIdTexture, so the read must be declared or
1201
+ // the executor may recycle the prepass target before the effect samples it.
1202
+ if (sssProfileIdHandle) {
1203
+ builder.read(sssProfileIdHandle);
1204
+ }
841
1205
  const surfaceAttachmentCount = Number(options.sceneRoughness) + Number(options.sceneNormal);
842
1206
  // Surface MRT products are exposed through blackboard handles.
843
1207
  const sceneRoughnessHandle = options.sceneRoughness ? builder.createTexture({
@@ -855,8 +1219,8 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
855
1219
  if (writeSSSDiffuse && writeSSSTransmission && surfaceAttachmentCount > 0 && getSSSLightingTextureFormat(ctx, 2, surfaceAttachmentCount) !== ctx.colorFormat) {
856
1220
  writeSSSTransmission = false;
857
1221
  }
858
- const writeSkinSSS = options.skinSSS;
859
- const sssLightingAttachmentCount = (writeSSSDiffuse ? 1 : 0) + (writeSSSTransmission ? 1 : 0) + (writeSkinSSS ? 1 : 0);
1222
+ const writePostSSS = options.postSSS;
1223
+ const sssLightingAttachmentCount = (writeSSSDiffuse ? 1 : 0) + (writeSSSTransmission ? 1 : 0) + (writePostSSS ? 1 : 0);
860
1224
  const sssLightingFormat = getSSSLightingTextureFormat(ctx, sssLightingAttachmentCount, surfaceAttachmentCount);
861
1225
  const sssDiffuseHandle = writeSSSDiffuse ? builder.createTexture({
862
1226
  format: sssLightingFormat,
@@ -868,10 +1232,10 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
868
1232
  label: 'sssTransmission',
869
1233
  allocationKey: 'ForwardPlus.SSSTransmission'
870
1234
  }) : undefined;
871
- const skinSSSHandle = writeSkinSSS ? builder.createTexture({
1235
+ const postSSSHandle = writePostSSS ? builder.createTexture({
872
1236
  format: sssLightingFormat,
873
- label: 'skinSSS',
874
- allocationKey: 'ForwardPlus.SkinSSS'
1237
+ label: 'postSSS',
1238
+ allocationKey: 'ForwardPlus.PostSSS'
875
1239
  }) : undefined;
876
1240
  const sceneColorFramebufferHandle = useFinalFramebufferAsIntermediate ? undefined : builder.createFramebuffer({
877
1241
  label: 'SceneColorFramebuffer',
@@ -885,6 +1249,7 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
885
1249
  const sceneColorCopyTex = sceneColorCopyHandle ? rgCtx.getTexture(sceneColorCopyHandle) : null;
886
1250
  // Legacy effects still read resolved MRT textures from DrawContext.
887
1251
  ctx.shadowMaskTexture = shadowMaskHandle ? rgCtx.getTexture(shadowMaskHandle) : null;
1252
+ ctx.transmissionThicknessTexture = thicknessHandle ? rgCtx.getTexture(thicknessHandle) : null;
888
1253
  ctx.HiZTexture = hiZHandle ? rgCtx.getTexture(hiZHandle) : null;
889
1254
  ctx.SceneRoughnessTexture = sceneRoughnessHandle ? rgCtx.getTexture(sceneRoughnessHandle) : null;
890
1255
  ctx.SceneNormalTexture = sceneNormalHandle ? rgCtx.getTexture(sceneNormalHandle) : null;
@@ -892,7 +1257,8 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
892
1257
  ctx.SSSParamTexture = sssParamHandle ? rgCtx.getTexture(sssParamHandle) : null;
893
1258
  ctx.SSSDiffuseTexture = sssDiffuseHandle ? rgCtx.getTexture(sssDiffuseHandle) : null;
894
1259
  ctx.SSSTransmissionTexture = sssTransmissionHandle ? rgCtx.getTexture(sssTransmissionHandle) : null;
895
- ctx.SkinSSSTexture = skinSSSHandle ? rgCtx.getTexture(skinSSSHandle) : null;
1260
+ ctx.SSSMaskTexture = postSSSHandle ? rgCtx.getTexture(postSSSHandle) : null;
1261
+ ctx.SSSProfileIdTexture = sssProfileIdHandle ? rgCtx.getTexture(sssProfileIdHandle) : null;
896
1262
  const renderLightPass = ()=>renderOpaqueScenePass(frame, sceneColorTex, sceneColorCopyTex, rgCtx, sceneColorFramebufferHandle);
897
1263
  if (historyManager && lightHistoryReadBindings.length > 0) {
898
1264
  const ssgiIrradianceBinding = lightHistoryReadBindings.find((binding)=>binding.name === RGHistoryResources.SSGI_IRRADIANCE);
@@ -921,7 +1287,7 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
921
1287
  sceneNormalHandle,
922
1288
  sssDiffuseHandle,
923
1289
  sssTransmissionHandle,
924
- skinSSSHandle
1290
+ postSSSHandle: postSSSHandle
925
1291
  };
926
1292
  });
927
1293
  const lightPassResult = opaquePassResult;
@@ -939,8 +1305,8 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
939
1305
  if (lightPassResult.sssTransmissionHandle) {
940
1306
  blackboard.set(FrameResources.SSSTransmission, lightPassResult.sssTransmissionHandle);
941
1307
  }
942
- if (lightPassResult.skinSSSHandle) {
943
- blackboard.set(FrameResources.SkinSSS, lightPassResult.skinSSSHandle);
1308
+ if (lightPassResult.postSSSHandle) {
1309
+ blackboard.set(FrameResources.PostSSS, lightPassResult.postSSSHandle);
944
1310
  }
945
1311
  }
946
1312
  };
@@ -1001,6 +1367,68 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
1001
1367
  }
1002
1368
  }
1003
1369
  };
1370
+ /** @internal */ const UnderwaterModule = {
1371
+ type: 'Underwater',
1372
+ reads: [
1373
+ {
1374
+ resource: FrameResources.SceneColor,
1375
+ version: 'current'
1376
+ }
1377
+ ],
1378
+ writes: [
1379
+ FrameResources.SceneColor
1380
+ ],
1381
+ prepare: ({ ctx })=>({
1382
+ enabled: !!ctx.underwater
1383
+ }),
1384
+ setup (fg) {
1385
+ const { graph, ctx, frame, blackboard } = fg;
1386
+ const depthPassResult = requireBuildState(fg, 'depth', 'DepthPrepass', 'Underwater');
1387
+ const lightPassResult = requireBuildState(fg, 'lightPass', 'LightPass', 'Underwater');
1388
+ const sceneColorHandle = blackboard.expect(FrameResources.SceneColor);
1389
+ const waterCausticsHandle = blackboard.get(FrameResources.WaterCaustics);
1390
+ const state = ctx.underwater;
1391
+ // After the sky, before the transparent pass: the medium has to cover the
1392
+ // sky - that is what the horizon fades into underwater - and the opaque
1393
+ // scene is the only thing the depth buffer describes, so it is also the only
1394
+ // thing this can reach. Transparent geometry is on its own, exactly as it is
1395
+ // for atmospheric fog.
1396
+ const underwaterColorHandle = graph.addPass('Underwater', (builder)=>{
1397
+ builder.read(sceneColorHandle);
1398
+ builder.read(depthPassResult.depthFramebufferHandle);
1399
+ if (lightPassResult.sceneColorFramebufferHandle) {
1400
+ builder.read(lightPassResult.sceneColorFramebufferHandle);
1401
+ }
1402
+ if (waterCausticsHandle) {
1403
+ // The light shafts sample the caustic map, which reaches the shader
1404
+ // through ctx.waterCausticTexture rather than through a handle. Same
1405
+ // lifetime argument as the shadow mask in CompositeTail: only a declared
1406
+ // read stops the executor returning it to the pool after LightPass.
1407
+ builder.read(waterCausticsHandle);
1408
+ }
1409
+ const out = builder.write(sceneColorHandle);
1410
+ builder.setExecute((rgCtx)=>{
1411
+ const colorTexture = rgCtx.getTexture(sceneColorHandle);
1412
+ const depthTexture = frame.depthFramebuffer?.getDepthAttachment();
1413
+ if (!colorTexture || !depthTexture) {
1414
+ return;
1415
+ }
1416
+ // Color only. The scene depth is sampled by this pass, which it cannot
1417
+ // be while it is still attached to the bound framebuffer.
1418
+ const colorFramebuffer = rgCtx.createFramebuffer({
1419
+ width: colorTexture.width,
1420
+ height: colorTexture.height,
1421
+ colorAttachments: colorTexture,
1422
+ depthAttachment: null
1423
+ });
1424
+ _underwaterRenderer.render(ctx, state, colorFramebuffer, depthTexture);
1425
+ });
1426
+ return out;
1427
+ });
1428
+ lightPassResult.sceneColorHandle = underwaterColorHandle;
1429
+ blackboard.set(FrameResources.SceneColor, underwaterColorHandle);
1430
+ }
1431
+ };
1004
1432
  /** @internal */ const CompositeTailModule = {
1005
1433
  type: 'CompositeTail',
1006
1434
  writes: [
@@ -1017,6 +1445,8 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
1017
1445
  const hiZHandle = blackboard.get(FrameResources.HiZ);
1018
1446
  const sceneColorCopyHandle = blackboard.get(FrameResources.SceneColorCopy);
1019
1447
  const shadowMaskHandle = blackboard.get(FrameResources.ShadowMask);
1448
+ const thicknessHandle = blackboard.get(FrameResources.TransmissionThickness);
1449
+ const waterCausticsHandle = blackboard.get(FrameResources.WaterCaustics);
1020
1450
  const lightPassResult = requireBuildState(fg, 'lightPass', 'LightPass', 'CompositeTail');
1021
1451
  const renderDepthAttachment = fg.state.renderDepthAttachment;
1022
1452
  const useFinalFramebufferAsIntermediate = fg.state.useFinalFramebufferAsIntermediate;
@@ -1044,7 +1474,10 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
1044
1474
  blackboard.get(FrameResources.SceneNormal),
1045
1475
  blackboard.get(FrameResources.SSSDiffuse),
1046
1476
  blackboard.get(FrameResources.SSSTransmission),
1047
- blackboard.get(FrameResources.SkinSSS)
1477
+ blackboard.get(FrameResources.PostSSS),
1478
+ // Reached through ctx.SSSProfileIdTexture: without the dependency the
1479
+ // allocator may hand the prepass target to a later pass.
1480
+ blackboard.get(FrameResources.SSSProfileId)
1048
1481
  ]){
1049
1482
  if (handle) {
1050
1483
  opaqueChainDeps.push(handle);
@@ -1069,6 +1502,7 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
1069
1502
  // Transparent geometry writes a new version of the opaque-chain output.
1070
1503
  const sceneColorHandle = graph.addPass('TransparentPass', (builder)=>{
1071
1504
  builder.read(blackboard.expect(FrameResources.LinearDepth));
1505
+ readVirtualTextureToken(builder, blackboard);
1072
1506
  builder.read(depthPassResult.depthFramebufferHandle);
1073
1507
  if (hiZHandle) {
1074
1508
  // Transparent materials may ray-march HiZ.
@@ -1090,6 +1524,16 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
1090
1524
  // after LightPass while this pass is still sampling it.
1091
1525
  builder.read(shadowMaskHandle);
1092
1526
  }
1527
+ if (thicknessHandle) {
1528
+ // Same reasoning as the shadow mask above: the thickness texture is
1529
+ // bound by the shared clustered-light bind group, so its lifetime has to
1530
+ // reach this pass too.
1531
+ builder.read(thicknessHandle);
1532
+ }
1533
+ if (waterCausticsHandle) {
1534
+ // Same lifetime argument as the shadow mask above.
1535
+ builder.read(waterCausticsHandle);
1536
+ }
1093
1537
  builder.read(opaqueChainResult.color);
1094
1538
  const out = builder.write(opaqueChainResult.color);
1095
1539
  builder.setExecute((rgCtx)=>{
@@ -1097,6 +1541,7 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
1097
1541
  // is the same, but relying on the leftover would make this pass depend
1098
1542
  // on execution order instead of on its own declared inputs.
1099
1543
  ctx.shadowMaskTexture = shadowMaskHandle ? rgCtx.getTexture(shadowMaskHandle) : null;
1544
+ ctx.transmissionThicknessTexture = thicknessHandle ? rgCtx.getTexture(thicknessHandle) : null;
1100
1545
  renderTransparentScenePass(frame, rgCtx, opaqueChainRan ? opaqueChainResult.color : null, lightPassResult.sceneColorFramebufferHandle);
1101
1546
  });
1102
1547
  return out;
@@ -1195,6 +1640,28 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
1195
1640
  return outputBackbuffer;
1196
1641
  });
1197
1642
  }
1643
+ // Debug views of raw data go on the presented image, past tone mapping and exposure
1644
+ const sky = ctx.scene.env.sky;
1645
+ if (sky.cloudDebugViewActive) {
1646
+ const presentedBeforeDebug = presentedBackbuffer;
1647
+ presentedBackbuffer = graph.addPass('VolumetricCloudDebug', (builder)=>{
1648
+ const out = builder.write(presentedBeforeDebug);
1649
+ builder.sideEffect();
1650
+ builder.setExecute(()=>{
1651
+ const device = ctx.device;
1652
+ device.pushDeviceStates();
1653
+ try {
1654
+ device.setFramebuffer(ctx.finalFramebuffer ?? null);
1655
+ device.setViewport(null);
1656
+ device.setScissor(null);
1657
+ sky.drawCloudDebugView();
1658
+ } finally{
1659
+ device.popDeviceStates();
1660
+ }
1661
+ });
1662
+ return out;
1663
+ });
1664
+ }
1198
1665
  fg.state.presentedBackbuffer = presentedBackbuffer;
1199
1666
  // Downstream modules may replace this sink registration.
1200
1667
  blackboard.set(FrameResources.PresentedColor, presentedBackbuffer);
@@ -1205,14 +1672,18 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
1205
1672
  ClusterLights: ClusterLightsModule,
1206
1673
  GPUPicking: GPUPickingModule,
1207
1674
  ShadowMaps: ShadowMapsModule,
1675
+ WaterCaustics: WaterCausticsModule,
1208
1676
  DepthPrepass: DepthPrepassModule,
1677
+ VirtualTextureUpdate: VirtualTextureModule,
1209
1678
  ShadowMask: ShadowMaskModule,
1679
+ TransmissionThickness: TransmissionThicknessModule,
1210
1680
  TransmissionDepthForSSR: TransmissionDepthForSSRModule,
1211
1681
  HiZ: HiZModule,
1212
1682
  SSSProfile: SSSProfileModule,
1213
1683
  SceneColorGrab: SceneColorGrabModule,
1214
1684
  LightPass: LightPassModule,
1215
1685
  SkyPass: SkyPassModule,
1686
+ Underwater: UnderwaterModule,
1216
1687
  CompositeTail: CompositeTailModule
1217
1688
  };
1218
1689
  /** The default module order assembled by {@link createForwardPlusPipeline}. */ const DEFAULT_FORWARD_PLUS_MODULES = [
@@ -1220,14 +1691,18 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
1220
1691
  ClusterLightsModule,
1221
1692
  GPUPickingModule,
1222
1693
  ShadowMapsModule,
1694
+ WaterCausticsModule,
1223
1695
  DepthPrepassModule,
1696
+ VirtualTextureModule,
1224
1697
  ShadowMaskModule,
1698
+ TransmissionThicknessModule,
1225
1699
  TransmissionDepthForSSRModule,
1226
1700
  HiZModule,
1227
1701
  SSSProfileModule,
1228
1702
  SceneColorGrabModule,
1229
1703
  LightPassModule,
1230
1704
  SkyPassModule,
1705
+ UnderwaterModule,
1231
1706
  CompositeTailModule
1232
1707
  ];
1233
1708
  /** @internal */ let _defaultForwardPlusPipeline = null;
@@ -1250,11 +1725,24 @@ function buildForwardPlusGraphInternal(graph, ctx, renderQueue, options) {
1250
1725
  options.ssgi &&= supportsSSGIRenderTargets(ctx);
1251
1726
  ctx.SSS = !!options.sss;
1252
1727
  ctx.SSGI = !!options.ssgi;
1728
+ ctx.postSSS = !!options.postSSS;
1729
+ // Before the compositor is consulted below: it collects requirements from,
1730
+ // and builds passes for, only the effects that are enabled at that moment.
1731
+ ctx.camera?.setPostSSSActive(ctx.postSSS);
1253
1732
  ctx.SSGIIrradianceHistoryTexture = null;
1254
1733
  ctx.SSGISurfaceHistoryTexture = null;
1255
- ctx.SkinSSSTexture = null;
1734
+ ctx.SSSMaskTexture = null;
1735
+ ctx.SSSProfileIdTexture = null;
1256
1736
  // ShadowMask sets this only when it produces a texture.
1257
1737
  ctx.shadowMaskTexture = null;
1738
+ ctx.transmissionThicknessTexture = null;
1739
+ // WaterCaustics turns these on together once it has produced a map; they are
1740
+ // read by every light pass, so they must start cleared each frame.
1741
+ ctx.waterCaustics = false;
1742
+ ctx.waterCausticLight = null;
1743
+ ctx.waterCausticTexture = null;
1744
+ ctx.waterCausticHeightTexture = null;
1745
+ ctx.waterCausticUniforms = null;
1258
1746
  const blackboard = new RGBlackboard();
1259
1747
  const frame = {
1260
1748
  ctx,
@@ -1288,6 +1776,10 @@ function buildForwardPlusGraphInternal(graph, ctx, renderQueue, options) {
1288
1776
  const requirements = ctx.compositor?.collectRequirements(ctx) ?? {};
1289
1777
  mergeFrameResourceRequirements(requirements, pipeline.collectRequirements(fg));
1290
1778
  resolveFrameResourceRequirements(ctx, options, requirements);
1779
+ // Must agree with TransmissionThicknessModule.prepare: the flag decides
1780
+ // whether the light pass declares and binds the thickness texture, so a
1781
+ // mismatch would leave the shader layout disagreeing with the bind group.
1782
+ ctx.transmissionThickness = options.postSSS && renderQueue.shadowedLights.some((light)=>light.transmission);
1291
1783
  pipeline.build(fg);
1292
1784
  validateProducedFrameResources(blackboard, options, renderQueue);
1293
1785
  const presented = blackboard.get(FrameResources.PresentedColor) ?? fg.state.presentedBackbuffer;
@@ -1384,7 +1876,7 @@ function disposeRenderQueue(frame) {
1384
1876
  }
1385
1877
  function releaseClusteredLight(frame) {
1386
1878
  if (!frame.clusteredLightReleased && frame.ctx.clusteredLight) {
1387
- freeClusteredLight(frame.ctx.clusteredLight);
1879
+ ClusteredLight.release(frame.ctx.clusteredLight);
1388
1880
  frame.ctx.clusteredLight = undefined;
1389
1881
  frame.clusteredLightReleased = true;
1390
1882
  }
@@ -1418,7 +1910,7 @@ function cleanupFrame(frame) {
1418
1910
  console.assert(!!existingDepthFb, 'renderSceneDepth: called without an existing depth framebuffer; the ' + 'self-allocating fallback path is untested in the forward+ pipeline.');
1419
1911
  if (!depthFramebuffer) {
1420
1912
  {
1421
- const format = ctx.device.type === 'webgl' ? ctx.SSRCalcThickness ? 'rgba16f' : 'rgba8unorm' : ctx.SSRCalcThickness ? 'rg32f' : 'r32f';
1913
+ const format = ctx.SSRCalcThickness ? 'rg32f' : 'r32f';
1422
1914
  const mvFormat = 'rgba16f';
1423
1915
  if (!ctx.finalFramebuffer) {
1424
1916
  depthFramebuffer = rgCtx.createFramebuffer({
@@ -1467,6 +1959,27 @@ function cleanupFrame(frame) {
1467
1959
  ctx.device.setFramebuffer(depthFramebuffer);
1468
1960
  _depthPass.encodeDepth = depthFramebuffer.getColorAttachments()[0].format === 'rgba8unorm';
1469
1961
  _depthPass.clearColor = transmission ? null : _depthPass.encodeDepth ? new Vector4(0, 0, 0, 1) : new Vector4(1, 1, 1, 1);
1962
+ // That clear value is chosen for the linear depth target, where 1 is the far
1963
+ // plane. The profile id target needs the opposite: 0 means "not skin", and
1964
+ // every other value addresses a real row of the profile table. Clearing it
1965
+ // alongside the depth made each background texel read as profile 255, so a
1966
+ // sampling disc wide enough to reach past the silhouette accepted those taps
1967
+ // and averaged the background into the skin instead of rejecting it — as
1968
+ // dark speckle, since only the pixels whose sequence happened to draw a
1969
+ // long radius reached that far.
1970
+ //
1971
+ // Assigning clearColor above resets the per-target list, so this has to come
1972
+ // after it; that also means a frame without the target cannot inherit a
1973
+ // stale list.
1974
+ if (!transmission && ctx.sssProfileId) {
1975
+ const attachmentCount = depthFramebuffer.getColorAttachments().length;
1976
+ if (attachmentCount > 1) {
1977
+ // The id is appended last; see DepthPrepassModule.
1978
+ const clearColors = new Array(attachmentCount).fill(_depthPass.clearColor);
1979
+ clearColors[attachmentCount - 1] = SKIN_PROFILE_ID_CLEAR;
1980
+ _depthPass.clearColors = clearColors;
1981
+ }
1982
+ }
1470
1983
  _depthPass.clearDepth = transmission ? null : DEPTH_CLEAR_VALUE;
1471
1984
  _depthPass.clearStencil = null;
1472
1985
  _depthPass.transmission = transmission;
@@ -1674,7 +2187,7 @@ function blitToCurrentColorAttachment(ctx, source) {
1674
2187
  ctx.materialFlags |= MaterialVaryingFlags.SSS_STORE_TRANSMISSION;
1675
2188
  }
1676
2189
  }
1677
- if (ctx.SkinSSSTexture) {
2190
+ if (ctx.SSSMaskTexture) {
1678
2191
  ctx.materialFlags |= MaterialVaryingFlags.SKIN_SSS_STORE;
1679
2192
  }
1680
2193
  // Prefer the graph framebuffer; final-target mode has no graph framebuffer.
@@ -1740,7 +2253,11 @@ function blitToCurrentColorAttachment(ctx, source) {
1740
2253
  device.setViewport(null);
1741
2254
  device.setScissor(null);
1742
2255
  ctx.scene.env.sky.renderSky(ctx);
1743
- if (ctx.scene.env.sky.fogPresents) {
2256
+ // Not while submerged: the medium between the eye and the scene is water,
2257
+ // and the Underwater module composites it. The sky itself is still drawn -
2258
+ // that pass then extinguishes it along with everything else at the far
2259
+ // plane, which is what makes the horizon read as water rather than as air.
2260
+ if (ctx.scene.env.sky.fogPresents && !ctx.underwater) {
1744
2261
  // Snapshot between sky and fog. Sky belongs in the copy - a ray that hits
1745
2262
  // the sky should read the sky - while fog does not, because the fog a
1746
2263
  // screen-space pass would sample lies along the camera ray rather than the
@@ -1822,8 +2339,17 @@ function blitToCurrentColorAttachment(ctx, source) {
1822
2339
  let motionVectorFrameCommitted = false;
1823
2340
  try {
1824
2341
  renderQueue = _scenePass.cullScene(ctx, ctx.camera);
2342
+ // Before the options are derived, because being submerged changes them: the
2343
+ // atmosphere is not the medium the camera is looking through, so its fog is
2344
+ // suppressed for the frame.
2345
+ ctx.underwater = selectUnderwaterSource(ctx);
1825
2346
  const options = deriveForwardPlusOptions(ctx.scene, ctx.camera, device.type, renderQueue);
1826
2347
  options.ssgi &&= supportsSSGIRenderTargets(ctx);
2348
+ // Atmospheric fog describes a column of air, which is not what a submerged
2349
+ // camera is looking through. The Underwater module owns the medium instead.
2350
+ // Clearing the flag here also stops SkyPass reserving a fog-free snapshot
2351
+ // for a frame that composites no fog.
2352
+ options.fogPresents &&= !ctx.underwater;
1827
2353
  ctx.SSS = options.sss;
1828
2354
  historyManager = ctx.camera.getHistoryResourceManager();
1829
2355
  if (!historyManager) {