@tinypoly/gpu-renderer 0.0.0-stage → 0.0.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/ARCHITECTURE.md +334 -0
- package/LICENSE +21 -0
- package/README.md +258 -2
- package/dist/accumulator.d.ts +27 -0
- package/dist/accumulator.d.ts.map +1 -0
- package/dist/accumulator.js +57 -0
- package/dist/accumulator.js.map +1 -0
- package/dist/bvh.d.ts +26 -0
- package/dist/bvh.d.ts.map +1 -0
- package/dist/bvh.js +207 -0
- package/dist/bvh.js.map +1 -0
- package/dist/camera.d.ts +29 -0
- package/dist/camera.d.ts.map +1 -0
- package/dist/camera.js +48 -0
- package/dist/camera.js.map +1 -0
- package/dist/canvas.d.ts +9 -0
- package/dist/canvas.d.ts.map +1 -0
- package/dist/canvas.js +11 -0
- package/dist/canvas.js.map +1 -0
- package/dist/denoise.d.ts +69 -0
- package/dist/denoise.d.ts.map +1 -0
- package/dist/denoise.js +455 -0
- package/dist/denoise.js.map +1 -0
- package/dist/device.d.ts +37 -0
- package/dist/device.d.ts.map +1 -0
- package/dist/device.js +52 -0
- package/dist/device.js.map +1 -0
- package/dist/dispatchScheduler.d.ts +43 -0
- package/dist/dispatchScheduler.d.ts.map +1 -0
- package/dist/dispatchScheduler.js +49 -0
- package/dist/dispatchScheduler.js.map +1 -0
- package/dist/environment.d.ts +39 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/environment.js +168 -0
- package/dist/environment.js.map +1 -0
- package/dist/frameLayout.d.ts +4 -0
- package/dist/frameLayout.d.ts.map +1 -0
- package/dist/frameLayout.js +58 -0
- package/dist/frameLayout.js.map +1 -0
- package/dist/geometry.d.ts +75 -0
- package/dist/geometry.d.ts.map +1 -0
- package/dist/geometry.js +671 -0
- package/dist/geometry.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/lights.d.ts +64 -0
- package/dist/lights.d.ts.map +1 -0
- package/dist/lights.js +264 -0
- package/dist/lights.js.map +1 -0
- package/dist/materialLayout.d.ts +143 -0
- package/dist/materialLayout.d.ts.map +1 -0
- package/dist/materialLayout.js +297 -0
- package/dist/materialLayout.js.map +1 -0
- package/dist/math.d.ts +10 -0
- package/dist/math.d.ts.map +1 -0
- package/dist/math.js +40 -0
- package/dist/math.js.map +1 -0
- package/dist/pathTracePass.d.ts +27 -0
- package/dist/pathTracePass.d.ts.map +1 -0
- package/dist/pathTracePass.js +83 -0
- package/dist/pathTracePass.js.map +1 -0
- package/dist/planarCapture.d.ts +38 -0
- package/dist/planarCapture.d.ts.map +1 -0
- package/dist/planarCapture.js +163 -0
- package/dist/planarCapture.js.map +1 -0
- package/dist/presentPass.d.ts +31 -0
- package/dist/presentPass.d.ts.map +1 -0
- package/dist/presentPass.js +78 -0
- package/dist/presentPass.js.map +1 -0
- package/dist/renderer.d.ts +196 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +839 -0
- package/dist/renderer.js.map +1 -0
- package/dist/sceneAssets.d.ts +24 -0
- package/dist/sceneAssets.d.ts.map +1 -0
- package/dist/sceneAssets.js +77 -0
- package/dist/sceneAssets.js.map +1 -0
- package/dist/sceneUpload.d.ts +61 -0
- package/dist/sceneUpload.d.ts.map +1 -0
- package/dist/sceneUpload.js +98 -0
- package/dist/sceneUpload.js.map +1 -0
- package/dist/shaderModule.d.ts +7 -0
- package/dist/shaderModule.d.ts.map +1 -0
- package/dist/shaderModule.js +19 -0
- package/dist/shaderModule.js.map +1 -0
- package/dist/shaderProgram.d.ts +124 -0
- package/dist/shaderProgram.d.ts.map +1 -0
- package/dist/shaderProgram.js +1030 -0
- package/dist/shaderProgram.js.map +1 -0
- package/dist/structLayout.d.ts +28 -0
- package/dist/structLayout.d.ts.map +1 -0
- package/dist/structLayout.js +58 -0
- package/dist/structLayout.js.map +1 -0
- package/dist/texturePack.d.ts +54 -0
- package/dist/texturePack.d.ts.map +1 -0
- package/dist/texturePack.js +86 -0
- package/dist/texturePack.js.map +1 -0
- package/dist/upstream.d.ts +7 -0
- package/dist/upstream.d.ts.map +1 -0
- package/dist/upstream.js +11 -0
- package/dist/upstream.js.map +1 -0
- package/dist/volumetric.d.ts +10 -0
- package/dist/volumetric.d.ts.map +1 -0
- package/dist/volumetric.js +34 -0
- package/dist/volumetric.js.map +1 -0
- package/dist/wgsl/bsdf.wgsl.d.ts +13 -0
- package/dist/wgsl/bsdf.wgsl.d.ts.map +1 -0
- package/dist/wgsl/bsdf.wgsl.js +619 -0
- package/dist/wgsl/bsdf.wgsl.js.map +1 -0
- package/dist/wgsl/bvh.wgsl.d.ts +3 -0
- package/dist/wgsl/bvh.wgsl.d.ts.map +1 -0
- package/dist/wgsl/bvh.wgsl.js +218 -0
- package/dist/wgsl/bvh.wgsl.js.map +1 -0
- package/dist/wgsl/common.wgsl.d.ts +3 -0
- package/dist/wgsl/common.wgsl.d.ts.map +1 -0
- package/dist/wgsl/common.wgsl.js +193 -0
- package/dist/wgsl/common.wgsl.js.map +1 -0
- package/dist/wgsl/environment.wgsl.d.ts +10 -0
- package/dist/wgsl/environment.wgsl.d.ts.map +1 -0
- package/dist/wgsl/environment.wgsl.js +145 -0
- package/dist/wgsl/environment.wgsl.js.map +1 -0
- package/dist/wgsl/lights.wgsl.d.ts +10 -0
- package/dist/wgsl/lights.wgsl.d.ts.map +1 -0
- package/dist/wgsl/lights.wgsl.js +434 -0
- package/dist/wgsl/lights.wgsl.js.map +1 -0
- package/dist/wgsl/pathTrace.wgsl.d.ts +14 -0
- package/dist/wgsl/pathTrace.wgsl.d.ts.map +1 -0
- package/dist/wgsl/pathTrace.wgsl.js +1020 -0
- package/dist/wgsl/pathTrace.wgsl.js.map +1 -0
- package/dist/wgsl/present.wgsl.d.ts +7 -0
- package/dist/wgsl/present.wgsl.d.ts.map +1 -0
- package/dist/wgsl/present.wgsl.js +154 -0
- package/dist/wgsl/present.wgsl.js.map +1 -0
- package/dist/wgsl/random.wgsl.d.ts +9 -0
- package/dist/wgsl/random.wgsl.d.ts.map +1 -0
- package/dist/wgsl/random.wgsl.js +91 -0
- package/dist/wgsl/random.wgsl.js.map +1 -0
- package/dist/wgsl/sampling.wgsl.d.ts +3 -0
- package/dist/wgsl/sampling.wgsl.d.ts.map +1 -0
- package/dist/wgsl/sampling.wgsl.js +31 -0
- package/dist/wgsl/sampling.wgsl.js.map +1 -0
- package/dist/wgsl/shaderSupport.wgsl.d.ts +6 -0
- package/dist/wgsl/shaderSupport.wgsl.d.ts.map +1 -0
- package/dist/wgsl/shaderSupport.wgsl.js +120 -0
- package/dist/wgsl/shaderSupport.wgsl.js.map +1 -0
- package/dist/wgsl/surface.wgsl.d.ts +10 -0
- package/dist/wgsl/surface.wgsl.d.ts.map +1 -0
- package/dist/wgsl/surface.wgsl.js +794 -0
- package/dist/wgsl/surface.wgsl.js.map +1 -0
- package/dist/wgsl/texture.wgsl.d.ts +11 -0
- package/dist/wgsl/texture.wgsl.d.ts.map +1 -0
- package/dist/wgsl/texture.wgsl.js +132 -0
- package/dist/wgsl/texture.wgsl.js.map +1 -0
- package/package.json +76 -3
- package/src/accumulator.ts +64 -0
- package/src/bvh.ts +272 -0
- package/src/camera.ts +77 -0
- package/src/canvas.ts +13 -0
- package/src/denoise.ts +514 -0
- package/src/device.ts +81 -0
- package/src/dispatchScheduler.ts +71 -0
- package/src/environment.ts +233 -0
- package/src/frameLayout.ts +59 -0
- package/src/geometry.ts +899 -0
- package/src/index.ts +61 -0
- package/src/lights.ts +365 -0
- package/src/materialLayout.ts +408 -0
- package/src/math.ts +48 -0
- package/src/pathTracePass.ts +97 -0
- package/src/planarCapture.ts +225 -0
- package/src/presentPass.ts +99 -0
- package/src/renderer.ts +1117 -0
- package/src/sceneAssets.ts +118 -0
- package/src/sceneUpload.ts +185 -0
- package/src/shaderModule.ts +19 -0
- package/src/shaderProgram.ts +1370 -0
- package/src/structLayout.ts +93 -0
- package/src/texturePack.ts +152 -0
- package/src/upstream.ts +56 -0
- package/src/volumetric.ts +42 -0
- package/src/wgsl/bsdf.wgsl.ts +618 -0
- package/src/wgsl/bvh.wgsl.ts +217 -0
- package/src/wgsl/common.wgsl.ts +193 -0
- package/src/wgsl/environment.wgsl.ts +144 -0
- package/src/wgsl/lights.wgsl.ts +433 -0
- package/src/wgsl/pathTrace.wgsl.ts +1019 -0
- package/src/wgsl/present.wgsl.ts +153 -0
- package/src/wgsl/random.wgsl.ts +90 -0
- package/src/wgsl/sampling.wgsl.ts +30 -0
- package/src/wgsl/shaderSupport.wgsl.ts +119 -0
- package/src/wgsl/surface.wgsl.ts +793 -0
- package/src/wgsl/texture.wgsl.ts +131 -0
package/dist/renderer.js
ADDED
|
@@ -0,0 +1,839 @@
|
|
|
1
|
+
import { PerspectiveCamera } from "three";
|
|
2
|
+
import { Accumulator } from "./accumulator.js";
|
|
3
|
+
import { buildBvh } from "./bvh.js";
|
|
4
|
+
import { cameraMatrices } from "./camera.js";
|
|
5
|
+
import { requestRenderDevice } from "./device.js";
|
|
6
|
+
import { loadEnvironment } from "./environment.js";
|
|
7
|
+
import { DispatchScheduler } from "./dispatchScheduler.js";
|
|
8
|
+
import { packGeometry } from "./geometry.js";
|
|
9
|
+
import { packLights } from "./lights.js";
|
|
10
|
+
import { NO_PROGRAM } from "./materialLayout.js";
|
|
11
|
+
import { NO_TEXTURE, planTextures, writeTextures } from "./texturePack.js";
|
|
12
|
+
import { PathTracePass } from "./pathTracePass.js";
|
|
13
|
+
import { PresentPass } from "./presentPass.js";
|
|
14
|
+
import { CONVERGED_ERROR, DEFAULT_DENOISER, DenoisePass, mergeDenoiser, } from "./denoise.js";
|
|
15
|
+
import { CaptureResolvePass, planarCaptures } from "./planarCapture.js";
|
|
16
|
+
import { prepareSceneAssets } from "./sceneAssets.js";
|
|
17
|
+
import { packVolumetricLights, VOLUMETRIC_WORDS } from "./volumetric.js";
|
|
18
|
+
import { environmentPower, uploadScene } from "./sceneUpload.js";
|
|
19
|
+
import { DEFAULT_RENDER_ENVIRONMENT, DEFAULT_RENDER_SETTINGS, encodeColor, EventEmitter, mergeSettings, serializeCamera, serializeScene, srgbToLinear, } from "./upstream.js";
|
|
20
|
+
function abortError(message) {
|
|
21
|
+
return Object.assign(new Error(message), { name: "AbortError" });
|
|
22
|
+
}
|
|
23
|
+
/** `render()` rejects with this when a newer frame, `cancel()` or `dispose()` replaced the frame it started. */
|
|
24
|
+
export const isAbortError = (error) => error instanceof Error && error.name === "AbortError";
|
|
25
|
+
/** `#rrggbb` to linear rgb, the space the renderer works in. */
|
|
26
|
+
function linearColor(color, fallback) {
|
|
27
|
+
const match = /^#?([0-9a-f]{6})$/i.exec(color ?? "");
|
|
28
|
+
if (!match)
|
|
29
|
+
return [...fallback];
|
|
30
|
+
const value = parseInt(match[1], 16);
|
|
31
|
+
return [(value >> 16) & 255, (value >> 8) & 255, value & 255].map(channel => srgbToLinear(channel / 255));
|
|
32
|
+
}
|
|
33
|
+
/** Hands the thread back to the page for a moment, so input and painting go on between submits. */
|
|
34
|
+
const yieldToPage = () => new Promise(resolve => setTimeout(resolve, 0));
|
|
35
|
+
/**
|
|
36
|
+
* How often the canvas is drawn and the page gets the thread back while a frame traces: about once a display frame.
|
|
37
|
+
* Waiting for an animation frame after every band would leave the device idle for most of each one.
|
|
38
|
+
*/
|
|
39
|
+
const SHOW_EVERY_MS = 16;
|
|
40
|
+
const EMPTY_SCENE = { meshes: [], lights: [], textures: [] };
|
|
41
|
+
/** Paths every pixel takes before adaptive sampling may stop it: below this its variance says too little. */
|
|
42
|
+
const ADAPTIVE_MIN = 32;
|
|
43
|
+
/** How many times the target a pixel that has not converged may take, when converged pixels leave the work to it. */
|
|
44
|
+
const ADAPTIVE_REACH = 4;
|
|
45
|
+
/**
|
|
46
|
+
* The error under which a pixel stops: its luminance's standard error over the square root of its luminance, about
|
|
47
|
+
* half a step of an 8 bit sRGB image in the mid tones.
|
|
48
|
+
*/
|
|
49
|
+
const ADAPTIVE_THRESHOLD = CONVERGED_ERROR;
|
|
50
|
+
/** Contiguous runs of the given sorted rows, as [first, end) pairs. */
|
|
51
|
+
function rowRanges(rows) {
|
|
52
|
+
const ranges = [];
|
|
53
|
+
for (const row of rows) {
|
|
54
|
+
const last = ranges[ranges.length - 1];
|
|
55
|
+
if (last && last[1] === row)
|
|
56
|
+
last[1] = row + 1;
|
|
57
|
+
else
|
|
58
|
+
ranges.push([row, row + 1]);
|
|
59
|
+
}
|
|
60
|
+
return ranges;
|
|
61
|
+
}
|
|
62
|
+
/** How many paths per pixel a planar capture gets. It is a texture a shader distorts, not the image itself. */
|
|
63
|
+
const CAPTURE_SAMPLES = 32;
|
|
64
|
+
/**
|
|
65
|
+
* Path traces a Three.js scene on the GPU, behind the same API as `CpuRenderer`. The setters record the inputs;
|
|
66
|
+
* `render()` opens a frame with whatever is current and accumulates samples until it reaches `settings.samples`.
|
|
67
|
+
*/
|
|
68
|
+
export class GpuRenderer extends EventEmitter {
|
|
69
|
+
current;
|
|
70
|
+
environment;
|
|
71
|
+
// Serializing walks the whole scene, so it is started by the setter and awaited by the frame that needs it.
|
|
72
|
+
scene = Promise.resolve(EMPTY_SCENE);
|
|
73
|
+
// A LightProbe's harmonics, which the snapshot does not carry: read from the live scene when there is one.
|
|
74
|
+
probe = new Array(27).fill(0);
|
|
75
|
+
// A Three camera is kept by reference and read at every `render()`, as the CPU renderer does; a snapshot stays fixed.
|
|
76
|
+
camera = null;
|
|
77
|
+
width;
|
|
78
|
+
height;
|
|
79
|
+
pixelRatio;
|
|
80
|
+
output = null;
|
|
81
|
+
frame = null;
|
|
82
|
+
isPaused = false;
|
|
83
|
+
resumed = null;
|
|
84
|
+
unpaused = Promise.resolve();
|
|
85
|
+
ready = null;
|
|
86
|
+
gpu = null;
|
|
87
|
+
accumulator = null;
|
|
88
|
+
pathTrace = null;
|
|
89
|
+
present = null;
|
|
90
|
+
buffers = null;
|
|
91
|
+
packed = null;
|
|
92
|
+
sceneDirty = true;
|
|
93
|
+
// Compute bakes, the procedural sky and translated shaders: prepared once per scene.
|
|
94
|
+
assets = null;
|
|
95
|
+
// What the packed triangles were built for, when points, lines, sprites or vertex shaders tie them to the camera.
|
|
96
|
+
packedView = "";
|
|
97
|
+
// Where the packed scene's own extra words went: the sky's uniforms and the volumetric lights.
|
|
98
|
+
target = null;
|
|
99
|
+
// Whether the scene has planar captures, which are taken from the camera and so follow it.
|
|
100
|
+
planar = false;
|
|
101
|
+
resolve = null;
|
|
102
|
+
denoisePass = null;
|
|
103
|
+
denoiserSettings = { ...DEFAULT_DENOISER };
|
|
104
|
+
// The size of the last frame that finished, which setDenoiser filters again; null while a frame traces.
|
|
105
|
+
finished = null;
|
|
106
|
+
// Whether the denoiser's output holds this frame yet: until it does, the canvas shows the accumulation itself.
|
|
107
|
+
denoised = false;
|
|
108
|
+
// How large a pixel is for the camera of the frame, which the specular filter sizes its footprint by.
|
|
109
|
+
denoiseView = { pixelScale: 1, orthographic: false };
|
|
110
|
+
// Reading an environment image is a fetch, so it is started once and awaited by the frame that needs it.
|
|
111
|
+
environmentSource = "";
|
|
112
|
+
environmentImage = Promise.resolve(null);
|
|
113
|
+
canvas = null;
|
|
114
|
+
context = null;
|
|
115
|
+
scheduler;
|
|
116
|
+
revision = 0;
|
|
117
|
+
frameIndex = 0;
|
|
118
|
+
disposed = false;
|
|
119
|
+
constructor(options) {
|
|
120
|
+
super();
|
|
121
|
+
this.width = Math.max(1, Math.floor(options.width));
|
|
122
|
+
this.height = Math.max(1, Math.floor(options.height));
|
|
123
|
+
this.pixelRatio = checkedPixelRatio(options.pixelRatio ?? 1);
|
|
124
|
+
this.current = mergeSettings(DEFAULT_RENDER_SETTINGS, options.settings);
|
|
125
|
+
this.denoiserSettings = mergeDenoiser(DEFAULT_DENOISER, options.denoiser);
|
|
126
|
+
this.environment = DEFAULT_RENDER_ENVIRONMENT;
|
|
127
|
+
this.scheduler = new DispatchScheduler({ maxRows: this.renderHeight });
|
|
128
|
+
this.setEnvironment(options.environment ?? DEFAULT_RENDER_ENVIRONMENT);
|
|
129
|
+
if (options.scene)
|
|
130
|
+
this.setScene(options.scene);
|
|
131
|
+
if (options.camera)
|
|
132
|
+
this.setCamera(options.camera);
|
|
133
|
+
}
|
|
134
|
+
/** The settings the next frame renders with. */
|
|
135
|
+
get settings() {
|
|
136
|
+
return this.current;
|
|
137
|
+
}
|
|
138
|
+
/** How the denoiser filters. */
|
|
139
|
+
get denoiser() {
|
|
140
|
+
return this.denoiserSettings;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Merges the given fields into the denoiser's settings. Filtering costs nothing like tracing, so a finished image is
|
|
144
|
+
* filtered again at once, without tracing anything: `image` and the canvas take the result and `pixels` fires. A frame
|
|
145
|
+
* in flight uses them from its next pass on.
|
|
146
|
+
*/
|
|
147
|
+
setDenoiser(input) {
|
|
148
|
+
this.denoiserSettings = mergeDenoiser(this.denoiserSettings, input);
|
|
149
|
+
this.refilter();
|
|
150
|
+
}
|
|
151
|
+
/** Filters the finished image again with the current denoiser settings, or shows it unfiltered when it is off. */
|
|
152
|
+
refilter() {
|
|
153
|
+
const finished = this.finished, gpu = this.gpu;
|
|
154
|
+
if (!finished || !gpu || !this.accumulator || this.rendering)
|
|
155
|
+
return;
|
|
156
|
+
const revision = this.revision;
|
|
157
|
+
const denoise = this.current.globalIllumination.denoise;
|
|
158
|
+
void (async () => {
|
|
159
|
+
const encoder = gpu.device.createCommandEncoder();
|
|
160
|
+
if (denoise)
|
|
161
|
+
this.encodeDenoise(gpu, encoder, finished.width, finished.height);
|
|
162
|
+
else
|
|
163
|
+
this.denoised = false;
|
|
164
|
+
this.encodePresent(encoder, finished.width, finished.height);
|
|
165
|
+
gpu.device.queue.submit([encoder.finish()]);
|
|
166
|
+
const image = await this.readImage(gpu, finished.width, finished.height, denoise ? this.denoisePass?.output ?? null : null);
|
|
167
|
+
// A frame started meanwhile owns the image now.
|
|
168
|
+
if (revision !== this.revision || this.rendering)
|
|
169
|
+
return;
|
|
170
|
+
this.output = image;
|
|
171
|
+
this.emit("pixels", { x: 0, y: 0, ...image });
|
|
172
|
+
})().catch((error) => this.emit("error", error instanceof Error ? error : new Error(String(error))));
|
|
173
|
+
}
|
|
174
|
+
/** A frame is in flight: `render()` was called and has not settled. */
|
|
175
|
+
get rendering() {
|
|
176
|
+
return this.frame !== null;
|
|
177
|
+
}
|
|
178
|
+
get paused() {
|
|
179
|
+
return this.isPaused;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* The image of the last finished frame, or an empty one of the render size while a frame traces; `null` before the
|
|
183
|
+
* first frame starts. Each frame allocates a new buffer, so a reference taken at `complete` stays as it is.
|
|
184
|
+
*/
|
|
185
|
+
get image() {
|
|
186
|
+
return this.output;
|
|
187
|
+
}
|
|
188
|
+
get renderWidth() {
|
|
189
|
+
return Math.max(1, Math.floor(this.width * this.pixelRatio * this.current.renderScale));
|
|
190
|
+
}
|
|
191
|
+
get renderHeight() {
|
|
192
|
+
return Math.max(1, Math.floor(this.height * this.pixelRatio * this.current.renderScale));
|
|
193
|
+
}
|
|
194
|
+
/** Image size in CSS pixels for the next frame; `pixelRatio` stays as it is when omitted. */
|
|
195
|
+
setSize(width, height, pixelRatio = this.pixelRatio) {
|
|
196
|
+
this.width = Math.max(1, Math.floor(width));
|
|
197
|
+
this.height = Math.max(1, Math.floor(height));
|
|
198
|
+
this.pixelRatio = checkedPixelRatio(pixelRatio);
|
|
199
|
+
}
|
|
200
|
+
setPixelRatio(pixelRatio) {
|
|
201
|
+
this.pixelRatio = checkedPixelRatio(pixelRatio);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Takes a Three scene, serialized in the background, or the result of `serializeScene`. The live scene is not read
|
|
205
|
+
* again until this is called once more.
|
|
206
|
+
*/
|
|
207
|
+
setScene(scene) {
|
|
208
|
+
this.probe = "isScene" in scene && scene.isScene ? lightProbes(scene) : new Array(27).fill(0);
|
|
209
|
+
this.scene = "isScene" in scene && scene.isScene
|
|
210
|
+
? serializeLive(scene)
|
|
211
|
+
: Promise.resolve("meshes" in scene ? scene : scene.scene);
|
|
212
|
+
this.sceneDirty = true;
|
|
213
|
+
this.assets = null;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* A Three camera is read at every `render()`, so moving it or changing its aspect only needs a new frame.
|
|
217
|
+
* A `CameraState` snapshot is used as it is.
|
|
218
|
+
*/
|
|
219
|
+
setCamera(camera) {
|
|
220
|
+
this.camera = camera;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Merges the given fields into the current settings, group by group. Turning the denoiser on or off also filters the
|
|
224
|
+
* finished image again at once, as `setDenoiser` does, since that needs no tracing.
|
|
225
|
+
*/
|
|
226
|
+
setSettings(settings) {
|
|
227
|
+
const denoise = this.current.globalIllumination.denoise;
|
|
228
|
+
this.current = mergeSettings(this.current, settings);
|
|
229
|
+
if (this.current.globalIllumination.denoise !== denoise)
|
|
230
|
+
this.refilter();
|
|
231
|
+
}
|
|
232
|
+
setEnvironment(environment) {
|
|
233
|
+
this.environment = environment;
|
|
234
|
+
// Only the image itself has to be read again; its strength and rotation are uniforms.
|
|
235
|
+
const source = environment.kind === "hdri" ? environment.url : "";
|
|
236
|
+
if (source === this.environmentSource)
|
|
237
|
+
return;
|
|
238
|
+
this.environmentSource = source;
|
|
239
|
+
this.environmentImage = loadEnvironment(environment).catch(() => null);
|
|
240
|
+
// The image shares a buffer with the scene's own textures, so the upload has to happen again.
|
|
241
|
+
this.sceneDirty = true;
|
|
242
|
+
}
|
|
243
|
+
/** Used by `attachCanvas`; pass `null` to detach. */
|
|
244
|
+
setOutputCanvas(canvas) {
|
|
245
|
+
this.canvas = canvas;
|
|
246
|
+
this.context = null;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Starts a frame with the current scene, camera, settings, environment and size, discarding any frame in
|
|
250
|
+
* progress, and accumulates `settings.samples` samples per pixel. Resolves with the finished image. Rejects with an
|
|
251
|
+
* `AbortError` when a later `render()`, `cancel()` or `dispose()` replaces the frame, and with the error when the
|
|
252
|
+
* frame fails.
|
|
253
|
+
*/
|
|
254
|
+
render() {
|
|
255
|
+
if (this.disposed)
|
|
256
|
+
return Promise.reject(new Error("GPU renderer: this renderer was disposed."));
|
|
257
|
+
// The camera as it is at this call, even if it moves while the scene is still serializing.
|
|
258
|
+
const camera = !this.camera ? defaultCamera()
|
|
259
|
+
: "isCamera" in this.camera ? serializeCamera(this.camera) : this.camera;
|
|
260
|
+
this.abort("A newer render() replaced this frame.");
|
|
261
|
+
const revision = ++this.revision;
|
|
262
|
+
const settled = new Promise((resolve, reject) => {
|
|
263
|
+
this.frame = { resolve, reject };
|
|
264
|
+
});
|
|
265
|
+
this.accumulate(revision, camera).then(image => {
|
|
266
|
+
if (revision !== this.revision)
|
|
267
|
+
return;
|
|
268
|
+
this.emit("complete");
|
|
269
|
+
this.settle()?.resolve(image);
|
|
270
|
+
}, (error) => {
|
|
271
|
+
// A replaced frame was already rejected by whatever replaced it.
|
|
272
|
+
if (revision !== this.revision || isAbortError(error))
|
|
273
|
+
return;
|
|
274
|
+
this.fail(error instanceof Error ? error : new Error(String(error)));
|
|
275
|
+
});
|
|
276
|
+
return settled;
|
|
277
|
+
}
|
|
278
|
+
/** Stops between two dispatches; `resume()` continues the same frame. */
|
|
279
|
+
pause() {
|
|
280
|
+
if (this.isPaused)
|
|
281
|
+
return;
|
|
282
|
+
this.isPaused = true;
|
|
283
|
+
this.unpaused = new Promise(resolve => { this.resumed = resolve; });
|
|
284
|
+
}
|
|
285
|
+
resume() {
|
|
286
|
+
if (!this.isPaused)
|
|
287
|
+
return;
|
|
288
|
+
this.isPaused = false;
|
|
289
|
+
this.resumed?.();
|
|
290
|
+
this.resumed = null;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Stops the frame in flight and keeps the device for the next one. `render()` rejects with an `AbortError` and
|
|
294
|
+
* `image` keeps what it held. A paused frame is cancelled too, and the next one starts unpaused. Does nothing
|
|
295
|
+
* without a frame in flight.
|
|
296
|
+
*/
|
|
297
|
+
cancel() {
|
|
298
|
+
if (this.disposed || !this.frame)
|
|
299
|
+
return;
|
|
300
|
+
this.revision++;
|
|
301
|
+
this.resume();
|
|
302
|
+
this.emitIdle();
|
|
303
|
+
this.abort("The frame was cancelled.");
|
|
304
|
+
}
|
|
305
|
+
/** Releases the device. A frame in flight rejects with an `AbortError`. */
|
|
306
|
+
dispose() {
|
|
307
|
+
if (this.disposed)
|
|
308
|
+
return;
|
|
309
|
+
this.disposed = true;
|
|
310
|
+
this.revision++;
|
|
311
|
+
this.resume();
|
|
312
|
+
this.abort("The renderer was disposed.");
|
|
313
|
+
this.buffers?.destroy();
|
|
314
|
+
this.accumulator?.destroy();
|
|
315
|
+
this.denoisePass?.destroy();
|
|
316
|
+
this.gpu?.device.destroy();
|
|
317
|
+
this.buffers = null;
|
|
318
|
+
this.accumulator = null;
|
|
319
|
+
this.gpu = null;
|
|
320
|
+
this.ready = null;
|
|
321
|
+
}
|
|
322
|
+
/** Takes the frame in flight, if any, so it can be settled after the events go out. */
|
|
323
|
+
settle() {
|
|
324
|
+
const frame = this.frame;
|
|
325
|
+
this.frame = null;
|
|
326
|
+
return frame;
|
|
327
|
+
}
|
|
328
|
+
/** Settles the frame in flight, if any, with an `AbortError`. */
|
|
329
|
+
abort(reason) {
|
|
330
|
+
this.settle()?.reject(abortError(reason));
|
|
331
|
+
}
|
|
332
|
+
fail(error) {
|
|
333
|
+
const frame = this.settle();
|
|
334
|
+
this.emitIdle();
|
|
335
|
+
this.emit("error", error);
|
|
336
|
+
frame?.reject(error);
|
|
337
|
+
}
|
|
338
|
+
emitIdle() {
|
|
339
|
+
this.emit("bucket", { worker: 0, bucket: null, width: this.output?.width ?? 0, height: this.output?.height ?? 0 });
|
|
340
|
+
}
|
|
341
|
+
async device() {
|
|
342
|
+
this.ready ??= requestRenderDevice({
|
|
343
|
+
onLost: reason => {
|
|
344
|
+
if (!this.disposed)
|
|
345
|
+
this.fail(new Error(`GPU renderer: the device was lost (${reason}).`));
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
this.gpu = await this.ready;
|
|
349
|
+
return this.gpu;
|
|
350
|
+
}
|
|
351
|
+
async accumulate(revision, camera) {
|
|
352
|
+
this.emit("progress", { completed: 0, total: 1, phase: "prepare" });
|
|
353
|
+
const gpu = await this.device();
|
|
354
|
+
const scene = await this.scene;
|
|
355
|
+
const environment = await this.environmentImage;
|
|
356
|
+
const width = this.renderWidth, height = this.renderHeight;
|
|
357
|
+
this.check(revision);
|
|
358
|
+
this.assets ??= prepareSceneAssets(scene);
|
|
359
|
+
const assets = await this.assets;
|
|
360
|
+
this.check(revision);
|
|
361
|
+
this.prepare(gpu, assets, camera, width, height, environment);
|
|
362
|
+
this.output = { width, height, data: new Uint8ClampedArray(width * height * 4) };
|
|
363
|
+
this.emit("start", { width, height });
|
|
364
|
+
const target = Math.max(1, Math.round(this.current.samples));
|
|
365
|
+
// Adaptive sampling: the frame has the work of `target` passes over the whole image to spend. Pixels that have
|
|
366
|
+
// converged stop taking samples, and a row with none left stops being dispatched, so what they would have used goes
|
|
367
|
+
// to the rows that still need it, up to a few times `target` paths in the noisiest pixels.
|
|
368
|
+
const adaptive = target > ADAPTIVE_MIN;
|
|
369
|
+
const maxPasses = adaptive ? target * ADAPTIVE_REACH : target;
|
|
370
|
+
const budget = target * height;
|
|
371
|
+
const accumulator = this.accumulator;
|
|
372
|
+
let active = Array.from({ length: height }, (_, row) => row);
|
|
373
|
+
let spent = 0, band = 0, pass = 0;
|
|
374
|
+
let shown = performance.now();
|
|
375
|
+
const denoise = this.current.globalIllumination.denoise;
|
|
376
|
+
this.denoised = false;
|
|
377
|
+
this.finished = null;
|
|
378
|
+
const matrices = cameraMatrices(camera, width, height);
|
|
379
|
+
// Two over the projection's vertical scale and the height: one pixel's extent, per unit of depth in perspective.
|
|
380
|
+
this.denoiseView = { pixelScale: 2 / (matrices.projection[5] * height), orthographic: matrices.orthographic };
|
|
381
|
+
while (pass < maxPasses && spent < budget && active.length) {
|
|
382
|
+
// Only a pass whose pixels may stop needs its rows flagged; before that every row is busy anyway.
|
|
383
|
+
const flagging = adaptive && pass >= ADAPTIVE_MIN;
|
|
384
|
+
if (flagging) {
|
|
385
|
+
const clear = gpu.device.createCommandEncoder();
|
|
386
|
+
clear.clearBuffer(accumulator.buffer, accumulator.rowFlagsOffset, accumulator.rowFlagsLength);
|
|
387
|
+
gpu.device.queue.submit([clear.finish()]);
|
|
388
|
+
}
|
|
389
|
+
for (const [first, end] of rowRanges(active)) {
|
|
390
|
+
let row = first;
|
|
391
|
+
while (row < end) {
|
|
392
|
+
if (this.isPaused)
|
|
393
|
+
await this.unpaused;
|
|
394
|
+
this.check(revision);
|
|
395
|
+
const rows = this.scheduler.next(end - row);
|
|
396
|
+
const started = performance.now();
|
|
397
|
+
const encoder = gpu.device.createCommandEncoder();
|
|
398
|
+
this.emit("bucket", { worker: 0, bucket: { x: 0, y: row, width, height: rows, index: band++ }, width, height });
|
|
399
|
+
this.pathTrace?.encode(encoder, {
|
|
400
|
+
...this.frameUniforms(camera, width, height, assets),
|
|
401
|
+
bandOrigin: [0, row],
|
|
402
|
+
bandSize: [width, rows],
|
|
403
|
+
sampleIndex: pass,
|
|
404
|
+
sampleCount: 1,
|
|
405
|
+
adaptiveMin: adaptive ? ADAPTIVE_MIN : 0,
|
|
406
|
+
adaptiveThreshold: ADAPTIVE_THRESHOLD,
|
|
407
|
+
});
|
|
408
|
+
// The canvas is drawn about once a display frame; in between the device only traces.
|
|
409
|
+
const show = this.canvas !== null && started - shown >= SHOW_EVERY_MS;
|
|
410
|
+
if (show)
|
|
411
|
+
this.encodePresent(encoder, width, height);
|
|
412
|
+
gpu.device.queue.submit([encoder.finish()]);
|
|
413
|
+
await gpu.device.queue.onSubmittedWorkDone();
|
|
414
|
+
this.scheduler.record(performance.now() - started, rows);
|
|
415
|
+
row += rows;
|
|
416
|
+
spent += rows;
|
|
417
|
+
this.emit("progress", { completed: Math.min(spent, budget), total: budget, phase: "shade" });
|
|
418
|
+
if (show) {
|
|
419
|
+
shown = performance.now();
|
|
420
|
+
await yieldToPage();
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
pass++;
|
|
425
|
+
this.frameIndex++;
|
|
426
|
+
// The canvas shows the denoised image from the end of each pass on, and the bands of the next pass refine
|
|
427
|
+
// underneath it until it is denoised again.
|
|
428
|
+
if (denoise && this.canvas && this.context) {
|
|
429
|
+
const encoder = gpu.device.createCommandEncoder();
|
|
430
|
+
this.encodeDenoise(gpu, encoder, width, height);
|
|
431
|
+
this.encodePresent(encoder, width, height);
|
|
432
|
+
gpu.device.queue.submit([encoder.finish()]);
|
|
433
|
+
}
|
|
434
|
+
this.emit("sample", { samples: Math.min(target, Math.round(spent / height)), target });
|
|
435
|
+
if (flagging)
|
|
436
|
+
active = await this.activeRows(gpu, accumulator);
|
|
437
|
+
}
|
|
438
|
+
// Converged early, or spent: either way the image is as finished as this frame makes it.
|
|
439
|
+
this.emit("progress", { completed: budget, total: budget, phase: "shade" });
|
|
440
|
+
this.emit("sample", { samples: target, target });
|
|
441
|
+
this.check(revision);
|
|
442
|
+
if (denoise) {
|
|
443
|
+
const encoder = gpu.device.createCommandEncoder();
|
|
444
|
+
this.encodeDenoise(gpu, encoder, width, height);
|
|
445
|
+
this.encodePresent(encoder, width, height);
|
|
446
|
+
gpu.device.queue.submit([encoder.finish()]);
|
|
447
|
+
}
|
|
448
|
+
const image = await this.readImage(gpu, width, height, denoise ? this.denoisePass?.output ?? null : null);
|
|
449
|
+
this.check(revision);
|
|
450
|
+
this.output = image;
|
|
451
|
+
this.finished = { width, height };
|
|
452
|
+
this.emitIdle();
|
|
453
|
+
this.emit("pixels", { x: 0, y: 0, ...image });
|
|
454
|
+
return image;
|
|
455
|
+
}
|
|
456
|
+
/** The rows a pass still traced something in, read back from the accumulator's row flags. */
|
|
457
|
+
async activeRows(gpu, accumulator) {
|
|
458
|
+
const staging = gpu.device.createBuffer({
|
|
459
|
+
size: accumulator.rowFlagsLength,
|
|
460
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
|
|
461
|
+
});
|
|
462
|
+
const encoder = gpu.device.createCommandEncoder();
|
|
463
|
+
encoder.copyBufferToBuffer(accumulator.buffer, accumulator.rowFlagsOffset, staging, 0, accumulator.rowFlagsLength);
|
|
464
|
+
gpu.device.queue.submit([encoder.finish()]);
|
|
465
|
+
await staging.mapAsync(GPUMapMode.READ);
|
|
466
|
+
const flags = new Float32Array(staging.getMappedRange().slice(0));
|
|
467
|
+
staging.unmap();
|
|
468
|
+
staging.destroy();
|
|
469
|
+
const rows = [];
|
|
470
|
+
for (let row = 0; row < flags.length / 4; row++)
|
|
471
|
+
if (flags[row * 4] > 0)
|
|
472
|
+
rows.push(row);
|
|
473
|
+
return rows;
|
|
474
|
+
}
|
|
475
|
+
check(revision) {
|
|
476
|
+
if (revision !== this.revision || this.disposed)
|
|
477
|
+
throw abortError("A newer render() replaced this frame.");
|
|
478
|
+
}
|
|
479
|
+
/** Uploads what changed and makes sure the passes point at the current buffers. */
|
|
480
|
+
prepare(gpu, assets, camera, width, height, image) {
|
|
481
|
+
this.pathTrace ??= new PathTracePass(gpu.device);
|
|
482
|
+
this.pathTrace.setPrograms(assets.programs);
|
|
483
|
+
// A procedural sky stands in for the renderer's own environment, as it does in the CPU renderer.
|
|
484
|
+
const environment = assets.sky?.environment ?? image;
|
|
485
|
+
const matrices = cameraMatrices(camera, width, height);
|
|
486
|
+
const view = `${JSON.stringify(camera)}|${width}|${height}`;
|
|
487
|
+
const moved = (!!this.packed?.geometry.cameraDependent || this.planar) && view !== this.packedView;
|
|
488
|
+
if (this.sceneDirty || !this.buffers || moved) {
|
|
489
|
+
// Planar surfaces need their captures, from this camera, before the scene that samples them is packed.
|
|
490
|
+
const planar = planarCaptures(assets.scene, camera, width, height);
|
|
491
|
+
const packed = this.packScene(gpu, planar?.scene ?? assets.scene, assets, matrices, width, height, environment);
|
|
492
|
+
this.packed = packed.packed;
|
|
493
|
+
this.buffers?.destroy();
|
|
494
|
+
this.buffers = packed.buffers;
|
|
495
|
+
this.target = packed;
|
|
496
|
+
this.sceneDirty = false;
|
|
497
|
+
this.packedView = view;
|
|
498
|
+
this.planar = !!planar;
|
|
499
|
+
if (planar)
|
|
500
|
+
this.renderCaptures(gpu, planar, assets, environment);
|
|
501
|
+
}
|
|
502
|
+
const resized = !this.accumulator || this.accumulator.resize(width, height);
|
|
503
|
+
this.accumulator ??= new Accumulator(gpu.device, width, height);
|
|
504
|
+
if (resized)
|
|
505
|
+
this.scheduler = new DispatchScheduler({ maxRows: height });
|
|
506
|
+
this.pathTrace.setBindings(this.buffers, this.accumulator.buffer);
|
|
507
|
+
this.configureCanvas(gpu, width, height);
|
|
508
|
+
// Every frame starts from nothing: the camera, the scene or the settings may have moved since the last one. The
|
|
509
|
+
// canvas is drawn from the cleared accumulation at once, so the last image does not linger until the first band of
|
|
510
|
+
// the new one lands: every pixel goes see-through, and the new samples arrive over whatever the page shows below.
|
|
511
|
+
const encoder = gpu.device.createCommandEncoder();
|
|
512
|
+
this.denoised = false;
|
|
513
|
+
this.accumulator.clear(encoder);
|
|
514
|
+
this.encodePresent(encoder, width, height);
|
|
515
|
+
gpu.device.queue.submit([encoder.finish()]);
|
|
516
|
+
}
|
|
517
|
+
/** Packs and uploads one scene: its textures, geometry, hierarchy, lights and every extra word they point at. */
|
|
518
|
+
packScene(gpu, scene, assets, matrices, width, height, environment) {
|
|
519
|
+
// The textures are planned first: a material refers to one by the index it lands on in that table, and to its
|
|
520
|
+
// extra words by where they land after the texels. The environment rides along in the same buffer.
|
|
521
|
+
const plan = planTextures(scene, environment ? [environment.texture] : []);
|
|
522
|
+
const extra = [];
|
|
523
|
+
const append = (words) => {
|
|
524
|
+
const at = plan.extraOffset + extra.length;
|
|
525
|
+
for (let i = 0; i < words.length; i++)
|
|
526
|
+
extra.push(words[i]);
|
|
527
|
+
return at;
|
|
528
|
+
};
|
|
529
|
+
const cdf = environment
|
|
530
|
+
? append(new Uint32Array(environment.distribution.buffer, environment.distribution.byteOffset, environment.distribution.length))
|
|
531
|
+
: plan.extraOffset;
|
|
532
|
+
const textureData = new Map(scene.textures.map(texture => [texture.id, texture]));
|
|
533
|
+
const geometry = packGeometry(scene, {
|
|
534
|
+
textures: plan.indexOf,
|
|
535
|
+
textureData,
|
|
536
|
+
camera: matrices,
|
|
537
|
+
width,
|
|
538
|
+
height,
|
|
539
|
+
programs: assets.programs,
|
|
540
|
+
extraBase: plan.extraOffset + extra.length,
|
|
541
|
+
fog: scene.fog,
|
|
542
|
+
});
|
|
543
|
+
append(geometry.extraWords);
|
|
544
|
+
const lights = packLights(geometry, scene.lights, { power: environmentPower(environment) }, { textures: plan.indexOf, extraBase: plan.extraOffset + extra.length });
|
|
545
|
+
append(lights.extraWords);
|
|
546
|
+
const foreground = assets.programs.foreground;
|
|
547
|
+
const skyData = foreground && assets.sky
|
|
548
|
+
? append(foreground.packUniforms({ ...assets.sky.uniforms, uCpuSky: { texture: "procedural-sky" } }, plan.indexOf))
|
|
549
|
+
: 0;
|
|
550
|
+
const volumetric = packVolumetricLights(scene.lights);
|
|
551
|
+
const volumetricData = append(new Uint32Array(volumetric.buffer));
|
|
552
|
+
const textures = writeTextures(plan, new Uint32Array(extra));
|
|
553
|
+
const packed = { geometry, bvh: buildBvh(geometry.positions, geometry.triangleCount), textures };
|
|
554
|
+
const buffers = uploadScene(gpu.device, geometry, packed.bvh, textures, lights, environment, cdf);
|
|
555
|
+
if (buffers.bytes.largest > gpu.maxStorageBufferBindingSize) {
|
|
556
|
+
buffers.destroy();
|
|
557
|
+
throw new Error("GPU renderer: this scene needs a larger storage buffer than the device allows.");
|
|
558
|
+
}
|
|
559
|
+
return {
|
|
560
|
+
packed,
|
|
561
|
+
buffers,
|
|
562
|
+
skyData,
|
|
563
|
+
volumetricData,
|
|
564
|
+
volumetricCount: Math.min(volumetric.length / VOLUMETRIC_WORDS, 8),
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Renders a scene's planar captures with this renderer, from the mirrored and the real camera, and writes each into
|
|
569
|
+
* the texels of its placeholder in the main scene's texture buffer. Everything is queued ahead of the frame itself,
|
|
570
|
+
* which the queue then runs in order.
|
|
571
|
+
*/
|
|
572
|
+
renderCaptures(gpu, planar, assets, environment) {
|
|
573
|
+
const main = this.target;
|
|
574
|
+
const pathTrace = this.pathTrace;
|
|
575
|
+
const { width, height } = planar;
|
|
576
|
+
const accumulator = new Accumulator(gpu.device, width, height);
|
|
577
|
+
const empty = { ...planar.captureScene, meshes: [] };
|
|
578
|
+
const scenes = new Map();
|
|
579
|
+
this.resolve ??= new CaptureResolvePass(gpu.device);
|
|
580
|
+
try {
|
|
581
|
+
for (const capture of planar.captures) {
|
|
582
|
+
let target = scenes.get(capture.scene);
|
|
583
|
+
if (!target) {
|
|
584
|
+
target = this.packScene(gpu, capture.scene === "scene" ? planar.captureScene : empty, assets, cameraMatrices(capture.camera, width, height), width, height, environment);
|
|
585
|
+
scenes.set(capture.scene, target);
|
|
586
|
+
}
|
|
587
|
+
const clear = gpu.device.createCommandEncoder();
|
|
588
|
+
accumulator.clear(clear);
|
|
589
|
+
gpu.device.queue.submit([clear.finish()]);
|
|
590
|
+
pathTrace.setBindings(target.buffers, accumulator.buffer);
|
|
591
|
+
const uniforms = {
|
|
592
|
+
...this.frameUniforms(capture.camera, width, height, assets, target, capture.background),
|
|
593
|
+
lens: [0, 1, 0, 0],
|
|
594
|
+
volumetricCount: 0,
|
|
595
|
+
clipPlane: capture.clip ?? [0, 0, 0, 0],
|
|
596
|
+
bandOrigin: [0, 0],
|
|
597
|
+
bandSize: [width, height],
|
|
598
|
+
sampleCount: 1,
|
|
599
|
+
};
|
|
600
|
+
for (let sample = 0; sample < CAPTURE_SAMPLES; sample++) {
|
|
601
|
+
const encoder = gpu.device.createCommandEncoder();
|
|
602
|
+
pathTrace.encode(encoder, { ...uniforms, sampleIndex: sample });
|
|
603
|
+
gpu.device.queue.submit([encoder.finish()]);
|
|
604
|
+
}
|
|
605
|
+
const offset = (id) => {
|
|
606
|
+
const index = id === undefined ? undefined : main.packed.textures.indexOf.get(id);
|
|
607
|
+
return index === undefined ? NO_TEXTURE : main.packed.textures.data[index * 16 + 2];
|
|
608
|
+
};
|
|
609
|
+
const encoder = gpu.device.createCommandEncoder();
|
|
610
|
+
this.resolve.encode(encoder, accumulator.buffer, main.buffers.textures, width, height, offset(capture.texture), offset(capture.depth));
|
|
611
|
+
gpu.device.queue.submit([encoder.finish()]);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
finally {
|
|
615
|
+
// The queue keeps what it still needs alive until the work that uses it is done.
|
|
616
|
+
for (const target of scenes.values())
|
|
617
|
+
target.buffers.destroy();
|
|
618
|
+
accumulator.destroy();
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
configureCanvas(gpu, width, height) {
|
|
622
|
+
if (!this.canvas)
|
|
623
|
+
return;
|
|
624
|
+
const format = navigator.gpu.getPreferredCanvasFormat();
|
|
625
|
+
this.canvas.width = width;
|
|
626
|
+
this.canvas.height = height;
|
|
627
|
+
// Premultiplied always: a transparent background needs it, and a pixel no sample has reached yet stays see-through
|
|
628
|
+
// rather than black, so a page can show something under the canvas until the image covers it.
|
|
629
|
+
if (!this.context) {
|
|
630
|
+
this.context = this.canvas.getContext("webgpu");
|
|
631
|
+
this.context?.configure({ device: gpu.device, format, alphaMode: "premultiplied" });
|
|
632
|
+
}
|
|
633
|
+
if (!this.present || this.present.format !== format)
|
|
634
|
+
this.present = new PresentPass(gpu.device, format);
|
|
635
|
+
this.present.setAccumulation(this.accumulator.buffer);
|
|
636
|
+
}
|
|
637
|
+
encodePresent(encoder, width, height) {
|
|
638
|
+
if (!this.context || !this.present)
|
|
639
|
+
return;
|
|
640
|
+
this.present.encode(encoder, this.context.getCurrentTexture().createView(), {
|
|
641
|
+
width,
|
|
642
|
+
height,
|
|
643
|
+
exposure: this.current.exposure,
|
|
644
|
+
toneMapping: this.current.toneMapping,
|
|
645
|
+
vignette: this.current.vignette,
|
|
646
|
+
grain: this.current.grain,
|
|
647
|
+
frameIndex: this.frameIndex,
|
|
648
|
+
transparent: this.current.background.mode === "transparent",
|
|
649
|
+
denoised: this.denoised ? this.denoisePass?.output : null,
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
/** Denoises the accumulation as it stands, for the canvas and for the finished image. */
|
|
653
|
+
encodeDenoise(gpu, encoder, width, height) {
|
|
654
|
+
this.denoisePass ??= new DenoisePass(gpu.device);
|
|
655
|
+
this.denoisePass.encode(encoder, this.accumulator.buffer, width, height, this.denoiserSettings, this.denoiseView);
|
|
656
|
+
this.denoised = true;
|
|
657
|
+
}
|
|
658
|
+
frameUniforms(camera, width, height, assets, target = this.target, backgroundOverride) {
|
|
659
|
+
const buffers = target?.buffers ?? null;
|
|
660
|
+
const matrices = cameraMatrices(camera, width, height);
|
|
661
|
+
const gradient = this.environment.kind === "gradient" ? this.environment : null;
|
|
662
|
+
const ambient = buffers?.ambient ?? { sky: [0, 0, 0], ground: [0, 0, 0], axis: [0, 1, 0] };
|
|
663
|
+
const surrounding = buffers?.environment;
|
|
664
|
+
const { shadows, fog, depthOfField } = this.current;
|
|
665
|
+
const background = { ...this.current.background, mode: backgroundOverride ?? this.current.background.mode };
|
|
666
|
+
const sky = assets.sky;
|
|
667
|
+
const sceneFog = assets.scene.fog;
|
|
668
|
+
// The CPU renderer's lens: a 35 mm frame, its focal length from the field of view, and the f-number's aperture.
|
|
669
|
+
const focalLength = 0.035 / (2 * Math.tan(matrices.fov * Math.PI / 360));
|
|
670
|
+
const lensRadius = depthOfField.enabled && !matrices.orthographic
|
|
671
|
+
? focalLength / (2 * Math.max(depthOfField.aperture, 1e-3))
|
|
672
|
+
: 0;
|
|
673
|
+
const backgroundMode = background.mode === "color" ? 1 : background.mode === "transparent" ? 2 : 0;
|
|
674
|
+
return {
|
|
675
|
+
cameraToWorld: matrices.cameraWorld,
|
|
676
|
+
worldToCamera: matrices.view,
|
|
677
|
+
projection: matrices.projection,
|
|
678
|
+
inverseProjection: matrices.inverseProjection,
|
|
679
|
+
environmentTop: [...linearColor(gradient?.topColor, [1, 1, 1]), gradient?.exponent ?? 2],
|
|
680
|
+
environmentBottom: [
|
|
681
|
+
...linearColor(gradient?.bottomColor, [0, 0, 0]),
|
|
682
|
+
sky ? sky.intensity : this.environment.intensity ?? 1,
|
|
683
|
+
],
|
|
684
|
+
ambientSky: [...ambient.sky, 0],
|
|
685
|
+
ambientGround: [...ambient.ground, 0],
|
|
686
|
+
ambientAxis: [...ambient.axis, 0],
|
|
687
|
+
...Object.fromEntries(Array.from({ length: 9 }, (_, k) => [`probe${k}`, [...this.probe.slice(k * 3, k * 3 + 3), 0]])),
|
|
688
|
+
// The settings' fog covers every surface; otherwise the scene's own fog covers the materials that accept it.
|
|
689
|
+
fogColor: fog.enabled
|
|
690
|
+
? [...linearColor(fog.color, [0.5, 0.5, 0.5]), 1]
|
|
691
|
+
: sceneFog ? [...sceneFog.color, sceneFog.density !== undefined ? 2 : 1] : [0, 0, 0, 0],
|
|
692
|
+
fogParams: fog.enabled
|
|
693
|
+
? [fog.near, fog.far, 0, 1]
|
|
694
|
+
: [sceneFog?.near ?? 0, sceneFog?.far ?? 0, sceneFog?.density ?? 0, 0],
|
|
695
|
+
background: [...linearColor(background.color, [0, 0, 0]), backgroundMode],
|
|
696
|
+
lens: [lensRadius, Math.max(depthOfField.focusDistance, 1e-3), depthOfField.blades, 0],
|
|
697
|
+
resolution: [width, height],
|
|
698
|
+
cameraKind: matrices.orthographic ? 1 : 0,
|
|
699
|
+
bounces: Math.max(1, Math.round(this.current.globalIllumination.bounces)),
|
|
700
|
+
triangleCount: buffers?.triangleCount ?? 0,
|
|
701
|
+
lightCount: buffers?.lightCount ?? 0,
|
|
702
|
+
areaLightCount: buffers?.areaLightCount ?? 0,
|
|
703
|
+
clampIndirect: Math.max(0, this.current.globalIllumination.clamp),
|
|
704
|
+
// The setting is the angular diameter a light covers, in degrees, as it is for the CPU renderer's shadow blur.
|
|
705
|
+
shadowSpread: shadows.enabled ? Math.tan(Math.max(0, shadows.softness) * Math.PI / 360) : 0,
|
|
706
|
+
shadowsEnabled: shadows.enabled,
|
|
707
|
+
uvOffset: buffers?.uvOffset ?? 0,
|
|
708
|
+
orderOffset: buffers?.orderOffset ?? 0,
|
|
709
|
+
colorOffset: buffers?.colorOffset ?? 0,
|
|
710
|
+
hasColors: buffers?.hasColors ?? false,
|
|
711
|
+
varyingOffset: buffers?.varyingOffset ?? 0,
|
|
712
|
+
environmentTexture: surrounding?.texture ?? NO_TEXTURE,
|
|
713
|
+
environmentCdf: surrounding?.cdf ?? 0,
|
|
714
|
+
environmentCdfSize: [surrounding?.width ?? 0, surrounding?.height ?? 0],
|
|
715
|
+
environmentTotal: surrounding?.total ?? 0,
|
|
716
|
+
// A procedural sky ignores the rotation, so its sun stays where its light is.
|
|
717
|
+
environmentRotation: sky ? 0 : (this.environment.rotation ?? 0) * Math.PI / 180,
|
|
718
|
+
environmentPick: surrounding?.pick ?? 0,
|
|
719
|
+
skyProgram: sky && assets.programs.foreground ? assets.programs.foreground.id : NO_PROGRAM,
|
|
720
|
+
skyData: target?.skyData ?? 0,
|
|
721
|
+
skyBackground: sky ? (sky.background ? 1 : 0) : 1,
|
|
722
|
+
skyIntensity: sky ? Math.max(sky.intensity, 1e-6) : 0,
|
|
723
|
+
pixelCount: width * height,
|
|
724
|
+
volumetricCount: target?.volumetricCount ?? 0,
|
|
725
|
+
volumetricData: target?.volumetricData ?? 0,
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Copies the accumulation buffer back and resolves it on the CPU, with the same exposure, tone mapping and sRGB
|
|
730
|
+
* encoding the presentation pass applies. Grain is a presentation effect and is not part of the returned image.
|
|
731
|
+
*/
|
|
732
|
+
async readImage(gpu, width, height, denoised) {
|
|
733
|
+
const accumulator = this.accumulator;
|
|
734
|
+
// The radiance and the coverage; the guides and the row flags after them are not part of the image.
|
|
735
|
+
const length = width * height * 32;
|
|
736
|
+
const read = async (buffer, bytes) => {
|
|
737
|
+
const usage = GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST;
|
|
738
|
+
const staging = gpu.device.createBuffer({ size: bytes, usage });
|
|
739
|
+
const encoder = gpu.device.createCommandEncoder();
|
|
740
|
+
encoder.copyBufferToBuffer(buffer, 0, staging, 0, bytes);
|
|
741
|
+
gpu.device.queue.submit([encoder.finish()]);
|
|
742
|
+
await staging.mapAsync(GPUMapMode.READ);
|
|
743
|
+
const floats = new Float32Array(staging.getMappedRange().slice(0));
|
|
744
|
+
staging.unmap();
|
|
745
|
+
staging.destroy();
|
|
746
|
+
return floats;
|
|
747
|
+
};
|
|
748
|
+
const floats = await read(accumulator.buffer, length);
|
|
749
|
+
// The denoiser's output is the mean radiance itself, one vec4 per pixel.
|
|
750
|
+
const filtered = denoised ? await read(denoised, width * height * 16) : null;
|
|
751
|
+
const data = new Uint8ClampedArray(width * height * 4);
|
|
752
|
+
const { exposure, toneMapping, vignette } = this.current;
|
|
753
|
+
const transparent = this.current.background.mode === "transparent";
|
|
754
|
+
const coverage = width * height * 4;
|
|
755
|
+
for (let pixel = 0; pixel < width * height; pixel++) {
|
|
756
|
+
const samples = floats[pixel * 4 + 3];
|
|
757
|
+
const x = (pixel % width) / width - 0.5, y = Math.floor(pixel / width) / height - 0.5;
|
|
758
|
+
const falloff = Math.max(0, 1 - vignette * (x * x + y * y) * 2);
|
|
759
|
+
// The radiance is premultiplied by the coverage, which a transparent background makes the alpha.
|
|
760
|
+
const alpha = !transparent ? 1
|
|
761
|
+
: samples > 0 ? Math.min(1, Math.max(0, floats[coverage + pixel * 4] / samples)) : 0;
|
|
762
|
+
const mean = (channel) => (filtered ? filtered[pixel * 4 + channel] : floats[pixel * 4 + channel] / samples);
|
|
763
|
+
const color = samples > 0 && alpha > 0
|
|
764
|
+
? [0, 1, 2].map(channel => mean(channel) / alpha * falloff)
|
|
765
|
+
: [0, 0, 0];
|
|
766
|
+
const [r, g, b] = encodeColor(color, exposure, toneMapping);
|
|
767
|
+
data.set([r, g, b, Math.round(alpha * 255)], pixel * 4);
|
|
768
|
+
}
|
|
769
|
+
return { width, height, data };
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
/** The material types the CPU renderer's snapshot accepts by name. */
|
|
773
|
+
const SERIALIZABLE = new Set([
|
|
774
|
+
"PointsMaterial", "LineBasicMaterial", "LineDashedMaterial", "SpriteMaterial", "MeshBasicMaterial",
|
|
775
|
+
"MeshLambertMaterial", "MeshPhongMaterial", "MeshToonMaterial", "MeshStandardMaterial", "MeshPhysicalMaterial",
|
|
776
|
+
"MeshNormalMaterial", "MeshDepthMaterial", "MeshDistanceMaterial", "MeshMatcapMaterial", "ShadowMaterial",
|
|
777
|
+
"ShaderMaterial", "RawShaderMaterial"
|
|
778
|
+
]);
|
|
779
|
+
/**
|
|
780
|
+
* Serializes a live scene. The snapshot accepts materials by their type name, so a subclass of ShaderMaterial with a
|
|
781
|
+
* name of its own (LineMaterial, for fat lines) would be refused; it passes as the class it extends, which is all the
|
|
782
|
+
* snapshot reads of it, and gets its name back once the materials have been read.
|
|
783
|
+
*/
|
|
784
|
+
async function serializeLive(scene) {
|
|
785
|
+
const renamed = new Map();
|
|
786
|
+
scene.traverse(object => {
|
|
787
|
+
const value = object.material;
|
|
788
|
+
for (const material of Array.isArray(value) ? value : value ? [value] : []) {
|
|
789
|
+
const shader = material;
|
|
790
|
+
if (shader.isShaderMaterial && !SERIALIZABLE.has(material.type) && !renamed.has(material)) {
|
|
791
|
+
renamed.set(material, material.type);
|
|
792
|
+
material.type = shader.isRawShaderMaterial ? "RawShaderMaterial" : "ShaderMaterial";
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
try {
|
|
797
|
+
return (await serializeScene(scene)).scene;
|
|
798
|
+
}
|
|
799
|
+
finally {
|
|
800
|
+
for (const [material, type] of renamed)
|
|
801
|
+
material.type = type;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* The summed coefficients of every LightProbe the renderer would draw, times its intensity, as Three's WebGLLights sums
|
|
806
|
+
* them. The snapshot keeps a probe's colour and intensity but not its harmonics, so they are read from the live scene.
|
|
807
|
+
*/
|
|
808
|
+
function lightProbes(scene) {
|
|
809
|
+
const sum = new Array(27).fill(0);
|
|
810
|
+
const visit = (object) => {
|
|
811
|
+
if (!object.visible || object.userData?.cpuRenderer?.exclude)
|
|
812
|
+
return;
|
|
813
|
+
const probe = object;
|
|
814
|
+
if (probe.isLightProbe)
|
|
815
|
+
probe.sh.coefficients.forEach((coefficient, k) => {
|
|
816
|
+
sum[k * 3] += coefficient.x * probe.intensity;
|
|
817
|
+
sum[k * 3 + 1] += coefficient.y * probe.intensity;
|
|
818
|
+
sum[k * 3 + 2] += coefficient.z * probe.intensity;
|
|
819
|
+
});
|
|
820
|
+
for (const child of object.children)
|
|
821
|
+
visit(child);
|
|
822
|
+
};
|
|
823
|
+
visit(scene);
|
|
824
|
+
return sum;
|
|
825
|
+
}
|
|
826
|
+
function checkedPixelRatio(pixelRatio) {
|
|
827
|
+
if (!Number.isFinite(pixelRatio) || pixelRatio <= 0)
|
|
828
|
+
throw new Error("GPU renderer: pixelRatio must be a positive number.");
|
|
829
|
+
return pixelRatio;
|
|
830
|
+
}
|
|
831
|
+
/** A perspective camera at (5, 5, 5) looking at the origin, for a frame rendered before any camera is set. */
|
|
832
|
+
function defaultCamera() {
|
|
833
|
+
const camera = new PerspectiveCamera(50, 1, 0.1, 1000);
|
|
834
|
+
camera.position.set(5, 5, 5);
|
|
835
|
+
camera.lookAt(0, 0, 0);
|
|
836
|
+
camera.updateMatrixWorld();
|
|
837
|
+
return serializeCamera(camera);
|
|
838
|
+
}
|
|
839
|
+
//# sourceMappingURL=renderer.js.map
|