@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.
Files changed (70) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +12 -0
  3. package/package.json +62 -0
  4. package/src/adapter/body-marks.ts +69 -0
  5. package/src/adapter/constraint.ts +78 -0
  6. package/src/adapter/first-party-navigation-system.ts +48 -0
  7. package/src/adapter/hierarchy-marks.ts +156 -0
  8. package/src/adapter/ingest/scene-capture.ts +865 -0
  9. package/src/adapter/ingest/structural-ids.ts +139 -0
  10. package/src/adapter/ingest/visible-capture-window.ts +302 -0
  11. package/src/adapter/object3d-authoring-subject.ts +50 -0
  12. package/src/adapter/rapier-physics-adapter.ts +226 -0
  13. package/src/adapter/reflection-probe.ts +75 -0
  14. package/src/adapter/renderer-config.ts +116 -0
  15. package/src/adapter/trigger-volume.ts +29 -0
  16. package/src/ai/navigation.ts +627 -0
  17. package/src/animation/animation-clock.ts +479 -0
  18. package/src/animation/runtime-inspection.ts +45 -0
  19. package/src/animation/xstate-animation-binding.ts +655 -0
  20. package/src/animation/xstate-animation-meta.ts +360 -0
  21. package/src/asset-formats/camera.ts +18 -0
  22. package/src/asset-formats/collider.ts +66 -0
  23. package/src/asset-formats/index.ts +45 -0
  24. package/src/asset-formats/light.ts +50 -0
  25. package/src/asset-formats/material.ts +93 -0
  26. package/src/asset-formats/mesh.ts +78 -0
  27. package/src/asset-formats/particles.ts +426 -0
  28. package/src/asset-formats/render-env.ts +318 -0
  29. package/src/asset-formats/tuples.ts +21 -0
  30. package/src/asset-loaders.ts +242 -0
  31. package/src/asset-parse-error.ts +29 -0
  32. package/src/assets.ts +68 -0
  33. package/src/behavior/xstate-inspection.ts +51 -0
  34. package/src/defaults.ts +135 -0
  35. package/src/ecs/object-marks.ts +75 -0
  36. package/src/ecs/scene-index.ts +439 -0
  37. package/src/ecs/scene-query.ts +43 -0
  38. package/src/ecs/user-data.ts +272 -0
  39. package/src/loader.ts +134 -0
  40. package/src/physics/collider-dimensions.ts +121 -0
  41. package/src/physics/collision-system.ts +76 -0
  42. package/src/physics/physics-registry.ts +83 -0
  43. package/src/physics/transform-writer.ts +41 -0
  44. package/src/render/directional-shadow-fit.ts +158 -0
  45. package/src/render/environment-capture.ts +133 -0
  46. package/src/render/ibl-override-material.ts +202 -0
  47. package/src/render/light-camera-factory.ts +103 -0
  48. package/src/render/matcap-texture.ts +92 -0
  49. package/src/render/material-factory.ts +201 -0
  50. package/src/render/mesh-shadow.ts +18 -0
  51. package/src/render/particles-factory.ts +589 -0
  52. package/src/render/render-batch-system.ts +260 -0
  53. package/src/render/render-features.ts +128 -0
  54. package/src/render/render-settings.ts +71 -0
  55. package/src/render/soft-particle-depth.ts +360 -0
  56. package/src/render/spark-renderer-lifecycle.ts +64 -0
  57. package/src/render/structural-signature.ts +231 -0
  58. package/src/render/viewport-shading.ts +163 -0
  59. package/src/render/voxel-surface.ts +358 -0
  60. package/src/setup/setup-particles.ts +32 -0
  61. package/src/setup/setup-physics.ts +67 -0
  62. package/src/setup/setup-renderer.ts +524 -0
  63. package/src/types-n8ao.d.ts +37 -0
  64. package/src/types-realism-effects.d.ts +61 -0
  65. package/vendor/realism-effects/LICENSE.md +21 -0
  66. package/vendor/realism-effects/UPSTREAM.md +19 -0
  67. package/vendor/realism-effects/dist/index.cjs +3447 -0
  68. package/vendor/realism-effects/dist/index.d.ts +59 -0
  69. package/vendor/realism-effects/dist/index.js +3434 -0
  70. 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>;