pelican-engine 0.0.0-stage → 0.3.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/AGENTS.md +147 -0
- package/LICENSE +22 -0
- package/README.md +129 -2
- package/distribution.json +197 -0
- package/docs/AI-WORKFLOW.md +517 -0
- package/docs/ARCHITECTURE.md +268 -0
- package/docs/CAPABILITIES.md +264 -0
- package/docs/DECISIONS.md +107 -0
- package/docs/M3-USAGE.md +192 -0
- package/docs/PELICAN-MIGRATION.md +38 -0
- package/docs/RENDER-BACKENDS.md +50 -0
- package/docs/ROADMAP.md +239 -0
- package/docs/THIRD-PARTY-NOTICES.md +151 -0
- package/docs/api/REFERENCE.md +1435 -0
- package/docs/api/index.json +1139 -0
- package/docs/npm/DISTRIBUTION.md +130 -0
- package/docs/npm/GAME-AUTHOR.md +147 -0
- package/engine.manifest.json +15 -0
- package/lib/assets/audio.js +67 -0
- package/lib/assets/errors.js +36 -0
- package/lib/assets/glb.js +277 -0
- package/lib/assets/index.js +53 -0
- package/lib/assets/jpeg.js +199 -0
- package/lib/assets/model.js +558 -0
- package/lib/assets/optimize.js +95 -0
- package/lib/assets/texture.js +187 -0
- package/lib/compiler/distribution.js +17 -0
- package/lib/compiler/extract.js +425 -0
- package/lib/compiler/graph.js +366 -0
- package/lib/compiler/index.js +574 -0
- package/lib/compiler/normalize.js +1194 -0
- package/lib/compiler/source-map.js +25 -0
- package/lib/compiler/types.js +2 -0
- package/lib/compiler/version.js +93 -0
- package/lib/experiments/simulation/index.js +2 -0
- package/lib/experiments/simulation/math.js +41 -0
- package/lib/experiments/simulation/probe.js +215 -0
- package/lib/experiments/simulation/scenes.js +192 -0
- package/lib/experiments/simulation/solver.js +346 -0
- package/lib/experiments/simulation/types.js +1 -0
- package/lib/experiments/world/adapters.js +30 -0
- package/lib/experiments/world/geometry.js +180 -0
- package/lib/experiments/world/index.js +2 -0
- package/lib/experiments/world/probe.js +288 -0
- package/lib/experiments/world/types.js +1 -0
- package/lib/experiments/world/world.js +599 -0
- package/lib/navigation/index.js +157 -0
- package/lib/physics/index.js +909 -0
- package/lib/protocol/index.js +92 -0
- package/lib/renderer/acceleration.js +609 -0
- package/lib/renderer/browser.js +2015 -0
- package/lib/renderer/bsdf.js +260 -0
- package/lib/renderer/checkpoint.js +88 -0
- package/lib/renderer/compute-executor.js +1034 -0
- package/lib/renderer/compute-texture-assets.js +49 -0
- package/lib/renderer/compute-view-contract.js +1 -0
- package/lib/renderer/denoiser.js +361 -0
- package/lib/renderer/dielectric.js +360 -0
- package/lib/renderer/embedded-image-pixels.js +368 -0
- package/lib/renderer/emissive-lights.js +67 -0
- package/lib/renderer/environment-importance.js +55 -0
- package/lib/renderer/environment.js +346 -0
- package/lib/renderer/gameplay.js +464 -0
- package/lib/renderer/geometry.js +103 -0
- package/lib/renderer/graph-expressions.js +485 -0
- package/lib/renderer/graph-tsl.js +141 -0
- package/lib/renderer/graph.js +214 -0
- package/lib/renderer/managed-texture-sampling.js +29 -0
- package/lib/renderer/material-graphs.js +297 -0
- package/lib/renderer/medium.js +348 -0
- package/lib/renderer/models.js +1053 -0
- package/lib/renderer/motion-shader.js +40 -0
- package/lib/renderer/path-shader.js +196 -0
- package/lib/renderer/pathtrace.js +324 -0
- package/lib/renderer/postprocess-contract.js +1 -0
- package/lib/renderer/postprocess.js +494 -0
- package/lib/renderer/render-buffers.js +132 -0
- package/lib/renderer/render-textures.js +155 -0
- package/lib/renderer/startup.js +50 -0
- package/lib/renderer/surface-capture.js +199 -0
- package/lib/renderer/surface-gpu.js +270 -0
- package/lib/renderer/surface-motion.js +84 -0
- package/lib/renderer/surface-ray-shader.js +91 -0
- package/lib/renderer/surface-shader.js +88 -0
- package/lib/renderer/surface-tracer.js +1720 -0
- package/lib/renderer/surface.js +1425 -0
- package/lib/renderer/temporal-filter.js +512 -0
- package/lib/renderer/texture-pixels.js +207 -0
- package/lib/renderer/traversal-nodes.js +54 -0
- package/lib/renderer/ui.js +446 -0
- package/lib/renderer/vertex-bounds.js +127 -0
- package/lib/renderer/vertex-contract.js +1 -0
- package/lib/renderer/vertex-graphs.js +197 -0
- package/lib/renderer/volume-shader.js +50 -0
- package/lib/runtime/compute-contract.js +1 -0
- package/lib/runtime/compute-service.js +674 -0
- package/lib/runtime/contracts.js +1 -0
- package/lib/runtime/error.js +37 -0
- package/lib/runtime/events.js +189 -0
- package/lib/runtime/game-services.js +772 -0
- package/lib/runtime/host.js +839 -0
- package/lib/runtime/material-parameters.js +145 -0
- package/lib/runtime/plugins.js +1 -0
- package/lib/runtime/postprocess-contract.js +1 -0
- package/lib/runtime/postprocess.js +189 -0
- package/lib/runtime/prefabs.js +13 -0
- package/lib/runtime/random.js +88 -0
- package/lib/runtime/render-buffer-contract.js +1 -0
- package/lib/runtime/render-buffers.js +243 -0
- package/lib/runtime/render-capabilities.js +17 -0
- package/lib/runtime/render-settings.js +249 -0
- package/lib/runtime/render-texture-contract.js +1 -0
- package/lib/runtime/render-textures.js +271 -0
- package/lib/runtime/resources.js +648 -0
- package/lib/runtime/scheduler.js +765 -0
- package/lib/runtime/storage.js +169 -0
- package/lib/runtime/world.js +594 -0
- package/lib/schema/index.js +24 -0
- package/lib/sdk/graph-values.js +16 -0
- package/lib/sdk/index.d.ts +800 -0
- package/lib/sdk/index.js +82 -0
- package/lib/tools/api-index.js +113 -0
- package/lib/tools/browser-session.js +89 -0
- package/lib/tools/cli.js +513 -0
- package/lib/tools/compress-dist.js +59 -0
- package/lib/tools/control-client.js +153 -0
- package/lib/tools/control-server.js +633 -0
- package/lib/tools/diagnostic-code.js +3 -0
- package/lib/tools/doctor.js +496 -0
- package/lib/tools/npm/browser-cli.js +6 -0
- package/lib/tools/npm/entry.js +40 -0
- package/lib/tools/npm/layout.js +195 -0
- package/lib/tools/setup.js +153 -0
- package/licenses/@dimforge/rapier2d-compat/LICENSE +201 -0
- package/licenses/@dimforge/rapier3d-compat/LICENSE +201 -0
- package/licenses/@gltf-transform/core/LICENSE.md +21 -0
- package/licenses/@gltf-transform/extensions/LICENSE.md +21 -0
- package/licenses/@gltf-transform/functions/LICENSE.md +21 -0
- package/licenses/@img/colour/LICENSE.md +82 -0
- package/licenses/@img/sharp-win32-x64/LICENSE +191 -0
- package/licenses/@oxc-project/types/LICENSE +22 -0
- package/licenses/@rolldown/pluginutils/LICENSE +21 -0
- package/licenses/@tweenjs/tween.js/LICENSE +23 -0
- package/licenses/@types/ndarray/LICENSE +21 -0
- package/licenses/@types/node/LICENSE +21 -0
- package/licenses/@types/stats.js/LICENSE +21 -0
- package/licenses/@types/three/@dimforge/rapier3d-compat/LICENSE +201 -0
- package/licenses/@types/three/LICENSE +21 -0
- package/licenses/@types/webxr/LICENSE +21 -0
- package/licenses/@webgpu/types/LICENSE +26 -0
- package/licenses/THIRD-PARTY-NOTICES.md +139 -0
- package/licenses/cwise-compiler/LICENSE +22 -0
- package/licenses/detect-libc/LICENSE +201 -0
- package/licenses/esbuild/LICENSE.md +21 -0
- package/licenses/fast-png/LICENSE +21 -0
- package/licenses/fdir/LICENSE +7 -0
- package/licenses/fflate/LICENSE +21 -0
- package/licenses/iobuffer/LICENSE +22 -0
- package/licenses/iota-array/LICENSE +22 -0
- package/licenses/is-buffer/LICENSE +21 -0
- package/licenses/ktx-parse/LICENSE +21 -0
- package/licenses/lightningcss/LICENSE +373 -0
- package/licenses/lightningcss-win32-x64-msvc/LICENSE +373 -0
- package/licenses/meshoptimizer/LICENSE.md +21 -0
- package/licenses/nanoid/LICENSE +20 -0
- package/licenses/ndarray/LICENSE +22 -0
- package/licenses/ndarray-lanczos/LICENSE +21 -0
- package/licenses/ndarray-ops/LICENSE +22 -0
- package/licenses/ndarray-pixels/LICENSE +21 -0
- package/licenses/ngraph.path/LICENSE +21 -0
- package/licenses/picocolors/LICENSE +15 -0
- package/licenses/picomatch/LICENSE +21 -0
- package/licenses/playwright/LICENSE +202 -0
- package/licenses/playwright/NOTICE +5 -0
- package/licenses/playwright-core/LICENSE +202 -0
- package/licenses/playwright-core/NOTICE +5 -0
- package/licenses/postcss/LICENSE +20 -0
- package/licenses/prettier/LICENSE +7 -0
- package/licenses/property-graph/LICENSE +21 -0
- package/licenses/rolldown/LICENSE +25 -0
- package/licenses/semver/LICENSE +15 -0
- package/licenses/sharp/LICENSE +191 -0
- package/licenses/source-map-js/LICENSE +28 -0
- package/licenses/three/LICENSE +21 -0
- package/licenses/tinyglobby/LICENSE +21 -0
- package/licenses/tsx/LICENSE +21 -0
- package/licenses/typescript/LICENSE.txt +55 -0
- package/licenses/undici-types/LICENSE +21 -0
- package/licenses/uniq/LICENSE +22 -0
- package/licenses/vite/LICENSE.md +2084 -0
- package/package.json +49 -4
- package/web/index.html +34 -0
|
@@ -0,0 +1,909 @@
|
|
|
1
|
+
import RAPIER from "@dimforge/rapier3d-compat";
|
|
2
|
+
import { cloneSerializable } from "../runtime/random.js";
|
|
3
|
+
import { resolveEntityDefinition } from "../runtime/prefabs.js";
|
|
4
|
+
const pluginId = "$engine.physics";
|
|
5
|
+
const limit = 100_000;
|
|
6
|
+
function fail(code, message) {
|
|
7
|
+
throw new Error(code + ": " + message);
|
|
8
|
+
}
|
|
9
|
+
function vec(v, name) {
|
|
10
|
+
if (!Array.isArray(v) ||
|
|
11
|
+
v.length !== 3 ||
|
|
12
|
+
![0, 1, 2].every((i) => typeof v[i] === "number" &&
|
|
13
|
+
Number.isFinite(v[i]) &&
|
|
14
|
+
Math.abs(v[i]) <= 1e12))
|
|
15
|
+
fail("TC_PHYSICS_VECTOR", name + " must contain three finite numbers with magnitude <= 1e12.");
|
|
16
|
+
return { x: v[0], y: v[1], z: v[2] };
|
|
17
|
+
}
|
|
18
|
+
function finite(n, name, min = 0, max = 1e12) {
|
|
19
|
+
if (typeof n !== "number" || !Number.isFinite(n) || n < min || n > max)
|
|
20
|
+
fail("TC_PHYSICS_DEFINITION", name + " is outside [" + min + ", " + max + "].");
|
|
21
|
+
}
|
|
22
|
+
function tuple(v) {
|
|
23
|
+
return [v.x === 0 ? 0 : v.x, v.y === 0 ? 0 : v.y, v.z === 0 ? 0 : v.z];
|
|
24
|
+
}
|
|
25
|
+
function rotation(v) {
|
|
26
|
+
const [x, y, z] = v.map((n) => n / 2), cx = Math.cos(x), sx = Math.sin(x), cy = Math.cos(y), sy = Math.sin(y), cz = Math.cos(z), sz = Math.sin(z);
|
|
27
|
+
return {
|
|
28
|
+
x: sx * cy * cz + cx * sy * sz,
|
|
29
|
+
y: cx * sy * cz - sx * cy * sz,
|
|
30
|
+
z: cx * cy * sz + sx * sy * cz,
|
|
31
|
+
w: cx * cy * cz - sx * sy * sz,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
// XYZ Euler order, matching the renderer's Object3D transforms.
|
|
35
|
+
function euler(q) {
|
|
36
|
+
const { x, y, z, w } = q, m13 = 2 * (x * z + w * y), m23 = 2 * (y * z - w * x), m33 = 1 - 2 * (x * x + y * y), m12 = 2 * (x * y - w * z), m11 = 1 - 2 * (y * y + z * z);
|
|
37
|
+
const ry = Math.asin(Math.max(-1, Math.min(1, m13)));
|
|
38
|
+
const angles = Math.abs(m13) < 0.9999999
|
|
39
|
+
? [Math.atan2(-m23, m33), ry, Math.atan2(-m12, m11)]
|
|
40
|
+
: [Math.atan2(2 * (x * y + w * z), 1 - 2 * (x * x + z * z)), ry, 0];
|
|
41
|
+
return angles.map((n) => (n === 0 ? 0 : n));
|
|
42
|
+
}
|
|
43
|
+
function same(a, b) {
|
|
44
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
45
|
+
}
|
|
46
|
+
function validateBody(d) {
|
|
47
|
+
const c = d.collider;
|
|
48
|
+
if (!["fixed", "dynamic", "kinematic"].includes(d.type) ||
|
|
49
|
+
!c ||
|
|
50
|
+
!["box", "sphere", "capsule"].includes(c.shape))
|
|
51
|
+
fail("TC_PHYSICS_DEFINITION", "Invalid body or collider definition.");
|
|
52
|
+
if (c.shape === "box") {
|
|
53
|
+
const s = vec(c.size, "size");
|
|
54
|
+
if (Math.min(s.x, s.y, s.z) <= 0)
|
|
55
|
+
fail("TC_PHYSICS_DEFINITION", "Box extents must be positive.");
|
|
56
|
+
}
|
|
57
|
+
else
|
|
58
|
+
finite(c.radius, "radius", 1e-6);
|
|
59
|
+
if (c.shape === "capsule")
|
|
60
|
+
finite(c.halfHeight, "halfHeight");
|
|
61
|
+
if (c.offset)
|
|
62
|
+
vec(c.offset, "offset");
|
|
63
|
+
if (d.mass !== undefined)
|
|
64
|
+
finite(d.mass, "mass", 1e-6);
|
|
65
|
+
if (d.friction !== undefined)
|
|
66
|
+
finite(d.friction, "friction");
|
|
67
|
+
if (d.restitution !== undefined)
|
|
68
|
+
finite(d.restitution, "restitution", 0, 1);
|
|
69
|
+
if (d.trigger !== undefined && typeof d.trigger !== "boolean")
|
|
70
|
+
fail("TC_PHYSICS_DEFINITION", "trigger must be boolean.");
|
|
71
|
+
}
|
|
72
|
+
function validateCharacter(d) {
|
|
73
|
+
finite(d.radius, "radius", 1e-6);
|
|
74
|
+
finite(d.height, "height", 2 * d.radius);
|
|
75
|
+
finite(d.speed, "speed");
|
|
76
|
+
if (d.jumpSpeed !== undefined)
|
|
77
|
+
finite(d.jumpSpeed, "jumpSpeed");
|
|
78
|
+
if (d.gravity !== undefined)
|
|
79
|
+
finite(d.gravity, "gravity");
|
|
80
|
+
}
|
|
81
|
+
function checkTransform(t, e) {
|
|
82
|
+
if (!t)
|
|
83
|
+
fail("TC_PHYSICS_TRANSFORM", e.id + " requires a transform.");
|
|
84
|
+
vec(t.position, e.id + ".position");
|
|
85
|
+
vec(t.rotation, e.id + ".rotation");
|
|
86
|
+
const s = vec(t.scale, e.id + ".scale");
|
|
87
|
+
if (Math.min(s.x, s.y, s.z) < 1e-6)
|
|
88
|
+
fail("TC_PHYSICS_TRANSFORM", e.id + " requires positive scale >= 1e-6.");
|
|
89
|
+
if (e.character &&
|
|
90
|
+
(!same(t.scale, [1, 1, 1]) || t.rotation[0] !== 0 || t.rotation[2] !== 0))
|
|
91
|
+
fail("TC_PHYSICS_TRANSFORM", e.id +
|
|
92
|
+
" character supports unit scale and yaw rotation only; remove pitch/roll and scale.");
|
|
93
|
+
if (e.rigidBody &&
|
|
94
|
+
e.rigidBody.collider.shape !== "box" &&
|
|
95
|
+
(s.x !== s.y || s.y !== s.z))
|
|
96
|
+
fail("TC_PHYSICS_TRANSFORM", e.id +
|
|
97
|
+
" sphere/capsule requires uniform scale; use a box for nonuniform scale.");
|
|
98
|
+
}
|
|
99
|
+
function validateDefinition(e) {
|
|
100
|
+
if (e.rigidBody && e.character)
|
|
101
|
+
fail("TC_PHYSICS_DEFINITION", e.id + " cannot be a body and character.");
|
|
102
|
+
if (e.parent)
|
|
103
|
+
fail("TC_PHYSICS_PARENT_TRANSFORM", e.id +
|
|
104
|
+
" uses unsupported parented physics; place physics entities at the scene root.");
|
|
105
|
+
if (e.rigidBody)
|
|
106
|
+
validateBody(e.rigidBody);
|
|
107
|
+
if (e.character)
|
|
108
|
+
validateCharacter(e.character);
|
|
109
|
+
checkTransform(e.transform, e);
|
|
110
|
+
}
|
|
111
|
+
function shape(e, t) {
|
|
112
|
+
const d = e.rigidBody.collider, s = t.scale;
|
|
113
|
+
if (d.shape === "box")
|
|
114
|
+
return new RAPIER.Cuboid((d.size[0] * s[0]) / 2, (d.size[1] * s[1]) / 2, (d.size[2] * s[2]) / 2);
|
|
115
|
+
if (d.shape === "sphere")
|
|
116
|
+
return new RAPIER.Ball(d.radius * s[0]);
|
|
117
|
+
return new RAPIER.Capsule(d.halfHeight * s[1], d.radius * s[0]);
|
|
118
|
+
}
|
|
119
|
+
let initialization;
|
|
120
|
+
let initialized = false;
|
|
121
|
+
const factory = (context) => createPlugin(context);
|
|
122
|
+
/** Initialize WASM once, including concurrent callers. */
|
|
123
|
+
export async function createPhysicsFactory() {
|
|
124
|
+
return (initialization ??= RAPIER.init().then(() => {
|
|
125
|
+
initialized = true;
|
|
126
|
+
return factory;
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
export const initPhysics = createPhysicsFactory;
|
|
130
|
+
/** Synchronous factory for callers that already awaited createPhysicsFactory. */
|
|
131
|
+
export function createFactory() {
|
|
132
|
+
if (!initialized)
|
|
133
|
+
fail("TC_PHYSICS_INIT", "Await createPhysicsFactory() before creating a synchronous factory.");
|
|
134
|
+
return factory;
|
|
135
|
+
}
|
|
136
|
+
function simulation(definitions, gravity, catalog) {
|
|
137
|
+
const world = new RAPIER.World(vec(gravity, "gravity"));
|
|
138
|
+
let queue;
|
|
139
|
+
const bodies = new Map(), byCollider = new Map(), pairs = new Map();
|
|
140
|
+
let tick = 0, topologyDirty = false;
|
|
141
|
+
const pairKey = (a, b) => (a < b ? a + ":" + b : b + ":" + a);
|
|
142
|
+
function controller(d) {
|
|
143
|
+
const c = world.createCharacterController(0.01);
|
|
144
|
+
c.setUp({ x: 0, y: 1, z: 0 });
|
|
145
|
+
c.enableAutostep(Math.min(0.3, d.height * 0.2), d.radius * 0.5, false);
|
|
146
|
+
c.enableSnapToGround(0.15);
|
|
147
|
+
return c;
|
|
148
|
+
}
|
|
149
|
+
function add(e) {
|
|
150
|
+
const type = e.character ? "character" : e.rigidBody.type;
|
|
151
|
+
const p = vec(e.transform.position, e.id), rd = type === "fixed"
|
|
152
|
+
? RAPIER.RigidBodyDesc.fixed()
|
|
153
|
+
: type === "dynamic"
|
|
154
|
+
? RAPIER.RigidBodyDesc.dynamic().setCcdEnabled(true)
|
|
155
|
+
: RAPIER.RigidBodyDesc.kinematicPositionBased();
|
|
156
|
+
rd.setTranslation(p.x, p.y, p.z).setRotation(rotation(e.transform.rotation));
|
|
157
|
+
const body = world.createRigidBody(rd);
|
|
158
|
+
let ownedController;
|
|
159
|
+
try {
|
|
160
|
+
let cd;
|
|
161
|
+
if (e.character)
|
|
162
|
+
cd = RAPIER.ColliderDesc.capsule((e.character.height - 2 * e.character.radius) / 2, e.character.radius);
|
|
163
|
+
else {
|
|
164
|
+
const d = e.rigidBody, o = d.collider.offset ?? [0, 0, 0];
|
|
165
|
+
cd = new RAPIER.ColliderDesc(shape(e, e.transform));
|
|
166
|
+
cd.setTranslation(o[0] * e.transform.scale[0], o[1] * e.transform.scale[1], o[2] * e.transform.scale[2])
|
|
167
|
+
.setFriction(d.friction ?? 0.5)
|
|
168
|
+
.setRestitution(d.restitution ?? 0)
|
|
169
|
+
.setSensor(d.trigger ?? false);
|
|
170
|
+
if (d.mass !== undefined)
|
|
171
|
+
cd.setMass(d.mass);
|
|
172
|
+
}
|
|
173
|
+
cd.setActiveEvents(RAPIER.ActiveEvents.COLLISION_EVENTS).setActiveCollisionTypes(RAPIER.ActiveCollisionTypes.ALL);
|
|
174
|
+
const collider = world.createCollider(cd, body);
|
|
175
|
+
const b = {
|
|
176
|
+
id: e.id,
|
|
177
|
+
definition: e,
|
|
178
|
+
body,
|
|
179
|
+
collider,
|
|
180
|
+
type,
|
|
181
|
+
transform: structuredClone(e.transform),
|
|
182
|
+
};
|
|
183
|
+
topologyDirty = true;
|
|
184
|
+
bodies.set(e.id, b);
|
|
185
|
+
byCollider.set(collider.handle, e.id);
|
|
186
|
+
if (e.character) {
|
|
187
|
+
const d = e.character;
|
|
188
|
+
b.controller = ownedController = controller(d);
|
|
189
|
+
b.character = {
|
|
190
|
+
entity: e.id,
|
|
191
|
+
bodyHandle: body.handle,
|
|
192
|
+
colliderHandle: collider.handle,
|
|
193
|
+
radius: d.radius,
|
|
194
|
+
height: d.height,
|
|
195
|
+
gravity: d.gravity ?? 9.81,
|
|
196
|
+
verticalVelocity: 0,
|
|
197
|
+
velocity: [0, 0, 0],
|
|
198
|
+
grounded: false,
|
|
199
|
+
requested: [0, 0, 0],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
if (ownedController)
|
|
205
|
+
world.removeCharacterController(ownedController);
|
|
206
|
+
const entry = bodies.get(e.id);
|
|
207
|
+
if (entry)
|
|
208
|
+
byCollider.delete(entry.collider.handle);
|
|
209
|
+
bodies.delete(e.id);
|
|
210
|
+
world.removeRigidBody(body);
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
queue = new RAPIER.EventQueue(true);
|
|
216
|
+
for (const e of definitions)
|
|
217
|
+
add(e);
|
|
218
|
+
// Build Rapier query structures without advancing physical time.
|
|
219
|
+
world.timestep = 0;
|
|
220
|
+
world.step(queue);
|
|
221
|
+
world.timestep = 1 / 60;
|
|
222
|
+
topologyDirty = false;
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
try {
|
|
226
|
+
world.free();
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
queue?.free();
|
|
230
|
+
}
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
function get(id) {
|
|
234
|
+
const b = bodies.get(id);
|
|
235
|
+
if (!b)
|
|
236
|
+
fail("TC_PHYSICS_ENTITY", id + " has no live physics body.");
|
|
237
|
+
return b;
|
|
238
|
+
}
|
|
239
|
+
function emitPair(p, started, emit) {
|
|
240
|
+
const a = byCollider.get(p.a), b = byCollider.get(p.b);
|
|
241
|
+
if (a && b)
|
|
242
|
+
emit("physics." +
|
|
243
|
+
(p.trigger ? "trigger" : "collision") +
|
|
244
|
+
"." +
|
|
245
|
+
(started ? "started" : "ended"), { a, b });
|
|
246
|
+
}
|
|
247
|
+
function remove(id, emit) {
|
|
248
|
+
const b = get(id);
|
|
249
|
+
for (const [key, p] of pairs)
|
|
250
|
+
if (p.a === b.collider.handle || p.b === b.collider.handle) {
|
|
251
|
+
emitPair(p, false, emit);
|
|
252
|
+
pairs.delete(key);
|
|
253
|
+
}
|
|
254
|
+
if (b.controller)
|
|
255
|
+
world.removeCharacterController(b.controller);
|
|
256
|
+
byCollider.delete(b.collider.handle);
|
|
257
|
+
world.removeRigidBody(b.body);
|
|
258
|
+
bodies.delete(id);
|
|
259
|
+
topologyDirty = true;
|
|
260
|
+
}
|
|
261
|
+
function drain(emit) {
|
|
262
|
+
queue.drainCollisionEvents((a, b, started) => {
|
|
263
|
+
if (!byCollider.has(a) || !byCollider.has(b))
|
|
264
|
+
return;
|
|
265
|
+
const key = pairKey(a, b), p = {
|
|
266
|
+
a,
|
|
267
|
+
b,
|
|
268
|
+
trigger: world.getCollider(a).isSensor() || world.getCollider(b).isSensor(),
|
|
269
|
+
};
|
|
270
|
+
if (started) {
|
|
271
|
+
if (pairs.has(key))
|
|
272
|
+
return;
|
|
273
|
+
pairs.set(key, p);
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
if (!pairs.has(key))
|
|
277
|
+
return;
|
|
278
|
+
pairs.delete(key);
|
|
279
|
+
}
|
|
280
|
+
emitPair(p, started, emit);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
function sync(frame, emit) {
|
|
284
|
+
if (!Array.isArray(frame) || frame.length > bodies.size)
|
|
285
|
+
fail("TC_PHYSICS_SNAPSHOT", "Invalid transform frame.");
|
|
286
|
+
const seen = new Set();
|
|
287
|
+
for (const f of frame) {
|
|
288
|
+
if (!f || seen.has(f.id))
|
|
289
|
+
fail("TC_PHYSICS_SNAPSHOT", "Duplicate transform entity.");
|
|
290
|
+
const b = get(f.id);
|
|
291
|
+
checkTransform(f.transform, b.definition);
|
|
292
|
+
seen.add(f.id);
|
|
293
|
+
}
|
|
294
|
+
let dirty = topologyDirty;
|
|
295
|
+
for (const id of bodies.keys())
|
|
296
|
+
if (!seen.has(id)) {
|
|
297
|
+
remove(id, emit);
|
|
298
|
+
dirty = true;
|
|
299
|
+
}
|
|
300
|
+
for (const f of frame) {
|
|
301
|
+
const b = get(f.id), t = f.transform, old = b.transform;
|
|
302
|
+
if (!same(t, old))
|
|
303
|
+
dirty = true;
|
|
304
|
+
if (!same(t.scale, old.scale)) {
|
|
305
|
+
b.collider.setShape(shape(b.definition, t));
|
|
306
|
+
const o = b.definition.rigidBody.collider.offset ?? [0, 0, 0];
|
|
307
|
+
b.collider.setTranslationWrtParent({
|
|
308
|
+
x: o[0] * t.scale[0],
|
|
309
|
+
y: o[1] * t.scale[1],
|
|
310
|
+
z: o[2] * t.scale[2],
|
|
311
|
+
});
|
|
312
|
+
if (b.definition.rigidBody.mass !== undefined)
|
|
313
|
+
b.collider.setMass(b.definition.rigidBody.mass);
|
|
314
|
+
b.body.recomputeMassPropertiesFromColliders();
|
|
315
|
+
}
|
|
316
|
+
// External Transform edits are explicit teleports for all body types.
|
|
317
|
+
if (!same(t.position, old.position)) {
|
|
318
|
+
b.body.setTranslation(vec(t.position, b.id), true);
|
|
319
|
+
if (b.character)
|
|
320
|
+
b.character.grounded = false;
|
|
321
|
+
}
|
|
322
|
+
if (!same(t.rotation, old.rotation))
|
|
323
|
+
b.body.setRotation(rotation(t.rotation), true);
|
|
324
|
+
b.transform = structuredClone(t);
|
|
325
|
+
}
|
|
326
|
+
world.propagateModifiedBodyPositionsToColliders();
|
|
327
|
+
if (dirty) {
|
|
328
|
+
const dt = world.timestep;
|
|
329
|
+
world.timestep = 0;
|
|
330
|
+
// Rapier 0.21 commits moved fixed-collider bounds on the following
|
|
331
|
+
// pipeline pass when dt is zero. Both passes advance no physical time.
|
|
332
|
+
world.step(queue);
|
|
333
|
+
drain(emit);
|
|
334
|
+
world.step(queue);
|
|
335
|
+
drain(emit);
|
|
336
|
+
world.timestep = dt;
|
|
337
|
+
}
|
|
338
|
+
topologyDirty = false;
|
|
339
|
+
}
|
|
340
|
+
/** Recover from Rapier KCC zero-TOI cycling without ignoring any blocking solid. */
|
|
341
|
+
function sweepSlide(b, desired) {
|
|
342
|
+
const start = b.collider.translation(), position = { x: start.x, y: start.y, z: start.z }, remaining = { ...desired }, ignored = new Set();
|
|
343
|
+
for (let iteration = 0; iteration < 8; iteration++) {
|
|
344
|
+
if (Math.hypot(remaining.x, remaining.y, remaining.z) < 1e-7)
|
|
345
|
+
break;
|
|
346
|
+
const hit = world.castShape(position, b.collider.rotation(), remaining, b.collider.shape, 0.01, 1, false, RAPIER.QueryFilterFlags.EXCLUDE_SENSORS, undefined, b.collider, b.body, (collider) => !ignored.has(collider.handle));
|
|
347
|
+
if (!hit) {
|
|
348
|
+
position.x += remaining.x;
|
|
349
|
+
position.y += remaining.y;
|
|
350
|
+
position.z += remaining.z;
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
const normal = hit.normal1, dot = remaining.x * normal.x +
|
|
354
|
+
remaining.y * normal.y +
|
|
355
|
+
remaining.z * normal.z;
|
|
356
|
+
// Touching a surface while moving tangentially or outward is not a blocker.
|
|
357
|
+
// The exclusion applies only to this linear sweep; other solids remain queried.
|
|
358
|
+
if (hit.time_of_impact < 1e-5 && dot >= -1e-6) {
|
|
359
|
+
ignored.add(hit.collider.handle);
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
const time = Math.max(0, Math.min(1, hit.time_of_impact));
|
|
363
|
+
position.x += remaining.x * time;
|
|
364
|
+
position.y += remaining.y * time;
|
|
365
|
+
position.z += remaining.z * time;
|
|
366
|
+
remaining.x *= 1 - time;
|
|
367
|
+
remaining.y *= 1 - time;
|
|
368
|
+
remaining.z *= 1 - time;
|
|
369
|
+
const into = remaining.x * normal.x +
|
|
370
|
+
remaining.y * normal.y +
|
|
371
|
+
remaining.z * normal.z;
|
|
372
|
+
if (into < 0) {
|
|
373
|
+
remaining.x -= into * normal.x;
|
|
374
|
+
remaining.y -= into * normal.y;
|
|
375
|
+
remaining.z -= into * normal.z;
|
|
376
|
+
}
|
|
377
|
+
else
|
|
378
|
+
ignored.add(hit.collider.handle);
|
|
379
|
+
}
|
|
380
|
+
const support = world.castShape(position, b.collider.rotation(), { x: 0, y: -1, z: 0 }, b.collider.shape, 0, 0.025, true, RAPIER.QueryFilterFlags.EXCLUDE_SENSORS, undefined, b.collider, b.body);
|
|
381
|
+
return {
|
|
382
|
+
movement: {
|
|
383
|
+
x: position.x - start.x,
|
|
384
|
+
y: position.y - start.y,
|
|
385
|
+
z: position.z - start.z,
|
|
386
|
+
},
|
|
387
|
+
grounded: desired.y <= 0 &&
|
|
388
|
+
!!support &&
|
|
389
|
+
support.normal1.y >= Math.cos(b.controller.maxSlopeClimbAngle()),
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
function apply(action, emit) {
|
|
393
|
+
if (!action ||
|
|
394
|
+
!["step", "move", "velocity", "impulse", "angularVelocity", "torqueImpulse", "impulseAtPoint"].includes(action.kind))
|
|
395
|
+
fail("TC_PHYSICS_SNAPSHOT", "Invalid history action.");
|
|
396
|
+
if (action.kind !== "step") {
|
|
397
|
+
if (["angularVelocity", "torqueImpulse", "impulseAtPoint"].includes(action.kind)) {
|
|
398
|
+
const keys = action.kind === "impulseAtPoint" ? ["kind", "id", "value", "point"] : ["kind", "id", "value"];
|
|
399
|
+
if (Object.keys(action).length !== keys.length || Object.keys(action).some((key) => !keys.includes(key)))
|
|
400
|
+
fail("TC_PHYSICS_SNAPSHOT", "Invalid angular history action fields.");
|
|
401
|
+
}
|
|
402
|
+
const b = get(action.id), v = vec(action.value, action.kind);
|
|
403
|
+
const point = action.kind === "impulseAtPoint" ? vec(action.point, "point") : undefined;
|
|
404
|
+
if (action.kind === "move") {
|
|
405
|
+
if (!b.character)
|
|
406
|
+
fail("TC_PHYSICS_CHARACTER", b.id + " is not a character.");
|
|
407
|
+
const c = b.character, requested = [
|
|
408
|
+
c.requested[0] + v.x,
|
|
409
|
+
c.requested[1] + v.y,
|
|
410
|
+
c.requested[2] + v.z,
|
|
411
|
+
];
|
|
412
|
+
vec(requested, "accumulated displacement");
|
|
413
|
+
c.requested = requested;
|
|
414
|
+
}
|
|
415
|
+
else if (action.kind === "velocity") {
|
|
416
|
+
if (b.character) {
|
|
417
|
+
b.character.velocity = tuple(v);
|
|
418
|
+
b.character.verticalVelocity = v.y;
|
|
419
|
+
if (v.y > 0)
|
|
420
|
+
b.character.grounded = false;
|
|
421
|
+
}
|
|
422
|
+
else if (b.type === "dynamic")
|
|
423
|
+
b.body.setLinvel(v, true);
|
|
424
|
+
else
|
|
425
|
+
fail("TC_PHYSICS_BODY_TYPE", "setVelocity requires a dynamic body or character (" + b.id + ").");
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
if (b.type !== "dynamic")
|
|
429
|
+
fail("TC_PHYSICS_BODY_TYPE", (action.kind === "angularVelocity" ? "setAngularVelocity" : action.kind) + " requires a dynamic body (" + b.id + ").");
|
|
430
|
+
if (action.kind === "angularVelocity")
|
|
431
|
+
b.body.setAngvel(v, true);
|
|
432
|
+
else if (action.kind === "torqueImpulse")
|
|
433
|
+
b.body.applyTorqueImpulse(v, true);
|
|
434
|
+
else if (action.kind === "impulseAtPoint")
|
|
435
|
+
b.body.applyImpulseAtPoint(v, point, true);
|
|
436
|
+
else
|
|
437
|
+
b.body.applyImpulse(v, true);
|
|
438
|
+
}
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
finite(action.dt, "dt", 1e-6, 1);
|
|
442
|
+
// Creation history contains content identity and transforms only. Never accept
|
|
443
|
+
// caller-supplied collider/body definitions during isolated snapshot replay.
|
|
444
|
+
if (!Array.isArray(action.created) || !Array.isArray(action.removed))
|
|
445
|
+
fail("TC_PHYSICS_SNAPSHOT", "Missing lifecycle operations.");
|
|
446
|
+
const removed = new Set();
|
|
447
|
+
for (const id of action.removed) {
|
|
448
|
+
if (typeof id !== "string" || removed.has(id))
|
|
449
|
+
fail("TC_PHYSICS_SNAPSHOT", "Duplicate/invalid removed entity.");
|
|
450
|
+
get(id);
|
|
451
|
+
removed.add(id);
|
|
452
|
+
}
|
|
453
|
+
const created = new Map();
|
|
454
|
+
for (const c of action.created) {
|
|
455
|
+
if (!c ||
|
|
456
|
+
typeof c.id !== "string" ||
|
|
457
|
+
!c.id ||
|
|
458
|
+
Object.keys(c).some((k) => !["id", "prefab", "transform"].includes(k)) ||
|
|
459
|
+
(c.prefab !== undefined && typeof c.prefab !== "string") ||
|
|
460
|
+
created.has(c.id) ||
|
|
461
|
+
(bodies.has(c.id) && !removed.has(c.id)))
|
|
462
|
+
fail("TC_PHYSICS_SNAPSHOT", "Invalid/duplicate creation identity.");
|
|
463
|
+
if (c.prefab !== undefined &&
|
|
464
|
+
!catalog.prefabs.some((p) => p.id === c.prefab))
|
|
465
|
+
fail("TC_PHYSICS_SNAPSHOT", "Unknown compiled prefab identity.");
|
|
466
|
+
const definition = resolveEntityDefinition(catalog, c);
|
|
467
|
+
if (!definition || (!definition.rigidBody && !definition.character))
|
|
468
|
+
fail("TC_PHYSICS_SNAPSHOT", "Creation must resolve to an authored physics entity or compiled physics prefab.");
|
|
469
|
+
const e = { ...definition, transform: c.transform };
|
|
470
|
+
validateDefinition(e);
|
|
471
|
+
created.set(c.id, e);
|
|
472
|
+
}
|
|
473
|
+
const expected = new Map([...bodies]
|
|
474
|
+
.filter(([id]) => !removed.has(id))
|
|
475
|
+
.map(([id, b]) => [id, b.definition]));
|
|
476
|
+
for (const [id, e] of created)
|
|
477
|
+
expected.set(id, e);
|
|
478
|
+
if (!Array.isArray(action.frame) || action.frame.length !== expected.size)
|
|
479
|
+
fail("TC_PHYSICS_SNAPSHOT", "Lifecycle frame does not match active bodies.");
|
|
480
|
+
const seen = new Set();
|
|
481
|
+
for (const f of action.frame) {
|
|
482
|
+
const e = f && expected.get(f.id);
|
|
483
|
+
if (!e || seen.has(f.id))
|
|
484
|
+
fail("TC_PHYSICS_SNAPSHOT", "Invalid lifecycle frame entity.");
|
|
485
|
+
checkTransform(f.transform, e);
|
|
486
|
+
if (created.has(f.id) && !same(f.transform, created.get(f.id).transform))
|
|
487
|
+
fail("TC_PHYSICS_SNAPSHOT", "Creation and frame transforms differ.");
|
|
488
|
+
seen.add(f.id);
|
|
489
|
+
}
|
|
490
|
+
drain(emit);
|
|
491
|
+
for (const id of removed)
|
|
492
|
+
remove(id, emit);
|
|
493
|
+
for (const e of created.values())
|
|
494
|
+
add(e);
|
|
495
|
+
sync(action.frame, emit);
|
|
496
|
+
world.timestep = action.dt;
|
|
497
|
+
for (const b of bodies.values())
|
|
498
|
+
if (b.character && b.controller) {
|
|
499
|
+
const c = b.character, dt = action.dt;
|
|
500
|
+
// A downward probe on every falling/grounded tick keeps grounded stable.
|
|
501
|
+
c.verticalVelocity -= c.gravity * dt;
|
|
502
|
+
const desired = {
|
|
503
|
+
x: c.requested[0] + c.velocity[0] * dt,
|
|
504
|
+
y: c.requested[1] + c.verticalVelocity * dt,
|
|
505
|
+
z: c.requested[2] + c.velocity[2] * dt,
|
|
506
|
+
};
|
|
507
|
+
b.controller.computeColliderMovement(b.collider, desired, RAPIER.QueryFilterFlags.EXCLUDE_SENSORS);
|
|
508
|
+
let m = b.controller.computedMovement();
|
|
509
|
+
const stalled = b.controller.numComputedCollisions() >= 20 &&
|
|
510
|
+
((Math.hypot(desired.x, desired.z) > 1e-6 &&
|
|
511
|
+
Math.hypot(m.x, m.z) < Math.hypot(desired.x, desired.z) * 0.01) ||
|
|
512
|
+
(Math.hypot(m.x, m.y, m.z) < 1e-7 &&
|
|
513
|
+
Math.hypot(desired.x, desired.y, desired.z) > 1e-6));
|
|
514
|
+
const recovered = stalled ? sweepSlide(b, desired) : undefined;
|
|
515
|
+
if (recovered)
|
|
516
|
+
m = recovered.movement;
|
|
517
|
+
const p = b.body.translation();
|
|
518
|
+
c.grounded = recovered?.grounded ?? b.controller.computedGrounded();
|
|
519
|
+
if ((c.grounded && c.verticalVelocity < 0) ||
|
|
520
|
+
(desired.y > 0 && m.y < desired.y - 1e-5 && c.verticalVelocity > 0))
|
|
521
|
+
c.verticalVelocity = 0;
|
|
522
|
+
b.body.setNextKinematicTranslation({
|
|
523
|
+
x: p.x + m.x,
|
|
524
|
+
y: p.y + m.y,
|
|
525
|
+
z: p.z + m.z,
|
|
526
|
+
});
|
|
527
|
+
c.requested = [0, 0, 0];
|
|
528
|
+
}
|
|
529
|
+
world.step(queue);
|
|
530
|
+
tick++;
|
|
531
|
+
drain(emit);
|
|
532
|
+
for (const b of bodies.values())
|
|
533
|
+
b.transform = {
|
|
534
|
+
position: tuple(b.body.translation()),
|
|
535
|
+
rotation: b.character ? b.transform.rotation : euler(b.body.rotation()),
|
|
536
|
+
scale: b.transform.scale,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
function header() {
|
|
540
|
+
const bytes = world.takeSnapshot();
|
|
541
|
+
if (bytes.length > 64 * 1024 * 1024)
|
|
542
|
+
fail("TC_PHYSICS_SNAPSHOT_SIZE", "Snapshot exceeds 64 MiB; reduce the scene size.");
|
|
543
|
+
return {
|
|
544
|
+
world: Array.from(bytes),
|
|
545
|
+
tick,
|
|
546
|
+
entities: [...bodies.values()].map((b) => ({
|
|
547
|
+
id: b.id,
|
|
548
|
+
bodyHandle: b.body.handle,
|
|
549
|
+
colliderHandle: b.collider.handle,
|
|
550
|
+
type: b.type,
|
|
551
|
+
transform: structuredClone(b.transform),
|
|
552
|
+
})),
|
|
553
|
+
characters: [...bodies.values()].flatMap((b) => b.character ? [structuredClone(b.character)] : []),
|
|
554
|
+
pairs: [...pairs.values()].map((p) => p.a < p.b ? [p.a, p.b] : [p.b, p.a]),
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
return {
|
|
558
|
+
get tick() {
|
|
559
|
+
return tick;
|
|
560
|
+
},
|
|
561
|
+
contactCount(handle) {
|
|
562
|
+
return [...pairs.values()].filter((p) => p.a === handle || p.b === handle)
|
|
563
|
+
.length;
|
|
564
|
+
},
|
|
565
|
+
world,
|
|
566
|
+
bodies,
|
|
567
|
+
byCollider,
|
|
568
|
+
get,
|
|
569
|
+
apply,
|
|
570
|
+
header,
|
|
571
|
+
remove,
|
|
572
|
+
sync,
|
|
573
|
+
dispose() {
|
|
574
|
+
try {
|
|
575
|
+
queue.free();
|
|
576
|
+
}
|
|
577
|
+
finally {
|
|
578
|
+
world.free();
|
|
579
|
+
}
|
|
580
|
+
},
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
function createPlugin(context) {
|
|
584
|
+
const catalog = structuredClone(context.scene);
|
|
585
|
+
const definitions = catalog.scene.entities.filter((e) => e.rigidBody || e.character);
|
|
586
|
+
const ids = new Set();
|
|
587
|
+
for (const e of definitions) {
|
|
588
|
+
if (ids.has(e.id))
|
|
589
|
+
fail("TC_PHYSICS_DEFINITION", "Duplicate physics entity " + e.id);
|
|
590
|
+
ids.add(e.id);
|
|
591
|
+
validateDefinition(e);
|
|
592
|
+
}
|
|
593
|
+
const gravity = catalog.scene.gravity ?? [0, -9.81, 0];
|
|
594
|
+
vec(gravity, "gravity");
|
|
595
|
+
const scene = JSON.stringify({
|
|
596
|
+
entities: catalog.scene.entities,
|
|
597
|
+
prefabs: catalog.prefabs,
|
|
598
|
+
gravity,
|
|
599
|
+
});
|
|
600
|
+
let state = simulation(definitions, gravity, catalog), history = [], disposed = false, pendingBind;
|
|
601
|
+
const refs = new Map();
|
|
602
|
+
function bindRefs(ids = state.bodies.keys()) {
|
|
603
|
+
refs.clear();
|
|
604
|
+
for (const id of ids)
|
|
605
|
+
if (context.world.isAlive(id))
|
|
606
|
+
refs.set(id, context.world.resolve(id));
|
|
607
|
+
}
|
|
608
|
+
bindRefs();
|
|
609
|
+
function ensureRefs() {
|
|
610
|
+
if (pendingBind) {
|
|
611
|
+
bindRefs(pendingBind);
|
|
612
|
+
pendingBind = undefined;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
const emit = (type, payload) => {
|
|
616
|
+
context.emit(type, payload);
|
|
617
|
+
};
|
|
618
|
+
function alive() {
|
|
619
|
+
if (disposed)
|
|
620
|
+
fail("TC_PHYSICS_DISPOSED", "Physics plugin disposed; create a new host.");
|
|
621
|
+
}
|
|
622
|
+
function lifecycle(dt) {
|
|
623
|
+
ensureRefs();
|
|
624
|
+
const frame = [], created = [], removed = [];
|
|
625
|
+
const active = new Map(context.world.records().map((r) => [r.id, r]));
|
|
626
|
+
for (const id of state.bodies.keys()) {
|
|
627
|
+
const ref = refs.get(id);
|
|
628
|
+
if (!ref || !context.world.isAlive(ref) || !active.has(id))
|
|
629
|
+
removed.push(id);
|
|
630
|
+
}
|
|
631
|
+
for (const r of active.values()) {
|
|
632
|
+
const definition = resolveEntityDefinition(catalog, r);
|
|
633
|
+
if (!definition || (!definition.rigidBody && !definition.character))
|
|
634
|
+
continue;
|
|
635
|
+
// World parent/transform overrides must obey the same constraints as authored bodies.
|
|
636
|
+
const e = {
|
|
637
|
+
...definition,
|
|
638
|
+
parent: r.parent ?? definition.parent,
|
|
639
|
+
transform: r.transform,
|
|
640
|
+
};
|
|
641
|
+
validateDefinition(e);
|
|
642
|
+
frame.push({ id: r.id, transform: r.transform });
|
|
643
|
+
if (!state.bodies.has(r.id) || removed.includes(r.id))
|
|
644
|
+
created.push({
|
|
645
|
+
id: r.id,
|
|
646
|
+
...(r.prefab === undefined ? {} : { prefab: r.prefab }),
|
|
647
|
+
transform: r.transform,
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
return { kind: "step", dt, frame, created, removed };
|
|
651
|
+
}
|
|
652
|
+
function record(action) {
|
|
653
|
+
alive();
|
|
654
|
+
if (history.length >= limit)
|
|
655
|
+
fail("TC_PHYSICS_HISTORY_LIMIT", "Physics history exceeds 100000 operations; start a new scene.");
|
|
656
|
+
state.apply(action, emit);
|
|
657
|
+
history.push(structuredClone(action));
|
|
658
|
+
}
|
|
659
|
+
function get(id) {
|
|
660
|
+
alive();
|
|
661
|
+
ensureRefs();
|
|
662
|
+
const ref = refs.get(id);
|
|
663
|
+
if (!ref || !context.world.isAlive(ref))
|
|
664
|
+
fail("TC_PHYSICS_ENTITY", id + " has no live physics body.");
|
|
665
|
+
return state.get(id);
|
|
666
|
+
}
|
|
667
|
+
function snapshot() {
|
|
668
|
+
alive();
|
|
669
|
+
ensureRefs();
|
|
670
|
+
return {
|
|
671
|
+
format: "pelican.physics-rapier3d",
|
|
672
|
+
version: 3,
|
|
673
|
+
scene,
|
|
674
|
+
history: structuredClone(history),
|
|
675
|
+
bindings: [...refs]
|
|
676
|
+
.filter(([, ref]) => context.world.isAlive(ref))
|
|
677
|
+
.map(([id]) => id),
|
|
678
|
+
...state.header(),
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
function validate(input) {
|
|
682
|
+
alive();
|
|
683
|
+
let candidate;
|
|
684
|
+
try {
|
|
685
|
+
const s = cloneSerializable(input);
|
|
686
|
+
if (!s ||
|
|
687
|
+
s.format !== "pelican.physics-rapier3d" ||
|
|
688
|
+
s.version !== 3 ||
|
|
689
|
+
s.scene !== scene ||
|
|
690
|
+
!Number.isSafeInteger(s.tick) ||
|
|
691
|
+
s.tick < 0 ||
|
|
692
|
+
!Array.isArray(s.history) ||
|
|
693
|
+
s.history.length > limit ||
|
|
694
|
+
!Array.isArray(s.world) ||
|
|
695
|
+
s.world.length === 0 ||
|
|
696
|
+
s.world.length > 64 * 1024 * 1024 ||
|
|
697
|
+
!s.world.every((n) => Number.isInteger(n) && n >= 0 && n <= 255) ||
|
|
698
|
+
!Array.isArray(s.bindings) ||
|
|
699
|
+
s.bindings.some((id) => typeof id !== "string") ||
|
|
700
|
+
new Set(s.bindings).size !== s.bindings.length ||
|
|
701
|
+
!Array.isArray(s.entities) ||
|
|
702
|
+
!Array.isArray(s.characters) ||
|
|
703
|
+
!Array.isArray(s.pairs))
|
|
704
|
+
fail("TC_PHYSICS_SNAPSHOT", "Malformed/incompatible snapshot header; use an intact version 3 snapshot of this scene.");
|
|
705
|
+
// No caller-controlled bytes ever reach World.restoreSnapshot. Reconstruct
|
|
706
|
+
// safe native state from checked operations, then verify ALL native/header data.
|
|
707
|
+
candidate = simulation(definitions, gravity, catalog);
|
|
708
|
+
for (const action of s.history)
|
|
709
|
+
candidate.apply(action, () => { });
|
|
710
|
+
if (s.bindings.some((id) => !candidate.bodies.has(id)))
|
|
711
|
+
fail("TC_PHYSICS_SNAPSHOT", "Binding has no reconstructed body.");
|
|
712
|
+
const actual = candidate.header();
|
|
713
|
+
if (!same(actual, {
|
|
714
|
+
world: s.world,
|
|
715
|
+
tick: s.tick,
|
|
716
|
+
entities: s.entities,
|
|
717
|
+
characters: s.characters,
|
|
718
|
+
pairs: s.pairs,
|
|
719
|
+
}))
|
|
720
|
+
fail("TC_PHYSICS_SNAPSHOT", "Native state, entity/handle mapping, character state or contact pairs differ from verified history.");
|
|
721
|
+
return { candidate, history: s.history, bindings: s.bindings };
|
|
722
|
+
}
|
|
723
|
+
catch (error) {
|
|
724
|
+
candidate?.dispose();
|
|
725
|
+
fail("TC_PHYSICS_SNAPSHOT", "Snapshot rejected atomically: " + String(error));
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
const physics = {
|
|
729
|
+
moveCharacter(id, value) {
|
|
730
|
+
get(id);
|
|
731
|
+
record({ kind: "move", id, value });
|
|
732
|
+
},
|
|
733
|
+
grounded(id) {
|
|
734
|
+
const c = get(id).character;
|
|
735
|
+
if (!c)
|
|
736
|
+
fail("TC_PHYSICS_CHARACTER", id + " is not a character.");
|
|
737
|
+
return c.grounded;
|
|
738
|
+
},
|
|
739
|
+
velocity(id) {
|
|
740
|
+
const b = get(id);
|
|
741
|
+
return b.character
|
|
742
|
+
? [
|
|
743
|
+
b.character.velocity[0],
|
|
744
|
+
b.character.verticalVelocity,
|
|
745
|
+
b.character.velocity[2],
|
|
746
|
+
]
|
|
747
|
+
: tuple(b.body.linvel());
|
|
748
|
+
},
|
|
749
|
+
setVelocity(id, value) {
|
|
750
|
+
get(id);
|
|
751
|
+
record({ kind: "velocity", id, value });
|
|
752
|
+
},
|
|
753
|
+
impulse(id, value) {
|
|
754
|
+
get(id);
|
|
755
|
+
record({ kind: "impulse", id, value });
|
|
756
|
+
},
|
|
757
|
+
angularVelocity(id) {
|
|
758
|
+
return tuple(get(id).body.angvel());
|
|
759
|
+
},
|
|
760
|
+
setAngularVelocity(id, value) {
|
|
761
|
+
get(id);
|
|
762
|
+
record({ kind: "angularVelocity", id, value: tuple(vec(value, "angular velocity")) });
|
|
763
|
+
},
|
|
764
|
+
torqueImpulse(id, value) {
|
|
765
|
+
get(id);
|
|
766
|
+
record({ kind: "torqueImpulse", id, value: tuple(vec(value, "torque impulse")) });
|
|
767
|
+
},
|
|
768
|
+
impulseAtPoint(id, value, point) {
|
|
769
|
+
get(id);
|
|
770
|
+
record({ kind: "impulseAtPoint", id, value: tuple(vec(value, "impulse")), point: tuple(vec(point, "point")) });
|
|
771
|
+
},
|
|
772
|
+
raycast(origin, direction, maxDistance, options) {
|
|
773
|
+
alive();
|
|
774
|
+
ensureRefs();
|
|
775
|
+
finite(maxDistance, "maxDistance");
|
|
776
|
+
const o = vec(origin, "origin"), d = vec(direction, "direction"), l = Math.hypot(d.x, d.y, d.z);
|
|
777
|
+
if (!l)
|
|
778
|
+
fail("TC_PHYSICS_RAY", "Ray direction is zero; supply a nonzero direction.");
|
|
779
|
+
if (options !== undefined &&
|
|
780
|
+
(!options ||
|
|
781
|
+
typeof options !== "object" ||
|
|
782
|
+
Array.isArray(options) ||
|
|
783
|
+
Object.keys(options).some((k) => k !== "exclude" && k !== "includeTriggers") ||
|
|
784
|
+
(options.exclude !== undefined &&
|
|
785
|
+
(!Array.isArray(options.exclude) ||
|
|
786
|
+
!options.exclude.every((id) => typeof id === "string"))) ||
|
|
787
|
+
(options.includeTriggers !== undefined &&
|
|
788
|
+
typeof options.includeTriggers !== "boolean")))
|
|
789
|
+
fail("TC_PHYSICS_RAY", "Ray options require exclude string IDs and includeTriggers boolean.");
|
|
790
|
+
const excluded = new Set(options?.exclude ?? []), ray = new RAPIER.Ray(o, { x: d.x / l, y: d.y / l, z: d.z / l });
|
|
791
|
+
const hit = state.world.castRayAndGetNormal(ray, maxDistance, true, undefined, undefined, undefined, undefined, (collider) => {
|
|
792
|
+
const id = state.byCollider.get(collider.handle), ref = id ? refs.get(id) : undefined;
|
|
793
|
+
return (!!id &&
|
|
794
|
+
!!ref &&
|
|
795
|
+
context.world.isAlive(ref) &&
|
|
796
|
+
!excluded.has(id) &&
|
|
797
|
+
(options?.includeTriggers === true || !collider.isSensor()));
|
|
798
|
+
});
|
|
799
|
+
if (!hit)
|
|
800
|
+
return null;
|
|
801
|
+
return {
|
|
802
|
+
entity: state.byCollider.get(hit.collider.handle),
|
|
803
|
+
distance: hit.timeOfImpact,
|
|
804
|
+
point: tuple(ray.pointAt(hit.timeOfImpact)),
|
|
805
|
+
normal: tuple(hit.normal),
|
|
806
|
+
};
|
|
807
|
+
},
|
|
808
|
+
};
|
|
809
|
+
return {
|
|
810
|
+
id: pluginId,
|
|
811
|
+
version: "3",
|
|
812
|
+
services: { physics },
|
|
813
|
+
systems: [
|
|
814
|
+
{
|
|
815
|
+
id: pluginId,
|
|
816
|
+
phase: "physics",
|
|
817
|
+
// records() checks access to every registered component. Omitted reads
|
|
818
|
+
// grants this internal system metadata access without changing World APIs.
|
|
819
|
+
writes: ["Transform"],
|
|
820
|
+
run(dt) {
|
|
821
|
+
record(lifecycle(dt));
|
|
822
|
+
bindRefs();
|
|
823
|
+
for (const b of state.bodies.values())
|
|
824
|
+
context.world.setTransform(refs.get(b.id), b.transform);
|
|
825
|
+
},
|
|
826
|
+
},
|
|
827
|
+
],
|
|
828
|
+
snapshot,
|
|
829
|
+
validateSnapshot(input) {
|
|
830
|
+
const checked = validate(input);
|
|
831
|
+
checked.candidate.dispose();
|
|
832
|
+
},
|
|
833
|
+
validateWorld(input, world) {
|
|
834
|
+
const checked = validate(input);
|
|
835
|
+
try {
|
|
836
|
+
const records = new Map(world.entities.map((r) => [r.id, r]));
|
|
837
|
+
for (const r of records.values()) {
|
|
838
|
+
const e = resolveEntityDefinition(catalog, r);
|
|
839
|
+
if (e && (e.rigidBody || e.character))
|
|
840
|
+
validateDefinition({
|
|
841
|
+
...e,
|
|
842
|
+
parent: r.parent ?? e.parent,
|
|
843
|
+
transform: r.transform,
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
for (const id of checked.bindings) {
|
|
847
|
+
const r = records.get(id), expected = checked.candidate.get(id).definition, actual = r && resolveEntityDefinition(catalog, r);
|
|
848
|
+
if (!actual ||
|
|
849
|
+
!same({
|
|
850
|
+
prefab: actual.prefab,
|
|
851
|
+
rigidBody: actual.rigidBody,
|
|
852
|
+
character: actual.character,
|
|
853
|
+
}, {
|
|
854
|
+
prefab: expected.prefab,
|
|
855
|
+
rigidBody: expected.rigidBody,
|
|
856
|
+
character: expected.character,
|
|
857
|
+
}))
|
|
858
|
+
fail("TC_PHYSICS_SNAPSHOT", "World binding identity differs for " + id);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
finally {
|
|
862
|
+
checked.candidate.dispose();
|
|
863
|
+
}
|
|
864
|
+
},
|
|
865
|
+
restore(input) {
|
|
866
|
+
const checked = validate(input), previous = state;
|
|
867
|
+
state = checked.candidate;
|
|
868
|
+
history = checked.history;
|
|
869
|
+
bindRefs(checked.bindings);
|
|
870
|
+
pendingBind = checked.bindings;
|
|
871
|
+
previous.dispose();
|
|
872
|
+
// RuntimeHost restores World before plugin state. A direct plugin context
|
|
873
|
+
// may restore physics alone, so synchronize its live entity transforms too.
|
|
874
|
+
for (const b of state.bodies.values()) {
|
|
875
|
+
const ref = refs.get(b.id);
|
|
876
|
+
if (ref)
|
|
877
|
+
context.world.setTransform(ref, b.transform);
|
|
878
|
+
}
|
|
879
|
+
},
|
|
880
|
+
inspect() {
|
|
881
|
+
alive();
|
|
882
|
+
ensureRefs();
|
|
883
|
+
return {
|
|
884
|
+
tick: state.tick,
|
|
885
|
+
bodies: [...state.bodies.values()]
|
|
886
|
+
.filter((b) => {
|
|
887
|
+
const ref = refs.get(b.id);
|
|
888
|
+
return !!ref && context.world.isAlive(ref);
|
|
889
|
+
})
|
|
890
|
+
.map((b) => ({
|
|
891
|
+
id: b.id,
|
|
892
|
+
type: b.type,
|
|
893
|
+
position: tuple(b.body.translation()),
|
|
894
|
+
velocity: physics.velocity(b.id),
|
|
895
|
+
grounded: b.character?.grounded ?? null,
|
|
896
|
+
contacts: state.contactCount(b.collider.handle),
|
|
897
|
+
})),
|
|
898
|
+
};
|
|
899
|
+
},
|
|
900
|
+
dispose() {
|
|
901
|
+
if (disposed)
|
|
902
|
+
return;
|
|
903
|
+
disposed = true;
|
|
904
|
+
state.dispose();
|
|
905
|
+
history = [];
|
|
906
|
+
refs.clear();
|
|
907
|
+
},
|
|
908
|
+
};
|
|
909
|
+
}
|