@vgai/engine 0.5.7 → 0.5.9
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/ingest/contract-debug-adapter.d.ts +35 -0
- package/dist/adapter/ingest/contract-debug-adapter.d.ts.map +1 -0
- package/dist/adapter/ingest/contract-debug-adapter.js +90 -0
- package/dist/adapter/ingest/game-contract.d.ts +60 -1
- package/dist/adapter/ingest/game-contract.d.ts.map +1 -1
- package/dist/adapter/ingest/game-contract.js +1 -1
- package/dist/adapter/setup-three-root-adapter.d.ts.map +1 -1
- package/dist/adapter/setup-three-root-adapter.js +94 -2
- package/dist/core/game-scoped-slot.d.ts +14 -0
- package/dist/core/game-scoped-slot.d.ts.map +1 -0
- package/dist/core/game-scoped-slot.js +24 -0
- package/dist/core/seeded-random.d.ts.map +1 -1
- package/dist/core/seeded-random.js +3 -2
- package/dist/core/sim-clock.d.ts.map +1 -1
- package/dist/core/sim-clock.js +3 -2
- package/dist/dev/performance-profiler.d.ts +13 -7
- package/dist/dev/performance-profiler.d.ts.map +1 -1
- package/dist/dev/performance-profiler.js +31 -3
- package/dist/dev/register-render-vitals.d.ts +95 -0
- package/dist/dev/register-render-vitals.d.ts.map +1 -0
- package/dist/dev/register-render-vitals.js +182 -0
- package/dist/dev/render-census.d.ts +135 -0
- package/dist/dev/render-census.d.ts.map +1 -0
- package/dist/dev/render-census.js +257 -0
- package/dist/dev/render-vitals.d.ts +181 -0
- package/dist/dev/render-vitals.d.ts.map +1 -0
- package/dist/dev/render-vitals.js +232 -0
- package/dist/dev/static-batch-advisor.d.ts +106 -0
- package/dist/dev/static-batch-advisor.d.ts.map +1 -0
- package/dist/dev/static-batch-advisor.js +141 -0
- package/dist/render/render-batch-system.d.ts.map +1 -1
- package/dist/render/render-batch-system.js +7 -18
- package/dist/render/structural-signature.d.ts +148 -0
- package/dist/render/structural-signature.d.ts.map +1 -0
- package/dist/render/structural-signature.js +193 -0
- package/dist/runtime/debug-registry.d.ts +2 -2
- package/dist/runtime/debug-registry.d.ts.map +1 -1
- package/dist/runtime/debug-registry.js +4 -3
- package/dist/runtime/dev-layers.d.ts.map +1 -1
- package/dist/runtime/dev-layers.js +6 -0
- package/dist/runtime/game.js +2 -2
- package/dist/runtime/gameplay-rng-trap.d.ts.map +1 -1
- package/dist/runtime/gameplay-rng-trap.js +2 -1
- package/package.json +1 -1
- package/schemas/engine-capabilities.json +5 -5
- package/src/adapter/ingest/contract-debug-adapter.ts +110 -0
- package/src/adapter/ingest/game-contract.ts +63 -1
- package/src/adapter/setup-three-root-adapter.ts +94 -2
- package/src/core/game-scoped-slot.ts +28 -0
- package/src/core/seeded-random.ts +3 -2
- package/src/core/sim-clock.ts +3 -2
- package/src/dev/performance-profiler.ts +47 -12
- package/src/dev/register-render-vitals.ts +249 -0
- package/src/dev/render-census.ts +351 -0
- package/src/dev/render-vitals.ts +338 -0
- package/src/dev/static-batch-advisor.ts +186 -0
- package/src/render/render-batch-system.ts +16 -19
- package/src/render/structural-signature.ts +231 -0
- package/src/runtime/debug-registry.ts +4 -3
- package/src/runtime/dev-layers.ts +7 -1
- package/src/runtime/game.ts +2 -2
- package/src/runtime/gameplay-rng-trap.ts +4 -2
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE ROUTING from a draw-call reading to the one-line fix.
|
|
3
|
+
*
|
|
4
|
+
* `dev/render-vitals.ts` can already tell a game it is submitting 4,000 draws
|
|
5
|
+
* and `dev/render-census.ts` can already say which subtree they are in. Both
|
|
6
|
+
* require someone to ASK, and both require that someone to already know that
|
|
7
|
+
* static batching exists, is possible here, and is spelled `<Frozen>`. An
|
|
8
|
+
* agent building a game does not know any of that, so the measurement has to
|
|
9
|
+
* do the routing itself — the same idiom as the dev-menu's unconfigured-section
|
|
10
|
+
* warning: the reading names the exact edit.
|
|
11
|
+
*
|
|
12
|
+
* ── THE DECISION IS PURE, THE SCHEDULE IS NOT ───────────────────────────────
|
|
13
|
+
* {@link decideStaticBatchAdvisory} takes a draw-call count and two scan
|
|
14
|
+
* reports and answers with an advisory or `null`. It reads no clock, no
|
|
15
|
+
* scene, no console. `dev/register-render-vitals.ts` owns the impure half —
|
|
16
|
+
* when to scan, and warning once — because that is where the profiler
|
|
17
|
+
* subscription already lives.
|
|
18
|
+
*
|
|
19
|
+
* ── COST ────────────────────────────────────────────────────────────────────
|
|
20
|
+
* The scan is TWO walks of the scene graph, once, after the frame rate has
|
|
21
|
+
* settled ({@link ADVISOR_SETTLE_FRAMES}). Never per frame: a walk of 4,000
|
|
22
|
+
* nodes every frame is itself the kind of cost this advisory exists to
|
|
23
|
+
* remove, and the answer does not change from one frame to the next in a world
|
|
24
|
+
* whose scenery is mount-static — which is the only world the advice applies
|
|
25
|
+
* to anyway.
|
|
26
|
+
*
|
|
27
|
+
* ── WHY IT ASKS INSTEAD OF ACTING ───────────────────────────────────────────
|
|
28
|
+
* "These 2,600 meshes are the same draw" is measurable. "These 2,600 meshes
|
|
29
|
+
* never move" is NOT — nothing in a scene graph distinguishes scenery from a
|
|
30
|
+
* thing that will move on the next input. Inferring it and batching anyway is
|
|
31
|
+
* how a batcher freezes a door half-open. So the advisory names the subtree
|
|
32
|
+
* and the wrapper, and the author (who knows) places it.
|
|
33
|
+
*/
|
|
34
|
+
import type { CensusReport, StructuralBatchReport } from './render-census';
|
|
35
|
+
/**
|
|
36
|
+
* Draw calls below which no advisory fires, however batchable the scene.
|
|
37
|
+
*
|
|
38
|
+
* The field measurement this capability came out of: ~4,000 draws cost ~11 ms
|
|
39
|
+
* of CPU submission per frame — about 2.75 µs each on a desktop browser. At
|
|
40
|
+
* 500 draws that is ~1.4 ms, roughly 8% of a 60 Hz frame: the first point
|
|
41
|
+
* where halving it is a visible win rather than noise a profiler cannot
|
|
42
|
+
* separate from jitter. Below it, an advisory would be a nag pointing at
|
|
43
|
+
* something that is not costing anything, and a nag that is usually wrong is
|
|
44
|
+
* one nobody reads when it is right.
|
|
45
|
+
*/
|
|
46
|
+
export declare const ADVISOR_DRAW_CALL_THRESHOLD = 500;
|
|
47
|
+
/**
|
|
48
|
+
* The share of the draw calls that must be collapsible before the advice is
|
|
49
|
+
* worth an edit. Half: below that, the wrapper leaves most of the cost exactly
|
|
50
|
+
* where it was, and "you could remove a third of a third" is not a payoff
|
|
51
|
+
* anyone should restructure a scene for.
|
|
52
|
+
*/
|
|
53
|
+
export declare const ADVISOR_COLLAPSIBLE_SHARE = 0.5;
|
|
54
|
+
/**
|
|
55
|
+
* A subtree must hold at least this share of the collapsible meshes to be
|
|
56
|
+
* named as THE address. Under it the advisory says "across the scene" — an
|
|
57
|
+
* invented address is worse than none, because the reader wraps the wrong
|
|
58
|
+
* group and measures no change.
|
|
59
|
+
*/
|
|
60
|
+
export declare const ADVISOR_SUBTREE_SHARE = 0.4;
|
|
61
|
+
/**
|
|
62
|
+
* Presented frames to wait before scanning — ~2 s at 60 Hz. Long enough for
|
|
63
|
+
* asset loads and the first setup pass to finish populating the graph (a scan
|
|
64
|
+
* at frame one measures an empty world and stays silent forever), short enough
|
|
65
|
+
* that the line lands while the author is still looking at the boot.
|
|
66
|
+
*/
|
|
67
|
+
export declare const ADVISOR_SETTLE_FRAMES = 120;
|
|
68
|
+
/** The finding, as a caller can present it however it likes. */
|
|
69
|
+
export interface StaticBatchAdvisory {
|
|
70
|
+
/** The reading that triggered it. */
|
|
71
|
+
readonly drawCalls: number;
|
|
72
|
+
/** Meshes sitting in a structural family of two or more. */
|
|
73
|
+
readonly collapsible: number;
|
|
74
|
+
/** The subtree holding most of them, or `null` when they are spread out. */
|
|
75
|
+
readonly subtree: string | null;
|
|
76
|
+
/** The exact edit, ready to paste — `<Frozen name="Terminal">`. */
|
|
77
|
+
readonly fix: string;
|
|
78
|
+
/** The one-line console message: payoff, address, edit, install. */
|
|
79
|
+
readonly message: string;
|
|
80
|
+
}
|
|
81
|
+
export interface StaticBatchAdvisoryInput {
|
|
82
|
+
/** `render.vitals`' own reading. `null` before the first presented frame. */
|
|
83
|
+
readonly drawCalls: number | null;
|
|
84
|
+
/** The one-level census, used only to sanity-check the address against a
|
|
85
|
+
* subtree that genuinely exists in the graph. */
|
|
86
|
+
readonly census: CensusReport;
|
|
87
|
+
/** The STRUCTURAL scan — see `render-census.ts` for why the identity scan
|
|
88
|
+
* cannot answer this. */
|
|
89
|
+
readonly structural: StructuralBatchReport;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Decide whether this frame's cost is worth an advisory, and what it should
|
|
93
|
+
* say. Pure — every input is an argument, and the same arguments always
|
|
94
|
+
* produce the same message.
|
|
95
|
+
*/
|
|
96
|
+
export declare function decideStaticBatchAdvisory(input: StaticBatchAdvisoryInput): StaticBatchAdvisory | null;
|
|
97
|
+
/**
|
|
98
|
+
* Emit `advisory` on the console, at most once per page. Answers whether it
|
|
99
|
+
* warned, so a caller can stop scanning.
|
|
100
|
+
*/
|
|
101
|
+
export declare function warnStaticBatchAdvisory(advisory: StaticBatchAdvisory,
|
|
102
|
+
/** Injectable so the decision is testable without a console. */
|
|
103
|
+
warn?: (message: string) => void): boolean;
|
|
104
|
+
/** Test-only: forget that the advisory was ever emitted. */
|
|
105
|
+
export declare function __resetStaticBatchAdvisoryForTest(): void;
|
|
106
|
+
//# sourceMappingURL=static-batch-advisor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"static-batch-advisor.d.ts","sourceRoot":"","sources":["../../src/dev/static-batch-advisor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAE3E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,2BAA2B,MAAM,CAAC;AAE/C;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAE7C;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,MAAM,CAAC;AAEzC;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,MAAM,CAAC;AAEzC,gEAAgE;AAChE,MAAM,WAAW,mBAAmB;IAClC,qCAAqC;IACrC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,4DAA4D;IAC5D,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,4EAA4E;IAC5E,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,mEAAmE;IACnE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,wBAAwB;IACvC,6EAA6E;IAC7E,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC;sDACkD;IAClD,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B;8BAC0B;IAC1B,QAAQ,CAAC,UAAU,EAAE,qBAAqB,CAAC;CAC5C;AAQD;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,wBAAwB,GAC9B,mBAAmB,GAAG,IAAI,CA+B5B;AAyBD;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,mBAAmB;AAC7B,gEAAgE;AAChE,IAAI,GAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAoB,GAC9C,OAAO,CAKT;AAED,4DAA4D;AAC5D,wBAAgB,iCAAiC,IAAI,IAAI,CAExD"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE ROUTING from a draw-call reading to the one-line fix.
|
|
3
|
+
*
|
|
4
|
+
* `dev/render-vitals.ts` can already tell a game it is submitting 4,000 draws
|
|
5
|
+
* and `dev/render-census.ts` can already say which subtree they are in. Both
|
|
6
|
+
* require someone to ASK, and both require that someone to already know that
|
|
7
|
+
* static batching exists, is possible here, and is spelled `<Frozen>`. An
|
|
8
|
+
* agent building a game does not know any of that, so the measurement has to
|
|
9
|
+
* do the routing itself — the same idiom as the dev-menu's unconfigured-section
|
|
10
|
+
* warning: the reading names the exact edit.
|
|
11
|
+
*
|
|
12
|
+
* ── THE DECISION IS PURE, THE SCHEDULE IS NOT ───────────────────────────────
|
|
13
|
+
* {@link decideStaticBatchAdvisory} takes a draw-call count and two scan
|
|
14
|
+
* reports and answers with an advisory or `null`. It reads no clock, no
|
|
15
|
+
* scene, no console. `dev/register-render-vitals.ts` owns the impure half —
|
|
16
|
+
* when to scan, and warning once — because that is where the profiler
|
|
17
|
+
* subscription already lives.
|
|
18
|
+
*
|
|
19
|
+
* ── COST ────────────────────────────────────────────────────────────────────
|
|
20
|
+
* The scan is TWO walks of the scene graph, once, after the frame rate has
|
|
21
|
+
* settled ({@link ADVISOR_SETTLE_FRAMES}). Never per frame: a walk of 4,000
|
|
22
|
+
* nodes every frame is itself the kind of cost this advisory exists to
|
|
23
|
+
* remove, and the answer does not change from one frame to the next in a world
|
|
24
|
+
* whose scenery is mount-static — which is the only world the advice applies
|
|
25
|
+
* to anyway.
|
|
26
|
+
*
|
|
27
|
+
* ── WHY IT ASKS INSTEAD OF ACTING ───────────────────────────────────────────
|
|
28
|
+
* "These 2,600 meshes are the same draw" is measurable. "These 2,600 meshes
|
|
29
|
+
* never move" is NOT — nothing in a scene graph distinguishes scenery from a
|
|
30
|
+
* thing that will move on the next input. Inferring it and batching anyway is
|
|
31
|
+
* how a batcher freezes a door half-open. So the advisory names the subtree
|
|
32
|
+
* and the wrapper, and the author (who knows) places it.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* Draw calls below which no advisory fires, however batchable the scene.
|
|
36
|
+
*
|
|
37
|
+
* The field measurement this capability came out of: ~4,000 draws cost ~11 ms
|
|
38
|
+
* of CPU submission per frame — about 2.75 µs each on a desktop browser. At
|
|
39
|
+
* 500 draws that is ~1.4 ms, roughly 8% of a 60 Hz frame: the first point
|
|
40
|
+
* where halving it is a visible win rather than noise a profiler cannot
|
|
41
|
+
* separate from jitter. Below it, an advisory would be a nag pointing at
|
|
42
|
+
* something that is not costing anything, and a nag that is usually wrong is
|
|
43
|
+
* one nobody reads when it is right.
|
|
44
|
+
*/
|
|
45
|
+
export const ADVISOR_DRAW_CALL_THRESHOLD = 500;
|
|
46
|
+
/**
|
|
47
|
+
* The share of the draw calls that must be collapsible before the advice is
|
|
48
|
+
* worth an edit. Half: below that, the wrapper leaves most of the cost exactly
|
|
49
|
+
* where it was, and "you could remove a third of a third" is not a payoff
|
|
50
|
+
* anyone should restructure a scene for.
|
|
51
|
+
*/
|
|
52
|
+
export const ADVISOR_COLLAPSIBLE_SHARE = 0.5;
|
|
53
|
+
/**
|
|
54
|
+
* A subtree must hold at least this share of the collapsible meshes to be
|
|
55
|
+
* named as THE address. Under it the advisory says "across the scene" — an
|
|
56
|
+
* invented address is worse than none, because the reader wraps the wrong
|
|
57
|
+
* group and measures no change.
|
|
58
|
+
*/
|
|
59
|
+
export const ADVISOR_SUBTREE_SHARE = 0.4;
|
|
60
|
+
/**
|
|
61
|
+
* Presented frames to wait before scanning — ~2 s at 60 Hz. Long enough for
|
|
62
|
+
* asset loads and the first setup pass to finish populating the graph (a scan
|
|
63
|
+
* at frame one measures an empty world and stays silent forever), short enough
|
|
64
|
+
* that the line lands while the author is still looking at the boot.
|
|
65
|
+
*/
|
|
66
|
+
export const ADVISOR_SETTLE_FRAMES = 120;
|
|
67
|
+
/** `2600` → `2,600`. Digits a person reads at a glance, in the one place the
|
|
68
|
+
* message is built, so every number in it is grouped the same way. */
|
|
69
|
+
function grouped(value) {
|
|
70
|
+
return value.toLocaleString('en-US');
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Decide whether this frame's cost is worth an advisory, and what it should
|
|
74
|
+
* say. Pure — every input is an argument, and the same arguments always
|
|
75
|
+
* produce the same message.
|
|
76
|
+
*/
|
|
77
|
+
export function decideStaticBatchAdvisory(input) {
|
|
78
|
+
const { drawCalls, census, structural } = input;
|
|
79
|
+
if (drawCalls === null || drawCalls < ADVISOR_DRAW_CALL_THRESHOLD)
|
|
80
|
+
return null;
|
|
81
|
+
const { collapsible } = structural;
|
|
82
|
+
if (collapsible < drawCalls * ADVISOR_COLLAPSIBLE_SHARE)
|
|
83
|
+
return null;
|
|
84
|
+
// The address, when one subtree genuinely dominates. `bySubtree` is already
|
|
85
|
+
// sorted, and a name that no census row confirms is not offered: it would
|
|
86
|
+
// send the reader looking for a node the other render.* commands cannot
|
|
87
|
+
// find either.
|
|
88
|
+
const addressable = new Set(census.subtrees.map((row) => row.name));
|
|
89
|
+
const leader = structural.bySubtree[0];
|
|
90
|
+
const subtree = leader &&
|
|
91
|
+
leader.collapsible >= collapsible * ADVISOR_SUBTREE_SHARE &&
|
|
92
|
+
addressable.has(leader.name)
|
|
93
|
+
? leader.name
|
|
94
|
+
: null;
|
|
95
|
+
const fix = subtree === null ? '<Frozen>' : `<Frozen name="${subtree}">`;
|
|
96
|
+
const where = subtree === null ? 'spread across the scene' : `mostly under "${subtree}"`;
|
|
97
|
+
const message = `[static-batch] ~${grouped(collapsible)} of ${grouped(drawCalls)} draw calls are the same ` +
|
|
98
|
+
`handful of draws repeated (${grouped(structural.familyCount)} structural families), ` +
|
|
99
|
+
`${where}. If that scenery is mount-static — nothing under it moves, re-colours or ` +
|
|
100
|
+
`unmounts after mount — one wrapper collapses it to a few draws: wrap it in ` +
|
|
101
|
+
`${fix}…</Frozen> (vgai add static-batch). Reactive scenery goes outside the wrapper, ` +
|
|
102
|
+
`and a subtree that must stay unbatched declares it: userData={{ staticBatch: false }}.`;
|
|
103
|
+
return { drawCalls, collapsible, subtree, fix, message };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Once per PAGE, not once per module evaluation — a hot reload re-runs this
|
|
107
|
+
* module, and an advisory that reappears on every save is one that gets muted
|
|
108
|
+
* along with everything else on the console. Same mechanism, and the same
|
|
109
|
+
* reason, as the dev menu's unconfigured-section warning.
|
|
110
|
+
*/
|
|
111
|
+
const WARNED_KEY = '__vgaiStaticBatchAdvised';
|
|
112
|
+
function alreadyWarned() {
|
|
113
|
+
return globalThis[WARNED_KEY] === true;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The console IS this advisory's channel: `vgai status` reports console
|
|
117
|
+
* warnings, which is where a building agent already looks. An in-editor
|
|
118
|
+
* banner would be one nobody opens, and a provider would be one nobody reads
|
|
119
|
+
* without already knowing to ask.
|
|
120
|
+
*/
|
|
121
|
+
function warnOnConsole(message) {
|
|
122
|
+
// biome-ignore lint/suspicious/noConsole: this function's entire job — see above.
|
|
123
|
+
console.warn(message);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Emit `advisory` on the console, at most once per page. Answers whether it
|
|
127
|
+
* warned, so a caller can stop scanning.
|
|
128
|
+
*/
|
|
129
|
+
export function warnStaticBatchAdvisory(advisory,
|
|
130
|
+
/** Injectable so the decision is testable without a console. */
|
|
131
|
+
warn = warnOnConsole) {
|
|
132
|
+
if (alreadyWarned())
|
|
133
|
+
return false;
|
|
134
|
+
globalThis[WARNED_KEY] = true;
|
|
135
|
+
warn(advisory.message);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
/** Test-only: forget that the advisory was ever emitted. */
|
|
139
|
+
export function __resetStaticBatchAdvisoryForTest() {
|
|
140
|
+
globalThis[WARNED_KEY] = undefined;
|
|
141
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-batch-system.d.ts","sourceRoot":"","sources":["../../src/render/render-batch-system.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"render-batch-system.d.ts","sourceRoot":"","sources":["../../src/render/render-batch-system.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AA6FhE,qBAAa,iBAAiB;IAU1B,OAAO,CAAC,KAAK;IACb,OAAO,CAAC,QAAQ;IAVlB,OAAO,CAAC,IAAI,CAAqB;IACjC,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,aAAa,CAAe;IAEpC,OAAO,CAAC,QAAQ,CAA6C;IAE7D,OAAO,CAAC,SAAS,CAAoD;gBAG3D,KAAK,EAAE,KAAK,CAAC,KAAK,EAClB,QAAQ,EAAE,sBAAsB;IAM1C,wEAAwE;IACxE,KAAK,IAAI,IAAI;IA0Bb,OAAO,CAAC,SAAS;IAyCjB;;;;;OAKG;IACH,MAAM,IAAI,IAAI;IAyBd;;;OAGG;IACH,OAAO,CAAC,SAAS,EAAE,KAAK,CAAC,SAAS,GAAG,KAAK,CAAC,IAAI,GAAG,IAAI;IAStD,kEAAkE;IAClE,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,4DAA4D;IAC5D,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,QAAQ,IAAI,IAAI;CAiBjB"}
|
|
@@ -1,25 +1,14 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
2
|
import { getUserData } from '../ecs/user-data';
|
|
3
|
+
import { staticBatchSignature } from './structural-signature';
|
|
3
4
|
// Structural signature — groups meshes that are VALUE-identical, not object-identical.
|
|
4
5
|
// The engine scene-loader instantiates a fresh geometry+material per entity (no dedup),
|
|
5
|
-
// so keying on .uuid would never batch a real scene
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return params ? `${g.type}:${JSON.stringify(params)}` : `uuid:${g.uuid}`;
|
|
12
|
-
};
|
|
13
|
-
const matKey = (m) => {
|
|
14
|
-
const s = m;
|
|
15
|
-
const col = s.color?.getHexString?.() ?? '';
|
|
16
|
-
const map = s.map?.uuid ?? '';
|
|
17
|
-
return `${m.type}:${col}:${s.roughness ?? ''}:${s.metalness ?? ''}:${map}:${m.side}:${m.transparent}:${m.vertexColors}`;
|
|
18
|
-
};
|
|
19
|
-
const SIG = (m) =>
|
|
20
|
-
// include shadow flags: meshes with different cast/receive must not share a batch
|
|
21
|
-
// (the InstancedMesh carries one flag for the whole group).
|
|
22
|
-
`${geoKey(m.geometry)}|${matKey(m.material)}|${m.castShadow ? 1 : 0}${m.receiveShadow ? 1 : 0}`;
|
|
6
|
+
// so keying on .uuid would never batch a real scene; a TSX/R3F world writing inline
|
|
7
|
+
// `<meshStandardMaterial>` elements has exactly the same property. That key lives in
|
|
8
|
+
// `render/structural-signature.ts` — ONE owner, shared with the `static-batch`
|
|
9
|
+
// capability's `<Frozen>`, so the two cannot drift into disagreeing about what
|
|
10
|
+
// "the same draw" means. `staticBatchSignature` is this file's original key, moved.
|
|
11
|
+
const SIG = staticBatchSignature;
|
|
23
12
|
/** Match Three.js renderer visibility: a hidden ancestor hides the whole subtree. */
|
|
24
13
|
function isEffectivelyVisible(obj) {
|
|
25
14
|
let cur = obj;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STRUCTURAL IDENTITY AND ELIGIBILITY for static render batching — the one
|
|
3
|
+
* place that answers "may this draw be collapsed at all?" and "are these two
|
|
4
|
+
* meshes the same thing drawn twice?".
|
|
5
|
+
*
|
|
6
|
+
* Three consumers, which is the whole reason it is a module rather than three
|
|
7
|
+
* private helpers: `render/render-batch-system.ts` (the engine's own
|
|
8
|
+
* lab-driven instancer), `dev/render-census.ts`'s structural scan (what the
|
|
9
|
+
* advisor and `render.families` measure), and the `static-batch` capability's
|
|
10
|
+
* `<Frozen>` (what a game actually installs). A batcher and the advisor that
|
|
11
|
+
* routes people to it MUST agree about what is batchable, or the advisor
|
|
12
|
+
* promises a win the wrapper then declines to take.
|
|
13
|
+
*
|
|
14
|
+
* ── THE UUID TRAP, WHICH IS WHY THIS MODULE EXISTS ──────────────────────────
|
|
15
|
+
* The obvious key for "can these two meshes be drawn as one" is object
|
|
16
|
+
* identity: same `geometry.uuid`, same `material.uuid`. It is also the key
|
|
17
|
+
* that silently batches NOTHING in the shape that matters most. A freshly
|
|
18
|
+
* scaffolded TSX/R3F world writes its materials INLINE —
|
|
19
|
+
*
|
|
20
|
+
* {crates.map((c) => (
|
|
21
|
+
* <mesh key={c.id} position={c.at}>
|
|
22
|
+
* <boxGeometry args={[1, 1, 1]} />
|
|
23
|
+
* <meshStandardMaterial color="#8a6a44" />
|
|
24
|
+
* </mesh>
|
|
25
|
+
* ))}
|
|
26
|
+
*
|
|
27
|
+
* — and every one of those `<meshStandardMaterial>` elements constructs its
|
|
28
|
+
* OWN `THREE.MeshStandardMaterial`. Five hundred crates are five hundred
|
|
29
|
+
* distinct uuids describing one identical appearance. A uuid-keyed grouper
|
|
30
|
+
* reports five hundred families of one, finds nothing to do, and is indistin-
|
|
31
|
+
* guishable from a correct batcher over an unbatchable scene. The engine's own
|
|
32
|
+
* scene construction has the same property, which is why
|
|
33
|
+
* `render/render-batch-system.ts` has keyed on VALUE since it was written.
|
|
34
|
+
*
|
|
35
|
+
* So the key here is the STRUCTURE: geometry `type` + its construction
|
|
36
|
+
* `parameters`, material `type` + the props that decide what the draw looks
|
|
37
|
+
* like. Two independently constructed `BoxGeometry(1,1,1)` +
|
|
38
|
+
* `MeshStandardMaterial({color:'#8a6a44'})` pairs are the same draw, and this
|
|
39
|
+
* module says so.
|
|
40
|
+
*
|
|
41
|
+
* ── WHERE VALUE CANNOT ANSWER, IDENTITY IS THE SAFE FALLBACK ────────────────
|
|
42
|
+
* Twice below, a structural comparison would be a guess, and the answer is
|
|
43
|
+
* `uuid:` — a key nothing else can equal, so the members simply do not group.
|
|
44
|
+
* Not grouping costs a draw call; grouping two things that only LOOK alike
|
|
45
|
+
* renders the wrong picture.
|
|
46
|
+
* - geometry with no `.parameters` (a loaded GLTF mesh, a hand-built
|
|
47
|
+
* `BufferGeometry`): its vertices are its identity and comparing them is
|
|
48
|
+
* not a signature, it is a diff.
|
|
49
|
+
* - a shader material ({@link materialMergeSignature} only): its appearance
|
|
50
|
+
* lives in shader source and uniforms that no fixed prop list can read.
|
|
51
|
+
*
|
|
52
|
+
* ── TWO KEYS, DELIBERATELY ──────────────────────────────────────────────────
|
|
53
|
+
* {@link staticBatchSignature} is `RenderBatchSystem`'s key, extracted here
|
|
54
|
+
* unchanged so there is one owner of the answer rather than two that drift.
|
|
55
|
+
* {@link materialMergeSignature} is STRICTER, and it is what the `static-batch`
|
|
56
|
+
* capability's `<Frozen>` groups by: that path hands ONE material instance to
|
|
57
|
+
* a merged/instanced product, so its members must be interchangeable, not
|
|
58
|
+
* merely similar. The looser key predates it and is kept exactly as it was —
|
|
59
|
+
* see that constant's own note.
|
|
60
|
+
*/
|
|
61
|
+
import type * as THREE from 'three';
|
|
62
|
+
/**
|
|
63
|
+
* The `userData` key a node opts OUT of static batching with — the one
|
|
64
|
+
* declaration that beats every measurement:
|
|
65
|
+
*
|
|
66
|
+
* <group name="Beacons" userData={{ staticBatch: false }}>
|
|
67
|
+
*
|
|
68
|
+
* Set on a node, it covers that node's whole subtree (a walker stops there).
|
|
69
|
+
* It is deliberately `userData` and not a component prop: the thing being
|
|
70
|
+
* excluded is a three node, and every authoring lane — TSX, a loaded GLTF, a
|
|
71
|
+
* hand-built graph — can set `userData` on one.
|
|
72
|
+
*/
|
|
73
|
+
export declare const STATIC_BATCH_OPT_OUT_KEY = "staticBatch";
|
|
74
|
+
/**
|
|
75
|
+
* Why one node may not be collapsed into a batch. `null` from
|
|
76
|
+
* {@link staticBatchSkipReason} means it may.
|
|
77
|
+
*
|
|
78
|
+
* Each of these is a case where a batched draw would render something DIFFERENT
|
|
79
|
+
* from the originals, not merely a case that is awkward to implement:
|
|
80
|
+
* - `instanced` / `batched` — already one draw; swallowing it would flatten
|
|
81
|
+
* per-instance transforms the batch does not carry.
|
|
82
|
+
* - `skinned` — its vertices are posed by a bone matrix palette every frame;
|
|
83
|
+
* baking one pose freezes the character mid-stride.
|
|
84
|
+
* - `morph-targets` — same, driven by influences instead of bones.
|
|
85
|
+
* - `multi-material` — the geometry's `groups` select a material per range;
|
|
86
|
+
* a batch carries one material.
|
|
87
|
+
* - `transparent` — blending is order-dependent and three sorts TRANSPARENT
|
|
88
|
+
* OBJECTS, not triangles. Collapsing them fixes their relative order to
|
|
89
|
+
* whatever the merge happened to write.
|
|
90
|
+
* - `opted-out` — see {@link STATIC_BATCH_OPT_OUT_KEY}.
|
|
91
|
+
*/
|
|
92
|
+
export type StaticBatchSkipReason = 'not-a-mesh' | 'instanced' | 'batched' | 'skinned' | 'no-geometry' | 'multi-material' | 'morph-targets' | 'transparent' | 'opted-out';
|
|
93
|
+
/**
|
|
94
|
+
* Why `node` may not join a static batch, or `null` when it may. Pure, cheap,
|
|
95
|
+
* and the SINGLE owner of that answer — see the module note for why the
|
|
96
|
+
* batcher and the advisor cannot each keep their own copy.
|
|
97
|
+
*
|
|
98
|
+
* Checks the node itself only. Subtree exclusion (an opted-out ancestor) is
|
|
99
|
+
* the caller's walk, because the walkers that need this already prune.
|
|
100
|
+
*/
|
|
101
|
+
export declare function staticBatchSkipReason(node: THREE.Object3D): StaticBatchSkipReason | null;
|
|
102
|
+
/**
|
|
103
|
+
* Geometry identity: construction `type` + `parameters` for the parametric
|
|
104
|
+
* primitives, object identity for everything else. See the module note.
|
|
105
|
+
*/
|
|
106
|
+
export declare function geometrySignature(geometry: THREE.BufferGeometry): string;
|
|
107
|
+
/**
|
|
108
|
+
* Material identity as `RenderBatchSystem` has always computed it: type plus
|
|
109
|
+
* the standard-material props that change the draw.
|
|
110
|
+
*
|
|
111
|
+
* FROZEN ON PURPOSE. This is one half of {@link staticBatchSignature}, which
|
|
112
|
+
* is a live batcher's grouping key; widening or narrowing it silently
|
|
113
|
+
* regroups that batcher's scenes. New discrimination goes in
|
|
114
|
+
* {@link materialMergeSignature}, which is free to be stricter because
|
|
115
|
+
* stricter only ever means "batches less".
|
|
116
|
+
*/
|
|
117
|
+
export declare function materialSignature(material: THREE.Material): string;
|
|
118
|
+
/**
|
|
119
|
+
* One mesh's batch key: geometry, material, and the shadow flags.
|
|
120
|
+
*
|
|
121
|
+
* The shadow flags are part of the key because a batched product carries ONE
|
|
122
|
+
* `castShadow`/`receiveShadow` pair for every member it swallowed — mixing a
|
|
123
|
+
* caster and a non-caster into one draw changes the picture.
|
|
124
|
+
*/
|
|
125
|
+
export declare function staticBatchSignature(mesh: THREE.Mesh): string;
|
|
126
|
+
/**
|
|
127
|
+
* Which vertex attributes a geometry carries, and whether it is indexed —
|
|
128
|
+
* the compatibility precondition for `mergeGeometries`, which refuses a batch
|
|
129
|
+
* whose members disagree.
|
|
130
|
+
*
|
|
131
|
+
* Item size is in the key too: two geometries can both have `uv` and disagree
|
|
132
|
+
* about whether it is 2- or 3-component, which merges into silent garbage
|
|
133
|
+
* rather than a refusal.
|
|
134
|
+
*/
|
|
135
|
+
export declare function geometryLayoutSignature(geometry: THREE.BufferGeometry): string;
|
|
136
|
+
/**
|
|
137
|
+
* Material identity for a path that will SHARE one material instance between
|
|
138
|
+
* every member it collapses — stricter than {@link materialSignature}.
|
|
139
|
+
*
|
|
140
|
+
* Everything the looser key reads, plus the props that decide the picture
|
|
141
|
+
* without touching colour/roughness/metalness: opacity and the depth/blend
|
|
142
|
+
* state, emissive, the remaining standard maps, and the flags that change the
|
|
143
|
+
* compiled program. A shader material answers with its uuid instead (see the
|
|
144
|
+
* module note): its appearance is source and uniforms, and there is no honest
|
|
145
|
+
* fixed-prop reading of it.
|
|
146
|
+
*/
|
|
147
|
+
export declare function materialMergeSignature(material: THREE.Material): string;
|
|
148
|
+
//# sourceMappingURL=structural-signature.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"structural-signature.d.ts","sourceRoot":"","sources":["../../src/render/structural-signature.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AAEH,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AAEpC;;;;;;;;;;GAUG;AACH,eAAO,MAAM,wBAAwB,gBAAgB,CAAC;AAEtD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,qBAAqB,GAC7B,YAAY,GACZ,WAAW,GACX,SAAS,GACT,SAAS,GACT,aAAa,GACb,gBAAgB,GAChB,eAAe,GACf,aAAa,GACb,WAAW,CAAC;AAWhB;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,KAAK,CAAC,QAAQ,GAAG,qBAAqB,GAAG,IAAI,CAYxF;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,KAAK,CAAC,cAAc,GAAG,MAAM,CAGxE;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,KAAK,CAAC,QAAQ,GAAG,MAAM,CAKlE;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,GAAG,MAAM,CAI7D;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,KAAK,CAAC,cAAc,GAAG,MAAM,CAM9E;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,KAAK,CAAC,QAAQ,GAAG,MAAM,CA2BvE"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STRUCTURAL IDENTITY AND ELIGIBILITY for static render batching — the one
|
|
3
|
+
* place that answers "may this draw be collapsed at all?" and "are these two
|
|
4
|
+
* meshes the same thing drawn twice?".
|
|
5
|
+
*
|
|
6
|
+
* Three consumers, which is the whole reason it is a module rather than three
|
|
7
|
+
* private helpers: `render/render-batch-system.ts` (the engine's own
|
|
8
|
+
* lab-driven instancer), `dev/render-census.ts`'s structural scan (what the
|
|
9
|
+
* advisor and `render.families` measure), and the `static-batch` capability's
|
|
10
|
+
* `<Frozen>` (what a game actually installs). A batcher and the advisor that
|
|
11
|
+
* routes people to it MUST agree about what is batchable, or the advisor
|
|
12
|
+
* promises a win the wrapper then declines to take.
|
|
13
|
+
*
|
|
14
|
+
* ── THE UUID TRAP, WHICH IS WHY THIS MODULE EXISTS ──────────────────────────
|
|
15
|
+
* The obvious key for "can these two meshes be drawn as one" is object
|
|
16
|
+
* identity: same `geometry.uuid`, same `material.uuid`. It is also the key
|
|
17
|
+
* that silently batches NOTHING in the shape that matters most. A freshly
|
|
18
|
+
* scaffolded TSX/R3F world writes its materials INLINE —
|
|
19
|
+
*
|
|
20
|
+
* {crates.map((c) => (
|
|
21
|
+
* <mesh key={c.id} position={c.at}>
|
|
22
|
+
* <boxGeometry args={[1, 1, 1]} />
|
|
23
|
+
* <meshStandardMaterial color="#8a6a44" />
|
|
24
|
+
* </mesh>
|
|
25
|
+
* ))}
|
|
26
|
+
*
|
|
27
|
+
* — and every one of those `<meshStandardMaterial>` elements constructs its
|
|
28
|
+
* OWN `THREE.MeshStandardMaterial`. Five hundred crates are five hundred
|
|
29
|
+
* distinct uuids describing one identical appearance. A uuid-keyed grouper
|
|
30
|
+
* reports five hundred families of one, finds nothing to do, and is indistin-
|
|
31
|
+
* guishable from a correct batcher over an unbatchable scene. The engine's own
|
|
32
|
+
* scene construction has the same property, which is why
|
|
33
|
+
* `render/render-batch-system.ts` has keyed on VALUE since it was written.
|
|
34
|
+
*
|
|
35
|
+
* So the key here is the STRUCTURE: geometry `type` + its construction
|
|
36
|
+
* `parameters`, material `type` + the props that decide what the draw looks
|
|
37
|
+
* like. Two independently constructed `BoxGeometry(1,1,1)` +
|
|
38
|
+
* `MeshStandardMaterial({color:'#8a6a44'})` pairs are the same draw, and this
|
|
39
|
+
* module says so.
|
|
40
|
+
*
|
|
41
|
+
* ── WHERE VALUE CANNOT ANSWER, IDENTITY IS THE SAFE FALLBACK ────────────────
|
|
42
|
+
* Twice below, a structural comparison would be a guess, and the answer is
|
|
43
|
+
* `uuid:` — a key nothing else can equal, so the members simply do not group.
|
|
44
|
+
* Not grouping costs a draw call; grouping two things that only LOOK alike
|
|
45
|
+
* renders the wrong picture.
|
|
46
|
+
* - geometry with no `.parameters` (a loaded GLTF mesh, a hand-built
|
|
47
|
+
* `BufferGeometry`): its vertices are its identity and comparing them is
|
|
48
|
+
* not a signature, it is a diff.
|
|
49
|
+
* - a shader material ({@link materialMergeSignature} only): its appearance
|
|
50
|
+
* lives in shader source and uniforms that no fixed prop list can read.
|
|
51
|
+
*
|
|
52
|
+
* ── TWO KEYS, DELIBERATELY ──────────────────────────────────────────────────
|
|
53
|
+
* {@link staticBatchSignature} is `RenderBatchSystem`'s key, extracted here
|
|
54
|
+
* unchanged so there is one owner of the answer rather than two that drift.
|
|
55
|
+
* {@link materialMergeSignature} is STRICTER, and it is what the `static-batch`
|
|
56
|
+
* capability's `<Frozen>` groups by: that path hands ONE material instance to
|
|
57
|
+
* a merged/instanced product, so its members must be interchangeable, not
|
|
58
|
+
* merely similar. The looser key predates it and is kept exactly as it was —
|
|
59
|
+
* see that constant's own note.
|
|
60
|
+
*/
|
|
61
|
+
/**
|
|
62
|
+
* The `userData` key a node opts OUT of static batching with — the one
|
|
63
|
+
* declaration that beats every measurement:
|
|
64
|
+
*
|
|
65
|
+
* <group name="Beacons" userData={{ staticBatch: false }}>
|
|
66
|
+
*
|
|
67
|
+
* Set on a node, it covers that node's whole subtree (a walker stops there).
|
|
68
|
+
* It is deliberately `userData` and not a component prop: the thing being
|
|
69
|
+
* excluded is a three node, and every authoring lane — TSX, a loaded GLTF, a
|
|
70
|
+
* hand-built graph — can set `userData` on one.
|
|
71
|
+
*/
|
|
72
|
+
export const STATIC_BATCH_OPT_OUT_KEY = 'staticBatch';
|
|
73
|
+
/**
|
|
74
|
+
* Why `node` may not join a static batch, or `null` when it may. Pure, cheap,
|
|
75
|
+
* and the SINGLE owner of that answer — see the module note for why the
|
|
76
|
+
* batcher and the advisor cannot each keep their own copy.
|
|
77
|
+
*
|
|
78
|
+
* Checks the node itself only. Subtree exclusion (an opted-out ancestor) is
|
|
79
|
+
* the caller's walk, because the walkers that need this already prune.
|
|
80
|
+
*/
|
|
81
|
+
export function staticBatchSkipReason(node) {
|
|
82
|
+
if (node.userData?.[STATIC_BATCH_OPT_OUT_KEY] === false)
|
|
83
|
+
return 'opted-out';
|
|
84
|
+
const mesh = node;
|
|
85
|
+
if (mesh.isInstancedMesh)
|
|
86
|
+
return 'instanced';
|
|
87
|
+
if (mesh.isBatchedMesh)
|
|
88
|
+
return 'batched';
|
|
89
|
+
if (mesh.isSkinnedMesh)
|
|
90
|
+
return 'skinned';
|
|
91
|
+
if (!mesh.isMesh)
|
|
92
|
+
return 'not-a-mesh';
|
|
93
|
+
if (!mesh.geometry || !mesh.material)
|
|
94
|
+
return 'no-geometry';
|
|
95
|
+
if (Array.isArray(mesh.material))
|
|
96
|
+
return 'multi-material';
|
|
97
|
+
if (Object.keys(mesh.geometry.morphAttributes).length > 0)
|
|
98
|
+
return 'morph-targets';
|
|
99
|
+
if (mesh.material.transparent)
|
|
100
|
+
return 'transparent';
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Geometry identity: construction `type` + `parameters` for the parametric
|
|
105
|
+
* primitives, object identity for everything else. See the module note.
|
|
106
|
+
*/
|
|
107
|
+
export function geometrySignature(geometry) {
|
|
108
|
+
const params = geometry.parameters;
|
|
109
|
+
return params ? `${geometry.type}:${JSON.stringify(params)}` : `uuid:${geometry.uuid}`;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Material identity as `RenderBatchSystem` has always computed it: type plus
|
|
113
|
+
* the standard-material props that change the draw.
|
|
114
|
+
*
|
|
115
|
+
* FROZEN ON PURPOSE. This is one half of {@link staticBatchSignature}, which
|
|
116
|
+
* is a live batcher's grouping key; widening or narrowing it silently
|
|
117
|
+
* regroups that batcher's scenes. New discrimination goes in
|
|
118
|
+
* {@link materialMergeSignature}, which is free to be stricter because
|
|
119
|
+
* stricter only ever means "batches less".
|
|
120
|
+
*/
|
|
121
|
+
export function materialSignature(material) {
|
|
122
|
+
const standard = material;
|
|
123
|
+
const color = standard.color?.getHexString?.() ?? '';
|
|
124
|
+
const map = standard.map?.uuid ?? '';
|
|
125
|
+
return `${material.type}:${color}:${standard.roughness ?? ''}:${standard.metalness ?? ''}:${map}:${material.side}:${material.transparent}:${material.vertexColors}`;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* One mesh's batch key: geometry, material, and the shadow flags.
|
|
129
|
+
*
|
|
130
|
+
* The shadow flags are part of the key because a batched product carries ONE
|
|
131
|
+
* `castShadow`/`receiveShadow` pair for every member it swallowed — mixing a
|
|
132
|
+
* caster and a non-caster into one draw changes the picture.
|
|
133
|
+
*/
|
|
134
|
+
export function staticBatchSignature(mesh) {
|
|
135
|
+
const geometry = mesh.geometry;
|
|
136
|
+
const material = mesh.material;
|
|
137
|
+
return `${geometrySignature(geometry)}|${materialSignature(material)}|${mesh.castShadow ? 1 : 0}${mesh.receiveShadow ? 1 : 0}`;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Which vertex attributes a geometry carries, and whether it is indexed —
|
|
141
|
+
* the compatibility precondition for `mergeGeometries`, which refuses a batch
|
|
142
|
+
* whose members disagree.
|
|
143
|
+
*
|
|
144
|
+
* Item size is in the key too: two geometries can both have `uv` and disagree
|
|
145
|
+
* about whether it is 2- or 3-component, which merges into silent garbage
|
|
146
|
+
* rather than a refusal.
|
|
147
|
+
*/
|
|
148
|
+
export function geometryLayoutSignature(geometry) {
|
|
149
|
+
const attributes = Object.keys(geometry.attributes)
|
|
150
|
+
.sort()
|
|
151
|
+
.map((name) => `${name}:${geometry.attributes[name]?.itemSize ?? '?'}`)
|
|
152
|
+
.join(',');
|
|
153
|
+
return `${attributes}${geometry.getIndex() ? '|i' : ''}`;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Material identity for a path that will SHARE one material instance between
|
|
157
|
+
* every member it collapses — stricter than {@link materialSignature}.
|
|
158
|
+
*
|
|
159
|
+
* Everything the looser key reads, plus the props that decide the picture
|
|
160
|
+
* without touching colour/roughness/metalness: opacity and the depth/blend
|
|
161
|
+
* state, emissive, the remaining standard maps, and the flags that change the
|
|
162
|
+
* compiled program. A shader material answers with its uuid instead (see the
|
|
163
|
+
* module note): its appearance is source and uniforms, and there is no honest
|
|
164
|
+
* fixed-prop reading of it.
|
|
165
|
+
*/
|
|
166
|
+
export function materialMergeSignature(material) {
|
|
167
|
+
const shader = material;
|
|
168
|
+
if (shader.isShaderMaterial || shader.isRawShaderMaterial)
|
|
169
|
+
return `uuid:${material.uuid}`;
|
|
170
|
+
const rich = material;
|
|
171
|
+
const maps = [rich.normalMap, rich.aoMap, rich.emissiveMap, rich.roughnessMap, rich.metalnessMap]
|
|
172
|
+
.map((map) => map?.uuid ?? '')
|
|
173
|
+
.join(',');
|
|
174
|
+
// A material with a patched `onBeforeCompile` declares its variant through
|
|
175
|
+
// this hook (that is what three itself keys its program cache on), so a
|
|
176
|
+
// non-empty value discriminates here too.
|
|
177
|
+
const program = material.customProgramCacheKey?.() ?? '';
|
|
178
|
+
return [
|
|
179
|
+
materialSignature(material),
|
|
180
|
+
material.opacity,
|
|
181
|
+
material.depthWrite,
|
|
182
|
+
material.depthTest,
|
|
183
|
+
material.alphaTest,
|
|
184
|
+
material.blending,
|
|
185
|
+
material.toneMapped,
|
|
186
|
+
material.visible,
|
|
187
|
+
rich.emissive?.getHexString?.() ?? '',
|
|
188
|
+
rich.flatShading ?? '',
|
|
189
|
+
rich.wireframe ?? '',
|
|
190
|
+
maps,
|
|
191
|
+
program,
|
|
192
|
+
].join('|');
|
|
193
|
+
}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* SAME registry via {@link DebugRegistry.forRoot}, so a name a game registers is
|
|
5
5
|
* visible (and name-collision-checked) across every world, not just the one that
|
|
6
6
|
* registered it. `createGame` (`runtime/game.ts`) constructs the registry
|
|
7
|
-
* alongside the state bridge and files it in
|
|
8
|
-
*
|
|
7
|
+
* alongside the state bridge and files it in a non-enumerable game-scoped slot;
|
|
8
|
+
* later consumers (the react `useDebugProvider`/
|
|
9
9
|
* `useDebugCommand` hooks, editor panels) reach it via {@link getDebugRegistry}
|
|
10
10
|
* rather than threading it through every call site.
|
|
11
11
|
*
|