@tinypoly/gpu-renderer 0.0.5 → 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 +59 -3
- package/README.md +52 -4
- 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/denoise.d.ts +11 -1
- package/dist/denoise.d.ts.map +1 -1
- package/dist/denoise.js +34 -6
- package/dist/denoise.js.map +1 -1
- package/dist/frameLayout.d.ts.map +1 -1
- package/dist/frameLayout.js +15 -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 +10 -0
- package/dist/lights.d.ts.map +1 -1
- package/dist/lights.js +54 -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 +118 -14
- package/dist/renderer.js.map +1 -1
- package/dist/sceneUpload.d.ts +5 -0
- package/dist/sceneUpload.d.ts.map +1 -1
- package/dist/sceneUpload.js +4 -0
- 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/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/common.wgsl.d.ts.map +1 -1
- package/dist/wgsl/common.wgsl.js +4 -0
- 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 +203 -31
- 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 +396 -25
- package/dist/wgsl/pathTrace.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 +21 -1
- package/dist/wgsl/surface.wgsl.js.map +1 -1
- package/package.json +1 -1
- package/src/atmosphere.ts +95 -0
- package/src/denoise.ts +44 -6
- package/src/frameLayout.ts +15 -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 +71 -1
- package/src/materialLayout.ts +52 -1
- package/src/pathTracePass.ts +19 -10
- package/src/renderer.ts +166 -22
- package/src/sceneUpload.ts +9 -0
- package/src/settings.ts +234 -0
- package/src/subsurface.ts +153 -0
- package/src/volumeContainment.ts +81 -0
- package/src/wgsl/bsdf.wgsl.ts +35 -3
- package/src/wgsl/common.wgsl.ts +4 -0
- package/src/wgsl/lens.wgsl.ts +314 -0
- package/src/wgsl/lights.wgsl.ts +203 -31
- package/src/wgsl/pathTrace.wgsl.ts +396 -25
- package/src/wgsl/sampling.wgsl.ts +25 -0
- package/src/wgsl/surface.wgsl.ts +21 -1
package/src/materialLayout.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { layoutStruct, writeStruct, type StructField } from "./structLayout.js";
|
|
2
|
+
import { bevelOf, subsurfaceOf, volumeOf } from "./subsurface.js";
|
|
2
3
|
import { NO_TEXTURE } from "./texturePack.js";
|
|
3
4
|
import type { SerializedMaterial, UniformValue } from "./upstream.js";
|
|
4
5
|
|
|
@@ -59,6 +60,20 @@ export interface MaterialProperties {
|
|
|
59
60
|
combine: number;
|
|
60
61
|
side: number;
|
|
61
62
|
blending: number;
|
|
63
|
+
/**
|
|
64
|
+
* Share of the diffuse layer that scatters light inside the material, from `userData.cpuRenderer.subsurface`; zero
|
|
65
|
+
* for none. Physical and standard materials only.
|
|
66
|
+
*/
|
|
67
|
+
subsurface: number;
|
|
68
|
+
/** Mean free path of red, green and blue inside the material, in world units. */
|
|
69
|
+
subsurfaceRadius: number[];
|
|
70
|
+
subsurfaceAnisotropy: number;
|
|
71
|
+
/** The medium a volume's boundary encloses: its single scattering albedo, density per scene unit and anisotropy. */
|
|
72
|
+
volumeAlbedo: number[];
|
|
73
|
+
volumeDensity: number;
|
|
74
|
+
volumeAnisotropy: number;
|
|
75
|
+
/** The radius over which edges are rounded at shading time, in scene units; zero leaves them sharp. */
|
|
76
|
+
bevel: number;
|
|
62
77
|
/** An index into the texture table per slot, or `NO_TEXTURE`. */
|
|
63
78
|
maps: Record<MaterialMap, number>;
|
|
64
79
|
}
|
|
@@ -137,6 +152,18 @@ export const MATERIAL_ALPHA_HASH = 2048;
|
|
|
137
152
|
/** A physical material, whose specular colour and intensity reach the lobe and whose grazing reflectance follows them. */
|
|
138
153
|
export const MATERIAL_SPECULAR = 4096;
|
|
139
154
|
|
|
155
|
+
/**
|
|
156
|
+
* The mesh only bounds a medium, from `userData.cpuRenderer.volume`: its surface is invisible and the space inside
|
|
157
|
+
* scatters light.
|
|
158
|
+
*/
|
|
159
|
+
export const MATERIAL_VOLUME = 8192;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* An unlit material whose colour is known without running anything: opaque, normal blending, no light map, environment
|
|
163
|
+
* map, alpha map or shader. What it shows is what it emits, so direct sampling can aim at it like an emissive one.
|
|
164
|
+
*/
|
|
165
|
+
export const MATERIAL_UNLIT_LIGHT = 16384;
|
|
166
|
+
|
|
140
167
|
/** What a material's `program` reads as when it has no shader. */
|
|
141
168
|
export const NO_PROGRAM = 0xffffffff;
|
|
142
169
|
|
|
@@ -186,6 +213,11 @@ const FIELDS: StructField[] = [
|
|
|
186
213
|
{ name: "dielectric", type: "f32" },
|
|
187
214
|
{ name: "afterProgram", type: "u32", doc: "What a hook does after <opaque_fragment>, or NO_PROGRAM." },
|
|
188
215
|
{ name: "afterData", type: "u32" },
|
|
216
|
+
{ name: "subsurface", type: "vec4f", doc: "rgb is the mean free path of each colour inside, w the share of the diffuse layer." },
|
|
217
|
+
{ name: "subsurfaceAnisotropy", type: "f32" },
|
|
218
|
+
{ name: "volume", type: "vec4f", doc: "rgb is the medium's single scattering albedo, w its density per scene unit." },
|
|
219
|
+
{ name: "volumeAnisotropy", type: "f32" },
|
|
220
|
+
{ name: "bevel", type: "f32", doc: "The radius edges are rounded over at shading time; zero leaves them sharp." },
|
|
189
221
|
{ name: "normalRow0", type: "vec4f", doc: "The mesh's world normal matrix, for an object space normal map." },
|
|
190
222
|
{ name: "normalRow1", type: "vec4f" },
|
|
191
223
|
{ name: "normalRow2", type: "vec4f" },
|
|
@@ -263,6 +295,8 @@ export function materialProperties(
|
|
|
263
295
|
const sheen = physical ? Math.max(0, number(values.sheen, 0)) : 0;
|
|
264
296
|
const anisotropy = physical ? number(values.anisotropy, 0) : 0;
|
|
265
297
|
const thickness = vector(values.iridescenceThicknessRange, [100, 400], 2);
|
|
298
|
+
const medium = pbr ? subsurfaceOf(material) : null;
|
|
299
|
+
const volume = volumeOf(material);
|
|
266
300
|
|
|
267
301
|
const maps = Object.fromEntries(MATERIAL_MAPS.map(slot => {
|
|
268
302
|
const id = material?.maps?.[slot];
|
|
@@ -285,7 +319,12 @@ export function materialProperties(
|
|
|
285
319
|
| (number(values.normalMapType, 0) === 1 ? MATERIAL_OBJECT_NORMAL_MAP : 0)
|
|
286
320
|
| (shader?.patched ? MATERIAL_PATCHED : 0)
|
|
287
321
|
| (flag(values.alphaHash, false) ? MATERIAL_ALPHA_HASH : 0)
|
|
288
|
-
| (physical ? MATERIAL_SPECULAR : 0)
|
|
322
|
+
| (physical ? MATERIAL_SPECULAR : 0)
|
|
323
|
+
| (volume ? MATERIAL_VOLUME : 0)
|
|
324
|
+
| (type === "MeshBasicMaterial" && !shader && !transparent && !flag(values.alphaHash, false)
|
|
325
|
+
&& !flag(values.wireframe, false) && number(values.blending, 1) === 1 && number(values.opacity, 1) >= 1
|
|
326
|
+
&& number(values.alphaTest, 0) <= 0 && maps.alphaMap === NO_TEXTURE && maps.envMap === NO_TEXTURE
|
|
327
|
+
&& maps.lightMap === NO_TEXTURE && !volume ? MATERIAL_UNLIT_LIGHT : 0);
|
|
289
328
|
|
|
290
329
|
return {
|
|
291
330
|
kind,
|
|
@@ -330,6 +369,13 @@ export function materialProperties(
|
|
|
330
369
|
combine: number(values.combine, 0),
|
|
331
370
|
side: number(values.side, 0),
|
|
332
371
|
blending: number(values.blending, 1),
|
|
372
|
+
subsurface: medium?.weight ?? 0,
|
|
373
|
+
subsurfaceRadius: medium?.radius ?? [1, 1, 1],
|
|
374
|
+
subsurfaceAnisotropy: medium?.anisotropy ?? 0,
|
|
375
|
+
volumeAlbedo: volume?.albedo ?? [1, 1, 1],
|
|
376
|
+
volumeDensity: volume?.density ?? 0,
|
|
377
|
+
volumeAnisotropy: volume?.anisotropy ?? 0,
|
|
378
|
+
bevel: LIT.has(type) ? bevelOf(material) : 0,
|
|
333
379
|
maps,
|
|
334
380
|
};
|
|
335
381
|
}
|
|
@@ -400,6 +446,11 @@ export function writeMaterial(
|
|
|
400
446
|
dielectric: p.dielectric,
|
|
401
447
|
afterProgram: placement.afterProgram,
|
|
402
448
|
afterData: placement.afterData,
|
|
449
|
+
subsurface: [...p.subsurfaceRadius, p.subsurface],
|
|
450
|
+
subsurfaceAnisotropy: p.subsurfaceAnisotropy,
|
|
451
|
+
volume: [...p.volumeAlbedo, p.volumeDensity],
|
|
452
|
+
volumeAnisotropy: p.volumeAnisotropy,
|
|
453
|
+
bevel: p.bevel,
|
|
403
454
|
normalRow0: [n[0], n[3], n[6], 0],
|
|
404
455
|
normalRow1: [n[1], n[4], n[7], 0],
|
|
405
456
|
normalRow2: [n[2], n[5], n[8], 0],
|
package/src/pathTracePass.ts
CHANGED
|
@@ -23,8 +23,12 @@ export type FrameUniforms = Record<keyof typeof FRAME_LAYOUT.offsets & string, n
|
|
|
23
23
|
* declarations. The scene's translated shaders sit between the texture code they call and the surface code that calls
|
|
24
24
|
* them.
|
|
25
25
|
*/
|
|
26
|
-
export function pathTraceSource(
|
|
27
|
-
|
|
26
|
+
export function pathTraceSource(
|
|
27
|
+
programs: ShaderPrograms | null = null, subsurface = true, volumes = true, bevel = true): string {
|
|
28
|
+
// The random walk inside materials and the media inside volumes are compiled in only for a scene that has them: the
|
|
29
|
+
// kernel is bound by its size, and either would cost every other scene a few percent for code that never runs.
|
|
30
|
+
return `const SUBSURFACE_WALK: bool = ${subsurface};\nconst VOLUMES: bool = ${volumes};\nconst BEVEL: bool = ${bevel};\n`
|
|
31
|
+
+ COMMON_WGSL + RANDOM_WGSL + BVH_WGSL + SAMPLING_WGSL + TEXTURE_WGSL + SHADER_SUPPORT_WGSL
|
|
28
32
|
+ programsWgsl(programs) + ENVIRONMENT_WGSL + SURFACE_WGSL + BSDF_WGSL + LIGHTS_WGSL + PATH_TRACE_WGSL;
|
|
29
33
|
}
|
|
30
34
|
|
|
@@ -37,26 +41,31 @@ export class PathTracePass {
|
|
|
37
41
|
private readonly uniforms: GPUBuffer;
|
|
38
42
|
private readonly values = new ArrayBuffer(FRAME_BYTES);
|
|
39
43
|
private bindGroup: GPUBindGroup | null = null;
|
|
40
|
-
private programKey = "";
|
|
44
|
+
private programKey = "|false|false|false";
|
|
41
45
|
|
|
42
46
|
constructor(private readonly device: GPUDevice) {
|
|
43
|
-
this.pipeline = this.build(null);
|
|
47
|
+
this.pipeline = this.build(null, false, false, false);
|
|
44
48
|
this.uniforms = device.createBuffer({ size: FRAME_BYTES, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
45
49
|
}
|
|
46
50
|
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Recompiles the kernel when the scene's shaders changed, or whether it has a material that scatters light inside;
|
|
53
|
+
* the same set keeps the pipeline it has.
|
|
54
|
+
*/
|
|
55
|
+
setPrograms(programs: ShaderPrograms | null, subsurface = false, volumes = false, bevel = false) {
|
|
56
|
+
const key = `${programs?.key ?? ""}|${subsurface}|${volumes}|${bevel}`;
|
|
50
57
|
|
|
51
58
|
if (key === this.programKey) return;
|
|
52
59
|
|
|
53
60
|
this.programKey = key;
|
|
54
|
-
this.pipeline = this.build(programs);
|
|
61
|
+
this.pipeline = this.build(programs, subsurface, volumes, bevel);
|
|
55
62
|
this.bindGroup = null;
|
|
56
63
|
}
|
|
57
64
|
|
|
58
|
-
private build(programs: ShaderPrograms | null
|
|
59
|
-
|
|
65
|
+
private build(programs: ShaderPrograms | null, subsurface: boolean, volumes: boolean,
|
|
66
|
+
bevel: boolean): GPUComputePipeline {
|
|
67
|
+
const module = createShaderModule(this.device, "path trace",
|
|
68
|
+
pathTraceSource(programs, subsurface, volumes, bevel));
|
|
60
69
|
|
|
61
70
|
return this.device.createComputePipeline({ layout: "auto", compute: { module, entryPoint: "main" } });
|
|
62
71
|
}
|
package/src/renderer.ts
CHANGED
|
@@ -12,6 +12,26 @@ import { NO_TEXTURE, planTextures, textureEntry, writeTextures } from "./texture
|
|
|
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`. */
|
|
@@ -136,6 +156,9 @@ const SHOW_EVERY_MS = 16;
|
|
|
136
156
|
*/
|
|
137
157
|
const BANDS_IN_FLIGHT = 2;
|
|
138
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
|
+
|
|
139
162
|
/** How often the canvas takes a freshly denoised image while a frame traces. */
|
|
140
163
|
const DENOISE_EVERY_MS = 200;
|
|
141
164
|
|
|
@@ -201,6 +224,8 @@ interface PackedTarget {
|
|
|
201
224
|
skyData: number;
|
|
202
225
|
volumetricData: number;
|
|
203
226
|
volumetricCount: number;
|
|
227
|
+
/** The boundaries of the scene's volumes, which say whether the camera stands inside one. */
|
|
228
|
+
volumes: VolumeTriangles | null;
|
|
204
229
|
}
|
|
205
230
|
|
|
206
231
|
/**
|
|
@@ -208,7 +233,7 @@ interface PackedTarget {
|
|
|
208
233
|
* `render()` opens a frame with whatever is current and accumulates samples until it reaches `settings.samples`.
|
|
209
234
|
*/
|
|
210
235
|
export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
211
|
-
private current:
|
|
236
|
+
private current: GpuRenderSettings;
|
|
212
237
|
private environment: RenderEnvironment;
|
|
213
238
|
// Serializing walks the whole scene, so it is started by the setter and awaited by the frame that needs it.
|
|
214
239
|
private scene: Promise<SerializedScene> = Promise.resolve(EMPTY_SCENE);
|
|
@@ -248,6 +273,7 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
248
273
|
private planar = false;
|
|
249
274
|
private resolve: CaptureResolvePass | null = null;
|
|
250
275
|
private denoisePass: DenoisePass | null = null;
|
|
276
|
+
private lensPass: LensPass | null = null;
|
|
251
277
|
private denoiserSettings: DenoiserSettings = { ...DEFAULT_DENOISER };
|
|
252
278
|
// The size of the last frame that finished, which setDenoiser filters again; null while a frame traces.
|
|
253
279
|
private finished: { width: number; height: number } | null = null;
|
|
@@ -257,6 +283,8 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
257
283
|
private denoiseView: DenoiseView = { pixelScale: 1, orthographic: false };
|
|
258
284
|
// Reading an environment image is a fetch, so it is started once and awaited by the frame that needs it.
|
|
259
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;
|
|
260
288
|
private environmentImage: Promise<PackedEnvironment | null> = Promise.resolve(null);
|
|
261
289
|
private canvas: HTMLCanvasElement | null = null;
|
|
262
290
|
private context: GPUCanvasContext | null = null;
|
|
@@ -274,7 +302,7 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
274
302
|
this.width = Math.max(1, Math.floor(options.width));
|
|
275
303
|
this.height = Math.max(1, Math.floor(options.height));
|
|
276
304
|
this.pixelRatio = checkedPixelRatio(options.pixelRatio ?? 1);
|
|
277
|
-
this.current =
|
|
305
|
+
this.current = mergeGpuSettings(DEFAULT_GPU_RENDER_SETTINGS, options.settings);
|
|
278
306
|
this.denoiserSettings = mergeDenoiser(DEFAULT_DENOISER, options.denoiser);
|
|
279
307
|
this.environment = DEFAULT_RENDER_ENVIRONMENT;
|
|
280
308
|
this.scheduler = new DispatchScheduler({ maxRows: this.renderHeight, maxSamples: BATCH_PATHS });
|
|
@@ -285,7 +313,7 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
285
313
|
}
|
|
286
314
|
|
|
287
315
|
/** The settings the next frame renders with. */
|
|
288
|
-
get settings(): Readonly<
|
|
316
|
+
get settings(): Readonly<GpuRenderSettings> {
|
|
289
317
|
return this.current;
|
|
290
318
|
}
|
|
291
319
|
|
|
@@ -396,15 +424,16 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
396
424
|
}
|
|
397
425
|
|
|
398
426
|
/**
|
|
399
|
-
* Merges the given fields into the current settings, group by group. Turning the denoiser on or off
|
|
400
|
-
*
|
|
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.
|
|
401
430
|
*/
|
|
402
|
-
setSettings(settings:
|
|
403
|
-
const
|
|
431
|
+
setSettings(settings: GpuRenderSettingsInput) {
|
|
432
|
+
const before = developing(this.current);
|
|
404
433
|
|
|
405
|
-
this.current =
|
|
434
|
+
this.current = mergeGpuSettings(this.current, settings);
|
|
406
435
|
|
|
407
|
-
if (this.current
|
|
436
|
+
if (developing(this.current) !== before) this.refilter();
|
|
408
437
|
}
|
|
409
438
|
|
|
410
439
|
setEnvironment(environment: RenderEnvironment) {
|
|
@@ -507,6 +536,8 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
507
536
|
this.buffers?.destroy();
|
|
508
537
|
this.accumulator?.destroy();
|
|
509
538
|
this.denoisePass?.destroy();
|
|
539
|
+
this.lensPass?.destroy();
|
|
540
|
+
this.lensPass = null;
|
|
510
541
|
this.imageResolve?.dispose();
|
|
511
542
|
this.imageResolve = null;
|
|
512
543
|
this.gpu?.device.destroy();
|
|
@@ -769,10 +800,20 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
769
800
|
height: number,
|
|
770
801
|
image: PackedEnvironment | null) {
|
|
771
802
|
this.pathTrace ??= new PathTracePass(gpu.device);
|
|
772
|
-
|
|
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));
|
|
773
812
|
|
|
774
813
|
// A procedural sky stands in for the renderer's own environment, as it does in the CPU renderer.
|
|
775
814
|
const environment = assets.sky?.environment ?? image;
|
|
815
|
+
|
|
816
|
+
this.environmentAverage = environment ? averageRadiance(environment) : null;
|
|
776
817
|
const matrices = cameraMatrices(camera, width, height);
|
|
777
818
|
const view = `${JSON.stringify(camera)}|${width}|${height}`;
|
|
778
819
|
const moved = (!!this.packed?.cameraDependent || this.planar) && view !== this.packedView;
|
|
@@ -902,6 +943,7 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
902
943
|
skyData,
|
|
903
944
|
volumetricData,
|
|
904
945
|
volumetricCount: Math.min(volumetric.length / VOLUMETRIC_WORDS, 8),
|
|
946
|
+
volumes: volumeTriangles(geometry),
|
|
905
947
|
};
|
|
906
948
|
}
|
|
907
949
|
|
|
@@ -940,6 +982,10 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
940
982
|
const uniforms = {
|
|
941
983
|
...this.frameUniforms(capture.camera, width, height, assets, target, capture.background),
|
|
942
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,
|
|
943
989
|
volumetricCount: 0,
|
|
944
990
|
clipPlane: capture.clip ?? [0, 0, 0, 0],
|
|
945
991
|
bandOrigin: [0, 0],
|
|
@@ -993,21 +1039,37 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
993
1039
|
}
|
|
994
1040
|
|
|
995
1041
|
private encodePresent(encoder: GPUCommandEncoder, width: number, height: number) {
|
|
996
|
-
if (!this.context || !this.present) return;
|
|
1042
|
+
if (!this.context || !this.present || !this.gpu) return;
|
|
997
1043
|
|
|
998
1044
|
this.present.encode(encoder, this.context.getCurrentTexture().createView(), {
|
|
999
1045
|
width,
|
|
1000
1046
|
height,
|
|
1001
|
-
exposure: this.current
|
|
1047
|
+
exposure: effectiveExposure(this.current),
|
|
1002
1048
|
toneMapping: this.current.toneMapping,
|
|
1003
1049
|
vignette: this.current.vignette,
|
|
1004
1050
|
grain: this.current.grain,
|
|
1005
1051
|
frameIndex: this.frameIndex,
|
|
1006
1052
|
transparent: this.current.background.mode === "transparent",
|
|
1007
|
-
denoised: this.
|
|
1053
|
+
denoised: this.encodeLens(this.gpu, encoder, width, height,
|
|
1054
|
+
this.denoised ? this.denoisePass?.output ?? null : null),
|
|
1008
1055
|
});
|
|
1009
1056
|
}
|
|
1010
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
|
+
|
|
1011
1073
|
/** Denoises the accumulation as it stands, for the canvas and for the finished image. */
|
|
1012
1074
|
private encodeDenoise(gpu: RenderDevice, encoder: GPUCommandEncoder, width: number, height: number) {
|
|
1013
1075
|
this.denoisePass ??= new DenoisePass(gpu.device);
|
|
@@ -1041,6 +1103,16 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
1041
1103
|
: 0;
|
|
1042
1104
|
|
|
1043
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);
|
|
1044
1116
|
|
|
1045
1117
|
return {
|
|
1046
1118
|
cameraToWorld: matrices.cameraWorld,
|
|
@@ -1069,11 +1141,28 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
1069
1141
|
resolution: [width, height],
|
|
1070
1142
|
cameraKind: matrices.orthographic ? 1 : 0,
|
|
1071
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],
|
|
1072
1157
|
triangleCount: buffers?.triangleCount ?? 0,
|
|
1073
1158
|
lightCount: buffers?.lightCount ?? 0,
|
|
1074
1159
|
areaLightCount: buffers?.areaLightCount ?? 0,
|
|
1075
1160
|
lightCdf: buffers?.lightCdf ?? 0,
|
|
1076
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,
|
|
1077
1166
|
clampIndirect: Math.max(0, this.current.globalIllumination.clamp),
|
|
1078
1167
|
// The setting is the angular diameter a light covers, in degrees, as it is for the CPU renderer's shadow blur.
|
|
1079
1168
|
shadowSpread: shadows.enabled ? Math.tan(Math.max(0, shadows.softness) * Math.PI / 360) : 0,
|
|
@@ -1108,13 +1197,20 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
|
|
|
1108
1197
|
private async readImage(
|
|
1109
1198
|
gpu: RenderDevice, width: number, height: number, denoised: GPUBuffer | null): Promise<RenderImage> {
|
|
1110
1199
|
const accumulator = this.accumulator as Accumulator;
|
|
1111
|
-
const {
|
|
1200
|
+
const { toneMapping, vignette } = this.current;
|
|
1201
|
+
const exposure = effectiveExposure(this.current);
|
|
1112
1202
|
const transparent = this.current.background.mode === "transparent";
|
|
1113
1203
|
|
|
1114
1204
|
// Resolved on the device, so the page only copies the bytes of the image.
|
|
1115
1205
|
this.imageResolve ??= new ImageResolvePass(gpu.device);
|
|
1116
1206
|
|
|
1117
|
-
|
|
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,
|
|
1118
1214
|
{ exposure, toneMapping, vignette, transparent });
|
|
1119
1215
|
|
|
1120
1216
|
return { width, height, data };
|
|
@@ -1135,6 +1231,9 @@ const SERIALIZABLE = new Set([
|
|
|
1135
1231
|
*/
|
|
1136
1232
|
async function serializeLive(scene: Scene): Promise<SerializedScene> {
|
|
1137
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 }[] = [];
|
|
1138
1237
|
|
|
1139
1238
|
scene.traverse(object => {
|
|
1140
1239
|
const value = (object as Object3D & { material?: Material | Material[] }).material;
|
|
@@ -1146,15 +1245,60 @@ async function serializeLive(scene: Scene): Promise<SerializedScene> {
|
|
|
1146
1245
|
renamed.set(material, material.type);
|
|
1147
1246
|
material.type = shader.isRawShaderMaterial ? "RawShaderMaterial" : "ShaderMaterial";
|
|
1148
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
|
+
}
|
|
1149
1258
|
}
|
|
1150
1259
|
});
|
|
1151
1260
|
|
|
1261
|
+
let snapshot: SerializedScene;
|
|
1262
|
+
|
|
1152
1263
|
try {
|
|
1153
1264
|
// The path tracer samples only the base level of a texture: the mip chain would be built and thrown away.
|
|
1154
|
-
|
|
1265
|
+
snapshot = (await serializeScene(scene, { mipmaps: false })).scene;
|
|
1155
1266
|
} finally {
|
|
1156
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
|
+
}
|
|
1157
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]);
|
|
1158
1302
|
}
|
|
1159
1303
|
|
|
1160
1304
|
/**
|
package/src/sceneUpload.ts
CHANGED
|
@@ -47,6 +47,11 @@ export interface SceneBuffers {
|
|
|
47
47
|
/** Where the lights' cumulative probabilities and the emissive ones' triangles sit in `textures`. */
|
|
48
48
|
lightCdf: number;
|
|
49
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;
|
|
50
55
|
/** Radiance an ambient or hemisphere light adds, which no shadow ray can reach. */
|
|
51
56
|
ambient: PackedLights["ambient"];
|
|
52
57
|
/** Where the surrounding image and its distributions ended up, for the frame uniform. */
|
|
@@ -197,6 +202,10 @@ export function uploadScene(
|
|
|
197
202
|
areaLightCount: lights.areaCount,
|
|
198
203
|
lightCdf: lights.cdfAt,
|
|
199
204
|
lightTriangles: lights.trianglesAt,
|
|
205
|
+
localLightCount: lights.localCount,
|
|
206
|
+
localLightPick: lights.localPick,
|
|
207
|
+
lightTree: lights.treeAt,
|
|
208
|
+
lightLeaves: lights.leavesAt,
|
|
200
209
|
ambient: lights.ambient,
|
|
201
210
|
environment: {
|
|
202
211
|
texture: environment ? textures.indexOf.get(environment.texture.id) ?? NO_TEXTURE : NO_TEXTURE,
|