@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.
- package/ARCHITECTURE.md +5 -2
- package/README.md +1 -1
- package/dist/imageResolve.d.ts +26 -0
- package/dist/imageResolve.d.ts.map +1 -0
- package/dist/imageResolve.js +114 -0
- package/dist/imageResolve.js.map +1 -0
- package/dist/renderer.d.ts +5 -1
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +33 -47
- package/dist/renderer.js.map +1 -1
- package/dist/sceneAssets.d.ts +19 -1
- package/dist/sceneAssets.d.ts.map +1 -1
- package/dist/sceneAssets.js +40 -11
- package/dist/sceneAssets.js.map +1 -1
- package/dist/skyBake.d.ts +17 -0
- package/dist/skyBake.d.ts.map +1 -0
- package/dist/skyBake.js +97 -0
- package/dist/skyBake.js.map +1 -0
- package/package.json +1 -1
- package/src/imageResolve.ts +136 -0
- package/src/renderer.ts +50 -64
- package/src/sceneAssets.ts +56 -11
- package/src/skyBake.ts +113 -0
package/src/sceneAssets.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
115
|
-
const
|
|
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
|
+
}
|