@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,1090 @@
1
+ import { Disposable, Vector3, Matrix4x4, Quaternion } from '@zephyr3d/base';
2
+ import { getDevice } from '../../app/api.js';
3
+ import { updateColliderFromNode } from '../spring/spring_collider.js';
4
+
5
+ /** Simulation step, in seconds. @internal */ const FIXED_SIMULATION_TIME_STEP = 1 / 60;
6
+ /** Largest backlog the accumulator will carry, in seconds. @internal */ const MAX_ACCUMULATED_SIMULATION_TIME = 1 / 20;
7
+ /** Threads per workgroup; one thread owns one strand. @internal */ const DEFAULT_WORKGROUP_SIZE = 64;
8
+ /** Length below which a direction is treated as degenerate. @internal */ const MIN_DISTANCE = 1e-7;
9
+ /** Floats per collider entry, matching the layouts the shader indexes. @internal */ const SPHERE_STRIDE = 4;
10
+ const CAPSULE_STRIDE = 8;
11
+ const PLANE_STRIDE = 8;
12
+ /** Colliders of one kind the buffers make room for. @internal */ const MAX_COLLIDERS = 16;
13
+ /** Solver defaults, from TressFXSimulationSettings. @internal */ const DEFAULT_DAMPING = 0.08;
14
+ const DEFAULT_GLOBAL_STIFFNESS = 0;
15
+ const DEFAULT_GLOBAL_RANGE = 0;
16
+ const DEFAULT_LOCAL_STIFFNESS = 0.9;
17
+ const DEFAULT_LOCAL_ITERATIONS = 2;
18
+ const DEFAULT_FTL_DAMPING = 0.7;
19
+ const DEFAULT_VSP_COEFF = 0.8;
20
+ /**
21
+ * Roughly five gravities. TressFX compares a per-frame second difference
22
+ * against 1.4 in its own units; as an acceleration it survives a unit change.
23
+ * @internal
24
+ */ const DEFAULT_VSP_ACCEL_THRESHOLD = 50;
25
+ /** Default per-substep speed ceiling, in segment lengths. @internal */ const DEFAULT_MAX_SPEED_FACTOR = 4;
26
+ /**
27
+ * Node motion beyond this many strand lengths in one frame is treated as a
28
+ * teleport and snaps the pose instead of swinging it.
29
+ * @internal
30
+ */ const TELEPORT_DISTANCE_FACTOR = 4;
31
+ /**
32
+ * Whether GPU hair dynamics can run on a device.
33
+ *
34
+ * @remarks
35
+ * WebGPU only: the solver is a compute pass. Strand rendering carries the same
36
+ * requirement, so there is no configuration where the hair draws but cannot
37
+ * simulate.
38
+ *
39
+ * @param device - Device to test, or the current one when omitted.
40
+ * @returns True when the simulation can run.
41
+ *
42
+ * @public
43
+ */ function isHairSimulationSupported(device) {
44
+ const resolved = device ?? getDevice();
45
+ return resolved?.type === 'webgpu';
46
+ }
47
+ function clamp(value, min, max) {
48
+ return value < min ? min : value > max ? max : value;
49
+ }
50
+ /** Exact element-wise matrix comparison, for skipping the interpolation. @internal */ function matrixEquals(a, b) {
51
+ for(let i = 0; i < 16; i++){
52
+ if (a[i] !== b[i]) {
53
+ return false;
54
+ }
55
+ }
56
+ return true;
57
+ }
58
+ /**
59
+ * Strand dynamics on the GPU.
60
+ *
61
+ * @remarks
62
+ * Created by {@link HairNode} when simulation is switched on; it writes the
63
+ * strand point buffer in place, so nothing about the draw call changes.
64
+ *
65
+ * @public
66
+ */ class GPUHairSimulation extends Disposable {
67
+ /** @internal */ _device;
68
+ /** @internal */ _enabled;
69
+ /** @internal */ _disabledReason;
70
+ /** @internal */ _program;
71
+ /** @internal */ _bindGroup;
72
+ /** @internal Live point positions; the same buffer the vertex shader reads. */ _pointBuffer;
73
+ /** @internal Previous positions, for Verlet integration and motion vectors. */ _prevPointBuffer;
74
+ /** @internal Authored positions, the target the pose pull reaches for. */ _restPointBuffer;
75
+ /**
76
+ * @internal Local positions as of the previously rendered frame, which is what
77
+ * a motion vector needs. Distinct from the Verlet history: that is one substep
78
+ * old, sits in the current frame's local space, and has the length, contact and
79
+ * friction corrections folded into it to encode velocity, so it is not a
80
+ * position anything was ever drawn at.
81
+ */ _framePointBuffer;
82
+ /** @internal Authored distance between consecutive control points. */ _restLengthBuffer;
83
+ /** @internal */ _sphereBuffer;
84
+ /** @internal */ _capsuleBuffer;
85
+ /** @internal */ _planeBuffer;
86
+ /** @internal */ _strandCount;
87
+ /** @internal */ _workgroupCount;
88
+ /** @internal */ _gravity;
89
+ /** @internal */ _damping;
90
+ /** @internal */ _globalStiffness;
91
+ /** @internal */ _globalRange;
92
+ /** @internal */ _localStiffness;
93
+ /** @internal */ _localIterations;
94
+ /** @internal */ _ftlDamping;
95
+ /** @internal */ _vspCoeff;
96
+ /** @internal */ _vspAccelThreshold;
97
+ /** @internal */ _substeps;
98
+ /** @internal */ _friction;
99
+ /** @internal */ _maxSpeedFactor;
100
+ /** @internal */ _colliders;
101
+ /** @internal */ _timeAccumulator;
102
+ /** @internal Scratch, so stepping allocates nothing. */ _sphereData;
103
+ _capsuleData;
104
+ _planeData;
105
+ _relativeTransform;
106
+ _invWorldMatrix;
107
+ _prevWorldMatrix;
108
+ /** @internal Bound while the node stands still, so those frames upload nothing new. */ _identity;
109
+ _localGravity;
110
+ _scratchVec;
111
+ _scratchVec2;
112
+ /** @internal Endpoints and scratch of the per-substep transform interpolation. */ _scaleFrom;
113
+ _scaleTo;
114
+ _translationFrom;
115
+ _translationTo;
116
+ _rotationFrom;
117
+ _rotationTo;
118
+ _rotationLerp;
119
+ _stepFrom;
120
+ _stepTo;
121
+ /** @internal True until the first step has seen a world matrix. */ _hasPrevWorldMatrix;
122
+ /** @internal Length of the longest strand, for the teleport threshold. */ _maxStrandLength;
123
+ /** @internal Centre of the rest pose, whose world motion detects teleports. */ _restCenter;
124
+ /**
125
+ * @internal Where the rest centre stood in the world last frame and how fast
126
+ * it was moving. The roots are pinned in local space, so their acceleration is
127
+ * the node's, which is what the shock propagation threshold tests.
128
+ */ _prevRootWorld;
129
+ _prevRootVelocity;
130
+ /** @internal Frames seen so far, capped at 2; acceleration needs three. */ _rootHistoryDepth;
131
+ /**
132
+ * Creates a simulation over an uploaded strand set.
133
+ *
134
+ * @param strands - GPU strand data; its point buffer is written in place.
135
+ * @param source - The control points the strands were built from, used to
136
+ * derive rest lengths and the rest pose.
137
+ * @param options - Tuning.
138
+ */ constructor(strands, source, options){
139
+ super();
140
+ this._device = getDevice();
141
+ this._enabled = false;
142
+ this._disabledReason = null;
143
+ this._program = null;
144
+ this._bindGroup = null;
145
+ this._pointBuffer = null;
146
+ this._prevPointBuffer = null;
147
+ this._restPointBuffer = null;
148
+ this._framePointBuffer = null;
149
+ this._restLengthBuffer = null;
150
+ this._sphereBuffer = null;
151
+ this._capsuleBuffer = null;
152
+ this._planeBuffer = null;
153
+ this._strandCount = strands.strandCount;
154
+ this._gravity = options?.gravity ? new Vector3(options.gravity) : new Vector3(0, -9.8, 0);
155
+ this._damping = clamp(options?.damping ?? DEFAULT_DAMPING, 0, 1);
156
+ this._globalStiffness = clamp(options?.globalStiffness ?? DEFAULT_GLOBAL_STIFFNESS, 0, 1);
157
+ this._globalRange = clamp(options?.globalRange ?? DEFAULT_GLOBAL_RANGE, 0, 1);
158
+ this._localStiffness = clamp(options?.localStiffness ?? DEFAULT_LOCAL_STIFFNESS, 0, 1);
159
+ this._localIterations = clamp(options?.localIterations ?? DEFAULT_LOCAL_ITERATIONS, 1, 8) | 0;
160
+ this._ftlDamping = clamp(options?.ftlDamping ?? DEFAULT_FTL_DAMPING, 0, 1);
161
+ this._vspCoeff = clamp(options?.vspCoeff ?? DEFAULT_VSP_COEFF, 0, 1);
162
+ this._vspAccelThreshold = Math.max(0, options?.vspAccelThreshold ?? DEFAULT_VSP_ACCEL_THRESHOLD);
163
+ this._substeps = clamp(options?.substeps ?? 1, 1, 8) | 0;
164
+ this._friction = clamp(options?.friction ?? 0.2, 0, 1);
165
+ this._maxSpeedFactor = Math.max(0, options?.maxSpeedFactor ?? DEFAULT_MAX_SPEED_FACTOR);
166
+ this._colliders = options?.colliders ? [
167
+ ...options.colliders
168
+ ] : [];
169
+ this._timeAccumulator = 0;
170
+ this._sphereData = new Float32Array(MAX_COLLIDERS * SPHERE_STRIDE);
171
+ this._capsuleData = new Float32Array(MAX_COLLIDERS * CAPSULE_STRIDE);
172
+ this._planeData = new Float32Array(MAX_COLLIDERS * PLANE_STRIDE);
173
+ this._relativeTransform = Matrix4x4.identity();
174
+ this._invWorldMatrix = Matrix4x4.identity();
175
+ this._prevWorldMatrix = Matrix4x4.identity();
176
+ this._identity = Matrix4x4.identity();
177
+ this._localGravity = new Vector3();
178
+ this._scratchVec = new Vector3();
179
+ this._scratchVec2 = new Vector3();
180
+ this._scaleFrom = new Vector3();
181
+ this._scaleTo = new Vector3();
182
+ this._translationFrom = new Vector3();
183
+ this._translationTo = new Vector3();
184
+ this._rotationFrom = new Quaternion();
185
+ this._rotationTo = new Quaternion();
186
+ this._rotationLerp = new Quaternion();
187
+ this._stepFrom = Matrix4x4.identity();
188
+ this._stepTo = Matrix4x4.identity();
189
+ this._hasPrevWorldMatrix = false;
190
+ this._maxStrandLength = 0;
191
+ this._restCenter = new Vector3();
192
+ this._prevRootWorld = new Vector3();
193
+ this._prevRootVelocity = new Vector3();
194
+ this._rootHistoryDepth = 0;
195
+ this._workgroupCount = Math.ceil(this._strandCount / DEFAULT_WORKGROUP_SIZE);
196
+ if (!isHairSimulationSupported(this._device)) {
197
+ this._disabledReason = 'GPU hair simulation is disabled: current backend is not WebGPU.';
198
+ return;
199
+ }
200
+ if (!strands.pointBuffer || !strands.headerBuffer) {
201
+ this._disabledReason = 'GPU hair simulation is disabled: strand data has no GPU buffers.';
202
+ return;
203
+ }
204
+ try {
205
+ this._init(strands, source);
206
+ this._enabled = true;
207
+ } catch (err) {
208
+ this._disabledReason = `GPU hair simulation is disabled: ${err}`;
209
+ this._releaseResources();
210
+ }
211
+ }
212
+ /** Whether the simulation is running. */ get enabled() {
213
+ return this._enabled;
214
+ }
215
+ /** Why the simulation is not running, when it is not. */ get disabledReason() {
216
+ return this._disabledReason;
217
+ }
218
+ /** World-space gravity. */ get gravity() {
219
+ return this._gravity;
220
+ }
221
+ set gravity(value) {
222
+ this._gravity = new Vector3(value);
223
+ }
224
+ /** Velocity lost per step, in [0, 1]. */ get damping() {
225
+ return this._damping;
226
+ }
227
+ set damping(value) {
228
+ this._damping = clamp(value, 0, 1);
229
+ }
230
+ /** Per-substep ceiling on point travel, in segment lengths. */ get maxSpeedFactor() {
231
+ return this._maxSpeedFactor;
232
+ }
233
+ set maxSpeedFactor(value) {
234
+ this._maxSpeedFactor = Math.max(0, value);
235
+ }
236
+ /** Pull back to the authored pose per fixed step, in [0, 1]. Off by default. */ get globalStiffness() {
237
+ return this._globalStiffness;
238
+ }
239
+ set globalStiffness(value) {
240
+ this._globalStiffness = clamp(value, 0, 1);
241
+ }
242
+ /** Fraction of a strand, from the root, the pose pull acts on, in [0, 1]. */ get globalRange() {
243
+ return this._globalRange;
244
+ }
245
+ set globalRange(value) {
246
+ this._globalRange = clamp(value, 0, 1);
247
+ }
248
+ /** How strongly a strand keeps its authored shape, in [0, 1]. */ get localStiffness() {
249
+ return this._localStiffness;
250
+ }
251
+ set localStiffness(value) {
252
+ this._localStiffness = clamp(value, 0, 1);
253
+ }
254
+ /** Local shape constraint passes per substep. */ get localIterations() {
255
+ return this._localIterations;
256
+ }
257
+ set localIterations(value) {
258
+ this._localIterations = clamp(value, 1, 8) | 0;
259
+ }
260
+ /** Share of a point's length correction fed back into its parent, in [0, 1]. */ get ftlDamping() {
261
+ return this._ftlDamping;
262
+ }
263
+ set ftlDamping(value) {
264
+ this._ftlDamping = clamp(value, 0, 1);
265
+ }
266
+ /** How much of the node's motion a strand is carried along by, in [0, 1]. */ get vspCoeff() {
267
+ return this._vspCoeff;
268
+ }
269
+ set vspCoeff(value) {
270
+ this._vspCoeff = clamp(value, 0, 1);
271
+ }
272
+ /** Node acceleration above which a strand is carried rigidly, in units/s². */ get vspAccelThreshold() {
273
+ return this._vspAccelThreshold;
274
+ }
275
+ set vspAccelThreshold(value) {
276
+ this._vspAccelThreshold = Math.max(0, value);
277
+ }
278
+ /** Integration substeps per fixed step. */ get substeps() {
279
+ return this._substeps;
280
+ }
281
+ set substeps(value) {
282
+ this._substeps = clamp(value, 1, 8) | 0;
283
+ }
284
+ /** Friction on tangential contact motion, in [0, 1]. */ get friction() {
285
+ return this._friction;
286
+ }
287
+ set friction(value) {
288
+ this._friction = clamp(value, 0, 1);
289
+ }
290
+ /** Colliders the strands are pushed out of. */ get colliders() {
291
+ return this._colliders;
292
+ }
293
+ set colliders(value) {
294
+ this._colliders = [
295
+ ...value
296
+ ];
297
+ }
298
+ /** Verlet history. Not a previous-frame position - see the field remarks. */ get prevPointBuffer() {
299
+ return this._prevPointBuffer;
300
+ }
301
+ /** Local positions as of the previously rendered frame, for motion vectors. */ get framePointBuffer() {
302
+ return this._framePointBuffer;
303
+ }
304
+ /**
305
+ * Discards accumulated motion and returns every strand to its rest pose.
306
+ *
307
+ * @remarks
308
+ * Wanted whenever a node is moved rather than animated: a teleport would
309
+ * otherwise register as one enormous frame of inertia and fling the hair.
310
+ * {@link GPUHairSimulation.update} calls this itself when the groom jumps
311
+ * farther in one frame than a few times its own strand length.
312
+ */ reset(worldMatrix) {
313
+ if (!this._enabled) {
314
+ return;
315
+ }
316
+ this._timeAccumulator = 0;
317
+ this._prevWorldMatrix.set(worldMatrix);
318
+ this._hasPrevWorldMatrix = true;
319
+ this._rootHistoryDepth = 0;
320
+ this._resetLivePoints();
321
+ }
322
+ /**
323
+ * Advances the simulation.
324
+ *
325
+ * @param deltaTime - Seconds since the last update.
326
+ * @param worldMatrix - The drawing node's current world matrix.
327
+ */ update(deltaTime, worldMatrix) {
328
+ if (!this._enabled) {
329
+ return;
330
+ }
331
+ if (!this._hasPrevWorldMatrix) {
332
+ this._prevWorldMatrix.set(worldMatrix);
333
+ this._hasPrevWorldMatrix = true;
334
+ } else if (this._maxStrandLength > 0) {
335
+ // A jump larger than a few times the hair itself is a teleport rather than
336
+ // motion; fed to the solver it would fling the strands. The rest centre
337
+ // stands in for the groom, so a rotation around a distant pivot counts as
338
+ // the displacement it actually produces.
339
+ this._prevWorldMatrix.transformPointAffine(this._restCenter, this._scratchVec);
340
+ worldMatrix.transformPointAffine(this._restCenter, this._scratchVec2);
341
+ const dx = this._scratchVec.x - this._scratchVec2.x;
342
+ const dy = this._scratchVec.y - this._scratchVec2.y;
343
+ const dz = this._scratchVec.z - this._scratchVec2.z;
344
+ const threshold = this._maxStrandLength * TELEPORT_DISTANCE_FACTOR;
345
+ if (dx * dx + dy * dy + dz * dz > threshold * threshold) {
346
+ this.reset(worldMatrix);
347
+ return;
348
+ }
349
+ }
350
+ // Before anything moves: put aside the points the last frame was drawn with,
351
+ // which is what the material differences against to build a motion vector.
352
+ // Unconditional, and ahead of the early returns below - a frame that takes no
353
+ // step still has to advance the snapshot, or it would report the previous
354
+ // frame's motion a second time and smear rather than sharpen.
355
+ this._snapshotFramePoints();
356
+ const frameDt = clamp(Number(deltaTime) || 0, 0, MAX_ACCUMULATED_SIMULATION_TIME);
357
+ if (frameDt <= 0) {
358
+ return;
359
+ }
360
+ this._timeAccumulator = Math.min(this._timeAccumulator + frameDt, MAX_ACCUMULATED_SIMULATION_TIME);
361
+ const stepCount = Math.floor((this._timeAccumulator + 1e-8) / FIXED_SIMULATION_TIME_STEP);
362
+ if (stepCount <= 0) {
363
+ return;
364
+ }
365
+ this._timeAccumulator = Math.max(0, this._timeAccumulator - stepCount * FIXED_SIMULATION_TIME_STEP);
366
+ // Whether the node is being shoved hard enough to suspend strand lag.
367
+ const rigid = this._measureRootAcceleration(worldMatrix, frameDt);
368
+ // Gravity and colliders come into local space from the frame-end transform;
369
+ // they change too slowly for the substep interpolation below to matter.
370
+ this._invWorldMatrix.set(worldMatrix);
371
+ this._invWorldMatrix.inplaceInvert();
372
+ this._invWorldMatrix.transformVectorAffine(this._gravity, this._localGravity);
373
+ this._uploadColliders(this._invWorldMatrix);
374
+ const bindGroup = this._bindGroup;
375
+ const substepTime = FIXED_SIMULATION_TIME_STEP / this._substeps;
376
+ bindGroup.setValue('strandCount', this._strandCount);
377
+ // An exponential decay rather than a linear scale, so halving the substep
378
+ // and applying it twice as often leaves the same velocity behind.
379
+ bindGroup.setValue('dampingDecay', Math.exp(-this._damping * substepTime * 60));
380
+ // The dial is the fraction removed per fixed step; the shader applies it
381
+ // once per substep, so convert it to the fraction that compounds back to it.
382
+ bindGroup.setValue('globalStiffness', 1 - Math.pow(1 - this._globalStiffness, 1 / this._substeps));
383
+ bindGroup.setValue('globalRange', this._globalRange);
384
+ bindGroup.setValue('localStiffness', this._localStiffness);
385
+ bindGroup.setValue('localIterations', this._localIterations);
386
+ bindGroup.setValue('ftlDamping', this._ftlDamping);
387
+ bindGroup.setValue('vspCoeff', rigid ? 1 : this._vspCoeff);
388
+ bindGroup.setValue('friction', this._friction);
389
+ bindGroup.setValue('maxSpeedFactor', this._maxSpeedFactor);
390
+ bindGroup.setValue('gravity', this._localGravity);
391
+ bindGroup.setValue('minDistance', MIN_DISTANCE);
392
+ bindGroup.setValue('deltaTime', substepTime);
393
+ const totalSubsteps = stepCount * this._substeps;
394
+ this._device.pushDeviceStates();
395
+ try {
396
+ this._device.setProgram(this._program);
397
+ this._device.setBindGroup(0, bindGroup);
398
+ if (matrixEquals(this._prevWorldMatrix, worldMatrix)) {
399
+ // The node stood still; nothing to distribute.
400
+ bindGroup.setValue('relativeTransform', this._identity);
401
+ for(let i = 0; i < totalSubsteps; i++){
402
+ this._device.compute(this._workgroupCount, 1, 1);
403
+ }
404
+ } else {
405
+ // The node's frame motion, spread evenly over the substeps. Lumping it
406
+ // into the first one would let a frame-time hitch move roots by more
407
+ // than a segment length in what the solver treats as a single substep.
408
+ this._prevWorldMatrix.decompose(this._scaleFrom, this._rotationFrom, this._translationFrom);
409
+ worldMatrix.decompose(this._scaleTo, this._rotationTo, this._translationTo);
410
+ this._stepFrom.set(this._prevWorldMatrix);
411
+ for(let i = 1; i <= totalSubsteps; i++){
412
+ if (i === totalSubsteps) {
413
+ // The exact endpoint, so decompose round-tripping cannot leave the
414
+ // solver's idea of the transform drifting from the node's.
415
+ this._stepTo.set(worldMatrix);
416
+ } else {
417
+ const t = i / totalSubsteps;
418
+ this._scratchVec.setXYZ(this._scaleFrom.x + (this._scaleTo.x - this._scaleFrom.x) * t, this._scaleFrom.y + (this._scaleTo.y - this._scaleFrom.y) * t, this._scaleFrom.z + (this._scaleTo.z - this._scaleFrom.z) * t);
419
+ this._scratchVec2.setXYZ(this._translationFrom.x + (this._translationTo.x - this._translationFrom.x) * t, this._translationFrom.y + (this._translationTo.y - this._translationFrom.y) * t, this._translationFrom.z + (this._translationTo.z - this._translationFrom.z) * t);
420
+ Quaternion.slerp(this._rotationFrom, this._rotationTo, t, this._rotationLerp);
421
+ this._stepTo.compose(this._scratchVec, this._rotationLerp, this._scratchVec2);
422
+ }
423
+ this._invWorldMatrix.set(this._stepTo);
424
+ this._invWorldMatrix.inplaceInvert();
425
+ Matrix4x4.multiply(this._invWorldMatrix, this._stepFrom, this._relativeTransform);
426
+ bindGroup.setValue('relativeTransform', this._relativeTransform);
427
+ this._device.compute(this._workgroupCount, 1, 1);
428
+ this._stepFrom.set(this._stepTo);
429
+ }
430
+ }
431
+ } finally{
432
+ this._device.popDeviceStates();
433
+ }
434
+ this._prevWorldMatrix.set(worldMatrix);
435
+ }
436
+ onDispose() {
437
+ this._releaseResources();
438
+ }
439
+ /**
440
+ * Whether the node is accelerating hard enough to suspend strand lag.
441
+ *
442
+ * @remarks
443
+ * Differentiated over the frame intervals actually spanned rather than the
444
+ * fixed step: a second difference of position is only an acceleration when
445
+ * the samples are evenly spaced, and frame times are not.
446
+ * @internal
447
+ */ _measureRootAcceleration(worldMatrix, frameDt) {
448
+ worldMatrix.transformPointAffine(this._restCenter, this._scratchVec);
449
+ let rigid = false;
450
+ if (this._rootHistoryDepth > 0) {
451
+ const vx = (this._scratchVec.x - this._prevRootWorld.x) / frameDt;
452
+ const vy = (this._scratchVec.y - this._prevRootWorld.y) / frameDt;
453
+ const vz = (this._scratchVec.z - this._prevRootWorld.z) / frameDt;
454
+ if (this._rootHistoryDepth > 1) {
455
+ const ax = (vx - this._prevRootVelocity.x) / frameDt;
456
+ const ay = (vy - this._prevRootVelocity.y) / frameDt;
457
+ const az = (vz - this._prevRootVelocity.z) / frameDt;
458
+ const threshold = this._vspAccelThreshold;
459
+ rigid = ax * ax + ay * ay + az * az > threshold * threshold;
460
+ }
461
+ this._prevRootVelocity.setXYZ(vx, vy, vz);
462
+ }
463
+ this._prevRootWorld.set(this._scratchVec);
464
+ if (this._rootHistoryDepth < 2) {
465
+ this._rootHistoryDepth++;
466
+ }
467
+ return rigid;
468
+ }
469
+ /**
470
+ * Copies the live points into the frame snapshot the material differences
471
+ * against.
472
+ *
473
+ * @remarks
474
+ * A dispatch of its own rather than a stage of the solve, because it has to
475
+ * happen on every frame while the solve only happens on frames that take a
476
+ * step, and because it has to see the points before the first substep touches
477
+ * them.
478
+ * @internal
479
+ */ _snapshotFramePoints() {
480
+ this._bindGroup.setValue('strandCount', this._strandCount);
481
+ this._bindGroup.setValue('snapshotFrame', 1);
482
+ this._device.pushDeviceStates();
483
+ try {
484
+ this._device.setProgram(this._program);
485
+ this._device.setBindGroup(0, this._bindGroup);
486
+ this._device.compute(this._workgroupCount, 1, 1);
487
+ } finally{
488
+ this._device.popDeviceStates();
489
+ }
490
+ this._bindGroup.setValue('snapshotFrame', 0);
491
+ }
492
+ /** Copies the rest pose over the live points. @internal */ _resetLivePoints() {
493
+ // Rest points are xyz only while the live buffer interleaves width, so this
494
+ // cannot be a straight buffer copy; the shader does it instead.
495
+ this._bindGroup.setValue('strandCount', this._strandCount);
496
+ this._bindGroup.setValue('resetPose', 1);
497
+ this._device.pushDeviceStates();
498
+ try {
499
+ this._device.setProgram(this._program);
500
+ this._device.setBindGroup(0, this._bindGroup);
501
+ this._device.compute(this._workgroupCount, 1, 1);
502
+ } finally{
503
+ this._device.popDeviceStates();
504
+ }
505
+ this._bindGroup.setValue('resetPose', 0);
506
+ }
507
+ /** Builds buffers and the compute pipeline. @internal */ _init(strands, source) {
508
+ const device = this._device;
509
+ const pointCount = strands.pointCount;
510
+ const scale = source.scale ?? 1;
511
+ // Rest pose and rest lengths, in the same units the strand data uploaded in
512
+ // - it applies the scale, so this has to as well or every constraint is
513
+ // wrong by it. The same pass measures the longest strand and the rest
514
+ // centre, which the teleport guard in update() works from.
515
+ const restPoints = new Float32Array(pointCount * 3);
516
+ const restLengths = new Float32Array(pointCount);
517
+ const counts = source.pointCounts;
518
+ let first = 0;
519
+ let maxStrandLength = 0;
520
+ let minX = Infinity;
521
+ let minY = Infinity;
522
+ let minZ = Infinity;
523
+ let maxX = -Infinity;
524
+ let maxY = -Infinity;
525
+ let maxZ = -Infinity;
526
+ for(let s = 0; s < counts.length; s++){
527
+ const count = counts[s];
528
+ let strandLength = 0;
529
+ for(let i = 0; i < count; i++){
530
+ const src = (first + i) * 3;
531
+ const dst = (first + i) * 3;
532
+ const x = source.positions[src] * scale;
533
+ const y = source.positions[src + 1] * scale;
534
+ const z = source.positions[src + 2] * scale;
535
+ restPoints[dst] = x;
536
+ restPoints[dst + 1] = y;
537
+ restPoints[dst + 2] = z;
538
+ minX = x < minX ? x : minX;
539
+ minY = y < minY ? y : minY;
540
+ minZ = z < minZ ? z : minZ;
541
+ maxX = x > maxX ? x : maxX;
542
+ maxY = y > maxY ? y : maxY;
543
+ maxZ = z > maxZ ? z : maxZ;
544
+ if (i > 0) {
545
+ const dx = x - restPoints[dst - 3];
546
+ const dy = y - restPoints[dst - 2];
547
+ const dz = z - restPoints[dst - 1];
548
+ const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
549
+ restLengths[first + i] = len;
550
+ strandLength += len;
551
+ }
552
+ }
553
+ if (strandLength > maxStrandLength) {
554
+ maxStrandLength = strandLength;
555
+ }
556
+ first += count;
557
+ }
558
+ this._maxStrandLength = maxStrandLength;
559
+ if (pointCount > 0) {
560
+ this._restCenter.setXYZ((minX + maxX) * 0.5, (minY + maxY) * 0.5, (minZ + maxZ) * 0.5);
561
+ }
562
+ this._pointBuffer = strands.pointBuffer;
563
+ this._restPointBuffer = this._createStorage(restPoints);
564
+ this._prevPointBuffer = this._createStorage(restPoints);
565
+ this._framePointBuffer = this._createStorage(restPoints);
566
+ this._restLengthBuffer = this._createStorage(restLengths);
567
+ this._sphereBuffer = this._createStorage(this._sphereData);
568
+ this._capsuleBuffer = this._createStorage(this._capsuleData);
569
+ this._planeBuffer = this._createStorage(this._planeData);
570
+ this._program = createHairSimulationProgram(device, DEFAULT_WORKGROUP_SIZE);
571
+ if (!this._program) {
572
+ throw new Error('failed to create the compute program');
573
+ }
574
+ this._bindGroup = device.createBindGroup(this._program.bindGroupLayouts[0]);
575
+ this._bindGroup.setBuffer('points', this._pointBuffer);
576
+ this._bindGroup.setBuffer('prevPoints', this._prevPointBuffer);
577
+ this._bindGroup.setBuffer('framePoints', this._framePointBuffer);
578
+ this._bindGroup.setBuffer('restPoints', this._restPointBuffer);
579
+ this._bindGroup.setBuffer('restLengths', this._restLengthBuffer);
580
+ this._bindGroup.setBuffer('headers', strands.headerBuffer);
581
+ this._bindGroup.setBuffer('sphereData', this._sphereBuffer);
582
+ this._bindGroup.setBuffer('capsuleData', this._capsuleBuffer);
583
+ this._bindGroup.setBuffer('planeData', this._planeBuffer);
584
+ this._bindGroup.setValue('strandCount', this._strandCount);
585
+ this._bindGroup.setValue('resetPose', 0);
586
+ this._bindGroup.setValue('relativeTransform', Matrix4x4.identity());
587
+ this._bindGroup.setValue('sphereCount', 0);
588
+ this._bindGroup.setValue('capsuleCount', 0);
589
+ this._bindGroup.setValue('planeCount', 0);
590
+ this._bindGroup.setValue('minDistance', MIN_DISTANCE);
591
+ this._bindGroup.setValue('snapshotFrame', 0);
592
+ this._bindGroup.setValue('maxSpeedFactor', this._maxSpeedFactor);
593
+ this._bindGroup.setValue('globalStiffness', this._globalStiffness);
594
+ this._bindGroup.setValue('globalRange', this._globalRange);
595
+ this._bindGroup.setValue('localStiffness', this._localStiffness);
596
+ this._bindGroup.setValue('localIterations', this._localIterations);
597
+ this._bindGroup.setValue('ftlDamping', this._ftlDamping);
598
+ this._bindGroup.setValue('vspCoeff', this._vspCoeff);
599
+ }
600
+ /** @internal */ _createStorage(data) {
601
+ const buffer = this._device.createBuffer(data.byteLength, {
602
+ usage: 'uniform',
603
+ storage: true,
604
+ dynamic: false,
605
+ managed: false
606
+ });
607
+ if (!buffer) {
608
+ throw new Error('failed to allocate a simulation buffer');
609
+ }
610
+ buffer.bufferSubData(0, data);
611
+ return buffer;
612
+ }
613
+ /** Brings colliders into local space and uploads them. @internal */ _uploadColliders(invWorldMatrix) {
614
+ let spheres = 0;
615
+ let capsules = 0;
616
+ let planes = 0;
617
+ for (const collider of this._colliders){
618
+ if (!collider.enabled) {
619
+ continue;
620
+ }
621
+ // Node-tracking colliders are world-space and refreshed here.
622
+ updateColliderFromNode(collider);
623
+ if (collider.type === 'sphere' && spheres < MAX_COLLIDERS) {
624
+ const sphere = collider;
625
+ invWorldMatrix.transformPointAffine(sphere.center, this._scratchVec);
626
+ const base = spheres * SPHERE_STRIDE;
627
+ this._sphereData[base] = this._scratchVec.x;
628
+ this._sphereData[base + 1] = this._scratchVec.y;
629
+ this._sphereData[base + 2] = this._scratchVec.z;
630
+ this._sphereData[base + 3] = sphere.radius * localRadiusScale(invWorldMatrix);
631
+ spheres++;
632
+ } else if (collider.type === 'capsule' && capsules < MAX_COLLIDERS) {
633
+ const capsule = collider;
634
+ invWorldMatrix.transformPointAffine(capsule.start, this._scratchVec);
635
+ invWorldMatrix.transformPointAffine(capsule.end, this._scratchVec2);
636
+ const base = capsules * CAPSULE_STRIDE;
637
+ this._capsuleData[base] = this._scratchVec.x;
638
+ this._capsuleData[base + 1] = this._scratchVec.y;
639
+ this._capsuleData[base + 2] = this._scratchVec.z;
640
+ this._capsuleData[base + 3] = capsule.radius * localRadiusScale(invWorldMatrix);
641
+ this._capsuleData[base + 4] = this._scratchVec2.x;
642
+ this._capsuleData[base + 5] = this._scratchVec2.y;
643
+ this._capsuleData[base + 6] = this._scratchVec2.z;
644
+ this._capsuleData[base + 7] = 0;
645
+ capsules++;
646
+ } else if (collider.type === 'plane' && planes < MAX_COLLIDERS) {
647
+ const plane = collider;
648
+ invWorldMatrix.transformPointAffine(plane.point, this._scratchVec);
649
+ // A normal transforms by the inverse transpose, and the inverse is to hand.
650
+ transformNormal(invWorldMatrix, plane.normal, this._scratchVec2);
651
+ const base = planes * PLANE_STRIDE;
652
+ this._planeData[base] = this._scratchVec.x;
653
+ this._planeData[base + 1] = this._scratchVec.y;
654
+ this._planeData[base + 2] = this._scratchVec.z;
655
+ this._planeData[base + 3] = 0;
656
+ this._planeData[base + 4] = this._scratchVec2.x;
657
+ this._planeData[base + 5] = this._scratchVec2.y;
658
+ this._planeData[base + 6] = this._scratchVec2.z;
659
+ this._planeData[base + 7] = 0;
660
+ planes++;
661
+ }
662
+ }
663
+ this._sphereBuffer.bufferSubData(0, this._sphereData);
664
+ this._capsuleBuffer.bufferSubData(0, this._capsuleData);
665
+ this._planeBuffer.bufferSubData(0, this._planeData);
666
+ this._bindGroup.setValue('sphereCount', spheres);
667
+ this._bindGroup.setValue('capsuleCount', capsules);
668
+ this._bindGroup.setValue('planeCount', planes);
669
+ }
670
+ /** @internal */ _releaseResources() {
671
+ this._enabled = false;
672
+ this._bindGroup?.dispose();
673
+ this._bindGroup = null;
674
+ this._program?.dispose();
675
+ this._program = null;
676
+ // The point buffer belongs to the strand data, not here.
677
+ this._pointBuffer = null;
678
+ this._prevPointBuffer?.dispose();
679
+ this._prevPointBuffer = null;
680
+ this._restPointBuffer?.dispose();
681
+ this._restPointBuffer = null;
682
+ this._framePointBuffer?.dispose();
683
+ this._framePointBuffer = null;
684
+ this._restLengthBuffer?.dispose();
685
+ this._restLengthBuffer = null;
686
+ this._sphereBuffer?.dispose();
687
+ this._sphereBuffer = null;
688
+ this._capsuleBuffer?.dispose();
689
+ this._capsuleBuffer = null;
690
+ this._planeBuffer?.dispose();
691
+ this._planeBuffer = null;
692
+ }
693
+ }
694
+ /** Mean axis scale of a matrix, for converting a radius. @internal */ function localRadiusScale(matrix) {
695
+ const sx = Math.hypot(matrix.m00, matrix.m10, matrix.m20);
696
+ const sy = Math.hypot(matrix.m01, matrix.m11, matrix.m21);
697
+ const sz = Math.hypot(matrix.m02, matrix.m12, matrix.m22);
698
+ return (sx + sy + sz) / 3;
699
+ }
700
+ /**
701
+ * Transforms a normal by the transpose of the inverse world matrix, which is the
702
+ * inverse-transpose a normal needs.
703
+ * @internal
704
+ */ function transformNormal(invWorldMatrix, normal, out) {
705
+ out.setXYZ(invWorldMatrix.m00 * normal.x + invWorldMatrix.m10 * normal.y + invWorldMatrix.m20 * normal.z, invWorldMatrix.m01 * normal.x + invWorldMatrix.m11 * normal.y + invWorldMatrix.m21 * normal.z, invWorldMatrix.m02 * normal.x + invWorldMatrix.m12 * normal.y + invWorldMatrix.m22 * normal.z);
706
+ const len = out.magnitude;
707
+ if (len > MIN_DISTANCE) {
708
+ out.scaleBy(1 / len);
709
+ } else {
710
+ out.setXYZ(0, 1, 0);
711
+ }
712
+ }
713
+ /**
714
+ * Builds the solver pass.
715
+ *
716
+ * @remarks
717
+ * TressFX's five simulation stages, run back to back inside one kernel with a
718
+ * thread per strand instead of five dispatches with a thread per vertex. The
719
+ * rearrangement keeps them numerically equivalent: integration, the global shape
720
+ * constraint and shock propagation are per-vertex with no cross-vertex
721
+ * dependency, and the local shape constraint is already a serial walk over a
722
+ * strand in TressFX. Solved in local space, where the bind-pose skinning
723
+ * quaternion is the identity and shock propagation reduces to a blend.
724
+ *
725
+ * Exported for shader-generation tests only.
726
+ * @internal
727
+ */ function createHairSimulationProgram(device, workgroupSize) {
728
+ const program = device.buildComputeProgram({
729
+ workgroupSize: [
730
+ workgroupSize,
731
+ 1,
732
+ 1
733
+ ],
734
+ compute (pb) {
735
+ // Positions and widths interleave, four floats per point; the solver
736
+ // writes xyz and leaves w alone, which is why they share a buffer.
737
+ this.points = pb.float[0]().storageBuffer(0);
738
+ this.prevPoints = pb.float[0]().storageBuffer(0);
739
+ this.framePoints = pb.float[0]().storageBuffer(0);
740
+ this.restPoints = pb.float[0]().storageBufferReadonly(0);
741
+ this.restLengths = pb.float[0]().storageBufferReadonly(0);
742
+ this.headers = pb.uint[0]().storageBufferReadonly(0);
743
+ this.sphereData = pb.float[0]().storageBufferReadonly(0);
744
+ this.capsuleData = pb.float[0]().storageBufferReadonly(0);
745
+ this.planeData = pb.float[0]().storageBufferReadonly(0);
746
+ this.strandCount = pb.uint().uniform(0);
747
+ this.deltaTime = pb.float().uniform(0);
748
+ this.dampingDecay = pb.float().uniform(0);
749
+ this.globalStiffness = pb.float().uniform(0);
750
+ this.globalRange = pb.float().uniform(0);
751
+ this.localStiffness = pb.float().uniform(0);
752
+ this.localIterations = pb.uint().uniform(0);
753
+ this.ftlDamping = pb.float().uniform(0);
754
+ this.vspCoeff = pb.float().uniform(0);
755
+ this.friction = pb.float().uniform(0);
756
+ this.maxSpeedFactor = pb.float().uniform(0);
757
+ this.gravity = pb.vec3().uniform(0);
758
+ this.relativeTransform = pb.mat4().uniform(0);
759
+ this.sphereCount = pb.uint().uniform(0);
760
+ this.capsuleCount = pb.uint().uniform(0);
761
+ this.planeCount = pb.uint().uniform(0);
762
+ this.minDistance = pb.float().uniform(0);
763
+ this.resetPose = pb.uint().uniform(0);
764
+ this.snapshotFrame = pb.uint().uniform(0);
765
+ // Pushes a point out of every collider. Position only; friction and
766
+ // carrying the history along are the caller's job, since both need the
767
+ // history it is still assembling.
768
+ pb.func('Z_hairResolveContacts', [
769
+ pb.vec3('position')
770
+ ], function() {
771
+ this.$l.result = this.position;
772
+ this.$for(pb.uint('i'), 0, this.sphereCount, function() {
773
+ this.$l.base = pb.mul(this.i, 4);
774
+ this.$l.centre = pb.vec3(this.sphereData.at(this.base), this.sphereData.at(pb.add(this.base, 1)), this.sphereData.at(pb.add(this.base, 2)));
775
+ this.$l.radius = this.sphereData.at(pb.add(this.base, 3));
776
+ this.$l.delta = pb.sub(this.result, this.centre);
777
+ this.$l.dist = pb.length(this.delta);
778
+ this.$if(pb.lessThan(this.dist, this.radius), function() {
779
+ this.$l.normal = pb.vec3(0, 1, 0);
780
+ this.$if(pb.greaterThan(this.dist, this.minDistance), function() {
781
+ this.normal = pb.div(this.delta, this.dist);
782
+ });
783
+ this.result = pb.add(this.centre, pb.mul(this.normal, this.radius));
784
+ });
785
+ });
786
+ this.$for(pb.uint('j'), 0, this.capsuleCount, function() {
787
+ this.$l.cbase = pb.mul(this.j, 8);
788
+ this.$l.start = pb.vec3(this.capsuleData.at(this.cbase), this.capsuleData.at(pb.add(this.cbase, 1)), this.capsuleData.at(pb.add(this.cbase, 2)));
789
+ this.$l.cradius = this.capsuleData.at(pb.add(this.cbase, 3));
790
+ this.$l.end = pb.vec3(this.capsuleData.at(pb.add(this.cbase, 4)), this.capsuleData.at(pb.add(this.cbase, 5)), this.capsuleData.at(pb.add(this.cbase, 6)));
791
+ this.$l.axis = pb.sub(this.end, this.start);
792
+ this.$l.axisLenSq = pb.dot(this.axis, this.axis);
793
+ this.$l.t = pb.float(0);
794
+ this.$if(pb.greaterThan(this.axisLenSq, this.minDistance), function() {
795
+ this.t = pb.clamp(pb.div(pb.dot(pb.sub(this.result, this.start), this.axis), this.axisLenSq), 0, 1);
796
+ });
797
+ this.$l.closest = pb.add(this.start, pb.mul(this.axis, this.t));
798
+ this.$l.cdelta = pb.sub(this.result, this.closest);
799
+ this.$l.cdist = pb.length(this.cdelta);
800
+ this.$if(pb.lessThan(this.cdist, this.cradius), function() {
801
+ this.$l.cnormal = pb.vec3(0, 1, 0);
802
+ this.$if(pb.greaterThan(this.cdist, this.minDistance), function() {
803
+ this.cnormal = pb.div(this.cdelta, this.cdist);
804
+ });
805
+ this.result = pb.add(this.closest, pb.mul(this.cnormal, this.cradius));
806
+ });
807
+ });
808
+ this.$for(pb.uint('k'), 0, this.planeCount, function() {
809
+ this.$l.pbase = pb.mul(this.k, 8);
810
+ this.$l.point = pb.vec3(this.planeData.at(this.pbase), this.planeData.at(pb.add(this.pbase, 1)), this.planeData.at(pb.add(this.pbase, 2)));
811
+ this.$l.pnormal = pb.vec3(this.planeData.at(pb.add(this.pbase, 4)), this.planeData.at(pb.add(this.pbase, 5)), this.planeData.at(pb.add(this.pbase, 6)));
812
+ this.$l.signedDist = pb.dot(pb.sub(this.result, this.point), this.pnormal);
813
+ this.$if(pb.lessThan(this.signedDist, 0), function() {
814
+ this.result = pb.sub(this.result, pb.mul(this.pnormal, this.signedDist));
815
+ });
816
+ });
817
+ this.$return(this.result);
818
+ });
819
+ // Accessors for the interleaved point buffer.
820
+ pb.func('Z_hairGetPoint', [
821
+ pb.uint('index')
822
+ ], function() {
823
+ this.$l.base = pb.mul(this.index, 4);
824
+ this.$return(pb.vec3(this.points.at(this.base), this.points.at(pb.add(this.base, 1)), this.points.at(pb.add(this.base, 2))));
825
+ });
826
+ pb.func('Z_hairSetPoint', [
827
+ pb.uint('index'),
828
+ pb.vec3('value')
829
+ ], function() {
830
+ this.$l.base = pb.mul(this.index, 4);
831
+ this.points.setAt(this.base, this.value.x);
832
+ this.points.setAt(pb.add(this.base, 1), this.value.y);
833
+ this.points.setAt(pb.add(this.base, 2), this.value.z);
834
+ this.$return(pb.float(0));
835
+ });
836
+ pb.func('Z_hairGetRest', [
837
+ pb.uint('index')
838
+ ], function() {
839
+ this.$l.base = pb.mul(this.index, 3);
840
+ this.$return(pb.vec3(this.restPoints.at(this.base), this.restPoints.at(pb.add(this.base, 1)), this.restPoints.at(pb.add(this.base, 2))));
841
+ });
842
+ pb.func('Z_hairGetHistory', [
843
+ pb.uint('index')
844
+ ], function() {
845
+ this.$l.base = pb.mul(this.index, 3);
846
+ this.$return(pb.vec3(this.prevPoints.at(this.base), this.prevPoints.at(pb.add(this.base, 1)), this.prevPoints.at(pb.add(this.base, 2))));
847
+ });
848
+ pb.func('Z_hairSetHistory', [
849
+ pb.uint('index'),
850
+ pb.vec3('value')
851
+ ], function() {
852
+ this.$l.base = pb.mul(this.index, 3);
853
+ this.prevPoints.setAt(this.base, this.value.x);
854
+ this.prevPoints.setAt(pb.add(this.base, 1), this.value.y);
855
+ this.prevPoints.setAt(pb.add(this.base, 2), this.value.z);
856
+ this.$return(pb.float(0));
857
+ });
858
+ pb.func('Z_hairSetFramePoint', [
859
+ pb.uint('index'),
860
+ pb.vec3('value')
861
+ ], function() {
862
+ this.$l.base = pb.mul(this.index, 3);
863
+ this.framePoints.setAt(this.base, this.value.x);
864
+ this.framePoints.setAt(pb.add(this.base, 1), this.value.y);
865
+ this.framePoints.setAt(pb.add(this.base, 2), this.value.z);
866
+ this.$return(pb.float(0));
867
+ });
868
+ // Rotates a vector by a quaternion.
869
+ pb.func('Z_hairQuatRotate', [
870
+ pb.vec4('q'),
871
+ pb.vec3('v')
872
+ ], function() {
873
+ this.$l.uv = pb.cross(this.q.xyz, this.v);
874
+ this.$l.uuv = pb.cross(this.q.xyz, this.uv);
875
+ this.$return(pb.add(this.v, pb.mul(this.uv, pb.mul(this.q.w, 2)), pb.mul(this.uuv, 2)));
876
+ });
877
+ // Shortest-arc rotation taking unit u onto unit v. Antiparallel inputs
878
+ // leave the axis undefined, so any perpendicular of u will do.
879
+ pb.func('Z_hairQuatFromUnitVectors', [
880
+ pb.vec3('u'),
881
+ pb.vec3('v')
882
+ ], function() {
883
+ this.$l.r = pb.add(1, pb.dot(this.u, this.v));
884
+ this.$l.axis = pb.cross(this.u, this.v);
885
+ this.$if(pb.lessThan(this.r, 1e-7), function() {
886
+ this.r = pb.float(0);
887
+ this.axis = this.$choice(pb.greaterThan(pb.abs(this.u.x), pb.abs(this.u.z)), pb.vec3(pb.neg(this.u.y), this.u.x, 0), pb.vec3(0, pb.neg(this.u.z), this.u.y));
888
+ });
889
+ this.$l.q = pb.vec4(this.axis, this.r);
890
+ this.$l.lengthSq = pb.dot(this.q, this.q);
891
+ this.$l.result = pb.vec4(0, 0, 0, 1);
892
+ this.$if(pb.greaterThan(this.lengthSq, 1e-10), function() {
893
+ this.result = pb.mul(this.q, pb.inverseSqrt(this.lengthSq));
894
+ });
895
+ this.$return(this.result);
896
+ });
897
+ // Bounds the velocity a stored history implies, so that one bad substep
898
+ // cannot hand the next a velocity damping will never recover from.
899
+ pb.func('Z_hairClampHistory', [
900
+ pb.uint('index'),
901
+ pb.vec3('position'),
902
+ pb.vec3('history')
903
+ ], function() {
904
+ this.$l.result = this.history;
905
+ this.$l.delta = pb.sub(this.position, this.result);
906
+ this.$l.speed = pb.length(this.delta);
907
+ this.$l.limit = pb.mul(this.restLengths.at(this.index), this.maxSpeedFactor);
908
+ this.$if(pb.greaterThan(this.speed, this.limit), function() {
909
+ this.result = pb.sub(this.position, pb.mul(this.delta, pb.div(this.limit, this.speed)));
910
+ });
911
+ this.$return(this.result);
912
+ });
913
+ pb.main(function() {
914
+ this.$l.strand = this.$builtins.globalInvocationId.x;
915
+ this.$if(pb.lessThan(this.strand, this.strandCount), function() {
916
+ this.$l.hbase = pb.mul(this.strand, 4);
917
+ this.$l.firstPoint = this.headers.at(this.hbase);
918
+ this.$l.pointCount = this.headers.at(pb.add(this.hbase, 1));
919
+ this.$if(pb.notEqual(this.snapshotFrame, 0), function() {
920
+ // Copy the points the last frame was drawn with aside, before this
921
+ // frame's substeps overwrite them. Run once per frame from update(),
922
+ // including on frames that take no step - skipping those would leave
923
+ // the snapshot a frame stale and report motion that already happened.
924
+ this.$for(pb.uint('i'), 0, this.pointCount, function() {
925
+ this.$l.idx = pb.add(this.firstPoint, this.i);
926
+ this.Z_hairSetFramePoint(this.idx, this.Z_hairGetPoint(this.idx));
927
+ });
928
+ }).$elseif(pb.notEqual(this.resetPose, 0), function() {
929
+ // Rest into all three, so the strand is still as well as in place
930
+ // and the frame it snaps on reports no motion of its own.
931
+ this.$for(pb.uint('i'), 0, this.pointCount, function() {
932
+ this.$l.idx = pb.add(this.firstPoint, this.i);
933
+ this.$l.rest = this.Z_hairGetRest(this.idx);
934
+ this.Z_hairSetPoint(this.idx, this.rest);
935
+ this.Z_hairSetHistory(this.idx, this.rest);
936
+ this.Z_hairSetFramePoint(this.idx, this.rest);
937
+ });
938
+ }).$else(function() {
939
+ // Two pinned points, not one: one fixes where a strand hangs from but
940
+ // leaves the direction it leaves the scalp in free, so the root end
941
+ // flops and can invert. The second gives every strand the root frame
942
+ // the local shape constraint is defined against. In local space
943
+ // "pinned" means unchanged - node motion rides the transform.
944
+ this.$l.rootRest = this.Z_hairGetRest(this.firstPoint);
945
+ this.Z_hairSetPoint(this.firstPoint, this.rootRest);
946
+ this.Z_hairSetHistory(this.firstPoint, this.rootRest);
947
+ this.$if(pb.greaterThan(this.pointCount, 1), function() {
948
+ this.$l.nextIdx = pb.add(this.firstPoint, 1);
949
+ this.$l.nextRest = this.Z_hairGetRest(this.nextIdx);
950
+ this.Z_hairSetPoint(this.nextIdx, this.nextRest);
951
+ this.Z_hairSetHistory(this.nextIdx, this.nextRest);
952
+ });
953
+ this.$l.dt2 = pb.mul(this.deltaTime, this.deltaTime);
954
+ this.$l.rangeLimit = pb.mul(this.globalRange, pb.float(this.pointCount));
955
+ // Stage 1 - integration, shock propagation, global shape constraint.
956
+ this.$for(pb.uint('i'), 2, this.pointCount, function() {
957
+ this.$l.idx = pb.add(this.firstPoint, this.i);
958
+ this.$l.current = this.Z_hairGetPoint(this.idx);
959
+ this.$l.previous = this.Z_hairGetHistory(this.idx);
960
+ // Both stored positions go through the node's frame-to-frame
961
+ // motion: transforming only one would read that motion as strand
962
+ // velocity and throw the hair ahead of the movement.
963
+ this.$l.lagged = pb.mul(this.relativeTransform, pb.vec4(this.current, 1)).xyz;
964
+ this.$l.laggedPrev = pb.mul(this.relativeTransform, pb.vec4(this.previous, 1)).xyz;
965
+ // Shock propagation. Leaving a point untransformed is exactly
966
+ // carrying it rigidly with the node, so the coefficient blends
967
+ // between the two ends TressFX interpolates: 0 lags fully, 1
968
+ // follows. It stops a fast root outrunning its strand in one step.
969
+ this.current = pb.mix(this.lagged, this.current, this.vspCoeff);
970
+ this.previous = pb.mix(this.laggedPrev, this.previous, this.vspCoeff);
971
+ this.$l.velocity = pb.mul(pb.sub(this.current, this.previous), this.dampingDecay);
972
+ this.$l.next = pb.add(this.current, this.velocity, pb.mul(this.gravity, this.dt2));
973
+ // Pull toward the authored pose, over the range's fraction of the
974
+ // strand. Off by default - anchoring a point to a fixed place is a
975
+ // spring, and the local shape constraint holds styling without it.
976
+ this.$if(pb.and(pb.greaterThan(this.globalStiffness, 0), pb.lessThan(pb.float(this.i), this.rangeLimit)), function() {
977
+ this.$l.rest = this.Z_hairGetRest(this.idx);
978
+ this.next = pb.add(this.next, pb.mul(pb.sub(this.rest, this.next), this.globalStiffness));
979
+ });
980
+ this.Z_hairSetHistory(this.idx, this.current);
981
+ this.Z_hairSetPoint(this.idx, this.next);
982
+ });
983
+ // Stage 2 - local shape constraint, what makes a groom read as hair
984
+ // rather than as a chain. It holds each segment's angle to the one
985
+ // before it in that segment's *current* frame, so a strand keeps its
986
+ // curl while free to hang and swing, and resists bending - which is
987
+ // also what stops a disturbance ringing for seconds. Stiffness is
988
+ // halved and capped below 1 as in TressFX, since the constraint
989
+ // moves both points it touches and would otherwise overshoot.
990
+ this.$l.localStrength = pb.mul(pb.min(this.localStiffness, 0.95), 0.5);
991
+ this.$if(pb.greaterThan(this.pointCount, 2), function() {
992
+ this.$for(pb.uint('iter'), 0, this.localIterations, function() {
993
+ this.$for(pb.uint('i'), 1, pb.sub(this.pointCount, 1), function() {
994
+ this.$l.idx = pb.add(this.firstPoint, this.i);
995
+ this.$l.pos = this.Z_hairGetPoint(this.idx);
996
+ this.$l.posPlus = this.Z_hairGetPoint(pb.add(this.idx, 1));
997
+ this.$l.posMinus = this.Z_hairGetPoint(pb.sub(this.idx, 1));
998
+ this.$l.bind = this.Z_hairGetRest(this.idx);
999
+ this.$l.bindPlus = this.Z_hairGetRest(pb.add(this.idx, 1));
1000
+ this.$l.bindMinus = this.Z_hairGetRest(pb.sub(this.idx, 1));
1001
+ // Where the next point sits if its bend still matches the bind
1002
+ // pose, carried into the previous segment's current direction.
1003
+ this.$l.lastVec = pb.sub(this.pos, this.posMinus);
1004
+ this.$l.bindVec = pb.sub(this.bindPlus, this.bind);
1005
+ this.$l.lastBindVec = pb.sub(this.bind, this.bindMinus);
1006
+ this.$l.rotation = this.Z_hairQuatFromUnitVectors(pb.normalize(this.lastBindVec), pb.normalize(this.lastVec));
1007
+ this.$l.bendTarget = pb.add(this.Z_hairQuatRotate(this.rotation, this.bindVec), this.pos);
1008
+ this.$l.del = pb.mul(pb.sub(this.bendTarget, this.posPlus), this.localStrength);
1009
+ // Shared between both ends, unless one is a pinned root.
1010
+ this.$if(pb.greaterThan(this.i, 1), function() {
1011
+ this.Z_hairSetPoint(this.idx, pb.sub(this.pos, this.del));
1012
+ });
1013
+ this.Z_hairSetPoint(pb.add(this.idx, 1), pb.add(this.posPlus, this.del));
1014
+ });
1015
+ });
1016
+ });
1017
+ // Stage 3 - length constraints, root to tip. Placing each point at
1018
+ // its authored distance from an already-final parent leaves the
1019
+ // strand inextensible in one pass.
1020
+ //
1021
+ // The one place the port leaves TressFX, which sweeps even and odd
1022
+ // segments Jacobi-style because a thread there owns a single vertex
1023
+ // and cannot walk a strand. Jacobi transports tension one segment
1024
+ // per sweep, so a hanging strand stretches until the residual
1025
+ // balances: 22% on a 24-point strand under earth gravity at its two
1026
+ // sweeps, still 4.5% at sixteen. TressFX's own default gravity is
1027
+ // zero, so it never sees this.
1028
+ //
1029
+ // Moving the child alone leaves that correction's momentum
1030
+ // unaccounted for, and unaccounted it pumps the chain until the
1031
+ // groom whips; handing it back to the parent is Mueller's FTL
1032
+ // damping.
1033
+ this.$if(pb.greaterThan(this.pointCount, 2), function() {
1034
+ this.$for(pb.uint('j'), 1, pb.sub(this.pointCount, 1), function() {
1035
+ this.$l.parentIdx = pb.add(this.firstPoint, this.j);
1036
+ this.$l.childIdx = pb.add(this.parentIdx, 1);
1037
+ this.$l.parent = this.Z_hairGetPoint(this.parentIdx);
1038
+ this.$l.predicted = this.Z_hairGetPoint(this.childIdx);
1039
+ // Rest lengths are indexed by the far end of a segment.
1040
+ this.$l.restLength = this.restLengths.at(this.childIdx);
1041
+ this.$l.offset = pb.sub(this.predicted, this.parent);
1042
+ this.$l.dist = pb.length(this.offset);
1043
+ // Coincident with the parent leaves the direction undefined; any
1044
+ // offset of the right length will do.
1045
+ this.$l.projected = pb.add(this.parent, pb.vec3(0, this.restLength, 0));
1046
+ this.$if(pb.greaterThan(this.dist, this.minDistance), function() {
1047
+ this.projected = pb.add(this.parent, pb.mul(pb.div(this.offset, this.dist), this.restLength));
1048
+ });
1049
+ this.Z_hairSetPoint(this.childIdx, this.projected);
1050
+ // The parent takes the reverse momentum, unless it is pinned.
1051
+ this.$if(pb.greaterThan(this.j, 1), function() {
1052
+ this.$l.correction = pb.sub(this.projected, this.predicted);
1053
+ this.Z_hairSetHistory(this.parentIdx, pb.add(this.Z_hairGetHistory(this.parentIdx), pb.mul(this.correction, this.ftlDamping)));
1054
+ });
1055
+ });
1056
+ });
1057
+ // Stage 4 - contacts, and the ceiling on this substep's travel.
1058
+ this.$for(pb.uint('i'), 2, this.pointCount, function() {
1059
+ this.$l.idx = pb.add(this.firstPoint, this.i);
1060
+ this.$l.position = this.Z_hairGetPoint(this.idx);
1061
+ this.$l.history = this.Z_hairGetHistory(this.idx);
1062
+ this.$l.resolved = this.Z_hairResolveContacts(this.position);
1063
+ // A push out is a position fix, not a shove: the history follows
1064
+ // it so the point keeps the velocity it arrived with, and friction
1065
+ // then takes its share of the tangential part.
1066
+ this.$l.push = pb.sub(this.resolved, this.position);
1067
+ this.$l.pushDist = pb.length(this.push);
1068
+ this.history = pb.add(this.history, this.push);
1069
+ this.$if(pb.greaterThan(this.pushDist, this.minDistance), function() {
1070
+ this.$l.normalDir = pb.div(this.push, this.pushDist);
1071
+ this.$l.contactVelocity = pb.sub(this.resolved, this.history);
1072
+ this.$l.tangential = pb.sub(this.contactVelocity, pb.mul(this.normalDir, pb.dot(this.contactVelocity, this.normalDir)));
1073
+ this.history = pb.add(this.history, pb.mul(this.tangential, this.friction));
1074
+ });
1075
+ this.Z_hairSetHistory(this.idx, this.Z_hairClampHistory(this.idx, this.resolved, this.history));
1076
+ this.Z_hairSetPoint(this.idx, this.resolved);
1077
+ });
1078
+ });
1079
+ });
1080
+ });
1081
+ }
1082
+ });
1083
+ if (program) {
1084
+ program.name = '@GPUHair_Simulate';
1085
+ }
1086
+ return program;
1087
+ }
1088
+
1089
+ export { GPUHairSimulation, createHairSimulationProgram, isHairSimulationSupported };
1090
+ //# sourceMappingURL=gpu_hair_simulation.js.map