@volter/blender-engine 0.1.94 → 0.1.95
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/browser/three/blender-graph-material.ts +70 -0
- package/browser/three/blender-material-palettes.ts +65 -0
- package/browser/three/blender-node-graph.ts +11 -2
- package/browser/three/blender-ordered-batches.ts +95 -6
- package/browser/three/blender-physical-material.ts +73 -1
- package/browser/three/blender-runtime-view.ts +2 -2
- package/browser/three/blender-transparent-instances.ts +38 -12
- package/package.json +1 -1
|
@@ -24,6 +24,7 @@ interface Binding {
|
|
|
24
24
|
ramps: THREE.DataTexture[];
|
|
25
25
|
/** Named UV layer to the channel the current draw's geometry holds it in. */
|
|
26
26
|
channels: Record<string, number>;
|
|
27
|
+
palette?: {indices: Int32Array; sources: readonly THREE.MeshPhysicalMaterial[]};
|
|
27
28
|
}
|
|
28
29
|
const bindings = new WeakMap<THREE.MeshPhysicalMaterial, Binding>();
|
|
29
30
|
/** Graph structures whose program the GPU refused. A material whose graph is
|
|
@@ -35,6 +36,59 @@ export function materialGraph(material: THREE.MeshPhysicalMaterial): CompiledGra
|
|
|
35
36
|
return bindings.get(material)?.compiled ?? null;
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
/** Only identical graph programs can share an ordered draw. The originals
|
|
40
|
+
* keep ownership of textures and ramp tables; their live uniform objects are
|
|
41
|
+
* borrowed, so value edits do not turn into baked shader constants. */
|
|
42
|
+
function compatiblePaletteGraphs(sources: readonly THREE.MeshPhysicalMaterial[]): CompiledGraph | null {
|
|
43
|
+
if (sources.length !== 2 || sources.some(source => pendings.has(source))) return null;
|
|
44
|
+
const graphs = sources.map(source => bindings.get(source)?.compiled);
|
|
45
|
+
const graph = graphs[0];
|
|
46
|
+
if (!graph || failed.has(graph.key) || graphs.some(other => !other || other.key !== graph.key ||
|
|
47
|
+
other.declarations !== graph.declarations || other.library !== graph.library ||
|
|
48
|
+
JSON.stringify([other.uvs, other.attributes, other.outputs]) !== JSON.stringify([graph.uvs, graph.attributes, graph.outputs]))) return null;
|
|
49
|
+
// Leave room for the physical material's environment and lighting samplers.
|
|
50
|
+
if (sources.reduce((n, source) => n + graph.images.reduce((n, image) => n + (image.tiled ? 2 : 1), 0) +
|
|
51
|
+
graph.ramps.length + [source.map, source.normalMap, source.roughnessMap].filter(Boolean).length, 0) > 8) return null;
|
|
52
|
+
return graph;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function paletteGraphReady(source: THREE.MeshPhysicalMaterial): boolean {
|
|
56
|
+
const graph = bindings.get(source)?.compiled;
|
|
57
|
+
return !!graph && !pendings.has(source) && !failed.has(graph.key);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function paletteGraphs(sources: readonly THREE.MeshPhysicalMaterial[]): CompiledGraph | null {
|
|
61
|
+
const graph = compatiblePaletteGraphs(sources);
|
|
62
|
+
if (!graph) return null;
|
|
63
|
+
const rename = (text: string, index: number) => text.replace(/\b(bg\w+|blenderGraph)\b/g, name => `bp${index}_${name}`);
|
|
64
|
+
const outputs = Object.values(graph.outputs).map(output => `${output.type} ${output.global};`);
|
|
65
|
+
if (graph.closure) outputs.push('float bgClosureP;', 'vec3 bgClosureE;', 'float bgClosureT;');
|
|
66
|
+
const names = [...Object.values(graph.outputs).map(output => output.global), ...(graph.closure ? ['bgClosureP', 'bgClosureE', 'bgClosureT'] : [])];
|
|
67
|
+
const program = [rename(graph.program, 0), rename(graph.program, 1), ...outputs,
|
|
68
|
+
// Evaluate both graphs before selecting. Derivatives and implicit texture
|
|
69
|
+
// LOD must never occur inside a potentially divergent fragment branch.
|
|
70
|
+
`void blenderGraph() { bp0_blenderGraph(); bp1_blenderGraph(); ${names.map(name =>
|
|
71
|
+
`${name} = vBlenderPaletteIndex == 0 ? bp0_${name} : bp1_${name};`).join(' ')} }`].join('\n');
|
|
72
|
+
return {...graph, key: `blender-palette-v1:${graph.key}`, program,
|
|
73
|
+
declarations: `${graph.library}\n${program}`, images: [], ramps: [], uniforms: new Map()};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function bindPaletteGraph(material: THREE.MeshPhysicalMaterial, compiled: CompiledGraph,
|
|
77
|
+
sources: readonly THREE.MeshPhysicalMaterial[], indices: Int32Array, geometry: THREE.BufferGeometry): boolean {
|
|
78
|
+
if (!compatiblePaletteGraphs(sources)) return false;
|
|
79
|
+
const values: Record<string, THREE.IUniform> = {blenderPaletteIndices: {value: indices}};
|
|
80
|
+
for (let index = 0; index < sources.length; index++) {
|
|
81
|
+
const source = sources[index]!;
|
|
82
|
+
for (const [name, uniform] of Object.entries(bindings.get(source)!.uniforms)) values[`bp${index}_${name}`] = uniform;
|
|
83
|
+
for (const name of ['map', 'normalMap', 'roughnessMap'] as const) {
|
|
84
|
+
if (source[name]) values[`bp${index}_${name}`] = {value: source[name]};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
bindings.set(material, {compiled, uniforms: values, ramps: [], channels: channelsFor(compiled, geometry), palette: {indices, sources}});
|
|
88
|
+
material.needsUpdate = true;
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
38
92
|
type ImageTexture = THREE.Texture | {tiles: THREE.Texture; map: THREE.Texture} | null;
|
|
39
93
|
|
|
40
94
|
/**
|
|
@@ -395,6 +449,22 @@ ${compiled.attributes.map(n => `${attributeVarying(n)} = ${graphAttributeName(n)
|
|
|
395
449
|
${compiled.uvs.map(n => `${uvVarying(n)} = ${attribute(channels[n] ?? 0)};`).join('\n')}
|
|
396
450
|
#include <project_vertex>`,
|
|
397
451
|
);
|
|
452
|
+
if (binding.palette) {
|
|
453
|
+
shader.vertexShader = `uniform int blenderPaletteIndices[64];\nflat varying int vBlenderPaletteIndex;\n${shader.vertexShader}`
|
|
454
|
+
.replace('#include <project_vertex>', 'vBlenderPaletteIndex = blenderPaletteIndices[int(getIndirectIndex(gl_DrawID))];\n#include <project_vertex>');
|
|
455
|
+
shader.fragmentShader = `flat varying int vBlenderPaletteIndex;\n${shader.fragmentShader}`;
|
|
456
|
+
for (const [name, uv, clip] of [['map', 'vMapUv', 'blenderMapClip'], ['normalMap', 'vNormalMapUv', 'blenderNormalClip'],
|
|
457
|
+
['roughnessMap', 'vRoughnessMapUv', 'blenderRoughnessClip']] as const) {
|
|
458
|
+
if (!material[name]) continue;
|
|
459
|
+
shader.fragmentShader = `uniform sampler2D bp0_${name}, bp1_${name};\n${shader.fragmentShader}`
|
|
460
|
+
.replaceAll(`blenderImageSample(${name}, ${uv}, ${clip})`, `blenderPalette_${name}(${uv}, ${clip})`)
|
|
461
|
+
.replace('void main() {', `vec4 blenderPalette_${name}(vec2 uv, bool clipImage) {
|
|
462
|
+
vec4 first = blenderImageSample(bp0_${name}, uv, clipImage);
|
|
463
|
+
vec4 second = blenderImageSample(bp1_${name}, uv, clipImage);
|
|
464
|
+
return vBlenderPaletteIndex == 0 ? first : second;
|
|
465
|
+
}\nvoid main() {`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
398
468
|
const out = (name: string) => compiled.outputs[name]?.global;
|
|
399
469
|
// [chunk, line, whether the line goes BEFORE the chunk]
|
|
400
470
|
const fragment: [string, string, boolean][] = [];
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import {materialGraph, paletteGraphReady, paletteGraphs} from './blender-graph-material';
|
|
3
|
+
import {physicalPaletteState, samePhysicalPaletteState} from './blender-physical-material';
|
|
4
|
+
import type {OrderedPalette} from './blender-ordered-batches';
|
|
5
|
+
|
|
6
|
+
type Snapshot = {source: THREE.MeshPhysicalMaterial; state: NonNullable<ReturnType<typeof physicalPaletteState>>;
|
|
7
|
+
compatible: NonNullable<ReturnType<typeof physicalPaletteState>>};
|
|
8
|
+
|
|
9
|
+
/** Pair only complete, equal shader/render states. Each frame observes shared
|
|
10
|
+
* sources once; member meshes keep their authored materials. Any change drops
|
|
11
|
+
* the disposable packed families before their old identities are released. */
|
|
12
|
+
export class BlenderMaterialPalettes {
|
|
13
|
+
private snapshots: Snapshot[] = [];
|
|
14
|
+
private readonly palettes = new Map<THREE.Material, OrderedPalette>();
|
|
15
|
+
private readonly owned: OrderedPalette[] = [];
|
|
16
|
+
|
|
17
|
+
prepare(materials: Iterable<THREE.Material>, releaseDraws: () => void): boolean {
|
|
18
|
+
const snapshots: Snapshot[] = [];
|
|
19
|
+
for (const source of new Set(materials)) {
|
|
20
|
+
if (!(source instanceof THREE.MeshPhysicalMaterial) || !source.visible || !source.transparent ||
|
|
21
|
+
source.transmission > 0 || (source.side === THREE.DoubleSide && !source.forceSinglePass) ||
|
|
22
|
+
!paletteGraphReady(source)) continue;
|
|
23
|
+
const state = physicalPaletteState(source, true);
|
|
24
|
+
const compatible = physicalPaletteState(source);
|
|
25
|
+
const graph = materialGraph(source);
|
|
26
|
+
if (!state || !compatible || !graph) continue;
|
|
27
|
+
state.push(graph.key, graph.declarations);
|
|
28
|
+
snapshots.push({source, state, compatible});
|
|
29
|
+
}
|
|
30
|
+
snapshots.sort((a, b) => a.source.uuid.localeCompare(b.source.uuid));
|
|
31
|
+
if (snapshots.length === this.snapshots.length && snapshots.every((snapshot, index) =>
|
|
32
|
+
snapshot.source === this.snapshots[index]!.source && samePhysicalPaletteState(snapshot.state, this.snapshots[index]!.state))) return false;
|
|
33
|
+
releaseDraws();
|
|
34
|
+
this.clear();
|
|
35
|
+
this.snapshots = snapshots;
|
|
36
|
+
const paired = new Set<THREE.Material>();
|
|
37
|
+
for (let first = 0; first < snapshots.length && this.owned.length < 16; first++) {
|
|
38
|
+
const a = snapshots[first]!;
|
|
39
|
+
if (paired.has(a.source)) continue;
|
|
40
|
+
for (let second = first + 1; second < snapshots.length; second++) {
|
|
41
|
+
const b = snapshots[second]!;
|
|
42
|
+
if (paired.has(b.source) || !samePhysicalPaletteState(a.compatible, b.compatible)) continue;
|
|
43
|
+
const sources = [a.source, b.source];
|
|
44
|
+
const compiled = paletteGraphs(sources);
|
|
45
|
+
if (!compiled) continue;
|
|
46
|
+
const palette: OrderedPalette = {identity: new THREE.Material(), sources, compiled};
|
|
47
|
+
this.owned.push(palette);
|
|
48
|
+
for (const source of sources) { paired.add(source); this.palettes.set(source, palette); }
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get(source: THREE.Material): OrderedPalette | undefined { return this.palettes.get(source); }
|
|
56
|
+
|
|
57
|
+
clear(): void {
|
|
58
|
+
for (const palette of this.owned) palette.identity.dispose();
|
|
59
|
+
this.owned.length = 0;
|
|
60
|
+
this.palettes.clear();
|
|
61
|
+
this.snapshots = [];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
inspect() { return {pairs: this.owned.length, limit: 16}; }
|
|
65
|
+
}
|
|
@@ -129,6 +129,11 @@ export interface CompiledGraph {
|
|
|
129
129
|
/** Declarations: the prelude, the library files in dependency order, the
|
|
130
130
|
* uniforms, the graph function. Goes before `main`. */
|
|
131
131
|
readonly declarations: string;
|
|
132
|
+
/** Shared library and graph-local declarations, kept separately so an
|
|
133
|
+
* ordered material palette can namespace graph inputs without copying the
|
|
134
|
+
* coordinate/library globals. Their join is exactly `declarations`. */
|
|
135
|
+
readonly library: string;
|
|
136
|
+
readonly program: string;
|
|
132
137
|
/** Uniform name to value; refreshed on every frame without recompiling. */
|
|
133
138
|
readonly uniforms: ReadonlyMap<string, number | readonly number[]>;
|
|
134
139
|
readonly images: readonly GraphImage[];
|
|
@@ -935,10 +940,12 @@ class Compiler {
|
|
|
935
940
|
};
|
|
936
941
|
for (const file of this.s.files) visit(file);
|
|
937
942
|
const graphBody = `void blenderGraph() {\n blender_init_globals();\n ${[...this.lines, ...assignments].join('\n ')}\n}`;
|
|
938
|
-
const
|
|
943
|
+
const library = [
|
|
939
944
|
BLENDER_NODE_GLSL_PRELUDE,
|
|
940
945
|
shakeLibrary(EEVEE_GLOBALS + ordered.map(f => BLENDER_NODE_GLSL[f]!.code).join('\n') + '\n' + COORDINATES,
|
|
941
946
|
[...this.s.functions, graphBody].join('\n')),
|
|
947
|
+
].join('\n');
|
|
948
|
+
const program = [
|
|
942
949
|
...[...this.s.uniformTypes].map(([n, t]) => `uniform ${t} ${n};`),
|
|
943
950
|
...this.images.map(i => i.tiled
|
|
944
951
|
? `uniform highp sampler2DArray ${i.uniform};\nuniform highp sampler2D ${i.uniform}Map;`
|
|
@@ -951,7 +958,9 @@ class Compiler {
|
|
|
951
958
|
].join('\n');
|
|
952
959
|
return {
|
|
953
960
|
key: JSON.stringify([this.graph.surface, this.s.structure, [...this.s.uvs].sort()]),
|
|
954
|
-
declarations
|
|
961
|
+
declarations: `${library}\n${program}`,
|
|
962
|
+
library,
|
|
963
|
+
program,
|
|
955
964
|
uniforms: this.uniforms,
|
|
956
965
|
images: this.images,
|
|
957
966
|
ramps: this.ramps,
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
|
+
import type {CompiledGraph} from './blender-node-graph';
|
|
3
|
+
import {physicalPaletteMaterial} from './blender-physical-material';
|
|
4
|
+
import {presenterChanged} from './blender-presenter-change';
|
|
2
5
|
|
|
3
6
|
export const ORDERED_BATCH_CAPACITY = 64;
|
|
4
7
|
const FAMILY_BYTES = 4 * 1024 * 1024;
|
|
5
8
|
const CACHE_BYTES = 32 * 1024 * 1024;
|
|
6
9
|
const MAX_DRAWS = 1024;
|
|
7
10
|
type Shape = {geometry: THREE.BufferGeometry; stamp: string; bytes: number};
|
|
11
|
+
export type OrderedPalette = {identity: THREE.Material; sources: readonly THREE.MeshPhysicalMaterial[]; compiled: CompiledGraph};
|
|
12
|
+
type Compilation = {material: THREE.MeshPhysicalMaterial; ready: boolean; refused: boolean; pending: boolean; program: unknown};
|
|
8
13
|
type Family = {material: THREE.Material; shapes: Map<number, Shape>; template: THREE.BatchedMesh | null;
|
|
9
|
-
ids: Map<number, number>; draws: THREE.BatchedMesh[]; dirty: boolean; bytes: number; reservedBytes: number; refused: boolean
|
|
14
|
+
ids: Map<number, number>; draws: THREE.BatchedMesh[]; dirty: boolean; bytes: number; reservedBytes: number; refused: boolean;
|
|
15
|
+
palette: OrderedPalette | undefined; compilations: Map<THREE.WebGLRenderer, Compilation>;
|
|
16
|
+
drawMaterials: Map<THREE.BatchedMesh, {material: THREE.MeshPhysicalMaterial; indices: Int32Array}>};
|
|
10
17
|
|
|
11
18
|
/** Immutable packed attributes are shared between ordered draw runs. Only the
|
|
12
19
|
* matrix/indirection textures and command lists belong to an individual run.
|
|
@@ -18,11 +25,14 @@ export class BlenderOrderedBatches {
|
|
|
18
25
|
private bytes = 0;
|
|
19
26
|
private drawCount = 0;
|
|
20
27
|
private created = 0;
|
|
28
|
+
private compiling = 0;
|
|
21
29
|
pending = false;
|
|
30
|
+
generation = 0;
|
|
22
31
|
|
|
23
32
|
begin(): void { this.created = 0; this.pending = false; this.registrations.clear(); }
|
|
33
|
+
beginPaletteFrame(): void { this.compiling = 0; }
|
|
24
34
|
|
|
25
|
-
register(geometry: THREE.BufferGeometry, material: THREE.Material, order: number): string | null {
|
|
35
|
+
register(geometry: THREE.BufferGeometry, material: THREE.Material, order: number, palette?: OrderedPalette): string | null {
|
|
26
36
|
const registration = `${geometry.id}:${material.uuid}:${order}`;
|
|
27
37
|
if (this.registrations.has(registration)) return this.registrations.get(registration)!;
|
|
28
38
|
this.registrations.set(registration, null);
|
|
@@ -56,7 +66,7 @@ export class BlenderOrderedBatches {
|
|
|
56
66
|
let family = this.families.get(key);
|
|
57
67
|
if (!family) {
|
|
58
68
|
family = {material, shapes: new Map(), template: null, ids: new Map(), draws: [], dirty: true,
|
|
59
|
-
bytes: 0, reservedBytes: 0, refused: false};
|
|
69
|
+
bytes: 0, reservedBytes: 0, refused: false, palette, compilations: new Map(), drawMaterials: new Map()};
|
|
60
70
|
this.families.set(key, family);
|
|
61
71
|
}
|
|
62
72
|
const stamp = JSON.stringify(versions);
|
|
@@ -105,12 +115,67 @@ export class BlenderOrderedBatches {
|
|
|
105
115
|
}
|
|
106
116
|
}
|
|
107
117
|
|
|
108
|
-
|
|
118
|
+
/** Compile before replacing a single canonical draw, with this renderer's
|
|
119
|
+
* context and actual scene lighting. Each frame starts at most two programs.
|
|
120
|
+
* The proxy borrows packed geometry and owns only its matrix textures. */
|
|
121
|
+
compilePalettes(renderer: THREE.WebGLRenderer, camera: THREE.Camera, scene: THREE.Scene): void {
|
|
122
|
+
if (renderer.getContext().isContextLost()) return;
|
|
123
|
+
for (const family of this.families.values()) {
|
|
124
|
+
const {template, palette} = family;
|
|
125
|
+
if (!template || !palette) continue;
|
|
126
|
+
let compilation = family.compilations.get(renderer);
|
|
127
|
+
if (compilation?.ready && (renderer.properties.get(compilation.material) as {currentProgram?: unknown}).currentProgram !== compilation.program) {
|
|
128
|
+
compilation.ready = false; // Context restoration clears renderer properties.
|
|
129
|
+
this.generation++;
|
|
130
|
+
}
|
|
131
|
+
if (compilation?.ready || compilation?.pending || compilation?.refused) continue;
|
|
132
|
+
if (this.compiling >= 2) { this.pending = true; continue; }
|
|
133
|
+
if (!compilation) {
|
|
134
|
+
const material = physicalPaletteMaterial(palette.compiled, palette.sources, new Int32Array(ORDERED_BATCH_CAPACITY), template.geometry);
|
|
135
|
+
if (!material) continue;
|
|
136
|
+
compilation = {material, ready: false, refused: false, pending: false, program: null};
|
|
137
|
+
family.compilations.set(renderer, compilation);
|
|
138
|
+
}
|
|
139
|
+
const held = compilation;
|
|
140
|
+
held.pending = true;
|
|
141
|
+
held.material.needsUpdate = true;
|
|
142
|
+
const proxy = new THREE.BatchedMesh(1, 0, 0, held.material);
|
|
143
|
+
proxy.geometry.dispose();
|
|
144
|
+
proxy.geometry = template.geometry;
|
|
145
|
+
Object.defineProperty(proxy, 'colorTexture', {value: null});
|
|
146
|
+
this.compiling++;
|
|
147
|
+
const settle = (resolved: boolean) => {
|
|
148
|
+
proxy.geometry = new THREE.BufferGeometry();
|
|
149
|
+
proxy.dispose();
|
|
150
|
+
if (family.template !== template || family.compilations.get(renderer) !== held) {
|
|
151
|
+
held.material.dispose(); return;
|
|
152
|
+
}
|
|
153
|
+
held.pending = false;
|
|
154
|
+
// compileAsync signals completion, including a refused program. Check
|
|
155
|
+
// the actual link result; never let a failed palette hide originals.
|
|
156
|
+
const program = (renderer.properties.get(held.material) as {currentProgram?: {program?: WebGLProgram}}).currentProgram;
|
|
157
|
+
const gl = renderer.getContext();
|
|
158
|
+
held.ready = resolved && !gl.isContextLost() && !!program?.program && gl.getProgramParameter(program.program, gl.LINK_STATUS) === true;
|
|
159
|
+
held.program = program;
|
|
160
|
+
held.refused = !held.ready && !gl.isContextLost();
|
|
161
|
+
this.generation++;
|
|
162
|
+
if (held.refused) console.warn('Blender ordered material palette did not link; canonical draws retained.');
|
|
163
|
+
presenterChanged();
|
|
164
|
+
};
|
|
165
|
+
try { renderer.compileAsync(proxy, camera, scene).then(() => settle(true), () => settle(false)); }
|
|
166
|
+
catch { settle(false); }
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
available(key: string, renderer?: THREE.WebGLRenderer): boolean {
|
|
171
|
+
const family = this.families.get(key);
|
|
172
|
+
return !!family?.template && (!family.palette || (!!renderer && family.compilations.get(renderer)?.ready === true));
|
|
173
|
+
}
|
|
109
174
|
|
|
110
|
-
get(key: string, index: number): THREE.BatchedMesh | null {
|
|
175
|
+
get(key: string, index: number, renderer?: THREE.WebGLRenderer): THREE.BatchedMesh | null {
|
|
111
176
|
const family = this.families.get(key)!;
|
|
112
177
|
const template = family.template;
|
|
113
|
-
if (!template) return null;
|
|
178
|
+
if (!template || !this.available(key, renderer)) return null;
|
|
114
179
|
let draw = family.draws[index];
|
|
115
180
|
if (!draw) {
|
|
116
181
|
if (this.drawCount >= MAX_DRAWS) return null;
|
|
@@ -133,6 +198,14 @@ export class BlenderOrderedBatches {
|
|
|
133
198
|
draw.addInstance(firstId);
|
|
134
199
|
draw.setVisibleAt(i, false);
|
|
135
200
|
}
|
|
201
|
+
if (family.palette) {
|
|
202
|
+
const indices = new Int32Array(ORDERED_BATCH_CAPACITY);
|
|
203
|
+
const material = physicalPaletteMaterial(family.palette.compiled, family.palette.sources, indices, draw.geometry);
|
|
204
|
+
if (!material) { for (const name of Object.keys(draw.geometry.attributes)) draw.geometry.deleteAttribute(name);
|
|
205
|
+
draw.geometry.setIndex(null); draw.dispose(); return null; }
|
|
206
|
+
draw.material = material;
|
|
207
|
+
family.drawMaterials.set(draw, {material, indices});
|
|
208
|
+
}
|
|
136
209
|
family.draws.push(draw);
|
|
137
210
|
this.created++;
|
|
138
211
|
this.drawCount++;
|
|
@@ -142,10 +215,26 @@ export class BlenderOrderedBatches {
|
|
|
142
215
|
|
|
143
216
|
geometryId(key: string, geometry: THREE.BufferGeometry): number { return this.families.get(key)!.ids.get(geometry.id)!; }
|
|
144
217
|
|
|
218
|
+
setMaterialAt(key: string, draw: THREE.BatchedMesh, index: number, source: THREE.Material): void {
|
|
219
|
+
const family = this.families.get(key)!;
|
|
220
|
+
const held = family.drawMaterials.get(draw);
|
|
221
|
+
if (held) held.indices[index] = family.palette!.sources.indexOf(source as THREE.MeshPhysicalMaterial);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
isPalette(key: string): boolean { return !!this.families.get(key)?.palette; }
|
|
225
|
+
|
|
145
226
|
inspect() { return {bytes: this.bytes, draws: this.drawCount, pending: this.pending,
|
|
227
|
+
paletteFamilies: [...this.families.values()].filter(f => !!f.palette).length,
|
|
228
|
+
paletteReady: [...this.families.values()].filter(f => [...f.compilations.values()].some(c => c.ready)).length,
|
|
229
|
+
paletteRefused: [...this.families.values()].filter(f => [...f.compilations.values()].some(c => c.refused)).length,
|
|
146
230
|
refusedFamilies: [...this.families.values()].filter(f => f.refused).length}; }
|
|
147
231
|
|
|
148
232
|
private release(family: Family): void {
|
|
233
|
+
this.generation++;
|
|
234
|
+
for (const compilation of family.compilations.values()) if (!compilation.pending) compilation.material.dispose();
|
|
235
|
+
family.compilations.clear();
|
|
236
|
+
for (const {material} of family.drawMaterials.values()) material.dispose();
|
|
237
|
+
family.drawMaterials.clear();
|
|
149
238
|
for (const draw of family.draws) {
|
|
150
239
|
draw.removeFromParent();
|
|
151
240
|
// BatchedMesh.dispose owns its geometry. Detach shared buffer attributes
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* that MeshPhysicalMaterial does not expose as properties. */
|
|
5
5
|
import * as THREE from 'three';
|
|
6
6
|
import {z} from 'zod';
|
|
7
|
-
import {applyGraphShader, bindGraphDraw, borrowGraphDrawBinding, graphProgramKey, graphUvChannels, setGraphViewport} from './blender-graph-material';
|
|
7
|
+
import {applyGraphShader, bindGraphDraw, bindPaletteGraph, borrowGraphDrawBinding, graphProgramKey, graphUvChannels, setGraphViewport} from './blender-graph-material';
|
|
8
|
+
import type {CompiledGraph} from './blender-node-graph';
|
|
8
9
|
import {bindNamedUvChannels} from './blender-texture-samplers';
|
|
9
10
|
|
|
10
11
|
const scalar = z.number().finite();
|
|
@@ -66,6 +67,77 @@ function graphShadow(material: THREE.MeshPhysicalMaterial): THREE.MeshPhysicalMa
|
|
|
66
67
|
return shadow;
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
type PaletteState = (string | number | boolean | null)[];
|
|
71
|
+
const identities = new WeakMap<object, number>();
|
|
72
|
+
let nextIdentity = 0;
|
|
73
|
+
const ignoredMaterial = new Set(['id', 'uuid', 'name', 'userData', 'version', '_listeners',
|
|
74
|
+
'onBeforeRender', 'onBeforeCompile', 'customProgramCacheKey']);
|
|
75
|
+
const ignoredTexture = new Set(['id', 'uuid', 'name', 'source', 'mipmaps', 'version', '_listeners', 'userData']);
|
|
76
|
+
const paletteMaps = new Set(['map', 'normalMap', 'roughnessMap']);
|
|
77
|
+
const identity = (object: object): number => {
|
|
78
|
+
let id = identities.get(object);
|
|
79
|
+
if (id === undefined) { id = ++nextIdentity; identities.set(object, id); }
|
|
80
|
+
return id;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** An exact, small snapshot of shader/render state, including the engine's
|
|
84
|
+
* private physical uniforms. No toJSON/image readback, lossy hash, or version
|
|
85
|
+
* assumption: direct property edits must invalidate a palette too. Only the
|
|
86
|
+
* three sampled image identities may differ, with identical sampler/UV state. */
|
|
87
|
+
export function physicalPaletteState(material: THREE.MeshPhysicalMaterial, imageIdentities = false): PaletteState | null {
|
|
88
|
+
const held = uniforms.get(material);
|
|
89
|
+
if (!held || !materialDrawHooksSupported(material)) return null;
|
|
90
|
+
const state: PaletteState = [];
|
|
91
|
+
const append = (value: unknown, depth = 0): void => {
|
|
92
|
+
if (value === null || typeof value === 'number' || typeof value === 'string' || typeof value === 'boolean') state.push(value);
|
|
93
|
+
else if (value === undefined) state.push('undefined');
|
|
94
|
+
else if (typeof value === 'object' || typeof value === 'function') {
|
|
95
|
+
if (depth > 8) { state.push('reference', identity(value)); return; }
|
|
96
|
+
if (Array.isArray(value)) { state.push('array', value.length); for (const entry of value) append(entry, depth + 1); }
|
|
97
|
+
else if (Object.getPrototypeOf(value) === Object.prototype || value instanceof THREE.Color ||
|
|
98
|
+
value instanceof THREE.Vector2 || value instanceof THREE.Vector3 || value instanceof THREE.Vector4 ||
|
|
99
|
+
value instanceof THREE.Matrix3 || value instanceof THREE.Matrix4 || value instanceof THREE.Plane) {
|
|
100
|
+
state.push('values');
|
|
101
|
+
for (const key of Object.keys(value).sort()) { state.push(key); append((value as Record<string, unknown>)[key], depth + 1); }
|
|
102
|
+
} else state.push('reference', identity(value));
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
for (const key of Object.keys(material).sort()) {
|
|
106
|
+
if (ignoredMaterial.has(key)) continue;
|
|
107
|
+
state.push(key);
|
|
108
|
+
const value = (material as unknown as Record<string, unknown>)[key];
|
|
109
|
+
if (paletteMaps.has(key) && value instanceof THREE.Texture) {
|
|
110
|
+
// Special sampler kinds require different GLSL declarations/conversions.
|
|
111
|
+
if (value.constructor !== THREE.Texture) return null;
|
|
112
|
+
if (value.matrixAutoUpdate) value.updateMatrix();
|
|
113
|
+
state.push('sampled-image');
|
|
114
|
+
if (imageIdentities) state.push(value.uuid);
|
|
115
|
+
for (const name of Object.keys(value).sort()) {
|
|
116
|
+
if (ignoredTexture.has(name)) continue;
|
|
117
|
+
state.push(name); append((value as unknown as Record<string, unknown>)[name]);
|
|
118
|
+
}
|
|
119
|
+
append(value.userData['blenderUvName'] ?? '');
|
|
120
|
+
} else append(value);
|
|
121
|
+
}
|
|
122
|
+
for (const [name, value] of Object.entries(held)) { state.push(name); append(value.value); }
|
|
123
|
+
return state;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function samePhysicalPaletteState(first: PaletteState, second: PaletteState): boolean {
|
|
127
|
+
return first.length === second.length && first.every((value, index) => value === second[index]);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** A palette draw owns its material/uniform index array, borrowing all image
|
|
131
|
+
* and ramp storage. It never substitutes a source material's hooks. */
|
|
132
|
+
export function physicalPaletteMaterial(compiled: CompiledGraph, sources: readonly THREE.MeshPhysicalMaterial[],
|
|
133
|
+
indices: Int32Array, geometry: THREE.BufferGeometry): THREE.MeshPhysicalMaterial | null {
|
|
134
|
+
for (const source of sources) bindNamedUvChannels(source, geometry);
|
|
135
|
+
const copy = graphShadow(sources[0]!);
|
|
136
|
+
if (bindPaletteGraph(copy, compiled, sources, indices, geometry)) return copy;
|
|
137
|
+
copy.dispose();
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
69
141
|
/** Copy physical values into an independently cached draw form. Textures and
|
|
70
142
|
* graph uniform values remain borrowed from the authoritative source material. */
|
|
71
143
|
export function syncPhysicalDrawMaterial(source: THREE.Material, target?: THREE.MeshPhysicalMaterial): THREE.MeshPhysicalMaterial | null {
|
|
@@ -1253,7 +1253,7 @@ export class BlenderRuntimeView {
|
|
|
1253
1253
|
|
|
1254
1254
|
/** Before the renderer uploads attributes/builds its queues, for this area's
|
|
1255
1255
|
* actual camera. Transparent instance runs are camera-order dependent. */
|
|
1256
|
-
prepareDraw(camera: THREE.Camera, options?: {interactive: boolean; height: number; multiDraw?: boolean}): () => void {
|
|
1256
|
+
prepareDraw(camera: THREE.Camera, options?: {interactive: boolean; height: number; multiDraw?: boolean; renderer?: THREE.WebGLRenderer}): () => void {
|
|
1257
1257
|
try {
|
|
1258
1258
|
const start = performance.now();
|
|
1259
1259
|
this.instances.prepareDraw(camera);
|
|
@@ -1269,7 +1269,7 @@ export class BlenderRuntimeView {
|
|
|
1269
1269
|
const motionDone = performance.now();
|
|
1270
1270
|
this.materialRanges.prepare();
|
|
1271
1271
|
const rangesDone = performance.now();
|
|
1272
|
-
this.transparentInstances.prepare(camera, options?.multiDraw === true);
|
|
1272
|
+
this.transparentInstances.prepare(camera, options?.multiDraw === true, options?.renderer);
|
|
1273
1273
|
const timing = {opaqueMs: opaqueDone - start, motionMs: motionDone - opaqueDone,
|
|
1274
1274
|
rangesMs: rangesDone - motionDone, transparentMs: performance.now() - rangesDone};
|
|
1275
1275
|
this.drawPreparation.last = timing;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
2
|
import {BlenderOrderedBatches, ORDERED_BATCH_CAPACITY} from './blender-ordered-batches';
|
|
3
3
|
import {BlenderDrawMaterials} from './blender-draw-materials';
|
|
4
|
+
import {BlenderMaterialPalettes} from './blender-material-palettes';
|
|
4
5
|
import {presenterChanged} from './blender-presenter-change';
|
|
5
6
|
import {graphDrawAttributes} from './blender-graph-material';
|
|
6
7
|
import {materialDrawHooksSupported} from './blender-physical-material';
|
|
@@ -8,7 +9,7 @@ import {instanceMatrixSupported, instanceObjectShown, instanceObjectHooksSupport
|
|
|
8
9
|
|
|
9
10
|
const CAPACITY = ORDERED_BATCH_CAPACITY;
|
|
10
11
|
type Entry = {mesh: THREE.Mesh; material: THREE.Material | null; key: string | null; batchKey: string | null; groupOrder: number; z: number; centre: THREE.Vector3};
|
|
11
|
-
type Draw = {mesh: THREE.InstancedMesh | THREE.BatchedMesh; members: THREE.Mesh[]};
|
|
12
|
+
type Draw = {mesh: THREE.InstancedMesh | THREE.BatchedMesh; members: THREE.Mesh[]; palette: boolean};
|
|
12
13
|
|
|
13
14
|
function groupOrder(mesh: THREE.Object3D): number {
|
|
14
15
|
for (let parent = mesh.parent; parent; parent = parent.parent)
|
|
@@ -25,6 +26,10 @@ export class BlenderTransparentInstances {
|
|
|
25
26
|
private objects: THREE.Mesh[] = [];
|
|
26
27
|
private readonly drawMaterials = new BlenderDrawMaterials();
|
|
27
28
|
private readonly ordered = new BlenderOrderedBatches();
|
|
29
|
+
private readonly palettes = new BlenderMaterialPalettes();
|
|
30
|
+
private readonly sourceMaterials = new Set<THREE.Material>();
|
|
31
|
+
private plannedGeneration = -1;
|
|
32
|
+
private renderer: THREE.WebGLRenderer | undefined;
|
|
28
33
|
private readonly mixedDraws = new Map<THREE.BatchedMesh, Draw>();
|
|
29
34
|
private multiDraw = false;
|
|
30
35
|
private readonly pools = new Map<string, Draw[]>();
|
|
@@ -52,23 +57,36 @@ export class BlenderTransparentInstances {
|
|
|
52
57
|
this.objects = [...objects].filter((object): object is THREE.Mesh => (object as THREE.Mesh).isMesh === true);
|
|
53
58
|
}
|
|
54
59
|
|
|
55
|
-
prepare(camera: THREE.Camera, multiDraw = false): void {
|
|
60
|
+
prepare(camera: THREE.Camera, multiDraw = false, renderer?: THREE.WebGLRenderer): void {
|
|
56
61
|
this.finishDraw();
|
|
57
62
|
this.drawMaterials.begin();
|
|
58
63
|
if (!this.objects.length) return;
|
|
59
64
|
camera.updateMatrixWorld();
|
|
60
65
|
this.projection.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
61
|
-
|
|
66
|
+
let scene: THREE.Object3D = this.root;
|
|
67
|
+
while (scene.parent) scene = scene.parent;
|
|
68
|
+
const targetScene = scene instanceof THREE.Scene ? scene : null;
|
|
69
|
+
const capabilityChanged = this.multiDraw !== multiDraw || this.renderer !== renderer;
|
|
70
|
+
this.renderer = renderer;
|
|
62
71
|
this.multiDraw = multiDraw;
|
|
63
72
|
const inputsChanged = this.planChanged(camera);
|
|
64
|
-
if (
|
|
73
|
+
if (inputsChanged) {
|
|
74
|
+
this.sourceMaterials.clear();
|
|
75
|
+
for (const mesh of this.objects) for (const material of Array.isArray(mesh.material) ? mesh.material : [mesh.material]) this.sourceMaterials.add(material);
|
|
76
|
+
}
|
|
77
|
+
const palettesChanged = this.palettes.prepare(this.sourceMaterials, () => {
|
|
78
|
+
this.ordered.clear(); this.mixedDraws.clear(); this.planned.length = 0;
|
|
79
|
+
});
|
|
80
|
+
this.ordered.beginPaletteFrame();
|
|
81
|
+
if (renderer && targetScene) this.ordered.compilePalettes(renderer, camera, targetScene);
|
|
82
|
+
if (!inputsChanged && !capabilityChanged && !palettesChanged && this.plannedGeneration === this.ordered.generation && !this.ordered.pending) {
|
|
65
83
|
// The draw plan is a disposable presentation, not evaluated scene data.
|
|
66
84
|
// Reuse only after exact comparison of every input to culling/ordering;
|
|
67
85
|
// a camera move, drag, visibility or material change rebuilds it below.
|
|
68
86
|
for (const draw of this.planned) {
|
|
69
87
|
draw.mesh.visible = true;
|
|
70
88
|
const source = draw.members[0]!.material;
|
|
71
|
-
draw.mesh.material = this.drawMaterials.get(Array.isArray(source) ? source[0]! : source, draw.mesh);
|
|
89
|
+
if (!draw.palette) draw.mesh.material = this.drawMaterials.get(Array.isArray(source) ? source[0]! : source, draw.mesh);
|
|
72
90
|
for (const member of draw.members) {
|
|
73
91
|
member.layers.disable(0);
|
|
74
92
|
this.hidden.push(member);
|
|
@@ -147,13 +165,15 @@ export class BlenderTransparentInstances {
|
|
|
147
165
|
: 'geometryLayersOrTransform';
|
|
148
166
|
this.excluded[reason] = (this.excluded[reason] ?? 0) + 1;
|
|
149
167
|
}
|
|
150
|
-
const
|
|
168
|
+
const palette = renderer && targetScene && material ? this.palettes.get(material) : undefined;
|
|
169
|
+
const batchKey = multiDraw && key && material ? this.ordered.register(mesh.geometry, palette?.identity ?? material, mesh.renderOrder, palette) : null;
|
|
151
170
|
entries.push({mesh, material, key, batchKey, groupOrder: order, z, centre});
|
|
152
171
|
}
|
|
153
172
|
this.ordered.pack();
|
|
173
|
+
if (renderer && targetScene) this.ordered.compilePalettes(renderer, camera, targetScene);
|
|
154
174
|
// A family may exceed the bounded cache. Retain its original instancing
|
|
155
175
|
// key rather than merging incompatible geometries on the fallback path.
|
|
156
|
-
for (const entry of entries) if (entry.batchKey && this.ordered.available(entry.batchKey)) entry.key = entry.batchKey;
|
|
176
|
+
for (const entry of entries) if (entry.batchKey && this.ordered.available(entry.batchKey, renderer)) entry.key = entry.batchKey;
|
|
157
177
|
for (const [mesh] of this.mixedDraws) if (!mesh.parent) this.mixedDraws.delete(mesh);
|
|
158
178
|
entries.sort((a, b) => a.groupOrder - b.groupOrder || a.mesh.renderOrder - b.mesh.renderOrder || b.z - a.z || a.mesh.id - b.mesh.id);
|
|
159
179
|
const used = new Map<string, number>();
|
|
@@ -181,11 +201,11 @@ export class BlenderTransparentInstances {
|
|
|
181
201
|
const mixed = first.key.startsWith('multi:');
|
|
182
202
|
let draw: Draw | undefined;
|
|
183
203
|
if (mixed) {
|
|
184
|
-
const mesh = this.ordered.get(first.key, index);
|
|
204
|
+
const mesh = this.ordered.get(first.key, index, renderer);
|
|
185
205
|
if (!mesh) continue;
|
|
186
206
|
draw = this.mixedDraws.get(mesh);
|
|
187
207
|
if (!draw) {
|
|
188
|
-
draw = {mesh, members: []};
|
|
208
|
+
draw = {mesh, members: [], palette: this.ordered.isPalette(first.key)};
|
|
189
209
|
this.mixedDraws.set(mesh, draw);
|
|
190
210
|
const held = draw;
|
|
191
211
|
mesh.raycast = (raycaster, hits) => {
|
|
@@ -208,7 +228,7 @@ export class BlenderTransparentInstances {
|
|
|
208
228
|
mesh.frustumCulled = false;
|
|
209
229
|
mesh.boundingSphere = new THREE.Sphere();
|
|
210
230
|
mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
|
|
211
|
-
draw = {mesh, members: []};
|
|
231
|
+
draw = {mesh, members: [], palette: false};
|
|
212
232
|
const held = draw;
|
|
213
233
|
mesh.raycast = (raycaster, hits) => {
|
|
214
234
|
if (!mesh.visible) return;
|
|
@@ -218,7 +238,7 @@ export class BlenderTransparentInstances {
|
|
|
218
238
|
pool.push(draw);
|
|
219
239
|
this.root.add(mesh);
|
|
220
240
|
}
|
|
221
|
-
draw.mesh.material = this.drawMaterials.get(first.material!, draw.mesh);
|
|
241
|
+
if (!draw.palette) draw.mesh.material = this.drawMaterials.get(first.material!, draw.mesh);
|
|
222
242
|
draw.mesh.visible = true;
|
|
223
243
|
draw.mesh.renderOrder = head.mesh.renderOrder;
|
|
224
244
|
draw.mesh.boundingSphere!.center.copy(head.centre).applyMatrix4(this.inverse);
|
|
@@ -229,6 +249,7 @@ export class BlenderTransparentInstances {
|
|
|
229
249
|
draw.mesh.setMatrixAt(i, this.matrix);
|
|
230
250
|
if (draw.mesh instanceof THREE.BatchedMesh) {
|
|
231
251
|
draw.mesh.setGeometryIdAt(i, this.ordered.geometryId(first.key, member.geometry));
|
|
252
|
+
this.ordered.setMaterialAt(first.key, draw.mesh, i, entries[offset + i]!.material!);
|
|
232
253
|
draw.mesh.setVisibleAt(i, true);
|
|
233
254
|
}
|
|
234
255
|
draw.members.push(member);
|
|
@@ -251,9 +272,10 @@ export class BlenderTransparentInstances {
|
|
|
251
272
|
// A quiet viewport must also finish bounded pool construction. Notify only
|
|
252
273
|
// after this draw's temporary layer/geometry swaps have been restored.
|
|
253
274
|
if (this.ordered.pending) queueMicrotask(presenterChanged);
|
|
275
|
+
this.plannedGeneration = this.ordered.generation;
|
|
254
276
|
}
|
|
255
277
|
|
|
256
|
-
inspect() { return {batches: this.batches, instances: this.instances, excluded: {...this.excluded}, multiDraw: this.ordered.inspect(), materials: this.drawMaterials.inspect()}; }
|
|
278
|
+
inspect() { return {batches: this.batches, instances: this.instances, excluded: {...this.excluded}, multiDraw: this.ordered.inspect(), materials: this.drawMaterials.inspect(), palettes: this.palettes.inspect()}; }
|
|
257
279
|
|
|
258
280
|
private observe(value: number | string): void {
|
|
259
281
|
if (this.planState[this.stateIndex] !== value) {
|
|
@@ -361,6 +383,10 @@ export class BlenderTransparentInstances {
|
|
|
361
383
|
}
|
|
362
384
|
this.pools.clear();
|
|
363
385
|
this.ordered.clear();
|
|
386
|
+
this.palettes.clear();
|
|
387
|
+
this.sourceMaterials.clear();
|
|
388
|
+
this.plannedGeneration = -1;
|
|
389
|
+
this.renderer = undefined;
|
|
364
390
|
this.drawMaterials.clear();
|
|
365
391
|
this.mixedDraws.clear();
|
|
366
392
|
this.planned.length = 0;
|