@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/dist/sceneAssets.d.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|
|
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"}
|
package/dist/sceneAssets.js
CHANGED
|
@@ -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,
|
|
52
|
-
function
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
74
|
-
const
|
|
75
|
-
|
|
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
|
package/dist/sceneAssets.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sceneAssets.js","sourceRoot":"","sources":["../src/sceneAssets.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAA0B,MAAM,kBAAkB,CAAC;AAChF,OAAO,EAAE,qBAAqB,EAAuB,MAAM,oBAAoB,CAAC;AAChF,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;
|
|
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"}
|
package/dist/skyBake.js
ADDED
|
@@ -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
|
@@ -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
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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?.
|
|
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
|
|
795
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
1052
|
-
|
|
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
|
-
|
|
1055
|
-
|
|
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
|
}
|