@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,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-party {@link RenderDebugAdapter} wiring (W4b, F11) — the glue between
|
|
3
|
+
* the WebGL2 frame capture (`webgl-frame-capture.ts`), a live three.js scene,
|
|
4
|
+
* and the render adapter's per-frame render pass.
|
|
5
|
+
*
|
|
6
|
+
* Split OUT of `vgai-scene-game-adapter.ts` so the capture/attribution/restore
|
|
7
|
+
* flow is unit-testable in a headless Node test with a mock GL context and a
|
|
8
|
+
* plain scene — a real non-headless GPU mount (EffectComposer + WebGLRenderer)
|
|
9
|
+
* cannot run under vitest here, the same reason the adapter suite drives
|
|
10
|
+
* `setup(ctx)` only through hotReload on a data scene. `mount()` owns the
|
|
11
|
+
* DECISION to construct this (only under a real WebGL2 context, never
|
|
12
|
+
* headless — see `frameCaptureContextFor`); this module owns the BEHAVIOR.
|
|
13
|
+
*
|
|
14
|
+
* Attribution model (honest by construction): `captureFrame()` wraps every
|
|
15
|
+
* renderable's `onBeforeRender` to feed one annotation to the capture. three
|
|
16
|
+
* calls `onBeforeRender` immediately before that object's MAIN-pass draw, so
|
|
17
|
+
* main-pass draws are attributed and shadow/composer-pass draws record
|
|
18
|
+
* `annotation: null` (counted as unattributed) — never a fabricated guess. The
|
|
19
|
+
* wrappers are all restored when the capture resolves, rejects, times out, or
|
|
20
|
+
* the adapter is disposed.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type * as THREE from 'three';
|
|
24
|
+
import type { RenderDebugAdapter } from '../adapter/system-adapter';
|
|
25
|
+
import type { RenderMemorySnapshot } from './render-memory';
|
|
26
|
+
import type { DrawAnnotation, FrameCapture, WebGLFrameCapture } from './webgl-frame-capture';
|
|
27
|
+
|
|
28
|
+
/** Decide whether a mount can host frame capture — a real WebGL2 context with
|
|
29
|
+
* the draw entry points, never a headless/stub renderer. Exported so the
|
|
30
|
+
* mount uses ONE predicate and a unit test can assert both sides (mock GL ⇒
|
|
31
|
+
* the context; headless/missing ⇒ null) without a GPU. */
|
|
32
|
+
export function frameCaptureContextFor(
|
|
33
|
+
headless: boolean,
|
|
34
|
+
rendererContext: unknown,
|
|
35
|
+
): WebGL2RenderingContext | null {
|
|
36
|
+
if (headless) return null;
|
|
37
|
+
if (!rendererContext || typeof rendererContext !== 'object') return null;
|
|
38
|
+
const gl = rendererContext as Partial<WebGL2RenderingContext>;
|
|
39
|
+
if (typeof gl.getExtension !== 'function') return null;
|
|
40
|
+
if (typeof gl.drawArrays !== 'function' || typeof gl.drawElements !== 'function') return null;
|
|
41
|
+
return rendererContext as WebGL2RenderingContext;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface RenderDebugAdapterDeps {
|
|
45
|
+
/** The armed-on-demand frame capture over the mount's real context. */
|
|
46
|
+
capture: WebGLFrameCapture;
|
|
47
|
+
/** The live scene root traversed to attribute draws. */
|
|
48
|
+
scene: THREE.Object3D;
|
|
49
|
+
/** Memory snapshot supplier (renderer.info-backed). When omitted, the
|
|
50
|
+
* adapter's `memorySnapshot` is absent (honest capability degradation). */
|
|
51
|
+
memory?: () => RenderMemorySnapshot;
|
|
52
|
+
/** Timeout (ms) after which a pending `captureFrame` rejects — the world
|
|
53
|
+
* never rendered (paused/stopped). Default 5000. */
|
|
54
|
+
timeoutMs?: number;
|
|
55
|
+
/** Injected timer, for tests. Defaults to the real `setTimeout`/
|
|
56
|
+
* `clearTimeout`. */
|
|
57
|
+
setTimeoutFn?: (fn: () => void, ms: number) => unknown;
|
|
58
|
+
clearTimeoutFn?: (handle: unknown) => void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface RenderDebugWiring {
|
|
62
|
+
readonly adapter: RenderDebugAdapter;
|
|
63
|
+
/** Call at the START of the host render pass, before draws. */
|
|
64
|
+
beforeRender(): void;
|
|
65
|
+
/** Call at the END of the host render pass, after draws (in a `finally`). */
|
|
66
|
+
afterRender(): void;
|
|
67
|
+
/** Restore any pending attribution wrappers and reject a pending capture. */
|
|
68
|
+
dispose(): void;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface Pending {
|
|
72
|
+
resolve(capture: FrameCapture): void;
|
|
73
|
+
reject(error: Error): void;
|
|
74
|
+
restores: (() => void)[];
|
|
75
|
+
timer: unknown;
|
|
76
|
+
settled: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Objects three actually issues draws for (and thus calls `onBeforeRender`
|
|
80
|
+
* on before the main-pass draw). */
|
|
81
|
+
function isRenderable(object: THREE.Object3D): boolean {
|
|
82
|
+
const o = object as unknown as {
|
|
83
|
+
isMesh?: boolean;
|
|
84
|
+
isSkinnedMesh?: boolean;
|
|
85
|
+
isPoints?: boolean;
|
|
86
|
+
isLine?: boolean;
|
|
87
|
+
isSprite?: boolean;
|
|
88
|
+
};
|
|
89
|
+
return Boolean(o.isMesh || o.isSkinnedMesh || o.isPoints || o.isLine || o.isSprite);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function annotationFor(object: THREE.Object3D): DrawAnnotation {
|
|
93
|
+
const mesh = object as THREE.Mesh;
|
|
94
|
+
const geometry = mesh.geometry as THREE.BufferGeometry | undefined;
|
|
95
|
+
const rawMaterial = mesh.material;
|
|
96
|
+
const material = (Array.isArray(rawMaterial) ? rawMaterial[0] : rawMaterial) as
|
|
97
|
+
| THREE.Material
|
|
98
|
+
| undefined;
|
|
99
|
+
const entityId = object.userData?.['entityId'];
|
|
100
|
+
return {
|
|
101
|
+
object: {
|
|
102
|
+
name: object.name || object.type,
|
|
103
|
+
type: object.type,
|
|
104
|
+
...(entityId !== undefined ? { entityId: String(entityId) } : {}),
|
|
105
|
+
},
|
|
106
|
+
geometry: {
|
|
107
|
+
type: geometry?.type ?? 'unknown',
|
|
108
|
+
name: geometry?.name ?? '',
|
|
109
|
+
attributes: geometry ? Object.keys(geometry.attributes) : [],
|
|
110
|
+
indexed: Boolean(geometry?.index),
|
|
111
|
+
},
|
|
112
|
+
material: {
|
|
113
|
+
type: material?.type ?? 'unknown',
|
|
114
|
+
name: material?.name ?? '',
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function createRenderDebugAdapter(deps: RenderDebugAdapterDeps): RenderDebugWiring {
|
|
120
|
+
const { capture, scene, memory } = deps;
|
|
121
|
+
const timeoutMs = deps.timeoutMs ?? 5000;
|
|
122
|
+
const setTimer = deps.setTimeoutFn ?? ((fn, ms) => setTimeout(fn, ms));
|
|
123
|
+
const clearTimer =
|
|
124
|
+
deps.clearTimeoutFn ?? ((h) => clearTimeout(h as ReturnType<typeof setTimeout>));
|
|
125
|
+
|
|
126
|
+
let pending: Pending | null = null;
|
|
127
|
+
|
|
128
|
+
function wrapOnBeforeRender(): (() => void)[] {
|
|
129
|
+
const restores: (() => void)[] = [];
|
|
130
|
+
scene.traverse((object) => {
|
|
131
|
+
if (!isRenderable(object)) return;
|
|
132
|
+
const original = object.onBeforeRender;
|
|
133
|
+
object.onBeforeRender = function patched(this: THREE.Object3D, ...args) {
|
|
134
|
+
capture.annotateNextDraw(annotationFor(object));
|
|
135
|
+
original.apply(this, args as Parameters<THREE.Object3D['onBeforeRender']>);
|
|
136
|
+
};
|
|
137
|
+
restores.push(() => {
|
|
138
|
+
object.onBeforeRender = original;
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
return restores;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function settlePending(): Pending | null {
|
|
145
|
+
const p = pending;
|
|
146
|
+
if (p && !p.settled) {
|
|
147
|
+
p.settled = true;
|
|
148
|
+
clearTimer(p.timer);
|
|
149
|
+
for (const restore of p.restores) restore();
|
|
150
|
+
pending = null;
|
|
151
|
+
return p;
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const adapter: RenderDebugAdapter = {
|
|
157
|
+
captureFrame(): Promise<FrameCapture> {
|
|
158
|
+
if (pending) {
|
|
159
|
+
return Promise.reject(new Error('RenderDebugAdapter: a frame capture is already pending'));
|
|
160
|
+
}
|
|
161
|
+
return new Promise<FrameCapture>((resolve, reject) => {
|
|
162
|
+
const restores = wrapOnBeforeRender();
|
|
163
|
+
capture.arm();
|
|
164
|
+
// Assign `pending` BEFORE arming the timer so a synchronous injected
|
|
165
|
+
// timer (test) sees a settle-able pending rather than a null one.
|
|
166
|
+
const p: Pending = { resolve, reject, restores, timer: null, settled: false };
|
|
167
|
+
pending = p;
|
|
168
|
+
p.timer = setTimer(() => {
|
|
169
|
+
if (settlePending()) {
|
|
170
|
+
reject(
|
|
171
|
+
new Error(
|
|
172
|
+
`RenderDebugAdapter: no frame rendered within ${timeoutMs}ms — the world may be paused or stopped`,
|
|
173
|
+
),
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}, timeoutMs);
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
...(memory ? { memorySnapshot: (): RenderMemorySnapshot => memory() } : {}),
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
adapter,
|
|
184
|
+
beforeRender(): void {
|
|
185
|
+
capture.beginPass();
|
|
186
|
+
},
|
|
187
|
+
afterRender(): void {
|
|
188
|
+
const result = capture.endPass();
|
|
189
|
+
if (result && pending) {
|
|
190
|
+
const p = settlePending();
|
|
191
|
+
p?.resolve(result);
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
dispose(): void {
|
|
195
|
+
const p = settlePending();
|
|
196
|
+
if (p) p.reject(new Error('RenderDebugAdapter: disposed before a frame was captured'));
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-party GPU-memory estimator (W4b, F11 profiler memory tab).
|
|
3
|
+
*
|
|
4
|
+
* Walks a live three.js scene and reports geometry/texture byte footprints
|
|
5
|
+
* from data this engine ALREADY holds — attribute buffers and texture images —
|
|
6
|
+
* plus the renderer's own resource counts (`renderer.info`). Uses three.js
|
|
7
|
+
* directly, no wrapper.
|
|
8
|
+
*
|
|
9
|
+
* HONESTY (adapters never fabricate; unmeasurable = absent-with-reason):
|
|
10
|
+
* - `estimateGeometryBytes` is EXACT: the summed `byteLength` of every
|
|
11
|
+
* attribute and index array. Not an estimate at all — it is what the
|
|
12
|
+
* buffers occupy in CPU memory (and, uploaded 1:1, on the GPU).
|
|
13
|
+
* - `estimateTextureBytes` returns `null` when the footprint is genuinely
|
|
14
|
+
* unknowable at this seam (no image dimensions, or a pixel format/type we
|
|
15
|
+
* can't size). A null NEVER enters the totals — it goes to `unestimated[]`
|
|
16
|
+
* with a reason, so the panel can say "N textures unmeasured" instead of
|
|
17
|
+
* silently undercounting.
|
|
18
|
+
* - JS heap is deliberately NOT here: `performance.memory` is a browser-only
|
|
19
|
+
* global read editor-side (M2), not an engine-scene property.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import * as THREE from 'three';
|
|
23
|
+
|
|
24
|
+
export interface RenderMemoryEntry {
|
|
25
|
+
readonly name: string;
|
|
26
|
+
readonly bytes: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface RenderMemoryUnestimated {
|
|
30
|
+
readonly name: string;
|
|
31
|
+
readonly reason: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface RenderMemorySnapshot {
|
|
35
|
+
readonly counts: {
|
|
36
|
+
readonly geometries: number;
|
|
37
|
+
readonly textures: number;
|
|
38
|
+
/** Compiled program count, or `null` when the renderer doesn't expose it
|
|
39
|
+
* (`renderer.info.programs` is populated only after the first render). */
|
|
40
|
+
readonly programs: number | null;
|
|
41
|
+
};
|
|
42
|
+
/** Sum of every ESTIMABLE texture (nulls excluded — they are in
|
|
43
|
+
* `unestimated`). */
|
|
44
|
+
readonly estimatedTextureBytes: number;
|
|
45
|
+
/** Exact sum of geometry attribute + index buffers. */
|
|
46
|
+
readonly estimatedGeometryBytes: number;
|
|
47
|
+
readonly topTextures: readonly RenderMemoryEntry[];
|
|
48
|
+
readonly topGeometries: readonly RenderMemoryEntry[];
|
|
49
|
+
readonly unestimated: readonly RenderMemoryUnestimated[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const TOP_N = 10;
|
|
53
|
+
|
|
54
|
+
/** Channel count per pixel format. Packed/compressed formats are handled by
|
|
55
|
+
* the byte-size logic in {@link estimateTextureBytes}, not here. */
|
|
56
|
+
const FORMAT_CHANNELS: Record<number, number> = {
|
|
57
|
+
[THREE.AlphaFormat]: 1,
|
|
58
|
+
[THREE.RedFormat]: 1,
|
|
59
|
+
[THREE.RedIntegerFormat]: 1,
|
|
60
|
+
[THREE.RGFormat]: 2,
|
|
61
|
+
[THREE.RGIntegerFormat]: 2,
|
|
62
|
+
[THREE.RGBAFormat]: 4,
|
|
63
|
+
[THREE.RGBAIntegerFormat]: 4,
|
|
64
|
+
[THREE.DepthFormat]: 1,
|
|
65
|
+
[THREE.DepthStencilFormat]: 2,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** Bytes per channel for a (non-packed) pixel type. */
|
|
69
|
+
const TYPE_BYTES: Record<number, number> = {
|
|
70
|
+
[THREE.UnsignedByteType]: 1,
|
|
71
|
+
[THREE.ByteType]: 1,
|
|
72
|
+
[THREE.ShortType]: 2,
|
|
73
|
+
[THREE.UnsignedShortType]: 2,
|
|
74
|
+
[THREE.IntType]: 4,
|
|
75
|
+
[THREE.UnsignedIntType]: 4,
|
|
76
|
+
[THREE.FloatType]: 4,
|
|
77
|
+
[THREE.HalfFloatType]: 2,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** Packed pixel types occupy a FIXED size per texel regardless of channel
|
|
81
|
+
* count — sized here directly rather than via channels × type-bytes. */
|
|
82
|
+
const PACKED_TYPE_TEXEL_BYTES: Record<number, number> = {
|
|
83
|
+
[THREE.UnsignedShort4444Type]: 2,
|
|
84
|
+
[THREE.UnsignedShort5551Type]: 2,
|
|
85
|
+
[THREE.UnsignedInt248Type]: 4,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
function bytesPerTexel(format: number, type: number): number | null {
|
|
89
|
+
const packed = PACKED_TYPE_TEXEL_BYTES[type];
|
|
90
|
+
if (packed !== undefined) return packed;
|
|
91
|
+
const channels = FORMAT_CHANNELS[format];
|
|
92
|
+
const typeBytes = TYPE_BYTES[type];
|
|
93
|
+
if (channels === undefined || typeBytes === undefined) return null;
|
|
94
|
+
return channels * typeBytes;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** EXACT CPU/GPU footprint of a geometry's attribute + index buffers. */
|
|
98
|
+
export function estimateGeometryBytes(geometry: THREE.BufferGeometry): number {
|
|
99
|
+
// Interleaved attributes share ONE backing buffer (`attribute.data.array`);
|
|
100
|
+
// a plain attribute owns its own (`attribute.array`). Count each unique
|
|
101
|
+
// backing view exactly once.
|
|
102
|
+
const counted = new Set<ArrayBufferView>();
|
|
103
|
+
let bytes = 0;
|
|
104
|
+
const add = (view: ArrayBufferView | undefined): void => {
|
|
105
|
+
if (view && !counted.has(view)) {
|
|
106
|
+
counted.add(view);
|
|
107
|
+
bytes += view.byteLength;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
for (const attribute of Object.values(geometry.attributes)) {
|
|
111
|
+
const interleaved = (attribute as THREE.InterleavedBufferAttribute).data?.array as
|
|
112
|
+
| ArrayBufferView
|
|
113
|
+
| undefined;
|
|
114
|
+
add(interleaved ?? ((attribute as THREE.BufferAttribute).array as ArrayBufferView | undefined));
|
|
115
|
+
}
|
|
116
|
+
add(geometry.index?.array as ArrayBufferView | undefined);
|
|
117
|
+
return bytes;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Estimated GPU footprint of a texture, or `null` when unknowable at this
|
|
121
|
+
* seam (the caller routes null into `unestimated[]` with a reason). */
|
|
122
|
+
export function estimateTextureBytes(texture: THREE.Texture): number | null {
|
|
123
|
+
// Compressed textures carry their real per-mip byte arrays — sum them; no
|
|
124
|
+
// format math needed and no estimate involved.
|
|
125
|
+
const compressed = texture as THREE.CompressedTexture;
|
|
126
|
+
if (
|
|
127
|
+
(compressed as { isCompressedTexture?: boolean }).isCompressedTexture &&
|
|
128
|
+
Array.isArray(compressed.mipmaps)
|
|
129
|
+
) {
|
|
130
|
+
let bytes = 0;
|
|
131
|
+
for (const mip of compressed.mipmaps) {
|
|
132
|
+
const data = (mip as { data?: ArrayBufferView }).data;
|
|
133
|
+
if (data?.byteLength) bytes += data.byteLength;
|
|
134
|
+
}
|
|
135
|
+
return bytes;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const image = texture.image as { width?: number; height?: number } | undefined;
|
|
139
|
+
const width = image?.width;
|
|
140
|
+
const height = image?.height;
|
|
141
|
+
if (!width || !height) return null;
|
|
142
|
+
|
|
143
|
+
const bpt = bytesPerTexel(texture.format, texture.type);
|
|
144
|
+
if (bpt === null) return null;
|
|
145
|
+
|
|
146
|
+
const base = width * height * bpt;
|
|
147
|
+
return texture.generateMipmaps ? Math.round(base * 1.333) : base;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function textureName(texture: THREE.Texture): string {
|
|
151
|
+
return texture.name || (texture.image as { src?: string })?.src || `texture:${texture.uuid}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function geometryName(geometry: THREE.BufferGeometry): string {
|
|
155
|
+
return geometry.name || `${geometry.type}:${geometry.uuid}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function unestimatedReason(texture: THREE.Texture): string {
|
|
159
|
+
const image = texture.image as { width?: number; height?: number } | undefined;
|
|
160
|
+
if (!image?.width || !image?.height) return 'no image dimensions available';
|
|
161
|
+
return `unsupported format/type (format 0x${texture.format.toString(16)}, type 0x${texture.type.toString(16)})`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Collect every unique geometry and texture referenced by a scene graph. */
|
|
165
|
+
function collectResources(scene: THREE.Object3D): {
|
|
166
|
+
geometries: Map<string, THREE.BufferGeometry>;
|
|
167
|
+
textures: Map<string, THREE.Texture>;
|
|
168
|
+
} {
|
|
169
|
+
const geometries = new Map<string, THREE.BufferGeometry>();
|
|
170
|
+
const textures = new Map<string, THREE.Texture>();
|
|
171
|
+
|
|
172
|
+
const addMaterialTextures = (material: THREE.Material): void => {
|
|
173
|
+
for (const value of Object.values(material as unknown as Record<string, unknown>)) {
|
|
174
|
+
if (value && (value as THREE.Texture).isTexture) {
|
|
175
|
+
const tex = value as THREE.Texture;
|
|
176
|
+
textures.set(tex.uuid, tex);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
scene.traverse((object) => {
|
|
182
|
+
const geometry = (object as THREE.Mesh).geometry as THREE.BufferGeometry | undefined;
|
|
183
|
+
if (geometry?.isBufferGeometry) geometries.set(geometry.uuid, geometry);
|
|
184
|
+
const material = (object as THREE.Mesh).material;
|
|
185
|
+
if (Array.isArray(material)) material.forEach(addMaterialTextures);
|
|
186
|
+
else if (material) addMaterialTextures(material);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Scene-level environment/background textures count too.
|
|
190
|
+
for (const maybe of [scene as unknown as THREE.Scene]) {
|
|
191
|
+
for (const value of [maybe.background, maybe.environment]) {
|
|
192
|
+
if (value && (value as THREE.Texture).isTexture) {
|
|
193
|
+
const tex = value as THREE.Texture;
|
|
194
|
+
textures.set(tex.uuid, tex);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { geometries, textures };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function collectRenderMemory(
|
|
203
|
+
scene: THREE.Object3D,
|
|
204
|
+
info: THREE.WebGLInfo,
|
|
205
|
+
): RenderMemorySnapshot {
|
|
206
|
+
const { geometries, textures } = collectResources(scene);
|
|
207
|
+
|
|
208
|
+
const geometryEntries: RenderMemoryEntry[] = [];
|
|
209
|
+
let estimatedGeometryBytes = 0;
|
|
210
|
+
for (const geometry of geometries.values()) {
|
|
211
|
+
const bytes = estimateGeometryBytes(geometry);
|
|
212
|
+
estimatedGeometryBytes += bytes;
|
|
213
|
+
geometryEntries.push({ name: geometryName(geometry), bytes });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const textureEntries: RenderMemoryEntry[] = [];
|
|
217
|
+
const unestimated: RenderMemoryUnestimated[] = [];
|
|
218
|
+
let estimatedTextureBytes = 0;
|
|
219
|
+
for (const texture of textures.values()) {
|
|
220
|
+
const bytes = estimateTextureBytes(texture);
|
|
221
|
+
if (bytes === null) {
|
|
222
|
+
unestimated.push({ name: textureName(texture), reason: unestimatedReason(texture) });
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
estimatedTextureBytes += bytes;
|
|
226
|
+
textureEntries.push({ name: textureName(texture), bytes });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const byBytesDesc = (a: RenderMemoryEntry, b: RenderMemoryEntry): number => b.bytes - a.bytes;
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
counts: {
|
|
233
|
+
geometries: info.memory.geometries,
|
|
234
|
+
textures: info.memory.textures,
|
|
235
|
+
programs: info.programs ? info.programs.length : null,
|
|
236
|
+
},
|
|
237
|
+
estimatedTextureBytes,
|
|
238
|
+
estimatedGeometryBytes,
|
|
239
|
+
topTextures: textureEntries.sort(byBytesDesc).slice(0, TOP_N),
|
|
240
|
+
topGeometries: geometryEntries.sort(byBytesDesc).slice(0, TOP_N),
|
|
241
|
+
unestimated,
|
|
242
|
+
};
|
|
243
|
+
}
|