@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
@@ -1,14 +1,14 @@
1
1
  import { Vector3, Quaternion } from '@zephyr3d/base';
2
2
  import { IKUtils } from '../ik/ik_utils.js';
3
+ import { updateColliderFromNode, resolveSphereCollision, resolveCapsuleCollision, resolvePlaneCollision } from './spring_collider.js';
4
+ import { SpringNodePoseTracker } from './spring_node_pose_tracker.js';
3
5
 
4
- /**
5
- * Physics engine for multiple spring chains with inter-chain constraints
6
- * Suitable for cloth, skirts, capes, and other multi-chain simulations
7
- *
8
- * @deprecated Use the new {@link JointDynamicsSystem} class instead.
9
- *
10
- * @public
11
- */ class MultiChainSpringSystem {
6
+ const FIXED_SIMULATION_TIME_STEP = 1 / 60;
7
+ const MAX_ACCUMULATED_SIMULATION_TIME = 1 / 20;
8
+ const MAX_SIMULATION_STEPS_PER_UPDATE = Math.max(1, Math.ceil(MAX_ACCUMULATED_SIMULATION_TIME / FIXED_SIMULATION_TIME_STEP));
9
+ const DEFAULT_PARTICLE_TARGET_SMOOTHING_TIME = 1 / 30;
10
+ const DEFAULT_COLLIDER_SMOOTHING_TIME = 1 / 30;
11
+ class MultiChainSpringSystem {
12
12
  _chains;
13
13
  _interChainConstraints;
14
14
  _iterations;
@@ -18,6 +18,23 @@ import { IKUtils } from '../ik/ik_utils.js';
18
18
  _centrifugalScale;
19
19
  _coriolisScale;
20
20
  _solver;
21
+ _poseFollow;
22
+ _maxPoseOffset;
23
+ _poseFollowRoot;
24
+ _poseFollowTip;
25
+ _poseFollowExponent;
26
+ _maxPoseOffsetRoot;
27
+ _maxPoseOffsetTip;
28
+ _colliders;
29
+ _timeAccumulator;
30
+ _smoothedParticleTargets;
31
+ _smoothedSphereCenters;
32
+ _smoothedCapsuleEndpoints;
33
+ _smoothedPlaneData;
34
+ _runtimeNodeMap;
35
+ _runtimeAnchorOffsets;
36
+ _runtimeRestLengths;
37
+ _nodePoseTracker;
21
38
  constructor(options){
22
39
  this._chains = [];
23
40
  this._interChainConstraints = [];
@@ -28,26 +45,32 @@ import { IKUtils } from '../ik/ik_utils.js';
28
45
  this._centrifugalScale = options?.centrifugalScale ?? 1.0;
29
46
  this._coriolisScale = options?.coriolisScale ?? 1.0;
30
47
  this._solver = options?.solver ?? 'verlet';
31
- }
32
- /**
33
- * Adds a spring chain to the system
34
- * @param chain - The chain to add
35
- * @returns The index of the added chain
36
- */ addChain(chain) {
48
+ this._poseFollow = Math.max(0, Math.min(1, options?.poseFollow ?? 0.35));
49
+ this._maxPoseOffset = Math.max(0, options?.maxPoseOffset ?? 0);
50
+ this._poseFollowRoot = Math.max(0, Math.min(1, options?.poseFollowRoot ?? this._poseFollow));
51
+ this._poseFollowTip = Math.max(0, Math.min(1, options?.poseFollowTip ?? this._poseFollow));
52
+ this._poseFollowExponent = Math.max(0.1, options?.poseFollowExponent ?? 1.6);
53
+ this._maxPoseOffsetRoot = Math.max(0, options?.maxPoseOffsetRoot ?? this._maxPoseOffset);
54
+ this._maxPoseOffsetTip = Math.max(0, options?.maxPoseOffsetTip ?? this._maxPoseOffset);
55
+ this._colliders = [];
56
+ this._timeAccumulator = 0;
57
+ this._smoothedParticleTargets = new WeakMap();
58
+ this._smoothedSphereCenters = new WeakMap();
59
+ this._smoothedCapsuleEndpoints = new WeakMap();
60
+ this._smoothedPlaneData = new WeakMap();
61
+ this._runtimeNodeMap = null;
62
+ this._runtimeAnchorOffsets = new WeakMap();
63
+ this._runtimeRestLengths = new WeakMap();
64
+ this._nodePoseTracker = new SpringNodePoseTracker();
65
+ }
66
+ addChain(chain) {
37
67
  this._chains.push(chain);
38
68
  return this._chains.length - 1;
39
69
  }
40
- /**
41
- * Adds an inter-chain constraint
42
- * @param constraint - The constraint to add
43
- */ addInterChainConstraint(constraint) {
70
+ addInterChainConstraint(constraint) {
44
71
  this._interChainConstraints.push(constraint);
45
72
  }
46
- /**
47
- * Creates radial constraints between adjacent chains
48
- * Useful for skirts, capes, and other radial multi-chain structures
49
- * @param options - Configuration options
50
- */ createRadialConstraints(options) {
73
+ createRadialConstraints(options) {
51
74
  const skipRows = options.skipRows ?? 0;
52
75
  const connectDistance = options.connectDistance ?? 1;
53
76
  const compliance = options.compliance ?? 0;
@@ -77,21 +100,272 @@ import { IKUtils } from '../ik/ik_utils.js';
77
100
  }
78
101
  }
79
102
  }
103
+ update(deltaTime) {
104
+ this._nodePoseTracker.restoreInputPose();
105
+ const frameDt = Math.min(Math.max(Number(deltaTime) || 0, 0), MAX_ACCUMULATED_SIMULATION_TIME);
106
+ if (frameDt <= 0) {
107
+ return;
108
+ }
109
+ this._timeAccumulator = Math.min(this._timeAccumulator + frameDt, MAX_ACCUMULATED_SIMULATION_TIME);
110
+ const stepCount = Math.min(MAX_SIMULATION_STEPS_PER_UPDATE, Math.floor((this._timeAccumulator + 1e-8) / FIXED_SIMULATION_TIME_STEP));
111
+ if (stepCount <= 0) {
112
+ return;
113
+ }
114
+ this._timeAccumulator = Math.max(0, this._timeAccumulator - stepCount * FIXED_SIMULATION_TIME_STEP);
115
+ for(let i = 0; i < stepCount; i++){
116
+ this.simulateStep(FIXED_SIMULATION_TIME_STEP, i === 0 ? frameDt : 0);
117
+ }
118
+ }
119
+ applyToNodes(weight = 1.0) {
120
+ const baseNodeStates = new Map();
121
+ const desiredWorldRotations = new Map();
122
+ for (const chain of this._chains){
123
+ this.collectChainNodeRotations(chain, weight, baseNodeStates, desiredWorldRotations);
124
+ }
125
+ const nodes = Array.from(desiredWorldRotations.keys());
126
+ nodes.sort((a, b)=>this.getNodeDepth(a) - this.getNodeDepth(b));
127
+ for (const node of nodes){
128
+ const worldRotation = desiredWorldRotations.get(node);
129
+ if (!worldRotation) {
130
+ continue;
131
+ }
132
+ const parent = node.parent;
133
+ const inputRotation = node.rotation.clone();
134
+ if (parent) {
135
+ const parentWorldRotation = new Quaternion();
136
+ parent.worldMatrix.decompose(null, parentWorldRotation, null);
137
+ const parentInvRotation = Quaternion.conjugate(parentWorldRotation, new Quaternion());
138
+ const localRotation = Quaternion.multiply(parentInvRotation, worldRotation, new Quaternion());
139
+ node.rotation = localRotation;
140
+ this._nodePoseTracker.recordAppliedRotation(node, inputRotation, localRotation);
141
+ } else {
142
+ node.rotation = worldRotation;
143
+ this._nodePoseTracker.recordAppliedRotation(node, inputRotation, worldRotation);
144
+ }
145
+ }
146
+ }
147
+ reset() {
148
+ this._nodePoseTracker.clear(true);
149
+ for (const chain of this._chains){
150
+ chain.reset();
151
+ for (const particle of chain.particles){
152
+ particle.animPosition.set(particle.originalPosition);
153
+ particle.lastFramePosition.set(particle.originalPosition);
154
+ if (particle.positionHistory) {
155
+ particle.positionHistory.length = 0;
156
+ }
157
+ }
158
+ }
159
+ for (const constraint of this._interChainConstraints){
160
+ constraint.lambda = 0;
161
+ }
162
+ this._timeAccumulator = 0;
163
+ this._smoothedParticleTargets = new WeakMap();
164
+ this._smoothedSphereCenters = new WeakMap();
165
+ this._smoothedCapsuleEndpoints = new WeakMap();
166
+ this._smoothedPlaneData = new WeakMap();
167
+ }
168
+ get chains() {
169
+ return this._chains;
170
+ }
171
+ get interChainConstraints() {
172
+ return this._interChainConstraints;
173
+ }
174
+ get gravity() {
175
+ return this._gravity;
176
+ }
177
+ set gravity(gravity) {
178
+ this._gravity.set(gravity);
179
+ }
180
+ get wind() {
181
+ return this._wind;
182
+ }
183
+ set wind(wind) {
184
+ this._wind.set(wind);
185
+ }
186
+ get iterations() {
187
+ return this._iterations;
188
+ }
189
+ set iterations(count) {
190
+ this._iterations = Math.max(1, count);
191
+ }
192
+ get enableInertialForces() {
193
+ return this._enableInertialForces;
194
+ }
195
+ set enableInertialForces(enabled) {
196
+ this._enableInertialForces = enabled;
197
+ }
198
+ get centrifugalScale() {
199
+ return this._centrifugalScale;
200
+ }
201
+ set centrifugalScale(scale) {
202
+ this._centrifugalScale = Math.max(0, scale);
203
+ }
204
+ get coriolisScale() {
205
+ return this._coriolisScale;
206
+ }
207
+ set coriolisScale(scale) {
208
+ this._coriolisScale = Math.max(0, scale);
209
+ }
210
+ get solver() {
211
+ return this._solver;
212
+ }
213
+ set solver(type) {
214
+ if (this._solver !== type) {
215
+ this._solver = type;
216
+ if (type === 'xpbd') {
217
+ this.resetConstraintLambdas();
218
+ }
219
+ }
220
+ }
221
+ get poseFollow() {
222
+ return this._poseFollow;
223
+ }
224
+ set poseFollow(value) {
225
+ const v = Math.max(0, Math.min(1, value));
226
+ this._poseFollow = v;
227
+ this._poseFollowRoot = v;
228
+ this._poseFollowTip = v;
229
+ }
230
+ get poseFollowRoot() {
231
+ return this._poseFollowRoot;
232
+ }
233
+ set poseFollowRoot(value) {
234
+ this._poseFollowRoot = Math.max(0, Math.min(1, value));
235
+ }
236
+ get poseFollowTip() {
237
+ return this._poseFollowTip;
238
+ }
239
+ set poseFollowTip(value) {
240
+ this._poseFollowTip = Math.max(0, Math.min(1, value));
241
+ }
242
+ get poseFollowExponent() {
243
+ return this._poseFollowExponent;
244
+ }
245
+ set poseFollowExponent(value) {
246
+ this._poseFollowExponent = Math.max(0.1, value);
247
+ }
248
+ get maxPoseOffset() {
249
+ return this._maxPoseOffset;
250
+ }
251
+ set maxPoseOffset(value) {
252
+ const v = Math.max(0, value);
253
+ this._maxPoseOffset = v;
254
+ this._maxPoseOffsetRoot = v;
255
+ this._maxPoseOffsetTip = v;
256
+ }
257
+ get maxPoseOffsetRoot() {
258
+ return this._maxPoseOffsetRoot;
259
+ }
260
+ set maxPoseOffsetRoot(value) {
261
+ this._maxPoseOffsetRoot = Math.max(0, value);
262
+ }
263
+ get maxPoseOffsetTip() {
264
+ return this._maxPoseOffsetTip;
265
+ }
266
+ set maxPoseOffsetTip(value) {
267
+ this._maxPoseOffsetTip = Math.max(0, value);
268
+ }
269
+ addCollider(collider) {
270
+ this._colliders.push(collider);
271
+ this._smoothedSphereCenters = new WeakMap();
272
+ this._smoothedCapsuleEndpoints = new WeakMap();
273
+ this._smoothedPlaneData = new WeakMap();
274
+ }
275
+ removeCollider(collider) {
276
+ const index = this._colliders.indexOf(collider);
277
+ if (index >= 0) {
278
+ this._colliders.splice(index, 1);
279
+ this._smoothedSphereCenters = new WeakMap();
280
+ this._smoothedCapsuleEndpoints = new WeakMap();
281
+ this._smoothedPlaneData = new WeakMap();
282
+ return true;
283
+ }
284
+ return false;
285
+ }
286
+ clearColliders() {
287
+ this._colliders = [];
288
+ this._smoothedSphereCenters = new WeakMap();
289
+ this._smoothedCapsuleEndpoints = new WeakMap();
290
+ this._smoothedPlaneData = new WeakMap();
291
+ }
292
+ get colliders() {
293
+ return this._colliders;
294
+ }
80
295
  /**
81
- * Updates the physics simulation for all chains
82
- * @param deltaTime - Time step in seconds
83
- */ update(deltaTime) {
84
- const dt = Math.min(deltaTime, 0.033);
85
- // Save all particle positions before updating
296
+ * Sets a runtime-only node remapping used by particles, anchors and colliders.
297
+ * The authored node references remain unchanged and therefore serialize normally.
298
+ */ setRuntimeNodeMap(nodeMap) {
299
+ this._nodePoseTracker.clear(false);
300
+ this._runtimeNodeMap = nodeMap;
301
+ this._runtimeAnchorOffsets = new WeakMap();
302
+ this._runtimeRestLengths = new WeakMap();
303
+ this.clearRuntimeCaches();
304
+ }
305
+ /**
306
+ * Rebuilds particle history from the current runtime-remapped node pose.
307
+ * This must be called after a hierarchy graft or skeleton rebind.
308
+ */ reinitializeFromCurrentPose(options) {
309
+ this._nodePoseTracker.clear(false);
310
+ const recomputeAnchorOffsets = options?.recomputeAnchorOffsets ?? true;
311
+ const recalculateRestLengths = options?.recalculateRestLengths ?? true;
312
+ this._runtimeAnchorOffsets = new WeakMap();
313
+ this._runtimeRestLengths = new WeakMap();
314
+ for (const chain of this._chains){
315
+ for (const particle of chain.particles){
316
+ const particleNode = this.resolveRuntimeNode(particle.node);
317
+ let position = particleNode ? new Vector3(particleNode.worldMatrix.m03, particleNode.worldMatrix.m13, particleNode.worldMatrix.m23) : particle.position.clone();
318
+ const anchorNode = this.resolveRuntimeNode(particle.anchorNode);
319
+ if (particle.fixed && anchorNode) {
320
+ let anchorOffset = particle.anchorOffset;
321
+ if (recomputeAnchorOffsets && anchorOffset && particleNode) {
322
+ anchorOffset = anchorNode.worldToThis(position, new Vector3());
323
+ this._runtimeAnchorOffsets.set(particle, anchorOffset);
324
+ }
325
+ position = anchorOffset ? anchorNode.worldMatrix.transformPointAffine(anchorOffset, new Vector3()) : new Vector3(anchorNode.worldMatrix.m03, anchorNode.worldMatrix.m13, anchorNode.worldMatrix.m23);
326
+ }
327
+ particle.position.set(position);
328
+ particle.prevPosition.set(position);
329
+ particle.animPosition.set(position);
330
+ particle.lastFramePosition.set(position);
331
+ if (particle.positionHistory) {
332
+ particle.positionHistory.length = 0;
333
+ }
334
+ }
335
+ for (const constraint of chain.constraints){
336
+ constraint.lambda = 0;
337
+ if (recalculateRestLengths) {
338
+ this._runtimeRestLengths.set(constraint, Vector3.distance(chain.particles[constraint.particleA].position, chain.particles[constraint.particleB].position));
339
+ }
340
+ }
341
+ }
342
+ for (const constraint of this._interChainConstraints){
343
+ constraint.lambda = 0;
344
+ if (recalculateRestLengths) {
345
+ const particleA = this._chains[constraint.chainAIndex]?.particles[constraint.particleAIndex];
346
+ const particleB = this._chains[constraint.chainBIndex]?.particles[constraint.particleBIndex];
347
+ if (particleA && particleB) {
348
+ this._runtimeRestLengths.set(constraint, Vector3.distance(particleA.position, particleB.position));
349
+ }
350
+ }
351
+ }
352
+ for (const collider of this._colliders){
353
+ const colliderNode = this.resolveRuntimeNode(collider.node);
354
+ if (colliderNode) {
355
+ updateColliderFromNode(collider, colliderNode);
356
+ }
357
+ }
358
+ this.clearRuntimeCaches();
359
+ }
360
+ simulateStep(dt, inputDeltaTime) {
86
361
  if (this._enableInertialForces) {
87
362
  for (const chain of this._chains){
88
- for (const p of chain.particles){
89
- p.lastFramePosition.set(p.position);
363
+ for (const particle of chain.particles){
364
+ particle.lastFramePosition.set(particle.position);
90
365
  }
91
366
  }
92
367
  }
93
- this.updateFixedParticles();
94
- // Calculate global rotation parameters
368
+ this.updateFixedParticles(inputDeltaTime);
95
369
  let rotationCenter = null;
96
370
  let angularVelocity = null;
97
371
  if (this._enableInertialForces && dt > 0.0001) {
@@ -107,7 +381,6 @@ import { IKUtils } from '../ik/ik_utils.js';
107
381
  const velocity = Vector3.sub(particle.position, particle.prevPosition, new Vector3());
108
382
  velocity.scaleBy(particle.damping);
109
383
  const acceleration = Vector3.add(this._gravity, this._wind, new Vector3());
110
- // Apply inertial forces
111
384
  if (this._enableInertialForces && rotationCenter && angularVelocity) {
112
385
  const inertialAccel = this.calculateInertialAcceleration(particle, rotationCenter, angularVelocity, velocity, this._centrifugalScale, this._coriolisScale);
113
386
  Vector3.add(acceleration, inertialAccel, acceleration);
@@ -119,14 +392,7 @@ import { IKUtils } from '../ik/ik_utils.js';
119
392
  }
120
393
  }
121
394
  if (this._solver === 'xpbd') {
122
- for (const chain of this._chains){
123
- for (const c of chain.constraints){
124
- c.lambda = 0;
125
- }
126
- }
127
- for (const c of this._interChainConstraints){
128
- c.lambda = 0;
129
- }
395
+ this.resetConstraintLambdas();
130
396
  }
131
397
  for(let iter = 0; iter < this._iterations; iter++){
132
398
  for (const chain of this._chains){
@@ -145,21 +411,30 @@ import { IKUtils } from '../ik/ik_utils.js';
145
411
  this.solveInterChainConstraint(constraint);
146
412
  }
147
413
  }
414
+ this.solvePosePreservation(this._iterations);
415
+ this.solveCollisions(inputDeltaTime);
148
416
  }
149
417
  }
150
- updateFixedParticles() {
418
+ updateFixedParticles(deltaTime) {
419
+ const blend = this.getTemporalBlendFactor(deltaTime, DEFAULT_PARTICLE_TARGET_SMOOTHING_TIME);
151
420
  for (const chain of this._chains){
152
421
  for (const particle of chain.particles){
153
- if (particle.fixed && particle.node) {
154
- const worldPos = new Vector3(particle.node.worldMatrix.m03, particle.node.worldMatrix.m13, particle.node.worldMatrix.m23);
155
- particle.position.set(worldPos);
156
- particle.prevPosition.set(worldPos);
157
- // Maintain position history
422
+ const sourceNode = this.resolveRuntimeNode(particle.anchorNode ?? particle.node);
423
+ if (!sourceNode) {
424
+ continue;
425
+ }
426
+ const anchorOffset = this._runtimeAnchorOffsets.get(particle) ?? particle.anchorOffset;
427
+ const worldPos = anchorOffset ? sourceNode.worldMatrix.transformPointAffine(anchorOffset) : new Vector3(sourceNode.worldMatrix.m03, sourceNode.worldMatrix.m13, sourceNode.worldMatrix.m23);
428
+ const smoothedTarget = this.getSmoothedParticleTarget(particle, worldPos, blend);
429
+ particle.animPosition.set(smoothedTarget);
430
+ if (particle.fixed) {
431
+ particle.position.set(smoothedTarget);
432
+ particle.prevPosition.set(smoothedTarget);
158
433
  if (this._enableInertialForces) {
159
434
  if (!particle.positionHistory) {
160
435
  particle.positionHistory = [];
161
436
  }
162
- particle.positionHistory.push(worldPos.clone());
437
+ particle.positionHistory.push(smoothedTarget.clone());
163
438
  if (particle.positionHistory.length > 5) {
164
439
  particle.positionHistory.shift();
165
440
  }
@@ -168,18 +443,125 @@ import { IKUtils } from '../ik/ik_utils.js';
168
443
  }
169
444
  }
170
445
  }
446
+ solvePosePreservation(totalIterations) {
447
+ if (this._poseFollowRoot <= 0 && this._poseFollowTip <= 0) {
448
+ return;
449
+ }
450
+ for (const chain of this._chains){
451
+ const lastIndex = Math.max(1, chain.particles.length - 1);
452
+ for(let i = 0; i < chain.particles.length; i++){
453
+ const particle = chain.particles[i];
454
+ if (particle.fixed) {
455
+ continue;
456
+ }
457
+ const t = Math.pow(i / lastIndex, this._poseFollowExponent);
458
+ const particlePoseFollow = this.lerp(this._poseFollowRoot, this._poseFollowTip, t);
459
+ const iterationFollow = totalIterations > 1 ? 1 - Math.pow(Math.max(0, 1 - particlePoseFollow), 1 / totalIterations) : particlePoseFollow;
460
+ const toAnim = Vector3.sub(particle.animPosition, particle.position, new Vector3());
461
+ const correction = Vector3.scale(toAnim, iterationFollow, new Vector3());
462
+ Vector3.add(particle.position, correction, particle.position);
463
+ const particleMaxPoseOffset = this.lerp(this._maxPoseOffsetRoot, this._maxPoseOffsetTip, t);
464
+ if (particleMaxPoseOffset > 0) {
465
+ const offset = Vector3.sub(particle.position, particle.animPosition, new Vector3());
466
+ const offsetLen = offset.magnitude;
467
+ if (offsetLen > particleMaxPoseOffset && offsetLen > 1e-6) {
468
+ offset.scaleBy(particleMaxPoseOffset / offsetLen);
469
+ Vector3.add(particle.animPosition, offset, particle.position);
470
+ }
471
+ }
472
+ }
473
+ }
474
+ }
475
+ solveCollisions(deltaTime) {
476
+ const blend = this.getTemporalBlendFactor(deltaTime, DEFAULT_COLLIDER_SMOOTHING_TIME);
477
+ const spheres = [];
478
+ const capsules = [];
479
+ const planes = [];
480
+ for (const collider of this._colliders){
481
+ const colliderNode = this.resolveRuntimeNode(collider.node);
482
+ if (colliderNode) {
483
+ updateColliderFromNode(collider, colliderNode);
484
+ }
485
+ if (!collider.enabled) {
486
+ continue;
487
+ }
488
+ switch(collider.type){
489
+ case 'sphere':
490
+ {
491
+ const source = collider;
492
+ spheres.push({
493
+ ...source,
494
+ center: this.getSmoothedSphereCenter(source, blend)
495
+ });
496
+ break;
497
+ }
498
+ case 'capsule':
499
+ {
500
+ const source = collider;
501
+ const endpoints = this.getSmoothedCapsuleEndpoints(source, blend);
502
+ capsules.push({
503
+ ...source,
504
+ start: endpoints.start,
505
+ end: endpoints.end
506
+ });
507
+ break;
508
+ }
509
+ case 'plane':
510
+ {
511
+ const source = collider;
512
+ const plane = this.getSmoothedPlaneData(source, blend);
513
+ planes.push({
514
+ ...source,
515
+ point: plane.point,
516
+ normal: plane.normal
517
+ });
518
+ break;
519
+ }
520
+ }
521
+ }
522
+ for (const chain of this._chains){
523
+ for (const particle of chain.particles){
524
+ if (particle.fixed) {
525
+ continue;
526
+ }
527
+ const positionBeforeCollision = particle.position.clone();
528
+ let collided = false;
529
+ for (const collider of spheres){
530
+ collided = resolveSphereCollision(particle.position, collider) || collided;
531
+ }
532
+ for (const collider of capsules){
533
+ collided = resolveCapsuleCollision(particle.position, collider) || collided;
534
+ }
535
+ for (const collider of planes){
536
+ collided = resolvePlaneCollision(particle.position, collider) || collided;
537
+ }
538
+ if (collided) {
539
+ Vector3.add(particle.prevPosition, Vector3.sub(particle.position, positionBeforeCollision, new Vector3()), particle.prevPosition);
540
+ }
541
+ }
542
+ }
543
+ }
171
544
  calculateGlobalRotation(dt) {
172
545
  const fixedParticles = [];
173
546
  const velocities = [];
547
+ const drivenNodes = new Set();
174
548
  for (const chain of this._chains){
175
- for (const p of chain.particles){
176
- if (!p.fixed) {
549
+ for (const particle of chain.particles){
550
+ const node = this.resolveRuntimeNode(particle.node);
551
+ if (node) {
552
+ drivenNodes.add(node);
553
+ }
554
+ }
555
+ }
556
+ for (const chain of this._chains){
557
+ for (const particle of chain.particles){
558
+ if (!particle.fixed || !this.isExternalInertialAnchor(particle, drivenNodes)) {
177
559
  continue;
178
560
  }
179
- const velocity = Vector3.sub(p.position, p.lastFramePosition, new Vector3());
561
+ const velocity = Vector3.sub(particle.position, particle.lastFramePosition, new Vector3());
180
562
  velocity.scaleBy(1.0 / dt);
181
563
  if (velocity.magnitudeSq > 0.001) {
182
- fixedParticles.push(p);
564
+ fixedParticles.push(particle);
183
565
  velocities.push(velocity);
184
566
  }
185
567
  }
@@ -190,18 +572,16 @@ import { IKUtils } from '../ik/ik_utils.js';
190
572
  omega: new Vector3(0, 0, 0)
191
573
  };
192
574
  }
193
- // Estimate rotation center
194
575
  let center;
195
576
  if (fixedParticles.length === 1) {
196
577
  center = this.estimateRotationCenterFromHistory(fixedParticles[0], velocities[0]);
197
578
  } else {
198
579
  center = new Vector3(0, 0, 0);
199
- for (const p of fixedParticles){
200
- Vector3.add(center, p.position, center);
580
+ for (const particle of fixedParticles){
581
+ Vector3.add(center, particle.position, center);
201
582
  }
202
583
  center.scaleBy(1.0 / fixedParticles.length);
203
584
  }
204
- // Estimate angular velocity
205
585
  let sumOmega = new Vector3(0, 0, 0);
206
586
  let count = 0;
207
587
  for(let i = 0; i < fixedParticles.length; i++){
@@ -236,16 +616,14 @@ import { IKUtils } from '../ik/ik_utils.js';
236
616
  if (perpDir.magnitudeSq < 0.0001) {
237
617
  perpDir.set(new Vector3(1, 0, 0));
238
618
  } else {
239
- Vector3.normalize(perpDir, perpDir);
619
+ perpDir.inplaceNormalize();
240
620
  }
241
- const center = Vector3.add(particle.position, Vector3.scale(perpDir, estimatedRadius, new Vector3()), new Vector3());
242
- return center;
621
+ return Vector3.add(particle.position, Vector3.scale(perpDir, estimatedRadius, new Vector3()), new Vector3());
243
622
  }
244
623
  const p1 = history[0];
245
624
  const p2 = history[Math.floor(history.length / 2)];
246
625
  const p3 = history[history.length - 1];
247
- const center = this.calculateCircleCenter(p1, p2, p3);
248
- return center;
626
+ return this.calculateCircleCenter(p1, p2, p3);
249
627
  }
250
628
  calculateCircleCenter(p1, p2, p3) {
251
629
  const mid12 = Vector3.scale(Vector3.add(p1, p2, new Vector3()), 0.5, new Vector3());
@@ -256,17 +634,14 @@ import { IKUtils } from '../ik/ik_utils.js';
256
634
  if (normal.magnitudeSq < 0.0001) {
257
635
  return Vector3.scale(Vector3.add(Vector3.add(p1, p2, new Vector3()), p3, new Vector3()), 1.0 / 3.0, new Vector3());
258
636
  }
259
- Vector3.normalize(normal, normal);
260
- const perp12 = Vector3.cross(dir12, normal, new Vector3());
261
- Vector3.normalize(perp12, perp12);
262
- const perp23 = Vector3.cross(dir23, normal, new Vector3());
263
- Vector3.normalize(perp23, perp23);
637
+ normal.inplaceNormalize();
638
+ const perp12 = Vector3.cross(dir12, normal, new Vector3()).inplaceNormalize();
639
+ const perp23 = Vector3.cross(dir23, normal, new Vector3()).inplaceNormalize();
264
640
  const diff = Vector3.sub(mid23, mid12, new Vector3());
265
641
  const det = perp12.x * perp23.y - perp12.y * perp23.x;
266
642
  if (Math.abs(det) > 0.0001) {
267
643
  const t = (diff.x * perp23.y - diff.y * perp23.x) / det;
268
- const center = Vector3.add(mid12, Vector3.scale(perp12, t, new Vector3()), new Vector3());
269
- return center;
644
+ return Vector3.add(mid12, Vector3.scale(perp12, t, new Vector3()), new Vector3());
270
645
  }
271
646
  return Vector3.scale(Vector3.add(Vector3.add(p1, p2, new Vector3()), p3, new Vector3()), 1.0 / 3.0, new Vector3());
272
647
  }
@@ -277,8 +652,7 @@ import { IKUtils } from '../ik/ik_utils.js';
277
652
  centrifugalAccel.scaleBy(centrifugalScale);
278
653
  const coriolisAccel = Vector3.cross(angularVelocity, particleVelocity, new Vector3());
279
654
  coriolisAccel.scaleBy(-2 * coriolisScale);
280
- const totalAccel = Vector3.add(centrifugalAccel, coriolisAccel, new Vector3());
281
- return totalAccel;
655
+ return Vector3.add(centrifugalAccel, coriolisAccel, new Vector3());
282
656
  }
283
657
  solveConstraint(chain, constraint) {
284
658
  const pA = chain.particles[constraint.particleA];
@@ -288,7 +662,7 @@ import { IKUtils } from '../ik/ik_utils.js';
288
662
  if (currentLength < 0.0001) {
289
663
  return;
290
664
  }
291
- const diff = (currentLength - constraint.restLength) / currentLength;
665
+ const diff = (currentLength - this.getRuntimeRestLength(constraint)) / currentLength;
292
666
  const correction = Vector3.scale(delta, diff * constraint.stiffness * 0.5, new Vector3());
293
667
  if (!pA.fixed) {
294
668
  Vector3.add(pA.position, correction, pA.position);
@@ -307,7 +681,7 @@ import { IKUtils } from '../ik/ik_utils.js';
307
681
  if (currentLength < 0.0001) {
308
682
  return;
309
683
  }
310
- const diff = (currentLength - constraint.restLength) / currentLength;
684
+ const diff = (currentLength - this.getRuntimeRestLength(constraint)) / currentLength;
311
685
  const correction = Vector3.scale(delta, diff * constraint.stiffness * 0.5, new Vector3());
312
686
  if (!pA.fixed) {
313
687
  Vector3.add(pA.position, correction, pA.position);
@@ -330,7 +704,7 @@ import { IKUtils } from '../ik/ik_utils.js';
330
704
  if (currentLength < 0.0001) {
331
705
  return;
332
706
  }
333
- const C = currentLength - constraint.restLength;
707
+ const C = currentLength - this.getRuntimeRestLength(constraint);
334
708
  const alphaTilde = constraint.compliance / (dt * dt);
335
709
  const deltaLambda = (-C - alphaTilde * constraint.lambda) / (wSum + alphaTilde);
336
710
  constraint.lambda += deltaLambda;
@@ -358,7 +732,7 @@ import { IKUtils } from '../ik/ik_utils.js';
358
732
  if (currentLength < 0.0001) {
359
733
  return;
360
734
  }
361
- const C = currentLength - constraint.restLength;
735
+ const C = currentLength - this.getRuntimeRestLength(constraint);
362
736
  const alphaTilde = constraint.compliance / (dt * dt);
363
737
  const deltaLambda = (-C - alphaTilde * constraint.lambda) / (wSum + alphaTilde);
364
738
  constraint.lambda += deltaLambda;
@@ -370,33 +744,40 @@ import { IKUtils } from '../ik/ik_utils.js';
370
744
  Vector3.add(pB.position, Vector3.scale(n, wB * deltaLambda, new Vector3()), pB.position);
371
745
  }
372
746
  }
373
- /**
374
- * Applies simulation results to scene nodes
375
- * @param weight - Blend weight [0-1] (default: 1.0)
376
- */ applyToNodes(weight = 1.0) {
377
- for (const chain of this._chains){
378
- this.applyChainToNodes(chain, weight);
379
- }
380
- }
381
- applyChainToNodes(chain, weight) {
747
+ collectChainNodeRotations(chain, weight, baseNodeStates, desiredWorldRotations) {
382
748
  for(let i = 0; i < chain.particles.length - 1; i++){
383
749
  const particle = chain.particles[i];
384
750
  const nextParticle = chain.particles[i + 1];
385
- if (!particle.node) {
751
+ const node = this.resolveRuntimeNode(particle.node);
752
+ if (!node) {
386
753
  continue;
387
754
  }
388
- // Get current bone direction from node's world matrix (before physics)
389
- const currentBonePos = new Vector3(particle.node.worldMatrix.m03, particle.node.worldMatrix.m13, particle.node.worldMatrix.m23);
390
- const nextNode = nextParticle.node;
755
+ const nextNode = this.resolveRuntimeNode(nextParticle.node);
391
756
  if (!nextNode) {
392
757
  continue;
393
758
  }
394
- const nextBonePos = new Vector3(nextNode.worldMatrix.m03, nextNode.worldMatrix.m13, nextNode.worldMatrix.m23);
395
- const originalDir = Vector3.sub(nextBonePos, currentBonePos, new Vector3());
396
- // Get current bone rotation from node's world matrix
397
- const currentBoneRotation = new Quaternion();
398
- particle.node.worldMatrix.decompose(null, currentBoneRotation, null);
399
- // Calculate new direction from physics simulation
759
+ let nodeState = baseNodeStates.get(node);
760
+ if (!nodeState) {
761
+ const currentBoneRotation = new Quaternion();
762
+ node.worldMatrix.decompose(null, currentBoneRotation, null);
763
+ nodeState = {
764
+ position: new Vector3(node.worldMatrix.m03, node.worldMatrix.m13, node.worldMatrix.m23),
765
+ rotation: currentBoneRotation
766
+ };
767
+ baseNodeStates.set(node, nodeState);
768
+ }
769
+ let nextNodeState = baseNodeStates.get(nextNode);
770
+ if (!nextNodeState) {
771
+ const nextBoneRotation = new Quaternion();
772
+ nextNode.worldMatrix.decompose(null, nextBoneRotation, null);
773
+ nextNodeState = {
774
+ position: new Vector3(nextNode.worldMatrix.m03, nextNode.worldMatrix.m13, nextNode.worldMatrix.m23),
775
+ rotation: nextBoneRotation
776
+ };
777
+ baseNodeStates.set(nextNode, nextNodeState);
778
+ }
779
+ const originalDir = Vector3.sub(nextNodeState.position, nodeState.position, new Vector3());
780
+ const currentBoneRotation = nodeState.rotation;
400
781
  const newDir = Vector3.sub(nextParticle.position, particle.position, new Vector3());
401
782
  const deltaRotation = new Quaternion();
402
783
  IKUtils.fromToRotation(originalDir, newDir, deltaRotation);
@@ -404,100 +785,117 @@ import { IKUtils } from '../ik/ik_utils.js';
404
785
  if (weight < 1) {
405
786
  Quaternion.slerp(currentBoneRotation, worldRotation, weight, worldRotation);
406
787
  }
407
- const parent = particle.node.parent;
408
- if (parent) {
409
- const parentWorldRotation = new Quaternion();
410
- parent.worldMatrix.decompose(null, parentWorldRotation, null);
411
- const parentInvRotation = Quaternion.conjugate(parentWorldRotation, new Quaternion());
412
- const localRotation = Quaternion.multiply(parentInvRotation, worldRotation, new Quaternion());
413
- particle.node.rotation = localRotation;
414
- } else {
415
- particle.node.rotation = worldRotation;
416
- }
788
+ desiredWorldRotations.set(node, worldRotation);
417
789
  }
418
790
  }
419
- /**
420
- * Resets the simulation to initial state
421
- */ reset() {
422
- for (const chain of this._chains){
423
- chain.reset();
791
+ getNodeDepth(node) {
792
+ let depth = 0;
793
+ let current = node?.parent;
794
+ while(current){
795
+ depth++;
796
+ current = current.parent;
424
797
  }
798
+ return depth;
425
799
  }
426
- /**
427
- * Gets the spring chains
428
- */ get chains() {
429
- return this._chains;
430
- }
431
- /**
432
- * Gets the inter-chain constraints
433
- */ get interChainConstraints() {
434
- return this._interChainConstraints;
435
- }
436
- /**
437
- * Gets the current gravity
438
- */ get gravity() {
439
- return this._gravity;
440
- }
441
- set gravity(gravity) {
442
- this._gravity.set(gravity);
443
- }
444
- /**
445
- * Gets the current wind
446
- */ get wind() {
447
- return this._wind;
448
- }
449
- set wind(wind) {
450
- this._wind.set(wind);
451
- }
452
- /**
453
- * Gets the number of iterations
454
- */ get iterations() {
455
- return this._iterations;
456
- }
457
- set iterations(count) {
458
- this._iterations = Math.max(1, count);
459
- }
460
- /**
461
- * Gets whether inertial forces are enabled
462
- */ get enableInertialForces() {
463
- return this._enableInertialForces;
464
- }
465
- set enableInertialForces(enabled) {
466
- this._enableInertialForces = enabled;
467
- }
468
- /**
469
- * Gets the centrifugal force scale
470
- */ get centrifugalScale() {
471
- return this._centrifugalScale;
472
- }
473
- set centrifugalScale(scale) {
474
- this._centrifugalScale = Math.max(0, scale);
475
- }
476
- /**
477
- * Gets the Coriolis force scale
478
- */ get coriolisScale() {
479
- return this._coriolisScale;
480
- }
481
- set coriolisScale(scale) {
482
- this._coriolisScale = Math.max(0, scale);
800
+ resetConstraintLambdas() {
801
+ for (const chain of this._chains){
802
+ for (const constraint of chain.constraints){
803
+ constraint.lambda = 0;
804
+ }
805
+ }
806
+ for (const constraint of this._interChainConstraints){
807
+ constraint.lambda = 0;
808
+ }
483
809
  }
484
- get solver() {
485
- return this._solver;
810
+ resolveRuntimeNode(node) {
811
+ return node ? this._runtimeNodeMap?.get(node) ?? node : null;
486
812
  }
487
- set solver(type) {
488
- if (this._solver !== type) {
489
- this._solver = type;
490
- if (type === 'xpbd') {
491
- for (const chain of this._chains){
492
- for (const c of chain.constraints){
493
- c.lambda = 0;
494
- }
495
- }
496
- for (const c of this._interChainConstraints){
497
- c.lambda = 0;
498
- }
813
+ isExternalInertialAnchor(particle, drivenNodes) {
814
+ const particleNode = this.resolveRuntimeNode(particle.node);
815
+ let current = this.resolveRuntimeNode(particle.anchorNode ?? particle.node);
816
+ while(current){
817
+ if (current !== particleNode && drivenNodes.has(current)) {
818
+ return false;
499
819
  }
820
+ current = current.parent;
821
+ }
822
+ return true;
823
+ }
824
+ getRuntimeRestLength(constraint) {
825
+ return this._runtimeRestLengths.get(constraint) ?? constraint.restLength;
826
+ }
827
+ clearRuntimeCaches() {
828
+ this._timeAccumulator = 0;
829
+ this._smoothedParticleTargets = new WeakMap();
830
+ this._smoothedSphereCenters = new WeakMap();
831
+ this._smoothedCapsuleEndpoints = new WeakMap();
832
+ this._smoothedPlaneData = new WeakMap();
833
+ }
834
+ lerp(a, b, t) {
835
+ return a + (b - a) * t;
836
+ }
837
+ getTemporalBlendFactor(deltaTime, smoothingTime) {
838
+ const dt = Math.min(Math.max(Number(deltaTime) || 0, 0), MAX_ACCUMULATED_SIMULATION_TIME);
839
+ if (smoothingTime <= 0) {
840
+ return 1;
841
+ }
842
+ if (dt <= 0) {
843
+ return 0;
844
+ }
845
+ return 1 - Math.exp(-dt / smoothingTime);
846
+ }
847
+ getSmoothedParticleTarget(target, current, blend) {
848
+ const cached = this._smoothedParticleTargets.get(target);
849
+ if (!cached || blend >= 1) {
850
+ const next = current.clone();
851
+ this._smoothedParticleTargets.set(target, next);
852
+ return next;
853
+ }
854
+ cached.setXYZ(cached.x + (current.x - cached.x) * blend, cached.y + (current.y - cached.y) * blend, cached.z + (current.z - cached.z) * blend);
855
+ return cached;
856
+ }
857
+ getSmoothedSphereCenter(collider, blend) {
858
+ const current = collider.center?.clone() ?? new Vector3();
859
+ const cached = this._smoothedSphereCenters.get(collider);
860
+ if (!cached || blend >= 1) {
861
+ this._smoothedSphereCenters.set(collider, current);
862
+ return current;
863
+ }
864
+ cached.setXYZ(cached.x + (current.x - cached.x) * blend, cached.y + (current.y - cached.y) * blend, cached.z + (current.z - cached.z) * blend);
865
+ return cached;
866
+ }
867
+ getSmoothedCapsuleEndpoints(collider, blend) {
868
+ const currentStart = collider.start?.clone() ?? new Vector3();
869
+ const currentEnd = collider.end?.clone() ?? new Vector3();
870
+ const cached = this._smoothedCapsuleEndpoints.get(collider);
871
+ if (!cached || blend >= 1) {
872
+ const next = {
873
+ start: currentStart,
874
+ end: currentEnd
875
+ };
876
+ this._smoothedCapsuleEndpoints.set(collider, next);
877
+ return next;
878
+ }
879
+ cached.start.setXYZ(cached.start.x + (currentStart.x - cached.start.x) * blend, cached.start.y + (currentStart.y - cached.start.y) * blend, cached.start.z + (currentStart.z - cached.start.z) * blend);
880
+ cached.end.setXYZ(cached.end.x + (currentEnd.x - cached.end.x) * blend, cached.end.y + (currentEnd.y - cached.end.y) * blend, cached.end.z + (currentEnd.z - cached.end.z) * blend);
881
+ return cached;
882
+ }
883
+ getSmoothedPlaneData(collider, blend) {
884
+ const currentPoint = collider.point?.clone() ?? new Vector3();
885
+ const currentNormal = collider.normal?.clone() ?? new Vector3(0, 1, 0);
886
+ const cached = this._smoothedPlaneData.get(collider);
887
+ if (!cached || blend >= 1) {
888
+ const next = {
889
+ point: currentPoint,
890
+ normal: currentNormal
891
+ };
892
+ this._smoothedPlaneData.set(collider, next);
893
+ return next;
500
894
  }
895
+ cached.point.setXYZ(cached.point.x + (currentPoint.x - cached.point.x) * blend, cached.point.y + (currentPoint.y - cached.point.y) * blend, cached.point.z + (currentPoint.z - cached.point.z) * blend);
896
+ cached.normal.setXYZ(cached.normal.x + (currentNormal.x - cached.normal.x) * blend, cached.normal.y + (currentNormal.y - cached.normal.y) * blend, cached.normal.z + (currentNormal.z - cached.normal.z) * blend);
897
+ cached.normal.inplaceNormalize();
898
+ return cached;
501
899
  }
502
900
  }
503
901