@tinypoly/gpu-renderer 0.0.2 → 0.0.4

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.
@@ -11,6 +11,11 @@ export interface SceneAssets {
11
11
  scene: SerializedScene;
12
12
  programs: ShaderPrograms;
13
13
  sky: ProceduralSkyEnvironment | null;
14
+ /** The baked sky, kept by the renderer so a scene with the same sky does not bake it again. */
15
+ skyBake: {
16
+ key: string;
17
+ baked: Promise<ProceduralSkyEnvironment | null>;
18
+ } | null;
14
19
  }
15
20
  export interface ProceduralSkyEnvironment {
16
21
  environment: PackedEnvironment;
@@ -20,5 +25,18 @@ export interface ProceduralSkyEnvironment {
20
25
  background: boolean;
21
26
  uniforms: Record<string, UniformValue>;
22
27
  }
23
- export declare function prepareSceneAssets(source: SerializedScene): Promise<SceneAssets>;
28
+ type ProceduralSky = NonNullable<SerializedScene["sky"]>;
29
+ /** What decides a baked sky: the same shader with the same uniforms bakes the same panorama. */
30
+ export declare function skyKey(sky: ProceduralSky | undefined): string;
31
+ export interface SceneAssetOptions {
32
+ /** Where the sky is baked; without one, on the CPU. */
33
+ device?: GPUDevice;
34
+ /** A sky baked for an earlier scene, reused when its key matches this scene's. */
35
+ sky?: {
36
+ key: string;
37
+ baked: Promise<ProceduralSkyEnvironment | null>;
38
+ } | null;
39
+ }
40
+ export declare function prepareSceneAssets(source: SerializedScene, options?: SceneAssetOptions): Promise<SceneAssets>;
41
+ export {};
24
42
  //# sourceMappingURL=sceneAssets.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sceneAssets.d.ts","sourceRoot":"","sources":["../src/sceneAssets.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAChF,OAAO,EAAyB,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEhF,OAAO,KAAK,EAAsB,eAAe,EAAqB,YAAY,EAAE,MAAM,eAAe,CAAC;AAE1G;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,yFAAyF;IACzF,KAAK,EAAE,eAAe,CAAC;IACvB,QAAQ,EAAE,cAAc,CAAC;IACzB,GAAG,EAAE,wBAAwB,GAAG,IAAI,CAAC;CACtC;AAED,MAAM,WAAW,wBAAwB;IACvC,WAAW,EAAE,iBAAiB,CAAC;IAC/B,6EAA6E;IAC7E,SAAS,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,UAAU,EAAE,OAAO,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CACxC;AAoFD,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,WAAW,CAAC,CAStF"}
1
+ {"version":3,"file":"sceneAssets.d.ts","sourceRoot":"","sources":["../src/sceneAssets.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAChF,OAAO,EAAyB,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGhF,OAAO,KAAK,EAAsB,eAAe,EAAqB,YAAY,EAAE,MAAM,eAAe,CAAC;AAE1G;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,yFAAyF;IACzF,KAAK,EAAE,eAAe,CAAC;IACvB,QAAQ,EAAE,cAAc,CAAC;IACzB,GAAG,EAAE,wBAAwB,GAAG,IAAI,CAAC;IACrC,+FAA+F;IAC/F,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAC,wBAAwB,GAAG,IAAI,CAAC,CAAA;KAAE,GAAG,IAAI,CAAC;CAClF;AAED,MAAM,WAAW,wBAAwB;IACvC,WAAW,EAAE,iBAAiB,CAAC;IAC/B,6EAA6E;IAC7E,SAAS,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,UAAU,EAAE,OAAO,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CACxC;AA6DD,KAAK,aAAa,GAAG,WAAW,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC;AA0CzD,gGAAgG;AAChG,wBAAgB,MAAM,CAAC,GAAG,EAAE,aAAa,GAAG,SAAS,GAAG,MAAM,CAE7D;AAED,MAAM,WAAW,iBAAiB;IAChC,uDAAuD;IACvD,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,kFAAkF;IAClF,GAAG,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAC,wBAAwB,GAAG,IAAI,CAAC,CAAA;KAAE,GAAG,IAAI,CAAC;CAC/E;AAED,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,eAAe,EAAE,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,WAAW,CAAC,CAmBhF"}
@@ -1,5 +1,6 @@
1
1
  import { environmentFromImage } from "./environment.js";
2
2
  import { compileShaderPrograms } from "./shaderProgram.js";
3
+ import { bakeSkyOnDevice, compileSkyProgram } from "./skyBake.js";
3
4
  import { CpuComputePipeline, CpuEnvironment } from "./upstream.js";
4
5
  /** How wide the sky is baked. Its foreground, the sharp part, is evaluated per pixel instead. */
5
6
  const SKY_WIDTH = 256;
@@ -48,30 +49,58 @@ function bakeComputations(source) {
48
49
  })),
49
50
  };
50
51
  }
51
- /** Bakes a procedural sky with the CPU renderer's own interpreter, at the size its environment uses. */
52
- function bakeSky(scene) {
53
- const sky = scene.sky;
54
- if (!sky)
55
- return null;
52
+ /** Bakes a procedural sky with the CPU renderer's own interpreter: the fallback, when the device cannot run it. */
53
+ function bakeSkyOnCpu(sky) {
56
54
  const generator = CpuEnvironment.fromProcedural(sky, SKY_WIDTH);
57
55
  let step = generator.next();
58
56
  while (!step.done)
59
57
  step = generator.next();
60
58
  // The snapshot is the environment's public face; its texture is the baked panorama.
61
59
  const texture = step.value.snapshot().texture;
62
- if (!texture || !(texture.data instanceof Float32Array))
60
+ return texture && texture.data instanceof Float32Array ? texture.data : null;
61
+ }
62
+ /**
63
+ * Bakes a procedural sky at the size its environment uses. On a device the sky's shader is translated and run there,
64
+ * over every texel at once; the CPU renderer's interpreter, which runs it texel by texel, is what is left when there is
65
+ * no device or the shader does not translate.
66
+ */
67
+ async function bakeSky(sky, device) {
68
+ let radiance = null;
69
+ if (device) {
70
+ try {
71
+ radiance = await bakeSkyOnDevice(device, await compileSkyProgram(sky.fragmentShader), sky.uniforms, SKY_WIDTH);
72
+ }
73
+ catch (error) {
74
+ console.warn("GPU renderer: the procedural sky is baked on the CPU, its shader did not run on the device.", error);
75
+ }
76
+ }
77
+ radiance ??= bakeSkyOnCpu(sky);
78
+ if (!radiance)
63
79
  return null;
64
- const environment = environmentFromImage(texture.data, texture.width, texture.height, "procedural-sky");
80
+ const environment = environmentFromImage(radiance, SKY_WIDTH, SKY_WIDTH / 2, "procedural-sky");
65
81
  if (!environment)
66
82
  return null;
67
83
  return { environment, intensity: sky.intensity, background: sky.background, uniforms: sky.uniforms };
68
84
  }
69
- export async function prepareSceneAssets(source) {
85
+ /** What decides a baked sky: the same shader with the same uniforms bakes the same panorama. */
86
+ export function skyKey(sky) {
87
+ return sky ? JSON.stringify([sky.fragmentShader, sky.uniforms]) : "";
88
+ }
89
+ export async function prepareSceneAssets(source, options = {}) {
70
90
  const scene = source.meshes.some(mesh => mesh.materials.some(material => material?.compute))
71
91
  ? bakeComputations(source)
72
92
  : source;
73
- const sky = bakeSky(scene);
74
- const programs = await compileShaderPrograms(scene, scene.sky?.foregroundShader);
75
- return { scene, programs, sky };
93
+ const key = skyKey(scene.sky);
94
+ const reused = options.sky && options.sky.key === key ? options.sky.baked : null;
95
+ const baked = reused ?? (scene.sky ? bakeSky(scene.sky, options.device) : Promise.resolve(null));
96
+ const [sky, programs] = await Promise.all([baked, compileShaderPrograms(scene, scene.sky?.foregroundShader)]);
97
+ // The sky the scene uses, with its baked panorama, for the next scene to reuse.
98
+ const skyBake = scene.sky ? { key, baked } : null;
99
+ // The panorama comes from the shader and its uniforms; how strongly it lights and whether it paints the background
100
+ // are this scene's own.
101
+ const current = sky && scene.sky
102
+ ? { ...sky, intensity: scene.sky.intensity, background: scene.sky.background, uniforms: scene.sky.uniforms }
103
+ : null;
104
+ return { scene, programs, sky: current, skyBake };
76
105
  }
77
106
  //# sourceMappingURL=sceneAssets.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sceneAssets.js","sourceRoot":"","sources":["../src/sceneAssets.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sceneAssets.js","sourceRoot":"","sources":["../src/sceneAssets.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import { type ShaderProgram } from "./shaderProgram.js";
2
+ import type { UniformValue } from "./upstream.js";
3
+ /**
4
+ * The bake's own kernel: the kernel's support code up to the translated shaders, the sky's program, and an entry that
5
+ * runs it once per texel of the panorama, along the direction the CPU renderer's bake gives that texel. The output
6
+ * goes to the binding the path tracer accumulates into, which is the one read-write buffer the support code declares.
7
+ */
8
+ export declare function skyBakeSource(program: ShaderProgram, width: number, height: number): string;
9
+ /** Translates a procedural sky's shader for the bake; the sky reads its direction from `vDirection`, as the CPU's does. */
10
+ export declare function compileSkyProgram(fragmentShader: string): Promise<ShaderProgram>;
11
+ /**
12
+ * Runs a procedural sky's shader on the device over an equirectangular panorama and reads the radiance back, as RGBA
13
+ * floats, row by row from the top: the same image the CPU renderer's interpreter bakes, in a fraction of the time. A
14
+ * texel that comes out not finite reads black, where the CPU bake would have refused the whole sky.
15
+ */
16
+ export declare function bakeSkyOnDevice(device: GPUDevice, program: ShaderProgram, uniforms: Record<string, UniformValue>, width: number): Promise<Float32Array>;
17
+ //# sourceMappingURL=skyBake.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skyBake.d.ts","sourceRoot":"","sources":["../src/skyBake.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAUlD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAuB3F;AAED,2HAA2H;AAC3H,wBAAgB,iBAAiB,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAEhF;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,EAAE,KAAK,EAAE,MAAM,GAC/F,OAAO,CAAC,YAAY,CAAC,CA0DvB"}
@@ -0,0 +1,97 @@
1
+ import { compileProgram } from "./shaderProgram.js";
2
+ import { createShaderModule } from "./shaderModule.js";
3
+ import { BVH_WGSL } from "./wgsl/bvh.wgsl.js";
4
+ import { COMMON_WGSL } from "./wgsl/common.wgsl.js";
5
+ import { RANDOM_WGSL } from "./wgsl/random.wgsl.js";
6
+ import { SAMPLING_WGSL } from "./wgsl/sampling.wgsl.js";
7
+ import { SHADER_SUPPORT_WGSL } from "./wgsl/shaderSupport.wgsl.js";
8
+ import { TEXTURE_WGSL } from "./wgsl/texture.wgsl.js";
9
+ const WORKGROUP = 8;
10
+ /**
11
+ * The bake's own kernel: the kernel's support code up to the translated shaders, the sky's program, and an entry that
12
+ * runs it once per texel of the panorama, along the direction the CPU renderer's bake gives that texel. The output
13
+ * goes to the binding the path tracer accumulates into, which is the one read-write buffer the support code declares.
14
+ */
15
+ export function skyBakeSource(program, width, height) {
16
+ return `${COMMON_WGSL}${RANDOM_WGSL}${BVH_WGSL}${SAMPLING_WGSL}${TEXTURE_WGSL}${SHADER_SUPPORT_WGSL}
17
+ ${program.wgsl}
18
+
19
+ @compute @workgroup_size(${WORKGROUP}, ${WORKGROUP})
20
+ fn main(@builtin(global_invocation_id) id: vec3<u32>) {
21
+ if (id.x >= ${width}u || id.y >= ${height}u) {
22
+ return;
23
+ }
24
+
25
+ let u = (f32(id.x) + 0.5) / ${width}.0;
26
+ let v = (f32(id.y) + 0.5) / ${height}.0;
27
+ let phi = (u - 0.5) * 2.0 * PI;
28
+ let theta = v * PI;
29
+ var input: ShaderInput;
30
+ input.data = 0u;
31
+ input.direction = vec3<f32>(sin(theta) * cos(phi), cos(theta), sin(theta) * sin(phi));
32
+ input.fragCoord = vec4<f32>(f32(id.x) + 0.5, f32(id.y) + 0.5, 0.0, 1.0);
33
+ input.diffuseColor = vec4<f32>(0.0, 0.0, 0.0, 1.0);
34
+
35
+ accumulation[id.y * ${width}u + id.x] = p${program.id}_run(input).color;
36
+ }
37
+ `;
38
+ }
39
+ /** Translates a procedural sky's shader for the bake; the sky reads its direction from `vDirection`, as the CPU's does. */
40
+ export function compileSkyProgram(fragmentShader) {
41
+ return compileProgram(0, "foreground", { fragment: fragmentShader, defines: {} }, true);
42
+ }
43
+ /**
44
+ * Runs a procedural sky's shader on the device over an equirectangular panorama and reads the radiance back, as RGBA
45
+ * floats, row by row from the top: the same image the CPU renderer's interpreter bakes, in a fraction of the time. A
46
+ * texel that comes out not finite reads black, where the CPU bake would have refused the whole sky.
47
+ */
48
+ export async function bakeSkyOnDevice(device, program, uniforms, width) {
49
+ const height = width / 2;
50
+ const texels = width * height;
51
+ const words = program.packUniforms(uniforms, new Map());
52
+ const module = createShaderModule(device, "sky bake", skyBakeSource(program, width, height));
53
+ const layout = device.createBindGroupLayout({
54
+ label: "sky bake",
55
+ entries: [
56
+ { binding: 7, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } },
57
+ { binding: 8, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
58
+ ],
59
+ });
60
+ const pipeline = await device.createComputePipelineAsync({
61
+ label: "sky bake",
62
+ layout: device.createPipelineLayout({ bindGroupLayouts: [layout] }),
63
+ compute: { module, entryPoint: "main" },
64
+ });
65
+ const output = device.createBuffer({ size: texels * 16, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC });
66
+ const data = device.createBuffer({
67
+ size: Math.max(16, words.byteLength), usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
68
+ });
69
+ const staging = device.createBuffer({ size: texels * 16, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST });
70
+ try {
71
+ device.queue.writeBuffer(data, 0, words);
72
+ const encoder = device.createCommandEncoder({ label: "sky bake" });
73
+ const pass = encoder.beginComputePass({ label: "sky bake" });
74
+ pass.setPipeline(pipeline);
75
+ pass.setBindGroup(0, device.createBindGroup({
76
+ layout,
77
+ entries: [{ binding: 7, resource: { buffer: output } }, { binding: 8, resource: { buffer: data } }],
78
+ }));
79
+ pass.dispatchWorkgroups(Math.ceil(width / WORKGROUP), Math.ceil(height / WORKGROUP));
80
+ pass.end();
81
+ encoder.copyBufferToBuffer(output, 0, staging, 0, texels * 16);
82
+ device.queue.submit([encoder.finish()]);
83
+ await staging.mapAsync(GPUMapMode.READ);
84
+ const radiance = new Float32Array(staging.getMappedRange().slice(0));
85
+ staging.unmap();
86
+ for (let i = 0; i < radiance.length; i++)
87
+ if (!Number.isFinite(radiance[i]))
88
+ radiance[i] = 0;
89
+ return radiance;
90
+ }
91
+ finally {
92
+ output.destroy();
93
+ data.destroy();
94
+ staging.destroy();
95
+ }
96
+ }
97
+ //# sourceMappingURL=skyBake.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skyBake.js","sourceRoot":"","sources":["../src/skyBake.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tinypoly/gpu-renderer",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "WebGPU path tracer for Three.js scenes: BVH traversal, progressive accumulation and tone mapping in WGSL compute shaders.",
5
5
  "keywords": [
6
6
  "three",
@@ -0,0 +1,136 @@
1
+ import { createShaderModule } from "./shaderModule.js";
2
+ import type { RenderToneMapping } from "./upstream.js";
3
+ import { PRESENT_WGSL } from "./wgsl/present.wgsl.js";
4
+
5
+ /** The order the presentation shader's `toneMapping` switch expects. */
6
+ const TONE_MAPPING = ["aces", "agx", "neutral", "linear"] as const;
7
+
8
+ const UNIFORM_BYTES = 48;
9
+
10
+ const WORKGROUP = 8;
11
+
12
+ /**
13
+ * The finished image, resolved on the device: the presentation pass's display transform, run as a compute pass that
14
+ * writes straight-alpha sRGB bytes, four per pixel. Reading those back is a sixth of the float accumulation and needs
15
+ * no loop over the pixels on the page, which is what held it for seconds at the end of a large frame.
16
+ *
17
+ * It follows the image `render()` has always resolved: no grain, and the vignette measured from each pixel's corner.
18
+ */
19
+ export const IMAGE_RESOLVE_WGSL = /* wgsl */ `${PRESENT_WGSL}
20
+ @group(0) @binding(3) var<storage, read_write> pixels: array<u32>;
21
+
22
+ fn encodeByte(value: f32) -> u32 {
23
+ return u32(round(clamp(value, 0.0, 1.0) * 255.0));
24
+ }
25
+
26
+ @compute @workgroup_size(${WORKGROUP}, ${WORKGROUP})
27
+ fn resolveMain(@builtin(global_invocation_id) id: vec3<u32>) {
28
+ let width = present.resolution.x;
29
+ let height = present.resolution.y;
30
+
31
+ if (id.x >= width || id.y >= height) {
32
+ return;
33
+ }
34
+
35
+ let index = id.y * width + id.x;
36
+ let stored = accumulation[index];
37
+ let samples = stored.w;
38
+ var alpha = 1.0;
39
+
40
+ if (present.transparent != 0u) {
41
+ alpha = select(0.0, clamp(accumulation[width * height + index].x / samples, 0.0, 1.0), samples > 0.0);
42
+ }
43
+
44
+ var color = vec3<f32>(0.0);
45
+
46
+ if (samples > 0.0 && alpha > 0.0) {
47
+ let mean = select(stored.rgb / samples, denoised[index].rgb, present.denoised != 0u);
48
+ let centred = vec2<f32>(f32(id.x) / f32(width), f32(id.y) / f32(height)) - 0.5;
49
+ let falloff = max(0.0, 1.0 - present.vignette * dot(centred, centred) * 2.0);
50
+
51
+ color = mean / alpha * falloff;
52
+ }
53
+
54
+ let encoded = linearToSrgb(toneMap(color));
55
+
56
+ pixels[index] = encodeByte(encoded.r) | (encodeByte(encoded.g) << 8u) | (encodeByte(encoded.b) << 16u)
57
+ | (encodeByte(alpha) << 24u);
58
+ }
59
+ `;
60
+
61
+ export interface ResolveSettings {
62
+ exposure: number;
63
+ toneMapping: RenderToneMapping;
64
+ vignette: number;
65
+ transparent: boolean;
66
+ }
67
+
68
+ export class ImageResolvePass {
69
+ private readonly pipeline: GPUComputePipeline;
70
+ private readonly uniforms: GPUBuffer;
71
+ private readonly values = new ArrayBuffer(UNIFORM_BYTES);
72
+
73
+ constructor(private readonly device: GPUDevice) {
74
+ const module = createShaderModule(device, "image resolve", IMAGE_RESOLVE_WGSL);
75
+
76
+ this.pipeline = device.createComputePipeline({ layout: "auto", compute: { module, entryPoint: "resolveMain" } });
77
+ this.uniforms = device.createBuffer({
78
+ size: UNIFORM_BYTES, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
79
+ });
80
+ }
81
+
82
+ /** The image as RGBA bytes, straight alpha, row by row from the top. */
83
+ async read(accumulation: GPUBuffer, denoised: GPUBuffer | null, width: number, height: number,
84
+ settings: ResolveSettings): Promise<Uint8ClampedArray<ArrayBuffer>> {
85
+ const floats = new Float32Array(this.values), integers = new Uint32Array(this.values);
86
+
87
+ integers.fill(0);
88
+ integers[0] = width;
89
+ integers[1] = height;
90
+ floats[2] = settings.exposure;
91
+ integers[3] = Math.max(0, TONE_MAPPING.indexOf(settings.toneMapping));
92
+ floats[4] = settings.vignette;
93
+ integers[7] = settings.transparent ? 1 : 0;
94
+ integers[8] = denoised ? 1 : 0;
95
+ this.device.queue.writeBuffer(this.uniforms, 0, this.values);
96
+
97
+ const bytes = width * height * 4;
98
+ const output = this.device.createBuffer({ size: bytes, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC });
99
+ const staging = this.device.createBuffer({ size: bytes, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST });
100
+
101
+ try {
102
+ const encoder = this.device.createCommandEncoder({ label: "image resolve" });
103
+ const pass = encoder.beginComputePass({ label: "image resolve" });
104
+
105
+ pass.setPipeline(this.pipeline);
106
+ pass.setBindGroup(0, this.device.createBindGroup({
107
+ layout: this.pipeline.getBindGroupLayout(0),
108
+ entries: [
109
+ { binding: 0, resource: { buffer: this.uniforms } },
110
+ { binding: 1, resource: { buffer: accumulation } },
111
+ { binding: 2, resource: { buffer: denoised ?? accumulation } },
112
+ { binding: 3, resource: { buffer: output } },
113
+ ],
114
+ }));
115
+ pass.dispatchWorkgroups(Math.ceil(width / WORKGROUP), Math.ceil(height / WORKGROUP));
116
+ pass.end();
117
+ encoder.copyBufferToBuffer(output, 0, staging, 0, bytes);
118
+ this.device.queue.submit([encoder.finish()]);
119
+
120
+ await staging.mapAsync(GPUMapMode.READ);
121
+
122
+ const data = new Uint8ClampedArray(staging.getMappedRange().slice(0));
123
+
124
+ staging.unmap();
125
+
126
+ return data;
127
+ } finally {
128
+ output.destroy();
129
+ staging.destroy();
130
+ }
131
+ }
132
+
133
+ dispose() {
134
+ this.uniforms.destroy();
135
+ }
136
+ }
package/src/renderer.ts CHANGED
@@ -1,16 +1,16 @@
1
1
  import { PerspectiveCamera, type LightProbe, type Material, type Object3D, type Scene } from "three";
2
2
  import { Accumulator } from "./accumulator.js";
3
- import { type PackedBvh } from "./bvh.js";
4
3
  import { BvhBuilder, createDefaultBvhWorker, type BvhWorkerFactory } from "./bvhBuilder.js";
5
4
  import { cameraMatrices, type CameraMatrices } from "./camera.js";
6
5
  import { requestRenderDevice, type RenderDevice } from "./device.js";
7
6
  import { loadEnvironment, type PackedEnvironment } from "./environment.js";
8
7
  import { DispatchScheduler } from "./dispatchScheduler.js";
9
- import { packGeometry, type PackedGeometry } from "./geometry.js";
8
+ import { packGeometry } from "./geometry.js";
10
9
  import { packLights } from "./lights.js";
11
10
  import { NO_PROGRAM } from "./materialLayout.js";
12
- import { NO_TEXTURE, planTextures, writeTextures, type PackedTextures } from "./texturePack.js";
11
+ import { NO_TEXTURE, planTextures, writeTextures } from "./texturePack.js";
13
12
  import { PathTracePass, type FrameUniforms } from "./pathTracePass.js";
13
+ import { ImageResolvePass } from "./imageResolve.js";
14
14
  import { PresentPass } from "./presentPass.js";
15
15
  import {
16
16
  CONVERGED_ERROR,
@@ -27,7 +27,6 @@ import { environmentPower, uploadScene, type SceneBuffers } from "./sceneUpload.
27
27
  import {
28
28
  DEFAULT_RENDER_ENVIRONMENT,
29
29
  DEFAULT_RENDER_SETTINGS,
30
- encodeColor,
31
30
  EventEmitter,
32
31
  mergeSettings,
33
32
  serializeCamera,
@@ -161,8 +160,18 @@ function rowRanges(rows: readonly number[]): [number, number][] {
161
160
  const CAPTURE_SAMPLES = 32;
162
161
 
163
162
  /** A scene on the device, with where its extra words went. */
163
+ /**
164
+ * What is kept of a packed scene once it is on the device: whether it follows the camera, and where each texture's
165
+ * texels start, for the captures that write into them. The packed arrays themselves, hundreds of megabytes for a large
166
+ * scene, are let go as soon as they are uploaded.
167
+ */
168
+ interface PackedSummary {
169
+ cameraDependent: boolean;
170
+ textureOffsets: ReadonlyMap<string, number>;
171
+ }
172
+
164
173
  interface PackedTarget {
165
- packed: { geometry: PackedGeometry; bvh: PackedBvh; textures: PackedTextures };
174
+ packed: PackedSummary;
166
175
  buffers: SceneBuffers;
167
176
  skyData: number;
168
177
  volumetricData: number;
@@ -178,6 +187,10 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
178
187
  private environment: RenderEnvironment;
179
188
  // Serializing walks the whole scene, so it is started by the setter and awaited by the frame that needs it.
180
189
  private scene: Promise<SerializedScene> = Promise.resolve(EMPTY_SCENE);
190
+ // The snapshot `setScene` was last handed, so handing the same one again keeps everything prepared for it.
191
+ private snapshot: SerializedScene | null = null;
192
+ // The last sky baked, which a scene with the same sky reuses instead of baking it again.
193
+ private skyBake: SceneAssets["skyBake"] = null;
181
194
  // A LightProbe's harmonics, which the snapshot does not carry: read from the live scene when there is one.
182
195
  private probe: number[] = new Array(27).fill(0);
183
196
  // A Three camera is kept by reference and read at every `render()`, as the CPU renderer does; a snapshot stays fixed.
@@ -197,7 +210,8 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
197
210
  private pathTrace: PathTracePass | null = null;
198
211
  private present: PresentPass | null = null;
199
212
  private buffers: SceneBuffers | null = null;
200
- private packed: { geometry: PackedGeometry; bvh: PackedBvh; textures: PackedTextures } | null = null;
213
+ private packed: PackedSummary | null = null;
214
+ private imageResolve: ImageResolvePass | null = null;
201
215
  private sceneDirty = true;
202
216
  // Compute bakes, the procedural sky and translated shaders: prepared once per scene.
203
217
  private assets: Promise<SceneAssets> | null = null;
@@ -332,13 +346,18 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
332
346
 
333
347
  /**
334
348
  * Takes a Three scene, serialized in the background, or the result of `serializeScene`. The live scene is not read
335
- * again until this is called once more.
349
+ * again until this is called once more. Handing over the same snapshot again keeps what was prepared for it, its
350
+ * packed triangles, hierarchy and textures, so a new frame of an unchanged scene starts tracing at once.
336
351
  */
337
352
  setScene(scene: RenderScene | SerializedScene) {
338
- this.probe = "isScene" in scene && scene.isScene ? lightProbes(scene as Scene) : new Array(27).fill(0);
339
- this.scene = "isScene" in scene && scene.isScene
340
- ? serializeLive(scene as Scene)
341
- : Promise.resolve("meshes" in scene ? scene : (scene as { scene: SerializedScene }).scene);
353
+ const live = "isScene" in scene && scene.isScene;
354
+ const snapshot = live ? null : "meshes" in scene ? scene as SerializedScene : (scene as { scene: SerializedScene }).scene;
355
+
356
+ if (snapshot && snapshot === this.snapshot) return;
357
+
358
+ this.snapshot = snapshot;
359
+ this.probe = live ? lightProbes(scene as Scene) : new Array(27).fill(0);
360
+ this.scene = snapshot ? Promise.resolve(snapshot) : serializeLive(scene as Scene);
342
361
  this.sceneDirty = true;
343
362
  this.assets = null;
344
363
  }
@@ -463,6 +482,8 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
463
482
  this.buffers?.destroy();
464
483
  this.accumulator?.destroy();
465
484
  this.denoisePass?.destroy();
485
+ this.imageResolve?.dispose();
486
+ this.imageResolve = null;
466
487
  this.gpu?.device.destroy();
467
488
  this.buffers = null;
468
489
  this.accumulator = null;
@@ -516,10 +537,12 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
516
537
  const width = this.renderWidth, height = this.renderHeight;
517
538
 
518
539
  this.check(revision);
519
- this.assets ??= prepareSceneAssets(scene);
540
+ this.assets ??= prepareSceneAssets(scene, { device: gpu.device, sky: this.skyBake });
520
541
 
521
542
  const assets = await this.assets;
522
543
 
544
+ this.skyBake = assets.skyBake;
545
+
523
546
  this.check(revision);
524
547
  await this.prepare(gpu, revision, assets, camera, width, height, environment);
525
548
  this.output = { width, height, data: new Uint8ClampedArray(width * height * 4) };
@@ -687,17 +710,22 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
687
710
  const environment = assets.sky?.environment ?? image;
688
711
  const matrices = cameraMatrices(camera, width, height);
689
712
  const view = `${JSON.stringify(camera)}|${width}|${height}`;
690
- const moved = (!!this.packed?.geometry.cameraDependent || this.planar) && view !== this.packedView;
713
+ const moved = (!!this.packed?.cameraDependent || this.planar) && view !== this.packedView;
691
714
 
692
715
  if (this.sceneDirty || !this.buffers || moved) {
693
716
  // Planar surfaces need their captures, from this camera, before the scene that samples them is packed.
694
717
  const planar = planarCaptures(assets.scene, camera, width, height);
695
718
 
719
+ // The scene on the device is replaced: letting it go first keeps the two from being held at once.
720
+ this.buffers?.destroy();
721
+ this.buffers = null;
722
+ this.target = null;
723
+ this.packed = null;
724
+
696
725
  const packed = await this.packScene(
697
726
  gpu, revision, planar?.scene ?? assets.scene, assets, matrices, width, height, environment);
698
727
 
699
728
  this.packed = packed.packed;
700
- this.buffers?.destroy();
701
729
  this.buffers = packed.buffers;
702
730
  this.target = packed;
703
731
  this.sceneDirty = false;
@@ -791,8 +819,9 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
791
819
  // Nothing is on the device yet, so a frame replaced while the hierarchy was built has nothing to give back.
792
820
  this.check(revision);
793
821
 
794
- const packed = { geometry, bvh, textures };
795
- const buffers = uploadScene(gpu.device, geometry, packed.bvh, textures, lights, environment, cdf);
822
+ const buffers = uploadScene(gpu.device, geometry, bvh, textures, lights, environment, cdf);
823
+ const textureOffsets = new Map([...textures.indexOf].map(([id, index]) => [id, textures.data[index * 16 + 2]]));
824
+ const packed: PackedSummary = { cameraDependent: geometry.cameraDependent, textureOffsets };
796
825
 
797
826
  if (buffers.bytes.largest > gpu.maxStorageBufferBindingSize) {
798
827
  buffers.destroy();
@@ -858,9 +887,7 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
858
887
  }
859
888
 
860
889
  const offset = (id: string | undefined) => {
861
- const index = id === undefined ? undefined : main.packed.textures.indexOf.get(id);
862
-
863
- return index === undefined ? NO_TEXTURE : main.packed.textures.data[index * 16 + 2];
890
+ return (id === undefined ? undefined : main.packed.textureOffsets.get(id)) ?? NO_TEXTURE;
864
891
  };
865
892
 
866
893
  const encoder = gpu.device.createCommandEncoder();
@@ -1010,55 +1037,14 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
1010
1037
  private async readImage(
1011
1038
  gpu: RenderDevice, width: number, height: number, denoised: GPUBuffer | null): Promise<RenderImage> {
1012
1039
  const accumulator = this.accumulator as Accumulator;
1013
- // The radiance and the coverage; the guides and the row flags after them are not part of the image.
1014
- const length = width * height * 32;
1015
-
1016
- const read = async(buffer: GPUBuffer, bytes: number) => {
1017
- const usage = GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST;
1018
- const staging = gpu.device.createBuffer({ size: bytes, usage });
1019
- const encoder = gpu.device.createCommandEncoder();
1020
-
1021
- encoder.copyBufferToBuffer(buffer, 0, staging, 0, bytes);
1022
- gpu.device.queue.submit([encoder.finish()]);
1023
- await staging.mapAsync(GPUMapMode.READ);
1024
-
1025
- const floats = new Float32Array(staging.getMappedRange().slice(0));
1026
-
1027
- staging.unmap();
1028
- staging.destroy();
1029
-
1030
- return floats;
1031
- };
1032
-
1033
- const floats = await read(accumulator.buffer, length);
1034
- // The denoiser's output is the mean radiance itself, one vec4 per pixel.
1035
- const filtered = denoised ? await read(denoised, width * height * 16) : null;
1036
-
1037
- const data = new Uint8ClampedArray(width * height * 4);
1038
1040
  const { exposure, toneMapping, vignette } = this.current;
1039
1041
  const transparent = this.current.background.mode === "transparent";
1040
- const coverage = width * height * 4;
1041
-
1042
- for (let pixel = 0; pixel < width * height; pixel++) {
1043
- const samples = floats[pixel * 4 + 3];
1044
- const x = (pixel % width) / width - 0.5, y = Math.floor(pixel / width) / height - 0.5;
1045
- const falloff = Math.max(0, 1 - vignette * (x * x + y * y) * 2);
1046
-
1047
- // The radiance is premultiplied by the coverage, which a transparent background makes the alpha.
1048
- const alpha = !transparent ? 1
1049
- : samples > 0 ? Math.min(1, Math.max(0, floats[coverage + pixel * 4] / samples)) : 0;
1050
1042
 
1051
- const mean = (channel: number) =>
1052
- (filtered ? filtered[pixel * 4 + channel] : floats[pixel * 4 + channel] / samples);
1043
+ // Resolved on the device, so the page only copies the bytes of the image.
1044
+ this.imageResolve ??= new ImageResolvePass(gpu.device);
1053
1045
 
1054
- const color = samples > 0 && alpha > 0
1055
- ? [0, 1, 2].map(channel => mean(channel) / alpha * falloff)
1056
- : [0, 0, 0];
1057
-
1058
- const [r, g, b] = encodeColor(color, exposure, toneMapping);
1059
-
1060
- data.set([r, g, b, Math.round(alpha * 255)], pixel * 4);
1061
- }
1046
+ const data = await this.imageResolve.read(accumulator.buffer, denoised, width, height,
1047
+ { exposure, toneMapping, vignette, transparent });
1062
1048
 
1063
1049
  return { width, height, data };
1064
1050
  }