@zephyr3d/scene 0.9.25 → 0.9.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (299) hide show
  1. package/dist/animation/animationset.js +92 -30
  2. package/dist/animation/animationset.js.map +1 -1
  3. package/dist/animation/hair/gpu_hair_simulation.js +1090 -0
  4. package/dist/animation/hair/gpu_hair_simulation.js.map +1 -0
  5. package/dist/animation/joint_dynamics/controller.js +7 -5
  6. package/dist/animation/joint_dynamics/controller.js.map +1 -1
  7. package/dist/animation/joint_dynamics/convex_collider.js +320 -0
  8. package/dist/animation/joint_dynamics/convex_collider.js.map +1 -0
  9. package/dist/animation/joint_dynamics/joint_dynamics_system.js +81 -5
  10. package/dist/animation/joint_dynamics/joint_dynamics_system.js.map +1 -1
  11. package/dist/animation/morphtrack.js +3 -4
  12. package/dist/animation/morphtrack.js.map +1 -1
  13. package/dist/animation/skeleton.js +79 -25
  14. package/dist/animation/skeleton.js.map +1 -1
  15. package/dist/animation/spring/multi_chain_spring_system.js +577 -179
  16. package/dist/animation/spring/multi_chain_spring_system.js.map +1 -1
  17. package/dist/animation/spring/spring_collider.js +4 -3
  18. package/dist/animation/spring/spring_collider.js.map +1 -1
  19. package/dist/animation/spring/spring_node_pose_tracker.js +40 -0
  20. package/dist/animation/spring/spring_node_pose_tracker.js.map +1 -0
  21. package/dist/animation/spring/spring_particle.js +2 -0
  22. package/dist/animation/spring/spring_particle.js.map +1 -1
  23. package/dist/animation/spring/spring_system.js +11 -3
  24. package/dist/animation/spring/spring_system.js.map +1 -1
  25. package/dist/animation/spring_modifier.js +10 -0
  26. package/dist/animation/spring_modifier.js.map +1 -1
  27. package/dist/app/engine.js +53 -43
  28. package/dist/app/engine.js.map +1 -1
  29. package/dist/app/scriptingsystem.js +119 -6
  30. package/dist/app/scriptingsystem.js.map +1 -1
  31. package/dist/app/scriptregistry.js +3 -2
  32. package/dist/app/scriptregistry.js.map +1 -1
  33. package/dist/asset/assetmanager.js +284 -67
  34. package/dist/asset/assetmanager.js.map +1 -1
  35. package/dist/asset/loaders/zhair/zhair_format.js +288 -0
  36. package/dist/asset/loaders/zhair/zhair_format.js.map +1 -0
  37. package/dist/asset/loaders/zhair/zhair_loader.js +221 -0
  38. package/dist/asset/loaders/zhair/zhair_loader.js.map +1 -0
  39. package/dist/asset/model.js +184 -69
  40. package/dist/asset/model.js.map +1 -1
  41. package/dist/camera/camera.js +268 -44
  42. package/dist/camera/camera.js.map +1 -1
  43. package/dist/camera/perspectivecamera.js +3 -1
  44. package/dist/camera/perspectivecamera.js.map +1 -1
  45. package/dist/index.d.ts +9044 -5139
  46. package/dist/index.js +17 -6
  47. package/dist/index.js.map +1 -1
  48. package/dist/material/eye.js +932 -0
  49. package/dist/material/eye.js.map +1 -0
  50. package/dist/material/hair.js +917 -0
  51. package/dist/material/hair.js.map +1 -0
  52. package/dist/material/hairstrand.js +842 -0
  53. package/dist/material/hairstrand.js.map +1 -0
  54. package/dist/material/hairstrand_data.js +162 -0
  55. package/dist/material/hairstrand_data.js.map +1 -0
  56. package/dist/material/lambert.js +5 -2
  57. package/dist/material/lambert.js.map +1 -1
  58. package/dist/material/material.js +48 -10
  59. package/dist/material/material.js.map +1 -1
  60. package/dist/material/meshmaterial.js +120 -19
  61. package/dist/material/meshmaterial.js.map +1 -1
  62. package/dist/material/mixins/albedocolor.js +1 -1
  63. package/dist/material/mixins/albedocolor.js.map +1 -1
  64. package/dist/material/mixins/lightmodel/blinnphong.js +5 -3
  65. package/dist/material/mixins/lightmodel/blinnphong.js.map +1 -1
  66. package/dist/material/mixins/lightmodel/lambert.js +5 -4
  67. package/dist/material/mixins/lightmodel/lambert.js.map +1 -1
  68. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js +3 -3
  69. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js.map +1 -1
  70. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js +5 -5
  71. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js.map +1 -1
  72. package/dist/material/mixins/lightmodel/pbrspecularglossness.js +3 -3
  73. package/dist/material/mixins/lightmodel/pbrspecularglossness.js.map +1 -1
  74. package/dist/material/mixins/lit.js +106 -19
  75. package/dist/material/mixins/lit.js.map +1 -1
  76. package/dist/material/mixins/pbr/common.js +4 -4
  77. package/dist/material/mixins/pbr/common.js.map +1 -1
  78. package/dist/material/mixins/texture.js +48 -11
  79. package/dist/material/mixins/texture.js.map +1 -1
  80. package/dist/material/mtoon.js +6 -5
  81. package/dist/material/mtoon.js.map +1 -1
  82. package/dist/material/pbrblueprint.js +146 -28
  83. package/dist/material/pbrblueprint.js.map +1 -1
  84. package/dist/material/pbrblueprint_instance.js +501 -0
  85. package/dist/material/pbrblueprint_instance.js.map +1 -0
  86. package/dist/material/pbrmr.js +15 -16
  87. package/dist/material/pbrmr.js.map +1 -1
  88. package/dist/material/shader/helper.js +734 -23
  89. package/dist/material/shader/helper.js.map +1 -1
  90. package/dist/material/shader/terrain_blend.js +55 -0
  91. package/dist/material/shader/terrain_blend.js.map +1 -0
  92. package/dist/material/skin.js +192 -32
  93. package/dist/material/skin.js.map +1 -1
  94. package/dist/material/spriteblueprint.js +16 -1
  95. package/dist/material/spriteblueprint.js.map +1 -1
  96. package/dist/material/subsurfaceprofile.js +86 -23
  97. package/dist/material/subsurfaceprofile.js.map +1 -1
  98. package/dist/material/water.js +2059 -72
  99. package/dist/material/water.js.map +1 -1
  100. package/dist/posteffect/bloom.js +167 -7
  101. package/dist/posteffect/bloom.js.map +1 -1
  102. package/dist/posteffect/coloradjust.js +2 -1
  103. package/dist/posteffect/coloradjust.js.map +1 -1
  104. package/dist/posteffect/compositor.js.map +1 -1
  105. package/dist/posteffect/fxaa.js +2 -1
  106. package/dist/posteffect/fxaa.js.map +1 -1
  107. package/dist/posteffect/grayscale.js +3 -1
  108. package/dist/posteffect/grayscale.js.map +1 -1
  109. package/dist/posteffect/motionblur.js +664 -56
  110. package/dist/posteffect/motionblur.js.map +1 -1
  111. package/dist/posteffect/posteffect.js +12 -1
  112. package/dist/posteffect/posteffect.js.map +1 -1
  113. package/dist/posteffect/sao.js +46 -44
  114. package/dist/posteffect/sao.js.map +1 -1
  115. package/dist/posteffect/skinsss.js +368 -69
  116. package/dist/posteffect/skinsss.js.map +1 -1
  117. package/dist/posteffect/ssgi.js +16 -9
  118. package/dist/posteffect/ssgi.js.map +1 -1
  119. package/dist/posteffect/ssr.js +3 -0
  120. package/dist/posteffect/ssr.js.map +1 -1
  121. package/dist/posteffect/sss.js +121 -36
  122. package/dist/posteffect/sss.js.map +1 -1
  123. package/dist/posteffect/taa.js.map +1 -1
  124. package/dist/posteffect/tonemap.js +44 -1
  125. package/dist/posteffect/tonemap.js.map +1 -1
  126. package/dist/render/abuffer_oit.js +8 -2
  127. package/dist/render/abuffer_oit.js.map +1 -1
  128. package/dist/render/clipmap.js +156 -5
  129. package/dist/render/clipmap.js.map +1 -1
  130. package/dist/render/cull_visitor.js +20 -0
  131. package/dist/render/cull_visitor.js.map +1 -1
  132. package/dist/render/depthpass.js +36 -2
  133. package/dist/render/depthpass.js.map +1 -1
  134. package/dist/render/drawable_mixin.js +26 -1
  135. package/dist/render/drawable_mixin.js.map +1 -1
  136. package/dist/render/dualdepthpeeling_oit.js +3 -2
  137. package/dist/render/dualdepthpeeling_oit.js.map +1 -1
  138. package/dist/render/envlight.js +1 -1
  139. package/dist/render/envlight.js.map +1 -1
  140. package/dist/render/fbm_wavegenerator.js +6 -3
  141. package/dist/render/fbm_wavegenerator.js.map +1 -1
  142. package/dist/render/fft_wavegenerator.js +57 -15
  143. package/dist/render/fft_wavegenerator.js.map +1 -1
  144. package/dist/render/gerstner_wavegenerator.js +130 -20
  145. package/dist/render/gerstner_wavegenerator.js.map +1 -1
  146. package/dist/render/globalbindgroup_allocator.js +3 -0
  147. package/dist/render/globalbindgroup_allocator.js.map +1 -1
  148. package/dist/render/hzb.js +80 -22
  149. package/dist/render/hzb.js.map +1 -1
  150. package/dist/render/lightpass.js +6 -3
  151. package/dist/render/lightpass.js.map +1 -1
  152. package/dist/render/oit.js +29 -0
  153. package/dist/render/oit.js.map +1 -0
  154. package/dist/render/render_queue.js +39 -0
  155. package/dist/render/render_queue.js.map +1 -1
  156. package/dist/render/renderer.js +4 -0
  157. package/dist/render/renderer.js.map +1 -1
  158. package/dist/render/rendergraph/blackboard.js +1 -0
  159. package/dist/render/rendergraph/blackboard.js.map +1 -1
  160. package/dist/render/rendergraph/device_pool_allocator.js.map +1 -1
  161. package/dist/render/rendergraph/executor.js +2 -0
  162. package/dist/render/rendergraph/executor.js.map +1 -1
  163. package/dist/render/rendergraph/forward_plus_builder.js +379 -10
  164. package/dist/render/rendergraph/forward_plus_builder.js.map +1 -1
  165. package/dist/render/rendergraph/frame_resource_requirements.js +1 -0
  166. package/dist/render/rendergraph/frame_resource_requirements.js.map +1 -1
  167. package/dist/render/rendergraph/history_resources.js +8 -1
  168. package/dist/render/rendergraph/history_resources.js.map +1 -1
  169. package/dist/render/rendergraph/rendergraph.js.map +1 -1
  170. package/dist/render/rendergraph/texture_affinity_cache.js +1 -1
  171. package/dist/render/rendergraph/texture_affinity_cache.js.map +1 -1
  172. package/dist/render/rendergraph/types.js.map +1 -1
  173. package/dist/render/shadow_mask_pass.js +73 -8
  174. package/dist/render/shadow_mask_pass.js.map +1 -1
  175. package/dist/render/sky.js +166 -126
  176. package/dist/render/sky.js.map +1 -1
  177. package/dist/render/underwater.js +624 -0
  178. package/dist/render/underwater.js.map +1 -0
  179. package/dist/render/water_caustics.js +1380 -0
  180. package/dist/render/water_caustics.js.map +1 -0
  181. package/dist/render/weightedblended_oit.js +3 -2
  182. package/dist/render/weightedblended_oit.js.map +1 -1
  183. package/dist/scene/basesprite.js +8 -0
  184. package/dist/scene/basesprite.js.map +1 -1
  185. package/dist/scene/batchgroup.js +3 -0
  186. package/dist/scene/batchgroup.js.map +1 -1
  187. package/dist/scene/environment.js +4 -1
  188. package/dist/scene/environment.js.map +1 -1
  189. package/dist/scene/hair.js +963 -0
  190. package/dist/scene/hair.js.map +1 -0
  191. package/dist/scene/light.js +5 -1
  192. package/dist/scene/light.js.map +1 -1
  193. package/dist/scene/mesh.js +481 -47
  194. package/dist/scene/mesh.js.map +1 -1
  195. package/dist/scene/meshdrawable.js +6 -0
  196. package/dist/scene/meshdrawable.js.map +1 -1
  197. package/dist/scene/msdftext.js +3 -0
  198. package/dist/scene/msdftext.js.map +1 -1
  199. package/dist/scene/msdftextsprite.js +3 -0
  200. package/dist/scene/msdftextsprite.js.map +1 -1
  201. package/dist/scene/particlesys.js +6 -0
  202. package/dist/scene/particlesys.js.map +1 -1
  203. package/dist/scene/scene.js +3 -0
  204. package/dist/scene/scene.js.map +1 -1
  205. package/dist/scene/scene_node.js +6 -1
  206. package/dist/scene/scene_node.js.map +1 -1
  207. package/dist/scene/terrain-cm/grass.js +3 -0
  208. package/dist/scene/terrain-cm/grass.js.map +1 -1
  209. package/dist/scene/terrain-cm/grassmaterial.js +3 -0
  210. package/dist/scene/terrain-cm/grassmaterial.js.map +1 -1
  211. package/dist/scene/terrain-cm/terrain-cm.js +6 -0
  212. package/dist/scene/terrain-cm/terrain-cm.js.map +1 -1
  213. package/dist/scene/terrain-cm/texture_cache.js +309 -0
  214. package/dist/scene/terrain-cm/texture_cache.js.map +1 -0
  215. package/dist/scene/water.js +546 -4
  216. package/dist/scene/water.js.map +1 -1
  217. package/dist/shaders/atmosphere.js.map +1 -1
  218. package/dist/shaders/diffusion.js +54 -0
  219. package/dist/shaders/diffusion.js.map +1 -0
  220. package/dist/shaders/shadow.js +33 -21
  221. package/dist/shaders/shadow.js.map +1 -1
  222. package/dist/shaders/temporal.js +71 -33
  223. package/dist/shaders/temporal.js.map +1 -1
  224. package/dist/shaders/water.js +22 -19
  225. package/dist/shaders/water.js.map +1 -1
  226. package/dist/shaders/water_medium.js +56 -0
  227. package/dist/shaders/water_medium.js.map +1 -0
  228. package/dist/shadow/dom.js +546 -0
  229. package/dist/shadow/dom.js.map +1 -0
  230. package/dist/shadow/pcf_opt.js +3 -5
  231. package/dist/shadow/pcf_opt.js.map +1 -1
  232. package/dist/shadow/pcf_pd.js +1 -1
  233. package/dist/shadow/pcf_pd.js.map +1 -1
  234. package/dist/shadow/receiver_bias.js +41 -0
  235. package/dist/shadow/receiver_bias.js.map +1 -0
  236. package/dist/shadow/shader.js +20 -60
  237. package/dist/shadow/shader.js.map +1 -1
  238. package/dist/shadow/shadow_impl.js +89 -0
  239. package/dist/shadow/shadow_impl.js.map +1 -1
  240. package/dist/shadow/shadow_region.js +1 -1
  241. package/dist/shadow/shadow_region.js.map +1 -1
  242. package/dist/shadow/shadowmapper.js +150 -37
  243. package/dist/shadow/shadowmapper.js.map +1 -1
  244. package/dist/shapes/shape.js +11 -2
  245. package/dist/shapes/shape.js.map +1 -1
  246. package/dist/shapes/sphere.js +50 -9
  247. package/dist/shapes/sphere.js.map +1 -1
  248. package/dist/utility/blueprint/common/constants.js +112 -2
  249. package/dist/utility/blueprint/common/constants.js.map +1 -1
  250. package/dist/utility/blueprint/material/func.js +142 -49
  251. package/dist/utility/blueprint/material/func.js.map +1 -1
  252. package/dist/utility/blueprint/material/ir.js +490 -25
  253. package/dist/utility/blueprint/material/ir.js.map +1 -1
  254. package/dist/utility/blueprint/material/texture.js +898 -4
  255. package/dist/utility/blueprint/material/texture.js.map +1 -1
  256. package/dist/utility/serialization/manager.js +434 -101
  257. package/dist/utility/serialization/manager.js.map +1 -1
  258. package/dist/utility/serialization/scene/animation.js +401 -5
  259. package/dist/utility/serialization/scene/animation.js.map +1 -1
  260. package/dist/utility/serialization/scene/batch.js +4 -0
  261. package/dist/utility/serialization/scene/batch.js.map +1 -1
  262. package/dist/utility/serialization/scene/camera.js +186 -26
  263. package/dist/utility/serialization/scene/camera.js.map +1 -1
  264. package/dist/utility/serialization/scene/common.js +26 -5
  265. package/dist/utility/serialization/scene/common.js.map +1 -1
  266. package/dist/utility/serialization/scene/hair.js +1141 -0
  267. package/dist/utility/serialization/scene/hair.js.map +1 -0
  268. package/dist/utility/serialization/scene/light.js +84 -4
  269. package/dist/utility/serialization/scene/light.js.map +1 -1
  270. package/dist/utility/serialization/scene/material.js +1867 -166
  271. package/dist/utility/serialization/scene/material.js.map +1 -1
  272. package/dist/utility/serialization/scene/mesh.js +231 -14
  273. package/dist/utility/serialization/scene/mesh.js.map +1 -1
  274. package/dist/utility/serialization/scene/node.js +141 -7
  275. package/dist/utility/serialization/scene/node.js.map +1 -1
  276. package/dist/utility/serialization/scene/particle.js +4 -0
  277. package/dist/utility/serialization/scene/particle.js.map +1 -1
  278. package/dist/utility/serialization/scene/primitive.js +18 -0
  279. package/dist/utility/serialization/scene/primitive.js.map +1 -1
  280. package/dist/utility/serialization/scene/sprite.js +4 -0
  281. package/dist/utility/serialization/scene/sprite.js.map +1 -1
  282. package/dist/utility/serialization/scene/terrain.js +4 -0
  283. package/dist/utility/serialization/scene/terrain.js.map +1 -1
  284. package/dist/utility/serialization/scene/text.js +4 -0
  285. package/dist/utility/serialization/scene/text.js.map +1 -1
  286. package/dist/utility/serialization/scene/water.js +1105 -21
  287. package/dist/utility/serialization/scene/water.js.map +1 -1
  288. package/dist/utility/serialization/types.js.map +1 -1
  289. package/dist/utility/shprojector.js +9 -8
  290. package/dist/utility/shprojector.js.map +1 -1
  291. package/dist/values.js +49 -2
  292. package/dist/values.js.map +1 -1
  293. package/package.json +3 -3
  294. package/dist/render/rendergraph/frame_graph_context.js +0 -41
  295. package/dist/render/rendergraph/frame_graph_context.js.map +0 -1
  296. package/dist/shaders/ssgi.js +0 -63
  297. package/dist/shaders/ssgi.js.map +0 -1
  298. package/dist/shaders/ssgi_reprojection.js +0 -118
  299. package/dist/shaders/ssgi_reprojection.js.map +0 -1
@@ -0,0 +1,546 @@
1
+ import { ShadowImpl } from './shadow_impl.js';
2
+ import { LIGHT_TYPE_POINT } from '../values.js';
3
+ import { ndcToShadowCoord3, shadowCoordDepthInRange, applyShadowDepthBias } from '../shaders/shadow.js';
4
+ import { computeShadowBias } from './shader.js';
5
+ import { ShaderHelper } from '../material/shader/helper.js';
6
+ import { getDevice } from '../app/api.js';
7
+ import { fetchSampler } from '../utility/misc.js';
8
+ import { Vector4, REVERSE_Z } from '@zephyr3d/base';
9
+
10
+ /**
11
+ * Opacity layers, which is three rather than four because the fourth channel of
12
+ * the result texture carries the depth the layers are measured from. Keeping
13
+ * both in one texture is what lets the receiver read the whole technique through
14
+ * the single shadow-map binding the lighting shaders declare.
15
+ * @internal
16
+ */ const DOM_LAYER_COUNT = 3;
17
+ /** @internal Caster-pass uniform holding the frontmost depth from geometry pass 0. */ const UNIFORM_NAME_DOM_FRONT_DEPTH = 'Z_domFrontDepth';
18
+ /**
19
+ * Transmittance a fully opaque caster is recorded as, since zero has no finite
20
+ * optical depth to blend. Dark enough to read as black, and small enough in the
21
+ * exponent that several opaque layers can stack without saturating half floats.
22
+ * @internal
23
+ */ const DOM_MIN_TRANSMITTANCE = 0.001;
24
+ /**
25
+ * Deep opacity map shadows.
26
+ *
27
+ * @remarks
28
+ * Hair is the case an ordinary shadow map cannot express. A shadow map answers
29
+ * "is this point behind the nearest caster", which is the right question for a
30
+ * wall and the wrong one for ten thousand strands: light entering a groom is not
31
+ * blocked at its surface, it is attenuated gradually as it passes more and more
32
+ * fibres. A binary test renders that as a hard edge with everything behind it
33
+ * uniformly black.
34
+ *
35
+ * A deep opacity map (Yuksel & Keyser 2008) records the attenuation instead of
36
+ * the boundary. Geometry pass 0 writes the frontmost caster depth `z0` per texel.
37
+ * Pass 1 rasterises the casters again and each fragment adds its optical depth to
38
+ * every layer lying behind it, giving cumulative absorption sampled at three
39
+ * depths measured *from `z0`* rather than from the light. Because the layers
40
+ * follow the hair surface, three of them resolve what a plain opacity shadow map
41
+ * needs dozens of uniformly spaced ones to match.
42
+ *
43
+ * Recording optical depth rather than coverage is what keeps it exact for solid
44
+ * casters as well as thin ones, so a scene does not have to reserve this mode for
45
+ * its hair: see the caster output for why the two differ.
46
+ *
47
+ * WebGPU only, and deliberately so: the strand geometry this exists to shadow is
48
+ * itself WebGPU only, so supporting the WebGL fallbacks - cascade atlases instead
49
+ * of array layers, RGBA8-packed depth instead of float - would double the surface
50
+ * area to serve no material.
51
+ *
52
+ * @internal
53
+ */ class DOM extends ShadowImpl {
54
+ /** @internal */ _layerDistance;
55
+ /** @internal */ _density;
56
+ /** @internal Width of the sampling kernel in texels; 1 disables filtering. */ _filterSize;
57
+ /** @internal */ _paramsScratch;
58
+ /**
59
+ * @internal
60
+ * Which geometry pass is being rasterised. Lives on the implementation rather
61
+ * than in `implData` because {@link DOM.getShaderHash} has no access to the
62
+ * params, and the hash must separate the two passes or they share a program.
63
+ */ _geometryPass;
64
+ /**
65
+ * @internal
66
+ * Depth the shadow camera spans, captured when the map is rendered. Needed to
67
+ * express {@link DOM.layerDistance} in world units: the shader compares device
68
+ * depths, and for the orthographic projection a directional light uses those
69
+ * are the world depth divided by exactly this.
70
+ */ _depthRange;
71
+ constructor(layerDistance = 0.25, density = 1, filterSize = 5){
72
+ super();
73
+ this._layerDistance = layerDistance;
74
+ this._density = density;
75
+ this._filterSize = DOM.clampFilterSize(filterSize);
76
+ this._paramsScratch = new Vector4();
77
+ this._geometryPass = 0;
78
+ this._depthRange = 1;
79
+ }
80
+ /** @internal */ static clampFilterSize(val) {
81
+ return val !== 1 && val !== 3 && val !== 5 && val !== 7 ? 5 : val;
82
+ }
83
+ /**
84
+ * Width of the sampling kernel, in shadow map texels. One of 1, 3, 5 or 7.
85
+ *
86
+ * @remarks
87
+ * The graded transmittance this technique records runs along the light, not
88
+ * across the map, so at the silhouette of a groom - where the change is one of
89
+ * coverage rather than of depth - a single tap is as abrupt as an unfiltered
90
+ * shadow map. This is the kernel that softens that edge, and it is the same
91
+ * knob PCF's kernel size is, with the difference that each tap here costs a
92
+ * layer lookup rather than a depth comparison.
93
+ *
94
+ * Set to 1 to sample once, which is measurably cheaper and visibly harder.
95
+ */ get filterSize() {
96
+ return this._filterSize;
97
+ }
98
+ set filterSize(val) {
99
+ this._filterSize = DOM.clampFilterSize(val);
100
+ }
101
+ /**
102
+ * Depth the layers span, in world units along the light direction.
103
+ *
104
+ * @remarks
105
+ * Set it to roughly the thickness of the hair. Too small and everything past
106
+ * the outer strands saturates the last layer, which is a hard shadow again;
107
+ * too large and all three layers land inside the first strand, leaving the
108
+ * interior unshadowed.
109
+ *
110
+ * Exact for directional lights, whose orthographic projection makes device
111
+ * depth a linear function of world depth. Spot and point lights project
112
+ * perspectively, so there the value is only accurate near the middle of the
113
+ * shadow camera's range - which is where hair lit by one usually sits.
114
+ */ get layerDistance() {
115
+ return this._layerDistance;
116
+ }
117
+ set layerDistance(val) {
118
+ this._layerDistance = val > 0 ? val : 0.0001;
119
+ }
120
+ /**
121
+ * Artistic multiplier on the absorption the casters actually recorded.
122
+ *
123
+ * @remarks
124
+ * One reproduces alpha compositing exactly and is the right default. Raise it
125
+ * to deepen the interior of a groom without moving the layers, lower it to let
126
+ * more light through than the geometry strictly would.
127
+ */ get density() {
128
+ return this._density;
129
+ }
130
+ set density(val) {
131
+ this._density = val < 0 ? 0 : val;
132
+ }
133
+ getType() {
134
+ return 'dom';
135
+ }
136
+ resourceDirty() {
137
+ return this._resourceDirty;
138
+ }
139
+ getShadowMapBorder() {
140
+ // Margin so the kernel's outer taps still land inside the map at the edges of
141
+ // the shadow region, matching what the PCF implementations reserve.
142
+ return this._filterSize;
143
+ }
144
+ getDepthScale() {
145
+ return 1;
146
+ }
147
+ setDepthScale(_val) {}
148
+ /**
149
+ * Layer geometry and density, for both the caster and the receiver. `z` and
150
+ * `w` are unused.
151
+ *
152
+ * @remarks
153
+ * `x` is the layer span converted from world units into the device-depth
154
+ * difference the shaders actually compare, using the range captured when the
155
+ * map was rendered.
156
+ */ getParams(out) {
157
+ const result = out ?? this._paramsScratch;
158
+ result.setXYZW(this._layerDistance / this._depthRange, this._density, 0, 0);
159
+ return result;
160
+ }
161
+ getGeometryPassCount() {
162
+ return 2;
163
+ }
164
+ supportsCascades() {
165
+ // Each split would need its own depth and layer set, and both already share
166
+ // the one texture the receiver can bind.
167
+ return false;
168
+ }
169
+ clipsCasterAlpha() {
170
+ // Neither pass wants the clip. Pass 1 integrates the alpha, and clipping is
171
+ // destructive to it in both of the forms the material applies: a dithered
172
+ // caster survives with probability alpha and then contributes the absorption
173
+ // of that same alpha, counting it twice, and a hard cutoff drops the sub-pixel
174
+ // strands that carry most of a groom's transmittance.
175
+ //
176
+ // Pass 0 skips it for a different reason. Its job is to find the frontmost
177
+ // strand, and under dithering the clip makes that choice per-pixel random -
178
+ // the layers would then start at a depth that flickers between strands, and
179
+ // every opacity reading behind it inherits the jitter.
180
+ //
181
+ // The case this gives up is a caster whose alpha is a real mask rather than a
182
+ // coverage value, such as a hair card with cut-out texels: pass 0 will place
183
+ // z0 on the card's surface even where it is fully transparent. Strand
184
+ // geometry, which is what this technique is for, has no such texels.
185
+ return false;
186
+ }
187
+ beginGeometryPass(shadowMapParams, pass) {
188
+ const implData = shadowMapParams.implData;
189
+ this._geometryPass = pass;
190
+ // The light camera is finalised by the time the casters are drawn, so this is
191
+ // where the world-to-device depth scale becomes known. It stays valid for the
192
+ // lighting pass that follows, which reads the same converted params back.
193
+ const range = shadowMapParams.cameraParams.y - shadowMapParams.cameraParams.x;
194
+ this._depthRange = range > 1e-6 ? range : 1;
195
+ getDevice().setFramebuffer(pass === 0 ? implData.depthFramebuffer : implData.opacityFramebuffer);
196
+ }
197
+ setCasterRenderStates(_shadowMapParams, stateSet) {
198
+ if (this._geometryPass !== 1) {
199
+ return;
200
+ }
201
+ // Every strand between z0 and the last layer has to contribute, so the pass
202
+ // must not depth-test against the frontmost surface pass 0 resolved, and must
203
+ // not write depth of its own - its framebuffer has no depth attachment at all.
204
+ // Optical depth sums across the three layer channels; alpha instead carries
205
+ // z0, which every fragment reads from the same texel, so replacing is correct
206
+ // where summing would multiply it by the number of overlapping strands.
207
+ stateSet.useBlendingState().enable(true).setBlendEquation('add', 'add').setBlendFuncRGB('one', 'one').setBlendFuncAlpha('one', 'zero');
208
+ stateSet.useDepthState().enableTest(false).enableWrite(false);
209
+ }
210
+ getShadowMapClearColor(_shadowMapParams) {
211
+ if (this._geometryPass === 1) {
212
+ // No hair over this texel means no absorption. Alpha carries z0, and the
213
+ // sentinel below makes an uncovered texel read as "nothing in front".
214
+ return new Vector4(0, 0, 0, REVERSE_Z ? 0 : 1);
215
+ }
216
+ const farthest = REVERSE_Z ? 0 : 1;
217
+ return new Vector4(farthest, farthest, farthest, 1);
218
+ }
219
+ getShadowMapColorFormat(_shadowMapParams) {
220
+ // Null keeps the framebuffer the caller allocates depth-only. The colour this
221
+ // technique needs is allocated here, against framebuffers of its own, so
222
+ // letting the caller also allocate a full-size colour attachment would waste
223
+ // it outright.
224
+ return null;
225
+ }
226
+ getShadowMapDepthFormat(_shadowMapParams) {
227
+ return this.preferredShadowMapDepthFormat();
228
+ }
229
+ useNativeShadowMap(_shadowMapParams) {
230
+ // The receiver needs the actual stored values - a depth to measure offsets
231
+ // from and three sums to interpolate - not the result of a hardware compare.
232
+ return false;
233
+ }
234
+ getShadowMap(shadowMapParams) {
235
+ return shadowMapParams.implData.result;
236
+ }
237
+ /**
238
+ * Format for both targets.
239
+ *
240
+ * @remarks
241
+ * Full float wherever the accumulation pass can blend into it, and the reason
242
+ * is the anchor depth, not the absorption sums. `z0` is stored absolute, in
243
+ * units of the shadow camera's whole depth range, and every layer coordinate
244
+ * on both sides of the technique is a difference against it divided by the
245
+ * layer span. At half precision its quantisation step is about 2^-11 of the
246
+ * stored value - roughly `0.0005 * depthRange` in world units - which for a
247
+ * shadow camera spanning a large scene is comparable to the depth of the hair
248
+ * itself. The error is independent per texel, so it does not read as bias but
249
+ * as blotches: patches of a backlit groom whose receivers land in front of
250
+ * their quantised `z0` and come out fully lit. A layer span large enough to
251
+ * drown the noise hides it, but throws away the graded falloff the technique
252
+ * exists for.
253
+ *
254
+ * Half float remains the fallback for devices whose blending cannot target
255
+ * full float; there the practical mitigation is a tight shadow distance.
256
+ * @internal
257
+ */ colorFormat() {
258
+ const caps = getDevice().getDeviceCaps().textureCaps;
259
+ if (caps.supportFloatColorBuffer && caps.supportFloatBlending) {
260
+ return 'rgba32f';
261
+ }
262
+ return caps.supportHalfFloatColorBuffer ? 'rgba16f' : 'rgba32f';
263
+ }
264
+ doUpdateResources(shadowMapParams) {
265
+ const device = getDevice();
266
+ const sharedDepth = shadowMapParams.shadowMapFramebuffer.getDepthAttachment();
267
+ const width = sharedDepth.width;
268
+ const height = sharedDepth.height;
269
+ const format = this.colorFormat();
270
+ // Everything here is pooled, per frame, the way the blur targets of VSM and
271
+ // ESM are. `implData` cannot cache across frames even though it looks like it
272
+ // could: it lives on a ShadowMapParams that is recycled between every light
273
+ // and cleared at the end of each frame, so holding directly-created textures
274
+ // in it leaks them at whatever rate the scene renders.
275
+ const frontDepth = device.pool.fetchTemporalTexture2D(false, format, width, height, false);
276
+ const result = device.pool.fetchTemporalTexture2D(false, format, width, height, false);
277
+ // Pass 0 borrows the shadow map's own depth attachment, which is otherwise
278
+ // unused - this implementation asks for no colour there - so the hardware
279
+ // depth test resolves the frontmost strand for it. Pass 1 gets no depth,
280
+ // which is both what stops it testing and what frees `frontDepth` to be
281
+ // sampled while `result` is written.
282
+ const depthFramebuffer = device.pool.createTemporalFramebuffer(true, [
283
+ frontDepth
284
+ ], sharedDepth);
285
+ const opacityFramebuffer = device.pool.createTemporalFramebuffer(true, [
286
+ result
287
+ ], null);
288
+ // The framebuffers hold the references now; these drop the fetch counts so
289
+ // both textures return to the pool when the frame ends.
290
+ device.pool.releaseTexture(frontDepth);
291
+ device.pool.releaseTexture(result);
292
+ shadowMapParams.implData = {
293
+ frontDepth,
294
+ result,
295
+ depthFramebuffer,
296
+ opacityFramebuffer
297
+ };
298
+ shadowMapParams.shadowMap = result;
299
+ shadowMapParams.shadowMapSampler = result.getDefaultSampler(false);
300
+ this._resourceDirty = false;
301
+ }
302
+ postRenderShadowMap(_shadowMapParams) {}
303
+ getShaderHash() {
304
+ // The two geometry passes emit different caster fragments and the shadow pass
305
+ // keys its programs on this hash, so the pass index has to appear in it. The
306
+ // kernel is unrolled into the receiver, so it belongs here too.
307
+ return `${DOM_LAYER_COUNT}_${this._geometryPass}_${this._filterSize}`;
308
+ }
309
+ declareCasterUniforms(scope, _shadowMapParams) {
310
+ if (this._geometryPass !== 1) {
311
+ return;
312
+ }
313
+ const pb = scope.$builder;
314
+ // sampleType before uniform(), matching how the receiver's shadow map is
315
+ // declared: the binding is created from the accumulated descriptor.
316
+ const tex = pb.tex2D();
317
+ tex.sampleType('unfilterable-float');
318
+ scope[UNIFORM_NAME_DOM_FRONT_DEPTH] = tex.uniform(0);
319
+ }
320
+ applyCasterUniforms(bindGroup, shadowMapParams) {
321
+ if (this._geometryPass !== 1) {
322
+ return;
323
+ }
324
+ const implData = shadowMapParams.implData;
325
+ bindGroup.setTexture(UNIFORM_NAME_DOM_FRONT_DEPTH, implData.frontDepth, fetchSampler('clamp_nearest_nomip'));
326
+ }
327
+ /**
328
+ * How far a depth lies behind the frontmost strand, in layer-span units.
329
+ *
330
+ * @remarks
331
+ * Both callers measure "deeper into the hair than z0", but which direction that
332
+ * is depends on the depth encoding: under reverse-Z a farther surface has the
333
+ * *smaller* value, so the plain subtraction is negative for every fragment
334
+ * behind the front and the clamp below flattens the whole technique to no
335
+ * shadow at all.
336
+ * @internal
337
+ */ layerCoord(scope, depth, z0, layerSpan) {
338
+ const pb = scope.$builder;
339
+ const delta = REVERSE_Z ? pb.sub(z0, depth) : pb.sub(depth, z0);
340
+ return pb.div(pb.max(delta, 0), layerSpan);
341
+ }
342
+ /**
343
+ * Caster output.
344
+ *
345
+ * Pass 0 writes the frontmost depth. Pass 1 writes this fragment's optical depth
346
+ * into the layers behind it, for additive blending to sum, and carries z0
347
+ * through alpha so the receiver can find where the layers start.
348
+ */ computeShadowMapDepth(shadowMapParams, scope, worldPos, alpha) {
349
+ const pb = scope.$builder;
350
+ const that = this;
351
+ const geometryPass = this._geometryPass;
352
+ // Alpha travels as a parameter. Reaching into the caller's scope for its
353
+ // `outColor` would be reading a local of a different shader function, and the
354
+ // two cases that matter - an opaque caster and a fully transparent one - are
355
+ // the same value there.
356
+ const hasAlpha = geometryPass === 1 && !!alpha;
357
+ const funcName = `lib_domCasterOutput_${geometryPass}_${hasAlpha ? 'a' : 'o'}`;
358
+ const params = hasAlpha ? [
359
+ pb.vec3('worldPos'),
360
+ pb.float('alpha')
361
+ ] : [
362
+ pb.vec3('worldPos')
363
+ ];
364
+ pb.func(funcName, params, function() {
365
+ this.$l.depth = that.casterDepth(shadowMapParams, this, this.worldPos);
366
+ if (geometryPass === 0) {
367
+ this.$return(pb.vec4(this.depth, 0, 0, 1));
368
+ } else {
369
+ this.$l.z0 = pb.textureLoad(this[UNIFORM_NAME_DOM_FRONT_DEPTH], pb.ivec2(this.$builtins.fragCoord.xy), 0).x;
370
+ this.$l.layerSpan = ShaderHelper.getShadowImplParams(this).x;
371
+ // Distance behind the frontmost strand, in layer-span units. A fragment at
372
+ // or in front of z0 lands at zero and fills every layer.
373
+ this.$l.t = that.layerCoord(this, this.depth, this.z0, this.layerSpan);
374
+ // Layer k's far boundary sits at (k+1)/3 of the span, and a fragment
375
+ // contributes to every layer whose boundary lies behind it. The last
376
+ // layer has no far boundary: it extends to the end of the caster volume,
377
+ // so a fragment deeper than the span still lands in it and the third
378
+ // layer holds the cumulative absorption of *everything* behind z0.
379
+ // Dropping deep fragments instead - step(t, 1) - deletes the hair behind
380
+ // the front slice from the map entirely, and any receiver deeper than
381
+ // the span reads only that slice: wherever it happens to be sparse, the
382
+ // middle of a dense backlit groom comes out fully lit in blotches.
383
+ this.$l.mask = pb.vec3(pb.step(this.t, 1 / 3), pb.step(this.t, 2 / 3), 1);
384
+ // A caster that computes no fragment colour covers fully.
385
+ this.$l.casterAlpha = hasAlpha ? this.alpha : pb.float(1);
386
+ // Optical depth rather than raw coverage, because what has to be additive
387
+ // here is the exponent, not the alpha. Compositing N layers multiplies
388
+ // their transmittances - prod(1 - a) - and blending can only sum, so each
389
+ // fragment contributes -log(1 - a) and the receiver's exp(-sum) reproduces
390
+ // that product exactly.
391
+ //
392
+ // Summing alpha instead makes exp(-sum) an approximation that only holds
393
+ // while alpha is small, which is true of a sub-pixel strand and false of
394
+ // anything solid: a single opaque caster would sum to 1 and shadow to
395
+ // exp(-1), a 37% grey rather than black.
396
+ //
397
+ // The floor bounds what alpha = 1 encodes. It has to be finite to blend,
398
+ // and 0.001 transmittance is already black; it also leaves room for
399
+ // several opaque layers to stack without leaving half precision.
400
+ this.$l.opticalDepth = pb.neg(pb.log(pb.max(pb.sub(1, this.casterAlpha), DOM_MIN_TRANSMITTANCE)));
401
+ this.$return(pb.vec4(pb.mul(this.mask, this.opticalDepth), this.z0));
402
+ }
403
+ });
404
+ const global = pb.getGlobalScope();
405
+ return hasAlpha ? global[funcName](worldPos, alpha) : global[funcName](worldPos);
406
+ }
407
+ /**
408
+ * Normalized light-space depth of a caster fragment, in the encoding the
409
+ * receiver reconstructs.
410
+ * @internal
411
+ */ casterDepth(shadowMapParams, scope, worldPos) {
412
+ const pb = scope.$builder;
413
+ if (shadowMapParams.lightType === LIGHT_TYPE_POINT) {
414
+ const lightSpacePos = pb.mul(ShaderHelper.getLightViewMatrixForShadow(scope), pb.vec4(worldPos, 1));
415
+ return pb.clamp(pb.div(pb.length(lightSpacePos.xyz), ShaderHelper.getLightPositionAndRangeForShadow(scope).w), 0, 1);
416
+ }
417
+ return pb.emulateDepthClamp ? pb.clamp(scope.$inputs.clamppedDepth, 0, 1) : scope.$builtins.fragCoord.z;
418
+ }
419
+ computeShadow(shadowMapParams, scope, shadowVertex, NdotL) {
420
+ const funcName = 'lib_computeShadowDOM';
421
+ const pb = scope.$builder;
422
+ const that = this;
423
+ pb.func(funcName, [
424
+ pb.vec4('shadowVertex'),
425
+ pb.float('NdotL')
426
+ ], function() {
427
+ this.$l.shadowCoord = pb.div(this.shadowVertex.xyz, this.shadowVertex.w);
428
+ this.$l.shadowCoord = ndcToShadowCoord3(this, this.shadowCoord.xyz);
429
+ this.$l.inRange = pb.all(pb.bvec2(pb.all(pb.bvec4(pb.greaterThanEqual(this.shadowCoord.x, 0), pb.lessThanEqual(this.shadowCoord.x, 1), pb.greaterThanEqual(this.shadowCoord.y, 0), pb.lessThanEqual(this.shadowCoord.y, 1))), shadowCoordDepthInRange(this, this.shadowCoord.z)));
430
+ this.$l.shadow = pb.float(1);
431
+ this.$if(this.inRange, function() {
432
+ // A single tap needs no bias: it reads the texel the receiver itself
433
+ // wrote, so its own surface sits exactly at z0 and contributes nothing.
434
+ // A kernel breaks that. Neighbouring texels record the frontmost surface
435
+ // at their own position, which on anything sloped is nearer the light
436
+ // than the receiver, and the receiver then reads itself as being behind a
437
+ // caster. On a solid surface that is the full jump from no absorption to
438
+ // all of it, so it shows up as the banding an unbiased shadow map has.
439
+ if (that._filterSize > 1) {
440
+ this.$l.bias = computeShadowBias(shadowMapParams.lightType, this, this.shadowCoord.z, this.NdotL, false);
441
+ this.shadowCoord.z = applyShadowDepthBias(this, this.shadowCoord.z, this.bias, true);
442
+ }
443
+ this.shadow = that.transmittance(this, this.shadowCoord);
444
+ });
445
+ this.$return(this.shadow);
446
+ });
447
+ return pb.getGlobalScope()[funcName](shadowVertex, NdotL);
448
+ }
449
+ computeShadowCSM(shadowMapParams, scope, shadowVertex, NdotL, _split) {
450
+ // Cascades would need a depth and a layer set per split, which no longer fits
451
+ // the one texture the single shadow-map binding allows. Callers keep working
452
+ // by shadowing from the first cascade's map.
453
+ return this.computeShadow(shadowMapParams, scope, shadowVertex, NdotL);
454
+ }
455
+ /**
456
+ * Fraction of light surviving the hair in front of the receiver, averaged over
457
+ * the filter kernel.
458
+ *
459
+ * @remarks
460
+ * The averaging is over transmittance, one value per tap, rather than over the
461
+ * stored texels. Blending the texels first would interpolate `z0` across the
462
+ * silhouette of the groom, where one side holds a real strand depth and the
463
+ * other the empty-texel sentinel, and the depth halfway between them describes
464
+ * nothing - every layer reading taken against it would be measured from the
465
+ * wrong origin. Averaging the results instead asks a well-posed question of
466
+ * each texel and combines only the answers, which is also what makes this an
467
+ * antialiased edge rather than a blurred one.
468
+ * @internal
469
+ */ transmittance(scope, shadowCoord) {
470
+ const pb = scope.$builder;
471
+ const that = this;
472
+ const size = this._filterSize;
473
+ const funcName = `lib_domTransmittance_${size}`;
474
+ pb.func(funcName, [
475
+ pb.vec3('shadowCoord')
476
+ ], function() {
477
+ if (size <= 1) {
478
+ this.$return(that.tapTransmittance(this, this.shadowCoord.xy, this.shadowCoord.z));
479
+ } else {
480
+ // cameraParams.z carries the shadow map size, and this implementation
481
+ // never splits into cascades, so the map is exactly that wide.
482
+ this.$l.texelSize = pb.div(1, ShaderHelper.getShadowCameraParams(this).z);
483
+ this.$l.sum = pb.float(0);
484
+ this.$l.tapUV = pb.vec2();
485
+ const radius = (size - 1) / 2;
486
+ for(let y = -radius; y <= radius; y++){
487
+ for(let x = -radius; x <= radius; x++){
488
+ this.tapUV = pb.add(this.shadowCoord.xy, pb.mul(pb.vec2(x, y), this.texelSize));
489
+ this.sum = pb.add(this.sum, that.tapTransmittance(this, this.tapUV, this.shadowCoord.z));
490
+ }
491
+ }
492
+ this.$return(pb.div(this.sum, size * size));
493
+ }
494
+ });
495
+ return pb.getGlobalScope()[funcName](shadowCoord);
496
+ }
497
+ /**
498
+ * Transmittance from a single shadow map texel.
499
+ * @internal
500
+ */ tapTransmittance(scope, uv, receiverDepth) {
501
+ const pb = scope.$builder;
502
+ const that = this;
503
+ const funcName = 'lib_domTap';
504
+ pb.func(funcName, [
505
+ pb.vec2('uv'),
506
+ pb.float('receiverDepth')
507
+ ], function() {
508
+ this.$l.texel = pb.textureSampleLevel(ShaderHelper.getShadowMap(this), this.uv, 0);
509
+ this.$l.layers = this.texel.rgb;
510
+ this.$l.z0 = this.texel.a;
511
+ this.$l.layerSpan = ShaderHelper.getShadowImplParams(this).x;
512
+ this.$l.density = ShaderHelper.getShadowImplParams(this).y;
513
+ this.$l.t = that.layerCoord(this, this.receiverDepth, this.z0, this.layerSpan);
514
+ // The layers are cumulative, so opacity at an arbitrary depth is a lerp
515
+ // between the two bracketing layer values. Scaling t by the layer count puts
516
+ // the integer part on the near layer and the fraction between the pair.
517
+ this.$l.scaled = pb.clamp(pb.mul(this.t, DOM_LAYER_COUNT), 0, DOM_LAYER_COUNT);
518
+ this.$l.index = pb.floor(this.scaled);
519
+ this.$l.frac = pb.sub(this.scaled, this.index);
520
+ // Nothing has been traversed before the first layer, so the near value at
521
+ // index 0 is zero rather than layers.x.
522
+ this.$l.near = pb.float(0);
523
+ this.$l.far = this.layers.x;
524
+ this.$if(pb.greaterThanEqual(this.index, 2), function() {
525
+ this.near = this.layers.y;
526
+ this.far = this.layers.z;
527
+ }).$elseif(pb.greaterThanEqual(this.index, 1), function() {
528
+ this.near = this.layers.x;
529
+ this.far = this.layers.y;
530
+ });
531
+ // Optical depth is what accumulates linearly through a medium, so the lerp
532
+ // between two layer readings is meaningful in a way that lerping stored
533
+ // alphas would not have been.
534
+ this.$l.absorption = pb.mix(this.near, this.far, this.frac);
535
+ // Beer-Lambert. With the casters recording -log(1 - alpha) this is exactly
536
+ // prod(1 - alpha) over everything crossed, so a solid caster reaches black
537
+ // and a stack of thin strands reaches the same value alpha compositing them
538
+ // would have.
539
+ this.$return(pb.exp(pb.neg(pb.mul(this.absorption, this.density))));
540
+ });
541
+ return pb.getGlobalScope()[funcName](uv, receiverDepth);
542
+ }
543
+ }
544
+
545
+ export { DOM };
546
+ //# sourceMappingURL=dom.js.map