@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,5 +1,5 @@
1
1
  import { Vector3, Vector4, Vector2, REVERSE_Z, DEPTH_FARTHEST, DEPTH_REDUCE_CLOSER, DEPTH_REDUCE_FARTHER } from '@zephyr3d/base';
2
- import { RENDER_PASS_TYPE_DEPTH, RENDER_PASS_TYPE_SHADOWMAP, RENDER_PASS_TYPE_OBJECT_COLOR, RENDER_PASS_TYPE_LIGHT, MaterialVaryingFlags, MORPH_WEIGHTS_VECTOR_COUNT, MORPH_TARGET_POSITION, MORPH_TARGET_NORMAL, MORPH_TARGET_TANGENT, MORPH_ATTRIBUTE_VECTOR_COUNT } from '../../values.js';
2
+ import { RENDER_PASS_TYPE_DEPTH, RENDER_PASS_TYPE_SHADOWMAP, RENDER_PASS_TYPE_OBJECT_COLOR, RENDER_PASS_TYPE_LIGHT, MaterialVaryingFlags, MAX_SKIN_EXTRA_INFLUENCE_PAIRS, MORPH_WEIGHTS_VECTOR_COUNT, MORPH_TARGET_POSITION, MORPH_TARGET_NORMAL, MORPH_TARGET_TANGENT, MORPH_ATTRIBUTE_VECTOR_COUNT, LIGHT_TYPE_DIRECTIONAL } from '../../values.js';
3
3
  import { ProgramBuilder } from '@zephyr3d/device';
4
4
  import { decodeNormalizedFloatFromRGBA, linearToGamma } from '../../shaders/misc.js';
5
5
  import { getSamplerOptions, fetchSampler } from '../../utility/misc.js';
@@ -7,6 +7,7 @@ import { PHYSICAL_BAKE_EXPOSURE } from '../../utility/physical.js';
7
7
  import { getDevice } from '../../app/api.js';
8
8
  import { getDefaultAtmosphereParams, getAtmosphereParamsStruct } from '../../shaders/atmosphere.js';
9
9
  import { getDefaultHeightFogParams, getHeightFogParamsStruct, calculateFog } from '../../shaders/fog.js';
10
+ import { getShadowReceiverNoL, getShadowReceiverBiasFactor } from '../../shadow/receiver_bias.js';
10
11
 
11
12
  const UNIFORM_NAME_LIGHT_BUFFER = 'Z_UniformLightBuffer';
12
13
  const UNIFORM_NAME_LIGHT_INDEX_TEXTURE = 'Z_UniformLightIndexTex';
@@ -16,6 +17,28 @@ const UNIFORM_NAME_SKYDISTANTLIGHT_LUT = 'Z_UniformSkyDistantLightLUT';
16
17
  const UNIFORM_NAME_SHADOW_MAP = 'Z_UniformShadowMap';
17
18
  const UNIFORM_NAME_SHADOW_MASK = 'Z_UniformShadowMask';
18
19
  const UNIFORM_NAME_SHADOW_MASK_MODE = 'Z_UniformShadowMaskMode';
20
+ const UNIFORM_NAME_CAUSTIC_MAP = 'Z_UniformCausticMap';
21
+ const UNIFORM_NAME_CAUSTIC_HEIGHT_MAP = 'Z_UniformCausticHeightMap';
22
+ /**
23
+ * Offset added to a surface height before it is stored in the caustic height
24
+ * map, so that a cleared texel - no surface on that ray - is distinguishable
25
+ * from a surface at the rest level. A ray with no water on it must read as
26
+ * dry, not as submerged under a flat surface: on the down-sun side of a
27
+ * footprint edge, a receiver below the rest level but above a trough would
28
+ * otherwise be lit through water that is not there. Eight keeps troughs down
29
+ * to seven metres positive and leaves half floats millimetre precision.
30
+ * @internal
31
+ */ const CAUSTIC_HEIGHT_BIAS = 8;
32
+ /**
33
+ * Water bodies that can cast caustics into one map.
34
+ *
35
+ * They share the map and the slice, so this is not a limit on how much water a
36
+ * scene may hold - only on how many separate surfaces the receiver will test
37
+ * itself against, which it does per fragment. Four covers a lake with a couple
38
+ * of pools beside it; beyond that the renderer keeps the largest four, which
39
+ * are the ones a viewer is most likely to be standing over.
40
+ */ const MAX_CAUSTIC_WATERS = 4;
41
+ const UNIFORM_NAME_CAUSTIC_PARAMS = 'Z_UniformCausticParams';
19
42
  const UNIFORM_NAME_LINEAR_DEPTH_MAP = 'Z_UniformLinearDepth';
20
43
  const UNIFORM_NAME_LINEAR_DEPTH_MAP_SIZE = 'Z_UniformLinearDepthSize';
21
44
  const UNIFORM_NAME_SCENE_COLOR_MAP = 'Z_UniformSceneColor';
@@ -32,6 +55,8 @@ const UNIFORM_NAME_INSTANCE_DATA_OFFSET = 'Z_UniformInstanceDataOffset';
32
55
  const UNIFORM_NAME_BONE_MATRICES = 'Z_UniformBoneMatrices';
33
56
  const UNIFORM_NAME_BONE_TEXTURE_SIZE = 'Z_UniformBoneTexSize';
34
57
  const UNIFORM_NAME_BONE_INV_BIND_MATRIX = 'Z_UniformBoneInvBindMatrix';
58
+ const UNIFORM_NAME_SKIN_INFLUENCE_DATA = 'Z_UniformSkinInfluenceData';
59
+ const UNIFORM_NAME_SKIN_INFLUENCE_INFO = 'Z_UniformSkinInfluenceInfo';
35
60
  const UNIFORM_NAME_MORPH_DATA = 'Z_UniformMorphData';
36
61
  const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
37
62
  /**
@@ -46,6 +71,7 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
46
71
  /** @internal */ static SKIN_MATRIX_NAME = 'Z_SkinMatrix';
47
72
  static SKIN_PREV_MATRIX_NAME = 'Z_PrevSkinMatrix';
48
73
  static SKIN_BONE_OFFSET = 'Z_boneOffset';
74
+ static RESOLVED_VERTEX_NORMAL_NAME = 'Z_ResolvedVertexNormal';
49
75
  /** @internal */ static _drawableBindGroupLayouts = {};
50
76
  /** @internal */ static _lightUniformShadow = {
51
77
  sunDir: new Vector3(),
@@ -101,6 +127,12 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
101
127
  static getBoneInvBindMatrixUniformName() {
102
128
  return UNIFORM_NAME_BONE_INV_BIND_MATRIX;
103
129
  }
130
+ static getSkinInfluenceDataUniformName() {
131
+ return UNIFORM_NAME_SKIN_INFLUENCE_DATA;
132
+ }
133
+ static getSkinInfluenceInfoUniformName() {
134
+ return UNIFORM_NAME_SKIN_INFLUENCE_INFO;
135
+ }
104
136
  static getMorphDataUniformName() {
105
137
  return UNIFORM_NAME_MORPH_DATA;
106
138
  }
@@ -183,10 +215,22 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
183
215
  pb.vec4('directionCutoff'),
184
216
  pb.mat4('viewMatrix'),
185
217
  pb.vec4('depthBias'),
218
+ // Same slot the lighting pass exposes through getShadowImplParams. A
219
+ // caster normally has no use for it, but one that encodes something
220
+ // beyond depth - deep opacity map layers - needs the same parameters the
221
+ // receiver will decode with.
222
+ pb.vec4('implParams'),
186
223
  pb.int('lightType')
187
224
  ]);
188
225
  scope.camera = cameraStruct().uniform(0);
189
226
  scope.light = lightStruct().uniform(0);
227
+ // Lets the shadow implementation add its own caster inputs. Only deep
228
+ // opacity maps use this, to read the frontmost depth its first pass wrote.
229
+ const shadowLight = ctx.renderPass.light;
230
+ const casterParams = shadowLight ? ctx.shadowMapInfo?.get(shadowLight) : null;
231
+ if (casterParams) {
232
+ casterParams.impl?.declareCasterUniforms(scope, casterParams);
233
+ }
190
234
  } else if (ctx.renderPass.type === RENDER_PASS_TYPE_DEPTH || ctx.renderPass.type === RENDER_PASS_TYPE_OBJECT_COLOR) {
191
235
  scope.camera = cameraStruct().uniform(0);
192
236
  } else if (ctx.renderPass.type === RENDER_PASS_TYPE_LIGHT) {
@@ -272,6 +316,9 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
272
316
  }
273
317
  scope[UNIFORM_NAME_SHADOW_MAP] = tex.uniform(0);
274
318
  }
319
+ if (this.usesWaterCaustics(ctx)) {
320
+ this.declareWaterCausticUniforms(pb);
321
+ }
275
322
  if (ctx.drawEnvLight) {
276
323
  ctx.env.light.envLight.initShaderBindings(pb, ctx);
277
324
  }
@@ -282,8 +329,18 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
282
329
  scope[UNIFORM_NAME_LINEAR_DEPTH_MAP_SIZE] = pb.vec2().uniform(0);
283
330
  }
284
331
  if (ctx.sceneColorTexture) {
285
- // Scene color copy: single mip level
286
- scope[UNIFORM_NAME_SCENE_COLOR_MAP] = pb.tex2D().uniform(0).withSampler(getSamplerOptions('clamp_linear_nomip'));
332
+ // Mip-filtered, because the refraction background is read at a LOD and
333
+ // not only at mip 0: the water picks one from how deep and how turbid
334
+ // the column it is looking through is. A `nomip` sampler pins mip 0
335
+ // whatever LOD the shader passes, so this and the copy's level count
336
+ // have to change together - and each backend only honours one of them.
337
+ // WebGPU binds the sampler declared here as a static binding the runtime
338
+ // cannot overwrite; WebGL falls back to the texture's own sampler, whose
339
+ // mip filter follows its level count.
340
+ //
341
+ // Every other reader of this texture passes an explicit LOD 0 (SSR hits,
342
+ // the sky blend), so none of them changes behaviour.
343
+ scope[UNIFORM_NAME_SCENE_COLOR_MAP] = pb.tex2D().uniform(0).withSampler(getSamplerOptions('clamp_linear'));
287
344
  scope[UNIFORM_NAME_SCENE_COLOR_MAP_SIZE] = pb.vec2().uniform(0);
288
345
  }
289
346
  if (ctx.HiZTexture) {
@@ -322,6 +379,8 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
322
379
  return null;
323
380
  }
324
381
  const pb = scope.$builder;
382
+ const isWebGL = pb.getDevice().type === 'webgl';
383
+ const supportsTextureLoad = !isWebGL;
325
384
  const funcNameGetBoneMatrixFromTexture = 'Z_getBoneMatrixFromTexture';
326
385
  pb.func(funcNameGetBoneMatrixFromTexture, [
327
386
  pb.int('boneIndex')
@@ -354,6 +413,33 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
354
413
  this.$l.m2 = scope.$g[funcNameGetBoneMatrixFromTexture](pb.int(blendIndices[2]));
355
414
  this.$l.m3 = scope.$g[funcNameGetBoneMatrixFromTexture](pb.int(blendIndices[3]));
356
415
  this.$l.m = pb.add(pb.mul(this.m0, blendWeights.x), pb.mul(this.m1, blendWeights.y), pb.mul(this.m2, blendWeights.z), pb.mul(this.m3, blendWeights.w));
416
+ this.$l.skinInfo = this[UNIFORM_NAME_SKIN_INFLUENCE_INFO];
417
+ this.$if(pb.greaterThan(this.skinInfo.z, 4), function() {
418
+ this.$l.vertexIndex = isWebGL ? pb.int(scope.$inputs.zFakeVertexID) : pb.int(scope.$builtins.vertexIndex);
419
+ this.$l.texWidth = pb.int(this.skinInfo.x);
420
+ this.$l.pairCount = pb.int(this.skinInfo.w);
421
+ this.$for(pb.int('pairIndex'), 0, MAX_SKIN_EXTRA_INFLUENCE_PAIRS, function() {
422
+ this.$if(pb.greaterThanEqual(this.pairIndex, this.pairCount), function() {
423
+ this.$break();
424
+ });
425
+ this.$l.pixelIndex = pb.add(pb.mul(this.vertexIndex, this.pairCount), this.pairIndex);
426
+ this.$l.xIndex = pb.mod(this.pixelIndex, this.texWidth);
427
+ this.$l.yIndex = pb.div(this.pixelIndex, this.texWidth);
428
+ if (supportsTextureLoad) {
429
+ this.$l.extra = pb.textureLoad(this[UNIFORM_NAME_SKIN_INFLUENCE_DATA], pb.ivec2(this.xIndex, this.yIndex), 0);
430
+ } else {
431
+ this.$l.u = pb.div(pb.add(pb.float(this.xIndex), 0.5), this.skinInfo.x);
432
+ this.$l.v = pb.div(pb.add(pb.float(this.yIndex), 0.5), this.skinInfo.y);
433
+ this.$l.extra = pb.textureSampleLevel(this[UNIFORM_NAME_SKIN_INFLUENCE_DATA], pb.vec2(this.u, this.v), 0);
434
+ }
435
+ this.$if(pb.greaterThan(this.extra.y, 0), function() {
436
+ this.m = pb.add(this.m, pb.mul(scope.$g[funcNameGetBoneMatrixFromTexture](pb.int(this.extra.x)), this.extra.y));
437
+ });
438
+ this.$if(pb.greaterThan(this.extra.w, 0), function() {
439
+ this.m = pb.add(this.m, pb.mul(scope.$g[funcNameGetBoneMatrixFromTexture](pb.int(this.extra.z)), this.extra.w));
440
+ });
441
+ });
442
+ });
357
443
  this.$return(pb.mul(invBindMatrix, this.m));
358
444
  });
359
445
  return scope.$g[funcNameGetSkinningMatrix]();
@@ -389,7 +475,8 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
389
475
  });
390
476
  this.$l.weight = morphInfo.at(pb.add(1, this.i)).at(this.j);
391
477
  this.$if(pb.notEqual(this.weight, 0), function() {
392
- this.$l.pixelIndex = pb.float(pb.add(this.offset, pb.mul(this.index, this.numVertices), this.vertexIndex));
478
+ this.$l.targetIndex = pb.int(morphInfo.at(pb.add(1 + MORPH_WEIGHTS_VECTOR_COUNT, this.i)).at(this.j));
479
+ this.$l.pixelIndex = pb.float(pb.add(this.offset, pb.mul(this.targetIndex, this.numVertices), this.vertexIndex));
393
480
  this.$l.xIndex = pb.mod(this.pixelIndex, this.texWidth);
394
481
  this.$l.yIndex = pb.floor(pb.div(this.pixelIndex, this.texWidth));
395
482
  this.$l.u = pb.div(pb.add(this.xIndex, 0.5), this.texWidth);
@@ -405,7 +492,8 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
405
492
  this.$l.j = pb.compAnd(this.t, 3);
406
493
  this.$l.weight = morphInfo.at(pb.add(1, this.i)).at(this.j);
407
494
  this.$if(pb.notEqual(this.weight, 0), function() {
408
- this.$l.pixelIndex = pb.float(pb.add(this.offset, pb.mul(this.t, this.numVertices), this.vertexIndex));
495
+ this.$l.targetIndex = pb.int(morphInfo.at(pb.add(1 + MORPH_WEIGHTS_VECTOR_COUNT, this.i)).at(this.j));
496
+ this.$l.pixelIndex = pb.float(pb.add(this.offset, pb.mul(this.targetIndex, this.numVertices), this.vertexIndex));
409
497
  this.$l.xIndex = pb.mod(this.pixelIndex, this.texWidth);
410
498
  this.$l.yIndex = pb.floor(pb.div(this.pixelIndex, this.texWidth));
411
499
  this.$l.u = pb.div(pb.add(this.xIndex, 0.5), this.texWidth);
@@ -417,17 +505,166 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
417
505
  }
418
506
  this.$return(this.value);
419
507
  });
420
- const pos = 1 + MORPH_WEIGHTS_VECTOR_COUNT + (attrib >> 2);
508
+ const pos = 1 + MORPH_WEIGHTS_VECTOR_COUNT * 2 + (attrib >> 2);
421
509
  const comp = attrib & 3;
422
510
  const offset = scope[this.getMorphInfoUniformName()][pos][comp];
423
511
  return scope[funcName](pb.int(offset));
424
512
  }
513
+ /**
514
+ * Normalize a morphed direction without allowing cancellation between
515
+ * multiple morph targets to turn a valid base direction into NaN/noise.
516
+ *
517
+ * Morph normal/tangent data is additive. When two targets cancel the base
518
+ * direction, the zero-length result is not a meaningful direction, so the
519
+ * authored vertex direction is the least surprising fallback.
520
+ * @internal
521
+ */ static defineNormalizeMorphedDirection(scope) {
522
+ const pb = scope.$builder;
523
+ const funcName = 'Z_normalizeMorphedDirection';
524
+ pb.func(funcName, [
525
+ pb.vec3('base'),
526
+ pb.vec3('delta')
527
+ ], function() {
528
+ this.$l.morphed = pb.add(this.base, this.delta);
529
+ this.$l.lengthSquared = pb.dot(this.morphed, this.morphed);
530
+ this.$if(pb.greaterThan(this.lengthSquared, 1e-6), function() {
531
+ this.$return(pb.mul(this.morphed, pb.inverseSqrt(this.lengthSquared)));
532
+ });
533
+ this.$l.baseLengthSquared = pb.dot(this.base, this.base);
534
+ this.$if(pb.greaterThan(this.baseLengthSquared, 1e-6), function() {
535
+ this.$return(pb.mul(this.base, pb.inverseSqrt(this.baseLengthSquared)));
536
+ });
537
+ this.$return(pb.vec3(0, 0, 1));
538
+ });
539
+ return funcName;
540
+ }
541
+ /**
542
+ * Mixes morph directions according to the amount of positional deformation
543
+ * contributed by each active target. This avoids letting a target with a
544
+ * large authored normal delta dominate a target that actually moves the
545
+ * surface much farther at the current vertex.
546
+ *
547
+ * The additive result remains the fallback for assets without position
548
+ * morph data, and for vertices whose position deltas are all zero.
549
+ * @internal
550
+ */ static calculateMorphDirectionByDisplacement(scope, base, attrib) {
551
+ const pb = scope.$builder;
552
+ const isWebGL1 = pb.getDevice().type === 'webgl';
553
+ if (pb.shaderKind !== 'vertex') {
554
+ throw new Error(`ShaderHelper.calculateMorphDirectionByDisplacement(): must be called at vertex stage`);
555
+ }
556
+ const that = this;
557
+ const funcName = 'Z_calculateMorphDirectionByDisplacement';
558
+ const normalizeName = this.defineNormalizeMorphedDirection(scope);
559
+ pb.func(funcName, [
560
+ pb.vec3('base'),
561
+ pb.int('directionOffset'),
562
+ pb.int('positionOffset')
563
+ ], function() {
564
+ this.$l.vertexIndex = isWebGL1 ? pb.int(scope.$inputs.zFakeVertexID) : pb.int(scope.$builtins.vertexIndex);
565
+ const morphInfo = scope[that.getMorphInfoUniformName()];
566
+ this.$l.metaData = pb.ivec4(morphInfo[0]);
567
+ this.$l.texWidth = pb.float(this.metaData.x);
568
+ this.$l.texHeight = pb.float(this.metaData.y);
569
+ this.$l.numVertices = this.metaData.z;
570
+ this.$l.numTargets = this.metaData.w;
571
+ this.$l.directionDelta = pb.vec3(0);
572
+ this.$l.weightedDirection = pb.vec3(0);
573
+ this.$l.weightSum = pb.float(0);
574
+ const accumulateTarget = (targetScope)=>{
575
+ targetScope.$if(pb.notEqual(targetScope.weight, 0), function() {
576
+ this.$l.targetIndex = pb.int(targetScope['targetIndex']);
577
+ this.$l.positionValue = pb.vec3(0);
578
+ this.$l.directionValue = pb.vec3(0);
579
+ this.$if(pb.greaterThanEqual(targetScope['directionOffset'], 0), function() {
580
+ this.$l.pixelIndex = pb.float(pb.add(targetScope['directionOffset'], pb.mul(this.targetIndex, targetScope['numVertices']), targetScope['vertexIndex']));
581
+ this.$l.xIndex = pb.mod(this.pixelIndex, targetScope['texWidth']);
582
+ this.$l.yIndex = pb.floor(pb.div(this.pixelIndex, targetScope['texWidth']));
583
+ this.$l.u = pb.div(pb.add(this.xIndex, 0.5), targetScope['texWidth']);
584
+ this.$l.v = pb.div(pb.add(this.yIndex, 0.5), targetScope['texHeight']);
585
+ this.directionValue = pb.textureSampleLevel(scope[that.getMorphDataUniformName()], pb.vec2(this.u, this.v), 0).xyz;
586
+ });
587
+ this.$if(pb.greaterThanEqual(targetScope['positionOffset'], 0), function() {
588
+ this.$l.pixelIndex = pb.float(pb.add(targetScope['positionOffset'], pb.mul(this.targetIndex, targetScope['numVertices']), targetScope['vertexIndex']));
589
+ this.$l.xIndex = pb.mod(this.pixelIndex, targetScope['texWidth']);
590
+ this.$l.yIndex = pb.floor(pb.div(this.pixelIndex, targetScope['texWidth']));
591
+ this.$l.u = pb.div(pb.add(this.xIndex, 0.5), targetScope['texWidth']);
592
+ this.$l.v = pb.div(pb.add(this.yIndex, 0.5), targetScope['texHeight']);
593
+ this.positionValue = pb.textureSampleLevel(scope[that.getMorphDataUniformName()], pb.vec2(this.u, this.v), 0).xyz;
594
+ });
595
+ this.directionDelta = pb.add(this.directionDelta, pb.mul(this.directionValue, targetScope.weight));
596
+ this.$l.targetDirection = scope.$g[normalizeName](this.base, pb.mul(this.directionValue, targetScope.weight));
597
+ this.$l.displacementWeight = pb.mul(pb.abs(targetScope.weight), pb.sqrt(pb.dot(this.positionValue, this.positionValue)));
598
+ this.weightedDirection = pb.add(this.weightedDirection, pb.mul(this.targetDirection, this.displacementWeight));
599
+ this.weightSum = pb.add(this.weightSum, this.displacementWeight);
600
+ });
601
+ };
602
+ if (isWebGL1) {
603
+ this.$for(pb.int('i'), 0, MORPH_WEIGHTS_VECTOR_COUNT, function() {
604
+ this.$for(pb.int('j'), 0, 4, function() {
605
+ this.$l.index = pb.add(pb.mul(this.i, 4), this.j);
606
+ this.$if(pb.greaterThanEqual(this.index, this.numTargets), function() {
607
+ this.$break();
608
+ });
609
+ this.$l.weight = morphInfo.at(pb.add(1, this.i)).at(this.j);
610
+ this.$l.targetIndex = pb.int(morphInfo.at(pb.add(1 + MORPH_WEIGHTS_VECTOR_COUNT, this.i)).at(this.j));
611
+ accumulateTarget(this);
612
+ });
613
+ });
614
+ } else {
615
+ this.$for(pb.int('t'), 0, this.numTargets, function() {
616
+ this.$l.i = pb.sar(this.t, 2);
617
+ this.$l.j = pb.compAnd(this.t, 3);
618
+ this.$l.weight = morphInfo.at(pb.add(1, this.i)).at(this.j);
619
+ this.$l.targetIndex = pb.int(morphInfo.at(pb.add(1 + MORPH_WEIGHTS_VECTOR_COUNT, this.i)).at(this.j));
620
+ accumulateTarget(this);
621
+ });
622
+ }
623
+ this.$l.fallback = scope.$g[normalizeName](this.base, this.directionDelta);
624
+ this.$if(pb.lessThan(this.weightSum, 1e-6), function() {
625
+ this.$return(this.fallback);
626
+ });
627
+ this.$l.weightedLengthSquared = pb.dot(this.weightedDirection, this.weightedDirection);
628
+ this.$if(pb.greaterThan(this.weightedLengthSquared, 1e-6), function() {
629
+ this.$return(pb.mul(this.weightedDirection, pb.inverseSqrt(this.weightedLengthSquared)));
630
+ });
631
+ this.$return(this.fallback);
632
+ });
633
+ const directionPos = 1 + MORPH_WEIGHTS_VECTOR_COUNT * 2 + (attrib >> 2);
634
+ const directionComp = attrib & 3;
635
+ const positionPos = 1 + MORPH_WEIGHTS_VECTOR_COUNT * 2 + (MORPH_TARGET_POSITION >> 2);
636
+ const positionComp = MORPH_TARGET_POSITION & 3;
637
+ const morphInfo = scope[this.getMorphInfoUniformName()];
638
+ return scope.$g[funcName](base.xyz, pb.int(morphInfo[directionPos][directionComp]), pb.int(morphInfo[positionPos][positionComp]));
639
+ }
640
+ /** @internal */ static orthogonalizeMorphedTangent(scope, normal, tangent) {
641
+ const pb = scope.$builder;
642
+ const funcName = 'Z_orthogonalizeMorphedTangent';
643
+ pb.func(funcName, [
644
+ pb.vec3('normal'),
645
+ pb.vec3('tangent')
646
+ ], function() {
647
+ this.$l.normalLengthSquared = pb.dot(this.normal, this.normal);
648
+ this.$if(pb.greaterThan(this.normalLengthSquared, 1e-6), function() {
649
+ this.$l.n = pb.mul(this.normal, pb.inverseSqrt(this.normalLengthSquared));
650
+ this.$l.projected = pb.sub(this.tangent, pb.mul(this.n, pb.dot(this.n, this.tangent)));
651
+ this.$l.tangentLengthSquared = pb.dot(this.projected, this.projected);
652
+ this.$if(pb.greaterThan(this.tangentLengthSquared, 1e-6), function() {
653
+ this.$return(pb.mul(this.projected, pb.inverseSqrt(this.tangentLengthSquared)));
654
+ });
655
+ });
656
+ this.$return(this.tangent);
657
+ });
658
+ return scope.$g[funcName](normal.xyz, tangent.xyz);
659
+ }
425
660
  /** @internal */ static prepareSkinAnimation(scope) {
426
661
  if (!this.hasSkinning(scope)) {
427
662
  return;
428
663
  }
429
664
  const that = this;
430
665
  const pb = scope.$builder;
666
+ const isWebGL = pb.getDevice().type === 'webgl';
667
+ const supportsTextureLoad = !isWebGL;
431
668
  const funcNameGetBoneMatrixFromTexture = 'Z_getBoneMatrixFromTexture';
432
669
  pb.func(funcNameGetBoneMatrixFromTexture, [
433
670
  pb.float('boneIndex'),
@@ -461,6 +698,33 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
461
698
  this.$l.m2 = scope.$g[funcNameGetBoneMatrixFromTexture](blendIndices[2], this.boneOffset);
462
699
  this.$l.m3 = scope.$g[funcNameGetBoneMatrixFromTexture](blendIndices[3], this.boneOffset);
463
700
  this.$l.m = pb.add(pb.mul(this.m0, blendWeights.x), pb.mul(this.m1, blendWeights.y), pb.mul(this.m2, blendWeights.z), pb.mul(this.m3, blendWeights.w));
701
+ this.$l.skinInfo = this[UNIFORM_NAME_SKIN_INFLUENCE_INFO];
702
+ this.$if(pb.greaterThan(this.skinInfo.z, 4), function() {
703
+ this.$l.vertexIndex = isWebGL ? pb.int(scope.$inputs.zFakeVertexID) : pb.int(scope.$builtins.vertexIndex);
704
+ this.$l.texWidth = pb.int(this.skinInfo.x);
705
+ this.$l.pairCount = pb.int(this.skinInfo.w);
706
+ this.$for(pb.int('pairIndex'), 0, MAX_SKIN_EXTRA_INFLUENCE_PAIRS, function() {
707
+ this.$if(pb.greaterThanEqual(this.pairIndex, this.pairCount), function() {
708
+ this.$break();
709
+ });
710
+ this.$l.pixelIndex = pb.add(pb.mul(this.vertexIndex, this.pairCount), this.pairIndex);
711
+ this.$l.xIndex = pb.mod(this.pixelIndex, this.texWidth);
712
+ this.$l.yIndex = pb.div(this.pixelIndex, this.texWidth);
713
+ if (supportsTextureLoad) {
714
+ this.$l.extra = pb.textureLoad(this[UNIFORM_NAME_SKIN_INFLUENCE_DATA], pb.ivec2(this.xIndex, this.yIndex), 0);
715
+ } else {
716
+ this.$l.u = pb.div(pb.add(pb.float(this.xIndex), 0.5), this.skinInfo.x);
717
+ this.$l.v = pb.div(pb.add(pb.float(this.yIndex), 0.5), this.skinInfo.y);
718
+ this.$l.extra = pb.textureSampleLevel(this[UNIFORM_NAME_SKIN_INFLUENCE_DATA], pb.vec2(this.u, this.v), 0);
719
+ }
720
+ this.$if(pb.greaterThan(this.extra.y, 0), function() {
721
+ this.m = pb.add(this.m, pb.mul(scope.$g[funcNameGetBoneMatrixFromTexture](this.extra.x, this.boneOffset), this.extra.y));
722
+ });
723
+ this.$if(pb.greaterThan(this.extra.w, 0), function() {
724
+ this.m = pb.add(this.m, pb.mul(scope.$g[funcNameGetBoneMatrixFromTexture](this.extra.z, this.boneOffset), this.extra.w));
725
+ });
726
+ });
727
+ });
464
728
  this.$return(pb.mul(invBindMatrix, this.m));
465
729
  });
466
730
  const motionVector = !!this.getUnjitteredViewProjectionMatrix(scope);
@@ -535,6 +799,19 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
535
799
  }
536
800
  }
537
801
  /**
802
+ * Gets the skinning matrix in the vertex shader, if the mesh being drawn is skinned.
803
+ *
804
+ * @remarks
805
+ * Lets a material apply the same skinning transform to its own uniform-supplied
806
+ * object-space vectors that {@link ShaderHelper.resolveVertexNormal} applies to
807
+ * the normal attribute, so such vectors stay attached to the deforming mesh.
808
+ *
809
+ * @param scope - Current shader scope, must be vertex stage
810
+ * @returns The skinning matrix of type mat4, or null when the mesh is not skinned
811
+ */ static getSkinMatrix(scope) {
812
+ return scope[this.SKIN_MATRIX_NAME] ?? null;
813
+ }
814
+ /**
538
815
  * Calculates the normal vector of type vec3 in object space
539
816
  *
540
817
  * @param scope - Current shader scope
@@ -550,6 +827,10 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
550
827
  if (!funcScope || !funcScope.$isMain()) {
551
828
  throw new Error(`ShaderHelper.resolveVertexNormal(): must be called at entry function`);
552
829
  }
830
+ const useVertexAttrib = !normal;
831
+ if (useVertexAttrib && scope[this.RESOLVED_VERTEX_NORMAL_NAME]) {
832
+ return scope[this.RESOLVED_VERTEX_NORMAL_NAME];
833
+ }
553
834
  if (!normal) {
554
835
  if (!scope.$getVertexAttrib('normal')) {
555
836
  scope.$inputs.Z_normal = pb.vec3().attrib('normal');
@@ -557,13 +838,17 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
557
838
  normal = scope.$getVertexAttrib('normal');
558
839
  }
559
840
  if (this.hasMorphing(scope)) {
560
- normal = pb.normalize(pb.add(normal, this.calculateMorphDelta(scope, MORPH_TARGET_NORMAL).xyz));
841
+ normal = this.calculateMorphDirectionByDisplacement(scope, normal, MORPH_TARGET_NORMAL);
561
842
  }
843
+ let resolvedNormal = normal;
562
844
  if (scope[this.SKIN_MATRIX_NAME]) {
563
- return pb.mul(scope[this.SKIN_MATRIX_NAME], pb.vec4(normal, 0)).xyz;
564
- } else {
565
- return normal;
845
+ resolvedNormal = pb.mul(scope[this.SKIN_MATRIX_NAME], pb.vec4(normal, 0)).xyz;
846
+ }
847
+ if (useVertexAttrib) {
848
+ scope.$l[this.RESOLVED_VERTEX_NORMAL_NAME] = resolvedNormal;
849
+ resolvedNormal = scope[this.RESOLVED_VERTEX_NORMAL_NAME];
566
850
  }
851
+ return resolvedNormal;
567
852
  }
568
853
  /**
569
854
  * Calculates the tangent vector of type vec3 in object space
@@ -588,13 +873,16 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
588
873
  tangent = scope.$getVertexAttrib('tangent');
589
874
  }
590
875
  if (this.hasMorphing(scope)) {
591
- tangent = pb.normalize(pb.add(tangent, pb.vec4(this.calculateMorphDelta(scope, MORPH_TARGET_TANGENT).xyz, 0)));
876
+ tangent = pb.vec4(this.calculateMorphDirectionByDisplacement(scope, tangent.xyz, MORPH_TARGET_TANGENT), tangent.w);
592
877
  }
593
878
  if (scope[this.SKIN_MATRIX_NAME]) {
594
- return pb.vec4(pb.mul(scope[this.SKIN_MATRIX_NAME], pb.vec4(tangent.xyz, 0)).xyz, tangent.w);
595
- } else {
596
- return tangent;
879
+ tangent = pb.vec4(pb.mul(scope[this.SKIN_MATRIX_NAME], pb.vec4(tangent.xyz, 0)).xyz, tangent.w);
880
+ }
881
+ if (this.hasMorphing(scope) && scope.$getVertexAttrib('normal')) {
882
+ const resolvedNormal = scope[this.RESOLVED_VERTEX_NORMAL_NAME] ?? this.resolveVertexNormal(scope);
883
+ tangent = pb.vec4(this.orthogonalizeMorphedTangent(scope, resolvedNormal, tangent.xyz), tangent.w);
597
884
  }
885
+ return tangent;
598
886
  }
599
887
  /**
600
888
  * Gets the uniform variable of type mat4 which holds the world matrix of current object to be drawn
@@ -676,10 +964,12 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
676
964
  scope[UNIFORM_NAME_BONE_MATRICES] = pb.tex2D().uniform(1).sampleType('unfilterable-float');
677
965
  scope[UNIFORM_NAME_BONE_INV_BIND_MATRIX] = pb.mat4().uniform(1);
678
966
  scope[UNIFORM_NAME_BONE_TEXTURE_SIZE] = pb.vec2().uniform(1);
967
+ scope[UNIFORM_NAME_SKIN_INFLUENCE_DATA] = pb.tex2D().uniform(1).sampleType('unfilterable-float');
968
+ scope[UNIFORM_NAME_SKIN_INFLUENCE_INFO] = pb.vec4().uniform(1);
679
969
  }
680
970
  if (morphing) {
681
971
  scope[UNIFORM_NAME_MORPH_DATA] = pb.tex2D().uniform(1).sampleType('unfilterable-float');
682
- scope[UNIFORM_NAME_MORPH_INFO] = pb.vec4[1 + MORPH_WEIGHTS_VECTOR_COUNT + MORPH_ATTRIBUTE_VECTOR_COUNT]().uniformBuffer(1);
972
+ scope[UNIFORM_NAME_MORPH_INFO] = pb.vec4[1 + MORPH_WEIGHTS_VECTOR_COUNT * 2 + MORPH_ATTRIBUTE_VECTOR_COUNT]().uniformBuffer(1);
683
973
  }
684
974
  }
685
975
  /** @internal */ static prepareVertexShaderCommon(pb, ctx) {
@@ -735,8 +1025,10 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
735
1025
  directionCutoff: light.directionAndCutoff,
736
1026
  viewMatrix: light.viewMatrix,
737
1027
  depthBias: shadowMapParams.depthBiasValues[0],
1028
+ implParams: shadowMapParams.impl.getParams(this._lightUniformShadow.implParams),
738
1029
  lightType: light.lightType
739
1030
  });
1031
+ shadowMapParams.impl?.applyCasterUniforms(bindGroup, shadowMapParams);
740
1032
  }
741
1033
  }
742
1034
  /** @internal */ static setFogUniforms(bindGroup, withAerialPerspective, fogType, additive, atmosphereParams, heightFogParams, aerialPerspectiveLUT, skyDistantLightLUT) {
@@ -834,10 +1126,329 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
834
1126
  bindGroup.setTexture(UNIFORM_NAME_SHADOW_MASK, ctx.shadowMaskTexture ?? this.getDummyShadowMask(ctx.device), fetchSampler('clamp_nearest'));
835
1127
  }
836
1128
  bindGroup.setTexture(UNIFORM_NAME_BAKED_SKY_MAP, ctx.scene.env.sky.getBakedSkyTexture(ctx));
1129
+ if (this.usesWaterCaustics(ctx)) {
1130
+ this.setWaterCausticUniforms(bindGroup, ctx);
1131
+ }
837
1132
  if (ctx.drawEnvLight) {
838
1133
  ctx.env.light.envLight.updateBindGroup(bindGroup, ctx);
839
1134
  }
840
1135
  }
1136
+ /**
1137
+ * Whether the pass being built modulates its light with the water caustic map.
1138
+ *
1139
+ * True for every light pass of a frame that produced a map; which light the
1140
+ * caustics actually attach to is decided per-fragment by
1141
+ * {@link ShaderHelper.calculateWaterCaustic}. Both the shader variant hash and
1142
+ * the global bind group hash fold this in, so a program that declares the
1143
+ * caustic uniforms is always paired with a bind group that supplies them.
1144
+ *
1145
+ * Deliberately a function of the draw context alone, with no material input.
1146
+ * The caustic uniforms sit in the global bind group, whose layout is built
1147
+ * once per {@link DrawContext.renderPassHash} and shared by every material in
1148
+ * the pass, so the declarations have to be uniform across the pass. A material
1149
+ * that must not receive caustics - water itself - opts out of the sampling
1150
+ * instead, through `receivesWaterCaustics()`; the uniforms stay declared and
1151
+ * simply go unread.
1152
+ *
1153
+ * @param ctx - Current draw context.
1154
+ * @returns True when the caustic uniforms take part in this pass.
1155
+ * @internal
1156
+ */ static usesWaterCaustics(ctx) {
1157
+ return !!ctx.waterCaustics && !!ctx.waterCausticLight && ctx.renderPass.type === RENDER_PASS_TYPE_LIGHT;
1158
+ }
1159
+ /**
1160
+ * Declares the global uniforms {@link ShaderHelper.calculateWaterCaustic} reads.
1161
+ *
1162
+ * Split out of the global uniform setup so a test can build a program that
1163
+ * exercises the sampling code without standing up a whole light pass.
1164
+ *
1165
+ * @param pb - Program builder whose global scope receives the uniforms.
1166
+ * @internal
1167
+ */ /**
1168
+ * Warps a linear caustic-map coordinate so texels concentrate near the centre.
1169
+ *
1170
+ * The map covers a fixed footprint, so its world texel size is the range
1171
+ * divided by the resolution and a range large enough to light the scene is
1172
+ * already too coarse to resolve a caustic cell. This trades the edge of the
1173
+ * map, which is far from the camera, for the middle, which is not:
1174
+ *
1175
+ * ```
1176
+ * warp(u) = u (1 + k) / (1 + k |u|)
1177
+ * unwarp(w) = w / ((1 + k) - k |w|)
1178
+ * ```
1179
+ *
1180
+ * Texel density is `1 + k` times uniform at the centre and `1 / (1 + k)` times
1181
+ * it at the border, and `k = 0` is exactly the identity, so the pass can apply
1182
+ * this unconditionally. Both ends of `[-1, 1]` are fixed points, which is what
1183
+ * lets the warp sit inside the existing parameterisation without moving the
1184
+ * footprint the receiver gates on. The first derivative is continuous at the
1185
+ * centre; the second flips sign there, which is not visible in a pattern this
1186
+ * noisy.
1187
+ *
1188
+ * Applied per axis rather than radially: the photon grid, the map and the blur
1189
+ * are all axis-aligned, so a separable warp keeps photon `(i,j)` on texel
1190
+ * `(i,j)` for calm water and leaves the map's normalisation to 1.0 intact.
1191
+ *
1192
+ * @param scope - Current shader scope.
1193
+ * @param ndc - Linear map coordinate in `[-1, 1]`.
1194
+ * @param strength - Warp strength `k`; 0 is the identity.
1195
+ * @returns The warped coordinate, also in `[-1, 1]`.
1196
+ * @internal
1197
+ */ static warpCausticNDC(scope, ndc, strength) {
1198
+ const pb = scope.$builder;
1199
+ const funcName = 'Z_warpCausticNDC';
1200
+ pb.func(funcName, [
1201
+ pb.vec2('ndc'),
1202
+ pb.float('k')
1203
+ ], function() {
1204
+ this.$return(pb.div(pb.mul(this.ndc, pb.add(this.k, 1)), pb.add(pb.vec2(1), pb.mul(pb.abs(this.ndc), this.k))));
1205
+ });
1206
+ return pb.getGlobalScope()[funcName](ndc, strength);
1207
+ }
1208
+ /**
1209
+ * Inverse of {@link ShaderHelper.warpCausticNDC}.
1210
+ *
1211
+ * The denominator is `1` at the border and `1 + k` at the centre, so it stays
1212
+ * positive for any input the passes feed it - all of which come from `[-1, 1]`.
1213
+ *
1214
+ * @param scope - Current shader scope.
1215
+ * @param warped - Warped map coordinate in `[-1, 1]`.
1216
+ * @param strength - Warp strength `k`; 0 is the identity.
1217
+ * @returns The linear coordinate the warp came from.
1218
+ * @internal
1219
+ */ static unwarpCausticNDC(scope, warped, strength) {
1220
+ const pb = scope.$builder;
1221
+ const funcName = 'Z_unwarpCausticNDC';
1222
+ pb.func(funcName, [
1223
+ pb.vec2('warped'),
1224
+ pb.float('k')
1225
+ ], function() {
1226
+ this.$return(pb.div(this.warped, pb.sub(pb.vec2(pb.add(this.k, 1)), pb.mul(pb.abs(this.warped), this.k))));
1227
+ });
1228
+ return pb.getGlobalScope()[funcName](warped, strength);
1229
+ }
1230
+ /** @internal */ static declareWaterCausticUniforms(pb) {
1231
+ const scope = pb.getGlobalScope();
1232
+ const causticStruct = pb.defineStruct([
1233
+ pb.vec4('frameX'),
1234
+ pb.vec4('frameY'),
1235
+ pb.vec4('center'),
1236
+ pb.vec4('lightDir'),
1237
+ pb.vec4('params'),
1238
+ pb.vec4('extinction'),
1239
+ // Per water body, because the slice is shared but the water is not: each
1240
+ // has its own surface height, footprint and medium, and none of the three
1241
+ // means anything until a receiver has resolved which water is above it.
1242
+ pb.int('waterCount'),
1243
+ pb.vec4[MAX_CAUSTIC_WATERS]('waterRegions'),
1244
+ // (sigma_t.xyz, surface height)
1245
+ pb.vec4[MAX_CAUSTIC_WATERS]('waterMedia')
1246
+ ]);
1247
+ scope[UNIFORM_NAME_CAUSTIC_PARAMS] = causticStruct().uniform(0);
1248
+ // The map is single mip and read bilinearly; clamping keeps receivers
1249
+ // outside the covered area reading the border rather than wrapping.
1250
+ scope[UNIFORM_NAME_CAUSTIC_MAP] = pb.tex2D().uniform(0).withSampler(getSamplerOptions('clamp_linear_nomip'));
1251
+ // Displaced surface height per water slot, on the same slice as the map.
1252
+ // Read with the map's uv, so it shares the map's sampling.
1253
+ scope[UNIFORM_NAME_CAUSTIC_HEIGHT_MAP] = pb.tex2D().uniform(0).withSampler(getSamplerOptions('clamp_linear_nomip'));
1254
+ }
1255
+ /**
1256
+ * Attenuation the water caustics apply to one light at a world position.
1257
+ *
1258
+ * Returns the product of two effects: the caustic pattern focused by the waves,
1259
+ * and the Beer-Lambert transmittance of the water column the light travelled
1260
+ * through to reach the point. The result is 1 for any light that is not the
1261
+ * caustic light, above the surface, and outside the water region, so the caller
1262
+ * can multiply unconditionally.
1263
+ *
1264
+ * The caustic light is identified at runtime by comparing directions rather
1265
+ * than by which pass is running. Shadow-casting lights reach the opaque queue
1266
+ * through the clustered path when the screen-space shadow mask is on (the
1267
+ * default) and through a per-light additive pass when it is off; only a runtime
1268
+ * test covers both. Two directional lights pointing the same way would both
1269
+ * pick up the caustics, but they are physically the same light.
1270
+ *
1271
+ * @param scope - Current shader scope.
1272
+ * @param worldPos - World position of the shaded fragment.
1273
+ * @param lightType - The light's type constant.
1274
+ * @param lightDirection - Direction the light travels, i.e. `directionAndCutoff.xyz`.
1275
+ * @param ctx - Current draw context.
1276
+ * @returns A `vec3` multiplier, or null when this pass has no caustics.
1277
+ */ static calculateWaterCaustic(scope, worldPos, lightType, lightDirection, ctx) {
1278
+ if (!this.usesWaterCaustics(ctx)) {
1279
+ return null;
1280
+ }
1281
+ return this.sampleWaterCaustic(scope, worldPos, lightType, lightDirection);
1282
+ }
1283
+ /**
1284
+ * {@link ShaderHelper.calculateWaterCaustic} without the pass gate.
1285
+ *
1286
+ * The gate asks whether the *light pass* has caustics, which is the right
1287
+ * question for a material and the wrong one for a standalone full-screen pass
1288
+ * that declared the same uniforms itself. Such a pass has already decided it
1289
+ * has a map; this is the sampling half on its own.
1290
+ *
1291
+ * @param scope - Current shader scope.
1292
+ * @param worldPos - World position of the shaded fragment.
1293
+ * @param lightType - The light's type constant.
1294
+ * @param lightDirection - Direction the light travels, i.e. `directionAndCutoff.xyz`.
1295
+ * @param defocus - Whether to fade the pattern's contrast by how far the point
1296
+ * is from the map's focal depth. True for a receiver surface, which is what
1297
+ * that fade describes. False for a point inside the water column: see the
1298
+ * comment on the term itself.
1299
+ * @returns A `vec3` multiplier.
1300
+ * @internal
1301
+ */ static sampleWaterCaustic(scope, worldPos, lightType, lightDirection, defocus = true) {
1302
+ const pb = scope.$builder;
1303
+ const funcName = defocus ? 'Z_calculateWaterCaustic' : 'Z_calculateWaterCausticVolume';
1304
+ pb.func(funcName, [
1305
+ pb.vec3('worldPos'),
1306
+ pb.int('lightType'),
1307
+ pb.vec3('lightDirection')
1308
+ ], function() {
1309
+ this.$l.cu = this[UNIFORM_NAME_CAUSTIC_PARAMS];
1310
+ // Only the one light the map was built for casts caustics.
1311
+ this.$if(pb.or(pb.notEqual(this.lightType, LIGHT_TYPE_DIRECTIONAL), pb.lessThan(pb.dot(this.lightDirection, this.cu.lightDir.xyz), 0.999)), function() {
1312
+ this.$return(pb.vec3(1));
1313
+ });
1314
+ // Project into the map: frameX/frameY are orthonormal and perpendicular to
1315
+ // the light, so the same two dot products the splat pass used invert it.
1316
+ this.$l.rel = pb.sub(this.worldPos, this.cu.center.xyz);
1317
+ // Per-axis inverse extents: the slice is fitted to the water, so it is
1318
+ // only square in texels, not in meters.
1319
+ this.$l.mapNDC = pb.mul(pb.vec2(pb.dot(this.rel, this.cu.frameX.xyz), pb.dot(this.rel, this.cu.frameY.xyz)), pb.vec2(this.cu.frameX.w, this.cu.frameY.w));
1320
+ // Texels are concentrated towards the middle of the map, so the lookup
1321
+ // goes through the same warp the splat laid the photons out under.
1322
+ this.$l.warpedNDC = ShaderHelper.warpCausticNDC(this, this.mapNDC, this.cu.extinction.w);
1323
+ this.$l.uv = pb.add(pb.mul(this.warpedNDC, 0.5), pb.vec2(0.5));
1324
+ // The displaced surface height of each water slot on this fragment's own
1325
+ // sun ray, biased by CAUSTIC_HEIGHT_BIAS. The height map rasterises the
1326
+ // displaced surface into the same light-space slice as the pattern, so
1327
+ // the texel under this uv holds the surface the ray through this fragment
1328
+ // passed through - no entry point has to be estimated. Beyond the map the
1329
+ // sampler would repeat its border, which is some other ray's wave; the
1330
+ // surface is taken as flat there.
1331
+ this.$l.insideMap = pb.all(pb.lessThanEqual(pb.abs(this.mapNDC), pb.vec2(1)));
1332
+ // How far this fragment is towards the map border, and how much the map is
1333
+ // therefore trusted. Hoisted above the water loop because the wave height
1334
+ // is faded by it too, not just the pattern.
1335
+ //
1336
+ // Measured on the linear coordinate, not the warped one. The warp fixes
1337
+ // both ends of [-1, 1], so the border is in the same world place either
1338
+ // way, but the band between them is not: warping it would make its world
1339
+ // width depend on the warp strength, and the CPU sized it in meters.
1340
+ //
1341
+ // Distance is measured under an L4 norm rather than per-axis. A per-axis
1342
+ // fade is a rectangle in light space, and the sun tilts that rectangle, so
1343
+ // its border lands on the sea bed as straight lines - which the eye picks
1344
+ // out of a caustic pattern however wide the gradient is. An L2 circle has
1345
+ // no straight part anywhere but discards the 21% of the map outside its
1346
+ // inscribed disc; L4 gives up 7% instead and still curves everywhere.
1347
+ this.$l.ndcSq = pb.mul(this.mapNDC, this.mapNDC);
1348
+ this.$l.edgeDist = pb.sqrt(pb.sqrt(pb.dot(this.ndcSq, this.ndcSq)));
1349
+ this.$l.edge = pb.sub(1, pb.smoothStep(this.cu.params.w, 1, this.edgeDist));
1350
+ this.$l.waveHeights = pb.textureSampleLevel(this[UNIFORM_NAME_CAUSTIC_HEIGHT_MAP], this.uv, 0);
1351
+ // Find the water above this point. Several can qualify - a pool inside a
1352
+ // lake, water on terraces - and the one that matters is the lowest of
1353
+ // them, because that is the last surface the sun crossed on its way here.
1354
+ this.$l.depth = pb.float(-1);
1355
+ this.$l.medium = pb.vec3(0);
1356
+ this.$for(pb.int('i'), 0, MAX_CAUSTIC_WATERS, function() {
1357
+ this.$if(pb.greaterThanEqual(this.i, this.cu.waterCount), function() {
1358
+ this.$break();
1359
+ });
1360
+ this.$l.slotMedium = this.cu.waterMedia.at(this.i);
1361
+ // Where the sun ray that lights this point crossed that water's rest
1362
+ // plane, for the footprint test. The splat pass clips photons by that
1363
+ // same crossing, so the receiver must ask the same question: a point
1364
+ // under the footprint that the sun reaches from outside it saw no water
1365
+ // at all, and a point beyond the footprint that the sun reaches through
1366
+ // it did. Testing the point's own xz gets both wrong by the offset
1367
+ // depth / tan(elevation). Only the footprint is decided here; the depth
1368
+ // itself comes from the height map, which needs no crossing point.
1369
+ this.$l.slotFlat = pb.sub(this.slotMedium.w, this.worldPos.y);
1370
+ this.$l.slotPath = pb.mul(this.slotFlat, this.cu.lightDir.w);
1371
+ this.$l.slotEntry = pb.sub(this.worldPos.xz, pb.mul(this.cu.lightDir.xz, this.slotPath));
1372
+ this.$l.slotRegion = this.cu.waterRegions.at(this.i);
1373
+ this.$if(pb.and(pb.all(pb.greaterThanEqual(this.slotEntry, this.slotRegion.xy)), pb.all(pb.lessThanEqual(this.slotEntry, this.slotRegion.zw))), function() {
1374
+ // Depth under the *displaced* surface on this ray, which is what
1375
+ // the splat refracted the photons through. Gating on the rest plane
1376
+ // instead cut the caustics off at a flat line across any receiver
1377
+ // that pokes above it under a crest. Slot i lives in channel i.
1378
+ this.$l.slotMask = pb.vec4(pb.float(pb.equal(this.i, 0)), pb.float(pb.equal(this.i, 1)), pb.float(pb.equal(this.i, 2)), pb.float(pb.equal(this.i, 3)));
1379
+ this.$l.slotSample = pb.dot(this.waveHeights, this.slotMask);
1380
+ // A cleared texel inside the map means this ray never meets that
1381
+ // body's surface - it crosses the rest plane beyond the footprint,
1382
+ // or the surface is on the far side of the receiver - and the
1383
+ // receiver is dry as far as this body is concerned. Outside the map
1384
+ // nothing is known and the surface is taken as flat.
1385
+ this.$l.slotCovered = pb.or(pb.not(this.insideMap), pb.greaterThan(this.slotSample, 0.5));
1386
+ this.$if(this.slotCovered, function() {
1387
+ // Faded into the rest plane over the same band the pattern fades
1388
+ // on, rather than switched off at the border. Beyond the map the
1389
+ // surface is taken as flat, and exp(-sigma * depth) is convex, so
1390
+ // a hard switch makes the wavy side systematically brighter than
1391
+ // the flat side - E[exp(-sigma*w)] > 1 - and the step lands as a
1392
+ // mean brightness seam that grows with both the swell and the
1393
+ // turbidity. Ramping the wave to zero puts the two sides on the
1394
+ // same value at the border.
1395
+ this.$l.slotWave = pb.mul(pb.sub(this.slotSample, CAUSTIC_HEIGHT_BIAS), pb.float(this.insideMap), this.edge);
1396
+ this.$l.slotDepth = pb.sub(pb.add(this.slotMedium.w, this.slotWave), this.worldPos.y);
1397
+ this.$if(pb.greaterThan(this.slotDepth, 0), function() {
1398
+ this.$if(pb.or(pb.lessThan(this.depth, 0), pb.lessThan(this.slotDepth, this.depth)), function() {
1399
+ this.depth = this.slotDepth;
1400
+ this.medium = this.slotMedium.xyz;
1401
+ });
1402
+ });
1403
+ });
1404
+ });
1405
+ });
1406
+ this.$if(pb.lessThanEqual(this.depth, 0), function() {
1407
+ this.$return(pb.vec3(1));
1408
+ });
1409
+ this.$l.pathLength = pb.mul(this.depth, this.cu.lightDir.w);
1410
+ this.$l.transmittance = pb.exp(pb.neg(pb.mul(this.medium, this.pathLength)));
1411
+ this.$l.pattern = pb.textureSampleLevel(this[UNIFORM_NAME_CAUSTIC_MAP], this.uv, 0).x;
1412
+ if (defocus) {
1413
+ // Receivers away from the focal plane see a defocused, lower-contrast
1414
+ // pattern rather than a displaced one.
1415
+ this.$l.defocus = pb.div(1, pb.add(1, pb.mul(pb.abs(pb.sub(this.depth, this.cu.params.y)), this.cu.params.z)));
1416
+ } else {
1417
+ // A point inside the water column is not a receiver, and has no depth of
1418
+ // its own to be out of focus at - the volumetric march visits every
1419
+ // depth along the ray. Charging it the receiver's fade makes the light
1420
+ // shafts fade according to where the *sea bed* was told to be sharpest:
1421
+ // the open-ocean preset focuses the map at 26m for its bed, which at the
1422
+ // default defocus leaves a point 2m down at 26% contrast, and the shafts
1423
+ // dissolve into grain. The shafts instead take the pattern as the map
1424
+ // holds it and fade with the edge and the transmittance like everything
1425
+ // else in the column.
1426
+ this.$l.defocus = pb.float(1);
1427
+ }
1428
+ this.$l.weight = pb.mul(this.cu.params.x, this.defocus, this.edge);
1429
+ this.$l.caustic = pb.add(1, pb.mul(pb.sub(this.pattern, 1), this.weight));
1430
+ // Soften the waterline so the transmittance does not switch on abruptly.
1431
+ this.$l.submerged = pb.smoothStep(0, 0.05, this.depth);
1432
+ this.$return(pb.mix(pb.vec3(1), pb.mul(pb.vec3(this.caustic), this.transmittance), this.submerged));
1433
+ });
1434
+ return pb.getGlobalScope()[funcName](worldPos, lightType, lightDirection);
1435
+ }
1436
+ /** @internal */ static setWaterCausticUniforms(bindGroup, ctx) {
1437
+ const uniforms = ctx.waterCausticUniforms;
1438
+ bindGroup.setValue(UNIFORM_NAME_CAUSTIC_PARAMS, {
1439
+ frameX: uniforms.frameX,
1440
+ frameY: uniforms.frameY,
1441
+ center: uniforms.center,
1442
+ lightDir: uniforms.lightDir,
1443
+ params: uniforms.params,
1444
+ extinction: uniforms.extinction,
1445
+ waterCount: uniforms.waterCount,
1446
+ waterRegions: uniforms.waterRegions,
1447
+ waterMedia: uniforms.waterMedia
1448
+ });
1449
+ bindGroup.setTexture(UNIFORM_NAME_CAUSTIC_MAP, ctx.waterCausticTexture, fetchSampler('clamp_linear_nomip'));
1450
+ bindGroup.setTexture(UNIFORM_NAME_CAUSTIC_HEIGHT_MAP, ctx.waterCausticHeightTexture, fetchSampler('clamp_linear_nomip'));
1451
+ }
841
1452
  /** @internal */ static setLightUniformsShadow(bindGroup, ctx, light) {
842
1453
  const shadowMapParams = ctx.shadowMapInfo.get(light);
843
1454
  this._lightUniformShadow.sunDir = ctx.sunLight ? ctx.sunLight.directionAndCutoff.xyz().scaleBy(-1) : this.defaultSunDir;
@@ -857,6 +1468,9 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
857
1468
  this._lightUniformShadow.envLightSpecularStrength = ctx.env?.light.specularStrength ?? 1;
858
1469
  bindGroup.setValue('light', this._lightUniformShadow);
859
1470
  bindGroup.setTexture(UNIFORM_NAME_SHADOW_MAP, shadowMapParams.shadowMap, shadowMapParams.shadowMapSampler);
1471
+ if (this.usesWaterCaustics(ctx)) {
1472
+ this.setWaterCausticUniforms(bindGroup, ctx);
1473
+ }
860
1474
  bindGroup.setTexture(UNIFORM_NAME_BAKED_SKY_MAP, ctx.scene.env.sky.getBakedSkyTexture(ctx));
861
1475
  if (ctx.drawEnvLight) {
862
1476
  ctx.env.light.envLight.updateBindGroup(bindGroup, ctx);
@@ -978,6 +1592,50 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
978
1592
  return scope.camera.elapsedTime;
979
1593
  }
980
1594
  /**
1595
+ * Declares the one camera field {@link ShaderHelper.getElapsedTime} reads.
1596
+ *
1597
+ * Wave generators animate off `camera.elapsedTime`, which the light and depth
1598
+ * passes provide as part of the full camera struct. A standalone program that
1599
+ * drives a wave generator - the caustics splat pass - has no such struct, so it
1600
+ * declares that single field under the same name. Any generator that starts
1601
+ * reading a second camera field will need it added here.
1602
+ *
1603
+ * The three padding floats round the struct up to 16 bytes. std140 aligns a
1604
+ * struct to a vec4 boundary, so a lone float leaves the GLSL block larger than
1605
+ * the buffer the engine sizes from the struct's own members - WebGL2 then
1606
+ * rejects every draw with "uniform buffer that is too small" and rasterises
1607
+ * nothing, while WebGPU is unaffected.
1608
+ *
1609
+ * @param scope - Global scope of the standalone program.
1610
+ * @param uniformGroup - Bind group the uniform belongs to.
1611
+ * @internal
1612
+ */ static declareStandaloneCameraTime(scope, uniformGroup) {
1613
+ const pb = scope.$builder;
1614
+ scope.camera = pb.defineStruct([
1615
+ pb.float('elapsedTime'),
1616
+ pb.float('_pad0'),
1617
+ pb.float('_pad1'),
1618
+ pb.float('_pad2')
1619
+ ])().uniform(uniformGroup);
1620
+ }
1621
+ /**
1622
+ * Uploads the value declared by {@link ShaderHelper.declareStandaloneCameraTime}.
1623
+ *
1624
+ * Must be the same clock the light pass uses, or a wave generator's caustics
1625
+ * would animate out of step with the surface they are refracted through.
1626
+ *
1627
+ * @param bindGroup - Bind group of the standalone program.
1628
+ * @param ctx - Current draw context.
1629
+ * @internal
1630
+ */ static setStandaloneCameraTime(bindGroup, ctx) {
1631
+ bindGroup.setValue('camera', {
1632
+ elapsedTime: ctx.device.frameInfo.elapsedOverall * 0.001,
1633
+ _pad0: 0,
1634
+ _pad1: 0,
1635
+ _pad2: 0
1636
+ });
1637
+ }
1638
+ /**
981
1639
  * Gets the elapsed time since last frame in seconds
982
1640
  * @param scope - Current shader scope
983
1641
  * @returns The elapsed time since last frame in seconds
@@ -1291,18 +1949,67 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
1291
1949
  return scope[UNIFORM_NAME_SHADOW_MAP];
1292
1950
  }
1293
1951
  /**
1952
+ * Per-cascade scale for both bias terms.
1953
+ *
1954
+ * @remarks
1955
+ * Only `depthBiasValues[0]` is uploaded; every cascade's value is recovered by
1956
+ * multiplying with this ratio. It is derived from the `.x` components, but
1957
+ * since `.x` and the normal offset `.y` are both proportional to the cascade's
1958
+ * texel world size, the same ratio applies to either.
1959
+ * @internal
1960
+ */ static getShadowCascadeBiasScale(scope, split) {
1961
+ const pb = scope.$builder;
1962
+ const splitFlags = pb.vec4(pb.float(pb.equal(split, 0)), pb.float(pb.equal(split, 1)), pb.float(pb.equal(split, 2)), pb.float(pb.equal(split, 3)));
1963
+ return pb.dot(this.getDepthBiasScales(scope), splitFlags);
1964
+ }
1965
+ /**
1966
+ * Moves the receiver along its normal before the shadow lookup.
1967
+ *
1968
+ * @remarks
1969
+ * Grazing-angle self-shadowing cannot be fixed with a depth bias: the depth
1970
+ * spanned by one shadow texel is `texelWorldSize * tan(theta)`, which diverges
1971
+ * as NdotL approaches zero. Offsetting the sample position sideways moves it
1972
+ * out of the offending texel instead, and the required distance is bounded.
1973
+ *
1974
+ * `depthBiasValues.y` carries the world-space distance of one `normalBias`
1975
+ * unit (see `ShadowMapper.calcDepthBiasParams`).
1976
+ * @internal
1977
+ */ static applyShadowNormalOffset(lightType, scope, worldPos, worldNormal, NdotL, cascadeScale) {
1978
+ const pb = scope.$builder;
1979
+ const depthBiasParam = this.getDepthBiasValues(scope);
1980
+ const receiverNoL = getShadowReceiverNoL(scope, NdotL, lightType);
1981
+ // sin(theta): 0 when facing the light, 1 at grazing incidence.
1982
+ const sinTheta = pb.sqrt(pb.clamp(pb.sub(1, pb.mul(receiverNoL, receiverNoL)), 0, 1));
1983
+ let offset = pb.mul(depthBiasParam.y, getShadowReceiverBiasFactor(scope), sinTheta);
1984
+ if (cascadeScale) {
1985
+ offset = pb.mul(offset, cascadeScale);
1986
+ }
1987
+ if (lightType !== LIGHT_TYPE_DIRECTIONAL) {
1988
+ // Perspective shadow cameras: one texel covers more world space further
1989
+ // from the light. depthBiasValues.w is the far/near footprint ratio.
1990
+ const posRange = this.getLightPositionAndRangeForShadow(scope);
1991
+ const linearDepth = pb.clamp(pb.div(pb.distance(worldPos, posRange.xyz), pb.max(posRange.w, 1e-6)), 0, 1);
1992
+ offset = pb.mul(offset, pb.mix(1, depthBiasParam.w, linearDepth));
1993
+ }
1994
+ // Guard against a degenerate interpolated normal rather than normalize()ing
1995
+ // a possibly-zero vector into NaN.
1996
+ const unitNormal = pb.mul(worldNormal, pb.div(1, pb.max(pb.length(worldNormal), 1e-6)));
1997
+ return pb.add(worldPos, pb.mul(unitNormal, offset));
1998
+ }
1999
+ /**
1294
2000
  * Calculates shadow of current fragment
1295
2001
  *
1296
2002
  * @param scope - Shader scope
1297
2003
  * @param NoL - NdotL vector
1298
2004
  * @returns Shadow of current fragment, 1 means no shadow and 0 means full shadowed.
1299
- */ static calculateShadow(scope, worldPos, NoL, ctx) {
2005
+ */ static calculateShadow(scope, worldPos, worldNormal, NoL, ctx) {
1300
2006
  const pb = scope.$builder;
1301
2007
  const that = this;
1302
2008
  const shadowMapParams = ctx.shadowMapInfo.get(ctx.currentShadowLight);
1303
2009
  const funcName = 'Z_calculateShadow';
1304
2010
  pb.func(funcName, [
1305
2011
  pb.vec3('worldPos'),
2012
+ pb.vec3('worldNormal'),
1306
2013
  pb.float('NoL')
1307
2014
  ], function() {
1308
2015
  if (shadowMapParams.numShadowCascades > 1) {
@@ -1313,18 +2020,21 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
1313
2020
  this.$l.comparison = pb.vec4(pb.greaterThan(pb.vec4(this.linearDepth), this.splitDistances));
1314
2021
  this.$l.cascadeFlags = pb.vec4(pb.float(pb.greaterThan(this.shadowCascades, 0)), pb.float(pb.greaterThan(this.shadowCascades, 1)), pb.float(pb.greaterThan(this.shadowCascades, 2)), pb.float(pb.greaterThan(this.shadowCascades, 3)));
1315
2022
  this.$l.split = pb.int(pb.dot(this.comparison, this.cascadeFlags));
2023
+ // Normal offset bias: move the receiver out of the shadow texel that
2024
+ // would self-occlude it before projecting into shadow space. Scaled per
2025
+ // cascade, because each cascade has its own texel world size.
2026
+ this.$l.biasedPos = that.applyShadowNormalOffset(shadowMapParams.lightType, this, this.worldPos, this.worldNormal, this.NoL, that.getShadowCascadeBiasScale(this, this.split));
1316
2027
  if (ctx.device.type === 'webgl') {
1317
2028
  this.$l.shadowVertex = pb.vec4();
1318
2029
  this.$for(pb.int('cascade'), 0, 4, function() {
1319
2030
  this.$if(pb.equal(this.cascade, this.split), function() {
1320
- this.shadowVertex = that.calculateShadowSpaceVertex(this, pb.vec4(this.worldPos, 1), this.cascade);
2031
+ this.shadowVertex = that.calculateShadowSpaceVertex(this, pb.vec4(this.biasedPos, 1), this.cascade);
1321
2032
  this.$break();
1322
2033
  });
1323
2034
  });
1324
2035
  } else {
1325
- this.$l.shadowVertex = that.calculateShadowSpaceVertex(this, pb.vec4(this.worldPos, 1), this.split);
2036
+ this.$l.shadowVertex = that.calculateShadowSpaceVertex(this, pb.vec4(this.biasedPos, 1), this.split);
1326
2037
  }
1327
- const shadowMapParams = ctx.shadowMapInfo.get(ctx.currentShadowLight);
1328
2038
  this.$l.shadow = shadowMapParams.impl.computeShadowCSM(shadowMapParams, this, this.shadowVertex, this.NoL, this.split);
1329
2039
  this.shadow = pb.clamp(this.shadow, 0, 1);
1330
2040
  this.$l.shadowDistance = that.getShadowCameraParams(scope).w;
@@ -1336,8 +2046,9 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
1336
2046
  });
1337
2047
  this.$return(this.shadow);
1338
2048
  } else {
1339
- this.$l.shadowVertex = that.calculateShadowSpaceVertex(this, pb.vec4(this.worldPos, 1));
1340
- const shadowMapParams = ctx.shadowMapInfo.get(ctx.currentShadowLight);
2049
+ // Normal offset bias - see the cascaded branch above.
2050
+ this.$l.biasedPos = that.applyShadowNormalOffset(shadowMapParams.lightType, this, this.worldPos, this.worldNormal, this.NoL);
2051
+ this.$l.shadowVertex = that.calculateShadowSpaceVertex(this, pb.vec4(this.biasedPos, 1));
1341
2052
  this.$l.shadow = shadowMapParams.impl.computeShadow(shadowMapParams, this, this.shadowVertex, this.NoL);
1342
2053
  this.shadow = pb.clamp(this.shadow, 0, 1);
1343
2054
  this.$l.shadowDistance = that.getShadowCameraParams(scope).w;
@@ -1347,7 +2058,7 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
1347
2058
  this.$return(this.shadow);
1348
2059
  }
1349
2060
  });
1350
- return pb.getGlobalScope()[funcName](worldPos, NoL);
2061
+ return pb.getGlobalScope()[funcName](worldPos, worldNormal, NoL);
1351
2062
  }
1352
2063
  static applyFog(scope, worldPos, color, ctx) {
1353
2064
  const pb = scope.$builder;
@@ -1555,5 +2266,5 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
1555
2266
  }
1556
2267
  }
1557
2268
 
1558
- export { ShaderHelper };
2269
+ export { CAUSTIC_HEIGHT_BIAS, MAX_CAUSTIC_WATERS, ShaderHelper };
1559
2270
  //# sourceMappingURL=helper.js.map