@zephyr3d/scene 0.8.2 → 0.9.1

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 (241) hide show
  1. package/dist/animation/animationset.js +473 -60
  2. package/dist/animation/animationset.js.map +1 -1
  3. package/dist/animation/cloth/gpu_cloth_system.js +2516 -0
  4. package/dist/animation/cloth/gpu_cloth_system.js.map +1 -0
  5. package/dist/animation/fixed_geometry_cache_track.js +138 -0
  6. package/dist/animation/fixed_geometry_cache_track.js.map +1 -0
  7. package/dist/animation/geometry_cache_utils.js +120 -0
  8. package/dist/animation/geometry_cache_utils.js.map +1 -0
  9. package/dist/animation/ik_modifier.js +9 -1
  10. package/dist/animation/ik_modifier.js.map +1 -1
  11. package/dist/animation/joint_dynamics/collision.js +469 -0
  12. package/dist/animation/joint_dynamics/collision.js.map +1 -0
  13. package/dist/animation/joint_dynamics/constraints.js +329 -0
  14. package/dist/animation/joint_dynamics/constraints.js.map +1 -0
  15. package/dist/animation/joint_dynamics/controller.js +699 -0
  16. package/dist/animation/joint_dynamics/controller.js.map +1 -0
  17. package/dist/animation/joint_dynamics/joint_dynamics_system.js +393 -0
  18. package/dist/animation/joint_dynamics/joint_dynamics_system.js.map +1 -0
  19. package/dist/animation/joint_dynamics/solver.js +794 -0
  20. package/dist/animation/joint_dynamics/solver.js.map +1 -0
  21. package/dist/animation/joint_dynamics/types.js +19 -0
  22. package/dist/animation/joint_dynamics/types.js.map +1 -0
  23. package/dist/animation/joint_dynamics/vrm_spring_bone.js +46 -0
  24. package/dist/animation/joint_dynamics/vrm_spring_bone.js.map +1 -0
  25. package/dist/animation/joint_dynamics_modifier.js +51 -0
  26. package/dist/animation/joint_dynamics_modifier.js.map +1 -0
  27. package/dist/animation/pca_geometry_cache_track.js +222 -0
  28. package/dist/animation/pca_geometry_cache_track.js.map +1 -0
  29. package/dist/animation/skeleton.js +2011 -139
  30. package/dist/animation/skeleton.js.map +1 -1
  31. package/dist/animation/skeleton_modifier.js +3 -5
  32. package/dist/animation/skeleton_modifier.js.map +1 -1
  33. package/dist/animation/spring/multi_chain_spring_system.js +2 -0
  34. package/dist/animation/spring/multi_chain_spring_system.js.map +1 -1
  35. package/dist/animation/spring/spring_chain.js +3 -2
  36. package/dist/animation/spring/spring_chain.js.map +1 -1
  37. package/dist/animation/spring/spring_collider.js +35 -5
  38. package/dist/animation/spring/spring_collider.js.map +1 -1
  39. package/dist/animation/spring/spring_particle.js +1 -0
  40. package/dist/animation/spring/spring_particle.js.map +1 -1
  41. package/dist/animation/spring/spring_system.js +325 -28
  42. package/dist/animation/spring/spring_system.js.map +1 -1
  43. package/dist/animation/spring2/collision.js +469 -0
  44. package/dist/animation/spring2/collision.js.map +1 -0
  45. package/dist/animation/spring2/constraints.js +329 -0
  46. package/dist/animation/spring2/constraints.js.map +1 -0
  47. package/dist/animation/spring2/controller.js +434 -0
  48. package/dist/animation/spring2/controller.js.map +1 -0
  49. package/dist/animation/spring2/math.js +16 -0
  50. package/dist/animation/spring2/math.js.map +1 -0
  51. package/dist/animation/spring2/solver.js +624 -0
  52. package/dist/animation/spring2/solver.js.map +1 -0
  53. package/dist/animation/spring2/spring_system.js +118 -0
  54. package/dist/animation/spring2/spring_system.js.map +1 -0
  55. package/dist/animation/spring2/types.js +19 -0
  56. package/dist/animation/spring2/types.js.map +1 -0
  57. package/dist/animation/spring_modifier.js +17 -1
  58. package/dist/animation/spring_modifier.js.map +1 -1
  59. package/dist/app/engine.js +42 -15
  60. package/dist/app/engine.js.map +1 -1
  61. package/dist/app/runtimescript.js +132 -1
  62. package/dist/app/runtimescript.js.map +1 -1
  63. package/dist/app/screen.js +3 -3
  64. package/dist/app/screen.js.map +1 -1
  65. package/dist/app/scriptingsystem.js +69 -37
  66. package/dist/app/scriptingsystem.js.map +1 -1
  67. package/dist/app/scriptregistry.js +14 -3
  68. package/dist/app/scriptregistry.js.map +1 -1
  69. package/dist/asset/assetmanager.js +2 -1
  70. package/dist/asset/assetmanager.js.map +1 -1
  71. package/dist/asset/loaders/zabc/zabc_loader.js +570 -0
  72. package/dist/asset/loaders/zabc/zabc_loader.js.map +1 -0
  73. package/dist/asset/model.js +192 -0
  74. package/dist/asset/model.js.map +1 -1
  75. package/dist/camera/base.js +26 -7
  76. package/dist/camera/base.js.map +1 -1
  77. package/dist/camera/camera.js +146 -24
  78. package/dist/camera/camera.js.map +1 -1
  79. package/dist/camera/fps.js +2 -2
  80. package/dist/camera/fps.js.map +1 -1
  81. package/dist/camera/orbit.js +2 -2
  82. package/dist/camera/orbit.js.map +1 -1
  83. package/dist/index.d.ts +11728 -8535
  84. package/dist/index.js +29 -8
  85. package/dist/index.js.map +1 -1
  86. package/dist/material/lambert.js +1 -1
  87. package/dist/material/lambert.js.map +1 -1
  88. package/dist/material/material.js +9 -3
  89. package/dist/material/material.js.map +1 -1
  90. package/dist/material/meshmaterial.js +54 -7
  91. package/dist/material/meshmaterial.js.map +1 -1
  92. package/dist/material/mixins/lightmodel/blinnphong.js +17 -7
  93. package/dist/material/mixins/lightmodel/blinnphong.js.map +1 -1
  94. package/dist/material/mixins/lightmodel/lambert.js +7 -2
  95. package/dist/material/mixins/lightmodel/lambert.js.map +1 -1
  96. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js +108 -16
  97. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js.map +1 -1
  98. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js +209 -14
  99. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js.map +1 -1
  100. package/dist/material/mixins/lightmodel/pbrspecularglossness.js +24 -12
  101. package/dist/material/mixins/lightmodel/pbrspecularglossness.js.map +1 -1
  102. package/dist/material/mixins/lit.js +11 -8
  103. package/dist/material/mixins/lit.js.map +1 -1
  104. package/dist/material/mixins/pbr/common.js +72 -5
  105. package/dist/material/mixins/pbr/common.js.map +1 -1
  106. package/dist/material/pbrblueprint.js +148 -1
  107. package/dist/material/pbrblueprint.js.map +1 -1
  108. package/dist/material/pbrmr.js +115 -2
  109. package/dist/material/pbrmr.js.map +1 -1
  110. package/dist/material/shader/helper.js +10 -4
  111. package/dist/material/shader/helper.js.map +1 -1
  112. package/dist/material/water.js +2 -2
  113. package/dist/material/water.js.map +1 -1
  114. package/dist/posteffect/coloradjust.js +145 -0
  115. package/dist/posteffect/coloradjust.js.map +1 -0
  116. package/dist/posteffect/taa.js +68 -32
  117. package/dist/posteffect/taa.js.map +1 -1
  118. package/dist/render/cluster_light.js +17 -12
  119. package/dist/render/cluster_light.js.map +1 -1
  120. package/dist/render/deferredlightpass.js +510 -0
  121. package/dist/render/deferredlightpass.js.map +1 -0
  122. package/dist/render/deferredshadowlightpass.js +428 -0
  123. package/dist/render/deferredshadowlightpass.js.map +1 -0
  124. package/dist/render/drawable.js.map +1 -1
  125. package/dist/render/drawable_mixin.js +21 -12
  126. package/dist/render/drawable_mixin.js.map +1 -1
  127. package/dist/render/envlight.js +20 -22
  128. package/dist/render/envlight.js.map +1 -1
  129. package/dist/render/gbufferpass.js +50 -0
  130. package/dist/render/gbufferpass.js.map +1 -0
  131. package/dist/render/lightpass.js +1 -2
  132. package/dist/render/lightpass.js.map +1 -1
  133. package/dist/render/primitive.js +1 -1
  134. package/dist/render/primitive.js.map +1 -1
  135. package/dist/render/render_queue.js +3 -2
  136. package/dist/render/render_queue.js.map +1 -1
  137. package/dist/render/renderer.js +5 -358
  138. package/dist/render/renderer.js.map +1 -1
  139. package/dist/render/rendergraph/executor.js +15 -12
  140. package/dist/render/rendergraph/executor.js.map +1 -1
  141. package/dist/render/rendergraph/forward_plus_builder.js +144 -88
  142. package/dist/render/rendergraph/forward_plus_builder.js.map +1 -1
  143. package/dist/render/rendergraph/history_resource_manager.js +178 -0
  144. package/dist/render/rendergraph/history_resource_manager.js.map +1 -0
  145. package/dist/render/rendergraph/rendergraph.js +12 -1
  146. package/dist/render/rendergraph/rendergraph.js.map +1 -1
  147. package/dist/render/rendergraph/types.js +2 -2
  148. package/dist/render/rendergraph/types.js.map +1 -1
  149. package/dist/render/sky.js +26 -9
  150. package/dist/render/sky.js.map +1 -1
  151. package/dist/scene/basesprite.js +4 -3
  152. package/dist/scene/basesprite.js.map +1 -1
  153. package/dist/scene/batchgroup.js +4 -4
  154. package/dist/scene/environment.js +7 -4
  155. package/dist/scene/environment.js.map +1 -1
  156. package/dist/scene/light.js +184 -18
  157. package/dist/scene/light.js.map +1 -1
  158. package/dist/scene/mesh.js +30 -0
  159. package/dist/scene/mesh.js.map +1 -1
  160. package/dist/scene/particlesys.js +3 -2
  161. package/dist/scene/particlesys.js.map +1 -1
  162. package/dist/scene/raycast_visitor.js +29 -3
  163. package/dist/scene/raycast_visitor.js.map +1 -1
  164. package/dist/scene/scene.js +86 -19
  165. package/dist/scene/scene.js.map +1 -1
  166. package/dist/scene/scene_node.js +87 -31
  167. package/dist/scene/scene_node.js.map +1 -1
  168. package/dist/scene/script_attachment.js +59 -0
  169. package/dist/scene/script_attachment.js.map +1 -0
  170. package/dist/scene/terrain-cm/grass.js +3 -2
  171. package/dist/scene/terrain-cm/grass.js.map +1 -1
  172. package/dist/scene/terrain-cm/terrain-cm.js +4 -3
  173. package/dist/scene/terrain-cm/terrain-cm.js.map +1 -1
  174. package/dist/scene/water.js +3 -2
  175. package/dist/scene/water.js.map +1 -1
  176. package/dist/shaders/shadow.js +2 -2
  177. package/dist/shaders/shadow.js.map +1 -1
  178. package/dist/shadow/shadowmapper.js +74 -4
  179. package/dist/shadow/shadowmapper.js.map +1 -1
  180. package/dist/shapes/box.js +163 -0
  181. package/dist/shapes/box.js.map +1 -1
  182. package/dist/shapes/capsule.js +216 -0
  183. package/dist/shapes/capsule.js.map +1 -0
  184. package/dist/shapes/cylinder.js +91 -0
  185. package/dist/shapes/cylinder.js.map +1 -1
  186. package/dist/shapes/plane.js +32 -0
  187. package/dist/shapes/plane.js.map +1 -1
  188. package/dist/shapes/tetrahedron.js +211 -0
  189. package/dist/shapes/tetrahedron.js.map +1 -1
  190. package/dist/shapes/torus.js +173 -0
  191. package/dist/shapes/torus.js.map +1 -1
  192. package/dist/utility/blueprint/common/math.js +111 -1
  193. package/dist/utility/blueprint/common/math.js.map +1 -1
  194. package/dist/utility/blueprint/material/inputs.js +235 -1
  195. package/dist/utility/blueprint/material/inputs.js.map +1 -1
  196. package/dist/utility/blueprint/material/ir.js +154 -10
  197. package/dist/utility/blueprint/material/ir.js.map +1 -1
  198. package/dist/utility/blueprint/material/texture.js +273 -19
  199. package/dist/utility/blueprint/material/texture.js.map +1 -1
  200. package/dist/utility/serialization/json.js +12 -2
  201. package/dist/utility/serialization/json.js.map +1 -1
  202. package/dist/utility/serialization/manager.js +30 -11
  203. package/dist/utility/serialization/manager.js.map +1 -1
  204. package/dist/utility/serialization/scene/animation.js +250 -7
  205. package/dist/utility/serialization/scene/animation.js.map +1 -1
  206. package/dist/utility/serialization/scene/batch.js +4 -2
  207. package/dist/utility/serialization/scene/batch.js.map +1 -1
  208. package/dist/utility/serialization/scene/camera.js +154 -2
  209. package/dist/utility/serialization/scene/camera.js.map +1 -1
  210. package/dist/utility/serialization/scene/cloth_script.js +834 -0
  211. package/dist/utility/serialization/scene/cloth_script.js.map +1 -0
  212. package/dist/utility/serialization/scene/common.js +7 -0
  213. package/dist/utility/serialization/scene/common.js.map +1 -1
  214. package/dist/utility/serialization/scene/light.js +120 -4
  215. package/dist/utility/serialization/scene/light.js.map +1 -1
  216. package/dist/utility/serialization/scene/material.js +453 -1
  217. package/dist/utility/serialization/scene/material.js.map +1 -1
  218. package/dist/utility/serialization/scene/mesh.js +4 -2
  219. package/dist/utility/serialization/scene/mesh.js.map +1 -1
  220. package/dist/utility/serialization/scene/node.js +298 -10
  221. package/dist/utility/serialization/scene/node.js.map +1 -1
  222. package/dist/utility/serialization/scene/particle.js +4 -2
  223. package/dist/utility/serialization/scene/particle.js.map +1 -1
  224. package/dist/utility/serialization/scene/primitive.js +100 -3
  225. package/dist/utility/serialization/scene/primitive.js.map +1 -1
  226. package/dist/utility/serialization/scene/scene.js +130 -3
  227. package/dist/utility/serialization/scene/scene.js.map +1 -1
  228. package/dist/utility/serialization/scene/script.js +66 -0
  229. package/dist/utility/serialization/scene/script.js.map +1 -0
  230. package/dist/utility/serialization/scene/spring_script.js +596 -0
  231. package/dist/utility/serialization/scene/spring_script.js.map +1 -0
  232. package/dist/utility/serialization/scene/sprite.js +5 -3
  233. package/dist/utility/serialization/scene/sprite.js.map +1 -1
  234. package/dist/utility/serialization/scene/terrain.js +4 -2
  235. package/dist/utility/serialization/scene/terrain.js.map +1 -1
  236. package/dist/utility/serialization/scene/water.js +4 -2
  237. package/dist/utility/serialization/scene/water.js.map +1 -1
  238. package/dist/utility/serialization/types.js.map +1 -1
  239. package/dist/values.js +5 -1
  240. package/dist/values.js.map +1 -1
  241. package/package.json +3 -3
@@ -1,7 +1,12 @@
1
1
  import { Vector3, Quaternion } from '@zephyr3d/base';
2
2
  import { IKUtils } from '../ik/ik_utils.js';
3
- import { updateColliderFromNode, resolvePlaneCollision, resolveCapsuleCollision, resolveSphereCollision } from './spring_collider.js';
3
+ import { updateColliderFromNode, resolveSphereCollision, resolveCapsuleCollision, resolvePlaneCollision } from './spring_collider.js';
4
4
 
5
+ const FIXED_SIMULATION_TIME_STEP = 1 / 60;
6
+ const MAX_ACCUMULATED_SIMULATION_TIME = 1 / 20;
7
+ const MAX_SIMULATION_STEPS_PER_UPDATE = Math.max(1, Math.ceil(MAX_ACCUMULATED_SIMULATION_TIME / FIXED_SIMULATION_TIME_STEP));
8
+ const DEFAULT_PARTICLE_TARGET_SMOOTHING_TIME = 1 / 30;
9
+ const DEFAULT_COLLIDER_SMOOTHING_TIME = 1 / 30;
5
10
  /**
6
11
  * Physics engine for spring-based particle simulation
7
12
  * Uses Verlet integration and iterative constraint solving
@@ -17,6 +22,18 @@ import { updateColliderFromNode, resolvePlaneCollision, resolveCapsuleCollision,
17
22
  _coriolisScale;
18
23
  _colliders;
19
24
  _solver;
25
+ _poseFollow;
26
+ _maxPoseOffset;
27
+ _poseFollowRoot;
28
+ _poseFollowTip;
29
+ _poseFollowExponent;
30
+ _maxPoseOffsetRoot;
31
+ _maxPoseOffsetTip;
32
+ _timeAccumulator;
33
+ _smoothedParticleTargets;
34
+ _smoothedSphereCenters;
35
+ _smoothedCapsuleEndpoints;
36
+ _smoothedPlaneData;
20
37
  constructor(chain, options){
21
38
  this._chain = chain;
22
39
  this._iterations = options?.iterations ?? 5;
@@ -27,13 +44,38 @@ import { updateColliderFromNode, resolvePlaneCollision, resolveCapsuleCollision,
27
44
  this._coriolisScale = options?.coriolisScale ?? 1.0;
28
45
  this._colliders = [];
29
46
  this._solver = options?.solver ?? 'verlet';
47
+ this._poseFollow = Math.max(0, Math.min(1, options?.poseFollow ?? 0.35));
48
+ this._maxPoseOffset = Math.max(0, options?.maxPoseOffset ?? 0);
49
+ this._poseFollowRoot = Math.max(0, Math.min(1, options?.poseFollowRoot ?? this._poseFollow));
50
+ this._poseFollowTip = Math.max(0, Math.min(1, options?.poseFollowTip ?? this._poseFollow));
51
+ this._poseFollowExponent = Math.max(0.1, options?.poseFollowExponent ?? 1.6);
52
+ this._maxPoseOffsetRoot = Math.max(0, options?.maxPoseOffsetRoot ?? this._maxPoseOffset);
53
+ this._maxPoseOffsetTip = Math.max(0, options?.maxPoseOffsetTip ?? this._maxPoseOffset);
54
+ this._timeAccumulator = 0;
55
+ this._smoothedParticleTargets = new WeakMap();
56
+ this._smoothedSphereCenters = new WeakMap();
57
+ this._smoothedCapsuleEndpoints = new WeakMap();
58
+ this._smoothedPlaneData = new WeakMap();
30
59
  }
31
60
  /**
32
61
  * Updates the physics simulation
33
62
  * @param deltaTime - Time step in seconds
34
63
  */ update(deltaTime) {
35
- // Clamp deltaTime to prevent instability
36
- const dt = Math.min(deltaTime, 0.033); // Max 30 FPS
64
+ const frameDt = Math.min(Math.max(Number(deltaTime) || 0, 0), MAX_ACCUMULATED_SIMULATION_TIME);
65
+ if (frameDt <= 0) {
66
+ return;
67
+ }
68
+ this._timeAccumulator = Math.min(this._timeAccumulator + frameDt, MAX_ACCUMULATED_SIMULATION_TIME);
69
+ const stepCount = Math.min(MAX_SIMULATION_STEPS_PER_UPDATE, Math.floor((this._timeAccumulator + 1e-8) / FIXED_SIMULATION_TIME_STEP));
70
+ if (stepCount <= 0) {
71
+ return;
72
+ }
73
+ this._timeAccumulator = Math.max(0, this._timeAccumulator - stepCount * FIXED_SIMULATION_TIME_STEP);
74
+ for(let i = 0; i < stepCount; i++){
75
+ this.simulateStep(FIXED_SIMULATION_TIME_STEP, i === 0 ? frameDt : 0);
76
+ }
77
+ }
78
+ simulateStep(dt, inputDeltaTime) {
37
79
  // Step 1: Save all particle positions before updating
38
80
  if (this._enableInertialForces) {
39
81
  for (const p of this._chain.particles){
@@ -41,7 +83,7 @@ import { updateColliderFromNode, resolvePlaneCollision, resolveCapsuleCollision,
41
83
  }
42
84
  }
43
85
  // Step 2: Update fixed particles from their scene nodes
44
- this.updateFixedParticles();
86
+ this.updateFixedParticles(inputDeltaTime);
45
87
  // Step 3: Calculate global rotation parameters
46
88
  let rotationCenter = null;
47
89
  let angularVelocity = null;
@@ -87,25 +129,34 @@ import { updateColliderFromNode, resolvePlaneCollision, resolveCapsuleCollision,
87
129
  this.solveConstraint(constraint);
88
130
  }
89
131
  }
132
+ // Pull particles back toward the animated pose to preserve hair silhouette.
133
+ // Normalize follow strength across solver iterations so tuning stays intuitive.
134
+ this.solvePosePreservation(this._iterations);
90
135
  // Apply collision constraints
91
- this.solveCollisions();
136
+ this.solveCollisions(inputDeltaTime);
92
137
  }
93
138
  }
94
139
  /**
95
140
  * Updates fixed particles to match their scene node positions
96
- */ updateFixedParticles() {
141
+ */ updateFixedParticles(deltaTime) {
142
+ const blend = this.getTemporalBlendFactor(deltaTime, DEFAULT_PARTICLE_TARGET_SMOOTHING_TIME);
97
143
  for (const particle of this._chain.particles){
98
- if (particle.node && particle.fixed) {
99
- const worldMatrix = particle.node.worldMatrix;
100
- const worldPos = new Vector3(worldMatrix.m03, worldMatrix.m13, worldMatrix.m23);
101
- particle.position.set(worldPos);
102
- particle.prevPosition.set(worldPos);
144
+ if (!particle.node) {
145
+ continue;
146
+ }
147
+ const worldMatrix = particle.node.worldMatrix;
148
+ const worldPos = new Vector3(worldMatrix.m03, worldMatrix.m13, worldMatrix.m23);
149
+ const smoothedTarget = this.getSmoothedParticleTarget(particle, worldPos, blend);
150
+ particle.animPosition.set(smoothedTarget);
151
+ if (particle.fixed) {
152
+ particle.position.set(smoothedTarget);
153
+ particle.prevPosition.set(smoothedTarget);
103
154
  // Maintain position history for rotation center estimation
104
155
  if (this._enableInertialForces) {
105
156
  if (!particle.positionHistory) {
106
157
  particle.positionHistory = [];
107
158
  }
108
- particle.positionHistory.push(worldPos.clone());
159
+ particle.positionHistory.push(smoothedTarget.clone());
109
160
  // Keep only last 5 frames
110
161
  if (particle.positionHistory.length > 5) {
111
162
  particle.positionHistory.shift();
@@ -115,6 +166,41 @@ import { updateColliderFromNode, resolvePlaneCollision, resolveCapsuleCollision,
115
166
  }
116
167
  }
117
168
  /**
169
+ * Solves pose preservation (long-range attachment to animated pose).
170
+ * This keeps strands close to authored shape while preserving dynamic movement.
171
+ */ solvePosePreservation(totalIterations) {
172
+ if (this._poseFollowRoot <= 0 && this._poseFollowTip <= 0) {
173
+ return;
174
+ }
175
+ const lastIndex = Math.max(1, this._chain.particles.length - 1);
176
+ for(let i = 0; i < this._chain.particles.length; i++){
177
+ const particle = this._chain.particles[i];
178
+ if (particle.fixed) {
179
+ continue;
180
+ }
181
+ const t = Math.pow(i / lastIndex, this._poseFollowExponent);
182
+ const particlePoseFollow = this.lerp(this._poseFollowRoot, this._poseFollowTip, t);
183
+ // Convert user-facing per-frame follow into per-iteration follow:
184
+ // effective = 1 - (1 - follow)^iterations
185
+ const iterationFollow = totalIterations > 1 ? 1 - Math.pow(Math.max(0, 1 - particlePoseFollow), 1 / totalIterations) : particlePoseFollow;
186
+ const toAnim = Vector3.sub(particle.animPosition, particle.position, new Vector3());
187
+ const correction = Vector3.scale(toAnim, iterationFollow, new Vector3());
188
+ Vector3.add(particle.position, correction, particle.position);
189
+ const particleMaxPoseOffset = this.lerp(this._maxPoseOffsetRoot, this._maxPoseOffsetTip, t);
190
+ if (particleMaxPoseOffset > 0) {
191
+ const offset = Vector3.sub(particle.position, particle.animPosition, new Vector3());
192
+ const offsetLen = offset.magnitude;
193
+ if (offsetLen > particleMaxPoseOffset && offsetLen > 1e-6) {
194
+ offset.scaleBy(particleMaxPoseOffset / offsetLen);
195
+ Vector3.add(particle.animPosition, offset, particle.position);
196
+ }
197
+ }
198
+ }
199
+ }
200
+ lerp(a, b, t) {
201
+ return a + (b - a) * t;
202
+ }
203
+ /**
118
204
  * Calculates global rotation parameters from fixed particle movements
119
205
  * Uses position history to estimate rotation center
120
206
  */ calculateGlobalRotation(dt) {
@@ -337,33 +423,75 @@ import { updateColliderFromNode, resolvePlaneCollision, resolveCapsuleCollision,
337
423
  }
338
424
  /**
339
425
  * Solves collisions for all particles
340
- */ solveCollisions() {
426
+ */ solveCollisions(deltaTime) {
341
427
  // Update dynamic colliders from their nodes
428
+ const blend = this.getTemporalBlendFactor(deltaTime, DEFAULT_COLLIDER_SMOOTHING_TIME);
429
+ const spheres = [];
430
+ const capsules = [];
431
+ const planes = [];
342
432
  for (const collider of this._colliders){
343
433
  if (collider.node) {
344
434
  updateColliderFromNode(collider);
345
435
  }
436
+ if (!collider.enabled) {
437
+ continue;
438
+ }
439
+ switch(collider.type){
440
+ case 'sphere':
441
+ {
442
+ const source = collider;
443
+ spheres.push({
444
+ particleCollider: source,
445
+ collider: {
446
+ ...source,
447
+ center: this.getSmoothedSphereCenter(source, blend)
448
+ }
449
+ });
450
+ break;
451
+ }
452
+ case 'capsule':
453
+ {
454
+ const source = collider;
455
+ const endpoints = this.getSmoothedCapsuleEndpoints(source, blend);
456
+ capsules.push({
457
+ particleCollider: source,
458
+ collider: {
459
+ ...source,
460
+ start: endpoints.start,
461
+ end: endpoints.end
462
+ }
463
+ });
464
+ break;
465
+ }
466
+ case 'plane':
467
+ {
468
+ const source = collider;
469
+ const plane = this.getSmoothedPlaneData(source, blend);
470
+ planes.push({
471
+ particleCollider: source,
472
+ collider: {
473
+ ...source,
474
+ point: plane.point,
475
+ normal: plane.normal
476
+ }
477
+ });
478
+ break;
479
+ }
480
+ }
346
481
  }
347
482
  // Check each particle against all colliders
348
483
  for (const particle of this._chain.particles){
349
484
  if (particle.fixed) {
350
485
  continue; // Skip fixed particles
351
486
  }
352
- for (const collider of this._colliders){
353
- if (!collider.enabled) {
354
- continue;
355
- }
356
- switch(collider.type){
357
- case 'sphere':
358
- resolveSphereCollision(particle.position, collider);
359
- break;
360
- case 'capsule':
361
- resolveCapsuleCollision(particle.position, collider);
362
- break;
363
- case 'plane':
364
- resolvePlaneCollision(particle.position, collider);
365
- break;
366
- }
487
+ for (const collider of spheres){
488
+ resolveSphereCollision(particle.position, collider.collider);
489
+ }
490
+ for (const collider of capsules){
491
+ resolveCapsuleCollision(particle.position, collider.collider);
492
+ }
493
+ for (const collider of planes){
494
+ resolvePlaneCollision(particle.position, collider.collider);
367
495
  }
368
496
  }
369
497
  }
@@ -421,6 +549,18 @@ import { updateColliderFromNode, resolvePlaneCollision, resolveCapsuleCollision,
421
549
  * Resets the simulation to initial state
422
550
  */ reset() {
423
551
  this._chain.reset();
552
+ for (const particle of this._chain.particles){
553
+ particle.animPosition.set(particle.originalPosition);
554
+ particle.lastFramePosition.set(particle.originalPosition);
555
+ if (particle.positionHistory) {
556
+ particle.positionHistory.length = 0;
557
+ }
558
+ }
559
+ this._timeAccumulator = 0;
560
+ this._smoothedParticleTargets = new WeakMap();
561
+ this._smoothedSphereCenters = new WeakMap();
562
+ this._smoothedCapsuleEndpoints = new WeakMap();
563
+ this._smoothedPlaneData = new WeakMap();
424
564
  }
425
565
  /**
426
566
  * Gets the spring chain
@@ -496,9 +636,88 @@ import { updateColliderFromNode, resolvePlaneCollision, resolveCapsuleCollision,
496
636
  }
497
637
  }
498
638
  /**
639
+ * Gets pose preservation strength [0-1]
640
+ */ get poseFollow() {
641
+ return this._poseFollow;
642
+ }
643
+ /**
644
+ * Sets pose preservation strength [0-1]
645
+ */ set poseFollow(value) {
646
+ const v = Math.max(0, Math.min(1, value));
647
+ this._poseFollow = v;
648
+ this._poseFollowRoot = v;
649
+ this._poseFollowTip = v;
650
+ }
651
+ /**
652
+ * Gets max allowed deviation from animated pose
653
+ */ get maxPoseOffset() {
654
+ return this._maxPoseOffset;
655
+ }
656
+ /**
657
+ * Sets max allowed deviation from animated pose. 0 disables clamping.
658
+ */ set maxPoseOffset(value) {
659
+ const v = Math.max(0, value);
660
+ this._maxPoseOffset = v;
661
+ this._maxPoseOffsetRoot = v;
662
+ this._maxPoseOffsetTip = v;
663
+ }
664
+ /**
665
+ * Gets root pose follow strength [0-1]
666
+ */ get poseFollowRoot() {
667
+ return this._poseFollowRoot;
668
+ }
669
+ /**
670
+ * Sets root pose follow strength [0-1]
671
+ */ set poseFollowRoot(value) {
672
+ this._poseFollowRoot = Math.max(0, Math.min(1, value));
673
+ }
674
+ /**
675
+ * Gets tip pose follow strength [0-1]
676
+ */ get poseFollowTip() {
677
+ return this._poseFollowTip;
678
+ }
679
+ /**
680
+ * Sets tip pose follow strength [0-1]
681
+ */ set poseFollowTip(value) {
682
+ this._poseFollowTip = Math.max(0, Math.min(1, value));
683
+ }
684
+ /**
685
+ * Gets exponent for root->tip interpolation
686
+ */ get poseFollowExponent() {
687
+ return this._poseFollowExponent;
688
+ }
689
+ /**
690
+ * Sets exponent for root->tip interpolation
691
+ */ set poseFollowExponent(value) {
692
+ this._poseFollowExponent = Math.max(0.1, value);
693
+ }
694
+ /**
695
+ * Gets root max allowed deviation from animated pose
696
+ */ get maxPoseOffsetRoot() {
697
+ return this._maxPoseOffsetRoot;
698
+ }
699
+ /**
700
+ * Sets root max allowed deviation from animated pose
701
+ */ set maxPoseOffsetRoot(value) {
702
+ this._maxPoseOffsetRoot = Math.max(0, value);
703
+ }
704
+ /**
705
+ * Gets tip max allowed deviation from animated pose
706
+ */ get maxPoseOffsetTip() {
707
+ return this._maxPoseOffsetTip;
708
+ }
709
+ /**
710
+ * Sets tip max allowed deviation from animated pose
711
+ */ set maxPoseOffsetTip(value) {
712
+ this._maxPoseOffsetTip = Math.max(0, value);
713
+ }
714
+ /**
499
715
  * Adds a collider to the system
500
716
  */ addCollider(collider) {
501
717
  this._colliders.push(collider);
718
+ this._smoothedSphereCenters = new WeakMap();
719
+ this._smoothedCapsuleEndpoints = new WeakMap();
720
+ this._smoothedPlaneData = new WeakMap();
502
721
  }
503
722
  /**
504
723
  * Removes a collider from the system
@@ -506,6 +725,9 @@ import { updateColliderFromNode, resolvePlaneCollision, resolveCapsuleCollision,
506
725
  const index = this._colliders.indexOf(collider);
507
726
  if (index >= 0) {
508
727
  this._colliders.splice(index, 1);
728
+ this._smoothedSphereCenters = new WeakMap();
729
+ this._smoothedCapsuleEndpoints = new WeakMap();
730
+ this._smoothedPlaneData = new WeakMap();
509
731
  return true;
510
732
  }
511
733
  return false;
@@ -514,12 +736,87 @@ import { updateColliderFromNode, resolvePlaneCollision, resolveCapsuleCollision,
514
736
  * Clears all colliders
515
737
  */ clearColliders() {
516
738
  this._colliders = [];
739
+ this._smoothedSphereCenters = new WeakMap();
740
+ this._smoothedCapsuleEndpoints = new WeakMap();
741
+ this._smoothedPlaneData = new WeakMap();
517
742
  }
518
743
  /**
519
744
  * Gets all colliders
520
745
  */ get colliders() {
521
746
  return this._colliders;
522
747
  }
748
+ getTemporalBlendFactor(deltaTime, smoothingTime) {
749
+ const dt = Math.min(Math.max(Number(deltaTime) || 0, 0), MAX_ACCUMULATED_SIMULATION_TIME);
750
+ if (smoothingTime <= 0) {
751
+ return 1;
752
+ }
753
+ if (dt <= 0) {
754
+ return 0;
755
+ }
756
+ return 1 - Math.exp(-dt / smoothingTime);
757
+ }
758
+ getSmoothedParticleTarget(target, current, blend) {
759
+ const cached = this._smoothedParticleTargets.get(target);
760
+ if (!cached || blend >= 1) {
761
+ const next = current.clone();
762
+ this._smoothedParticleTargets.set(target, next);
763
+ return next;
764
+ }
765
+ cached.setXYZ(cached.x + (current.x - cached.x) * blend, cached.y + (current.y - cached.y) * blend, cached.z + (current.z - cached.z) * blend);
766
+ return cached;
767
+ }
768
+ getSmoothedSphereCenter(collider, blend) {
769
+ const current = collider.center?.clone() ?? new Vector3();
770
+ const cached = this._smoothedSphereCenters.get(collider);
771
+ if (!cached || blend >= 1) {
772
+ this._smoothedSphereCenters.set(collider, current);
773
+ return current;
774
+ }
775
+ cached.setXYZ(cached.x + (current.x - cached.x) * blend, cached.y + (current.y - cached.y) * blend, cached.z + (current.z - cached.z) * blend);
776
+ return cached;
777
+ }
778
+ getSmoothedCapsuleEndpoints(collider, blend) {
779
+ const currentStart = collider.start?.clone() ?? new Vector3();
780
+ const currentEnd = collider.end?.clone() ?? new Vector3();
781
+ const cached = this._smoothedCapsuleEndpoints.get(collider);
782
+ if (!cached || blend >= 1) {
783
+ const next = {
784
+ start: currentStart,
785
+ end: currentEnd
786
+ };
787
+ this._smoothedCapsuleEndpoints.set(collider, next);
788
+ return next;
789
+ }
790
+ 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);
791
+ 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);
792
+ return cached;
793
+ }
794
+ getSmoothedPlaneData(collider, blend) {
795
+ const currentPoint = collider.point?.clone() ?? new Vector3();
796
+ const currentNormal = collider.normal?.clone() ?? Vector3.axisPY();
797
+ if (currentNormal.magnitudeSq > 1e-8) {
798
+ currentNormal.inplaceNormalize();
799
+ } else {
800
+ currentNormal.setXYZ(0, 1, 0);
801
+ }
802
+ const cached = this._smoothedPlaneData.get(collider);
803
+ if (!cached || blend >= 1) {
804
+ const next = {
805
+ point: currentPoint,
806
+ normal: currentNormal
807
+ };
808
+ this._smoothedPlaneData.set(collider, next);
809
+ return next;
810
+ }
811
+ 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);
812
+ 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);
813
+ if (cached.normal.magnitudeSq > 1e-8) {
814
+ cached.normal.inplaceNormalize();
815
+ } else {
816
+ cached.normal.setXYZ(0, 1, 0);
817
+ }
818
+ return cached;
819
+ }
523
820
  }
524
821
 
525
822
  export { SpringSystem };