@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
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createR3FAdapter` — wraps a react-three-fiber tree as a first-party
|
|
3
|
+
* `GameAdapter` so an R3F scene mounts as an ordinary `kind: "threejs"`
|
|
4
|
+
* world: the engine hands `mount()` a `HostContext` (ITS OWN renderer +
|
|
5
|
+
* canvas + gated loop, per `../adapter/host-context.ts`); this returns a
|
|
6
|
+
* `MountedThreeWorld` (`../adapter/game-adapter.ts`) whose `scene`/`camera`
|
|
7
|
+
* are fiber's REAL `THREE.Scene`/`THREE.Camera` instances.
|
|
8
|
+
*
|
|
9
|
+
* Upstreamed from `examples/r3f-first-party/src/r3f-adapter.tsx` (R4,
|
|
10
|
+
* docs/R3F-FOLLOW-THROUGH-SPEC.md — the `@engine/world3d-react` opt-in
|
|
11
|
+
* module R3F-FIRST-PARTY-DESIGN §1.B Phase 2 names, mirroring how `world2d/`
|
|
12
|
+
* is pixi's opt-in home). The mount semantics are byte-for-byte the
|
|
13
|
+
* example's live-proven Phase-1 bridge; the one addition is the optional
|
|
14
|
+
* `components` registry threaded into `EngineBridge` so `<Behavior>` can
|
|
15
|
+
* resolve project component classes without the engine importing project
|
|
16
|
+
* code. External adopters (alien-stories-style) keep the exact same calling
|
|
17
|
+
* contract.
|
|
18
|
+
*
|
|
19
|
+
* three.js identity (docs/ADAPTER-AUTHORING-GUIDE.md §2): this module never
|
|
20
|
+
* imports `three` itself for scene objects — fiber's internal `import * as
|
|
21
|
+
* THREE from 'three'` must resolve to the SAME instance the engine's
|
|
22
|
+
* `HostContext.three` points at, which the importing project guarantees via
|
|
23
|
+
* its Vite `resolve.dedupe: ['three', 'react', 'react-dom']` (see
|
|
24
|
+
* `examples/r3f-first-party/vite.config.ts`).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { advance, createRoot, extend, type RootState } from '@react-three/fiber';
|
|
28
|
+
import { createElement, type ReactNode } from 'react';
|
|
29
|
+
import type { GameAdapter, HostContext, MountedThreeWorld } from '../adapter';
|
|
30
|
+
import { type BehaviorRegistry, EngineBridge, type EngineBridgeValue } from './engine-bridge';
|
|
31
|
+
import { createR3FWorldContext } from './world-context';
|
|
32
|
+
|
|
33
|
+
/** What {@link createR3FAdapter} needs to build one `GameAdapter`. */
|
|
34
|
+
export interface CreateR3FAdapterOptions {
|
|
35
|
+
/** Stable id (telemetry/registry/conformance) — `GameAdapter.id`. */
|
|
36
|
+
readonly id: string;
|
|
37
|
+
/** The R3F scene tree to mount — drei helpers, `useFrame` hooks, etc. all
|
|
38
|
+
* work unchanged (design §1.C: "drei works unchanged (it is fiber-context
|
|
39
|
+
* userland)"). */
|
|
40
|
+
readonly content: ReactNode;
|
|
41
|
+
/** The project's behavior registry — `<Behavior type="…">` resolves against
|
|
42
|
+
* it through `EngineBridge`. Optional: a tree with no `<Behavior>` usage
|
|
43
|
+
* needs none; a `<Behavior>` rendered without it degrades loudly (warn,
|
|
44
|
+
* no attach) rather than crashing. */
|
|
45
|
+
readonly components?: BehaviorRegistry | undefined;
|
|
46
|
+
/** Input map loaded through the game-owned `Game.loadInputMap` (load-once
|
|
47
|
+
* across roots). Defaults to the classic adapter's conventional path,
|
|
48
|
+
* `/inputmaps/default.inputmap.json`; `null` opts out (a world with no
|
|
49
|
+
* actions). A missing/invalid map degrades loudly without failing the
|
|
50
|
+
* mount — see `world-context.ts`. */
|
|
51
|
+
readonly inputMapPath?: string | null | undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Build a `GameAdapter` that mounts `options.content` through
|
|
56
|
+
* react-three-fiber, gated entirely by the host's own loop and rendering
|
|
57
|
+
* through the host's own `WebGLRenderer` — never a second renderer, never a
|
|
58
|
+
* second `requestAnimationFrame` loop (design §1.C).
|
|
59
|
+
*/
|
|
60
|
+
export function createR3FAdapter(options: CreateR3FAdapterOptions): GameAdapter {
|
|
61
|
+
const { id, content, components: registry, inputMapPath } = options;
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
id,
|
|
65
|
+
|
|
66
|
+
async mount(host: HostContext): Promise<MountedThreeWorld> {
|
|
67
|
+
// Headless honesty (design §1.D; `docs/ADAPTER-AUTHORING-GUIDE.md` §2's
|
|
68
|
+
// "Your `mount()` must work headless" rule): the conformance kit / CI
|
|
69
|
+
// runs `mount()` in Node — no canvas, no WebGL, no fiber reconciler.
|
|
70
|
+
// Guard the ENTIRE fiber mount behind `!host.headless` and return a
|
|
71
|
+
// bare, real scene + camera instead (still `instanceof
|
|
72
|
+
// host.three.Scene` / `.Camera` — the identity rule holds even here,
|
|
73
|
+
// since this uses the host's own `three` instance, not a fresh
|
|
74
|
+
// import). Residual: the hierarchy is empty under headless
|
|
75
|
+
// conformance — recorded as design O2, not hidden; a react-nil-style
|
|
76
|
+
// headless reconciler mount is the known fix, out of scope for v1.
|
|
77
|
+
if (host.headless) {
|
|
78
|
+
const scene = new host.three.Scene();
|
|
79
|
+
const camera = new host.three.PerspectiveCamera();
|
|
80
|
+
return {
|
|
81
|
+
kind: 'threejs',
|
|
82
|
+
scene,
|
|
83
|
+
camera,
|
|
84
|
+
drivesOwnLoop: false,
|
|
85
|
+
dispose(): void {
|
|
86
|
+
/* nothing was ever mounted */
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Fiber v9 made the THREE catalogue tree-shakeable: `<Canvas>` calls
|
|
92
|
+
// `extend(THREE)` for you, a bare `createRoot` does NOT — without this,
|
|
93
|
+
// the FIRST three intrinsic in the tree (`<color>`, `<ambientLight>`,
|
|
94
|
+
// …) throws "X is not part of the THREE namespace! Did you forget to
|
|
95
|
+
// extend?" at reconcile time. Extending with `host.three` (not a fresh
|
|
96
|
+
// `import * as THREE`) keeps the catalogue on the host's deduped three
|
|
97
|
+
// instance — the same identity rule the returned scene/camera rely on.
|
|
98
|
+
// `extend` merges into a module-global catalogue, so calling it once
|
|
99
|
+
// per mount is idempotent.
|
|
100
|
+
extend(host.three as unknown as Parameters<typeof extend>[0]);
|
|
101
|
+
|
|
102
|
+
// W3 (docs/R3F-SURFACE-SPIKE.md) — the engine component runtime this
|
|
103
|
+
// world OWNS: a real SystemRunner + ComponentManager pair, provided to
|
|
104
|
+
// the R3F tree via `EngineBridge` so `<Behavior>` (./behavior.tsx) can
|
|
105
|
+
// attach registry GameComponents with the engine's REAL attach path.
|
|
106
|
+
// `update(dt)` below runs the phases under the host loop, so attached
|
|
107
|
+
// components tick in engine phase order and freeze under pause exactly
|
|
108
|
+
// like `useFrame` work does.
|
|
109
|
+
//
|
|
110
|
+
// HONESTY NOTE: the manager's ctx is still a PARTIAL GameContext, but
|
|
111
|
+
// the shared game-scoped subsystems are now REAL classic-adapter parity
|
|
112
|
+
// (`./world-context.ts`): `ctx.debug`/`ctx.random`/`ctx.game`/`ctx.roots`,
|
|
113
|
+
// and `ctx.input` (the game-owned InputManager, with this world's
|
|
114
|
+
// virtual-input/actions/trace debug seams registered and the project's
|
|
115
|
+
// input map loaded). The subsystems an R3F world genuinely does not
|
|
116
|
+
// build (Rapier, composer, audio, particles, debugDraw) are THROWING
|
|
117
|
+
// getters naming the limitation — never a silent `undefined`.
|
|
118
|
+
// `scene`/`camera` are backfilled right after fiber's first commit
|
|
119
|
+
// resolves them, before any tick can run.
|
|
120
|
+
const runtime = createR3FWorldContext(host, { id, inputMapPath });
|
|
121
|
+
const { systems, components } = runtime;
|
|
122
|
+
const bridge: EngineBridgeValue = { components, registry };
|
|
123
|
+
// Actions must exist before any Behavior component's init() reads them —
|
|
124
|
+
// wait for the (never-rejecting) input-map load before the first commit.
|
|
125
|
+
await runtime.inputMapReady;
|
|
126
|
+
|
|
127
|
+
const canvas = host.surface.canvas;
|
|
128
|
+
const root = createRoot(canvas);
|
|
129
|
+
|
|
130
|
+
// `RootState` (the live scene/camera/gl fiber built) only arrives via
|
|
131
|
+
// the `onCreated` callback — `root.render()`'s return value is
|
|
132
|
+
// TECHNICALLY the same store, but `onCreated` is the hook the design
|
|
133
|
+
// doc's §1.C sketch names, and waiting for it (rather than assuming
|
|
134
|
+
// the first commit already ran synchronously) is the honest choice
|
|
135
|
+
// under React 19's concurrent renderer, which does not guarantee a
|
|
136
|
+
// synchronous first commit the way legacy ReactDOM.render did.
|
|
137
|
+
let resolveState!: (state: RootState) => void;
|
|
138
|
+
const statePromise = new Promise<RootState>((resolve) => {
|
|
139
|
+
resolveState = resolve;
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// `root.configure()` is ASYNC in fiber v9 (`Promise<ReconcilerRoot>`)
|
|
143
|
+
// — the design doc's §1.C sketch shows it called synchronously; the
|
|
144
|
+
// real v9 API (verified against
|
|
145
|
+
// `node_modules/@react-three/fiber/dist/declarations/src/core/index.d.ts`)
|
|
146
|
+
// requires awaiting it before `render()`.
|
|
147
|
+
await root.configure({
|
|
148
|
+
// The engine's renderer, not a second one (design §1.C) — fiber
|
|
149
|
+
// renders THROUGH `host.renderer` instead of constructing its own
|
|
150
|
+
// `WebGLRenderer`.
|
|
151
|
+
gl: host.renderer,
|
|
152
|
+
// The engine's gated loop is the ONLY loop — fiber must never run
|
|
153
|
+
// its own rAF (that would defeat editor pause; design §1.C's
|
|
154
|
+
// `drivesOwnLoop: false` contract, guide §2's loop-model note).
|
|
155
|
+
frameloop: 'never',
|
|
156
|
+
// `RenderProps.size` types as the FULL `Size` (width/height/top/left),
|
|
157
|
+
// not `Partial<Size>` — the design sketch's `{ width, height }` alone
|
|
158
|
+
// does not satisfy fiber v9's real type, so `top`/`left` are pinned
|
|
159
|
+
// to 0 (this bridge always fills its whole canvas; no offset
|
|
160
|
+
// viewport in v1).
|
|
161
|
+
size: { width: host.surface.width, height: host.surface.height, top: 0, left: 0 },
|
|
162
|
+
// `events` deliberately left at fiber's default (binds to
|
|
163
|
+
// `gl.domElement`, i.e. `host.surface.canvas`) — design O1's open
|
|
164
|
+
// question about a delegating multi-canvas input router only
|
|
165
|
+
// matters for a shell+net composite; this bridge is a genuine
|
|
166
|
+
// single-canvas world, the case O1 already says is unaffected.
|
|
167
|
+
onCreated: (state) => resolveState(state),
|
|
168
|
+
});
|
|
169
|
+
// No `<StrictMode>` (design §1.C: "no StrictMode: host mounts once")
|
|
170
|
+
// — this bridge mounts exactly once per `mount()` call; StrictMode's
|
|
171
|
+
// deliberate double-invoke of effects would double-subscribe
|
|
172
|
+
// `useFrame` callbacks against a host loop that only ticks once per
|
|
173
|
+
// frame. The `EngineBridge` provider threads the component runtime
|
|
174
|
+
// (above) to any `<Behavior>` wrapper in the tree (W3).
|
|
175
|
+
root.render(createElement(EngineBridge.Provider, { value: bridge }, content));
|
|
176
|
+
// A reconcile-time crash (e.g. a missing `extend` catalogue entry)
|
|
177
|
+
// surfaces as an uncaught window error and `onCreated` never fires —
|
|
178
|
+
// without this guard, `mount()` would await `statePromise` FOREVER and
|
|
179
|
+
// silently wedge every world declared after this one (roots mount
|
|
180
|
+
// sequentially). Convert that class of failure into a loud mount error.
|
|
181
|
+
const state = await new Promise<RootState>((resolve, reject) => {
|
|
182
|
+
const onError = (event: ErrorEvent) => {
|
|
183
|
+
cleanup();
|
|
184
|
+
root.unmount();
|
|
185
|
+
runtime.dispose();
|
|
186
|
+
reject(
|
|
187
|
+
new Error(
|
|
188
|
+
`r3f-adapter: fiber crashed before its first commit — ${event.message} ` +
|
|
189
|
+
'(mount() fails loudly instead of hanging on onCreated)',
|
|
190
|
+
),
|
|
191
|
+
);
|
|
192
|
+
};
|
|
193
|
+
const timer = setTimeout(() => {
|
|
194
|
+
cleanup();
|
|
195
|
+
root.unmount();
|
|
196
|
+
runtime.dispose();
|
|
197
|
+
reject(
|
|
198
|
+
new Error(
|
|
199
|
+
'r3f-adapter: onCreated did not fire within 10s — the R3F tree never reached ' +
|
|
200
|
+
'its first commit (mount() fails loudly instead of hanging)',
|
|
201
|
+
),
|
|
202
|
+
);
|
|
203
|
+
}, 10_000);
|
|
204
|
+
const cleanup = () => {
|
|
205
|
+
clearTimeout(timer);
|
|
206
|
+
window.removeEventListener('error', onError);
|
|
207
|
+
};
|
|
208
|
+
window.addEventListener('error', onError);
|
|
209
|
+
void statePromise.then((s) => {
|
|
210
|
+
cleanup();
|
|
211
|
+
resolve(s);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Clock hardening (field-proven by the first external adopter,
|
|
216
|
+
// alien-stories' proto): fiber's internal `update()` calls
|
|
217
|
+
// `state.clock.getDelta()` BEFORE its `frameloop:'never'` branch, and a
|
|
218
|
+
// RUNNING (or autoStart) `THREE.Clock` accumulates WALL time into
|
|
219
|
+
// `elapsedTime` as a side effect — skewing the deltas the 'never'
|
|
220
|
+
// branch derives from the game timestamps `update(dt)` feeds below.
|
|
221
|
+
// Stopped + autoStart=false makes `getDelta()` a pure no-op, so
|
|
222
|
+
// `useFrame` deltas come from game time alone.
|
|
223
|
+
state.clock.autoStart = false;
|
|
224
|
+
state.clock.stop();
|
|
225
|
+
|
|
226
|
+
// Backfill the component runtime's ctx with fiber's real scene/camera
|
|
227
|
+
// (W3 — see the HONESTY NOTE above). This happens before the first
|
|
228
|
+
// `update(dt)` tick, so no component ever observes them missing.
|
|
229
|
+
runtime.setSceneCamera(state.scene, state.camera);
|
|
230
|
+
|
|
231
|
+
// The engine drives every `useFrame` through the mounted world's
|
|
232
|
+
// `update(dt)` hook — NOT `host.loop.onUpdate`. The distinction is the
|
|
233
|
+
// whole pause story: `runFrameImpl` (`runtime/game.ts`) calls
|
|
234
|
+
// `mounted.update?.(dt)` per substep for a host-driven
|
|
235
|
+
// (`drivesOwnLoop: false`) world and SKIPS it while that world is
|
|
236
|
+
// frozen, and `Game.play.step()` ticks it exactly once — whereas the
|
|
237
|
+
// raw loop's `onUpdate` extraUpdaters fire unconditionally, pause or
|
|
238
|
+
// not (they exist for host-level concerns; `create-runtime.ts` runs
|
|
239
|
+
// them outside `game.runFrame`'s gate). Advancing fiber from
|
|
240
|
+
// `onUpdate` therefore LOOKS right and silently breaks acceptance
|
|
241
|
+
// gate 2 — proven by the 36-r3f-first-party e2e, whose paused
|
|
242
|
+
// instance-matrix samples kept moving until this moved to `update`.
|
|
243
|
+
//
|
|
244
|
+
// `advance(timestamp, runGlobalEffects, state)`'s `timestamp` is
|
|
245
|
+
// consumed as `THREE.Clock.elapsedTime` DIRECTLY when
|
|
246
|
+
// `frameloop:'never'` (verified against fiber's `update()`
|
|
247
|
+
// implementation, not just its `.d.ts` — the declared signature alone
|
|
248
|
+
// doesn't say this): `delta = timestamp - clock.elapsedTime;
|
|
249
|
+
// clock.elapsedTime = timestamp`. So `timestamp` must be a
|
|
250
|
+
// monotonically increasing SECONDS value — GAME time, not wall time:
|
|
251
|
+
// `update` simply isn't called while this world is frozen, so
|
|
252
|
+
// accumulating its `dt` (seconds; the loop runs a fixed 1/60 timestep)
|
|
253
|
+
// means fiber's clock does not advance across a pause. Wall clock
|
|
254
|
+
// (`performance.now()`) would leak the pause duration into the first
|
|
255
|
+
// resumed frame as one giant `useFrame` delta — the exact "time passed
|
|
256
|
+
// while frozen" illusion acceptance gate 2 forbids.
|
|
257
|
+
let elapsed = 0;
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
kind: 'threejs',
|
|
261
|
+
// Fiber's REAL `THREE.Scene`/`THREE.Camera` — `three` identity is
|
|
262
|
+
// deduped to the host's single instance by the project's Vite
|
|
263
|
+
// config (see this file's header comment), so these pass the
|
|
264
|
+
// editor's `instanceof THREE.Scene` / `instanceof THREE.Camera`
|
|
265
|
+
// gates unmodified (design acceptance gate 3). `state.camera`'s
|
|
266
|
+
// type (`Camera = (OrthographicCamera | PerspectiveCamera) & {
|
|
267
|
+
// manual?: boolean }`) is a structural subtype of `THREE.Camera`, so
|
|
268
|
+
// no cast is needed.
|
|
269
|
+
scene: state.scene,
|
|
270
|
+
camera: state.camera,
|
|
271
|
+
drivesOwnLoop: false,
|
|
272
|
+
// The engine's REAL ComponentManager for this world — what makes
|
|
273
|
+
// `Game.queryByComponent` see `<Behavior>`-attached components
|
|
274
|
+
// (`runtime/game.ts` aggregates this optional capability for every
|
|
275
|
+
// non-first-party mount that exposes one).
|
|
276
|
+
components,
|
|
277
|
+
// Adapter surface: `debug` pre-seeded (the shared game registry's
|
|
278
|
+
// adapter); game code adds capabilities via
|
|
279
|
+
// `ctx.registerSystemAdapter` exactly as in a classic world.
|
|
280
|
+
systems: runtime.systemAdapters,
|
|
281
|
+
update(dt: number): void {
|
|
282
|
+
elapsed += dt;
|
|
283
|
+
// Engine phases FIRST (attached GameComponents mutate transforms),
|
|
284
|
+
// then fiber's advance (useFrame callbacks + the actual render see
|
|
285
|
+
// the fresh state). Both behavior models — `useFrame` and
|
|
286
|
+
// `<Behavior>` — are host-gated: neither runs while frozen.
|
|
287
|
+
systems.run(dt);
|
|
288
|
+
advance(elapsed, true, state);
|
|
289
|
+
},
|
|
290
|
+
resize(width: number, height: number): void {
|
|
291
|
+
state.setSize(width, height);
|
|
292
|
+
},
|
|
293
|
+
dispose(): void {
|
|
294
|
+
// Unmount FIRST so <Behavior> effect cleanups detach through the
|
|
295
|
+
// live manager, then tear the runtime down (components.clear +
|
|
296
|
+
// scoped debug-registry strip).
|
|
297
|
+
root.unmount();
|
|
298
|
+
runtime.dispose();
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createR3FWorldContext` — the ENGINE runtime an R3F world owns, split out of
|
|
3
|
+
* `./r3f-adapter.tsx` so it stays react-free and headlessly unit-testable
|
|
4
|
+
* (`test/world3d-react-context-parity.test.tsx`).
|
|
5
|
+
*
|
|
6
|
+
* Context parity with the classic first-party adapter
|
|
7
|
+
* (`../adapter/vgai-scene-game-adapter.ts`) — closed here after a dogfood
|
|
8
|
+
* build had to shim around three silent gaps:
|
|
9
|
+
*
|
|
10
|
+
* 1. **`ctx.debug` / `ctx.random` / `ctx.game` / `ctx.roots` are wired**
|
|
11
|
+
* exactly the way the classic adapter wires them: the ONE game-scoped
|
|
12
|
+
* debug registry (`getDebugRegistry(host.game)`) provides
|
|
13
|
+
* `ctx.debug = registry.forWorld(id)`, so a Behavior-attached component's
|
|
14
|
+
* `init(ctx)` can `ctx.debug.registerStateProvider(...)` and be visible
|
|
15
|
+
* to `vgai e2e`/the editor's debug panels — previously a silent no-op.
|
|
16
|
+
* 2. **`ctx.input` is the game-owned `InputManager`** (`host.game.input` —
|
|
17
|
+
* the same instance the classic adapter uses when hosted), and this
|
|
18
|
+
* module registers the SAME per-world debug-registry seams the classic
|
|
19
|
+
* adapter registers (`setVirtualInputTarget`/`setInputActionsSource`/
|
|
20
|
+
* `setInputTraceSource`), which is what makes `game.input.*` (the bot
|
|
21
|
+
* input doctrine) work instead of throwing `DEBUG_INPUT_UNAVAILABLE`.
|
|
22
|
+
* The project's input map is loaded through `Game.loadInputMap` (the
|
|
23
|
+
* game-owned, load-once path every root shares) with the SAME default
|
|
24
|
+
* path the classic adapter uses. A missing/unparseable map degrades
|
|
25
|
+
* LOUDLY (console.error naming the path and what breaks) without
|
|
26
|
+
* failing the mount — an R3F tree with no actions (e.g. a pure
|
|
27
|
+
* OrbitControls demo, `examples/r3f-first-party`) is legal.
|
|
28
|
+
* 3. **Absent subsystems fail loudly at the ctx surface.** An R3F world
|
|
29
|
+
* builds no Rapier world, composer, audio, particles, or debug-draw —
|
|
30
|
+
* reading those `ctx` fields used to yield `undefined` (then fail
|
|
31
|
+
* somewhere downstream, or silently no-op). They are now throwing
|
|
32
|
+
* getters naming the limitation and the sanctioned alternative.
|
|
33
|
+
*
|
|
34
|
+
* The `ComponentManager` built here is also exposed on the mounted world
|
|
35
|
+
* (`MountedWorldBase.components`) so `Game.queryByComponent` aggregates R3F
|
|
36
|
+
* Behavior components exactly like scene-authored ones — see
|
|
37
|
+
* `runtime/game.ts`.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import type * as THREE from 'three';
|
|
41
|
+
import type { HostContext } from '../adapter';
|
|
42
|
+
import type { SystemAdapters } from '../adapter/system-adapter';
|
|
43
|
+
import {
|
|
44
|
+
createSeededRandom,
|
|
45
|
+
DEFAULT_SEEDED_RANDOM_SEED,
|
|
46
|
+
getSeededRandom,
|
|
47
|
+
} from '../core/seeded-random';
|
|
48
|
+
import { createSystemRunner } from '../core/system-runner';
|
|
49
|
+
import { type ComponentManager, createComponentManager } from '../ecs/component-manager';
|
|
50
|
+
import {
|
|
51
|
+
createDebugRegistry,
|
|
52
|
+
type DebugRegistry,
|
|
53
|
+
getDebugRegistry,
|
|
54
|
+
} from '../runtime/debug-registry';
|
|
55
|
+
import type { GameContext } from '../runtime/types';
|
|
56
|
+
|
|
57
|
+
/** The classic adapter's default input-map path — the conventional location
|
|
58
|
+
* every scaffolded project ships (`public/inputmaps/default.inputmap.json`). */
|
|
59
|
+
export const DEFAULT_INPUT_MAP_PATH = '/inputmaps/default.inputmap.json';
|
|
60
|
+
|
|
61
|
+
export interface R3FWorldContextOptions {
|
|
62
|
+
/** World id — provenance for every debug-registry registration (matches the
|
|
63
|
+
* manifest root id by the same convention the classic adapter follows). */
|
|
64
|
+
readonly id: string;
|
|
65
|
+
/**
|
|
66
|
+
* Input map to load through `Game.loadInputMap` (game-owned, load-once —
|
|
67
|
+
* several roots asking for the SAME path share one load; competing paths
|
|
68
|
+
* throw there). Defaults to {@link DEFAULT_INPUT_MAP_PATH}, the classic
|
|
69
|
+
* adapter's own default. Pass `null` to skip loading (a world with no
|
|
70
|
+
* actions). Ignored when no `host.game` is present.
|
|
71
|
+
*/
|
|
72
|
+
readonly inputMapPath?: string | null | undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** What {@link createR3FWorldContext} returns — the engine runtime one R3F
|
|
76
|
+
* world owns, plus the wiring hooks `createR3FAdapter`'s `mount()` drives. */
|
|
77
|
+
export interface R3FWorldRuntime {
|
|
78
|
+
/** The (partial, loud-on-absence) `GameContext` handed to every attached
|
|
79
|
+
* GameComponent. See this module's header for exactly what is present. */
|
|
80
|
+
readonly ctx: GameContext;
|
|
81
|
+
/** The world-local phase runner `mount().update(dt)` ticks. */
|
|
82
|
+
readonly systems: ReturnType<typeof createSystemRunner>;
|
|
83
|
+
/** The engine's REAL ComponentManager for this world — `<Behavior>`
|
|
84
|
+
* attaches through it; expose it as `mounted.components` so
|
|
85
|
+
* `Game.queryByComponent` spans this world. */
|
|
86
|
+
readonly components: ComponentManager;
|
|
87
|
+
/** The mounted world's `SystemAdapters` bag (`mounted.systems`) —
|
|
88
|
+
* `ctx.registerSystemAdapter` writes into it; `debug` is pre-seeded. */
|
|
89
|
+
readonly systemAdapters: SystemAdapters;
|
|
90
|
+
/** Resolves once the input map load settles (immediately when skipped).
|
|
91
|
+
* Never rejects — a failed load reports loudly and resolves. */
|
|
92
|
+
readonly inputMapReady: Promise<void>;
|
|
93
|
+
/** Backfill fiber's real scene/camera onto `ctx` after the first commit
|
|
94
|
+
* resolves them — before any tick can run. `camera` is fiber's union
|
|
95
|
+
* (perspective OR orthographic) — the ctx surface exposes whatever the
|
|
96
|
+
* tree declared, exactly as the mounted world does. */
|
|
97
|
+
setSceneCamera(scene: THREE.Scene, camera: THREE.Camera): void;
|
|
98
|
+
/** Detach every component and strip this world's debug registrations
|
|
99
|
+
* (scoped, like the classic adapter's teardown — sibling roots and
|
|
100
|
+
* react-door registrations are untouched when a Game hosts this world). */
|
|
101
|
+
dispose(): void;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Install a throwing getter for a `GameContext` field this world genuinely
|
|
105
|
+
* does not build — a component reaching for it fails LOUDLY at its own call
|
|
106
|
+
* site, naming the limitation, instead of reading `undefined` and breaking
|
|
107
|
+
* somewhere downstream (or silently no-op-ing). Non-enumerable so spreads/
|
|
108
|
+
* serialization of the ctx never trip it. */
|
|
109
|
+
function defineAbsentCtxField(
|
|
110
|
+
target: Record<string, unknown>,
|
|
111
|
+
worldId: string,
|
|
112
|
+
key: string,
|
|
113
|
+
why: string,
|
|
114
|
+
): void {
|
|
115
|
+
Object.defineProperty(target, key, {
|
|
116
|
+
configurable: true,
|
|
117
|
+
enumerable: false,
|
|
118
|
+
get(): never {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`R3F world "${worldId}": ctx.${key} is not available — ${why} ` +
|
|
121
|
+
'(An R3F world ctx provides: scene, camera, input, assets, systems, components, ' +
|
|
122
|
+
'debug, random, game, roots, registerSystemAdapter. For the full classic runtime, ' +
|
|
123
|
+
'use a .vscn scene root instead.)',
|
|
124
|
+
);
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Build the engine runtime for one R3F world from a `HostContext` — the same
|
|
131
|
+
* wiring, at the same layer, as the classic `VgaiSceneGameAdapter.mount()`
|
|
132
|
+
* performs for the subsystems an R3F world shares with it (debug, input,
|
|
133
|
+
* random, components); loud throwing getters for the ones it doesn't.
|
|
134
|
+
*/
|
|
135
|
+
export function createR3FWorldContext(
|
|
136
|
+
host: HostContext,
|
|
137
|
+
options: R3FWorldContextOptions,
|
|
138
|
+
): R3FWorldRuntime {
|
|
139
|
+
const { id, inputMapPath = DEFAULT_INPUT_MAP_PATH } = options;
|
|
140
|
+
|
|
141
|
+
// Same observer threading as the classic adapter — profiler sees this
|
|
142
|
+
// world's phases under the shared game profiler when hosted.
|
|
143
|
+
const systems = createSystemRunner(host.game?.profiler.systemObserver, 'threejs');
|
|
144
|
+
|
|
145
|
+
// The mounted world's adapter surface (`mounted.systems`) — declared before
|
|
146
|
+
// ctx so `ctx.registerSystemAdapter` can close over it (classic parity: a
|
|
147
|
+
// game-owned capability like networking registers here from component code).
|
|
148
|
+
const systemAdapters: SystemAdapters = {};
|
|
149
|
+
|
|
150
|
+
const ctxRaw: Record<string, unknown> = {
|
|
151
|
+
systems,
|
|
152
|
+
assets: host.assets,
|
|
153
|
+
registerSystemAdapter: (kind: keyof SystemAdapters, adapter: SystemAdapters[typeof kind]) => {
|
|
154
|
+
// biome-ignore lint/suspicious/noExplicitAny: same per-key record write the classic adapter performs; correct by construction
|
|
155
|
+
(systemAdapters as any)[kind] = adapter;
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
const ctx = ctxRaw as unknown as GameContext;
|
|
159
|
+
if (host.game) {
|
|
160
|
+
ctx.game = host.game;
|
|
161
|
+
ctx.roots = host.game.roots;
|
|
162
|
+
}
|
|
163
|
+
const components = createComponentManager(ctx);
|
|
164
|
+
ctxRaw['components'] = components;
|
|
165
|
+
|
|
166
|
+
// --- ctx.debug (gap 1) — the ONE registry per Game root, shared across
|
|
167
|
+
// every world mounted onto it; a bare mount with no Game shell gets a
|
|
168
|
+
// private, mount-local registry instead (same absence precedent as the
|
|
169
|
+
// classic adapter). ---
|
|
170
|
+
const debugRegistry: DebugRegistry =
|
|
171
|
+
(host.game ? getDebugRegistry(host.game) : null) ??
|
|
172
|
+
createDebugRegistry({ getTick: () => 0, getSimT: () => 0 });
|
|
173
|
+
systemAdapters.debug = debugRegistry.adapter;
|
|
174
|
+
ctx.debug = debugRegistry.forWorld(id);
|
|
175
|
+
ctx.random =
|
|
176
|
+
(host.game ? getSeededRandom(host.game) : null) ??
|
|
177
|
+
createSeededRandom(DEFAULT_SEEDED_RANDOM_SEED);
|
|
178
|
+
|
|
179
|
+
// --- ctx.input + the debug-registry input seams (gap 3) — the game-owned
|
|
180
|
+
// InputManager, polled by the game-scoped runner each frame; this world
|
|
181
|
+
// registers the SAME per-world seams the classic adapter registers so
|
|
182
|
+
// `game.input.*` (virtual input) and the built-in `input.actions`/
|
|
183
|
+
// `input.trace` providers resolve to it. ---
|
|
184
|
+
let inputMapReady: Promise<void> = Promise.resolve();
|
|
185
|
+
if (host.game) {
|
|
186
|
+
const game = host.game;
|
|
187
|
+
const input = game.input;
|
|
188
|
+
ctxRaw['input'] = input;
|
|
189
|
+
debugRegistry.setInputActionsSource(id, () =>
|
|
190
|
+
input.actionNames().map((name) => ({ name, valueType: input.getActionValueType(name) })),
|
|
191
|
+
);
|
|
192
|
+
debugRegistry.setInputTraceSource(id, () => {
|
|
193
|
+
const raw = input.getInputTrace();
|
|
194
|
+
return {
|
|
195
|
+
version: raw.version,
|
|
196
|
+
seed: getSeededRandom(game)?.seed ?? null,
|
|
197
|
+
fixedDt: game.loop.fixedDt,
|
|
198
|
+
ticks: raw.ticks,
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
debugRegistry.setVirtualInputTarget(id, {
|
|
202
|
+
setVirtualAction: (action, value) => input.setVirtualAction(action, value),
|
|
203
|
+
tapVirtualAction: (action) => input.tapVirtualAction(action),
|
|
204
|
+
clearVirtualActions: () => input.clearVirtualActions(),
|
|
205
|
+
scheduleActionAtTick: (tick, action, value) =>
|
|
206
|
+
input.scheduleActionAtTick(tick, action, value),
|
|
207
|
+
startInputRecording: () => input.startInputRecording(),
|
|
208
|
+
stopInputRecording: () => input.stopInputRecording(),
|
|
209
|
+
isInputRecording: () => input.isInputRecording(),
|
|
210
|
+
injectAxis: (sourceId, value) => input.injectAxis(sourceId, value),
|
|
211
|
+
injectVector2: (sourceId, value) => input.injectVector2(sourceId, value),
|
|
212
|
+
injectPointerDelta: (sourceId, delta) => input.injectPointerDelta(sourceId, delta),
|
|
213
|
+
injectPointerPosition: (sourceId, value) => input.injectPointerPosition(sourceId, value),
|
|
214
|
+
});
|
|
215
|
+
if (!host.headless && inputMapPath !== null) {
|
|
216
|
+
// Load-once through the game-owned path (classic parity — competing
|
|
217
|
+
// paths across roots throw THERE, loudly). A FAILED load (missing/bad
|
|
218
|
+
// file) must not fail this mount: an R3F tree with no declared actions
|
|
219
|
+
// is legal. It degrades loudly instead — naming exactly what breaks.
|
|
220
|
+
inputMapReady = game.loadInputMap(inputMapPath).catch((err: unknown) => {
|
|
221
|
+
// biome-ignore lint/suspicious/noConsole: deliberate loud degrade — the documented alternative to failing the mount (see comment above)
|
|
222
|
+
console.error(
|
|
223
|
+
`R3F world "${id}": failed to load input map "${inputMapPath}" — declared input ` +
|
|
224
|
+
'actions and `game.input.*` (bot/virtual input) will not work until a valid map ' +
|
|
225
|
+
'loads. Ship one at the conventional path or pass `inputMapPath` to ' +
|
|
226
|
+
`createR3FAdapter (null to opt out). Cause: ${err instanceof Error ? err.message : String(err)}`,
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
defineAbsentCtxField(
|
|
232
|
+
ctxRaw,
|
|
233
|
+
id,
|
|
234
|
+
'input',
|
|
235
|
+
'input is game-owned and this mount has no Game host (host.game is absent — bare ' +
|
|
236
|
+
'harness/foreign host); mount through the vgai host to get the shared InputManager.',
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// --- Loud absence for the classic-only subsystems (gap 1's honesty rule:
|
|
241
|
+
// a thrown error naming the limitation beats a silent undefined). ---
|
|
242
|
+
const rapierWhy =
|
|
243
|
+
'R3F worlds do not build the first-party Rapier physics runtime; use a physics ' +
|
|
244
|
+
'solution inside the fiber tree (e.g. @react-three/rapier) or a classic .vscn world root.';
|
|
245
|
+
defineAbsentCtxField(ctxRaw, id, 'rapierWorld', rapierWhy);
|
|
246
|
+
defineAbsentCtxField(ctxRaw, id, 'rapier', rapierWhy);
|
|
247
|
+
defineAbsentCtxField(ctxRaw, id, 'physics', rapierWhy);
|
|
248
|
+
defineAbsentCtxField(ctxRaw, id, 'collisions', rapierWhy);
|
|
249
|
+
defineAbsentCtxField(
|
|
250
|
+
ctxRaw,
|
|
251
|
+
id,
|
|
252
|
+
'composer',
|
|
253
|
+
'R3F worlds render through react-three-fiber against the host renderer; there is no ' +
|
|
254
|
+
'postprocessing EffectComposer here (use fiber-native postprocessing inside the tree).',
|
|
255
|
+
);
|
|
256
|
+
defineAbsentCtxField(
|
|
257
|
+
ctxRaw,
|
|
258
|
+
id,
|
|
259
|
+
'audio',
|
|
260
|
+
'R3F worlds build no first-party audio context; use Web Audio directly (or drei audio ' +
|
|
261
|
+
'helpers) inside the tree.',
|
|
262
|
+
);
|
|
263
|
+
defineAbsentCtxField(
|
|
264
|
+
ctxRaw,
|
|
265
|
+
id,
|
|
266
|
+
'particles',
|
|
267
|
+
'R3F worlds build no three.quarks particle runtime; drive particles from the fiber tree.',
|
|
268
|
+
);
|
|
269
|
+
defineAbsentCtxField(
|
|
270
|
+
ctxRaw,
|
|
271
|
+
id,
|
|
272
|
+
'debugDraw',
|
|
273
|
+
'R3F worlds build no debug-draw helper; add helper objects to the fiber tree directly.',
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
ctx,
|
|
278
|
+
systems,
|
|
279
|
+
components,
|
|
280
|
+
systemAdapters,
|
|
281
|
+
inputMapReady,
|
|
282
|
+
setSceneCamera(scene, camera): void {
|
|
283
|
+
ctxRaw['scene'] = scene;
|
|
284
|
+
ctxRaw['camera'] = camera;
|
|
285
|
+
},
|
|
286
|
+
dispose(): void {
|
|
287
|
+
components.clear();
|
|
288
|
+
// Scoped strip when a Game hosts this world (sibling roots stay live);
|
|
289
|
+
// a bare standalone mount owns its private registry and clears it all.
|
|
290
|
+
if (host.game) debugRegistry.strip(id);
|
|
291
|
+
else debugRegistry.strip();
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 0beqz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# realism-effects compatibility fork
|
|
2
|
+
|
|
3
|
+
This directory vendors the published `realism-effects@1.1.2` distribution
|
|
4
|
+
(MIT license) so `@vgai/engine` remains installable as a self-contained source
|
|
5
|
+
package. The upstream package still imports Three's removed
|
|
6
|
+
`WebGLMultipleRenderTargets` API and has no release compatible with Three r180.
|
|
7
|
+
|
|
8
|
+
VGAI's fork contains only the mechanical r180 MRT migration:
|
|
9
|
+
|
|
10
|
+
- `WebGLMultipleRenderTargets(width, height, count, options)` becomes
|
|
11
|
+
`WebGLRenderTarget(width, height, { ...options, count })`.
|
|
12
|
+
- MRT attachment access moves from `.texture[index]` to `.textures[index]`.
|
|
13
|
+
|
|
14
|
+
Upstream repository: <https://github.com/0beqz/realism-effects>
|
|
15
|
+
|
|
16
|
+
Upstream package/version: `realism-effects@1.1.2`
|
|
17
|
+
|
|
18
|
+
Remove this fork when an upstream release supports Three r180 or newer and the
|
|
19
|
+
isolated packed-engine consumer proof passes against it.
|