@zephyr3d/scene 0.9.25 → 0.9.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (299) hide show
  1. package/dist/animation/animationset.js +92 -30
  2. package/dist/animation/animationset.js.map +1 -1
  3. package/dist/animation/hair/gpu_hair_simulation.js +1090 -0
  4. package/dist/animation/hair/gpu_hair_simulation.js.map +1 -0
  5. package/dist/animation/joint_dynamics/controller.js +7 -5
  6. package/dist/animation/joint_dynamics/controller.js.map +1 -1
  7. package/dist/animation/joint_dynamics/convex_collider.js +320 -0
  8. package/dist/animation/joint_dynamics/convex_collider.js.map +1 -0
  9. package/dist/animation/joint_dynamics/joint_dynamics_system.js +81 -5
  10. package/dist/animation/joint_dynamics/joint_dynamics_system.js.map +1 -1
  11. package/dist/animation/morphtrack.js +3 -4
  12. package/dist/animation/morphtrack.js.map +1 -1
  13. package/dist/animation/skeleton.js +79 -25
  14. package/dist/animation/skeleton.js.map +1 -1
  15. package/dist/animation/spring/multi_chain_spring_system.js +577 -179
  16. package/dist/animation/spring/multi_chain_spring_system.js.map +1 -1
  17. package/dist/animation/spring/spring_collider.js +4 -3
  18. package/dist/animation/spring/spring_collider.js.map +1 -1
  19. package/dist/animation/spring/spring_node_pose_tracker.js +40 -0
  20. package/dist/animation/spring/spring_node_pose_tracker.js.map +1 -0
  21. package/dist/animation/spring/spring_particle.js +2 -0
  22. package/dist/animation/spring/spring_particle.js.map +1 -1
  23. package/dist/animation/spring/spring_system.js +11 -3
  24. package/dist/animation/spring/spring_system.js.map +1 -1
  25. package/dist/animation/spring_modifier.js +10 -0
  26. package/dist/animation/spring_modifier.js.map +1 -1
  27. package/dist/app/engine.js +53 -43
  28. package/dist/app/engine.js.map +1 -1
  29. package/dist/app/scriptingsystem.js +119 -6
  30. package/dist/app/scriptingsystem.js.map +1 -1
  31. package/dist/app/scriptregistry.js +3 -2
  32. package/dist/app/scriptregistry.js.map +1 -1
  33. package/dist/asset/assetmanager.js +284 -67
  34. package/dist/asset/assetmanager.js.map +1 -1
  35. package/dist/asset/loaders/zhair/zhair_format.js +288 -0
  36. package/dist/asset/loaders/zhair/zhair_format.js.map +1 -0
  37. package/dist/asset/loaders/zhair/zhair_loader.js +221 -0
  38. package/dist/asset/loaders/zhair/zhair_loader.js.map +1 -0
  39. package/dist/asset/model.js +184 -69
  40. package/dist/asset/model.js.map +1 -1
  41. package/dist/camera/camera.js +268 -44
  42. package/dist/camera/camera.js.map +1 -1
  43. package/dist/camera/perspectivecamera.js +3 -1
  44. package/dist/camera/perspectivecamera.js.map +1 -1
  45. package/dist/index.d.ts +9044 -5139
  46. package/dist/index.js +17 -6
  47. package/dist/index.js.map +1 -1
  48. package/dist/material/eye.js +932 -0
  49. package/dist/material/eye.js.map +1 -0
  50. package/dist/material/hair.js +917 -0
  51. package/dist/material/hair.js.map +1 -0
  52. package/dist/material/hairstrand.js +842 -0
  53. package/dist/material/hairstrand.js.map +1 -0
  54. package/dist/material/hairstrand_data.js +162 -0
  55. package/dist/material/hairstrand_data.js.map +1 -0
  56. package/dist/material/lambert.js +5 -2
  57. package/dist/material/lambert.js.map +1 -1
  58. package/dist/material/material.js +48 -10
  59. package/dist/material/material.js.map +1 -1
  60. package/dist/material/meshmaterial.js +120 -19
  61. package/dist/material/meshmaterial.js.map +1 -1
  62. package/dist/material/mixins/albedocolor.js +1 -1
  63. package/dist/material/mixins/albedocolor.js.map +1 -1
  64. package/dist/material/mixins/lightmodel/blinnphong.js +5 -3
  65. package/dist/material/mixins/lightmodel/blinnphong.js.map +1 -1
  66. package/dist/material/mixins/lightmodel/lambert.js +5 -4
  67. package/dist/material/mixins/lightmodel/lambert.js.map +1 -1
  68. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js +3 -3
  69. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js.map +1 -1
  70. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js +5 -5
  71. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js.map +1 -1
  72. package/dist/material/mixins/lightmodel/pbrspecularglossness.js +3 -3
  73. package/dist/material/mixins/lightmodel/pbrspecularglossness.js.map +1 -1
  74. package/dist/material/mixins/lit.js +106 -19
  75. package/dist/material/mixins/lit.js.map +1 -1
  76. package/dist/material/mixins/pbr/common.js +4 -4
  77. package/dist/material/mixins/pbr/common.js.map +1 -1
  78. package/dist/material/mixins/texture.js +48 -11
  79. package/dist/material/mixins/texture.js.map +1 -1
  80. package/dist/material/mtoon.js +6 -5
  81. package/dist/material/mtoon.js.map +1 -1
  82. package/dist/material/pbrblueprint.js +146 -28
  83. package/dist/material/pbrblueprint.js.map +1 -1
  84. package/dist/material/pbrblueprint_instance.js +501 -0
  85. package/dist/material/pbrblueprint_instance.js.map +1 -0
  86. package/dist/material/pbrmr.js +15 -16
  87. package/dist/material/pbrmr.js.map +1 -1
  88. package/dist/material/shader/helper.js +734 -23
  89. package/dist/material/shader/helper.js.map +1 -1
  90. package/dist/material/shader/terrain_blend.js +55 -0
  91. package/dist/material/shader/terrain_blend.js.map +1 -0
  92. package/dist/material/skin.js +192 -32
  93. package/dist/material/skin.js.map +1 -1
  94. package/dist/material/spriteblueprint.js +16 -1
  95. package/dist/material/spriteblueprint.js.map +1 -1
  96. package/dist/material/subsurfaceprofile.js +86 -23
  97. package/dist/material/subsurfaceprofile.js.map +1 -1
  98. package/dist/material/water.js +2059 -72
  99. package/dist/material/water.js.map +1 -1
  100. package/dist/posteffect/bloom.js +167 -7
  101. package/dist/posteffect/bloom.js.map +1 -1
  102. package/dist/posteffect/coloradjust.js +2 -1
  103. package/dist/posteffect/coloradjust.js.map +1 -1
  104. package/dist/posteffect/compositor.js.map +1 -1
  105. package/dist/posteffect/fxaa.js +2 -1
  106. package/dist/posteffect/fxaa.js.map +1 -1
  107. package/dist/posteffect/grayscale.js +3 -1
  108. package/dist/posteffect/grayscale.js.map +1 -1
  109. package/dist/posteffect/motionblur.js +664 -56
  110. package/dist/posteffect/motionblur.js.map +1 -1
  111. package/dist/posteffect/posteffect.js +12 -1
  112. package/dist/posteffect/posteffect.js.map +1 -1
  113. package/dist/posteffect/sao.js +46 -44
  114. package/dist/posteffect/sao.js.map +1 -1
  115. package/dist/posteffect/skinsss.js +368 -69
  116. package/dist/posteffect/skinsss.js.map +1 -1
  117. package/dist/posteffect/ssgi.js +16 -9
  118. package/dist/posteffect/ssgi.js.map +1 -1
  119. package/dist/posteffect/ssr.js +3 -0
  120. package/dist/posteffect/ssr.js.map +1 -1
  121. package/dist/posteffect/sss.js +121 -36
  122. package/dist/posteffect/sss.js.map +1 -1
  123. package/dist/posteffect/taa.js.map +1 -1
  124. package/dist/posteffect/tonemap.js +44 -1
  125. package/dist/posteffect/tonemap.js.map +1 -1
  126. package/dist/render/abuffer_oit.js +8 -2
  127. package/dist/render/abuffer_oit.js.map +1 -1
  128. package/dist/render/clipmap.js +156 -5
  129. package/dist/render/clipmap.js.map +1 -1
  130. package/dist/render/cull_visitor.js +20 -0
  131. package/dist/render/cull_visitor.js.map +1 -1
  132. package/dist/render/depthpass.js +36 -2
  133. package/dist/render/depthpass.js.map +1 -1
  134. package/dist/render/drawable_mixin.js +26 -1
  135. package/dist/render/drawable_mixin.js.map +1 -1
  136. package/dist/render/dualdepthpeeling_oit.js +3 -2
  137. package/dist/render/dualdepthpeeling_oit.js.map +1 -1
  138. package/dist/render/envlight.js +1 -1
  139. package/dist/render/envlight.js.map +1 -1
  140. package/dist/render/fbm_wavegenerator.js +6 -3
  141. package/dist/render/fbm_wavegenerator.js.map +1 -1
  142. package/dist/render/fft_wavegenerator.js +57 -15
  143. package/dist/render/fft_wavegenerator.js.map +1 -1
  144. package/dist/render/gerstner_wavegenerator.js +130 -20
  145. package/dist/render/gerstner_wavegenerator.js.map +1 -1
  146. package/dist/render/globalbindgroup_allocator.js +3 -0
  147. package/dist/render/globalbindgroup_allocator.js.map +1 -1
  148. package/dist/render/hzb.js +80 -22
  149. package/dist/render/hzb.js.map +1 -1
  150. package/dist/render/lightpass.js +6 -3
  151. package/dist/render/lightpass.js.map +1 -1
  152. package/dist/render/oit.js +29 -0
  153. package/dist/render/oit.js.map +1 -0
  154. package/dist/render/render_queue.js +39 -0
  155. package/dist/render/render_queue.js.map +1 -1
  156. package/dist/render/renderer.js +4 -0
  157. package/dist/render/renderer.js.map +1 -1
  158. package/dist/render/rendergraph/blackboard.js +1 -0
  159. package/dist/render/rendergraph/blackboard.js.map +1 -1
  160. package/dist/render/rendergraph/device_pool_allocator.js.map +1 -1
  161. package/dist/render/rendergraph/executor.js +2 -0
  162. package/dist/render/rendergraph/executor.js.map +1 -1
  163. package/dist/render/rendergraph/forward_plus_builder.js +379 -10
  164. package/dist/render/rendergraph/forward_plus_builder.js.map +1 -1
  165. package/dist/render/rendergraph/frame_resource_requirements.js +1 -0
  166. package/dist/render/rendergraph/frame_resource_requirements.js.map +1 -1
  167. package/dist/render/rendergraph/history_resources.js +8 -1
  168. package/dist/render/rendergraph/history_resources.js.map +1 -1
  169. package/dist/render/rendergraph/rendergraph.js.map +1 -1
  170. package/dist/render/rendergraph/texture_affinity_cache.js +1 -1
  171. package/dist/render/rendergraph/texture_affinity_cache.js.map +1 -1
  172. package/dist/render/rendergraph/types.js.map +1 -1
  173. package/dist/render/shadow_mask_pass.js +73 -8
  174. package/dist/render/shadow_mask_pass.js.map +1 -1
  175. package/dist/render/sky.js +166 -126
  176. package/dist/render/sky.js.map +1 -1
  177. package/dist/render/underwater.js +624 -0
  178. package/dist/render/underwater.js.map +1 -0
  179. package/dist/render/water_caustics.js +1380 -0
  180. package/dist/render/water_caustics.js.map +1 -0
  181. package/dist/render/weightedblended_oit.js +3 -2
  182. package/dist/render/weightedblended_oit.js.map +1 -1
  183. package/dist/scene/basesprite.js +8 -0
  184. package/dist/scene/basesprite.js.map +1 -1
  185. package/dist/scene/batchgroup.js +3 -0
  186. package/dist/scene/batchgroup.js.map +1 -1
  187. package/dist/scene/environment.js +4 -1
  188. package/dist/scene/environment.js.map +1 -1
  189. package/dist/scene/hair.js +963 -0
  190. package/dist/scene/hair.js.map +1 -0
  191. package/dist/scene/light.js +5 -1
  192. package/dist/scene/light.js.map +1 -1
  193. package/dist/scene/mesh.js +481 -47
  194. package/dist/scene/mesh.js.map +1 -1
  195. package/dist/scene/meshdrawable.js +6 -0
  196. package/dist/scene/meshdrawable.js.map +1 -1
  197. package/dist/scene/msdftext.js +3 -0
  198. package/dist/scene/msdftext.js.map +1 -1
  199. package/dist/scene/msdftextsprite.js +3 -0
  200. package/dist/scene/msdftextsprite.js.map +1 -1
  201. package/dist/scene/particlesys.js +6 -0
  202. package/dist/scene/particlesys.js.map +1 -1
  203. package/dist/scene/scene.js +3 -0
  204. package/dist/scene/scene.js.map +1 -1
  205. package/dist/scene/scene_node.js +6 -1
  206. package/dist/scene/scene_node.js.map +1 -1
  207. package/dist/scene/terrain-cm/grass.js +3 -0
  208. package/dist/scene/terrain-cm/grass.js.map +1 -1
  209. package/dist/scene/terrain-cm/grassmaterial.js +3 -0
  210. package/dist/scene/terrain-cm/grassmaterial.js.map +1 -1
  211. package/dist/scene/terrain-cm/terrain-cm.js +6 -0
  212. package/dist/scene/terrain-cm/terrain-cm.js.map +1 -1
  213. package/dist/scene/terrain-cm/texture_cache.js +309 -0
  214. package/dist/scene/terrain-cm/texture_cache.js.map +1 -0
  215. package/dist/scene/water.js +546 -4
  216. package/dist/scene/water.js.map +1 -1
  217. package/dist/shaders/atmosphere.js.map +1 -1
  218. package/dist/shaders/diffusion.js +54 -0
  219. package/dist/shaders/diffusion.js.map +1 -0
  220. package/dist/shaders/shadow.js +33 -21
  221. package/dist/shaders/shadow.js.map +1 -1
  222. package/dist/shaders/temporal.js +71 -33
  223. package/dist/shaders/temporal.js.map +1 -1
  224. package/dist/shaders/water.js +22 -19
  225. package/dist/shaders/water.js.map +1 -1
  226. package/dist/shaders/water_medium.js +56 -0
  227. package/dist/shaders/water_medium.js.map +1 -0
  228. package/dist/shadow/dom.js +546 -0
  229. package/dist/shadow/dom.js.map +1 -0
  230. package/dist/shadow/pcf_opt.js +3 -5
  231. package/dist/shadow/pcf_opt.js.map +1 -1
  232. package/dist/shadow/pcf_pd.js +1 -1
  233. package/dist/shadow/pcf_pd.js.map +1 -1
  234. package/dist/shadow/receiver_bias.js +41 -0
  235. package/dist/shadow/receiver_bias.js.map +1 -0
  236. package/dist/shadow/shader.js +20 -60
  237. package/dist/shadow/shader.js.map +1 -1
  238. package/dist/shadow/shadow_impl.js +89 -0
  239. package/dist/shadow/shadow_impl.js.map +1 -1
  240. package/dist/shadow/shadow_region.js +1 -1
  241. package/dist/shadow/shadow_region.js.map +1 -1
  242. package/dist/shadow/shadowmapper.js +150 -37
  243. package/dist/shadow/shadowmapper.js.map +1 -1
  244. package/dist/shapes/shape.js +11 -2
  245. package/dist/shapes/shape.js.map +1 -1
  246. package/dist/shapes/sphere.js +50 -9
  247. package/dist/shapes/sphere.js.map +1 -1
  248. package/dist/utility/blueprint/common/constants.js +112 -2
  249. package/dist/utility/blueprint/common/constants.js.map +1 -1
  250. package/dist/utility/blueprint/material/func.js +142 -49
  251. package/dist/utility/blueprint/material/func.js.map +1 -1
  252. package/dist/utility/blueprint/material/ir.js +490 -25
  253. package/dist/utility/blueprint/material/ir.js.map +1 -1
  254. package/dist/utility/blueprint/material/texture.js +898 -4
  255. package/dist/utility/blueprint/material/texture.js.map +1 -1
  256. package/dist/utility/serialization/manager.js +434 -101
  257. package/dist/utility/serialization/manager.js.map +1 -1
  258. package/dist/utility/serialization/scene/animation.js +401 -5
  259. package/dist/utility/serialization/scene/animation.js.map +1 -1
  260. package/dist/utility/serialization/scene/batch.js +4 -0
  261. package/dist/utility/serialization/scene/batch.js.map +1 -1
  262. package/dist/utility/serialization/scene/camera.js +186 -26
  263. package/dist/utility/serialization/scene/camera.js.map +1 -1
  264. package/dist/utility/serialization/scene/common.js +26 -5
  265. package/dist/utility/serialization/scene/common.js.map +1 -1
  266. package/dist/utility/serialization/scene/hair.js +1141 -0
  267. package/dist/utility/serialization/scene/hair.js.map +1 -0
  268. package/dist/utility/serialization/scene/light.js +84 -4
  269. package/dist/utility/serialization/scene/light.js.map +1 -1
  270. package/dist/utility/serialization/scene/material.js +1867 -166
  271. package/dist/utility/serialization/scene/material.js.map +1 -1
  272. package/dist/utility/serialization/scene/mesh.js +231 -14
  273. package/dist/utility/serialization/scene/mesh.js.map +1 -1
  274. package/dist/utility/serialization/scene/node.js +141 -7
  275. package/dist/utility/serialization/scene/node.js.map +1 -1
  276. package/dist/utility/serialization/scene/particle.js +4 -0
  277. package/dist/utility/serialization/scene/particle.js.map +1 -1
  278. package/dist/utility/serialization/scene/primitive.js +18 -0
  279. package/dist/utility/serialization/scene/primitive.js.map +1 -1
  280. package/dist/utility/serialization/scene/sprite.js +4 -0
  281. package/dist/utility/serialization/scene/sprite.js.map +1 -1
  282. package/dist/utility/serialization/scene/terrain.js +4 -0
  283. package/dist/utility/serialization/scene/terrain.js.map +1 -1
  284. package/dist/utility/serialization/scene/text.js +4 -0
  285. package/dist/utility/serialization/scene/text.js.map +1 -1
  286. package/dist/utility/serialization/scene/water.js +1105 -21
  287. package/dist/utility/serialization/scene/water.js.map +1 -1
  288. package/dist/utility/serialization/types.js.map +1 -1
  289. package/dist/utility/shprojector.js +9 -8
  290. package/dist/utility/shprojector.js.map +1 -1
  291. package/dist/values.js +49 -2
  292. package/dist/values.js.map +1 -1
  293. package/package.json +3 -3
  294. package/dist/render/rendergraph/frame_graph_context.js +0 -41
  295. package/dist/render/rendergraph/frame_graph_context.js.map +0 -1
  296. package/dist/shaders/ssgi.js +0 -63
  297. package/dist/shaders/ssgi.js.map +0 -1
  298. package/dist/shaders/ssgi_reprojection.js +0 -118
  299. package/dist/shaders/ssgi_reprojection.js.map +0 -1
@@ -1,17 +1,18 @@
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 { getHiZFormat, buildHiZ } from '../hzb.js';
9
9
  import '../primitive.js';
10
10
  import '@zephyr3d/device';
11
11
  import { CopyBlitter } from '../../blitter/copy.js';
12
12
  import { fetchSampler } from '../../utility/misc.js';
13
13
  import { MaterialVaryingFlags } from '../../values.js';
14
- import { ShaderHelper } from '../../material/shader/helper.js';
14
+ import { ShaderHelper, MAX_CAUSTIC_WATERS } from '../../material/shader/helper.js';
15
+ import { REFRACT_BLUR_MAX_LOD } from '../../material/water.js';
15
16
  import { PostEffectLayer, AbstractPostEffect } from '../../posteffect/posteffect.js';
16
17
  import { RenderGraph } from './rendergraph.js';
17
18
  import { RenderGraphExecutor } from './executor.js';
@@ -31,12 +32,16 @@ import '../../shapes/torus.js';
31
32
  import '../../shapes/plane.js';
32
33
  import '../../shapes/sphere.js';
33
34
  import '../../shapes/tetrahedron.js';
35
+ import { WaterCausticsRenderer } from '../water_caustics.js';
36
+ import { UnderwaterRenderer, selectUnderwaterSource } from '../underwater.js';
34
37
 
35
38
  const _scenePass = new LightPass();
36
39
  const _depthPass = new DepthPass();
37
40
  const _shadowMapPass = new ShadowMapPass();
38
41
  const _clusters = [];
39
42
  const _shadowMaskRenderer = new ShadowMaskRenderer();
43
+ const _waterCausticsRenderer = new WaterCausticsRenderer();
44
+ const _underwaterRenderer = new UnderwaterRenderer();
40
45
  const _devicePoolAllocator = new DevicePoolAllocator();
41
46
  const _textureAffinityCaches = new WeakMap();
42
47
  function getTextureAffinityCache(camera, device) {
@@ -210,6 +215,21 @@ function getSSSLightingTextureFormat(ctx, attachmentCount, surfaceAttachmentCoun
210
215
  function getFullMipLevelCount(width, height) {
211
216
  return Math.max(1, Math.floor(Math.log2(Math.max(1, width, height))) + 1);
212
217
  }
218
+ /**
219
+ * Levels the scene colour copy is allocated with.
220
+ *
221
+ * Deep enough to serve the refraction blur, which clamps its own LOD, and no
222
+ * deeper: the coarsest levels of a full chain are averages of the whole frame,
223
+ * which is not a plausible answer for "what is behind this water", and every
224
+ * level costs bandwidth to generate on each grab. Capped against the full chain
225
+ * so a small render target stays legal.
226
+ *
227
+ * Derived from the water material's clamp rather than restated, so the two
228
+ * cannot drift: a level count one short of it would leave the sampler asking for
229
+ * a level that does not exist.
230
+ */ function getSceneColorMipLevelCount(width, height) {
231
+ return Math.min(REFRACT_BLUR_MAX_LOD + 1, getFullMipLevelCount(width, height));
232
+ }
213
233
  function hasSurfaceMRT(ctx) {
214
234
  return !!(ctx.materialFlags & SURFACE_MRT_FLAGS);
215
235
  }
@@ -244,10 +264,16 @@ function getLightPassColorAttachments(ctx, colorAttachment) {
244
264
  const skinSSS = camera.skinSSS && renderQueueHasActiveSkinSSS(renderQueue);
245
265
  const needSceneColor = renderQueue.needSceneColor();
246
266
  const needSceneColorWithDepth = renderQueue.needSceneColorWithDepth();
267
+ // Requested by materials rather than by a post effect, so it is resolved here
268
+ // instead of in resolveFrameResourceRequirements. WebGL1 has no Hi-Z at all,
269
+ // and a material asking for one there would turn a missing feature into a
270
+ // thrown frame; the material is expected to shade without it.
271
+ const needHiZNearest = (renderQueue.needHiZNearest() || camera.HiZNearest) && deviceType !== 'webgl';
247
272
  return {
248
273
  depthPrepass: true,
249
274
  motionVectors: false,
250
- hiZ: false,
275
+ hiZ: needHiZNearest,
276
+ hiZNearest: needHiZNearest,
251
277
  sceneNormal: false,
252
278
  sceneRoughness: false,
253
279
  shadowMask: false,
@@ -266,7 +292,10 @@ function getLightPassColorAttachments(ctx, colorAttachment) {
266
292
  function resolveFrameResourceRequirements(ctx, options, requirements) {
267
293
  const deviceType = ctx.device.type;
268
294
  options.motionVectors ||= !!requirements.motionVector;
269
- options.hiZ ||= !!requirements.hiZ;
295
+ options.hiZNearest ||= !!requirements.hiZNearest;
296
+ // The nearest channel lives on the same pyramid, so asking for it is asking
297
+ // for the pyramid.
298
+ options.hiZ ||= !!requirements.hiZ || options.hiZNearest;
270
299
  options.sceneNormal ||= !!requirements.sceneNormal;
271
300
  options.sceneRoughness ||= !!requirements.sceneRoughness;
272
301
  options.shadowMask ||= !!requirements.shadowMask;
@@ -401,6 +430,152 @@ 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 (ctx.device.type === 'webgl' || 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: [
@@ -462,6 +637,11 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
462
637
  builder.addSubpass('SkyMotionVectors', (rgCtx)=>{
463
638
  renderSkyMotionVectors(ctx, rgCtx, skyMotionVectorFramebufferHandle);
464
639
  });
640
+ // After the sky, which writes at the far plane: a blended surface in
641
+ // front of empty background has to replace what the sky put there.
642
+ builder.addSubpass('TransparentMotionVectors', (rgCtx)=>{
643
+ renderTransparentMotionVectors(frame, rgCtx, skyMotionVectorFramebufferHandle);
644
+ });
465
645
  }
466
646
  return {
467
647
  depthHandle,
@@ -575,7 +755,7 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
575
755
  enabled: options.hiZ
576
756
  }),
577
757
  setup (fg) {
578
- const { graph, ctx, frame, blackboard } = fg;
758
+ const { graph, ctx, frame, blackboard, options } = fg;
579
759
  const depthPassResult = requireBuildState(fg, 'depth', 'DepthPrepass', 'HiZ');
580
760
  const preLightTransmissionDepthToken = fg.state.preLightTransmissionDepthToken;
581
761
  //const hiZWidth = nextPowerOf2(ctx.renderWidth);
@@ -588,11 +768,14 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
588
768
  builder.read(preLightTransmissionDepthToken);
589
769
  }
590
770
  hiZHandle = builder.createTexture({
591
- format: REVERSE_Z ? 'r16f' : 'r32f',
771
+ format: getHiZFormat(options.hiZNearest),
592
772
  label: 'hiZ',
593
773
  sizeMode: 'backbuffer-relative',
594
774
  mipLevels: getFullMipLevelCount(ctx.renderWidth, ctx.renderHeight),
595
- allocationKey: 'ForwardPlus.HiZ'
775
+ // Keyed on the channel count: a one- and a two-channel pyramid are
776
+ // different textures, and pooling them under one key would hand a
777
+ // consumer a pyramid with no nearest channel in it.
778
+ allocationKey: `ForwardPlus.HiZ${options.hiZNearest ? '.MinMax' : ''}`
596
779
  });
597
780
  const hiZFramebufferHandle = builder.createFramebuffer({
598
781
  label: 'HiZFramebuffer',
@@ -628,6 +811,7 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
628
811
  const depthPassResult = requireBuildState(fg, 'depth', 'DepthPrepass', 'SSSProfile');
629
812
  const preLightTransmissionDepthToken = fg.state.preLightTransmissionDepthToken;
630
813
  const shadowMaskHandle = blackboard.get(FrameResources.ShadowMask);
814
+ const waterCausticsHandle = blackboard.get(FrameResources.WaterCaustics);
631
815
  const renderDepthAttachment = fg.state.renderDepthAttachment;
632
816
  const sssProfileResult = graph.addPass('SSSProfile', (builder)=>{
633
817
  builder.read(blackboard.expect(FrameResources.LinearDepth));
@@ -638,6 +822,12 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
638
822
  if (shadowMaskHandle) {
639
823
  builder.read(shadowMaskHandle);
640
824
  }
825
+ if (waterCausticsHandle) {
826
+ // Same reason as the shadow mask: the map reaches the shader through
827
+ // ctx.waterCausticTexture, so only a declared read stops the executor
828
+ // from recycling it while this pass is still sampling it.
829
+ builder.read(waterCausticsHandle);
830
+ }
641
831
  const profileHandle = builder.createTexture({
642
832
  format: 'rgba16f',
643
833
  label: 'sssProfile',
@@ -686,6 +876,7 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
686
876
  const depthPassResult = requireBuildState(fg, 'depth', 'DepthPrepass', 'SceneColorGrab');
687
877
  const preLightTransmissionDepthToken = fg.state.preLightTransmissionDepthToken;
688
878
  const shadowMaskHandle = blackboard.get(FrameResources.ShadowMask);
879
+ const waterCausticsHandle = blackboard.get(FrameResources.WaterCaustics);
689
880
  const renderDepthAttachment = fg.state.renderDepthAttachment;
690
881
  const grabResult = graph.addPass('SceneColorGrab', (builder)=>{
691
882
  builder.read(blackboard.expect(FrameResources.LinearDepth));
@@ -696,9 +887,34 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
696
887
  if (shadowMaskHandle) {
697
888
  builder.read(shadowMaskHandle);
698
889
  }
890
+ if (waterCausticsHandle) {
891
+ // Same reason as the shadow mask: the map reaches the shader through
892
+ // ctx.waterCausticTexture, so only a declared read stops the executor
893
+ // from recycling it while this pass is still sampling it.
894
+ builder.read(waterCausticsHandle);
895
+ }
896
+ // A few mip levels, because the refraction background is sampled at a LOD
897
+ // and not only at mip 0: water picks one from how deep and how turbid the
898
+ // column is, so a stone under clear shallows stays sharp while the same
899
+ // stone under metres of silt smears. With a single-level texture the
900
+ // sampler returned mip 0 whatever LOD was asked for, and the blur did
901
+ // nothing.
902
+ //
903
+ // Nothing here generates the chain - both backends do it when the
904
+ // framebuffer is unbound, for any colour attachment with more than one
905
+ // level (WebGL `framebuffer_webgl.ts` unbind, WebGPU `renderpass_webgpu.ts`
906
+ // end), and `generateMipmaps` defaults to true on colour attachments. The
907
+ // level count is the whole precondition.
908
+ //
909
+ // Not the full chain down to 1x1. The consumer clamps its LOD, and the
910
+ // coarsest levels of a full chain average across the whole frame - a
911
+ // sample from one stops being "what is behind the water" and becomes the
912
+ // mean of the image. Generating them would cost bandwidth every frame to
913
+ // produce levels nothing may read.
699
914
  const copyHandle = builder.createTexture({
700
915
  format: ctx.colorFormat,
701
916
  label: 'sceneColorCopy',
917
+ mipLevels: getSceneColorMipLevelCount(ctx.renderWidth, ctx.renderHeight),
702
918
  allocationKey: 'ForwardPlus.SceneColorCopy'
703
919
  });
704
920
  // Isolate depth when SSR inserts transmission depth before LightPass.
@@ -739,6 +955,7 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
739
955
  const { graph, ctx, frame, blackboard, options, backbuffer } = fg;
740
956
  const depthPassResult = requireBuildState(fg, 'depth', 'DepthPrepass', 'LightPass');
741
957
  const shadowMaskHandle = blackboard.get(FrameResources.ShadowMask);
958
+ const waterCausticsHandle = blackboard.get(FrameResources.WaterCaustics);
742
959
  const preLightTransmissionDepthToken = fg.state.preLightTransmissionDepthToken;
743
960
  const hiZHandle = blackboard.get(FrameResources.HiZ);
744
961
  const sssProfileHandle = blackboard.get(FrameResources.SSSProfile);
@@ -809,6 +1026,12 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
809
1026
  if (shadowMaskHandle) {
810
1027
  builder.read(shadowMaskHandle);
811
1028
  }
1029
+ if (waterCausticsHandle) {
1030
+ // Same reason as the shadow mask: the map reaches the shader through
1031
+ // ctx.waterCausticTexture, so only a declared read stops the executor
1032
+ // from recycling it while this pass is still sampling it.
1033
+ builder.read(waterCausticsHandle);
1034
+ }
812
1035
  if (preLightTransmissionDepthToken) {
813
1036
  builder.read(preLightTransmissionDepthToken);
814
1037
  }
@@ -996,6 +1219,68 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
996
1219
  }
997
1220
  }
998
1221
  };
1222
+ /** @internal */ const UnderwaterModule = {
1223
+ type: 'Underwater',
1224
+ reads: [
1225
+ {
1226
+ resource: FrameResources.SceneColor,
1227
+ version: 'current'
1228
+ }
1229
+ ],
1230
+ writes: [
1231
+ FrameResources.SceneColor
1232
+ ],
1233
+ prepare: ({ ctx })=>({
1234
+ enabled: !!ctx.underwater
1235
+ }),
1236
+ setup (fg) {
1237
+ const { graph, ctx, frame, blackboard } = fg;
1238
+ const depthPassResult = requireBuildState(fg, 'depth', 'DepthPrepass', 'Underwater');
1239
+ const lightPassResult = requireBuildState(fg, 'lightPass', 'LightPass', 'Underwater');
1240
+ const sceneColorHandle = blackboard.expect(FrameResources.SceneColor);
1241
+ const waterCausticsHandle = blackboard.get(FrameResources.WaterCaustics);
1242
+ const state = ctx.underwater;
1243
+ // After the sky, before the transparent pass: the medium has to cover the
1244
+ // sky - that is what the horizon fades into underwater - and the opaque
1245
+ // scene is the only thing the depth buffer describes, so it is also the only
1246
+ // thing this can reach. Transparent geometry is on its own, exactly as it is
1247
+ // for atmospheric fog.
1248
+ const underwaterColorHandle = graph.addPass('Underwater', (builder)=>{
1249
+ builder.read(sceneColorHandle);
1250
+ builder.read(depthPassResult.depthFramebufferHandle);
1251
+ if (lightPassResult.sceneColorFramebufferHandle) {
1252
+ builder.read(lightPassResult.sceneColorFramebufferHandle);
1253
+ }
1254
+ if (waterCausticsHandle) {
1255
+ // The light shafts sample the caustic map, which reaches the shader
1256
+ // through ctx.waterCausticTexture rather than through a handle. Same
1257
+ // lifetime argument as the shadow mask in CompositeTail: only a declared
1258
+ // read stops the executor returning it to the pool after LightPass.
1259
+ builder.read(waterCausticsHandle);
1260
+ }
1261
+ const out = builder.write(sceneColorHandle);
1262
+ builder.setExecute((rgCtx)=>{
1263
+ const colorTexture = rgCtx.getTexture(sceneColorHandle);
1264
+ const depthTexture = frame.depthFramebuffer?.getDepthAttachment();
1265
+ if (!colorTexture || !depthTexture) {
1266
+ return;
1267
+ }
1268
+ // Color only. The scene depth is sampled by this pass, which it cannot
1269
+ // be while it is still attached to the bound framebuffer.
1270
+ const colorFramebuffer = rgCtx.createFramebuffer({
1271
+ width: colorTexture.width,
1272
+ height: colorTexture.height,
1273
+ colorAttachments: colorTexture,
1274
+ depthAttachment: null
1275
+ });
1276
+ _underwaterRenderer.render(ctx, state, colorFramebuffer, depthTexture);
1277
+ });
1278
+ return out;
1279
+ });
1280
+ lightPassResult.sceneColorHandle = underwaterColorHandle;
1281
+ blackboard.set(FrameResources.SceneColor, underwaterColorHandle);
1282
+ }
1283
+ };
999
1284
  /** @internal */ const CompositeTailModule = {
1000
1285
  type: 'CompositeTail',
1001
1286
  writes: [
@@ -1011,6 +1296,8 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
1011
1296
  const depthPassResult = requireBuildState(fg, 'depth', 'DepthPrepass', 'CompositeTail');
1012
1297
  const hiZHandle = blackboard.get(FrameResources.HiZ);
1013
1298
  const sceneColorCopyHandle = blackboard.get(FrameResources.SceneColorCopy);
1299
+ const shadowMaskHandle = blackboard.get(FrameResources.ShadowMask);
1300
+ const waterCausticsHandle = blackboard.get(FrameResources.WaterCaustics);
1014
1301
  const lightPassResult = requireBuildState(fg, 'lightPass', 'LightPass', 'CompositeTail');
1015
1302
  const renderDepthAttachment = fg.state.renderDepthAttachment;
1016
1303
  const useFinalFramebufferAsIntermediate = fg.state.useFinalFramebufferAsIntermediate;
@@ -1074,9 +1361,27 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
1074
1361
  if (sceneColorCopyHandle) {
1075
1362
  builder.read(sceneColorCopyHandle);
1076
1363
  }
1364
+ if (shadowMaskHandle) {
1365
+ // Transparent geometry goes through the same clustered-light bind group
1366
+ // as the opaque pass, which binds ctx.shadowMaskTexture whenever
1367
+ // screenSpaceShadowMask is on (ShaderHelper, UNIFORM_NAME_SHADOW_MASK).
1368
+ // Declaring the read is what keeps the mask alive: the executor derives
1369
+ // each resource's lastUse from declared reads and returns the texture to
1370
+ // the pool immediately after, so without this the mask would be recycled
1371
+ // after LightPass while this pass is still sampling it.
1372
+ builder.read(shadowMaskHandle);
1373
+ }
1374
+ if (waterCausticsHandle) {
1375
+ // Same lifetime argument as the shadow mask above.
1376
+ builder.read(waterCausticsHandle);
1377
+ }
1077
1378
  builder.read(opaqueChainResult.color);
1078
1379
  const out = builder.write(opaqueChainResult.color);
1079
1380
  builder.setExecute((rgCtx)=>{
1381
+ // Pulled here rather than inherited from LightPass's execute. The value
1382
+ // is the same, but relying on the leftover would make this pass depend
1383
+ // on execution order instead of on its own declared inputs.
1384
+ ctx.shadowMaskTexture = shadowMaskHandle ? rgCtx.getTexture(shadowMaskHandle) : null;
1080
1385
  renderTransparentScenePass(frame, rgCtx, opaqueChainRan ? opaqueChainResult.color : null, lightPassResult.sceneColorFramebufferHandle);
1081
1386
  });
1082
1387
  return out;
@@ -1185,6 +1490,7 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
1185
1490
  ClusterLights: ClusterLightsModule,
1186
1491
  GPUPicking: GPUPickingModule,
1187
1492
  ShadowMaps: ShadowMapsModule,
1493
+ WaterCaustics: WaterCausticsModule,
1188
1494
  DepthPrepass: DepthPrepassModule,
1189
1495
  ShadowMask: ShadowMaskModule,
1190
1496
  TransmissionDepthForSSR: TransmissionDepthForSSRModule,
@@ -1193,6 +1499,7 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
1193
1499
  SceneColorGrab: SceneColorGrabModule,
1194
1500
  LightPass: LightPassModule,
1195
1501
  SkyPass: SkyPassModule,
1502
+ Underwater: UnderwaterModule,
1196
1503
  CompositeTail: CompositeTailModule
1197
1504
  };
1198
1505
  /** The default module order assembled by {@link createForwardPlusPipeline}. */ const DEFAULT_FORWARD_PLUS_MODULES = [
@@ -1200,6 +1507,7 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
1200
1507
  ClusterLightsModule,
1201
1508
  GPUPickingModule,
1202
1509
  ShadowMapsModule,
1510
+ WaterCausticsModule,
1203
1511
  DepthPrepassModule,
1204
1512
  ShadowMaskModule,
1205
1513
  TransmissionDepthForSSRModule,
@@ -1208,6 +1516,7 @@ function validateProducedFrameResources(blackboard, options, renderQueue) {
1208
1516
  SceneColorGrabModule,
1209
1517
  LightPassModule,
1210
1518
  SkyPassModule,
1519
+ UnderwaterModule,
1211
1520
  CompositeTailModule
1212
1521
  ];
1213
1522
  /** @internal */ let _defaultForwardPlusPipeline = null;
@@ -1235,6 +1544,13 @@ function buildForwardPlusGraphInternal(graph, ctx, renderQueue, options) {
1235
1544
  ctx.SkinSSSTexture = null;
1236
1545
  // ShadowMask sets this only when it produces a texture.
1237
1546
  ctx.shadowMaskTexture = null;
1547
+ // WaterCaustics turns these on together once it has produced a map; they are
1548
+ // read by every light pass, so they must start cleared each frame.
1549
+ ctx.waterCaustics = false;
1550
+ ctx.waterCausticLight = null;
1551
+ ctx.waterCausticTexture = null;
1552
+ ctx.waterCausticHeightTexture = null;
1553
+ ctx.waterCausticUniforms = null;
1238
1554
  const blackboard = new RGBlackboard();
1239
1555
  const frame = {
1240
1556
  ctx,
@@ -1485,7 +1801,47 @@ function cleanupFrame(frame) {
1485
1801
  let _skyMVProgram = null;
1486
1802
  let _skyMVBindGroup = null;
1487
1803
  let _skyMVBox = null;
1488
- /** @internal */ function renderSkyMotionVectors(ctx, rgCtx, framebufferHandle) {
1804
+ /** @internal */ /**
1805
+ * Fills in motion vectors for blended geometry that opted into them.
1806
+ *
1807
+ * @remarks
1808
+ * The transparent queue never reaches the ordinary prepass, so without this a
1809
+ * blended surface carries whatever velocity belongs to what is behind it. The
1810
+ * framebuffer here holds the motion vector attachment alone, so nothing touches
1811
+ * the linear depth the light pass and the screen-space effects read, and the
1812
+ * materials run with depth test on and depth write off, so the scene depth is
1813
+ * left as the opaque pass built it.
1814
+ *
1815
+ * Materials that have not opted in discard, which leaves the velocity beneath
1816
+ * them intact. See {@link MeshMaterial.transparentMotionVector} for why that is
1817
+ * the right default and where the approximation breaks.
1818
+ * @internal
1819
+ */ function renderTransparentMotionVectors(frame, rgCtx, framebufferHandle) {
1820
+ const ctx = frame.ctx;
1821
+ const renderQueue = frame.renderQueue;
1822
+ if (!ctx.motionVectors || !renderQueue.itemList?.transparent) {
1823
+ return;
1824
+ }
1825
+ const device = ctx.device;
1826
+ device.pushDeviceStates();
1827
+ try {
1828
+ device.setFramebuffer(rgCtx.getFramebuffer(framebufferHandle));
1829
+ _depthPass.motionVectorOnly = true;
1830
+ _depthPass.encodeDepth = false;
1831
+ _depthPass.transmission = false;
1832
+ _depthPass.renderBackface = false;
1833
+ // Nothing is cleared: this pass only replaces the pixels its own geometry
1834
+ // covers, and everything else in the target is already correct.
1835
+ _depthPass.clearColor = null;
1836
+ _depthPass.clearDepth = null;
1837
+ _depthPass.clearStencil = null;
1838
+ _depthPass.render(ctx, null, null, renderQueue);
1839
+ } finally{
1840
+ _depthPass.motionVectorOnly = false;
1841
+ device.popDeviceStates();
1842
+ }
1843
+ }
1844
+ function renderSkyMotionVectors(ctx, rgCtx, framebufferHandle) {
1489
1845
  if (!ctx.motionVectorTexture) {
1490
1846
  return;
1491
1847
  }
@@ -1680,7 +2036,11 @@ function blitToCurrentColorAttachment(ctx, source) {
1680
2036
  device.setViewport(null);
1681
2037
  device.setScissor(null);
1682
2038
  ctx.scene.env.sky.renderSky(ctx);
1683
- if (ctx.scene.env.sky.fogPresents) {
2039
+ // Not while submerged: the medium between the eye and the scene is water,
2040
+ // and the Underwater module composites it. The sky itself is still drawn -
2041
+ // that pass then extinguishes it along with everything else at the far
2042
+ // plane, which is what makes the horizon read as water rather than as air.
2043
+ if (ctx.scene.env.sky.fogPresents && !ctx.underwater) {
1684
2044
  // Snapshot between sky and fog. Sky belongs in the copy - a ray that hits
1685
2045
  // the sky should read the sky - while fog does not, because the fog a
1686
2046
  // screen-space pass would sample lies along the camera ray rather than the
@@ -1762,8 +2122,17 @@ function blitToCurrentColorAttachment(ctx, source) {
1762
2122
  let motionVectorFrameCommitted = false;
1763
2123
  try {
1764
2124
  renderQueue = _scenePass.cullScene(ctx, ctx.camera);
2125
+ // Before the options are derived, because being submerged changes them: the
2126
+ // atmosphere is not the medium the camera is looking through, so its fog is
2127
+ // suppressed for the frame.
2128
+ ctx.underwater = selectUnderwaterSource(ctx);
1765
2129
  const options = deriveForwardPlusOptions(ctx.scene, ctx.camera, device.type, renderQueue);
1766
2130
  options.ssgi &&= supportsSSGIRenderTargets(ctx);
2131
+ // Atmospheric fog describes a column of air, which is not what a submerged
2132
+ // camera is looking through. The Underwater module owns the medium instead.
2133
+ // Clearing the flag here also stops SkyPass reserving a fog-free snapshot
2134
+ // for a frame that composites no fog.
2135
+ options.fogPresents &&= !ctx.underwater;
1767
2136
  ctx.SSS = options.sss;
1768
2137
  historyManager = ctx.camera.getHistoryResourceManager();
1769
2138
  if (!historyManager) {