@volter/blender-engine 0.1.6 → 0.1.7
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/runtime.ts +5 -0
- package/browser/session.py +505 -2
- package/browser/three/blender-frame-columns.ts +4 -0
- package/browser/three/blender-graph-material.ts +402 -0
- package/browser/three/blender-node-glsl.generated.ts +401 -0
- package/browser/three/blender-node-graph.ts +1026 -0
- package/browser/three/blender-physical-material.ts +32 -2
- package/browser/three/blender-presenter-change.ts +24 -0
- package/browser/three/blender-runtime-geometry.ts +86 -6
- package/browser/three/blender-runtime-lighting.ts +3 -0
- package/browser/three/blender-runtime-view.ts +173 -5
- package/browser/three/blender-texture-samplers.ts +2 -0
- package/package.json +1 -1
- package/wasm/BUNDLE.json +11 -31
- package/wasm/blender_browser.wasm.br +0 -0
|
@@ -4,6 +4,7 @@
|
|
|
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, graphProgramKey, setGraphViewport} from './blender-graph-material';
|
|
7
8
|
import {bindNamedUvChannels} from './blender-texture-samplers';
|
|
8
9
|
|
|
9
10
|
const scalar = z.number().finite();
|
|
@@ -33,6 +34,28 @@ export function applyWorldExtinction(material: THREE.MeshPhysicalMaterial, extin
|
|
|
33
34
|
uniforms.get(material)!.blenderWorldExtinction.value.copy(extinction);
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
/** A copy of `material` with its own hooks and the same physical values:
|
|
38
|
+
* the proxy a pending graph program compiles through, whose program key is
|
|
39
|
+
* the one `material` asks for once the graph is swapped in. */
|
|
40
|
+
function graphShadow(material: THREE.MeshPhysicalMaterial): THREE.MeshPhysicalMaterial {
|
|
41
|
+
const shadow = new THREE.MeshPhysicalMaterial();
|
|
42
|
+
const held = uniforms.get(material);
|
|
43
|
+
// The hooks, then the values: `applyPhysicalMaterial` sets its defaults,
|
|
44
|
+
// which `copy` replaces with the material's own (it leaves hooks alone).
|
|
45
|
+
applyPhysicalMaterial(shadow);
|
|
46
|
+
shadow.copy(material);
|
|
47
|
+
const values = uniforms.get(shadow)!;
|
|
48
|
+
if (held) {
|
|
49
|
+
values.blenderCoatIor.value = held.blenderCoatIor.value;
|
|
50
|
+
values.blenderCoatTint.value.copy(held.blenderCoatTint.value);
|
|
51
|
+
values.blenderMapClip.value = held.blenderMapClip.value;
|
|
52
|
+
values.blenderRoughnessClip.value = held.blenderRoughnessClip.value;
|
|
53
|
+
values.blenderNormalClip.value = held.blenderNormalClip.value;
|
|
54
|
+
values.blenderWorldExtinction.value.copy(held.blenderWorldExtinction.value);
|
|
55
|
+
}
|
|
56
|
+
return shadow;
|
|
57
|
+
}
|
|
58
|
+
|
|
36
59
|
export function applyPhysicalMaterial(material: THREE.MeshPhysicalMaterial, input?: Physical,
|
|
37
60
|
clips: {map?: boolean; roughness?: boolean; normal?: boolean} = {}): void {
|
|
38
61
|
const data = input ?? defaults;
|
|
@@ -55,8 +78,12 @@ export function applyPhysicalMaterial(material: THREE.MeshPhysicalMaterial, inpu
|
|
|
55
78
|
blenderWorldExtinction: {value: new THREE.Vector3()}};
|
|
56
79
|
uniforms.set(material, values);
|
|
57
80
|
const held = values;
|
|
58
|
-
material.customProgramCacheKey = () =>
|
|
59
|
-
material.onBeforeRender = (
|
|
81
|
+
material.customProgramCacheKey = () => `blender-principled-physical-v5${graphProgramKey(material)}`;
|
|
82
|
+
material.onBeforeRender = (renderer, scene, camera, geometry, object) => {
|
|
83
|
+
bindNamedUvChannels(material, geometry);
|
|
84
|
+
bindGraphDraw(material, geometry, renderer, scene, camera, object, graphShadow);
|
|
85
|
+
setGraphViewport(renderer);
|
|
86
|
+
};
|
|
60
87
|
material.onBeforeCompile = shader => {
|
|
61
88
|
// Three declares channels 0..3. Blender has eight named UV maps in
|
|
62
89
|
// addition to the default channel; only the used attributes survive GLSL.
|
|
@@ -121,6 +148,9 @@ export function applyPhysicalMaterial(material: THREE.MeshPhysicalMaterial, inpu
|
|
|
121
148
|
blenderCoatIor * blenderCoatIor - (1.0 - dotNVcc * dotNVcc)));
|
|
122
149
|
outgoingLight *= mix(vec3(1.0), pow(max(blenderCoatTint, vec3(0.0)), vec3(coatPath)), material.clearcoat);
|
|
123
150
|
vec3 Fcc = F_Schlick`);
|
|
151
|
+
// The material's node graph, when the session shipped one, drives the
|
|
152
|
+
// surface inputs it carries (`blender-graph-material.ts`).
|
|
153
|
+
applyGraphShader(material, shader);
|
|
124
154
|
};
|
|
125
155
|
material.needsUpdate = true;
|
|
126
156
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WORK THAT FINISHES AFTER A FRAME WAS APPLIED, announced.
|
|
3
|
+
*
|
|
4
|
+
* A presented frame is not the last change to what the presenter draws: a
|
|
5
|
+
* PNG decodes after the frame that named it, a clone's sampler uploads after
|
|
6
|
+
* that decode, a sky is derived on a worker, the area-light tables load on
|
|
7
|
+
* demand, and a graph's program swaps in once `compileAsync` links it. A
|
|
8
|
+
* stage that draws only when something changed (`BlenderRuntimeView.onChange`)
|
|
9
|
+
* needs every one of those to say so, or the viewport holds the picture from
|
|
10
|
+
* before it -- an untextured mesh, a missing sky -- until something else
|
|
11
|
+
* happens to move. Each such completion calls {@link presenterChanged}.
|
|
12
|
+
*/
|
|
13
|
+
const listeners = new Set<() => void>();
|
|
14
|
+
|
|
15
|
+
export function presenterChanged(): void {
|
|
16
|
+
for (const listener of [...listeners]) listener();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function onPresenterChange(listener: () => void): () => void {
|
|
20
|
+
listeners.add(listener);
|
|
21
|
+
return () => {
|
|
22
|
+
listeners.delete(listener);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -41,6 +41,13 @@ export interface DrawArrays {
|
|
|
41
41
|
uv: Float32Array | null;
|
|
42
42
|
/** All named UV layers; material inputs choose their own layer per draw. */
|
|
43
43
|
uvLayers?: {name: string; data: Float32Array}[] | undefined;
|
|
44
|
+
/** The mesh's generic attribute layers a material's Attribute or Color
|
|
45
|
+
* Attribute node can read, four floats per drawn vertex as EEVEE loads them
|
|
46
|
+
* (`graphAttributeLayers`). */
|
|
47
|
+
attributeLayers?: {name: string; data: Float32Array}[] | undefined;
|
|
48
|
+
/** Per drawn vertex, a deformed mesh's Generated coordinates (0..1), or
|
|
49
|
+
* null when the positions are undeformed and orco derives from them. */
|
|
50
|
+
orco?: Float32Array | null | undefined;
|
|
44
51
|
indices: Uint32Array;
|
|
45
52
|
groups: { start: number; count: number; materialIndex: number }[];
|
|
46
53
|
/** THE BLENDER VERTEX EACH DRAWN VERTEX CAME FROM, one per drawn vertex.
|
|
@@ -61,6 +68,8 @@ interface DrawView {
|
|
|
61
68
|
vertexCount: number;
|
|
62
69
|
normals: Normal[] | null;
|
|
63
70
|
cornerNormal?: Float32Array | undefined;
|
|
71
|
+
/** Blender's stored orco (-1..1), 3 per vertex. */
|
|
72
|
+
orco?: Float32Array | undefined;
|
|
64
73
|
co(i: number): THREE.Vector3;
|
|
65
74
|
/** CSR faces: corner `starts[i]..starts[i+1]` hold vertex indices. */
|
|
66
75
|
starts: Uint32Array;
|
|
@@ -73,6 +82,49 @@ interface DrawView {
|
|
|
73
82
|
/** A corner's UV, or undefined when the mesh has no map. */
|
|
74
83
|
uv: ((corner: number) => [number, number]) | null;
|
|
75
84
|
uvLayers?: {name: string; read: (corner: number) => [number, number]}[];
|
|
85
|
+
/** A corner's value of each generic attribute layer, by vertex, corner and
|
|
86
|
+
* face; `corner` and `face` are undefined for a loose vertex. */
|
|
87
|
+
attributeLayers?: {name: string; domain: string;
|
|
88
|
+
read: (vertex: number, corner: number | undefined, face: number | undefined) => ArrayLike<number>}[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** EEVEE's float4 of an attribute value: float (f, f, f, 1), float2 (x, y, 0,
|
|
92
|
+
* 1), float3 (x, y, z, 1), a colour as itself, a byte colour decoded from
|
|
93
|
+
* sRGB (`ColorGeometry4b::decode`). */
|
|
94
|
+
const GRAPH_ATTRIBUTE_TYPES = new Set(['FLOAT', 'FLOAT2', 'FLOAT_VECTOR', 'FLOAT_COLOR', 'BYTE_COLOR']);
|
|
95
|
+
function srgbToLinear(byte: number): number {
|
|
96
|
+
const c = byte / 255;
|
|
97
|
+
return c < 0.04045 ? (c < 0 ? 0 : c * (1 / 12.92)) : ((c + 0.055) * (1 / 1.055)) ** 2.4;
|
|
98
|
+
}
|
|
99
|
+
function attributeFloat4(type: string, values: ArrayLike<number>, i: number): [number, number, number, number] {
|
|
100
|
+
switch (type) {
|
|
101
|
+
case 'FLOAT': { const f = Number(values[i]); return [f, f, f, 1]; }
|
|
102
|
+
case 'FLOAT2': return [Number(values[i * 2]), Number(values[i * 2 + 1]), 0, 1];
|
|
103
|
+
case 'FLOAT_VECTOR': return [Number(values[i * 3]), Number(values[i * 3 + 1]), Number(values[i * 3 + 2]), 1];
|
|
104
|
+
case 'BYTE_COLOR': return [srgbToLinear(Number(values[i * 4])), srgbToLinear(Number(values[i * 4 + 1])),
|
|
105
|
+
srgbToLinear(Number(values[i * 4 + 2])), Number(values[i * 4 + 3]) / 255];
|
|
106
|
+
default: return [Number(values[i * 4]), Number(values[i * 4 + 1]), Number(values[i * 4 + 2]), Number(values[i * 4 + 3])];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** The readable layers of `attributes`: every generic layer of a type EEVEE
|
|
110
|
+
* loads as a float4, on a domain a drawn vertex has a value on. UV maps stay
|
|
111
|
+
* `uvLayers`; names Blender keeps internal (`.select_vert`, the positions,
|
|
112
|
+
* the custom normals) are not material inputs. */
|
|
113
|
+
function graphAttributeLayers(
|
|
114
|
+
attributes: readonly {name: string; domain: string; type: string; data: ArrayLike<unknown>}[],
|
|
115
|
+
flat: (data: ArrayLike<unknown>, type: string) => ArrayLike<number>,
|
|
116
|
+
): NonNullable<DrawView['attributeLayers']> {
|
|
117
|
+
return attributes
|
|
118
|
+
.filter(a => GRAPH_ATTRIBUTE_TYPES.has(a.type) && a.domain !== 'EDGE' && !a.name.startsWith('.')
|
|
119
|
+
&& a.name !== 'position' && a.name !== 'custom_normal' && !(a.type === 'FLOAT2' && a.domain === 'CORNER'))
|
|
120
|
+
.map(a => {
|
|
121
|
+
const values = flat(a.data, a.type);
|
|
122
|
+
const zero = [0, 0, 0, 0];
|
|
123
|
+
return {name: a.name, domain: a.domain, read: (vertex: number, corner: number | undefined, face: number | undefined) =>
|
|
124
|
+
a.domain === 'POINT' ? attributeFloat4(a.type, values, vertex)
|
|
125
|
+
: a.domain === 'CORNER' ? (corner === undefined ? zero : attributeFloat4(a.type, values, corner))
|
|
126
|
+
: face === undefined ? zero : attributeFloat4(a.type, values, face)};
|
|
127
|
+
});
|
|
76
128
|
}
|
|
77
129
|
|
|
78
130
|
function drawCore(view: DrawView): Omit<DrawArrays, 'hash'> {
|
|
@@ -152,14 +204,19 @@ function drawCore(view: DrawView): Omit<DrawArrays, 'hash'> {
|
|
|
152
204
|
texcoords = new Float32Array(capacity * 2);
|
|
153
205
|
const sourceVertex = new Uint32Array(capacity);
|
|
154
206
|
const uvLayers = (view.uvLayers ?? []).map(layer => ({...layer, data: new Float32Array(capacity * 2)}));
|
|
207
|
+
const attributeLayers = (view.attributeLayers ?? []).map(layer => ({...layer, data: new Float32Array(capacity * 4)}));
|
|
208
|
+
const orco = view.orco ? new Float32Array(capacity * 3) : null;
|
|
155
209
|
const indices = new Uint32Array(capacity),
|
|
156
210
|
shared = new Map<string, number>();
|
|
157
211
|
const groups: DrawArrays['groups'] = [];
|
|
158
212
|
const normals = view.normals || view.cornerNormal ? new Float32Array(capacity * 3) : null;
|
|
159
213
|
let count = 0,
|
|
160
214
|
indexCount = 0;
|
|
161
|
-
const emit = (v: number, texture?: [number, number], normal?: Normal, sourceCorner?: number): number => {
|
|
215
|
+
const emit = (v: number, texture?: [number, number], normal?: Normal, sourceCorner?: number, face?: number): number => {
|
|
162
216
|
const index = count++;
|
|
217
|
+
for (const layer of attributeLayers) layer.data.set(layer.read(v, sourceCorner, face), index * 4);
|
|
218
|
+
// attr_load_orco: the stored -1..1 orco, mapped to 0..1.
|
|
219
|
+
if (orco) for (let k = 0; k < 3; k++) orco[index * 3 + k] = view.orco![v * 3 + k]! * 0.5 + 0.5;
|
|
163
220
|
sourceVertex[index] = v;
|
|
164
221
|
view.co(v).toArray(positions, index * 3);
|
|
165
222
|
if (texture) texcoords.set(texture, index * 2);
|
|
@@ -167,16 +224,18 @@ function drawCore(view: DrawView): Omit<DrawArrays, 'hash'> {
|
|
|
167
224
|
if (sourceCorner !== undefined) for (const layer of uvLayers) layer.data.set(layer.read(sourceCorner), index * 2);
|
|
168
225
|
return index;
|
|
169
226
|
};
|
|
170
|
-
const corner = (c: number): number => {
|
|
227
|
+
const corner = (c: number, face: number): number => {
|
|
171
228
|
const texture = uv?.(c);
|
|
172
229
|
const normal: Normal | undefined = view.cornerNormal
|
|
173
230
|
? [view.cornerNormal[c * 3]!, view.cornerNormal[c * 3 + 1]!, view.cornerNormal[c * 3 + 2]!]
|
|
174
231
|
: view.normals?.[c];
|
|
175
|
-
if (!mixed) return emit(cornerVerts[c]!, texture, normal, c);
|
|
176
|
-
|
|
232
|
+
if (!mixed) return emit(cornerVerts[c]!, texture, normal, c, face);
|
|
233
|
+
// A POINT layer's value is the vertex's, which the smooth fan already
|
|
234
|
+
// shares; a face or corner layer splits the fan where its values differ.
|
|
235
|
+
const key = `${smooth[c] ? find(c) : c}${texture ? `:${texture[0]}:${texture[1]}` : ''}${normal ? `:n${normal.join(':')}` : ''}${uvLayers.map(layer => ':' + layer.read(c).join(':')).join('')}${attributeLayers.map(layer => layer.domain === 'POINT' ? '' : ':' + Array.from(layer.read(cornerVerts[c]!, c, face)).join(':')).join('')}`;
|
|
177
236
|
let index = shared.get(key);
|
|
178
237
|
if (index === undefined) {
|
|
179
|
-
index = emit(cornerVerts[c]!, texture, normal, c);
|
|
238
|
+
index = emit(cornerVerts[c]!, texture, normal, c, face);
|
|
180
239
|
shared.set(key, index);
|
|
181
240
|
}
|
|
182
241
|
return index;
|
|
@@ -188,7 +247,7 @@ function drawCore(view: DrawView): Omit<DrawArrays, 'hash'> {
|
|
|
188
247
|
const points: THREE.Vector3[] = [];
|
|
189
248
|
for (let j = 0; j < length; j++) points.push(view.co(cornerVerts[faceStart + j]!));
|
|
190
249
|
for (const triangle of triangulatePolygon(points).triangles)
|
|
191
|
-
for (const c of triangle) indices[indexCount++] = corner(faceStart + c);
|
|
250
|
+
for (const c of triangle) indices[indexCount++] = corner(faceStart + c, i);
|
|
192
251
|
const materialIndex = view.material(i),
|
|
193
252
|
previous = groups.at(-1);
|
|
194
253
|
if (previous?.materialIndex === materialIndex) previous.count += indexCount - start;
|
|
@@ -200,12 +259,21 @@ function drawCore(view: DrawView): Omit<DrawArrays, 'hash'> {
|
|
|
200
259
|
normals: normals?.slice(0, count * 3) ?? null,
|
|
201
260
|
uv: uv ? texcoords.slice(0, count * 2) : null,
|
|
202
261
|
uvLayers: uvLayers.map(layer => ({name: layer.name, data: layer.data.slice(0, count * 2)})),
|
|
262
|
+
attributeLayers: attributeLayers.map(layer => ({name: layer.name, data: layer.data.slice(0, count * 4)})),
|
|
263
|
+
orco: orco ? orco.slice(0, count * 3) : null,
|
|
203
264
|
indices: indices.slice(0, indexCount),
|
|
204
265
|
sourceVertex: sourceVertex.slice(0, count),
|
|
205
266
|
groups,
|
|
206
267
|
};
|
|
207
268
|
}
|
|
208
269
|
|
|
270
|
+
/** The three attribute a graph reads Blender attribute `name` from: a GLSL
|
|
271
|
+
* identifier spelled from the name's UTF-8 bytes, so every name has one and
|
|
272
|
+
* no two names share one. */
|
|
273
|
+
export function graphAttributeName(name: string): string {
|
|
274
|
+
return `blenderA_${Array.from(new TextEncoder().encode(name), b => b.toString(16).padStart(2, '0')).join('')}`;
|
|
275
|
+
}
|
|
276
|
+
|
|
209
277
|
export function geometryFromDrawArrays(
|
|
210
278
|
arrays: Omit<DrawArrays, 'hash' | 'sourceVertex'> & { sourceVertex?: Uint32Array },
|
|
211
279
|
): THREE.BufferGeometry {
|
|
@@ -225,6 +293,13 @@ export function geometryFromDrawArrays(
|
|
|
225
293
|
geometry.setAttribute(`uv${index + 1}`, new THREE.BufferAttribute(layer.data, 2));
|
|
226
294
|
}
|
|
227
295
|
geometry.userData['blenderUvChannels'] = channels;
|
|
296
|
+
for (const layer of arrays.attributeLayers ?? [])
|
|
297
|
+
geometry.setAttribute(graphAttributeName(layer.name), new THREE.BufferAttribute(layer.data, 4));
|
|
298
|
+
geometry.userData['blenderAttributes'] = (arrays.attributeLayers ?? []).map(layer => layer.name);
|
|
299
|
+
if (arrays.orco) {
|
|
300
|
+
geometry.setAttribute('blenderOrco', new THREE.BufferAttribute(arrays.orco, 3));
|
|
301
|
+
geometry.userData['blenderOrcoFromDoor'] = true;
|
|
302
|
+
}
|
|
228
303
|
for (const group of arrays.groups)
|
|
229
304
|
geometry.addGroup(group.start, group.count, group.materialIndex);
|
|
230
305
|
if (arrays.normals) geometry.setAttribute('normal', new THREE.BufferAttribute(arrays.normals, 3));
|
|
@@ -288,6 +363,7 @@ export function drawArraysFromColumns(c: MeshColumns, hash: string): DrawArrays
|
|
|
288
363
|
vertexCount: nv,
|
|
289
364
|
normals: normalMesh ? customCornerNormals(normalMesh, customData) : null,
|
|
290
365
|
cornerNormal: c.cornerNormal,
|
|
366
|
+
orco: c.orco,
|
|
291
367
|
co: (i) => scratch.set(c.co[i * 3]!, c.co[i * 3 + 1]!, c.co[i * 3 + 2]!).clone(),
|
|
292
368
|
starts: c.faceStart,
|
|
293
369
|
cornerVerts: c.corner,
|
|
@@ -298,6 +374,7 @@ export function drawArraysFromColumns(c: MeshColumns, hash: string): DrawArrays
|
|
|
298
374
|
uv: uvData ? (corner) => [uvData[corner * 2]!, uvData[corner * 2 + 1]!] : null,
|
|
299
375
|
uvLayers: maps.map(layer => ({name: layer.name, read: (corner: number) =>
|
|
300
376
|
[Number(layer.data[corner * 2]), Number(layer.data[corner * 2 + 1])] as [number, number]})),
|
|
377
|
+
attributeLayers: graphAttributeLayers(c.attributes, data => data as ArrayLike<number>),
|
|
301
378
|
};
|
|
302
379
|
if (map && ATTRIBUTE_LAYOUT.FLOAT2.size !== 2) throw new Error('FLOAT2 layout');
|
|
303
380
|
return { ...drawCore(view), hash };
|
|
@@ -360,6 +437,9 @@ export function drawRuntimeGeometry(data: MeshData): THREE.BufferGeometry {
|
|
|
360
437
|
uv: uv ? (corner) => uv[corner]! : null,
|
|
361
438
|
uvLayers: maps.map(layer => ({name: layer.name, read: (corner: number) =>
|
|
362
439
|
(layer.data as [number, number][])[corner]!})),
|
|
440
|
+
// The JSON door nests each element's components; the reader wants them flat.
|
|
441
|
+
attributeLayers: graphAttributeLayers(data.attributes ?? [],
|
|
442
|
+
values => (Array.from(values) as (number | number[])[]).flat()),
|
|
363
443
|
};
|
|
364
444
|
return geometryFromDrawArrays(drawCore(view));
|
|
365
445
|
}
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
* faked with a substitute light the scene never declared.
|
|
36
36
|
*/
|
|
37
37
|
import * as THREE from 'three';
|
|
38
|
+
import {presenterChanged} from './blender-presenter-change';
|
|
38
39
|
import { z } from 'zod';
|
|
39
40
|
import type { GradientType } from './blender-gradient-texture';
|
|
40
41
|
import { hasSkyTexture, primeSkyTexture, type SkyParameters } from './blender-sky';
|
|
@@ -76,6 +77,7 @@ function prepareRectArea(): Promise<void> {
|
|
|
76
77
|
rectAreaPromise ??= import('three/examples/jsm/lights/RectAreaLightUniformsLib.js').then(
|
|
77
78
|
({ RectAreaLightUniformsLib }) => {
|
|
78
79
|
RectAreaLightUniformsLib.init();
|
|
80
|
+
presenterChanged();
|
|
79
81
|
},
|
|
80
82
|
);
|
|
81
83
|
return rectAreaPromise;
|
|
@@ -765,6 +767,7 @@ export class WorldBackground {
|
|
|
765
767
|
// composing now would paint a sky nobody asked for over the current one.
|
|
766
768
|
if (this.generation !== generation || this.applied?.scene !== scene) return;
|
|
767
769
|
this.compose(scene, world, expression, camera);
|
|
770
|
+
presenterChanged();
|
|
768
771
|
})();
|
|
769
772
|
this.pending = pending;
|
|
770
773
|
pendingWorld.add(pending);
|
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import * as THREE from 'three';
|
|
6
6
|
import { z } from 'zod';
|
|
7
|
+
import { onPresenterChange, presenterChanged } from './blender-presenter-change';
|
|
7
8
|
import { bytesFromBase64 } from './blender-base64';
|
|
8
9
|
import { ArmatureOverlay, armatureSchema } from './blender-runtime-armature';
|
|
9
10
|
import { UNKNOWN_GEOMETRY, UNKNOWN_IMAGE } from './blender-runtime-frame';
|
|
10
11
|
import {
|
|
11
12
|
drawArraysFromColumns,
|
|
13
|
+
graphAttributeName,
|
|
12
14
|
drawRuntimeGeometry,
|
|
13
15
|
geometryFromDrawArrays,
|
|
14
16
|
} from './blender-runtime-geometry';
|
|
@@ -25,6 +27,8 @@ import {
|
|
|
25
27
|
import { fitModelDirectionalShadow, visibleShadowReceivers } from './blender-runtime-shadows';
|
|
26
28
|
import { volumeMesh, volumeSchema } from './blender-runtime-volume';
|
|
27
29
|
import { applyPhysicalMaterial, applyWorldExtinction, physicalMaterialSchema } from './blender-physical-material';
|
|
30
|
+
import { prepareGraphGeometry, setMaterialGraph } from './blender-graph-material';
|
|
31
|
+
import { type CompiledGraph, compileMaterialGraph, graphSeeThrough, materialGraphSchema } from './blender-node-graph';
|
|
28
32
|
import {worldMedium, WorldVolumePass} from './blender-world-volume';
|
|
29
33
|
import { BlenderTextureSamplers } from './blender-texture-samplers';
|
|
30
34
|
import { WeightOverlay, weightsSchema } from './blender-runtime-weights';
|
|
@@ -107,6 +111,8 @@ const drawArraysSchema = z
|
|
|
107
111
|
normals: z.instanceof(Float32Array).nullable(),
|
|
108
112
|
uv: z.instanceof(Float32Array).nullable(),
|
|
109
113
|
uvLayers: z.array(z.object({name: z.string(), data: z.instanceof(Float32Array)}).strict()).optional(),
|
|
114
|
+
attributeLayers: z.array(z.object({name: z.string(), data: z.instanceof(Float32Array)}).strict()).optional(),
|
|
115
|
+
orco: z.instanceof(Float32Array).nullable().optional(),
|
|
110
116
|
indices: z.instanceof(Uint32Array),
|
|
111
117
|
groups: z.array(
|
|
112
118
|
z
|
|
@@ -163,6 +169,7 @@ const columnsSchema = z
|
|
|
163
169
|
.object({
|
|
164
170
|
co: z.instanceof(Float64Array),
|
|
165
171
|
cornerNormal: z.instanceof(Float32Array).optional(),
|
|
172
|
+
orco: z.instanceof(Float32Array).optional(),
|
|
166
173
|
faceStart: z.instanceof(Uint32Array),
|
|
167
174
|
corner: z.instanceof(Uint32Array),
|
|
168
175
|
cornerEdge: z.instanceof(Int32Array),
|
|
@@ -256,6 +263,7 @@ function loadPngTexture(png: Uint8Array): { texture: THREE.Texture; ready: Promi
|
|
|
256
263
|
}
|
|
257
264
|
texture.image = bitmap;
|
|
258
265
|
texture.needsUpdate = true;
|
|
266
|
+
presenterChanged();
|
|
259
267
|
})
|
|
260
268
|
.finally(() => {
|
|
261
269
|
pendingTextures.delete(decoding);
|
|
@@ -294,6 +302,47 @@ function rasterTexture(
|
|
|
294
302
|
return texture;
|
|
295
303
|
}
|
|
296
304
|
|
|
305
|
+
/**
|
|
306
|
+
* A UDIM IMAGE AS A GRAPH SAMPLES IT: every tile a layer of one array texture
|
|
307
|
+
* (each at its layer's origin, the layer as large as the largest tile), and
|
|
308
|
+
* the tile map `blender_tile_lookup` reads -- row 0 each tile's layer (-1 for
|
|
309
|
+
* none), row 1 its offset and scale within the layer, Blender's
|
|
310
|
+
* `GPU_image_tiled` layout.
|
|
311
|
+
*/
|
|
312
|
+
function udimTextures(
|
|
313
|
+
name: string,
|
|
314
|
+
frame: NonNullable<z.infer<typeof rasterImageSchema>['tiles']>,
|
|
315
|
+
colorspace: 'sRGB' | 'data',
|
|
316
|
+
): {tiles: THREE.DataArrayTexture; map: THREE.DataTexture; frame: typeof frame} {
|
|
317
|
+
if (frame.length === 0) throw new Error(`Runtime image ${name} is tiled with no tiles`);
|
|
318
|
+
const width = Math.max(...frame.map(t => t.width));
|
|
319
|
+
const height = Math.max(...frame.map(t => t.height));
|
|
320
|
+
const layers = new Uint8Array(width * height * 4 * frame.length);
|
|
321
|
+
const count = Math.max(...frame.map(t => t.number - 1001)) + 1;
|
|
322
|
+
const map = new Float32Array(count * 2 * 4);
|
|
323
|
+
for (let i = 0; i < count; i++) map[i * 4] = -1;
|
|
324
|
+
frame.forEach((tile, layer) => {
|
|
325
|
+
const rgba = tile.rgba ?? (tile.rgbaBase64 ? bytesFromBase64(tile.rgbaBase64) : undefined);
|
|
326
|
+
if (!rgba) throw new Error(`Runtime image ${name} tile ${tile.number} carries no raster`);
|
|
327
|
+
for (let row = 0; row < tile.height; row++)
|
|
328
|
+
layers.set(rgba.subarray(row * tile.width * 4, (row + 1) * tile.width * 4),
|
|
329
|
+
((layer * height + row) * width) * 4);
|
|
330
|
+
const index = tile.number - 1001;
|
|
331
|
+
map[index * 4] = layer;
|
|
332
|
+
map.set([0, 0, tile.width / width, tile.height / height], (count + index) * 4);
|
|
333
|
+
});
|
|
334
|
+
const tiles = new THREE.DataArrayTexture(layers, width, height, frame.length);
|
|
335
|
+
tiles.colorSpace = colorspace === 'data' ? THREE.NoColorSpace : THREE.SRGBColorSpace;
|
|
336
|
+
tiles.magFilter = THREE.LinearFilter;
|
|
337
|
+
tiles.minFilter = THREE.LinearMipmapLinearFilter;
|
|
338
|
+
tiles.generateMipmaps = true;
|
|
339
|
+
tiles.needsUpdate = true;
|
|
340
|
+
const mapTexture = new THREE.DataTexture(map, count, 2, THREE.RGBAFormat, THREE.FloatType);
|
|
341
|
+
mapTexture.magFilter = mapTexture.minFilter = THREE.NearestFilter;
|
|
342
|
+
mapTexture.needsUpdate = true;
|
|
343
|
+
return {tiles, map: mapTexture, frame};
|
|
344
|
+
}
|
|
345
|
+
|
|
297
346
|
/** One image the frame carries in full, in the only two shapes it comes in.
|
|
298
347
|
*
|
|
299
348
|
* A RASTER states its size, because a `DataTexture` cannot be built without
|
|
@@ -312,6 +361,14 @@ const rasterImageSchema = z
|
|
|
312
361
|
/** Blender's `colorspace_settings.name`, reduced to the two the sampler
|
|
313
362
|
* tells apart. Absent means sRGB. */
|
|
314
363
|
colorspace: z.enum(['sRGB', 'data']).optional(),
|
|
364
|
+
/** A UDIM image's tiles, by tile number (the door's `write_image`). */
|
|
365
|
+
tiles: z.array(z.object({
|
|
366
|
+
number: z.number().int().min(1001),
|
|
367
|
+
width: z.number().int().positive(),
|
|
368
|
+
height: z.number().int().positive(),
|
|
369
|
+
rgba: z.instanceof(Uint8Array).optional(),
|
|
370
|
+
rgbaBase64: z.string().optional(),
|
|
371
|
+
}).strict()).optional(),
|
|
315
372
|
})
|
|
316
373
|
.strict();
|
|
317
374
|
const pngImageSchema = z
|
|
@@ -360,6 +417,11 @@ const materialSchema = z
|
|
|
360
417
|
.object({ color: z.tuple([scalar, scalar, scalar]), strength: scalar })
|
|
361
418
|
.strict()
|
|
362
419
|
.optional(),
|
|
420
|
+
/** The node graph driving a Principled BSDF's or Emission's linked
|
|
421
|
+
* inputs (`session.py`'s `material_graph`), compiled by
|
|
422
|
+
* `blender-node-graph.ts`. The constants above still apply to every
|
|
423
|
+
* input the graph does not carry. */
|
|
424
|
+
graph: materialGraphSchema.optional(),
|
|
363
425
|
})
|
|
364
426
|
.strict();
|
|
365
427
|
/** THE FRAME CONTRACT, and the reason it is exported: the C++ export door
|
|
@@ -410,6 +472,11 @@ export const frameSchema = z
|
|
|
410
472
|
render_visible: z.boolean().default(true),
|
|
411
473
|
selected: z.boolean(),
|
|
412
474
|
parent: z.string().nullable(),
|
|
475
|
+
/** A manual texture space (`[location, size]`), which Generated
|
|
476
|
+
* coordinates map through; absent means Blender's automatic one,
|
|
477
|
+
* the evaluated bounds (`blender-graph-material.ts`'s orco). */
|
|
478
|
+
texspace: z.tuple([z.tuple([scalar, scalar, scalar]), z.tuple([scalar, scalar, scalar])]).optional(),
|
|
479
|
+
default_color: z.string().optional(),
|
|
413
480
|
})
|
|
414
481
|
.strict(),
|
|
415
482
|
),
|
|
@@ -505,6 +572,8 @@ export class BlenderRuntimeView {
|
|
|
505
572
|
revision: number;
|
|
506
573
|
png?: Uint8Array;
|
|
507
574
|
ready?: Promise<void>;
|
|
575
|
+
/** A UDIM image's tiles as a graph samples them (`udimTextures`). */
|
|
576
|
+
udim?: {tiles: THREE.DataArrayTexture; map: THREE.DataTexture; frame: NonNullable<z.infer<typeof rasterImageSchema>['tiles']>};
|
|
508
577
|
}
|
|
509
578
|
>();
|
|
510
579
|
/** Which image each material's `map` is currently pointing at. */
|
|
@@ -512,6 +581,24 @@ export class BlenderRuntimeView {
|
|
|
512
581
|
/** Which runtime image each material's roughness map is, by material id. */
|
|
513
582
|
private readonly roughnessTextureNames = new Map<string, string>();
|
|
514
583
|
private readonly textureSamplers = new BlenderTextureSamplers();
|
|
584
|
+
/** Each material's graph image samplers, by sampler key. */
|
|
585
|
+
private readonly graphSamplers = new Map<string, Set<string>>();
|
|
586
|
+
/** A view built for one photograph: its graph programs compile before its
|
|
587
|
+
* one draw rather than off it (`setMaterialGraph`'s `immediate`). */
|
|
588
|
+
private photograph = false;
|
|
589
|
+
/** Compiled graphs by their JSON: a frame re-sends every material. */
|
|
590
|
+
private readonly graphs = new Map<string, CompiledGraph>();
|
|
591
|
+
|
|
592
|
+
private compiledGraph(graph: z.infer<typeof materialGraphSchema>): CompiledGraph {
|
|
593
|
+
const text = JSON.stringify(graph);
|
|
594
|
+
let compiled = this.graphs.get(text);
|
|
595
|
+
if (!compiled) {
|
|
596
|
+
compiled = compileMaterialGraph(graph);
|
|
597
|
+
if (this.graphs.size > 256) this.graphs.clear();
|
|
598
|
+
this.graphs.set(text, compiled);
|
|
599
|
+
}
|
|
600
|
+
return compiled;
|
|
601
|
+
}
|
|
515
602
|
private frame: Frame | null = null;
|
|
516
603
|
private readonly retiredSessions = new Set<string>();
|
|
517
604
|
private geometryBuilds = 0;
|
|
@@ -550,6 +637,18 @@ export class BlenderRuntimeView {
|
|
|
550
637
|
};
|
|
551
638
|
}
|
|
552
639
|
|
|
640
|
+
/** EVERY CHANGE TO WHAT THIS VIEW DRAWS: each applied frame, and each piece
|
|
641
|
+
* of work that finishes after one (`blender-presenter-change.ts`). A stage
|
|
642
|
+
* that subscribes may draw only when this fires. */
|
|
643
|
+
onChange(listener: () => void): () => void {
|
|
644
|
+
const stopFrames = this.subscribeFrames(listener);
|
|
645
|
+
const stopWork = onPresenterChange(listener);
|
|
646
|
+
return () => {
|
|
647
|
+
stopFrames();
|
|
648
|
+
stopWork();
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
553
652
|
/** The Blender datablock NAME of a presented object, or null when this
|
|
554
653
|
* object is not one of the engine's (an editor helper, a light three.js
|
|
555
654
|
* parents under one). It is a lookup by IDENTITY, never by `object.name`:
|
|
@@ -987,6 +1086,12 @@ export class BlenderRuntimeView {
|
|
|
987
1086
|
if ('width' in data) {
|
|
988
1087
|
const rgba = data.rgba ?? (data.rgbaBase64 ? bytesFromBase64(data.rgbaBase64) : undefined);
|
|
989
1088
|
if (!rgba) throw new Error(`Runtime image ${name} carries no raster`);
|
|
1089
|
+
const udim = data.tiles ? udimTextures(name, data.tiles, data.colorspace ?? 'sRGB') : undefined;
|
|
1090
|
+
if (held?.udim) {
|
|
1091
|
+
held.udim.tiles.dispose();
|
|
1092
|
+
held.udim.map.dispose();
|
|
1093
|
+
delete held.udim;
|
|
1094
|
+
}
|
|
990
1095
|
// A REPAINT AT THE SAME SIZE IS AN UPLOAD, not a new texture: the
|
|
991
1096
|
// bytes go into the resident image and three re-uploads it, so every
|
|
992
1097
|
// material pointing at it keeps pointing at it. Native does exactly
|
|
@@ -997,6 +1102,7 @@ export class BlenderRuntimeView {
|
|
|
997
1102
|
image.data.set(rgba);
|
|
998
1103
|
held.texture.needsUpdate = true;
|
|
999
1104
|
held.revision = data.revision;
|
|
1105
|
+
if (udim) held.udim = udim;
|
|
1000
1106
|
continue;
|
|
1001
1107
|
}
|
|
1002
1108
|
}
|
|
@@ -1006,6 +1112,7 @@ export class BlenderRuntimeView {
|
|
|
1006
1112
|
width: data.width,
|
|
1007
1113
|
height: data.height,
|
|
1008
1114
|
revision: data.revision,
|
|
1115
|
+
...(udim ? {udim} : {}),
|
|
1009
1116
|
});
|
|
1010
1117
|
continue;
|
|
1011
1118
|
}
|
|
@@ -1016,6 +1123,8 @@ export class BlenderRuntimeView {
|
|
|
1016
1123
|
const png = data.png ?? (data.pngBase64 ? bytesFromBase64(data.pngBase64) : undefined);
|
|
1017
1124
|
if (!png) throw new Error(`Runtime image ${name} carries no bytes`);
|
|
1018
1125
|
held?.texture.dispose();
|
|
1126
|
+
held?.udim?.tiles.dispose();
|
|
1127
|
+
held?.udim?.map.dispose();
|
|
1019
1128
|
const { texture, ready } = loadPngTexture(png);
|
|
1020
1129
|
texture.colorSpace = THREE.SRGBColorSpace;
|
|
1021
1130
|
this.textures.set(name, {
|
|
@@ -1063,7 +1172,10 @@ export class BlenderRuntimeView {
|
|
|
1063
1172
|
// carries no alpha on a texture, and Blender's Alpha is a separate
|
|
1064
1173
|
// Principled socket.
|
|
1065
1174
|
material.opacity = data.color[3];
|
|
1066
|
-
|
|
1175
|
+
// A graph-driven Alpha, or a Transparent BSDF in a shader mix, is per
|
|
1176
|
+
// pixel; the constant cannot say whether the surface is see-through, so
|
|
1177
|
+
// such a graph is.
|
|
1178
|
+
const transparent = material.opacity < 1 || (data.graph !== undefined && graphSeeThrough(data.graph));
|
|
1067
1179
|
if (material.transparent !== transparent) {
|
|
1068
1180
|
material.transparent = transparent;
|
|
1069
1181
|
material.needsUpdate = true;
|
|
@@ -1172,6 +1284,37 @@ export class BlenderRuntimeView {
|
|
|
1172
1284
|
material.needsUpdate = true;
|
|
1173
1285
|
}
|
|
1174
1286
|
}
|
|
1287
|
+
// THE NODE GRAPH, when the session shipped one: compiled once per
|
|
1288
|
+
// distinct graph, its images through the same cache and per-input
|
|
1289
|
+
// sampler ownership as the maps above.
|
|
1290
|
+
const graph = data.graph ? this.compiledGraph(data.graph) : null;
|
|
1291
|
+
const graphSamplers = new Set<string>();
|
|
1292
|
+
setMaterialGraph(material, graph, (image) => {
|
|
1293
|
+
const held = this.textures.get(image.name);
|
|
1294
|
+
if (!held)
|
|
1295
|
+
throw new Error(
|
|
1296
|
+
`${UNKNOWN_IMAGE}: the presenter does not hold runtime image ${image.name} ` +
|
|
1297
|
+
`(revision ${image.revision})`,
|
|
1298
|
+
);
|
|
1299
|
+
if (image.tiled) {
|
|
1300
|
+
if (!held.udim) throw new Error(`${UNKNOWN_IMAGE}: runtime image ${image.name} arrived without its UDIM tiles`);
|
|
1301
|
+
return held.udim;
|
|
1302
|
+
}
|
|
1303
|
+
const key = `${id}:graph:${image.node}`;
|
|
1304
|
+
graphSamplers.add(key);
|
|
1305
|
+
const texture = this.textureSamplers.get(key, held.texture, image.extension, held.ready);
|
|
1306
|
+
const filter = image.closest ? THREE.NearestFilter : THREE.LinearFilter;
|
|
1307
|
+
const minFilter = image.closest ? THREE.NearestFilter
|
|
1308
|
+
: image.mipmap ? THREE.LinearMipmapLinearFilter : THREE.LinearFilter;
|
|
1309
|
+
if (texture.magFilter !== filter || texture.minFilter !== minFilter) {
|
|
1310
|
+
texture.magFilter = filter;
|
|
1311
|
+
texture.minFilter = minFilter;
|
|
1312
|
+
texture.needsUpdate = true;
|
|
1313
|
+
}
|
|
1314
|
+
return texture;
|
|
1315
|
+
}, this.photograph);
|
|
1316
|
+
for (const key of this.graphSamplers.get(id) ?? []) if (!graphSamplers.has(key)) this.textureSamplers.delete(key);
|
|
1317
|
+
this.graphSamplers.set(id, graphSamplers);
|
|
1175
1318
|
this.materials.set(id, material);
|
|
1176
1319
|
}
|
|
1177
1320
|
for (const obj of next.objects) {
|
|
@@ -1201,12 +1344,17 @@ export class BlenderRuntimeView {
|
|
|
1201
1344
|
object.matrix.premultiply(parent.invert());
|
|
1202
1345
|
}
|
|
1203
1346
|
object.matrix.decompose(object.position, object.quaternion, object.scale);
|
|
1347
|
+
if (obj.texspace) object.userData['blenderTexspace'] = obj.texspace;
|
|
1348
|
+
else delete object.userData['blenderTexspace'];
|
|
1349
|
+
if (obj.default_color !== undefined) object.userData['blenderDefaultColor'] = obj.default_color;
|
|
1350
|
+
else delete object.userData['blenderDefaultColor'];
|
|
1204
1351
|
if (obj.mesh !== null) {
|
|
1205
1352
|
const mesh = object as THREE.Mesh;
|
|
1206
1353
|
mesh.geometry = this.meshes.get(obj.mesh)!.geometry;
|
|
1207
1354
|
mesh.material = obj.materials.length
|
|
1208
1355
|
? obj.materials.map((id) => (id === null ? this.fallback : this.materials.get(id)!))
|
|
1209
1356
|
: this.fallback;
|
|
1357
|
+
prepareGraphGeometry(mesh);
|
|
1210
1358
|
}
|
|
1211
1359
|
if (obj.light) {
|
|
1212
1360
|
// The light hangs on its object's own node, so the object's matrix
|
|
@@ -1247,6 +1395,8 @@ export class BlenderRuntimeView {
|
|
|
1247
1395
|
for (const [id, material] of this.materials)
|
|
1248
1396
|
if (!next.materials[id]) {
|
|
1249
1397
|
for (const slot of ['normal', 'roughness', 'color']) this.textureSamplers.delete(`${id}:${slot}`);
|
|
1398
|
+
for (const key of this.graphSamplers.get(id) ?? []) this.textureSamplers.delete(key);
|
|
1399
|
+
this.graphSamplers.delete(id);
|
|
1250
1400
|
material.dispose();
|
|
1251
1401
|
this.materials.delete(id);
|
|
1252
1402
|
}
|
|
@@ -1333,6 +1483,12 @@ export class BlenderRuntimeView {
|
|
|
1333
1483
|
if (!data) throw new Error(`Blender capture mesh ${id} is missing UV layer ${name}`);
|
|
1334
1484
|
return {name, data};
|
|
1335
1485
|
}),
|
|
1486
|
+
orco: geometry.userData['blenderOrcoFromDoor'] ? attribute('blenderOrco') : null,
|
|
1487
|
+
attributeLayers: (geometry.userData['blenderAttributes'] as string[] | undefined ?? []).map(name => {
|
|
1488
|
+
const data = attribute(graphAttributeName(name));
|
|
1489
|
+
if (!data) throw new Error(`Blender capture mesh ${id} is missing attribute ${name}`);
|
|
1490
|
+
return {name, data};
|
|
1491
|
+
}),
|
|
1336
1492
|
indices: index
|
|
1337
1493
|
? new Uint32Array(index.array)
|
|
1338
1494
|
: Uint32Array.from({ length: positions.length / 3 }, (_, i) => i),
|
|
@@ -1347,9 +1503,15 @@ export class BlenderRuntimeView {
|
|
|
1347
1503
|
frame.volumes[id] = volumeSchema.parse(JSON.parse(signature));
|
|
1348
1504
|
const images = new Set(
|
|
1349
1505
|
Object.values(frame.materials).flatMap((material) =>
|
|
1350
|
-
[
|
|
1351
|
-
|
|
1352
|
-
|
|
1506
|
+
[
|
|
1507
|
+
material.texture?.image.name,
|
|
1508
|
+
material.roughness_texture?.image.name,
|
|
1509
|
+
material.normal_texture?.image.name,
|
|
1510
|
+
// The images a material's node graph samples.
|
|
1511
|
+
...Object.values(material.graph?.nodes ?? {}).map(
|
|
1512
|
+
(node) => (node.props['image'] as { name?: string } | null | undefined)?.name,
|
|
1513
|
+
),
|
|
1514
|
+
].filter((name): name is string => name !== undefined),
|
|
1353
1515
|
),
|
|
1354
1516
|
);
|
|
1355
1517
|
for (const name of images) {
|
|
@@ -1367,11 +1529,13 @@ export class BlenderRuntimeView {
|
|
|
1367
1529
|
height: held.height,
|
|
1368
1530
|
rgba: data.slice(),
|
|
1369
1531
|
colorspace: held.texture.colorSpace === THREE.NoColorSpace ? 'data' : 'sRGB',
|
|
1532
|
+
...(held.udim ? {tiles: held.udim.frame} : {}),
|
|
1370
1533
|
};
|
|
1371
1534
|
}
|
|
1372
1535
|
}
|
|
1373
1536
|
const effect = this.createWorldVolumePass();
|
|
1374
1537
|
const detached = new BlenderRuntimeView();
|
|
1538
|
+
detached.photograph = true;
|
|
1375
1539
|
let disposed = false;
|
|
1376
1540
|
const dispose = () => {
|
|
1377
1541
|
if (disposed) return;
|
|
@@ -1461,7 +1625,11 @@ export class BlenderRuntimeView {
|
|
|
1461
1625
|
// The pictures go with the session that sent them: the cache OWNS every
|
|
1462
1626
|
// texture in it (a material only points at one), so this is the one place
|
|
1463
1627
|
// they are disposed, beside the meshes for the same reason.
|
|
1464
|
-
for (const { texture } of this.textures.values())
|
|
1628
|
+
for (const { texture, udim } of this.textures.values()) {
|
|
1629
|
+
texture.dispose();
|
|
1630
|
+
udim?.tiles.dispose();
|
|
1631
|
+
udim?.map.dispose();
|
|
1632
|
+
}
|
|
1465
1633
|
this.textures.clear();
|
|
1466
1634
|
this.textureNames.clear();
|
|
1467
1635
|
this.roughnessTextureNames.clear();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
|
+
import {presenterChanged} from './blender-presenter-change';
|
|
2
3
|
|
|
3
4
|
export type TextureExtension = 'REPEAT' | 'EXTEND' | 'MIRROR' | 'CLIP';
|
|
4
5
|
|
|
@@ -48,6 +49,7 @@ export class BlenderTextureSamplers {
|
|
|
48
49
|
if (!held.disposed) {
|
|
49
50
|
held.texture.needsUpdate = true;
|
|
50
51
|
held.version = source.version;
|
|
52
|
+
presenterChanged();
|
|
51
53
|
}
|
|
52
54
|
}, () => {});
|
|
53
55
|
}
|