@aethervtt/shard-physics 0.0.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/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/character.d.ts +62 -0
- package/dist/character.js +509 -0
- package/dist/components.d.ts +155 -0
- package/dist/components.js +340 -0
- package/dist/gravity.d.ts +17 -0
- package/dist/gravity.js +73 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +64 -0
- package/dist/methods.d.ts +2 -0
- package/dist/methods.js +131 -0
- package/dist/plugin.d.ts +13 -0
- package/dist/plugin.js +853 -0
- package/dist/pose.d.ts +41 -0
- package/dist/pose.js +341 -0
- package/dist/world.d.ts +246 -0
- package/dist/world.js +1104 -0
- package/package.json +40 -0
package/dist/pose.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type Entity, type World } from '@aethervtt/shard-core';
|
|
2
|
+
/**
|
|
3
|
+
* A rigid pose plus scale: translation (0..2), rotation quaternion (3..6), scale (7..9). Poses are
|
|
4
|
+
* composed from Transforms, not read from GlobalTransform, so bodies spawned before the first
|
|
5
|
+
* propagation get the right place.
|
|
6
|
+
*/
|
|
7
|
+
export type Pose = Float64Array;
|
|
8
|
+
export declare const createPose: () => Pose;
|
|
9
|
+
export declare function identityPose(out: Pose): Pose;
|
|
10
|
+
/** Rotates (x, y, z) by quaternion q (offset qo) into out[oo..oo+2]. */
|
|
11
|
+
export declare function rotate(out: ArrayLike<number> & {
|
|
12
|
+
[i: number]: number;
|
|
13
|
+
}, oo: number, q: ArrayLike<number>, qo: number, x: number, y: number, z: number): void;
|
|
14
|
+
/** out = a * b (quaternions at offsets). `out` may alias either input. */
|
|
15
|
+
export declare function mulQuat(out: {
|
|
16
|
+
[i: number]: number;
|
|
17
|
+
}, oo: number, a: ArrayLike<number>, ao: number, b: ArrayLike<number>, bo: number): void;
|
|
18
|
+
/** The entity's parent, read from the ChildOf column (no allocation). */
|
|
19
|
+
export declare function parentOf(world: World, entity: Entity): Entity | undefined;
|
|
20
|
+
/** Whether an entity is a Grid (spec 0040). */
|
|
21
|
+
export declare function isGrid(world: World, entity: Entity): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Pose of an entity in the origin frame (where Rapier simulates), from its own and its ancestors'
|
|
24
|
+
* Transforms (up to, not including, `stop` when given: then the pose is relative to `stop`).
|
|
25
|
+
* Ignores shear from non-uniform scale under rotation, like physics shapes do. Under a Grid, the
|
|
26
|
+
* cell and the grid's frame are applied in f64, so the pose is exact however far the grid cell is
|
|
27
|
+
* from the grid's origin.
|
|
28
|
+
*/
|
|
29
|
+
export declare function poseOf(world: World, entity: Entity, out: Pose, stop?: Entity): Pose;
|
|
30
|
+
/**
|
|
31
|
+
* The inverse of a parent's world matrix applied to an origin-frame position and rotation, for
|
|
32
|
+
* writing a body's pose back into its local Transform. Uses the parent's GlobalTransform, except
|
|
33
|
+
* when the parent is a Grid (or there is no parent and the origin is in a grid): then the grid's
|
|
34
|
+
* f64 frame, keeping the translation small by moving `child`'s GridCell when it leaves the cell
|
|
35
|
+
* (the same rule as `transform/recenter`, which then has nothing left to do).
|
|
36
|
+
*/
|
|
37
|
+
export declare function worldToLocal(world: World, parent: Entity | undefined, child: Entity, pos: ArrayLike<number>, rot: ArrayLike<number>, outPos: {
|
|
38
|
+
[i: number]: number;
|
|
39
|
+
}, po: number, outRot: {
|
|
40
|
+
[i: number]: number;
|
|
41
|
+
}, ro: number): void;
|
package/dist/pose.js
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { ChildOf } from "@aethervtt/shard-core";
|
|
2
|
+
import {
|
|
3
|
+
GlobalTransform,
|
|
4
|
+
Grid,
|
|
5
|
+
GridCell,
|
|
6
|
+
GridFramesResource,
|
|
7
|
+
originMatrix64,
|
|
8
|
+
Transform
|
|
9
|
+
} from "@aethervtt/shard-transform";
|
|
10
|
+
const createPose = () => {
|
|
11
|
+
const p = new Float64Array(10);
|
|
12
|
+
p[6] = 1;
|
|
13
|
+
p[7] = 1;
|
|
14
|
+
p[8] = 1;
|
|
15
|
+
p[9] = 1;
|
|
16
|
+
return p;
|
|
17
|
+
};
|
|
18
|
+
function identityPose(out) {
|
|
19
|
+
out.fill(0);
|
|
20
|
+
out[6] = 1;
|
|
21
|
+
out[7] = out[8] = out[9] = 1;
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
function rotate(out, oo, q, qo, x, y, z) {
|
|
25
|
+
const qx = q[qo];
|
|
26
|
+
const qy = q[qo + 1];
|
|
27
|
+
const qz = q[qo + 2];
|
|
28
|
+
const qw = q[qo + 3];
|
|
29
|
+
const tx = 2 * (qy * z - qz * y);
|
|
30
|
+
const ty = 2 * (qz * x - qx * z);
|
|
31
|
+
const tz = 2 * (qx * y - qy * x);
|
|
32
|
+
out[oo] = x + qw * tx + (qy * tz - qz * ty);
|
|
33
|
+
out[oo + 1] = y + qw * ty + (qz * tx - qx * tz);
|
|
34
|
+
out[oo + 2] = z + qw * tz + (qx * ty - qy * tx);
|
|
35
|
+
}
|
|
36
|
+
function mulQuat(out, oo, a, ao, b, bo) {
|
|
37
|
+
const ax = a[ao];
|
|
38
|
+
const ay = a[ao + 1];
|
|
39
|
+
const az = a[ao + 2];
|
|
40
|
+
const aw = a[ao + 3];
|
|
41
|
+
const bx = b[bo];
|
|
42
|
+
const by = b[bo + 1];
|
|
43
|
+
const bz = b[bo + 2];
|
|
44
|
+
const bw = b[bo + 3];
|
|
45
|
+
out[oo] = aw * bx + ax * bw + ay * bz - az * by;
|
|
46
|
+
out[oo + 1] = aw * by - ax * bz + ay * bw + az * bx;
|
|
47
|
+
out[oo + 2] = aw * bz + ax * by - ay * bx + az * bw;
|
|
48
|
+
out[oo + 3] = aw * bw - ax * bx - ay * by - az * bz;
|
|
49
|
+
}
|
|
50
|
+
const chain = [];
|
|
51
|
+
const tmp = new Float64Array(4);
|
|
52
|
+
function composeLocal(out, t, to, r, ro, s, so) {
|
|
53
|
+
rotate(tmp, 0, out, 3, out[7] * t[to], out[8] * t[to + 1], out[9] * t[to + 2]);
|
|
54
|
+
out[0] = out[0] + tmp[0];
|
|
55
|
+
out[1] = out[1] + tmp[1];
|
|
56
|
+
out[2] = out[2] + tmp[2];
|
|
57
|
+
mulQuat(out, 3, out, 3, r, ro);
|
|
58
|
+
out[7] = out[7] * s[so];
|
|
59
|
+
out[8] = out[8] * s[so + 1];
|
|
60
|
+
out[9] = out[9] * s[so + 2];
|
|
61
|
+
}
|
|
62
|
+
function parentOf(world, entity) {
|
|
63
|
+
const table = world.entityTableUnchecked(entity);
|
|
64
|
+
if (!table.has(ChildOf)) return void 0;
|
|
65
|
+
const p = table.column(ChildOf, "parent")[world.entityRowUnchecked(entity)];
|
|
66
|
+
return p >= 0 ? p : void 0;
|
|
67
|
+
}
|
|
68
|
+
function isGrid(world, entity) {
|
|
69
|
+
return world.entityTableUnchecked(entity).has(Grid);
|
|
70
|
+
}
|
|
71
|
+
const frame = { m: new Float64Array(12), rx: 0, ry: 0, rz: 0, cs: 0, identity: true };
|
|
72
|
+
const fq = new Float64Array(4);
|
|
73
|
+
function frameOf(world, grid, out) {
|
|
74
|
+
const m = out.m;
|
|
75
|
+
const frames = world.tryResource(GridFramesResource);
|
|
76
|
+
const slot = frames?.active ? grid === void 0 ? 0 : frames.slotOf.get(grid) : void 0;
|
|
77
|
+
if (slot !== void 0) {
|
|
78
|
+
const a = frames.a;
|
|
79
|
+
const o = slot * 12;
|
|
80
|
+
for (let i = 0; i < 12; i++) m[i] = a[o + i];
|
|
81
|
+
out.rx = frames.ref[slot * 3];
|
|
82
|
+
out.ry = frames.ref[slot * 3 + 1];
|
|
83
|
+
out.rz = frames.ref[slot * 3 + 2];
|
|
84
|
+
out.cs = slot === 0 ? 0 : frames.cellSize[slot];
|
|
85
|
+
} else if (grid === void 0) {
|
|
86
|
+
m.fill(0);
|
|
87
|
+
m[0] = m[5] = m[10] = 1;
|
|
88
|
+
out.rx = out.ry = out.rz = out.cs = 0;
|
|
89
|
+
} else {
|
|
90
|
+
originMatrix64(world, grid, m);
|
|
91
|
+
out.rx = out.ry = out.rz = 0;
|
|
92
|
+
const table = world.entityTableUnchecked(grid);
|
|
93
|
+
out.cs = table.column(Grid, "cellSize")[world.entityRowUnchecked(grid)];
|
|
94
|
+
}
|
|
95
|
+
out.identity = m[0] === 1 && m[1] === 0 && m[2] === 0 && m[4] === 0 && m[5] === 1 && m[6] === 0 && m[8] === 0 && m[9] === 0 && m[10] === 1;
|
|
96
|
+
}
|
|
97
|
+
function frameRotation(m, scale) {
|
|
98
|
+
const sx = Math.sqrt(m[0] * m[0] + m[4] * m[4] + m[8] * m[8]) || 1;
|
|
99
|
+
const sy = Math.sqrt(m[1] * m[1] + m[5] * m[5] + m[9] * m[9]) || 1;
|
|
100
|
+
const sz = Math.sqrt(m[2] * m[2] + m[6] * m[6] + m[10] * m[10]) || 1;
|
|
101
|
+
scale[0] = sx;
|
|
102
|
+
scale[1] = sy;
|
|
103
|
+
scale[2] = sz;
|
|
104
|
+
quatFromBasis(
|
|
105
|
+
fq,
|
|
106
|
+
m[0] / sx,
|
|
107
|
+
m[4] / sx,
|
|
108
|
+
m[8] / sx,
|
|
109
|
+
m[1] / sy,
|
|
110
|
+
m[5] / sy,
|
|
111
|
+
m[9] / sy,
|
|
112
|
+
m[2] / sz,
|
|
113
|
+
m[6] / sz,
|
|
114
|
+
m[10] / sz
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const frameScale = new Float64Array(3);
|
|
118
|
+
function applyFrame(out, f, kx, ky, kz) {
|
|
119
|
+
const m = f.m;
|
|
120
|
+
const px = (kx - f.rx) * f.cs + out[0];
|
|
121
|
+
const py = (ky - f.ry) * f.cs + out[1];
|
|
122
|
+
const pz = (kz - f.rz) * f.cs + out[2];
|
|
123
|
+
out[0] = m[0] * px + m[1] * py + m[2] * pz + m[3];
|
|
124
|
+
out[1] = m[4] * px + m[5] * py + m[6] * pz + m[7];
|
|
125
|
+
out[2] = m[8] * px + m[9] * py + m[10] * pz + m[11];
|
|
126
|
+
if (f.identity) return;
|
|
127
|
+
frameRotation(m, frameScale);
|
|
128
|
+
mulQuat(out, 3, fq, 0, out, 3);
|
|
129
|
+
out[7] = out[7] * frameScale[0];
|
|
130
|
+
out[8] = out[8] * frameScale[1];
|
|
131
|
+
out[9] = out[9] * frameScale[2];
|
|
132
|
+
}
|
|
133
|
+
function poseOf(world, entity, out, stop) {
|
|
134
|
+
chain.length = 0;
|
|
135
|
+
let grid;
|
|
136
|
+
let e = entity;
|
|
137
|
+
while (e !== void 0 && e !== stop) {
|
|
138
|
+
if (stop === void 0 && e !== entity && isGrid(world, e)) {
|
|
139
|
+
grid = e;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
chain.push(e);
|
|
143
|
+
e = parentOf(world, e);
|
|
144
|
+
}
|
|
145
|
+
identityPose(out);
|
|
146
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
147
|
+
const ent = chain[i];
|
|
148
|
+
if (!world.has(ent, Transform)) continue;
|
|
149
|
+
const table2 = world.entityTable(ent);
|
|
150
|
+
const row = world.entityRow(ent);
|
|
151
|
+
composeLocal(
|
|
152
|
+
out,
|
|
153
|
+
table2.column(Transform, "translation"),
|
|
154
|
+
row * 3,
|
|
155
|
+
table2.column(Transform, "rotation"),
|
|
156
|
+
row * 4,
|
|
157
|
+
table2.column(Transform, "scale"),
|
|
158
|
+
row * 3
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
if (stop !== void 0) return out;
|
|
162
|
+
if (grid === void 0) {
|
|
163
|
+
const frames = world.tryResource(GridFramesResource);
|
|
164
|
+
if (frames === void 0 || frames.rootIdentity) return out;
|
|
165
|
+
frameOf(world, void 0, frame);
|
|
166
|
+
applyFrame(out, frame, 0, 0, 0);
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
const top = chain[chain.length - 1];
|
|
170
|
+
const table = world.entityTableUnchecked(top);
|
|
171
|
+
let kx = 0;
|
|
172
|
+
let ky = 0;
|
|
173
|
+
let kz = 0;
|
|
174
|
+
if (table.has(GridCell)) {
|
|
175
|
+
const cell = table.column(GridCell, "cell");
|
|
176
|
+
const o = world.entityRowUnchecked(top) * 3;
|
|
177
|
+
kx = cell[o];
|
|
178
|
+
ky = cell[o + 1];
|
|
179
|
+
kz = cell[o + 2];
|
|
180
|
+
}
|
|
181
|
+
frameOf(world, grid, frame);
|
|
182
|
+
applyFrame(out, frame, kx, ky, kz);
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
function worldToLocal(world, parent, child, pos, rot, outPos, po, outRot, ro) {
|
|
186
|
+
if (parent === void 0 || isGrid(world, parent)) {
|
|
187
|
+
gridToLocal(world, parent, child, pos, rot, outPos, po, outRot, ro);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const table = world.entityTable(parent);
|
|
191
|
+
if (!table.has(GlobalTransform)) {
|
|
192
|
+
outPos[po] = pos[0];
|
|
193
|
+
outPos[po + 1] = pos[1];
|
|
194
|
+
outPos[po + 2] = pos[2];
|
|
195
|
+
outRot[ro] = rot[0];
|
|
196
|
+
outRot[ro + 1] = rot[1];
|
|
197
|
+
outRot[ro + 2] = rot[2];
|
|
198
|
+
outRot[ro + 3] = rot[3];
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const m = table.column(GlobalTransform, "matrix");
|
|
202
|
+
const o = world.entityRow(parent) * 12;
|
|
203
|
+
const dx = pos[0] - m[o + 3];
|
|
204
|
+
const dy = pos[1] - m[o + 7];
|
|
205
|
+
const dz = pos[2] - m[o + 11];
|
|
206
|
+
const a = m[o];
|
|
207
|
+
const b = m[o + 1];
|
|
208
|
+
const c = m[o + 2];
|
|
209
|
+
const d = m[o + 4];
|
|
210
|
+
const e = m[o + 5];
|
|
211
|
+
const f = m[o + 6];
|
|
212
|
+
const g = m[o + 8];
|
|
213
|
+
const h = m[o + 9];
|
|
214
|
+
const i = m[o + 10];
|
|
215
|
+
const A = e * i - f * h;
|
|
216
|
+
const B = -(d * i - f * g);
|
|
217
|
+
const C = d * h - e * g;
|
|
218
|
+
const det = a * A + b * B + c * C || 1;
|
|
219
|
+
const inv = 1 / det;
|
|
220
|
+
outPos[po] = (A * dx + (c * h - b * i) * dy + (b * f - c * e) * dz) * inv;
|
|
221
|
+
outPos[po + 1] = (B * dx + (a * i - c * g) * dy + (c * d - a * f) * dz) * inv;
|
|
222
|
+
outPos[po + 2] = (C * dx + (b * g - a * h) * dy + (a * e - b * d) * dz) * inv;
|
|
223
|
+
const sx = Math.sqrt(a * a + d * d + g * g) || 1;
|
|
224
|
+
const sy = Math.sqrt(b * b + e * e + h * h) || 1;
|
|
225
|
+
const sz = Math.sqrt(c * c + f * f + i * i) || 1;
|
|
226
|
+
quatFromBasis(tmp, a / sx, d / sx, g / sx, b / sy, e / sy, h / sy, c / sz, f / sz, i / sz);
|
|
227
|
+
tmp[0] = -tmp[0];
|
|
228
|
+
tmp[1] = -tmp[1];
|
|
229
|
+
tmp[2] = -tmp[2];
|
|
230
|
+
mulQuat(outRot, ro, tmp, 0, rot, 0);
|
|
231
|
+
}
|
|
232
|
+
function gridToLocal(world, grid, child, pos, rot, outPos, po, outRot, ro) {
|
|
233
|
+
frameOf(world, grid, frame);
|
|
234
|
+
const m = frame.m;
|
|
235
|
+
const dx = pos[0] - m[3];
|
|
236
|
+
const dy = pos[1] - m[7];
|
|
237
|
+
const dz = pos[2] - m[11];
|
|
238
|
+
let lx = dx;
|
|
239
|
+
let ly = dy;
|
|
240
|
+
let lz = dz;
|
|
241
|
+
if (!frame.identity) {
|
|
242
|
+
const a = m[0];
|
|
243
|
+
const b = m[1];
|
|
244
|
+
const c = m[2];
|
|
245
|
+
const d = m[4];
|
|
246
|
+
const e = m[5];
|
|
247
|
+
const f = m[6];
|
|
248
|
+
const g = m[8];
|
|
249
|
+
const h = m[9];
|
|
250
|
+
const i = m[10];
|
|
251
|
+
const A = e * i - f * h;
|
|
252
|
+
const B = -(d * i - f * g);
|
|
253
|
+
const C = d * h - e * g;
|
|
254
|
+
const inv = 1 / (a * A + b * B + c * C || 1);
|
|
255
|
+
lx = (A * dx + (c * h - b * i) * dy + (b * f - c * e) * dz) * inv;
|
|
256
|
+
ly = (B * dx + (a * i - c * g) * dy + (c * d - a * f) * dz) * inv;
|
|
257
|
+
lz = (C * dx + (b * g - a * h) * dy + (a * e - b * d) * dz) * inv;
|
|
258
|
+
}
|
|
259
|
+
const table = world.entityTableUnchecked(child);
|
|
260
|
+
if (grid !== void 0 && table.has(GridCell)) {
|
|
261
|
+
const cell = table.column(GridCell, "cell");
|
|
262
|
+
const row = world.entityRowUnchecked(child);
|
|
263
|
+
const o = row * 3;
|
|
264
|
+
const cs = frame.cs;
|
|
265
|
+
lx -= (cell[o] - frame.rx) * cs;
|
|
266
|
+
ly -= (cell[o + 1] - frame.ry) * cs;
|
|
267
|
+
lz -= (cell[o + 2] - frame.rz) * cs;
|
|
268
|
+
const gt = world.entityTableUnchecked(grid);
|
|
269
|
+
const limit = cs / 2 + gt.column(Grid, "hysteresis")[world.entityRowUnchecked(grid)];
|
|
270
|
+
if (lx > limit || lx < -limit || ly > limit || ly < -limit || lz > limit || lz < -limit) {
|
|
271
|
+
const jx = lx > limit || lx < -limit ? Math.round(lx / cs) : 0;
|
|
272
|
+
const jy = ly > limit || ly < -limit ? Math.round(ly / cs) : 0;
|
|
273
|
+
const jz = lz > limit || lz < -limit ? Math.round(lz / cs) : 0;
|
|
274
|
+
cell[o] = cell[o] + jx;
|
|
275
|
+
cell[o + 1] = cell[o + 1] + jy;
|
|
276
|
+
cell[o + 2] = cell[o + 2] + jz;
|
|
277
|
+
lx -= jx * cs;
|
|
278
|
+
ly -= jy * cs;
|
|
279
|
+
lz -= jz * cs;
|
|
280
|
+
table.markChanged(GridCell, row);
|
|
281
|
+
}
|
|
282
|
+
} else if (grid !== void 0) {
|
|
283
|
+
const cs = frame.cs;
|
|
284
|
+
lx += frame.rx * cs;
|
|
285
|
+
ly += frame.ry * cs;
|
|
286
|
+
lz += frame.rz * cs;
|
|
287
|
+
}
|
|
288
|
+
outPos[po] = lx;
|
|
289
|
+
outPos[po + 1] = ly;
|
|
290
|
+
outPos[po + 2] = lz;
|
|
291
|
+
if (frame.identity) {
|
|
292
|
+
outRot[ro] = rot[0];
|
|
293
|
+
outRot[ro + 1] = rot[1];
|
|
294
|
+
outRot[ro + 2] = rot[2];
|
|
295
|
+
outRot[ro + 3] = rot[3];
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
frameRotation(m, frameScale);
|
|
299
|
+
fq[0] = -fq[0];
|
|
300
|
+
fq[1] = -fq[1];
|
|
301
|
+
fq[2] = -fq[2];
|
|
302
|
+
mulQuat(outRot, ro, fq, 0, rot, 0);
|
|
303
|
+
}
|
|
304
|
+
function quatFromBasis(out, m00, m10, m20, m01, m11, m21, m02, m12, m22) {
|
|
305
|
+
const trace = m00 + m11 + m22;
|
|
306
|
+
if (trace > 0) {
|
|
307
|
+
const s = 0.5 / Math.sqrt(trace + 1);
|
|
308
|
+
out[3] = 0.25 / s;
|
|
309
|
+
out[0] = (m21 - m12) * s;
|
|
310
|
+
out[1] = (m02 - m20) * s;
|
|
311
|
+
out[2] = (m10 - m01) * s;
|
|
312
|
+
} else if (m00 > m11 && m00 > m22) {
|
|
313
|
+
const s = 2 * Math.sqrt(1 + m00 - m11 - m22);
|
|
314
|
+
out[3] = (m21 - m12) / s;
|
|
315
|
+
out[0] = 0.25 * s;
|
|
316
|
+
out[1] = (m01 + m10) / s;
|
|
317
|
+
out[2] = (m02 + m20) / s;
|
|
318
|
+
} else if (m11 > m22) {
|
|
319
|
+
const s = 2 * Math.sqrt(1 + m11 - m00 - m22);
|
|
320
|
+
out[3] = (m02 - m20) / s;
|
|
321
|
+
out[0] = (m01 + m10) / s;
|
|
322
|
+
out[1] = 0.25 * s;
|
|
323
|
+
out[2] = (m12 + m21) / s;
|
|
324
|
+
} else {
|
|
325
|
+
const s = 2 * Math.sqrt(1 + m22 - m00 - m11);
|
|
326
|
+
out[3] = (m10 - m01) / s;
|
|
327
|
+
out[0] = (m02 + m20) / s;
|
|
328
|
+
out[1] = (m12 + m21) / s;
|
|
329
|
+
out[2] = 0.25 * s;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
export {
|
|
333
|
+
createPose,
|
|
334
|
+
identityPose,
|
|
335
|
+
isGrid,
|
|
336
|
+
mulQuat,
|
|
337
|
+
parentOf,
|
|
338
|
+
poseOf,
|
|
339
|
+
rotate,
|
|
340
|
+
worldToLocal
|
|
341
|
+
};
|
package/dist/world.d.ts
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { type Entity, ShardError, type World } from '@aethervtt/shard-core';
|
|
2
|
+
import type { Mesh } from '@aethervtt/shard-mesh';
|
|
3
|
+
import type RAPIER from '@dimforge/rapier3d-compat';
|
|
4
|
+
import { type CollisionEventData, type PhysicsConfigValue } from './components.js';
|
|
5
|
+
export type Rapier = typeof RAPIER;
|
|
6
|
+
type RWorld = InstanceType<Rapier['World']>;
|
|
7
|
+
type RBody = InstanceType<Rapier['RigidBody']>;
|
|
8
|
+
type RCollider = InstanceType<Rapier['Collider']>;
|
|
9
|
+
type RJoint = InstanceType<Rapier['ImpulseJoint']>;
|
|
10
|
+
/** The physics world, once Rapier has loaded (the plugin's `ready` step). */
|
|
11
|
+
export declare const Physics: import("@aethervtt/shard-core").ResourceDef<PhysicsWorld>;
|
|
12
|
+
/** Loads and initializes Rapier's WASM for a dimension, once per process. */
|
|
13
|
+
export declare function loadRapier(dim: 2 | 3): Promise<Rapier>;
|
|
14
|
+
/** A body the physics world owns, by entity. */
|
|
15
|
+
export interface BodyRecord {
|
|
16
|
+
entity: Entity;
|
|
17
|
+
handle: number;
|
|
18
|
+
body: RBody;
|
|
19
|
+
kind: number;
|
|
20
|
+
/** Dense index into the interpolation buffers. */
|
|
21
|
+
slot: number;
|
|
22
|
+
/** Position in `PhysicsWorld.list`. */
|
|
23
|
+
index: number;
|
|
24
|
+
/** Out of PhysicsRange: disabled in Rapier, the entity has PhysicsParked. */
|
|
25
|
+
parked: boolean;
|
|
26
|
+
}
|
|
27
|
+
/** A character's Rapier controller and the body and capsule it moves. */
|
|
28
|
+
export interface CharacterRecord {
|
|
29
|
+
entity: Entity;
|
|
30
|
+
body: BodyRecord;
|
|
31
|
+
collider: RCollider;
|
|
32
|
+
kcc: InstanceType<Rapier['KinematicCharacterController']>;
|
|
33
|
+
/** Whether snap-to-ground is on right now (off while moving up, so jumps leave the ground). */
|
|
34
|
+
snapping: boolean;
|
|
35
|
+
snapDistance: number;
|
|
36
|
+
/** Cosine of maxSlope: ground normals at least this aligned with up are walkable. */
|
|
37
|
+
cosMaxSlope: number;
|
|
38
|
+
/** From the capsule's center to its bottom, m. */
|
|
39
|
+
bottom: number;
|
|
40
|
+
}
|
|
41
|
+
/** The gap Rapier keeps between a character and what it touches, m. */
|
|
42
|
+
export declare const CHARACTER_OFFSET = 0.01;
|
|
43
|
+
export interface RayHit {
|
|
44
|
+
/** The collider's entity. */
|
|
45
|
+
entity: Entity;
|
|
46
|
+
/** The body it belongs to, or -1 for a fixed collider without a body. */
|
|
47
|
+
body: Entity;
|
|
48
|
+
distance: number;
|
|
49
|
+
point: Float32Array;
|
|
50
|
+
normal: Float32Array;
|
|
51
|
+
}
|
|
52
|
+
export declare const createRayHit: () => RayHit;
|
|
53
|
+
export interface QueryOptions {
|
|
54
|
+
/** Longest ray or cast, m (default: no limit). */
|
|
55
|
+
maxDistance?: number;
|
|
56
|
+
/** Only colliders in these layers (bitmask, default all). */
|
|
57
|
+
mask?: number;
|
|
58
|
+
/** The query's own layers, for colliders whose mask filters queries (default all). */
|
|
59
|
+
layers?: number;
|
|
60
|
+
/** Skip this entity's colliders (and its body's). */
|
|
61
|
+
exclude?: Entity;
|
|
62
|
+
/** A ray starting inside a shape hits at distance 0 (default true). */
|
|
63
|
+
solid?: boolean;
|
|
64
|
+
/** Include sensors (default false). */
|
|
65
|
+
sensors?: boolean;
|
|
66
|
+
}
|
|
67
|
+
export interface QueryShape {
|
|
68
|
+
shape: 'ball' | 'cuboid' | 'capsule';
|
|
69
|
+
radius?: number;
|
|
70
|
+
halfExtents?: ArrayLike<number>;
|
|
71
|
+
halfHeight?: number;
|
|
72
|
+
}
|
|
73
|
+
export interface PhysicsStats {
|
|
74
|
+
bodies: Record<string, number>;
|
|
75
|
+
sleeping: number;
|
|
76
|
+
colliders: Record<string, number>;
|
|
77
|
+
joints: number;
|
|
78
|
+
/** Character controllers (not counted in bodies or colliders). */
|
|
79
|
+
characters: number;
|
|
80
|
+
contacts: number;
|
|
81
|
+
stepMs: number;
|
|
82
|
+
steps: number;
|
|
83
|
+
pending: number;
|
|
84
|
+
/** Bodies out of PhysicsRange, set aside until the origin comes near (spec 0040). */
|
|
85
|
+
parked: number;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The Rapier world for one app, plus the maps between Rapier handles and entities. Systems in
|
|
89
|
+
* `plugin.ts` keep it in sync with the components; queries read it.
|
|
90
|
+
*/
|
|
91
|
+
export declare class PhysicsWorld {
|
|
92
|
+
readonly R: Rapier;
|
|
93
|
+
readonly dim: 2 | 3;
|
|
94
|
+
readonly raw: RWorld;
|
|
95
|
+
readonly events: InstanceType<Rapier['EventQueue']>;
|
|
96
|
+
readonly bodies: Map<number, BodyRecord>;
|
|
97
|
+
/** Every body record, for loops that shouldn't allocate an iterator. */
|
|
98
|
+
readonly list: BodyRecord[];
|
|
99
|
+
readonly bodyByHandle: Map<number, BodyRecord>;
|
|
100
|
+
readonly colliders: Map<number, RAPIER.Collider>;
|
|
101
|
+
readonly colliderEntity: Map<number, number>;
|
|
102
|
+
/** Collider entity → the body entity it's attached to (absent: fixed). */
|
|
103
|
+
readonly colliderOwner: Map<number, number>;
|
|
104
|
+
/** Shape kind per collider entity, for describe and the overlay. */
|
|
105
|
+
readonly colliderShape: Map<number, "ball" | "capsule" | "cone" | "convex" | "cuboid" | "cylinder" | "heightfield" | "polyline" | "segment" | "trimesh">;
|
|
106
|
+
readonly joints: Map<number, {
|
|
107
|
+
joint: RJoint;
|
|
108
|
+
a: Entity;
|
|
109
|
+
b: Entity;
|
|
110
|
+
}>;
|
|
111
|
+
/** Character controllers by entity; each also has a body record and a capsule collider. */
|
|
112
|
+
readonly characters: Map<number, CharacterRecord>;
|
|
113
|
+
/** Colliders to (re)build at the next sync, e.g. after their body changed. */
|
|
114
|
+
readonly dirtyColliders: Set<number>;
|
|
115
|
+
readonly dirtyJoints: Set<number>;
|
|
116
|
+
readonly dirtyBodies: Set<number>;
|
|
117
|
+
/** Collider entities waiting for their mesh to load. */
|
|
118
|
+
readonly pendingMesh: Set<number>;
|
|
119
|
+
/** Mesh colliders and the mesh version they were built from, to rebuild on hot reload. */
|
|
120
|
+
readonly meshColliders: Map<number, {
|
|
121
|
+
mesh: Mesh;
|
|
122
|
+
version: number;
|
|
123
|
+
}>;
|
|
124
|
+
/** Last step's poses (slot × 7: translation, rotation), and the ones before, for interpolation. */
|
|
125
|
+
prev: Float64Array<ArrayBuffer>;
|
|
126
|
+
curr: Float64Array<ArrayBuffer>;
|
|
127
|
+
private freeSlots;
|
|
128
|
+
private nextSlot;
|
|
129
|
+
/** Ticks: when sync-out last wrote Transforms, and when interpolation did. */
|
|
130
|
+
writeTick: number;
|
|
131
|
+
interpTick: number;
|
|
132
|
+
/** Whether the world had grids at the last sync (spec 0040), so a change resyncs every body. */
|
|
133
|
+
gridsActive: boolean;
|
|
134
|
+
/** Bodies parked out of PhysicsRange. */
|
|
135
|
+
parkedCount: number;
|
|
136
|
+
stepMs: number;
|
|
137
|
+
steps: number;
|
|
138
|
+
readonly config: PhysicsConfigValue;
|
|
139
|
+
private readonly scratchPose;
|
|
140
|
+
private readonly scratchPose2;
|
|
141
|
+
readonly v3: {
|
|
142
|
+
x: number;
|
|
143
|
+
y: number;
|
|
144
|
+
z: number;
|
|
145
|
+
};
|
|
146
|
+
readonly q4: {
|
|
147
|
+
x: number;
|
|
148
|
+
y: number;
|
|
149
|
+
z: number;
|
|
150
|
+
w: number;
|
|
151
|
+
};
|
|
152
|
+
readonly rawVec: unknown | undefined;
|
|
153
|
+
private readonly ray;
|
|
154
|
+
private logged;
|
|
155
|
+
constructor(R: Rapier, dim: 2 | 3, config: PhysicsConfigValue);
|
|
156
|
+
gravityVector(): {
|
|
157
|
+
x: number;
|
|
158
|
+
y: number;
|
|
159
|
+
z: number;
|
|
160
|
+
};
|
|
161
|
+
/** Writes a vector into the reused {x, y, z} object (Rapier's 2D API ignores z). */
|
|
162
|
+
vec(x: number, y: number, z: number): {
|
|
163
|
+
x: number;
|
|
164
|
+
y: number;
|
|
165
|
+
z: number;
|
|
166
|
+
};
|
|
167
|
+
/** A rotation for Rapier from a quaternion at offset o (in 2D, its angle around Z). */
|
|
168
|
+
rotation(q: ArrayLike<number>, o?: number): RAPIER.Rotation;
|
|
169
|
+
/** Logs a problem once per entity and code, so a bad collider doesn't flood the log. */
|
|
170
|
+
report(world: World, entity: Entity, err: ShardError): void;
|
|
171
|
+
/** Slots the interpolation buffers hold. */
|
|
172
|
+
get slotCapacity(): number;
|
|
173
|
+
private allocSlot;
|
|
174
|
+
createBody(world: World, entity: Entity): void;
|
|
175
|
+
private addBody;
|
|
176
|
+
removeBody(world: World, entity: Entity): void;
|
|
177
|
+
/** Copies a body's current pose into `curr` at its slot. */
|
|
178
|
+
storePose(record: BodyRecord): void;
|
|
179
|
+
/** Teleports a body (or sets a kinematic target) to the entity's current world pose. */
|
|
180
|
+
moveBody(world: World, record: BodyRecord, wake?: boolean): void;
|
|
181
|
+
setVelocity(world: World, record: BodyRecord): void;
|
|
182
|
+
/**
|
|
183
|
+
* The floating origin moved: `offset` (m) takes a position in the old origin frame to the new
|
|
184
|
+
* one. Moves every body and free collider by it without waking them (velocities and contacts
|
|
185
|
+
* are relative, so they don't change), shifts the interpolation buffers, and refreshes the
|
|
186
|
+
* broad phase with a zero-length step so queries see the new places before the next step.
|
|
187
|
+
* O(bodies), once per cell crossing.
|
|
188
|
+
*/
|
|
189
|
+
shiftOrigin(ox: number, oy: number, oz: number, onCollision: (e: CollisionEventData) => void): void;
|
|
190
|
+
/**
|
|
191
|
+
* The frame Rapier simulates in changed without an OriginShift (grids appeared or went away):
|
|
192
|
+
* puts every body and free collider back where its Transform says.
|
|
193
|
+
*/
|
|
194
|
+
resync(world: World): void;
|
|
195
|
+
/**
|
|
196
|
+
* Parks bodies farther than `radius` from the origin and brings parked ones back within
|
|
197
|
+
* 0.95 × radius. Active bodies are measured by their last simulated pose, parked ones by their
|
|
198
|
+
* GlobalTransform. Allocation-free unless a body changes state.
|
|
199
|
+
*/
|
|
200
|
+
updateParking(world: World, radius: number): void;
|
|
201
|
+
/** Takes a body out of the simulation, storing its velocity on PhysicsParked. */
|
|
202
|
+
park(world: World, record: BodyRecord): void;
|
|
203
|
+
/** Puts a parked body back where its Transform says, moving with its stored velocity. */
|
|
204
|
+
unpark(world: World, record: BodyRecord): void;
|
|
205
|
+
/** Grids went away: nothing is out of range any more. */
|
|
206
|
+
unparkAll(world: World): void;
|
|
207
|
+
/**
|
|
208
|
+
* (Re)builds a character's kinematic body, capsule collider, and Rapier controller from its
|
|
209
|
+
* CharacterController. The capsule is centered on the entity, its axis along the entity's +Y.
|
|
210
|
+
*/
|
|
211
|
+
createCharacter(world: World, entity: Entity): void;
|
|
212
|
+
private markSubtreeColliders;
|
|
213
|
+
/** Marks colliders and bodies under an entity whose place in the hierarchy changed. */
|
|
214
|
+
reparented(world: World, entity: Entity): void;
|
|
215
|
+
private forgetCollider;
|
|
216
|
+
removeCollider(entity: Entity): void;
|
|
217
|
+
/** The nearest ancestor-or-self with a body. */
|
|
218
|
+
ownerOf(world: World, entity: Entity): Entity | undefined;
|
|
219
|
+
createCollider(world: World, entity: Entity): void;
|
|
220
|
+
private meshFor;
|
|
221
|
+
/** Scales every collider's density so the body weighs exactly its Mass. */
|
|
222
|
+
fixMass(world: World, owner: Entity): void;
|
|
223
|
+
private shapeDesc;
|
|
224
|
+
/** Shape points as a flat array for the current dimension, scaled. */
|
|
225
|
+
private points;
|
|
226
|
+
removeJoint(entity: Entity): void;
|
|
227
|
+
createJoint(world: World, entity: Entity): void;
|
|
228
|
+
step(dt: number, onCollision: (e: CollisionEventData) => void, onForce: (a: Entity, b: Entity, f: number) => void): void;
|
|
229
|
+
private filter;
|
|
230
|
+
/** The nearest hit along a ray, written into `out`. Direction needn't be normalized. */
|
|
231
|
+
raycast(origin: ArrayLike<number>, direction: ArrayLike<number>, options: QueryOptions | undefined, out: RayHit): boolean;
|
|
232
|
+
/** Every hit along a ray, nearest first. Cold path. */
|
|
233
|
+
raycastAll(origin: ArrayLike<number>, direction: ArrayLike<number>, options?: QueryOptions): RayHit[];
|
|
234
|
+
private queryShape;
|
|
235
|
+
/** Sweeps a shape along `velocity` × maxDistance; the first hit's entity and distance. */
|
|
236
|
+
shapeCast(shape: QueryShape, position: ArrayLike<number>, rotation: ArrayLike<number>, direction: ArrayLike<number>, options: QueryOptions | undefined, out: RayHit): boolean;
|
|
237
|
+
/** Calls `visit` for each collider entity containing the point; return false to stop. */
|
|
238
|
+
overlapPoint(point: ArrayLike<number>, options: QueryOptions | undefined, visit: (entity: Entity) => boolean | undefined): void;
|
|
239
|
+
/** Calls `visit` for each collider entity overlapping the shape; return false to stop. */
|
|
240
|
+
overlapShape(shape: QueryShape, position: ArrayLike<number>, rotation: ArrayLike<number>, options: QueryOptions | undefined, visit: (entity: Entity) => boolean | undefined): void;
|
|
241
|
+
describe(): PhysicsStats;
|
|
242
|
+
free(): void;
|
|
243
|
+
}
|
|
244
|
+
/** Angle around Z of a quaternion (2D rotation). */
|
|
245
|
+
export declare function quatToAngle(q: ArrayLike<number>, o?: number): number;
|
|
246
|
+
export {};
|