@volter/blender-engine 0.1.5 → 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 +31 -2
- package/browser/session.py +563 -11
- package/browser/three/blender-frame-columns.ts +6 -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 +162 -0
- package/browser/three/blender-presenter-change.ts +24 -0
- package/browser/three/blender-runtime-geometry.ts +112 -9
- package/browser/three/blender-runtime-lighting.ts +11 -0
- package/browser/three/blender-runtime-view.ts +237 -49
- package/browser/three/blender-texture-samplers.ts +79 -0
- package/browser/three/blender-world-volume.ts +235 -0
- package/browser/three/presenter.ts +6 -1
- package/package.json +1 -1
- package/wasm/BUNDLE.json +13 -25
- package/wasm/blender_browser.data.br +0 -0
- package/wasm/blender_browser.wasm.br +0 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/** Principled socket values stay Blender-owned; this file applies them to the
|
|
2
|
+
* actual Three material. Units cross the wire unchanged (turns and nanometres).
|
|
3
|
+
* Shader adjustments cover the coat IOR/tint and specular grazing behavior
|
|
4
|
+
* that MeshPhysicalMaterial does not expose as properties. */
|
|
5
|
+
import * as THREE from 'three';
|
|
6
|
+
import {z} from 'zod';
|
|
7
|
+
import {applyGraphShader, bindGraphDraw, graphProgramKey, setGraphViewport} from './blender-graph-material';
|
|
8
|
+
import {bindNamedUvChannels} from './blender-texture-samplers';
|
|
9
|
+
|
|
10
|
+
const scalar = z.number().finite();
|
|
11
|
+
const color = z.tuple([scalar, scalar, scalar]);
|
|
12
|
+
export const physicalMaterialSchema = z.object({
|
|
13
|
+
coat: scalar, coat_roughness: scalar, coat_ior: scalar, coat_tint: color,
|
|
14
|
+
sheen: scalar, sheen_roughness: scalar, sheen_tint: color,
|
|
15
|
+
anisotropy: scalar, anisotropy_rotation: scalar,
|
|
16
|
+
specular_level: scalar, specular_tint: color,
|
|
17
|
+
film_thickness: scalar, film_ior: scalar,
|
|
18
|
+
}).strict();
|
|
19
|
+
type Physical = z.infer<typeof physicalMaterialSchema>;
|
|
20
|
+
const defaults: Physical = {
|
|
21
|
+
coat: 0, coat_roughness: 0.03, coat_ior: 1.5, coat_tint: [1, 1, 1],
|
|
22
|
+
sheen: 0, sheen_roughness: 0.5, sheen_tint: [1, 1, 1],
|
|
23
|
+
anisotropy: 0, anisotropy_rotation: 0,
|
|
24
|
+
specular_level: 0.5, specular_tint: [1, 1, 1], film_thickness: 0, film_ior: 1.33,
|
|
25
|
+
};
|
|
26
|
+
const uniforms = new WeakMap<THREE.MeshPhysicalMaterial, {
|
|
27
|
+
blenderCoatIor: {value: number}; blenderCoatTint: {value: THREE.Color};
|
|
28
|
+
blenderMapClip: {value: boolean}; blenderRoughnessClip: {value: boolean}; blenderNormalClip: {value: boolean};
|
|
29
|
+
blenderWorldExtinction: {value: THREE.Vector3};
|
|
30
|
+
}>();
|
|
31
|
+
|
|
32
|
+
export function applyWorldExtinction(material: THREE.MeshPhysicalMaterial, extinction: THREE.Vector3): void {
|
|
33
|
+
if (!uniforms.has(material)) applyPhysicalMaterial(material);
|
|
34
|
+
uniforms.get(material)!.blenderWorldExtinction.value.copy(extinction);
|
|
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
|
+
|
|
59
|
+
export function applyPhysicalMaterial(material: THREE.MeshPhysicalMaterial, input?: Physical,
|
|
60
|
+
clips: {map?: boolean; roughness?: boolean; normal?: boolean} = {}): void {
|
|
61
|
+
const data = input ?? defaults;
|
|
62
|
+
material.clearcoat = data.coat;
|
|
63
|
+
material.clearcoatRoughness = data.coat_roughness;
|
|
64
|
+
material.sheen = data.sheen;
|
|
65
|
+
material.sheenRoughness = data.sheen_roughness;
|
|
66
|
+
material.sheenColor.setRGB(...data.sheen_tint, THREE.LinearSRGBColorSpace);
|
|
67
|
+
material.anisotropy = data.anisotropy;
|
|
68
|
+
material.anisotropyRotation = data.anisotropy_rotation * Math.PI * 2;
|
|
69
|
+
material.specularIntensity = data.specular_level * 2;
|
|
70
|
+
material.specularColor.setRGB(...data.specular_tint, THREE.LinearSRGBColorSpace);
|
|
71
|
+
material.iridescence = data.film_thickness > 0 ? 1 : 0;
|
|
72
|
+
material.iridescenceIOR = data.film_ior;
|
|
73
|
+
material.iridescenceThicknessRange = [data.film_thickness, data.film_thickness];
|
|
74
|
+
let values = uniforms.get(material);
|
|
75
|
+
if (!values) {
|
|
76
|
+
values = {blenderCoatIor: {value: 1.5}, blenderCoatTint: {value: new THREE.Color(1, 1, 1)},
|
|
77
|
+
blenderMapClip: {value: false}, blenderRoughnessClip: {value: false}, blenderNormalClip: {value: false},
|
|
78
|
+
blenderWorldExtinction: {value: new THREE.Vector3()}};
|
|
79
|
+
uniforms.set(material, values);
|
|
80
|
+
const held = values;
|
|
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
|
+
};
|
|
87
|
+
material.onBeforeCompile = shader => {
|
|
88
|
+
// Three declares channels 0..3. Blender has eight named UV maps in
|
|
89
|
+
// addition to the default channel; only the used attributes survive GLSL.
|
|
90
|
+
shader.vertexShader = [4, 5, 6, 7, 8, 9].map(i => `attribute vec2 uv${i};`).join('\n') + '\n' + shader.vertexShader;
|
|
91
|
+
Object.assign(shader.uniforms, held);
|
|
92
|
+
shader.fragmentShader = `uniform float blenderCoatIor;
|
|
93
|
+
uniform vec3 blenderWorldExtinction;
|
|
94
|
+
vec3 blenderInfiniteTransmission() { return vec3(
|
|
95
|
+
blenderWorldExtinction.x>0.?0.:1., blenderWorldExtinction.y>0.?0.:1., blenderWorldExtinction.z>0.?0.:1.); }
|
|
96
|
+
uniform vec3 blenderCoatTint;
|
|
97
|
+
uniform bool blenderMapClip, blenderRoughnessClip, blenderNormalClip;
|
|
98
|
+
vec4 blenderImageSample(sampler2D image, vec2 uv, bool clipImage) {
|
|
99
|
+
vec4 pixel = texture2D(image, uv);
|
|
100
|
+
if (clipImage && (any(lessThan(uv, vec2(0.0))) || any(greaterThan(uv, vec2(1.0))))) pixel = vec4(0.0);
|
|
101
|
+
return pixel;
|
|
102
|
+
}
|
|
103
|
+
` + shader.fragmentShader;
|
|
104
|
+
const physical = THREE.ShaderChunk.lights_physical_fragment
|
|
105
|
+
.replace('material.specularF90 = mix( specularIntensityFactor, 1.0, metalnessFactor );',
|
|
106
|
+
'material.specularF90 = 1.0;')
|
|
107
|
+
.replace('material.clearcoatF0 = vec3( 0.04 );',
|
|
108
|
+
'material.clearcoatF0 = vec3( pow2( ( blenderCoatIor - 1.0 ) / ( blenderCoatIor + 1.0 ) ) );');
|
|
109
|
+
const normals = THREE.ShaderChunk.normal_fragment_maps
|
|
110
|
+
.replace('normal = texture2D( normalMap, vNormalMapUv ).xyz * 2.0 - 1.0;',
|
|
111
|
+
'vec3 blenderBaseNormal = normal; normal = texture2D( normalMap, vNormalMapUv ).xyz * 2.0 - 1.0; normal.y *= normalScale.y < 0.0 ? -1.0 : 1.0;')
|
|
112
|
+
.replace('normal = normalize( normalMatrix * normal );',
|
|
113
|
+
'normal = normalize(mix(blenderBaseNormal, normalize(normalMatrix * normal), max(0.0, normalScale.x)));')
|
|
114
|
+
.replace('mapN.xy *= normalScale;',
|
|
115
|
+
'mapN.xy *= normalScale; mapN.z = mix(1.0, mapN.z, clamp(normalScale.x, 0.0, 1.0));')
|
|
116
|
+
.replaceAll('texture2D( normalMap, vNormalMapUv )', 'blenderImageSample(normalMap, vNormalMapUv, blenderNormalClip)');
|
|
117
|
+
const baseMap = THREE.ShaderChunk.map_fragment
|
|
118
|
+
.replace('texture2D( map, vMapUv )', 'blenderImageSample(map, vMapUv, blenderMapClip)')
|
|
119
|
+
// Blender's Color socket does not connect Image Alpha to Principled Alpha.
|
|
120
|
+
.replace('diffuseColor *= sampledDiffuseColor;', 'diffuseColor.rgb *= sampledDiffuseColor.rgb;');
|
|
121
|
+
const roughnessMap = THREE.ShaderChunk.roughnessmap_fragment
|
|
122
|
+
.replace('texture2D( roughnessMap, vRoughnessMapUv )', 'blenderImageSample(roughnessMap, vRoughnessMapUv, blenderRoughnessClip)');
|
|
123
|
+
const lighting = THREE.ShaderChunk.lights_fragment_begin
|
|
124
|
+
.replace('getPointLightInfo( pointLight, geometryPosition, directLight );',
|
|
125
|
+
'getPointLightInfo( pointLight, geometryPosition, directLight ); directLight.color *= exp(-blenderWorldExtinction * length(pointLight.position-geometryPosition));')
|
|
126
|
+
.replace('getSpotLightInfo( spotLight, geometryPosition, directLight );',
|
|
127
|
+
'getSpotLightInfo( spotLight, geometryPosition, directLight ); directLight.color *= exp(-blenderWorldExtinction * length(spotLight.position-geometryPosition));')
|
|
128
|
+
.replace('getDirectionalLightInfo( directionalLight, directLight );',
|
|
129
|
+
'getDirectionalLightInfo( directionalLight, directLight ); directLight.color *= blenderInfiniteTransmission();')
|
|
130
|
+
// LTC integrates the rectangle without participating media. Its center
|
|
131
|
+
// distance is the attenuation approximation; the volume pass samples
|
|
132
|
+
// the rectangle itself. Neither is claimed as path-traced area lighting.
|
|
133
|
+
.replace('rectAreaLight = rectAreaLights[ i ];',
|
|
134
|
+
'rectAreaLight = rectAreaLights[ i ]; rectAreaLight.color *= exp(-blenderWorldExtinction * length(rectAreaLight.position-geometryPosition));')
|
|
135
|
+
.replace('getAmbientLightIrradiance( ambientLightColor )', 'getAmbientLightIrradiance( ambientLightColor ) * blenderInfiniteTransmission()');
|
|
136
|
+
const environment = THREE.ShaderChunk.lights_fragment_maps
|
|
137
|
+
.replaceAll(/(getIBL(?:Irradiance|Radiance|AnisotropyRadiance)\([^;]+\))/g, '$1 * blenderInfiniteTransmission()');
|
|
138
|
+
shader.fragmentShader = shader.fragmentShader.replace('#include <lights_physical_fragment>', physical)
|
|
139
|
+
.replace('#include <lights_fragment_begin>', lighting)
|
|
140
|
+
.replace('#include <lights_fragment_maps>', environment)
|
|
141
|
+
.replace('#include <normal_fragment_maps>', normals)
|
|
142
|
+
.replace('#include <map_fragment>', baseMap)
|
|
143
|
+
.replace('#include <roughnessmap_fragment>', roughnessMap)
|
|
144
|
+
.replace('vec3 Fcc = F_Schlick', `
|
|
145
|
+
// Blender's slab_transmittance_at_angle (gpu_shader_material_principled.glsl):
|
|
146
|
+
// Beer-Lambert attenuation through a refracting coat at this view angle.
|
|
147
|
+
float coatPath = blenderCoatIor * inversesqrt(max(1e-8,
|
|
148
|
+
blenderCoatIor * blenderCoatIor - (1.0 - dotNVcc * dotNVcc)));
|
|
149
|
+
outgoingLight *= mix(vec3(1.0), pow(max(blenderCoatTint, vec3(0.0)), vec3(coatPath)), material.clearcoat);
|
|
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);
|
|
154
|
+
};
|
|
155
|
+
material.needsUpdate = true;
|
|
156
|
+
}
|
|
157
|
+
values.blenderCoatIor.value = Math.max(1, data.coat_ior);
|
|
158
|
+
values.blenderCoatTint.value.setRGB(...data.coat_tint, THREE.LinearSRGBColorSpace);
|
|
159
|
+
values.blenderMapClip.value = clips.map === true;
|
|
160
|
+
values.blenderRoughnessClip.value = clips.roughness === true;
|
|
161
|
+
values.blenderNormalClip.value = clips.normal === true;
|
|
162
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -39,6 +39,15 @@ export interface DrawArrays {
|
|
|
39
39
|
normals: Float32Array | null;
|
|
40
40
|
/** Per drawn vertex, or null when the mesh has no UV map. */
|
|
41
41
|
uv: Float32Array | null;
|
|
42
|
+
/** All named UV layers; material inputs choose their own layer per draw. */
|
|
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;
|
|
42
51
|
indices: Uint32Array;
|
|
43
52
|
groups: { start: number; count: number; materialIndex: number }[];
|
|
44
53
|
/** THE BLENDER VERTEX EACH DRAWN VERTEX CAME FROM, one per drawn vertex.
|
|
@@ -58,6 +67,9 @@ export interface DrawArrays {
|
|
|
58
67
|
interface DrawView {
|
|
59
68
|
vertexCount: number;
|
|
60
69
|
normals: Normal[] | null;
|
|
70
|
+
cornerNormal?: Float32Array | undefined;
|
|
71
|
+
/** Blender's stored orco (-1..1), 3 per vertex. */
|
|
72
|
+
orco?: Float32Array | undefined;
|
|
61
73
|
co(i: number): THREE.Vector3;
|
|
62
74
|
/** CSR faces: corner `starts[i]..starts[i+1]` hold vertex indices. */
|
|
63
75
|
starts: Uint32Array;
|
|
@@ -69,6 +81,50 @@ interface DrawView {
|
|
|
69
81
|
sharp: Iterable<[number, number]>;
|
|
70
82
|
/** A corner's UV, or undefined when the mesh has no map. */
|
|
71
83
|
uv: ((corner: number) => [number, number]) | null;
|
|
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
|
+
});
|
|
72
128
|
}
|
|
73
129
|
|
|
74
130
|
function drawCore(view: DrawView): Omit<DrawArrays, 'hash'> {
|
|
@@ -147,28 +203,39 @@ function drawCore(view: DrawView): Omit<DrawArrays, 'hash'> {
|
|
|
147
203
|
const positions = new Float32Array(capacity * 3),
|
|
148
204
|
texcoords = new Float32Array(capacity * 2);
|
|
149
205
|
const sourceVertex = new Uint32Array(capacity);
|
|
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;
|
|
150
209
|
const indices = new Uint32Array(capacity),
|
|
151
210
|
shared = new Map<string, number>();
|
|
152
211
|
const groups: DrawArrays['groups'] = [];
|
|
153
|
-
const normals = view.normals ? new Float32Array(capacity * 3) : null;
|
|
212
|
+
const normals = view.normals || view.cornerNormal ? new Float32Array(capacity * 3) : null;
|
|
154
213
|
let count = 0,
|
|
155
214
|
indexCount = 0;
|
|
156
|
-
const emit = (v: number, texture?: [number, number], normal?: Normal): number => {
|
|
215
|
+
const emit = (v: number, texture?: [number, number], normal?: Normal, sourceCorner?: number, face?: number): number => {
|
|
157
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;
|
|
158
220
|
sourceVertex[index] = v;
|
|
159
221
|
view.co(v).toArray(positions, index * 3);
|
|
160
222
|
if (texture) texcoords.set(texture, index * 2);
|
|
161
223
|
if (normal) normals!.set(normal, index * 3);
|
|
224
|
+
if (sourceCorner !== undefined) for (const layer of uvLayers) layer.data.set(layer.read(sourceCorner), index * 2);
|
|
162
225
|
return index;
|
|
163
226
|
};
|
|
164
|
-
const corner = (c: number): number => {
|
|
227
|
+
const corner = (c: number, face: number): number => {
|
|
165
228
|
const texture = uv?.(c);
|
|
166
|
-
const normal = view.
|
|
167
|
-
|
|
168
|
-
|
|
229
|
+
const normal: Normal | undefined = view.cornerNormal
|
|
230
|
+
? [view.cornerNormal[c * 3]!, view.cornerNormal[c * 3 + 1]!, view.cornerNormal[c * 3 + 2]!]
|
|
231
|
+
: view.normals?.[c];
|
|
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('')}`;
|
|
169
236
|
let index = shared.get(key);
|
|
170
237
|
if (index === undefined) {
|
|
171
|
-
index = emit(cornerVerts[c]!, texture, normal);
|
|
238
|
+
index = emit(cornerVerts[c]!, texture, normal, c, face);
|
|
172
239
|
shared.set(key, index);
|
|
173
240
|
}
|
|
174
241
|
return index;
|
|
@@ -180,7 +247,7 @@ function drawCore(view: DrawView): Omit<DrawArrays, 'hash'> {
|
|
|
180
247
|
const points: THREE.Vector3[] = [];
|
|
181
248
|
for (let j = 0; j < length; j++) points.push(view.co(cornerVerts[faceStart + j]!));
|
|
182
249
|
for (const triangle of triangulatePolygon(points).triangles)
|
|
183
|
-
for (const c of triangle) indices[indexCount++] = corner(faceStart + c);
|
|
250
|
+
for (const c of triangle) indices[indexCount++] = corner(faceStart + c, i);
|
|
184
251
|
const materialIndex = view.material(i),
|
|
185
252
|
previous = groups.at(-1);
|
|
186
253
|
if (previous?.materialIndex === materialIndex) previous.count += indexCount - start;
|
|
@@ -191,12 +258,22 @@ function drawCore(view: DrawView): Omit<DrawArrays, 'hash'> {
|
|
|
191
258
|
positions: positions.slice(0, count * 3),
|
|
192
259
|
normals: normals?.slice(0, count * 3) ?? null,
|
|
193
260
|
uv: uv ? texcoords.slice(0, count * 2) : null,
|
|
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,
|
|
194
264
|
indices: indices.slice(0, indexCount),
|
|
195
265
|
sourceVertex: sourceVertex.slice(0, count),
|
|
196
266
|
groups,
|
|
197
267
|
};
|
|
198
268
|
}
|
|
199
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
|
+
|
|
200
277
|
export function geometryFromDrawArrays(
|
|
201
278
|
arrays: Omit<DrawArrays, 'hash' | 'sourceVertex'> & { sourceVertex?: Uint32Array },
|
|
202
279
|
): THREE.BufferGeometry {
|
|
@@ -207,6 +284,22 @@ export function geometryFromDrawArrays(
|
|
|
207
284
|
new THREE.BufferAttribute(arrays.uv ?? new Float32Array((arrays.positions.length / 3) * 2), 2),
|
|
208
285
|
);
|
|
209
286
|
geometry.setIndex(new THREE.BufferAttribute(arrays.indices, 1));
|
|
287
|
+
const channels: Record<string, number> = Object.create(null);
|
|
288
|
+
for (const [index, layer] of (arrays.uvLayers ?? []).entries()) {
|
|
289
|
+
// Blender's MAX_MTFACE is eight. Channel zero is the active render map;
|
|
290
|
+
// named inputs use their independently selectable channel, including it.
|
|
291
|
+
if (index >= 8) throw new Error('A Blender mesh supports at most eight UV maps');
|
|
292
|
+
channels[layer.name] = index + 1;
|
|
293
|
+
geometry.setAttribute(`uv${index + 1}`, new THREE.BufferAttribute(layer.data, 2));
|
|
294
|
+
}
|
|
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
|
+
}
|
|
210
303
|
for (const group of arrays.groups)
|
|
211
304
|
geometry.addGroup(group.start, group.count, group.materialIndex);
|
|
212
305
|
if (arrays.normals) geometry.setAttribute('normal', new THREE.BufferAttribute(arrays.normals, 3));
|
|
@@ -233,7 +326,7 @@ export function drawArraysFromColumns(c: MeshColumns, hash: string): DrawArrays
|
|
|
233
326
|
for (let i = 0; i < ne; i++) edges[i] = [c.edge[i * 2]!, c.edge[i * 2 + 1]!];
|
|
234
327
|
const sharp: [number, number][] = [];
|
|
235
328
|
for (let i = 0; i < ne; i++) if (c.edgeSharp[i] === 1) sharp.push(edges[i]!);
|
|
236
|
-
const custom = c.attributes.find((a) => a.name === 'custom_normal');
|
|
329
|
+
const custom = c.cornerNormal ? undefined : c.attributes.find((a) => a.name === 'custom_normal');
|
|
237
330
|
const normalMesh = custom
|
|
238
331
|
? {
|
|
239
332
|
v: Array.from(
|
|
@@ -269,6 +362,8 @@ export function drawArraysFromColumns(c: MeshColumns, hash: string): DrawArrays
|
|
|
269
362
|
const view: DrawView = {
|
|
270
363
|
vertexCount: nv,
|
|
271
364
|
normals: normalMesh ? customCornerNormals(normalMesh, customData) : null,
|
|
365
|
+
cornerNormal: c.cornerNormal,
|
|
366
|
+
orco: c.orco,
|
|
272
367
|
co: (i) => scratch.set(c.co[i * 3]!, c.co[i * 3 + 1]!, c.co[i * 3 + 2]!).clone(),
|
|
273
368
|
starts: c.faceStart,
|
|
274
369
|
cornerVerts: c.corner,
|
|
@@ -277,6 +372,9 @@ export function drawArraysFromColumns(c: MeshColumns, hash: string): DrawArrays
|
|
|
277
372
|
edges,
|
|
278
373
|
sharp,
|
|
279
374
|
uv: uvData ? (corner) => [uvData[corner * 2]!, uvData[corner * 2 + 1]!] : null,
|
|
375
|
+
uvLayers: maps.map(layer => ({name: layer.name, read: (corner: number) =>
|
|
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>),
|
|
280
378
|
};
|
|
281
379
|
if (map && ATTRIBUTE_LAYOUT.FLOAT2.size !== 2) throw new Error('FLOAT2 layout');
|
|
282
380
|
return { ...drawCore(view), hash };
|
|
@@ -337,6 +435,11 @@ export function drawRuntimeGeometry(data: MeshData): THREE.BufferGeometry {
|
|
|
337
435
|
edges: data.edge_order ?? data.e ?? null,
|
|
338
436
|
sharp: data.sharp ?? [],
|
|
339
437
|
uv: uv ? (corner) => uv[corner]! : null,
|
|
438
|
+
uvLayers: maps.map(layer => ({name: layer.name, read: (corner: number) =>
|
|
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()),
|
|
340
443
|
};
|
|
341
444
|
return geometryFromDrawArrays(drawCore(view));
|
|
342
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;
|
|
@@ -531,6 +533,14 @@ const worldRadiance = z.object({
|
|
|
531
533
|
});
|
|
532
534
|
export const worldSchema = worldRadiance
|
|
533
535
|
.extend({
|
|
536
|
+
volume: z.array(z.object({
|
|
537
|
+
kind: z.enum(['absorption', 'scatter', 'principled', 'emission']),
|
|
538
|
+
phase: z.string(), alpha: worldExpression,
|
|
539
|
+
weight: worldExpression, color: worldExpression, density: worldExpression,
|
|
540
|
+
anisotropy: worldExpression, absorption_color: worldExpression,
|
|
541
|
+
emission_color: worldExpression, emission_strength: worldExpression,
|
|
542
|
+
blackbody_intensity: worldExpression, temperature: worldExpression, blackbody_tint: worldExpression,
|
|
543
|
+
}).strict()).optional(),
|
|
534
544
|
/** The world that LIGHTS the scene, present only when Blender's Light Path
|
|
535
545
|
* `Is Camera Ray` split it from the world the camera photographs -- one
|
|
536
546
|
* sky at two strengths is how a scene gets a bright backdrop and
|
|
@@ -757,6 +767,7 @@ export class WorldBackground {
|
|
|
757
767
|
// composing now would paint a sky nobody asked for over the current one.
|
|
758
768
|
if (this.generation !== generation || this.applied?.scene !== scene) return;
|
|
759
769
|
this.compose(scene, world, expression, camera);
|
|
770
|
+
presenterChanged();
|
|
760
771
|
})();
|
|
761
772
|
this.pending = pending;
|
|
762
773
|
pendingWorld.add(pending);
|