@zephyr3d/scene 0.9.24 → 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 (264) 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 +4040 -1290
  42. package/dist/index.js +16 -6
  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 +766 -456
  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/cluster_light.js +4 -4
  122. package/dist/render/cluster_light.js.map +1 -1
  123. package/dist/render/cull_visitor.js +19 -0
  124. package/dist/render/cull_visitor.js.map +1 -1
  125. package/dist/render/depthpass.js +36 -2
  126. package/dist/render/depthpass.js.map +1 -1
  127. package/dist/render/drawable_mixin.js +26 -1
  128. package/dist/render/drawable_mixin.js.map +1 -1
  129. package/dist/render/dualdepthpeeling_oit.js +52 -51
  130. package/dist/render/dualdepthpeeling_oit.js.map +1 -1
  131. package/dist/render/envlight.js +1 -1
  132. package/dist/render/envlight.js.map +1 -1
  133. package/dist/render/fbm_wavegenerator.js +3 -0
  134. package/dist/render/fbm_wavegenerator.js.map +1 -1
  135. package/dist/render/gerstner_wavegenerator.js +3 -0
  136. package/dist/render/gerstner_wavegenerator.js.map +1 -1
  137. package/dist/render/globalbindgroup_allocator.js +3 -0
  138. package/dist/render/globalbindgroup_allocator.js.map +1 -1
  139. package/dist/render/oit.js +29 -0
  140. package/dist/render/oit.js.map +1 -0
  141. package/dist/render/render_queue.js +3 -0
  142. package/dist/render/render_queue.js.map +1 -1
  143. package/dist/render/rendergraph/device_pool_allocator.js.map +1 -1
  144. package/dist/render/rendergraph/executor.js +2 -0
  145. package/dist/render/rendergraph/executor.js.map +1 -1
  146. package/dist/render/rendergraph/forward_plus_builder.js +61 -1
  147. package/dist/render/rendergraph/forward_plus_builder.js.map +1 -1
  148. package/dist/render/rendergraph/rendergraph.js.map +1 -1
  149. package/dist/render/rendergraph/texture_affinity_cache.js +1 -1
  150. package/dist/render/rendergraph/texture_affinity_cache.js.map +1 -1
  151. package/dist/render/rendergraph/types.js.map +1 -1
  152. package/dist/render/shadow_mask_pass.js +73 -8
  153. package/dist/render/shadow_mask_pass.js.map +1 -1
  154. package/dist/render/sky.js +166 -126
  155. package/dist/render/sky.js.map +1 -1
  156. package/dist/render/weightedblended_oit.js +36 -35
  157. package/dist/render/weightedblended_oit.js.map +1 -1
  158. package/dist/scene/basesprite.js +8 -0
  159. package/dist/scene/basesprite.js.map +1 -1
  160. package/dist/scene/batchgroup.js +3 -0
  161. package/dist/scene/batchgroup.js.map +1 -1
  162. package/dist/scene/environment.js +4 -1
  163. package/dist/scene/environment.js.map +1 -1
  164. package/dist/scene/hair.js +963 -0
  165. package/dist/scene/hair.js.map +1 -0
  166. package/dist/scene/light.js +5 -1
  167. package/dist/scene/light.js.map +1 -1
  168. package/dist/scene/mesh.js +481 -47
  169. package/dist/scene/mesh.js.map +1 -1
  170. package/dist/scene/meshdrawable.js +6 -0
  171. package/dist/scene/meshdrawable.js.map +1 -1
  172. package/dist/scene/msdftext.js +3 -0
  173. package/dist/scene/msdftext.js.map +1 -1
  174. package/dist/scene/msdftextsprite.js +3 -0
  175. package/dist/scene/msdftextsprite.js.map +1 -1
  176. package/dist/scene/particlesys.js +6 -0
  177. package/dist/scene/particlesys.js.map +1 -1
  178. package/dist/scene/scene.js +3 -0
  179. package/dist/scene/scene.js.map +1 -1
  180. package/dist/scene/scene_node.js +6 -1
  181. package/dist/scene/scene_node.js.map +1 -1
  182. package/dist/scene/terrain-cm/grass.js +273 -249
  183. package/dist/scene/terrain-cm/grass.js.map +1 -1
  184. package/dist/scene/terrain-cm/grassmaterial.js +3 -0
  185. package/dist/scene/terrain-cm/grassmaterial.js.map +1 -1
  186. package/dist/scene/terrain-cm/terrain-cm.js +6 -0
  187. package/dist/scene/terrain-cm/terrain-cm.js.map +1 -1
  188. package/dist/scene/terrain-cm/texture_cache.js +309 -0
  189. package/dist/scene/terrain-cm/texture_cache.js.map +1 -0
  190. package/dist/scene/water.js +6 -0
  191. package/dist/scene/water.js.map +1 -1
  192. package/dist/shaders/atmosphere.js.map +1 -1
  193. package/dist/shaders/diffusion.js +54 -0
  194. package/dist/shaders/diffusion.js.map +1 -0
  195. package/dist/shaders/shadow.js +33 -21
  196. package/dist/shaders/shadow.js.map +1 -1
  197. package/dist/shaders/temporal.js +71 -33
  198. package/dist/shaders/temporal.js.map +1 -1
  199. package/dist/shadow/dom.js +546 -0
  200. package/dist/shadow/dom.js.map +1 -0
  201. package/dist/shadow/pcf_opt.js +3 -5
  202. package/dist/shadow/pcf_opt.js.map +1 -1
  203. package/dist/shadow/pcf_pd.js +1 -1
  204. package/dist/shadow/pcf_pd.js.map +1 -1
  205. package/dist/shadow/receiver_bias.js +41 -0
  206. package/dist/shadow/receiver_bias.js.map +1 -0
  207. package/dist/shadow/shader.js +20 -60
  208. package/dist/shadow/shader.js.map +1 -1
  209. package/dist/shadow/shadow_impl.js +89 -0
  210. package/dist/shadow/shadow_impl.js.map +1 -1
  211. package/dist/shadow/shadow_region.js +50 -50
  212. package/dist/shadow/shadow_region.js.map +1 -1
  213. package/dist/shadow/shadowmapper.js +150 -37
  214. package/dist/shadow/shadowmapper.js.map +1 -1
  215. package/dist/shapes/shape.js +11 -2
  216. package/dist/shapes/shape.js.map +1 -1
  217. package/dist/shapes/sphere.js +50 -9
  218. package/dist/shapes/sphere.js.map +1 -1
  219. package/dist/utility/blueprint/common/constants.js +112 -2
  220. package/dist/utility/blueprint/common/constants.js.map +1 -1
  221. package/dist/utility/blueprint/material/func.js +142 -49
  222. package/dist/utility/blueprint/material/func.js.map +1 -1
  223. package/dist/utility/blueprint/material/ir.js +490 -25
  224. package/dist/utility/blueprint/material/ir.js.map +1 -1
  225. package/dist/utility/blueprint/material/texture.js +898 -4
  226. package/dist/utility/blueprint/material/texture.js.map +1 -1
  227. package/dist/utility/serialization/manager.js +431 -100
  228. package/dist/utility/serialization/manager.js.map +1 -1
  229. package/dist/utility/serialization/scene/animation.js +370 -3
  230. package/dist/utility/serialization/scene/animation.js.map +1 -1
  231. package/dist/utility/serialization/scene/batch.js +4 -0
  232. package/dist/utility/serialization/scene/batch.js.map +1 -1
  233. package/dist/utility/serialization/scene/camera.js +133 -1
  234. package/dist/utility/serialization/scene/camera.js.map +1 -1
  235. package/dist/utility/serialization/scene/common.js +26 -5
  236. package/dist/utility/serialization/scene/common.js.map +1 -1
  237. package/dist/utility/serialization/scene/hair.js +1141 -0
  238. package/dist/utility/serialization/scene/hair.js.map +1 -0
  239. package/dist/utility/serialization/scene/light.js +84 -4
  240. package/dist/utility/serialization/scene/light.js.map +1 -1
  241. package/dist/utility/serialization/scene/material.js +1867 -166
  242. package/dist/utility/serialization/scene/material.js.map +1 -1
  243. package/dist/utility/serialization/scene/mesh.js +231 -14
  244. package/dist/utility/serialization/scene/mesh.js.map +1 -1
  245. package/dist/utility/serialization/scene/node.js +141 -7
  246. package/dist/utility/serialization/scene/node.js.map +1 -1
  247. package/dist/utility/serialization/scene/particle.js +4 -0
  248. package/dist/utility/serialization/scene/particle.js.map +1 -1
  249. package/dist/utility/serialization/scene/primitive.js +18 -0
  250. package/dist/utility/serialization/scene/primitive.js.map +1 -1
  251. package/dist/utility/serialization/scene/sprite.js +4 -0
  252. package/dist/utility/serialization/scene/sprite.js.map +1 -1
  253. package/dist/utility/serialization/scene/terrain.js +92 -69
  254. package/dist/utility/serialization/scene/terrain.js.map +1 -1
  255. package/dist/utility/serialization/scene/text.js +4 -0
  256. package/dist/utility/serialization/scene/text.js.map +1 -1
  257. package/dist/utility/serialization/scene/water.js +4 -0
  258. package/dist/utility/serialization/scene/water.js.map +1 -1
  259. package/dist/utility/serialization/types.js.map +1 -1
  260. package/dist/utility/shprojector.js +96 -95
  261. package/dist/utility/shprojector.js.map +1 -1
  262. package/dist/values.js +105 -58
  263. package/dist/values.js.map +1 -1
  264. package/package.json +3 -3
@@ -0,0 +1,963 @@
1
+ import { applyMixins, DRef, Vector3 } from '@zephyr3d/base';
2
+ import { GraphNode } from './graph_node.js';
3
+ import { mixinDrawable } from '../render/drawable_mixin.js';
4
+ import { QUEUE_OPAQUE } from '../values.js';
5
+ import '../render/render_queue.js';
6
+ import { getEngine } from '../app/api.js';
7
+ import { Primitive } from '../render/primitive.js';
8
+ import '@zephyr3d/device';
9
+ import '../material/shader/helper.js';
10
+ import '../material/lambert.js';
11
+ import '../material/blinn.js';
12
+ import '../material/unlit.js';
13
+ import '../material/particle.js';
14
+ import '../material/subsurfaceprofile.js';
15
+ import '../material/skin.js';
16
+ import '../material/eye.js';
17
+ import '../material/meshmaterial.js';
18
+ import '../material/hair.js';
19
+ import { HairStrandMaterial } from '../material/hairstrand.js';
20
+ import { HairStrandData } from '../material/hairstrand_data.js';
21
+ import '../material/grassmaterial.js';
22
+ import '../material/terrain-cm.js';
23
+ import '../material/pbrmr.js';
24
+ import '../material/pbrsg.js';
25
+ import '../material/mtoon.js';
26
+ import '../material/pbrblueprint.js';
27
+ import '../material/sprite.js';
28
+ import '../material/sprite_std.js';
29
+ import '../utility/blueprint/material/texture.js';
30
+ import '../render/rendergraph/forward_plus_builder.js';
31
+ import '../render/lightpass.js';
32
+ import '../render/sky.js';
33
+ import '../render/clipmap.js';
34
+ import '../render/envlight.js';
35
+ import '../render/dualdepthpeeling_oit.js';
36
+ import '../shaders/atmosphere.js';
37
+ import '../render/rendergraph/scene_render_context.js';
38
+ import { isHairSimulationSupported, GPUHairSimulation } from '../animation/hair/gpu_hair_simulation.js';
39
+ import { parseZHair } from '../asset/loaders/zhair/zhair_format.js';
40
+ import { mergeHairStrandSources, loadZHairStrandSources } from '../asset/loaders/zhair/zhair_loader.js';
41
+ import { BoundingBox } from '../utility/bounding_volume.js';
42
+
43
+ /**
44
+ * Hair scene node.
45
+ *
46
+ * @remarks
47
+ * Point it at a `.zhair` asset with {@link HairNode.setHairAsset}, or hand it
48
+ * control points directly with {@link HairNode.setStrands}. Everything the draw
49
+ * call needs is derived from them.
50
+ *
51
+ * @public
52
+ */ class HairNode extends applyMixins(GraphNode, mixinDrawable) {
53
+ /** @internal */ _pickTarget;
54
+ /** @internal */ _material;
55
+ /** @internal */ _primitive;
56
+ /** @internal */ _strands;
57
+ /** @internal Local-space bounds of the control points, width included. */ _bounds;
58
+ /** @internal Length of the longest strand, for padding the bounds while simulating. */ _maxStrandLength;
59
+ /** @internal */ _castShadow;
60
+ /** @internal Kept so the simulation can be rebuilt from the authored pose. */ _source;
61
+ /** @internal Path of the `.zhair` asset, empty when strands were set directly. */ _hairAsset;
62
+ /**
63
+ * @internal The opened asset, kept so decimation can be changed without
64
+ * reading the file again.
65
+ */ _parsedAsset;
66
+ /** @internal */ _strandStride;
67
+ /** @internal */ _maxStrands;
68
+ /** @internal */ _simulation;
69
+ /** @internal */ _simulationEnabled;
70
+ /**
71
+ * @internal Simulation tuning, owned by the node.
72
+ *
73
+ * Held here rather than on the simulation because the node outlives it: the
74
+ * dials survive the simulation being switched off and on, and can be set
75
+ * before any strands have loaded.
76
+ */ _simulationOptions;
77
+ /**
78
+ * Creates an empty hair node.
79
+ *
80
+ * @param scene - Scene the node belongs to.
81
+ */ constructor(scene){
82
+ super(scene);
83
+ this._pickTarget = {
84
+ node: this
85
+ };
86
+ this._material = new DRef(new HairStrandMaterial());
87
+ this._primitive = new DRef();
88
+ this._strands = new DRef();
89
+ this._bounds = null;
90
+ this._maxStrandLength = 0;
91
+ this._castShadow = true;
92
+ this._source = null;
93
+ this._hairAsset = '';
94
+ this._parsedAsset = null;
95
+ this._strandStride = 1;
96
+ this._maxStrands = 0;
97
+ this._simulation = null;
98
+ this._simulationEnabled = false;
99
+ this._simulationOptions = {
100
+ gravity: new Vector3(0, -9.8, 0),
101
+ damping: 0.08,
102
+ globalStiffness: 0,
103
+ globalRange: 0,
104
+ localStiffness: 0.9,
105
+ localIterations: 2,
106
+ ftlDamping: 0.7,
107
+ vspCoeff: 0.8,
108
+ vspAccelThreshold: 50,
109
+ substeps: 1,
110
+ friction: 0.2,
111
+ maxSpeedFactor: 4
112
+ };
113
+ }
114
+ //
115
+ // Geometry
116
+ //
117
+ /** Path of the `.zhair` asset this node draws, or an empty string. */ get hairAsset() {
118
+ return this._hairAsset;
119
+ }
120
+ /**
121
+ * Loads a `.zhair` asset.
122
+ *
123
+ * @remarks
124
+ * The opened file is kept, so changing {@link HairNode.strandStride} or
125
+ * {@link HairNode.maxStrands} afterwards re-decimates what is already in
126
+ * memory instead of reading it again - and never touches the source archive
127
+ * the asset was converted from.
128
+ *
129
+ * @param path - Asset path, or an empty string to clear the node.
130
+ */ async setHairAsset(path) {
131
+ const assetId = path?.trim() ?? '';
132
+ if (assetId === this._hairAsset) {
133
+ return;
134
+ }
135
+ this._hairAsset = assetId;
136
+ this._parsedAsset = null;
137
+ if (!assetId) {
138
+ this.setStrands(null);
139
+ return;
140
+ }
141
+ let parsed;
142
+ try {
143
+ const data = await getEngine().resourceManager.assetManager.fetchBinaryData(assetId);
144
+ parsed = parseZHair(data);
145
+ } catch (err) {
146
+ console.error(`Load hair asset failed: ${assetId}: ${err}`);
147
+ this.setStrands(null);
148
+ return;
149
+ }
150
+ // A load that was superseded while it was in flight must not overwrite the
151
+ // asset that replaced it.
152
+ if (this._hairAsset !== assetId) {
153
+ return;
154
+ }
155
+ this._parsedAsset = parsed;
156
+ this._applyAssetDecimation();
157
+ }
158
+ /**
159
+ * Draw one strand in every `strandStride`.
160
+ *
161
+ * @remarks
162
+ * A stride rather than a leading slice, so the kept strands stay spread over
163
+ * the whole scalp instead of leaving a bald patch. Only meaningful when the
164
+ * strands came from an asset; strands set directly are used as given.
165
+ */ get strandStride() {
166
+ return this._strandStride;
167
+ }
168
+ set strandStride(val) {
169
+ const v = val < 1 ? 1 : val | 0;
170
+ if (v !== this._strandStride) {
171
+ this._strandStride = v;
172
+ this._applyAssetDecimation();
173
+ }
174
+ }
175
+ /**
176
+ * Upper bound on the number of strands drawn, or 0 for no bound.
177
+ *
178
+ * @remarks
179
+ * Applied on top of {@link HairNode.strandStride} by widening the stride
180
+ * further, so the result stays evenly spread.
181
+ */ get maxStrands() {
182
+ return this._maxStrands;
183
+ }
184
+ set maxStrands(val) {
185
+ const v = val < 0 ? 0 : val | 0;
186
+ if (v !== this._maxStrands) {
187
+ this._maxStrands = v;
188
+ this._applyAssetDecimation();
189
+ }
190
+ }
191
+ /** GPU-resident strand geometry, or null when none has been assigned. */ get strands() {
192
+ return this._strands.get();
193
+ }
194
+ /** Number of strands currently drawn. */ get strandCount() {
195
+ return this._strands.get()?.strandCount ?? 0;
196
+ }
197
+ /**
198
+ * Replaces the strands this node draws.
199
+ *
200
+ * @remarks
201
+ * The direct route, for strands built in code rather than loaded from an
202
+ * asset. Uploads the control points, sizes the draw call and recomputes the
203
+ * bounding box; the previous strand data is released and any running
204
+ * simulation is rebuilt, since its rest pose comes from the strands.
205
+ *
206
+ * @param source - Control points and topology, or null to clear the node.
207
+ */ setStrands(source) {
208
+ this._disposeSimulation();
209
+ this._strands.dispose();
210
+ this._bounds = null;
211
+ this._maxStrandLength = 0;
212
+ this._source = null;
213
+ if (!source) {
214
+ this.material.strands = null;
215
+ this._primitive.dispose();
216
+ this.invalidateBoundingVolume();
217
+ return;
218
+ }
219
+ const data = new HairStrandData(source);
220
+ this._strands.set(data);
221
+ this._source = source;
222
+ this.material.strands = data;
223
+ const extents = computeStrandExtents(source);
224
+ this._bounds = extents.bounds;
225
+ this._maxStrandLength = extents.maxStrandLength;
226
+ this._ensurePrimitive();
227
+ this._syncDrawRange();
228
+ this.invalidateBoundingVolume();
229
+ if (this._simulationEnabled) {
230
+ this._createSimulation();
231
+ }
232
+ }
233
+ //
234
+ // Shading
235
+ //
236
+ // The material is an implementation detail of this node - see the class
237
+ // remarks - so its controls are surfaced here. Only the ones that do something
238
+ // on the strand path are: the vertex shader emits no texture coordinates, so
239
+ // the material's albedo, shift and occlusion maps have nothing to sample and
240
+ // are deliberately left unreachable.
241
+ //
242
+ /** Base colour of the strands. */ get albedoColor() {
243
+ return this.material.albedoColor;
244
+ }
245
+ set albedoColor(value) {
246
+ this.material.albedoColor = value;
247
+ }
248
+ /**
249
+ * Which scattering model shades the hair.
250
+ *
251
+ * @remarks
252
+ * `kajiya-kay` is the art-directed double lobe; `marschner` derives the lobes
253
+ * from the fibre and is the one that renders back-lighting.
254
+ */ get shadingModel() {
255
+ return this.material.shadingModel;
256
+ }
257
+ set shadingModel(value) {
258
+ this.material.shadingModel = value;
259
+ }
260
+ /** Colour of the primary, near-white specular lobe. */ get specular1Color() {
261
+ return this.material.specular1Color;
262
+ }
263
+ set specular1Color(value) {
264
+ this.material.specular1Color = value;
265
+ }
266
+ /** Exponent of the primary specular lobe. */ get specular1Power() {
267
+ return this.material.specular1Power;
268
+ }
269
+ set specular1Power(value) {
270
+ this.material.specular1Power = value;
271
+ }
272
+ /** Shift of the primary specular lobe along the strand. */ get specular1Shift() {
273
+ return this.material.specular1Shift;
274
+ }
275
+ set specular1Shift(value) {
276
+ this.material.specular1Shift = value;
277
+ }
278
+ /** Colour of the secondary, tinted specular lobe. */ get specular2Color() {
279
+ return this.material.specular2Color;
280
+ }
281
+ set specular2Color(value) {
282
+ this.material.specular2Color = value;
283
+ }
284
+ /** Exponent of the secondary specular lobe. */ get specular2Power() {
285
+ return this.material.specular2Power;
286
+ }
287
+ set specular2Power(value) {
288
+ this.material.specular2Power = value;
289
+ }
290
+ /** Shift of the secondary specular lobe along the strand. */ get specular2Shift() {
291
+ return this.material.specular2Shift;
292
+ }
293
+ set specular2Shift(value) {
294
+ this.material.specular2Shift = value;
295
+ }
296
+ /** How far diffuse lighting wraps around the fibre. */ get diffuseWrap() {
297
+ return this.material.diffuseWrap;
298
+ }
299
+ set diffuseWrap(value) {
300
+ this.material.diffuseWrap = value;
301
+ }
302
+ /** Colour of light transmitted through the fibre. */ get transmissionColor() {
303
+ return this.material.transmissionColor;
304
+ }
305
+ set transmissionColor(value) {
306
+ this.material.transmissionColor = value;
307
+ }
308
+ /** Strength of back-lit transmission. */ get transmissionIntensity() {
309
+ return this.material.transmissionIntensity;
310
+ }
311
+ set transmissionIntensity(value) {
312
+ this.material.transmissionIntensity = value;
313
+ }
314
+ /** Falloff exponent of back-lit transmission. */ get transmissionPower() {
315
+ return this.material.transmissionPower;
316
+ }
317
+ set transmissionPower(value) {
318
+ this.material.transmissionPower = value;
319
+ }
320
+ /** Colour of multiple scattering within the groom. */ get scatterColor() {
321
+ return this.material.scatterColor;
322
+ }
323
+ set scatterColor(value) {
324
+ this.material.scatterColor = value;
325
+ }
326
+ /** Strength of multiple scattering. */ get scatterIntensity() {
327
+ return this.material.scatterIntensity;
328
+ }
329
+ set scatterIntensity(value) {
330
+ this.material.scatterIntensity = value;
331
+ }
332
+ /** Share of scattering taken as local rather than global. */ get scatterLocal() {
333
+ return this.material.scatterLocal;
334
+ }
335
+ set scatterLocal(value) {
336
+ this.material.scatterLocal = value;
337
+ }
338
+ /** How far scattered light wraps around the fibre. */ get scatterWrap() {
339
+ return this.material.scatterWrap;
340
+ }
341
+ set scatterWrap(value) {
342
+ this.material.scatterWrap = value;
343
+ }
344
+ /** Longitudinal shift of the Marschner lobes. */ get marschnerShift() {
345
+ return this.material.marschnerShift;
346
+ }
347
+ set marschnerShift(value) {
348
+ this.material.marschnerShift = value;
349
+ }
350
+ /** Longitudinal roughness of the Marschner lobes. */ get marschnerRoughness() {
351
+ return this.material.marschnerRoughness;
352
+ }
353
+ set marschnerRoughness(value) {
354
+ this.material.marschnerRoughness = value;
355
+ }
356
+ /** Index of refraction of the fibre. */ get marschnerIOR() {
357
+ return this.material.marschnerIOR;
358
+ }
359
+ set marschnerIOR(value) {
360
+ this.material.marschnerIOR = value;
361
+ }
362
+ /**
363
+ * How strongly pigmented the fibre is.
364
+ *
365
+ * @remarks
366
+ * A multiplier on the distance transmitted light travels before leaving, so
367
+ * raising it deepens the colour of the TT and TRT paths without touching the
368
+ * white surface reflection.
369
+ */ get marschnerAbsorption() {
370
+ return this.material.marschnerAbsorption;
371
+ }
372
+ set marschnerAbsorption(value) {
373
+ this.material.marschnerAbsorption = value;
374
+ }
375
+ /** Relative weight of the R, TT and TRT lobes. */ get marschnerLobes() {
376
+ return this.material.marschnerLobes;
377
+ }
378
+ set marschnerLobes(value) {
379
+ this.material.marschnerLobes = value;
380
+ }
381
+ //
382
+ // Strand expansion
383
+ //
384
+ /**
385
+ * Ribbon segments generated per strand.
386
+ *
387
+ * @remarks
388
+ * The main quality dial, and independent of how many control points a strand
389
+ * has: the shader resamples the curve, so the same 30-point strand can be
390
+ * drawn with 12 segments close up and 3 far away.
391
+ */ get segmentsPerStrand() {
392
+ return this.material.segmentsPerStrand;
393
+ }
394
+ set segmentsPerStrand(val) {
395
+ if (val !== this.material.segmentsPerStrand) {
396
+ this.material.segmentsPerStrand = val;
397
+ this._syncDrawRange();
398
+ }
399
+ }
400
+ /** Multiplier on the width stored in the strand data. */ get strandWidthScale() {
401
+ return this.material.strandWidthScale;
402
+ }
403
+ set strandWidthScale(value) {
404
+ this.material.strandWidthScale = value;
405
+ }
406
+ /** Lower bound on ribbon width, in world units. */ get minStrandWidth() {
407
+ return this.material.minStrandWidth;
408
+ }
409
+ set minStrandWidth(value) {
410
+ this.material.minStrandWidth = value;
411
+ }
412
+ /**
413
+ * Lower bound on ribbon width, in pixels.
414
+ *
415
+ * @remarks
416
+ * Real hair is thinner than a pixel and rasterises to a flickering dotted
417
+ * line. Widening it to about a pixel and paying the extra coverage back in
418
+ * alpha keeps distant hair fading rather than breaking up.
419
+ */ get minPixelWidth() {
420
+ return this.material.minPixelWidth;
421
+ }
422
+ set minPixelWidth(value) {
423
+ this.material.minPixelWidth = value;
424
+ }
425
+ /**
426
+ * How far the shading normal bends across the ribbon, in [0, 1].
427
+ *
428
+ * @remarks
429
+ * At 0 a strand shades as a flat tape, at 1 as a cylinder.
430
+ */ get strandRoundness() {
431
+ return this.material.strandRoundness;
432
+ }
433
+ set strandRoundness(value) {
434
+ this.material.strandRoundness = value;
435
+ }
436
+ /**
437
+ * Strength of the root-depth ambient occlusion, in [0, 1].
438
+ *
439
+ * @remarks
440
+ * Environment irradiance is not attenuated by anything - a shadow map answers
441
+ * for one light, not for the sky - so without this every strand of a groom
442
+ * receives the full sky and a dense hairstyle is lit as brightly in its
443
+ * interior as on its surface, which reads as a flat silhouette with no volume.
444
+ * Most obvious on pale hair, where there is little pigment to darken the
445
+ * interior instead. Set to 0 to disable.
446
+ *
447
+ * Ambient light only: direct light is already attenuated by the shadow map,
448
+ * and under DOM by a graded transmittance, so it is left alone.
449
+ */ get rootOcclusion() {
450
+ return this.material.rootOcclusion;
451
+ }
452
+ set rootOcclusion(value) {
453
+ this.material.rootOcclusion = value;
454
+ }
455
+ /**
456
+ * How far along a strand the root occlusion reaches, in (0, 1].
457
+ *
458
+ * @remarks
459
+ * Short for a groom whose strands leave the scalp quickly, longer for one
460
+ * where they lie against each other most of their length.
461
+ */ get rootOcclusionRange() {
462
+ return this.material.rootOcclusionRange;
463
+ }
464
+ set rootOcclusionRange(value) {
465
+ this.material.rootOcclusionRange = value;
466
+ }
467
+ /** Drop strands with distance, redistributing their coverage to the survivors. */ get strandLOD() {
468
+ return this.material.strandLOD;
469
+ }
470
+ set strandLOD(value) {
471
+ this.material.strandLOD = value;
472
+ }
473
+ /** Floor on the fraction of strands distance decimation keeps. */ get minStrandLODRatio() {
474
+ return this.material.minStrandLODRatio;
475
+ }
476
+ set minStrandLODRatio(value) {
477
+ this.material.minStrandLODRatio = value;
478
+ }
479
+ //
480
+ // Transparency and shadows
481
+ //
482
+ /**
483
+ * How the strands blend with what is behind them.
484
+ *
485
+ * @remarks
486
+ * The pixel-width floor pays its widening back in alpha, which only means
487
+ * anything once blending is on; left at `none` every strand covers its pixel
488
+ * fully and a dense groom reads as a solid shell.
489
+ */ get blendMode() {
490
+ return this.material.blendMode;
491
+ }
492
+ set blendMode(value) {
493
+ this.material.blendMode = value;
494
+ }
495
+ /** Alpha below which a fragment is discarded. */ get alphaCutoff() {
496
+ return this.material.alphaCutoff;
497
+ }
498
+ set alphaCutoff(value) {
499
+ this.material.alphaCutoff = value;
500
+ }
501
+ /** Dither the alpha test, so a temporal filter resolves the soft edge. */ get alphaDither() {
502
+ return this.material.alphaDither;
503
+ }
504
+ set alphaDither(value) {
505
+ this.material.alphaDither = value;
506
+ }
507
+ /**
508
+ * Whether a blended groom contributes motion vectors.
509
+ *
510
+ * @remarks
511
+ * Only meaningful with {@link HairNode.blendMode} set to something other than
512
+ * `none`. Blended geometry is otherwise absent from the prepass, so a groom
513
+ * carries the velocity of whatever is behind it and a temporal filter smears
514
+ * it as it swings - the same ghosting the dithered path avoids by being
515
+ * opaque. On by default, because a groom is dense enough to own its pixels
516
+ * and there is no reason to want the ghosting.
517
+ */ get transparentMotionVector() {
518
+ return this.material.transparentMotionVector;
519
+ }
520
+ set transparentMotionVector(value) {
521
+ this.material.transparentMotionVector = value;
522
+ }
523
+ /** Whether the groom is drawn into shadow maps. */ get castShadow() {
524
+ return this._castShadow;
525
+ }
526
+ set castShadow(val) {
527
+ this._castShadow = !!val;
528
+ }
529
+ /** Let the shadow caster pass respect strand alpha, for softer self-shadowing. */ get transparentShadowCaster() {
530
+ return this.material.transparentShadowCaster;
531
+ }
532
+ set transparentShadowCaster(value) {
533
+ this.material.transparentShadowCaster = value;
534
+ }
535
+ /** Alpha below which a fragment casts no shadow. */ get shadowAlphaCutoff() {
536
+ return this.material.shadowAlphaCutoff;
537
+ }
538
+ set shadowAlphaCutoff(value) {
539
+ this.material.shadowAlphaCutoff = value;
540
+ }
541
+ //
542
+ // Simulation
543
+ //
544
+ /**
545
+ * Whether strand dynamics run on this groom.
546
+ *
547
+ * @remarks
548
+ * WebGPU only. Switching it on with no strands loaded is allowed - the
549
+ * simulation is built when the strands arrive.
550
+ */ get simulationEnabled() {
551
+ return this._simulationEnabled;
552
+ }
553
+ set simulationEnabled(val) {
554
+ const enabled = !!val;
555
+ if (enabled === this._simulationEnabled) {
556
+ return;
557
+ }
558
+ this._simulationEnabled = enabled;
559
+ if (enabled) {
560
+ this._createSimulation();
561
+ } else {
562
+ this._disposeSimulation();
563
+ // Without this the groom keeps whatever pose it was last simulated into,
564
+ // which reads as a bug rather than as hair that stopped moving.
565
+ if (this._source) {
566
+ this._strands.get()?.dispose();
567
+ this._strands.set(new HairStrandData(this._source));
568
+ this.material.strands = this._strands.get();
569
+ }
570
+ // Back to the tight rest bounds.
571
+ this.invalidateBoundingVolume();
572
+ }
573
+ }
574
+ /**
575
+ * The running strand dynamics, or null when they are off or unsupported.
576
+ *
577
+ * @remarks
578
+ * Exposed so a caller can reach past the node's own dials - to set colliders,
579
+ * for one, which have no serialised form here.
580
+ */ get simulation() {
581
+ return this._simulation;
582
+ }
583
+ /** World-space gravity acting on the strands. */ get gravity() {
584
+ return this._simulationOptions.gravity;
585
+ }
586
+ set gravity(value) {
587
+ this._simulationOptions.gravity = new Vector3(value);
588
+ if (this._simulation) {
589
+ this._simulation.gravity = this._simulationOptions.gravity;
590
+ }
591
+ }
592
+ /**
593
+ * How strongly strands keep the shape they were authored with, in [0, 1].
594
+ *
595
+ * @remarks
596
+ * The dial to reach for first. It holds the angle each segment makes with the
597
+ * one before it, measured against that segment's current direction rather
598
+ * than a fixed pose, so a groom keeps its curl while remaining free to hang
599
+ * and swing. It is also what damps a groom: without bending resistance a
600
+ * strand carries a disturbance up and down its length for seconds.
601
+ */ get localStiffness() {
602
+ return this._simulationOptions.localStiffness;
603
+ }
604
+ set localStiffness(value) {
605
+ this._simulationOptions.localStiffness = value;
606
+ if (this._simulation) {
607
+ this._simulation.localStiffness = value;
608
+ }
609
+ }
610
+ /** Local shape constraint passes per substep. */ get localIterations() {
611
+ return this._simulationOptions.localIterations;
612
+ }
613
+ set localIterations(value) {
614
+ this._simulationOptions.localIterations = value;
615
+ if (this._simulation) {
616
+ this._simulation.localIterations = value;
617
+ }
618
+ }
619
+ /**
620
+ * How strongly strands are pulled back to their authored pose, in [0, 1].
621
+ *
622
+ * @remarks
623
+ * Off by default. It anchors a point to a fixed place rather than to its
624
+ * neighbours, which is a spring, and a groom held by springs reads as one.
625
+ * {@link HairNode.localStiffness} is what holds styling without that; this is
626
+ * for grooms that genuinely should not move far from how they were authored,
627
+ * and wants {@link HairNode.globalRange} set as well.
628
+ */ get globalStiffness() {
629
+ return this._simulationOptions.globalStiffness;
630
+ }
631
+ set globalStiffness(value) {
632
+ this._simulationOptions.globalStiffness = value;
633
+ if (this._simulation) {
634
+ this._simulation.globalStiffness = value;
635
+ }
636
+ }
637
+ /** Fraction of a strand, from the root, the pose pull acts on, in [0, 1]. */ get globalRange() {
638
+ return this._simulationOptions.globalRange;
639
+ }
640
+ set globalRange(value) {
641
+ this._simulationOptions.globalRange = value;
642
+ if (this._simulation) {
643
+ this._simulation.globalRange = value;
644
+ }
645
+ }
646
+ /**
647
+ * Share of a point's length correction fed back into its parent, in [0, 1].
648
+ *
649
+ * @remarks
650
+ * What keeps the solver from gaining energy as the node moves. Lowering it
651
+ * makes a groom livelier and, past a point, makes it whip; 0 removes the
652
+ * feedback entirely.
653
+ */ get ftlDamping() {
654
+ return this._simulationOptions.ftlDamping;
655
+ }
656
+ set ftlDamping(value) {
657
+ this._simulationOptions.ftlDamping = value;
658
+ if (this._simulation) {
659
+ this._simulation.ftlDamping = value;
660
+ }
661
+ }
662
+ /** Velocity lost each step, in [0, 1]. */ get damping() {
663
+ return this._simulationOptions.damping;
664
+ }
665
+ set damping(value) {
666
+ this._simulationOptions.damping = value;
667
+ if (this._simulation) {
668
+ this._simulation.damping = value;
669
+ }
670
+ }
671
+ /** Tangential motion removed at a contact, in [0, 1]. */ get friction() {
672
+ return this._simulationOptions.friction;
673
+ }
674
+ set friction(value) {
675
+ this._simulationOptions.friction = value;
676
+ if (this._simulation) {
677
+ this._simulation.friction = value;
678
+ }
679
+ }
680
+ /**
681
+ * How much of the node's motion strands are carried along by, in [0, 1].
682
+ *
683
+ * @remarks
684
+ * 0 leaves a groom exactly where it was in the world and lets the pinned
685
+ * roots drag it along, which is what produces lag; 1 carries it rigidly with
686
+ * the node, so it never lags. Lower values give a livelier, floppier groom.
687
+ */ get vspCoeff() {
688
+ return this._simulationOptions.vspCoeff;
689
+ }
690
+ set vspCoeff(value) {
691
+ this._simulationOptions.vspCoeff = value;
692
+ if (this._simulation) {
693
+ this._simulation.vspCoeff = value;
694
+ }
695
+ }
696
+ /** Node acceleration above which strands are carried rigidly, in units/s². */ get vspAccelThreshold() {
697
+ return this._simulationOptions.vspAccelThreshold;
698
+ }
699
+ set vspAccelThreshold(value) {
700
+ this._simulationOptions.vspAccelThreshold = value;
701
+ if (this._simulation) {
702
+ this._simulation.vspAccelThreshold = value;
703
+ }
704
+ }
705
+ /**
706
+ * Per-substep ceiling on how far a point may travel, in segment lengths.
707
+ *
708
+ * @remarks
709
+ * A backstop against a single bad substep, not a dial to shape motion with:
710
+ * at the default it never engages during ordinary movement.
711
+ */ get maxSpeedFactor() {
712
+ return this._simulationOptions.maxSpeedFactor;
713
+ }
714
+ set maxSpeedFactor(value) {
715
+ this._simulationOptions.maxSpeedFactor = value;
716
+ if (this._simulation) {
717
+ this._simulation.maxSpeedFactor = value;
718
+ }
719
+ }
720
+ /** Integration substeps per fixed step. */ get substeps() {
721
+ return this._simulationOptions.substeps;
722
+ }
723
+ set substeps(value) {
724
+ this._simulationOptions.substeps = value;
725
+ if (this._simulation) {
726
+ this._simulation.substeps = value;
727
+ }
728
+ }
729
+ //
730
+ // Scene node and drawable
731
+ //
732
+ /**
733
+ * {@inheritDoc SceneNode.update}
734
+ *
735
+ * @remarks
736
+ * Drives the strand solver. The node re-queues itself every frame, because a
737
+ * simulation only stands still when it is switched off.
738
+ */ update(frameId, elapsedInSeconds, deltaInSeconds) {
739
+ super.update(frameId, elapsedInSeconds, deltaInSeconds);
740
+ const simulation = this._simulation;
741
+ if (simulation?.enabled) {
742
+ simulation.update(deltaInSeconds, this.worldMatrix);
743
+ this.scene?.queueUpdateNode(this);
744
+ }
745
+ }
746
+ /**
747
+ * {@inheritDoc SceneNode.computeBoundingVolume}
748
+ *
749
+ * @remarks
750
+ * Derived on the CPU from the control points, because the ribbons are built in
751
+ * the vertex shader and the engine never sees them. The box is in local space;
752
+ * the base class applies the world matrix.
753
+ *
754
+ * While a simulation runs, the strands swing out of their authored pose, so
755
+ * the rest bounds are padded by the longest strand: the solver keeps every
756
+ * point within its strand's length of the pinned root, which bounds any pose
757
+ * it can produce.
758
+ */ computeBoundingVolume() {
759
+ if (!this._bounds) {
760
+ return null;
761
+ }
762
+ const box = new BoundingBox(this._bounds);
763
+ if (this._simulation) {
764
+ const pad = this._maxStrandLength;
765
+ box.minPoint.setXYZ(box.minPoint.x - pad, box.minPoint.y - pad, box.minPoint.z - pad);
766
+ box.maxPoint.setXYZ(box.maxPoint.x + pad, box.maxPoint.y + pad, box.maxPoint.z + pad);
767
+ }
768
+ return box;
769
+ }
770
+ /** {@inheritDoc SceneNode.isHair} */ isHair() {
771
+ return true;
772
+ }
773
+ /** {@inheritDoc Drawable.getPickTarget} */ getPickTarget() {
774
+ return this._pickTarget;
775
+ }
776
+ /** {@inheritDoc Drawable.getMaterial} */ getMaterial() {
777
+ return this._material.get();
778
+ }
779
+ /** {@inheritDoc Drawable.getPrimitive} */ getPrimitive() {
780
+ return this._primitive.get();
781
+ }
782
+ /** {@inheritDoc Drawable.getMorphData} */ getMorphData() {
783
+ return null;
784
+ }
785
+ /** {@inheritDoc Drawable.getMorphInfo} */ getMorphInfo() {
786
+ return null;
787
+ }
788
+ /** {@inheritDoc Drawable.getSkinInfluenceData} */ getSkinInfluenceData() {
789
+ return null;
790
+ }
791
+ /** {@inheritDoc Drawable.getQueueType} */ getQueueType() {
792
+ return this._material.get()?.getQueueType() ?? QUEUE_OPAQUE;
793
+ }
794
+ /** {@inheritDoc Drawable.isUnlit} */ isUnlit() {
795
+ return !this._material.get()?.supportLighting();
796
+ }
797
+ /** {@inheritDoc Drawable.needSceneColor} */ needSceneColor() {
798
+ return this._material.get()?.needSceneColor() ?? false;
799
+ }
800
+ /** {@inheritDoc Drawable.needSceneDepth} */ needSceneDepth() {
801
+ return this._material.get()?.needSceneDepth() ?? false;
802
+ }
803
+ /** {@inheritDoc Drawable.draw} */ draw(ctx, renderQueue) {
804
+ const material = this._material.get();
805
+ const primitive = this._primitive.get();
806
+ // An empty groom is a legitimate state - a node exists before its asset has
807
+ // finished loading - and the draw range is zero until strands arrive.
808
+ if (material && primitive && material.vertexCount > 0) {
809
+ this.bind(ctx, renderQueue);
810
+ material.draw(primitive, ctx);
811
+ }
812
+ }
813
+ onDispose() {
814
+ super.onDispose();
815
+ this._disposeSimulation();
816
+ this._primitive.dispose();
817
+ this._strands.dispose();
818
+ this._material.dispose();
819
+ this._parsedAsset = null;
820
+ this._source = null;
821
+ this._bounds = null;
822
+ }
823
+ //
824
+ // Internals
825
+ //
826
+ /**
827
+ * The material, which is an implementation detail of this node.
828
+ * @internal
829
+ */ get material() {
830
+ return this._material.get();
831
+ }
832
+ /**
833
+ * Re-decimates the opened asset into the strands actually drawn.
834
+ * @internal
835
+ */ _applyAssetDecimation() {
836
+ const parsed = this._parsedAsset;
837
+ if (!parsed) {
838
+ return;
839
+ }
840
+ // A `.zhair` holds one set per source curve object, and an XGen export
841
+ // routinely carries several spline descriptions for a single hairstyle;
842
+ // merging lets the whole head draw in one call.
843
+ this.setStrands(mergeHairStrandSources(loadZHairStrandSources(parsed, {
844
+ strandStride: this._strandStride,
845
+ maxStrands: this._maxStrands
846
+ })));
847
+ }
848
+ /**
849
+ * Starts strand dynamics over the current strands.
850
+ * @internal
851
+ */ _createSimulation() {
852
+ this._disposeSimulation();
853
+ const data = this._strands.get();
854
+ if (!data || !this._source || !isHairSimulationSupported()) {
855
+ return;
856
+ }
857
+ const simulation = new GPUHairSimulation(data, this._source, this._simulationOptions);
858
+ if (simulation.enabled) {
859
+ this._simulation = simulation;
860
+ // Motion vectors have to follow the control points once something is
861
+ // moving them, or a groom swinging under a still head reports no motion
862
+ // and a temporal filter smears it.
863
+ this.material.prevPoints = simulation.framePointBuffer;
864
+ // The bounds widen while a simulation runs - see computeBoundingVolume.
865
+ this.invalidateBoundingVolume();
866
+ this.scene?.queueUpdateNode(this);
867
+ } else {
868
+ console.warn(simulation.disabledReason ?? 'GPU hair simulation could not start');
869
+ simulation.dispose();
870
+ }
871
+ }
872
+ /** @internal */ _disposeSimulation() {
873
+ this.material.prevPoints = null;
874
+ this._simulation?.dispose();
875
+ this._simulation = null;
876
+ }
877
+ /**
878
+ * Builds the primitive the draw call is issued against.
879
+ *
880
+ * @remarks
881
+ * No vertex attribute is ever read - the vertex shader derives everything from
882
+ * the vertex index and the storage buffers - but a draw still needs a vertex
883
+ * layout to exist, and one float is the smallest buffer that establishes one.
884
+ * @internal
885
+ */ _ensurePrimitive() {
886
+ if (this._primitive.get()) {
887
+ return;
888
+ }
889
+ const primitive = new Primitive();
890
+ primitive.createAndSetVertexBuffer('position_f32x3', new Float32Array(3));
891
+ primitive.primitiveType = 'triangle-list';
892
+ this._primitive.set(primitive);
893
+ }
894
+ /**
895
+ * Points the draw range at however many vertices the material now needs.
896
+ *
897
+ * @remarks
898
+ * Drawing is non-indexed, six vertices per ribbon segment, so the count moves
899
+ * with both the strand count and {@link HairNode.segmentsPerStrand}.
900
+ * @internal
901
+ */ _syncDrawRange() {
902
+ const primitive = this._primitive.get();
903
+ if (primitive) {
904
+ primitive.indexCount = this.material.vertexCount;
905
+ }
906
+ }
907
+ }
908
+ /**
909
+ * Local-space bounds of a strand source, padded by the widest strand, along
910
+ * with the length of the longest strand.
911
+ *
912
+ * @remarks
913
+ * The ribbon is built around the control point, so half a width would suffice;
914
+ * a full width is used because {@link HairNode.minPixelWidth} widens sub-pixel
915
+ * strands beyond their authored size and the box has to survive that.
916
+ *
917
+ * The strand length is measured here because the same pass already touches
918
+ * every point; it pads the bounds while a simulation runs.
919
+ * @internal
920
+ */ function computeStrandExtents(source) {
921
+ const scale = source.scale ?? 1;
922
+ const positions = source.positions;
923
+ const box = new BoundingBox();
924
+ box.beginExtend();
925
+ let maxStrandLength = 0;
926
+ let first = 0;
927
+ for(let s = 0; s < source.pointCounts.length; s++){
928
+ const count = source.pointCounts[s];
929
+ let length = 0;
930
+ for(let i = 0; i < count; i++){
931
+ const p = (first + i) * 3;
932
+ const x = positions[p] * scale;
933
+ const y = positions[p + 1] * scale;
934
+ const z = positions[p + 2] * scale;
935
+ box.extend3(x, y, z);
936
+ if (i > 0) {
937
+ length += Math.hypot(x - positions[p - 3] * scale, y - positions[p - 2] * scale, z - positions[p - 1] * scale);
938
+ }
939
+ }
940
+ if (length > maxStrandLength) {
941
+ maxStrandLength = length;
942
+ }
943
+ first += count;
944
+ }
945
+ let widest = source.defaultWidth ?? 0.0001;
946
+ if (source.widths) {
947
+ for(let i = 0; i < source.widths.length; i++){
948
+ if (source.widths[i] > widest) {
949
+ widest = source.widths[i];
950
+ }
951
+ }
952
+ }
953
+ const pad = widest * scale * (source.widthScale ?? 1);
954
+ box.minPoint.setXYZ(box.minPoint.x - pad, box.minPoint.y - pad, box.minPoint.z - pad);
955
+ box.maxPoint.setXYZ(box.maxPoint.x + pad, box.maxPoint.y + pad, box.maxPoint.z + pad);
956
+ return {
957
+ bounds: box,
958
+ maxStrandLength
959
+ };
960
+ }
961
+
962
+ export { HairNode };
963
+ //# sourceMappingURL=hair.js.map