@volter/blender-engine 0.1.82 → 0.1.84
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/browser/three/blender-draw-materials.ts +55 -0
- package/browser/three/blender-graph-material.ts +17 -0
- package/browser/three/blender-ordered-batches.ts +5 -0
- package/browser/three/blender-physical-material.ts +22 -1
- package/browser/three/blender-transparent-instances.ts +20 -1
- package/package.json +1 -1
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import {syncPhysicalDrawMaterial} from './blender-physical-material';
|
|
3
|
+
|
|
4
|
+
type Variant = {material: THREE.MeshPhysicalMaterial; sourceVersion: number; frame: number; ready: boolean};
|
|
5
|
+
const MAX_VARIANTS = 128;
|
|
6
|
+
|
|
7
|
+
/** Three caches one current shader form per Material. Alternating ordinary,
|
|
8
|
+
* instanced and batched objects under that material repeatedly rebuilds its
|
|
9
|
+
* program parameters/uniform list, even when all programs are already linked.
|
|
10
|
+
* These small, disposable material identities isolate those caches without
|
|
11
|
+
* changing canonical material slots, geometry, transparency order or values. */
|
|
12
|
+
export class BlenderDrawMaterials {
|
|
13
|
+
private readonly sources = new Map<THREE.Material, Map<string, Variant>>();
|
|
14
|
+
private readonly geometryKeys = new Map<THREE.BufferGeometry, string>();
|
|
15
|
+
private frame = 0;
|
|
16
|
+
private count = 0;
|
|
17
|
+
|
|
18
|
+
begin(): void { this.frame++; this.geometryKeys.clear(); }
|
|
19
|
+
|
|
20
|
+
get(source: THREE.Material, mesh: THREE.InstancedMesh | THREE.BatchedMesh): THREE.Material {
|
|
21
|
+
let geometryKey = this.geometryKeys.get(mesh.geometry);
|
|
22
|
+
if (geometryKey === undefined) {
|
|
23
|
+
geometryKey = JSON.stringify([mesh.geometry.userData['blenderUvChannels'] ?? null,
|
|
24
|
+
Object.keys(mesh.geometry.attributes).sort().map(name => [name, mesh.geometry.getAttribute(name).itemSize])]);
|
|
25
|
+
this.geometryKeys.set(mesh.geometry, geometryKey);
|
|
26
|
+
}
|
|
27
|
+
const key = `${mesh instanceof THREE.BatchedMesh ? 'batch' : 'instance'}:${geometryKey}`;
|
|
28
|
+
let variants = this.sources.get(source);
|
|
29
|
+
if (!variants) { variants = new Map(); this.sources.set(source, variants); }
|
|
30
|
+
let variant = variants.get(key);
|
|
31
|
+
if (!variant) {
|
|
32
|
+
if (this.count >= MAX_VARIANTS) return source;
|
|
33
|
+
const material = syncPhysicalDrawMaterial(source);
|
|
34
|
+
if (!material) return source;
|
|
35
|
+
variant = {material, sourceVersion: source.version, frame: this.frame, ready: true};
|
|
36
|
+
variants.set(key, variant);
|
|
37
|
+
this.count++;
|
|
38
|
+
} else if (variant.frame !== this.frame) {
|
|
39
|
+
variant.ready = syncPhysicalDrawMaterial(source, variant.material) !== null;
|
|
40
|
+
if (variant.sourceVersion !== source.version) variant.material.needsUpdate = true;
|
|
41
|
+
variant.sourceVersion = source.version;
|
|
42
|
+
variant.frame = this.frame;
|
|
43
|
+
}
|
|
44
|
+
return variant.ready ? variant.material : source;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
clear(): void {
|
|
48
|
+
for (const variants of this.sources.values()) for (const {material} of variants.values()) material.dispose();
|
|
49
|
+
this.sources.clear();
|
|
50
|
+
this.geometryKeys.clear();
|
|
51
|
+
this.count = 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
inspect() { return {variants: this.count, limit: MAX_VARIANTS}; }
|
|
55
|
+
}
|
|
@@ -105,6 +105,23 @@ export function setMaterialGraph(
|
|
|
105
105
|
pendings.set(material, {binding, started: false});
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/** A disposable draw material borrows the source's live uniform values, never
|
|
109
|
+
* its owned ramp textures or geometry-dependent UV selector. Pending graphs
|
|
110
|
+
* must first draw through their source so its asynchronous compile can finish. */
|
|
111
|
+
export function borrowGraphDrawBinding(source: THREE.MeshPhysicalMaterial, target: THREE.MeshPhysicalMaterial): boolean {
|
|
112
|
+
if (pendings.has(source)) return false;
|
|
113
|
+
const sourceBinding = bindings.get(source);
|
|
114
|
+
const binding = sourceBinding && !failed.has(sourceBinding.compiled.key) ? sourceBinding : undefined;
|
|
115
|
+
const held = bindings.get(target);
|
|
116
|
+
if (!binding) {
|
|
117
|
+
if (held) { bindings.delete(target); target.needsUpdate = true; }
|
|
118
|
+
} else if (!held || held.compiled.key !== binding.compiled.key || held.uniforms !== binding.uniforms) {
|
|
119
|
+
bindings.set(target, {compiled: binding.compiled, uniforms: binding.uniforms, ramps: [], channels: {}});
|
|
120
|
+
target.needsUpdate = true;
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
108
125
|
/** A binding's values: uniforms, ramp tables and textures. */
|
|
109
126
|
function fill(binding: Binding, compiled: CompiledGraph, texture: (image: CompiledGraph['images'][number]) => ImageTexture): void {
|
|
110
127
|
binding.compiled = compiled;
|
|
@@ -120,6 +120,11 @@ export class BlenderOrderedBatches {
|
|
|
120
120
|
draw = new THREE.BatchedMesh(1, 0, 0, family.material);
|
|
121
121
|
draw.dispose();
|
|
122
122
|
draw.copy(template);
|
|
123
|
+
// Three r180's program invalidation reads colorTexture, whereas its
|
|
124
|
+
// program parameters read _colorsTexture. These owned draws never use
|
|
125
|
+
// per-instance colors: expose that same null state to both checks so
|
|
126
|
+
// every draw does not rebuild the already-correct program parameters.
|
|
127
|
+
Object.defineProperty(draw, 'colorTexture', {value: null});
|
|
123
128
|
for (const name of Object.keys(template.geometry.attributes))
|
|
124
129
|
draw.geometry.setAttribute(name, template.geometry.getAttribute(name));
|
|
125
130
|
draw.geometry.setIndex(template.geometry.index);
|
|
@@ -4,7 +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, graphUvChannels, setGraphViewport} from './blender-graph-material';
|
|
7
|
+
import {applyGraphShader, bindGraphDraw, borrowGraphDrawBinding, graphProgramKey, graphUvChannels, setGraphViewport} from './blender-graph-material';
|
|
8
8
|
import {bindNamedUvChannels} from './blender-texture-samplers';
|
|
9
9
|
|
|
10
10
|
const scalar = z.number().finite();
|
|
@@ -56,6 +56,27 @@ function graphShadow(material: THREE.MeshPhysicalMaterial): THREE.MeshPhysicalMa
|
|
|
56
56
|
return shadow;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/** Copy physical values into an independently cached draw form. Textures and
|
|
60
|
+
* graph uniform values remain borrowed from the authoritative source material. */
|
|
61
|
+
export function syncPhysicalDrawMaterial(source: THREE.Material, target?: THREE.MeshPhysicalMaterial): THREE.MeshPhysicalMaterial | null {
|
|
62
|
+
if (!(source instanceof THREE.MeshPhysicalMaterial) || !uniforms.has(source)) return null;
|
|
63
|
+
const copy = target ?? graphShadow(source);
|
|
64
|
+
if (!borrowGraphDrawBinding(source, copy)) {
|
|
65
|
+
if (!target) copy.dispose();
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
copy.copy(source);
|
|
69
|
+
const held = uniforms.get(source)!;
|
|
70
|
+
const values = uniforms.get(copy)!;
|
|
71
|
+
values.blenderCoatIor.value = held.blenderCoatIor.value;
|
|
72
|
+
values.blenderCoatTint.value.copy(held.blenderCoatTint.value);
|
|
73
|
+
values.blenderMapClip.value = held.blenderMapClip.value;
|
|
74
|
+
values.blenderRoughnessClip.value = held.blenderRoughnessClip.value;
|
|
75
|
+
values.blenderNormalClip.value = held.blenderNormalClip.value;
|
|
76
|
+
values.blenderWorldExtinction.value.copy(held.blenderWorldExtinction.value);
|
|
77
|
+
return copy;
|
|
78
|
+
}
|
|
79
|
+
|
|
59
80
|
export function applyPhysicalMaterial(material: THREE.MeshPhysicalMaterial, input?: Physical,
|
|
60
81
|
clips: {map?: boolean; roughness?: boolean; normal?: boolean} = {}): void {
|
|
61
82
|
const data = input ?? defaults;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
2
|
import {BlenderOrderedBatches, ORDERED_BATCH_CAPACITY} from './blender-ordered-batches';
|
|
3
|
+
import {BlenderDrawMaterials} from './blender-draw-materials';
|
|
3
4
|
import {presenterChanged} from './blender-presenter-change';
|
|
4
5
|
import {graphDrawAttributes} from './blender-graph-material';
|
|
5
6
|
import {instanceMatrixSupported, instanceObjectShown} from './blender-runtime-instances';
|
|
@@ -21,6 +22,7 @@ function groupOrder(mesh: THREE.Object3D): number {
|
|
|
21
22
|
* This does not turn blended foliage into cutouts or change its opacity. */
|
|
22
23
|
export class BlenderTransparentInstances {
|
|
23
24
|
private objects: THREE.Mesh[] = [];
|
|
25
|
+
private readonly drawMaterials = new BlenderDrawMaterials();
|
|
24
26
|
private readonly ordered = new BlenderOrderedBatches();
|
|
25
27
|
private readonly mixedDraws = new Map<THREE.BatchedMesh, Draw>();
|
|
26
28
|
private multiDraw = false;
|
|
@@ -35,6 +37,8 @@ export class BlenderTransparentInstances {
|
|
|
35
37
|
private batches = 0;
|
|
36
38
|
private instances = 0;
|
|
37
39
|
private excluded: Record<string, number> = {};
|
|
40
|
+
private readonly plannedProjection = new THREE.Matrix4();
|
|
41
|
+
private projectionKnown = false;
|
|
38
42
|
private readonly planState: (number | string)[] = [];
|
|
39
43
|
private readonly planned: Draw[] = [];
|
|
40
44
|
private stateIndex = 0;
|
|
@@ -49,6 +53,7 @@ export class BlenderTransparentInstances {
|
|
|
49
53
|
|
|
50
54
|
prepare(camera: THREE.Camera, multiDraw = false): void {
|
|
51
55
|
this.finishDraw();
|
|
56
|
+
this.drawMaterials.begin();
|
|
52
57
|
if (!this.objects.length) return;
|
|
53
58
|
camera.updateMatrixWorld();
|
|
54
59
|
this.projection.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
@@ -61,6 +66,8 @@ export class BlenderTransparentInstances {
|
|
|
61
66
|
// a camera move, drag, visibility or material change rebuilds it below.
|
|
62
67
|
for (const draw of this.planned) {
|
|
63
68
|
draw.mesh.visible = true;
|
|
69
|
+
const source = draw.members[0]!.material;
|
|
70
|
+
draw.mesh.material = this.drawMaterials.get(Array.isArray(source) ? source[0]! : source, draw.mesh);
|
|
64
71
|
for (const member of draw.members) {
|
|
65
72
|
member.layers.disable(0);
|
|
66
73
|
this.hidden.push(member);
|
|
@@ -209,6 +216,7 @@ export class BlenderTransparentInstances {
|
|
|
209
216
|
pool.push(draw);
|
|
210
217
|
this.root.add(mesh);
|
|
211
218
|
}
|
|
219
|
+
draw.mesh.material = this.drawMaterials.get(first.material!, draw.mesh);
|
|
212
220
|
draw.mesh.visible = true;
|
|
213
221
|
draw.mesh.renderOrder = head.mesh.renderOrder;
|
|
214
222
|
draw.mesh.boundingSphere!.center.copy(head.centre).applyMatrix4(this.inverse);
|
|
@@ -243,7 +251,7 @@ export class BlenderTransparentInstances {
|
|
|
243
251
|
if (this.ordered.pending) queueMicrotask(presenterChanged);
|
|
244
252
|
}
|
|
245
253
|
|
|
246
|
-
inspect() { return {batches: this.batches, instances: this.instances, excluded: {...this.excluded}, multiDraw: this.ordered.inspect()}; }
|
|
254
|
+
inspect() { return {batches: this.batches, instances: this.instances, excluded: {...this.excluded}, multiDraw: this.ordered.inspect(), materials: this.drawMaterials.inspect()}; }
|
|
247
255
|
|
|
248
256
|
private observe(value: number | string): void {
|
|
249
257
|
if (this.planState[this.stateIndex] !== value) {
|
|
@@ -256,6 +264,15 @@ export class BlenderTransparentInstances {
|
|
|
256
264
|
/** Compare inputs without cloning 10,000 centres, allocating entries or
|
|
257
265
|
* sorting/uploading matrices again on an unchanged frame. No lossy hash. */
|
|
258
266
|
private planChanged(camera: THREE.Camera): boolean {
|
|
267
|
+
// A moved camera already invalidates culling and ordering. Do not first
|
|
268
|
+
// compare every object's attributes/matrix only to rebuild the same list.
|
|
269
|
+
// Invalidate the saved inputs; the first quiet frame records them afresh.
|
|
270
|
+
if (!this.projectionKnown || !this.plannedProjection.equals(this.projection)) {
|
|
271
|
+
this.plannedProjection.copy(this.projection);
|
|
272
|
+
this.projectionKnown = true;
|
|
273
|
+
this.planState.length = 0;
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
259
276
|
this.stateIndex = 0;
|
|
260
277
|
this.stateChanged = this.planState.length === 0;
|
|
261
278
|
this.observe(camera.layers.mask);
|
|
@@ -333,9 +350,11 @@ export class BlenderTransparentInstances {
|
|
|
333
350
|
}
|
|
334
351
|
this.pools.clear();
|
|
335
352
|
this.ordered.clear();
|
|
353
|
+
this.drawMaterials.clear();
|
|
336
354
|
this.mixedDraws.clear();
|
|
337
355
|
this.planned.length = 0;
|
|
338
356
|
this.planState.length = 0;
|
|
357
|
+
this.projectionKnown = false;
|
|
339
358
|
this.objects = [];
|
|
340
359
|
this.batches = this.instances = 0;
|
|
341
360
|
}
|