@volter/blender-engine 0.1.94 → 0.1.96
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/session.py +18 -8
- 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 +101 -12
- package/browser/three/blender-transparent-instances.ts +38 -12
- package/package.json +1 -1
package/browser/session.py
CHANGED
|
@@ -806,6 +806,8 @@ def _saved_view():
|
|
|
806
806
|
"distance": float(region.view_distance),
|
|
807
807
|
"perspective": region.view_perspective,
|
|
808
808
|
"shading": space.shading.type,
|
|
809
|
+
"scene_world": bool(space.shading.use_scene_world),
|
|
810
|
+
"scene_lights": bool(space.shading.use_scene_lights),
|
|
809
811
|
}
|
|
810
812
|
return None
|
|
811
813
|
|
|
@@ -4901,13 +4903,12 @@ def rna_outliner(selected=None):
|
|
|
4901
4903
|
# `add_layer_collections_recursive`: an EXCLUDED collection's objects
|
|
4902
4904
|
# are not added at all -- it is not in the view layer.
|
|
4903
4905
|
if not lc.exclude:
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
+
# Fold parenting before paging: a parent may follow its children in
|
|
4907
|
+
# collection order, and must not disappear behind the flat cap.
|
|
4908
|
+
for obj in collection.objects:
|
|
4906
4909
|
object_row = _outliner_object(obj, view_layer, seen, chosen, active)
|
|
4907
4910
|
place(obj, object_row, row)
|
|
4908
4911
|
children.append(object_row)
|
|
4909
|
-
if more:
|
|
4910
|
-
row["more"] = more
|
|
4911
4912
|
row["children"] = children
|
|
4912
4913
|
return row
|
|
4913
4914
|
|
|
@@ -4920,13 +4921,10 @@ def rna_outliner(selected=None):
|
|
|
4920
4921
|
_outliner_key("%s.layer_collection.children" % view_layer_path,
|
|
4921
4922
|
child.collection.name))
|
|
4922
4923
|
for child in root.children]
|
|
4923
|
-
|
|
4924
|
-
for obj in page:
|
|
4924
|
+
for obj in root.collection.objects:
|
|
4925
4925
|
object_row = _outliner_object(obj, view_layer, seen, chosen, active)
|
|
4926
4926
|
place(obj, object_row, scene_collection)
|
|
4927
4927
|
children.append(object_row)
|
|
4928
|
-
if more:
|
|
4929
|
-
scene_collection["more"] = more
|
|
4930
4928
|
scene_collection["children"] = children
|
|
4931
4929
|
|
|
4932
4930
|
# `ObjectsChildrenBuilder::make_object_parent_hierarchy_collections`, parents
|
|
@@ -4974,6 +4972,18 @@ def rna_outliner(selected=None):
|
|
|
4974
4972
|
parent_row["children"].append(duplicate)
|
|
4975
4973
|
mine.append([duplicate, parent_row, parent_collection])
|
|
4976
4974
|
|
|
4975
|
+
# Cap the hierarchy's visible child lists, not the collection's flat input.
|
|
4976
|
+
# Every parent participates in folding even when its child list needs a page.
|
|
4977
|
+
def page_tree(row):
|
|
4978
|
+
children, more = _outliner_page(row.get("children", []))
|
|
4979
|
+
row["children"] = children
|
|
4980
|
+
if more:
|
|
4981
|
+
row["more"] = more
|
|
4982
|
+
for child in children:
|
|
4983
|
+
page_tree(child)
|
|
4984
|
+
|
|
4985
|
+
page_tree(scene_collection)
|
|
4986
|
+
|
|
4977
4987
|
return {
|
|
4978
4988
|
"scene": scene_path,
|
|
4979
4989
|
"viewLayer": view_layer_path,
|
|
@@ -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 {
|
|
@@ -576,6 +576,8 @@ export const frameSchema = z
|
|
|
576
576
|
perspective: z.enum(['PERSP', 'ORTHO', 'CAMERA']),
|
|
577
577
|
lens: z.number().finite().positive().default(50),
|
|
578
578
|
shading: z.enum(['WIREFRAME', 'SOLID', 'MATERIAL', 'RENDERED']).optional(),
|
|
579
|
+
scene_world: z.boolean().optional(),
|
|
580
|
+
scene_lights: z.boolean().optional(),
|
|
579
581
|
})
|
|
580
582
|
.nullable()
|
|
581
583
|
.optional(),
|
|
@@ -693,9 +695,18 @@ export class BlenderRuntimeView {
|
|
|
693
695
|
private readonly lightExtrasRoot = new THREE.Group();
|
|
694
696
|
private readonly emptyExtrasRoot = new THREE.Group();
|
|
695
697
|
private rendered = false;
|
|
698
|
+
private sceneLights = false;
|
|
699
|
+
private shadowFit: {
|
|
700
|
+
camera: THREE.Camera | undefined;
|
|
701
|
+
cameraMatrix: THREE.Matrix4 | undefined;
|
|
702
|
+
projection: THREE.Matrix4 | undefined;
|
|
703
|
+
objects: Map<THREE.Object3D, { matrix: THREE.Matrix4; visible: boolean }>;
|
|
704
|
+
} | null = null;
|
|
696
705
|
/** The camera the VIEWPORT is held in render lighting through (Blender's Rendered shading),
|
|
697
706
|
* or null when the viewport shows modeling lighting. See {@link holdRendered}. */
|
|
698
707
|
private heldRendered: (() => THREE.Camera) | null = null;
|
|
708
|
+
/** Material Preview can use scene lighting while retaining viewport visibility. */
|
|
709
|
+
private heldVisibility: 'viewport' | 'render' = 'render';
|
|
699
710
|
/** What the World was last composed for ({@link worldKeyFor}), or null when it is cleared:
|
|
700
711
|
* composing rebuilds its textures, so it happens only when the key changes. */
|
|
701
712
|
private worldApplied: string | null = null;
|
|
@@ -881,6 +892,8 @@ export class BlenderRuntimeView {
|
|
|
881
892
|
readonly projection: 'perspective' | 'orthographic';
|
|
882
893
|
readonly lens: number;
|
|
883
894
|
readonly drawMode: 'wireframe' | 'solid' | 'preview' | 'rendered' | null;
|
|
895
|
+
readonly sceneWorld: boolean;
|
|
896
|
+
readonly sceneLights: boolean;
|
|
884
897
|
} | null {
|
|
885
898
|
const saved = this.frame?.view;
|
|
886
899
|
if (!saved) return null;
|
|
@@ -896,6 +909,8 @@ export class BlenderRuntimeView {
|
|
|
896
909
|
projection: saved.perspective === 'ORTHO' ? 'orthographic' : 'perspective',
|
|
897
910
|
lens: saved.lens,
|
|
898
911
|
drawMode: saved.shading ? SAVED_SHADING[saved.shading] : null,
|
|
912
|
+
sceneWorld: saved.scene_world ?? false,
|
|
913
|
+
sceneLights: saved.scene_lights ?? false,
|
|
899
914
|
};
|
|
900
915
|
}
|
|
901
916
|
|
|
@@ -1005,7 +1020,16 @@ export class BlenderRuntimeView {
|
|
|
1005
1020
|
}
|
|
1006
1021
|
|
|
1007
1022
|
stageMode(): string | null {
|
|
1008
|
-
return this.mode;
|
|
1023
|
+
return this.playing ? 'PLAY' : this.mode;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/** PLAY IS A MODE OF THE STAGE: while the document plays, the keys are the player's, and the
|
|
1027
|
+
* mode this context publishes is what the frame's key rules stand down on
|
|
1028
|
+
* (`scripts/workbench/generate-keymaps.mjs`, `whenFor`). Blender's own mode returns on stop. */
|
|
1029
|
+
private playing = false;
|
|
1030
|
+
|
|
1031
|
+
setPlaying(playing: boolean): void {
|
|
1032
|
+
this.playing = playing;
|
|
1009
1033
|
}
|
|
1010
1034
|
|
|
1011
1035
|
/** The inverse: the presented object for a Blender datablock NAME, or null
|
|
@@ -1153,15 +1177,26 @@ export class BlenderRuntimeView {
|
|
|
1153
1177
|
* ({@link setRendered}) — the scene's own lights, its World behind and around the model,
|
|
1154
1178
|
* `hide_render` visibility and shadows — seen through `drawCamera()`, the camera the stage
|
|
1155
1179
|
* draws with (orthographic in an orthographic view). `null` returns the viewport to modeling.
|
|
1180
|
+
* Material Preview passes viewport visibility independently of its lighting choices.
|
|
1156
1181
|
* A render taken meanwhile ends back in this state rather than in modeling.
|
|
1157
1182
|
*/
|
|
1158
|
-
|
|
1159
|
-
|
|
1183
|
+
private heldSceneLighting: { world: boolean; lights: boolean } | undefined;
|
|
1184
|
+
holdRendered(drawCamera: (() => THREE.Camera) | null, sceneLighting?: { world: boolean; lights: boolean }, visibility: 'viewport' | 'render' = 'render'): void {
|
|
1185
|
+
if (drawCamera === this.heldRendered && sceneLighting?.world === this.heldSceneLighting?.world && sceneLighting?.lights === this.heldSceneLighting?.lights && visibility === this.heldVisibility) return;
|
|
1160
1186
|
this.heldRendered = drawCamera;
|
|
1187
|
+
this.heldSceneLighting = sceneLighting;
|
|
1188
|
+
this.heldVisibility = visibility;
|
|
1161
1189
|
if (this.capturing) return;
|
|
1162
1190
|
this.report(drawCamera === null ? this.applyRendered(false) : this.applyRendered(true, drawCamera()));
|
|
1163
1191
|
}
|
|
1164
1192
|
|
|
1193
|
+
/** Prepare a rendered viewport before revealing its first frame, using the
|
|
1194
|
+
* same lighting/image/World readiness as a photograph. Call after the stage
|
|
1195
|
+
* has attached the root to its scene. */
|
|
1196
|
+
async prepareRendered(camera: THREE.Camera): Promise<void> {
|
|
1197
|
+
await this.applyRendered(true, camera);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1165
1200
|
/**
|
|
1166
1201
|
* Re-apply the held render lighting if the draw camera or the World changed since it was
|
|
1167
1202
|
* last applied. Cheap when nothing did; the stage calls it every frame it draws.
|
|
@@ -1170,7 +1205,8 @@ export class BlenderRuntimeView {
|
|
|
1170
1205
|
// A photograph in progress keeps its own state; it returns to this one when it ends.
|
|
1171
1206
|
if (this.heldRendered === null || this.capturing) return;
|
|
1172
1207
|
const camera = this.heldRendered();
|
|
1173
|
-
|
|
1208
|
+
const worldKey = this.heldSceneLighting?.world === false ? null : this.worldKeyFor(camera);
|
|
1209
|
+
if (worldKey === this.worldApplied) return;
|
|
1174
1210
|
this.report(this.applyRendered(true, camera));
|
|
1175
1211
|
}
|
|
1176
1212
|
|
|
@@ -1183,7 +1219,12 @@ export class BlenderRuntimeView {
|
|
|
1183
1219
|
const ortho = (camera as THREE.OrthographicCamera).isOrthographicCamera;
|
|
1184
1220
|
// Coarse, so an orbit recomposes a handful of times rather than every frame.
|
|
1185
1221
|
const turn = ortho ? camera.getWorldDirection(new THREE.Vector3()).toArray().map((v) => v.toFixed(1)).join(',') : '';
|
|
1186
|
-
|
|
1222
|
+
// A stage can announce itself before parenting the model into its Scene.
|
|
1223
|
+
// Retry after that attachment; the first World apply otherwise finds no Scene
|
|
1224
|
+
// and its cached key leaves the viewport with no sky or ambient light.
|
|
1225
|
+
let owner: THREE.Object3D = this.root;
|
|
1226
|
+
while (owner.parent) owner = owner.parent;
|
|
1227
|
+
return `${owner.uuid}:${camera.uuid}:${turn}:${this.worldKey}`;
|
|
1187
1228
|
}
|
|
1188
1229
|
|
|
1189
1230
|
private report(work: Promise<void>): void {
|
|
@@ -1253,11 +1294,15 @@ export class BlenderRuntimeView {
|
|
|
1253
1294
|
|
|
1254
1295
|
/** Before the renderer uploads attributes/builds its queues, for this area's
|
|
1255
1296
|
* actual camera. Transparent instance runs are camera-order dependent. */
|
|
1256
|
-
prepareDraw(camera: THREE.Camera, options?: {interactive: boolean; height: number; multiDraw?: boolean}): () => void {
|
|
1297
|
+
prepareDraw(camera: THREE.Camera, options?: {interactive: boolean; height: number; multiDraw?: boolean; renderer?: THREE.WebGLRenderer}): () => void {
|
|
1257
1298
|
try {
|
|
1258
1299
|
const start = performance.now();
|
|
1259
1300
|
this.instances.prepareDraw(camera);
|
|
1260
1301
|
this.root.updateMatrixWorld(true);
|
|
1302
|
+
// The script/navigation has just posed the camera and the detached
|
|
1303
|
+
// objects. Fit shadows for that draw, rather than the camera's pose
|
|
1304
|
+
// when Rendered shading was first entered. Captures use this path too.
|
|
1305
|
+
if (this.rendered) this.applyShadows(true, camera);
|
|
1261
1306
|
const opaqueDone = performance.now();
|
|
1262
1307
|
const motion = this.motionGeometry.prepare(camera, !this.rendered && (this.mode ?? this.frame?.mode) === 'OBJECT' &&
|
|
1263
1308
|
options?.interactive === true, options?.height ?? 0);
|
|
@@ -1269,7 +1314,7 @@ export class BlenderRuntimeView {
|
|
|
1269
1314
|
const motionDone = performance.now();
|
|
1270
1315
|
this.materialRanges.prepare();
|
|
1271
1316
|
const rangesDone = performance.now();
|
|
1272
|
-
this.transparentInstances.prepare(camera, options?.multiDraw === true);
|
|
1317
|
+
this.transparentInstances.prepare(camera, options?.multiDraw === true, options?.renderer);
|
|
1273
1318
|
const timing = {opaqueMs: opaqueDone - start, motionMs: motionDone - opaqueDone,
|
|
1274
1319
|
rangesMs: rangesDone - motionDone, transparentMs: performance.now() - rangesDone};
|
|
1275
1320
|
this.drawPreparation.last = timing;
|
|
@@ -1321,7 +1366,11 @@ export class BlenderRuntimeView {
|
|
|
1321
1366
|
}
|
|
1322
1367
|
|
|
1323
1368
|
private async applyRendered(rendered: boolean, camera?: THREE.Camera): Promise<void> {
|
|
1324
|
-
|
|
1369
|
+
// A frame or shading change can replace geometry, visibility and lights.
|
|
1370
|
+
this.shadowFit = null;
|
|
1371
|
+
const sceneWorld = rendered && (this.capturing || this.heldSceneLighting?.world !== false);
|
|
1372
|
+
const sceneLights = rendered && (this.capturing || this.heldSceneLighting?.lights !== false);
|
|
1373
|
+
const extinction = (sceneWorld ? worldMedium(this.frame?.world)?.extinction : null) ?? new THREE.Vector3();
|
|
1325
1374
|
for (const material of [...this.materials.values(), this.fallback]) applyWorldExtinction(material, extinction);
|
|
1326
1375
|
// An area light cannot be DRAWN until its lookup tables are uploaded, and a
|
|
1327
1376
|
// render is one photograph with no second chance at it.
|
|
@@ -1329,12 +1378,15 @@ export class BlenderRuntimeView {
|
|
|
1329
1378
|
// same way it waits for area-light tables and image decodes: one
|
|
1330
1379
|
// photograph, no second chance at it.
|
|
1331
1380
|
this.rendered = rendered;
|
|
1381
|
+
this.sceneLights = sceneLights;
|
|
1382
|
+
// This is the studio rig, not the authored lights. A scene-lit preview
|
|
1383
|
+
// never adds the Solid studio, including World-only Material Preview.
|
|
1332
1384
|
this.lighting.setRendered(rendered);
|
|
1333
1385
|
// The scene's world is what a render sees past the geometry AND its
|
|
1334
1386
|
// ambient light; modeling keeps the document's own backdrop and fill.
|
|
1335
1387
|
// A photograph always composes its own; the viewport recomposes only when the key changed.
|
|
1336
|
-
const worldKey =
|
|
1337
|
-
if (!
|
|
1388
|
+
const worldKey = sceneWorld && camera ? this.worldKeyFor(camera) : null;
|
|
1389
|
+
if (!sceneWorld) this.world.clear();
|
|
1338
1390
|
else if (this.capturing || worldKey !== this.worldApplied) this.world.apply(this.root, this.frame?.world ?? null, camera);
|
|
1339
1391
|
this.worldApplied = this.capturing ? null : worldKey;
|
|
1340
1392
|
this.applyVisibility();
|
|
@@ -1372,6 +1424,21 @@ export class BlenderRuntimeView {
|
|
|
1372
1424
|
*/
|
|
1373
1425
|
private applyShadows(rendered: boolean, camera?: THREE.Camera): void {
|
|
1374
1426
|
this.root.updateMatrixWorld(true);
|
|
1427
|
+
const fitted = this.shadowFit;
|
|
1428
|
+
if (rendered && fitted && fitted.camera === camera &&
|
|
1429
|
+
(!camera || (fitted.cameraMatrix?.equals(camera.matrixWorld) && fitted.projection?.equals(camera.projectionMatrix))) &&
|
|
1430
|
+
fitted.objects.size === this.objects.size &&
|
|
1431
|
+
[...this.objects.values()].every(object => {
|
|
1432
|
+
const previous = fitted.objects.get(object);
|
|
1433
|
+
return previous?.visible === object.visible && previous.matrix.equals(object.matrixWorld);
|
|
1434
|
+
})) return;
|
|
1435
|
+
this.shadowFit = rendered ? {
|
|
1436
|
+
camera,
|
|
1437
|
+
cameraMatrix: camera?.matrixWorld.clone(),
|
|
1438
|
+
projection: camera?.projectionMatrix.clone(),
|
|
1439
|
+
objects: new Map([...this.objects.values()].map(object =>
|
|
1440
|
+
[object, { matrix: object.matrixWorld.clone(), visible: object.visible }])),
|
|
1441
|
+
} : null;
|
|
1375
1442
|
const boxes: THREE.Box3[] = [];
|
|
1376
1443
|
for (const object of this.objects.values()) {
|
|
1377
1444
|
const mesh = object as THREE.Mesh;
|
|
@@ -1497,12 +1564,14 @@ export class BlenderRuntimeView {
|
|
|
1497
1564
|
}
|
|
1498
1565
|
|
|
1499
1566
|
private applyVisibility(): void {
|
|
1567
|
+
const renderVisibility = this.rendered && (this.capturing || this.heldVisibility === 'render');
|
|
1500
1568
|
for (const obj of this.frame?.objects ?? []) {
|
|
1501
1569
|
const object = this.objects.get(obj.id);
|
|
1502
|
-
|
|
1570
|
+
const visible = renderVisibility ? obj.render_visible : obj.visible;
|
|
1571
|
+
if (object) object.visible = visible;
|
|
1503
1572
|
if (!obj.light) continue;
|
|
1504
1573
|
const light = this.lights.get(obj.light);
|
|
1505
|
-
if (light) light.visible = this.
|
|
1574
|
+
if (light) light.visible = this.sceneLights && visible;
|
|
1506
1575
|
}
|
|
1507
1576
|
// AN OVERLAY IS MODELING CHROME AND IS NEVER PHOTOGRAPHED. The same
|
|
1508
1577
|
// distinction the loop above draws between what the viewport shows and
|
|
@@ -2224,6 +2293,26 @@ export class BlenderRuntimeView {
|
|
|
2224
2293
|
};
|
|
2225
2294
|
}
|
|
2226
2295
|
|
|
2296
|
+
/**
|
|
2297
|
+
* A COPY OF THE MODEL THAT NO LONGER FOLLOWS IT, for Play: the graph `follow` builds, from the
|
|
2298
|
+
* frame this view holds now, and handed nothing afterwards. What moves its objects is the
|
|
2299
|
+
* caller's; Blender's data and this view never see it, and disposing it is the whole undo.
|
|
2300
|
+
*/
|
|
2301
|
+
detach(): { readonly view: BlenderRuntimeView; dispose(): void } {
|
|
2302
|
+
const detached = new BlenderRuntimeView();
|
|
2303
|
+
if (this.frame) detached.applyFrame(this.fullFrame());
|
|
2304
|
+
let disposed = false;
|
|
2305
|
+
return {
|
|
2306
|
+
view: detached,
|
|
2307
|
+
dispose: () => {
|
|
2308
|
+
if (disposed) return;
|
|
2309
|
+
disposed = true;
|
|
2310
|
+
detached.dispose();
|
|
2311
|
+
detached.root.removeFromParent();
|
|
2312
|
+
},
|
|
2313
|
+
};
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2227
2316
|
captureSnapshot() {
|
|
2228
2317
|
const frame = this.fullFrame();
|
|
2229
2318
|
const source = this.frame!;
|
|
@@ -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;
|