@volter/blender-engine 0.1.6 → 0.1.8
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/blender-emscripten-engine.mts +57 -6
- package/browser/blender-engine.mts +2 -0
- package/browser/runtime.ts +10 -5
- package/browser/session.py +529 -17
- package/browser/three/blender-frame-columns.ts +4 -0
- package/browser/three/blender-graph-material.ts +406 -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 +42 -5
- 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/browser/worker.ts +3 -0
- package/package.json +1 -1
- package/wasm/BUNDLE.json +11 -31
- package/wasm/blender_browser.wasm.br +0 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A COMPILED MATERIAL GRAPH ON THE PRESENTER'S STANDARD MATERIAL.
|
|
3
|
+
*
|
|
4
|
+
* The graph (`blender-node-graph.ts`) drives the surface inputs it carries;
|
|
5
|
+
* everything else -- lighting, the physical inputs, the door's normal map --
|
|
6
|
+
* stays three's MeshPhysicalMaterial with the door's constants. The graph's
|
|
7
|
+
* program is spliced into that material's own shader at the chunk each input
|
|
8
|
+
* is read in, so an input the graph drives is read from the graph and nothing
|
|
9
|
+
* else about the material changes.
|
|
10
|
+
*
|
|
11
|
+
* Per draw, the geometry decides which attribute channel each named UV layer
|
|
12
|
+
* is (`blenderUvChannels`, as the door's textures read it) and carries the
|
|
13
|
+
* Generated coordinates as an `blenderOrco` attribute -- Blender's own orco,
|
|
14
|
+
* built from the mesh's texture space (`orcoAttribute`).
|
|
15
|
+
*/
|
|
16
|
+
import * as THREE from 'three';
|
|
17
|
+
import {presenterChanged} from './blender-presenter-change';
|
|
18
|
+
import {attributeVarying, type CompiledGraph, rampTexture, uvVarying} from './blender-node-graph';
|
|
19
|
+
import {graphAttributeName} from './blender-runtime-geometry';
|
|
20
|
+
|
|
21
|
+
interface Binding {
|
|
22
|
+
compiled: CompiledGraph;
|
|
23
|
+
uniforms: Record<string, THREE.IUniform>;
|
|
24
|
+
ramps: THREE.DataTexture[];
|
|
25
|
+
/** Named UV layer to the channel the current draw's geometry holds it in. */
|
|
26
|
+
channels: Record<string, number>;
|
|
27
|
+
}
|
|
28
|
+
const bindings = new WeakMap<THREE.MeshPhysicalMaterial, Binding>();
|
|
29
|
+
/** Graph structures whose program the GPU refused. A material whose graph is
|
|
30
|
+
* one of these is drawn from the door's constants instead -- a compiler
|
|
31
|
+
* defect must never make geometry disappear. */
|
|
32
|
+
const failed = new Set<string>();
|
|
33
|
+
|
|
34
|
+
export function materialGraph(material: THREE.MeshPhysicalMaterial): CompiledGraph | null {
|
|
35
|
+
return bindings.get(material)?.compiled ?? null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type ImageTexture = THREE.Texture | {tiles: THREE.Texture; map: THREE.Texture} | null;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A NEW PROGRAM COMPILES OFF THE DRAW, as EEVEE's do: until it is ready the
|
|
42
|
+
* material draws what it drew before (its previous graph, or the door's
|
|
43
|
+
* constants), and the swap happens on the draw after `compileAsync` reports
|
|
44
|
+
* the program linked (`bindGraphDraw`). A structural edit therefore never
|
|
45
|
+
* blocks a frame on the GPU's compiler. A PHOTOGRAPH (`immediate`) cannot
|
|
46
|
+
* wait for a later frame and switches at once.
|
|
47
|
+
*/
|
|
48
|
+
interface Pending {
|
|
49
|
+
binding: Binding;
|
|
50
|
+
started: boolean;
|
|
51
|
+
}
|
|
52
|
+
const pendings = new WeakMap<THREE.MeshPhysicalMaterial, Pending>();
|
|
53
|
+
/** The proxy material a swapped-in program was compiled through, disposed
|
|
54
|
+
* once the material itself holds that program (its second draw after). */
|
|
55
|
+
const retiring = new WeakMap<THREE.MeshPhysicalMaterial, {shadow: THREE.Material; draws: number}>();
|
|
56
|
+
|
|
57
|
+
function dropPending(material: THREE.MeshPhysicalMaterial): void {
|
|
58
|
+
const waiting = pendings.get(material);
|
|
59
|
+
if (!waiting) return;
|
|
60
|
+
pendings.delete(material);
|
|
61
|
+
for (const ramp of waiting.binding.ramps) ramp.dispose();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Point `material` at `compiled` (or at nothing). `texture` answers each
|
|
65
|
+
* image the graph samples; a new structure recompiles, new values do not. */
|
|
66
|
+
export function setMaterialGraph(
|
|
67
|
+
material: THREE.MeshPhysicalMaterial,
|
|
68
|
+
compiled: CompiledGraph | null,
|
|
69
|
+
texture: (image: CompiledGraph['images'][number]) => ImageTexture,
|
|
70
|
+
immediate = false,
|
|
71
|
+
): void {
|
|
72
|
+
const held = bindings.get(material);
|
|
73
|
+
if (compiled && failed.has(compiled.key)) compiled = null;
|
|
74
|
+
if (!compiled) {
|
|
75
|
+
dropPending(material);
|
|
76
|
+
if (held) {
|
|
77
|
+
for (const ramp of held.ramps) ramp.dispose();
|
|
78
|
+
bindings.delete(material);
|
|
79
|
+
material.needsUpdate = true;
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (held && held.compiled.key === compiled.key) {
|
|
84
|
+
dropPending(material);
|
|
85
|
+
fill(held, compiled, texture);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (immediate) {
|
|
89
|
+
dropPending(material);
|
|
90
|
+
for (const ramp of held?.ramps ?? []) ramp.dispose();
|
|
91
|
+
const binding: Binding = {compiled, uniforms: {}, ramps: [], channels: held?.channels ?? {}};
|
|
92
|
+
fill(binding, compiled, texture);
|
|
93
|
+
bindings.set(material, binding);
|
|
94
|
+
material.needsUpdate = true;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const waiting = pendings.get(material);
|
|
98
|
+
if (waiting && waiting.binding.compiled.key === compiled.key) {
|
|
99
|
+
fill(waiting.binding, compiled, texture);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
dropPending(material);
|
|
103
|
+
const binding: Binding = {compiled, uniforms: {}, ramps: [], channels: held?.channels ?? {}};
|
|
104
|
+
fill(binding, compiled, texture);
|
|
105
|
+
pendings.set(material, {binding, started: false});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** A binding's values: uniforms, ramp tables and textures. */
|
|
109
|
+
function fill(binding: Binding, compiled: CompiledGraph, texture: (image: CompiledGraph['images'][number]) => ImageTexture): void {
|
|
110
|
+
binding.compiled = compiled;
|
|
111
|
+
for (const [name, value] of compiled.uniforms) {
|
|
112
|
+
const next = typeof value === 'number' ? value
|
|
113
|
+
: value.length === 2 ? new THREE.Vector2(...(value as [number, number]))
|
|
114
|
+
: value.length === 3 ? new THREE.Vector3(...(value as [number, number, number]))
|
|
115
|
+
: new THREE.Vector4(...(value as [number, number, number, number]));
|
|
116
|
+
const uniform = binding.uniforms[name];
|
|
117
|
+
if (uniform) uniform.value = next;
|
|
118
|
+
else binding.uniforms[name] = {value: next};
|
|
119
|
+
}
|
|
120
|
+
for (const ramp of binding.ramps) ramp.dispose();
|
|
121
|
+
binding.ramps = compiled.ramps.map(r => {
|
|
122
|
+
const tex = rampTexture(r.table);
|
|
123
|
+
(binding.uniforms[r.uniform] ??= {value: null}).value = tex;
|
|
124
|
+
return tex;
|
|
125
|
+
});
|
|
126
|
+
for (const image of compiled.images) {
|
|
127
|
+
const value = texture(image);
|
|
128
|
+
if (image.tiled) {
|
|
129
|
+
const tiled = value !== null && 'tiles' in value ? value : null;
|
|
130
|
+
(binding.uniforms[image.uniform] ??= {value: null}).value = tiled?.tiles ?? null;
|
|
131
|
+
(binding.uniforms[`${image.uniform}Map`] ??= {value: null}).value = tiled?.map ?? null;
|
|
132
|
+
} else {
|
|
133
|
+
(binding.uniforms[image.uniform] ??= {value: null}).value = value !== null && 'tiles' in value ? null : value;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Part of the material's program cache key: the graph's structure and the
|
|
139
|
+
* UV channels the current geometry puts its layers in. */
|
|
140
|
+
export function graphProgramKey(material: THREE.MeshPhysicalMaterial): string {
|
|
141
|
+
const binding = bindings.get(material);
|
|
142
|
+
if (!binding) return '';
|
|
143
|
+
return `${binding.compiled.key}|${JSON.stringify(binding.channels)}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The UV channels the material's graph reads on the current draw. */
|
|
147
|
+
export function graphUvChannels(material: THREE.MeshPhysicalMaterial): number[] {
|
|
148
|
+
return Object.values(bindings.get(material)?.channels ?? {});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The channel each named UV layer is in on `geometry` (a layer the mesh
|
|
152
|
+
* lacks reads zeros from channel 9, Blender's missing attribute). */
|
|
153
|
+
function channelsFor(compiled: CompiledGraph, geometry: THREE.BufferGeometry): Record<string, number> {
|
|
154
|
+
const named = geometry.userData['blenderUvChannels'] as Record<string, number> | undefined;
|
|
155
|
+
const count = geometry.getAttribute('position')?.count ?? 0;
|
|
156
|
+
const channels: Record<string, number> = {};
|
|
157
|
+
for (const name of compiled.uvs) {
|
|
158
|
+
let channel = name === '' ? 0 : named?.[name];
|
|
159
|
+
if (channel === undefined) {
|
|
160
|
+
channel = 9;
|
|
161
|
+
if (geometry.getAttribute('uv9')?.count !== count)
|
|
162
|
+
geometry.setAttribute('uv9', new THREE.BufferAttribute(new Float32Array(count * 2), 2));
|
|
163
|
+
}
|
|
164
|
+
channels[name] = channel;
|
|
165
|
+
}
|
|
166
|
+
return channels;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Per draw: a pending program started compiling (see `Pending`), a compiled
|
|
170
|
+
* one swapped in, a refused one dropped, and the channel of each named layer
|
|
171
|
+
* on this geometry. `shadowOf` makes the proxy a program compiles through:
|
|
172
|
+
* the same material, hooks and all, bound to the pending graph. */
|
|
173
|
+
export function bindGraphDraw(material: THREE.MeshPhysicalMaterial, geometry: THREE.BufferGeometry,
|
|
174
|
+
renderer: THREE.WebGLRenderer, scene: THREE.Scene, camera: THREE.Camera, object: THREE.Object3D,
|
|
175
|
+
shadowOf: (material: THREE.MeshPhysicalMaterial) => THREE.MeshPhysicalMaterial): void {
|
|
176
|
+
const retired = retiring.get(material);
|
|
177
|
+
if (retired && ++retired.draws > 1) {
|
|
178
|
+
retired.shadow.dispose();
|
|
179
|
+
retiring.delete(material);
|
|
180
|
+
}
|
|
181
|
+
const waiting = pendings.get(material);
|
|
182
|
+
if (waiting && !waiting.started) {
|
|
183
|
+
waiting.started = true;
|
|
184
|
+
waiting.binding.channels = channelsFor(waiting.binding.compiled, geometry);
|
|
185
|
+
const shadow = shadowOf(material);
|
|
186
|
+
bindings.set(shadow, waiting.binding);
|
|
187
|
+
const proxy = new THREE.Mesh(geometry, shadow);
|
|
188
|
+
proxy.castShadow = object.castShadow;
|
|
189
|
+
proxy.receiveShadow = object.receiveShadow;
|
|
190
|
+
const swap = () => {
|
|
191
|
+
if (pendings.get(material) !== waiting) {
|
|
192
|
+
shadow.dispose();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
pendings.delete(material);
|
|
196
|
+
for (const ramp of bindings.get(material)?.ramps ?? []) ramp.dispose();
|
|
197
|
+
bindings.set(material, waiting.binding);
|
|
198
|
+
material.needsUpdate = true;
|
|
199
|
+
retiring.get(material)?.shadow.dispose();
|
|
200
|
+
retiring.set(material, {shadow, draws: 0});
|
|
201
|
+
presenterChanged();
|
|
202
|
+
};
|
|
203
|
+
// A program the GPU refuses is found on the material's own first draw
|
|
204
|
+
// with it (below), exactly as an immediate one is.
|
|
205
|
+
renderer.compileAsync(proxy, camera, scene).then(swap, swap);
|
|
206
|
+
}
|
|
207
|
+
const binding = bindings.get(material);
|
|
208
|
+
if (!binding) return;
|
|
209
|
+
// THE PROGRAM THE LAST DRAW USED, if the GPU refused it: fall back to the
|
|
210
|
+
// constants from the next draw on, and say so once. Three has already
|
|
211
|
+
// logged the compiler's own errors.
|
|
212
|
+
const program = (renderer.properties.get(material) as {currentProgram?: unknown}).currentProgram as
|
|
213
|
+
{cacheKey?: string; diagnostics?: {runnable: boolean; fragmentShader: {log: string}}} | undefined;
|
|
214
|
+
if (program?.diagnostics?.runnable === false && program.cacheKey?.includes(binding.compiled.key)) {
|
|
215
|
+
failed.add(binding.compiled.key);
|
|
216
|
+
bindings.delete(material);
|
|
217
|
+
material.needsUpdate = true;
|
|
218
|
+
const first = program.diagnostics.fragmentShader.log.split('\n').find(line => line.startsWith('ERROR')) ?? '';
|
|
219
|
+
console.warn(`Blender material "${material.name}": its node graph did not compile for WebGL, ` +
|
|
220
|
+
`so it is drawn with its constant values. ${first}`);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const channels = channelsFor(binding.compiled, geometry);
|
|
224
|
+
if (JSON.stringify(channels) !== JSON.stringify(binding.channels)) {
|
|
225
|
+
binding.channels = channels;
|
|
226
|
+
material.needsUpdate = true;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* THE ORCO A GRAPH-DRAWN MESH CARRIES, set when the view gives a mesh its
|
|
232
|
+
* materials -- never from a draw hook. Three uploads a geometry's attributes
|
|
233
|
+
* before any draw hook runs, so an attribute first added there has no GPU
|
|
234
|
+
* buffer on that draw and the shader reads zeros: invisible in a viewport
|
|
235
|
+
* that draws again next frame, and a constant pattern in a render, which
|
|
236
|
+
* draws once.
|
|
237
|
+
*/
|
|
238
|
+
export function prepareGraphGeometry(mesh: THREE.Mesh): void {
|
|
239
|
+
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
|
240
|
+
if (!materials.some(m => m instanceof THREE.MeshPhysicalMaterial && bindings.has(m))) return;
|
|
241
|
+
const geometry = mesh.geometry;
|
|
242
|
+
const texspace = (mesh.userData['blenderTexspace'] ?? null) as [number[], number[]] | null;
|
|
243
|
+
const position = geometry.getAttribute('position') as THREE.BufferAttribute | undefined;
|
|
244
|
+
const stamp = JSON.stringify([texspace, position?.version ?? 0, position?.count ?? 0]);
|
|
245
|
+
// A deformed mesh's orco came with its columns (`DrawArrays.orco`).
|
|
246
|
+
if (!geometry.userData['blenderOrcoFromDoor'] && geometry.userData['blenderOrcoStamp'] !== stamp) {
|
|
247
|
+
geometry.setAttribute('blenderOrco', orcoAttribute(geometry, texspace));
|
|
248
|
+
geometry.userData['blenderOrcoStamp'] = stamp;
|
|
249
|
+
}
|
|
250
|
+
// THE ATTRIBUTES THE GRAPHS READ. The draw carries every readable layer
|
|
251
|
+
// (`graphAttributeLayers`); '' is the mesh's default colour attribute
|
|
252
|
+
// (`GPU_attribute_default_color`), and a layer the mesh lacks reads zeros,
|
|
253
|
+
// as a missing attribute does in EEVEE.
|
|
254
|
+
const count = position?.count ?? 0;
|
|
255
|
+
const defaultColor = mesh.userData['blenderDefaultColor'] as string | undefined;
|
|
256
|
+
for (const material of materials) {
|
|
257
|
+
const binding = material instanceof THREE.MeshPhysicalMaterial ? bindings.get(material) : undefined;
|
|
258
|
+
for (const name of binding?.compiled.attributes ?? []) {
|
|
259
|
+
const target = graphAttributeName(name);
|
|
260
|
+
const source = name === '' ? (defaultColor === undefined ? undefined : geometry.getAttribute(graphAttributeName(defaultColor)))
|
|
261
|
+
: geometry.getAttribute(target);
|
|
262
|
+
if (source && source.count === count) {
|
|
263
|
+
if (geometry.getAttribute(target) !== source) geometry.setAttribute(target, source);
|
|
264
|
+
} else if (geometry.getAttribute(target)?.count !== count) {
|
|
265
|
+
geometry.setAttribute(target, new THREE.BufferAttribute(new Float32Array(count * 4), 4));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* GENERATED COORDINATES, as EEVEE's `attr_load_orco` reads them for a mesh
|
|
273
|
+
* without deformation: the position through the texture space,
|
|
274
|
+
* `(co - location) / size * 0.5 + 0.5`. `texspace` is the evaluated mesh's
|
|
275
|
+
* (`session.py` puts it on the object row); without one, the bounds are used,
|
|
276
|
+
* which is what Blender's automatic texture space is.
|
|
277
|
+
*/
|
|
278
|
+
export function orcoAttribute(
|
|
279
|
+
geometry: THREE.BufferGeometry,
|
|
280
|
+
texspace: readonly [readonly number[], readonly number[]] | null,
|
|
281
|
+
): THREE.BufferAttribute {
|
|
282
|
+
const position = geometry.getAttribute('position');
|
|
283
|
+
let location: number[];
|
|
284
|
+
let size: number[];
|
|
285
|
+
if (texspace) {
|
|
286
|
+
location = [...texspace[0]];
|
|
287
|
+
size = [...texspace[1]];
|
|
288
|
+
} else {
|
|
289
|
+
geometry.computeBoundingBox();
|
|
290
|
+
const box = geometry.boundingBox!;
|
|
291
|
+
location = [(box.min.x + box.max.x) / 2, (box.min.y + box.max.y) / 2, (box.min.z + box.max.z) / 2];
|
|
292
|
+
size = [(box.max.x - box.min.x) / 2, (box.max.y - box.min.y) / 2, (box.max.z - box.min.z) / 2];
|
|
293
|
+
}
|
|
294
|
+
// BKE_mesh_texspace_calc: a zero extent becomes one.
|
|
295
|
+
size = size.map(s => (s === 0 ? 1 : s));
|
|
296
|
+
const orco = new Float32Array(position.count * 3);
|
|
297
|
+
for (let i = 0; i < position.count; i++) {
|
|
298
|
+
orco[i * 3] = (position.getX(i) - location[0]!) / size[0]! * 0.5 + 0.5;
|
|
299
|
+
orco[i * 3 + 1] = (position.getY(i) - location[1]!) / size[1]! * 0.5 + 0.5;
|
|
300
|
+
orco[i * 3 + 2] = (position.getZ(i) - location[2]!) / size[2]! * 0.5 + 0.5;
|
|
301
|
+
}
|
|
302
|
+
return new THREE.BufferAttribute(orco, 3);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Splice the graph into three's physical shader. Called from the material's
|
|
306
|
+
* own `onBeforeCompile` after its physical adjustments. */
|
|
307
|
+
export function applyGraphShader(material: THREE.MeshPhysicalMaterial, shader: THREE.WebGLProgramParametersWithUniforms): void {
|
|
308
|
+
const binding = bindings.get(material);
|
|
309
|
+
if (!binding) return;
|
|
310
|
+
const {compiled, channels} = binding;
|
|
311
|
+
Object.assign(shader.uniforms, binding.uniforms, {blenderViewport: {value: viewport}});
|
|
312
|
+
const attribute = (channel: number) => (channel === 0 ? 'uv' : `uv${channel}`);
|
|
313
|
+
const varyings = `varying vec3 vBlenderObjectPosition;
|
|
314
|
+
varying vec3 vBlenderObjectNormal;
|
|
315
|
+
varying vec3 vBlenderWorldPosition;
|
|
316
|
+
varying vec3 vBlenderWorldNormal;
|
|
317
|
+
varying vec3 vBlenderOrco;
|
|
318
|
+
${compiled.attributes.map(n => `varying vec4 ${attributeVarying(n)};`).join('\n')}
|
|
319
|
+
${compiled.uvs.map(n => `varying vec2 ${uvVarying(n)};`).join('\n')}`;
|
|
320
|
+
const attributes = compiled.attributes.map(n => `attribute vec4 ${graphAttributeName(n)};`).join('\n');
|
|
321
|
+
shader.vertexShader = `attribute vec3 blenderOrco;\n${attributes}\n${varyings}\n${shader.vertexShader}`.replace(
|
|
322
|
+
'#include <project_vertex>',
|
|
323
|
+
`vBlenderObjectPosition = transformed;
|
|
324
|
+
vBlenderObjectNormal = objectNormal;
|
|
325
|
+
vBlenderWorldPosition = (modelMatrix * vec4(transformed, 1.0)).xyz;
|
|
326
|
+
vBlenderWorldNormal = normalize(transpose(inverse(mat3(modelMatrix))) * objectNormal);
|
|
327
|
+
vBlenderOrco = blenderOrco;
|
|
328
|
+
${compiled.attributes.map(n => `${attributeVarying(n)} = ${graphAttributeName(n)};`).join('\n')}
|
|
329
|
+
${compiled.uvs.map(n => `${uvVarying(n)} = ${attribute(channels[n] ?? 0)};`).join('\n')}
|
|
330
|
+
#include <project_vertex>`,
|
|
331
|
+
);
|
|
332
|
+
const out = (name: string) => compiled.outputs[name]?.global;
|
|
333
|
+
// [chunk, line, whether the line goes BEFORE the chunk]
|
|
334
|
+
const fragment: [string, string, boolean][] = [];
|
|
335
|
+
if (compiled.surface === 'ShaderNodeEmission') {
|
|
336
|
+
fragment.push(['#include <color_fragment>', 'diffuseColor = vec4(0.0, 0.0, 0.0, 1.0);', false]);
|
|
337
|
+
fragment.push(['#include <emissivemap_fragment>', `totalEmissiveRadiance = ${out('Color')}.rgb * ${out('Strength')};`, false]);
|
|
338
|
+
// An Emission surface reflects nothing: what leaves it is its emission.
|
|
339
|
+
fragment.push(['#include <opaque_fragment>', 'outgoingLight = totalEmissiveRadiance;', true]);
|
|
340
|
+
} else if (compiled.surface === 'closure') {
|
|
341
|
+
// A mix with no lit shader: nothing reflects, the composition below is all.
|
|
342
|
+
fragment.push(['#include <color_fragment>', 'diffuseColor = vec4(0.0, 0.0, 0.0, 1.0);', false]);
|
|
343
|
+
fragment.push(['#include <emissivemap_fragment>', 'totalEmissiveRadiance = vec3(0.0);', false]);
|
|
344
|
+
} else {
|
|
345
|
+
fragment.push(['#include <color_fragment>', `diffuseColor.rgb = ${out('Base Color')}.rgb;`, false]);
|
|
346
|
+
fragment.push(['#include <alphamap_fragment>', `diffuseColor.a = ${out('Alpha')};`, false]);
|
|
347
|
+
// The physical material rewrites roughnessmap_fragment in place; the
|
|
348
|
+
// chunk three reads next is metalnessmap_fragment.
|
|
349
|
+
fragment.push(['#include <metalnessmap_fragment>', `roughnessFactor = ${out('Roughness')};`, true]);
|
|
350
|
+
fragment.push(['#include <metalnessmap_fragment>', `metalnessFactor = ${out('Metallic')};`, false]);
|
|
351
|
+
fragment.push(['#include <emissivemap_fragment>',
|
|
352
|
+
`totalEmissiveRadiance = ${out('Emission Color')}.rgb * ${out('Emission Strength')};`, false]);
|
|
353
|
+
// A linked Normal replaces three's shading normal (view space), after its
|
|
354
|
+
// own normal map and before the clearcoat normal is derived from it.
|
|
355
|
+
if (out('Normal'))
|
|
356
|
+
fragment.push(['#include <clearcoat_normal_fragment_begin>',
|
|
357
|
+
`normal = normalize((viewMatrix * vec4(transpose(blender_from_three) * ${out('Normal')}, 0.0)).xyz);`, true]);
|
|
358
|
+
}
|
|
359
|
+
if (compiled.closure) {
|
|
360
|
+
// THE SHADER MIX'S COMPOSITION (`bgClosure*`), before three writes the
|
|
361
|
+
// pixel: radiance is the lit shader's (its own alpha already a mix with
|
|
362
|
+
// transparency) plus emission; transmittance is the transparent weight
|
|
363
|
+
// plus what the lit shader's alpha lets through. Three blends straight
|
|
364
|
+
// alpha, so the radiance goes out divided by the coverage.
|
|
365
|
+
fragment.push(['#include <opaque_fragment>', `{
|
|
366
|
+
float blenderCoverage = diffuseColor.a;
|
|
367
|
+
vec3 blenderRadiance = bgClosureP * blenderCoverage * outgoingLight + bgClosureE;
|
|
368
|
+
float blenderAlpha = 1.0 - min(bgClosureT + bgClosureP * (1.0 - blenderCoverage), 1.0);
|
|
369
|
+
#ifdef OPAQUE
|
|
370
|
+
outgoingLight = blenderRadiance;
|
|
371
|
+
#else
|
|
372
|
+
outgoingLight = blenderRadiance / max(blenderAlpha, 1e-4);
|
|
373
|
+
#endif
|
|
374
|
+
diffuseColor.a = blenderAlpha;
|
|
375
|
+
}`, true]);
|
|
376
|
+
}
|
|
377
|
+
// MACROS BOTH SIDES DEFINE (three's `saturate` and Blender's, say): each
|
|
378
|
+
// side's code is compiled under its own definition -- Blender's library
|
|
379
|
+
// between an #undef and a restore of three's line.
|
|
380
|
+
// The shader still names its chunks (`#include <common>`) at this point;
|
|
381
|
+
// three expands them after this hook, so the scan expands them itself.
|
|
382
|
+
const expand = (text: string): string => text.replace(/^[ \t]*#include <(\w+)>/gm,
|
|
383
|
+
(_, name: string) => expand((THREE.ShaderChunk as Record<string, string>)[name] ?? ''));
|
|
384
|
+
const head = expand(shader.fragmentShader.slice(0, shader.fragmentShader.indexOf('void main() {')));
|
|
385
|
+
const theirs = new Map([...head.matchAll(/^[ \t]*#define[ \t]+(\w+).*$/gm)].map(m => [m[1]!, m[0]!.trim()]));
|
|
386
|
+
const ours = new Set([...compiled.declarations.matchAll(/^[ \t]*#define[ \t]+(\w+)/gm)].map(m => m[1]!));
|
|
387
|
+
const shared = [...ours].filter(name => theirs.has(name));
|
|
388
|
+
const library = [
|
|
389
|
+
...shared.map(name => `#undef ${name}`),
|
|
390
|
+
compiled.declarations,
|
|
391
|
+
...shared.flatMap(name => [`#undef ${name}`, theirs.get(name)!]),
|
|
392
|
+
].join('\n');
|
|
393
|
+
let text = shader.fragmentShader.replace('void main() {', `${varyings}\n${library}\nvoid main() {`)
|
|
394
|
+
.replace('#include <clipping_planes_fragment>', '#include <clipping_planes_fragment>\n\tblenderGraph();');
|
|
395
|
+
for (const [chunk, line, before] of fragment) {
|
|
396
|
+
if (!text.includes(chunk)) throw new Error(`three's physical shader has no ${chunk}`);
|
|
397
|
+
text = text.replace(chunk, before ? `${line}\n${chunk}` : `${chunk}\n${line}`);
|
|
398
|
+
}
|
|
399
|
+
shader.fragmentShader = text;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** The drawing buffer size `coordinate_screen` divides by, refreshed per frame. */
|
|
403
|
+
const viewport = new THREE.Vector2(1, 1);
|
|
404
|
+
export function setGraphViewport(renderer: THREE.WebGLRenderer): void {
|
|
405
|
+
renderer.getDrawingBufferSize(viewport);
|
|
406
|
+
}
|