@volter/threejs-runtime 0.5.65
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 +202 -0
- package/NOTICE +12 -0
- package/package.json +62 -0
- package/src/adapter/body-marks.ts +69 -0
- package/src/adapter/constraint.ts +78 -0
- package/src/adapter/first-party-navigation-system.ts +48 -0
- package/src/adapter/hierarchy-marks.ts +156 -0
- package/src/adapter/ingest/scene-capture.ts +865 -0
- package/src/adapter/ingest/structural-ids.ts +139 -0
- package/src/adapter/ingest/visible-capture-window.ts +302 -0
- package/src/adapter/object3d-authoring-subject.ts +50 -0
- package/src/adapter/rapier-physics-adapter.ts +226 -0
- package/src/adapter/reflection-probe.ts +75 -0
- package/src/adapter/renderer-config.ts +116 -0
- package/src/adapter/trigger-volume.ts +29 -0
- package/src/ai/navigation.ts +627 -0
- package/src/animation/animation-clock.ts +479 -0
- package/src/animation/runtime-inspection.ts +45 -0
- package/src/animation/xstate-animation-binding.ts +655 -0
- package/src/animation/xstate-animation-meta.ts +360 -0
- package/src/asset-formats/camera.ts +18 -0
- package/src/asset-formats/collider.ts +66 -0
- package/src/asset-formats/index.ts +45 -0
- package/src/asset-formats/light.ts +50 -0
- package/src/asset-formats/material.ts +93 -0
- package/src/asset-formats/mesh.ts +78 -0
- package/src/asset-formats/particles.ts +426 -0
- package/src/asset-formats/render-env.ts +318 -0
- package/src/asset-formats/tuples.ts +21 -0
- package/src/asset-loaders.ts +242 -0
- package/src/asset-parse-error.ts +29 -0
- package/src/assets.ts +68 -0
- package/src/behavior/xstate-inspection.ts +51 -0
- package/src/defaults.ts +135 -0
- package/src/ecs/object-marks.ts +75 -0
- package/src/ecs/scene-index.ts +439 -0
- package/src/ecs/scene-query.ts +43 -0
- package/src/ecs/user-data.ts +272 -0
- package/src/loader.ts +134 -0
- package/src/physics/collider-dimensions.ts +121 -0
- package/src/physics/collision-system.ts +76 -0
- package/src/physics/physics-registry.ts +83 -0
- package/src/physics/transform-writer.ts +41 -0
- package/src/render/directional-shadow-fit.ts +158 -0
- package/src/render/environment-capture.ts +133 -0
- package/src/render/ibl-override-material.ts +202 -0
- package/src/render/light-camera-factory.ts +103 -0
- package/src/render/matcap-texture.ts +92 -0
- package/src/render/material-factory.ts +201 -0
- package/src/render/mesh-shadow.ts +18 -0
- package/src/render/particles-factory.ts +589 -0
- package/src/render/render-batch-system.ts +260 -0
- package/src/render/render-features.ts +128 -0
- package/src/render/render-settings.ts +71 -0
- package/src/render/soft-particle-depth.ts +360 -0
- package/src/render/spark-renderer-lifecycle.ts +64 -0
- package/src/render/structural-signature.ts +231 -0
- package/src/render/viewport-shading.ts +163 -0
- package/src/render/voxel-surface.ts +358 -0
- package/src/setup/setup-particles.ts +32 -0
- package/src/setup/setup-physics.ts +67 -0
- package/src/setup/setup-renderer.ts +524 -0
- package/src/types-n8ao.d.ts +37 -0
- package/src/types-realism-effects.d.ts +61 -0
- package/vendor/realism-effects/LICENSE.md +21 -0
- package/vendor/realism-effects/UPSTREAM.md +19 -0
- package/vendor/realism-effects/dist/index.cjs +3447 -0
- package/vendor/realism-effects/dist/index.d.ts +59 -0
- package/vendor/realism-effects/dist/index.js +3434 -0
- package/vendor/realism-effects/package.json +23 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type * as THREE from 'three';
|
|
2
|
+
import { getUserData } from './user-data';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Runtime queries over a loaded THREE scene graph (P1.6d).
|
|
6
|
+
*
|
|
7
|
+
* These helpers walk a THREE.Object3D graph and read entity metadata off
|
|
8
|
+
* `userData`, so gameplay code can find objects by tag or name without
|
|
9
|
+
* bookkeeping its own indexes.
|
|
10
|
+
*
|
|
11
|
+
* Canonical metadata locations:
|
|
12
|
+
* - tags: `userData['tags']` (string[])
|
|
13
|
+
* - name: `Object3D.name`
|
|
14
|
+
*
|
|
15
|
+
* These are one-shot walks that notify nobody. For a repeated query, or for a
|
|
16
|
+
* signal when the answer changes, use the LIVE index instead —
|
|
17
|
+
* `ctx.sceneIndex` (`./scene-index.ts`): it reads the same `userData['tags']`
|
|
18
|
+
* storage, answers without traversing, and dispatches `entityadded`/
|
|
19
|
+
* `entityremoved`/`tagadded`/`tagremoved` on an `EventTarget`.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
function getTags(obj: THREE.Object3D): string[] {
|
|
23
|
+
const direct = getUserData(obj, 'tags');
|
|
24
|
+
return Array.isArray(direct) ? direct : [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** All objects in the graph carrying the given tag (depth-first, document order). */
|
|
28
|
+
export function queryByTag(root: THREE.Object3D, tag: string): THREE.Object3D[] {
|
|
29
|
+
const matches: THREE.Object3D[] = [];
|
|
30
|
+
root.traverse((obj) => {
|
|
31
|
+
if (getTags(obj).includes(tag)) matches.push(obj);
|
|
32
|
+
});
|
|
33
|
+
return matches;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** All objects in the graph with the given name (depth-first, document order). */
|
|
37
|
+
export function queryByName(root: THREE.Object3D, name: string): THREE.Object3D[] {
|
|
38
|
+
const matches: THREE.Object3D[] = [];
|
|
39
|
+
root.traverse((obj) => {
|
|
40
|
+
if (obj.name === name) matches.push(obj);
|
|
41
|
+
});
|
|
42
|
+
return matches;
|
|
43
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed registry for `Object3D.userData` keys used by the engine and editor.
|
|
3
|
+
*
|
|
4
|
+
* Three.js lets any code stash arbitrary values on `object.userData`, and over
|
|
5
|
+
* time the engine + editor accumulated ~20 stringly-typed keys that together
|
|
6
|
+
* form a *parallel object model* — meaning carried entirely in undocumented
|
|
7
|
+
* string literals. This module is the ONE documented place that lists every
|
|
8
|
+
* such key, its value type, and what it means. All engine/editor-internal
|
|
9
|
+
* `userData` access for these keys must go through {@link getUserData} /
|
|
10
|
+
* {@link setUserData} / {@link hasUserData} / {@link deleteUserData} so the key
|
|
11
|
+
* strings live in exactly one place and are type-checked at every call site.
|
|
12
|
+
* Structural readers may use the narrow `object-marks` accessors instead;
|
|
13
|
+
* this registry includes that module's canonical keys and value schema.
|
|
14
|
+
*
|
|
15
|
+
* NOTE: This covers engine/editor *internal* keys only. User game code in the
|
|
16
|
+
* example template may stash its own ad-hoc keys; those are out of scope.
|
|
17
|
+
*
|
|
18
|
+
* ## Key reference
|
|
19
|
+
*
|
|
20
|
+
* Serialization / identity (load-bearing — editor identity):
|
|
21
|
+
* - `entityId` — the entity's stable string id. Presence also marks an
|
|
22
|
+
* Object3D as a "real" entity (vs editor helper/env object).
|
|
23
|
+
*
|
|
24
|
+
* Runtime scene-query metadata (read by `scene-query.ts` and `scene-index.ts`):
|
|
25
|
+
* - `tags` — string[] tags for `queryByTag` and the live index.
|
|
26
|
+
* - `attributes` — per-object `Record<string, string|number|boolean>`
|
|
27
|
+
* of game-defined attributes (P2 `observe`). Written
|
|
28
|
+
* through `SceneIndex.setAttribute`, which emits an
|
|
29
|
+
* `attributechanged` signal; JSON-simple values only,
|
|
30
|
+
* so an attribute survives serialization unchanged.
|
|
31
|
+
*
|
|
32
|
+
* Runtime gameplay metadata:
|
|
33
|
+
* - `forward` — `[x,y,z]` model-space visual forward exported as
|
|
34
|
+
* ordinary glTF extras for asset-facing validation.
|
|
35
|
+
* - `navRole` — `'walkable' | 'obstacle'` navmesh role; collected at runtime.
|
|
36
|
+
* - `pivot` — `[x,y,z]` local-space pivot for rotate/scale-around-pivot.
|
|
37
|
+
* - `splineCurve` — resolved THREE curve for a spline-following object.
|
|
38
|
+
* - `_camera` — THREE.Camera owned by a camera entity (collected on load).
|
|
39
|
+
* - `_particleSystem` — three.quarks ParticleSystem (scene-sync registration + census).
|
|
40
|
+
* - `gaussianSplat` — native Spark splat metadata used for renderer discovery,
|
|
41
|
+
* inspector facts, bounds, and deterministic disposal.
|
|
42
|
+
*
|
|
43
|
+
* Animation (load-bearing — ED5 disposal contract):
|
|
44
|
+
* - `_animMixer` — THREE.AnimationMixer driving this subtree's clips.
|
|
45
|
+
* - `_animClips` — Map<string, AnimationClip> discovered on the GLTF (E5 —
|
|
46
|
+
* lets game code build its own XState-driven
|
|
47
|
+
* binding via `bindXStateAnimation` over the SAME
|
|
48
|
+
* mixer; see xstate-animation-binding.ts).
|
|
49
|
+
* - `_availableClips` — string[] of clip names discovered on the GLTF
|
|
50
|
+
* (inspector dropdown; same names as `_animClips`' keys).
|
|
51
|
+
* - `_animationRuntime` — format-neutral live native mixer/action inspection.
|
|
52
|
+
* - `_xstateBehavior` — optional live XState behavior actor inspection.
|
|
53
|
+
* - `_xstateAnimation` — compatibility handle for the optional XState→Three
|
|
54
|
+
* bridge; animation UI never discovers through it.
|
|
55
|
+
*
|
|
56
|
+
* Disposal contract:
|
|
57
|
+
* - `__sharedGeometry` — `true` when a mesh's geometry is shared/cached and MUST
|
|
58
|
+
* NOT be disposed by per-object cleanup (P0.2 contract).
|
|
59
|
+
*
|
|
60
|
+
* Editor shading overrides (editor-only, see `scene-sync.ts`):
|
|
61
|
+
* - `__shadeOrig` — original material(s) stashed before a shading override.
|
|
62
|
+
* - `__shadeUnlit` — generated unlit material(s) for the unlit view mode.
|
|
63
|
+
*
|
|
64
|
+
* Unmodified-game ingestion (editor-only, see `editor/src/ingest/`):
|
|
65
|
+
* - `__ingest` — `true` on every Object3D minted by ingestion; routes
|
|
66
|
+
* edits to live mutation.
|
|
67
|
+
* - `__ingestNextId` — monotonic id counter stashed on the captured Scene
|
|
68
|
+
* root so re-reflects keep minting unique ids.
|
|
69
|
+
*
|
|
70
|
+
* Editor scene-graph tagging (editor-only):
|
|
71
|
+
* - `engineInternal` — `true` on engine-owned infrastructure in the game scene
|
|
72
|
+
* (particle BatchedRenderer, debug-draw + its subtree).
|
|
73
|
+
* The editor's play-mode hierarchy skips these so they
|
|
74
|
+
* don't show up as selectable "entities".
|
|
75
|
+
* - `authoringRoot` — explicit opt-in for a runtime descendant to become
|
|
76
|
+
* its own authoring object instead of a part of the
|
|
77
|
+
* nearest ancestor owner.
|
|
78
|
+
* - `authoringInstance` — source OID of the custom-component callsite that
|
|
79
|
+
* rendered this existing Object3D. Used to group
|
|
80
|
+
* implementation parts without adding wrappers.
|
|
81
|
+
* - `authoringLabel` — authored literal name (or component tag fallback)
|
|
82
|
+
* for that source-backed component instance.
|
|
83
|
+
* - `authoringComponent` — exact source component identity for portable-CSF
|
|
84
|
+
* association; unlike the label, never user-facing copy.
|
|
85
|
+
* - `authoringDocument` — generic reference to the project-tool document
|
|
86
|
+
* that owns this scene instance's authored asset.
|
|
87
|
+
* - `authoringHierarchyId`, `authoringHierarchyParentId`,
|
|
88
|
+
* `authoringHierarchyOrder` — source-owned semantic identity, parent identity,
|
|
89
|
+
* and sibling ordinal shared across native surfaces.
|
|
90
|
+
* - `editorHelper` — `true` for editor-only helper objects (gizmos, wireframes).
|
|
91
|
+
*
|
|
92
|
+
* Hierarchy-presentation convention (written by GAME code, read by the editor —
|
|
93
|
+
* see `../adapter/hierarchy-marks.ts`, which is the ONLY place these two are
|
|
94
|
+
* read/written from; the `vgai` prefix marks them as the HOST's namespace on a
|
|
95
|
+
* node a game owns, unlike every other key above, which the engine/editor also
|
|
96
|
+
* write):
|
|
97
|
+
* - `vgaiComponentRoot` — display name of the component instance this subtree
|
|
98
|
+
* IS. The node renders as one collapsed, expandable
|
|
99
|
+
* row named for it.
|
|
100
|
+
* - `vgaiBuiltInternal` — `true` on the ROOT of a subtree runtime code
|
|
101
|
+
* CONSTRUCTED rather than authored (skeleton bones,
|
|
102
|
+
* particle renderers). Subtree-scoped: everything
|
|
103
|
+
* below a marked node is built-internal too.
|
|
104
|
+
* - `vgaiBodyOwner` — the rigid body that OWNS this node's pose, written
|
|
105
|
+
* by the code that attaches the body (see
|
|
106
|
+
* `../adapter/body-marks.ts`, the only reader/writer).
|
|
107
|
+
* A transform edit on such a node must be applied to
|
|
108
|
+
* the BODY, or the next physics step writes the node
|
|
109
|
+
* straight back.
|
|
110
|
+
* - `editorHelperType` — which kind of helper (lights/particles/pivot/navmesh/...).
|
|
111
|
+
* - `editorIcon` — `true` for editor billboard icon sprites.
|
|
112
|
+
* - `skeletonVisible` — per-entity editor preference for its bone overlay.
|
|
113
|
+
* - `skeletonEnabled` — resolved visibility preference on a skeleton helper.
|
|
114
|
+
* - `envObject` — `true` for environment objects (ambient light, etc.).
|
|
115
|
+
* - `reflectionProbe` — live project-owned reflection probe projected from
|
|
116
|
+
* JSX props for renderer/editor integration.
|
|
117
|
+
* - `triggerVolume` — plain `{ radius }` a GAME writes on the node that IS
|
|
118
|
+
* a trigger volume, so the editor can draw its ring
|
|
119
|
+
* (see `../adapter/trigger-volume.ts`).
|
|
120
|
+
* - `constraints` — live project-owned spatial constraints projected
|
|
121
|
+
* into the shared Inspector and viewport.
|
|
122
|
+
* - `authoringSubject` — transient identity/inspection for an ecosystem-native
|
|
123
|
+
* subject represented by an Object3D proxy.
|
|
124
|
+
* - `splineControlPoint` — index of a spline control-point drag handle.
|
|
125
|
+
* - `vcDirIdx` — view-cube face direction index (0=+X,1=-X,2=+Y,...).
|
|
126
|
+
*/
|
|
127
|
+
|
|
128
|
+
import type * as THREE from 'three';
|
|
129
|
+
import type { ParticleSystem } from 'three.quarks';
|
|
130
|
+
import type { ConstraintMark } from '../adapter/constraint';
|
|
131
|
+
import type { Object3DAuthoringSubjectMark } from '../adapter/object3d-authoring-subject';
|
|
132
|
+
import type { RapierEditableBody } from '../adapter/rapier-physics-adapter';
|
|
133
|
+
import type { ReflectionProbeMark } from '../adapter/reflection-probe';
|
|
134
|
+
import type { TriggerVolumeMark } from '../adapter/trigger-volume';
|
|
135
|
+
import type { AnimationRuntimeInspection } from '../animation/runtime-inspection';
|
|
136
|
+
import type { XStateAnimationBinding } from '../animation/xstate-animation-binding';
|
|
137
|
+
import type { XStateBehaviorInspection } from '../behavior/xstate-inspection';
|
|
138
|
+
import { ObjectMarkKeys, type ObjectMarkSchema } from './object-marks';
|
|
139
|
+
|
|
140
|
+
export type { EditorHelperType } from './object-marks';
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Maps each canonical accessor name to its value type. This is the single
|
|
144
|
+
* source of truth for what every known `userData` key holds.
|
|
145
|
+
*/
|
|
146
|
+
export interface UserDataSchema extends ObjectMarkSchema {
|
|
147
|
+
entityId: string;
|
|
148
|
+
forward: [number, number, number];
|
|
149
|
+
tags: string[];
|
|
150
|
+
attributes: Record<string, string | number | boolean>;
|
|
151
|
+
navRole: 'walkable' | 'obstacle';
|
|
152
|
+
pivot: [number, number, number];
|
|
153
|
+
splineCurve: THREE.Curve<THREE.Vector3>;
|
|
154
|
+
splineControlPoint: number;
|
|
155
|
+
_camera: THREE.Camera;
|
|
156
|
+
_particleSystem: ParticleSystem;
|
|
157
|
+
gaussianSplat: { src: string; numSplats: number };
|
|
158
|
+
_animMixer: THREE.AnimationMixer;
|
|
159
|
+
_animClips: Map<string, THREE.AnimationClip>;
|
|
160
|
+
_availableClips: string[];
|
|
161
|
+
_animationRuntime: AnimationRuntimeInspection;
|
|
162
|
+
_xstateBehavior: XStateBehaviorInspection;
|
|
163
|
+
_xstateAnimation: XStateAnimationBinding;
|
|
164
|
+
__sharedGeometry: boolean;
|
|
165
|
+
__shadeOrig: THREE.Material | THREE.Material[];
|
|
166
|
+
__shadeUnlit: THREE.Material[];
|
|
167
|
+
__ingest: boolean;
|
|
168
|
+
__ingestNextId: number;
|
|
169
|
+
authoringRoot: boolean;
|
|
170
|
+
authoringInstance: string;
|
|
171
|
+
authoringLabel: string;
|
|
172
|
+
vgaiBodyOwner: RapierEditableBody;
|
|
173
|
+
authoringComponent: string;
|
|
174
|
+
authoringDocument: {
|
|
175
|
+
readonly kind: 'project-tool';
|
|
176
|
+
readonly name: string;
|
|
177
|
+
readonly title: string;
|
|
178
|
+
};
|
|
179
|
+
/** Project-owned identity used to join authored hierarchy rows across adapter roots. */
|
|
180
|
+
authoringHierarchyId: string;
|
|
181
|
+
/** Project-owned parent identity when the authored parent can live on another surface. */
|
|
182
|
+
authoringHierarchyParentId: string;
|
|
183
|
+
/** Source-owned sibling ordinal used when hierarchy rows cross native roots. */
|
|
184
|
+
authoringHierarchyOrder: number;
|
|
185
|
+
editorIcon: boolean;
|
|
186
|
+
envObject: boolean;
|
|
187
|
+
constraints: readonly ConstraintMark[];
|
|
188
|
+
authoringSubject: Object3DAuthoringSubjectMark;
|
|
189
|
+
reflectionProbe: ReflectionProbeMark;
|
|
190
|
+
triggerVolume: TriggerVolumeMark;
|
|
191
|
+
vcDirIdx: number;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Any key in the typed registry. */
|
|
195
|
+
export type UserDataKey = keyof UserDataSchema;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Canonical name → literal `userData` string key. The accessor names match the
|
|
199
|
+
* raw string keys 1:1 (this is the documented set referenced by the AC), so the
|
|
200
|
+
* literal strings are owned here or by the included structural mark registry.
|
|
201
|
+
*/
|
|
202
|
+
export const UserDataKeys = {
|
|
203
|
+
...ObjectMarkKeys,
|
|
204
|
+
entityId: 'entityId',
|
|
205
|
+
forward: 'forward',
|
|
206
|
+
tags: 'tags',
|
|
207
|
+
attributes: 'attributes',
|
|
208
|
+
navRole: 'navRole',
|
|
209
|
+
pivot: 'pivot',
|
|
210
|
+
splineCurve: 'splineCurve',
|
|
211
|
+
splineControlPoint: 'splineControlPoint',
|
|
212
|
+
_camera: '_camera',
|
|
213
|
+
_particleSystem: '_particleSystem',
|
|
214
|
+
gaussianSplat: 'gaussianSplat',
|
|
215
|
+
_animMixer: '_animMixer',
|
|
216
|
+
_animClips: '_animClips',
|
|
217
|
+
_availableClips: '_availableClips',
|
|
218
|
+
_animationRuntime: '_animationRuntime',
|
|
219
|
+
_xstateBehavior: '_xstateBehavior',
|
|
220
|
+
_xstateAnimation: '_xstateAnimation',
|
|
221
|
+
__sharedGeometry: '__sharedGeometry',
|
|
222
|
+
__shadeOrig: '__shadeOrig',
|
|
223
|
+
__shadeUnlit: '__shadeUnlit',
|
|
224
|
+
__ingest: '__ingest',
|
|
225
|
+
__ingestNextId: '__ingestNextId',
|
|
226
|
+
authoringRoot: 'authoringRoot',
|
|
227
|
+
authoringInstance: 'authoringInstance',
|
|
228
|
+
authoringLabel: 'authoringLabel',
|
|
229
|
+
vgaiBodyOwner: 'vgaiBodyOwner',
|
|
230
|
+
authoringComponent: 'authoringComponent',
|
|
231
|
+
authoringDocument: 'authoringDocument',
|
|
232
|
+
authoringHierarchyId: 'authoringHierarchyId',
|
|
233
|
+
authoringHierarchyParentId: 'authoringHierarchyParentId',
|
|
234
|
+
authoringHierarchyOrder: 'authoringHierarchyOrder',
|
|
235
|
+
editorIcon: 'editorIcon',
|
|
236
|
+
envObject: 'envObject',
|
|
237
|
+
constraints: 'constraints',
|
|
238
|
+
authoringSubject: 'authoringSubject',
|
|
239
|
+
reflectionProbe: 'reflectionProbe',
|
|
240
|
+
triggerVolume: 'triggerVolume',
|
|
241
|
+
vcDirIdx: 'vcDirIdx',
|
|
242
|
+
} as const satisfies Record<UserDataKey, string>;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Read a typed `userData` value. Returns `undefined` when the key is unset or
|
|
246
|
+
* when `obj` is nullish (so call sites can pass an optionally-resolved object).
|
|
247
|
+
*/
|
|
248
|
+
export function getUserData<K extends UserDataKey>(
|
|
249
|
+
obj: THREE.Object3D | null | undefined,
|
|
250
|
+
key: K,
|
|
251
|
+
): UserDataSchema[K] | undefined {
|
|
252
|
+
return obj ? (obj.userData[UserDataKeys[key]] as UserDataSchema[K] | undefined) : undefined;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Write a typed `userData` value. */
|
|
256
|
+
export function setUserData<K extends UserDataKey>(
|
|
257
|
+
obj: THREE.Object3D,
|
|
258
|
+
key: K,
|
|
259
|
+
value: UserDataSchema[K],
|
|
260
|
+
): void {
|
|
261
|
+
obj.userData[UserDataKeys[key]] = value;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** True when `key` is set (not `undefined`) on the object's `userData`. */
|
|
265
|
+
export function hasUserData(obj: THREE.Object3D | null | undefined, key: UserDataKey): boolean {
|
|
266
|
+
return obj ? obj.userData[UserDataKeys[key]] !== undefined : false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Remove a `userData` key. */
|
|
270
|
+
export function deleteUserData(obj: THREE.Object3D, key: UserDataKey): void {
|
|
271
|
+
delete obj.userData[UserDataKeys[key]];
|
|
272
|
+
}
|
package/src/loader.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared asset loading infrastructure.
|
|
3
|
+
*
|
|
4
|
+
* Provides a single LoadingManager (for Three.js loaders) and resolveUrl()
|
|
5
|
+
* (for raw fetch calls) so that all engine asset requests go through one
|
|
6
|
+
* configurable URL prefix. Call setAssetPrefix() once at runtime startup.
|
|
7
|
+
*
|
|
8
|
+
* Three.js loaders constructed with `loadingManager` automatically rewrite
|
|
9
|
+
* URLs via setURLModifier(). For raw fetch() calls, wrap the URL with
|
|
10
|
+
* resolveUrl() to apply the same prefix.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as THREE from 'three';
|
|
14
|
+
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';
|
|
15
|
+
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
|
|
16
|
+
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
|
17
|
+
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
|
|
18
|
+
|
|
19
|
+
let _prefix = '/';
|
|
20
|
+
|
|
21
|
+
function rewriteUrl(url: string): string {
|
|
22
|
+
if (
|
|
23
|
+
url.startsWith('/') ||
|
|
24
|
+
url.startsWith('http') ||
|
|
25
|
+
url.startsWith('data:') ||
|
|
26
|
+
url.startsWith('blob:')
|
|
27
|
+
) {
|
|
28
|
+
return url;
|
|
29
|
+
}
|
|
30
|
+
return _prefix + url;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Shared Three.js LoadingManager — all engine loaders should use this. */
|
|
34
|
+
export const loadingManager = new THREE.LoadingManager();
|
|
35
|
+
loadingManager.setURLModifier(rewriteUrl);
|
|
36
|
+
|
|
37
|
+
/** Pre-configured TextureLoader using the shared manager. */
|
|
38
|
+
export const textureLoader = new THREE.TextureLoader(loadingManager);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Shared DRACOLoader for decoding DRACO-compressed GLTF/GLB meshes.
|
|
42
|
+
*
|
|
43
|
+
* The decoder wasm/js is vendored under
|
|
44
|
+
* packages/editor/template/public/jsm/libs/draco/gltf/ and served at the
|
|
45
|
+
* absolute runtime path '/jsm/libs/draco/gltf/' (DRACOLoader appends
|
|
46
|
+
* draco_wasm_wrapper.js / draco_decoder.wasm to this path). This is an
|
|
47
|
+
* absolute path, so it deliberately does NOT go through the asset prefix —
|
|
48
|
+
* the decoder is engine infrastructure, not a scene asset.
|
|
49
|
+
*/
|
|
50
|
+
export const dracoLoader = new DRACOLoader(loadingManager);
|
|
51
|
+
dracoLoader.setDecoderPath('/jsm/libs/draco/gltf/');
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Shared KTX2Loader for decoding KHR_texture_basisu (KTX2/Basis) textures.
|
|
55
|
+
*
|
|
56
|
+
* Vendored and served EXACTLY like the DRACO decoder above — the Basis
|
|
57
|
+
* transcoder js/wasm live in repo-root `public/jsm/libs/basis/` (the editor)
|
|
58
|
+
* and `packages/editor/template/public/jsm/libs/basis/` (a scaffolded game's
|
|
59
|
+
* own build), pinned in `vendor/upstream-assets.lock.json`, and KTX2Loader
|
|
60
|
+
* appends `basis_transcoder.js` / `.wasm` to this absolute path.
|
|
61
|
+
*
|
|
62
|
+
* Without this wiring, the Asset Budget's "Compress textures · KTX2" output
|
|
63
|
+
* is a GLB nothing in the engine can load — which is why the op and this
|
|
64
|
+
* loader landed together.
|
|
65
|
+
*/
|
|
66
|
+
export const ktx2Loader = new KTX2Loader(loadingManager);
|
|
67
|
+
ktx2Loader.setTranscoderPath('/jsm/libs/basis/');
|
|
68
|
+
|
|
69
|
+
let ktx2SupportDetected = false;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Teach the shared KTX2Loader which compressed formats THIS GPU accepts.
|
|
73
|
+
*
|
|
74
|
+
* KTX2Loader refuses to transcode until it has seen a renderer, and the
|
|
75
|
+
* answer is a property of the page's GPU rather than of any one renderer —
|
|
76
|
+
* so the FIRST renderer to exist supplies it and every later one is a no-op.
|
|
77
|
+
* Called by the engine's own renderer setup (`setup/setup-renderer.ts`) and
|
|
78
|
+
* by the editor's viewport, which between them cover every surface that
|
|
79
|
+
* parses a GLB.
|
|
80
|
+
*
|
|
81
|
+
* This sits on the MOUNT path, so it must never throw for a renderer it
|
|
82
|
+
* cannot question. `detectSupport` reads `renderer.extensions.has/get` (or
|
|
83
|
+
* `renderer.hasFeature` on a WebGPU renderer) — surface a headless stand-in
|
|
84
|
+
* (the test harnesses' fake renderers, any non-WebGL host) does not have.
|
|
85
|
+
* Such a renderer is skipped WITHOUT latching, so the first renderer that
|
|
86
|
+
* can actually answer still configures the loader for the whole page.
|
|
87
|
+
*/
|
|
88
|
+
export function detectKtx2Support(renderer: THREE.WebGLRenderer): void {
|
|
89
|
+
if (ktx2SupportDetected) return;
|
|
90
|
+
const probe = renderer as unknown as
|
|
91
|
+
| {
|
|
92
|
+
isWebGPURenderer?: boolean;
|
|
93
|
+
hasFeature?: unknown;
|
|
94
|
+
extensions?: { has?: unknown; get?: unknown };
|
|
95
|
+
}
|
|
96
|
+
| null
|
|
97
|
+
| undefined;
|
|
98
|
+
const canAnswer =
|
|
99
|
+
probe != null &&
|
|
100
|
+
(probe.isWebGPURenderer === true
|
|
101
|
+
? typeof probe.hasFeature === 'function'
|
|
102
|
+
: typeof probe.extensions?.has === 'function' && typeof probe.extensions?.get === 'function');
|
|
103
|
+
if (!canAnswer) return;
|
|
104
|
+
ktx2Loader.detectSupport(renderer);
|
|
105
|
+
ktx2SupportDetected = true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Pre-configured GLTFLoader using the shared manager, with DRACO decoding wired in. */
|
|
109
|
+
export const gltfLoader = new GLTFLoader(loadingManager);
|
|
110
|
+
gltfLoader.setDRACOLoader(dracoLoader);
|
|
111
|
+
gltfLoader.setKTX2Loader(ktx2Loader);
|
|
112
|
+
// EXT_meshopt_compression decoding (three's own bundled decoder — inline
|
|
113
|
+
// wasm, no fetch). Without this, a meshopt-compressed GLB — including the
|
|
114
|
+
// output of the editor's Asset Budget "Compress · meshopt" action (W4a M3) —
|
|
115
|
+
// throws at load time in every runtime and the editor viewport. Symmetric
|
|
116
|
+
// with the DRACO wiring above (CB2): the ONE shared loader owns codec setup.
|
|
117
|
+
gltfLoader.setMeshoptDecoder(MeshoptDecoder);
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Set the URL prefix prepended to relative asset paths.
|
|
121
|
+
* Called once by createGameRuntime(). Defaults to '/'.
|
|
122
|
+
*/
|
|
123
|
+
export function setAssetPrefix(prefix: string): void {
|
|
124
|
+
_prefix = prefix.endsWith('/') ? prefix : prefix ? `${prefix}/` : '/';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Resolve a relative asset URL using the current prefix.
|
|
129
|
+
* Use this for raw fetch() calls — Three.js loaders using
|
|
130
|
+
* `loadingManager` handle this automatically.
|
|
131
|
+
*/
|
|
132
|
+
export function resolveUrl(url: string): string {
|
|
133
|
+
return rewriteUrl(url);
|
|
134
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared world-scale collider dimension math (T1.2).
|
|
3
|
+
*
|
|
4
|
+
* Rapier colliders have no notion of a parent Object3D's `scale` — a body's
|
|
5
|
+
* shape is defined in absolute world units at creation time. Three.js meshes,
|
|
6
|
+
* by contrast, scale visually for free via the normal Object3D hierarchy. To
|
|
7
|
+
* keep the physical collider matching what's on screen, the LOADER must bake
|
|
8
|
+
* the entity's composed WORLD scale (self × every transformed ancestor) into
|
|
9
|
+
* the collider's dimensions at creation time.
|
|
10
|
+
*
|
|
11
|
+
* This is the ONE place that does that math, so every caller — the physics
|
|
12
|
+
* body creation path and the editor's collider gizmo alike — agrees on
|
|
13
|
+
* exactly the same numbers. Two copies silently agree by coincidence for
|
|
14
|
+
* uniform scale and disagree for everything else.
|
|
15
|
+
*
|
|
16
|
+
* Round colliders (ball/capsule) can only represent a uniform radius — Rapier
|
|
17
|
+
* has no ellipsoid/elliptical-capsule shape. A non-uniform world scale on a
|
|
18
|
+
* round collider's round axes throws a loud error naming the entity, rather
|
|
19
|
+
* than silently simulating (or rendering) the wrong shape.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { ColliderDescriptor } from '../asset-formats/collider';
|
|
23
|
+
import { DEFAULTS } from '../defaults';
|
|
24
|
+
|
|
25
|
+
/** Axes are considered "uniform enough" within this tolerance (float error only). */
|
|
26
|
+
const UNIFORM_SCALE_EPSILON = 1e-4;
|
|
27
|
+
|
|
28
|
+
export interface ColliderWorldDimensions {
|
|
29
|
+
/** cuboid only: per-axis half-extents, world scale applied. */
|
|
30
|
+
halfExtents?: [number, number, number];
|
|
31
|
+
/** ball/capsule only: radius, world scale applied (uniform axes required). */
|
|
32
|
+
radius?: number;
|
|
33
|
+
/** capsule only: half-height along the capsule's axis, world scale applied. */
|
|
34
|
+
halfHeight?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Compute a collider's WORLD-space dimensions given the entity's composed
|
|
39
|
+
* world scale (self × ancestors, per-axis, in the entity's own local frame,
|
|
40
|
+
* in X/Y/Z order).
|
|
41
|
+
*
|
|
42
|
+
* @param collider - the entity's authored (unscaled) collider definition.
|
|
43
|
+
* @param worldScale - composed world scale `[x, y, z]`.
|
|
44
|
+
* @param entityLabel - human-readable entity identifier, used only in thrown
|
|
45
|
+
* error messages (loud-failure naming, never silently wrong).
|
|
46
|
+
* @throws if a ball/capsule collider's round cross-section axes have a
|
|
47
|
+
* non-uniform world scale (beyond {@link UNIFORM_SCALE_EPSILON}).
|
|
48
|
+
*/
|
|
49
|
+
export function computeColliderWorldDimensions(
|
|
50
|
+
collider: ColliderDescriptor,
|
|
51
|
+
worldScale: readonly [number, number, number],
|
|
52
|
+
entityLabel: string,
|
|
53
|
+
): ColliderWorldDimensions {
|
|
54
|
+
const [sx, sy, sz] = worldScale;
|
|
55
|
+
|
|
56
|
+
switch (collider.type) {
|
|
57
|
+
case 'cuboid': {
|
|
58
|
+
const he = collider.halfExtents ?? DEFAULTS.collider.cuboid.halfExtents;
|
|
59
|
+
return { halfExtents: [he[0]! * sx, he[1]! * sy, he[2]! * sz] };
|
|
60
|
+
}
|
|
61
|
+
case 'ball': {
|
|
62
|
+
if (
|
|
63
|
+
Math.abs(sx - sy) > UNIFORM_SCALE_EPSILON ||
|
|
64
|
+
Math.abs(sy - sz) > UNIFORM_SCALE_EPSILON ||
|
|
65
|
+
Math.abs(sx - sz) > UNIFORM_SCALE_EPSILON
|
|
66
|
+
) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Entity "${entityLabel}" has a "ball" collider under a non-uniform world scale ` +
|
|
69
|
+
`[${sx}, ${sy}, ${sz}]. Rapier ball colliders are always perfect spheres — a ` +
|
|
70
|
+
`non-uniform scale cannot be represented and would silently simulate the wrong ` +
|
|
71
|
+
`shape. Use a uniform scale on this entity (and every transformed ancestor), or ` +
|
|
72
|
+
`switch to a "cuboid" collider.`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const radius = collider.radius ?? DEFAULTS.collider.ball.radius;
|
|
76
|
+
return { radius: radius * sx };
|
|
77
|
+
}
|
|
78
|
+
case 'capsule': {
|
|
79
|
+
// Rapier capsules run their half-height along the local Y axis, with a
|
|
80
|
+
// circular cross-section in the XZ plane — so only X/Z need to match.
|
|
81
|
+
if (Math.abs(sx - sz) > UNIFORM_SCALE_EPSILON) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Entity "${entityLabel}" has a "capsule" collider whose X/Z world scale differs ` +
|
|
84
|
+
`(x=${sx}, z=${sz}). Rapier capsule colliders have a circular cross-section — a ` +
|
|
85
|
+
`non-uniform X/Z scale cannot be represented and would silently simulate the ` +
|
|
86
|
+
`wrong shape. Use a uniform X/Z scale on this entity (and every transformed ` +
|
|
87
|
+
`ancestor), or switch to a "cuboid" collider.`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
const radius = collider.radius ?? DEFAULTS.collider.capsule.radius;
|
|
91
|
+
const halfHeight = collider.halfHeight ?? DEFAULTS.collider.capsule.halfHeight;
|
|
92
|
+
return { radius: radius * sx, halfHeight: halfHeight * sy };
|
|
93
|
+
}
|
|
94
|
+
default:
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Compute a collider's WORLD-scaled translation offset (sibling to
|
|
101
|
+
* {@link computeColliderWorldDimensions}, same input contract).
|
|
102
|
+
*
|
|
103
|
+
* Rapier's `ColliderDesc.setTranslation` is a raw physics-engine offset with no
|
|
104
|
+
* notion of a parent Object3D's scale — like dimensions, an authored `offset`
|
|
105
|
+
* (in the entity's own local units) must be scaled per-axis by the entity's
|
|
106
|
+
* composed world scale before being handed to Rapier, or the collider's center
|
|
107
|
+
* silently drifts away from its visual anchor point under any non-1 scale.
|
|
108
|
+
* Applies to every collider type (including trimesh, which is otherwise not
|
|
109
|
+
* handled by {@link computeColliderWorldDimensions}).
|
|
110
|
+
*
|
|
111
|
+
* Returns `undefined` when no offset is authored, mirroring the `if (col.offset)`
|
|
112
|
+
* guard call sites already use.
|
|
113
|
+
*/
|
|
114
|
+
export function computeColliderWorldOffset(
|
|
115
|
+
offset: readonly [number, number, number] | undefined,
|
|
116
|
+
worldScale: readonly [number, number, number],
|
|
117
|
+
): [number, number, number] | undefined {
|
|
118
|
+
if (!offset) return undefined;
|
|
119
|
+
const [sx, sy, sz] = worldScale;
|
|
120
|
+
return [offset[0] * sx, offset[1] * sy, offset[2] * sz];
|
|
121
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type RAPIER from '@dimforge/rapier3d-compat';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Collision event handler.
|
|
5
|
+
* Called with the two collider handles and whether contact started or stopped.
|
|
6
|
+
*/
|
|
7
|
+
export type CollisionHandler = (handleA: number, handleB: number, started: boolean) => void;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Drains the Rapier event queue each frame and dispatches collision events.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* const collisions = createCollisionSystem(rapierWorld, eventQueue);
|
|
14
|
+
* collisions.onCollision((a, b, started) => { ... });
|
|
15
|
+
* // In physics phase: rapierWorld.step(eventQueue);
|
|
16
|
+
* // In postPhysics: collisions.drain();
|
|
17
|
+
*/
|
|
18
|
+
export function createCollisionSystem(rapierWorld: RAPIER.World, eventQueue: RAPIER.EventQueue) {
|
|
19
|
+
const handlers: CollisionHandler[] = [];
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
/** Register a collision handler. */
|
|
23
|
+
onCollision(handler: CollisionHandler) {
|
|
24
|
+
handlers.push(handler);
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
/** Remove a collision handler. */
|
|
28
|
+
offCollision(handler: CollisionHandler) {
|
|
29
|
+
const idx = handlers.indexOf(handler);
|
|
30
|
+
if (idx !== -1) handlers.splice(idx, 1);
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Drain the event queue and dispatch collision events.
|
|
35
|
+
* Call this in POST_PHYSICS phase, after rapierWorld.step(eventQueue).
|
|
36
|
+
*
|
|
37
|
+
* Events are buffered first, then handlers run *after* the drain callback
|
|
38
|
+
* returns. This is required: a handler that calls back into the Rapier world
|
|
39
|
+
* (e.g. trigger dispatch resolving a collider, or an `onTriggerEnter` that
|
|
40
|
+
* spawns/removes a body) would otherwise re-enter Rapier while it is still
|
|
41
|
+
* borrowed by `drainCollisionEvents`, throwing "recursive use of an object
|
|
42
|
+
* ... unsafe aliasing in rust".
|
|
43
|
+
*/
|
|
44
|
+
drain() {
|
|
45
|
+
const events: Array<[number, number, boolean]> = [];
|
|
46
|
+
eventQueue.drainCollisionEvents((handle1, handle2, started) => {
|
|
47
|
+
events.push([handle1, handle2, started]);
|
|
48
|
+
});
|
|
49
|
+
eventQueue.drainContactForceEvents((_event) => {
|
|
50
|
+
// Contact force events available for advanced use.
|
|
51
|
+
// Game code can subscribe to these by extending this system.
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
for (const [handle1, handle2, started] of events) {
|
|
55
|
+
for (let i = 0; i < handlers.length; i++) {
|
|
56
|
+
handlers[i]!(handle1, handle2, started);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Look up the rigid body handle that owns a collider handle.
|
|
63
|
+
* To resolve a collider directly to its Object3D entity, use the physics
|
|
64
|
+
* registry's `getByColliderHandle` reverse index instead.
|
|
65
|
+
*/
|
|
66
|
+
getBodyHandleFromCollider(colliderHandle: number): number | undefined {
|
|
67
|
+
const collider = rapierWorld.getCollider(colliderHandle);
|
|
68
|
+
if (!collider) return undefined;
|
|
69
|
+
const body = collider.parent();
|
|
70
|
+
if (!body) return undefined;
|
|
71
|
+
return body.handle;
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type CollisionSystem = ReturnType<typeof createCollisionSystem>;
|