@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.
@@ -1,5 +1,6 @@
1
1
  import { environmentFromImage, type PackedEnvironment } from "./environment.js";
2
2
  import { compileShaderPrograms, type ShaderPrograms } from "./shaderProgram.js";
3
+ import { bakeSkyOnDevice, compileSkyProgram } from "./skyBake.js";
3
4
  import { CpuComputePipeline, CpuEnvironment } from "./upstream.js";
4
5
  import type { SerializedMaterial, SerializedScene, SerializedTexture, UniformValue } from "./upstream.js";
5
6
 
@@ -13,6 +14,8 @@ export interface SceneAssets {
13
14
  scene: SerializedScene;
14
15
  programs: ShaderPrograms;
15
16
  sky: ProceduralSkyEnvironment | null;
17
+ /** The baked sky, kept by the renderer so a scene with the same sky does not bake it again. */
18
+ skyBake: { key: string; baked: Promise<ProceduralSkyEnvironment | null> } | null;
16
19
  }
17
20
 
18
21
  export interface ProceduralSkyEnvironment {
@@ -83,12 +86,10 @@ function bakeComputations(source: SerializedScene): SerializedScene {
83
86
  };
84
87
  }
85
88
 
86
- /** Bakes a procedural sky with the CPU renderer's own interpreter, at the size its environment uses. */
87
- function bakeSky(scene: SerializedScene): ProceduralSkyEnvironment | null {
88
- const sky = scene.sky;
89
-
90
- if (!sky) return null;
89
+ type ProceduralSky = NonNullable<SerializedScene["sky"]>;
91
90
 
91
+ /** Bakes a procedural sky with the CPU renderer's own interpreter: the fallback, when the device cannot run it. */
92
+ function bakeSkyOnCpu(sky: ProceduralSky): Float32Array | null {
92
93
  const generator = CpuEnvironment.fromProcedural(sky, SKY_WIDTH);
93
94
  let step = generator.next();
94
95
 
@@ -97,22 +98,66 @@ function bakeSky(scene: SerializedScene): ProceduralSkyEnvironment | null {
97
98
  // The snapshot is the environment's public face; its texture is the baked panorama.
98
99
  const texture = step.value.snapshot().texture;
99
100
 
100
- if (!texture || !(texture.data instanceof Float32Array)) return null;
101
+ return texture && texture.data instanceof Float32Array ? texture.data : null;
102
+ }
103
+
104
+ /**
105
+ * Bakes a procedural sky at the size its environment uses. On a device the sky's shader is translated and run there,
106
+ * over every texel at once; the CPU renderer's interpreter, which runs it texel by texel, is what is left when there is
107
+ * no device or the shader does not translate.
108
+ */
109
+ async function bakeSky(sky: ProceduralSky, device?: GPUDevice): Promise<ProceduralSkyEnvironment | null> {
110
+ let radiance: Float32Array | null = null;
111
+
112
+ if (device) {
113
+ try {
114
+ radiance = await bakeSkyOnDevice(device, await compileSkyProgram(sky.fragmentShader), sky.uniforms, SKY_WIDTH);
115
+ } catch(error) {
116
+ console.warn("GPU renderer: the procedural sky is baked on the CPU, its shader did not run on the device.", error);
117
+ }
118
+ }
119
+
120
+ radiance ??= bakeSkyOnCpu(sky);
101
121
 
102
- const environment = environmentFromImage(texture.data, texture.width, texture.height, "procedural-sky");
122
+ if (!radiance) return null;
123
+
124
+ const environment = environmentFromImage(radiance, SKY_WIDTH, SKY_WIDTH / 2, "procedural-sky");
103
125
 
104
126
  if (!environment) return null;
105
127
 
106
128
  return { environment, intensity: sky.intensity, background: sky.background, uniforms: sky.uniforms };
107
129
  }
108
130
 
109
- export async function prepareSceneAssets(source: SerializedScene): Promise<SceneAssets> {
131
+ /** What decides a baked sky: the same shader with the same uniforms bakes the same panorama. */
132
+ export function skyKey(sky: ProceduralSky | undefined): string {
133
+ return sky ? JSON.stringify([sky.fragmentShader, sky.uniforms]) : "";
134
+ }
135
+
136
+ export interface SceneAssetOptions {
137
+ /** Where the sky is baked; without one, on the CPU. */
138
+ device?: GPUDevice;
139
+ /** A sky baked for an earlier scene, reused when its key matches this scene's. */
140
+ sky?: { key: string; baked: Promise<ProceduralSkyEnvironment | null> } | null;
141
+ }
142
+
143
+ export async function prepareSceneAssets(
144
+ source: SerializedScene, options: SceneAssetOptions = {}): Promise<SceneAssets> {
110
145
  const scene = source.meshes.some(mesh => mesh.materials.some(material => material?.compute))
111
146
  ? bakeComputations(source)
112
147
  : source;
113
148
 
114
- const sky = bakeSky(scene);
115
- const programs = await compileShaderPrograms(scene, scene.sky?.foregroundShader);
149
+ const key = skyKey(scene.sky);
150
+ const reused = options.sky && options.sky.key === key ? options.sky.baked : null;
151
+ const baked = reused ?? (scene.sky ? bakeSky(scene.sky, options.device) : Promise.resolve(null));
152
+ const [sky, programs] = await Promise.all([baked, compileShaderPrograms(scene, scene.sky?.foregroundShader)]);
153
+ // The sky the scene uses, with its baked panorama, for the next scene to reuse.
154
+ const skyBake = scene.sky ? { key, baked } : null;
155
+
156
+ // The panorama comes from the shader and its uniforms; how strongly it lights and whether it paints the background
157
+ // are this scene's own.
158
+ const current = sky && scene.sky
159
+ ? { ...sky, intensity: scene.sky.intensity, background: scene.sky.background, uniforms: scene.sky.uniforms }
160
+ : null;
116
161
 
117
- return { scene, programs, sky };
162
+ return { scene, programs, sky: current, skyBake };
118
163
  }
package/src/skyBake.ts ADDED
@@ -0,0 +1,113 @@
1
+ import { compileProgram, type ShaderProgram } from "./shaderProgram.js";
2
+ import { createShaderModule } from "./shaderModule.js";
3
+ import type { UniformValue } from "./upstream.js";
4
+ import { BVH_WGSL } from "./wgsl/bvh.wgsl.js";
5
+ import { COMMON_WGSL } from "./wgsl/common.wgsl.js";
6
+ import { RANDOM_WGSL } from "./wgsl/random.wgsl.js";
7
+ import { SAMPLING_WGSL } from "./wgsl/sampling.wgsl.js";
8
+ import { SHADER_SUPPORT_WGSL } from "./wgsl/shaderSupport.wgsl.js";
9
+ import { TEXTURE_WGSL } from "./wgsl/texture.wgsl.js";
10
+
11
+ const WORKGROUP = 8;
12
+
13
+ /**
14
+ * The bake's own kernel: the kernel's support code up to the translated shaders, the sky's program, and an entry that
15
+ * runs it once per texel of the panorama, along the direction the CPU renderer's bake gives that texel. The output
16
+ * goes to the binding the path tracer accumulates into, which is the one read-write buffer the support code declares.
17
+ */
18
+ export function skyBakeSource(program: ShaderProgram, width: number, height: number): string {
19
+ return `${COMMON_WGSL}${RANDOM_WGSL}${BVH_WGSL}${SAMPLING_WGSL}${TEXTURE_WGSL}${SHADER_SUPPORT_WGSL}
20
+ ${program.wgsl}
21
+
22
+ @compute @workgroup_size(${WORKGROUP}, ${WORKGROUP})
23
+ fn main(@builtin(global_invocation_id) id: vec3<u32>) {
24
+ if (id.x >= ${width}u || id.y >= ${height}u) {
25
+ return;
26
+ }
27
+
28
+ let u = (f32(id.x) + 0.5) / ${width}.0;
29
+ let v = (f32(id.y) + 0.5) / ${height}.0;
30
+ let phi = (u - 0.5) * 2.0 * PI;
31
+ let theta = v * PI;
32
+ var input: ShaderInput;
33
+ input.data = 0u;
34
+ input.direction = vec3<f32>(sin(theta) * cos(phi), cos(theta), sin(theta) * sin(phi));
35
+ input.fragCoord = vec4<f32>(f32(id.x) + 0.5, f32(id.y) + 0.5, 0.0, 1.0);
36
+ input.diffuseColor = vec4<f32>(0.0, 0.0, 0.0, 1.0);
37
+
38
+ accumulation[id.y * ${width}u + id.x] = p${program.id}_run(input).color;
39
+ }
40
+ `;
41
+ }
42
+
43
+ /** Translates a procedural sky's shader for the bake; the sky reads its direction from `vDirection`, as the CPU's does. */
44
+ export function compileSkyProgram(fragmentShader: string): Promise<ShaderProgram> {
45
+ return compileProgram(0, "foreground", { fragment: fragmentShader, defines: {} }, true);
46
+ }
47
+
48
+ /**
49
+ * Runs a procedural sky's shader on the device over an equirectangular panorama and reads the radiance back, as RGBA
50
+ * floats, row by row from the top: the same image the CPU renderer's interpreter bakes, in a fraction of the time. A
51
+ * texel that comes out not finite reads black, where the CPU bake would have refused the whole sky.
52
+ */
53
+ export async function bakeSkyOnDevice(
54
+ device: GPUDevice, program: ShaderProgram, uniforms: Record<string, UniformValue>, width: number,
55
+ ): Promise<Float32Array> {
56
+ const height = width / 2;
57
+ const texels = width * height;
58
+ const words = program.packUniforms(uniforms, new Map());
59
+ const module = createShaderModule(device, "sky bake", skyBakeSource(program, width, height));
60
+
61
+ const layout = device.createBindGroupLayout({
62
+ label: "sky bake",
63
+ entries: [
64
+ { binding: 7, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } },
65
+ { binding: 8, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
66
+ ],
67
+ });
68
+
69
+ const pipeline = await device.createComputePipelineAsync({
70
+ label: "sky bake",
71
+ layout: device.createPipelineLayout({ bindGroupLayouts: [layout] }),
72
+ compute: { module, entryPoint: "main" },
73
+ });
74
+
75
+ const output = device.createBuffer({ size: texels * 16, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC });
76
+
77
+ const data = device.createBuffer({
78
+ size: Math.max(16, words.byteLength), usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
79
+ });
80
+
81
+ const staging = device.createBuffer({ size: texels * 16, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST });
82
+
83
+ try {
84
+ device.queue.writeBuffer(data, 0, words);
85
+
86
+ const encoder = device.createCommandEncoder({ label: "sky bake" });
87
+ const pass = encoder.beginComputePass({ label: "sky bake" });
88
+
89
+ pass.setPipeline(pipeline);
90
+ pass.setBindGroup(0, device.createBindGroup({
91
+ layout,
92
+ entries: [{ binding: 7, resource: { buffer: output } }, { binding: 8, resource: { buffer: data } }],
93
+ }));
94
+ pass.dispatchWorkgroups(Math.ceil(width / WORKGROUP), Math.ceil(height / WORKGROUP));
95
+ pass.end();
96
+ encoder.copyBufferToBuffer(output, 0, staging, 0, texels * 16);
97
+ device.queue.submit([encoder.finish()]);
98
+
99
+ await staging.mapAsync(GPUMapMode.READ);
100
+
101
+ const radiance = new Float32Array(staging.getMappedRange().slice(0));
102
+
103
+ staging.unmap();
104
+
105
+ for (let i = 0; i < radiance.length; i++) if (!Number.isFinite(radiance[i])) radiance[i] = 0;
106
+
107
+ return radiance;
108
+ } finally {
109
+ output.destroy();
110
+ data.destroy();
111
+ staging.destroy();
112
+ }
113
+ }