@vgai/engine 0.4.1 → 0.5.0-canary.20260719.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 +48 -15
- package/package.json +11 -25
- package/schemas/engine-capabilities.json +10 -10
- package/schemas/entity2d.schema.json +468 -0
- package/schemas/mat.schema.json +2 -33
- package/schemas/prefab.schema.json +16 -172
- package/schemas/scn2d.schema.json +42 -23
- package/schemas/vgai-game.schema.json +34 -0
- package/schemas/vscn.schema.json +16 -172
- package/src/adapter/authoring.ts +152 -2
- package/src/adapter/colyseus-networking-adapter.ts +35 -1
- package/src/adapter/first-party-systems.ts +7 -1
- package/src/adapter/game-adapter.ts +13 -0
- package/src/adapter/index.ts +25 -0
- package/src/adapter/rapier-physics-adapter.ts +55 -2
- package/src/adapter/system-adapter.ts +249 -2
- package/src/adapter/vgai-scene-game-adapter.ts +149 -25
- package/src/ai/navigation.ts +28 -0
- package/src/animation/clip-map.ts +1 -8
- package/src/animation/theatre-director.ts +50 -0
- package/src/animation/xstate-animation-binding.ts +6 -0
- package/src/audio/audio-introspection.ts +290 -0
- package/src/audio/tone-context.ts +46 -0
- package/src/dev/chrome-trace.ts +153 -0
- package/src/dev/performance-profiler.ts +93 -6
- package/src/dev/render-debug-adapter.ts +199 -0
- package/src/dev/render-memory.ts +243 -0
- package/src/dev/webgl-frame-capture.ts +424 -0
- package/src/ecs/component-manager.ts +43 -10
- package/src/ecs/game-component.ts +39 -10
- package/src/input/input-manager.ts +24 -19
- package/src/input/input-types.ts +1 -1
- package/src/loader.ts +7 -0
- package/src/manifest/load.ts +14 -0
- package/src/manifest/schema.ts +65 -0
- package/src/react/game-state.tsx +1 -1
- package/src/render/render-batch-system.ts +26 -12
- package/src/render/spark-renderer-lifecycle.ts +64 -0
- package/src/runtime/create-runtime.ts +33 -5
- package/src/runtime/debug-bridge.ts +5 -5
- package/src/runtime/game.ts +102 -20
- package/src/runtime/mount-manifest.ts +1 -1
- package/src/runtime/render-control.ts +121 -0
- package/src/runtime/types.ts +1 -1
- package/src/scene/asset-loaders.ts +77 -3
- package/src/scene/instance-mesh.ts +25 -0
- package/src/scene/material-factory.ts +4 -15
- package/src/scene/mesh-shadow.ts +18 -0
- package/src/scene/particles-factory.ts +59 -0
- package/src/scene/scene-loader.ts +41 -16
- package/src/scene/schema/instances.ts +1 -2
- package/src/scene/schema/material.ts +83 -94
- package/src/scene/schema/mesh.ts +76 -90
- package/src/scene/schema/scene-file.ts +1 -2
- package/src/scene/user-data.ts +30 -14
- package/src/setup/setup-renderer.ts +6 -1
- package/src/world2d/asset-paths2d.ts +44 -0
- package/src/world2d/collision-2d.ts +7 -14
- package/src/world2d/entity2d-asset.ts +22 -0
- package/src/world2d/index.ts +27 -2
- package/src/world2d/physics2d-transform.ts +173 -0
- package/src/world2d/physics2d-units.ts +10 -0
- package/src/world2d/pixi-game-adapter.ts +148 -36
- package/src/world2d/scene2d-identity.ts +49 -0
- package/src/world2d/scene2d-loader.ts +243 -119
- package/src/world2d/schema/entity2d.ts +51 -33
- package/src/world2d/schema/physics2d.ts +14 -3
- package/src/world2d/schema/sprite.ts +32 -4
- package/src/world2d/schema/tilemap.ts +26 -9
- package/src/world2d/transform-writer-2d.ts +29 -11
- package/src/world2d/types.ts +21 -8
- package/src/world3d-react/behavior.tsx +138 -0
- package/src/world3d-react/engine-bridge.ts +48 -0
- package/src/world3d-react/index.ts +44 -0
- package/src/world3d-react/r3f-adapter.tsx +303 -0
- package/src/world3d-react/world-context.ts +294 -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
- package/src/character/cloth-sim.ts +0 -533
- package/src/character/spring-chain.ts +0 -307
- package/src/humanoid/body.ts +0 -663
- package/src/humanoid/clips.ts +0 -149
- package/src/humanoid/compose.ts +0 -209
- package/src/humanoid/generate.ts +0 -189
- package/src/humanoid/index.ts +0 -36
- package/src/humanoid/schema.ts +0 -108
- package/src/humanoid/skeleton.ts +0 -345
- package/src/react/humanoid-bake.document.tsx +0 -337
- package/src/scene/geometries/index.ts +0 -7
- package/src/scene/geometries/terrain.ts +0 -42
- package/src/scene/geometry-registry.ts +0 -42
- package/src/scene/instance-registry.ts +0 -84
- package/src/scene/instancers/grid.ts +0 -38
- package/src/scene/instancers/index.ts +0 -7
- package/src/scene/material-registry.ts +0 -73
- package/src/scene/materials/index.ts +0 -7
- package/src/scene/materials/water.ts +0 -56
- package/src/world2d/components-2d.ts +0 -86
- package/tools/humanoid-bake.tool.ts +0 -274
package/src/scene/user-data.ts
CHANGED
|
@@ -28,11 +28,15 @@
|
|
|
28
28
|
* `entity.components`.
|
|
29
29
|
*
|
|
30
30
|
* Runtime gameplay metadata (set by the scene loader):
|
|
31
|
+
* - `forward` — `[x,y,z]` model-space visual forward exported as
|
|
32
|
+
* ordinary glTF extras for asset-facing validation.
|
|
31
33
|
* - `navRole` — `'walkable' | 'obstacle'` navmesh role; collected at runtime.
|
|
32
34
|
* - `pivot` — `[x,y,z]` local-space pivot for rotate/scale-around-pivot.
|
|
33
35
|
* - `splineCurve` — resolved THREE curve built from the entity's spline.
|
|
34
36
|
* - `_camera` — THREE.Camera owned by a camera entity (collected on load).
|
|
35
37
|
* - `_particleSystem` — three.quarks ParticleSystem (scene-sync registration + census).
|
|
38
|
+
* - `gaussianSplat` — native Spark splat metadata used for renderer discovery,
|
|
39
|
+
* inspector facts, bounds, and deterministic disposal.
|
|
36
40
|
*
|
|
37
41
|
* Animation (load-bearing — ED5 disposal contract):
|
|
38
42
|
* - `_animMixer` — THREE.AnimationMixer driving this subtree's clips.
|
|
@@ -53,13 +57,6 @@
|
|
|
53
57
|
* - `__shadeOrig` — original material(s) stashed before a shading override.
|
|
54
58
|
* - `__shadeUnlit` — generated unlit material(s) for the unlit view mode.
|
|
55
59
|
*
|
|
56
|
-
* Custom-shader materials (see `material-registry.ts`):
|
|
57
|
-
* - `__matDef` — name of the registered custom-material definition.
|
|
58
|
-
* - `__uniforms` — authored uniform values passed to the def's update().
|
|
59
|
-
*
|
|
60
|
-
* Instancing (see `instance-registry.ts`):
|
|
61
|
-
* - `__instancer` — name of the registered instancer that built the mesh.
|
|
62
|
-
*
|
|
63
60
|
* Unmodified-game ingestion (editor-only, see `editor/src/ingest/`):
|
|
64
61
|
* - `__ingest` — `true` on every Object3D minted by ingestion (vs the
|
|
65
62
|
* first-party scene loader); routes edits to live
|
|
@@ -98,11 +95,27 @@ export type EditorHelperType =
|
|
|
98
95
|
| 'cameras'
|
|
99
96
|
| 'audio'
|
|
100
97
|
| 'colliders'
|
|
98
|
+
// Joint anchor/axis/limit gizmos (W2a) — distinct from 'colliders' so the
|
|
99
|
+
// viewport's per-type visibility filter can toggle them independently.
|
|
100
|
+
| 'joints'
|
|
101
|
+
// LOD distance rings (W2b) — one ring per `mesh.lod` level, radius = the
|
|
102
|
+
// level's camera-distance threshold in world units.
|
|
103
|
+
| 'lod'
|
|
101
104
|
| 'splines'
|
|
105
|
+
// 'particles' tags the LIVE three.quarks emitter Object3D itself (so the
|
|
106
|
+
// editor's helper machinery skips disposing it); 'particle-shape' tags the
|
|
107
|
+
// editor's emitter-shape WIREFRAME (W1b) — they must stay distinct: sweeping
|
|
108
|
+
// "particles"-tagged children would detach the running emitter, and quarks
|
|
109
|
+
// self-disposes any system whose emitter leaves the scene.
|
|
102
110
|
| 'particles'
|
|
111
|
+
| 'particle-shape'
|
|
103
112
|
| 'pivot'
|
|
104
113
|
| 'navmesh'
|
|
105
|
-
| 'skeletons'
|
|
114
|
+
| 'skeletons'
|
|
115
|
+
// Per-component viewport gizmo contributions (W3a extension contract,
|
|
116
|
+
// critique O11): objects a project GameComponent's `static editorGizmo`
|
|
117
|
+
// hook draws for entities carrying that component.
|
|
118
|
+
| 'components';
|
|
106
119
|
|
|
107
120
|
/**
|
|
108
121
|
* Maps each canonical accessor name to its value type. This is the single
|
|
@@ -111,6 +124,7 @@ export type EditorHelperType =
|
|
|
111
124
|
export interface UserDataSchema {
|
|
112
125
|
entity: SceneEntity;
|
|
113
126
|
entityId: string;
|
|
127
|
+
forward: [number, number, number];
|
|
114
128
|
tags: string[];
|
|
115
129
|
components: Record<string, unknown> | string[];
|
|
116
130
|
navRole: 'walkable' | 'obstacle';
|
|
@@ -119,6 +133,7 @@ export interface UserDataSchema {
|
|
|
119
133
|
splineControlPoint: number;
|
|
120
134
|
_camera: THREE.Camera;
|
|
121
135
|
_particleSystem: ParticleSystem;
|
|
136
|
+
gaussianSplat: { src: string; numSplats: number };
|
|
122
137
|
_animMixer: THREE.AnimationMixer;
|
|
123
138
|
_animClips: Map<string, THREE.AnimationClip>;
|
|
124
139
|
_availableClips: string[];
|
|
@@ -126,9 +141,6 @@ export interface UserDataSchema {
|
|
|
126
141
|
__sharedGeometry: boolean;
|
|
127
142
|
__shadeOrig: THREE.Material | THREE.Material[];
|
|
128
143
|
__shadeUnlit: THREE.Material[];
|
|
129
|
-
__matDef: string;
|
|
130
|
-
__uniforms: Record<string, unknown>;
|
|
131
|
-
__instancer: string;
|
|
132
144
|
__ingest: boolean;
|
|
133
145
|
__ingestNextId: number;
|
|
134
146
|
engineInternal: boolean;
|
|
@@ -140,6 +152,10 @@ export interface UserDataSchema {
|
|
|
140
152
|
skeletonEnabled: boolean;
|
|
141
153
|
envObject: boolean;
|
|
142
154
|
vcDirIdx: number;
|
|
155
|
+
/** W1a navmesh path-probe preview polyline: the routed waypoints, mirrored
|
|
156
|
+
* onto the preview line Object3D so e2e can read the path back from the
|
|
157
|
+
* scene graph (`EditorViewport.setNavPathPreview`). */
|
|
158
|
+
navPathPoints: { x: number; y: number; z: number }[];
|
|
143
159
|
}
|
|
144
160
|
|
|
145
161
|
/** Any key in the typed registry. */
|
|
@@ -153,6 +169,7 @@ export type UserDataKey = keyof UserDataSchema;
|
|
|
153
169
|
export const UserDataKeys = {
|
|
154
170
|
entity: 'entity',
|
|
155
171
|
entityId: 'entityId',
|
|
172
|
+
forward: 'forward',
|
|
156
173
|
tags: 'tags',
|
|
157
174
|
components: 'components',
|
|
158
175
|
navRole: 'navRole',
|
|
@@ -161,6 +178,7 @@ export const UserDataKeys = {
|
|
|
161
178
|
splineControlPoint: 'splineControlPoint',
|
|
162
179
|
_camera: '_camera',
|
|
163
180
|
_particleSystem: '_particleSystem',
|
|
181
|
+
gaussianSplat: 'gaussianSplat',
|
|
164
182
|
_animMixer: '_animMixer',
|
|
165
183
|
_animClips: '_animClips',
|
|
166
184
|
_availableClips: '_availableClips',
|
|
@@ -168,9 +186,6 @@ export const UserDataKeys = {
|
|
|
168
186
|
__sharedGeometry: '__sharedGeometry',
|
|
169
187
|
__shadeOrig: '__shadeOrig',
|
|
170
188
|
__shadeUnlit: '__shadeUnlit',
|
|
171
|
-
__matDef: '__matDef',
|
|
172
|
-
__uniforms: '__uniforms',
|
|
173
|
-
__instancer: '__instancer',
|
|
174
189
|
__ingest: '__ingest',
|
|
175
190
|
__ingestNextId: '__ingestNextId',
|
|
176
191
|
engineInternal: 'engineInternal',
|
|
@@ -182,6 +197,7 @@ export const UserDataKeys = {
|
|
|
182
197
|
skeletonEnabled: 'skeletonEnabled',
|
|
183
198
|
envObject: 'envObject',
|
|
184
199
|
vcDirIdx: 'vcDirIdx',
|
|
200
|
+
navPathPoints: 'navPathPoints',
|
|
185
201
|
} as const satisfies Record<UserDataKey, string>;
|
|
186
202
|
|
|
187
203
|
/**
|
|
@@ -28,8 +28,13 @@ import {
|
|
|
28
28
|
TiltShiftEffect,
|
|
29
29
|
VignetteEffect,
|
|
30
30
|
} from 'postprocessing';
|
|
31
|
-
import { MotionBlurEffect, SSGIEffect, SSREffect, VelocityDepthNormalPass } from 'realism-effects';
|
|
32
31
|
import * as THREE from 'three';
|
|
32
|
+
import {
|
|
33
|
+
MotionBlurEffect,
|
|
34
|
+
SSGIEffect,
|
|
35
|
+
SSREffect,
|
|
36
|
+
VelocityDepthNormalPass,
|
|
37
|
+
} from '../../vendor/realism-effects/dist/index.js';
|
|
33
38
|
import { type RenderScope, resolveRenderSettings } from '../render/render-settings';
|
|
34
39
|
import { DEFAULTS } from '../scene/defaults';
|
|
35
40
|
import type { SceneEnvironment } from '../scene/scene-types';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Entity2D, Entity2DAsset, Scene2DFile } from './schema/entity2d';
|
|
2
|
+
|
|
3
|
+
function visit(entities: Entity2D[], apply: (entity: Entity2D) => void): void {
|
|
4
|
+
for (const entity of entities) {
|
|
5
|
+
apply(entity);
|
|
6
|
+
if (entity.children) visit(entity.children, apply);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function collectEntity2DAssetPaths(entities: Entity2D[]): Set<string> {
|
|
11
|
+
const paths = new Set<string>();
|
|
12
|
+
visit(entities, (entity) => {
|
|
13
|
+
if (entity.sprite?.texture) paths.add(entity.sprite.texture);
|
|
14
|
+
if (entity.tilemap?.texture) paths.add(entity.tilemap.texture);
|
|
15
|
+
});
|
|
16
|
+
return paths;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function rename(entities: Entity2D[], oldPath: string, newPath: string): void {
|
|
20
|
+
visit(entities, (entity) => {
|
|
21
|
+
if (entity.sprite?.texture === oldPath) entity.sprite.texture = newPath;
|
|
22
|
+
if (entity.tilemap?.texture === oldPath) entity.tilemap.texture = newPath;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function applyScene2DAssetRename(
|
|
27
|
+
document: Scene2DFile,
|
|
28
|
+
oldPath: string,
|
|
29
|
+
newPath: string,
|
|
30
|
+
): Scene2DFile {
|
|
31
|
+
const cloned = structuredClone(document);
|
|
32
|
+
rename(cloned.entities, oldPath, newPath);
|
|
33
|
+
return cloned;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function applyEntity2DAssetRename(
|
|
37
|
+
asset: Entity2DAsset,
|
|
38
|
+
oldPath: string,
|
|
39
|
+
newPath: string,
|
|
40
|
+
): Entity2DAsset {
|
|
41
|
+
const cloned = structuredClone(asset);
|
|
42
|
+
rename([cloned.root], oldPath, newPath);
|
|
43
|
+
return cloned;
|
|
44
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type RAPIER from '@dimforge/rapier2d-compat';
|
|
2
2
|
import type { NodeOf } from '../ecs/game-component';
|
|
3
|
-
import type { GameContext } from '../runtime/types';
|
|
4
3
|
import type { Physics2DRegistry } from './physics2d-registry';
|
|
4
|
+
import type { World2DContext } from './types';
|
|
5
5
|
|
|
6
6
|
export type Collision2DHandler = (handleA: number, handleB: number, started: boolean) => void;
|
|
7
7
|
|
|
@@ -52,8 +52,8 @@ export type Collision2DSystem = ReturnType<typeof createCollision2DSystem>;
|
|
|
52
52
|
*/
|
|
53
53
|
export interface TriggerDispatchComponents {
|
|
54
54
|
getComponents(node: NodeOf<'pixijs'>): ReadonlyArray<{
|
|
55
|
-
onTriggerEnter?(other: NodeOf<'pixijs'>, ctx:
|
|
56
|
-
onTriggerExit?(other: NodeOf<'pixijs'>, ctx:
|
|
55
|
+
onTriggerEnter?(other: NodeOf<'pixijs'>, ctx: World2DContext): void;
|
|
56
|
+
onTriggerExit?(other: NodeOf<'pixijs'>, ctx: World2DContext): void;
|
|
57
57
|
}>;
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -62,21 +62,14 @@ export interface TriggerDispatchComponents {
|
|
|
62
62
|
* the GameComponents of the two involved display objects — the 2D analog of
|
|
63
63
|
* `createTriggerDispatch`. Only sensor-involved collisions dispatch triggers.
|
|
64
64
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* before passing it here; individual `GameComponent<'pixijs'>` subclasses
|
|
68
|
-
* that need real 2D fields off `ctx` widen their own lifecycle-method
|
|
69
|
-
* signature to `GameContext | World2DContext` and narrow back down (see
|
|
70
|
-
* `Camera2DFollow.update` in `./components-2d.ts` — the worked example of
|
|
71
|
-
* the pattern since the world2d-demo silo and its `first-party-scene.ts`
|
|
72
|
-
* were deleted, T6.1 slice 4). This function only ever forwards `ctx` — it
|
|
73
|
-
* never reads a field off it itself.
|
|
65
|
+
* The callback receives the native `World2DContext`; Pixi components do not
|
|
66
|
+
* widen or cast their lifecycle context to reach their own first-party APIs.
|
|
74
67
|
*/
|
|
75
68
|
export function createTriggerDispatch2D(
|
|
76
69
|
physics: Physics2DRegistry,
|
|
77
70
|
components: TriggerDispatchComponents,
|
|
78
71
|
rapierWorld: RAPIER.World,
|
|
79
|
-
ctx:
|
|
72
|
+
ctx: World2DContext,
|
|
80
73
|
): Collision2DHandler {
|
|
81
74
|
return (handleA, handleB, started) => {
|
|
82
75
|
const colA = rapierWorld.getCollider(handleA);
|
|
@@ -97,7 +90,7 @@ function notify(
|
|
|
97
90
|
self: NodeOf<'pixijs'>,
|
|
98
91
|
other: NodeOf<'pixijs'>,
|
|
99
92
|
started: boolean,
|
|
100
|
-
ctx:
|
|
93
|
+
ctx: World2DContext,
|
|
101
94
|
): void {
|
|
102
95
|
for (const inst of components.getComponents(self)) {
|
|
103
96
|
if (started) inst.onTriggerEnter?.(other, ctx);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { SceneParseError } from '../scene/parse';
|
|
2
|
+
import { type Entity2D, type Entity2DAsset, Entity2DAssetSchema } from './schema/entity2d';
|
|
3
|
+
|
|
4
|
+
/** Parse fetched reusable 2D entity bytes through the same named-error
|
|
5
|
+
* convention as every other authored project asset. */
|
|
6
|
+
export function parseEntity2DAsset(value: unknown, sourceName?: string): Entity2DAsset {
|
|
7
|
+
const result = Entity2DAssetSchema.safeParse(value);
|
|
8
|
+
if (!result.success) throw new SceneParseError(result.error.issues, sourceName);
|
|
9
|
+
return result.data;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Copy a reusable subtree into a scene. Every copy receives fresh authored
|
|
13
|
+
* identities so repeated drops cannot alias hierarchy/history targets. */
|
|
14
|
+
export function instantiateEntity2DAsset(asset: Entity2DAsset): Entity2D {
|
|
15
|
+
const root = structuredClone(asset.root);
|
|
16
|
+
const assignFreshIds = (entity: Entity2D): void => {
|
|
17
|
+
entity.id = crypto.randomUUID();
|
|
18
|
+
for (const child of entity.children ?? []) assignFreshIds(child);
|
|
19
|
+
};
|
|
20
|
+
assignFreshIds(root);
|
|
21
|
+
return root;
|
|
22
|
+
}
|
package/src/world2d/index.ts
CHANGED
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
* See docs/PIXI-WORLD2D-MAXIMAL-SPEC.json (FROZEN).
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
export {
|
|
17
|
+
applyEntity2DAssetRename,
|
|
18
|
+
applyScene2DAssetRename,
|
|
19
|
+
collectEntity2DAssetPaths,
|
|
20
|
+
} from './asset-paths2d';
|
|
16
21
|
export {
|
|
17
22
|
AuthoringAdapter2D,
|
|
18
23
|
type EditorNode2D,
|
|
@@ -27,7 +32,7 @@ export {
|
|
|
27
32
|
createCollision2DSystem,
|
|
28
33
|
createTriggerDispatch2D,
|
|
29
34
|
} from './collision-2d';
|
|
30
|
-
export {
|
|
35
|
+
export { instantiateEntity2DAsset, parseEntity2DAsset } from './entity2d-asset';
|
|
31
36
|
export {
|
|
32
37
|
type EmbedMount2D,
|
|
33
38
|
type IframeReachableMount2D,
|
|
@@ -40,6 +45,19 @@ export {
|
|
|
40
45
|
type Physics2DRefs,
|
|
41
46
|
type Physics2DRegistry,
|
|
42
47
|
} from './physics2d-registry';
|
|
48
|
+
export {
|
|
49
|
+
assertMovingPhysicsParent2D,
|
|
50
|
+
computePhysicsPose2D,
|
|
51
|
+
type PhysicsPose2D,
|
|
52
|
+
relativeTransform2D,
|
|
53
|
+
type ScaledCollider2D,
|
|
54
|
+
scaleCollider2D,
|
|
55
|
+
} from './physics2d-transform';
|
|
56
|
+
export {
|
|
57
|
+
DEFAULT_WORLD2D_GRAVITY_METERS_PER_SECOND_SQUARED,
|
|
58
|
+
DEFAULT_WORLD2D_PIXELS_PER_METER,
|
|
59
|
+
defaultWorld2DGravityY,
|
|
60
|
+
} from './physics2d-units';
|
|
43
61
|
export {
|
|
44
62
|
createWorld2DRuntime,
|
|
45
63
|
type MountedGame2D,
|
|
@@ -55,7 +73,14 @@ export {
|
|
|
55
73
|
installSceneCapture2D,
|
|
56
74
|
type SceneCapture2DHandle,
|
|
57
75
|
} from './scene-capture-2d';
|
|
58
|
-
export {
|
|
76
|
+
export { materializeScene2DEntityIds, resolveScene2DEntityIds } from './scene2d-identity';
|
|
77
|
+
export {
|
|
78
|
+
applySceneEntityTransform2D,
|
|
79
|
+
captureSceneSpriteBaseScale2D,
|
|
80
|
+
getSceneTilemapTextures2D,
|
|
81
|
+
loadScene2DFromData,
|
|
82
|
+
type Scene2DInstance,
|
|
83
|
+
} from './scene2d-loader';
|
|
59
84
|
export * from './schema/entity2d';
|
|
60
85
|
export * from './schema/physics2d';
|
|
61
86
|
export * from './schema/sprite';
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { type Container, Matrix } from 'pixi.js';
|
|
2
|
+
import type { Entity2D } from './schema/entity2d';
|
|
3
|
+
import type { SceneCollider2D } from './schema/physics2d';
|
|
4
|
+
|
|
5
|
+
const EPSILON = 1e-6;
|
|
6
|
+
|
|
7
|
+
export interface PhysicsPose2D {
|
|
8
|
+
matrix: Matrix;
|
|
9
|
+
position: [number, number];
|
|
10
|
+
rotation: number;
|
|
11
|
+
scale: [number, number];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type ScaledCollider2D =
|
|
15
|
+
| { shape: 'cuboid'; halfExtents: [number, number] }
|
|
16
|
+
| { shape: 'ball'; radius: number }
|
|
17
|
+
| { shape: 'capsule'; halfHeight: number; radius: number };
|
|
18
|
+
|
|
19
|
+
function isFiniteMatrix(matrix: Matrix): boolean {
|
|
20
|
+
return [matrix.a, matrix.b, matrix.c, matrix.d, matrix.tx, matrix.ty].every(Number.isFinite);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function linearMetrics(matrix: Matrix): {
|
|
24
|
+
determinant: number;
|
|
25
|
+
orthogonality: number;
|
|
26
|
+
scaleX: number;
|
|
27
|
+
scaleY: number;
|
|
28
|
+
} {
|
|
29
|
+
return {
|
|
30
|
+
determinant: matrix.a * matrix.d - matrix.b * matrix.c,
|
|
31
|
+
orthogonality: Math.abs(matrix.a * matrix.c + matrix.b * matrix.d),
|
|
32
|
+
scaleX: Math.hypot(matrix.a, matrix.b),
|
|
33
|
+
scaleY: Math.hypot(matrix.c, matrix.d),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function assertRigidRepresentable(matrix: Matrix, label: string): [number, number] {
|
|
38
|
+
if (!isFiniteMatrix(matrix)) {
|
|
39
|
+
throw new Error(`world2d physics entity "${label}" has a non-finite composed transform`);
|
|
40
|
+
}
|
|
41
|
+
const { determinant, orthogonality, scaleX, scaleY } = linearMetrics(matrix);
|
|
42
|
+
if (scaleX <= EPSILON || scaleY <= EPSILON || Math.abs(determinant) <= EPSILON) {
|
|
43
|
+
throw new Error(`world2d physics entity "${label}" has a singular composed transform`);
|
|
44
|
+
}
|
|
45
|
+
if (orthogonality > EPSILON * scaleX * scaleY) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`world2d physics entity "${label}" has a sheared composed transform; ` +
|
|
48
|
+
'Rapier rigid bodies cannot represent shear',
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return [scaleX, scaleY];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Matrix from `container` local coordinates into `root` coordinates. The
|
|
55
|
+
* root's own camera/pan transform is deliberately cancelled: Rapier lives in
|
|
56
|
+
* the world2d root's logical pixel space, not application/screen space. */
|
|
57
|
+
export function relativeTransform2D(container: Container, root: Container): Matrix {
|
|
58
|
+
let ancestor: Container | null = container;
|
|
59
|
+
while (ancestor && ancestor !== root) ancestor = ancestor.parent;
|
|
60
|
+
if (ancestor !== root) {
|
|
61
|
+
throw new Error('world2d physics object is not a descendant of the scene root');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const rootGlobal = root.getGlobalTransform();
|
|
65
|
+
const global = container.getGlobalTransform();
|
|
66
|
+
const origin = rootGlobal.applyInverse(global.apply({ x: 0, y: 0 }));
|
|
67
|
+
const unitX = rootGlobal.applyInverse(global.apply({ x: 1, y: 0 }));
|
|
68
|
+
const unitY = rootGlobal.applyInverse(global.apply({ x: 0, y: 1 }));
|
|
69
|
+
return new Matrix(
|
|
70
|
+
unitX.x - origin.x,
|
|
71
|
+
unitX.y - origin.y,
|
|
72
|
+
unitY.x - origin.x,
|
|
73
|
+
unitY.y - origin.y,
|
|
74
|
+
origin.x,
|
|
75
|
+
origin.y,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Moving Rapier bodies may rotate after spawn. Their Pixi parent chain must
|
|
80
|
+
* therefore be conformal (uniform scale plus rotation/reflection); otherwise a
|
|
81
|
+
* later rigid-body rotation would become shear when converted back to the
|
|
82
|
+
* parent-local display transform. Fixed bodies can retain a non-uniform parent
|
|
83
|
+
* because their authored relative rotation does not change. */
|
|
84
|
+
export function assertMovingPhysicsParent2D(
|
|
85
|
+
display: Container,
|
|
86
|
+
root: Container,
|
|
87
|
+
label: string,
|
|
88
|
+
): void {
|
|
89
|
+
const parent = display.parent;
|
|
90
|
+
if (!parent) throw new Error(`world2d physics entity "${label}" is not mounted`);
|
|
91
|
+
if (parent === root) return;
|
|
92
|
+
const parentMatrix = relativeTransform2D(parent, root);
|
|
93
|
+
const [scaleX, scaleY] = assertRigidRepresentable(parentMatrix, label);
|
|
94
|
+
if (Math.abs(scaleX - scaleY) > EPSILON * Math.max(scaleX, scaleY)) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`world2d moving physics entity "${label}" has a non-conformal parent transform ` +
|
|
97
|
+
`(composed scale [${scaleX}, ${scaleY}]); rotating a Rapier body beneath ` +
|
|
98
|
+
'non-uniform parent scale would shear the Pixi visual',
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Compose the transform Rapier actually owns for one authored entity.
|
|
104
|
+
*
|
|
105
|
+
* The parent uses its real Pixi transform so nested visuals and physics agree.
|
|
106
|
+
* The entity's own linear transform comes from `transform2d`, not from the
|
|
107
|
+
* display object's `scale`: Sprite width/height are implemented internally by
|
|
108
|
+
* Pixi as scale, but collider dimensions are already authored in pixels and
|
|
109
|
+
* must not be multiplied by that implementation detail. The body origin is
|
|
110
|
+
* the entity position (the world position of Pixi's pivot), matching the
|
|
111
|
+
* existing world2d physics contract. */
|
|
112
|
+
export function computePhysicsPose2D(
|
|
113
|
+
entity: Entity2D,
|
|
114
|
+
display: Container,
|
|
115
|
+
root: Container,
|
|
116
|
+
label = entity.name,
|
|
117
|
+
): PhysicsPose2D {
|
|
118
|
+
const parent = display.parent;
|
|
119
|
+
if (!parent) throw new Error(`world2d physics entity "${label}" is not mounted`);
|
|
120
|
+
const parentMatrix = parent === root ? new Matrix() : relativeTransform2D(parent, root);
|
|
121
|
+
const transform = entity.transform2d;
|
|
122
|
+
const position = transform?.position ?? [0, 0];
|
|
123
|
+
const scale = transform?.scale ?? [1, 1];
|
|
124
|
+
const local = new Matrix().setTransform(
|
|
125
|
+
position[0],
|
|
126
|
+
position[1],
|
|
127
|
+
0,
|
|
128
|
+
0,
|
|
129
|
+
scale[0],
|
|
130
|
+
scale[1],
|
|
131
|
+
transform?.rotation ?? 0,
|
|
132
|
+
0,
|
|
133
|
+
0,
|
|
134
|
+
);
|
|
135
|
+
const matrix = new Matrix().appendFrom(local, parentMatrix);
|
|
136
|
+
const [scaleX, scaleY] = assertRigidRepresentable(matrix, label);
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
matrix,
|
|
140
|
+
position: [matrix.tx, matrix.ty],
|
|
141
|
+
rotation: Math.atan2(matrix.b, matrix.a),
|
|
142
|
+
scale: [scaleX, scaleY],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Bake the composed visual scale into the native Rapier collider dimensions.
|
|
147
|
+
* Round shapes require uniform scale because Rapier has no ellipse collider. */
|
|
148
|
+
export function scaleCollider2D(
|
|
149
|
+
collider: SceneCollider2D,
|
|
150
|
+
scale: readonly [number, number],
|
|
151
|
+
label: string,
|
|
152
|
+
): ScaledCollider2D {
|
|
153
|
+
const [scaleX, scaleY] = scale;
|
|
154
|
+
if (collider.shape === 'cuboid') {
|
|
155
|
+
return {
|
|
156
|
+
shape: 'cuboid',
|
|
157
|
+
halfExtents: [collider.halfExtents[0] * scaleX, collider.halfExtents[1] * scaleY],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
if (Math.abs(scaleX - scaleY) > EPSILON * Math.max(scaleX, scaleY)) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
`world2d physics entity "${label}" uses non-uniform composed scale ` +
|
|
163
|
+
`[${scaleX}, ${scaleY}] with a ${collider.shape} collider; Rapier cannot represent ` +
|
|
164
|
+
'an elliptical round collider',
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
if (collider.shape === 'ball') return { shape: 'ball', radius: collider.radius * scaleX };
|
|
168
|
+
return {
|
|
169
|
+
shape: 'capsule',
|
|
170
|
+
halfHeight: collider.halfHeight * scaleY,
|
|
171
|
+
radius: collider.radius * scaleX,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Canonical Pixi-world scale: 100 authored pixels represent one physics meter. */
|
|
2
|
+
export const DEFAULT_WORLD2D_PIXELS_PER_METER = 100;
|
|
3
|
+
|
|
4
|
+
/** Earth-like downward acceleration used when a 2D scene omits gravity. */
|
|
5
|
+
export const DEFAULT_WORLD2D_GRAVITY_METERS_PER_SECOND_SQUARED = 9.8;
|
|
6
|
+
|
|
7
|
+
/** Derive the pixel-space default while preserving friendly decimal values. */
|
|
8
|
+
export function defaultWorld2DGravityY(pixelsPerMeter: number): number {
|
|
9
|
+
return (DEFAULT_WORLD2D_GRAVITY_METERS_PER_SECOND_SQUARED * 10 * pixelsPerMeter) / 10;
|
|
10
|
+
}
|