@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.
Files changed (110) hide show
  1. package/ARCHITECTURE.md +59 -3
  2. package/README.md +52 -4
  3. package/dist/atmosphere.d.ts +21 -0
  4. package/dist/atmosphere.d.ts.map +1 -0
  5. package/dist/atmosphere.js +72 -0
  6. package/dist/atmosphere.js.map +1 -0
  7. package/dist/denoise.d.ts +11 -1
  8. package/dist/denoise.d.ts.map +1 -1
  9. package/dist/denoise.js +34 -6
  10. package/dist/denoise.js.map +1 -1
  11. package/dist/frameLayout.d.ts.map +1 -1
  12. package/dist/frameLayout.js +15 -0
  13. package/dist/frameLayout.js.map +1 -1
  14. package/dist/geometry.d.ts.map +1 -1
  15. package/dist/geometry.js +6 -3
  16. package/dist/geometry.js.map +1 -1
  17. package/dist/index.d.ts +4 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +3 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/lensPass.d.ts +60 -0
  22. package/dist/lensPass.d.ts.map +1 -0
  23. package/dist/lensPass.js +235 -0
  24. package/dist/lensPass.js.map +1 -0
  25. package/dist/lightTree.d.ts +43 -0
  26. package/dist/lightTree.d.ts.map +1 -0
  27. package/dist/lightTree.js +107 -0
  28. package/dist/lightTree.js.map +1 -0
  29. package/dist/lights.d.ts +10 -0
  30. package/dist/lights.d.ts.map +1 -1
  31. package/dist/lights.js +54 -1
  32. package/dist/lights.js.map +1 -1
  33. package/dist/materialLayout.d.ts +24 -0
  34. package/dist/materialLayout.d.ts.map +1 -1
  35. package/dist/materialLayout.js +36 -1
  36. package/dist/materialLayout.js.map +1 -1
  37. package/dist/pathTracePass.d.ts +6 -3
  38. package/dist/pathTracePass.d.ts.map +1 -1
  39. package/dist/pathTracePass.js +16 -10
  40. package/dist/pathTracePass.js.map +1 -1
  41. package/dist/renderer.d.ts +19 -7
  42. package/dist/renderer.d.ts.map +1 -1
  43. package/dist/renderer.js +118 -14
  44. package/dist/renderer.js.map +1 -1
  45. package/dist/sceneUpload.d.ts +5 -0
  46. package/dist/sceneUpload.d.ts.map +1 -1
  47. package/dist/sceneUpload.js +4 -0
  48. package/dist/sceneUpload.js.map +1 -1
  49. package/dist/settings.d.ts +136 -0
  50. package/dist/settings.d.ts.map +1 -0
  51. package/dist/settings.js +100 -0
  52. package/dist/settings.js.map +1 -0
  53. package/dist/subsurface.d.ts +76 -0
  54. package/dist/subsurface.d.ts.map +1 -0
  55. package/dist/subsurface.js +70 -0
  56. package/dist/subsurface.js.map +1 -0
  57. package/dist/volumeContainment.d.ts +17 -0
  58. package/dist/volumeContainment.d.ts.map +1 -0
  59. package/dist/volumeContainment.js +61 -0
  60. package/dist/volumeContainment.js.map +1 -0
  61. package/dist/wgsl/bsdf.wgsl.d.ts +1 -1
  62. package/dist/wgsl/bsdf.wgsl.d.ts.map +1 -1
  63. package/dist/wgsl/bsdf.wgsl.js +35 -3
  64. package/dist/wgsl/bsdf.wgsl.js.map +1 -1
  65. package/dist/wgsl/common.wgsl.d.ts.map +1 -1
  66. package/dist/wgsl/common.wgsl.js +4 -0
  67. package/dist/wgsl/common.wgsl.js.map +1 -1
  68. package/dist/wgsl/lens.wgsl.d.ts +17 -0
  69. package/dist/wgsl/lens.wgsl.d.ts.map +1 -0
  70. package/dist/wgsl/lens.wgsl.js +315 -0
  71. package/dist/wgsl/lens.wgsl.js.map +1 -0
  72. package/dist/wgsl/lights.wgsl.d.ts +1 -1
  73. package/dist/wgsl/lights.wgsl.d.ts.map +1 -1
  74. package/dist/wgsl/lights.wgsl.js +203 -31
  75. package/dist/wgsl/lights.wgsl.js.map +1 -1
  76. package/dist/wgsl/pathTrace.wgsl.d.ts +1 -1
  77. package/dist/wgsl/pathTrace.wgsl.d.ts.map +1 -1
  78. package/dist/wgsl/pathTrace.wgsl.js +396 -25
  79. package/dist/wgsl/pathTrace.wgsl.js.map +1 -1
  80. package/dist/wgsl/sampling.wgsl.d.ts +1 -1
  81. package/dist/wgsl/sampling.wgsl.d.ts.map +1 -1
  82. package/dist/wgsl/sampling.wgsl.js +25 -0
  83. package/dist/wgsl/sampling.wgsl.js.map +1 -1
  84. package/dist/wgsl/surface.wgsl.d.ts +1 -1
  85. package/dist/wgsl/surface.wgsl.d.ts.map +1 -1
  86. package/dist/wgsl/surface.wgsl.js +21 -1
  87. package/dist/wgsl/surface.wgsl.js.map +1 -1
  88. package/package.json +1 -1
  89. package/src/atmosphere.ts +95 -0
  90. package/src/denoise.ts +44 -6
  91. package/src/frameLayout.ts +15 -0
  92. package/src/geometry.ts +6 -2
  93. package/src/index.ts +13 -0
  94. package/src/lensPass.ts +299 -0
  95. package/src/lightTree.ts +172 -0
  96. package/src/lights.ts +71 -1
  97. package/src/materialLayout.ts +52 -1
  98. package/src/pathTracePass.ts +19 -10
  99. package/src/renderer.ts +166 -22
  100. package/src/sceneUpload.ts +9 -0
  101. package/src/settings.ts +234 -0
  102. package/src/subsurface.ts +153 -0
  103. package/src/volumeContainment.ts +81 -0
  104. package/src/wgsl/bsdf.wgsl.ts +35 -3
  105. package/src/wgsl/common.wgsl.ts +4 -0
  106. package/src/wgsl/lens.wgsl.ts +314 -0
  107. package/src/wgsl/lights.wgsl.ts +203 -31
  108. package/src/wgsl/pathTrace.wgsl.ts +396 -25
  109. package/src/wgsl/sampling.wgsl.ts +25 -0
  110. package/src/wgsl/surface.wgsl.ts +21 -1
@@ -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],
@@ -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(programs: ShaderPrograms | null = null): string {
27
- return COMMON_WGSL + RANDOM_WGSL + BVH_WGSL + SAMPLING_WGSL + TEXTURE_WGSL + SHADER_SUPPORT_WGSL
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
- /** Recompiles the kernel when the scene's shaders changed; the same set of programs keeps the pipeline it has. */
48
- setPrograms(programs: ShaderPrograms | null) {
49
- const key = programs?.key ?? "";
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): GPUComputePipeline {
59
- const module = createShaderModule(this.device, "path trace", pathTraceSource(programs));
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
- /** Merged over `DEFAULT_RENDER_SETTINGS`, group by group. */
78
- settings?: RenderSettingsInput;
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: RenderSettings;
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 = mergeSettings(DEFAULT_RENDER_SETTINGS, options.settings);
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<RenderSettings> {
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 also filters the
400
- * finished image again at once, as `setDenoiser` does, since that needs no tracing.
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: RenderSettingsInput) {
403
- const denoise = this.current.globalIllumination.denoise;
431
+ setSettings(settings: GpuRenderSettingsInput) {
432
+ const before = developing(this.current);
404
433
 
405
- this.current = mergeSettings(this.current, settings);
434
+ this.current = mergeGpuSettings(this.current, settings);
406
435
 
407
- if (this.current.globalIllumination.denoise !== denoise) this.refilter();
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
- this.pathTrace.setPrograms(assets.programs);
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.exposure,
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.denoised ? this.denoisePass?.output : null,
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 { exposure, toneMapping, vignette } = this.current;
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
- const data = await this.imageResolve.read(accumulator.buffer, denoised, width, height,
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
- return (await serializeScene(scene, { mipmaps: false })).scene;
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
  /**
@@ -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,