@volter/blender-engine 0.1.136 → 0.1.138
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/pull-job.mts
CHANGED
|
@@ -10,8 +10,23 @@ export class PullJob<T> {
|
|
|
10
10
|
private expected: string | null = null;
|
|
11
11
|
private resume: (() => void) | null = null;
|
|
12
12
|
private waiter: { resolve: (value: PullResult<T>) => void; reject: (error: unknown) => void } | null = null;
|
|
13
|
+
/**
|
|
14
|
+
* How the work ended, when it ended while nobody was waiting on a step. Work normally ends only
|
|
15
|
+
* while a step waits, but work that races something against a checkpoint (an engine compiling
|
|
16
|
+
* beside an import) can fail in the gap after a `continue` was handed out. The outcome is kept
|
|
17
|
+
* for the step that comes next, so the caller is told what actually happened. Before this, the
|
|
18
|
+
* failure was thrown at nobody and the next step answered "Invalid or replayed load
|
|
19
|
+
* continuation", which named the protocol instead of the fault (measured 2026-10-08: a Blender
|
|
20
|
+
* start refused by a content security policy showed only that message, on every retry).
|
|
21
|
+
*/
|
|
22
|
+
private ended: { readonly value: T } | { readonly error: unknown } | null = null;
|
|
13
23
|
constructor(private readonly work: (checkpoint: (phase: string) => Promise<void>) => Promise<T>) {}
|
|
14
24
|
step(token?: string): Promise<PullResult<T>> {
|
|
25
|
+
if (this.ended && !this.waiter && token === this.expected) {
|
|
26
|
+
const ended = this.ended;
|
|
27
|
+
this.ended = null; this.expected = null; this.resume = null;
|
|
28
|
+
return 'error' in ended ? Promise.reject(ended.error) : Promise.resolve({ load: 'done', value: ended.value });
|
|
29
|
+
}
|
|
15
30
|
if (this.finished || this.waiter || (this.started ? token !== this.expected || !this.resume : token !== undefined))
|
|
16
31
|
return Promise.reject(new Error('Invalid or replayed load continuation'));
|
|
17
32
|
const answer = new Promise<PullResult<T>>((resolve, reject) => { this.waiter = { resolve, reject }; });
|
|
@@ -24,12 +39,18 @@ export class PullJob<T> {
|
|
|
24
39
|
waiter.resolve({ load: 'continue', token: this.expected, phase });
|
|
25
40
|
}))).then(value => {
|
|
26
41
|
this.finished = true;
|
|
27
|
-
const waiter = this.waiter
|
|
28
|
-
waiter.resolve({ load: 'done', value });
|
|
42
|
+
const waiter = this.waiter; this.waiter = null;
|
|
43
|
+
if (waiter) waiter.resolve({ load: 'done', value });
|
|
44
|
+
else this.ended = { value };
|
|
29
45
|
}, error => {
|
|
30
46
|
this.finished = true;
|
|
31
|
-
const waiter = this.waiter
|
|
32
|
-
waiter.reject(error);
|
|
47
|
+
const waiter = this.waiter; this.waiter = null;
|
|
48
|
+
if (waiter) waiter.reject(error);
|
|
49
|
+
else {
|
|
50
|
+
this.ended = { error };
|
|
51
|
+
// Said now as well: if the owner never asks for the next step, nobody would collect it.
|
|
52
|
+
console.warn('A Blender load failed between steps; the next step reports it:', error);
|
|
53
|
+
}
|
|
33
54
|
});
|
|
34
55
|
} else {
|
|
35
56
|
const resume = this.resume!; this.resume = null; this.expected = null; resume();
|
package/browser/runtime.ts
CHANGED
|
@@ -273,14 +273,26 @@ export class BlenderRuntime {
|
|
|
273
273
|
);
|
|
274
274
|
this.#project = project;
|
|
275
275
|
this.#document ??= document ?? 'models/model.blend';
|
|
276
|
-
this.#started ??= this.#request({
|
|
276
|
+
this.#started ??= (this.#request({
|
|
277
277
|
op: 'start',
|
|
278
278
|
project,
|
|
279
279
|
...(document === undefined ? {} : { document }),
|
|
280
|
-
}) as Promise<RuntimeStart
|
|
280
|
+
}) as Promise<RuntimeStart>).catch((error: unknown) => {
|
|
281
|
+
this.#startFailed = true;
|
|
282
|
+
throw error;
|
|
283
|
+
});
|
|
281
284
|
return this.#started;
|
|
282
285
|
}
|
|
283
286
|
|
|
287
|
+
/**
|
|
288
|
+
* True once THIS session's own start has failed (not a refusal to start, such as a file another
|
|
289
|
+
* document holds). A start is asked for once and its outcome kept, and the worker that failed may
|
|
290
|
+
* hold a half-started engine, so such a session never starts: its owner discards it and makes a
|
|
291
|
+
* new one, with a new worker, for the next attempt.
|
|
292
|
+
*/
|
|
293
|
+
get startFailed(): boolean { return this.#startFailed; }
|
|
294
|
+
#startFailed = false;
|
|
295
|
+
|
|
284
296
|
#ready(): Promise<RuntimeStart> {
|
|
285
297
|
if (this.#project === null)
|
|
286
298
|
return Promise.reject(
|
package/browser/session.py
CHANGED
|
@@ -1216,11 +1216,16 @@ def _depsgraph_placements(frame, depsgraph):
|
|
|
1216
1216
|
continue
|
|
1217
1217
|
if not instance.show_self:
|
|
1218
1218
|
continue
|
|
1219
|
+
# AN UNSUPPORTED PLACEMENT IS LEFT OUT, NAMED ONCE (`warn`): raising here
|
|
1220
|
+
# lost the whole frame, so one odd instance kept the entire scene from
|
|
1221
|
+
# opening. The native exporter skips the same duplis.
|
|
1219
1222
|
if (instance.instance_object is not None and instance.object.data != instance.instance_object.data and
|
|
1220
1223
|
(instance.object.data is None or geometry.get(source.name) != instance.object.data.as_pointer())):
|
|
1221
|
-
|
|
1224
|
+
warn("Depsgraph instances of %r are not drawn: their geometry differs from the exported source" % source.name)
|
|
1225
|
+
continue
|
|
1222
1226
|
if row is None or (source.type == "MESH" and row["mesh"] is None):
|
|
1223
|
-
|
|
1227
|
+
warn("Depsgraph instances of %r are not drawn: the source has no exported geometry" % source.name)
|
|
1228
|
+
continue
|
|
1224
1229
|
if row["mesh"] is None:
|
|
1225
1230
|
if source.type not in ("EMPTY", "CAMERA"):
|
|
1226
1231
|
warn("Depsgraph instance %r of type %s is not drawn" % (source.name, source.type))
|
|
@@ -1228,7 +1233,8 @@ def _depsgraph_placements(frame, depsgraph):
|
|
|
1228
1233
|
owner = instance.parent.original if instance.parent is not None else None
|
|
1229
1234
|
owner_row = by_name.get(owner.name) if owner is not None else None
|
|
1230
1235
|
if owner is not None and owner_row is None:
|
|
1231
|
-
|
|
1236
|
+
warn("Depsgraph instances owned by %r are not drawn: the owner is not exported" % owner.name)
|
|
1237
|
+
continue
|
|
1232
1238
|
persistent = list(instance.persistent_id)
|
|
1233
1239
|
particle_index = persistent[0]
|
|
1234
1240
|
psys = instance.particle_system
|
|
@@ -1687,7 +1693,11 @@ class Session:
|
|
|
1687
1693
|
for row in frame["objects"]:
|
|
1688
1694
|
if not any(m in graphs for m in row["materials"] if m is not None):
|
|
1689
1695
|
continue
|
|
1690
|
-
|
|
1696
|
+
# An instance source from a collection outside the scene is exported
|
|
1697
|
+
# too (`instance_sources`) and shares these graphs; its Object Info
|
|
1698
|
+
# and Particle Info inputs come from the file's own objects. Only
|
|
1699
|
+
# those few rows miss the scene's map.
|
|
1700
|
+
obj = by_name.get(row["name"]) or bpy.data.objects.get(row["name"])
|
|
1691
1701
|
if obj is not None:
|
|
1692
1702
|
row["object_info"] = {
|
|
1693
1703
|
"color": [float(v) for v in obj.color],
|
|
@@ -7,7 +7,7 @@ import {z} from 'zod';
|
|
|
7
7
|
import {applyGraphShader, bindGraphDraw, bindPaletteGraph, borrowGraphDrawBinding, graphProgramKey, graphUvChannels, setGraphViewport} from './blender-graph-material';
|
|
8
8
|
import type {CompiledGraph} from './blender-node-graph';
|
|
9
9
|
import {bindNamedUvChannels} from './blender-texture-samplers';
|
|
10
|
-
import {copyWorldVisibility,worldVisibilityShader,worldVisibilityUniforms} from './blender-world-visibility';
|
|
10
|
+
import {WORLD_VISIBILITY,copyWorldVisibility,worldVisibilityShader,worldVisibilityUniforms} from './blender-world-visibility';
|
|
11
11
|
|
|
12
12
|
const scalar = z.number().finite();
|
|
13
13
|
const color = z.tuple([scalar, scalar, scalar]);
|
|
@@ -195,7 +195,7 @@ export function applyPhysicalMaterial(material: THREE.MeshPhysicalMaterial, inpu
|
|
|
195
195
|
blenderWorldExtinction: {value: new THREE.Vector3()}};
|
|
196
196
|
uniforms.set(material, values);
|
|
197
197
|
const held = values;
|
|
198
|
-
material.customProgramCacheKey = () => `blender-principled-physical-v6${graphProgramKey(material)}`;
|
|
198
|
+
material.customProgramCacheKey = () => `blender-principled-physical-v6${WORLD_VISIBILITY ? '-atlas' : ''}${graphProgramKey(material)}`;
|
|
199
199
|
material.onBeforeRender = (renderer, scene, camera, geometry, object) => {
|
|
200
200
|
bindNamedUvChannels(material, geometry);
|
|
201
201
|
bindGraphDraw(material, geometry, renderer, scene, camera, object, graphShadow);
|
|
@@ -213,12 +213,16 @@ export function applyPhysicalMaterial(material: THREE.MeshPhysicalMaterial, inpu
|
|
|
213
213
|
.map(i => (i <= 3 ? `#ifndef USE_UV${i}\nattribute vec2 uv${i};\n#endif` : `attribute vec2 uv${i};`))
|
|
214
214
|
.join('\n') + '\n' + shader.vertexShader;
|
|
215
215
|
Object.assign(shader.uniforms, held);
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
'#include <
|
|
216
|
+
// The sky-light atlas exists in the shader only while it is on: its three
|
|
217
|
+
// samplers would count against every material's sixteen (WORLD_VISIBILITY).
|
|
218
|
+
if (WORLD_VISIBILITY) {
|
|
219
|
+
worldVisibilityUniforms(material,shader.uniforms);
|
|
220
|
+
shader.vertexShader='varying vec3 blenderWorldPosition;\n'+shader.vertexShader;
|
|
221
|
+
shader.vertexShader=shader.vertexShader.replace('#include <worldpos_vertex>',
|
|
222
|
+
'#include <worldpos_vertex>\n#if defined(USE_ENVMAP) || defined(USE_SHADOWMAP) || defined(USE_TRANSMISSION)\nblenderWorldPosition=worldPosition.xyz;\n#else\nblenderWorldPosition=(modelMatrix*vec4(transformed,1.)).xyz;\n#endif');
|
|
223
|
+
shader.fragmentShader=shader.fragmentShader.replace('#include <envmap_physical_pars_fragment>',
|
|
224
|
+
'#include <envmap_physical_pars_fragment>\n'+worldVisibilityShader);
|
|
225
|
+
}
|
|
222
226
|
shader.fragmentShader = `uniform float blenderCoatIor;
|
|
223
227
|
uniform vec3 blenderWorldExtinction;
|
|
224
228
|
vec3 blenderInfiniteTransmission() { return vec3(
|
|
@@ -263,11 +267,13 @@ export function applyPhysicalMaterial(material: THREE.MeshPhysicalMaterial, inpu
|
|
|
263
267
|
.replace('rectAreaLight = rectAreaLights[ i ];',
|
|
264
268
|
'rectAreaLight = rectAreaLights[ i ]; rectAreaLight.color *= exp(-blenderWorldExtinction * length(rectAreaLight.position-geometryPosition));')
|
|
265
269
|
.replace('getAmbientLightIrradiance( ambientLightColor )', 'getAmbientLightIrradiance( ambientLightColor ) * blenderInfiniteTransmission()');
|
|
266
|
-
const
|
|
270
|
+
const visible = WORLD_VISIBILITY ? THREE.ShaderChunk.lights_fragment_maps
|
|
267
271
|
.replace('getIBLIrradiance( geometryNormal )',
|
|
268
272
|
'blenderWorldIrradiance( geometryNormal, getIBLIrradiance( geometryNormal ) )')
|
|
269
273
|
.replaceAll(/(getIBL(?:Radiance|AnisotropyRadiance)\([^;]+\))/g,
|
|
270
274
|
'$1 * blenderWorldReflectionVisibility(geometryViewDir,geometryNormal,material.roughness)')
|
|
275
|
+
: THREE.ShaderChunk.lights_fragment_maps;
|
|
276
|
+
const environment = visible
|
|
271
277
|
.replaceAll(/(getIBL(?:Irradiance|Radiance|AnisotropyRadiance)\([^;]+\))/g, '$1 * blenderInfiniteTransmission()');
|
|
272
278
|
shader.fragmentShader = shader.fragmentShader.replace('#include <lights_physical_fragment>', physical)
|
|
273
279
|
.replace('#include <lights_fragment_begin>', lighting)
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
worldSchema,
|
|
28
28
|
} from './blender-runtime-lighting';
|
|
29
29
|
import { fitModelDirectionalShadow, visibleShadowReceivers } from './blender-runtime-shadows';
|
|
30
|
-
import {BlenderWorldVisibility,bindWorldVisibility} from './blender-world-visibility';
|
|
30
|
+
import {BlenderWorldVisibility,WORLD_VISIBILITY,bindWorldVisibility} from './blender-world-visibility';
|
|
31
31
|
import { volumeMesh, volumeSchema } from './blender-runtime-volume';
|
|
32
32
|
import { applyPhysicalMaterial, applyWorldExtinction, physicalMaterialSchema } from './blender-physical-material';
|
|
33
33
|
import { prepareGraphGeometry, setMaterialGraph } from './blender-graph-material';
|
|
@@ -58,16 +58,6 @@ const SAVED_SHADING = {
|
|
|
58
58
|
MATERIAL: 'preview',
|
|
59
59
|
RENDERED: 'rendered',
|
|
60
60
|
} as const;
|
|
61
|
-
/** SKY-LIGHT OCCLUSION (`blender-world-visibility.ts`) IS OFF IN EVERY VIEW. Its
|
|
62
|
-
* sixteen 512-square tiles each span the whole scene, so a 360 m level gets
|
|
63
|
-
* about a metre per texel, and each lookup is one nearest texel answering
|
|
64
|
-
* yes or no. Faceted surfaces then occlude their own sky at random pixels --
|
|
65
|
-
* static on every polygon in Rendered and Play -- and narrow specular lobes,
|
|
66
|
-
* dominated by a few of the sixteen directions, plausibly flash as the camera
|
|
67
|
-
* moves. It measured right in a closed room a few metres across. Turn it back
|
|
68
|
-
* on once its lookups are filtered and its resolution follows the scene's size
|
|
69
|
-
* (docs/BLENDER-RENDERING-PARITY.md lists the rest). */
|
|
70
|
-
const WORLD_VISIBILITY = false;
|
|
71
61
|
const scalar = z.number().finite();
|
|
72
62
|
const point = z.tuple([scalar, scalar, scalar]);
|
|
73
63
|
const edge = z.tuple([z.number().int().nonnegative(), z.number().int().nonnegative()]);
|
|
@@ -4,6 +4,20 @@
|
|
|
4
4
|
* directional quadrature, not indirect light transport. */
|
|
5
5
|
import * as THREE from 'three';
|
|
6
6
|
|
|
7
|
+
/** SKY-LIGHT OCCLUSION IS OFF IN EVERY VIEW, and compiled out of every material. Its
|
|
8
|
+
* sixteen 512-square tiles each span the whole scene, so a 360 m level gets
|
|
9
|
+
* about a metre per texel, and each lookup is one nearest texel answering
|
|
10
|
+
* yes or no. Faceted surfaces then occlude their own sky at random pixels --
|
|
11
|
+
* static on every polygon in Rendered and Play -- and narrow specular lobes,
|
|
12
|
+
* dominated by a few of the sixteen directions, plausibly flash as the camera
|
|
13
|
+
* moves. It measured right in a closed room a few metres across. Turn it back
|
|
14
|
+
* on once its lookups are filtered and its resolution follows the scene's size
|
|
15
|
+
* (docs/BLENDER-RENDERING-PARITY.md lists the rest). Off, the view neither
|
|
16
|
+
* syncs nor draws the atlas (`blender-runtime-view.ts`), and physical
|
|
17
|
+
* materials declare none of its samplers (`blender-physical-material.ts`):
|
|
18
|
+
* three of them counted against a material's sixteen even while unused. */
|
|
19
|
+
export const WORLD_VISIBILITY = false;
|
|
20
|
+
|
|
7
21
|
const SAMPLE_COUNT=16, TILES=4, TILE_SIZE=512;
|
|
8
22
|
type Segment={mesh:THREE.Mesh; geometry:THREE.BufferGeometry; position:THREE.BufferAttribute|THREE.InterleavedBufferAttribute;
|
|
9
23
|
index:THREE.BufferAttribute|null; positionVersion:number; indexVersion:number; start:number; count:number;
|