@tinypoly/gpu-renderer 0.0.4 → 0.1.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 +81 -10
- package/README.md +53 -5
- package/dist/atmosphere.d.ts +21 -0
- package/dist/atmosphere.d.ts.map +1 -0
- package/dist/atmosphere.js +72 -0
- package/dist/atmosphere.js.map +1 -0
- package/dist/bvh.d.ts +11 -6
- package/dist/bvh.d.ts.map +1 -1
- package/dist/bvh.js +64 -11
- package/dist/bvh.js.map +1 -1
- package/dist/denoise.d.ts +17 -2
- package/dist/denoise.d.ts.map +1 -1
- package/dist/denoise.js +95 -27
- package/dist/denoise.js.map +1 -1
- package/dist/dispatchScheduler.d.ts +11 -4
- package/dist/dispatchScheduler.d.ts.map +1 -1
- package/dist/dispatchScheduler.js +14 -7
- package/dist/dispatchScheduler.js.map +1 -1
- package/dist/frameLayout.d.ts.map +1 -1
- package/dist/frameLayout.js +18 -0
- package/dist/frameLayout.js.map +1 -1
- package/dist/geometry.d.ts.map +1 -1
- package/dist/geometry.js +6 -3
- package/dist/geometry.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/lensPass.d.ts +60 -0
- package/dist/lensPass.d.ts.map +1 -0
- package/dist/lensPass.js +235 -0
- package/dist/lensPass.js.map +1 -0
- package/dist/lightTree.d.ts +43 -0
- package/dist/lightTree.d.ts.map +1 -0
- package/dist/lightTree.js +107 -0
- package/dist/lightTree.js.map +1 -0
- package/dist/lights.d.ts +19 -1
- package/dist/lights.d.ts.map +1 -1
- package/dist/lights.js +63 -1
- package/dist/lights.js.map +1 -1
- package/dist/materialLayout.d.ts +24 -0
- package/dist/materialLayout.d.ts.map +1 -1
- package/dist/materialLayout.js +36 -1
- package/dist/materialLayout.js.map +1 -1
- package/dist/pathTracePass.d.ts +6 -3
- package/dist/pathTracePass.d.ts.map +1 -1
- package/dist/pathTracePass.js +16 -10
- package/dist/pathTracePass.js.map +1 -1
- package/dist/renderer.d.ts +19 -7
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +198 -36
- package/dist/renderer.js.map +1 -1
- package/dist/sceneUpload.d.ts +19 -1
- package/dist/sceneUpload.d.ts.map +1 -1
- package/dist/sceneUpload.js +25 -3
- package/dist/sceneUpload.js.map +1 -1
- package/dist/settings.d.ts +136 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +100 -0
- package/dist/settings.js.map +1 -0
- package/dist/subsurface.d.ts +76 -0
- package/dist/subsurface.d.ts.map +1 -0
- package/dist/subsurface.js +70 -0
- package/dist/subsurface.js.map +1 -0
- package/dist/texturePack.d.ts +16 -4
- package/dist/texturePack.d.ts.map +1 -1
- package/dist/texturePack.js +61 -8
- package/dist/texturePack.js.map +1 -1
- package/dist/volumeContainment.d.ts +17 -0
- package/dist/volumeContainment.d.ts.map +1 -0
- package/dist/volumeContainment.js +61 -0
- package/dist/volumeContainment.js.map +1 -0
- package/dist/wgsl/bsdf.wgsl.d.ts +1 -1
- package/dist/wgsl/bsdf.wgsl.d.ts.map +1 -1
- package/dist/wgsl/bsdf.wgsl.js +35 -3
- package/dist/wgsl/bsdf.wgsl.js.map +1 -1
- package/dist/wgsl/bvh.wgsl.d.ts +8 -2
- package/dist/wgsl/bvh.wgsl.d.ts.map +1 -1
- package/dist/wgsl/bvh.wgsl.js +167 -96
- package/dist/wgsl/bvh.wgsl.js.map +1 -1
- package/dist/wgsl/common.wgsl.d.ts.map +1 -1
- package/dist/wgsl/common.wgsl.js +24 -7
- package/dist/wgsl/common.wgsl.js.map +1 -1
- package/dist/wgsl/lens.wgsl.d.ts +17 -0
- package/dist/wgsl/lens.wgsl.d.ts.map +1 -0
- package/dist/wgsl/lens.wgsl.js +315 -0
- package/dist/wgsl/lens.wgsl.js.map +1 -0
- package/dist/wgsl/lights.wgsl.d.ts +1 -1
- package/dist/wgsl/lights.wgsl.d.ts.map +1 -1
- package/dist/wgsl/lights.wgsl.js +206 -33
- package/dist/wgsl/lights.wgsl.js.map +1 -1
- package/dist/wgsl/pathTrace.wgsl.d.ts +1 -1
- package/dist/wgsl/pathTrace.wgsl.d.ts.map +1 -1
- package/dist/wgsl/pathTrace.wgsl.js +410 -25
- package/dist/wgsl/pathTrace.wgsl.js.map +1 -1
- package/dist/wgsl/random.wgsl.d.ts +1 -1
- package/dist/wgsl/random.wgsl.d.ts.map +1 -1
- package/dist/wgsl/random.wgsl.js +9 -17
- package/dist/wgsl/random.wgsl.js.map +1 -1
- package/dist/wgsl/sampling.wgsl.d.ts +1 -1
- package/dist/wgsl/sampling.wgsl.d.ts.map +1 -1
- package/dist/wgsl/sampling.wgsl.js +25 -0
- package/dist/wgsl/sampling.wgsl.js.map +1 -1
- package/dist/wgsl/surface.wgsl.d.ts +1 -1
- package/dist/wgsl/surface.wgsl.d.ts.map +1 -1
- package/dist/wgsl/surface.wgsl.js +47 -4
- package/dist/wgsl/surface.wgsl.js.map +1 -1
- package/dist/wgsl/texture.wgsl.d.ts +1 -1
- package/dist/wgsl/texture.wgsl.d.ts.map +1 -1
- package/dist/wgsl/texture.wgsl.js +10 -11
- package/dist/wgsl/texture.wgsl.js.map +1 -1
- package/package.json +1 -1
- package/src/atmosphere.ts +95 -0
- package/src/bvh.ts +81 -12
- package/src/denoise.ts +110 -27
- package/src/dispatchScheduler.ts +16 -7
- package/src/frameLayout.ts +18 -0
- package/src/geometry.ts +6 -2
- package/src/index.ts +13 -0
- package/src/lensPass.ts +299 -0
- package/src/lightTree.ts +172 -0
- package/src/lights.ts +90 -2
- package/src/materialLayout.ts +52 -1
- package/src/pathTracePass.ts +19 -10
- package/src/renderer.ts +258 -43
- package/src/sceneUpload.ts +41 -4
- package/src/settings.ts +234 -0
- package/src/subsurface.ts +153 -0
- package/src/texturePack.ts +80 -13
- package/src/volumeContainment.ts +81 -0
- package/src/wgsl/bsdf.wgsl.ts +35 -3
- package/src/wgsl/bvh.wgsl.ts +167 -96
- package/src/wgsl/common.wgsl.ts +24 -7
- package/src/wgsl/lens.wgsl.ts +314 -0
- package/src/wgsl/lights.wgsl.ts +206 -33
- package/src/wgsl/pathTrace.wgsl.ts +410 -25
- package/src/wgsl/random.wgsl.ts +9 -17
- package/src/wgsl/sampling.wgsl.ts +25 -0
- package/src/wgsl/surface.wgsl.ts +47 -4
- package/src/wgsl/texture.wgsl.ts +10 -11
package/src/renderer.ts
CHANGED
|
@@ -8,10 +8,30 @@ import { DispatchScheduler } from "./dispatchScheduler.js";
|
|
|
8
8
|
import { packGeometry } from "./geometry.js";
|
|
9
9
|
import { packLights } from "./lights.js";
|
|
10
10
|
import { NO_PROGRAM } from "./materialLayout.js";
|
|
11
|
-
import { NO_TEXTURE, planTextures, writeTextures } from "./texturePack.js";
|
|
11
|
+
import { NO_TEXTURE, planTextures, textureEntry, writeTextures } from "./texturePack.js";
|
|
12
12
|
import { PathTracePass, type FrameUniforms } from "./pathTracePass.js";
|
|
13
13
|
import { ImageResolvePass } from "./imageResolve.js";
|
|
14
14
|
import { PresentPass } from "./presentPass.js";
|
|
15
|
+
import { LensPass, lensEffects } from "./lensPass.js";
|
|
16
|
+
import {
|
|
17
|
+
DEFAULT_GPU_RENDER_SETTINGS,
|
|
18
|
+
effectiveExposure,
|
|
19
|
+
mergeGpuSettings,
|
|
20
|
+
type GpuRenderSettings,
|
|
21
|
+
type GpuRenderSettingsInput,
|
|
22
|
+
} from "./settings.js";
|
|
23
|
+
import {
|
|
24
|
+
bevelOf,
|
|
25
|
+
bevelRadius,
|
|
26
|
+
type BevelMaterial,
|
|
27
|
+
subsurfaceMedium,
|
|
28
|
+
subsurfaceOf,
|
|
29
|
+
volumeMedium,
|
|
30
|
+
volumeOf,
|
|
31
|
+
type SubsurfaceMaterial,
|
|
32
|
+
type VolumeMaterial,
|
|
33
|
+
} from "./subsurface.js";
|
|
34
|
+
import { containingVolume, NO_MATERIAL, volumeTriangles, type VolumeTriangles } from "./volumeContainment.js";
|
|
15
35
|
import {
|
|
16
36
|
CONVERGED_ERROR,
|
|
17
37
|
DEFAULT_DENOISER,
|
|
@@ -24,11 +44,10 @@ import { CaptureResolvePass, planarCaptures, type PlanarCaptures } from "./plana
|
|
|
24
44
|
import { prepareSceneAssets, type SceneAssets } from "./sceneAssets.js";
|
|
25
45
|
import { packVolumetricLights, VOLUMETRIC_WORDS } from "./volumetric.js";
|
|
26
46
|
import { environmentPower, uploadScene, type SceneBuffers } from "./sceneUpload.js";
|
|
47
|
+
import { atmosphereExtinction, averageRadiance, brightestSun, gradientAverage } from "./atmosphere.js";
|
|
27
48
|
import {
|
|
28
49
|
DEFAULT_RENDER_ENVIRONMENT,
|
|
29
|
-
DEFAULT_RENDER_SETTINGS,
|
|
30
50
|
EventEmitter,
|
|
31
|
-
mergeSettings,
|
|
32
51
|
serializeCamera,
|
|
33
52
|
serializeScene,
|
|
34
53
|
srgbToLinear,
|
|
@@ -41,8 +60,6 @@ import {
|
|
|
41
60
|
type RenderPhase,
|
|
42
61
|
type RenderPixels,
|
|
43
62
|
type RenderScene,
|
|
44
|
-
type RenderSettings,
|
|
45
|
-
type RenderSettingsInput,
|
|
46
63
|
type SerializedScene,
|
|
47
64
|
} from "./upstream.js";
|
|
48
65
|
|
|
@@ -74,8 +91,11 @@ export interface GpuRendererOptions {
|
|
|
74
91
|
scene?: RenderScene | SerializedScene;
|
|
75
92
|
/** Defaults to a perspective camera at (5, 5, 5) looking at the origin. */
|
|
76
93
|
camera?: RenderCamera;
|
|
77
|
-
/**
|
|
78
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Merged over `DEFAULT_GPU_RENDER_SETTINGS`, group by group: the CPU renderer's settings, and the groups only this
|
|
96
|
+
* renderer reads (`bloom`, `lens`, `camera`, `subsurface`, the roulette under `globalIllumination`).
|
|
97
|
+
*/
|
|
98
|
+
settings?: GpuRenderSettingsInput;
|
|
79
99
|
/** Defaults to `DEFAULT_RENDER_ENVIRONMENT`. */
|
|
80
100
|
environment?: RenderEnvironment;
|
|
81
101
|
/** Image size in CSS pixels; the render size also applies `pixelRatio` and `settings.renderScale`. */
|
|
@@ -130,6 +150,34 @@ const yieldToPage = () => new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
|
130
150
|
*/
|
|
131
151
|
const SHOW_EVERY_MS = 16;
|
|
132
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Submits on the device at once. Two keep it busy: the next band is queued while the last one runs, and each submit
|
|
155
|
+
* still stays short enough for the watchdog on its own.
|
|
156
|
+
*/
|
|
157
|
+
const BANDS_IN_FLIGHT = 2;
|
|
158
|
+
|
|
159
|
+
/** Throughput under which Russian roulette may end a path, with the chance of carrying on equal to the throughput. */
|
|
160
|
+
const ROULETTE_THRESHOLD = 0.2;
|
|
161
|
+
|
|
162
|
+
/** How often the canvas takes a freshly denoised image while a frame traces. */
|
|
163
|
+
const DENOISE_EVERY_MS = 200;
|
|
164
|
+
|
|
165
|
+
/** The most paths per pixel one dispatch takes, while no pixel can stop yet. */
|
|
166
|
+
const BATCH_PATHS = 4;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Paths per pixel the next pass takes. The first takes one, so the canvas shows something at once. Until adaptive
|
|
170
|
+
* sampling may stop a pixel, every pixel takes every path anyway, so a pass takes several and pays for reading and
|
|
171
|
+
* writing the accumulation once; from then on one at a time, so a pixel stops as soon as it has converged.
|
|
172
|
+
*/
|
|
173
|
+
function pathsPerDispatch(taken: number, until: number, adaptive: boolean): number {
|
|
174
|
+
if (taken === 0) return 1;
|
|
175
|
+
|
|
176
|
+
if (adaptive && taken >= until) return 1;
|
|
177
|
+
|
|
178
|
+
return Math.max(1, Math.min(BATCH_PATHS, until - taken));
|
|
179
|
+
}
|
|
180
|
+
|
|
133
181
|
const EMPTY_SCENE: SerializedScene = { meshes: [], lights: [], textures: [] };
|
|
134
182
|
|
|
135
183
|
/** Paths every pixel takes before adaptive sampling may stop it: below this its variance says too little. */
|
|
@@ -176,6 +224,8 @@ interface PackedTarget {
|
|
|
176
224
|
skyData: number;
|
|
177
225
|
volumetricData: number;
|
|
178
226
|
volumetricCount: number;
|
|
227
|
+
/** The boundaries of the scene's volumes, which say whether the camera stands inside one. */
|
|
228
|
+
volumes: VolumeTriangles | null;
|
|
179
229
|
}
|
|
180
230
|
|
|
181
231
|
/**
|
|
@@ -183,7 +233,7 @@ interface PackedTarget {
|
|
|
183
233
|
* `render()` opens a frame with whatever is current and accumulates samples until it reaches `settings.samples`.
|
|
184
234
|
*/
|
|
185
235
|
export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
186
|
-
private current:
|
|
236
|
+
private current: GpuRenderSettings;
|
|
187
237
|
private environment: RenderEnvironment;
|
|
188
238
|
// Serializing walks the whole scene, so it is started by the setter and awaited by the frame that needs it.
|
|
189
239
|
private scene: Promise<SerializedScene> = Promise.resolve(EMPTY_SCENE);
|
|
@@ -223,6 +273,7 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
223
273
|
private planar = false;
|
|
224
274
|
private resolve: CaptureResolvePass | null = null;
|
|
225
275
|
private denoisePass: DenoisePass | null = null;
|
|
276
|
+
private lensPass: LensPass | null = null;
|
|
226
277
|
private denoiserSettings: DenoiserSettings = { ...DEFAULT_DENOISER };
|
|
227
278
|
// The size of the last frame that finished, which setDenoiser filters again; null while a frame traces.
|
|
228
279
|
private finished: { width: number; height: number } | null = null;
|
|
@@ -232,6 +283,8 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
232
283
|
private denoiseView: DenoiseView = { pixelScale: 1, orthographic: false };
|
|
233
284
|
// Reading an environment image is a fetch, so it is started once and awaited by the frame that needs it.
|
|
234
285
|
private environmentSource = "";
|
|
286
|
+
// The surroundings' radiance averaged over the sphere, which the air glows with; null for the gradient.
|
|
287
|
+
private environmentAverage: [number, number, number] | null = null;
|
|
235
288
|
private environmentImage: Promise<PackedEnvironment | null> = Promise.resolve(null);
|
|
236
289
|
private canvas: HTMLCanvasElement | null = null;
|
|
237
290
|
private context: GPUCanvasContext | null = null;
|
|
@@ -249,10 +302,10 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
249
302
|
this.width = Math.max(1, Math.floor(options.width));
|
|
250
303
|
this.height = Math.max(1, Math.floor(options.height));
|
|
251
304
|
this.pixelRatio = checkedPixelRatio(options.pixelRatio ?? 1);
|
|
252
|
-
this.current =
|
|
305
|
+
this.current = mergeGpuSettings(DEFAULT_GPU_RENDER_SETTINGS, options.settings);
|
|
253
306
|
this.denoiserSettings = mergeDenoiser(DEFAULT_DENOISER, options.denoiser);
|
|
254
307
|
this.environment = DEFAULT_RENDER_ENVIRONMENT;
|
|
255
|
-
this.scheduler = new DispatchScheduler({ maxRows: this.renderHeight });
|
|
308
|
+
this.scheduler = new DispatchScheduler({ maxRows: this.renderHeight, maxSamples: BATCH_PATHS });
|
|
256
309
|
this.setEnvironment(options.environment ?? DEFAULT_RENDER_ENVIRONMENT);
|
|
257
310
|
|
|
258
311
|
if (options.scene) this.setScene(options.scene);
|
|
@@ -260,7 +313,7 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
260
313
|
}
|
|
261
314
|
|
|
262
315
|
/** The settings the next frame renders with. */
|
|
263
|
-
get settings(): Readonly<
|
|
316
|
+
get settings(): Readonly<GpuRenderSettings> {
|
|
264
317
|
return this.current;
|
|
265
318
|
}
|
|
266
319
|
|
|
@@ -371,15 +424,16 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
371
424
|
}
|
|
372
425
|
|
|
373
426
|
/**
|
|
374
|
-
* Merges the given fields into the current settings, group by group. Turning the denoiser on or off
|
|
375
|
-
*
|
|
427
|
+
* Merges the given fields into the current settings, group by group. Turning the denoiser on or off, or changing
|
|
428
|
+
* what is done to the light after it is traced (exposure, tone mapping, vignette, bloom, chromatic aberration, white
|
|
429
|
+
* balance), shows the finished image again at once, as `setDenoiser` does, since none of that needs tracing.
|
|
376
430
|
*/
|
|
377
|
-
setSettings(settings:
|
|
378
|
-
const
|
|
431
|
+
setSettings(settings: GpuRenderSettingsInput) {
|
|
432
|
+
const before = developing(this.current);
|
|
379
433
|
|
|
380
|
-
this.current =
|
|
434
|
+
this.current = mergeGpuSettings(this.current, settings);
|
|
381
435
|
|
|
382
|
-
if (this.current
|
|
436
|
+
if (developing(this.current) !== before) this.refilter();
|
|
383
437
|
}
|
|
384
438
|
|
|
385
439
|
setEnvironment(environment: RenderEnvironment) {
|
|
@@ -482,6 +536,8 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
482
536
|
this.buffers?.destroy();
|
|
483
537
|
this.accumulator?.destroy();
|
|
484
538
|
this.denoisePass?.destroy();
|
|
539
|
+
this.lensPass?.destroy();
|
|
540
|
+
this.lensPass = null;
|
|
485
541
|
this.imageResolve?.dispose();
|
|
486
542
|
this.imageResolve = null;
|
|
487
543
|
this.gpu?.device.destroy();
|
|
@@ -549,29 +605,57 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
549
605
|
this.emit("start", { width, height });
|
|
550
606
|
|
|
551
607
|
const target = Math.max(1, Math.round(this.current.samples));
|
|
552
|
-
// Adaptive sampling: the frame has the work of `target`
|
|
608
|
+
// Adaptive sampling: the frame has the work of `target` paths over the whole image to spend. Pixels that have
|
|
553
609
|
// converged stop taking samples, and a row with none left stops being dispatched, so what they would have used goes
|
|
554
610
|
// to the rows that still need it, up to a few times `target` paths in the noisiest pixels.
|
|
555
611
|
const adaptive = target > ADAPTIVE_MIN;
|
|
556
|
-
const
|
|
612
|
+
const maxPaths = adaptive ? target * ADAPTIVE_REACH : target;
|
|
613
|
+
// In rows times paths, the unit the scheduler measures work in.
|
|
557
614
|
const budget = target * height;
|
|
558
615
|
const accumulator = this.accumulator as Accumulator;
|
|
559
616
|
let active: number[] = Array.from({ length: height }, (_, row) => row);
|
|
560
|
-
|
|
617
|
+
// Paths every pixel still in the running has taken so far, which is also the next sample index.
|
|
618
|
+
let taken = 0;
|
|
619
|
+
let spent = 0, band = 0;
|
|
561
620
|
let shown = performance.now();
|
|
621
|
+
let denoisedAt = -Infinity;
|
|
562
622
|
const denoise = this.current.globalIllumination.denoise;
|
|
623
|
+
// Bands on the device, oldest first. The next one is encoded and queued while the last one runs, so the device
|
|
624
|
+
// never waits for the page to hear that a band finished.
|
|
625
|
+
const flight: { done: Promise<unknown>; submitted: number; rows: number; samples: number }[] = [];
|
|
626
|
+
let settledAt = 0;
|
|
627
|
+
|
|
628
|
+
const settle = async() => {
|
|
629
|
+
const oldest = flight.shift();
|
|
630
|
+
|
|
631
|
+
if (!oldest) return;
|
|
632
|
+
|
|
633
|
+
await oldest.done;
|
|
634
|
+
|
|
635
|
+
const now = performance.now();
|
|
636
|
+
|
|
637
|
+
// A band starts on the device when the one before it ends, or when it was submitted if the device was idle. The
|
|
638
|
+
// page may hear of that end late, which would make this band look shorter than it ran; since it was submitted it
|
|
639
|
+
// has waited for at most the bands ahead of it, so its share of that wait bounds it from below.
|
|
640
|
+
const ran = Math.max(now - Math.max(oldest.submitted, settledAt), (now - oldest.submitted) / BANDS_IN_FLIGHT);
|
|
641
|
+
|
|
642
|
+
this.scheduler.record(ran, oldest.rows, oldest.samples);
|
|
643
|
+
settledAt = now;
|
|
644
|
+
};
|
|
563
645
|
|
|
564
646
|
this.denoised = false;
|
|
565
647
|
this.finished = null;
|
|
566
648
|
|
|
567
649
|
const matrices = cameraMatrices(camera, width, height);
|
|
650
|
+
const uniforms = this.frameUniforms(camera, width, height, assets);
|
|
568
651
|
|
|
569
652
|
// Two over the projection's vertical scale and the height: one pixel's extent, per unit of depth in perspective.
|
|
570
653
|
this.denoiseView = { pixelScale: 2 / (matrices.projection[5] * height), orthographic: matrices.orthographic };
|
|
571
654
|
|
|
572
|
-
while (
|
|
655
|
+
while (taken < maxPaths && spent < budget && active.length) {
|
|
573
656
|
// Only a pass whose pixels may stop needs its rows flagged; before that every row is busy anyway.
|
|
574
|
-
const flagging = adaptive &&
|
|
657
|
+
const flagging = adaptive && taken >= ADAPTIVE_MIN;
|
|
658
|
+
const samples = pathsPerDispatch(taken, adaptive ? ADAPTIVE_MIN : target, adaptive);
|
|
575
659
|
|
|
576
660
|
if (flagging) {
|
|
577
661
|
const clear = gpu.device.createCommandEncoder();
|
|
@@ -588,17 +672,17 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
588
672
|
|
|
589
673
|
this.check(revision);
|
|
590
674
|
|
|
591
|
-
const rows = this.scheduler.next(end - row);
|
|
675
|
+
const rows = this.scheduler.next(end - row, samples);
|
|
592
676
|
const started = performance.now();
|
|
593
677
|
const encoder = gpu.device.createCommandEncoder();
|
|
594
678
|
|
|
595
679
|
this.emit("bucket", { worker: 0, bucket: { x: 0, y: row, width, height: rows, index: band++ }, width, height });
|
|
596
680
|
this.pathTrace?.encode(encoder, {
|
|
597
|
-
...
|
|
681
|
+
...uniforms,
|
|
598
682
|
bandOrigin: [0, row],
|
|
599
683
|
bandSize: [width, rows],
|
|
600
|
-
sampleIndex:
|
|
601
|
-
sampleCount:
|
|
684
|
+
sampleIndex: taken,
|
|
685
|
+
sampleCount: samples,
|
|
602
686
|
adaptiveMin: adaptive ? ADAPTIVE_MIN : 0,
|
|
603
687
|
adaptiveThreshold: ADAPTIVE_THRESHOLD,
|
|
604
688
|
});
|
|
@@ -608,11 +692,12 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
608
692
|
if (show) this.encodePresent(encoder, width, height);
|
|
609
693
|
|
|
610
694
|
gpu.device.queue.submit([encoder.finish()]);
|
|
611
|
-
|
|
695
|
+
flight.push({ done: gpu.device.queue.onSubmittedWorkDone(), submitted: performance.now(), rows, samples });
|
|
696
|
+
|
|
697
|
+
while (flight.length >= BANDS_IN_FLIGHT) await settle();
|
|
612
698
|
|
|
613
|
-
this.scheduler.record(performance.now() - started, rows);
|
|
614
699
|
row += rows;
|
|
615
|
-
spent += rows;
|
|
700
|
+
spent += rows * samples;
|
|
616
701
|
this.emit("progress", { completed: Math.min(spent, budget), total: budget, phase: "shade" });
|
|
617
702
|
|
|
618
703
|
if (show) {
|
|
@@ -622,24 +707,35 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
622
707
|
}
|
|
623
708
|
}
|
|
624
709
|
|
|
625
|
-
|
|
710
|
+
taken += samples;
|
|
626
711
|
this.frameIndex++;
|
|
627
712
|
|
|
628
|
-
// The
|
|
629
|
-
//
|
|
630
|
-
|
|
713
|
+
// The rows this pass still traced in are copied out before anything else is queued, so they can be read while
|
|
714
|
+
// the device goes on with the denoiser.
|
|
715
|
+
const rowsRead = flagging ? this.activeRows(gpu, accumulator) : null;
|
|
716
|
+
|
|
717
|
+
// The canvas shows the denoised image, refreshed a few times a second while the bands refine underneath it:
|
|
718
|
+
// filtering the whole image after every pass would cost a cheap scene as much as tracing it.
|
|
719
|
+
if (denoise && this.canvas && this.context && performance.now() - denoisedAt >= DENOISE_EVERY_MS) {
|
|
631
720
|
const encoder = gpu.device.createCommandEncoder();
|
|
632
721
|
|
|
633
722
|
this.encodeDenoise(gpu, encoder, width, height);
|
|
634
723
|
this.encodePresent(encoder, width, height);
|
|
635
724
|
gpu.device.queue.submit([encoder.finish()]);
|
|
725
|
+
denoisedAt = performance.now();
|
|
636
726
|
}
|
|
637
727
|
|
|
638
728
|
this.emit("sample", { samples: Math.min(target, Math.round(spent / height)), target });
|
|
639
729
|
|
|
640
|
-
if (
|
|
730
|
+
if (rowsRead) {
|
|
731
|
+
while (flight.length) await settle();
|
|
732
|
+
|
|
733
|
+
active = await rowsRead;
|
|
734
|
+
}
|
|
641
735
|
}
|
|
642
736
|
|
|
737
|
+
while (flight.length) await settle();
|
|
738
|
+
|
|
643
739
|
// Converged early, or spent: either way the image is as finished as this frame makes it.
|
|
644
740
|
this.emit("progress", { completed: budget, total: budget, phase: "shade" });
|
|
645
741
|
this.emit("sample", { samples: target, target });
|
|
@@ -704,10 +800,20 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
704
800
|
height: number,
|
|
705
801
|
image: PackedEnvironment | null) {
|
|
706
802
|
this.pathTrace ??= new PathTracePass(gpu.device);
|
|
707
|
-
|
|
803
|
+
const materials = assets.scene.meshes.flatMap(mesh => mesh.materials);
|
|
804
|
+
|
|
805
|
+
this.pathTrace.setPrograms(assets.programs,
|
|
806
|
+
this.current.subsurface.enabled && materials.some(material =>
|
|
807
|
+
(material.type === "MeshStandardMaterial" || material.type === "MeshPhysicalMaterial")
|
|
808
|
+
&& subsurfaceOf(material) !== null),
|
|
809
|
+
// Even turned off, a volume's boundary has to stay invisible, which takes the same code.
|
|
810
|
+
materials.some(material => volumeOf(material) !== null),
|
|
811
|
+
this.current.bevel.enabled && materials.some(material => bevelOf(material) > 0));
|
|
708
812
|
|
|
709
813
|
// A procedural sky stands in for the renderer's own environment, as it does in the CPU renderer.
|
|
710
814
|
const environment = assets.sky?.environment ?? image;
|
|
815
|
+
|
|
816
|
+
this.environmentAverage = environment ? averageRadiance(environment) : null;
|
|
711
817
|
const matrices = cameraMatrices(camera, width, height);
|
|
712
818
|
const view = `${JSON.stringify(camera)}|${width}|${height}`;
|
|
713
819
|
const moved = (!!this.packed?.cameraDependent || this.planar) && view !== this.packedView;
|
|
@@ -741,7 +847,7 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
741
847
|
|
|
742
848
|
this.accumulator ??= new Accumulator(gpu.device, width, height);
|
|
743
849
|
|
|
744
|
-
if (resized) this.scheduler = new DispatchScheduler({ maxRows: height });
|
|
850
|
+
if (resized) this.scheduler = new DispatchScheduler({ maxRows: height, maxSamples: BATCH_PATHS });
|
|
745
851
|
|
|
746
852
|
this.pathTrace.setBindings(this.buffers, this.accumulator.buffer);
|
|
747
853
|
this.configureCanvas(gpu, width, height);
|
|
@@ -820,7 +926,10 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
820
926
|
this.check(revision);
|
|
821
927
|
|
|
822
928
|
const buffers = uploadScene(gpu.device, geometry, bvh, textures, lights, environment, cdf);
|
|
823
|
-
|
|
929
|
+
|
|
930
|
+
const textureOffsets = new Map([...textures.indexOf]
|
|
931
|
+
.map(([id, index]) => [id, textures.data[textureEntry(index) + 2]]));
|
|
932
|
+
|
|
824
933
|
const packed: PackedSummary = { cameraDependent: geometry.cameraDependent, textureOffsets };
|
|
825
934
|
|
|
826
935
|
if (buffers.bytes.largest > gpu.maxStorageBufferBindingSize) {
|
|
@@ -834,6 +943,7 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
834
943
|
skyData,
|
|
835
944
|
volumetricData,
|
|
836
945
|
volumetricCount: Math.min(volumetric.length / VOLUMETRIC_WORDS, 8),
|
|
946
|
+
volumes: volumeTriangles(geometry),
|
|
837
947
|
};
|
|
838
948
|
}
|
|
839
949
|
|
|
@@ -872,6 +982,10 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
872
982
|
const uniforms = {
|
|
873
983
|
...this.frameUniforms(capture.camera, width, height, assets, target, capture.background),
|
|
874
984
|
lens: [0, 1, 0, 0],
|
|
985
|
+
lensDistortion: 0,
|
|
986
|
+
// A capture is a texture of the scene, seen again by the camera, which lays the air over it then.
|
|
987
|
+
atmosphereColor: [0, 0, 0, 0],
|
|
988
|
+
cameraVolume: NO_MATERIAL,
|
|
875
989
|
volumetricCount: 0,
|
|
876
990
|
clipPlane: capture.clip ?? [0, 0, 0, 0],
|
|
877
991
|
bandOrigin: [0, 0],
|
|
@@ -925,21 +1039,37 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
925
1039
|
}
|
|
926
1040
|
|
|
927
1041
|
private encodePresent(encoder: GPUCommandEncoder, width: number, height: number) {
|
|
928
|
-
if (!this.context || !this.present) return;
|
|
1042
|
+
if (!this.context || !this.present || !this.gpu) return;
|
|
929
1043
|
|
|
930
1044
|
this.present.encode(encoder, this.context.getCurrentTexture().createView(), {
|
|
931
1045
|
width,
|
|
932
1046
|
height,
|
|
933
|
-
exposure: this.current
|
|
1047
|
+
exposure: effectiveExposure(this.current),
|
|
934
1048
|
toneMapping: this.current.toneMapping,
|
|
935
1049
|
vignette: this.current.vignette,
|
|
936
1050
|
grain: this.current.grain,
|
|
937
1051
|
frameIndex: this.frameIndex,
|
|
938
1052
|
transparent: this.current.background.mode === "transparent",
|
|
939
|
-
denoised: this.
|
|
1053
|
+
denoised: this.encodeLens(this.gpu, encoder, width, height,
|
|
1054
|
+
this.denoised ? this.denoisePass?.output ?? null : null),
|
|
940
1055
|
});
|
|
941
1056
|
}
|
|
942
1057
|
|
|
1058
|
+
/**
|
|
1059
|
+
* The lens and the sensor over the radiance (or the denoiser's output), when the settings ask for any of them: the
|
|
1060
|
+
* buffer of graded colour the display transform reads instead, or the input as it was.
|
|
1061
|
+
*/
|
|
1062
|
+
private encodeLens(gpu: RenderDevice, encoder: GPUCommandEncoder, width: number, height: number,
|
|
1063
|
+
denoised: GPUBuffer | null): GPUBuffer | null {
|
|
1064
|
+
const effects = lensEffects(this.current);
|
|
1065
|
+
|
|
1066
|
+
if (!effects) return denoised;
|
|
1067
|
+
|
|
1068
|
+
this.lensPass ??= new LensPass(gpu.device);
|
|
1069
|
+
|
|
1070
|
+
return this.lensPass.encode(encoder, (this.accumulator as Accumulator).buffer, denoised, width, height, effects);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
943
1073
|
/** Denoises the accumulation as it stands, for the canvas and for the finished image. */
|
|
944
1074
|
private encodeDenoise(gpu: RenderDevice, encoder: GPUCommandEncoder, width: number, height: number) {
|
|
945
1075
|
this.denoisePass ??= new DenoisePass(gpu.device);
|
|
@@ -973,6 +1103,16 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
973
1103
|
: 0;
|
|
974
1104
|
|
|
975
1105
|
const backgroundMode = background.mode === "color" ? 1 : background.mode === "transparent" ? 2 : 0;
|
|
1106
|
+
const { atmosphere } = this.current;
|
|
1107
|
+
const strength = sky ? sky.intensity : this.environment.intensity ?? 1;
|
|
1108
|
+
|
|
1109
|
+
const airGlow = atmosphere.color
|
|
1110
|
+
? linearColor(atmosphere.color, [0, 0, 0])
|
|
1111
|
+
: (this.environmentAverage ?? gradientAverage(linearColor(gradient?.topColor, [1, 1, 1]),
|
|
1112
|
+
linearColor(gradient?.bottomColor, [0, 0, 0]), gradient?.exponent ?? 2)).map(channel => channel * strength);
|
|
1113
|
+
|
|
1114
|
+
const extinction = atmosphereExtinction(atmosphere);
|
|
1115
|
+
const sun = brightestSun(assets.scene.lights);
|
|
976
1116
|
|
|
977
1117
|
return {
|
|
978
1118
|
cameraToWorld: matrices.cameraWorld,
|
|
@@ -1001,15 +1141,35 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
1001
1141
|
resolution: [width, height],
|
|
1002
1142
|
cameraKind: matrices.orthographic ? 1 : 0,
|
|
1003
1143
|
bounces: Math.max(1, Math.round(this.current.globalIllumination.bounces)),
|
|
1144
|
+
rouletteThreshold: this.current.globalIllumination.russianRoulette === false ? 0 : ROULETTE_THRESHOLD,
|
|
1145
|
+
rouletteStart: Math.max(0, Math.round(this.current.globalIllumination.rouletteStart ?? 0)),
|
|
1146
|
+
lensDistortion: Number.isFinite(this.current.lens.distortion) ? this.current.lens.distortion : 0,
|
|
1147
|
+
subsurfaceEnabled: this.current.subsurface.enabled,
|
|
1148
|
+
bevelEnabled: this.current.bevel.enabled,
|
|
1149
|
+
volumesEnabled: this.current.volumes.enabled,
|
|
1150
|
+
cameraVolume: this.current.volumes.enabled
|
|
1151
|
+
? containingVolume(target?.volumes ?? null, matrices.cameraWorld.slice(12, 15))
|
|
1152
|
+
: NO_MATERIAL,
|
|
1153
|
+
atmosphereColor: [...airGlow, extinction.some(value => value > 0) ? 1 : 0],
|
|
1154
|
+
atmosphereExtinction: [...extinction, Math.min(1, Math.max(0, Number(atmosphere.haze) || 0))],
|
|
1155
|
+
atmosphereSun: [...sun?.direction ?? [0, 1, 0], 0],
|
|
1156
|
+
atmosphereSunColor: [...sun?.irradiance ?? [0, 0, 0], 0],
|
|
1004
1157
|
triangleCount: buffers?.triangleCount ?? 0,
|
|
1005
1158
|
lightCount: buffers?.lightCount ?? 0,
|
|
1006
1159
|
areaLightCount: buffers?.areaLightCount ?? 0,
|
|
1160
|
+
lightCdf: buffers?.lightCdf ?? 0,
|
|
1161
|
+
lightTriangles: buffers?.lightTriangles ?? 0,
|
|
1162
|
+
localLightCount: buffers?.localLightCount ?? 0,
|
|
1163
|
+
localLightPick: buffers?.localLightPick ?? 0,
|
|
1164
|
+
lightTree: this.current.globalIllumination.lightTree === false ? 0 : buffers?.lightTree ?? 0,
|
|
1165
|
+
lightLeaves: buffers?.lightLeaves ?? 0,
|
|
1007
1166
|
clampIndirect: Math.max(0, this.current.globalIllumination.clamp),
|
|
1008
1167
|
// The setting is the angular diameter a light covers, in degrees, as it is for the CPU renderer's shadow blur.
|
|
1009
1168
|
shadowSpread: shadows.enabled ? Math.tan(Math.max(0, shadows.softness) * Math.PI / 360) : 0,
|
|
1010
1169
|
shadowsEnabled: shadows.enabled,
|
|
1011
1170
|
uvOffset: buffers?.uvOffset ?? 0,
|
|
1012
1171
|
orderOffset: buffers?.orderOffset ?? 0,
|
|
1172
|
+
leafOffset: buffers?.leafOffset ?? 0,
|
|
1013
1173
|
colorOffset: buffers?.colorOffset ?? 0,
|
|
1014
1174
|
hasColors: buffers?.hasColors ?? false,
|
|
1015
1175
|
varyingOffset: buffers?.varyingOffset ?? 0,
|
|
@@ -1037,13 +1197,20 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
1037
1197
|
private async readImage(
|
|
1038
1198
|
gpu: RenderDevice, width: number, height: number, denoised: GPUBuffer | null): Promise<RenderImage> {
|
|
1039
1199
|
const accumulator = this.accumulator as Accumulator;
|
|
1040
|
-
const {
|
|
1200
|
+
const { toneMapping, vignette } = this.current;
|
|
1201
|
+
const exposure = effectiveExposure(this.current);
|
|
1041
1202
|
const transparent = this.current.background.mode === "transparent";
|
|
1042
1203
|
|
|
1043
1204
|
// Resolved on the device, so the page only copies the bytes of the image.
|
|
1044
1205
|
this.imageResolve ??= new ImageResolvePass(gpu.device);
|
|
1045
1206
|
|
|
1046
|
-
|
|
1207
|
+
// The lens's effects are encoded on their own, ahead of the resolve, which reads the colour they leave.
|
|
1208
|
+
const encoder = gpu.device.createCommandEncoder({ label: "lens" });
|
|
1209
|
+
const graded = this.encodeLens(gpu, encoder, width, height, denoised);
|
|
1210
|
+
|
|
1211
|
+
gpu.device.queue.submit([encoder.finish()]);
|
|
1212
|
+
|
|
1213
|
+
const data = await this.imageResolve.read(accumulator.buffer, graded, width, height,
|
|
1047
1214
|
{ exposure, toneMapping, vignette, transparent });
|
|
1048
1215
|
|
|
1049
1216
|
return { width, height, data };
|
|
@@ -1064,6 +1231,9 @@ const SERIALIZABLE = new Set([
|
|
|
1064
1231
|
*/
|
|
1065
1232
|
async function serializeLive(scene: Scene): Promise<SerializedScene> {
|
|
1066
1233
|
const renamed = new Map<Material, string>();
|
|
1234
|
+
// The snapshot keeps a material's name but not its media, so the name carries a mark to them.
|
|
1235
|
+
const named = new Map<Material, string>();
|
|
1236
|
+
const media: { subsurface?: unknown; volume?: unknown; bevel?: unknown }[] = [];
|
|
1067
1237
|
|
|
1068
1238
|
scene.traverse(object => {
|
|
1069
1239
|
const value = (object as Object3D & { material?: Material | Material[] }).material;
|
|
@@ -1075,15 +1245,60 @@ async function serializeLive(scene: Scene): Promise<SerializedScene> {
|
|
|
1075
1245
|
renamed.set(material, material.type);
|
|
1076
1246
|
material.type = shader.isRawShaderMaterial ? "RawShaderMaterial" : "ShaderMaterial";
|
|
1077
1247
|
}
|
|
1248
|
+
|
|
1249
|
+
const data = material.userData?.cpuRenderer as
|
|
1250
|
+
{ subsurface?: unknown; volume?: unknown; bevel?: unknown } | undefined;
|
|
1251
|
+
|
|
1252
|
+
if (!named.has(material)
|
|
1253
|
+
&& (subsurfaceMedium(data?.subsurface) || volumeMedium(data?.volume) || bevelRadius(data?.bevel) > 0)) {
|
|
1254
|
+
named.set(material, material.name);
|
|
1255
|
+
material.name = `${material.name}${MEDIA_MARK}${media.length}`;
|
|
1256
|
+
media.push({ subsurface: data?.subsurface, volume: data?.volume, bevel: data?.bevel });
|
|
1257
|
+
}
|
|
1078
1258
|
}
|
|
1079
1259
|
});
|
|
1080
1260
|
|
|
1261
|
+
let snapshot: SerializedScene;
|
|
1262
|
+
|
|
1081
1263
|
try {
|
|
1082
1264
|
// The path tracer samples only the base level of a texture: the mip chain would be built and thrown away.
|
|
1083
|
-
|
|
1265
|
+
snapshot = (await serializeScene(scene, { mipmaps: false })).scene;
|
|
1084
1266
|
} finally {
|
|
1085
1267
|
for (const [material, type] of renamed) material.type = type;
|
|
1268
|
+
for (const [material, name] of named) material.name = name;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// Materials are shared between the meshes that use them, so each one is unmarked once and seen marked no more.
|
|
1272
|
+
for (const mesh of snapshot.meshes) {
|
|
1273
|
+
for (const material of mesh.materials as (SubsurfaceMaterial & VolumeMaterial & BevelMaterial)[]) {
|
|
1274
|
+
const at = material.name.lastIndexOf(MEDIA_MARK);
|
|
1275
|
+
|
|
1276
|
+
if (at < 0) continue;
|
|
1277
|
+
|
|
1278
|
+
const medium = media[Number(material.name.slice(at + MEDIA_MARK.length))];
|
|
1279
|
+
|
|
1280
|
+
material.name = material.name.slice(0, at) || material.type;
|
|
1281
|
+
if (medium.subsurface !== undefined) material.subsurface = medium.subsurface as SubsurfaceMaterial["subsurface"];
|
|
1282
|
+
if (medium.volume !== undefined) material.volume = medium.volume as VolumeMaterial["volume"];
|
|
1283
|
+
if (medium.bevel !== undefined) material.bevel = medium.bevel as BevelMaterial["bevel"];
|
|
1284
|
+
}
|
|
1086
1285
|
}
|
|
1286
|
+
|
|
1287
|
+
return snapshot;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
/** Ends a material's name while it is serialized, followed by the index of its media. */
|
|
1291
|
+
const MEDIA_MARK = "\u0000media:";
|
|
1292
|
+
|
|
1293
|
+
/**
|
|
1294
|
+
* Everything that changes the finished image without tracing it again, as one string: the denoiser switch and what
|
|
1295
|
+
* happens to the light between the radiance and the screen.
|
|
1296
|
+
*/
|
|
1297
|
+
function developing(settings: GpuRenderSettings): string {
|
|
1298
|
+
const { exposure, toneMapping, vignette, bloom, glare, lens, camera, depthOfField, globalIllumination } = settings;
|
|
1299
|
+
|
|
1300
|
+
return JSON.stringify([globalIllumination.denoise, exposure, toneMapping, vignette, bloom, glare,
|
|
1301
|
+
lens.chromaticAberration, camera, camera.physicalExposure ? depthOfField.aperture : 0]);
|
|
1087
1302
|
}
|
|
1088
1303
|
|
|
1089
1304
|
/**
|
package/src/sceneUpload.ts
CHANGED
|
@@ -11,7 +11,7 @@ export interface SceneByteLength {
|
|
|
11
11
|
materials: number;
|
|
12
12
|
/** Vertex normals, the UV sets, vertex colours and shader varyings, in one buffer. */
|
|
13
13
|
attributes: number;
|
|
14
|
-
/** One material index per triangle
|
|
14
|
+
/** One material index per triangle, then the hierarchy's triangle order and its inverse, in one buffer. */
|
|
15
15
|
indices: number;
|
|
16
16
|
nodes: number;
|
|
17
17
|
lights: number;
|
|
@@ -37,11 +37,21 @@ export interface SceneBuffers {
|
|
|
37
37
|
varyingOffset: number;
|
|
38
38
|
hasColors: boolean;
|
|
39
39
|
orderOffset: number;
|
|
40
|
+
/** Where each triangle's position in the leaf order starts inside `indices`. */
|
|
41
|
+
leafOffset: number;
|
|
40
42
|
triangleCount: number;
|
|
41
43
|
/** Everything the integrator can aim a shadow ray at. */
|
|
42
44
|
lightCount: number;
|
|
43
45
|
/** How many of those are emissive triangles, which lead the buffer. */
|
|
44
46
|
areaLightCount: number;
|
|
47
|
+
/** Where the lights' cumulative probabilities and the emissive ones' triangles sit in `textures`. */
|
|
48
|
+
lightCdf: number;
|
|
49
|
+
lightTriangles: number;
|
|
50
|
+
/** The light tree over the lights that sit in the scene: see lightTree.ts. */
|
|
51
|
+
localLightCount: number;
|
|
52
|
+
localLightPick: number;
|
|
53
|
+
lightTree: number;
|
|
54
|
+
lightLeaves: number;
|
|
45
55
|
/** Radiance an ambient or hemisphere light adds, which no shadow ray can reach. */
|
|
46
56
|
ambient: PackedLights["ambient"];
|
|
47
57
|
/** Where the surrounding image and its distributions ended up, for the frame uniform. */
|
|
@@ -76,7 +86,7 @@ export function sceneByteLength(
|
|
|
76
86
|
const lengths = {
|
|
77
87
|
positions: sized(geometry.positions.byteLength),
|
|
78
88
|
attributes: sized(attributeBytes(geometry)),
|
|
79
|
-
indices: sized(geometry.materials.byteLength + bvh.order.byteLength),
|
|
89
|
+
indices: sized(geometry.materials.byteLength + 2 * bvh.order.byteLength),
|
|
80
90
|
materials: sized(Math.max(1, geometry.materialCount) * MATERIAL_WORDS * 4),
|
|
81
91
|
nodes: sized(bvh.nodes.byteLength),
|
|
82
92
|
lights: sized(lights.entries.byteLength),
|
|
@@ -110,6 +120,25 @@ export function environmentPower(environment: PackedEnvironment | null): number
|
|
|
110
120
|
return environment ? environment.total * 2 * Math.PI ** 2 / (environment.width * environment.height) : 0;
|
|
111
121
|
}
|
|
112
122
|
|
|
123
|
+
/**
|
|
124
|
+
* The positions in the hierarchy's leaf order, which is the order its traversal reads them in, so a leaf is one run of
|
|
125
|
+
* the buffer; and where each triangle went, for everything that reads a triangle by its own number.
|
|
126
|
+
*/
|
|
127
|
+
export function leafOrder(
|
|
128
|
+
positions: Float32Array, order: Uint32Array): { positions: Float32Array; inverse: Uint32Array } {
|
|
129
|
+
const ordered = new Float32Array(order.length * 9);
|
|
130
|
+
const inverse = new Uint32Array(order.length);
|
|
131
|
+
|
|
132
|
+
for (let at = 0; at < order.length; at++) {
|
|
133
|
+
const triangle = order[at];
|
|
134
|
+
|
|
135
|
+
ordered.set(positions.subarray(triangle * 9, triangle * 9 + 9), at * 9);
|
|
136
|
+
inverse[triangle] = at;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { positions: ordered, inverse };
|
|
140
|
+
}
|
|
141
|
+
|
|
113
142
|
export function uploadScene(
|
|
114
143
|
device: GPUDevice,
|
|
115
144
|
geometry: PackedGeometry,
|
|
@@ -147,11 +176,12 @@ export function uploadScene(
|
|
|
147
176
|
|
|
148
177
|
const colors = geometry.colors ?? new Float32Array(0);
|
|
149
178
|
const varyings = geometry.varyings ?? new Float32Array(0);
|
|
179
|
+
const leaves = leafOrder(geometry.positions, bvh.order);
|
|
150
180
|
|
|
151
181
|
const buffers = {
|
|
152
|
-
positions: write(
|
|
182
|
+
positions: write(leaves.positions, lengths.positions),
|
|
153
183
|
attributes: join([geometry.normals, geometry.uvs, colors, varyings], lengths.attributes),
|
|
154
|
-
indices: join([geometry.materials, bvh.order], lengths.indices),
|
|
184
|
+
indices: join([geometry.materials, bvh.order, leaves.inverse], lengths.indices),
|
|
155
185
|
materials: write(new Uint8Array(packMaterials(geometry)), lengths.materials),
|
|
156
186
|
nodes: write(new Uint8Array(bvh.nodes), lengths.nodes),
|
|
157
187
|
lights: write(new Uint8Array(lights.entries), lengths.lights),
|
|
@@ -166,9 +196,16 @@ export function uploadScene(
|
|
|
166
196
|
varyingOffset: geometry.normals.length + geometry.uvs.length + colors.length,
|
|
167
197
|
hasColors: colors.length > 0,
|
|
168
198
|
orderOffset: geometry.materials.length,
|
|
199
|
+
leafOffset: geometry.materials.length + bvh.order.length,
|
|
169
200
|
triangleCount: geometry.triangleCount,
|
|
170
201
|
lightCount: lights.count,
|
|
171
202
|
areaLightCount: lights.areaCount,
|
|
203
|
+
lightCdf: lights.cdfAt,
|
|
204
|
+
lightTriangles: lights.trianglesAt,
|
|
205
|
+
localLightCount: lights.localCount,
|
|
206
|
+
localLightPick: lights.localPick,
|
|
207
|
+
lightTree: lights.treeAt,
|
|
208
|
+
lightLeaves: lights.leavesAt,
|
|
172
209
|
ambient: lights.ambient,
|
|
173
210
|
environment: {
|
|
174
211
|
texture: environment ? textures.indexOf.get(environment.texture.id) ?? NO_TEXTURE : NO_TEXTURE,
|