@zephyr3d/scene 0.9.25 → 0.9.26

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 (262) hide show
  1. package/dist/animation/hair/gpu_hair_simulation.js +1090 -0
  2. package/dist/animation/hair/gpu_hair_simulation.js.map +1 -0
  3. package/dist/animation/joint_dynamics/controller.js +7 -5
  4. package/dist/animation/joint_dynamics/controller.js.map +1 -1
  5. package/dist/animation/joint_dynamics/joint_dynamics_system.js +81 -5
  6. package/dist/animation/joint_dynamics/joint_dynamics_system.js.map +1 -1
  7. package/dist/animation/morphtrack.js +3 -4
  8. package/dist/animation/morphtrack.js.map +1 -1
  9. package/dist/animation/skeleton.js +58 -24
  10. package/dist/animation/skeleton.js.map +1 -1
  11. package/dist/animation/spring/multi_chain_spring_system.js +577 -179
  12. package/dist/animation/spring/multi_chain_spring_system.js.map +1 -1
  13. package/dist/animation/spring/spring_collider.js +4 -3
  14. package/dist/animation/spring/spring_collider.js.map +1 -1
  15. package/dist/animation/spring/spring_node_pose_tracker.js +40 -0
  16. package/dist/animation/spring/spring_node_pose_tracker.js.map +1 -0
  17. package/dist/animation/spring/spring_particle.js +2 -0
  18. package/dist/animation/spring/spring_particle.js.map +1 -1
  19. package/dist/animation/spring/spring_system.js +11 -3
  20. package/dist/animation/spring/spring_system.js.map +1 -1
  21. package/dist/animation/spring_modifier.js +10 -0
  22. package/dist/animation/spring_modifier.js.map +1 -1
  23. package/dist/app/engine.js +53 -43
  24. package/dist/app/engine.js.map +1 -1
  25. package/dist/app/scriptingsystem.js +119 -6
  26. package/dist/app/scriptingsystem.js.map +1 -1
  27. package/dist/app/scriptregistry.js +3 -2
  28. package/dist/app/scriptregistry.js.map +1 -1
  29. package/dist/asset/assetmanager.js +284 -67
  30. package/dist/asset/assetmanager.js.map +1 -1
  31. package/dist/asset/loaders/zhair/zhair_format.js +288 -0
  32. package/dist/asset/loaders/zhair/zhair_format.js.map +1 -0
  33. package/dist/asset/loaders/zhair/zhair_loader.js +221 -0
  34. package/dist/asset/loaders/zhair/zhair_loader.js.map +1 -0
  35. package/dist/asset/model.js +178 -67
  36. package/dist/asset/model.js.map +1 -1
  37. package/dist/camera/camera.js +193 -21
  38. package/dist/camera/camera.js.map +1 -1
  39. package/dist/camera/perspectivecamera.js +3 -1
  40. package/dist/camera/perspectivecamera.js.map +1 -1
  41. package/dist/index.d.ts +4019 -1278
  42. package/dist/index.js +15 -5
  43. package/dist/index.js.map +1 -1
  44. package/dist/material/eye.js +932 -0
  45. package/dist/material/eye.js.map +1 -0
  46. package/dist/material/hair.js +917 -0
  47. package/dist/material/hair.js.map +1 -0
  48. package/dist/material/hairstrand.js +842 -0
  49. package/dist/material/hairstrand.js.map +1 -0
  50. package/dist/material/hairstrand_data.js +162 -0
  51. package/dist/material/hairstrand_data.js.map +1 -0
  52. package/dist/material/lambert.js +5 -2
  53. package/dist/material/lambert.js.map +1 -1
  54. package/dist/material/material.js +338 -300
  55. package/dist/material/material.js.map +1 -1
  56. package/dist/material/meshmaterial.js +120 -19
  57. package/dist/material/meshmaterial.js.map +1 -1
  58. package/dist/material/mixins/albedocolor.js +1 -1
  59. package/dist/material/mixins/albedocolor.js.map +1 -1
  60. package/dist/material/mixins/lightmodel/blinnphong.js +5 -3
  61. package/dist/material/mixins/lightmodel/blinnphong.js.map +1 -1
  62. package/dist/material/mixins/lightmodel/lambert.js +5 -4
  63. package/dist/material/mixins/lightmodel/lambert.js.map +1 -1
  64. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js +3 -3
  65. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js.map +1 -1
  66. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js +5 -5
  67. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js.map +1 -1
  68. package/dist/material/mixins/lightmodel/pbrspecularglossness.js +3 -3
  69. package/dist/material/mixins/lightmodel/pbrspecularglossness.js.map +1 -1
  70. package/dist/material/mixins/lit.js +73 -18
  71. package/dist/material/mixins/lit.js.map +1 -1
  72. package/dist/material/mixins/pbr/common.js +4 -4
  73. package/dist/material/mixins/pbr/common.js.map +1 -1
  74. package/dist/material/mixins/texture.js +48 -11
  75. package/dist/material/mixins/texture.js.map +1 -1
  76. package/dist/material/mtoon.js +6 -5
  77. package/dist/material/mtoon.js.map +1 -1
  78. package/dist/material/pbrblueprint.js +146 -28
  79. package/dist/material/pbrblueprint.js.map +1 -1
  80. package/dist/material/pbrblueprint_instance.js +501 -0
  81. package/dist/material/pbrblueprint_instance.js.map +1 -0
  82. package/dist/material/pbrmr.js +15 -16
  83. package/dist/material/pbrmr.js.map +1 -1
  84. package/dist/material/shader/helper.js +330 -20
  85. package/dist/material/shader/helper.js.map +1 -1
  86. package/dist/material/shader/terrain_blend.js +55 -0
  87. package/dist/material/shader/terrain_blend.js.map +1 -0
  88. package/dist/material/skin.js +192 -32
  89. package/dist/material/skin.js.map +1 -1
  90. package/dist/material/spriteblueprint.js +16 -1
  91. package/dist/material/spriteblueprint.js.map +1 -1
  92. package/dist/material/subsurfaceprofile.js +86 -23
  93. package/dist/material/subsurfaceprofile.js.map +1 -1
  94. package/dist/material/water.js +3 -1
  95. package/dist/material/water.js.map +1 -1
  96. package/dist/posteffect/bloom.js +167 -7
  97. package/dist/posteffect/bloom.js.map +1 -1
  98. package/dist/posteffect/coloradjust.js +2 -1
  99. package/dist/posteffect/coloradjust.js.map +1 -1
  100. package/dist/posteffect/compositor.js.map +1 -1
  101. package/dist/posteffect/fxaa.js +2 -1
  102. package/dist/posteffect/fxaa.js.map +1 -1
  103. package/dist/posteffect/grayscale.js +3 -1
  104. package/dist/posteffect/grayscale.js.map +1 -1
  105. package/dist/posteffect/motionblur.js +4 -1
  106. package/dist/posteffect/motionblur.js.map +1 -1
  107. package/dist/posteffect/posteffect.js.map +1 -1
  108. package/dist/posteffect/skinsss.js +368 -69
  109. package/dist/posteffect/skinsss.js.map +1 -1
  110. package/dist/posteffect/ssgi.js +16 -9
  111. package/dist/posteffect/ssgi.js.map +1 -1
  112. package/dist/posteffect/ssr.js +3 -0
  113. package/dist/posteffect/ssr.js.map +1 -1
  114. package/dist/posteffect/sss.js +91 -34
  115. package/dist/posteffect/sss.js.map +1 -1
  116. package/dist/posteffect/taa.js.map +1 -1
  117. package/dist/posteffect/tonemap.js +44 -1
  118. package/dist/posteffect/tonemap.js.map +1 -1
  119. package/dist/render/abuffer_oit.js +45 -39
  120. package/dist/render/abuffer_oit.js.map +1 -1
  121. package/dist/render/cull_visitor.js +19 -0
  122. package/dist/render/cull_visitor.js.map +1 -1
  123. package/dist/render/depthpass.js +36 -2
  124. package/dist/render/depthpass.js.map +1 -1
  125. package/dist/render/drawable_mixin.js +26 -1
  126. package/dist/render/drawable_mixin.js.map +1 -1
  127. package/dist/render/dualdepthpeeling_oit.js +52 -51
  128. package/dist/render/dualdepthpeeling_oit.js.map +1 -1
  129. package/dist/render/envlight.js +1 -1
  130. package/dist/render/envlight.js.map +1 -1
  131. package/dist/render/fbm_wavegenerator.js +3 -0
  132. package/dist/render/fbm_wavegenerator.js.map +1 -1
  133. package/dist/render/gerstner_wavegenerator.js +3 -0
  134. package/dist/render/gerstner_wavegenerator.js.map +1 -1
  135. package/dist/render/globalbindgroup_allocator.js +3 -0
  136. package/dist/render/globalbindgroup_allocator.js.map +1 -1
  137. package/dist/render/oit.js +29 -0
  138. package/dist/render/oit.js.map +1 -0
  139. package/dist/render/render_queue.js +3 -0
  140. package/dist/render/render_queue.js.map +1 -1
  141. package/dist/render/rendergraph/device_pool_allocator.js.map +1 -1
  142. package/dist/render/rendergraph/executor.js +2 -0
  143. package/dist/render/rendergraph/executor.js.map +1 -1
  144. package/dist/render/rendergraph/forward_plus_builder.js +61 -1
  145. package/dist/render/rendergraph/forward_plus_builder.js.map +1 -1
  146. package/dist/render/rendergraph/rendergraph.js.map +1 -1
  147. package/dist/render/rendergraph/texture_affinity_cache.js +1 -1
  148. package/dist/render/rendergraph/texture_affinity_cache.js.map +1 -1
  149. package/dist/render/rendergraph/types.js.map +1 -1
  150. package/dist/render/shadow_mask_pass.js +73 -8
  151. package/dist/render/shadow_mask_pass.js.map +1 -1
  152. package/dist/render/sky.js +166 -126
  153. package/dist/render/sky.js.map +1 -1
  154. package/dist/render/weightedblended_oit.js +36 -35
  155. package/dist/render/weightedblended_oit.js.map +1 -1
  156. package/dist/scene/basesprite.js +8 -0
  157. package/dist/scene/basesprite.js.map +1 -1
  158. package/dist/scene/batchgroup.js +3 -0
  159. package/dist/scene/batchgroup.js.map +1 -1
  160. package/dist/scene/environment.js +4 -1
  161. package/dist/scene/environment.js.map +1 -1
  162. package/dist/scene/hair.js +963 -0
  163. package/dist/scene/hair.js.map +1 -0
  164. package/dist/scene/light.js +5 -1
  165. package/dist/scene/light.js.map +1 -1
  166. package/dist/scene/mesh.js +481 -47
  167. package/dist/scene/mesh.js.map +1 -1
  168. package/dist/scene/meshdrawable.js +6 -0
  169. package/dist/scene/meshdrawable.js.map +1 -1
  170. package/dist/scene/msdftext.js +3 -0
  171. package/dist/scene/msdftext.js.map +1 -1
  172. package/dist/scene/msdftextsprite.js +3 -0
  173. package/dist/scene/msdftextsprite.js.map +1 -1
  174. package/dist/scene/particlesys.js +6 -0
  175. package/dist/scene/particlesys.js.map +1 -1
  176. package/dist/scene/scene.js +3 -0
  177. package/dist/scene/scene.js.map +1 -1
  178. package/dist/scene/scene_node.js +6 -1
  179. package/dist/scene/scene_node.js.map +1 -1
  180. package/dist/scene/terrain-cm/grass.js +3 -0
  181. package/dist/scene/terrain-cm/grass.js.map +1 -1
  182. package/dist/scene/terrain-cm/grassmaterial.js +3 -0
  183. package/dist/scene/terrain-cm/grassmaterial.js.map +1 -1
  184. package/dist/scene/terrain-cm/terrain-cm.js +6 -0
  185. package/dist/scene/terrain-cm/terrain-cm.js.map +1 -1
  186. package/dist/scene/terrain-cm/texture_cache.js +309 -0
  187. package/dist/scene/terrain-cm/texture_cache.js.map +1 -0
  188. package/dist/scene/water.js +6 -0
  189. package/dist/scene/water.js.map +1 -1
  190. package/dist/shaders/atmosphere.js.map +1 -1
  191. package/dist/shaders/diffusion.js +54 -0
  192. package/dist/shaders/diffusion.js.map +1 -0
  193. package/dist/shaders/shadow.js +33 -21
  194. package/dist/shaders/shadow.js.map +1 -1
  195. package/dist/shaders/temporal.js +71 -33
  196. package/dist/shaders/temporal.js.map +1 -1
  197. package/dist/shadow/dom.js +546 -0
  198. package/dist/shadow/dom.js.map +1 -0
  199. package/dist/shadow/pcf_opt.js +3 -5
  200. package/dist/shadow/pcf_opt.js.map +1 -1
  201. package/dist/shadow/pcf_pd.js +1 -1
  202. package/dist/shadow/pcf_pd.js.map +1 -1
  203. package/dist/shadow/receiver_bias.js +41 -0
  204. package/dist/shadow/receiver_bias.js.map +1 -0
  205. package/dist/shadow/shader.js +20 -60
  206. package/dist/shadow/shader.js.map +1 -1
  207. package/dist/shadow/shadow_impl.js +89 -0
  208. package/dist/shadow/shadow_impl.js.map +1 -1
  209. package/dist/shadow/shadow_region.js +50 -50
  210. package/dist/shadow/shadow_region.js.map +1 -1
  211. package/dist/shadow/shadowmapper.js +150 -37
  212. package/dist/shadow/shadowmapper.js.map +1 -1
  213. package/dist/shapes/shape.js +11 -2
  214. package/dist/shapes/shape.js.map +1 -1
  215. package/dist/shapes/sphere.js +50 -9
  216. package/dist/shapes/sphere.js.map +1 -1
  217. package/dist/utility/blueprint/common/constants.js +112 -2
  218. package/dist/utility/blueprint/common/constants.js.map +1 -1
  219. package/dist/utility/blueprint/material/func.js +142 -49
  220. package/dist/utility/blueprint/material/func.js.map +1 -1
  221. package/dist/utility/blueprint/material/ir.js +490 -25
  222. package/dist/utility/blueprint/material/ir.js.map +1 -1
  223. package/dist/utility/blueprint/material/texture.js +898 -4
  224. package/dist/utility/blueprint/material/texture.js.map +1 -1
  225. package/dist/utility/serialization/manager.js +431 -100
  226. package/dist/utility/serialization/manager.js.map +1 -1
  227. package/dist/utility/serialization/scene/animation.js +370 -3
  228. package/dist/utility/serialization/scene/animation.js.map +1 -1
  229. package/dist/utility/serialization/scene/batch.js +4 -0
  230. package/dist/utility/serialization/scene/batch.js.map +1 -1
  231. package/dist/utility/serialization/scene/camera.js +133 -1
  232. package/dist/utility/serialization/scene/camera.js.map +1 -1
  233. package/dist/utility/serialization/scene/common.js +26 -5
  234. package/dist/utility/serialization/scene/common.js.map +1 -1
  235. package/dist/utility/serialization/scene/hair.js +1141 -0
  236. package/dist/utility/serialization/scene/hair.js.map +1 -0
  237. package/dist/utility/serialization/scene/light.js +84 -4
  238. package/dist/utility/serialization/scene/light.js.map +1 -1
  239. package/dist/utility/serialization/scene/material.js +1867 -166
  240. package/dist/utility/serialization/scene/material.js.map +1 -1
  241. package/dist/utility/serialization/scene/mesh.js +231 -14
  242. package/dist/utility/serialization/scene/mesh.js.map +1 -1
  243. package/dist/utility/serialization/scene/node.js +141 -7
  244. package/dist/utility/serialization/scene/node.js.map +1 -1
  245. package/dist/utility/serialization/scene/particle.js +4 -0
  246. package/dist/utility/serialization/scene/particle.js.map +1 -1
  247. package/dist/utility/serialization/scene/primitive.js +18 -0
  248. package/dist/utility/serialization/scene/primitive.js.map +1 -1
  249. package/dist/utility/serialization/scene/sprite.js +4 -0
  250. package/dist/utility/serialization/scene/sprite.js.map +1 -1
  251. package/dist/utility/serialization/scene/terrain.js +4 -0
  252. package/dist/utility/serialization/scene/terrain.js.map +1 -1
  253. package/dist/utility/serialization/scene/text.js +4 -0
  254. package/dist/utility/serialization/scene/text.js.map +1 -1
  255. package/dist/utility/serialization/scene/water.js +4 -0
  256. package/dist/utility/serialization/scene/water.js.map +1 -1
  257. package/dist/utility/serialization/types.js.map +1 -1
  258. package/dist/utility/shprojector.js +96 -95
  259. package/dist/utility/shprojector.js.map +1 -1
  260. package/dist/values.js +49 -2
  261. package/dist/values.js.map +1 -1
  262. package/package.json +3 -3
@@ -0,0 +1,917 @@
1
+ import { applyMaterialMixins, MeshMaterial } from './meshmaterial.js';
2
+ import { mixinLight } from './mixins/lit.js';
3
+ import { mixinVertexColor } from './mixins/vertexcolor.js';
4
+ import { mixinTextureProps } from './mixins/texture.js';
5
+ import { mixinPBRBRDF } from './mixins/pbr/brdf.js';
6
+ import { ShaderHelper } from './shader/helper.js';
7
+ import { RENDER_PASS_TYPE_LIGHT, MaterialVaryingFlags, LIGHT_TYPE_POINT } from '../values.js';
8
+ import { Vector3, Vector4 } from '@zephyr3d/base';
9
+
10
+ /**
11
+ * Hair-card material using a Kajiya-Kay style double-lobe anisotropic lighting model.
12
+ *
13
+ * Lighting terms:
14
+ * - Wrap diffuse to soften the terminator across thin cards.
15
+ * - Primary specular lobe (usually near-white, sharp, shifted toward the root).
16
+ * - Secondary specular lobe (tinted by hair color, broader, shifted toward the tip),
17
+ * with an optional shift texture that jitters both lobes per strand to break up
18
+ * the "angel ring" into natural streaks.
19
+ * - Optional view-dependent transmission for backlit tips.
20
+ * - Optional baked occlusion map (root darkening).
21
+ *
22
+ * Alpha handling comes from the MeshMaterial base: use `alphaCutoff` plus
23
+ * `alphaDither` for TAA-converged soft edges on the opaque core, and
24
+ * `blendMode='blend'` for the outer flyaway layer.
25
+ * @public
26
+ */ class HairMaterial extends applyMaterialMixins(MeshMaterial, mixinLight, mixinPBRBRDF, mixinVertexColor, mixinTextureProps('specularShift'), mixinTextureProps('occlusion')) {
27
+ /** @internal */ static FEATURE_VERTEX_NORMAL = this.defineFeature();
28
+ /** @internal */ static FEATURE_VERTEX_TANGENT = this.defineFeature();
29
+ /** @internal */ static FEATURE_STRAND_DIRECTION = this.defineFeature();
30
+ /** @internal */ static FEATURE_TRANSMISSION = this.defineFeature();
31
+ /** @internal */ static FEATURE_SCATTER = this.defineFeature();
32
+ /** @internal Selects the scattering model. Holds a {@link HairShadingModel}. */ static FEATURE_SHADING_MODEL = this.defineFeature();
33
+ /** @internal Primary lobe color (usually near-white). */ _specular1Color;
34
+ /** @internal Primary lobe exponent. */ _specular1Power;
35
+ /** @internal Primary lobe shift along the normal (negative moves toward root). */ _specular1Shift;
36
+ /** @internal Secondary lobe color, multiplied by albedo in the shader. */ _specular2Color;
37
+ /** @internal Secondary lobe exponent. */ _specular2Power;
38
+ /** @internal Secondary lobe shift along the normal. */ _specular2Shift;
39
+ /** @internal Scale applied to (shiftTexture.r - 0.5). */ _shiftMapScale;
40
+ /** @internal Wrap diffuse amount in [0, 1]. */ _diffuseWrap;
41
+ /** @internal Transmission tint color. */ _transmissionColor;
42
+ /** @internal Transmission intensity, 0 disables the term. */ _transmissionIntensity;
43
+ /** @internal Transmission view-alignment exponent. */ _transmissionPower;
44
+ /** @internal Occlusion map strength in [0, 1]. */ _occlusionStrength;
45
+ /** @internal Marschner longitudinal shift of the R lobe, in sine units. */ _marschnerShift;
46
+ /** @internal Marschner longitudinal roughness. */ _marschnerRoughness;
47
+ /** @internal Refractive index of the fibre. */ _marschnerIOR;
48
+ /** @internal Multiplier on the Beer-Lambert path length. */ _marschnerAbsorption;
49
+ /** @internal Per-lobe intensities: R, TT, TRT. */ _marschnerLobes;
50
+ /** @internal Tint of multiply-scattered light. */ _scatterColor;
51
+ /** @internal Multiple scattering intensity, 0 disables the term. */ _scatterIntensity;
52
+ /** @internal Share of the scattering that survives where direct light does not. */ _scatterLocal;
53
+ /** @internal Angular width of the forward-scattered lobe. */ _scatterWrap;
54
+ /** @internal Scratch vector for packing uniform values without per-frame allocation. */ _uniformScratch;
55
+ /**
56
+ * Creates an instance of HairMaterial with defaults tuned for dark stylized hair.
57
+ */ constructor(){
58
+ super();
59
+ this._specular1Color = new Vector3(0.35, 0.35, 0.35);
60
+ this._specular1Power = 160;
61
+ this._specular1Shift = -0.15;
62
+ this._specular2Color = new Vector3(0.5, 0.45, 0.4);
63
+ this._specular2Power = 40;
64
+ this._specular2Shift = 0.1;
65
+ this._shiftMapScale = 1;
66
+ this._diffuseWrap = 0.5;
67
+ this._transmissionColor = new Vector3(0.9, 0.65, 0.45);
68
+ this._transmissionIntensity = 0;
69
+ this._transmissionPower = 6;
70
+ this._occlusionStrength = 1;
71
+ // Marschner defaults follow the measured fibre: the cuticle scales tilt the
72
+ // surface by a few degrees, which is what separates the three lobes at all.
73
+ this._marschnerShift = 0.035;
74
+ this._marschnerRoughness = 0.3;
75
+ this._marschnerIOR = 1.55;
76
+ this._marschnerAbsorption = 1;
77
+ this._marschnerLobes = new Vector3(1, 1, 1);
78
+ this._scatterColor = new Vector3(1, 0.85, 0.7);
79
+ this._scatterIntensity = 0;
80
+ this._scatterLocal = 0.5;
81
+ this._scatterWrap = 1;
82
+ this._uniformScratch = new Vector4();
83
+ this.useFeature(HairMaterial.FEATURE_VERTEX_NORMAL, true);
84
+ this.useFeature(HairMaterial.FEATURE_STRAND_DIRECTION, 'binormal');
85
+ this.useFeature(HairMaterial.FEATURE_TRANSMISSION, false);
86
+ this.useFeature(HairMaterial.FEATURE_SCATTER, false);
87
+ this.useFeature(HairMaterial.FEATURE_SHADING_MODEL, 'kajiya-kay');
88
+ // Hair cards are visible from both sides.
89
+ this.cullMode = 'none';
90
+ }
91
+ clone() {
92
+ const other = new HairMaterial();
93
+ other.copyFrom(this);
94
+ return other;
95
+ }
96
+ copyFrom(other) {
97
+ super.copyFrom(other);
98
+ this.specular1Color = other.specular1Color;
99
+ this.specular1Power = other.specular1Power;
100
+ this.specular1Shift = other.specular1Shift;
101
+ this.specular2Color = other.specular2Color;
102
+ this.specular2Power = other.specular2Power;
103
+ this.specular2Shift = other.specular2Shift;
104
+ this.shiftMapScale = other.shiftMapScale;
105
+ this.diffuseWrap = other.diffuseWrap;
106
+ this.transmissionColor = other.transmissionColor;
107
+ this.transmissionIntensity = other.transmissionIntensity;
108
+ this.transmissionPower = other.transmissionPower;
109
+ this.occlusionStrength = other.occlusionStrength;
110
+ this.scatterColor = other.scatterColor;
111
+ this.scatterIntensity = other.scatterIntensity;
112
+ this.scatterLocal = other.scatterLocal;
113
+ this.scatterWrap = other.scatterWrap;
114
+ this.shadingModel = other.shadingModel;
115
+ this.marschnerShift = other.marschnerShift;
116
+ this.marschnerRoughness = other.marschnerRoughness;
117
+ this.marschnerIOR = other.marschnerIOR;
118
+ this.marschnerAbsorption = other.marschnerAbsorption;
119
+ this.marschnerLobes = other.marschnerLobes;
120
+ }
121
+ /** true if vertex normal attribute presents */ get vertexNormal() {
122
+ return this.featureUsed(HairMaterial.FEATURE_VERTEX_NORMAL);
123
+ }
124
+ set vertexNormal(val) {
125
+ this.useFeature(HairMaterial.FEATURE_VERTEX_NORMAL, !!val);
126
+ }
127
+ /** true if vertex tangent attribute presents */ get vertexTangent() {
128
+ return this.featureUsed(HairMaterial.FEATURE_VERTEX_TANGENT);
129
+ }
130
+ set vertexTangent(val) {
131
+ this.useFeature(HairMaterial.FEATURE_VERTEX_TANGENT, !!val);
132
+ }
133
+ /** Which TBN axis runs along the strands */ get strandDirection() {
134
+ return this.featureUsed(HairMaterial.FEATURE_STRAND_DIRECTION);
135
+ }
136
+ set strandDirection(val) {
137
+ this.useFeature(HairMaterial.FEATURE_STRAND_DIRECTION, val);
138
+ }
139
+ /** Primary specular lobe color */ get specular1Color() {
140
+ return this._specular1Color;
141
+ }
142
+ set specular1Color(val) {
143
+ if (!val.equalsTo(this._specular1Color)) {
144
+ this._specular1Color.set(val);
145
+ this.uniformChanged();
146
+ }
147
+ }
148
+ /** Primary specular lobe exponent */ get specular1Power() {
149
+ return this._specular1Power;
150
+ }
151
+ set specular1Power(val) {
152
+ if (val !== this._specular1Power) {
153
+ this._specular1Power = val;
154
+ this.uniformChanged();
155
+ }
156
+ }
157
+ /** Primary specular lobe shift along the normal */ get specular1Shift() {
158
+ return this._specular1Shift;
159
+ }
160
+ set specular1Shift(val) {
161
+ if (val !== this._specular1Shift) {
162
+ this._specular1Shift = val;
163
+ this.uniformChanged();
164
+ }
165
+ }
166
+ /** Secondary specular lobe color (multiplied by albedo) */ get specular2Color() {
167
+ return this._specular2Color;
168
+ }
169
+ set specular2Color(val) {
170
+ if (!val.equalsTo(this._specular2Color)) {
171
+ this._specular2Color.set(val);
172
+ this.uniformChanged();
173
+ }
174
+ }
175
+ /** Secondary specular lobe exponent */ get specular2Power() {
176
+ return this._specular2Power;
177
+ }
178
+ set specular2Power(val) {
179
+ if (val !== this._specular2Power) {
180
+ this._specular2Power = val;
181
+ this.uniformChanged();
182
+ }
183
+ }
184
+ /** Secondary specular lobe shift along the normal */ get specular2Shift() {
185
+ return this._specular2Shift;
186
+ }
187
+ set specular2Shift(val) {
188
+ if (val !== this._specular2Shift) {
189
+ this._specular2Shift = val;
190
+ this.uniformChanged();
191
+ }
192
+ }
193
+ /** Scale applied to the shift texture */ get shiftMapScale() {
194
+ return this._shiftMapScale;
195
+ }
196
+ set shiftMapScale(val) {
197
+ if (val !== this._shiftMapScale) {
198
+ this._shiftMapScale = val;
199
+ this.uniformChanged();
200
+ }
201
+ }
202
+ /** Wrap diffuse amount in [0, 1] */ get diffuseWrap() {
203
+ return this._diffuseWrap;
204
+ }
205
+ set diffuseWrap(val) {
206
+ val = val < 0 ? 0 : val > 1 ? 1 : val;
207
+ if (val !== this._diffuseWrap) {
208
+ this._diffuseWrap = val;
209
+ this.uniformChanged();
210
+ }
211
+ }
212
+ /** Transmission tint color */ get transmissionColor() {
213
+ return this._transmissionColor;
214
+ }
215
+ set transmissionColor(val) {
216
+ if (!val.equalsTo(this._transmissionColor)) {
217
+ this._transmissionColor.set(val);
218
+ this.uniformChanged();
219
+ }
220
+ }
221
+ /** Transmission intensity, 0 disables the transmission term */ get transmissionIntensity() {
222
+ return this._transmissionIntensity;
223
+ }
224
+ set transmissionIntensity(val) {
225
+ val = val < 0 ? 0 : val;
226
+ if (val !== this._transmissionIntensity) {
227
+ this._transmissionIntensity = val;
228
+ this.useFeature(HairMaterial.FEATURE_TRANSMISSION, val > 0);
229
+ this.uniformChanged();
230
+ }
231
+ }
232
+ /** Transmission view-alignment exponent */ get transmissionPower() {
233
+ return this._transmissionPower;
234
+ }
235
+ set transmissionPower(val) {
236
+ if (val !== this._transmissionPower) {
237
+ this._transmissionPower = val;
238
+ this.uniformChanged();
239
+ }
240
+ }
241
+ /**
242
+ * Tint of multiply-scattered light.
243
+ *
244
+ * @remarks
245
+ * Multiple scattering is strongly wavelength dependent - it is why blonde hair
246
+ * glows and black hair does not - and the tint is applied on top of the hair's
247
+ * own colour, which the term is already modulated by. Warm by default, since
248
+ * the light that survives several bounces through keratin has lost its blue.
249
+ */ get scatterColor() {
250
+ return this._scatterColor;
251
+ }
252
+ set scatterColor(val) {
253
+ if (!val.equalsTo(this._scatterColor)) {
254
+ this._scatterColor.set(val);
255
+ this.uniformChanged();
256
+ }
257
+ }
258
+ /**
259
+ * Multiple scattering intensity. Zero disables the term, which is the default.
260
+ *
261
+ * @remarks
262
+ * Absorption alone makes the inside of a groom black, because that is what
263
+ * absorption does: past a few fibres essentially nothing of the direct beam
264
+ * survives. Real hair is not black there, and the difference is light that
265
+ * reached the point after bouncing off neighbouring fibres rather than
266
+ * travelling straight through.
267
+ *
268
+ * This adds an approximation of it in two parts, following the decomposition
269
+ * dual scattering makes: a global term, the light that came through the hair in
270
+ * front and arrives spread over a wide angle, and a local term, the light that
271
+ * did not come through but re-emerges after scattering nearby. The first fades
272
+ * with depth into the groom, the second does not, and it is the second that
273
+ * stops the interior going black.
274
+ *
275
+ * The forward transmittance both terms are built from is the shadow value, so
276
+ * this is at its most meaningful under a shadow mode that grades it - a deep
277
+ * opacity map. Under a binary filter the global term simply follows the shadow
278
+ * and the local term fills its complement.
279
+ *
280
+ * Off by default: it adds light the material did not previously emit, so it
281
+ * changes the look of any groom it is enabled on rather than refining it.
282
+ */ get scatterIntensity() {
283
+ return this._scatterIntensity;
284
+ }
285
+ set scatterIntensity(val) {
286
+ val = val < 0 ? 0 : val;
287
+ if (val !== this._scatterIntensity) {
288
+ this._scatterIntensity = val;
289
+ this.useFeature(HairMaterial.FEATURE_SCATTER, val > 0);
290
+ this.uniformChanged();
291
+ }
292
+ }
293
+ /**
294
+ * Balance between the two scattering terms, in [0, 1].
295
+ *
296
+ * @remarks
297
+ * Zero keeps only the global term, so the scattering follows the light and
298
+ * vanishes deep in the groom. One keeps only the local term, which is brightest
299
+ * exactly where the direct light is blocked. The default splits them evenly;
300
+ * denser and lighter hair wants more local.
301
+ */ get scatterLocal() {
302
+ return this._scatterLocal;
303
+ }
304
+ set scatterLocal(val) {
305
+ val = val < 0 ? 0 : val > 1 ? 1 : val;
306
+ if (val !== this._scatterLocal) {
307
+ this._scatterLocal = val;
308
+ this.uniformChanged();
309
+ }
310
+ }
311
+ /**
312
+ * Angular width of the forward-scattered lobe.
313
+ *
314
+ * @remarks
315
+ * Scattering spreads light as it crosses fibres, so the global term is not a
316
+ * cosine lobe but a much broader one; this is how far past the terminator it
317
+ * reaches. Larger than {@link HairMaterial.diffuseWrap} on purpose - that one
318
+ * softens a terminator, this one models a beam that has genuinely lost its
319
+ * direction.
320
+ */ get scatterWrap() {
321
+ return this._scatterWrap;
322
+ }
323
+ set scatterWrap(val) {
324
+ val = val < 0 ? 0 : val;
325
+ if (val !== this._scatterWrap) {
326
+ this._scatterWrap = val;
327
+ this.uniformChanged();
328
+ }
329
+ }
330
+ /**
331
+ * Which scattering model shades the hair. Defaults to `kajiya-kay`.
332
+ *
333
+ * @remarks
334
+ * Switching to `marschner` changes the look of any hair it is enabled on
335
+ * rather than refining it, so it is opted into. See {@link HairShadingModel}
336
+ * for what the two models are; the `marschner*` properties are inert under
337
+ * `kajiya-kay` and the specular lobe properties are inert under `marschner`.
338
+ *
339
+ * Two existing properties overlap with it and should be left alone when it is
340
+ * on: {@link HairMaterial.transmissionIntensity}, whose whole job is faking the
341
+ * backlit glow that Marschner's TT path produces for real, and - on
342
+ * `HairStrandMaterial` - `strandRoundness`, whose bent normal no longer reaches
343
+ * the highlight, because Marschner already integrates across the fibre.
344
+ */ get shadingModel() {
345
+ return this.featureUsed(HairMaterial.FEATURE_SHADING_MODEL);
346
+ }
347
+ set shadingModel(val) {
348
+ this.useFeature(HairMaterial.FEATURE_SHADING_MODEL, val);
349
+ }
350
+ /**
351
+ * Longitudinal shift of the Marschner lobes, in sine units.
352
+ *
353
+ * @remarks
354
+ * Hair is not a smooth cylinder: overlapping cuticle scales tilt its surface
355
+ * by a few degrees toward the tip. That tilt is the only reason the three
356
+ * lobes sit at different angles instead of on top of one another, so this is
357
+ * what makes them read as separate highlights at all. The three shifts are
358
+ * derived from it in the ratio the measurements give, R getting `-2x` and TRT
359
+ * `4x`, so one dial moves the whole set consistently.
360
+ *
361
+ * Defaults to 0.035, roughly the measured 2 degrees.
362
+ */ get marschnerShift() {
363
+ return this._marschnerShift;
364
+ }
365
+ set marschnerShift(val) {
366
+ if (val !== this._marschnerShift) {
367
+ this._marschnerShift = val;
368
+ this.uniformChanged();
369
+ }
370
+ }
371
+ /**
372
+ * Longitudinal roughness of the Marschner lobes, in (0, 1].
373
+ *
374
+ * @remarks
375
+ * The angular width of the highlight bands. Per-lobe widths are derived from
376
+ * it - TT half, TRT double - which is the ordering real fibres show: light
377
+ * that only passes through stays tight, light that has bounced inside spreads.
378
+ */ get marschnerRoughness() {
379
+ return this._marschnerRoughness;
380
+ }
381
+ set marschnerRoughness(val) {
382
+ const v = val < 0.005 ? 0.005 : val > 1 ? 1 : val;
383
+ if (v !== this._marschnerRoughness) {
384
+ this._marschnerRoughness = v;
385
+ this.uniformChanged();
386
+ }
387
+ }
388
+ /**
389
+ * Refractive index of the fibre. Defaults to the measured 1.55 for keratin.
390
+ *
391
+ * @remarks
392
+ * Sets how much light reflects at the surface rather than entering, and how
393
+ * sharply it bends once inside - so it moves the balance between the white R
394
+ * highlight and the two coloured paths. Rarely worth changing from the
395
+ * measured value, but exposed because it is a real physical quantity.
396
+ */ get marschnerIOR() {
397
+ return this._marschnerIOR;
398
+ }
399
+ set marschnerIOR(val) {
400
+ const v = val < 1.01 ? 1.01 : val;
401
+ if (v !== this._marschnerIOR) {
402
+ this._marschnerIOR = v;
403
+ this.uniformChanged();
404
+ }
405
+ }
406
+ /**
407
+ * Multiplier on how far light travels through the fibre before leaving.
408
+ *
409
+ * @remarks
410
+ * The transmitted paths are tinted by absorbing the hair's own colour over the
411
+ * distance they cross, so raising this deepens the colour of TT and TRT
412
+ * without touching the white surface reflection. It is the dial for "how
413
+ * strongly pigmented", separate from which colour the pigment is.
414
+ */ get marschnerAbsorption() {
415
+ return this._marschnerAbsorption;
416
+ }
417
+ set marschnerAbsorption(val) {
418
+ const v = val < 0 ? 0 : val;
419
+ if (v !== this._marschnerAbsorption) {
420
+ this._marschnerAbsorption = v;
421
+ this.uniformChanged();
422
+ }
423
+ }
424
+ /**
425
+ * Per-path intensities as `(R, TT, TRT)`, all 1 by default.
426
+ *
427
+ * @remarks
428
+ * Art override on top of the physics, and the fastest way to see what each
429
+ * path contributes: zero two of them and the third is on its own. R is the
430
+ * white surface highlight, TT the backlit glow, TRT the coloured secondary.
431
+ */ get marschnerLobes() {
432
+ return this._marschnerLobes;
433
+ }
434
+ set marschnerLobes(val) {
435
+ if (!val.equalsTo(this._marschnerLobes)) {
436
+ this._marschnerLobes.set(val);
437
+ this.uniformChanged();
438
+ }
439
+ }
440
+ /** Occlusion map strength in [0, 1] */ get occlusionStrength() {
441
+ return this._occlusionStrength;
442
+ }
443
+ set occlusionStrength(val) {
444
+ val = val < 0 ? 0 : val > 1 ? 1 : val;
445
+ if (val !== this._occlusionStrength) {
446
+ this._occlusionStrength = val;
447
+ this.uniformChanged();
448
+ }
449
+ }
450
+ vertexShader(scope) {
451
+ super.vertexShader(scope);
452
+ this.vertexShaderImpl(scope);
453
+ }
454
+ fragmentShader(scope) {
455
+ super.fragmentShader(scope);
456
+ const pb = scope.$builder;
457
+ if (this.needFragmentColor() && this.drawContext.renderPass.type === RENDER_PASS_TYPE_LIGHT && !(this.drawContext.materialFlags & MaterialVaryingFlags.INSTANCING)) {
458
+ scope.zHairSpec1 = pb.vec4().uniform(2);
459
+ scope.zHairSpec2 = pb.vec4().uniform(2);
460
+ scope.zHairShift = pb.vec4().uniform(2);
461
+ scope.zHairParams = pb.vec4().uniform(2);
462
+ if (this.featureUsed(HairMaterial.FEATURE_TRANSMISSION)) {
463
+ scope.zHairTransmission = pb.vec4().uniform(2);
464
+ }
465
+ if (this.featureUsed(HairMaterial.FEATURE_SCATTER)) {
466
+ scope.zHairScatter = pb.vec4().uniform(2);
467
+ }
468
+ if (this.shadingModel === 'marschner') {
469
+ scope.zHairMarschner1 = pb.vec4().uniform(2);
470
+ scope.zHairMarschner2 = pb.vec4().uniform(2);
471
+ }
472
+ }
473
+ if (this.needFragmentColorInput()) {
474
+ scope.$l.albedo = this.calculateAlbedoColor(scope);
475
+ if (this.vertexColor) {
476
+ scope.albedo = pb.mul(scope.albedo, this.getVertexColor(scope));
477
+ }
478
+ if (this.drawContext.renderPass.type === RENDER_PASS_TYPE_LIGHT) {
479
+ scope.$l.normalInfo = this.calculateNormalAndTBN(scope, scope.$inputs.worldPos, scope.$inputs.wNorm, scope.$inputs.wTangent, scope.$inputs.wBinormal);
480
+ scope.$l.normal = this.calculateShadingNormal(scope, scope.normalInfo);
481
+ scope.$l.strandT = this.strandDirection === 'tangent' ? scope.normalInfo.TBN[0] : scope.normalInfo.TBN[1];
482
+ scope.$l.viewVec = this.calculateViewVector(scope, scope.$inputs.worldPos);
483
+ if (this.specularShiftTexture) {
484
+ scope.$l.shiftVal = pb.mul(pb.sub(this.sampleSpecularShiftTexture(scope).r, 0.5), scope.zHairShift.z);
485
+ } else {
486
+ scope.$l.shiftVal = pb.float(0);
487
+ }
488
+ scope.$l.hairAO = this.calculateHairOcclusion(scope);
489
+ scope.$l.hairDirectAO = this.calculateHairDirectOcclusion(scope);
490
+ scope.$l.litColor = this.hairLight(scope, scope.$inputs.worldPos, scope.normal, scope.strandT, scope.viewVec, scope.albedo, scope.shiftVal, scope.hairAO, scope.hairDirectAO, scope.normalInfo.TBN[2]);
491
+ if (this.drawContext.materialFlags & (MaterialVaryingFlags.SCENE_STORE_ROUGHNESS | MaterialVaryingFlags.SCENE_STORE_NORMAL)) {
492
+ // Screen-space reflections only produce noise on thin hair strands,
493
+ // so write zero reflectance to keep SSR off for hair pixels.
494
+ this.outputFragmentColor(scope, scope.$inputs.worldPos, pb.vec4(scope.litColor, scope.albedo.a), pb.vec4(0, 0, 0, 1), pb.vec4(pb.add(pb.mul(scope.normal, 0.5), pb.vec3(0.5)), 1));
495
+ } else {
496
+ this.outputFragmentColor(scope, scope.$inputs.worldPos, pb.vec4(scope.litColor, scope.albedo.a));
497
+ }
498
+ } else {
499
+ this.outputFragmentColor(scope, scope.$inputs.worldPos, scope.albedo);
500
+ }
501
+ } else {
502
+ this.outputFragmentColor(scope, scope.$inputs.worldPos, null);
503
+ }
504
+ }
505
+ applyUniformValues(bindGroup, ctx, pass) {
506
+ super.applyUniformValues(bindGroup, ctx, pass);
507
+ if (this.needFragmentColor(ctx) && ctx.renderPass.type === RENDER_PASS_TYPE_LIGHT && !(ctx.materialFlags & MaterialVaryingFlags.INSTANCING)) {
508
+ const scratch = this._uniformScratch;
509
+ bindGroup.setValue('zHairSpec1', scratch.setXYZW(this._specular1Color.x, this._specular1Color.y, this._specular1Color.z, this._specular1Power));
510
+ bindGroup.setValue('zHairSpec2', scratch.setXYZW(this._specular2Color.x, this._specular2Color.y, this._specular2Color.z, this._specular2Power));
511
+ bindGroup.setValue('zHairShift', scratch.setXYZW(this._specular1Shift, this._specular2Shift, this._shiftMapScale, this._diffuseWrap));
512
+ bindGroup.setValue('zHairParams', scratch.setXYZW(this._transmissionPower, this._occlusionStrength, this._scatterLocal, this._scatterWrap));
513
+ if (this.featureUsed(HairMaterial.FEATURE_TRANSMISSION)) {
514
+ bindGroup.setValue('zHairTransmission', scratch.setXYZW(this._transmissionColor.x, this._transmissionColor.y, this._transmissionColor.z, this._transmissionIntensity));
515
+ }
516
+ if (this.featureUsed(HairMaterial.FEATURE_SCATTER)) {
517
+ bindGroup.setValue('zHairScatter', scratch.setXYZW(this._scatterColor.x, this._scatterColor.y, this._scatterColor.z, this._scatterIntensity));
518
+ }
519
+ if (this.shadingModel === 'marschner') {
520
+ bindGroup.setValue('zHairMarschner1', scratch.setXYZW(this._marschnerShift, this._marschnerRoughness, this._marschnerIOR, this._marschnerAbsorption));
521
+ // Normal-incidence reflectance from the index, folded here because it
522
+ // does not vary per fragment. 1.55 gives roughly 0.046.
523
+ const f0 = (this._marschnerIOR - 1) / (this._marschnerIOR + 1);
524
+ bindGroup.setValue('zHairMarschner2', scratch.setXYZW(this._marschnerLobes.x, this._marschnerLobes.y, this._marschnerLobes.z, f0 * f0));
525
+ }
526
+ }
527
+ }
528
+ vertexShaderImpl(scope) {
529
+ const pb = scope.$builder;
530
+ scope.$l.oPos = ShaderHelper.resolveVertexPosition(scope);
531
+ scope.$outputs.worldPos = pb.mul(ShaderHelper.getWorldMatrix(scope), pb.vec4(scope.oPos, 1)).xyz;
532
+ ShaderHelper.setClipSpacePosition(scope, pb.mul(ShaderHelper.getViewProjectionMatrix(scope), pb.vec4(scope.$outputs.worldPos, 1)));
533
+ if (this.vertexNormal) {
534
+ scope.$l.oNorm = ShaderHelper.resolveVertexNormal(scope);
535
+ scope.$outputs.wNorm = pb.mul(ShaderHelper.getNormalMatrix(scope), pb.vec4(scope.oNorm, 0)).xyz;
536
+ if (this.vertexTangent) {
537
+ scope.$l.oTangent = ShaderHelper.resolveVertexTangent(scope);
538
+ scope.$outputs.wTangent = pb.mul(ShaderHelper.getNormalMatrix(scope), pb.vec4(scope.oTangent.xyz, 0)).xyz;
539
+ scope.$outputs.wBinormal = pb.mul(pb.cross(scope.$outputs.wNorm, scope.$outputs.wTangent), scope.oTangent.w);
540
+ }
541
+ }
542
+ }
543
+ /**
544
+ * Shading normal for the current fragment.
545
+ *
546
+ * @remarks
547
+ * Split out from the interpolated frame so a subclass whose polygons stand in
548
+ * for a different surface can say what that surface is. A hair card really is
549
+ * the flat sheet it is drawn as, so the frame's own normal is right here;
550
+ * {@link HairStrandMaterial} overrides this, because its quads stand in for
551
+ * round fibres and shading them flat is the difference between hair and tape.
552
+ * @internal
553
+ */ calculateShadingNormal(scope, normalInfo) {
554
+ return normalInfo.normal;
555
+ }
556
+ /**
557
+ * Ambient occlusion for the current fragment.
558
+ *
559
+ * @remarks
560
+ * Applied to the light that no shadow query answers for: environment
561
+ * irradiance and the multiple-scattering term. On a dense groom this is the
562
+ * only thing that keeps the strands buried inside it from receiving the full
563
+ * sky, which is what gives the hairstyle its volume.
564
+ *
565
+ * For a hair card that is the occlusion map's job, which is what this does.
566
+ * {@link HairStrandMaterial} has no texture coordinates to sample one with and
567
+ * overrides this with a geometric approximation instead.
568
+ * @internal
569
+ */ calculateHairOcclusion(scope) {
570
+ return this.sampleBaseOcclusion(scope);
571
+ }
572
+ /**
573
+ * Occlusion applied to direct (per-light) shading.
574
+ *
575
+ * @remarks
576
+ * Direct light already carries its own occlusion answer - the shadow map, or
577
+ * under DOM a graded transmittance - so an ambient term multiplied on top
578
+ * counts the same blockage twice: the map says thirty percent of the beam got
579
+ * through, and the AO halves it again. That is why this is a separate hook
580
+ * rather than one value for everything.
581
+ *
582
+ * The card path deliberately keeps the occlusion map here as well: a card
583
+ * groom's map is authored as the look of the whole stack and its cards rarely
584
+ * self-shadow credibly, so dimming direct light is part of what the author
585
+ * painted. The strand path's geometric term makes the opposite choice and
586
+ * leaves direct light to the shadow map - see
587
+ * {@link HairStrandMaterial.rootOcclusion}.
588
+ * @internal
589
+ */ calculateHairDirectOcclusion(scope) {
590
+ return this.sampleBaseOcclusion(scope);
591
+ }
592
+ /** The occlusion map contribution both hooks share. @internal */ sampleBaseOcclusion(scope) {
593
+ const pb = scope.$builder;
594
+ if (this.occlusionTexture) {
595
+ return pb.mix(pb.float(1), this.sampleOcclusionTexture(scope).r, scope.zHairParams.y);
596
+ }
597
+ return pb.float(1);
598
+ }
599
+ /**
600
+ * Approximates the light that reaches a point after bouncing off neighbouring
601
+ * fibres rather than travelling straight to it.
602
+ *
603
+ * @remarks
604
+ * Follows the split dual scattering makes, without its precomputation - the
605
+ * average forward and backward attenuations that theory derives from the fibre
606
+ * BCSDF are stand-in constants here, because this material's lighting is a
607
+ * phenomenological double lobe rather than a BCSDF to integrate.
608
+ *
609
+ * - **Global**, `throughput * wrapped(NoL)`: the beam that did cross the hair in
610
+ * front, arriving spread out. Scattering randomises direction as it goes, so
611
+ * the lobe is far wider than a cosine and reaches well past the terminator.
612
+ * - **Local**, `1 - throughput`: the beam that did not get through. Some of it
613
+ * leaves the fibres it hit and re-emerges here, which is why it is brightest
614
+ * exactly where the direct light is fully blocked, and why it does not fade
615
+ * with depth the way the global term does.
616
+ *
617
+ * Both are modulated by the hair's own colour as well as the scatter tint,
618
+ * because light that has bounced several times has been filtered by keratin
619
+ * several times: it is the reason pale hair glows in shadow and dark hair
620
+ * stays dark, and multiplying by albedo is what carries that difference.
621
+ * @internal
622
+ */ hairScatter(scope, lightColor, albedo, NoL, throughput, ao) {
623
+ const pb = scope.$builder;
624
+ const funcName = 'Z_hairScatter';
625
+ pb.func(funcName, [
626
+ pb.vec3('lightColor'),
627
+ pb.vec3('albedo'),
628
+ pb.float('NoL'),
629
+ pb.float('throughput'),
630
+ pb.float('ao')
631
+ ], function() {
632
+ this.$l.localMix = this.zHairParams.z;
633
+ this.$l.wrap = pb.max(this.zHairParams.w, 1e-4);
634
+ // Wrapped well past the terminator, and never negative: a point facing
635
+ // away from the light still receives scattered light, which is the whole
636
+ // point of the term.
637
+ this.$l.spread = pb.clamp(pb.div(pb.add(this.NoL, this.wrap), pb.add(1, this.wrap)), 0, 1);
638
+ this.$l.global = pb.mul(this.throughput, this.spread);
639
+ this.$l.local = pb.sub(1, this.throughput);
640
+ this.$l.amount = pb.mix(this.global, this.local, this.localMix);
641
+ this.$return(pb.mul(this.lightColor, this.albedo, this.zHairScatter.rgb, this.zHairScatter.a, this.amount, this.ao));
642
+ });
643
+ return pb.getGlobalScope()[funcName](lightColor, albedo, NoL, throughput, ao);
644
+ }
645
+ /**
646
+ * Normalised longitudinal lobe: a unit-integral Gaussian of width `width`.
647
+ *
648
+ * @remarks
649
+ * Stands in for the exact longitudinal term, which involves a Bessel function
650
+ * and, in the energy-conserving formulation, `cosh` - and `cosh` is not
651
+ * available on WebGL1 in this shader builder. The Gaussian is the
652
+ * approximation Marschner's own paper offers and is what real-time
653
+ * implementations use.
654
+ * @internal
655
+ */ hairLongitudinal(scope, width, offset) {
656
+ const pb = scope.$builder;
657
+ const funcName = 'Z_hairLongitudinal';
658
+ pb.func(funcName, [
659
+ pb.float('width'),
660
+ pb.float('offset')
661
+ ], function() {
662
+ this.$l.b = pb.max(this.width, 0.0001);
663
+ this.$l.t = pb.div(this.offset, this.b);
664
+ this.$return(pb.div(pb.exp(pb.mul(this.t, this.t, -0.5)), pb.mul(this.b, Math.sqrt(2 * Math.PI))));
665
+ });
666
+ return pb.getGlobalScope()[funcName](width, offset);
667
+ }
668
+ /**
669
+ * Marschner fibre scattering: the three paths light can take through a strand.
670
+ *
671
+ * @remarks
672
+ * Treats the strand as a translucent dielectric cylinder and adds up what
673
+ * leaves it, split by how many times the light crossed a surface:
674
+ *
675
+ * - **R** bounces straight off the outside. It never meets the pigment, so it
676
+ * is the colour of the light - the white sheen on dark hair.
677
+ * - **TT** goes in one side and out the other, crossing the pigment twice. It
678
+ * leaves roughly opposite where it entered, so it is what you see when the
679
+ * light is behind the head, and it carries the deepest colour.
680
+ * - **TRT** goes in, reflects off the far inside wall, and comes back out. It
681
+ * emerges near the side it entered, so it reads as a second highlight
682
+ * alongside R - but having crossed the pigment three times it is tinted,
683
+ * which is why the secondary highlight of real hair is hair-coloured. The
684
+ * double lobe has to be told that; here it falls out.
685
+ *
686
+ * Each path is a longitudinal lobe `M` in the angle up the fibre times an
687
+ * azimuthal lobe `N` in the angle around it, over `cos²θd` to convert the
688
+ * fibre's scattering into a BCSDF. `M` is a Gaussian, offset per path by the
689
+ * tilt of the cuticle scales - which is the entire reason the three appear at
690
+ * different angles rather than on top of each other.
691
+ *
692
+ * `N` is where the original model is expensive: solving it exactly means
693
+ * finding the roots of the exit-angle equation, a cubic for TRT. These are the
694
+ * published analytic fits to those solutions, which is the standard real-time
695
+ * trade - it costs the sharp caustic glint that a true root solve produces,
696
+ * and keeps everything else.
697
+ *
698
+ * Deliberately never touches the shading normal: the fibre frame is the
699
+ * tangent plus the two directions, and the round cross-section is already
700
+ * integrated into `N`. That makes it immune to the flat-ribbon normal problem
701
+ * that `HairStrandMaterial.strandRoundness` exists to work around.
702
+ * @internal
703
+ */ hairMarschner(scope, strandT, lightDir, viewVec, albedo, shiftVal) {
704
+ const pb = scope.$builder;
705
+ const that = this;
706
+ const funcName = 'Z_hairMarschner';
707
+ pb.func(funcName, [
708
+ pb.vec3('T'),
709
+ pb.vec3('L'),
710
+ pb.vec3('V'),
711
+ pb.vec3('albedo'),
712
+ pb.float('shiftVal')
713
+ ], function() {
714
+ // Longitudinal angles, measured off the plane perpendicular to the fibre.
715
+ this.$l.sinThetaL = pb.clamp(pb.dot(this.T, this.L), -1, 1);
716
+ this.$l.sinThetaV = pb.clamp(pb.dot(this.T, this.V), -1, 1);
717
+ // The difference angle. Every path length inside the fibre scales with
718
+ // it, because a ray that enters obliquely has further to travel.
719
+ this.$l.thetaD = pb.mul(pb.abs(pb.sub(pb.asin(this.sinThetaV), pb.asin(this.sinThetaL))), 0.5);
720
+ // Floored well above zero because the model divides by its square at the
721
+ // end. The degenerate case is real: viewing straight down a fibre that is
722
+ // lit straight up it puts thetaD at ninety degrees, and while the tinted
723
+ // paths kill themselves there through Beer-Lambert, R does not - Fresnel
724
+ // goes to one and M sits at its peak, so a 0.001 floor lets a single tip
725
+ // pointing at the camera flash at a million times the light's intensity.
726
+ // 0.2 keeps the gain within the range the azimuthal fits were made for.
727
+ this.$l.cosThetaD = pb.max(pb.cos(this.thetaD), 0.2);
728
+ // Azimuth, from the two directions projected onto the fibre's cross
729
+ // section. cos(phi) alone is enough: every N below is symmetric about
730
+ // the plane through the light, so the sign of phi never matters.
731
+ this.$l.Lp = pb.sub(this.L, pb.mul(this.T, this.sinThetaL));
732
+ this.$l.Vp = pb.sub(this.V, pb.mul(this.T, this.sinThetaV));
733
+ this.$l.cosPhi = pb.clamp(pb.mul(pb.dot(this.Lp, this.Vp), pb.inverseSqrt(pb.add(pb.mul(pb.dot(this.Lp, this.Lp), pb.dot(this.Vp, this.Vp)), 1e-5))), -1, 1);
734
+ this.$l.cosHalfPhi = pb.sqrt(pb.clamp(pb.add(pb.mul(this.cosPhi, 0.5), 0.5), 0, 1));
735
+ this.$l.ior = pb.max(this.zHairMarschner1.z, 1.01);
736
+ // F0 is a function of the index alone, so it is folded on the CPU.
737
+ this.$l.f0 = pb.vec3(this.zHairMarschner2.w);
738
+ this.$l.f90 = pb.vec3(1);
739
+ // Bravais' virtual index. A ray crossing the fibre at a slant refracts as
740
+ // though the material had a different index; this is that index. The
741
+ // widely used `1.19/cosThetaD + 0.36*cosThetaD` is a fit to this
742
+ // expression for one particular value of the index, and would ignore the
743
+ // index entirely - which would quietly make an exposed IOR do nothing.
744
+ // The exact form costs one sqrt and keeps the parameter meaningful.
745
+ this.$l.nPrime = pb.div(pb.sqrt(pb.max(pb.add(pb.mul(this.ior, this.ior), pb.mul(this.cosThetaD, this.cosThetaD), -1), 0.01)), this.cosThetaD);
746
+ // Cuticle tilt. The ratios between the three are what the measurements
747
+ // give; the shift map perturbs all three together, per strand.
748
+ this.$l.shift = pb.add(this.zHairMarschner1.x, this.shiftVal);
749
+ this.$l.beta = pb.max(this.zHairMarschner1.y, 0.005);
750
+ this.$l.width = pb.mul(this.beta, this.beta);
751
+ this.$l.theta = pb.add(this.sinThetaL, this.sinThetaV);
752
+ this.$l.absorb = this.zHairMarschner1.w;
753
+ // Guards pow() against a zero base, which is legal but not uniformly
754
+ // well behaved across drivers.
755
+ this.$l.tint = pb.max(this.albedo, pb.vec3(0.0001));
756
+ // --- R: off the surface, uncoloured.
757
+ this.$l.mR = that.hairLongitudinal(this, this.width, pb.sub(this.theta, pb.mul(this.shift, -2)));
758
+ this.$l.nR = pb.mul(this.cosHalfPhi, 0.25);
759
+ this.$l.fR = that.fresnelSchlick(this, pb.sqrt(pb.clamp(pb.add(pb.mul(pb.dot(this.V, this.L), 0.5), 0.5), 0, 1)), this.f0, this.f90);
760
+ this.$l.sR = pb.mul(this.fR, this.mR, this.nR, this.zHairMarschner2.x);
761
+ // --- TT: in one side, out the other. Two pigment crossings.
762
+ this.$l.mTT = that.hairLongitudinal(this, pb.mul(this.width, 0.5), pb.sub(this.theta, this.shift));
763
+ this.$l.a = pb.div(1, this.nPrime);
764
+ // Where across the fibre the ray that reaches the eye entered. The fit
765
+ // stands in for inverting the refraction, and drives both how much
766
+ // reflects away and how far the rest travels through the pigment.
767
+ this.$l.h = pb.clamp(pb.mul(this.cosHalfPhi, pb.add(1, pb.mul(this.a, pb.sub(0.6, pb.mul(this.cosPhi, 0.8))))), -1, 1);
768
+ this.$l.fTT = that.fresnelSchlick(this, pb.mul(this.cosThetaD, pb.sqrt(pb.max(pb.sub(1, pb.mul(this.h, this.h)), 0))), this.f0, this.f90);
769
+ this.$l.oneMinusFTT = pb.sub(pb.vec3(1), this.fTT);
770
+ this.$l.ha = pb.mul(this.h, this.a);
771
+ this.$l.pathTT = pb.div(pb.mul(pb.sqrt(pb.max(pb.sub(1, pb.mul(this.ha, this.ha)), 0)), 0.5), this.cosThetaD);
772
+ this.$l.tTT = pb.pow(this.tint, pb.vec3(pb.mul(this.pathTT, this.absorb)));
773
+ this.$l.nTT = pb.exp(pb.sub(pb.mul(this.cosPhi, -3.65), 3.98));
774
+ this.$l.sTT = pb.mul(this.oneMinusFTT, this.oneMinusFTT, this.tTT, this.mTT, this.nTT, this.zHairMarschner2.y);
775
+ // --- TRT: in, off the far wall, back out. Three pigment crossings, so
776
+ // this is the lobe that carries the hair's colour into the highlight.
777
+ this.$l.mTRT = that.hairLongitudinal(this, pb.mul(this.width, 2), pb.sub(this.theta, pb.mul(this.shift, 4)));
778
+ this.$l.fTRT = that.fresnelSchlick(this, pb.mul(this.cosThetaD, 0.5), this.f0, this.f90);
779
+ this.$l.oneMinusFTRT = pb.sub(pb.vec3(1), this.fTRT);
780
+ this.$l.tTRT = pb.pow(this.tint, pb.vec3(pb.mul(pb.div(0.8, this.cosThetaD), this.absorb)));
781
+ this.$l.nTRT = pb.exp(pb.sub(pb.mul(this.cosPhi, 17), 16.78));
782
+ this.$l.sTRT = pb.mul(this.oneMinusFTRT, this.oneMinusFTRT, this.fTRT, this.tTRT, this.mTRT, this.nTRT, this.zHairMarschner2.z);
783
+ this.$return(pb.div(pb.add(this.sR, this.sTT, this.sTRT), pb.mul(this.cosThetaD, this.cosThetaD)));
784
+ });
785
+ return pb.getGlobalScope()[funcName](strandT, lightDir, viewVec, albedo, shiftVal);
786
+ }
787
+ /**
788
+ * Scheuermann-style anisotropic strand specular: shift the strand tangent along
789
+ * the normal, then raise sin(T', H) to the lobe exponent with a directional fade.
790
+ * @internal
791
+ */ hairStrandSpecular(scope, strandT, normal, halfVec, shift, power) {
792
+ const pb = scope.$builder;
793
+ const funcName = 'Z_hairStrandSpecular';
794
+ pb.func(funcName, [
795
+ pb.vec3('strandT'),
796
+ pb.vec3('normal'),
797
+ pb.vec3('halfVec'),
798
+ pb.float('shift'),
799
+ pb.float('power')
800
+ ], function() {
801
+ this.$l.shiftedT = pb.normalize(pb.add(this.strandT, pb.mul(this.normal, this.shift)));
802
+ this.$l.TdotH = pb.dot(this.shiftedT, this.halfVec);
803
+ this.$l.sinTH = pb.sqrt(pb.max(pb.sub(1, pb.mul(this.TdotH, this.TdotH)), 0));
804
+ this.$l.dirAtten = pb.smoothStep(-1, 0, this.TdotH);
805
+ this.$return(pb.mul(this.dirAtten, pb.pow(pb.max(this.sinTH, 0.0001), this.power)));
806
+ });
807
+ return pb.getGlobalScope()[funcName](strandT, normal, halfVec, shift, power);
808
+ }
809
+ /**
810
+ * Computes the lit color for hair with wrap diffuse, double-lobe anisotropic
811
+ * specular and optional transmission.
812
+ * @internal
813
+ */ /**
814
+ * @param ao - Ambient occlusion, applied to environment irradiance and the
815
+ * scatter term - the light no shadow query answers for.
816
+ * @param aoDirect - Occlusion applied to per-light shading, whose direct
817
+ * blockage the shadow map already accounts for. See
818
+ * {@link HairMaterial.calculateHairDirectOcclusion}.
819
+ * @param geometricNormal - Interpolated vertex normal, used for shadow normal
820
+ * offset bias. Defaults to `normal`.
821
+ */ hairLight(scope, worldPos, normal, strandT, viewVec, albedo, shiftVal, ao, aoDirect, geometricNormal) {
822
+ const pb = scope.$builder;
823
+ const funcName = 'Z_hairLight';
824
+ const that = this;
825
+ const baseLightPass = !that.drawContext.lightBlending;
826
+ const useTransmission = that.featureUsed(HairMaterial.FEATURE_TRANSMISSION);
827
+ const useScatter = that.featureUsed(HairMaterial.FEATURE_SCATTER);
828
+ const useMarschner = that.shadingModel === 'marschner';
829
+ pb.func(funcName, [
830
+ pb.vec3('worldPos'),
831
+ pb.vec3('normal'),
832
+ pb.vec3('strandT'),
833
+ pb.vec3('viewVec'),
834
+ pb.vec4('albedo'),
835
+ pb.float('shiftVal'),
836
+ pb.float('ao'),
837
+ pb.float('aoDirect'),
838
+ pb.vec3('geometricNormal')
839
+ ], function() {
840
+ if (!that.needFragmentColor()) {
841
+ this.$return(this.albedo.rgb);
842
+ } else {
843
+ if (that.needCalculateEnvLight() && baseLightPass) {
844
+ this.$l.diffuseColor = pb.mul(that.getEnvLightIrradiance(this, this.normal), this.ao);
845
+ } else {
846
+ this.$l.diffuseColor = pb.vec3(0);
847
+ }
848
+ this.$l.specularColor = pb.vec3(0);
849
+ this.$l.transmissionColor = pb.vec3(0);
850
+ this.$l.scatterColor = pb.vec3(0);
851
+ that.forEachLight(this, function(type, posRange, dirCutoff, colorIntensity, extra, shadow) {
852
+ this.$l.diffuseScale = pb.float(1);
853
+ this.$l.specularScale = pb.float(1);
854
+ this.$if(pb.equal(type, LIGHT_TYPE_POINT), function() {
855
+ this.diffuseScale = extra.x;
856
+ this.specularScale = extra.y;
857
+ });
858
+ this.$l.lightAtten = that.calculateLightAttenuation(this, type, this.worldPos, posRange, dirCutoff, extra);
859
+ this.$l.lightDir = that.calculateLightDirection(this, type, this.worldPos, posRange, dirCutoff);
860
+ this.$l.NoL = pb.dot(this.normal, this.lightDir);
861
+ this.$l.lightColor = pb.mul(colorIntensity.rgb, colorIntensity.a, this.lightAtten);
862
+ // Wrap diffuse softens the terminator across thin cards.
863
+ this.$l.wrap = this.zHairShift.w;
864
+ this.$l.diffFactor = pb.clamp(pb.div(pb.add(this.NoL, this.wrap), pb.add(1, this.wrap)), 0, 1);
865
+ if (useMarschner) {
866
+ // No half vector and no specular fade here. The fade exists to stop
867
+ // the double lobe glowing where no light reaches; Marschner's TT
868
+ // path is light that genuinely came through the fibre from behind,
869
+ // so fading it out on the unlit side would delete the one thing the
870
+ // model offers that the double lobe cannot produce at all.
871
+ this.$l.specTerm = that.hairMarschner(this, this.strandT, this.lightDir, this.viewVec, this.albedo.rgb, this.shiftVal);
872
+ } else {
873
+ this.$l.halfVec = pb.normalize(pb.add(this.viewVec, this.lightDir));
874
+ this.$l.spec1 = that.hairStrandSpecular(this, this.strandT, this.normal, this.halfVec, pb.add(this.zHairShift.x, this.shiftVal), this.zHairSpec1.w);
875
+ this.$l.spec2 = that.hairStrandSpecular(this, this.strandT, this.normal, this.halfVec, pb.add(this.zHairShift.y, this.shiftVal), this.zHairSpec2.w);
876
+ // Fade specular out on the fully unlit side to avoid glowing shadows.
877
+ this.$l.specFade = pb.smoothStep(-0.35, 0.15, this.NoL);
878
+ this.$l.specTerm = pb.mul(pb.add(pb.mul(this.zHairSpec1.rgb, this.spec1), pb.mul(this.zHairSpec2.rgb, this.albedo.rgb, this.spec2)), this.specFade);
879
+ }
880
+ this.$l.diffuse = pb.mul(this.lightColor, 1 / Math.PI, this.diffFactor, this.aoDirect, this.diffuseScale);
881
+ this.$l.specular = pb.mul(this.lightColor, this.specTerm, this.aoDirect, this.specularScale);
882
+ if (useTransmission) {
883
+ this.$l.transDir = pb.normalize(pb.add(this.lightDir, pb.mul(this.normal, 0.3)));
884
+ this.$l.VoL = pb.clamp(pb.dot(this.viewVec, pb.neg(this.transDir)), 0, 1);
885
+ this.$l.transFactor = pb.mul(pb.pow(this.VoL, this.zHairParams.x), this.zHairTransmission.a);
886
+ this.$l.transmission = pb.mul(this.lightColor, this.zHairTransmission.rgb, this.transFactor, this.aoDirect);
887
+ } else {
888
+ this.$l.transmission = pb.vec3(0);
889
+ }
890
+ // Fraction of the beam that reached this point without being absorbed.
891
+ // Under a deep opacity map this grades with how much hair the light
892
+ // crossed, which is exactly what the scattering terms want; under a
893
+ // binary filter it is one or zero and they degrade to following it.
894
+ this.$l.throughput = pb.float(1);
895
+ if (shadow) {
896
+ this.$l.shadow = that.calculateShadow(this, this.worldPos, this.geometricNormal, pb.max(this.NoL, 0));
897
+ this.throughput = this.shadow;
898
+ this.diffuse = pb.mul(this.diffuse, this.shadow);
899
+ this.specular = pb.mul(this.specular, this.shadow);
900
+ this.transmission = pb.mul(this.transmission, this.shadow);
901
+ }
902
+ if (useScatter) {
903
+ this.scatterColor = pb.add(this.scatterColor, that.hairScatter(this, this.lightColor, this.albedo.rgb, this.NoL, this.throughput, this.ao));
904
+ }
905
+ this.diffuseColor = pb.add(this.diffuseColor, this.diffuse);
906
+ this.specularColor = pb.add(this.specularColor, this.specular);
907
+ this.transmissionColor = pb.add(this.transmissionColor, this.transmission);
908
+ });
909
+ this.$return(pb.add(pb.mul(this.albedo.rgb, this.diffuseColor), this.specularColor, this.transmissionColor, this.scatterColor));
910
+ }
911
+ });
912
+ return pb.getGlobalScope()[funcName](worldPos, normal, strandT, viewVec, albedo, shiftVal, ao, aoDirect, geometricNormal ?? normal);
913
+ }
914
+ }
915
+
916
+ export { HairMaterial };
917
+ //# sourceMappingURL=hair.js.map