@tinypoly/gpu-renderer 0.0.0-stage → 0.0.1

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 (204) hide show
  1. package/ARCHITECTURE.md +339 -0
  2. package/LICENSE +21 -0
  3. package/README.md +284 -2
  4. package/dist/accumulator.d.ts +27 -0
  5. package/dist/accumulator.d.ts.map +1 -0
  6. package/dist/accumulator.js +57 -0
  7. package/dist/accumulator.js.map +1 -0
  8. package/dist/bvh.d.ts +26 -0
  9. package/dist/bvh.d.ts.map +1 -0
  10. package/dist/bvh.js +296 -0
  11. package/dist/bvh.js.map +1 -0
  12. package/dist/bvhBuilder.d.ts +72 -0
  13. package/dist/bvhBuilder.d.ts.map +1 -0
  14. package/dist/bvhBuilder.js +126 -0
  15. package/dist/bvhBuilder.js.map +1 -0
  16. package/dist/bvhWorker.d.ts +2 -0
  17. package/dist/bvhWorker.d.ts.map +1 -0
  18. package/dist/bvhWorker.js +8 -0
  19. package/dist/bvhWorker.js.map +1 -0
  20. package/dist/camera.d.ts +29 -0
  21. package/dist/camera.d.ts.map +1 -0
  22. package/dist/camera.js +48 -0
  23. package/dist/camera.js.map +1 -0
  24. package/dist/canvas.d.ts +9 -0
  25. package/dist/canvas.d.ts.map +1 -0
  26. package/dist/canvas.js +11 -0
  27. package/dist/canvas.js.map +1 -0
  28. package/dist/denoise.d.ts +69 -0
  29. package/dist/denoise.d.ts.map +1 -0
  30. package/dist/denoise.js +455 -0
  31. package/dist/denoise.js.map +1 -0
  32. package/dist/device.d.ts +37 -0
  33. package/dist/device.d.ts.map +1 -0
  34. package/dist/device.js +52 -0
  35. package/dist/device.js.map +1 -0
  36. package/dist/dispatchScheduler.d.ts +43 -0
  37. package/dist/dispatchScheduler.d.ts.map +1 -0
  38. package/dist/dispatchScheduler.js +49 -0
  39. package/dist/dispatchScheduler.js.map +1 -0
  40. package/dist/environment.d.ts +39 -0
  41. package/dist/environment.d.ts.map +1 -0
  42. package/dist/environment.js +168 -0
  43. package/dist/environment.js.map +1 -0
  44. package/dist/frameLayout.d.ts +4 -0
  45. package/dist/frameLayout.d.ts.map +1 -0
  46. package/dist/frameLayout.js +58 -0
  47. package/dist/frameLayout.js.map +1 -0
  48. package/dist/geometry.d.ts +75 -0
  49. package/dist/geometry.d.ts.map +1 -0
  50. package/dist/geometry.js +799 -0
  51. package/dist/geometry.js.map +1 -0
  52. package/dist/index.d.ts +27 -0
  53. package/dist/index.d.ts.map +1 -0
  54. package/dist/index.js +19 -0
  55. package/dist/index.js.map +1 -0
  56. package/dist/lights.d.ts +64 -0
  57. package/dist/lights.d.ts.map +1 -0
  58. package/dist/lights.js +264 -0
  59. package/dist/lights.js.map +1 -0
  60. package/dist/materialLayout.d.ts +143 -0
  61. package/dist/materialLayout.d.ts.map +1 -0
  62. package/dist/materialLayout.js +297 -0
  63. package/dist/materialLayout.js.map +1 -0
  64. package/dist/math.d.ts +10 -0
  65. package/dist/math.d.ts.map +1 -0
  66. package/dist/math.js +40 -0
  67. package/dist/math.js.map +1 -0
  68. package/dist/pathTracePass.d.ts +27 -0
  69. package/dist/pathTracePass.d.ts.map +1 -0
  70. package/dist/pathTracePass.js +83 -0
  71. package/dist/pathTracePass.js.map +1 -0
  72. package/dist/planarCapture.d.ts +38 -0
  73. package/dist/planarCapture.d.ts.map +1 -0
  74. package/dist/planarCapture.js +163 -0
  75. package/dist/planarCapture.js.map +1 -0
  76. package/dist/presentPass.d.ts +31 -0
  77. package/dist/presentPass.d.ts.map +1 -0
  78. package/dist/presentPass.js +78 -0
  79. package/dist/presentPass.js.map +1 -0
  80. package/dist/renderer.d.ts +209 -0
  81. package/dist/renderer.d.ts.map +1 -0
  82. package/dist/renderer.js +847 -0
  83. package/dist/renderer.js.map +1 -0
  84. package/dist/sceneAssets.d.ts +24 -0
  85. package/dist/sceneAssets.d.ts.map +1 -0
  86. package/dist/sceneAssets.js +77 -0
  87. package/dist/sceneAssets.js.map +1 -0
  88. package/dist/sceneUpload.d.ts +61 -0
  89. package/dist/sceneUpload.d.ts.map +1 -0
  90. package/dist/sceneUpload.js +98 -0
  91. package/dist/sceneUpload.js.map +1 -0
  92. package/dist/shaderModule.d.ts +7 -0
  93. package/dist/shaderModule.d.ts.map +1 -0
  94. package/dist/shaderModule.js +19 -0
  95. package/dist/shaderModule.js.map +1 -0
  96. package/dist/shaderProgram.d.ts +124 -0
  97. package/dist/shaderProgram.d.ts.map +1 -0
  98. package/dist/shaderProgram.js +1030 -0
  99. package/dist/shaderProgram.js.map +1 -0
  100. package/dist/structLayout.d.ts +28 -0
  101. package/dist/structLayout.d.ts.map +1 -0
  102. package/dist/structLayout.js +58 -0
  103. package/dist/structLayout.js.map +1 -0
  104. package/dist/texturePack.d.ts +54 -0
  105. package/dist/texturePack.d.ts.map +1 -0
  106. package/dist/texturePack.js +86 -0
  107. package/dist/texturePack.js.map +1 -0
  108. package/dist/upstream.d.ts +7 -0
  109. package/dist/upstream.d.ts.map +1 -0
  110. package/dist/upstream.js +11 -0
  111. package/dist/upstream.js.map +1 -0
  112. package/dist/volumetric.d.ts +10 -0
  113. package/dist/volumetric.d.ts.map +1 -0
  114. package/dist/volumetric.js +34 -0
  115. package/dist/volumetric.js.map +1 -0
  116. package/dist/wgsl/bsdf.wgsl.d.ts +13 -0
  117. package/dist/wgsl/bsdf.wgsl.d.ts.map +1 -0
  118. package/dist/wgsl/bsdf.wgsl.js +619 -0
  119. package/dist/wgsl/bsdf.wgsl.js.map +1 -0
  120. package/dist/wgsl/bvh.wgsl.d.ts +3 -0
  121. package/dist/wgsl/bvh.wgsl.d.ts.map +1 -0
  122. package/dist/wgsl/bvh.wgsl.js +218 -0
  123. package/dist/wgsl/bvh.wgsl.js.map +1 -0
  124. package/dist/wgsl/common.wgsl.d.ts +3 -0
  125. package/dist/wgsl/common.wgsl.d.ts.map +1 -0
  126. package/dist/wgsl/common.wgsl.js +193 -0
  127. package/dist/wgsl/common.wgsl.js.map +1 -0
  128. package/dist/wgsl/environment.wgsl.d.ts +10 -0
  129. package/dist/wgsl/environment.wgsl.d.ts.map +1 -0
  130. package/dist/wgsl/environment.wgsl.js +145 -0
  131. package/dist/wgsl/environment.wgsl.js.map +1 -0
  132. package/dist/wgsl/lights.wgsl.d.ts +10 -0
  133. package/dist/wgsl/lights.wgsl.d.ts.map +1 -0
  134. package/dist/wgsl/lights.wgsl.js +434 -0
  135. package/dist/wgsl/lights.wgsl.js.map +1 -0
  136. package/dist/wgsl/pathTrace.wgsl.d.ts +14 -0
  137. package/dist/wgsl/pathTrace.wgsl.d.ts.map +1 -0
  138. package/dist/wgsl/pathTrace.wgsl.js +1020 -0
  139. package/dist/wgsl/pathTrace.wgsl.js.map +1 -0
  140. package/dist/wgsl/present.wgsl.d.ts +7 -0
  141. package/dist/wgsl/present.wgsl.d.ts.map +1 -0
  142. package/dist/wgsl/present.wgsl.js +154 -0
  143. package/dist/wgsl/present.wgsl.js.map +1 -0
  144. package/dist/wgsl/random.wgsl.d.ts +9 -0
  145. package/dist/wgsl/random.wgsl.d.ts.map +1 -0
  146. package/dist/wgsl/random.wgsl.js +91 -0
  147. package/dist/wgsl/random.wgsl.js.map +1 -0
  148. package/dist/wgsl/sampling.wgsl.d.ts +3 -0
  149. package/dist/wgsl/sampling.wgsl.d.ts.map +1 -0
  150. package/dist/wgsl/sampling.wgsl.js +31 -0
  151. package/dist/wgsl/sampling.wgsl.js.map +1 -0
  152. package/dist/wgsl/shaderSupport.wgsl.d.ts +6 -0
  153. package/dist/wgsl/shaderSupport.wgsl.d.ts.map +1 -0
  154. package/dist/wgsl/shaderSupport.wgsl.js +120 -0
  155. package/dist/wgsl/shaderSupport.wgsl.js.map +1 -0
  156. package/dist/wgsl/surface.wgsl.d.ts +10 -0
  157. package/dist/wgsl/surface.wgsl.d.ts.map +1 -0
  158. package/dist/wgsl/surface.wgsl.js +794 -0
  159. package/dist/wgsl/surface.wgsl.js.map +1 -0
  160. package/dist/wgsl/texture.wgsl.d.ts +11 -0
  161. package/dist/wgsl/texture.wgsl.d.ts.map +1 -0
  162. package/dist/wgsl/texture.wgsl.js +132 -0
  163. package/dist/wgsl/texture.wgsl.js.map +1 -0
  164. package/package.json +77 -3
  165. package/src/accumulator.ts +64 -0
  166. package/src/bvh.ts +336 -0
  167. package/src/bvhBuilder.ts +178 -0
  168. package/src/bvhWorker.ts +13 -0
  169. package/src/camera.ts +77 -0
  170. package/src/canvas.ts +13 -0
  171. package/src/denoise.ts +514 -0
  172. package/src/device.ts +81 -0
  173. package/src/dispatchScheduler.ts +71 -0
  174. package/src/environment.ts +233 -0
  175. package/src/frameLayout.ts +59 -0
  176. package/src/geometry.ts +1067 -0
  177. package/src/index.ts +64 -0
  178. package/src/lights.ts +365 -0
  179. package/src/materialLayout.ts +408 -0
  180. package/src/math.ts +48 -0
  181. package/src/pathTracePass.ts +97 -0
  182. package/src/planarCapture.ts +225 -0
  183. package/src/presentPass.ts +99 -0
  184. package/src/renderer.ts +1145 -0
  185. package/src/sceneAssets.ts +118 -0
  186. package/src/sceneUpload.ts +185 -0
  187. package/src/shaderModule.ts +19 -0
  188. package/src/shaderProgram.ts +1370 -0
  189. package/src/structLayout.ts +93 -0
  190. package/src/texturePack.ts +152 -0
  191. package/src/upstream.ts +56 -0
  192. package/src/volumetric.ts +42 -0
  193. package/src/wgsl/bsdf.wgsl.ts +618 -0
  194. package/src/wgsl/bvh.wgsl.ts +217 -0
  195. package/src/wgsl/common.wgsl.ts +193 -0
  196. package/src/wgsl/environment.wgsl.ts +144 -0
  197. package/src/wgsl/lights.wgsl.ts +433 -0
  198. package/src/wgsl/pathTrace.wgsl.ts +1019 -0
  199. package/src/wgsl/present.wgsl.ts +153 -0
  200. package/src/wgsl/random.wgsl.ts +90 -0
  201. package/src/wgsl/sampling.wgsl.ts +30 -0
  202. package/src/wgsl/shaderSupport.wgsl.ts +119 -0
  203. package/src/wgsl/surface.wgsl.ts +793 -0
  204. package/src/wgsl/texture.wgsl.ts +131 -0
package/README.md CHANGED
@@ -1,3 +1,285 @@
1
- # Temporary Holding Version
1
+ # @tinypoly/gpu-renderer
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm version](https://img.shields.io/npm/v/@tinypoly/gpu-renderer)](https://www.npmjs.com/package/@tinypoly/gpu-renderer)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@tinypoly/gpu-renderer)](https://www.npmjs.com/package/@tinypoly/gpu-renderer)
5
+ [![CI](https://github.com/tinypoly/gpu-renderer/actions/workflows/ci.yml/badge.svg)](https://github.com/tinypoly/gpu-renderer/actions/workflows/ci.yml)
6
+ [![types](https://img.shields.io/npm/types/@tinypoly/gpu-renderer)](https://www.npmjs.com/package/@tinypoly/gpu-renderer)
7
+ [![license](https://img.shields.io/npm/l/@tinypoly/gpu-renderer)](LICENSE)
8
+
9
+ A WebGPU path tracer for [Three.js](https://threejs.org) scenes. It takes an ordinary `THREE.Scene` and traces it in WGSL compute shaders, accumulating paths until the image converges, and refines a canvas in place while it does.
10
+
11
+ It is the GPU sibling of [@tinypoly/cpu-renderer](https://github.com/tinypoly/cpu-renderer), which powers the render mode of the [Tinypoly](https://tinypoly.com) editor: the same API, the same scene snapshot and the same settings, so a host picks one at startup and writes the rest once.
12
+
13
+ **[Live example](https://tinypoly.github.io/gpu-renderer/)**
14
+
15
+ - Unidirectional path tracing with multiple importance sampling: every light is aimed at with a shadow ray and weighed against the bounce that might find it, so a small bright lamp or a sun in an HDR converges in tens of samples.
16
+ - Physically based shading as Three r185 lights it: Standard and Physical materials (transmission, dispersion, clearcoat, sheen, iridescence, anisotropy), Lambert, Phong, Toon, Basic, Matcap, Normal, Depth and `ShadowMaterial`, with all of their maps.
17
+ - Real glass: thin walls that light crosses, and closed volumes that refract and absorb.
18
+ - Perspective and orthographic cameras, any projection Three builds; meshes, lines (`Line`, `LineSegments`, `LineLoop`), dashed lines, fat lines (`Line2`), sprites and `PointsMaterial`.
19
+ - Your custom `ShaderMaterial` GLSL, `RawShaderMaterial` and `onBeforeCompile` hooks: fragment shaders are translated to WGSL by [naga](https://github.com/gfx-rs/wgpu/tree/trunk/naga) and run in the kernel, with real derivatives; vertex shaders run on the CPU renderer's interpreter.
20
+ - HDR, gradient or procedural sky environments, light probes, depth of field with polygonal bokeh, fog, volumetric lights, vignette and grain, and ACES, AgX or Neutral tone mapping.
21
+ - Adaptive sampling: pixels whose noise is gone stop taking paths, and the work they leave goes to the dark, glossy and out of focus areas that still need it.
22
+ - A variance-guided denoiser (the spatial filter of SVGF, with diffuse and specular light filtered apart as NVIDIA's RELAX does): each signal is separated from its albedo, filtered by an edge-avoiding wavelet that stops at normals, depths and roughness and trusts each pixel as far as its own variance says, and multiplied back, so textures stay sharp, a sharp reflection stays sharp and flat areas keep their exact mean.
23
+ - Progressive output: the canvas refines sample by sample, and the render can be paused, resumed and cancelled.
24
+ - No worker pool and no `SharedArrayBuffer`: the scene lives on the device, so the page needs no cross-origin isolation. A single worker builds the BVH of a large scene, so the page keeps responding while it is prepared.
25
+
26
+ ## Install
27
+
28
+ ```sh
29
+ npm install @tinypoly/gpu-renderer three
30
+ ```
31
+
32
+ `three` is a peer dependency (0.185 or newer). `@tinypoly/cpu-renderer` comes along: the scene snapshot, the settings and the tone mapping are shared with it rather than written twice. The browser needs WebGPU.
33
+
34
+ ## Usage
35
+
36
+ ```ts
37
+ import * as THREE from "three";
38
+ import { GpuRenderer, attachCanvas, requestWebGpuSupport } from "@tinypoly/gpu-renderer";
39
+
40
+ const { supported, reason } = await requestWebGpuSupport();
41
+ if (!supported) throw new Error(reason); // the host decides what to fall back to: the CPU renderer, for one
42
+
43
+ const scene = new THREE.Scene();
44
+ const sphere = new THREE.Mesh(new THREE.SphereGeometry(1, 48, 24), new THREE.MeshPhysicalMaterial({ color: "tomato" }));
45
+ scene.add(sphere);
46
+
47
+ const camera = new THREE.PerspectiveCamera(40, 1, 0.1, 100);
48
+ camera.position.set(0, 1, 5);
49
+ camera.lookAt(0, 0, 0);
50
+
51
+ const canvas = document.querySelector("canvas")!;
52
+
53
+ const renderer = new GpuRenderer({
54
+ width: canvas.clientWidth,
55
+ height: canvas.clientHeight,
56
+ pixelRatio: window.devicePixelRatio,
57
+ scene,
58
+ camera,
59
+ settings: { samples: 256 }, // paths per pixel
60
+ environment: { kind: "gradient", topColor: "#9cb8d8", bottomColor: "#2b2a28" },
61
+ });
62
+
63
+ attachCanvas(renderer, canvas); // sizes the canvas and refines it in place while the image converges
64
+ renderer.on("sample", ({ samples, target }) => console.log(`${samples} / ${target}`));
65
+ const { width, height, data } = await renderer.render(); // RGBA bytes of the finished image
66
+ ```
67
+
68
+ `width` and `height` are pixels; the render size multiplies them by `pixelRatio` and `settings.renderScale`. Every mesh takes part in shadows: a traced shadow comes from the geometry that casts it, so `castShadow` and `receiveShadow` on a mesh are not read.
69
+
70
+ Without a canvas, take the image when it is done:
71
+
72
+ ```ts
73
+ renderer.on("start", ({ width, height }) => { /* the render size */ });
74
+ renderer.on("pixels", ({ x, y, width, height, data }) => { /* the whole image, once, when the last sample lands */ });
75
+ ```
76
+
77
+ Setters record the inputs; `render()` starts a frame with the current ones:
78
+
79
+ ```ts
80
+ camera.position.x += 1; // the camera is read at every render()
81
+ renderer.setSettings({ samples: 1024, globalIllumination: { bounces: 8 } });
82
+ const image = await renderer.render();
83
+ ```
84
+
85
+ ## API
86
+
87
+ ### `new GpuRenderer(options)`
88
+
89
+ Every option has a setter of the same name, except `bvhWorker` and `createBvhWorker`, which are fixed for the renderer's life.
90
+
91
+ | Option | Type | |
92
+ | --- | --- | --- |
93
+ | `scene` | `THREE.Scene` or the result of `serializeScene` | A Three scene is serialized in the background. Defaults to an empty scene. |
94
+ | `camera` | `THREE.Camera` or `CameraState` | A Three camera is read at every `render()`. Any projection works: perspective, orthographic, zoom and view offsets. Defaults to a perspective camera at (5, 5, 5) looking at the origin. |
95
+ | `settings` | `RenderSettingsInput` | Merged over `DEFAULT_RENDER_SETTINGS`, group by group. See [Settings](#settings). |
96
+ | `environment` | `RenderEnvironment` | See [Environment](#environment). Defaults to `DEFAULT_RENDER_ENVIRONMENT`, a gradient. |
97
+ | `width`, `height` | `number` | Required. Image size in CSS pixels. |
98
+ | `pixelRatio` | `number` | Defaults to 1. Pass `window.devicePixelRatio` for a sharp image on a high-density screen. |
99
+ | `denoiser` | `Partial<DenoiserSettings>` | How the denoiser filters. Merged over `DEFAULT_DENOISER`. See [Denoiser](#denoiser). |
100
+ | `bvhWorker` | `boolean` | Builds the BVH of a scene with at least `BVH_WORKER_MIN_TRIANGLES` (50 000) triangles in a Web Worker, so the page keeps responding. The tree, and so the image, is the same either way. Defaults to `true` where Web Workers exist or when `createBvhWorker` is given; `false` builds on the main thread. See [BVH worker](#bvh-worker). |
101
+ | `createBvhWorker` | `() => BvhWorker` | Starts the BVH worker. Defaults to `createDefaultBvhWorker`. Passing one turns the worker on. |
102
+
103
+ ### Methods
104
+
105
+ | Method | |
106
+ | --- | --- |
107
+ | `render()` | Starts a frame with the current inputs and discards the one in progress. Resolves with the finished image, `{ width, height, data }`: `data` is RGBA bytes, straight sRGB, `width * height * 4` long. Rejects with an `AbortError` (see `isAbortError`) when a later `render()`, `cancel()` or `dispose()` replaces the frame, and with the error when the frame fails, a lost device included. |
108
+ | `setScene(scene)` | A Three scene is serialized in the background; `render()` waits for it. |
109
+ | `setCamera(camera)` | A Three camera is read as it is at every `render()`, so moving it or changing its aspect only needs a new frame. A `CameraState` from `serializeCamera` stays fixed. |
110
+ | `setSettings(settings)` | Merges the given fields into the current settings, group by group: `{ fog: { enabled: true } }` keeps the other fog fields. |
111
+ | `setEnvironment(environment)` | Replaces the environment. A change of intensity or rotation alone does not load the image again. |
112
+ | `setSize(width, height, pixelRatio?)` | Image size in CSS pixels for the next frame; the pixel ratio stays unless given. |
113
+ | `setPixelRatio(pixelRatio)` | |
114
+ | `setDenoiser(denoiser)` | Merges the given fields into the denoiser's settings. A finished image is filtered again at once, without tracing anything: `image` and the canvas take the result and `pixels` fires. |
115
+ | `pause()`, `resume()` | Stops or continues the frame between two dispatches. |
116
+ | `cancel()` | Stops the frame in flight and keeps the device for the next one. `render()` rejects with an `AbortError` and `image` keeps what it held. A paused frame is cancelled too, and the next one starts unpaused. |
117
+ | `dispose()` | Releases the device. |
118
+ | `settings` | The settings the next frame renders with. |
119
+ | `denoiser` | The denoiser's settings. |
120
+ | `rendering`, `paused` | Whether a frame is in flight and whether it is paused. |
121
+ | `image` | The last finished image, in the shape `render()` resolves with, or an empty one of the render size while a frame traces. `null` before the first frame starts. Each frame allocates a new buffer, so the image one `render()` resolved with stays as it is. |
122
+ | `renderWidth`, `renderHeight` | The size the next frame renders at: CSS pixels times `pixelRatio` and `settings.renderScale`. |
123
+
124
+ ### Events
125
+
126
+ `renderer.on(event, listener)` returns the function that removes the listener; `once` and `off` exist too. The payloads are the CPU renderer's; what differs is when they fire, because no pixel is finished before the last sample.
127
+
128
+ | Event | Payload | |
129
+ | --- | --- | --- |
130
+ | `start` | `{ width, height }` | The scene is on the device and tracing starts. `image` has the render size and is empty. |
131
+ | `sample` | `{ samples, target }` | One more path per pixel over the whole image. Only this renderer has it. |
132
+ | `pixels` | `{ x, y, width, height, data }` | The finished image, **once**, as a single region at (0, 0). `image` already holds it. |
133
+ | `progress` | `{ completed, total, phase }` | Rows traced out of `total`, over every sample. `phase` is `"prepare"` while the scene uploads and `"shade"` while it traces. |
134
+ | `bucket` | `{ worker, bucket, width, height }` | The band of rows on the device, always as worker 0, or `null` when it is idle. The renderer sizes its dispatches to keep the page responsive. |
135
+ | `complete` | | The image is finished. `render()` resolves with it right after. |
136
+ | `error` | `Error` | The frame failed. `render()` rejects with the same error. |
137
+
138
+ ### `attachCanvas(renderer, canvas)`
139
+
140
+ Draws the output on an `HTMLCanvasElement` through a WebGPU context: the canvas takes the render size and is refined in place after every dispatch. A pixel no sample has reached yet stays see-through, so a page can show a preview under the canvas until the image covers it. The canvas must not already hold another context. Returns the function that detaches it.
141
+
142
+ A WebGPU canvas does not keep what it presented: to save the image, use the pixels `render()` resolves with, or `image`.
143
+
144
+ ### WebGPU support
145
+
146
+ | Function | |
147
+ | --- | --- |
148
+ | `isWebGpuAvailable()` | The cheap synchronous check: whether `navigator.gpu` exists. It does not prove an adapter does. |
149
+ | `requestWebGpuSupport()` | Asks for an adapter, which is the only honest answer, because a browser can expose `navigator.gpu` and still hand back nothing. Resolves with `{ supported, reason }`. |
150
+
151
+ The renderer never falls back on its own. Choosing another renderer when WebGPU is missing is the host's decision, and the CPU renderer takes the same calls.
152
+
153
+ ### Settings
154
+
155
+ `RenderSettings`, grouped by feature, the CPU renderer's own. Defaults are `DEFAULT_RENDER_SETTINGS`; `mergeSettings(base, input)` applies a partial `RenderSettingsInput` the way `setSettings` does. A path tracer reads some of them differently:
156
+
157
+ | Field | Default | |
158
+ | --- | --- | --- |
159
+ | `renderScale` | `1` | Multiplier on the image size. |
160
+ | `samples` | `4` | **Paths per pixel**, on average: one number does the antialiasing and the global illumination in a path tracer, and a few hundred is a clean image. Above 32, a pixel that has converged stops early and the noisiest ones take up to four times as many. |
161
+ | `toneMapping` | `"aces"` | `"aces"`, `"agx"`, `"neutral"` or `"linear"`. |
162
+ | `exposure` | `1` | |
163
+ | `background.mode` | `"environment"` | `"environment"`, `"color"` or `"transparent"`, which makes the image's alpha the coverage of every pixel. |
164
+ | `background.color` | `"#1a1a1a"` | Used by `mode: "color"`. |
165
+ | `shadows.enabled` | `true` | Whether Three's lights are occlusion tested. Emissive geometry always is. |
166
+ | `shadows.softness` | `1` | Angular diameter of the lights in degrees. `0` gives hard shadows. |
167
+ | `globalIllumination.bounces` | `3` | How far a path goes. Glass wants four or more. |
168
+ | `globalIllumination.clamp` | `0` | Maximum luminance one indirect contribution may add, which trades a little bias for fewer fireflies. `0` disables clamping. |
169
+ | `depthOfField.enabled` | `false` | |
170
+ | `depthOfField.focusDistance` | `10` | World units. |
171
+ | `depthOfField.aperture` | `2.8` | f-number. |
172
+ | `depthOfField.blades` | `0` | Polygon sides of the bokeh. `0` is circular. |
173
+ | `fog.enabled` | `false` | Linear fog over every surface. Without it, `scene.fog` applies. |
174
+ | `fog.color` | `"#b8c4d0"` | |
175
+ | `fog.near`, `fog.far` | `10`, `60` | View depth. |
176
+ | `vignette` | `0` | `0` to `1`. |
177
+ | `grain` | `0` | `0` to `1`. |
178
+
179
+ `globalIllumination.denoise` (default `true`) turns the denoiser on: on the canvas at the end of every pass, and on the image `render()` resolves with. Switching it alone filters the finished image again without tracing.
180
+
181
+ ### Denoiser
182
+
183
+ A variance-guided wavelet filter, the spatial half of SVGF, with the diffuse and the specular light filtered apart, as NVIDIA's RELAX and REBLUR do. Every path is split at its first scatter by the lobe it took; each signal is divided by its own albedo (the diffuse colour, the GGX lobe's directional albedo), filtered by passes that stop at normals and depths, and multiplied back, so textures stay sharp. The specular filter also stops at roughness, and reaches only as far as the footprint the lobe leaves on the screen, from the roughness and the distance to what it reflects: a mirror is left alone, a rough reflection of something far is smoothed widely. Each pixel is trusted as far as its own variance says. `DenoiserSettings`, defaults in `DEFAULT_DENOISER`:
184
+
185
+ | Field | Default | |
186
+ | --- | --- | --- |
187
+ | `strength` | `1` | How much of the filtered image replaces the traced one, from `0` to `1`. |
188
+ | `passes` | `3` | Wavelet passes, `1` to `5`: how far the filter reaches, 2, 6, 14, 30 or 62 pixels. |
189
+ | `edge` | `2` | How large a luminance difference still counts as noise, in standard deviations of the pixel's own noise. Lower keeps more detail and removes less noise. |
190
+ | `onlyNoisy` | `true` | Leaves the pixels that have converged as they were traced, and filters only where noise is still visible. |
191
+ | `antiFirefly` | `true` | Before filtering, clamps a pixel brighter than every one of its eight neighbours to the brightest of them, in each signal. |
192
+
193
+ A denoiser trades noise for detail. What it smooths most is what its guides cannot see: the relief of a lacquer, an out of focus area. With `onlyNoisy` it leaves alone whatever has already converged, so more samples always mean less filtering.
194
+
195
+ ```ts
196
+ renderer.setDenoiser({ strength: 0.6, edge: 1 }); // gentler: filters the finished image again, no tracing
197
+ ```
198
+
199
+ Global illumination is always on, because tracing paths is what this renderer does: `globalIllumination.enabled`, `samples`, `intensity`, `cache` and `cacheSpacing`, `ambientOcclusion` and `tileSize` are the CPU renderer's and are not read.
200
+
201
+ ### Environment
202
+
203
+ `RenderEnvironment`. Lights the scene and paints the background when `background.mode` is `"environment"`. An HDR image gets a luminance distribution, so a shadow ray can aim at its sun.
204
+
205
+ ```ts
206
+ { kind: "hdri", url: "studio.hdr" } // equirectangular .hdr
207
+ { kind: "gradient", topColor: "#9cb8d8", bottomColor: "#2b2a28", exponent: 2 } // exponent defaults to 2
208
+ { kind: "hdri", url: "studio.hdr", intensity: 0.8, rotation: 90 } // intensity 1 and rotation 0 by default
209
+ ```
210
+
211
+ ### Object options
212
+
213
+ The renderer reads the CPU renderer's optional settings from `userData.cpuRenderer` on Three objects; the key keeps that name, so a scene renders the same in both. A plain scene needs none of them.
214
+
215
+ | Where | Field | |
216
+ | --- | --- | --- |
217
+ | Any object | `exclude` | Leaves the object and its children out of the render. |
218
+ | Light | `volumetric` | Scatters the light in the air: `true`, or a medium `{ density, anisotropy, spread }`. |
219
+ | Scene | `sky` | A `ProceduralSky`: a shader that replaces the environment. |
220
+ | Material | `time` | Value of the shader's `time` uniform. |
221
+ | Material | `planarCapture` | Renders planar reflection and refraction textures for a flat surface, path traced from the mirrored camera. The surface's local +Y is its normal. |
222
+ | Material | `compute` | A `MaterialCompute`: fragment passes baked on the CPU into the material's uniforms. |
223
+ | Material | `uniforms` | The live uniforms of a material patched in `onBeforeCompile`. |
224
+ | Material | `prepare` | Called with the scene before it is serialized. |
225
+
226
+ ```ts
227
+ light.userData.cpuRenderer = { volumetric: { density: 0.05 } };
228
+ ```
229
+
230
+ ### Helpers
231
+
232
+ | Function | |
233
+ | --- | --- |
234
+ | `serializeScene(scene)` | Async. The snapshot `setScene` builds from a Three scene, to reuse across renderers, the CPU one included. |
235
+ | `serializeCamera(camera)` | A fixed `CameraState` snapshot of a Three camera, including zoom and view offsets. |
236
+ | `sceneSignature(scene)` | A string that changes whenever something the renderer reads changes. Compare it before re-serializing an interactive scene. |
237
+ | `isAbortError(error)` | Whether a `render()` rejection means the frame was replaced or cancelled rather than failed. |
238
+ | `createDefaultBvhWorker()` | The BVH worker this package ships, started from `new URL("./bvhWorker.js", import.meta.url)`. |
239
+ | `respondToBvhRequest(request)` | The worker's side of the exchange, for a worker of your own: returns the response and the buffers to transfer. |
240
+ | `packGeometry`, `buildBvh`, `packLights`, `packTextures`, `sceneByteLength` | The scene packing, published because the sizes it produces are what a host checks against a device's limits before uploading. |
241
+
242
+ ## Bundlers
243
+
244
+ The renderer loads naga's WebAssembly the first time a scene has a shader, from `new URL("naga_bg.wasm", import.meta.url)` next to the `naga-wasm` module, which Vite, webpack 5, Rspack and Parcel resolve. With **Vite**, keep it out of dependency pre-bundling, or the dev server moves the module away from its binary:
245
+
246
+ ```ts
247
+ // vite.config.ts
248
+ export default defineConfig({
249
+ optimizeDeps: { exclude: ["naga-wasm"] },
250
+ });
251
+ ```
252
+
253
+ The BVH worker is started the same way, from `new URL("./bvhWorker.js", import.meta.url)` next to the package's own module, which the same bundlers emit as a separate chunk. Where yours does not, pass `createBvhWorker`, or `bvhWorker: false`; see [BVH worker](#bvh-worker).
254
+
255
+ ### BVH worker
256
+
257
+ Splitting a large scene into its BVH takes about a second per million triangles, which would hold the page if it ran there. The renderer sends a copy of the positions to a Web Worker instead, starts it on the first scene large enough to need it, and keeps it until `dispose()`. A smaller scene is built on the main thread, where it takes less time than the trip to the worker and back.
258
+
259
+ If the worker cannot start or fails to load, the renderer warns once in the console and builds every BVH on the main thread from then on: the image does not change, only the page holds while the scene is prepared.
260
+
261
+ ```ts
262
+ // Off: every BVH on the main thread.
263
+ new GpuRenderer({ width, height, scene, bvhWorker: false });
264
+
265
+ // A worker of your own, when the bundler cannot follow the default one's URL.
266
+ new GpuRenderer({
267
+ width, height, scene,
268
+ createBvhWorker: () => new Worker(new URL("@tinypoly/gpu-renderer/bvhWorker.js", import.meta.url),
269
+ { type: "module" }),
270
+ });
271
+ ```
272
+
273
+ Anything shaped like a Web Worker fits `BvhWorker`: `postMessage(request, transfer)`, `terminate()`, `onmessage` and `onerror`. A worker of your own answers each `BvhRequest` with `respondToBvhRequest(request)`, posting its `response` with its `transfer` list.
274
+
275
+ ## Limits
276
+
277
+ This is a path tracer, not Three.js or WebGL conformance, and not a match for the CPU renderer's image: shadows, indirect light, reflections and transmission come out differently, usually better, never identical. Where it deliberately disagrees with Three (ambient lights as surroundings, stochastic transparency, unlit surfaces that emit, refracting glass) and what it reads, [ARCHITECTURE.md](ARCHITECTURE.md) lists in full. A scene has to fit the device's largest storage binding, and it is not available in Node, which has no WebGPU.
278
+
279
+ ## Development
280
+
281
+ `pnpm dev` opens the example gallery, the same scenes as the CPU renderer's, traced here. `pnpm test` runs the CI gate, which needs no GPU; `pnpm verify` renders scenes with answers known on paper on a real GPU in headless Chrome; `pnpm capture:scenes` renders the gallery pictures with the example running. `pnpm verify --page divergence.html` measures what the megakernel loses to divergence: the same grid of spheres with one material at a time and with all of them mixed.
282
+
283
+ ## License
284
+
285
+ [MIT](LICENSE)
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Where the samples pile up: one `vec4<f32>` per pixel, rgb summed radiance and w the number of samples that reached
3
+ * it, then a second `vec4<f32>` per pixel whose x sums the coverage, which is what a transparent background reads as
4
+ * alpha, y the depth of the first surface, z and w the squared luminance of the diffuse and the specular signal, which
5
+ * give each its variance. Then the denoiser's guides and signals, each summed: the diffuse albedo with the roughness,
6
+ * the normal with the distance, the diffuse radiance with the specular hit distance, and the specular albedo. Last comes
7
+ * one `vec4<f32>` per row, set when a pass traced anything in that row: adaptive sampling reads it back to know which
8
+ * rows still have pixels that have not converged. A storage buffer rather than a storage texture, because `rgba32float` only allows write access as a texture,
9
+ * and the presentation pass has to read back what is already there.
10
+ */
11
+ export declare class Accumulator {
12
+ private readonly device;
13
+ buffer: GPUBuffer;
14
+ width: number;
15
+ height: number;
16
+ constructor(device: GPUDevice, width: number, height: number);
17
+ get byteLength(): number;
18
+ /** Where the per-row flags start, in bytes. */
19
+ get rowFlagsOffset(): number;
20
+ get rowFlagsLength(): number;
21
+ /** Reallocates only when the size actually changed; the caller clears it either way. */
22
+ resize(width: number, height: number): boolean;
23
+ clear(encoder: GPUCommandEncoder): void;
24
+ destroy(): void;
25
+ private create;
26
+ }
27
+ //# sourceMappingURL=accumulator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accumulator.d.ts","sourceRoot":"","sources":["../src/accumulator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,qBAAa,WAAW;IAKV,OAAO,CAAC,QAAQ,CAAC,MAAM;IAJnC,MAAM,EAAE,SAAS,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;gBAEc,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAM7E,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED,+CAA+C;IAC/C,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED,wFAAwF;IACxF,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO;IAa9C,KAAK,CAAC,OAAO,EAAE,iBAAiB;IAIhC,OAAO;IAIP,OAAO,CAAC,MAAM;CAOf"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Where the samples pile up: one `vec4<f32>` per pixel, rgb summed radiance and w the number of samples that reached
3
+ * it, then a second `vec4<f32>` per pixel whose x sums the coverage, which is what a transparent background reads as
4
+ * alpha, y the depth of the first surface, z and w the squared luminance of the diffuse and the specular signal, which
5
+ * give each its variance. Then the denoiser's guides and signals, each summed: the diffuse albedo with the roughness,
6
+ * the normal with the distance, the diffuse radiance with the specular hit distance, and the specular albedo. Last comes
7
+ * one `vec4<f32>` per row, set when a pass traced anything in that row: adaptive sampling reads it back to know which
8
+ * rows still have pixels that have not converged. A storage buffer rather than a storage texture, because `rgba32float` only allows write access as a texture,
9
+ * and the presentation pass has to read back what is already there.
10
+ */
11
+ export class Accumulator {
12
+ device;
13
+ buffer;
14
+ width;
15
+ height;
16
+ constructor(device, width, height) {
17
+ this.device = device;
18
+ this.width = Math.max(1, Math.floor(width));
19
+ this.height = Math.max(1, Math.floor(height));
20
+ this.buffer = this.create();
21
+ }
22
+ get byteLength() {
23
+ return (this.width * this.height * 6 + this.height) * 16;
24
+ }
25
+ /** Where the per-row flags start, in bytes. */
26
+ get rowFlagsOffset() {
27
+ return this.width * this.height * 96;
28
+ }
29
+ get rowFlagsLength() {
30
+ return this.height * 16;
31
+ }
32
+ /** Reallocates only when the size actually changed; the caller clears it either way. */
33
+ resize(width, height) {
34
+ const next = { width: Math.max(1, Math.floor(width)), height: Math.max(1, Math.floor(height)) };
35
+ if (next.width === this.width && next.height === this.height)
36
+ return false;
37
+ this.buffer.destroy();
38
+ this.width = next.width;
39
+ this.height = next.height;
40
+ this.buffer = this.create();
41
+ return true;
42
+ }
43
+ clear(encoder) {
44
+ encoder.clearBuffer(this.buffer);
45
+ }
46
+ destroy() {
47
+ this.buffer.destroy();
48
+ }
49
+ create() {
50
+ return this.device.createBuffer({
51
+ size: this.byteLength,
52
+ // clearBuffer is a copy into the buffer: without COPY_DST it fails validation and clears nothing.
53
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
54
+ });
55
+ }
56
+ }
57
+ //# sourceMappingURL=accumulator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accumulator.js","sourceRoot":"","sources":["../src/accumulator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,OAAO,WAAW;IAKO;IAJ7B,MAAM,CAAY;IAClB,KAAK,CAAS;IACd,MAAM,CAAS;IAEf,YAA6B,MAAiB,EAAE,KAAa,EAAE,MAAc;QAAhD,WAAM,GAAN,MAAM,CAAW;QAC5C,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5C,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;QAC9C,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;IAC9B,CAAC;IAED,IAAI,UAAU;QACZ,OAAO,CAAC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC;IAC3D,CAAC;IAED,+CAA+C;IAC/C,IAAI,cAAc;QAChB,OAAO,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;IACvC,CAAC;IAED,IAAI,cAAc;QAChB,OAAO,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;IAC1B,CAAC;IAED,wFAAwF;IACxF,MAAM,CAAC,KAAa,EAAE,MAAc;QAClC,MAAM,IAAI,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;QAEhG,IAAI,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM;YAAE,OAAO,KAAK,CAAC;QAE3E,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACtB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;QACxB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1B,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QAE5B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,CAAC,OAA0B;QAC9B,OAAO,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACnC,CAAC;IAED,OAAO;QACL,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;IACxB,CAAC;IAEO,MAAM;QACZ,OAAO,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC;YAC9B,IAAI,EAAE,IAAI,CAAC,UAAU;YACrB,kGAAkG;YAClG,KAAK,EAAE,cAAc,CAAC,OAAO,GAAG,cAAc,CAAC,QAAQ,GAAG,cAAc,CAAC,QAAQ;SAClF,CAAC,CAAC;IACL,CAAC;CACF"}
package/dist/bvh.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Binned SAH bounding volume hierarchy, built straight into the layout the shader reads: eight words per node, the
3
+ * first four a minimum corner and a left child or leaf start, the last four a maximum corner and a right child or
4
+ * triangle count, matching `struct BvhNode` in `wgsl/bvh.wgsl.ts`.
5
+ *
6
+ * The bounds are computed from the same `Float32Array` the shader traverses, so every corner is already a float and
7
+ * needs no widening. Building from double precision positions, as the CPU renderer does, would need each minimum
8
+ * rounded down and each maximum rounded up before they reach the GPU.
9
+ */
10
+ /** Words per node in the packed buffer. */
11
+ export declare const NODE_WORDS = 8;
12
+ /**
13
+ * Deepest node the builder produces. The shader traverses with a fixed stack, and one entry per level is enough
14
+ * because a node only pushes its far child. Depth is capped rather than trusted.
15
+ */
16
+ export declare const MAX_BVH_DEPTH = 32;
17
+ export interface PackedBvh {
18
+ /** `nodeCount * NODE_WORDS` words, read as floats for the corners and as signed integers for the links. */
19
+ nodes: ArrayBuffer;
20
+ nodeCount: number;
21
+ /** Triangle indices in leaf order: a leaf covers `order[start] .. order[start + count - 1]`. */
22
+ order: Uint32Array;
23
+ maxDepth: number;
24
+ }
25
+ export declare function buildBvh(positions: Float32Array, triangleCount: number): PackedBvh;
26
+ //# sourceMappingURL=bvh.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bvh.d.ts","sourceRoot":"","sources":["../src/bvh.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,2CAA2C;AAC3C,eAAO,MAAM,UAAU,IAAI,CAAC;AAW5B;;;GAGG;AACH,eAAO,MAAM,aAAa,KAAK,CAAC;AAEhC,MAAM,WAAW,SAAS;IACxB,2GAA2G;IAC3G,KAAK,EAAE,WAAW,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,gGAAgG;IAChG,KAAK,EAAE,WAAW,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAuBD,wBAAgB,QAAQ,CAAC,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,GAAG,SAAS,CA6HlF"}