@vgai/engine 0.2.0 → 0.4.0-canary.20260715.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 +3 -1
- package/package.json +24 -4
- package/schemas/engine-api.json +124 -0
- package/schemas/engine-api.md +53 -0
- package/schemas/engine-capabilities.json +124 -0
- package/schemas/inputmap.schema.json +314 -0
- package/schemas/mat.schema.json +286 -0
- package/schemas/prefab.schema.json +10148 -0
- package/schemas/scn2d.schema.json +475 -0
- package/schemas/vgai-game.schema.json +383 -0
- package/schemas/vscn.schema.json +11007 -0
- package/src/adapter/{world-kind.ts → adapter-surface.ts} +6 -6
- package/src/adapter/authoring.ts +77 -0
- package/src/adapter/first-party-systems.ts +23 -34
- package/src/adapter/game-adapter.ts +8 -8
- package/src/adapter/host-context.ts +2 -4
- package/src/adapter/index.ts +4 -4
- package/src/adapter/system-adapter.ts +88 -22
- package/src/adapter/vgai-scene-game-adapter.ts +244 -194
- package/src/animation/anim-graph-types.ts +12 -43
- package/src/animation/animation-clock.ts +479 -0
- package/src/animation/camera-ownership.ts +467 -0
- package/src/animation/cinematic-cues.ts +451 -0
- package/src/animation/clip-map.ts +41 -0
- package/src/animation/gsap-registration.ts +184 -0
- package/src/animation/theatre-clock-binding.ts +111 -0
- package/src/animation/theatre-director.ts +347 -0
- package/src/animation/theatre-object-binding.ts +661 -0
- package/src/animation/xstate-animation-binding.ts +436 -0
- package/src/animation/xstate-animation-meta.ts +319 -0
- package/src/audio/index.ts +39 -7
- package/src/audio/tone-clock-binding.ts +98 -0
- package/src/audio/tone-context.ts +129 -0
- package/src/audio/tone-offline-render.ts +167 -0
- package/src/audio/wav-encode.ts +119 -0
- package/src/character/cloth-sim.ts +533 -0
- package/src/character/spring-chain.ts +307 -0
- package/src/core/game-loop.ts +57 -2
- package/src/core/seeded-random.ts +161 -0
- package/src/core/system-runner.ts +20 -3
- package/src/core/types.ts +50 -0
- package/src/data/data-asset.ts +167 -0
- package/src/data/data-check-core.ts +242 -0
- package/src/data/data-ref.ts +145 -0
- package/src/data/vite-plugin-data.ts +290 -0
- package/src/dev/performance-profiler.ts +213 -0
- package/src/dev/webgl-gpu-timer.ts +53 -0
- package/src/ecs/component-manager.ts +45 -12
- package/src/ecs/game-component.ts +95 -11
- package/src/humanoid/bake.operation.ts +326 -0
- package/src/humanoid/body.ts +663 -0
- package/src/humanoid/clips.ts +149 -0
- package/src/humanoid/compose.ts +209 -0
- package/src/humanoid/generate.ts +189 -0
- package/src/humanoid/index.ts +36 -0
- package/src/humanoid/schema.ts +108 -0
- package/src/humanoid/skeleton.ts +345 -0
- package/src/index.ts +48 -0
- package/src/input/input-manager.ts +1886 -33
- package/src/input/input-types.ts +158 -3
- package/src/input/prompt-labels.ts +122 -0
- package/src/input/rebind-controller.ts +105 -0
- package/src/input/schema.ts +206 -52
- package/src/manifest/index.ts +5 -5
- package/src/manifest/load.ts +125 -72
- package/src/manifest/schema.ts +362 -255
- package/src/react/game-state.tsx +135 -32
- package/src/react/root-adapter.tsx +49 -0
- package/src/react/unmanaged-root-detector.ts +66 -0
- package/src/react/use-data.ts +124 -0
- package/src/react/use-selection.tsx +135 -0
- package/src/runtime/create-runtime.ts +112 -273
- package/src/runtime/debug-bridge.ts +483 -0
- package/src/runtime/debug-registry.ts +856 -0
- package/src/runtime/game.ts +342 -93
- package/src/runtime/gameplay-rng-trap.ts +134 -0
- package/src/runtime/input-router.ts +7 -7
- package/src/runtime/mount-game.ts +40 -38
- package/src/runtime/mount-manifest.ts +169 -37
- package/src/runtime/render-audio-control.ts +168 -0
- package/src/runtime/render-control.ts +522 -0
- package/src/runtime/render-seed.ts +79 -0
- package/src/runtime/state-bridge.ts +24 -10
- package/src/runtime/types.ts +110 -33
- package/src/scene/asset-loaders.ts +10 -36
- package/src/scene/asset-paths.ts +0 -2
- package/src/scene/asset-ref-check.ts +248 -0
- package/src/scene/asset-registry.ts +22 -0
- package/src/scene/component-registry.ts +14 -3
- package/src/scene/defaults.ts +1 -0
- package/src/scene/light-camera-factory.ts +11 -3
- package/src/scene/parse.ts +133 -0
- package/src/scene/scene-apply.ts +55 -4
- package/src/scene/scene-loader.ts +91 -123
- package/src/scene/scene-types.ts +0 -1
- package/src/scene/schema/animation.ts +30 -79
- package/src/scene/schema/entity.ts +20 -0
- package/src/scene/schema/index.ts +2 -46
- package/src/scene/schema/light.ts +16 -1
- package/src/scene/schema/material.ts +96 -91
- package/src/scene/schema/scene-file.ts +1 -7
- package/src/scene/user-data.ts +22 -10
- package/src/setup/setup-renderer.ts +10 -3
- package/src/tools/define-tool.ts +191 -0
- package/src/world2d/authoring-2d.ts +17 -1
- package/src/world2d/collision-2d.ts +1 -1
- package/src/world2d/pixi-game-adapter.ts +19 -17
- package/src/world2d/scene2d-loader.ts +1 -0
- package/src/world2d/types.ts +8 -2
- package/src/animation/anim-graph.ts +0 -406
- package/src/animation/anim-system.ts +0 -28
- package/src/animation/property-track.ts +0 -178
- package/src/animation/schema.ts +0 -204
- package/src/audio/ambient.ts +0 -300
- package/src/audio/impacts.ts +0 -212
- package/src/audio/movement.ts +0 -140
- package/src/audio/musical.ts +0 -200
- package/src/audio/ui-sounds.ts +0 -171
- package/src/audio/vehicle.ts +0 -235
- package/src/audio/weapons.ts +0 -152
- package/src/runtime/scene-ui-bridge.ts +0 -86
- package/src/runtime/scene-ui-data.ts +0 -119
- package/src/scene/schema/ui.ts +0 -602
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Character primitive — Jolt soft-body cloth grids (capes, skirts, banners)
|
|
3
|
+
* on top of the raw `jolt-physics` WASM bindings.
|
|
4
|
+
*
|
|
5
|
+
* THIN COMPOSITION HELPER, not a wrapper (CLAUDE.md "use libraries
|
|
6
|
+
* directly"): every export assembles the library's own objects
|
|
7
|
+
* (`Jolt.SoftBodySharedSettings`, `Jolt.Body`, `Jolt.JoltInterface`) —
|
|
8
|
+
* nothing from the library is re-exported. This file never imports the WASM
|
|
9
|
+
* module itself — the caller does the async `JoltInit()` and passes the
|
|
10
|
+
* resolved module in, mirroring `setup-physics.ts`'s `setupPhysics(rapier:
|
|
11
|
+
* typeof RAPIER, ...)` pattern for Rapier. `import type Jolt from
|
|
12
|
+
* 'jolt-physics/wasm-compat'` below is TYPE-ONLY (erased at compile time —
|
|
13
|
+
* no runtime WASM import here).
|
|
14
|
+
*
|
|
15
|
+
* Sharp edges this module exists partly to contain (see the mission brief /
|
|
16
|
+
* `cloth-hair-bench/jolt-cloth.ts`'s header for the full discovery):
|
|
17
|
+
* - `Body.GetMotionProperties()` needs the `Jolt.castObject` downcast to
|
|
18
|
+
* `Jolt.SoftBodyMotionProperties` to reach `GetVertex`.
|
|
19
|
+
* - `SoftBodySharedSettings.CreateConstraints` takes a POINTER-typed
|
|
20
|
+
* 1-element `ArraySoftBodySharedSettingsVertexAttributes` (push_back,
|
|
21
|
+
* not resize+at — `.at()` on these embind vectors returns a copy).
|
|
22
|
+
* - Kinematic pins must set BOTH `SoftBodyVertex.mPosition` and
|
|
23
|
+
* `.mVelocity` (finite-differenced) every tick, or the cloth's own
|
|
24
|
+
* constraint solver fights a teleporting anchor.
|
|
25
|
+
* - NO per-frame embind allocations (reviewer finding on Phase 1's
|
|
26
|
+
* spike, which leaked a `new Jolt.Vec3(...)`/`new Jolt.RVec3(...)` per
|
|
27
|
+
* pinned vertex per frame): `tick()` below preallocates ONE `Jolt.Vec3`
|
|
28
|
+
* for position and ONE for velocity and reuses them via `.Set(...)` —
|
|
29
|
+
* embind property setters (`vertex.mPosition = tmpVec3`) copy the value
|
|
30
|
+
* in, so reassigning the SAME wrapper object across many vertices in one
|
|
31
|
+
* frame is safe. `KinematicCollider.moveTo` does the same with one
|
|
32
|
+
* `Jolt.RVec3` + one `Jolt.Quat` (identity, orientation is not simulated
|
|
33
|
+
* here) allocated once at construction.
|
|
34
|
+
* - CONSTRUCTION-TIME embind temporaries need `Jolt.destroy(...)` too, once
|
|
35
|
+
* Jolt has copied their data out (reviewer finding on Phase 2 — these
|
|
36
|
+
* were leaking WASM heap per build/dispose cycle even with `tick()`
|
|
37
|
+
* itself allocation-free). Two different lifetimes are at play:
|
|
38
|
+
* - Per-vertex/per-face grid temporaries (`SoftBodySharedSettingsVertex`
|
|
39
|
+
* + its `Float3`, `SoftBodySharedSettingsFace`, the single
|
|
40
|
+
* `vertexAttr`) are copied BY VALUE into `sharedSettings`'s own
|
|
41
|
+
* storage the instant `push_back`/the property setter runs — freed
|
|
42
|
+
* right there, matching the reuse-then-`Jolt.destroy()`-once pattern
|
|
43
|
+
* in Jolt's own `soft-body-creator.js` sample. `bodyCreation`
|
|
44
|
+
* (`SoftBodyCreationSettings`) is the same story: `CreateSoftBody`
|
|
45
|
+
* copies what it needs, so it's freed right after — same contract as
|
|
46
|
+
* `JoltSettings` → `JoltInterface` below.
|
|
47
|
+
* - `sharedSettings` itself (`SoftBodySharedSettings`) is different: it
|
|
48
|
+
* is REFCOUNTED (`AddRef`/`Release`/`GetRefCount` in the vendored
|
|
49
|
+
* typings, like `Shape`) and the body reads it every step
|
|
50
|
+
* (`SoftBodyMotionProperties.GetSettings()`) for face/constraint
|
|
51
|
+
* topology. Refcounted RefTargets are freed BY JOLT ITSELF when the
|
|
52
|
+
* last reference drops (Release of the final ref IS the delete) —
|
|
53
|
+
* so it gets NO explicit `Jolt.destroy(...)` at all: destroying
|
|
54
|
+
* `bodyCreation` releases the ctor's ref, and `dispose()`'s
|
|
55
|
+
* `DestroyBody` releases the body's (last) ref, which deletes it.
|
|
56
|
+
* An explicit destroy on top would be a use-after-free (empirically
|
|
57
|
+
* confirmed with a `GetRefCount()` probe against real Jolt WASM).
|
|
58
|
+
* Same rule for the collider's `Shape`: `destroy(creation)` releases
|
|
59
|
+
* the creation-settings ref, `DestroyBody` releases the body's —
|
|
60
|
+
* never destroy the shape directly.
|
|
61
|
+
* - The preallocated tick-loop temporaries described above (`tmpPos`/
|
|
62
|
+
* `tmpVel` in `buildClothGrid`, `tmpPos`/`identityQuat` in
|
|
63
|
+
* `buildKinematicCollider`) are plain (non-refcounted) `Vec3`/`RVec3`/
|
|
64
|
+
* `Quat` wrappers reused for the object's whole lifetime — freed in that
|
|
65
|
+
* object's own `dispose()`.
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
import type JoltNS from 'jolt-physics/wasm-compat';
|
|
69
|
+
import * as THREE from 'three';
|
|
70
|
+
|
|
71
|
+
/** The type of an ALREADY-INITIALIZED `jolt-physics/wasm-compat` module
|
|
72
|
+
* (i.e. what `await JoltInit()` resolves to) — the parameter type every
|
|
73
|
+
* function below takes instead of importing/initializing Jolt itself. */
|
|
74
|
+
export type JoltModule = typeof JoltNS;
|
|
75
|
+
|
|
76
|
+
const LAYER_NON_MOVING = 0;
|
|
77
|
+
const LAYER_MOVING = 1;
|
|
78
|
+
const NUM_OBJECT_LAYERS = 2;
|
|
79
|
+
const NUM_BROAD_PHASE_LAYERS = 2;
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Jolt world (the minimal two-layer broadphase boilerplate every Jolt scene
|
|
83
|
+
// needs, regardless of what bodies it holds)
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
export interface JoltWorld {
|
|
87
|
+
jolt: JoltModule;
|
|
88
|
+
joltInterface: JoltNS.JoltInterface;
|
|
89
|
+
physicsSystem: JoltNS.PhysicsSystem;
|
|
90
|
+
bodyInterface: JoltNS.BodyInterface;
|
|
91
|
+
/** The single "moving" object layer every body built through this world's
|
|
92
|
+
* helpers is placed on (cloth + kinematic colliders all collide with
|
|
93
|
+
* each other; nothing here needs more than one moving layer). */
|
|
94
|
+
movingLayer: number;
|
|
95
|
+
/** Advance the simulation by `dt` seconds (`options.collisionSteps` Jolt
|
|
96
|
+
* collision sub-steps). Position any kinematic bodies (colliders, cloth
|
|
97
|
+
* pins) for this frame BEFORE calling `step` — this is what
|
|
98
|
+
* `ClothGrid.tick` does internally. */
|
|
99
|
+
step(dt: number): void;
|
|
100
|
+
dispose(): void;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface JoltWorldOptions {
|
|
104
|
+
gravity?: { x: number; y: number; z: number };
|
|
105
|
+
/** Jolt collision sub-steps per `step()` call. Soft-body contact has no
|
|
106
|
+
* CCD — a cloth vertex moving faster than its collision radius per
|
|
107
|
+
* collision step TUNNELS through kinematic colliders (measured live on a
|
|
108
|
+
* whipping cape). 2 halves the per-step travel for ~2x the (sub-ms) sim
|
|
109
|
+
* cost. @default 1 */
|
|
110
|
+
collisionSteps?: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function createJoltWorld(jolt: JoltModule, options: JoltWorldOptions = {}): JoltWorld {
|
|
114
|
+
const gravity = options.gravity ?? { x: 0, y: -9.81, z: 0 };
|
|
115
|
+
const collisionSteps = options.collisionSteps ?? 1;
|
|
116
|
+
const objectFilter = new jolt.ObjectLayerPairFilterTable(NUM_OBJECT_LAYERS);
|
|
117
|
+
objectFilter.EnableCollision(LAYER_MOVING, LAYER_MOVING);
|
|
118
|
+
const bpInterface = new jolt.BroadPhaseLayerInterfaceTable(
|
|
119
|
+
NUM_OBJECT_LAYERS,
|
|
120
|
+
NUM_BROAD_PHASE_LAYERS,
|
|
121
|
+
);
|
|
122
|
+
bpInterface.MapObjectToBroadPhaseLayer(
|
|
123
|
+
LAYER_NON_MOVING,
|
|
124
|
+
new jolt.BroadPhaseLayer(LAYER_NON_MOVING),
|
|
125
|
+
);
|
|
126
|
+
bpInterface.MapObjectToBroadPhaseLayer(LAYER_MOVING, new jolt.BroadPhaseLayer(LAYER_MOVING));
|
|
127
|
+
|
|
128
|
+
const settings = new jolt.JoltSettings();
|
|
129
|
+
settings.mObjectLayerPairFilter = objectFilter;
|
|
130
|
+
settings.mBroadPhaseLayerInterface = bpInterface;
|
|
131
|
+
settings.mObjectVsBroadPhaseLayerFilter = new jolt.ObjectVsBroadPhaseLayerFilterTable(
|
|
132
|
+
bpInterface,
|
|
133
|
+
NUM_BROAD_PHASE_LAYERS,
|
|
134
|
+
objectFilter,
|
|
135
|
+
NUM_OBJECT_LAYERS,
|
|
136
|
+
);
|
|
137
|
+
const joltInterface = new jolt.JoltInterface(settings);
|
|
138
|
+
jolt.destroy(settings);
|
|
139
|
+
const physicsSystem = joltInterface.GetPhysicsSystem();
|
|
140
|
+
physicsSystem.SetGravity(new jolt.Vec3(gravity.x, gravity.y, gravity.z));
|
|
141
|
+
const bodyInterface = physicsSystem.GetBodyInterface();
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
jolt,
|
|
145
|
+
joltInterface,
|
|
146
|
+
physicsSystem,
|
|
147
|
+
bodyInterface,
|
|
148
|
+
movingLayer: LAYER_MOVING,
|
|
149
|
+
step(dt: number): void {
|
|
150
|
+
joltInterface.Step(dt, collisionSteps);
|
|
151
|
+
},
|
|
152
|
+
dispose(): void {
|
|
153
|
+
jolt.destroy(joltInterface);
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// Kinematic colliders (torso/head/limb shapes the cloth drapes over)
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
export type KinematicColliderShape =
|
|
163
|
+
| { kind: 'sphere'; radius: number }
|
|
164
|
+
| { kind: 'capsule'; radius: number; halfHeight: number };
|
|
165
|
+
|
|
166
|
+
export interface KinematicCollider {
|
|
167
|
+
body: JoltNS.Body;
|
|
168
|
+
/** Move the collider to `worldPos` (identity rotation — these fixtures
|
|
169
|
+
* only need translation) for tick `dt`. Call BEFORE the cloth grid's
|
|
170
|
+
* `tick()` for the same frame (that is what steps the Jolt world). */
|
|
171
|
+
moveTo(worldPos: THREE.Vector3, dt: number): void;
|
|
172
|
+
dispose(): void;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function buildKinematicCollider(
|
|
176
|
+
world: JoltWorld,
|
|
177
|
+
shape: KinematicColliderShape,
|
|
178
|
+
): KinematicCollider {
|
|
179
|
+
const { jolt, bodyInterface } = world;
|
|
180
|
+
const joltShape =
|
|
181
|
+
shape.kind === 'sphere'
|
|
182
|
+
? new jolt.SphereShape(shape.radius)
|
|
183
|
+
: new jolt.CapsuleShape(shape.halfHeight, shape.radius);
|
|
184
|
+
const creation = new jolt.BodyCreationSettings(
|
|
185
|
+
joltShape,
|
|
186
|
+
new jolt.RVec3(0, 0, 0),
|
|
187
|
+
new jolt.Quat(0, 0, 0, 1),
|
|
188
|
+
jolt.EMotionType_Kinematic,
|
|
189
|
+
world.movingLayer,
|
|
190
|
+
);
|
|
191
|
+
// A per-tick-driven kinematic collider must NEVER sleep: Jolt puts a
|
|
192
|
+
// near-zero-velocity kinematic body to sleep after ~0.5 s, and
|
|
193
|
+
// `Body.MoveKinematic` (unlike the BodyInterface variant) does NOT wake
|
|
194
|
+
// it — after any brief stillness (e.g. a garment-settle warm-up) the
|
|
195
|
+
// collider would silently freeze at its old pose FOREVER while the
|
|
196
|
+
// character walks away (observed live: the whole cape sailed through the
|
|
197
|
+
// torso and every contact-robustness knob was powerless against it).
|
|
198
|
+
creation.mAllowSleeping = false;
|
|
199
|
+
const body = bodyInterface.CreateBody(creation);
|
|
200
|
+
bodyInterface.AddBody(body.GetID(), jolt.EActivation_Activate);
|
|
201
|
+
// `CreateBody` copied what it needs — free the one-shot settings struct
|
|
202
|
+
// now (same contract as `SoftBodyCreationSettings` in `buildClothGrid`).
|
|
203
|
+
// This also releases `creation`'s reference on the REFCOUNTED `joltShape`;
|
|
204
|
+
// the body holds the remaining shape ref, so `joltShape` must NOT get a
|
|
205
|
+
// `jolt.destroy` anywhere — `DestroyBody` in `dispose()` releases that
|
|
206
|
+
// last ref and Jolt deletes the shape itself (see module header).
|
|
207
|
+
jolt.destroy(creation);
|
|
208
|
+
|
|
209
|
+
// Preallocated once, reused every `moveTo` call — see module header.
|
|
210
|
+
const tmpPos = new jolt.RVec3(0, 0, 0);
|
|
211
|
+
const identityQuat = new jolt.Quat(0, 0, 0, 1);
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
body,
|
|
215
|
+
moveTo(worldPos: THREE.Vector3, dt: number): void {
|
|
216
|
+
tmpPos.Set(worldPos.x, worldPos.y, worldPos.z);
|
|
217
|
+
body.MoveKinematic(tmpPos, identityQuat, dt);
|
|
218
|
+
},
|
|
219
|
+
dispose(): void {
|
|
220
|
+
bodyInterface.RemoveBody(body.GetID());
|
|
221
|
+
bodyInterface.DestroyBody(body.GetID());
|
|
222
|
+
jolt.destroy(tmpPos);
|
|
223
|
+
jolt.destroy(identityQuat);
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// Cloth grid
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
export interface ClothPin {
|
|
233
|
+
row: number;
|
|
234
|
+
col: number;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export interface ClothGridOptions {
|
|
238
|
+
cols: number;
|
|
239
|
+
rows: number;
|
|
240
|
+
/** World-space rest position for grid vertex (row, col) — row 0 is
|
|
241
|
+
* conventionally the top/pinned edge, but that's a caller convention,
|
|
242
|
+
* not enforced here. */
|
|
243
|
+
restPosition(row: number, col: number): THREE.Vector3;
|
|
244
|
+
/** Which vertices are pinned (kinematically driven every tick, infinite
|
|
245
|
+
* mass). Default: every vertex in row 0. */
|
|
246
|
+
pins?: ClothPin[];
|
|
247
|
+
vertexMassKg?: number;
|
|
248
|
+
compliance?: number;
|
|
249
|
+
shearCompliance?: number;
|
|
250
|
+
bendCompliance?: number;
|
|
251
|
+
numIterations?: number;
|
|
252
|
+
gravityFactor?: number;
|
|
253
|
+
/** Collision radius of every cloth vertex (Jolt `mVertexRadius`), metres.
|
|
254
|
+
* The default 0 lets a pressed vertex sit exactly ON (and, under
|
|
255
|
+
* sustained gait-driven contact, sink INTO) a collider surface; a few cm
|
|
256
|
+
* here keeps the cloth standing off colliders and makes contact
|
|
257
|
+
* resolution far more robust against sinking. @default 0 (Jolt's own) */
|
|
258
|
+
vertexRadius?: number;
|
|
259
|
+
/** Jolt `mLinearDamping` for the soft body (higher = calmer cloth).
|
|
260
|
+
* @default Jolt's own default */
|
|
261
|
+
linearDamping?: number;
|
|
262
|
+
color?: number;
|
|
263
|
+
/** @default true */
|
|
264
|
+
doubleSided?: boolean;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export interface ClothGrid {
|
|
268
|
+
cols: number;
|
|
269
|
+
rows: number;
|
|
270
|
+
vertexCount: number;
|
|
271
|
+
/** Pin list this grid was built with, in the order `tick()`'s
|
|
272
|
+
* `pinTargets` must match. */
|
|
273
|
+
pins: ClothPin[];
|
|
274
|
+
mesh: THREE.Mesh;
|
|
275
|
+
geometry: THREE.BufferGeometry;
|
|
276
|
+
/** One tick: overwrite each pinned vertex's position/velocity from
|
|
277
|
+
* `pinTargets[i]` (world-space, matching `pins` order — velocity is
|
|
278
|
+
* finite-differenced against the previous call), STEP the owning
|
|
279
|
+
* `JoltWorld` (this is the only thing in this module that calls
|
|
280
|
+
* `world.step`, so build at most one `ClothGrid` per `JoltWorld` unless
|
|
281
|
+
* you step the world yourself and skip that side effect), then sync
|
|
282
|
+
* `geometry`'s `position` attribute + recompute normals from the
|
|
283
|
+
* simulated vertices. Position any kinematic colliders for this frame
|
|
284
|
+
* BEFORE calling this. */
|
|
285
|
+
tick(dt: number, pinTargets: readonly THREE.Vector3[]): void;
|
|
286
|
+
/** World-space position of every vertex, row-major (`row * cols + col`).
|
|
287
|
+
* Reuses `out` if given (avoids an allocation for callers sampling every
|
|
288
|
+
* frame, e.g. a numeric collision-clearance check). */
|
|
289
|
+
samplePoints(out?: THREE.Vector3[]): THREE.Vector3[];
|
|
290
|
+
dispose(): void;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
type GridIndex = (row: number, col: number) => number;
|
|
294
|
+
|
|
295
|
+
/** Build the `SoftBodySharedSettings` (vertex grid + face list + edge/shear/
|
|
296
|
+
* bend constraints) for a `cols`x`rows` cloth. Split out of `buildClothGrid`
|
|
297
|
+
* purely to keep that function's complexity down — not reused elsewhere. */
|
|
298
|
+
function buildSoftBodySharedSettings(
|
|
299
|
+
jolt: JoltModule,
|
|
300
|
+
options: ClothGridOptions,
|
|
301
|
+
pins: ClothPin[],
|
|
302
|
+
idx: GridIndex,
|
|
303
|
+
): JoltNS.SoftBodySharedSettings {
|
|
304
|
+
const { cols, rows } = options;
|
|
305
|
+
const vertexMassKg = options.vertexMassKg ?? 0.02;
|
|
306
|
+
const pinSet = new Set(pins.map((p) => idx(p.row, p.col)));
|
|
307
|
+
|
|
308
|
+
const sharedSettings = new jolt.SoftBodySharedSettings();
|
|
309
|
+
const vertices = sharedSettings.mVertices;
|
|
310
|
+
for (let row = 0; row < rows; row++) {
|
|
311
|
+
for (let col = 0; col < cols; col++) {
|
|
312
|
+
const p = options.restPosition(row, col);
|
|
313
|
+
const v = new jolt.SoftBodySharedSettingsVertex();
|
|
314
|
+
const pos = new jolt.Float3(p.x, p.y, p.z);
|
|
315
|
+
v.mPosition = pos;
|
|
316
|
+
v.mInvMass = pinSet.has(idx(row, col)) ? 0 : 1 / vertexMassKg;
|
|
317
|
+
vertices.push_back(v);
|
|
318
|
+
// `push_back` (like the property-setter assignment above it) copies
|
|
319
|
+
// BY VALUE into `sharedSettings`'s own storage — Jolt's own
|
|
320
|
+
// `soft-body-creator.js` sample reuses/destroys a single `v` the same
|
|
321
|
+
// way across its whole grid loop — so both temporaries are safe to
|
|
322
|
+
// free the instant the copy has happened.
|
|
323
|
+
jolt.destroy(pos);
|
|
324
|
+
jolt.destroy(v);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
sharedSettings.mVertices = vertices;
|
|
328
|
+
|
|
329
|
+
const faces = sharedSettings.mFaces;
|
|
330
|
+
for (let row = 0; row < rows - 1; row++) {
|
|
331
|
+
for (let col = 0; col < cols - 1; col++) {
|
|
332
|
+
const faceA = new jolt.SoftBodySharedSettingsFace(
|
|
333
|
+
idx(row, col),
|
|
334
|
+
idx(row + 1, col),
|
|
335
|
+
idx(row, col + 1),
|
|
336
|
+
0,
|
|
337
|
+
);
|
|
338
|
+
faces.push_back(faceA);
|
|
339
|
+
jolt.destroy(faceA);
|
|
340
|
+
const faceB = new jolt.SoftBodySharedSettingsFace(
|
|
341
|
+
idx(row, col + 1),
|
|
342
|
+
idx(row + 1, col),
|
|
343
|
+
idx(row + 1, col + 1),
|
|
344
|
+
0,
|
|
345
|
+
);
|
|
346
|
+
faces.push_back(faceB);
|
|
347
|
+
jolt.destroy(faceB);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
sharedSettings.mFaces = faces;
|
|
351
|
+
|
|
352
|
+
const vertexAttr = new jolt.SoftBodySharedSettingsVertexAttributes();
|
|
353
|
+
vertexAttr.mCompliance = options.compliance ?? 0.0002;
|
|
354
|
+
vertexAttr.mShearCompliance = options.shearCompliance ?? 0.0002;
|
|
355
|
+
vertexAttr.mBendCompliance = options.bendCompliance ?? 0.002;
|
|
356
|
+
const vertexAttrArr = new jolt.ArraySoftBodySharedSettingsVertexAttributes();
|
|
357
|
+
vertexAttrArr.push_back(vertexAttr);
|
|
358
|
+
// Same value-copy-then-free story as above: `CreateConstraints` below
|
|
359
|
+
// reads from `vertexAttrArr`'s OWN backing copy (via `.data()`), not this
|
|
360
|
+
// original wrapper, so it's safe to free right after the push_back.
|
|
361
|
+
jolt.destroy(vertexAttr);
|
|
362
|
+
sharedSettings.CreateConstraints(
|
|
363
|
+
vertexAttrArr.data(),
|
|
364
|
+
vertexAttrArr.size(),
|
|
365
|
+
jolt.SoftBodySharedSettings_EBendType_Distance,
|
|
366
|
+
);
|
|
367
|
+
sharedSettings.Optimize();
|
|
368
|
+
jolt.destroy(vertexAttrArr);
|
|
369
|
+
return sharedSettings;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Build the renderable (non-skinned) mesh: a plain `BufferGeometry` whose
|
|
373
|
+
* `position` attribute `ClothGrid.tick` overwrites every frame. */
|
|
374
|
+
function buildClothMesh(
|
|
375
|
+
scene: THREE.Scene,
|
|
376
|
+
options: ClothGridOptions,
|
|
377
|
+
idx: GridIndex,
|
|
378
|
+
): {
|
|
379
|
+
mesh: THREE.Mesh;
|
|
380
|
+
geometry: THREE.BufferGeometry;
|
|
381
|
+
positionAttr: THREE.BufferAttribute;
|
|
382
|
+
material: THREE.MeshStandardMaterial;
|
|
383
|
+
} {
|
|
384
|
+
const { cols, rows } = options;
|
|
385
|
+
const geometry = new THREE.BufferGeometry();
|
|
386
|
+
const positionAttr = new THREE.BufferAttribute(new Float32Array(rows * cols * 3), 3);
|
|
387
|
+
positionAttr.setUsage(THREE.DynamicDrawUsage);
|
|
388
|
+
geometry.setAttribute('position', positionAttr);
|
|
389
|
+
const indices: number[] = [];
|
|
390
|
+
for (let row = 0; row < rows - 1; row++) {
|
|
391
|
+
for (let col = 0; col < cols - 1; col++) {
|
|
392
|
+
indices.push(idx(row, col), idx(row + 1, col), idx(row, col + 1));
|
|
393
|
+
indices.push(idx(row, col + 1), idx(row + 1, col), idx(row + 1, col + 1));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
geometry.setIndex(indices);
|
|
397
|
+
const material = new THREE.MeshStandardMaterial({
|
|
398
|
+
color: options.color ?? 0x7a2f3a,
|
|
399
|
+
roughness: 0.85,
|
|
400
|
+
side: options.doubleSided === false ? THREE.FrontSide : THREE.DoubleSide,
|
|
401
|
+
});
|
|
402
|
+
const mesh = new THREE.Mesh(geometry, material);
|
|
403
|
+
mesh.castShadow = true;
|
|
404
|
+
mesh.frustumCulled = false;
|
|
405
|
+
scene.add(mesh);
|
|
406
|
+
return { mesh, geometry, positionAttr, material };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export function buildClothGrid(
|
|
410
|
+
world: JoltWorld,
|
|
411
|
+
scene: THREE.Scene,
|
|
412
|
+
options: ClothGridOptions,
|
|
413
|
+
): ClothGrid {
|
|
414
|
+
const { jolt, bodyInterface } = world;
|
|
415
|
+
const { cols, rows } = options;
|
|
416
|
+
const pins = options.pins ?? Array.from({ length: cols }, (_, col) => ({ row: 0, col }));
|
|
417
|
+
const idx: GridIndex = (row, col) => row * cols + col;
|
|
418
|
+
|
|
419
|
+
const sharedSettings = buildSoftBodySharedSettings(jolt, options, pins, idx);
|
|
420
|
+
|
|
421
|
+
const bodyCreation = new jolt.SoftBodyCreationSettings(
|
|
422
|
+
sharedSettings,
|
|
423
|
+
new jolt.RVec3(0, 0, 0),
|
|
424
|
+
new jolt.Quat(0, 0, 0, 1),
|
|
425
|
+
world.movingLayer,
|
|
426
|
+
);
|
|
427
|
+
bodyCreation.mNumIterations = options.numIterations ?? 4;
|
|
428
|
+
if (options.vertexRadius !== undefined) bodyCreation.mVertexRadius = options.vertexRadius;
|
|
429
|
+
if (options.linearDamping !== undefined) bodyCreation.mLinearDamping = options.linearDamping;
|
|
430
|
+
bodyCreation.mPressure = 0;
|
|
431
|
+
bodyCreation.mGravityFactor = options.gravityFactor ?? 1;
|
|
432
|
+
bodyCreation.mFacesDoubleSided = true;
|
|
433
|
+
bodyCreation.mUpdatePosition = false;
|
|
434
|
+
// Kinematically-pinned cloth must never sleep either: `tick()` drives the
|
|
435
|
+
// pins by writing `SoftBodyVertex.mPosition/.mVelocity` DIRECTLY, which
|
|
436
|
+
// does not count as activity — a cape that settles still for ~0.5 s would
|
|
437
|
+
// sleep and stay frozen while its pins walk away.
|
|
438
|
+
bodyCreation.mAllowSleeping = false;
|
|
439
|
+
const clothBody = bodyInterface.CreateSoftBody(bodyCreation);
|
|
440
|
+
bodyInterface.AddBody(clothBody.GetID(), jolt.EActivation_Activate);
|
|
441
|
+
// `CreateSoftBody` copies what it needs out of `bodyCreation` into the
|
|
442
|
+
// body's own storage (same "settings object is a one-shot constructor
|
|
443
|
+
// argument" contract as `JoltSettings` → `JoltInterface` above) — safe to
|
|
444
|
+
// free right away, same as that pattern. `bodyCreation` itself is a plain
|
|
445
|
+
// (non-refcounted) settings struct, unlike `sharedSettings` below: freeing
|
|
446
|
+
// it here still correctly releases bodyCreation's OWN internal reference
|
|
447
|
+
// to `sharedSettings` (which the body has, by now, separately taken its
|
|
448
|
+
// own reference to via `CreateSoftBody`).
|
|
449
|
+
jolt.destroy(bodyCreation);
|
|
450
|
+
// `Body` only exposes the base `GetMotionProperties()`; soft bodies need
|
|
451
|
+
// the downcast to reach `GetVertex`/`GetVertices`.
|
|
452
|
+
const motionProperties = jolt.castObject(
|
|
453
|
+
clothBody.GetMotionProperties(),
|
|
454
|
+
jolt.SoftBodyMotionProperties,
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
const { mesh, geometry, positionAttr, material } = buildClothMesh(scene, options, idx);
|
|
458
|
+
|
|
459
|
+
// Preallocated tick-loop temporaries — see module header.
|
|
460
|
+
const tmpPos = new jolt.Vec3(0, 0, 0);
|
|
461
|
+
const tmpVel = new jolt.Vec3(0, 0, 0);
|
|
462
|
+
const prevPinWorld: THREE.Vector3[] = pins.map((p) => options.restPosition(p.row, p.col));
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
cols,
|
|
466
|
+
rows,
|
|
467
|
+
vertexCount: rows * cols,
|
|
468
|
+
pins,
|
|
469
|
+
mesh,
|
|
470
|
+
geometry,
|
|
471
|
+
tick(dt: number, pinTargets: readonly THREE.Vector3[]): void {
|
|
472
|
+
for (let i = 0; i < pins.length; i++) {
|
|
473
|
+
const target = pinTargets[i];
|
|
474
|
+
if (!target) continue;
|
|
475
|
+
const pin = pins[i]!;
|
|
476
|
+
const prev = prevPinWorld[i]!;
|
|
477
|
+
const vertex = motionProperties.GetVertex(idx(pin.row, pin.col));
|
|
478
|
+
tmpPos.Set(target.x, target.y, target.z);
|
|
479
|
+
vertex.mPosition = tmpPos;
|
|
480
|
+
if (dt > 0) {
|
|
481
|
+
tmpVel.Set((target.x - prev.x) / dt, (target.y - prev.y) / dt, (target.z - prev.z) / dt);
|
|
482
|
+
} else {
|
|
483
|
+
tmpVel.Set(0, 0, 0);
|
|
484
|
+
}
|
|
485
|
+
vertex.mVelocity = tmpVel;
|
|
486
|
+
prev.copy(target);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
world.step(dt);
|
|
490
|
+
|
|
491
|
+
const posArr = positionAttr.array as Float32Array;
|
|
492
|
+
for (let i = 0; i < rows * cols; i++) {
|
|
493
|
+
const v = motionProperties.GetVertex(i);
|
|
494
|
+
const p = v.mPosition;
|
|
495
|
+
posArr[i * 3] = p.GetX();
|
|
496
|
+
posArr[i * 3 + 1] = p.GetY();
|
|
497
|
+
posArr[i * 3 + 2] = p.GetZ();
|
|
498
|
+
}
|
|
499
|
+
positionAttr.needsUpdate = true;
|
|
500
|
+
geometry.computeVertexNormals();
|
|
501
|
+
},
|
|
502
|
+
samplePoints(out: THREE.Vector3[] = []): THREE.Vector3[] {
|
|
503
|
+
const n = rows * cols;
|
|
504
|
+
out.length = n;
|
|
505
|
+
for (let i = 0; i < n; i++) {
|
|
506
|
+
const v = motionProperties.GetVertex(i);
|
|
507
|
+
const p = v.mPosition;
|
|
508
|
+
if (!out[i]) out[i] = new THREE.Vector3();
|
|
509
|
+
out[i]!.set(p.GetX(), p.GetY(), p.GetZ());
|
|
510
|
+
}
|
|
511
|
+
return out;
|
|
512
|
+
},
|
|
513
|
+
dispose(): void {
|
|
514
|
+
bodyInterface.RemoveBody(clothBody.GetID());
|
|
515
|
+
// NO explicit `jolt.destroy(sharedSettings)` — anywhere. It is a
|
|
516
|
+
// refcounted RefTarget that Jolt frees ITSELF when its last reference
|
|
517
|
+
// drops, and in Jolt's RefTarget::Release releasing the last ref IS
|
|
518
|
+
// the delete. Refcount chain, probed live against real Jolt WASM via
|
|
519
|
+
// `GetRefCount()`: 0 after `new` → 1 after the
|
|
520
|
+
// `SoftBodyCreationSettings` ctor took its ref → 2 after
|
|
521
|
+
// `CreateSoftBody` (the body's own ref) → back to 1 after the
|
|
522
|
+
// construction-time `destroy(bodyCreation)` → `DestroyBody` below
|
|
523
|
+
// releases that last (body) reference and thereby deletes it. Any
|
|
524
|
+
// `destroy(sharedSettings)` after this line would be a use-after-free.
|
|
525
|
+
bodyInterface.DestroyBody(clothBody.GetID());
|
|
526
|
+
jolt.destroy(tmpPos);
|
|
527
|
+
jolt.destroy(tmpVel);
|
|
528
|
+
scene.remove(mesh);
|
|
529
|
+
geometry.dispose();
|
|
530
|
+
material.dispose();
|
|
531
|
+
},
|
|
532
|
+
};
|
|
533
|
+
}
|