@series-inc/rundot-kinetix 0.0.0-bootstrap.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.
- package/README.md +77 -0
- package/authoring.d.ts +30 -0
- package/index.d.ts +221 -0
- package/native/component_runtime.cpp +341 -0
- package/native/component_runtime.hpp +112 -0
- package/native/deterministic_math.cpp +594 -0
- package/native/deterministic_math.hpp +21 -0
- package/native/kinetix-f64-native-profile.cpp +406 -0
- package/native/kinetix-f64-native-profile.hpp +5 -0
- package/native/kinetix-fixed1000-native-profile.cpp +777 -0
- package/native/kinetix-fixed1000-native-profile.hpp +58 -0
- package/native/kinetix-fixed1000-physics.cpp +887 -0
- package/native/kinetix-fixed1000-physics.hpp +28 -0
- package/native/kinetix-installed-f64-renderer.cpp +344 -0
- package/native/kinetix-installed-f64-renderer.hpp +35 -0
- package/native/kinetix-installed-f64-runtime.cpp +1085 -0
- package/native/kinetix-installed-f64-runtime.hpp +141 -0
- package/native/kinetix-installed-fixed1000-data.hpp +77 -0
- package/native/kinetix-native-main.cpp +37 -0
- package/native/kinetix-native-runtime.cpp +20 -0
- package/native/kinetix-native-runtime.hpp +25 -0
- package/native/kinetix-render-projection.cpp +20 -0
- package/native/kinetix-render-projection.hpp +14 -0
- package/package.json +65 -0
- package/runtime.d.ts +76 -0
- package/scripts/build-native.mjs +67 -0
- package/scripts/emit-product-digests.mjs +33 -0
- package/scripts/preflight.mjs +76 -0
- package/src/index.mjs +57 -0
- package/src/kinetix-authoring.mjs +69 -0
- package/src/kinetix-baker.mjs +587 -0
- package/src/kinetix-deterministic-math.mjs +1044 -0
- package/src/kinetix-fixed1000-runtime-adapter.mjs +33 -0
- package/src/kinetix-fixed1000-runtime.mjs +954 -0
- package/src/kinetix-installed-f64-reference.mjs +157 -0
- package/src/kinetix-installed-f64-render-frames.mjs +53 -0
- package/src/kinetix-installed-f64-renderer.mjs +240 -0
- package/src/kinetix-installed-f64-runtime-adapter.mjs +68 -0
- package/src/kinetix-installed-f64-runtime.mjs +607 -0
- package/src/kinetix-installed-mechanics.mjs +377 -0
- package/src/kinetix-installed-systems.mjs +181 -0
- package/src/kinetix-native-product.mjs +72 -0
- package/src/kinetix-product-contract.mjs +121 -0
- package/src/kinetix-project-compiler.mjs +1017 -0
- package/src/kinetix-render-projection.mjs +28 -0
- package/src/kinetix-runtime-adapter-utils.mjs +168 -0
- package/src/kinetix-runtime-contract.mjs +54 -0
- package/src/kinetix-session-config.mjs +170 -0
- package/src/kinetix-source-snapshot.mjs +24 -0
- package/src/kinetix-survival-runtime-adapter.mjs +305 -0
- package/src/kinetix-survival-runtime.generated.mjs +1 -0
- package/src/kinetix-world-kernel.mjs +580 -0
- package/src/kinetix-world-runtime.mjs +171 -0
- package/src/runtime.mjs +14 -0
- package/src/shared/f64-bits.mjs +14 -0
- package/src/shared/kinetix-envelope-v1.mjs +589 -0
- package/src/shared/render-records-v1.mjs +168 -0
- package/src/shared/sha256.mjs +73 -0
|
@@ -0,0 +1,887 @@
|
|
|
1
|
+
// Verbatim C++ transliteration of the syncplay physics slice (see header).
|
|
2
|
+
// Reference: venus packages/syncplay/src/physics3d.ts (createDeterministicPhysicsWorld3D,
|
|
3
|
+
// buildSolverBody, runRigidSolver readback), physics3d-solver.ts (solveRigidStep,
|
|
4
|
+
// sphereSphere/sphereBox narrowphase, solveContactSoft, applyImpulseScalar),
|
|
5
|
+
// physics3d-shared.ts (solverSqrt, quantizeDistance, vector kernel).
|
|
6
|
+
//
|
|
7
|
+
// Slice collapse facts (verified against the sources):
|
|
8
|
+
// - No material -> combined mu = restitution = 0; tangent solves are numeric
|
|
9
|
+
// no-ops but are kept structurally (they touch +/-0 signs).
|
|
10
|
+
// - No `orientation` on any body -> angularLock = {0,0,0} -> the locked world
|
|
11
|
+
// inverse inertia is the zero matrix; identity quaternions everywhere.
|
|
12
|
+
// - Bodies are fresh each call: sleeping=false, lowVelFrames=0, warm cache
|
|
13
|
+
// empty, no joints, no CCD. islandSleep can never trip within one step from
|
|
14
|
+
// a fresh world (lowVelFrames <= 1 < SLEEP_FRAMES 20), so it is omitted.
|
|
15
|
+
|
|
16
|
+
#include "kinetix-fixed1000-physics.hpp"
|
|
17
|
+
|
|
18
|
+
#include <algorithm>
|
|
19
|
+
#include <array>
|
|
20
|
+
#include <cmath>
|
|
21
|
+
#include <cstdint>
|
|
22
|
+
#include <cstdio>
|
|
23
|
+
#include <cstdlib>
|
|
24
|
+
#include <cstring>
|
|
25
|
+
#include <limits>
|
|
26
|
+
|
|
27
|
+
namespace {
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// JS numeric semantics
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
// ECMAScript Math.round: nearest integer, ties toward +Infinity, preserves -0
|
|
34
|
+
// for -0.5 <= x < 0. V8 computes it as ceil(x) minus 1 when ceil overshoots by
|
|
35
|
+
// more than 0.5 -- NOT floor(x+0.5) (which double-rounds near ties).
|
|
36
|
+
double jsRound(double x) {
|
|
37
|
+
double r = std::ceil(x);
|
|
38
|
+
if (r - 0.5 <= x) {
|
|
39
|
+
return r;
|
|
40
|
+
}
|
|
41
|
+
return r - 1.0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ECMAScript Math.max(a, b): NaN-propagating, +0 beats -0.
|
|
45
|
+
double jsMax(double a, double b) {
|
|
46
|
+
if (std::isnan(a) || std::isnan(b)) {
|
|
47
|
+
return std::numeric_limits<double>::quiet_NaN();
|
|
48
|
+
}
|
|
49
|
+
if (a > b) {
|
|
50
|
+
return a;
|
|
51
|
+
}
|
|
52
|
+
if (b > a) {
|
|
53
|
+
return b;
|
|
54
|
+
}
|
|
55
|
+
if (a == 0.0) {
|
|
56
|
+
return std::signbit(a) ? b : a;
|
|
57
|
+
}
|
|
58
|
+
return a;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// physics3d-shared.ts clampNumber
|
|
62
|
+
double clampNum(double value, double lo, double hi) {
|
|
63
|
+
return value < lo ? lo : value > hi ? hi : value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// physics3d-shared.ts quantizeDistance
|
|
67
|
+
double quantizeDistance(double distance) {
|
|
68
|
+
const double quantized = jsRound(distance * 1e6) / 1e6;
|
|
69
|
+
return quantized == 0.0 ? 0.0 : quantized;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// physics3d-shared.ts solverSqrt (unquantized Newton, exact seed schedule)
|
|
73
|
+
double solverSqrt(double value) {
|
|
74
|
+
if (value <= 0.0) {
|
|
75
|
+
return 0.0;
|
|
76
|
+
}
|
|
77
|
+
if (!std::isfinite(value)) {
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
double estimate = 1.0;
|
|
81
|
+
double magnitude = value;
|
|
82
|
+
while (magnitude >= 4.0) {
|
|
83
|
+
magnitude /= 4.0;
|
|
84
|
+
estimate *= 2.0;
|
|
85
|
+
}
|
|
86
|
+
if (magnitude > 1.0) {
|
|
87
|
+
estimate *= 2.0;
|
|
88
|
+
}
|
|
89
|
+
for (int iteration = 0; iteration < 8; iteration += 1) {
|
|
90
|
+
estimate = (estimate + value / estimate) / 2.0;
|
|
91
|
+
}
|
|
92
|
+
return estimate;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Vector / quaternion / matrix kernel (physics3d-shared.ts, verbatim order)
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
struct Vec3 {
|
|
100
|
+
double x, y, z;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
struct Quat {
|
|
104
|
+
double x, y, z, w;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
using Mat3 = std::array<double, 9>;
|
|
108
|
+
|
|
109
|
+
constexpr Vec3 ZERO_VEC{0.0, 0.0, 0.0};
|
|
110
|
+
|
|
111
|
+
Vec3 addV(const Vec3& a, const Vec3& b) { return {a.x + b.x, a.y + b.y, a.z + b.z}; }
|
|
112
|
+
Vec3 subV(const Vec3& a, const Vec3& b) { return {a.x - b.x, a.y - b.y, a.z - b.z}; }
|
|
113
|
+
Vec3 scaleV(const Vec3& a, double s) { return {a.x * s, a.y * s, a.z * s}; }
|
|
114
|
+
double dotV(const Vec3& a, const Vec3& b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
|
|
115
|
+
Vec3 crossV(const Vec3& a, const Vec3& b) {
|
|
116
|
+
return {
|
|
117
|
+
a.y * b.z - a.z * b.y,
|
|
118
|
+
a.z * b.x - a.x * b.z,
|
|
119
|
+
a.x * b.y - a.y * b.x,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
Mat3 quatToMat3(const Quat& q) {
|
|
124
|
+
const double x = q.x;
|
|
125
|
+
const double y = q.y;
|
|
126
|
+
const double z = q.z;
|
|
127
|
+
const double w = q.w;
|
|
128
|
+
return {
|
|
129
|
+
1 - 2 * (y * y + z * z), 2 * (x * y - w * z), 2 * (x * z + w * y),
|
|
130
|
+
2 * (x * y + w * z), 1 - 2 * (x * x + z * z), 2 * (y * z - w * x),
|
|
131
|
+
2 * (x * z - w * y), 2 * (y * z + w * x), 1 - 2 * (x * x + y * y),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
Vec3 mulMat3Vec(const Mat3& m, const Vec3& a) {
|
|
136
|
+
return {
|
|
137
|
+
m[0] * a.x + m[1] * a.y + m[2] * a.z,
|
|
138
|
+
m[3] * a.x + m[4] * a.y + m[5] * a.z,
|
|
139
|
+
m[6] * a.x + m[7] * a.y + m[8] * a.z,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// World-space inverse inertia = R * diag(invI) * R^T (physics3d-shared.ts)
|
|
144
|
+
Mat3 worldInvInertia(const Mat3& rot, const Vec3& invI) {
|
|
145
|
+
const double invIArr[3] = {invI.x, invI.y, invI.z};
|
|
146
|
+
Mat3 out{};
|
|
147
|
+
for (int row = 0; row < 3; row += 1) {
|
|
148
|
+
for (int col = 0; col < 3; col += 1) {
|
|
149
|
+
out[row * 3 + col] =
|
|
150
|
+
rot[row * 3 + 0] * invIArr[0] * rot[col * 3 + 0] +
|
|
151
|
+
rot[row * 3 + 1] * invIArr[1] * rot[col * 3 + 1] +
|
|
152
|
+
rot[row * 3 + 2] * invIArr[2] * rot[col * 3 + 2];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
Quat quatConjugate(const Quat& q) { return {-q.x, -q.y, -q.z, q.w}; }
|
|
159
|
+
|
|
160
|
+
// Rotate a vector by a quaternion using only mul/add/cross.
|
|
161
|
+
Vec3 rotatePointByQuat(const Quat& q, const Vec3& p) {
|
|
162
|
+
const Vec3 u{q.x, q.y, q.z};
|
|
163
|
+
const Vec3 t = scaleV(crossV(u, p), 2.0);
|
|
164
|
+
return addV(addV(p, scaleV(t, q.w)), crossV(u, t));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
Vec3 rotatePointByQuatInverse(const Quat& q, const Vec3& p) {
|
|
168
|
+
return rotatePointByQuat(quatConjugate(q), p);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// Solver constants (physics3d-solver.ts SOLVER_CONSTANTS + module constants)
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
constexpr double SLOP = 0.005;
|
|
176
|
+
constexpr double LINEAR_DAMPING = 0.995;
|
|
177
|
+
constexpr double ANGULAR_DAMPING = 0.98;
|
|
178
|
+
constexpr int SUBSTEPS = 6;
|
|
179
|
+
constexpr int RELAX_ITERATION_SCALE = 2;
|
|
180
|
+
constexpr double CONTACT_ZETA = 10.0;
|
|
181
|
+
constexpr double CONTACT_HERTZ_FRACTION = 0.25;
|
|
182
|
+
constexpr double STATIC_HERTZ_SCALE = 2.0;
|
|
183
|
+
constexpr double MAX_CONTACT_PUSHOUT = 0.05;
|
|
184
|
+
constexpr double RESTITUTION_THRESHOLD = 1.0;
|
|
185
|
+
constexpr double TWO_PI = 6.283185307179586;
|
|
186
|
+
constexpr int VELOCITY_ITERATIONS = 3; // physics3d.ts DEFAULT_VELOCITY_ITERATIONS
|
|
187
|
+
constexpr double DT = 1.0;
|
|
188
|
+
constexpr double GRAVITY_Y = 0.0;
|
|
189
|
+
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Solver body (buildSolverBody, physics3d.ts, specialized to the slice)
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
struct SBody {
|
|
195
|
+
int index = 0;
|
|
196
|
+
bool isDynamic = false;
|
|
197
|
+
bool sleeping = false; // fresh worlds: always false
|
|
198
|
+
Vec3 p{}, v{}, w{}, vb{}, wb{};
|
|
199
|
+
Quat q{0.0, 0.0, 0.0, 1.0}; // no body has `orientation`
|
|
200
|
+
Vec3 invMassVec{};
|
|
201
|
+
Vec3 invInertiaLocal{};
|
|
202
|
+
Vec3 angularLock{}; // hasOrientation=false -> {0,0,0}
|
|
203
|
+
double linearDamping = LINEAR_DAMPING;
|
|
204
|
+
double angularDamping = ANGULAR_DAMPING;
|
|
205
|
+
double gravityScale = 1.0;
|
|
206
|
+
bool isSphere = false;
|
|
207
|
+
double radius = 0.0;
|
|
208
|
+
Vec3 half{};
|
|
209
|
+
int layer = 0;
|
|
210
|
+
unsigned mask = 0;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
bool solid(const SBody& b) { return b.isDynamic && !b.sleeping; }
|
|
214
|
+
|
|
215
|
+
// physics3d.ts deriveInverseInertia (sphere + box arms; other shapes are
|
|
216
|
+
// outside the slice).
|
|
217
|
+
Vec3 deriveInverseInertia(const SBody& proto, double mass) {
|
|
218
|
+
if (proto.isSphere) {
|
|
219
|
+
const double i = (2.0 / 5.0) * mass * proto.radius * proto.radius;
|
|
220
|
+
return {1.0 / i, 1.0 / i, 1.0 / i};
|
|
221
|
+
}
|
|
222
|
+
const double sx = 2.0 * proto.half.x;
|
|
223
|
+
const double sy = 2.0 * proto.half.y;
|
|
224
|
+
const double sz = 2.0 * proto.half.z;
|
|
225
|
+
const double ix = (mass / 12.0) * (sy * sy + sz * sz);
|
|
226
|
+
const double iy = (mass / 12.0) * (sx * sx + sz * sz);
|
|
227
|
+
const double iz = (mass / 12.0) * (sx * sx + sy * sy);
|
|
228
|
+
return {1.0 / ix, 1.0 / iy, 1.0 / iz};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// physics3d-solver.ts worldInvInertiaLocked
|
|
232
|
+
Mat3 worldInvInertiaLocked(const SBody& b) {
|
|
233
|
+
if (!solid(b)) {
|
|
234
|
+
return {0, 0, 0, 0, 0, 0, 0, 0, 0};
|
|
235
|
+
}
|
|
236
|
+
const Mat3 base = worldInvInertia(quatToMat3(b.q), b.invInertiaLocal);
|
|
237
|
+
const double lockArr[3] = {b.angularLock.x, b.angularLock.y, b.angularLock.z};
|
|
238
|
+
Mat3 out{};
|
|
239
|
+
for (int row = 0; row < 3; row += 1) {
|
|
240
|
+
for (int col = 0; col < 3; col += 1) {
|
|
241
|
+
out[row * 3 + col] = base[row * 3 + col] * lockArr[row] * lockArr[col];
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
Vec3 invMassOf(const SBody& b) { return solid(b) ? b.invMassVec : ZERO_VEC; }
|
|
248
|
+
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// Narrowphase (physics3d-solver.ts, sphere-sphere + sphere-box only; box-box
|
|
251
|
+
// pairs are both static and never reach the narrowphase in this slice)
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
struct ContactPoint {
|
|
255
|
+
Vec3 rA{}, rB{};
|
|
256
|
+
double penetration = 0.0;
|
|
257
|
+
int feature = 0;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
struct Manifold {
|
|
261
|
+
int a = 0;
|
|
262
|
+
int b = 0;
|
|
263
|
+
Vec3 normal{};
|
|
264
|
+
// The slice's manifolds always have exactly one point (pointContact /
|
|
265
|
+
// emitSingle); reducePoints over one point is the identity.
|
|
266
|
+
ContactPoint point{};
|
|
267
|
+
bool valid = false;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// physics3d-solver.ts pointContact
|
|
271
|
+
Manifold pointContact(
|
|
272
|
+
const Vec3& posA, const Vec3& posB, double radiusA, double radiusB,
|
|
273
|
+
const Vec3& centerA, const Vec3& centerB,
|
|
274
|
+
double speculativeMargin, int feature, bool flip
|
|
275
|
+
) {
|
|
276
|
+
const Vec3 delta = subV(posB, posA);
|
|
277
|
+
const double distSq = dotV(delta, delta);
|
|
278
|
+
const double dist = solverSqrt(distSq);
|
|
279
|
+
const double sumR = radiusA + radiusB;
|
|
280
|
+
const double penetration = sumR - dist;
|
|
281
|
+
if (penetration < -speculativeMargin) {
|
|
282
|
+
return {};
|
|
283
|
+
}
|
|
284
|
+
Vec3 normal{};
|
|
285
|
+
if (dist > 1e-9) {
|
|
286
|
+
normal = scaleV(delta, 1.0 / dist);
|
|
287
|
+
} else {
|
|
288
|
+
normal = {0.0, 1.0, 0.0};
|
|
289
|
+
}
|
|
290
|
+
const Vec3 surfaceA = addV(posA, scaleV(normal, radiusA));
|
|
291
|
+
const Vec3 surfaceB = subV(posB, scaleV(normal, radiusB));
|
|
292
|
+
const Vec3 mid = scaleV(addV(surfaceA, surfaceB), 0.5);
|
|
293
|
+
const Vec3 outNormal = flip ? scaleV(normal, -1.0) : normal;
|
|
294
|
+
const Vec3 aCenter = flip ? centerB : centerA;
|
|
295
|
+
const Vec3 bCenter = flip ? centerA : centerB;
|
|
296
|
+
Manifold m;
|
|
297
|
+
m.normal = outNormal;
|
|
298
|
+
m.point = {subV(mid, aCenter), subV(mid, bCenter), penetration, feature};
|
|
299
|
+
m.valid = true;
|
|
300
|
+
return m;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
struct ClosestOnBox {
|
|
304
|
+
Vec3 point{};
|
|
305
|
+
bool inside = false;
|
|
306
|
+
Vec3 localClamped{};
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
// physics3d-solver.ts closestPointOnBox (box.q is always identity here, but the
|
|
310
|
+
// quaternion rotations are kept verbatim -- they shape +/-0 signs).
|
|
311
|
+
ClosestOnBox closestPointOnBox(const SBody& box, const Vec3& worldPoint) {
|
|
312
|
+
const Vec3 half = box.half;
|
|
313
|
+
const Vec3 local = rotatePointByQuatInverse(box.q, subV(worldPoint, box.p));
|
|
314
|
+
const Vec3 clampedLocal{
|
|
315
|
+
clampNum(local.x, -half.x, half.x),
|
|
316
|
+
clampNum(local.y, -half.y, half.y),
|
|
317
|
+
clampNum(local.z, -half.z, half.z),
|
|
318
|
+
};
|
|
319
|
+
const bool inside = clampedLocal.x == local.x && clampedLocal.y == local.y && clampedLocal.z == local.z;
|
|
320
|
+
return {addV(box.p, rotatePointByQuat(box.q, clampedLocal)), inside, clampedLocal};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// physics3d-solver.ts emitSingle
|
|
324
|
+
Manifold emitSingle(
|
|
325
|
+
const Vec3& nSphereToBox, const Vec3& mid, double penetration,
|
|
326
|
+
const Vec3& cSphere, const Vec3& cBox, int feature, bool flip
|
|
327
|
+
) {
|
|
328
|
+
const Vec3 normal = flip ? scaleV(nSphereToBox, -1.0) : nSphereToBox;
|
|
329
|
+
const Vec3 aCenter = flip ? cBox : cSphere;
|
|
330
|
+
const Vec3 bCenter = flip ? cSphere : cBox;
|
|
331
|
+
Manifold m;
|
|
332
|
+
m.normal = normal;
|
|
333
|
+
m.point = {subV(mid, aCenter), subV(mid, bCenter), penetration, feature};
|
|
334
|
+
m.valid = true;
|
|
335
|
+
return m;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// physics3d-solver.ts sphereBox
|
|
339
|
+
Manifold sphereBox(
|
|
340
|
+
const SBody& sphere, const SBody& box,
|
|
341
|
+
const Vec3& cSphere, const Vec3& cBox,
|
|
342
|
+
double margin, int feature, bool flip
|
|
343
|
+
) {
|
|
344
|
+
const double radius = sphere.radius;
|
|
345
|
+
const ClosestOnBox closest = closestPointOnBox(box, sphere.p);
|
|
346
|
+
const Vec3 half = box.half;
|
|
347
|
+
if (closest.inside) {
|
|
348
|
+
const Vec3 local = closest.localClamped;
|
|
349
|
+
const double distances[3] = {
|
|
350
|
+
half.x - std::fabs(local.x),
|
|
351
|
+
half.y - std::fabs(local.y),
|
|
352
|
+
half.z - std::fabs(local.z),
|
|
353
|
+
};
|
|
354
|
+
int axis = 0;
|
|
355
|
+
if (distances[1] < distances[axis]) axis = 1;
|
|
356
|
+
if (distances[2] < distances[axis]) axis = 2;
|
|
357
|
+
const Vec3 localAxes[3] = {{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}};
|
|
358
|
+
const double localComponent = axis == 0 ? local.x : axis == 1 ? local.y : local.z;
|
|
359
|
+
const Vec3 localNormal = scaleV(localAxes[axis], localComponent >= 0.0 ? 1.0 : -1.0);
|
|
360
|
+
const Vec3 worldNormal = rotatePointByQuat(box.q, localNormal);
|
|
361
|
+
const double penetration = distances[axis] + radius;
|
|
362
|
+
const Vec3 nSphereToBox = scaleV(worldNormal, -1.0);
|
|
363
|
+
const Vec3 mid = sphere.p;
|
|
364
|
+
return emitSingle(nSphereToBox, mid, penetration, cSphere, cBox, feature, flip);
|
|
365
|
+
}
|
|
366
|
+
const Vec3 delta = subV(sphere.p, closest.point);
|
|
367
|
+
const double dist = solverSqrt(dotV(delta, delta));
|
|
368
|
+
const double penetration = radius - dist;
|
|
369
|
+
if (penetration < -margin) {
|
|
370
|
+
return {};
|
|
371
|
+
}
|
|
372
|
+
const Vec3 nBoxToSphere = dist > 1e-9 ? scaleV(delta, 1.0 / dist) : Vec3{0.0, 1.0, 0.0};
|
|
373
|
+
const Vec3 nSphereToBox = scaleV(nBoxToSphere, -1.0);
|
|
374
|
+
const Vec3 mid = scaleV(addV(closest.point, subV(sphere.p, scaleV(nBoxToSphere, radius))), 0.5);
|
|
375
|
+
return emitSingle(nSphereToBox, mid, penetration, cSphere, cBox, feature, flip);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// generateManifold single-primitive path + collidePrimitives dispatch.
|
|
379
|
+
Manifold generateManifold(const SBody& a, const SBody& b, double speculativeMargin) {
|
|
380
|
+
const int featureBase = 0;
|
|
381
|
+
Manifold m;
|
|
382
|
+
if (a.isSphere && b.isSphere) {
|
|
383
|
+
m = pointContact(a.p, b.p, a.radius, b.radius, a.p, b.p, speculativeMargin, featureBase, false);
|
|
384
|
+
} else if (a.isSphere && !b.isSphere) {
|
|
385
|
+
m = sphereBox(a, b, a.p, b.p, speculativeMargin, featureBase, false);
|
|
386
|
+
} else if (!a.isSphere && b.isSphere) {
|
|
387
|
+
m = sphereBox(b, a, b.p, a.p, speculativeMargin, featureBase, true);
|
|
388
|
+
} else {
|
|
389
|
+
// box-box: unreachable (both static pairs are skipped in emitPair).
|
|
390
|
+
std::fprintf(stderr, "physics slice: box-box narrowphase is outside the slice\n");
|
|
391
|
+
std::abort();
|
|
392
|
+
}
|
|
393
|
+
if (!m.valid) {
|
|
394
|
+
return {};
|
|
395
|
+
}
|
|
396
|
+
m.a = a.index;
|
|
397
|
+
m.b = b.index;
|
|
398
|
+
return m;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
// Broadphase AABBs (physics3d-solver.ts solverBodyAabb / shapeHalfExtent;
|
|
403
|
+
// ccd=false so no velocity sweep)
|
|
404
|
+
// ---------------------------------------------------------------------------
|
|
405
|
+
|
|
406
|
+
struct Aabb {
|
|
407
|
+
double minX, maxX, minY, maxY, minZ, maxZ;
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
Vec3 shapeHalfExtent(const SBody& b) {
|
|
411
|
+
if (!b.isSphere) {
|
|
412
|
+
const Mat3 rot = quatToMat3(b.q);
|
|
413
|
+
return {
|
|
414
|
+
std::fabs(rot[0]) * b.half.x + std::fabs(rot[1]) * b.half.y + std::fabs(rot[2]) * b.half.z,
|
|
415
|
+
std::fabs(rot[3]) * b.half.x + std::fabs(rot[4]) * b.half.y + std::fabs(rot[5]) * b.half.z,
|
|
416
|
+
std::fabs(rot[6]) * b.half.x + std::fabs(rot[7]) * b.half.y + std::fabs(rot[8]) * b.half.z,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
return {b.radius, b.radius, b.radius};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
Aabb solverBodyAabb(const SBody& body) {
|
|
423
|
+
const Vec3 half = shapeHalfExtent(body);
|
|
424
|
+
return {
|
|
425
|
+
body.p.x - half.x, body.p.x + half.x,
|
|
426
|
+
body.p.y - half.y, body.p.y + half.y,
|
|
427
|
+
body.p.z - half.z, body.p.z + half.z,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ---------------------------------------------------------------------------
|
|
432
|
+
// Contact constraints + impulse kernel (physics3d-solver.ts)
|
|
433
|
+
// ---------------------------------------------------------------------------
|
|
434
|
+
|
|
435
|
+
struct Constraint {
|
|
436
|
+
int a = 0, b = 0;
|
|
437
|
+
Vec3 normal{}, t1{}, t2{};
|
|
438
|
+
Vec3 rA{}, rB{};
|
|
439
|
+
double penetration = 0.0;
|
|
440
|
+
double massN = 0.0, massT1 = 0.0, massT2 = 0.0;
|
|
441
|
+
double biasRate = 0.0, massScale = 1.0, impulseScale = 0.0;
|
|
442
|
+
double vn0 = 0.0;
|
|
443
|
+
double accN = 0.0, accT1 = 0.0, accT2 = 0.0, accNStep = 0.0;
|
|
444
|
+
double mu = 0.0;
|
|
445
|
+
double restitution = 0.0;
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
double effectiveMass(
|
|
449
|
+
const Vec3& axis, const Vec3& rA, const Vec3& rB,
|
|
450
|
+
const Vec3& imA, const Vec3& imB, const Mat3& iiA, const Mat3& iiB
|
|
451
|
+
) {
|
|
452
|
+
const double linA = axis.x * axis.x * imA.x + axis.y * axis.y * imA.y + axis.z * axis.z * imA.z;
|
|
453
|
+
const double linB = axis.x * axis.x * imB.x + axis.y * axis.y * imB.y + axis.z * axis.z * imB.z;
|
|
454
|
+
const Vec3 raxn = crossV(rA, axis);
|
|
455
|
+
const Vec3 rbxn = crossV(rB, axis);
|
|
456
|
+
const double denom = linA + linB + dotV(raxn, mulMat3Vec(iiA, raxn)) + dotV(rbxn, mulMat3Vec(iiB, rbxn));
|
|
457
|
+
return denom == 0.0 ? 0.0 : 1.0 / denom;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
Vec3 normalizeVec3Local(const Vec3& a) {
|
|
461
|
+
const double l = solverSqrt(dotV(a, a));
|
|
462
|
+
return l == 0.0 ? Vec3{0.0, 0.0, 0.0} : scaleV(a, 1.0 / l);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
void tangentBasis(const Vec3& n, Vec3& t1, Vec3& t2) {
|
|
466
|
+
t1 = std::fabs(n.x) >= 0.57735
|
|
467
|
+
? normalizeVec3Local({n.y, -n.x, 0.0})
|
|
468
|
+
: normalizeVec3Local({0.0, n.z, -n.y});
|
|
469
|
+
t2 = crossV(n, t1);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
double toInt10(double unit) { return jsRound(unit * 10.0); }
|
|
473
|
+
|
|
474
|
+
// physics3d-solver.ts applyImpulseScalar (verbatim scalar form)
|
|
475
|
+
void applyImpulseScalar(
|
|
476
|
+
std::vector<SBody>& bodies, const std::vector<Vec3>& invMass, const std::vector<Mat3>& invInertia,
|
|
477
|
+
int a, int b, const Vec3& rA, const Vec3& rB,
|
|
478
|
+
double px, double py, double pz, bool bias
|
|
479
|
+
) {
|
|
480
|
+
SBody& A = bodies[a];
|
|
481
|
+
SBody& B = bodies[b];
|
|
482
|
+
if (solid(A)) {
|
|
483
|
+
const double cx = rA.y * pz - rA.z * py;
|
|
484
|
+
const double cy = rA.z * px - rA.x * pz;
|
|
485
|
+
const double cz = rA.x * py - rA.y * px;
|
|
486
|
+
const Mat3& m = invInertia[a];
|
|
487
|
+
const Vec3& inv = invMass[a];
|
|
488
|
+
Vec3& lin = bias ? A.vb : A.v;
|
|
489
|
+
Vec3& ang = bias ? A.wb : A.w;
|
|
490
|
+
lin.x = lin.x - px * inv.x;
|
|
491
|
+
lin.y = lin.y - py * inv.y;
|
|
492
|
+
lin.z = lin.z - pz * inv.z;
|
|
493
|
+
ang.x = ang.x - (m[0] * cx + m[1] * cy + m[2] * cz);
|
|
494
|
+
ang.y = ang.y - (m[3] * cx + m[4] * cy + m[5] * cz);
|
|
495
|
+
ang.z = ang.z - (m[6] * cx + m[7] * cy + m[8] * cz);
|
|
496
|
+
}
|
|
497
|
+
if (solid(B)) {
|
|
498
|
+
const double cx = rB.y * pz - rB.z * py;
|
|
499
|
+
const double cy = rB.z * px - rB.x * pz;
|
|
500
|
+
const double cz = rB.x * py - rB.y * px;
|
|
501
|
+
const Mat3& m = invInertia[b];
|
|
502
|
+
const Vec3& inv = invMass[b];
|
|
503
|
+
Vec3& lin = bias ? B.vb : B.v;
|
|
504
|
+
Vec3& ang = bias ? B.wb : B.w;
|
|
505
|
+
lin.x = lin.x + px * inv.x;
|
|
506
|
+
lin.y = lin.y + py * inv.y;
|
|
507
|
+
lin.z = lin.z + pz * inv.z;
|
|
508
|
+
ang.x = ang.x + (m[0] * cx + m[1] * cy + m[2] * cz);
|
|
509
|
+
ang.y = ang.y + (m[3] * cx + m[4] * cy + m[5] * cz);
|
|
510
|
+
ang.z = ang.z + (m[6] * cx + m[7] * cy + m[8] * cz);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
double relativeVelocityAlong(
|
|
515
|
+
const SBody& A, const SBody& B, const Vec3& rA, const Vec3& rB,
|
|
516
|
+
double dx, double dy, double dz
|
|
517
|
+
) {
|
|
518
|
+
const double relX = (B.v.x + (B.w.y * rB.z - B.w.z * rB.y)) - (A.v.x + (A.w.y * rA.z - A.w.z * rA.y));
|
|
519
|
+
const double relY = (B.v.y + (B.w.z * rB.x - B.w.x * rB.z)) - (A.v.y + (A.w.z * rA.x - A.w.x * rA.z));
|
|
520
|
+
const double relZ = (B.v.z + (B.w.x * rB.y - B.w.y * rB.x)) - (A.v.z + (A.w.x * rA.y - A.w.y * rA.x));
|
|
521
|
+
return relX * dx + relY * dy + relZ * dz;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
double solveTangentImpulse(
|
|
525
|
+
std::vector<SBody>& bodies, const std::vector<Vec3>& invMass, const std::vector<Mat3>& invInertia,
|
|
526
|
+
Constraint& c, const SBody& A, const SBody& B,
|
|
527
|
+
const Vec3& t, double mass, double acc, double maxF
|
|
528
|
+
) {
|
|
529
|
+
const double vt = relativeVelocityAlong(A, B, c.rA, c.rB, t.x, t.y, t.z);
|
|
530
|
+
double dF = mass * -vt;
|
|
531
|
+
const double clamped = clampNum(acc + dF, -maxF, maxF);
|
|
532
|
+
dF = clamped - acc;
|
|
533
|
+
applyImpulseScalar(bodies, invMass, invInertia, c.a, c.b, c.rA, c.rB, t.x * dF, t.y * dF, t.z * dF, false);
|
|
534
|
+
return clamped;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// physics3d-solver.ts solveContactSoft. NOTE: the normal impulse is applied
|
|
538
|
+
// with bias=false ALWAYS (source line 2370) -- the soft bias folds into real
|
|
539
|
+
// velocity and the relax pass drains it. (The transliteration spec's
|
|
540
|
+
// "into vb/wb when useBias" is wrong; the source wins.)
|
|
541
|
+
void solveContactSoft(
|
|
542
|
+
std::vector<SBody>& bodies, const std::vector<Vec3>& invMass, const std::vector<Mat3>& invInertia,
|
|
543
|
+
Constraint& c, double h, bool useBias
|
|
544
|
+
) {
|
|
545
|
+
const SBody& A = bodies[c.a];
|
|
546
|
+
const SBody& B = bodies[c.b];
|
|
547
|
+
const double vn = relativeVelocityAlong(A, B, c.rA, c.rB, c.normal.x, c.normal.y, c.normal.z);
|
|
548
|
+
double bias = 0.0;
|
|
549
|
+
double massScale = 1.0;
|
|
550
|
+
double impulseScale = 0.0;
|
|
551
|
+
if (c.penetration < 0.0) {
|
|
552
|
+
bias = c.penetration / h;
|
|
553
|
+
} else if (useBias) {
|
|
554
|
+
const double raw = c.biasRate * (c.penetration - SLOP);
|
|
555
|
+
bias = clampNum(raw, 0.0, MAX_CONTACT_PUSHOUT / h);
|
|
556
|
+
massScale = c.massScale;
|
|
557
|
+
impulseScale = c.impulseScale;
|
|
558
|
+
}
|
|
559
|
+
double dLambda = massScale * c.massN * (-(vn) + bias) - impulseScale * c.accN;
|
|
560
|
+
const double newAcc = jsMax(0.0, c.accN + dLambda);
|
|
561
|
+
dLambda = newAcc - c.accN;
|
|
562
|
+
c.accN = newAcc;
|
|
563
|
+
applyImpulseScalar(bodies, invMass, invInertia, c.a, c.b, c.rA, c.rB,
|
|
564
|
+
c.normal.x * dLambda, c.normal.y * dLambda, c.normal.z * dLambda, false);
|
|
565
|
+
if (c.penetration < 0.0) {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
const double maxF = c.mu * c.accN;
|
|
569
|
+
c.accT1 = solveTangentImpulse(bodies, invMass, invInertia, c, A, B, c.t1, c.massT1, c.accT1, maxF);
|
|
570
|
+
c.accT2 = solveTangentImpulse(bodies, invMass, invInertia, c, A, B, c.t2, c.massT2, c.accT2, maxF);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
void debugDump(const std::vector<SBody>& bodies, const char* phase, int substep) {
|
|
574
|
+
for (const SBody& b : bodies) {
|
|
575
|
+
if (!b.isDynamic) {
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
std::uint64_t bits[6];
|
|
579
|
+
const double vals[6] = {b.p.x, b.p.y, b.p.z, b.v.x, b.v.y, b.v.z};
|
|
580
|
+
std::memcpy(bits, vals, sizeof(vals));
|
|
581
|
+
std::fprintf(stderr, "[cpp %s s%d] idx=%d p=%016llx,%016llx,%016llx v=%016llx,%016llx,%016llx\n",
|
|
582
|
+
phase, substep, b.index,
|
|
583
|
+
(unsigned long long)bits[0], (unsigned long long)bits[1], (unsigned long long)bits[2],
|
|
584
|
+
(unsigned long long)bits[3], (unsigned long long)bits[4], (unsigned long long)bits[5]);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
} // namespace
|
|
589
|
+
|
|
590
|
+
// ---------------------------------------------------------------------------
|
|
591
|
+
// One rigid step over the slice world (solveRigidStep specialized: no joints,
|
|
592
|
+
// empty warm cache, no CCD, no sleeping) + quantized readback (runRigidSolver).
|
|
593
|
+
// ---------------------------------------------------------------------------
|
|
594
|
+
|
|
595
|
+
void stepPhysicsSlice(std::vector<PhysBody>& bodies) {
|
|
596
|
+
const bool debug = std::getenv("PHYS_SLICE_DEBUG") != nullptr;
|
|
597
|
+
|
|
598
|
+
// normalizeBodies: canonical lexicographic id order (plain byte-wise <,
|
|
599
|
+
// identical to JS UTF-16 compare for ASCII ids).
|
|
600
|
+
std::sort(bodies.begin(), bodies.end(), [](const PhysBody& l, const PhysBody& r) {
|
|
601
|
+
return l.id < r.id;
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// buildSolverBody
|
|
605
|
+
std::vector<SBody> sb(bodies.size());
|
|
606
|
+
for (std::size_t i = 0; i < bodies.size(); i += 1) {
|
|
607
|
+
const PhysBody& src = bodies[i];
|
|
608
|
+
SBody& b = sb[i];
|
|
609
|
+
b.index = static_cast<int>(i);
|
|
610
|
+
b.isDynamic = src.dynamic;
|
|
611
|
+
b.sleeping = false;
|
|
612
|
+
b.p = {src.x, src.y, src.z};
|
|
613
|
+
b.v = {src.vx, src.vy, src.vz};
|
|
614
|
+
b.w = {0.0, 0.0, 0.0};
|
|
615
|
+
b.vb = {0.0, 0.0, 0.0};
|
|
616
|
+
b.wb = {0.0, 0.0, 0.0};
|
|
617
|
+
const double mass = 1.0;
|
|
618
|
+
const double baseInvMass = src.dynamic ? 1.0 / mass : 0.0;
|
|
619
|
+
b.invMassVec = {baseInvMass, baseInvMass, baseInvMass};
|
|
620
|
+
b.isSphere = src.isSphere;
|
|
621
|
+
b.radius = src.radius;
|
|
622
|
+
b.half = {src.halfX, src.halfY, src.halfZ};
|
|
623
|
+
b.invInertiaLocal = src.dynamic ? deriveInverseInertia(b, mass) : Vec3{0.0, 0.0, 0.0};
|
|
624
|
+
b.angularLock = {0.0, 0.0, 0.0}; // no orientation on any slice body
|
|
625
|
+
b.layer = src.layer;
|
|
626
|
+
b.mask = src.mask;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const double dt = DT;
|
|
630
|
+
const double h = dt / SUBSTEPS;
|
|
631
|
+
|
|
632
|
+
// 1. reset pseudo-velocities; zero locked axes (invMass components are all
|
|
633
|
+
// nonzero for dynamics here; angular lock zeroes w).
|
|
634
|
+
for (SBody& b : sb) {
|
|
635
|
+
b.vb = {0.0, 0.0, 0.0};
|
|
636
|
+
b.wb = {0.0, 0.0, 0.0};
|
|
637
|
+
if (!solid(b)) {
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
b.v = {
|
|
641
|
+
b.invMassVec.x == 0.0 ? 0.0 : b.v.x,
|
|
642
|
+
b.invMassVec.y == 0.0 ? 0.0 : b.v.y,
|
|
643
|
+
b.invMassVec.z == 0.0 ? 0.0 : b.v.z,
|
|
644
|
+
};
|
|
645
|
+
b.w = {
|
|
646
|
+
b.angularLock.x == 0.0 ? 0.0 : b.w.x,
|
|
647
|
+
b.angularLock.y == 0.0 ? 0.0 : b.w.y,
|
|
648
|
+
b.angularLock.z == 0.0 ? 0.0 : b.w.z,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// 2. broadphase sort-and-sweep + narrowphase manifolds.
|
|
653
|
+
std::vector<Manifold> manifolds;
|
|
654
|
+
auto emitPair = [&](int i, int j) {
|
|
655
|
+
const SBody& A = sb[i];
|
|
656
|
+
const SBody& B = sb[j];
|
|
657
|
+
if (!A.isDynamic && !B.isDynamic) {
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
// noCollide (trigger/sensor) and noCollidePairs: absent from the slice.
|
|
661
|
+
if ((static_cast<std::int32_t>(A.mask) & B.layer) == 0 || (static_cast<std::int32_t>(B.mask) & A.layer) == 0) {
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
const double margin = 0.0; // no ccd body in the slice
|
|
665
|
+
const Manifold manifold = generateManifold(A, B, margin);
|
|
666
|
+
if (manifold.valid) {
|
|
667
|
+
manifolds.push_back(manifold);
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
{
|
|
671
|
+
std::vector<Aabb> aabbs(sb.size());
|
|
672
|
+
for (std::size_t i = 0; i < sb.size(); i += 1) {
|
|
673
|
+
aabbs[i] = solverBodyAabb(sb[i]);
|
|
674
|
+
}
|
|
675
|
+
std::vector<int> order(sb.size());
|
|
676
|
+
for (std::size_t i = 0; i < sb.size(); i += 1) {
|
|
677
|
+
order[i] = static_cast<int>(i);
|
|
678
|
+
}
|
|
679
|
+
std::sort(order.begin(), order.end(), [&](int l, int r) {
|
|
680
|
+
if (aabbs[l].minX != aabbs[r].minX) {
|
|
681
|
+
return aabbs[l].minX < aabbs[r].minX;
|
|
682
|
+
}
|
|
683
|
+
return l < r;
|
|
684
|
+
});
|
|
685
|
+
for (std::size_t sweepA = 0; sweepA < order.size(); sweepA += 1) {
|
|
686
|
+
const int i = order[sweepA];
|
|
687
|
+
const Aabb& boundsI = aabbs[i];
|
|
688
|
+
for (std::size_t sweepB = sweepA + 1; sweepB < order.size(); sweepB += 1) {
|
|
689
|
+
const int j = order[sweepB];
|
|
690
|
+
const Aabb& boundsJ = aabbs[j];
|
|
691
|
+
if (boundsJ.minX > boundsI.maxX) {
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
if (boundsJ.minY > boundsI.maxY || boundsJ.maxY < boundsI.minY || boundsJ.minZ > boundsI.maxZ || boundsJ.maxZ < boundsI.minZ) {
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
emitPair(std::min(i, j), std::max(i, j));
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
std::sort(manifolds.begin(), manifolds.end(), [](const Manifold& l, const Manifold& r) {
|
|
702
|
+
if (l.a != r.a) {
|
|
703
|
+
return l.a < r.a;
|
|
704
|
+
}
|
|
705
|
+
return l.b < r.b;
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
// 3. wake pass: nothing sleeps in a fresh world -- no-op.
|
|
709
|
+
|
|
710
|
+
// 4. build contact constraints (+ warm start: cache empty, accumulators 0).
|
|
711
|
+
std::vector<Vec3> invMass(sb.size());
|
|
712
|
+
std::vector<Mat3> invInertia(sb.size());
|
|
713
|
+
for (std::size_t i = 0; i < sb.size(); i += 1) {
|
|
714
|
+
invMass[i] = invMassOf(sb[i]);
|
|
715
|
+
invInertia[i] = worldInvInertiaLocked(sb[i]);
|
|
716
|
+
}
|
|
717
|
+
std::vector<Constraint> constraints;
|
|
718
|
+
constraints.reserve(manifolds.size());
|
|
719
|
+
for (const Manifold& m : manifolds) {
|
|
720
|
+
const SBody& A = sb[m.a];
|
|
721
|
+
const SBody& B = sb[m.b];
|
|
722
|
+
Vec3 t1{}, t2{};
|
|
723
|
+
tangentBasis(m.normal, t1, t2);
|
|
724
|
+
// matFriction = matRestitution = 0 on every slice body.
|
|
725
|
+
const double combinedMu = std::floor((toInt10(0.0) + toInt10(0.0)) / 2.0) / 10.0;
|
|
726
|
+
const double combinedRest = jsMax(toInt10(0.0), toInt10(0.0)) / 10.0;
|
|
727
|
+
const bool softStatic = !A.isDynamic || !B.isDynamic;
|
|
728
|
+
const double hertz = (CONTACT_HERTZ_FRACTION * SUBSTEPS / dt) * (softStatic ? STATIC_HERTZ_SCALE : 1.0);
|
|
729
|
+
const double omega = TWO_PI * hertz;
|
|
730
|
+
const double a1 = 2.0 * CONTACT_ZETA + omega * h;
|
|
731
|
+
const double a2 = h * omega * a1;
|
|
732
|
+
const double a3 = 1.0 / (1.0 + a2);
|
|
733
|
+
const double biasRate = omega / a1;
|
|
734
|
+
const double massScale = a2 * a3;
|
|
735
|
+
const double impulseScale = a3;
|
|
736
|
+
const ContactPoint& pt = m.point;
|
|
737
|
+
Vec3 rA = pt.rA;
|
|
738
|
+
Vec3 rB = pt.rB;
|
|
739
|
+
if (pt.penetration < 0.0) {
|
|
740
|
+
// speculative pullback -- unreachable with margin 0, kept verbatim.
|
|
741
|
+
const double half = -pt.penetration * 0.5;
|
|
742
|
+
rA = {pt.rA.x - m.normal.x * half, pt.rA.y - m.normal.y * half, pt.rA.z - m.normal.z * half};
|
|
743
|
+
rB = {pt.rB.x + m.normal.x * half, pt.rB.y + m.normal.y * half, pt.rB.z + m.normal.z * half};
|
|
744
|
+
}
|
|
745
|
+
Constraint c;
|
|
746
|
+
c.a = m.a;
|
|
747
|
+
c.b = m.b;
|
|
748
|
+
c.normal = m.normal;
|
|
749
|
+
c.t1 = t1;
|
|
750
|
+
c.t2 = t2;
|
|
751
|
+
c.rA = rA;
|
|
752
|
+
c.rB = rB;
|
|
753
|
+
c.penetration = pt.penetration;
|
|
754
|
+
c.massN = effectiveMass(m.normal, rA, rB, invMass[m.a], invMass[m.b], invInertia[m.a], invInertia[m.b]);
|
|
755
|
+
c.massT1 = effectiveMass(t1, rA, rB, invMass[m.a], invMass[m.b], invInertia[m.a], invInertia[m.b]);
|
|
756
|
+
c.massT2 = effectiveMass(t2, rA, rB, invMass[m.a], invMass[m.b], invInertia[m.a], invInertia[m.b]);
|
|
757
|
+
c.biasRate = biasRate;
|
|
758
|
+
c.massScale = massScale;
|
|
759
|
+
c.impulseScale = impulseScale;
|
|
760
|
+
c.vn0 = relativeVelocityAlong(A, B, rA, rB, m.normal.x, m.normal.y, m.normal.z);
|
|
761
|
+
c.accN = 0.0; // matchWarm on an empty cache
|
|
762
|
+
c.accT1 = 0.0;
|
|
763
|
+
c.accT2 = 0.0;
|
|
764
|
+
c.accNStep = 0.0;
|
|
765
|
+
c.mu = combinedMu;
|
|
766
|
+
c.restitution = combinedRest;
|
|
767
|
+
constraints.push_back(c);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// 5. TGS-Soft sub-step loop.
|
|
771
|
+
for (int substep = 0; substep < SUBSTEPS; substep += 1) {
|
|
772
|
+
for (SBody& b : sb) {
|
|
773
|
+
if (solid(b) && b.invMassVec.y != 0.0) {
|
|
774
|
+
b.v.y = b.v.y + GRAVITY_Y * b.gravityScale * h;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
// (no joints) warm-start apply with the current accumulators.
|
|
778
|
+
for (Constraint& c : constraints) {
|
|
779
|
+
applyImpulseScalar(
|
|
780
|
+
sb, invMass, invInertia, c.a, c.b, c.rA, c.rB,
|
|
781
|
+
c.normal.x * c.accN + (c.t1.x * c.accT1 + c.t2.x * c.accT2),
|
|
782
|
+
c.normal.y * c.accN + (c.t1.y * c.accT1 + c.t2.y * c.accT2),
|
|
783
|
+
c.normal.z * c.accN + (c.t1.z * c.accT1 + c.t2.z * c.accT2),
|
|
784
|
+
false);
|
|
785
|
+
}
|
|
786
|
+
for (int iter = 0; iter < VELOCITY_ITERATIONS; iter += 1) {
|
|
787
|
+
for (Constraint& c : constraints) {
|
|
788
|
+
solveContactSoft(sb, invMass, invInertia, c, h, true);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
for (SBody& b : sb) {
|
|
792
|
+
if (!solid(b)) {
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
b.p = addV(b.p, scaleV(addV(b.v, b.vb), h));
|
|
796
|
+
// hasOrientation=false: no quaternion integration.
|
|
797
|
+
}
|
|
798
|
+
for (Constraint& c : constraints) {
|
|
799
|
+
const SBody& A = sb[c.a];
|
|
800
|
+
const SBody& B = sb[c.b];
|
|
801
|
+
const Vec3 dpA = solid(A) ? scaleV(addV(A.v, A.vb), h) : ZERO_VEC;
|
|
802
|
+
const Vec3 dpB = solid(B) ? scaleV(addV(B.v, B.vb), h) : ZERO_VEC;
|
|
803
|
+
const Vec3 rotA = solid(A) ? scaleV(crossV(addV(A.w, A.wb), c.rA), h) : ZERO_VEC;
|
|
804
|
+
const Vec3 rotB = solid(B) ? scaleV(crossV(addV(B.w, B.wb), c.rB), h) : ZERO_VEC;
|
|
805
|
+
const Vec3 rel = subV(addV(dpB, rotB), addV(dpA, rotA));
|
|
806
|
+
c.penetration -= dotV(rel, c.normal);
|
|
807
|
+
c.rA = addV(c.rA, rotA);
|
|
808
|
+
c.rB = addV(c.rB, rotB);
|
|
809
|
+
}
|
|
810
|
+
for (SBody& b : sb) {
|
|
811
|
+
b.vb = {0.0, 0.0, 0.0};
|
|
812
|
+
b.wb = {0.0, 0.0, 0.0};
|
|
813
|
+
}
|
|
814
|
+
const int relaxIterations = VELOCITY_ITERATIONS * RELAX_ITERATION_SCALE;
|
|
815
|
+
for (int iter = 0; iter < relaxIterations; iter += 1) {
|
|
816
|
+
for (Constraint& c : constraints) {
|
|
817
|
+
solveContactSoft(sb, invMass, invInertia, c, h, false);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
for (Constraint& c : constraints) {
|
|
821
|
+
c.accNStep += c.accN;
|
|
822
|
+
}
|
|
823
|
+
if (debug) {
|
|
824
|
+
debugDump(sb, "substep", substep);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// 5b. restitution pass (combined restitution is always 0 in the slice; the
|
|
829
|
+
// guard short-circuits every constraint, kept verbatim).
|
|
830
|
+
for (Constraint& c : constraints) {
|
|
831
|
+
if (c.restitution <= 0.0 || c.vn0 >= -RESTITUTION_THRESHOLD || c.accN <= 0.0) {
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
const SBody& A = sb[c.a];
|
|
835
|
+
const SBody& B = sb[c.b];
|
|
836
|
+
const double vn = relativeVelocityAlong(A, B, c.rA, c.rB, c.normal.x, c.normal.y, c.normal.z);
|
|
837
|
+
double dLambda = c.massN * (-c.restitution * c.vn0 - vn);
|
|
838
|
+
const double newAcc = jsMax(0.0, c.accN + dLambda);
|
|
839
|
+
dLambda = newAcc - c.accN;
|
|
840
|
+
c.accN = newAcc;
|
|
841
|
+
c.accNStep += dLambda;
|
|
842
|
+
applyImpulseScalar(sb, invMass, invInertia, c.a, c.b, c.rA, c.rB,
|
|
843
|
+
c.normal.x * dLambda, c.normal.y * dLambda, c.normal.z * dLambda, false);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// 6. warm-cache store: not observable through position/velocity readback of
|
|
847
|
+
// a single step -- omitted.
|
|
848
|
+
|
|
849
|
+
// 7. damping + axis-lock zeroing. (lowVelFrames bookkeeping and step 8
|
|
850
|
+
// islandSleep are omitted: from a fresh world lowVelFrames <= 1 < 20, so no
|
|
851
|
+
// body can sleep within this step and v/w are never zeroed by sleep.)
|
|
852
|
+
for (SBody& b : sb) {
|
|
853
|
+
if (!solid(b)) {
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
b.v = scaleV(b.v, b.linearDamping);
|
|
857
|
+
b.w = scaleV(b.w, b.angularDamping);
|
|
858
|
+
b.v = {
|
|
859
|
+
b.invMassVec.x == 0.0 ? 0.0 : b.v.x,
|
|
860
|
+
b.invMassVec.y == 0.0 ? 0.0 : b.v.y,
|
|
861
|
+
b.invMassVec.z == 0.0 ? 0.0 : b.v.z,
|
|
862
|
+
};
|
|
863
|
+
b.w = {
|
|
864
|
+
b.angularLock.x == 0.0 ? 0.0 : b.w.x,
|
|
865
|
+
b.angularLock.y == 0.0 ? 0.0 : b.w.y,
|
|
866
|
+
b.angularLock.z == 0.0 ? 0.0 : b.w.z,
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
if (debug) {
|
|
871
|
+
debugDump(sb, "final", -1);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Readback (runRigidSolver): dynamic bodies get quantized position+velocity;
|
|
875
|
+
// statics pass through untouched.
|
|
876
|
+
for (std::size_t i = 0; i < bodies.size(); i += 1) {
|
|
877
|
+
if (!bodies[i].dynamic) {
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
bodies[i].x = quantizeDistance(sb[i].p.x);
|
|
881
|
+
bodies[i].y = quantizeDistance(sb[i].p.y);
|
|
882
|
+
bodies[i].z = quantizeDistance(sb[i].p.z);
|
|
883
|
+
bodies[i].vx = quantizeDistance(sb[i].v.x);
|
|
884
|
+
bodies[i].vy = quantizeDistance(sb[i].v.y);
|
|
885
|
+
bodies[i].vz = quantizeDistance(sb[i].v.z);
|
|
886
|
+
}
|
|
887
|
+
}
|