rayzee 9.1.2 → 9.2.0
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/README.md +174 -15
- package/dist/{NeuralRetouch-BRquqfmZ.js → NeuralRetouch-DgnfldWO.js} +1 -1
- package/dist/{NeuralSuperRes-CbNvQjCw.js → NeuralSuperRes-Baa2w8VU.js} +72 -40
- package/dist/{ToneMapGPU-DpUZDMbQ.js → ToneMapGPU-CNgexVto.js} +75 -71
- package/dist/rayzee.es.js +4915 -4314
- package/dist/rayzee.umd.js +96 -96
- package/package.json +3 -2
- package/src/AssetConfig.js +10 -5
- package/src/EngineEvents.js +41 -1
- package/src/EngineIssues.js +3 -0
- package/src/Headless.js +23 -7
- package/src/HeadlessCanvas.js +104 -0
- package/src/Passes/OIDNDenoiser.js +29 -4
- package/src/Passes/OIDNTemporalHistory.js +7 -0
- package/src/PathTracerApp.js +432 -119
- package/src/Platform.js +76 -0
- package/src/Processor/AssetLoader.js +66 -4
- package/src/Processor/BVHBuilder.js +2 -1
- package/src/Processor/EquirectHDRInfo.js +2 -1
- package/src/Processor/HostMemory.js +14 -0
- package/src/Processor/PBRT/PBRTSceneBuilder.js +26 -0
- package/src/Processor/ParallelBVHBuilder.js +7 -6
- package/src/Processor/PlatformImageLoader.js +118 -0
- package/src/Processor/ResampleRGBA8.js +102 -0
- package/src/Processor/SceneProcessor.js +7 -6
- package/src/Processor/TextureCreator.js +162 -13
- package/src/Processor/Workers/PackWorker.js +28 -0
- package/src/Stages/Compositor.js +25 -6
- package/src/Stages/PathTracer.js +142 -25
- package/src/Stages/PathTracerStage.js +6 -96
- package/src/TSL/DebugKernel.js +1 -1
- package/src/TSL/GenerateKernel.js +2 -5
- package/src/TSL/Random.js +3 -120
- package/src/TSL/ShadeKernel.js +3 -3
- package/src/index.js +2 -1
- package/src/managers/CameraManager.js +2 -2
- package/src/managers/DenoisingManager.js +76 -7
- package/src/managers/GoboManager.js +8 -1
- package/src/managers/IESManager.js +13 -2
- package/src/managers/InteractionManager.js +1 -1
- package/src/managers/LightManager.js +31 -0
- package/src/managers/OverlayManager.js +11 -1
- package/src/managers/VideoRenderManager.js +8 -0
- package/src/node/NodeWorker.js +144 -0
- package/src/node/index.js +9 -0
- package/src/node/nodePlatform.js +86 -0
package/README.md
CHANGED
|
@@ -267,16 +267,12 @@ export default defineConfig({
|
|
|
267
267
|
|
|
268
268
|
### Configuring Assets (CDN URLs & cache namespace)
|
|
269
269
|
|
|
270
|
-
By default, the engine loads
|
|
270
|
+
By default, the engine loads GLTF Draco/KTX2 decoders, OIDN denoiser weights, ONNX upscaler models, and the onnxruntime-web bundle from upstream CDNs. If you're self-hosting, embedding the engine alongside a different consumer of the same caches, or operating offline, override them **once before constructing `PathTracerApp`**:
|
|
271
271
|
|
|
272
272
|
```js
|
|
273
273
|
import { configureAssets } from 'rayzee';
|
|
274
274
|
|
|
275
275
|
configureAssets({
|
|
276
|
-
// STBN atlases (PNG, decoded as Float textures)
|
|
277
|
-
stbnScalarAtlas: '/assets/stbn_scalar_atlas.png',
|
|
278
|
-
stbnVec2Atlas: '/assets/stbn_vec2_atlas.png',
|
|
279
|
-
|
|
280
276
|
// onnxruntime-web (loaded by AI upscaler worker via dynamic import)
|
|
281
277
|
ortRuntimeUrl: '/ort/ort.webgpu.bundle.min.mjs',
|
|
282
278
|
ortWasmPaths: '/ort/',
|
|
@@ -319,12 +315,15 @@ const engine = new PathTracerApp(canvas, options?)
|
|
|
319
315
|
| Parameter | Type | Description |
|
|
320
316
|
|---|---|---|
|
|
321
317
|
| `canvas` | `HTMLCanvasElement` | Rendering target |
|
|
322
|
-
| `
|
|
318
|
+
| `canvas` may be `null` | | Headless: the engine makes its own canvas and runs no render loop — see [Running in Node](#running-in-node) |
|
|
319
|
+
| `options.headless` | `boolean` | Headless with a canvas of your own (default: `true` when `canvas` is `null`) |
|
|
320
|
+
| `options.autoResize` | `boolean` | Auto-resize on window resize (default: `true`; always off headless) |
|
|
323
321
|
| `options.container` | `HTMLElement` | Single DOM parent the engine mounts auxiliary elements into — HUD overlay (tile borders, helpers) and denoiser canvas. Defaults to `canvas.parentNode`. |
|
|
324
322
|
| `options.strict` | `boolean` | Throw an `EngineIssueError` where the engine would otherwise degrade and carry on (default: `false`). See [Degradation contract](#degradation-contract). |
|
|
325
323
|
| `options.profile` | `string` | `'viewer'` (default) or `'physical'` — product tuning that is not a physical constant: area-light scale, environment rotation, tone mapping, saturation. An unknown name throws. |
|
|
326
324
|
| `options.maxSceneBytes` | `number` | Raise or lower the CPU memory ceiling a scene may need before the engine refuses it (default 9,216 MB). See [Memory monitoring](#memory-monitoring). |
|
|
327
|
-
| `options.
|
|
325
|
+
| `options.hostMemoryGB` | `number` | The host's memory, for runtimes without Chrome's `navigator.deviceMemory` (which then read as 4 GB and cap the render reserve at 2048). Sizes the reserve and the path pool. |
|
|
326
|
+
| `options.storage` | `false \| 'auto' \| StorageManager` | On-disk storage (default: `configureAssets( { storage } )`; off under `strict` unless you set it there or here). A manager you pass stays yours to dispose. See [On-disk storage](#on-disk-storage-opfs). |
|
|
328
327
|
| `options.memorySpill` | `boolean` | Experimental, default `false`: build a large static scene through disk — triangle records, BLAS nodes and the three.js geometry are written out as the build finishes with them — and raise the pbrt triangle and placement caps to 60M / 8M. See [On-disk storage](#on-disk-storage-opfs). |
|
|
329
328
|
|
|
330
329
|
The engine creates and mounts everything it needs (denoiser canvas, tile/HUD overlay) into a single parent on `init()`. Performance HUDs (e.g. `stats-gl`) are not bundled — listen to `EngineEvents.FRAME` and tick your own panel.
|
|
@@ -552,10 +551,16 @@ engine.lightManager.add('PointLight') // Add a light (PointLight, SpotLigh
|
|
|
552
551
|
engine.lightManager.remove(uuid) // Remove by UUID
|
|
553
552
|
engine.lightManager.clear() // Remove all lights
|
|
554
553
|
engine.lightManager.getAll() // Get all light descriptors
|
|
555
|
-
engine.lightManager.
|
|
554
|
+
engine.lightManager.setIntensity(uuid, 40) // Set one light's power and re-upload
|
|
555
|
+
engine.lightManager.getLight(uuid) // The traced three.js light, to edit other properties
|
|
556
|
+
engine.lightManager.sync() // Re-upload light data to GPU after editing a light
|
|
556
557
|
engine.lightManager.showHelpers(true) // Toggle visual helpers
|
|
557
558
|
```
|
|
558
559
|
|
|
560
|
+
The path tracer traces **copies** of a model's lights, made when the model loads. Changing a light
|
|
561
|
+
inside the loaded model does nothing; change the copy — `getLight(uuid)` with a UUID from `getAll()` —
|
|
562
|
+
and call `sync()`, or use `setIntensity()`.
|
|
563
|
+
|
|
559
564
|
Light `intensity` follows Blender: radiant power in watts for point, spot and area lights, irradiance in W/m² for directional. Dividing power by area only means something in metres, so the engine assumes **one world unit is one metre**; scenes authored in cm or mm must carry that scale in their node transforms, as glTF exporters do. glTF `RectAreaLightPlaceholder` nodes author `intensity` as three.js radiance (their `power` field is `intensity · width · height · π`); the importer converts it to power through the light's world area so the authored radiance is reproduced exactly, then applies the profile's `areaLightIntensityScale`.
|
|
560
565
|
|
|
561
566
|
### engine.animationManager
|
|
@@ -824,6 +829,12 @@ surface** — pin a version and branch on the strings; they are never renamed or
|
|
|
824
829
|
| `scene.memory_budget` | the scene needs more CPU memory than is safe, or more than is possible |
|
|
825
830
|
| `emissive.instances_collapsed` | an emissive instanced mesh was too large to expand, so its copies light the scene as one |
|
|
826
831
|
| `refit.shared_geometry` | a deform was asked for on a mesh that shares its triangles, and was skipped |
|
|
832
|
+
| `denoiser.unavailable` | a requested denoise or upscale produced nothing — the denoiser was not built, OIDN was off while accumulating, or the pass needs a canvas in a document |
|
|
833
|
+
| `output.source_fallback` | `renderToBuffer( { source: 'display' } )` found no denoised picture and returned the raw accumulation |
|
|
834
|
+
| `light.placeholder_skipped` | a `RectAreaLightPlaceholder` node lacked `userData.name` or `userData.type: 'RectAreaLight'`, so no light was made for it |
|
|
835
|
+
|
|
836
|
+
`asset.unreachable` also covers what the engine fetches for itself: OIDN weights, IES profiles and
|
|
837
|
+
gobos (`detail.asset` says which).
|
|
827
838
|
|
|
828
839
|
`settings.getEffective()` is the companion for the `setting.unknown_key` case: it returns every live
|
|
829
840
|
setting as `{ value, source, routed }`, and `routed: false` means stored but reaching no stage.
|
|
@@ -868,7 +879,7 @@ engine.setReservedRenderResolution(2048, { allowLower: true }) // lower, payin
|
|
|
868
879
|
engine.getReservedRenderResolution() // the reserve actually in force
|
|
869
880
|
```
|
|
870
881
|
|
|
871
|
-
The request is **device-capped**: a 4096 reserve pins roughly 1.5 GB of MRT textures, so it is only granted on
|
|
882
|
+
The request is **device-capped**: a 4096 reserve pins roughly 1.5 GB of MRT textures, so it is only granted on hosts reporting ≥ 8 GB and a ≥ 1 GB `maxStorageBufferBindingSize`; weaker devices clamp to 2048, recorded as `render.reserve_capped`. The memory figure is `options.hostMemoryGB`, else `navigator.deviceMemory`, else an assumed 4 — so outside Chrome, pass it. `MAX_RESERVABLE_RENDER_SIZE` (4096) is the ceiling on any request.
|
|
872
883
|
|
|
873
884
|
Raises are monotonic unless you pass `allowLower` — UI-driven callers ask for whatever the current view needs, and honouring every decrease made the reserve oscillate across preview↔render switches, paying a full kernel rebuild each time.
|
|
874
885
|
|
|
@@ -877,10 +888,10 @@ Callable at any point in the lifecycle:
|
|
|
877
888
|
- **Before `init()`** — recorded and applied during `init()`, after the device exists but before the stages are constructed, so they allocate at the raised size directly. The device gate cannot run without a device, so the return value here is the *request*, not the verdict.
|
|
878
889
|
- **After `init()`** — applied immediately, re-initialising the reserved GPU storage in place.
|
|
879
890
|
|
|
880
|
-
Either way the verdict arrives as
|
|
891
|
+
Either way the verdict arrives as `EngineEvents.RESERVED_RENDER_SIZE_CHANGED`:
|
|
881
892
|
|
|
882
893
|
```js
|
|
883
|
-
engine.addEventListener(
|
|
894
|
+
engine.addEventListener(EngineEvents.RESERVED_RENDER_SIZE_CHANGED, e => console.log('reserve:', e.size));
|
|
884
895
|
engine.setReservedRenderResolution(4096);
|
|
885
896
|
await engine.init();
|
|
886
897
|
console.log(engine.getReservedRenderResolution()); // 4096, or 2048 if the device declined
|
|
@@ -894,7 +905,7 @@ Track GPU (VRAM) usage across the whole pipeline. Sizes are measured from live G
|
|
|
894
905
|
|
|
895
906
|
```js
|
|
896
907
|
const { current, peak, byCategory } = engine.getMemoryInfo(); // bytes
|
|
897
|
-
// byCategory: { rays, queues, gbuffer, accum, geometry, materials, environment, stages }
|
|
908
|
+
// byCategory: { rays, queues, gbuffer, accum, geometry, materials, environment, stages, denoiser, canvas }
|
|
898
909
|
|
|
899
910
|
engine.vram.resetPeak(); // reset the high-water mark to the current value
|
|
900
911
|
engine.vram.getReport(); // formatted one-line summary string
|
|
@@ -902,6 +913,12 @@ engine.vram.getReport(); // formatted one-line summary string
|
|
|
902
913
|
|
|
903
914
|
`peak` is a high-water mark, reset when a final render begins (`configureForMode('production')`). The engine's VRAM is largely monotonic — the ray pool only grows and the per-stage storage textures are fixed-size — so `peak` equals `current` during a steady render and only exceeds it after memory is released (lower resolution, a smaller scene, or removing the HDRI). The `stages` + `accum` categories (fixed 2048² storage textures) dominate the baseline.
|
|
904
915
|
|
|
916
|
+
`denoiser` is what the engine allocates for OIDN — its three float inputs, its half-float output and
|
|
917
|
+
the motion history. `canvas` is one image per presented surface (a browser may keep one or two more)
|
|
918
|
+
plus the buffer three.js's output pass tone-maps through. Not counted: oidn-web's own network weights
|
|
919
|
+
and activations, which it reports by count, not by size; and the neural passes, which run on a
|
|
920
|
+
`GPUDevice` of their own.
|
|
921
|
+
|
|
905
922
|
The React app surfaces this as a `Memory: … | Peak: …` readout in the on-canvas stats overlay.
|
|
906
923
|
|
|
907
924
|
#### CPU memory
|
|
@@ -974,7 +991,133 @@ The RNG is already pure — `hash(pixel, rayIndex, frame)`, no clock, no `Math.r
|
|
|
974
991
|
|
|
975
992
|
- `engine.isDeterministic` — whether output is currently bit-reproducible.
|
|
976
993
|
- `setDeterministicMode(true, { pinDispatch: false })` keeps the two readback-driven dispatch heuristics active. Output is then *not* reproducible; this exists so performance measurements reflect shipping behaviour rather than a configuration production never runs.
|
|
977
|
-
- `renderFrames`
|
|
994
|
+
- `renderFrames` raises `maxSamples` if needed, and throws if something retires the render early.
|
|
995
|
+
|
|
996
|
+
#### Production renders, reproducibly
|
|
997
|
+
|
|
998
|
+
Deterministic mode buys reproducibility by turning adaptive sampling off. To render the way the
|
|
999
|
+
product does — adaptive sampling, pixel freeze and the per-bounce early exit all on — and still get
|
|
1000
|
+
the same image for the same input, drive the render with `renderUntilComplete`:
|
|
1001
|
+
|
|
1002
|
+
```js
|
|
1003
|
+
engine.configureForMode('production');
|
|
1004
|
+
const { samples, retiredBy, denoised } = await engine.renderUntilComplete({ signal });
|
|
1005
|
+
// retiredBy: 'samples' | 'converged' | 'timeLimit'
|
|
1006
|
+
```
|
|
1007
|
+
|
|
1008
|
+
It renders without `requestAnimationFrame` until the sample ceiling, convergence or the time limit —
|
|
1009
|
+
whichever comes first — then runs the final OIDN denoise once, and fires `RENDER_COMPLETE` as the
|
|
1010
|
+
loop does. Adaptive sampling decides from counts read back from the GPU; normally each is applied
|
|
1011
|
+
whenever it lands, so the frame that retires depends on how fast frames are submitted. For its
|
|
1012
|
+
duration `renderUntilComplete` runs those readbacks in **lockstep**: one issued at frame N is applied
|
|
1013
|
+
at frame N + 4 exactly, and the render waits if it has not landed; every reset also starts from seed
|
|
1014
|
+
0 and from nothing the previous render measured. It turns interaction mode off meanwhile (a
|
|
1015
|
+
wall-clock mode for camera drags), and keeps submissions at most 2 × `drainEvery` frames ahead of
|
|
1016
|
+
the GPU. A time limit is wall-clock by nature, so that one stop is not reproducible; neither is
|
|
1017
|
+
auto-exposure, if you turn it on.
|
|
1018
|
+
|
|
1019
|
+
`engine.setLockstepReadbacks(true)` applies the same lockstep to the rAF loop and to `renderFrames`.
|
|
1020
|
+
|
|
1021
|
+
#### Batch rendering
|
|
1022
|
+
|
|
1023
|
+
The supported entry point for a render farm is `rayzee/src/Headless.js`, exported from the package.
|
|
1024
|
+
Its defaults are a batch renderer's — `strict`, `profile: 'physical'`, deterministic, storage off —
|
|
1025
|
+
so a degraded render throws instead of shipping:
|
|
1026
|
+
|
|
1027
|
+
```js
|
|
1028
|
+
import { openHeadless, captureHeadless } from 'rayzee';
|
|
1029
|
+
|
|
1030
|
+
const app = await openHeadless({ canvas, model: url, width: 1920, height: 1080 });
|
|
1031
|
+
try {
|
|
1032
|
+
const frame = await captureHeadless(app, { samples: 256, denoise: true });
|
|
1033
|
+
// frame.data: RGBA8 (colorSpace 'srgb') or Float32 (colorSpace 'linear')
|
|
1034
|
+
// frame.source: what was read — 'oidn' here, 'accumulation' without denoise
|
|
1035
|
+
// frame.issues: everything the engine survived, when not strict
|
|
1036
|
+
} finally {
|
|
1037
|
+
app.dispose();
|
|
1038
|
+
}
|
|
1039
|
+
```
|
|
1040
|
+
|
|
1041
|
+
`denoise: true` turns OIDN on before accumulating (it reads the albedo and normal buffers, which
|
|
1042
|
+
are written only while it is on), runs **one** final denoise with `app.runFinalDenoise()`, and reads
|
|
1043
|
+
it back with `renderToBuffer( { source: 'display' } )`. Driving it yourself is the same three calls.
|
|
1044
|
+
`renderToBuffer` reports the picture it read as `source`, and records `output.source_fallback` when
|
|
1045
|
+
`'display'` was asked for, a denoiser is in use, and nothing had published — so a strict host cannot
|
|
1046
|
+
ship a noisy image by mistake. Its `'srgb'` bytes are tone-mapped on the CPU and match the canvas to
|
|
1047
|
+
within one level (it rounds half a level up); `'linear'` is exact.
|
|
1048
|
+
|
|
1049
|
+
Constructing `PathTracerApp` yourself instead: pass `strict: true`; storage is then off unless you
|
|
1050
|
+
set it. Outside Chrome, pass `hostMemoryGB`.
|
|
1051
|
+
|
|
1052
|
+
**Provenance.** `engine.getProvenance()` — also `frame.provenance` from `captureHeadless` — is plain
|
|
1053
|
+
JSON naming what produced the image: engine and three.js versions, the profile by name *and* by
|
|
1054
|
+
value, the adapter, every live setting with its source, the colour pipeline, the render size and
|
|
1055
|
+
samples, and whether it ran headless, strict, deterministic or lockstepped. Store it beside each
|
|
1056
|
+
render and "what made this?" has an answer without rendering again.
|
|
1057
|
+
|
|
1058
|
+
**Pinning a look.** A change to what a render looks like when a host sets nothing — a default
|
|
1059
|
+
setting, a mode preset, a profile's values, light units — is released as a **major**, with the
|
|
1060
|
+
change described under BREAKING CHANGES in the release notes. Pin a major to pin a look; read the
|
|
1061
|
+
notes before moving to the next one.
|
|
1062
|
+
|
|
1063
|
+
#### Running in Node
|
|
1064
|
+
|
|
1065
|
+
The published build renders in plain Node on Dawn — the `webgpu` package, the WebGPU implementation
|
|
1066
|
+
inside Chrome — with no browser and no DOM shim:
|
|
1067
|
+
|
|
1068
|
+
```js
|
|
1069
|
+
import { create, globals } from 'webgpu';
|
|
1070
|
+
import sharp from 'sharp';
|
|
1071
|
+
import { configurePlatform, openHeadless } from 'rayzee';
|
|
1072
|
+
import { nodePlatform } from 'rayzee/node';
|
|
1073
|
+
|
|
1074
|
+
Object.assign(globalThis, globals);
|
|
1075
|
+
const gpu = create([]); // keep a reference: Dawn crashes if it is collected
|
|
1076
|
+
Object.defineProperty(navigator, 'gpu', { value: gpu });
|
|
1077
|
+
|
|
1078
|
+
const decodeImage = async (bytes) => {
|
|
1079
|
+
const { data, info } = await sharp(bytes).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
1080
|
+
return { data, width: info.width, height: info.height };
|
|
1081
|
+
};
|
|
1082
|
+
configurePlatform(nodePlatform({ decodeImage })); // after the webgpu globals, before any app
|
|
1083
|
+
const app = await openHeadless({ width: 1920, height: 1080, hostMemoryGB: 16 }); // no canvas: headless
|
|
1084
|
+
await app.loadModel('https://…/room.glb');
|
|
1085
|
+
const { samples } = await app.renderUntilComplete();
|
|
1086
|
+
const frame = await app.renderToBuffer({ source: 'display' });
|
|
1087
|
+
app.dispose();
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
- **No canvas means headless.** `new PathTracerApp(null)` — or `{ headless: true }` — makes its own
|
|
1091
|
+
canvas (a WebGPU context over a plain texture), runs no render loop (drive it with `renderFrames` or
|
|
1092
|
+
`renderUntilComplete`), and builds neither the overlay renderer nor the transform gizmo.
|
|
1093
|
+
- **`configurePlatform({ Worker, decodeImage })`** is where a host without a browser supplies what one
|
|
1094
|
+
would. `nodePlatform()` from `rayzee/node` fills it: `NodeWorker`, the Web Worker API over
|
|
1095
|
+
`worker_threads`, runs the engine's inlined workers; `decodeImage(bytes, mimeType)` is yours — PNG,
|
|
1096
|
+
JPEG or WebP to RGBA8, straight alpha, top row first — since the engine carries no decoder. It decodes
|
|
1097
|
+
every glTF image, embedded or not, and JPEG/PNG skies. PNGs with bytes after `IEND`, which browsers
|
|
1098
|
+
accept and strict decoders refuse, are trimmed first; a texture that still fails is recorded as
|
|
1099
|
+
`texture.build_failed` rather than dropped. Measured on a 91-texture model: `sharp` and
|
|
1100
|
+
`@napi-rs/canvas` (`await loadImage()`, then a 2D canvas) both decode in parallel, ~0.3 s against
|
|
1101
|
+
pngjs's 2.5 s serial. The canvas route premultiplies, which zeroes colour under alpha 0 and loses
|
|
1102
|
+
it under low alpha; `sharp` is exact. `jpeg-js` differs from libjpeg-turbo by up to 63 levels.
|
|
1103
|
+
- **`nodePlatform()` wraps the `webgpu` queue**, so install its globals first. dawn.node 0.6.1
|
|
1104
|
+
segfaults on `writeBuffer` / `writeTexture` from a `SharedArrayBuffer`, which WebGPU allows and the
|
|
1105
|
+
engine's triangle and BVH stores are once a scene is large; those uploads are copied out, 64 MB at
|
|
1106
|
+
a time.
|
|
1107
|
+
- **Textures are packed on the CPU** where there is no `createImageBitmap`: exact when a map fits its
|
|
1108
|
+
bucket, bilinear otherwise (a browser's canvas filter differs slightly there). A bucket over 8 MB
|
|
1109
|
+
packs in a worker, reading images decoded by `decodeImage` in place (they are kept in shared
|
|
1110
|
+
memory): on the main thread it held up the BVH workers, and a 1.9M-triangle, 91-texture model
|
|
1111
|
+
loaded in 4.1–4.9 s. It now loads in 2.1 s, against Chrome's 2.9 s on the same machine.
|
|
1112
|
+
- `nodePlatform()` also defines `ProgressEvent`, which three.js's `FileLoader` constructs while
|
|
1113
|
+
streaming; that is the only global it sets. three.js's own Draco and KTX2 workers call the global
|
|
1114
|
+
`Worker`, so a model using either also needs `globalThis.Worker = NodeWorker`.
|
|
1115
|
+
|
|
1116
|
+
Measured on this bench's corpus: all 29 scenes match the Chrome goldens (`npm run bench:node`, RMSE
|
|
1117
|
+
≤ 0.0036, no pixel over 0.02 — the CPU tone map accounts for most of it), and a textured glTF with an
|
|
1118
|
+
HDR, a PNG sky or a gradient matches Chrome block for block within 0.05 of a level. Not available
|
|
1119
|
+
without a browser: on-disk storage (OPFS), gobo libraries (they draw on a 2D canvas), and the AI
|
|
1120
|
+
upscaler.
|
|
978
1121
|
|
|
979
1122
|
#### GPU timing
|
|
980
1123
|
|
|
@@ -1005,7 +1148,7 @@ list. Both need `timestamp-query`, and `getDenoiseProfile()` returns `null` when
|
|
|
1005
1148
|
|
|
1006
1149
|
`engine.storage` is a `StorageManager` over the browser's origin private file system, or `null` where
|
|
1007
1150
|
there is none (a private window, Node) — everything works without it, only slower. The engine keeps
|
|
1008
|
-
its caches there: downloads (models, skies, OIDN weights
|
|
1151
|
+
its caches there: downloads (models, skies, OIDN weights — revalidated with `HEAD` at most daily, and served from the cache meanwhile),
|
|
1009
1152
|
unpacked `.tar.gz` archives and archive indexes, built scenes (reopened without rebuilding BVHs when
|
|
1010
1153
|
the first build took ≥ 10 s), and environment sampling tables. Caches share a budget of 30 % of the
|
|
1011
1154
|
quota and are evicted least-recently-used; `engine.storage.usage()` reports each area.
|
|
@@ -1119,6 +1262,18 @@ engine.addEventListener(EngineEvents.RENDER_COMPLETE, (e) => {
|
|
|
1119
1262
|
| `VIDEO_RENDER_PROGRESS` / `VIDEO_RENDER_COMPLETE` | Video export progress |
|
|
1120
1263
|
| `DEVICE_LOST` | The GPU device was lost (driver crash/reset) — rendering halts instead of throwing into a dead device |
|
|
1121
1264
|
| `DISPOSE` | Engine is being disposed (fires before teardown begins, so listeners can release their own references) |
|
|
1265
|
+
| `ISSUE` | An issue was recorded — see [Degradation contract](#degradation-contract) |
|
|
1266
|
+
| `MODEL_LOADED` / `OBJECT3D_LOADED` / `MODEL_ADDED` / `SCENE_OBJECT_REMOVED` / `SCENE_UNLOADED` | Scene content changed |
|
|
1267
|
+
| `SCENE_REBUILD` / `SCENE_SPILLED` / `SCENE_METADATA_APPLIED` | The scene was rebuilt, moved to disk, or had its authored environment applied |
|
|
1268
|
+
| `ENVIRONMENT_LOADED` / `TEXTURES_REPROCESSED` | An environment or the texture arrays were (re)built |
|
|
1269
|
+
| `CAMERAS_UPDATED` / `CAMERA_SWITCHED` / `FOCUS_CHANGED` | Camera list, active camera, or focus distance changed |
|
|
1270
|
+
| `RESOLUTION_CHANGED` / `RESERVED_RENDER_SIZE_CHANGED` | The render size or the reserve changed |
|
|
1271
|
+
| `STORAGE_CHANGED` / `TIMELINE_CHANGED` | On-disk storage or timeline keys changed |
|
|
1272
|
+
|
|
1273
|
+
Several of these used to be plain strings (`'ModelLoaded'`, `'RenderComplete'`, `'resolution_changed'`, …).
|
|
1274
|
+
The engine still dispatches each under its old name as well, until the next major; `LEGACY_EVENT_NAMES`
|
|
1275
|
+
maps new to old. Listening for a name the engine never dispatches logs a warning once, since such a
|
|
1276
|
+
listener fails silently otherwise.
|
|
1122
1277
|
|
|
1123
1278
|
### Advanced: Custom Pipeline Stages
|
|
1124
1279
|
|
|
@@ -1144,7 +1299,11 @@ class MyCustomStage extends RenderStage {
|
|
|
1144
1299
|
|
|
1145
1300
|
```js
|
|
1146
1301
|
// Core
|
|
1147
|
-
import { PathTracerApp, EngineEvents } from 'rayzee';
|
|
1302
|
+
import { PathTracerApp, EngineEvents, LEGACY_EVENT_NAMES } from 'rayzee';
|
|
1303
|
+
|
|
1304
|
+
// Platform services for hosts without a browser (see Running in Node)
|
|
1305
|
+
import { configurePlatform, getPlatform } from 'rayzee';
|
|
1306
|
+
import { nodePlatform, NodeWorker } from 'rayzee/node';
|
|
1148
1307
|
|
|
1149
1308
|
// Configuration & presets
|
|
1150
1309
|
import {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as e } from "./rolldown-runtime-CAFD8bLK.js";
|
|
2
|
-
import {
|
|
2
|
+
import { C as t, t as n, y as r } from "./ToneMapGPU-CNgexVto.js";
|
|
3
3
|
//#region src/neural/NeuralRetouch.js
|
|
4
4
|
var i = /* @__PURE__ */ e({
|
|
5
5
|
NeuralRetouch: () => m,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as e } from "./rolldown-runtime-CAFD8bLK.js";
|
|
2
|
-
import {
|
|
2
|
+
import { C as t, t as n, y as r } from "./ToneMapGPU-CNgexVto.js";
|
|
3
3
|
//#region src/EngineEvents.js
|
|
4
4
|
var i = {
|
|
5
5
|
RENDER_COMPLETE: "engine:renderComplete",
|
|
@@ -20,6 +20,9 @@ var i = {
|
|
|
20
20
|
OBJECT_TRANSFORM_START: "engine:objectTransformStart",
|
|
21
21
|
OBJECT_TRANSFORM_END: "engine:objectTransformEnd",
|
|
22
22
|
TRANSFORM_MODE_CHANGED: "engine:transformModeChanged",
|
|
23
|
+
CAMERA_SWITCHED: "engine:cameraSwitched",
|
|
24
|
+
CAMERAS_UPDATED: "engine:camerasUpdated",
|
|
25
|
+
FOCUS_CHANGED: "engine:focusChanged",
|
|
23
26
|
AUTO_FOCUS_UPDATED: "engine:autoFocusUpdated",
|
|
24
27
|
ORTHO_HEIGHT_UPDATED: "engine:orthoHeightUpdated",
|
|
25
28
|
AUTO_EXPOSURE_UPDATED: "engine:autoExposureUpdated",
|
|
@@ -35,21 +38,50 @@ var i = {
|
|
|
35
38
|
DISPOSE: "engine:dispose",
|
|
36
39
|
DEVICE_LOST: "engine:deviceLost",
|
|
37
40
|
ISSUE: "engine:issue",
|
|
41
|
+
MODEL_LOADED: "engine:modelLoaded",
|
|
42
|
+
OBJECT3D_LOADED: "engine:object3dLoaded",
|
|
43
|
+
MODEL_ADDED: "engine:modelAdded",
|
|
44
|
+
SCENE_OBJECT_REMOVED: "engine:sceneObjectRemoved",
|
|
45
|
+
SCENE_UNLOADED: "engine:sceneUnloaded",
|
|
46
|
+
SCENE_REBUILD: "engine:sceneRebuild",
|
|
47
|
+
SCENE_SPILLED: "engine:sceneSpilled",
|
|
38
48
|
SCENE_METADATA_APPLIED: "engine:sceneMetadataApplied",
|
|
49
|
+
ENVIRONMENT_LOADED: "engine:environmentLoaded",
|
|
50
|
+
TEXTURES_REPROCESSED: "engine:texturesReprocessed",
|
|
51
|
+
RESOLUTION_CHANGED: "engine:resolutionChanged",
|
|
52
|
+
RESERVED_RENDER_SIZE_CHANGED: "engine:reservedRenderSizeChanged",
|
|
39
53
|
STORAGE_CHANGED: "engine:storageChanged"
|
|
40
|
-
}, a =
|
|
41
|
-
|
|
42
|
-
|
|
54
|
+
}, a = Object.freeze({
|
|
55
|
+
[i.RENDER_COMPLETE]: "RenderComplete",
|
|
56
|
+
[i.RENDER_RESET]: "RenderReset",
|
|
57
|
+
[i.CAMERA_SWITCHED]: "CameraSwitched",
|
|
58
|
+
[i.CAMERAS_UPDATED]: "CamerasUpdated",
|
|
59
|
+
[i.FOCUS_CHANGED]: "focusChanged",
|
|
60
|
+
[i.MODEL_LOADED]: "ModelLoaded",
|
|
61
|
+
[i.OBJECT3D_LOADED]: "Object3DLoaded",
|
|
62
|
+
[i.MODEL_ADDED]: "ModelAdded",
|
|
63
|
+
[i.SCENE_OBJECT_REMOVED]: "SceneObjectRemoved",
|
|
64
|
+
[i.SCENE_UNLOADED]: "SceneUnloaded",
|
|
65
|
+
[i.SCENE_REBUILD]: "SceneRebuild",
|
|
66
|
+
[i.SCENE_SPILLED]: "SceneSpilled",
|
|
67
|
+
[i.SCENE_METADATA_APPLIED]: "SceneMetadataApplied",
|
|
68
|
+
[i.ENVIRONMENT_LOADED]: "EnvironmentLoaded",
|
|
69
|
+
[i.TEXTURES_REPROCESSED]: "TexturesReprocessed",
|
|
70
|
+
[i.RESOLUTION_CHANGED]: "resolution_changed",
|
|
71
|
+
[i.RESERVED_RENDER_SIZE_CHANGED]: "reserved_render_size_changed"
|
|
72
|
+
}), o = /* @__PURE__ */ e({
|
|
73
|
+
NeuralSuperRes: () => _,
|
|
74
|
+
SR_MAX_INPUT: () => l,
|
|
43
75
|
SR_SCALE: () => 2,
|
|
44
|
-
presentRGBA8: () =>
|
|
45
|
-
readDenoisedHalf: () =>
|
|
46
|
-
releaseDenoisedReader: () =>
|
|
47
|
-
renderUpscaled: () =>
|
|
48
|
-
}),
|
|
49
|
-
function
|
|
50
|
-
if (
|
|
76
|
+
presentRGBA8: () => x,
|
|
77
|
+
readDenoisedHalf: () => g,
|
|
78
|
+
releaseDenoisedReader: () => h,
|
|
79
|
+
renderUpscaled: () => y
|
|
80
|
+
}), s = 2, c = t("neural"), l = 2048, u = null;
|
|
81
|
+
function d() {
|
|
82
|
+
if (u) return u;
|
|
51
83
|
let { neuralRuntimeUrl: e, neuralAssetBaseUrl: t } = r();
|
|
52
|
-
return t ? (globalThis.__NEURAL_ASSET_BASE__ = t,
|
|
84
|
+
return t ? (globalThis.__NEURAL_ASSET_BASE__ = t, u = new Promise((t, n) => {
|
|
53
85
|
if (globalThis.NeuralRuntime?.getNativeSR) return t(globalThis.NeuralRuntime.getNativeSR());
|
|
54
86
|
let r = document.createElement("script");
|
|
55
87
|
r.src = e, r.onload = () => {
|
|
@@ -57,13 +89,13 @@ function u() {
|
|
|
57
89
|
if (!e) return n(/* @__PURE__ */ Error("Neural runtime loaded but exposed no super-resolution entry"));
|
|
58
90
|
t(e());
|
|
59
91
|
}, r.onerror = () => n(/* @__PURE__ */ Error(`Failed to load the neural runtime from ${e}`)), document.head.appendChild(r);
|
|
60
|
-
}),
|
|
92
|
+
}), u) : Promise.reject(/* @__PURE__ */ Error("No neural model location: set neuralAssetBaseUrl with configureAssets()"));
|
|
61
93
|
}
|
|
62
|
-
var
|
|
63
|
-
function
|
|
64
|
-
if (
|
|
94
|
+
var f = "\n@group(0) @binding(0) var src: texture_2d<f32>;\n@group(0) @binding(1) var<storage, read_write> dst: array<u32>;\n\n@compute @workgroup_size(8, 8)\nfn main( @builtin(global_invocation_id) gid: vec3<u32> ) {\n let size = textureDimensions( src );\n if ( gid.x >= size.x || gid.y >= size.y ) { return; }\n let c = textureLoad( src, vec2<i32>( gid.xy ), 0 ).rgb;\n let a = ( gid.y * size.x + gid.x ) * 2u;\n dst[ a ] = pack2x16float( c.rg );\n dst[ a + 1u ] = pack2x16float( vec2<f32>( c.b, 1.0 ) );\n}\n", p = null;
|
|
95
|
+
function m(e, t, n) {
|
|
96
|
+
if (p && (p.device !== e || p.width !== t || p.height !== n) && h(), p) return p;
|
|
65
97
|
let r = t * n * 8;
|
|
66
|
-
return
|
|
98
|
+
return p = {
|
|
67
99
|
device: e,
|
|
68
100
|
width: t,
|
|
69
101
|
height: n,
|
|
@@ -83,20 +115,20 @@ function p(e, t, n) {
|
|
|
83
115
|
compute: {
|
|
84
116
|
module: e.createShaderModule({
|
|
85
117
|
label: "rayzee:neural-sr-pack",
|
|
86
|
-
code:
|
|
118
|
+
code: f
|
|
87
119
|
}),
|
|
88
120
|
entryPoint: "main"
|
|
89
121
|
}
|
|
90
122
|
})
|
|
91
|
-
},
|
|
123
|
+
}, p;
|
|
92
124
|
}
|
|
93
|
-
function
|
|
94
|
-
|
|
125
|
+
function h() {
|
|
126
|
+
p?.storage.destroy(), p?.map.destroy(), p = null;
|
|
95
127
|
}
|
|
96
|
-
async function
|
|
128
|
+
async function g(e) {
|
|
97
129
|
let t = e?._outGPUTexture;
|
|
98
130
|
if (!t) throw Error("NeuralSuperRes: no denoised frame — enable OIDN and finish a render first");
|
|
99
|
-
let n = e.gpuDevice, { width: r, height: i } = e._outTexSize, a =
|
|
131
|
+
let n = e.gpuDevice, { width: r, height: i } = e._outTexSize, a = m(n, r, i), o = n.createBindGroup({
|
|
100
132
|
layout: a.pipeline.getBindGroupLayout(0),
|
|
101
133
|
entries: [{
|
|
102
134
|
binding: 0,
|
|
@@ -114,7 +146,7 @@ async function h(e) {
|
|
|
114
146
|
height: i
|
|
115
147
|
};
|
|
116
148
|
}
|
|
117
|
-
var
|
|
149
|
+
var _ = class e {
|
|
118
150
|
constructor(e, t) {
|
|
119
151
|
this.sr = e, this.canvas = t, this.slot = 0, this._color = null, this._depth = null, this._motion = null, this._toneMapper = null, this.disposed = !1;
|
|
120
152
|
}
|
|
@@ -135,8 +167,8 @@ var g = class e {
|
|
|
135
167
|
}
|
|
136
168
|
static async create({ width: t, height: n, onProgress: r = () => {} } = {}) {
|
|
137
169
|
if (!Number.isInteger(t) || !Number.isInteger(n) || t < 1 || n < 1) throw Error("NeuralSuperRes.create: width and height must be positive integers");
|
|
138
|
-
if (t > 2048 || n > 2048) throw Error(`NeuralSuperRes.create: input capped at ${
|
|
139
|
-
let i = await
|
|
170
|
+
if (t > 2048 || n > 2048) throw Error(`NeuralSuperRes.create: input capped at ${l}px per side (${l * 2}px output), got ${t}x${n}. Above that the runtime's history plane exceeds maxTextureDimension2D and it fails later with an unrelated bind-group error.`);
|
|
171
|
+
let i = await d(), a = document.createElement("canvas");
|
|
140
172
|
a.style.cssText = "position:fixed;left:-10000px;top:0;pointer-events:none", document.body.appendChild(a);
|
|
141
173
|
let o;
|
|
142
174
|
try {
|
|
@@ -199,12 +231,12 @@ var g = class e {
|
|
|
199
231
|
try {
|
|
200
232
|
this.sr.destroy();
|
|
201
233
|
} catch (e) {
|
|
202
|
-
|
|
234
|
+
c.warn("super resolution teardown:", e);
|
|
203
235
|
}
|
|
204
236
|
}
|
|
205
237
|
}
|
|
206
238
|
};
|
|
207
|
-
function
|
|
239
|
+
function v(e, t, n) {
|
|
208
240
|
return new Promise((r, a) => {
|
|
209
241
|
let o = !1, s = (t, n) => {
|
|
210
242
|
o || (o = !0, e.removeEventListener(i.DENOISING_END, l), clearInterval(u), clearTimeout(d), t ? r() : a(n));
|
|
@@ -217,7 +249,7 @@ function _(e, t, n) {
|
|
|
217
249
|
}, 200), d = setTimeout(() => s(!1, /* @__PURE__ */ Error("NeuralSuperRes: timed out waiting for the render to finish")), n);
|
|
218
250
|
});
|
|
219
251
|
}
|
|
220
|
-
async function
|
|
252
|
+
async function y(e, { outputWidth: t, outputHeight: n, samples: r = 64, upscaler: i = null, timeoutMs: a = 6e5, present: o = !0, onProgress: s = () => {} } = {}) {
|
|
221
253
|
if (t % 2 || n % 2) throw Error(`renderUpscaled: output must be divisible by 2, got ${t}x${n}`);
|
|
222
254
|
let c = t / 2, l = n / 2, u = e.stages?.pathTracer, d = {
|
|
223
255
|
width: u?.width,
|
|
@@ -229,7 +261,7 @@ async function v(e, { outputWidth: t, outputHeight: n, samples: r = 64, upscaler
|
|
|
229
261
|
try {
|
|
230
262
|
if (!p) {
|
|
231
263
|
let e = performance.now();
|
|
232
|
-
s("Loading the super-resolution model"), p = await
|
|
264
|
+
s("Loading the super-resolution model"), p = await _.create({
|
|
233
265
|
width: c,
|
|
234
266
|
height: l,
|
|
235
267
|
onProgress: s
|
|
@@ -237,17 +269,17 @@ async function v(e, { outputWidth: t, outputHeight: n, samples: r = 64, upscaler
|
|
|
237
269
|
} else if (p.inputWidth !== c || p.inputHeight !== l) throw Error(`renderUpscaled: upscaler is ${p.inputWidth}x${p.inputHeight}, need ${c}x${l}`);
|
|
238
270
|
let i = performance.now();
|
|
239
271
|
s(`Tracing ${c}x${l} at ${r} spp`), e.pauseRendering = !1, e.setCanvasSize(c, l), await new Promise((e) => setTimeout(e, 300)), e.denoisingManager.applyOIDNEnabled(!0), e.settings.set("maxSamples", r);
|
|
240
|
-
let u =
|
|
272
|
+
let u = v(e, r, a);
|
|
241
273
|
e.reset(), await u, e.pauseRendering = !0, m.render = performance.now() - i, s("Reading the denoised frame");
|
|
242
|
-
let d = performance.now(), f = await
|
|
274
|
+
let d = performance.now(), f = await g(e.denoisingManager?.denoiser);
|
|
243
275
|
m.read = performance.now() - d, s(`Upscaling to ${t}x${n}`);
|
|
244
|
-
let
|
|
276
|
+
let h = {
|
|
245
277
|
exposure: e.renderer.toneMappingExposure,
|
|
246
278
|
toneMapping: e.renderer.toneMapping,
|
|
247
279
|
saturation: e.settings.get("saturation") ?? 1
|
|
248
|
-
}, { rgba8:
|
|
249
|
-
return m.upscale = C, m.tonemap = w, o &&
|
|
250
|
-
rgba8:
|
|
280
|
+
}, { rgba8: y, width: x, height: S, ms: C, toneMs: w } = await p.upscaleToRGBA8(f, h);
|
|
281
|
+
return m.upscale = C, m.tonemap = w, o && b(e.denoisingManager?.upscalerCanvas, y, x, S), {
|
|
282
|
+
rgba8: y,
|
|
251
283
|
width: x,
|
|
252
284
|
height: S,
|
|
253
285
|
timings: m,
|
|
@@ -259,11 +291,11 @@ async function v(e, { outputWidth: t, outputHeight: n, samples: r = 64, upscaler
|
|
|
259
291
|
d.width && d.height && e.setCanvasSize(d.width, d.height), d.maxSamples !== void 0 && e.settings?.set?.("maxSamples", d.maxSamples), d.finalDenoise !== void 0 && e.denoisingManager?.applyOIDNEnabled(d.finalDenoise), e.pauseRendering = d.paused;
|
|
260
292
|
}
|
|
261
293
|
}
|
|
262
|
-
function
|
|
294
|
+
function b(e, t, n, r) {
|
|
263
295
|
return e ? (e.width = n, e.height = r, e.getContext("2d").putImageData(new ImageData(new Uint8ClampedArray(t.buffer ?? t), n, r), 0, 0), e.style.display = "block", !0) : !1;
|
|
264
296
|
}
|
|
265
|
-
function
|
|
266
|
-
return
|
|
297
|
+
function x(e, t, n, r) {
|
|
298
|
+
return b(e, t, n, r);
|
|
267
299
|
}
|
|
268
300
|
//#endregion
|
|
269
|
-
export { i as a,
|
|
301
|
+
export { i as a, y as i, l as n, a as o, s as r, o as t };
|