@vgai/engine 0.5.13 → 0.5.15
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/dist/adapter/adapter-module.d.ts +447 -0
- package/dist/adapter/adapter-module.d.ts.map +1 -0
- package/dist/adapter/adapter-module.js +308 -0
- package/dist/adapter/authoring.d.ts +357 -10
- package/dist/adapter/authoring.d.ts.map +1 -1
- package/dist/adapter/authoring.js +87 -1
- package/dist/adapter/constraint.d.ts +9 -3
- package/dist/adapter/constraint.d.ts.map +1 -1
- package/dist/adapter/finders/finder-result.d.ts +28 -0
- package/dist/adapter/finders/finder-result.d.ts.map +1 -0
- package/dist/adapter/finders/finder-result.js +10 -0
- package/dist/adapter/finders/index.d.ts +38 -0
- package/dist/adapter/finders/index.d.ts.map +1 -0
- package/dist/adapter/finders/index.js +41 -0
- package/dist/adapter/finders/prefabs-from-stories.d.ts +64 -0
- package/dist/adapter/finders/prefabs-from-stories.d.ts.map +1 -0
- package/dist/adapter/finders/prefabs-from-stories.js +92 -0
- package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts +67 -0
- package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts.map +1 -0
- package/dist/adapter/finders/scenes-from-entrypoint-selection.js +226 -0
- package/dist/adapter/first-party-systems.d.ts.map +1 -1
- package/dist/adapter/first-party-systems.js +29 -0
- package/dist/adapter/index.d.ts +7 -3
- package/dist/adapter/index.d.ts.map +1 -1
- package/dist/adapter/index.js +5 -2
- package/dist/adapter/ingest/contract-system-adapters.d.ts +24 -1
- package/dist/adapter/ingest/contract-system-adapters.d.ts.map +1 -1
- package/dist/adapter/ingest/contract-system-adapters.js +100 -34
- package/dist/adapter/ingest/game-contract.d.ts +188 -2
- package/dist/adapter/ingest/game-contract.d.ts.map +1 -1
- package/dist/adapter/ingest/game-contract.js +107 -0
- package/dist/adapter/ingest/mount-readiness.d.ts +84 -0
- package/dist/adapter/ingest/mount-readiness.d.ts.map +1 -0
- package/dist/adapter/ingest/mount-readiness.js +81 -0
- package/dist/adapter/ingest/scene-capture.d.ts +91 -2
- package/dist/adapter/ingest/scene-capture.d.ts.map +1 -1
- package/dist/adapter/ingest/scene-capture.js +110 -18
- package/dist/adapter/ingest/structural-ids.d.ts +11 -5
- package/dist/adapter/ingest/structural-ids.d.ts.map +1 -1
- package/dist/adapter/ingest/structural-ids.js +18 -9
- package/dist/adapter/ingest/upstream-pin.d.ts +4 -2
- package/dist/adapter/ingest/upstream-pin.d.ts.map +1 -1
- package/dist/adapter/ingest/upstream-pin.js +4 -2
- package/dist/adapter/ingest/visible-capture-window.d.ts +116 -0
- package/dist/adapter/ingest/visible-capture-window.d.ts.map +1 -0
- package/dist/adapter/ingest/visible-capture-window.js +141 -0
- package/dist/adapter/loop-gate-report.d.ts +3 -4
- package/dist/adapter/loop-gate-report.d.ts.map +1 -1
- package/dist/adapter/loop-gate-report.js +3 -4
- package/dist/adapter/root-adapter.d.ts +22 -2
- package/dist/adapter/root-adapter.d.ts.map +1 -1
- package/dist/adapter/setup-three-root-adapter.d.ts.map +1 -1
- package/dist/adapter/setup-three-root-adapter.js +15 -0
- package/dist/adapter/system-adapter.d.ts +161 -2
- package/dist/adapter/system-adapter.d.ts.map +1 -1
- package/dist/adapter/system-adapter.js +18 -1
- package/dist/animation/camera-ownership.d.ts +4 -2
- package/dist/animation/camera-ownership.d.ts.map +1 -1
- package/dist/animation/camera-ownership.js +58 -1
- package/dist/animation/runtime-inspection.d.ts +70 -0
- package/dist/animation/runtime-inspection.d.ts.map +1 -0
- package/dist/animation/runtime-inspection.js +25 -0
- package/dist/animation/xstate-animation-binding.d.ts +9 -1
- package/dist/animation/xstate-animation-binding.d.ts.map +1 -1
- package/dist/animation/xstate-animation-binding.js +48 -1
- package/dist/asset-formats/material.d.ts +1 -1
- package/dist/asset-formats/mesh.d.ts +1 -1
- package/dist/asset-formats/particles.d.ts +13 -0
- package/dist/asset-formats/particles.d.ts.map +1 -1
- package/dist/asset-formats/particles.js +28 -0
- package/dist/behavior/xstate-inspection.d.ts +24 -0
- package/dist/behavior/xstate-inspection.d.ts.map +1 -0
- package/dist/behavior/xstate-inspection.js +25 -0
- package/dist/core/game-loop.d.ts +7 -5
- package/dist/core/game-loop.d.ts.map +1 -1
- package/dist/core/game-loop.js +39 -18
- package/dist/core/types.d.ts +11 -12
- package/dist/core/types.d.ts.map +1 -1
- package/dist/data/data-asset.js +1 -1
- package/dist/dev/instruments.d.ts.map +1 -1
- package/dist/dev/instruments.js +5 -1
- package/dist/dev/render-debug-adapter.d.ts +19 -2
- package/dist/dev/render-debug-adapter.d.ts.map +1 -1
- package/dist/dev/render-debug-adapter.js +10 -0
- package/dist/ecs/user-data.d.ts +10 -2
- package/dist/ecs/user-data.d.ts.map +1 -1
- package/dist/ecs/user-data.js +6 -2
- package/dist/input/binding-identity.d.ts +10 -0
- package/dist/input/binding-identity.d.ts.map +1 -0
- package/dist/input/binding-identity.js +36 -0
- package/dist/input/input-manager.d.ts +18 -185
- package/dist/input/input-manager.d.ts.map +1 -1
- package/dist/input/input-manager.js +78 -68
- package/dist/manifest/load.d.ts +14 -0
- package/dist/manifest/load.d.ts.map +1 -1
- package/dist/manifest/load.js +6 -1
- package/dist/manifest/schema.d.ts +18 -0
- package/dist/manifest/schema.d.ts.map +1 -1
- package/dist/manifest/schema.js +65 -8
- package/dist/pixi/authoring.d.ts +27 -0
- package/dist/pixi/authoring.d.ts.map +1 -1
- package/dist/pixi/authoring.js +10 -3
- package/dist/pixi/index.d.ts +1 -1
- package/dist/pixi/index.d.ts.map +1 -1
- package/dist/pixi/index.js +1 -1
- package/dist/pixi/ingest.d.ts +8 -0
- package/dist/pixi/ingest.d.ts.map +1 -1
- package/dist/pixi/ingest.js +10 -1
- package/dist/pixi/render-pass-bracket.d.ts +56 -0
- package/dist/pixi/render-pass-bracket.d.ts.map +1 -0
- package/dist/pixi/render-pass-bracket.js +98 -0
- package/dist/pixi/scene-capture.d.ts +32 -1
- package/dist/pixi/scene-capture.d.ts.map +1 -1
- package/dist/pixi/scene-capture.js +21 -9
- package/dist/pixi/system-adapters.d.ts +29 -0
- package/dist/pixi/system-adapters.d.ts.map +1 -1
- package/dist/pixi/system-adapters.js +27 -0
- package/dist/render/particles-factory.d.ts +16 -2
- package/dist/render/particles-factory.d.ts.map +1 -1
- package/dist/render/particles-factory.js +88 -5
- package/dist/render/soft-particle-depth.d.ts +149 -0
- package/dist/render/soft-particle-depth.d.ts.map +1 -0
- package/dist/render/soft-particle-depth.js +305 -0
- package/dist/render/voxel-surface.d.ts +117 -0
- package/dist/render/voxel-surface.d.ts.map +1 -0
- package/dist/render/voxel-surface.js +263 -0
- package/dist/runtime/create-runtime.js +7 -0
- package/dist/runtime/debug-bridge.d.ts +15 -15
- package/dist/runtime/debug-bridge.d.ts.map +1 -1
- package/dist/runtime/debug-bridge.js +27 -26
- package/dist/runtime/game.d.ts.map +1 -1
- package/dist/runtime/game.js +1 -2
- package/dist/setup/setup-particles.d.ts.map +1 -1
- package/dist/setup/setup-particles.js +9 -0
- package/dist/world3d-react/r3f-adapter.d.ts.map +1 -1
- package/dist/world3d-react/r3f-adapter.js +42 -3
- package/dist/world3d-react/rapier-physics-bridge.d.ts.map +1 -1
- package/dist/world3d-react/rapier-physics-bridge.js +201 -11
- package/dist-config/config.js.map +1 -1
- package/package.json +4 -2
- package/schemas/engine-capabilities.json +12 -12
- package/schemas/vgai-project.schema.json +42 -3
- package/src/adapter/adapter-module.ts +607 -0
- package/src/adapter/authoring.ts +440 -10
- package/src/adapter/constraint.ts +10 -3
- package/src/adapter/finders/finder-result.ts +29 -0
- package/src/adapter/finders/index.ts +62 -0
- package/src/adapter/finders/prefabs-from-stories.ts +142 -0
- package/src/adapter/finders/scenes-from-entrypoint-selection.ts +304 -0
- package/src/adapter/first-party-systems.ts +30 -0
- package/src/adapter/index.ts +32 -1
- package/src/adapter/ingest/contract-system-adapters.ts +116 -32
- package/src/adapter/ingest/game-contract.ts +265 -2
- package/src/adapter/ingest/mount-readiness.ts +140 -0
- package/src/adapter/ingest/scene-capture.ts +199 -22
- package/src/adapter/ingest/structural-ids.ts +21 -9
- package/src/adapter/ingest/upstream-pin.ts +4 -2
- package/src/adapter/ingest/visible-capture-window.ts +213 -0
- package/src/adapter/loop-gate-report.ts +3 -4
- package/src/adapter/root-adapter.ts +22 -2
- package/src/adapter/setup-three-root-adapter.ts +16 -0
- package/src/adapter/system-adapter.ts +188 -2
- package/src/animation/camera-ownership.ts +68 -3
- package/src/animation/runtime-inspection.ts +104 -0
- package/src/animation/xstate-animation-binding.ts +53 -1
- package/src/asset-formats/particles.ts +31 -0
- package/src/behavior/xstate-inspection.ts +51 -0
- package/src/core/game-loop.ts +39 -17
- package/src/core/types.ts +11 -12
- package/src/data/data-asset.ts +1 -1
- package/src/dev/instruments.ts +5 -1
- package/src/dev/render-debug-adapter.ts +22 -2
- package/src/ecs/user-data.ts +10 -2
- package/src/input/binding-identity.ts +37 -0
- package/src/input/input-manager.ts +93 -68
- package/src/manifest/load.ts +23 -2
- package/src/manifest/schema.ts +77 -8
- package/src/pixi/authoring.ts +37 -5
- package/src/pixi/index.ts +2 -0
- package/src/pixi/ingest.ts +15 -2
- package/src/pixi/render-pass-bracket.ts +117 -0
- package/src/pixi/scene-capture.ts +63 -13
- package/src/pixi/system-adapters.ts +41 -0
- package/src/render/particles-factory.ts +89 -6
- package/src/render/soft-particle-depth.ts +336 -0
- package/src/render/voxel-surface.ts +358 -0
- package/src/runtime/create-runtime.ts +7 -0
- package/src/runtime/debug-bridge.ts +32 -31
- package/src/runtime/game.ts +1 -2
- package/src/setup/setup-particles.ts +9 -0
- package/src/world3d-react/r3f-adapter.tsx +48 -2
- package/src/world3d-react/rapier-physics-bridge.tsx +249 -11
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A face-culled surface mesh for a dense grid of cubic cells, as one real `THREE.BufferGeometry`.
|
|
3
|
+
*
|
|
4
|
+
* A game that stores its world as a grid of cubes — terrain a player digs, a builder's block
|
|
5
|
+
* palette, a level authored as a 3D tilemap — cannot draw a `BoxGeometry` per cell: a modest
|
|
6
|
+
* 80×7×80 volume is 44 800 boxes, and the ones buried inside the solid are invisible forever. The
|
|
7
|
+
* fix is a single surface built from only the faces that touch an empty neighbour. That is what
|
|
8
|
+
* this file is, and it is the whole of it.
|
|
9
|
+
*
|
|
10
|
+
* ## It is a HELPER, not a wrapper
|
|
11
|
+
*
|
|
12
|
+
* `buildVoxelSurface` takes plain arrays and hands back three's own `BufferGeometry`. Every call
|
|
13
|
+
* after it is three's API — pick your own material, wrap it in a `Mesh` or an `InstancedMesh`,
|
|
14
|
+
* `.translate()` it, hand it to Rapier's trimesh collider. Nothing here holds a scene, a camera, a
|
|
15
|
+
* frame loop or a cache, and nothing here is a class you inherit from.
|
|
16
|
+
*
|
|
17
|
+
* ## The rung audit that put ~200 lines here instead of a dependency (2026-08-14)
|
|
18
|
+
*
|
|
19
|
+
* Checked against the npm registry on the day this landed, not from memory:
|
|
20
|
+
*
|
|
21
|
+
* - **`voxel-mesh` 0.3.0** (voxel.js) — last publish **2013-06-13**. Its `index.js` builds a
|
|
22
|
+
* `THREE.Geometry` out of `THREE.Face3`, both REMOVED from three in r125. It cannot run against
|
|
23
|
+
* this repo's three at all.
|
|
24
|
+
* - **`voxel-mesher` 0.14.4** (2016-03-26) and **`greedy-mesher` 1.0.3** (2016-02-12) — the
|
|
25
|
+
* voxel.js meshing pair. Both are `ndarray`-shaped, untyped, and stop at a list of quads: the
|
|
26
|
+
* `BufferGeometry` is still yours to write. `voxel-mesher` additionally pulls a WebGL1 buffer
|
|
27
|
+
* stack (`gl-buffer`, `gl-vao`) that has nothing to do with three.
|
|
28
|
+
* - **`voxel-engine` 0.20.2** (2015-04-11) — a whole game framework (20 dependencies, its own
|
|
29
|
+
* camera, controls and physics). The thing this rule bans.
|
|
30
|
+
* - **`@voxelize/core` 3.0.0** (2026-08-03) — the one MAINTAINED option, and the one to say no to
|
|
31
|
+
* deliberately. It is a client for a multiplayer voxel platform: `socket.io-client`, its own
|
|
32
|
+
* wire protocol, its own physics engine and a WASM mesher fed by its own chunk/world objects.
|
|
33
|
+
* You do not call its mesher; you build your game inside it. Adopting it to draw a static grid
|
|
34
|
+
* would put a platform between the game and three.
|
|
35
|
+
*
|
|
36
|
+
* So: every maintained option is an ENGINE you build inside, and every option that is a helper died
|
|
37
|
+
* with voxel.js a decade ago against a three that no longer exists. The general shape *is* ready —
|
|
38
|
+
* "cull the hidden faces of a dense grid" has been settled since 2012 — it is the packaging that is
|
|
39
|
+
* not. Hence a helper here, in host vocabulary, that hands back three's own object.
|
|
40
|
+
*
|
|
41
|
+
* ## What is deliberately NOT here
|
|
42
|
+
*
|
|
43
|
+
* **Greedy meshing.** Merging coplanar same-type faces into larger quads is the standard next step
|
|
44
|
+
* and it does not fall out of this one: it needs a per-slice sweep with its own visited mask and
|
|
45
|
+
* its own bugs, and it changes the vertex↔cell relationship that per-face culling keeps exact.
|
|
46
|
+
* Per-face culling already removes the interior, which is the order-of-magnitude win. Add greedy
|
|
47
|
+
* meshing the day a measured frame budget asks for it, as its own function beside this one.
|
|
48
|
+
*
|
|
49
|
+
* **Lighting, ambient occlusion, textures, atlases, transparency sorting and chunking.** Each is a
|
|
50
|
+
* real decision a game makes for itself; this returns geometry and gets out of the way.
|
|
51
|
+
*/
|
|
52
|
+
import { BufferAttribute, BufferGeometry, Color } from 'three';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A dense grid of cubic cells.
|
|
56
|
+
*
|
|
57
|
+
* `cells` is indexed **Y-major, then Z, then X** — `((y * sizeZ) + z) * sizeX + x`. Y outermost is
|
|
58
|
+
* the ordering that makes each horizontal LAYER contiguous, which is what lets a mostly-flat world
|
|
59
|
+
* ship as a handful of runs (see {@link expandVoxelRuns}) instead of a megabyte of literals.
|
|
60
|
+
*/
|
|
61
|
+
export interface VoxelVolume {
|
|
62
|
+
/** Cell counts along X, Y and Z. */
|
|
63
|
+
readonly size: readonly [number, number, number];
|
|
64
|
+
/** `sizeX * sizeY * sizeZ` type ids. `0` is empty unless {@link VoxelSurfaceOptions.emptyType}. */
|
|
65
|
+
readonly cells: ArrayLike<number>;
|
|
66
|
+
/** World position of the volume's minimum corner. Defaults to the origin. */
|
|
67
|
+
readonly origin?: readonly [number, number, number];
|
|
68
|
+
/** World edge length of one cell. Defaults to 1. */
|
|
69
|
+
readonly cellSize?: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface VoxelSurfaceOptions {
|
|
73
|
+
/**
|
|
74
|
+
* Type id → `0xRRGGBB`, written into the geometry's `color` attribute. Supply it and the mesh
|
|
75
|
+
* draws with `vertexColors: true` in one draw call; omit it and the geometry carries no colour
|
|
76
|
+
* attribute at all. A solid type absent from the map is an ERROR, not a silent default: a
|
|
77
|
+
* missing colour is the shape of a forgotten row, and a mesh that quietly paints it white looks
|
|
78
|
+
* like a lighting bug three files away.
|
|
79
|
+
*/
|
|
80
|
+
readonly colors?: ReadonlyMap<number, number>;
|
|
81
|
+
/** The type id that means "no cell here". Defaults to `0`. */
|
|
82
|
+
readonly emptyType?: number;
|
|
83
|
+
/**
|
|
84
|
+
* Whether cells just outside the volume count as solid. `false` (the default) walls the volume
|
|
85
|
+
* in — the right answer for a standalone model. `true` leaves the boundary open, which is what a
|
|
86
|
+
* chunk meshed beside its neighbours wants.
|
|
87
|
+
*/
|
|
88
|
+
readonly closedBoundary?: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface VoxelSurface {
|
|
92
|
+
/** Indexed, with `position`, `normal` and (when colours were supplied) `color`. */
|
|
93
|
+
readonly geometry: BufferGeometry;
|
|
94
|
+
/** Faces that survived culling. `vertices` is `4 ×` this, `triangles` `2 ×`. */
|
|
95
|
+
readonly quads: number;
|
|
96
|
+
readonly vertices: number;
|
|
97
|
+
readonly triangles: number;
|
|
98
|
+
/** Cells whose type is not the empty type — the input's own size, for a sanity check. */
|
|
99
|
+
readonly solidCells: number;
|
|
100
|
+
/** Faces contributed per type id, in ascending id order. */
|
|
101
|
+
readonly quadsByType: ReadonlyMap<number, number>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export class VoxelSurfaceError extends Error {
|
|
105
|
+
constructor(message: string) {
|
|
106
|
+
super(message);
|
|
107
|
+
this.name = 'VoxelSurfaceError';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The six faces of the unit cell, each as four corners in counter-clockwise order seen from
|
|
113
|
+
* OUTSIDE — which is the winding three's default `FrontSide` draws.
|
|
114
|
+
*/
|
|
115
|
+
const FACES: readonly {
|
|
116
|
+
readonly normal: readonly [number, number, number];
|
|
117
|
+
readonly corners: readonly (readonly [number, number, number])[];
|
|
118
|
+
}[] = [
|
|
119
|
+
{
|
|
120
|
+
normal: [1, 0, 0],
|
|
121
|
+
corners: [
|
|
122
|
+
[1, 0, 1],
|
|
123
|
+
[1, 0, 0],
|
|
124
|
+
[1, 1, 0],
|
|
125
|
+
[1, 1, 1],
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
normal: [-1, 0, 0],
|
|
130
|
+
corners: [
|
|
131
|
+
[0, 0, 0],
|
|
132
|
+
[0, 0, 1],
|
|
133
|
+
[0, 1, 1],
|
|
134
|
+
[0, 1, 0],
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
normal: [0, 1, 0],
|
|
139
|
+
corners: [
|
|
140
|
+
[0, 1, 1],
|
|
141
|
+
[1, 1, 1],
|
|
142
|
+
[1, 1, 0],
|
|
143
|
+
[0, 1, 0],
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
normal: [0, -1, 0],
|
|
148
|
+
corners: [
|
|
149
|
+
[0, 0, 0],
|
|
150
|
+
[1, 0, 0],
|
|
151
|
+
[1, 0, 1],
|
|
152
|
+
[0, 0, 1],
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
normal: [0, 0, 1],
|
|
157
|
+
corners: [
|
|
158
|
+
[0, 0, 1],
|
|
159
|
+
[1, 0, 1],
|
|
160
|
+
[1, 1, 1],
|
|
161
|
+
[0, 1, 1],
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
normal: [0, 0, -1],
|
|
166
|
+
corners: [
|
|
167
|
+
[1, 0, 0],
|
|
168
|
+
[0, 0, 0],
|
|
169
|
+
[0, 1, 0],
|
|
170
|
+
[1, 1, 0],
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Expand `[count, type, count, type, …]` run pairs into a dense cell array.
|
|
177
|
+
*
|
|
178
|
+
* A voxel volume is enormous and almost entirely repetitive, so the compact form is what a game
|
|
179
|
+
* SHIPS — in a source file, in a save, over the wire — and the dense form is what it meshes. The
|
|
180
|
+
* total of the counts must be exactly `cellCount`: a short or long run list is a corrupt volume,
|
|
181
|
+
* and silently padding it with empties would mesh a plausible wrong world.
|
|
182
|
+
*/
|
|
183
|
+
export function expandVoxelRuns(
|
|
184
|
+
runs: ArrayLike<number>,
|
|
185
|
+
cellCount: number,
|
|
186
|
+
): Uint16Array | Uint32Array {
|
|
187
|
+
if (runs.length % 2 !== 0) {
|
|
188
|
+
throw new VoxelSurfaceError(
|
|
189
|
+
`a run list is [count, type] pairs, so its length must be even; got ${runs.length}`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
let widest = 0;
|
|
193
|
+
let total = 0;
|
|
194
|
+
for (let pair = 0; pair < runs.length; pair += 2) {
|
|
195
|
+
const count = runs[pair] as number;
|
|
196
|
+
if (!Number.isInteger(count) || count <= 0) {
|
|
197
|
+
throw new VoxelSurfaceError(`run ${pair / 2} has a count of ${count}; counts are positive`);
|
|
198
|
+
}
|
|
199
|
+
total += count;
|
|
200
|
+
widest = Math.max(widest, runs[pair + 1] as number);
|
|
201
|
+
}
|
|
202
|
+
if (total !== cellCount) {
|
|
203
|
+
throw new VoxelSurfaceError(`the runs cover ${total} cells; the volume holds ${cellCount}`);
|
|
204
|
+
}
|
|
205
|
+
const cells = widest > 0xffff ? new Uint32Array(cellCount) : new Uint16Array(cellCount);
|
|
206
|
+
let at = 0;
|
|
207
|
+
for (let pair = 0; pair < runs.length; pair += 2) {
|
|
208
|
+
const count = runs[pair] as number;
|
|
209
|
+
cells.fill(runs[pair + 1] as number, at, at + count);
|
|
210
|
+
at += count;
|
|
211
|
+
}
|
|
212
|
+
return cells;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** The volume's size, after checking that the cell array is exactly the size it claims. */
|
|
216
|
+
function assertVolumeShape(volume: VoxelVolume): readonly [number, number, number] {
|
|
217
|
+
const [sizeX, sizeY, sizeZ] = volume.size;
|
|
218
|
+
if (sizeX <= 0 || sizeY <= 0 || sizeZ <= 0) {
|
|
219
|
+
throw new VoxelSurfaceError(`a volume needs a positive size on every axis; got ${volume.size}`);
|
|
220
|
+
}
|
|
221
|
+
const cellCount = sizeX * sizeY * sizeZ;
|
|
222
|
+
if (volume.cells.length !== cellCount) {
|
|
223
|
+
throw new VoxelSurfaceError(
|
|
224
|
+
`a ${sizeX}×${sizeY}×${sizeZ} volume holds ${cellCount} cells; got ${volume.cells.length}`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
return [sizeX, sizeY, sizeZ];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Every array the walk appends to, so the per-cell step is a plain function. */
|
|
231
|
+
interface SurfaceBuffers {
|
|
232
|
+
readonly positions: number[];
|
|
233
|
+
readonly normals: number[];
|
|
234
|
+
readonly colors: number[];
|
|
235
|
+
readonly indices: number[];
|
|
236
|
+
readonly quadsByType: Map<number, number>;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** The linear-space vertex colour for one type, or `undefined` when colours were not asked for. */
|
|
240
|
+
function vertexColor(
|
|
241
|
+
colors: ReadonlyMap<number, number> | undefined,
|
|
242
|
+
type: number,
|
|
243
|
+
where: string,
|
|
244
|
+
into: Color,
|
|
245
|
+
): Color | undefined {
|
|
246
|
+
if (colors === undefined) return undefined;
|
|
247
|
+
const hex = colors.get(type);
|
|
248
|
+
if (hex === undefined) {
|
|
249
|
+
throw new VoxelSurfaceError(
|
|
250
|
+
`no colour for type ${type}, which cell ${where} holds — every solid type in the volume ` +
|
|
251
|
+
'needs a row, so a forgotten one fails here rather than drawing white',
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
// `setHex` defaults to `SRGBColorSpace` and converts INTO the working space itself, which is
|
|
255
|
+
// exactly what a material does with a colour uniform. Do not convert again on top of it: an
|
|
256
|
+
// extra `convertSRGBToLinear()` here squared the conversion and drew a mid-green world in near
|
|
257
|
+
// black, with every channel still plausible enough to read as a lighting problem.
|
|
258
|
+
return into.setHex(hex);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Append the faces of one solid cell that face an empty neighbour. Returns how many. */
|
|
262
|
+
function emitCellFaces(
|
|
263
|
+
buffers: SurfaceBuffers,
|
|
264
|
+
cell: readonly [number, number, number],
|
|
265
|
+
type: number,
|
|
266
|
+
color: Color | undefined,
|
|
267
|
+
solidAt: (x: number, y: number, z: number) => boolean,
|
|
268
|
+
place: (x: number, y: number, z: number) => [number, number, number],
|
|
269
|
+
): number {
|
|
270
|
+
const [x, y, z] = cell;
|
|
271
|
+
let emitted = 0;
|
|
272
|
+
for (const face of FACES) {
|
|
273
|
+
const [nx, ny, nz] = face.normal;
|
|
274
|
+
if (solidAt(x + nx, y + ny, z + nz)) continue;
|
|
275
|
+
const base = buffers.positions.length / 3;
|
|
276
|
+
for (const [cx, cy, cz] of face.corners) {
|
|
277
|
+
buffers.positions.push(...place(x + cx, y + cy, z + cz));
|
|
278
|
+
buffers.normals.push(nx, ny, nz);
|
|
279
|
+
if (color !== undefined) buffers.colors.push(color.r, color.g, color.b);
|
|
280
|
+
}
|
|
281
|
+
buffers.indices.push(base, base + 1, base + 2, base, base + 2, base + 3);
|
|
282
|
+
buffers.quadsByType.set(type, (buffers.quadsByType.get(type) ?? 0) + 1);
|
|
283
|
+
emitted += 1;
|
|
284
|
+
}
|
|
285
|
+
return emitted;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Build the visible surface of a voxel volume: one indexed `BufferGeometry` carrying every face
|
|
290
|
+
* that touches an empty neighbour, and nothing else.
|
|
291
|
+
*/
|
|
292
|
+
export function buildVoxelSurface(
|
|
293
|
+
volume: VoxelVolume,
|
|
294
|
+
options: VoxelSurfaceOptions = {},
|
|
295
|
+
): VoxelSurface {
|
|
296
|
+
const [sizeX, sizeY, sizeZ] = assertVolumeShape(volume);
|
|
297
|
+
const empty = options.emptyType ?? 0;
|
|
298
|
+
const openBoundary = options.closedBoundary !== true;
|
|
299
|
+
const scale = volume.cellSize ?? 1;
|
|
300
|
+
const [originX, originY, originZ] = volume.origin ?? [0, 0, 0];
|
|
301
|
+
const cells = volume.cells;
|
|
302
|
+
|
|
303
|
+
const at = (x: number, y: number, z: number): number =>
|
|
304
|
+
cells[(y * sizeZ + z) * sizeX + x] as number;
|
|
305
|
+
const solidAt = (x: number, y: number, z: number): boolean => {
|
|
306
|
+
const outsideVolume = x < 0 || y < 0 || z < 0 || x >= sizeX || y >= sizeY || z >= sizeZ;
|
|
307
|
+
return outsideVolume ? !openBoundary : at(x, y, z) !== empty;
|
|
308
|
+
};
|
|
309
|
+
const place = (x: number, y: number, z: number): [number, number, number] => [
|
|
310
|
+
originX + x * scale,
|
|
311
|
+
originY + y * scale,
|
|
312
|
+
originZ + z * scale,
|
|
313
|
+
];
|
|
314
|
+
|
|
315
|
+
const buffers: SurfaceBuffers = {
|
|
316
|
+
positions: [],
|
|
317
|
+
normals: [],
|
|
318
|
+
colors: [],
|
|
319
|
+
indices: [],
|
|
320
|
+
quadsByType: new Map(),
|
|
321
|
+
};
|
|
322
|
+
const scratch = new Color();
|
|
323
|
+
let solidCells = 0;
|
|
324
|
+
let quads = 0;
|
|
325
|
+
for (let y = 0; y < sizeY; y += 1) {
|
|
326
|
+
for (let z = 0; z < sizeZ; z += 1) {
|
|
327
|
+
for (let x = 0; x < sizeX; x += 1) {
|
|
328
|
+
const type = at(x, y, z);
|
|
329
|
+
if (type === empty) continue;
|
|
330
|
+
solidCells += 1;
|
|
331
|
+
const color = vertexColor(options.colors, type, `(${x}, ${y}, ${z})`, scratch);
|
|
332
|
+
quads += emitCellFaces(buffers, [x, y, z], type, color, solidAt, place);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const geometry = new BufferGeometry();
|
|
338
|
+
geometry.setAttribute('position', new BufferAttribute(new Float32Array(buffers.positions), 3));
|
|
339
|
+
geometry.setAttribute('normal', new BufferAttribute(new Float32Array(buffers.normals), 3));
|
|
340
|
+
if (options.colors !== undefined) {
|
|
341
|
+
geometry.setAttribute('color', new BufferAttribute(new Float32Array(buffers.colors), 3));
|
|
342
|
+
}
|
|
343
|
+
const vertices = buffers.positions.length / 3;
|
|
344
|
+
const indices =
|
|
345
|
+
vertices > 0xffff ? new Uint32Array(buffers.indices) : new Uint16Array(buffers.indices);
|
|
346
|
+
geometry.setIndex(new BufferAttribute(indices, 1));
|
|
347
|
+
geometry.computeBoundingSphere();
|
|
348
|
+
geometry.computeBoundingBox();
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
geometry,
|
|
352
|
+
quads,
|
|
353
|
+
vertices,
|
|
354
|
+
triangles: quads * 2,
|
|
355
|
+
solidCells,
|
|
356
|
+
quadsByType: new Map([...buffers.quadsByType].sort(([a], [b]) => a - b)),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
@@ -832,6 +832,13 @@ async function createRootsGameRuntime(config: RootsRuntimeConfig): Promise<GameS
|
|
|
832
832
|
const isReact = kindById.get(entry.id) === 'dom';
|
|
833
833
|
const surface = document.createElement(isReact ? 'div' : 'canvas') as HTMLElement;
|
|
834
834
|
surface.dataset['vgaiRootSurface'] = 'true';
|
|
835
|
+
// WHICH ROOT THIS SURFACE PRESENTS — the host mounted it, so the host
|
|
836
|
+
// knows, and stamping it here is the difference between a declaration and
|
|
837
|
+
// the guess every late reader had to make instead ("the first <canvas> in
|
|
838
|
+
// DOM order is the game's"). Read by `packages/editor/src/presentation-
|
|
839
|
+
// surface.ts`, which is the one door the capture/staleness/screenshot
|
|
840
|
+
// sites now ask (ARCHITECTURE-CORE §The editor protocol, zero inference).
|
|
841
|
+
surface.dataset['vgaiRootId'] = entry.id;
|
|
835
842
|
// The capture's marker (`composite-screenshot.ts` reads exactly this
|
|
836
843
|
// attribute): a dev layer is excluded from a play capture unless the
|
|
837
844
|
// capture asks for it, because a contaminated "does the game look right"
|
|
@@ -104,10 +104,10 @@ export interface VgaiDebugInputHandle {
|
|
|
104
104
|
* `state` from another). */
|
|
105
105
|
export interface VgaiDebugSnapshot {
|
|
106
106
|
/** `loopLiveness` (issue #175): the REAL `GameLoop.liveness` behind this
|
|
107
|
-
* session — `'
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
107
|
+
* session — `'loop-starved'` when no recent host rAF progress was observed,
|
|
108
|
+
* `null` only when no loop is wired at all (a bare debug-registry test
|
|
109
|
+
* stand-in with no real `Game`). Never a fabricated `'running'` and never
|
|
110
|
+
* a conclusion about tab visibility. */
|
|
111
111
|
time: { simSeconds: number; tick: number; loopLiveness: GameLoopLiveness | null };
|
|
112
112
|
state: Record<string, unknown>;
|
|
113
113
|
events: TickStampedEvent[];
|
|
@@ -233,28 +233,29 @@ const HOLD_FOR_POLL_MS = 50;
|
|
|
233
233
|
* the same neighborhood. */
|
|
234
234
|
const HOLD_FOR_STALL_POLL_LIMIT = 100;
|
|
235
235
|
|
|
236
|
-
/** Ticks driven per synchronous
|
|
236
|
+
/** Ticks driven per synchronous starved-loop batch. Sized like
|
|
237
237
|
* `fast-forward.ts`'s own batching rationale: big enough that per-batch
|
|
238
238
|
* bookkeeping is negligible, small enough that one batch of a complex game's
|
|
239
239
|
* phases stays a short synchronous burst. */
|
|
240
|
-
const
|
|
240
|
+
const STARVED_DRIVE_BATCH_TICKS = 60;
|
|
241
241
|
|
|
242
|
-
/** Hard ceiling on
|
|
242
|
+
/** Hard ceiling on starved-drive batches for ONE hold — `STARVED_DRIVE_BATCH_TICKS
|
|
243
243
|
* * this` ≈ 33 sim-minutes. A bound, not a timeout: it exists so a
|
|
244
244
|
* `runTicks` target that advances the tick counter without advancing the sim
|
|
245
245
|
* clock can never spin forever, and it is far past any honest hold. */
|
|
246
|
-
const
|
|
246
|
+
const STARVED_DRIVE_MAX_BATCHES = 2000;
|
|
247
247
|
|
|
248
|
-
/** The message a hold gets when the loop is
|
|
248
|
+
/** The message a hold gets when the loop is loop-starved and NOTHING can
|
|
249
249
|
* drive it — the one case where the platform genuinely prevents the verb
|
|
250
250
|
* from working. Names the cause and the fix rather than expiring into a
|
|
251
251
|
* generic stall (which reported the wrong cause: "play stopped during
|
|
252
252
|
* hold"). One exported constant so both doors and their tests read the same
|
|
253
253
|
* string. */
|
|
254
|
-
export const
|
|
255
|
-
|
|
256
|
-
'
|
|
257
|
-
'
|
|
254
|
+
export const HOLD_STARVED_NO_DRIVER_REASON =
|
|
255
|
+
"the host loop reported liveness 'loop-starved' and this session has no way to drive " +
|
|
256
|
+
'ticks — no recent rAF progress was observed. Check `vgai status` for the separate ' +
|
|
257
|
+
'visibility readings; foreground/reload the editor, or start play to wire the run-ticks ' +
|
|
258
|
+
'target, then retry.';
|
|
258
259
|
|
|
259
260
|
/** What `adapter.state('time')` returns for the two fields this loop reads,
|
|
260
261
|
* plus the liveness the hidden path branches on. */
|
|
@@ -279,8 +280,8 @@ interface HoldClockReading {
|
|
|
279
280
|
* `HOLD_FOR_POLL_MS` and gives up after `HOLD_FOR_STALL_POLL_LIMIT`
|
|
280
281
|
* consecutive unchanged-tick polls (`stalled: true` — play stopped/paused).
|
|
281
282
|
*
|
|
282
|
-
* - **
|
|
283
|
-
*
|
|
283
|
+
* - **Loop-starved**: no recent host rAF progress is observable, so sim time
|
|
284
|
+
* only moves reliably if this drives it. It therefore drives
|
|
284
285
|
* the WHOLE remaining budget in synchronous batches, yielding a MICROTASK
|
|
285
286
|
* between them — never a timer. That distinction is load-bearing, not
|
|
286
287
|
* stylistic: a hidden tab clamps `setTimeout` to ~1s (and to ~1/minute under
|
|
@@ -293,35 +294,35 @@ interface HoldClockReading {
|
|
|
293
294
|
* are not throttled, so the hidden path now costs sim-work time and nothing
|
|
294
295
|
* else.
|
|
295
296
|
*
|
|
296
|
-
* The regime is re-read every iteration, so
|
|
297
|
-
* mid-hold crosses over without restarting the budget.
|
|
297
|
+
* The regime is re-read every iteration, so an rAF chain that resumes or
|
|
298
|
+
* becomes starved mid-hold crosses over without restarting the budget.
|
|
298
299
|
*
|
|
299
|
-
* `
|
|
300
|
-
* no `
|
|
300
|
+
* `starvedWithoutDriver: true` is the one honest refusal: loop-starved with
|
|
301
|
+
* no `driveStarvedTicks` hook means nothing in this process can advance the
|
|
301
302
|
* clock, so it reports that IMMEDIATELY (see
|
|
302
|
-
* {@link
|
|
303
|
+
* {@link HOLD_STARVED_NO_DRIVER_REASON}) instead of burning the stall guard
|
|
303
304
|
* and then blaming a stopped game.
|
|
304
305
|
*/
|
|
305
306
|
export async function waitForHoldBudget(
|
|
306
307
|
adapter: DebugAdapter,
|
|
307
308
|
simSeconds: number,
|
|
308
|
-
|
|
309
|
-
): Promise<{ stalled: boolean;
|
|
309
|
+
driveStarvedTicks?: (n: number) => void,
|
|
310
|
+
): Promise<{ stalled: boolean; starvedWithoutDriver?: boolean }> {
|
|
310
311
|
const readTime = () => adapter.state('time') as HoldClockReading;
|
|
311
312
|
const start = readTime();
|
|
312
313
|
let lastTick = start.tick;
|
|
313
314
|
let stalledPolls = 0;
|
|
314
|
-
let
|
|
315
|
+
let starvedBatches = 0;
|
|
315
316
|
|
|
316
317
|
for (;;) {
|
|
317
318
|
const current = readTime();
|
|
318
319
|
if (current.simSeconds - start.simSeconds >= simSeconds) return { stalled: false };
|
|
319
320
|
|
|
320
|
-
if (current.loopLiveness === '
|
|
321
|
-
if (!
|
|
322
|
-
if (
|
|
323
|
-
|
|
324
|
-
|
|
321
|
+
if (current.loopLiveness === 'loop-starved') {
|
|
322
|
+
if (!driveStarvedTicks) return { stalled: true, starvedWithoutDriver: true };
|
|
323
|
+
if (starvedBatches >= STARVED_DRIVE_MAX_BATCHES) return { stalled: true };
|
|
324
|
+
starvedBatches += 1;
|
|
325
|
+
driveStarvedTicks(STARVED_DRIVE_BATCH_TICKS);
|
|
325
326
|
const after = readTime();
|
|
326
327
|
// A batch that moved neither the tick counter nor the sim clock means
|
|
327
328
|
// the drive is a no-op (paused game, torn-down runtime) — the same
|
|
@@ -333,7 +334,7 @@ export async function waitForHoldBudget(
|
|
|
333
334
|
lastTick = after.tick;
|
|
334
335
|
stalledPolls = 0;
|
|
335
336
|
// Yield so the page can service other work between batches, WITHOUT
|
|
336
|
-
// handing control to
|
|
337
|
+
// handing control to the same timer lane the browser may be starving.
|
|
337
338
|
await Promise.resolve();
|
|
338
339
|
continue;
|
|
339
340
|
}
|
|
@@ -481,7 +482,7 @@ function buildDebugHandle(opts: {
|
|
|
481
482
|
: { delivered: false };
|
|
482
483
|
}
|
|
483
484
|
const runTicksTarget = registry.getRunTicksTarget();
|
|
484
|
-
const { stalled,
|
|
485
|
+
const { stalled, starvedWithoutDriver } = await waitForHoldBudget(
|
|
485
486
|
adapter,
|
|
486
487
|
simSeconds,
|
|
487
488
|
runTicksTarget ? (n) => runTicksTarget.runTicks(n, { render: 'last' }) : undefined,
|
|
@@ -490,7 +491,7 @@ function buildDebugHandle(opts: {
|
|
|
490
491
|
if (!stalled) return { delivered: true };
|
|
491
492
|
return {
|
|
492
493
|
delivered: false,
|
|
493
|
-
reason:
|
|
494
|
+
reason: starvedWithoutDriver ? HOLD_STARVED_NO_DRIVER_REASON : 'play stopped during hold',
|
|
494
495
|
};
|
|
495
496
|
},
|
|
496
497
|
uninstall() {
|
package/src/runtime/game.ts
CHANGED
|
@@ -987,8 +987,7 @@ export function createGame(opts: {
|
|
|
987
987
|
// Other native `console.warn`/`console.error` call sites in this file are
|
|
988
988
|
// unsuppressed and already counted in the lint baseline (see `runFrame`'s
|
|
989
989
|
// impl below); this one is a NEW site, so it's suppressed to keep this
|
|
990
|
-
// task's diff at zero NEW warnings
|
|
991
|
-
// identical suppression).
|
|
990
|
+
// task's diff at zero NEW warnings.
|
|
992
991
|
// biome-ignore lint/suspicious/noConsole: see comment above
|
|
993
992
|
console.warn(message);
|
|
994
993
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type * as THREE from 'three';
|
|
2
2
|
import { BatchedRenderer } from 'three.quarks';
|
|
3
|
+
import { farDepthTexture } from '../render/soft-particle-depth';
|
|
3
4
|
|
|
4
5
|
export interface ParticlesContext {
|
|
5
6
|
batchedRenderer: BatchedRenderer;
|
|
@@ -17,6 +18,14 @@ export interface ParticlesContext {
|
|
|
17
18
|
*/
|
|
18
19
|
export function setupParticles(scene: THREE.Scene): ParticlesContext {
|
|
19
20
|
const batchedRenderer = new BatchedRenderer();
|
|
21
|
+
// Seed the soft-particle depth sampler BEFORE any batch exists, so every
|
|
22
|
+
// batch this renderer later builds inherits it (`BatchedRenderer.addSystem`
|
|
23
|
+
// applies `this.depthTexture` to each new batch). A `softParticles` batch
|
|
24
|
+
// whose `depthTexture` uniform is still `null` samples black and multiplies
|
|
25
|
+
// every particle away; the far-depth default makes that same batch render
|
|
26
|
+
// exactly as an unfaded one until a real depth pass supplies the scene's own.
|
|
27
|
+
// See `render/soft-particle-depth.ts`.
|
|
28
|
+
batchedRenderer.setDepthTexture(farDepthTexture());
|
|
20
29
|
scene.add(batchedRenderer);
|
|
21
30
|
|
|
22
31
|
return { batchedRenderer };
|
|
@@ -25,8 +25,9 @@ import {
|
|
|
25
25
|
extend,
|
|
26
26
|
events as pointerEvents,
|
|
27
27
|
type RootState,
|
|
28
|
+
useFrame,
|
|
28
29
|
} from '@react-three/fiber';
|
|
29
|
-
import { createElement, type ReactNode } from 'react';
|
|
30
|
+
import { createElement, Fragment, type ReactNode, useEffect, useMemo } from 'react';
|
|
30
31
|
import type { MountedThreeRoot, RootAdapter, ThreeHostContext } from '../adapter';
|
|
31
32
|
import { type RenderVitalsRegistration, registerRenderVitals } from '../dev/register-render-vitals';
|
|
32
33
|
import {
|
|
@@ -38,6 +39,7 @@ import { collectRenderMemory } from '../dev/render-memory';
|
|
|
38
39
|
import { RENDER_SUBMIT_PHASE } from '../dev/render-vitals';
|
|
39
40
|
import { createWebGLFrameCapture } from '../dev/webgl-frame-capture';
|
|
40
41
|
import { WorldProvider } from '../react/world-state';
|
|
42
|
+
import { createSoftParticleDepthPass } from '../render/soft-particle-depth';
|
|
41
43
|
import { devLayersEnabled } from '../runtime/dev-layers';
|
|
42
44
|
import { EngineBridge, type EngineBridgeValue } from './engine-bridge';
|
|
43
45
|
import { applyWorldRendererConfig, type WorldRendererConfig } from './renderer-config';
|
|
@@ -52,6 +54,37 @@ interface RendererCounters {
|
|
|
52
54
|
readonly memory: { readonly geometries: number; readonly textures: number };
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Scene depth for any soft-particle system this world mounted, drawn between
|
|
59
|
+
* gameplay and the draw.
|
|
60
|
+
*
|
|
61
|
+
* `useFrame`'s priority is the whole reason this is a component rather than an
|
|
62
|
+
* engine phase: `systems.run(dt)` below finishes BEFORE `advance()`, and
|
|
63
|
+
* `advance()` is where every `useFrame` gameplay callback moves the frame's
|
|
64
|
+
* transforms — depth taken from an engine phase would be one frame stale.
|
|
65
|
+
* Priority `0` puts it after every negative-priority callback (fiber sorts
|
|
66
|
+
* subscribers by priority) and before both a positive-priority composer and
|
|
67
|
+
* fiber's own `state.gl.render` — and, unlike a positive priority, it does NOT
|
|
68
|
+
* flip fiber into manual-render mode (`internal.priority` only counts
|
|
69
|
+
* subscribers with `priority > 0`), so mounting it can never stop an ordinary
|
|
70
|
+
* world from drawing itself.
|
|
71
|
+
*
|
|
72
|
+
* Rendered as the LAST child of the adapter's tree so its subscription lands
|
|
73
|
+
* after the world's own (React completes children before siblings, and fiber's
|
|
74
|
+
* sort is stable) — the tie-break among priority-0 callbacks is insertion
|
|
75
|
+
* order.
|
|
76
|
+
*
|
|
77
|
+
* Costs nothing while no soft-particle system is armed — see
|
|
78
|
+
* `../render/soft-particle-depth.ts` for what "armed" means and what the pass
|
|
79
|
+
* spends when it is.
|
|
80
|
+
*/
|
|
81
|
+
function SoftParticleDepth(): null {
|
|
82
|
+
const pass = useMemo(() => createSoftParticleDepthPass(), []);
|
|
83
|
+
useEffect(() => () => pass.dispose(), [pass]);
|
|
84
|
+
useFrame(({ gl, scene, camera }) => pass.render(gl, scene, camera), 0);
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
55
88
|
/** What {@link createR3FAdapter} needs to build one `RootAdapter`. */
|
|
56
89
|
export interface CreateR3FAdapterOptions {
|
|
57
90
|
/** Stable id (telemetry/registry/conformance) — `RootAdapter.id`. */
|
|
@@ -266,11 +299,19 @@ export function createR3FAdapter(options: CreateR3FAdapterOptions): RootAdapter
|
|
|
266
299
|
// hosts, so the provider is conditional and those hooks stay the inert
|
|
267
300
|
// no-ops they already document themselves to be.
|
|
268
301
|
const game = runtime.ctx.game;
|
|
302
|
+
// `SoftParticleDepth` trails the world's own tree deliberately — see its
|
|
303
|
+
// docblock for why the subscription must land last.
|
|
304
|
+
const world = createElement(
|
|
305
|
+
Fragment,
|
|
306
|
+
null,
|
|
307
|
+
content,
|
|
308
|
+
createElement(SoftParticleDepth, { key: 'vgai-soft-particle-depth' }),
|
|
309
|
+
);
|
|
269
310
|
root.render(
|
|
270
311
|
createElement(
|
|
271
312
|
EngineBridge.Provider,
|
|
272
313
|
{ value: bridge },
|
|
273
|
-
game ? createElement(WorldProvider, { game },
|
|
314
|
+
game ? createElement(WorldProvider, { game }, world) : world,
|
|
274
315
|
),
|
|
275
316
|
);
|
|
276
317
|
// A reconcile-time crash (e.g. a missing `extend` catalogue entry)
|
|
@@ -474,6 +515,11 @@ export function createR3FAdapter(options: CreateR3FAdapterOptions): RootAdapter
|
|
|
474
515
|
return live().camera;
|
|
475
516
|
},
|
|
476
517
|
drivesOwnLoop: false,
|
|
518
|
+
// REPORTED for a host that draws this scene with a renderer other than
|
|
519
|
+
// the one it mounted us against — see `MountedThreeRoot.rendererConfig`.
|
|
520
|
+
// It is the same object already applied to `host.renderer` above; the
|
|
521
|
+
// report never applies anything itself.
|
|
522
|
+
rendererConfig: rendererConfig,
|
|
477
523
|
// Adapter surface: `debug` (the shared game registry) and, when a real
|
|
478
524
|
// WebGL2 context exists, `renderDebug` are engine-seeded. Game code
|
|
479
525
|
// adds its own capabilities via `ctx.registerSystemAdapter` exactly
|