@woosh/meep-engine 3.63.3 → 3.64.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 (190) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/package.json +1 -1
  3. package/src/core/color/xyz/m3_chromatic_adaptation_bradford.d.ts +14 -0
  4. package/src/core/color/xyz/m3_chromatic_adaptation_bradford.d.ts.map +1 -0
  5. package/src/core/color/xyz/m3_chromatic_adaptation_bradford.js +51 -0
  6. package/src/core/math/linalg/eigen/matrix_symmetric_3x3_eigen.d.ts +2 -1
  7. package/src/core/math/linalg/eigen/matrix_symmetric_3x3_eigen.d.ts.map +1 -1
  8. package/src/core/math/linalg/eigen/matrix_symmetric_3x3_eigen.js +51 -70
  9. package/src/core/math/linalg/eigen/matrix_symmetric_4x4_eigen.d.ts.map +1 -1
  10. package/src/core/math/linalg/eigen/matrix_symmetric_4x4_eigen.js +8 -64
  11. package/src/core/math/linalg/eigen/matrix_symmetric_jacobi_rotate_in_place.d.ts +17 -0
  12. package/src/core/math/linalg/eigen/matrix_symmetric_jacobi_rotate_in_place.d.ts.map +1 -0
  13. package/src/core/math/linalg/eigen/matrix_symmetric_jacobi_rotate_in_place.js +64 -0
  14. package/src/core/math/linalg/solve_box_constrained_qp.d.ts +30 -0
  15. package/src/core/math/linalg/solve_box_constrained_qp.d.ts.map +1 -0
  16. package/src/core/math/linalg/solve_box_constrained_qp.js +205 -0
  17. package/src/engine/graphics/texture/sampler/exr_to_sampler2d.d.ts +27 -0
  18. package/src/engine/graphics/texture/sampler/exr_to_sampler2d.d.ts.map +1 -0
  19. package/src/engine/graphics/texture/sampler/exr_to_sampler2d.js +91 -0
  20. package/src/engine/graphics/texture/sampler/sampler2d_to_exr.d.ts +23 -0
  21. package/src/engine/graphics/texture/sampler/sampler2d_to_exr.d.ts.map +1 -0
  22. package/src/engine/graphics/texture/sampler/sampler2d_to_exr.js +64 -0
  23. package/src/engine/physics/constraint/soft_spring_coefficients.d.ts +24 -0
  24. package/src/engine/physics/constraint/soft_spring_coefficients.d.ts.map +1 -0
  25. package/src/engine/physics/constraint/soft_spring_coefficients.js +38 -0
  26. package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -1
  27. package/src/engine/physics/constraint/solve_constraints.js +16 -8
  28. package/src/engine/physics/ecs/PhysicsSystem.d.ts +54 -0
  29. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  30. package/src/engine/physics/ecs/PhysicsSystem.js +192 -7
  31. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  32. package/src/engine/physics/inertia/world_inverse_inertia.d.ts +3 -5
  33. package/src/engine/physics/inertia/world_inverse_inertia.d.ts.map +1 -1
  34. package/src/engine/physics/inertia/world_inverse_inertia.js +3 -5
  35. package/src/engine/physics/vehicle/RaycastVehicle.d.ts +183 -12
  36. package/src/engine/physics/vehicle/RaycastVehicle.d.ts.map +1 -1
  37. package/src/engine/physics/vehicle/RaycastVehicle.js +1046 -121
  38. package/src/format/image/exr/ExrCompression.d.ts +22 -0
  39. package/src/format/image/exr/ExrCompression.d.ts.map +1 -0
  40. package/src/format/image/exr/ExrCompression.js +27 -0
  41. package/src/format/image/exr/ExrImage.d.ts +32 -0
  42. package/src/format/image/exr/ExrImage.d.ts.map +1 -0
  43. package/src/format/image/exr/ExrImage.js +32 -0
  44. package/src/format/image/exr/ExrPixelType.d.ts +13 -0
  45. package/src/format/image/exr/ExrPixelType.d.ts.map +1 -0
  46. package/src/format/image/exr/ExrPixelType.js +18 -0
  47. package/src/format/image/exr/exr_compare_names.d.ts +16 -0
  48. package/src/format/image/exr/exr_compare_names.d.ts.map +1 -0
  49. package/src/format/image/exr/exr_compare_names.js +25 -0
  50. package/src/format/image/exr/exr_constants.d.ts +49 -0
  51. package/src/format/image/exr/exr_constants.d.ts.map +1 -0
  52. package/src/format/image/exr/exr_constants.js +56 -0
  53. package/src/format/image/exr/exr_read.d.ts +32 -0
  54. package/src/format/image/exr/exr_read.d.ts.map +1 -0
  55. package/src/format/image/exr/exr_read.js +531 -0
  56. package/src/format/image/exr/exr_sample_bits.d.ts +16 -0
  57. package/src/format/image/exr/exr_sample_bits.d.ts.map +1 -0
  58. package/src/format/image/exr/exr_sample_bits.js +21 -0
  59. package/src/format/image/exr/exr_scanlines_per_chunk.d.ts +17 -0
  60. package/src/format/image/exr/exr_scanlines_per_chunk.d.ts.map +1 -0
  61. package/src/format/image/exr/exr_scanlines_per_chunk.js +31 -0
  62. package/src/format/image/exr/exr_write.d.ts +27 -0
  63. package/src/format/image/exr/exr_write.d.ts.map +1 -0
  64. package/src/format/image/exr/exr_write.js +378 -0
  65. package/src/shade/playground/probe_clipmap/index.html +5 -0
  66. package/src/shade/playground/probe_clipmap/main.d.ts.map +1 -1
  67. package/src/shade/playground/probe_clipmap/main.js +34 -1
  68. package/src/shade/playground/tonemap_sdr/README.md +11 -8
  69. package/src/shade/playground/tonemap_sdr/ToneMapCompare.js +2 -2
  70. package/src/shade/renderer/Renderer.d.ts +5 -6
  71. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  72. package/src/shade/renderer/Renderer.js +15 -88
  73. package/src/shade/renderer/animation/chunk_animation_curve_evaluate_keys.d.ts +14 -0
  74. package/src/shade/renderer/animation/chunk_animation_curve_evaluate_keys.d.ts.map +1 -0
  75. package/src/shade/renderer/animation/chunk_animation_curve_evaluate_keys.js +62 -0
  76. package/src/shade/renderer/deferred/tonemap/ColorGrade.d.ts +158 -0
  77. package/src/shade/renderer/deferred/tonemap/ColorGrade.d.ts.map +1 -0
  78. package/src/shade/renderer/deferred/tonemap/ColorGrade.js +337 -0
  79. package/src/shade/renderer/deferred/tonemap/SAMPLER_TONEMAP_LUT.d.ts +9 -0
  80. package/src/shade/renderer/deferred/tonemap/SAMPLER_TONEMAP_LUT.d.ts.map +1 -0
  81. package/src/shade/renderer/deferred/tonemap/SAMPLER_TONEMAP_LUT.js +15 -0
  82. package/src/shade/renderer/deferred/tonemap/TONE_MAP_OPERATOR_ACES.d.ts +6 -0
  83. package/src/shade/renderer/deferred/tonemap/TONE_MAP_OPERATOR_ACES.d.ts.map +1 -0
  84. package/src/shade/renderer/deferred/tonemap/TONE_MAP_OPERATOR_ACES.js +84 -0
  85. package/src/shade/renderer/deferred/tonemap/TONE_MAP_OPERATOR_GT7.d.ts +10 -0
  86. package/src/shade/renderer/deferred/tonemap/TONE_MAP_OPERATOR_GT7.d.ts.map +1 -0
  87. package/src/shade/renderer/deferred/tonemap/TONE_MAP_OPERATOR_GT7.js +25 -0
  88. package/src/shade/renderer/deferred/tonemap/TONE_MAP_OPERATOR_NONE.d.ts +9 -0
  89. package/src/shade/renderer/deferred/tonemap/TONE_MAP_OPERATOR_NONE.d.ts.map +1 -0
  90. package/src/shade/renderer/deferred/tonemap/TONE_MAP_OPERATOR_NONE.js +20 -0
  91. package/src/shade/renderer/deferred/tonemap/TONE_MAP_OPERATOR_PBR_NEUTRAL.d.ts +14 -0
  92. package/src/shade/renderer/deferred/tonemap/TONE_MAP_OPERATOR_PBR_NEUTRAL.d.ts.map +1 -0
  93. package/src/shade/renderer/deferred/tonemap/TONE_MAP_OPERATOR_PBR_NEUTRAL.js +30 -0
  94. package/src/shade/renderer/deferred/tonemap/ToneMapOperator.d.ts +54 -0
  95. package/src/shade/renderer/deferred/tonemap/ToneMapOperator.d.ts.map +1 -0
  96. package/src/shade/renderer/deferred/tonemap/ToneMapOperator.js +63 -0
  97. package/src/shade/renderer/deferred/tonemap/ToneMapper.d.ts +106 -0
  98. package/src/shade/renderer/deferred/tonemap/ToneMapper.d.ts.map +1 -0
  99. package/src/shade/renderer/deferred/tonemap/ToneMapper.js +474 -0
  100. package/src/shade/renderer/deferred/tonemap/TonemapAces2Tables.d.ts +5 -13
  101. package/src/shade/renderer/deferred/tonemap/TonemapAces2Tables.d.ts.map +1 -1
  102. package/src/shade/renderer/deferred/tonemap/TonemapAces2Tables.js +5 -44
  103. package/src/shade/renderer/deferred/tonemap/TonemapDisplaySettings.d.ts +7 -25
  104. package/src/shade/renderer/deferred/tonemap/TonemapDisplaySettings.d.ts.map +1 -1
  105. package/src/shade/renderer/deferred/tonemap/TonemapDisplaySettings.js +11 -51
  106. package/src/shade/renderer/deferred/tonemap/m3_white_balance_rec709.d.ts +20 -0
  107. package/src/shade/renderer/deferred/tonemap/m3_white_balance_rec709.d.ts.map +1 -0
  108. package/src/shade/renderer/deferred/tonemap/m3_white_balance_rec709.js +103 -0
  109. package/src/shade/renderer/deferred/tonemap/shader_tonemap_HDR_lut.d.ts +9 -0
  110. package/src/shade/renderer/deferred/tonemap/shader_tonemap_HDR_lut.d.ts.map +1 -0
  111. package/src/shade/renderer/deferred/tonemap/{shader_tonemap_HDR_none.js → shader_tonemap_HDR_lut.js} +22 -18
  112. package/src/shade/renderer/deferred/tonemap/shader_tonemap_LDR_lut.d.ts +9 -0
  113. package/src/shade/renderer/deferred/tonemap/shader_tonemap_LDR_lut.d.ts.map +1 -0
  114. package/src/shade/renderer/deferred/tonemap/{shader_tonemap_LDR_aces.js → shader_tonemap_LDR_lut.js} +24 -24
  115. package/src/shade/renderer/deferred/tonemap/shader_tonemap_lut_bake.d.ts +23 -0
  116. package/src/shade/renderer/deferred/tonemap/shader_tonemap_lut_bake.d.ts.map +1 -0
  117. package/src/shade/renderer/deferred/tonemap/shader_tonemap_lut_bake.js +92 -0
  118. package/src/shade/renderer/deferred/tonemap/shader_tonemap_lut_sdr_from_hdr.d.ts +15 -0
  119. package/src/shade/renderer/deferred/tonemap/shader_tonemap_lut_sdr_from_hdr.d.ts.map +1 -0
  120. package/src/shade/renderer/deferred/tonemap/shader_tonemap_lut_sdr_from_hdr.js +73 -0
  121. package/src/shade/renderer/fluid/GPUFluidCascade.d.ts +3 -3
  122. package/src/shade/renderer/fluid/GPUFluidCascadeStack.d.ts +1 -1
  123. package/src/shade/renderer/fluid/forcing/GPUFluidForcing.d.ts +6 -6
  124. package/src/shade/renderer/fluid/pressure/GPUFluidPressureSolve.d.ts +1 -1
  125. package/src/shade/renderer/fluid/query/GPUFluidQueries.d.ts +1 -1
  126. package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.d.ts +1 -1
  127. package/src/shade/renderer/global_illumination/probe_clipmap/probe/PROBE_CLIPMAP.d.ts +3 -15
  128. package/src/shade/renderer/global_illumination/probe_clipmap/probe/PROBE_CLIPMAP.d.ts.map +1 -1
  129. package/src/shade/renderer/global_illumination/probe_clipmap/probe/PROBE_CLIPMAP.js +4 -17
  130. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap.d.ts +3 -3
  131. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap.d.ts.map +1 -1
  132. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap.js +15 -13
  133. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_accumulate.d.ts +7 -3
  134. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_accumulate.d.ts.map +1 -1
  135. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_accumulate.js +28 -17
  136. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_project.d.ts +18 -18
  137. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_project.d.ts.map +1 -1
  138. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_project.js +44 -48
  139. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  140. package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
  141. package/src/shade/renderer/shader/chunk/color/conversion/chunk_CRM_REC709_AP1.d.ts +22 -0
  142. package/src/shade/renderer/shader/chunk/color/conversion/chunk_CRM_REC709_AP1.d.ts.map +1 -0
  143. package/src/shade/renderer/shader/chunk/color/conversion/chunk_CRM_REC709_AP1.js +62 -0
  144. package/src/shade/renderer/shader/chunk/color/grade/chunk_color_grade.d.ts +13 -0
  145. package/src/shade/renderer/shader/chunk/color/grade/chunk_color_grade.d.ts.map +1 -0
  146. package/src/shade/renderer/shader/chunk/color/grade/chunk_color_grade.js +140 -0
  147. package/src/shade/renderer/shader/chunk/color/oklab/chunk_oklab_compute_max_saturation.d.ts +11 -0
  148. package/src/shade/renderer/shader/chunk/color/oklab/chunk_oklab_compute_max_saturation.d.ts.map +1 -0
  149. package/src/shade/renderer/shader/chunk/color/oklab/chunk_oklab_compute_max_saturation.js +65 -0
  150. package/src/shade/renderer/shader/chunk/color/tonemap/chunk_bt2390_eetf.d.ts +14 -0
  151. package/src/shade/renderer/shader/chunk/color/tonemap/chunk_bt2390_eetf.d.ts.map +1 -0
  152. package/src/shade/renderer/shader/chunk/color/tonemap/chunk_bt2390_eetf.js +38 -0
  153. package/src/shade/renderer/shader/chunk/color/tonemap/chunk_tonemap_lut.d.ts +16 -0
  154. package/src/shade/renderer/shader/chunk/color/tonemap/chunk_tonemap_lut.d.ts.map +1 -0
  155. package/src/shade/renderer/shader/chunk/color/tonemap/chunk_tonemap_lut.js +41 -0
  156. package/src/shade/renderer/shader/chunk/filter/chunk_multiscale_mean.d.ts +8 -0
  157. package/src/shade/renderer/shader/chunk/filter/chunk_multiscale_mean.d.ts.map +1 -1
  158. package/src/shade/renderer/shader/chunk/filter/chunk_multiscale_mean.js +21 -3
  159. package/src/shade/renderer/water/query/GPUWaterQueries.d.ts +1 -1
  160. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +8 -8
  161. package/src/shade/wgsl/emulator/WGSLLanguageCore.d.ts.map +1 -1
  162. package/src/shade/wgsl/emulator/WGSLLanguageCore.js +2 -0
  163. package/src/shade/renderer/ShadeToneMappingMode.d.ts +0 -15
  164. package/src/shade/renderer/ShadeToneMappingMode.d.ts.map +0 -1
  165. package/src/shade/renderer/ShadeToneMappingMode.js +0 -41
  166. package/src/shade/renderer/deferred/tonemap/shader_tonemap_HDR.d.ts +0 -9
  167. package/src/shade/renderer/deferred/tonemap/shader_tonemap_HDR.d.ts.map +0 -1
  168. package/src/shade/renderer/deferred/tonemap/shader_tonemap_HDR.js +0 -79
  169. package/src/shade/renderer/deferred/tonemap/shader_tonemap_HDR_aces.d.ts +0 -17
  170. package/src/shade/renderer/deferred/tonemap/shader_tonemap_HDR_aces.d.ts.map +0 -1
  171. package/src/shade/renderer/deferred/tonemap/shader_tonemap_HDR_aces.js +0 -101
  172. package/src/shade/renderer/deferred/tonemap/shader_tonemap_HDR_none.d.ts +0 -9
  173. package/src/shade/renderer/deferred/tonemap/shader_tonemap_HDR_none.d.ts.map +0 -1
  174. package/src/shade/renderer/deferred/tonemap/shader_tonemap_HDR_pbr_neutral.d.ts +0 -10
  175. package/src/shade/renderer/deferred/tonemap/shader_tonemap_HDR_pbr_neutral.d.ts.map +0 -1
  176. package/src/shade/renderer/deferred/tonemap/shader_tonemap_HDR_pbr_neutral.js +0 -80
  177. package/src/shade/renderer/deferred/tonemap/shader_tonemap_LDR.d.ts +0 -9
  178. package/src/shade/renderer/deferred/tonemap/shader_tonemap_LDR.d.ts.map +0 -1
  179. package/src/shade/renderer/deferred/tonemap/shader_tonemap_LDR.js +0 -80
  180. package/src/shade/renderer/deferred/tonemap/shader_tonemap_LDR_aces.d.ts +0 -10
  181. package/src/shade/renderer/deferred/tonemap/shader_tonemap_LDR_aces.d.ts.map +0 -1
  182. package/src/shade/renderer/deferred/tonemap/shader_tonemap_LDR_none.d.ts +0 -9
  183. package/src/shade/renderer/deferred/tonemap/shader_tonemap_LDR_none.d.ts.map +0 -1
  184. package/src/shade/renderer/deferred/tonemap/shader_tonemap_LDR_none.js +0 -71
  185. package/src/shade/renderer/deferred/tonemap/shader_tonemap_LDR_pbr_neutral.d.ts +0 -10
  186. package/src/shade/renderer/deferred/tonemap/shader_tonemap_LDR_pbr_neutral.d.ts.map +0 -1
  187. package/src/shade/renderer/deferred/tonemap/shader_tonemap_LDR_pbr_neutral.js +0 -76
  188. package/src/shade/renderer/shader/chunk/color/tonemap/chunk_tonemap_hdr_bt2390.d.ts +0 -3
  189. package/src/shade/renderer/shader/chunk/color/tonemap/chunk_tonemap_hdr_bt2390.d.ts.map +0 -1
  190. package/src/shade/renderer/shader/chunk/color/tonemap/chunk_tonemap_hdr_bt2390.js +0 -44
@@ -1,33 +1,92 @@
1
1
  import { Ray3 } from "../../../core/geom/3d/ray/Ray3.js";
2
- import { PhysicsSurfacePoint } from "../queries/PhysicsSurfacePoint.js";
2
+ import { v3_length } from "../../../core/geom/vec3/v3_length.js";
3
3
  import { v3_quaternion_apply } from "../../../core/geom/vec3/v3_quaternion_apply.js";
4
4
  import { v3_set } from "../../../core/geom/vec3/v3_set.js";
5
- import { v3_length } from "../../../core/geom/vec3/v3_length.js";
6
- import { angular_effective_mass_raw } from "../inertia/world_inverse_inertia.js";
7
- import { BodyKind } from "../ecs/BodyKind.js";
5
+ import { lu_factor_linear_system } from "../../../core/math/linalg/lu_factor_linear_system.js";
6
+ import { lu_solve_linear_system } from "../../../core/math/linalg/lu_solve_linear_system.js";
7
+ import { solve_box_constrained_qp } from "../../../core/math/linalg/solve_box_constrained_qp.js";
8
+ import { soft_spring_coefficients } from "../constraint/soft_spring_coefficients.js";
9
+ import { BodyKind } from "../ecs/BodyKind.js";
8
10
  import { locked_inverse_inertia } from "../inertia/locked_inverse_inertia.js";
11
+ import { world_inverse_inertia_apply_raw } from "../inertia/world_inverse_inertia.js";
12
+ import { PhysicsSurfacePoint } from "../queries/PhysicsSurfacePoint.js";
9
13
 
10
14
  /**
11
15
  * # Raycast vehicle controller
12
16
  *
13
17
  * The vehicle model most games ship: a single rigid-body **chassis** plus a set
14
18
  * of **raycast wheels**. Each wheel is not a body — it is a downward ray from a
15
- * chassis-local mount point. Every frame, for each wheel, the controller:
19
+ * chassis-local mount point. Every step, the controller:
16
20
  *
17
- * 1. casts the suspension ray and, on a ground hit, measures the spring
18
- * compression;
19
- * 2. applies a spring + damper **suspension force** along the contact normal
20
- * (this is what holds the chassis up);
21
- * 3. applies **tyre friction** at the contact patch — a lateral grip impulse
21
+ * 1. casts each wheel's suspension ray and, on a ground hit, measures the
22
+ * spring compression;
23
+ * 2. applies **tyre friction** at each contact patch — a lateral grip impulse
22
24
  * that resists sideways slide, plus a longitudinal drive / brake impulse,
23
- * the two clamped together to a friction circle `μ·N`.
25
+ * the two clamped together to a friction circle `μ·N`, `N` the push the
26
+ * wheel's spring gives over the step — against the patch's motion across
27
+ * the whole step (below);
28
+ * 3. pushes the chassis away from the ground at the grounded wheels' contacts
29
+ * with their spring + damper **suspension**, all of them solved together
30
+ * (below) — this is what holds the chassis up.
31
+ *
32
+ * ## The suspension is one implicit step of every grounded spring
33
+ *
34
+ * Each grounded wheel's spring is a soft constraint along its contact normal
35
+ * ({@link soft_spring_coefficients}), and the controller solves all of them at
36
+ * once as a single backward-Euler step: it finds the impulses after which every
37
+ * spring pushes with `k·x + c·ẋ`, `x` the compression the coming step ends at and
38
+ * `ẋ` its rate across the step, given where the step carries the chassis — under
39
+ * gravity, its damping and the forces accumulated on it
40
+ * ({@link PhysicsSystem#predictStepMeanVelocity}), the tyres' impulses, applied
41
+ * first, and the springs' own, every one of which moves the chassis at every
42
+ * wheel. That is stable for any stiffness, damping, chassis mass and inertia,
43
+ * wheel layout and step; a car left alone settles with each spring at its static
44
+ * deflection. A spring pushes, and no harder than `suspensionMaxForce`: the solve
45
+ * is a small bound-constrained problem ({@link solve_box_constrained_qp}).
46
+ *
47
+ * `M⁻¹` below is how an impulse applied before the step moves the chassis' mean
48
+ * motion across it: its inverse mass and inertia, times what of a velocity change
49
+ * the step keeps in its mean as the chassis' damping decays it
50
+ * ({@link PhysicsSystem#predictStepMeanVelocityShare}).
51
+ *
52
+ * A tyre's friction circle needs the push its spring gives over the step, which
53
+ * waits on the tyres. The springs are solved once beforehand for it, against the
54
+ * chassis' motion with every tyre pushing as it did the step before: exact while
55
+ * the car holds a line, a step behind when it changes one. It only bounds the
56
+ * tyres' impulses, so it costs the solve nothing in stability.
57
+ *
58
+ * ## A tyre grips against the step's motion
59
+ *
60
+ * A gripping tyre holds its patch still across the step, not only as the step
61
+ * starts. Two things move the patch after the tyres act: gravity, through the
62
+ * step ({@link PhysicsSystem#predictStepMeanVelocity}), and the springs. The
63
+ * patch sits below the chassis' centre of mass, so a tyre impulse tips the
64
+ * chassis as well as sliding it, and the springs push the tip back. So each tyre
65
+ * cancels its patch's slip, along and across at once, in the chassis' mean
66
+ * motion over the step, with the springs in it: pushing as the solve beforehand
67
+ * found, and every spring that pushes freely in that solve answering each tyre
68
+ * impulse as the spring solve will. The tyres see the chassis through
69
+ * `W = M⁻¹ − M⁻¹·J_sᵀ·(J_s·M⁻¹·J_sᵀ + Γ)⁻¹·J_s·M⁻¹`, `J_s` those springs' rows
70
+ * and `Γ` their compliances. That holds while the same springs push freely: in
71
+ * the step one starts or stops, the tyres miss by its change. Like the springs,
72
+ * the tyres take each contact, its lever and its directions as the step starts.
73
+ * A braked car on a slope it can hold stays put at any step.
24
74
  *
25
- * Steering rotates the steered wheels' forward direction; a drive force is split
26
- * across the driven wheels. It is a *controller on top of the public physics
27
- * API* (`raycast` + `applyForceAt` + `applyImpulseAt`), not a new constraint —
28
- * call {@link update} once per frame **before** `PhysicsSystem.fixedUpdate`, so
29
- * the suspension force it accumulates is integrated by that step and the
30
- * friction impulses act on the current velocity.
75
+ * The tyres take turns, once each per step, each starting from the impulse it
76
+ * applied the step before, within this step's brake and friction circle. A car
77
+ * on a slope it can hold settles with every patch held, whatever order its
78
+ * wheels are listed in. How the wheels share the hold is not fixed: rigid
79
+ * patches grip in more directions than the chassis has ways to slide, and the
80
+ * share the turns settle on depends on that order.
81
+ *
82
+ * Steering turns the steered wheels so that their axles pass through one turn
83
+ * centre ({@link setSteering}): the tyres' grips then agree on the point the
84
+ * chassis turns about. A drive force is split across the driven wheels.
85
+ *
86
+ * It is a *controller on top of the public physics API* (`raycast`,
87
+ * `predictStepMeanVelocity`, `applyImpulseAt`), not a new constraint: call
88
+ * {@link update} once per fixed step with that step's `dt`, and the impulses it
89
+ * applies to the chassis are integrated by the next `PhysicsSystem.fixedUpdate`.
31
90
  *
32
91
  * The 6-DOF joint (spring linear + motor angular) is the alternative
33
92
  * "simulated wheel" path; this raycast controller is the lighter, more robust
@@ -38,33 +97,61 @@ import { locked_inverse_inertia } from "../inertia/locked_inverse_inertia.js";
38
97
  * @copyright Company Named Limited (c) 2026
39
98
  */
40
99
 
100
+ /** Offset of the contact normal in a wheel's contact frame. */
101
+ const FRAME_NORMAL = 0;
102
+ /** Offset of the forward (rolling) direction in a wheel's contact frame. */
103
+ const FRAME_FORWARD = 3;
104
+ /** Offset of the side direction in a wheel's contact frame. */
105
+ const FRAME_SIDE = 6;
106
+
41
107
  const scratch_v = new Float64Array(3);
42
- const scratch_i = new Float64Array(3);
43
- const scratch_ii = new Float64Array(3);
108
+
109
+ /** What the chassis' mean motion across the step keeps of a velocity change, linear at 0 and angular at 1. */
110
+ const scratch_share = new Float64Array(2);
111
+
112
+ /** A wheel's stiffness, damping and the step, for {@link soft_spring_coefficients}. */
113
+ const scratch_spring_params = new Float64Array(3);
114
+ /** A wheel's spring compliance `γ` at 0 and bias rate `β` at 1, from {@link soft_spring_coefficients}. */
115
+ const scratch_spring = new Float64Array(2);
44
116
 
45
117
  /**
46
- * Effective mass denominator for a unit-direction velocity constraint at a
47
- * body-relative point: `invM + (r×d)·Iw⁻¹·(r×d)`. The angular term is the
48
- * shared {@link angular_effective_mass_raw} kernel — the same math the
49
- * contact/joint solver runs, so the two paths stay bit-identical.
50
- * @returns {number}
118
+ * A tyre's drive, brake and side impulse for {@link RaycastVehicle#__grip}. The numbers travel in an
119
+ * array because a number handed to a call V8 does not inline is boxed into a new heap object.
51
120
  */
52
- function effective_mass(rb, transform, invM, rx, ry, rz, dx, dy, dz) {
53
- let k = invM;
54
- locked_inverse_inertia(scratch_ii, 0, rb);
55
- const iix = scratch_ii[0];
56
- const iiy = scratch_ii[1];
57
- const iiz = scratch_ii[2];
58
- if (iix !== 0 || iiy !== 0 || iiz !== 0) {
59
- k += angular_effective_mass_raw(
60
- iix, iiy, iiz,
61
- transform.rotation_x, transform.rotation_y, transform.rotation_z, transform.rotation_w,
62
- rx, ry, rz,
63
- dx, dy, dz,
64
- scratch_i
65
- );
121
+ const scratch_grip = new Float64Array(3);
122
+
123
+ /** A wheel's directions in the plane it steers in, from {@link wheel_plan_frame}. */
124
+ const scratch_plan = new Float64Array(6);
125
+
126
+ /**
127
+ * Wheel `w`'s directions in the plane it steers in, square to its suspension axis, local: its forward
128
+ * direction there at `out[0..2]`, then `up × forward` at `out[3..5]`, the way a positive
129
+ * {@link Wheel#steering} turns it. Both are unit, or zero for a wheel whose forward direction is its
130
+ * suspension axis.
131
+ * @param {Float64Array} out
132
+ * @param {Wheel} w
133
+ */
134
+ function wheel_plan_frame(out, w) {
135
+ const d = w.localSuspensionDir;
136
+ const f = w.localForward;
137
+
138
+ const along = f[0] * d[0] + f[1] * d[1] + f[2] * d[2];
139
+ let fx = f[0] - along * d[0], fy = f[1] - along * d[1], fz = f[2] - along * d[2];
140
+
141
+ const length = v3_length(fx, fy, fz);
142
+
143
+ if (length > 0) {
144
+ fx /= length; fy /= length; fz /= length;
66
145
  }
67
- return k;
146
+
147
+ out[0] = fx;
148
+ out[1] = fy;
149
+ out[2] = fz;
150
+
151
+ // up × forward with up = −d: forward × d.
152
+ out[3] = fy * d[2] - fz * d[1];
153
+ out[4] = fz * d[0] - fx * d[2];
154
+ out[5] = fx * d[1] - fy * d[0];
68
155
  }
69
156
 
70
157
  /** Rotate vector `(vx,vy,vz)` about unit axis `(ux,uy,uz)` by `angle`, into `out`. */
@@ -119,16 +206,25 @@ export class Wheel {
119
206
  /** Body id of the surface under the wheel, or −1. */
120
207
  contactBodyId = -1;
121
208
 
122
- /** Current steering angle (rad), set via {@link RaycastVehicle#setSteering}. */
209
+ /**
210
+ * This wheel's steering angle (rad): its forward direction turned about its suspension axis, toward
211
+ * `up × forward` when positive. {@link RaycastVehicle#setSteering} sets every steered wheel's.
212
+ */
123
213
  steering = 0;
124
214
 
125
215
  /** Accumulated spin angle (rad) for rendering. */
126
216
  rotation = 0;
127
217
 
128
- /** Forward ground speed at the contact (m/s). */
218
+ /**
219
+ * Forward ground speed at the contact across the coming step (m/s): the chassis' mean velocity there
220
+ * over the step, along the wheel's forward direction, with every impulse of the update in it.
221
+ */
129
222
  forwardSpeed = 0;
130
223
 
131
- /** Suspension force magnitude applied this frame (N). */
224
+ /**
225
+ * The suspension's push in the last {@link RaycastVehicle#update}, as a force: the impulse it
226
+ * applied along the contact normal, divided by the step (N).
227
+ */
132
228
  suspensionForce = 0;
133
229
 
134
230
  constructor() {
@@ -147,15 +243,135 @@ export class RaycastVehicle {
147
243
  __ray = new Ray3();
148
244
  __hit = new PhysicsSurfacePoint();
149
245
 
150
- // Reused force / impulse / point payloads for the public API calls.
151
- __force = new Float64Array(3);
246
+ /** Reused impulse payload for applyImpulseAt. @type {Float64Array} */
247
+ __impulse = new Float64Array(3);
248
+
249
+ /**
250
+ * The chassis' mean linear and angular velocity across the coming step: before the springs push, for
251
+ * the spring solves; with them pushing, while the tyres solve and for each wheel's ground speed.
252
+ * @type {Float64Array}
253
+ */
254
+ __mean_linear = new Float64Array(3);
255
+ /** @type {Float64Array} */
256
+ __mean_angular = new Float64Array(3);
257
+
258
+ /** The chassis' mean velocity across the step per unit impulse applied before it, `M⁻¹`'s linear part. */
259
+ __mean_inverse_mass = 0;
260
+
261
+ /**
262
+ * `M⁻¹`'s angular part, as the chassis' body-local inverse inertia diagonal (LockRot*-masked) times the
263
+ * angular share: turned to the world by the pose, its mean spin across the step per unit angular impulse.
264
+ * @type {Float64Array}
265
+ */
266
+ __mean_inverse_inertia = new Float64Array(3);
267
+
268
+ // ── Per wheel ─────────────────────────────────────────────────────────────
269
+
270
+ /** Contact normal, forward and side directions (at FRAME_*), 9 per wheel. @type {Float64Array} */
271
+ __wheel_frame = new Float64Array(0);
272
+
273
+ /** The contact's offset from the chassis' centre, 3 per wheel. @type {Float64Array} */
274
+ __wheel_lever = new Float64Array(0);
275
+
276
+ /** 1 where the forward direction lies in the contact plane, so the tyre can grip and roll. @type {Uint8Array} */
277
+ __wheel_rolls = new Uint8Array(0);
278
+
279
+ /**
280
+ * The tyre impulse the wheel applied in the last update, along its forward and side directions, 2 per
281
+ * wheel; zero for a wheel that applied none.
282
+ * @type {Float64Array}
283
+ */
284
+ __wheel_tyre = new Float64Array(0);
285
+
286
+ /** The most impulse the tyre may apply this step, `μ·N·dt`. @type {Float64Array} */
287
+ __wheel_budget = new Float64Array(0);
288
+
289
+ /**
290
+ * The brake's part of the forward impulse in {@link __wheel_tyre}: what the tyre applied along its
291
+ * forward direction, less the drive. The tyre starts the next update from it.
292
+ * @type {Float64Array}
293
+ */
294
+ __wheel_brake = new Float64Array(0);
295
+
296
+ /**
297
+ * The chassis' mean velocity across the step per unit tyre impulse, the free springs answering it:
298
+ * `W·Jᵀ` for the forward, then the side direction, each linear then angular, 12 per wheel.
299
+ * @type {Float64Array}
300
+ */
301
+ __wheel_response = new Float64Array(0);
302
+
303
+ /**
304
+ * The wheel's block of `J·W·Jᵀ`, how a unit tyre impulse changes its patch's slip: forward on forward,
305
+ * side on side, and either on the other, 3 per wheel.
306
+ * @type {Float64Array}
307
+ */
308
+ __wheel_block = new Float64Array(0);
309
+
310
+ // ── The springs that push freely in the solve before the tyres, answering them ──
311
+
312
+ /** How many there are. */
313
+ __free_count = 0;
314
+
315
+ /** Their rows. @type {Uint32Array} */
316
+ __free_row = new Uint32Array(0);
317
+
318
+ /**
319
+ * Their block of {@link __row_matrix}, `J_s·M⁻¹·J_sᵀ + Γ`, times {@link __free_scale}, LU-factored.
320
+ * @type {Float64Array}
321
+ */
322
+ __free_matrix = new Float64Array(0);
323
+
324
+ /** The LU factorisation's pivots. @type {Uint32Array} */
325
+ __free_pivot = new Uint32Array(0);
326
+
327
+ /** One over the block's largest diagonal. */
328
+ __free_scale = 1;
329
+
330
+ /** Each free spring's speed per unit of one tyre impulse, `J_s·M⁻¹·Jᵀ`. @type {Float64Array} */
331
+ __free_speed = new Float64Array(0);
152
332
 
153
333
  /**
154
- * World contact point handed to applyForceAt / applyImpulseAt, which read a pose by index so a
155
- * transform's own translation view can go straight in.
334
+ * How much less each free spring pushes per unit of one tyre impulse,
335
+ * `(J_s·M⁻¹·J_sᵀ + Γ)⁻¹·J_s·M⁻¹·Jᵀ`.
156
336
  * @type {Float64Array}
157
337
  */
158
- __point = new Float64Array(3);
338
+ __free_answer = new Float64Array(0);
339
+
340
+ // ── Per spring row: a grounded wheel whose spring has stiffness or damping ──
341
+
342
+ /** The wheel. @type {Uint32Array} */
343
+ __row_wheel = new Uint32Array(0);
344
+
345
+ /** The contact normal `n`, then `r × n`, 6 per row. @type {Float64Array} */
346
+ __row_jacobian = new Float64Array(0);
347
+
348
+ /**
349
+ * `M⁻¹`'s angular part on `r × n`: the chassis' mean spin across the step per unit impulse along the row,
350
+ * 3 per row.
351
+ * @type {Float64Array}
352
+ */
353
+ __row_response = new Float64Array(0);
354
+
355
+ /** The spring's compliance `γ`. @type {Float64Array} */
356
+ __row_compliance = new Float64Array(0);
357
+
358
+ /** `β·x`, the velocity the spring restores at. @type {Float64Array} */
359
+ __row_restore = new Float64Array(0);
360
+
361
+ /** `β·x − J·v̄`: what the spring asks for against where the step carries the chassis. @type {Float64Array} */
362
+ __row_rhs = new Float64Array(0);
363
+
364
+ /** The least impulse: a spring does not pull. @type {Float64Array} */
365
+ __row_lower = new Float64Array(0);
366
+
367
+ /** The most impulse, `suspensionMaxForce·dt`. @type {Float64Array} */
368
+ __row_upper = new Float64Array(0);
369
+
370
+ /** The impulses solved for. @type {Float64Array} */
371
+ __row_impulse = new Float64Array(0);
372
+
373
+ /** Rows × rows, column-major: `J·M⁻¹·Jᵀ` plus each row's compliance on the diagonal. @type {Float64Array} */
374
+ __row_matrix = new Float64Array(0);
159
375
 
160
376
  /**
161
377
  * @param {PhysicsSystem} system
@@ -215,10 +431,115 @@ export class RaycastVehicle {
215
431
  return w;
216
432
  }
217
433
 
218
- /** Set the steering angle (rad) on every steered wheel. */
434
+ /**
435
+ * Steer: turn every steered wheel so that its axle passes through one turn centre on a line across the
436
+ * chassis, and the tyres' grips agree on the point the chassis turns about. Each wheel's own angle goes
437
+ * into its {@link Wheel#steering}, from the wheels as they are configured when this is called. The
438
+ * wheels are taken to share their suspension axis and their straight-ahead direction, as a car's do;
439
+ * where they differ, their axles need not meet.
440
+ *
441
+ * The line passes through the unsteered wheels' mean mount position: the line their axles share.
442
+ * Where they sit on more than one line, no centre suits them all, and this one leaves them the least
443
+ * side slip, summed in squares. With every wheel steered, it passes through all the wheels' mean
444
+ * position: the line that needs the least steering, summed in squares, in a gentle turn.
445
+ *
446
+ * `angle` is the single-track ("bicycle") model's steering angle of the steered wheels farthest from
447
+ * the line, which turn the most: that of one wheel standing at the steered wheels' mean place across
448
+ * the chassis and as far ahead of the line as they are from it, with its axle through the centre.
449
+ * Steered wheels nearer the line turn less, and one on it changes nothing. Positive turns the chassis
450
+ * toward `up × forward` (+x for the default axes), where a positive {@link Wheel#steering} turns a
451
+ * wheel's forward direction; a steered wheel behind the line turns the other way. At 0 every wheel
452
+ * points straight. At ±π/2 the chassis pivots about the point of the line beside that one wheel, and
453
+ * the steered wheels on the inside of it turn past π/2. With every steered wheel on the line, to the
454
+ * rounding of the mean positions, none can move the centre along it, and each points straight.
455
+ *
456
+ * @param {number} angle radians
457
+ */
219
458
  setSteering(angle) {
220
- for (let i = 0; i < this.wheels.length; i++) {
221
- if (this.wheels[i].steered) this.wheels[i].steering = angle;
459
+ const wheels = this.wheels;
460
+ const wheel_count = wheels.length;
461
+
462
+ // The unsteered wheels' and the steered wheels' mean mount positions, and the largest coordinate,
463
+ // which bounds the rounding those means carry.
464
+ let unsteered = 0, ux = 0, uy = 0, uz = 0;
465
+ let steered = 0, sx = 0, sy = 0, sz = 0;
466
+ let extent = 0;
467
+
468
+ for (let i = 0; i < wheel_count; i++) {
469
+ const w = wheels[i];
470
+ const p = w.localPosition;
471
+
472
+ if (w.steered) {
473
+ sx += p[0]; sy += p[1]; sz += p[2];
474
+ steered++;
475
+ } else {
476
+ ux += p[0]; uy += p[1]; uz += p[2];
477
+ unsteered++;
478
+ }
479
+
480
+ extent = Math.max(extent, Math.abs(p[0]), Math.abs(p[1]), Math.abs(p[2]));
481
+ }
482
+
483
+ if (steered === 0) {
484
+ return;
485
+ }
486
+
487
+ sx /= steered; sy /= steered; sz /= steered;
488
+
489
+ // A point of the centre's line: the unsteered wheels' mean, or with none, every wheel's.
490
+ let cx = sx, cy = sy, cz = sz;
491
+
492
+ if (unsteered > 0) {
493
+ cx = ux / unsteered; cy = uy / unsteered; cz = uz / unsteered;
494
+ }
495
+
496
+ const plan = scratch_plan;
497
+
498
+ // How far ahead of the line the single-track wheel stands: as far as the steered wheels farthest
499
+ // from it.
500
+ let reach = 0;
501
+
502
+ for (let i = 0; i < wheel_count; i++) {
503
+ const w = wheels[i];
504
+
505
+ if (!w.steered) {
506
+ continue;
507
+ }
508
+
509
+ wheel_plan_frame(plan, w);
510
+
511
+ const p = w.localPosition;
512
+ const distance = Math.abs((p[0] - cx) * plan[0] + (p[1] - cy) * plan[1] + (p[2] - cz) * plan[2]);
513
+
514
+ if (distance > reach) {
515
+ reach = distance;
516
+ }
517
+ }
518
+
519
+ // A mean of n coordinates carries up to (n − 1)·ε/2 of the largest; taken along a unit direction and
520
+ // off a mount position, a distance under 2·n·ε of it is rounding, not a wheel off the line.
521
+ const rounding = 2 * wheel_count * Number.EPSILON * extent;
522
+
523
+ const sine = Math.sin(angle);
524
+ const cosine = Math.cos(angle);
525
+
526
+ for (let i = 0; i < wheel_count; i++) {
527
+ const w = wheels[i];
528
+
529
+ if (!w.steered) {
530
+ continue;
531
+ }
532
+
533
+ wheel_plan_frame(plan, w);
534
+
535
+ // The wheel's distance ahead of the line, and the single-track wheel's offset across from it.
536
+ const p = w.localPosition;
537
+ const ahead = (p[0] - cx) * plan[0] + (p[1] - cy) * plan[1] + (p[2] - cz) * plan[2];
538
+ const across = (sx - p[0]) * plan[3] + (sy - p[1]) * plan[4] + (sz - p[2]) * plan[5];
539
+
540
+ // The centre sits `reach·cot(angle)` across from the single-track wheel, on the line; the wheel
541
+ // turns square to the way to it, rolling as the single-track wheel does.
542
+ w.steering = reach > rounding ? Math.atan2(ahead * sine, across * sine + reach * cosine) : 0;
222
543
  }
223
544
  }
224
545
 
@@ -229,28 +550,105 @@ export class RaycastVehicle {
229
550
  setBrake(force) { this.__brakeForce = force; }
230
551
 
231
552
  /**
232
- * Step the vehicle: cast suspension rays, apply suspension + tyre forces.
233
- * Call once per frame **before** `PhysicsSystem.fixedUpdate(dt)` with the
234
- * same `dt`.
553
+ * Size the per-wheel and per-row storage for `wheel_count` wheels, keeping each wheel's last tyre
554
+ * impulse and its brake's part.
555
+ * @param {number} wheel_count
556
+ * @private
557
+ */
558
+ __reserve(wheel_count) {
559
+ if (this.__wheel_rolls.length >= wheel_count) {
560
+ return;
561
+ }
562
+
563
+ const tyre = new Float64Array(wheel_count * 2);
564
+ tyre.set(this.__wheel_tyre);
565
+ this.__wheel_tyre = tyre;
566
+
567
+ const brake = new Float64Array(wheel_count);
568
+ brake.set(this.__wheel_brake);
569
+ this.__wheel_brake = brake;
570
+
571
+ this.__wheel_frame = new Float64Array(wheel_count * 9);
572
+ this.__wheel_lever = new Float64Array(wheel_count * 3);
573
+ this.__wheel_rolls = new Uint8Array(wheel_count);
574
+ this.__wheel_budget = new Float64Array(wheel_count);
575
+ this.__wheel_response = new Float64Array(wheel_count * 12);
576
+ this.__wheel_block = new Float64Array(wheel_count * 3);
577
+
578
+ this.__free_row = new Uint32Array(wheel_count);
579
+ this.__free_matrix = new Float64Array(wheel_count * wheel_count);
580
+ this.__free_pivot = new Uint32Array(wheel_count);
581
+ this.__free_speed = new Float64Array(wheel_count);
582
+ this.__free_answer = new Float64Array(wheel_count);
583
+
584
+ this.__row_wheel = new Uint32Array(wheel_count);
585
+ this.__row_jacobian = new Float64Array(wheel_count * 6);
586
+ this.__row_response = new Float64Array(wheel_count * 3);
587
+ this.__row_compliance = new Float64Array(wheel_count);
588
+ this.__row_restore = new Float64Array(wheel_count);
589
+ this.__row_rhs = new Float64Array(wheel_count);
590
+ this.__row_lower = new Float64Array(wheel_count);
591
+ this.__row_upper = new Float64Array(wheel_count);
592
+ this.__row_impulse = new Float64Array(wheel_count);
593
+ this.__row_matrix = new Float64Array(wheel_count * wheel_count);
594
+ }
595
+
596
+ /**
597
+ * Step the vehicle: cast the suspension rays, apply the tyre impulses, then push the chassis up on its
598
+ * grounded springs. Every impulse acts on the chassis velocity at once, for the next
599
+ * `PhysicsSystem.fixedUpdate` to integrate. Call once per fixed step, with that step's `dt`.
235
600
  * @param {number} dt
236
601
  */
237
602
  update(dt) {
238
603
  const rb = this.chassisBody;
239
604
  if (rb.kind !== BodyKind.Dynamic || dt <= 0) return;
240
605
  const tr = this.chassisTransform;
241
- const invMass = rb.mass > 0 ? 1 / rb.mass : 0;
606
+ const wheels = this.wheels;
607
+ const wheel_count = wheels.length;
608
+
609
+ this.__reserve(wheel_count);
610
+
611
+ // M⁻¹: what of an impulse applied now the chassis' mean motion across the step carries.
612
+ this.system.predictStepMeanVelocityShare(scratch_share, rb, dt);
613
+
614
+ const mean_inverse_mass = rb.mass > 0 ? scratch_share[0] / rb.mass : 0;
615
+ const mean_inverse_inertia = this.__mean_inverse_inertia;
616
+
617
+ locked_inverse_inertia(mean_inverse_inertia, 0, rb);
618
+ mean_inverse_inertia[0] *= scratch_share[1];
619
+ mean_inverse_inertia[1] *= scratch_share[1];
620
+ mean_inverse_inertia[2] *= scratch_share[1];
621
+
622
+ this.__mean_inverse_mass = mean_inverse_mass;
623
+
242
624
  const qx = tr.rotation_x, qy = tr.rotation_y, qz = tr.rotation_z, qw = tr.rotation_w;
243
625
  const px = tr.translation_x, py = tr.translation_y, pz = tr.translation_z;
244
- const lv = rb.linearVelocity, av = rb.angularVelocity;
245
- let drivenCount = 0;
246
- for (let i = 0; i < this.wheels.length; i++) {
247
- if (this.wheels[i].driven) drivenCount++;
248
- }
249
- this.__drivenCount = drivenCount;
626
+ let drivenCount = 0;
627
+ for (let i = 0; i < wheel_count; i++) {
628
+ if (wheels[i].driven) drivenCount++;
629
+ }
630
+ this.__drivenCount = drivenCount;
250
631
  const drivePer = drivenCount > 0 ? this.__driveForce / drivenCount : 0;
251
632
 
252
- for (let wi = 0; wi < this.wheels.length; wi++) {
253
- const w = this.wheels[wi];
633
+ const wheel_frame = this.__wheel_frame;
634
+ const wheel_lever = this.__wheel_lever;
635
+ const wheel_rolls = this.__wheel_rolls;
636
+ const wheel_tyre = this.__wheel_tyre;
637
+ const wheel_budget = this.__wheel_budget;
638
+ const row_wheel = this.__row_wheel;
639
+ const row_compliance = this.__row_compliance;
640
+ const row_restore = this.__row_restore;
641
+ const row_upper = this.__row_upper;
642
+
643
+ scratch_spring_params[2] = dt;
644
+
645
+ let rows = 0;
646
+
647
+ // --- Suspension rays: each grounded wheel whose spring works is a row of the spring solve ---
648
+ for (let wi = 0; wi < wheel_count; wi++) {
649
+ const w = wheels[wi];
650
+
651
+ wheel_budget[wi] = 0;
254
652
 
255
653
  // World mount point and suspension axis.
256
654
  v3_quaternion_apply(scratch_v, 0, w.localPosition[0], w.localPosition[1], w.localPosition[2], qx, qy, qz, qw);
@@ -276,12 +674,15 @@ export class RaycastVehicle {
276
674
  w.compression = 0;
277
675
  w.contactBodyId = -1; // documented no-contact sentinel
278
676
  w.forwardSpeed = 0;
677
+ wheel_tyre[wi * 2] = 0;
678
+ wheel_tyre[wi * 2 + 1] = 0;
279
679
  continue;
280
680
  }
281
681
 
282
682
  const t = this.__hit.t;
283
683
  w.inContact = true;
284
684
  w.contactBodyId = this.__hit.body_id;
685
+ w.suspensionForce = 0;
285
686
  w.suspensionLength = t - w.radius;
286
687
  if (w.suspensionLength < 0) w.suspensionLength = 0;
287
688
  w.compression = w.suspensionRestLength - w.suspensionLength;
@@ -294,26 +695,10 @@ export class RaycastVehicle {
294
695
  w.contactPoint[0] = cx; w.contactPoint[1] = cy; w.contactPoint[2] = cz;
295
696
  w.contactNormal[0] = nx; w.contactNormal[1] = ny; w.contactNormal[2] = nz;
296
697
 
297
- // Velocity of the chassis at the contact point.
298
- const rx = cx - px, ry = cy - py, rz = cz - pz;
299
- const vptx = lv[0] + av[1] * rz - av[2] * ry;
300
- const vpty = lv[1] + av[2] * rx - av[0] * rz;
301
- const vptz = lv[2] + av[0] * ry - av[1] * rx;
302
-
303
- // --- Suspension: spring + damper along the contact normal ---
304
- const projVel = vptx * nx + vpty * ny + vptz * nz; // closing speed along normal
305
- let susForce = w.suspensionStiffness * w.compression - w.suspensionDamping * projVel;
306
- if (susForce < 0) susForce = 0;
307
- else if (susForce > w.suspensionMaxForce) susForce = w.suspensionMaxForce;
308
- w.suspensionForce = susForce;
309
-
310
- if (susForce !== 0) {
311
- v3_set(this.__force, 0, nx * susForce, ny * susForce, nz * susForce);
312
- this.__point[0] = cx; this.__point[1] = cy; this.__point[2] = cz;
313
- this.system.applyForceAt(rb, tr, this.__force, this.__point);
314
- }
698
+ const l = wi * 3;
699
+ wheel_lever[l] = cx - px; wheel_lever[l + 1] = cy - py; wheel_lever[l + 2] = cz - pz;
315
700
 
316
- // --- Tyre friction: forward / side basis in the contact plane ---
701
+ // Forward / side basis in the contact plane.
317
702
  v3_quaternion_apply(scratch_v, 0, w.localForward[0], w.localForward[1], w.localForward[2], qx, qy, qz, qw);
318
703
  let fwx = scratch_v[0], fwy = scratch_v[1], fwz = scratch_v[2];
319
704
  if (w.steering !== 0) {
@@ -322,54 +707,594 @@ export class RaycastVehicle {
322
707
  fwx = scratch_v[0]; fwy = scratch_v[1]; fwz = scratch_v[2];
323
708
  }
324
709
  // Project forward onto the contact plane, normalise.
325
- let fdotn = fwx * nx + fwy * ny + fwz * nz;
710
+ const fdotn = fwx * nx + fwy * ny + fwz * nz;
326
711
  fwx -= fdotn * nx; fwy -= fdotn * ny; fwz -= fdotn * nz;
327
- let fl = v3_length(fwx, fwy, fwz);
328
- if (fl < 1e-6) continue; // degenerate (forward parallel to normal)
329
- fwx /= fl; fwy /= fl; fwz /= fl;
330
- // Side = normal × forward (already unit & in-plane).
331
- const sx = ny * fwz - nz * fwy;
332
- const sy = nz * fwx - nx * fwz;
333
- const sz = nx * fwy - ny * fwx;
334
-
335
- const fwdVel = vptx * fwx + vpty * fwy + vptz * fwz;
336
- const sideVel = vptx * sx + vpty * sy + vptz * sz;
337
- w.forwardSpeed = fwdVel;
338
-
339
- // Lateral grip: impulse that cancels the side velocity.
340
- const denomSide = effective_mass(rb, tr, invMass, rx, ry, rz, sx, sy, sz);
341
- let jSide = denomSide > 0 ? -sideVel / denomSide : 0;
342
-
343
- // Longitudinal: engine drive minus brake (brake never reverses).
344
- let jFwd = w.driven ? drivePer * dt : 0;
345
- if (this.__brakeForce > 0 && fwdVel !== 0) {
346
- const denomFwd = effective_mass(rb, tr, invMass, rx, ry, rz, fwx, fwy, fwz);
347
- const stopImpulse = denomFwd > 0 ? Math.abs(fwdVel) / denomFwd : 0;
348
- const brakeImpulse = Math.min(this.__brakeForce * dt, stopImpulse);
349
- jFwd -= Math.sign(fwdVel) * brakeImpulse;
712
+ const fl = v3_length(fwx, fwy, fwz);
713
+ const rolls = fl >= 1e-6; // forward along the normal: the tyre can neither roll nor grip
714
+ if (rolls) {
715
+ fwx /= fl; fwy /= fl; fwz /= fl;
350
716
  }
351
717
 
352
- // Friction circle: clamp the combined tyre impulse to μ·N·dt.
353
- const budget = w.friction * susForce * dt;
354
- const mag = Math.sqrt(jFwd * jFwd + jSide * jSide);
355
- if (mag > budget && mag > 0) {
356
- const scale = budget / mag;
357
- jFwd *= scale; jSide *= scale;
718
+ const f = wi * 9;
719
+ wheel_frame[f + FRAME_NORMAL] = nx;
720
+ wheel_frame[f + FRAME_NORMAL + 1] = ny;
721
+ wheel_frame[f + FRAME_NORMAL + 2] = nz;
722
+ wheel_frame[f + FRAME_FORWARD] = fwx;
723
+ wheel_frame[f + FRAME_FORWARD + 1] = fwy;
724
+ wheel_frame[f + FRAME_FORWARD + 2] = fwz;
725
+ // Side = normal × forward (already unit & in-plane).
726
+ wheel_frame[f + FRAME_SIDE] = ny * fwz - nz * fwy;
727
+ wheel_frame[f + FRAME_SIDE + 1] = nz * fwx - nx * fwz;
728
+ wheel_frame[f + FRAME_SIDE + 2] = nx * fwy - ny * fwx;
729
+ wheel_rolls[wi] = rolls ? 1 : 0;
730
+
731
+ scratch_spring_params[0] = w.suspensionStiffness;
732
+ scratch_spring_params[1] = w.suspensionDamping;
733
+ if (!soft_spring_coefficients(scratch_spring, 0, scratch_spring_params, 0)) {
734
+ continue; // no stiffness and no damping: the spring pushes nothing, so the tyre grips nothing
358
735
  }
359
736
 
360
- if (jFwd !== 0 || jSide !== 0) {
361
- v3_set(
362
- this.__force, 0,
363
- fwx * jFwd + sx * jSide,
364
- fwy * jFwd + sy * jSide,
365
- fwz * jFwd + sz * jSide
737
+ // A spring: J·v̄⁺ + β·C + γ·λ = 0 with C = −compression, so (J·M⁻¹·Jᵀ + γ)·λ = β·compression − J·v̄.
738
+ row_wheel[rows] = wi;
739
+ row_compliance[rows] = scratch_spring[0];
740
+ row_restore[rows] = scratch_spring[1] * w.compression;
741
+ row_upper[rows] = w.suspensionMaxForce * dt;
742
+ rows++;
743
+ }
744
+
745
+ const mean_linear = this.__mean_linear;
746
+ const mean_angular = this.__mean_angular;
747
+
748
+ if (rows > 0) {
749
+ this.__prepare_springs(rows);
750
+
751
+ // Each tyre's friction circle, from the push its spring gives with every tyre pushing as it did
752
+ // the step before.
753
+ this.system.predictStepMeanVelocity(mean_linear, mean_angular, rb, tr, dt);
754
+
755
+ for (let r = 0; r < rows; r++) {
756
+ const wi = row_wheel[r];
757
+
758
+ if (wheel_rolls[wi] === 0) {
759
+ continue;
760
+ }
761
+
762
+ const forward = wheel_tyre[wi * 2];
763
+ const side = wheel_tyre[wi * 2 + 1];
764
+
765
+ if (forward === 0 && side === 0) {
766
+ continue;
767
+ }
768
+
769
+ const f = wi * 9;
770
+ const jx = wheel_frame[f + FRAME_FORWARD] * forward + wheel_frame[f + FRAME_SIDE] * side;
771
+ const jy = wheel_frame[f + FRAME_FORWARD + 1] * forward + wheel_frame[f + FRAME_SIDE + 1] * side;
772
+ const jz = wheel_frame[f + FRAME_FORWARD + 2] * forward + wheel_frame[f + FRAME_SIDE + 2] * side;
773
+
774
+ const l = wi * 3;
775
+ const rx = wheel_lever[l], ry = wheel_lever[l + 1], rz = wheel_lever[l + 2];
776
+
777
+ mean_linear[0] += jx * mean_inverse_mass;
778
+ mean_linear[1] += jy * mean_inverse_mass;
779
+ mean_linear[2] += jz * mean_inverse_mass;
780
+
781
+ world_inverse_inertia_apply_raw(
782
+ scratch_v, 0,
783
+ mean_inverse_inertia[0], mean_inverse_inertia[1], mean_inverse_inertia[2],
784
+ qx, qy, qz, qw,
785
+ ry * jz - rz * jy, rz * jx - rx * jz, rx * jy - ry * jx
366
786
  );
367
- this.__point[0] = cx; this.__point[1] = cy; this.__point[2] = cz;
368
- this.system.applyImpulseAt(rb, tr, this.__force, this.__point);
787
+
788
+ mean_angular[0] += scratch_v[0];
789
+ mean_angular[1] += scratch_v[1];
790
+ mean_angular[2] += scratch_v[2];
791
+ }
792
+
793
+ this.__solve_springs(rows);
794
+
795
+ const impulse = this.__row_impulse;
796
+ for (let r = 0; r < rows; r++) {
797
+ const wi = row_wheel[r];
798
+ wheel_budget[wi] = wheels[wi].friction * impulse[r];
799
+ }
800
+
801
+ this.__prepare_tyres(rows);
802
+ }
803
+
804
+ // --- Tyre friction, against the step's motion with the springs answering ---
805
+ const wheel_brake = this.__wheel_brake;
806
+ const brake_limit = this.__brakeForce * dt;
807
+
808
+ // Each tyre starts from the impulse it applied the step before, within this step's brake and
809
+ // friction circle.
810
+ for (let wi = 0; wi < wheel_count; wi++) {
811
+ const w = wheels[wi];
812
+
813
+ const side_before = wheel_tyre[wi * 2 + 1];
814
+
815
+ wheel_tyre[wi * 2] = 0;
816
+ wheel_tyre[wi * 2 + 1] = 0;
817
+
818
+ if (!w.inContact || wheel_rolls[wi] === 0) {
819
+ wheel_brake[wi] = 0;
820
+ continue;
821
+ }
822
+
823
+ const budget = wheel_budget[wi];
824
+
825
+ if (budget <= 0) {
826
+ wheel_brake[wi] = 0; // nothing pushes the patch into the ground, so it grips nothing
827
+ continue;
828
+ }
829
+
830
+ const brake = wheel_brake[wi];
831
+
832
+ scratch_grip[0] = w.driven ? drivePer * dt : 0;
833
+ scratch_grip[1] = brake < -brake_limit ? -brake_limit : (brake > brake_limit ? brake_limit : brake);
834
+ scratch_grip[2] = side_before;
835
+ this.__grip(wi);
836
+ }
837
+
838
+ // Each in turn cancels its patch's slip in that motion: across with grip, along with the brake, up
839
+ // to its force.
840
+ if (rows > 0) {
841
+ const ml = this.__mean_linear;
842
+ const ma = this.__mean_angular;
843
+ const wheel_block = this.__wheel_block;
844
+
845
+ for (let wi = 0; wi < wheel_count; wi++) {
846
+ const w = wheels[wi];
847
+
848
+ if (!w.inContact || wheel_rolls[wi] === 0 || wheel_budget[wi] <= 0) {
849
+ continue;
850
+ }
851
+
852
+ const l = wi * 3;
853
+ const rx = wheel_lever[l], ry = wheel_lever[l + 1], rz = wheel_lever[l + 2];
854
+
855
+ // The patch's velocity in the chassis' mean motion across the step.
856
+ const ux = ml[0] + ma[1] * rz - ma[2] * ry;
857
+ const uy = ml[1] + ma[2] * rx - ma[0] * rz;
858
+ const uz = ml[2] + ma[0] * ry - ma[1] * rx;
859
+
860
+ const f = wi * 9;
861
+ const forward_slip = ux * wheel_frame[f + FRAME_FORWARD] + uy * wheel_frame[f + FRAME_FORWARD + 1] + uz * wheel_frame[f + FRAME_FORWARD + 2];
862
+ const side_slip = ux * wheel_frame[f + FRAME_SIDE] + uy * wheel_frame[f + FRAME_SIDE + 1] + uz * wheel_frame[f + FRAME_SIDE + 2];
863
+
864
+ // Both at once: the impulses `K⁻¹·slip`, `K` this wheel's block of `J·W·Jᵀ`. With the brake at
865
+ // its force, the side cancels what the brake's change leaves.
866
+ const b = wi * 3;
867
+ const k_forward = wheel_block[b];
868
+ const k_side = wheel_block[b + 1];
869
+ const k_coupling = wheel_block[b + 2];
870
+
871
+ let brake = 0;
872
+
873
+ if (brake_limit > 0) {
874
+ const determinant = k_forward * k_side - k_coupling * k_coupling;
875
+ brake = wheel_brake[wi] - (determinant > 0 ? (k_side * forward_slip - k_coupling * side_slip) / determinant : 0);
876
+ brake = brake < -brake_limit ? -brake_limit : (brake > brake_limit ? brake_limit : brake);
877
+ }
878
+
879
+ const brake_change = brake - wheel_brake[wi];
880
+
881
+ scratch_grip[0] = w.driven ? drivePer * dt : 0;
882
+ scratch_grip[1] = brake;
883
+ scratch_grip[2] = wheel_tyre[wi * 2 + 1] - (k_side > 0 ? (side_slip + k_coupling * brake_change) / k_side : 0);
884
+ this.__grip(wi);
885
+ }
886
+ }
887
+
888
+ // Each wheel's ground speed: its contact's in the step's mean motion, every impulse above in it.
889
+ if (rows === 0) {
890
+ this.system.predictStepMeanVelocity(mean_linear, mean_angular, rb, tr, dt);
891
+ }
892
+
893
+ for (let wi = 0; wi < wheel_count; wi++) {
894
+ const w = wheels[wi];
895
+
896
+ if (!w.inContact) {
897
+ continue;
898
+ }
899
+
900
+ const l = wi * 3;
901
+ const rx = wheel_lever[l], ry = wheel_lever[l + 1], rz = wheel_lever[l + 2];
902
+
903
+ const f = wi * 9 + FRAME_FORWARD;
904
+ w.forwardSpeed = (mean_linear[0] + mean_angular[1] * rz - mean_angular[2] * ry) * wheel_frame[f]
905
+ + (mean_linear[1] + mean_angular[2] * rx - mean_angular[0] * rz) * wheel_frame[f + 1]
906
+ + (mean_linear[2] + mean_angular[0] * ry - mean_angular[1] * rx) * wheel_frame[f + 2];
907
+
908
+ // Visual wheel spin: the ground the contact covers over the step.
909
+ if (w.radius > 0) w.rotation += (w.forwardSpeed / w.radius) * dt;
910
+ }
911
+
912
+ // --- The springs, against where the step now carries the chassis, the tyres' impulses in it ---
913
+ if (rows > 0) {
914
+ this.system.predictStepMeanVelocity(mean_linear, mean_angular, rb, tr, dt);
915
+ this.__solve_springs(rows);
916
+
917
+ const impulse = this.__row_impulse;
918
+ const jacobian = this.__row_jacobian;
919
+ const out = this.__impulse;
920
+
921
+ for (let r = 0; r < rows; r++) {
922
+ const w = wheels[row_wheel[r]];
923
+ const lambda = impulse[r];
924
+
925
+ w.suspensionForce = lambda / dt;
926
+
927
+ if (lambda > 0) {
928
+ const j = r * 6;
929
+ v3_set(out, 0, jacobian[j] * lambda, jacobian[j + 1] * lambda, jacobian[j + 2] * lambda);
930
+ this.system.applyImpulseAt(rb, tr, out, w.contactPoint);
931
+ }
932
+ }
933
+ }
934
+ }
935
+
936
+ /**
937
+ * Fill the `rows` springs' Jacobians, the chassis' angular response to each, and the matrix
938
+ * `J·M⁻¹·Jᵀ` with each spring's compliance on its diagonal — all fixed by the pose, so shared by
939
+ * every solve of the step.
940
+ * @param {number} rows
941
+ * @private
942
+ */
943
+ __prepare_springs(rows) {
944
+ const tr = this.chassisTransform;
945
+ const inverse_mass = this.__mean_inverse_mass;
946
+ const inverse_inertia = this.__mean_inverse_inertia;
947
+ const frame = this.__wheel_frame;
948
+ const lever = this.__wheel_lever;
949
+ const row_wheel = this.__row_wheel;
950
+ const jacobian = this.__row_jacobian;
951
+ const response = this.__row_response;
952
+ const compliance = this.__row_compliance;
953
+ const matrix = this.__row_matrix;
954
+
955
+ for (let r = 0; r < rows; r++) {
956
+ const wi = row_wheel[r];
957
+
958
+ const f = wi * 9 + FRAME_NORMAL;
959
+ const nx = frame[f], ny = frame[f + 1], nz = frame[f + 2];
960
+
961
+ const l = wi * 3;
962
+ const rx = lever[l], ry = lever[l + 1], rz = lever[l + 2];
963
+
964
+ const ax = ry * nz - rz * ny;
965
+ const ay = rz * nx - rx * nz;
966
+ const az = rx * ny - ry * nx;
967
+
968
+ const j = r * 6;
969
+ jacobian[j] = nx;
970
+ jacobian[j + 1] = ny;
971
+ jacobian[j + 2] = nz;
972
+ jacobian[j + 3] = ax;
973
+ jacobian[j + 4] = ay;
974
+ jacobian[j + 5] = az;
975
+
976
+ world_inverse_inertia_apply_raw(
977
+ response, r * 3,
978
+ inverse_inertia[0], inverse_inertia[1], inverse_inertia[2],
979
+ tr.rotation_x, tr.rotation_y, tr.rotation_z, tr.rotation_w,
980
+ ax, ay, az
981
+ );
982
+ }
983
+
984
+ for (let i = 0; i < rows; i++) {
985
+ const ji = i * 6;
986
+
987
+ for (let k = i; k < rows; k++) {
988
+ const jk = k * 6;
989
+ const rk = k * 3;
990
+
991
+ const value = inverse_mass * (
992
+ jacobian[ji] * jacobian[jk]
993
+ + jacobian[ji + 1] * jacobian[jk + 1]
994
+ + jacobian[ji + 2] * jacobian[jk + 2]
995
+ )
996
+ + jacobian[ji + 3] * response[rk]
997
+ + jacobian[ji + 4] * response[rk + 1]
998
+ + jacobian[ji + 5] * response[rk + 2];
999
+
1000
+ matrix[k * rows + i] = value;
1001
+ matrix[i * rows + k] = value;
369
1002
  }
370
1003
 
371
- // Visual wheel spin from forward ground speed.
372
- if (w.radius > 0) w.rotation += (fwdVel / w.radius) * dt;
1004
+ matrix[i * rows + i] += compliance[i];
1005
+ }
1006
+ }
1007
+
1008
+ /**
1009
+ * Solve the `rows` prepared springs as one backward-Euler step against the chassis' mean motion in
1010
+ * {@link __mean_linear} / {@link __mean_angular}, into {@link __row_impulse}: each pushes, and no
1011
+ * harder than its most force.
1012
+ * @param {number} rows
1013
+ * @private
1014
+ */
1015
+ __solve_springs(rows) {
1016
+ const jacobian = this.__row_jacobian;
1017
+ const restore = this.__row_restore;
1018
+ const rhs = this.__row_rhs;
1019
+ const ml = this.__mean_linear;
1020
+ const ma = this.__mean_angular;
1021
+
1022
+ for (let r = 0; r < rows; r++) {
1023
+ const j = r * 6;
1024
+
1025
+ // The chassis' speed away from the ground at the contact, across the step: n·v̄ + (r × n)·ω̄.
1026
+ const separating = jacobian[j] * ml[0] + jacobian[j + 1] * ml[1] + jacobian[j + 2] * ml[2]
1027
+ + jacobian[j + 3] * ma[0] + jacobian[j + 4] * ma[1] + jacobian[j + 5] * ma[2];
1028
+
1029
+ rhs[r] = restore[r] - separating;
1030
+ }
1031
+
1032
+ solve_box_constrained_qp(this.__row_matrix, rhs, this.__row_lower, this.__row_upper, this.__row_impulse, rows);
1033
+ }
1034
+
1035
+ /**
1036
+ * Ready the tyres' solve, after the springs are solved against the tyres pushing as they did the step
1037
+ * before. Factors the springs that push freely in that solve, which answer the tyres; turns the motion
1038
+ * in {@link __mean_linear} / {@link __mean_angular} into the step's with those springs pushing and no
1039
+ * tyre; and works out each tyre's response, `W·Jᵀ` and `J·W·Jᵀ`.
1040
+ * @param {number} rows
1041
+ * @private
1042
+ */
1043
+ __prepare_tyres(rows) {
1044
+ const inverse_mass = this.__mean_inverse_mass;
1045
+ const impulse = this.__row_impulse;
1046
+ const lower = this.__row_lower;
1047
+ const upper = this.__row_upper;
1048
+ const matrix = this.__row_matrix;
1049
+ const jacobian = this.__row_jacobian;
1050
+ const response = this.__row_response;
1051
+ const free_row = this.__free_row;
1052
+ const free_matrix = this.__free_matrix;
1053
+
1054
+ // A spring between its bounds answers a tyre's push; one at a bound holds its impulse.
1055
+ let free_count = 0;
1056
+ let diagonal_max = 0;
1057
+
1058
+ for (let r = 0; r < rows; r++) {
1059
+ const lambda = impulse[r];
1060
+
1061
+ if (lambda > lower[r] && lambda < upper[r]) {
1062
+ free_row[free_count++] = r;
1063
+
1064
+ const d = matrix[r * rows + r];
1065
+
1066
+ if (d > diagonal_max) {
1067
+ diagonal_max = d;
1068
+ }
1069
+ }
1070
+ }
1071
+
1072
+ // lu_factor_linear_system refuses a pivot under an absolute 1e-12; with the largest diagonal scaled
1073
+ // to one, that is relative to the block.
1074
+ const scale = diagonal_max > 0 ? 1 / diagonal_max : 1;
1075
+
1076
+ for (let a = 0; a < free_count; a++) {
1077
+ const row = free_row[a];
1078
+
1079
+ for (let b = 0; b < free_count; b++) {
1080
+ free_matrix[b * free_count + a] = matrix[free_row[b] * rows + row] * scale;
1081
+ }
1082
+ }
1083
+
1084
+ // Positive definite, a compliance on every diagonal; should it still not factor, the tyres see the
1085
+ // chassis without the springs answering.
1086
+ if (free_count > 0 && !lu_factor_linear_system(free_matrix, this.__free_pivot, free_count)) {
1087
+ free_count = 0;
1088
+ }
1089
+
1090
+ this.__free_count = free_count;
1091
+ this.__free_scale = scale;
1092
+
1093
+ // The springs' push in the step's motion.
1094
+ const ml = this.__mean_linear;
1095
+ const ma = this.__mean_angular;
1096
+
1097
+ for (let r = 0; r < rows; r++) {
1098
+ const lambda = impulse[r];
1099
+ const j = r * 6;
1100
+ const k = r * 3;
1101
+
1102
+ ml[0] += jacobian[j] * lambda * inverse_mass;
1103
+ ml[1] += jacobian[j + 1] * lambda * inverse_mass;
1104
+ ml[2] += jacobian[j + 2] * lambda * inverse_mass;
1105
+
1106
+ ma[0] += response[k] * lambda;
1107
+ ma[1] += response[k + 1] * lambda;
1108
+ ma[2] += response[k + 2] * lambda;
1109
+ }
1110
+
1111
+ // Each tyre's response; and out with the push the spring solve took it to give.
1112
+ const row_wheel = this.__row_wheel;
1113
+ const rolls = this.__wheel_rolls;
1114
+ const tyre = this.__wheel_tyre;
1115
+ const wheel_response = this.__wheel_response;
1116
+ const lever = this.__wheel_lever;
1117
+ const frame = this.__wheel_frame;
1118
+
1119
+ for (let r = 0; r < rows; r++) {
1120
+ const wi = row_wheel[r];
1121
+
1122
+ if (rolls[wi] === 0) {
1123
+ continue;
1124
+ }
1125
+
1126
+ this.__tyre_response(wi, FRAME_FORWARD);
1127
+ this.__tyre_response(wi, FRAME_SIDE);
1128
+
1129
+ const o = wi * 12;
1130
+
1131
+ // The coupling, `J_f·W·J_sᵀ`: the patch's forward speed per unit side impulse.
1132
+ const rx = lever[wi * 3], ry = lever[wi * 3 + 1], rz = lever[wi * 3 + 2];
1133
+ const s = o + 6;
1134
+ const f = wi * 9 + FRAME_FORWARD;
1135
+
1136
+ this.__wheel_block[wi * 3 + 2] =
1137
+ (wheel_response[s] + wheel_response[s + 4] * rz - wheel_response[s + 5] * ry) * frame[f]
1138
+ + (wheel_response[s + 1] + wheel_response[s + 5] * rx - wheel_response[s + 3] * rz) * frame[f + 1]
1139
+ + (wheel_response[s + 2] + wheel_response[s + 3] * ry - wheel_response[s + 4] * rx) * frame[f + 2];
1140
+
1141
+ const forward = tyre[wi * 2];
1142
+ const side = tyre[wi * 2 + 1];
1143
+
1144
+ for (let i = 0; i < 3; i++) {
1145
+ ml[i] -= wheel_response[o + i] * forward + wheel_response[o + 6 + i] * side;
1146
+ ma[i] -= wheel_response[o + 3 + i] * forward + wheel_response[o + 9 + i] * side;
1147
+ }
1148
+ }
1149
+ }
1150
+
1151
+ /**
1152
+ * The chassis' mean velocity across the step per unit impulse along wheel `wi`'s forward or side
1153
+ * direction at its contact, the free springs answering: `W·Jᵀ`, linear then angular, into
1154
+ * {@link __wheel_response}, and `J·W·Jᵀ`, the change of the contact's speed along that direction per
1155
+ * unit impulse, into its diagonal of {@link __wheel_block}.
1156
+ * @param {number} wi
1157
+ * @param {number} direction {@link FRAME_FORWARD} or {@link FRAME_SIDE}
1158
+ * @private
1159
+ */
1160
+ __tyre_response(wi, direction) {
1161
+ const tr = this.chassisTransform;
1162
+ const inverse_mass = this.__mean_inverse_mass;
1163
+ const inverse_inertia = this.__mean_inverse_inertia;
1164
+
1165
+ const lever = this.__wheel_lever;
1166
+ const rx = lever[wi * 3], ry = lever[wi * 3 + 1], rz = lever[wi * 3 + 2];
1167
+
1168
+ const frame = this.__wheel_frame;
1169
+ const f = wi * 9 + direction;
1170
+ const dx = frame[f], dy = frame[f + 1], dz = frame[f + 2];
1171
+
1172
+ const ax = ry * dz - rz * dy;
1173
+ const ay = rz * dx - rx * dz;
1174
+ const az = rx * dy - ry * dx;
1175
+
1176
+ // M⁻¹·Jᵀ: the free chassis.
1177
+ world_inverse_inertia_apply_raw(
1178
+ scratch_v, 0,
1179
+ inverse_inertia[0], inverse_inertia[1], inverse_inertia[2],
1180
+ tr.rotation_x, tr.rotation_y, tr.rotation_z, tr.rotation_w,
1181
+ ax, ay, az
1182
+ );
1183
+
1184
+ let vx = dx * inverse_mass, vy = dy * inverse_mass, vz = dz * inverse_mass;
1185
+ let wx = scratch_v[0], wy = scratch_v[1], wz = scratch_v[2];
1186
+ let denominator = dx * vx + dy * vy + dz * vz + ax * wx + ay * wy + az * wz;
1187
+
1188
+ const free_count = this.__free_count;
1189
+
1190
+ if (free_count > 0) {
1191
+ const free_row = this.__free_row;
1192
+ const jacobian = this.__row_jacobian;
1193
+ const response = this.__row_response;
1194
+ const speed = this.__free_speed;
1195
+ const answer = this.__free_answer;
1196
+ const scale = this.__free_scale;
1197
+
1198
+ for (let a = 0; a < free_count; a++) {
1199
+ const j = free_row[a] * 6;
1200
+
1201
+ const s = jacobian[j] * vx + jacobian[j + 1] * vy + jacobian[j + 2] * vz
1202
+ + jacobian[j + 3] * wx + jacobian[j + 4] * wy + jacobian[j + 5] * wz;
1203
+
1204
+ speed[a] = s;
1205
+ answer[a] = s * scale;
1206
+ }
1207
+
1208
+ lu_solve_linear_system(this.__free_matrix, this.__free_pivot, answer, free_count);
1209
+
1210
+ // Less each free spring's push.
1211
+ for (let a = 0; a < free_count; a++) {
1212
+ const r = free_row[a];
1213
+ const y = answer[a];
1214
+ const j = r * 6;
1215
+ const k = r * 3;
1216
+
1217
+ denominator -= speed[a] * y;
1218
+
1219
+ vx -= jacobian[j] * y * inverse_mass;
1220
+ vy -= jacobian[j + 1] * y * inverse_mass;
1221
+ vz -= jacobian[j + 2] * y * inverse_mass;
1222
+
1223
+ wx -= response[k] * y;
1224
+ wy -= response[k + 1] * y;
1225
+ wz -= response[k + 2] * y;
1226
+ }
1227
+ }
1228
+
1229
+ const along_side = direction === FRAME_SIDE ? 1 : 0;
1230
+ const out = this.__wheel_response;
1231
+ const offset = wi * 12 + along_side * 6;
1232
+
1233
+ out[offset] = vx;
1234
+ out[offset + 1] = vy;
1235
+ out[offset + 2] = vz;
1236
+ out[offset + 3] = wx;
1237
+ out[offset + 4] = wy;
1238
+ out[offset + 5] = wz;
1239
+
1240
+ this.__wheel_block[wi * 3 + along_side] = denominator;
1241
+ }
1242
+
1243
+ /**
1244
+ * Set wheel `wi`'s tyre impulse for the step from {@link scratch_grip}'s drive, brake and side:
1245
+ * `drive + brake` along its forward direction and `side` along its side, clamped together to its
1246
+ * friction circle. The change applies to the chassis, and to its motion in {@link __mean_linear} /
1247
+ * {@link __mean_angular} with the free springs answering.
1248
+ * @param {number} wi
1249
+ * @private
1250
+ */
1251
+ __grip(wi) {
1252
+ const tyre = this.__wheel_tyre;
1253
+ const budget = this.__wheel_budget[wi];
1254
+
1255
+ const drive = scratch_grip[0];
1256
+ let forward = drive + scratch_grip[1];
1257
+ let side = scratch_grip[2];
1258
+
1259
+ const magnitude = Math.sqrt(forward * forward + side * side);
1260
+
1261
+ if (magnitude > budget) {
1262
+ const s = budget / magnitude;
1263
+ forward *= s;
1264
+ side *= s;
1265
+ }
1266
+
1267
+ this.__wheel_brake[wi] = forward - drive;
1268
+
1269
+ const d_forward = forward - tyre[wi * 2];
1270
+ const d_side = side - tyre[wi * 2 + 1];
1271
+
1272
+ tyre[wi * 2] = forward;
1273
+ tyre[wi * 2 + 1] = side;
1274
+
1275
+ if (d_forward === 0 && d_side === 0) {
1276
+ return;
1277
+ }
1278
+
1279
+ const frame = this.__wheel_frame;
1280
+ const f = wi * 9;
1281
+
1282
+ v3_set(
1283
+ this.__impulse, 0,
1284
+ frame[f + FRAME_FORWARD] * d_forward + frame[f + FRAME_SIDE] * d_side,
1285
+ frame[f + FRAME_FORWARD + 1] * d_forward + frame[f + FRAME_SIDE + 1] * d_side,
1286
+ frame[f + FRAME_FORWARD + 2] * d_forward + frame[f + FRAME_SIDE + 2] * d_side
1287
+ );
1288
+ this.system.applyImpulseAt(this.chassisBody, this.chassisTransform, this.__impulse, this.wheels[wi].contactPoint);
1289
+
1290
+ const response = this.__wheel_response;
1291
+ const o = wi * 12;
1292
+ const ml = this.__mean_linear;
1293
+ const ma = this.__mean_angular;
1294
+
1295
+ for (let i = 0; i < 3; i++) {
1296
+ ml[i] += response[o + i] * d_forward + response[o + 6 + i] * d_side;
1297
+ ma[i] += response[o + 3 + i] * d_forward + response[o + 9 + i] * d_side;
373
1298
  }
374
1299
  }
375
1300
  }