@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
|
@@ -133,9 +133,37 @@ function moduleFiles(module: BlenderModule): BlenderFiles {
|
|
|
133
133
|
};
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
/**
|
|
137
|
+
* BLENDER'S ARTIFACTS ARE FETCHED ONCE PER BUILD. The wasm (86 MB decoded), the
|
|
138
|
+
* `.data` package (53 MB) and the Essentials payload are immutable for a build,
|
|
139
|
+
* and the page keeps them in Cache Storage under the digest the artifact door
|
|
140
|
+
* reports, so a later open reads them at the browser's own speed instead of
|
|
141
|
+
* from the editor's server. Measured in the browser substrate, where that
|
|
142
|
+
* server runs in the tab: the three took 7.2 s per open (4.2 s for the wasm
|
|
143
|
+
* alone), about half of it decoding their brotli. A file the door gives no
|
|
144
|
+
* digest for is fetched as it always was.
|
|
145
|
+
*/
|
|
146
|
+
const ARTIFACT_CACHE = 'volter-blender-artifacts';
|
|
147
|
+
|
|
148
|
+
async function cachedArtifact(file: string, digest: string | undefined): Promise<Response> {
|
|
149
|
+
const url = artifactUrl(file);
|
|
150
|
+
if (!digest || typeof caches === 'undefined') return fetch(url);
|
|
151
|
+
const key = `${url}?sha256=${digest}`;
|
|
152
|
+
const cache = await caches.open(ARTIFACT_CACHE);
|
|
153
|
+
const hit = await cache.match(key);
|
|
154
|
+
if (hit) return hit;
|
|
155
|
+
const response = await fetch(url);
|
|
156
|
+
if (!response.ok) return response;
|
|
157
|
+
// One build's bytes per file: an older build's are dropped as this one lands.
|
|
158
|
+
for (const old of await cache.keys()) if (old.url.startsWith(`${url}?sha256=`) && old.url !== key) await cache.delete(old);
|
|
159
|
+
const kept = new Response(await response.blob(), { headers: { 'content-type': response.headers.get('content-type') ?? 'application/octet-stream' } });
|
|
160
|
+
await cache.put(key, kept.clone());
|
|
161
|
+
return kept;
|
|
162
|
+
}
|
|
163
|
+
|
|
136
164
|
export async function startEmscriptenBlenderEngine(
|
|
137
165
|
options: BlenderEngineOptions,
|
|
138
|
-
|
|
166
|
+
status: BlenderArtifactStatus,
|
|
139
167
|
): Promise<BlenderEngine> {
|
|
140
168
|
if (!self.crossOriginIsolated)
|
|
141
169
|
throw new Error(
|
|
@@ -144,7 +172,17 @@ export async function startEmscriptenBlenderEngine(
|
|
|
144
172
|
'every response). This worker reports crossOriginIsolated=false.',
|
|
145
173
|
);
|
|
146
174
|
const glueUrl = artifactUrl('blender_browser.js');
|
|
147
|
-
const
|
|
175
|
+
const digests = status.digests ?? {};
|
|
176
|
+
// The `.data` package is handed to the glue whole (`getPreloadedPackage`),
|
|
177
|
+
// so it is read before the module starts; the wasm streams in beside it.
|
|
178
|
+
const [factory, preloaded] = await Promise.all([
|
|
179
|
+
loadFactory(glueUrl),
|
|
180
|
+
cachedArtifact('blender_browser.data', digests['blender_browser.data']).then(async (response) => {
|
|
181
|
+
if (!response.ok) throw new Error(`blender_browser.data: HTTP ${response.status}`);
|
|
182
|
+
return response.arrayBuffer();
|
|
183
|
+
}),
|
|
184
|
+
]);
|
|
185
|
+
let bootError: unknown = null;
|
|
148
186
|
const started = performance.now();
|
|
149
187
|
let readyLine: string | null = null;
|
|
150
188
|
// BLENDER'S OWN STREAMS ARE PAGE OUTPUT, NOT EDITOR-CONSOLE CONDITIONS.
|
|
@@ -164,6 +202,16 @@ export async function startEmscriptenBlenderEngine(
|
|
|
164
202
|
const module = await factory({
|
|
165
203
|
arguments: ['--background', '--factory-startup', '--python', SESSION_SCRIPT],
|
|
166
204
|
locateFile: (file: string) => artifactUrl(file),
|
|
205
|
+
getPreloadedPackage: () => preloaded,
|
|
206
|
+
instantiateWasm: (imports: WebAssembly.Imports, receive: (instance: WebAssembly.Instance, module: WebAssembly.Module) => void) => {
|
|
207
|
+
void (async () => {
|
|
208
|
+
const response = await cachedArtifact('blender_browser.wasm', digests['blender_browser.wasm']);
|
|
209
|
+
if (!response.ok) throw new Error(`blender_browser.wasm: HTTP ${response.status}`);
|
|
210
|
+
const { instance, module: compiled } = await WebAssembly.instantiateStreaming(response, imports);
|
|
211
|
+
receive(instance, compiled);
|
|
212
|
+
})().catch((error: unknown) => { bootError = error; });
|
|
213
|
+
return {};
|
|
214
|
+
},
|
|
167
215
|
// The pthreads load the SAME patched glue this worker just evaluated.
|
|
168
216
|
mainScriptUrlOrBlob: glueUrl,
|
|
169
217
|
print: (text: string) => say('log', text),
|
|
@@ -209,7 +257,10 @@ export async function startEmscriptenBlenderEngine(
|
|
|
209
257
|
});
|
|
210
258
|
// `main()` runs on a pthread (`-sPROXY_TO_PTHREAD`), so the factory resolves
|
|
211
259
|
// long before the session exists. The ready line is what says it does.
|
|
212
|
-
while (readyLine === null)
|
|
260
|
+
while (readyLine === null) {
|
|
261
|
+
if (bootError) throw bootError instanceof Error ? bootError : new Error(String(bootError));
|
|
262
|
+
await sleep(5);
|
|
263
|
+
}
|
|
213
264
|
const bootMs = performance.now() - started;
|
|
214
265
|
const banner = JSON.parse(readyLine) as { blender: string; engines: string[] };
|
|
215
266
|
|
|
@@ -268,7 +319,7 @@ export async function startEmscriptenBlenderEngine(
|
|
|
268
319
|
|
|
269
320
|
const files = moduleFiles(module);
|
|
270
321
|
FS.chmod('/bw/datafiles', 0o755);
|
|
271
|
-
await mountEssentials(files);
|
|
322
|
+
await mountEssentials(files, digests['essentials.bin']);
|
|
272
323
|
const { request } = openSessionChannel(files, options);
|
|
273
324
|
|
|
274
325
|
return {
|
|
@@ -293,9 +344,9 @@ export async function startEmscriptenBlenderEngine(
|
|
|
293
344
|
/** Assets are data, not a second engine. Ship them separately so a data update
|
|
294
345
|
* does not relink the 86 MB Wasm binary. Both payload and per-file bounds are
|
|
295
346
|
* checked before anything enters Blender's filesystem. */
|
|
296
|
-
async function mountEssentials(files: BlenderFiles): Promise<void> {
|
|
347
|
+
async function mountEssentials(files: BlenderFiles, digest: string | undefined): Promise<void> {
|
|
297
348
|
const [indexResponse, payloadResponse] = await Promise.all([
|
|
298
|
-
fetch(artifactUrl('essentials.json')),
|
|
349
|
+
fetch(artifactUrl('essentials.json')), cachedArtifact('essentials.bin', digest),
|
|
299
350
|
]);
|
|
300
351
|
if (!indexResponse.ok || !payloadResponse.ok)
|
|
301
352
|
throw new Error(`Blender Essentials assets are missing (${indexResponse.status}/${payloadResponse.status})`);
|
|
@@ -179,6 +179,8 @@ export interface BlenderArtifactStatus {
|
|
|
179
179
|
encoded: Record<string, 'br'>;
|
|
180
180
|
/** Why it is unavailable, named. Empty when it is available. */
|
|
181
181
|
missing: string[];
|
|
182
|
+
/** Emscripten: each file's SHA-256 as it decodes, the key a page keeps it under. */
|
|
183
|
+
digests?: Record<string, string>;
|
|
182
184
|
/** WALI only: the `sha256-` SRI of `blender.wasm`, which the program loader
|
|
183
185
|
* requires for any non-blob URL. */
|
|
184
186
|
integrity?: string;
|
package/browser/runtime.ts
CHANGED
|
@@ -202,6 +202,11 @@ export class BlenderRuntime {
|
|
|
202
202
|
return this.#project;
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
/** The startup document owned by this worker, including while boot is pending. */
|
|
206
|
+
get document(): string | null {
|
|
207
|
+
return this.#document;
|
|
208
|
+
}
|
|
209
|
+
|
|
205
210
|
/** Idempotent: the first call boots Blender at `project`'s
|
|
206
211
|
* absolute path; later calls await it (a different path is refused).
|
|
207
212
|
*
|
|
@@ -429,10 +434,10 @@ export class BlenderRuntime {
|
|
|
429
434
|
if (this.#terminated) return Promise.resolve();
|
|
430
435
|
if (this.#stopping) return this.#stopping;
|
|
431
436
|
this.#stopping = (async () => {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
437
|
+
// A start that failed opened no document, so there is nothing to flush: its stale
|
|
438
|
+
// rejection must not refuse every later close.
|
|
439
|
+
const started = this.#started ? await this.#started.then(() => true, () => false) : false;
|
|
440
|
+
if (started) await this.#request({ op: 'flush-document' }, true);
|
|
436
441
|
this.terminate();
|
|
437
442
|
})().catch(error => {
|
|
438
443
|
this.#stopping = null;
|
|
@@ -465,7 +470,7 @@ export class BlenderRuntime {
|
|
|
465
470
|
* A `present` is not counted separately — the worker only presents from
|
|
466
471
|
* inside a call this meter is already holding open, and the page-side cost of
|
|
467
472
|
* displaying the frame is main-thread time, which is the long-task observer's
|
|
468
|
-
* subject (`
|
|
473
|
+
* subject (the host's `worker-call-metrics.ts`).
|
|
469
474
|
*/
|
|
470
475
|
metrics(now: number = performance.now()): BlenderCallMetrics {
|
|
471
476
|
let oldest: number | null = null;
|