@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
@@ -0,0 +1,339 @@
1
+ # Architecture
2
+
3
+ How `@tinypoly/gpu-renderer` works inside, and the decisions behind it. For installation and usage, see the
4
+ [README](README.md).
5
+
6
+ The renderer takes a serialized Three.js scene, uploads geometry, a BVH, materials, textures and lights to the GPU, and
7
+ runs a path tracing compute shader that accumulates radiance into a float buffer. A small render pipeline presents that
8
+ buffer, dividing by the sample count and applying exposure, tone mapping, vignette and grain. There is no worker pool and
9
+ no `SharedArrayBuffer`: the state lives on the device. The one worker there is builds the BVH of a large scene off the
10
+ page's thread, and changes nothing about the tree it builds.
11
+
12
+ ## Relationship with @tinypoly/cpu-renderer
13
+
14
+ The two renderers share a contract instead of a code base. Everything that reads Three.js or defines the shape of the
15
+ data comes from the CPU renderer, through a single module (`src/upstream.ts`), so a version bump has one place to audit:
16
+
17
+ - `serializeScene`, `serializeCamera`, `sceneSignature` and the `Serialized*` types: the scene snapshot.
18
+ - `USER_DATA_KEY` and the `userData.cpuRenderer` extensions. The key stays `cpuRenderer` in both renderers: it is
19
+ historical, and renaming it would break scenes that already carry it.
20
+ - `RenderSettings`, `FrameSettings` and the tone mapping transforms.
21
+ - The GPGPU pipeline descriptors a module publishes.
22
+
23
+ What is rewritten in WGSL: material evaluation, the integrator, texture sampling and the environment. The CPU sources
24
+ stay the algorithmic reference, line by line.
25
+
26
+ ## Modules
27
+
28
+ - `device.ts`: adapter and device acquisition, requested limits, loss handling.
29
+ - `camera.ts`: the camera's matrices. Every ray goes through the inverse of the camera's own projection.
30
+ - `geometry.ts`, `bvh.ts`: flat triangle and attribute buffers, and the BVH built straight into 32 byte nodes. Packing
31
+ also builds what Three draws in screen space (points, lines, sprites, dashes) as triangles facing the camera, applies
32
+ displacement maps, and runs vertex shaders. A plain mesh, with no vertex shader and no displacement, takes a faster
33
+ path that transforms each vertex once into typed arrays and writes exactly what the general one would.
34
+ - `bvhBuilder.ts`, `bvhWorker.ts`: the BVH built in a Web Worker from a copy of the positions, for scenes of at least
35
+ `BVH_WORKER_MIN_TRIANGLES`; on the main thread below that, when `bvhWorker` is false, or for good once the worker
36
+ fails to start or load.
37
+ - `structLayout.ts`, `frameLayout.ts`, `materialLayout.ts`: one description of each struct, from which both the WGSL
38
+ declaration and the TypeScript writer are generated, so their word offsets cannot drift apart.
39
+ - `texturePack.ts`, `lights.ts`: the remaining storage buffer layouts. `lights.ts` holds the emissive triangles and
40
+ Three's analytic lights in one cumulative distribution, the triangles first and in triangle order, so the same buffer
41
+ answers both questions the integrator asks: pick a light by probability, and, given a triangle a bounce ray hit, how
42
+ likely direct sampling was to have found it.
43
+ - `environment.ts`: the surrounding image and the distribution that lets a shadow ray aim at its bright parts.
44
+ - `shaderProgram.ts`, `sceneAssets.ts`: the GLSL translation, the procedural sky and the compute bakes (see below).
45
+ - `planarCapture.ts`, `volumetric.ts`: the CPU renderer's planar captures and volumetric lights.
46
+ - `sceneUpload.ts`: the uploads, and the joining of buffers described below.
47
+ - `accumulator.ts`, `pathTracePass.ts`, `presentPass.ts`, `dispatchScheduler.ts`: the frame loop.
48
+ - `wgsl/*.wgsl.ts`: the shader source.
49
+
50
+ ## Decisions worth not relitigating
51
+
52
+ **A megakernel, not a wavefront.** One compute invocation per pixel, the bounce loop inside the kernel, `8 x 8`
53
+ workgroups so neighbouring pixels stay coherent. A wavefront pays off with many distinct materials and high register
54
+ pressure; here there is a single evaluator behind flags, and the queue traffic (a ray state per pixel per bounce) costs
55
+ most on exactly the weak hardware this has to serve. The WGSL is split into pure functions so that moving to a wavefront
56
+ later rewrites the scheduler, not the BRDFs. The signal to revisit: a kernel past roughly 700 lines, or throughput
57
+ falling off a cliff when a lobe is added.
58
+
59
+ **Arrays of the same kind share a binding.** A WebGPU device only has to offer eight storage buffers per shader stage,
60
+ and the kernel wants eleven arrays. Normals and the UV sets go in one buffer, the per-triangle material indices and the
61
+ hierarchy's triangle order in another, and the texture table sits in front of the texels it points at, with the uniform
62
+ saying where each second half begins. Positions, materials, nodes, lights and the accumulation buffer keep their own.
63
+ Merging costs a scene some headroom against `maxStorageBufferBindingSize`, which is why positions stay separate: they are
64
+ the largest array and the hottest one.
65
+
66
+ **Textures in a storage buffer, sampled by hand.** Core WebGPU has no binding arrays, and `texture_2d_array` demands one
67
+ size and one format for every layer, which the scene snapshot does not provide (arbitrary sizes, per texture wrap and UV
68
+ matrix, authored mipmaps). Compute shaders have no implicit derivatives either, so the level of detail is computed by
69
+ hand regardless and half of a hardware sampler's advantage disappears. A fast path can add `texture_2d_array` later, for
70
+ scenes where every texture happens to be uniform.
71
+
72
+ **WGSL lives in `src/wgsl/*.wgsl.ts`, as template literals.** The build is plain `tsc`, and `tsc` does not copy `.wgsl`
73
+ files into `dist`. The Tinypoly rule of keeping shader source in its own file exists because of a build time obfuscation
74
+ plugin that does not apply to a published library.
75
+
76
+ **The hierarchy is built here, not imported.** The CPU renderer has a binned SAH builder, but it stores bounds as
77
+ doubles for a scalar traversal, and re-packing that tree would still leave every minimum needing to be rounded down and
78
+ every maximum up before a float32 shader may use it. Building from the same `Float32Array` the shader reads makes every
79
+ corner exactly representable and the widening unnecessary. Revisit if the two builders start to drift.
80
+
81
+ **Shaders: vertex stages interpreted, fragment stages translated by naga.** A Three shader is two different problems.
82
+
83
+ - **A vertex shader decides where the triangles are**, so it has to run before the hierarchy is built, which happens on
84
+ the CPU. It runs there, once per vertex, on the CPU renderer's own GLSL interpreter (`CpuShader`), with the inputs the
85
+ CPU renderer gives it. Its `gl_Position` is taken back to world space through the camera, which is how displacement,
86
+ billboards and `gl_PointSize` come out exactly as drawn; its varyings are stored per triangle corner.
87
+ - **A fragment shader runs per sample**, inside the kernel. `naga-wasm` (naga 30) translates it to WGSL after a rewrite
88
+ that turns a program into functions: every uniform, varying and built-in becomes a module scope global the kernel
89
+ fills in, every texture call becomes a function the kernel provides (reading the texture table by hand), and `discard`
90
+ sets a flag. Uniforms are loaded from the texture buffer by code generated from the types naga wrote, so arrays,
91
+ structs and matrices need no table of their own. What comes back is renamed into the kernel's namespace and called
92
+ through a `switch` on the material's program. naga is loaded only for a scene that has shaders.
93
+ - **Derivatives are real.** A shader that takes `dFdx`, `dFdy` or `fwidth` runs three times per hit, for the pixel to the
94
+ right, the pixel above and the pixel itself; each call site stores what the neighbours computed there, and the third run
95
+ reads the difference. The neighbours' varyings come from the barycentrics of their camera rays on the hit triangle's
96
+ plane. It is a quad's work in sequence, correct wherever the three runs take the same path through the shader, which is
97
+ the assumption a GPU's own quads make.
98
+ - **An `onBeforeCompile` hook** runs on the material's values before lighting (`diffuseColor`, `normal` in view space,
99
+ `roughnessFactor`, `metalnessFactor`, `totalEmissiveRadiance`), which is the part of Three's shader the CPU renderer's
100
+ snapshot captures. The path tracer then lights the result itself.
101
+
102
+ The CPU renderer's tree-walking front end, considered in an earlier version of this decision, is not used: naga parses,
103
+ type checks and restructures, which was the bulk of the work. A shader that naga refuses (a `switch` that falls through
104
+ was the one known case, and is now rewritten) is reported with `console.warn` and its material renders as its colour,
105
+ rather than failing the frame.
106
+
107
+ **Adaptive sampling spends a fixed budget where the noise is.** A frame has the work of `settings.samples` passes over
108
+ the whole image to spend. Every pixel keeps the sum of its squared luminance beside its radiance, which gives its
109
+ variance; after 32 paths, a pixel whose standard error (over the square root of its luminance, roughly how visible the
110
+ error is once encoded) is under half an 8 bit step stops, provided its 3 x 3 neighbourhood has too. The neighbourhood is
111
+ what keeps a rare bright path honest: a pixel judged alone would stop where it happened not to have seen one yet, and
112
+ that area would converge too dark. Each traced row sets a flag, read back after the pass, and the next pass dispatches
113
+ only the rows that still have work, until the budget is spent or a pixel reaches four times its share. Stopping on the
114
+ samples themselves is a slight bias, the usual price of adaptive sampling.
115
+
116
+ **The denoiser is the spatial filter of SVGF, with diffuse and specular light apart.** The temporal half of SVGF, and of
117
+ NVIDIA's NRD, has nothing to do here: the image does not move while it converges, and the accumulation buffer already
118
+ is a perfect history. What carries over from NRD's RELAX and REBLUR is splitting the signal.
119
+
120
+ - **Two signals.** Every path is split at its first scatter: what the diffuse lobe carried on (and what the camera sees
121
+ directly) is diffuse, what a specular lobe, the clearcoat or transmission carried on is specular. Direct light at that
122
+ vertex is split by evaluating the diffuse term alone. Each pixel keeps the diffuse radiance beside the total, and the
123
+ squared luminance of each signal, which gives each its own variance.
124
+ - **Guides**, taken at the first surface the camera ray stops at: the diffuse albedo, the specular one (the GGX lobe's
125
+ directional albedo from Karis' fit, with the clearcoat, white through glass), the normal, the distance, the roughness,
126
+ and how far the specular continuation went before it hit something.
127
+ - **Filtering.** Each signal is divided by its albedo, so the filter sees light and never blurs a texture. A pixel
128
+ brighter than all eight neighbours is clamped to the brightest of them (anti-firefly). Then up to five passes of a
129
+ 5 x 5 a-trous wavelet, taps 1 to 16 pixels apart. Diffuse taps are weighed by the agreement of normals and depths and
130
+ by their luminance against the pixel's own variance, prefiltered over 3 x 3. Specular taps also by roughness, by
131
+ normals more sharply the smoother the surface, and within the footprint the lobe leaves on the screen: the lobe's angle
132
+ (alpha) times the distance to what it reflects, over that distance plus the surface's, in pixels. A mirror is left
133
+ alone, a rough reflection of something far is smoothed widely. The variance is filtered along, so each pass trusts the
134
+ last.
135
+ - **Mixing back.** Weights are normalized, so a flat area keeps its mean exactly. Each signal is then mixed with what
136
+ was traced by how noisy it still is, by the error adaptive sampling measures, from where it counts as converged to four
137
+ times that: a converged pixel keeps exactly what was traced. `DenoiserSettings` scales the mix, the reach, the
138
+ luminance tolerance and the anti-firefly; changing it filters the finished accumulation again, without tracing.
139
+
140
+ It runs in its own pipelines, outside the kernel's eight storage buffers, on the canvas at the end of every pass and on
141
+ the image `render()` resolves with. The accumulation holds six vec4 per pixel for all of this, 96 bytes, and the filter
142
+ five more buffers of 16: a 2000 x 1350 image takes some 460 MB on the device.
143
+
144
+ ## Compatibility
145
+
146
+ This is a path tracer, **not Three.js/WebGL conformance, and not a match for the CPU renderer's image**. What follows is
147
+ what the renderer reads today. Anything absent is ignored in silence unless it is marked as an error, so a host reading
148
+ this table is the only way to know.
149
+
150
+ ### Geometry and cameras
151
+
152
+ | Works | Notes |
153
+ | --- | --- |
154
+ | Triangle meshes, transformed to world space | Every triangle carries its own three vertices |
155
+ | `InstancedMesh`, with `instanceColor` | The scene snapshot expands it to one mesh per instance |
156
+ | `SkinnedMesh`, morph targets | The snapshot bakes the deformed positions |
157
+ | Per-group materials, indexed or not, `drawRange` | |
158
+ | Vertex normals, or a face normal when absent; `flatShading` | |
159
+ | Vertex colours (`vertexColors`), three or four components | |
160
+ | Two UV sets, `uv` and `uv1` | A texture asking for a higher channel falls back to `uv1` |
161
+ | A mirroring transform | The winding is turned around, as Three flips its front face |
162
+ | `Points` | A square of `size` pixels facing the camera, with `sizeAttenuation`, as `gl_PointCoord` maps it |
163
+ | `Line`, `LineSegments`, `LineLoop`, `LineDashedMaterial` | One pixel wide, as WebGL draws every line; dashes are cut into pieces |
164
+ | `Sprite` | Laid out around its `center`, with `rotation` and `sizeAttenuation` |
165
+ | Any camera with a projection matrix | Perspective, orthographic, zoom, `setViewOffset`, film offset |
166
+
167
+ Camera rays start on the near plane and stop at the far plane, which clip a surface as Three clips a fragment.
168
+ Points, lines, sprites and vertex shaders face or depend on the camera, so a scene with them is packed again whenever the
169
+ camera or the image size changes.
170
+
171
+ `material.side` culls the faces Three culls, for the camera's own rays only: a mesh still casts shadows and holds light
172
+ in from both sides, which is what a closed room drawn `BackSide` for a cutaway view needs. Per-mesh `castShadow` and
173
+ `receiveShadow` are ignored: a traced shadow comes from the geometry that casts it.
174
+
175
+ ### Materials
176
+
177
+ | Type | What happens |
178
+ | --- | --- |
179
+ | `MeshStandardMaterial` | Three r185's direct lighting: Lambert diffuse, GGX with its multiple scattering term |
180
+ | `MeshPhysicalMaterial` | Adds transmission, dispersion, clearcoat, sheen, iridescence, anisotropy, specular colour and intensity |
181
+ | `MeshLambertMaterial` | Lambert diffuse |
182
+ | `MeshPhongMaterial` | Lambert diffuse and Three's Blinn-Phong highlight, sampled by its own lobe |
183
+ | `MeshToonMaterial` | Three's ramp, from `gradientMap` or its two steps, in place of the cosine |
184
+ | `MeshBasicMaterial`, `PointsMaterial`, `LineBasicMaterial`, `SpriteMaterial` | Unlit, which in a path tracer means the surface emits its own colour |
185
+ | `MeshNormalMaterial`, `MeshDepthMaterial`, `MeshDistanceMaterial`, `MeshMatcapMaterial` | Unlit, computed as Three's shaders compute them |
186
+ | `ShadowMaterial` | A shadow catcher: its colour where a shadow casting light is blocked, nothing elsewhere |
187
+ | `ShaderMaterial`, `RawShaderMaterial` | The fragment shader runs, translated; its output is the colour the surface shows, unlit |
188
+ | A built-in material patched with `onBeforeCompile` | The hooks run on the material's values before lighting |
189
+
190
+ Maps read: `map`, `alphaMap`, `emissiveMap`, `roughnessMap`, `metalnessMap`, `normalMap` (tangent or object space),
191
+ `bumpMap`, `displacementMap`, `lightMap`, `envMap`, `specularMap`, `specularColorMap`, `specularIntensityMap`,
192
+ `transmissionMap`, `thicknessMap`, `clearcoatMap`, `clearcoatRoughnessMap`, `clearcoatNormalMap`, `sheenColorMap`,
193
+ `sheenRoughnessMap`, `iridescenceMap`, `iridescenceThicknessMap`, `anisotropyMap`, `gradientMap`, `matcap`. Each one reads
194
+ the channel Three's chunks read and honours the texture's own UV transform, wrap modes, filter and colour space.
195
+
196
+ Coverage follows Three's chunks: `alphaTest` cuts, `alphaHash` keeps the share, and only a `transparent` material with
197
+ normal blending lets the rest through; an opaque one is opaque whatever its opacity. Additive blending adds the surface
198
+ to what is behind it, multiply and subtractive filter it. Material clipping planes (with `clipIntersection`),
199
+ `wireframe` (one pixel wide edges) and `visible`/`colorWrite` are honoured.
200
+
201
+ A **material's own `envMap`** is what the paths leaving it see instead of the scene's surroundings; on Basic, Lambert
202
+ and Phong it blends over the colour through `combine` and `reflectivity`, mirrored or bent by `refractionRatio`. A
203
+ **light map** adds its light to what the camera sees of the surface and is not traced further: the path traces that light
204
+ itself, and counting the bake again would double it.
205
+
206
+ A shader's output is taken as linear light, as in the CPU renderer: the tone mapping and colour space chunks are emptied,
207
+ because the renderer applies both to the image. Its fog chunk is emptied too, because the integrator lays Three's fog over
208
+ every material that accepts it. A fragment shader runs only where a path hits the surface, so a shader whose output
209
+ depends on `gl_FragCoord` sees the camera's projection of the hit, which off screen can be anywhere.
210
+
211
+ **Transmission** follows Three's split: `transmission`, times its map and less metalness, is the share of the diffuse
212
+ layer that lets light through, and the specular lobe stays on top. A path picks that share by chance and then meets a
213
+ dielectric of index `ior`, reflecting by the exact Fresnel term and refracting the rest, rough or smooth. The base colour
214
+ tints light once, on the way in. `thickness` decides what the surface is:
215
+
216
+ - **Zero (Three's default) is a thin wall**, a window: light crosses it without bending, and so do shadow rays, tinted
217
+ by the same amount. A room lit by the sun through its glazing works as it should.
218
+ - **Above zero the mesh bounds a volume** that refracts, and `attenuationColor` over `attenuationDistance` absorbs along
219
+ the path inside it (Beer-Lambert). The mesh has to be closed with outward normals. A shadow ray cannot bend, so a volume
220
+ blocks it: **a glass ball under a directional, point or spot light casts a solid shadow**, with no caustic, because
221
+ those lights occupy no area a refracted path could find. Under an area light, emissive geometry or the environment
222
+ the caustic appears, slowly. **Dispersion** splits a path into one colour the first time it enters such a volume, which
223
+ then refracts with that colour's index, spread as Three spreads it.
224
+
225
+ A refraction does not spend a bounce; a reflection inside the glass does.
226
+
227
+ `aoMap` is ignored deliberately, not by omission. It bakes the occlusion this renderer traces, and applying both would
228
+ darken the same shadow twice.
229
+
230
+ Only the top mip level of a texture is read. A rasterizer needs the lower levels so minified detail does not alias; this
231
+ renderer already draws many jittered samples through every pixel, which averages that detail properly. Levels would buy
232
+ memory locality, not correctness. A bump map's slope is taken over one texel, as in the CPU renderer, where Three takes
233
+ it over one pixel.
234
+
235
+ ### Lights
236
+
237
+ | Works | Notes |
238
+ | --- | --- |
239
+ | `DirectionalLight` | Aimed at directly, with `shadows.softness` as its angular diameter |
240
+ | `PointLight` | Three's own distance falloff and cutoff |
241
+ | `SpotLight` | Cone and penumbra as Three computes them, and its projected `map` |
242
+ | `RectAreaLight` | A real area light, sampled over its rectangle, so its penumbra is traced rather than approximated |
243
+ | `AmbientLight`, `HemisphereLight` | See the divergence below |
244
+ | Emissive geometry | Any triangle whose material emits, weighted by area and power |
245
+
246
+ `light.castShadow` gates the occlusion ray, as does `settings.shadows.enabled`. A `RectAreaLight` always occludes,
247
+ because Three has no shadow switch for one and an area light that cast none would look broken once traced. A toon
248
+ surface lit from behind is not shadowed by its own mesh, as Three's shadow map leaves it.
249
+
250
+ ### The environment and the background
251
+
252
+ A vertical gradient, an equirectangular `.hdr` image, or the scene's procedural sky (`userData.cpuRenderer.sky`), with a
253
+ luminance distribution built over the image so a shadow ray can aim at a sun. `intensity` and `rotation` apply to the
254
+ gradient and the image. A procedural sky is baked by the CPU renderer's interpreter, lights with its own `intensity`,
255
+ ignores the rotation so its sun stays with its light, and paints the background without its intensity, with its
256
+ `foregroundShader` evaluated per pixel over it.
257
+
258
+ `settings.background` decides what the camera sees where nothing is in the way: the environment, a colour, or nothing,
259
+ in which case the image's alpha is the coverage of every pixel and the canvas is configured premultiplied. The
260
+ background shows through transmission, as Three's transmission shows it, and not in a reflection, which shows the
261
+ environment.
262
+
263
+ `scene.fog` (`Fog` and `FogExp2`) is laid over what the camera sees through the first surface, at that surface's view
264
+ depth, for every material that accepts fog, and the background stays clear, as in Three. `settings.fog`, when enabled,
265
+ covers every surface instead.
266
+
267
+ ### Settings
268
+
269
+ Read: `samples`, `renderScale`, `exposure`, `toneMapping`, `vignette`, `grain`, `background`, `fog`, `depthOfField`
270
+ (the CPU renderer's 35 mm lens, `focusDistance`, `aperture` as an f-number, `blades` for polygonal bokeh),
271
+ `globalIllumination.bounces`, `globalIllumination.clamp`, `shadows.enabled`, `shadows.softness`.
272
+
273
+ Ignored: `tileSize`, `ambientOcclusion`, and everything else under `globalIllumination` (`enabled`, `samples`,
274
+ `intensity`, `cache`, `cacheSpacing`, `denoise`).
275
+
276
+ ### The CPU renderer's extensions, and the rest
277
+
278
+ - **Light probes.** A `LightProbe`'s harmonics are radiance in Three's SH3 basis; they join the ambient surrounding,
279
+ which delivers exactly the irradiance Three's shader convolves from them. The snapshot does not carry them, so they are
280
+ read from the live scene `setScene` receives: a scene handed over already serialized has none.
281
+ - **Volumetric lights** (`userData.cpuRenderer.volumetric` on a point or spot light) are marched along every camera ray
282
+ as the CPU renderer's `volumetricLight.js` marches them, up to the first surface, and added over the image the same way.
283
+ - **Planar captures** (`planarCapture`) are rendered by this renderer, path traced from the CPU renderer's mirrored and
284
+ real cameras with its clip planes, into the textures the material samples. They follow the camera, so a scene with one
285
+ is captured again whenever it moves.
286
+ - **Compute simulations** (`compute`) are baked on the CPU, by the CPU renderer's own pipeline.
287
+ - **Instanced geometry drawn by a shader**, such as `Line2`, `LineSegments2` and `Wireframe`: the vertex shader runs over
288
+ the template once per instance, with the per-instance attributes at that instance.
289
+ - **A `switch` whose cases fall through**, which WGSL cannot express, is rewritten for that shader only: one pass of a
290
+ loop, so `break` still leaves it, with an `if` per case. A `continue` inside such a case leaves the switch instead of
291
+ continuing the loop around it.
292
+ - **Hooks after `<opaque_fragment>`** run on the radiance the path found on the camera's line of sight from that
293
+ surface on, before the fog, as Three runs them on the lit colour. For a hook that is not linear in `gl_FragColor` this
294
+ is applied per path rather than to the converged colour.
295
+
296
+ ### Where this deliberately diverges from Three
297
+
298
+ - **`settings.samples` means paths per pixel here**, where the CPU renderer reads it as antialiasing samples and takes
299
+ its path count from `globalIllumination.samples`. One number does both jobs in a path tracer.
300
+ - **An `AmbientLight` or a `HemisphereLight` becomes an equally bright surrounding.** Three adds them straight to a
301
+ surface's diffuse term, which no path can carry; light that arrives from every direction is light from outside the
302
+ scene. The irradiance matches Three exactly wherever the sky is open, and **contributes nothing inside a closed room**,
303
+ which is the difference a host has to know about. It is not painted in the background, as in Three.
304
+ - **Transparency is stochastic.** A surface with an opacity below one lets that share of rays straight through, which is
305
+ what makes a cutout cast a cutout shadow, and what makes the result converge rather than composite.
306
+ - **Unlit surfaces emit.** A basic material, a shader material, a sprite: whatever colour Three would draw, the surface
307
+ radiates, so it also lights its surroundings (found by bounce rays, not aimed at).
308
+ - Global illumination is always on. There is no flag for it: tracing paths is what this renderer does.
309
+ - **Glass refracts** where Three's transmission samples a blurred copy of the opaque scene behind it. A volume bends what
310
+ is behind it and reflects its surroundings properly; a thin wall is exact. Neither is what Three draws.
311
+
312
+ ## Testing
313
+
314
+ The CI gate needs no GPU: scene packing (points, lines, sprites, dashes, displacement and winding included), BVH
315
+ packing, buffer layouts and alignment, the scheduler's feedback loop, and the upstream symbols imported from the CPU
316
+ renderer. `kernel.test.ts` validates the whole kernel with naga, with translated shaders of every kind in it, so a WGSL
317
+ mistake fails in CI rather than on a device. The hierarchy is checked against
318
+ brute force over every triangle, with a traversal in the test file that mirrors the WGSL one step for step, so a packing
319
+ mistake fails here instead of on a device where nothing can be printed.
320
+
321
+ GPU suites are named `*.gpu.test.ts` and skip themselves when no adapter is available. They exist to catch WGSL that does
322
+ not compile, which is the most common and most tedious error to find in a browser.
323
+
324
+ Headless browsers do not judge images here. The software path for WebGPU differs from real drivers in floating point
325
+ results, optional features and limits, which are the three dimensions a path tracer diverges in.
326
+
327
+ What does judge the integrator is `pnpm verify`, which needs a real GPU and so stays out of CI. It drives headless Chrome
328
+ over `scripts/verify.html` and prints a reading against an expectation for each check. Every scene in it has an answer
329
+ known on paper rather than a stored reference image: a unit albedo Lambert surface facing a light of irradiance `pi/4`
330
+ leaves a radiance of exactly `0.25`, whichever kind of light delivered it, so the same number checks the directional
331
+ falloff, the inverse square law, the spot cone, the rectangle's projected area, the ambient conversion, the sRGB decode
332
+ of a texture and the Fresnel split of the specular lobe. A wrong density or a misread texel compiles perfectly and simply
333
+ produces the wrong picture; these numbers say which part is wrong.
334
+
335
+ `pnpm verify --page other.html` runs another page under `scripts/` instead, for chasing one check without running all
336
+ of them.
337
+
338
+ It also reports how far neighbouring pixels sit apart at a low sample count, which is what a broken estimator and a badly
339
+ weighted light both show up as, and it is the measurement to trust over any single image.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Danilo Gadelha
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.