@perplexdotgg/bounce 1.0.0

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 (140) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -0
  3. package/package.json +32 -0
  4. package/src/builders/ConvexHullBuilder.ts +437 -0
  5. package/src/builders/ConvexHullBuilder2d.ts +344 -0
  6. package/src/builders/ConvexHullBuilder3d.ts +1689 -0
  7. package/src/builders/HeightMapBuilder.ts +414 -0
  8. package/src/builders/TriangleMeshBuilder.ts +92 -0
  9. package/src/collision/CastShapesModule.ts +184 -0
  10. package/src/collision/CollideShapesModule.ts +152 -0
  11. package/src/collision/HeightMapCaster.ts +38 -0
  12. package/src/collision/HeightMapCollider.ts +33 -0
  13. package/src/collision/TriangleCaster.ts +249 -0
  14. package/src/collision/TriangleCollider.ts +308 -0
  15. package/src/collision/TriangleCollider2.ts +379 -0
  16. package/src/collision/activeEdge.ts +146 -0
  17. package/src/collision/cast/cast.ts +139 -0
  18. package/src/collision/cast/castCompoundVsCompound.ts +59 -0
  19. package/src/collision/cast/castCompoundVsConvex.ts +116 -0
  20. package/src/collision/cast/castConvexVsCompound.ts +123 -0
  21. package/src/collision/cast/castConvexVsConvex.ts +213 -0
  22. package/src/collision/cast/castConvexVsHeightMap.ts +73 -0
  23. package/src/collision/cast/castConvexVsTriangleMesh.ts +56 -0
  24. package/src/collision/cast/castRayVsCompound.ts +44 -0
  25. package/src/collision/cast/castRayVsConvex.ts +45 -0
  26. package/src/collision/cast/castRayVsHeightMap.ts +58 -0
  27. package/src/collision/cast/castRayVsTriangleMesh.ts +58 -0
  28. package/src/collision/closestPoints/closestPoints.ts +23 -0
  29. package/src/collision/closestPoints/computeBarycentricCoordinates2d.ts +32 -0
  30. package/src/collision/closestPoints/computeBarycentricCoordinates3d.ts +81 -0
  31. package/src/collision/closestPoints/computeClosestPointOnLine.ts +30 -0
  32. package/src/collision/closestPoints/computeClosestPointOnTetrahedron.ts +96 -0
  33. package/src/collision/closestPoints/computeClosestPointOnTriangle.ts +195 -0
  34. package/src/collision/closestPoints/isOriginOutsideOfPlane.ts +25 -0
  35. package/src/collision/closestPoints/isOriginOutsideOfTrianglePlanes.ts +72 -0
  36. package/src/collision/collide/collide.ts +146 -0
  37. package/src/collision/collide/collideCompoundVsCompound.ts +60 -0
  38. package/src/collision/collide/collideCompoundVsConvex.ts +59 -0
  39. package/src/collision/collide/collideCompoundVsHeightMap.ts +73 -0
  40. package/src/collision/collide/collideCompoundVsTriangleMesh.ts +56 -0
  41. package/src/collision/collide/collideConvexVsCompound.ts +57 -0
  42. package/src/collision/collide/collideConvexVsConvex.ts +225 -0
  43. package/src/collision/collide/collideConvexVsConvexImp.ts +236 -0
  44. package/src/collision/collide/collideConvexVsHeightMap.ts +53 -0
  45. package/src/collision/collide/collideConvexVsTriangleMesh.ts +58 -0
  46. package/src/collision/collide/collideHeightMapVsCompound.ts +69 -0
  47. package/src/collision/collide/collideHeightMapVsConvex.ts +53 -0
  48. package/src/collision/collide/collideSphereVsSphere.ts +81 -0
  49. package/src/collision/collide/collideTriangleMeshVsCompound.ts +58 -0
  50. package/src/collision/collide/collideTriangleMeshVsConvex.ts +58 -0
  51. package/src/collision/epa/EpaConvexHullBuilder.ts +397 -0
  52. package/src/collision/epa/StaticArray.ts +154 -0
  53. package/src/collision/epa/TriangleFactory.ts +32 -0
  54. package/src/collision/epa/arrays.ts +99 -0
  55. package/src/collision/epa/binaryHeap.ts +82 -0
  56. package/src/collision/epa/structs.ts +227 -0
  57. package/src/collision/gjk/GjkModule.ts +864 -0
  58. package/src/collision/gjk/PenetrationDepthModule.ts +493 -0
  59. package/src/collision/gjk/SupportPoints.ts +50 -0
  60. package/src/collision/imp/MinkowskiDifference.ts +36 -0
  61. package/src/collision/imp/computeExploredDistanceLowerUpperBound.ts +40 -0
  62. package/src/collision/imp/finalizeImpResult.ts +69 -0
  63. package/src/collision/imp/findContactImp.ts +196 -0
  64. package/src/collision/imp/imp.ts +28 -0
  65. package/src/collision/imp/incrementalMinimumDistanceExploreDirection.ts +207 -0
  66. package/src/collision/mpr/findPortal.ts +152 -0
  67. package/src/collision/mpr/mpr.ts +29 -0
  68. package/src/collision/mpr/updatePortal.ts +52 -0
  69. package/src/constraints/BaseConstraint.ts +50 -0
  70. package/src/constraints/ConstraintOptions.ts +22 -0
  71. package/src/constraints/ConstraintSolver.ts +119 -0
  72. package/src/constraints/DistanceConstraint.ts +229 -0
  73. package/src/constraints/FixedConstraint.ts +203 -0
  74. package/src/constraints/HingeConstraint.ts +460 -0
  75. package/src/constraints/PointConstraint.ts +108 -0
  76. package/src/constraints/components/AngleComponent.ts +226 -0
  77. package/src/constraints/components/AxisComponent.ts +263 -0
  78. package/src/constraints/components/HingeComponent.ts +215 -0
  79. package/src/constraints/components/Motor.ts +36 -0
  80. package/src/constraints/components/PointConstraintComponent.ts +179 -0
  81. package/src/constraints/components/RotationEulerComponent.ts +139 -0
  82. package/src/constraints/components/Spring.ts +30 -0
  83. package/src/constraints/components/SpringComponent.ts +71 -0
  84. package/src/constraints/types.ts +6 -0
  85. package/src/helpers.ts +147 -0
  86. package/src/index.ts +50 -0
  87. package/src/math/BasicTransform.ts +19 -0
  88. package/src/math/NumberValue.ts +13 -0
  89. package/src/math/isometry.ts +64 -0
  90. package/src/math/mat3.ts +529 -0
  91. package/src/math/mat4.ts +588 -0
  92. package/src/math/quat.ts +193 -0
  93. package/src/math/scalar.ts +81 -0
  94. package/src/math/tensor.ts +17 -0
  95. package/src/math/vec3.ts +589 -0
  96. package/src/math/vec4.ts +10 -0
  97. package/src/physics/Body.ts +581 -0
  98. package/src/physics/CollisionFilter.ts +52 -0
  99. package/src/physics/SleepModule.ts +163 -0
  100. package/src/physics/broadphase/BodyPairsModule.ts +363 -0
  101. package/src/physics/broadphase/BvhModule.ts +237 -0
  102. package/src/physics/broadphase/BvhTree.ts +803 -0
  103. package/src/physics/broadphase/ConstraintPairsModule.ts +385 -0
  104. package/src/physics/broadphase/TriangleMeshBvhTree.ts +379 -0
  105. package/src/physics/manifold/ContactManifold.ts +227 -0
  106. package/src/physics/manifold/ContactManifoldModule.ts +623 -0
  107. package/src/physics/manifold/Face.ts +119 -0
  108. package/src/physics/manifold/ManifoldCache.ts +116 -0
  109. package/src/physics/manifold/clipping/clipPolyVsEdge.ts +131 -0
  110. package/src/physics/manifold/clipping/clipPolyVsPlane.ts +73 -0
  111. package/src/physics/manifold/clipping/clipPolyVsPoly.ts +72 -0
  112. package/src/physics/narrowphase/CollideBodiesModule.ts +755 -0
  113. package/src/physics/solver/ContactConstraintModule.ts +659 -0
  114. package/src/physics/solver/ManifoldConstraint.ts +420 -0
  115. package/src/physics/solver/estimateCollisionResponse.ts +146 -0
  116. package/src/shape/Aabb.ts +400 -0
  117. package/src/shape/Box.ts +231 -0
  118. package/src/shape/Capsule.ts +332 -0
  119. package/src/shape/CompoundShape.ts +288 -0
  120. package/src/shape/Convex.ts +130 -0
  121. package/src/shape/ConvexHull.ts +423 -0
  122. package/src/shape/Cylinder.ts +313 -0
  123. package/src/shape/HeightMap.ts +511 -0
  124. package/src/shape/Line.ts +14 -0
  125. package/src/shape/Plane.ts +116 -0
  126. package/src/shape/Ray.ts +81 -0
  127. package/src/shape/Segment.ts +25 -0
  128. package/src/shape/Shape.ts +77 -0
  129. package/src/shape/Sphere.ts +181 -0
  130. package/src/shape/TransformedShape.ts +51 -0
  131. package/src/shape/Triangle.ts +122 -0
  132. package/src/shape/TriangleMesh.ts +186 -0
  133. package/src/types.ts +1 -0
  134. package/src/world.ts +1335 -0
  135. package/tests/BodyPairsModule.test.ts +71 -0
  136. package/tests/BvhTree.test.ts +406 -0
  137. package/tests/test.md +642 -0
  138. package/tests/vec3.test.ts +12 -0
  139. package/tsconfig.json +20 -0
  140. package/vite.config.js +40 -0
@@ -0,0 +1,226 @@
1
+ import {
2
+ createClass,
3
+ LazyReferenceType,
4
+ MonomorphType,
5
+ NumberType,
6
+ PropertyDefinitionMap,
7
+ PropertyDefinitionReference,
8
+ } from "monomorph";
9
+ import { Body, BodyType } from "../../physics/Body";
10
+ import { ConstraintOptions } from "../ConstraintOptions";
11
+ import { SpringComponent } from "./SpringComponent";
12
+ import { Vec3 } from "../../math/vec3";
13
+ import type { World } from "../../world";
14
+ import { clamp } from "../../math/scalar";
15
+ import { Spring, SpringMode } from "./Spring";
16
+
17
+ const spinBA = /*@__PURE__*/ Vec3.create();
18
+ const deltaSpin = /*@__PURE__*/ Vec3.create();
19
+ const inverseEffectiveMass = /*@__PURE__*/ Vec3.create();
20
+
21
+ export const angleComponentProps = {
22
+ bodyA: LazyReferenceType((() => Body) as () => never) as PropertyDefinitionReference<Body | null, true>,
23
+ bodyB: LazyReferenceType((() => Body) as () => never) as PropertyDefinitionReference<Body | null, true>,
24
+ options: MonomorphType(ConstraintOptions, {
25
+ positionBaumgarte: 0.8,
26
+ velocityBaumgarte: 1.0,
27
+ strength: 1.0,
28
+ }),
29
+ springConstraintPart: MonomorphType(SpringComponent, undefined, true),
30
+ effectiveMass: NumberType(0, true),
31
+ effectiveInverseInertiaA: MonomorphType(Vec3, undefined, true),
32
+ effectiveInverseInertiaB: MonomorphType(Vec3, undefined, true),
33
+ totalLambda: NumberType(0, true),
34
+ } as const satisfies PropertyDefinitionMap;
35
+
36
+ export class AngleComponent extends createClass<AngleComponent, typeof angleComponentProps>(angleComponentProps) {
37
+ declare world: World;
38
+
39
+ deactivate(): void {
40
+ this.effectiveMass = 0;
41
+ this.totalLambda = 0;
42
+ }
43
+
44
+ isActive(): boolean {
45
+ return this.effectiveMass !== 0;
46
+ }
47
+
48
+ applyDirectImpulse(bodyA: Body, bodyB: Body, impulse: number): void {
49
+ if (impulse === 0) {
50
+ return;
51
+ }
52
+
53
+ if (bodyA.type === BodyType.dynamic) {
54
+ bodyA.angularVelocity.subScaledVector(this.effectiveInverseInertiaA, impulse);
55
+ }
56
+
57
+ if (bodyB.type === BodyType.dynamic) {
58
+ bodyB.angularVelocity.addScaledVector(this.effectiveInverseInertiaB, impulse);
59
+ }
60
+ }
61
+
62
+ warmStart(bodyA: Body, bodyB: Body, warmStartImpulseRatio: number): void {
63
+ this.totalLambda *= warmStartImpulseRatio;
64
+ this.applyDirectImpulse(bodyA, bodyB, this.totalLambda);
65
+ }
66
+
67
+ solveVelocity(bodyA: Body, bodyB: Body, worldSpaceAxis: Vec3, minLambda: number, maxLambda: number): void {
68
+ spinBA.subtractVectors(bodyA.angularVelocity, bodyB.angularVelocity);
69
+
70
+ let lambda =
71
+ this.effectiveMass * (worldSpaceAxis.dot(spinBA) - this.springConstraintPart.getBias(this.totalLambda));
72
+ lambda *= this.options.velocityBaumgarte;
73
+ const newLambda = clamp(this.totalLambda + lambda, minLambda, maxLambda);
74
+ lambda = newLambda - this.totalLambda;
75
+ this.totalLambda = newLambda;
76
+ this.applyDirectImpulse(bodyA, bodyB, lambda);
77
+ }
78
+
79
+ solvePosition(bodyA: Body, bodyB: Body, constraint: number): void {
80
+ if (constraint === 0 || this.springConstraintPart.isActive()) {
81
+ return;
82
+ }
83
+
84
+ const lambda = -this.effectiveMass * constraint * this.options.positionBaumgarte;
85
+
86
+ if (bodyA.type === BodyType.dynamic) {
87
+ deltaSpin.copy(this.effectiveInverseInertiaA).scale(lambda);
88
+ bodyA.subRotationDelta(deltaSpin);
89
+ }
90
+
91
+ if (bodyB.type === BodyType.dynamic) {
92
+ deltaSpin.copy(this.effectiveInverseInertiaB).scale(lambda);
93
+ bodyB.addRotationDelta(deltaSpin);
94
+ }
95
+ }
96
+
97
+ setupInverseEffectiveMass(bodyA: Body, bodyB: Body, axis: Vec3): number {
98
+ // TODO: assert axis is normalized?
99
+
100
+ if (bodyA.type === BodyType.dynamic) {
101
+ bodyA.computeEffectiveInverseInertiaVector(this.effectiveInverseInertiaA, bodyA.orientation, axis);
102
+ } else {
103
+ this.effectiveInverseInertiaA.zero();
104
+ }
105
+
106
+ if (bodyB.type === BodyType.dynamic) {
107
+ bodyB.computeEffectiveInverseInertiaVector(this.effectiveInverseInertiaB, bodyB.orientation, axis);
108
+ } else {
109
+ this.effectiveInverseInertiaB.zero();
110
+ }
111
+
112
+ inverseEffectiveMass.addVectors(this.effectiveInverseInertiaA, this.effectiveInverseInertiaB);
113
+ return axis.dot(inverseEffectiveMass);
114
+ }
115
+
116
+ setup(bodyA: Body, bodyB: Body, worldSpaceAxis: Vec3, bias: number = 0.0): void {
117
+ const inverseEffectiveMass = this.setupInverseEffectiveMass(bodyA, bodyB, worldSpaceAxis);
118
+
119
+ if (inverseEffectiveMass === 0) {
120
+ this.deactivate();
121
+ } else {
122
+ this.effectiveMass = 1 / inverseEffectiveMass;
123
+ this.springConstraintPart.setupSpringPropertiesWithBias(bias);
124
+ }
125
+ this.effectiveMass *= this.options.strength;
126
+ }
127
+
128
+ setupWithFrequency(
129
+ bodyA: Body,
130
+ bodyB: Body,
131
+ worldSpaceAxis: Vec3,
132
+ bias: number,
133
+ constraint: number,
134
+ frequency: number,
135
+ damping: number,
136
+ deltaTime: number
137
+ ): void {
138
+ const inverseEffectiveMass = this.setupInverseEffectiveMass(bodyA, bodyB, worldSpaceAxis);
139
+
140
+ if (inverseEffectiveMass === 0) {
141
+ this.deactivate();
142
+ } else {
143
+ this.effectiveMass = this.springConstraintPart.setupWithFrequency(
144
+ deltaTime,
145
+ inverseEffectiveMass,
146
+ bias,
147
+ constraint,
148
+ frequency,
149
+ damping
150
+ );
151
+ }
152
+ }
153
+
154
+ setupWithStiffness(
155
+ bodyA: Body,
156
+ bodyB: Body,
157
+ worldSpaceAxis: Vec3,
158
+ bias: number,
159
+ constraint: number,
160
+ stiffness: number,
161
+ damping: number,
162
+ deltaTime: number
163
+ ): void {
164
+ const inverseEffectiveMass = this.setupInverseEffectiveMass(bodyA, bodyB, worldSpaceAxis);
165
+
166
+ if (inverseEffectiveMass === 0) {
167
+ this.deactivate();
168
+ } else {
169
+ this.effectiveMass = this.springConstraintPart.setupWithStiffness(
170
+ deltaTime,
171
+ inverseEffectiveMass,
172
+ bias,
173
+ constraint,
174
+ stiffness,
175
+ damping
176
+ );
177
+ }
178
+ }
179
+
180
+ setupWithSpring(
181
+ bodyA: Body,
182
+ bodyB: Body,
183
+ worldSpaceAxis: Vec3,
184
+ bias: number,
185
+ constraint: number,
186
+ spring: Spring,
187
+ deltaTime: number
188
+ ): void {
189
+ const inverseEffectiveMass = this.setupInverseEffectiveMass(bodyA, bodyB, worldSpaceAxis);
190
+
191
+ if (inverseEffectiveMass === 0) {
192
+ this.deactivate();
193
+ return;
194
+ }
195
+
196
+ switch (spring.mode) {
197
+ case SpringMode.UseFrequency: {
198
+ this.effectiveMass = this.springConstraintPart.setupWithFrequency(
199
+ deltaTime,
200
+ inverseEffectiveMass,
201
+ bias,
202
+ constraint,
203
+ spring.frequency,
204
+ spring.damping
205
+ );
206
+ break;
207
+ }
208
+
209
+ case SpringMode.UseStiffness: {
210
+ this.effectiveMass = this.springConstraintPart.setupWithStiffness(
211
+ deltaTime,
212
+ inverseEffectiveMass,
213
+ bias,
214
+ constraint,
215
+ spring.stiffness,
216
+ spring.damping
217
+ );
218
+ break;
219
+ }
220
+
221
+ default: {
222
+ throw new Error("Invalid spring mode");
223
+ }
224
+ }
225
+ }
226
+ }
@@ -0,0 +1,263 @@
1
+ import {
2
+ createClass,
3
+ LazyReferenceType,
4
+ MonomorphType,
5
+ NumberType,
6
+ PropertyDefinitionMap,
7
+ PropertyDefinitionReference,
8
+ } from "monomorph";
9
+ import { ConstraintOptions } from "../ConstraintOptions";
10
+ import { Body, BodyType } from "../../physics/Body";
11
+ import { Vec3 } from "../../math/vec3";
12
+ import { SpringComponent } from "./SpringComponent";
13
+ import type { World } from "../../world";
14
+ import { Mat3 } from "../../math/mat3";
15
+ import { Spring, SpringMode } from "./Spring";
16
+ import { clamp } from "../../math/scalar";
17
+
18
+ const linearVelocityBA = /*@__PURE__*/ Vec3.create();
19
+ const inverseInertiaA = /*@__PURE__*/ Mat3.create();
20
+ const inverseInertiaB = /*@__PURE__*/ Mat3.create();
21
+ const linearImpulse = /*@__PURE__*/ Vec3.create();
22
+ const angularImpulse = /*@__PURE__*/ Vec3.create();
23
+
24
+ export const axisComponentProps = {
25
+ bodyA: LazyReferenceType((() => Body) as () => never) as PropertyDefinitionReference<Body | null, true>,
26
+ bodyB: LazyReferenceType((() => Body) as () => never) as PropertyDefinitionReference<Body | null, true>,
27
+ options: MonomorphType(ConstraintOptions, {
28
+ positionBaumgarte: 0.8,
29
+ velocityBaumgarte: 1.0,
30
+ strength: 1.0,
31
+ }),
32
+ mR1PlusUxAxis: MonomorphType(Vec3, { x: 0, y: 0, z: 0 }),
33
+ mR2xAxis: MonomorphType(Vec3, { x: 0, y: 0, z: 0 }),
34
+ mInvI1_R1PlusUxAxis: MonomorphType(Vec3, { x: 0, y: 0, z: 0 }),
35
+ mInvI2_R2xAxis: MonomorphType(Vec3, { x: 0, y: 0, z: 0 }),
36
+ effectiveMass: NumberType(0),
37
+ totalLambda: NumberType(0),
38
+ springComponent: MonomorphType(SpringComponent),
39
+ } as const satisfies PropertyDefinitionMap;
40
+
41
+ export class AxisComponent extends createClass<AxisComponent, typeof axisComponentProps>(axisComponentProps) {
42
+ declare world: World;
43
+
44
+ deactivate(): void {
45
+ this.effectiveMass = 0;
46
+ this.totalLambda = 0;
47
+ }
48
+
49
+ isActive(): boolean {
50
+ return this.effectiveMass > 0;
51
+ }
52
+
53
+ computeInverseEffectiveMass(
54
+ inverseMassA: number,
55
+ inverseMassB: number,
56
+ inverseInertiaA: Mat3,
57
+ inverseInertiaB: Mat3,
58
+ momentArmA: Vec3,
59
+ momentArmB: Vec3,
60
+ direction: Vec3
61
+ ): number {
62
+ const bodyA = this.bodyA!;
63
+ const bodyB = this.bodyB!;
64
+
65
+ // #v-ifdef DEV
66
+ // assert that the direction is normalized within some tolerance
67
+ if (direction.isNormalized(1e-5) === false) {
68
+ throw new Error("direction must be normalized");
69
+ }
70
+ // #v-endif
71
+
72
+ // zero out the linear jacobians (to handle static bodies)
73
+ this.mR1PlusUxAxis.zero();
74
+ this.mR2xAxis.zero();
75
+
76
+ // if not a static body, calculate the linear jacobians
77
+ if (bodyA.type !== BodyType.static) {
78
+ this.mR1PlusUxAxis.crossVectors(momentArmA, direction);
79
+ }
80
+
81
+ if (bodyB.type !== BodyType.static) {
82
+ this.mR2xAxis.crossVectors(momentArmB, direction);
83
+ }
84
+
85
+ // the inverse effective mass is K = J * M^-1 * J_T
86
+ let inverseEffectiveMass = 0;
87
+
88
+ if (bodyA.type === BodyType.dynamic) {
89
+ this.mInvI1_R1PlusUxAxis.transformVectorFromMat3(this.mR1PlusUxAxis, inverseInertiaA);
90
+ inverseEffectiveMass += inverseMassA + this.mInvI1_R1PlusUxAxis.dot(this.mR1PlusUxAxis);
91
+ }
92
+
93
+ if (bodyB.type === BodyType.dynamic) {
94
+ this.mInvI2_R2xAxis.transformVectorFromMat3(this.mR2xAxis, inverseInertiaB);
95
+ inverseEffectiveMass += inverseMassB + this.mInvI2_R2xAxis.dot(this.mR2xAxis);
96
+ }
97
+
98
+ return inverseEffectiveMass;
99
+ }
100
+
101
+ // TODO: this is copied from DirectionalConstraint, should be refactored
102
+ getTotalLambda(direction: Vec3): number {
103
+ const bodyA = this.bodyA!;
104
+ const bodyB = this.bodyB!;
105
+
106
+ // #v-ifdef DEV
107
+ // assert at least one body is dynamic
108
+ if (bodyA.type !== BodyType.dynamic && bodyB.type !== BodyType.dynamic) {
109
+ throw new Error("at least one body must be dynamic");
110
+ }
111
+ // #v-endif
112
+ // calculate the jacobian multiplied by the velocity
113
+ let jv = 0;
114
+
115
+ // jacobian multiplied by the linear relative velocity
116
+
117
+ linearVelocityBA.subtractVectors(bodyA.linearVelocity, bodyB.linearVelocity);
118
+
119
+ jv += direction.dot(linearVelocityBA);
120
+
121
+ // jacobian multiplied by the angular relative velocity
122
+
123
+ if (bodyA.type !== BodyType.static) {
124
+ jv += this.mR1PlusUxAxis.dot(bodyA.angularVelocity);
125
+ }
126
+
127
+ if (bodyB.type !== BodyType.static) {
128
+ jv -= this.mR2xAxis.dot(bodyB.angularVelocity);
129
+ }
130
+
131
+ const springBias = this.springComponent.getBias(this.totalLambda);
132
+ const lambda = this.effectiveMass * (jv - springBias);
133
+
134
+ const accumulatedLambda = this.totalLambda + lambda;
135
+
136
+ return accumulatedLambda;
137
+ }
138
+
139
+ applyDirectImpulse(direction: Vec3, lambda: number): void {
140
+ if (lambda === 0) {
141
+ return;
142
+ }
143
+
144
+ const bodyA = this.bodyA!;
145
+ const bodyB = this.bodyB!;
146
+
147
+ if (bodyA.type === BodyType.dynamic) {
148
+ linearImpulse.scaleVector(direction, lambda * bodyA.inverseMass);
149
+ angularImpulse.scaleVector(this.mInvI1_R1PlusUxAxis, lambda);
150
+ bodyA.linearVelocity.subtractVectors(bodyA.linearVelocity, linearImpulse);
151
+ bodyA.angularVelocity.subtractVectors(bodyA.angularVelocity, angularImpulse);
152
+ }
153
+
154
+ if (bodyB.type === BodyType.dynamic) {
155
+ linearImpulse.scaleVector(direction, lambda * bodyB.inverseMass);
156
+ angularImpulse.scaleVector(this.mInvI2_R2xAxis, lambda);
157
+ bodyB.linearVelocity.addVectors(bodyB.linearVelocity, linearImpulse);
158
+ bodyB.angularVelocity.addVectors(bodyB.angularVelocity, angularImpulse);
159
+ }
160
+ }
161
+
162
+ setup(deltaTime: number, armA: Vec3, armB: Vec3, direction: Vec3, bias: number, error: number, spring: Spring): void {
163
+ // calculate effective mass
164
+ const inverseMassA = this.bodyA!.inverseMass;
165
+ const inverseMassB = this.bodyB!.inverseMass;
166
+
167
+ this.bodyA!.computeInverseInertiaTensor(inverseInertiaA);
168
+ this.bodyB!.computeInverseInertiaTensor(inverseInertiaB);
169
+
170
+ const inverseEffectiveMass = this.computeInverseEffectiveMass(
171
+ inverseMassA,
172
+ inverseMassB,
173
+ inverseInertiaA,
174
+ inverseInertiaB,
175
+ armA,
176
+ armB,
177
+ direction
178
+ );
179
+
180
+ switch (true) {
181
+ case inverseEffectiveMass === 0: {
182
+ this.deactivate();
183
+ break;
184
+ }
185
+
186
+ case spring.mode === SpringMode.UseFrequency: {
187
+ this.effectiveMass = this.springComponent.setupWithFrequency(
188
+ deltaTime,
189
+ inverseEffectiveMass,
190
+ bias,
191
+ error,
192
+ spring.frequency,
193
+ spring.damping
194
+ );
195
+ break;
196
+ }
197
+
198
+ case spring.mode === SpringMode.UseStiffness: {
199
+ this.effectiveMass = this.springComponent.setupWithStiffness(
200
+ deltaTime,
201
+ inverseEffectiveMass,
202
+ bias,
203
+ error,
204
+ spring.stiffness,
205
+ spring.damping
206
+ );
207
+ break;
208
+ }
209
+
210
+ default: {
211
+ throw new Error("Invalid settings");
212
+ }
213
+ }
214
+
215
+ this.effectiveMass *= this.options.strength;
216
+ }
217
+
218
+ warmStart(direction: Vec3, warmStartImpulseRatio: number): void {
219
+ this.totalLambda *= warmStartImpulseRatio;
220
+ this.applyDirectImpulse(direction, this.totalLambda);
221
+ }
222
+
223
+ solveVelocity(direction: Vec3, minLambda: number, maxLambda: number): void {
224
+ let totalLambda = this.getTotalLambda(direction);
225
+ totalLambda *= this.options.velocityBaumgarte;
226
+ totalLambda = clamp(totalLambda, minLambda, maxLambda);
227
+ const deltaLambda = totalLambda - this.totalLambda;
228
+ this.totalLambda = totalLambda;
229
+ this.applyDirectImpulse(direction, deltaLambda);
230
+ }
231
+
232
+ solvePosition(direction: Vec3, error: number): void {
233
+ const bodyA = this.bodyA!;
234
+ const bodyB = this.bodyB!;
235
+
236
+ // // TODO: do we need the check of active?
237
+ // if (error === 0 || this.isActive()) {
238
+ // return;
239
+ // }
240
+
241
+ if (error === 0) {
242
+ return;
243
+ }
244
+
245
+ const lambda = this.effectiveMass * error * -this.options.positionBaumgarte;
246
+
247
+ if (bodyA.type === BodyType.dynamic) {
248
+ linearImpulse.scaleVector(direction, lambda * bodyA.inverseMass);
249
+ bodyA.computedCenterOfMassPosition.subtractVectors(bodyA.computedCenterOfMassPosition, linearImpulse);
250
+
251
+ angularImpulse.scaleVector(this.mInvI1_R1PlusUxAxis, lambda);
252
+ bodyA.subRotationDelta(angularImpulse);
253
+ }
254
+
255
+ if (bodyB.type === BodyType.dynamic) {
256
+ linearImpulse.scaleVector(direction, lambda * bodyB.inverseMass);
257
+ bodyB.computedCenterOfMassPosition.addVectors(bodyB.computedCenterOfMassPosition, linearImpulse);
258
+
259
+ angularImpulse.scaleVector(this.mInvI2_R2xAxis, lambda);
260
+ bodyB.addRotationDelta(angularImpulse);
261
+ }
262
+ }
263
+ }
@@ -0,0 +1,215 @@
1
+ import {
2
+ createClass,
3
+ LazyReferenceType,
4
+ MonomorphType,
5
+ NumberType,
6
+ PropertyDefinitionMap,
7
+ PropertyDefinitionReference,
8
+ } from "monomorph";
9
+ import { Body, BodyType } from "../../physics/Body";
10
+ import { ConstraintOptions } from "../ConstraintOptions";
11
+ import { Vec3 } from "../../math/vec3";
12
+ import { Mat3 } from "../../math/mat3";
13
+ import type { World } from "../../world";
14
+
15
+ const spinBA = /*@__PURE__*/ Vec3.create();
16
+ const impulse = /*@__PURE__*/ Vec3.create();
17
+ const axisA2 = /*@__PURE__*/ Vec3.create();
18
+ const perpendicular = /*@__PURE__*/ Vec3.create();
19
+ const summedInverseInertia = /*@__PURE__*/ Mat3.create();
20
+ const massBA = /*@__PURE__*/ Vec3.create();
21
+ const massCA = /*@__PURE__*/ Vec3.create();
22
+
23
+ const hingeComponentProps = {
24
+ bodyA: LazyReferenceType((() => Body) as () => never) as PropertyDefinitionReference<Body | null, true>,
25
+ bodyB: LazyReferenceType((() => Body) as () => never) as PropertyDefinitionReference<Body | null, true>,
26
+ options: MonomorphType(ConstraintOptions, {
27
+ positionBaumgarte: 0.8,
28
+ velocityBaumgarte: 1.0,
29
+ strength: 1.0,
30
+ }),
31
+ axisA: MonomorphType(Vec3, undefined, true),
32
+ axisB: MonomorphType(Vec3, undefined, true),
33
+ axisC: MonomorphType(Vec3, undefined, true),
34
+ crossBA: MonomorphType(Vec3, undefined, true),
35
+ crossCA: MonomorphType(Vec3, undefined, true),
36
+ effectiveMass00: NumberType(0, true),
37
+ effectiveMass01: NumberType(0, true),
38
+ effectiveMass10: NumberType(0, true),
39
+ effectiveMass11: NumberType(0, true),
40
+ inverseInertiaA: MonomorphType(Mat3, undefined, true),
41
+ inverseInertiaB: MonomorphType(Mat3, undefined, true),
42
+ totalLambdaA: NumberType(0, true),
43
+ totalLambdaB: NumberType(0, true),
44
+ } as const satisfies PropertyDefinitionMap;
45
+
46
+ function getOrthogonalVector(out: Vec3, a: Vec3) {
47
+ // y closer to zero, project onto xz plane
48
+ if (Math.abs(a.x) > Math.abs(a.y)) {
49
+ out.fromArray([a.z, 0, -a.x]);
50
+ }
51
+ // x closer to zero, project onto yz plane
52
+ else {
53
+ out.fromArray([0, a.z, -a.y]);
54
+ }
55
+ }
56
+
57
+ export class HingeComponent extends createClass<HingeComponent, typeof hingeComponentProps>(hingeComponentProps) {
58
+ declare world: World;
59
+
60
+ deactivate(): void {
61
+ this.effectiveMass00 = 0;
62
+ this.effectiveMass01 = 0;
63
+ this.totalLambdaA = 0;
64
+ this.totalLambdaB = 0;
65
+ }
66
+
67
+ applyDirectImpulse(lambdaA: number, lambdaB: number): void {
68
+ if (lambdaA === 0 && lambdaB === 0) {
69
+ return;
70
+ }
71
+
72
+ const bodyA = this.bodyA!;
73
+ const bodyB = this.bodyB!;
74
+
75
+ impulse.zero();
76
+ impulse.addScaledVector(this.crossBA, lambdaA);
77
+ impulse.addScaledVector(this.crossCA, lambdaB);
78
+
79
+ if (bodyA.type === BodyType.dynamic) {
80
+ bodyA.angularVelocity.subRotatedVector(impulse, this.inverseInertiaA);
81
+ }
82
+
83
+ if (bodyB.type === BodyType.dynamic) {
84
+ bodyB.angularVelocity.addRotatedVector(impulse, this.inverseInertiaB);
85
+ }
86
+ }
87
+
88
+ warmStart(warmStartImpulseRatio: number): void {
89
+ this.totalLambdaA *= warmStartImpulseRatio;
90
+ this.totalLambdaB *= warmStartImpulseRatio;
91
+ this.applyDirectImpulse(this.totalLambdaA, this.totalLambdaB);
92
+ }
93
+
94
+ solveVelocity(): void {
95
+ const bodyA = this.bodyA!;
96
+ const bodyB = this.bodyB!;
97
+
98
+ spinBA.subtractVectors(bodyA.angularVelocity, bodyB.angularVelocity);
99
+
100
+ const jvA = this.crossBA.dot(spinBA);
101
+ const jvB = this.crossCA.dot(spinBA);
102
+
103
+ const lambdaA = this.options.velocityBaumgarte * (this.effectiveMass00 * jvA + this.effectiveMass10 * jvB);
104
+ const lambdaB = this.options.velocityBaumgarte * (this.effectiveMass01 * jvA + this.effectiveMass11 * jvB);
105
+
106
+ this.totalLambdaA += lambdaA;
107
+ this.totalLambdaB += lambdaB;
108
+
109
+ this.applyDirectImpulse(lambdaA, lambdaB);
110
+ }
111
+
112
+ solvePosition(): void {
113
+ const bodyA = this.bodyA!;
114
+ const bodyB = this.bodyB!;
115
+
116
+ // Constraint needs Axis of body 1 perpendicular to both B and C from body 2 (which are both perpendicular to the Axis of body 2)
117
+ const cA = this.axisA.dot(this.axisB);
118
+ const cB = this.axisA.dot(this.axisC);
119
+ // TODO: we may need a check for "close to zero" instead of "equal to zero"
120
+ if (cA == 0 && cB == 0) {
121
+ return;
122
+ }
123
+
124
+ const lambdaA = -this.options.positionBaumgarte * (this.effectiveMass00 * cA + this.effectiveMass10 * cB);
125
+ const lambdaB = -this.options.positionBaumgarte * (this.effectiveMass01 * cA + this.effectiveMass11 * cB);
126
+
127
+ impulse.zero();
128
+ impulse.addScaledVector(this.crossBA, lambdaA);
129
+ impulse.addScaledVector(this.crossCA, lambdaB);
130
+
131
+ if (bodyA.type === BodyType.dynamic) {
132
+ bodyA.subRotationPositionImpulse(impulse, this.inverseInertiaA);
133
+ }
134
+
135
+ if (bodyB.type === BodyType.dynamic) {
136
+ bodyB.addRotationPositionImpulse(impulse, this.inverseInertiaB);
137
+ }
138
+ }
139
+
140
+ setup(hingeAxisA: Vec3, hingeAxisB: Vec3): void {
141
+ const bodyA = this.bodyA!;
142
+ const bodyB = this.bodyB!;
143
+
144
+ this.axisA.copy(hingeAxisA);
145
+
146
+ axisA2.copy(hingeAxisB);
147
+
148
+ let dot = this.axisA.dot(axisA2);
149
+ if (dot <= 1e-3) {
150
+ // World space axes are more than 90 degrees apart, get a perpendicular vector in the plane formed by mA1 and a2 as hinge axis until the rotation is less than 90 degrees
151
+
152
+ // perpendicular.copy(axisA2).subScaledVector(this.axisA, dot);
153
+ perpendicular.addScaledToVector(axisA2, this.axisA, -dot);
154
+ if (perpendicular.squaredLength() < 1e-6) {
155
+ // mA1 ~ -a2, take random perpendicular
156
+ // perpendicular.copy(this.axisA.computeNormalizedPerpendicular());
157
+ getOrthogonalVector(perpendicular, this.axisA);
158
+ perpendicular.normalize();
159
+ }
160
+
161
+ axisA2.zero();
162
+ axisA2.addScaledVector(perpendicular, 0.99);
163
+ axisA2.addScaledVector(this.axisA, 0.01);
164
+ axisA2.normalize();
165
+ }
166
+
167
+ getOrthogonalVector(this.axisB, axisA2);
168
+ this.axisB.normalize();
169
+
170
+ this.axisC.crossVectors(axisA2, this.axisB);
171
+ this.axisC.normalize();
172
+
173
+ // Calculate properties used during constraint solving
174
+ if (bodyA.type === BodyType.dynamic) {
175
+ bodyA.computeInverseInertiaTensor(this.inverseInertiaA);
176
+ } else {
177
+ this.inverseInertiaA.zero();
178
+ }
179
+
180
+ if (bodyB.type === BodyType.dynamic) {
181
+ bodyB.computeInverseInertiaTensor(this.inverseInertiaB);
182
+ } else {
183
+ this.inverseInertiaB.zero();
184
+ }
185
+
186
+ this.crossBA.crossVectors(this.axisB, this.axisA);
187
+ this.crossCA.crossVectors(this.axisC, this.axisA);
188
+
189
+ summedInverseInertia.addMatrices(this.inverseInertiaA, this.inverseInertiaB);
190
+
191
+ massBA.transformVectorFromMat3(this.crossBA, summedInverseInertia);
192
+ massCA.transformVectorFromMat3(this.crossCA, summedInverseInertia);
193
+
194
+ const effectiveMass00 = this.crossBA.dot(massBA);
195
+ const effectiveMass01 = this.crossBA.dot(massCA);
196
+ const effectiveMass10 = this.crossCA.dot(massBA);
197
+ const effectiveMass11 = this.crossCA.dot(massCA);
198
+
199
+ const determinant = effectiveMass00 * effectiveMass11 - effectiveMass01 * effectiveMass10;
200
+ // TODO: we may need a check for "close to zero" instead of "equal to zero"
201
+ if (determinant === 0) {
202
+ this.deactivate();
203
+ } else {
204
+ this.effectiveMass00 = effectiveMass11 / determinant;
205
+ this.effectiveMass01 = -effectiveMass10 / determinant;
206
+ this.effectiveMass10 = -effectiveMass01 / determinant;
207
+ this.effectiveMass11 = effectiveMass00 / determinant;
208
+ }
209
+
210
+ this.effectiveMass00 *= this.options.strength;
211
+ this.effectiveMass01 *= this.options.strength;
212
+ this.effectiveMass10 *= this.options.strength;
213
+ this.effectiveMass11 *= this.options.strength;
214
+ }
215
+ }