@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.
- package/CHANGELOG.md +51 -0
- package/package.json +1 -1
- package/src/core/color/xyz/m3_chromatic_adaptation_bradford.d.ts +14 -0
- package/src/core/color/xyz/m3_chromatic_adaptation_bradford.d.ts.map +1 -0
- package/src/core/color/xyz/m3_chromatic_adaptation_bradford.js +51 -0
- package/src/core/math/linalg/eigen/matrix_symmetric_3x3_eigen.d.ts +2 -1
- package/src/core/math/linalg/eigen/matrix_symmetric_3x3_eigen.d.ts.map +1 -1
- package/src/core/math/linalg/eigen/matrix_symmetric_3x3_eigen.js +51 -70
- package/src/core/math/linalg/eigen/matrix_symmetric_4x4_eigen.d.ts.map +1 -1
- package/src/core/math/linalg/eigen/matrix_symmetric_4x4_eigen.js +8 -64
- package/src/core/math/linalg/eigen/matrix_symmetric_jacobi_rotate_in_place.d.ts +17 -0
- package/src/core/math/linalg/eigen/matrix_symmetric_jacobi_rotate_in_place.d.ts.map +1 -0
- package/src/core/math/linalg/eigen/matrix_symmetric_jacobi_rotate_in_place.js +64 -0
- package/src/core/math/linalg/solve_box_constrained_qp.d.ts +30 -0
- package/src/core/math/linalg/solve_box_constrained_qp.d.ts.map +1 -0
- package/src/core/math/linalg/solve_box_constrained_qp.js +205 -0
- package/src/engine/graphics/texture/sampler/exr_to_sampler2d.d.ts +27 -0
- package/src/engine/graphics/texture/sampler/exr_to_sampler2d.d.ts.map +1 -0
- package/src/engine/graphics/texture/sampler/exr_to_sampler2d.js +91 -0
- package/src/engine/graphics/texture/sampler/sampler2d_to_exr.d.ts +23 -0
- package/src/engine/graphics/texture/sampler/sampler2d_to_exr.d.ts.map +1 -0
- package/src/engine/graphics/texture/sampler/sampler2d_to_exr.js +64 -0
- package/src/engine/physics/constraint/soft_spring_coefficients.d.ts +24 -0
- package/src/engine/physics/constraint/soft_spring_coefficients.d.ts.map +1 -0
- package/src/engine/physics/constraint/soft_spring_coefficients.js +38 -0
- package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -1
- package/src/engine/physics/constraint/solve_constraints.js +16 -8
- package/src/engine/physics/ecs/PhysicsSystem.d.ts +54 -0
- package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
- package/src/engine/physics/ecs/PhysicsSystem.js +192 -7
- package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- package/src/engine/physics/inertia/world_inverse_inertia.d.ts +3 -5
- package/src/engine/physics/inertia/world_inverse_inertia.d.ts.map +1 -1
- package/src/engine/physics/inertia/world_inverse_inertia.js +3 -5
- package/src/engine/physics/vehicle/RaycastVehicle.d.ts +183 -12
- package/src/engine/physics/vehicle/RaycastVehicle.d.ts.map +1 -1
- package/src/engine/physics/vehicle/RaycastVehicle.js +1046 -121
- package/src/format/image/exr/ExrCompression.d.ts +22 -0
- package/src/format/image/exr/ExrCompression.d.ts.map +1 -0
- package/src/format/image/exr/ExrCompression.js +27 -0
- package/src/format/image/exr/ExrImage.d.ts +32 -0
- package/src/format/image/exr/ExrImage.d.ts.map +1 -0
- package/src/format/image/exr/ExrImage.js +32 -0
- package/src/format/image/exr/ExrPixelType.d.ts +13 -0
- package/src/format/image/exr/ExrPixelType.d.ts.map +1 -0
- package/src/format/image/exr/ExrPixelType.js +18 -0
- package/src/format/image/exr/exr_compare_names.d.ts +16 -0
- package/src/format/image/exr/exr_compare_names.d.ts.map +1 -0
- package/src/format/image/exr/exr_compare_names.js +25 -0
- package/src/format/image/exr/exr_constants.d.ts +49 -0
- package/src/format/image/exr/exr_constants.d.ts.map +1 -0
- package/src/format/image/exr/exr_constants.js +56 -0
- package/src/format/image/exr/exr_read.d.ts +32 -0
- package/src/format/image/exr/exr_read.d.ts.map +1 -0
- package/src/format/image/exr/exr_read.js +531 -0
- package/src/format/image/exr/exr_sample_bits.d.ts +16 -0
- package/src/format/image/exr/exr_sample_bits.d.ts.map +1 -0
- package/src/format/image/exr/exr_sample_bits.js +21 -0
- package/src/format/image/exr/exr_scanlines_per_chunk.d.ts +17 -0
- package/src/format/image/exr/exr_scanlines_per_chunk.d.ts.map +1 -0
- package/src/format/image/exr/exr_scanlines_per_chunk.js +31 -0
- package/src/format/image/exr/exr_write.d.ts +27 -0
- package/src/format/image/exr/exr_write.d.ts.map +1 -0
- package/src/format/image/exr/exr_write.js +378 -0
- package/src/shade/playground/probe_clipmap/index.html +5 -0
- package/src/shade/playground/probe_clipmap/main.d.ts.map +1 -1
- package/src/shade/playground/probe_clipmap/main.js +34 -1
- package/src/shade/playground/tonemap_sdr/README.md +11 -8
- package/src/shade/playground/tonemap_sdr/ToneMapCompare.js +2 -2
- package/src/shade/renderer/Renderer.d.ts +5 -6
- package/src/shade/renderer/Renderer.d.ts.map +1 -1
- package/src/shade/renderer/Renderer.js +15 -88
- package/src/shade/renderer/animation/chunk_animation_curve_evaluate_keys.d.ts +14 -0
- package/src/shade/renderer/animation/chunk_animation_curve_evaluate_keys.d.ts.map +1 -0
- package/src/shade/renderer/animation/chunk_animation_curve_evaluate_keys.js +62 -0
- package/src/shade/renderer/deferred/tonemap/ColorGrade.d.ts +158 -0
- package/src/shade/renderer/deferred/tonemap/ColorGrade.d.ts.map +1 -0
- package/src/shade/renderer/deferred/tonemap/ColorGrade.js +337 -0
- package/src/shade/renderer/deferred/tonemap/SAMPLER_TONEMAP_LUT.d.ts +9 -0
- package/src/shade/renderer/deferred/tonemap/SAMPLER_TONEMAP_LUT.d.ts.map +1 -0
- package/src/shade/renderer/deferred/tonemap/SAMPLER_TONEMAP_LUT.js +15 -0
- package/src/shade/renderer/deferred/tonemap/TONE_MAP_OPERATOR_ACES.d.ts +6 -0
- package/src/shade/renderer/deferred/tonemap/TONE_MAP_OPERATOR_ACES.d.ts.map +1 -0
- package/src/shade/renderer/deferred/tonemap/TONE_MAP_OPERATOR_ACES.js +84 -0
- package/src/shade/renderer/deferred/tonemap/TONE_MAP_OPERATOR_GT7.d.ts +10 -0
- package/src/shade/renderer/deferred/tonemap/TONE_MAP_OPERATOR_GT7.d.ts.map +1 -0
- package/src/shade/renderer/deferred/tonemap/TONE_MAP_OPERATOR_GT7.js +25 -0
- package/src/shade/renderer/deferred/tonemap/TONE_MAP_OPERATOR_NONE.d.ts +9 -0
- package/src/shade/renderer/deferred/tonemap/TONE_MAP_OPERATOR_NONE.d.ts.map +1 -0
- package/src/shade/renderer/deferred/tonemap/TONE_MAP_OPERATOR_NONE.js +20 -0
- package/src/shade/renderer/deferred/tonemap/TONE_MAP_OPERATOR_PBR_NEUTRAL.d.ts +14 -0
- package/src/shade/renderer/deferred/tonemap/TONE_MAP_OPERATOR_PBR_NEUTRAL.d.ts.map +1 -0
- package/src/shade/renderer/deferred/tonemap/TONE_MAP_OPERATOR_PBR_NEUTRAL.js +30 -0
- package/src/shade/renderer/deferred/tonemap/ToneMapOperator.d.ts +54 -0
- package/src/shade/renderer/deferred/tonemap/ToneMapOperator.d.ts.map +1 -0
- package/src/shade/renderer/deferred/tonemap/ToneMapOperator.js +63 -0
- package/src/shade/renderer/deferred/tonemap/ToneMapper.d.ts +106 -0
- package/src/shade/renderer/deferred/tonemap/ToneMapper.d.ts.map +1 -0
- package/src/shade/renderer/deferred/tonemap/ToneMapper.js +474 -0
- package/src/shade/renderer/deferred/tonemap/TonemapAces2Tables.d.ts +5 -13
- package/src/shade/renderer/deferred/tonemap/TonemapAces2Tables.d.ts.map +1 -1
- package/src/shade/renderer/deferred/tonemap/TonemapAces2Tables.js +5 -44
- package/src/shade/renderer/deferred/tonemap/TonemapDisplaySettings.d.ts +7 -25
- package/src/shade/renderer/deferred/tonemap/TonemapDisplaySettings.d.ts.map +1 -1
- package/src/shade/renderer/deferred/tonemap/TonemapDisplaySettings.js +11 -51
- package/src/shade/renderer/deferred/tonemap/m3_white_balance_rec709.d.ts +20 -0
- package/src/shade/renderer/deferred/tonemap/m3_white_balance_rec709.d.ts.map +1 -0
- package/src/shade/renderer/deferred/tonemap/m3_white_balance_rec709.js +103 -0
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_HDR_lut.d.ts +9 -0
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_HDR_lut.d.ts.map +1 -0
- package/src/shade/renderer/deferred/tonemap/{shader_tonemap_HDR_none.js → shader_tonemap_HDR_lut.js} +22 -18
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_LDR_lut.d.ts +9 -0
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_LDR_lut.d.ts.map +1 -0
- package/src/shade/renderer/deferred/tonemap/{shader_tonemap_LDR_aces.js → shader_tonemap_LDR_lut.js} +24 -24
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_lut_bake.d.ts +23 -0
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_lut_bake.d.ts.map +1 -0
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_lut_bake.js +92 -0
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_lut_sdr_from_hdr.d.ts +15 -0
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_lut_sdr_from_hdr.d.ts.map +1 -0
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_lut_sdr_from_hdr.js +73 -0
- package/src/shade/renderer/fluid/GPUFluidCascade.d.ts +3 -3
- package/src/shade/renderer/fluid/GPUFluidCascadeStack.d.ts +1 -1
- package/src/shade/renderer/fluid/forcing/GPUFluidForcing.d.ts +6 -6
- package/src/shade/renderer/fluid/pressure/GPUFluidPressureSolve.d.ts +1 -1
- package/src/shade/renderer/fluid/query/GPUFluidQueries.d.ts +1 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.d.ts +1 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/PROBE_CLIPMAP.d.ts +3 -15
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/PROBE_CLIPMAP.d.ts.map +1 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/PROBE_CLIPMAP.js +4 -17
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap.d.ts +3 -3
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap.d.ts.map +1 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap.js +15 -13
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_accumulate.d.ts +7 -3
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_accumulate.d.ts.map +1 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_accumulate.js +28 -17
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_project.d.ts +18 -18
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_project.d.ts.map +1 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_project.js +44 -48
- package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
- package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
- package/src/shade/renderer/shader/chunk/color/conversion/chunk_CRM_REC709_AP1.d.ts +22 -0
- package/src/shade/renderer/shader/chunk/color/conversion/chunk_CRM_REC709_AP1.d.ts.map +1 -0
- package/src/shade/renderer/shader/chunk/color/conversion/chunk_CRM_REC709_AP1.js +62 -0
- package/src/shade/renderer/shader/chunk/color/grade/chunk_color_grade.d.ts +13 -0
- package/src/shade/renderer/shader/chunk/color/grade/chunk_color_grade.d.ts.map +1 -0
- package/src/shade/renderer/shader/chunk/color/grade/chunk_color_grade.js +140 -0
- package/src/shade/renderer/shader/chunk/color/oklab/chunk_oklab_compute_max_saturation.d.ts +11 -0
- package/src/shade/renderer/shader/chunk/color/oklab/chunk_oklab_compute_max_saturation.d.ts.map +1 -0
- package/src/shade/renderer/shader/chunk/color/oklab/chunk_oklab_compute_max_saturation.js +65 -0
- package/src/shade/renderer/shader/chunk/color/tonemap/chunk_bt2390_eetf.d.ts +14 -0
- package/src/shade/renderer/shader/chunk/color/tonemap/chunk_bt2390_eetf.d.ts.map +1 -0
- package/src/shade/renderer/shader/chunk/color/tonemap/chunk_bt2390_eetf.js +38 -0
- package/src/shade/renderer/shader/chunk/color/tonemap/chunk_tonemap_lut.d.ts +16 -0
- package/src/shade/renderer/shader/chunk/color/tonemap/chunk_tonemap_lut.d.ts.map +1 -0
- package/src/shade/renderer/shader/chunk/color/tonemap/chunk_tonemap_lut.js +41 -0
- package/src/shade/renderer/shader/chunk/filter/chunk_multiscale_mean.d.ts +8 -0
- package/src/shade/renderer/shader/chunk/filter/chunk_multiscale_mean.d.ts.map +1 -1
- package/src/shade/renderer/shader/chunk/filter/chunk_multiscale_mean.js +21 -3
- package/src/shade/renderer/water/query/GPUWaterQueries.d.ts +1 -1
- package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +8 -8
- package/src/shade/wgsl/emulator/WGSLLanguageCore.d.ts.map +1 -1
- package/src/shade/wgsl/emulator/WGSLLanguageCore.js +2 -0
- package/src/shade/renderer/ShadeToneMappingMode.d.ts +0 -15
- package/src/shade/renderer/ShadeToneMappingMode.d.ts.map +0 -1
- package/src/shade/renderer/ShadeToneMappingMode.js +0 -41
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_HDR.d.ts +0 -9
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_HDR.d.ts.map +0 -1
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_HDR.js +0 -79
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_HDR_aces.d.ts +0 -17
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_HDR_aces.d.ts.map +0 -1
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_HDR_aces.js +0 -101
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_HDR_none.d.ts +0 -9
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_HDR_none.d.ts.map +0 -1
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_HDR_pbr_neutral.d.ts +0 -10
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_HDR_pbr_neutral.d.ts.map +0 -1
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_HDR_pbr_neutral.js +0 -80
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_LDR.d.ts +0 -9
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_LDR.d.ts.map +0 -1
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_LDR.js +0 -80
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_LDR_aces.d.ts +0 -10
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_LDR_aces.d.ts.map +0 -1
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_LDR_none.d.ts +0 -9
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_LDR_none.d.ts.map +0 -1
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_LDR_none.js +0 -71
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_LDR_pbr_neutral.d.ts +0 -10
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_LDR_pbr_neutral.d.ts.map +0 -1
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_LDR_pbr_neutral.js +0 -76
- package/src/shade/renderer/shader/chunk/color/tonemap/chunk_tonemap_hdr_bt2390.d.ts +0 -3
- package/src/shade/renderer/shader/chunk/color/tonemap/chunk_tonemap_hdr_bt2390.d.ts.map +0 -1
- 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 {
|
|
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 {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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
|
|
19
|
+
* chassis-local mount point. Every step, the controller:
|
|
16
20
|
*
|
|
17
|
-
* 1. casts
|
|
18
|
-
* compression;
|
|
19
|
-
* 2. applies
|
|
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
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
*
|
|
47
|
-
*
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
151
|
-
|
|
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
|
-
*
|
|
155
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
221
|
-
|
|
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
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
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
|
|
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
|
-
|
|
245
|
-
let
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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
|
-
|
|
298
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
710
|
+
const fdotn = fwx * nx + fwy * ny + fwz * nz;
|
|
326
711
|
fwx -= fdotn * nx; fwy -= fdotn * ny; fwz -= fdotn * nz;
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
368
|
-
|
|
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
|
-
|
|
372
|
-
|
|
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
|
}
|