@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.
- package/ARCHITECTURE.md +339 -0
- package/LICENSE +21 -0
- package/README.md +284 -2
- package/dist/accumulator.d.ts +27 -0
- package/dist/accumulator.d.ts.map +1 -0
- package/dist/accumulator.js +57 -0
- package/dist/accumulator.js.map +1 -0
- package/dist/bvh.d.ts +26 -0
- package/dist/bvh.d.ts.map +1 -0
- package/dist/bvh.js +296 -0
- package/dist/bvh.js.map +1 -0
- package/dist/bvhBuilder.d.ts +72 -0
- package/dist/bvhBuilder.d.ts.map +1 -0
- package/dist/bvhBuilder.js +126 -0
- package/dist/bvhBuilder.js.map +1 -0
- package/dist/bvhWorker.d.ts +2 -0
- package/dist/bvhWorker.d.ts.map +1 -0
- package/dist/bvhWorker.js +8 -0
- package/dist/bvhWorker.js.map +1 -0
- package/dist/camera.d.ts +29 -0
- package/dist/camera.d.ts.map +1 -0
- package/dist/camera.js +48 -0
- package/dist/camera.js.map +1 -0
- package/dist/canvas.d.ts +9 -0
- package/dist/canvas.d.ts.map +1 -0
- package/dist/canvas.js +11 -0
- package/dist/canvas.js.map +1 -0
- package/dist/denoise.d.ts +69 -0
- package/dist/denoise.d.ts.map +1 -0
- package/dist/denoise.js +455 -0
- package/dist/denoise.js.map +1 -0
- package/dist/device.d.ts +37 -0
- package/dist/device.d.ts.map +1 -0
- package/dist/device.js +52 -0
- package/dist/device.js.map +1 -0
- package/dist/dispatchScheduler.d.ts +43 -0
- package/dist/dispatchScheduler.d.ts.map +1 -0
- package/dist/dispatchScheduler.js +49 -0
- package/dist/dispatchScheduler.js.map +1 -0
- package/dist/environment.d.ts +39 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/environment.js +168 -0
- package/dist/environment.js.map +1 -0
- package/dist/frameLayout.d.ts +4 -0
- package/dist/frameLayout.d.ts.map +1 -0
- package/dist/frameLayout.js +58 -0
- package/dist/frameLayout.js.map +1 -0
- package/dist/geometry.d.ts +75 -0
- package/dist/geometry.d.ts.map +1 -0
- package/dist/geometry.js +799 -0
- package/dist/geometry.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/lights.d.ts +64 -0
- package/dist/lights.d.ts.map +1 -0
- package/dist/lights.js +264 -0
- package/dist/lights.js.map +1 -0
- package/dist/materialLayout.d.ts +143 -0
- package/dist/materialLayout.d.ts.map +1 -0
- package/dist/materialLayout.js +297 -0
- package/dist/materialLayout.js.map +1 -0
- package/dist/math.d.ts +10 -0
- package/dist/math.d.ts.map +1 -0
- package/dist/math.js +40 -0
- package/dist/math.js.map +1 -0
- package/dist/pathTracePass.d.ts +27 -0
- package/dist/pathTracePass.d.ts.map +1 -0
- package/dist/pathTracePass.js +83 -0
- package/dist/pathTracePass.js.map +1 -0
- package/dist/planarCapture.d.ts +38 -0
- package/dist/planarCapture.d.ts.map +1 -0
- package/dist/planarCapture.js +163 -0
- package/dist/planarCapture.js.map +1 -0
- package/dist/presentPass.d.ts +31 -0
- package/dist/presentPass.d.ts.map +1 -0
- package/dist/presentPass.js +78 -0
- package/dist/presentPass.js.map +1 -0
- package/dist/renderer.d.ts +209 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +847 -0
- package/dist/renderer.js.map +1 -0
- package/dist/sceneAssets.d.ts +24 -0
- package/dist/sceneAssets.d.ts.map +1 -0
- package/dist/sceneAssets.js +77 -0
- package/dist/sceneAssets.js.map +1 -0
- package/dist/sceneUpload.d.ts +61 -0
- package/dist/sceneUpload.d.ts.map +1 -0
- package/dist/sceneUpload.js +98 -0
- package/dist/sceneUpload.js.map +1 -0
- package/dist/shaderModule.d.ts +7 -0
- package/dist/shaderModule.d.ts.map +1 -0
- package/dist/shaderModule.js +19 -0
- package/dist/shaderModule.js.map +1 -0
- package/dist/shaderProgram.d.ts +124 -0
- package/dist/shaderProgram.d.ts.map +1 -0
- package/dist/shaderProgram.js +1030 -0
- package/dist/shaderProgram.js.map +1 -0
- package/dist/structLayout.d.ts +28 -0
- package/dist/structLayout.d.ts.map +1 -0
- package/dist/structLayout.js +58 -0
- package/dist/structLayout.js.map +1 -0
- package/dist/texturePack.d.ts +54 -0
- package/dist/texturePack.d.ts.map +1 -0
- package/dist/texturePack.js +86 -0
- package/dist/texturePack.js.map +1 -0
- package/dist/upstream.d.ts +7 -0
- package/dist/upstream.d.ts.map +1 -0
- package/dist/upstream.js +11 -0
- package/dist/upstream.js.map +1 -0
- package/dist/volumetric.d.ts +10 -0
- package/dist/volumetric.d.ts.map +1 -0
- package/dist/volumetric.js +34 -0
- package/dist/volumetric.js.map +1 -0
- package/dist/wgsl/bsdf.wgsl.d.ts +13 -0
- package/dist/wgsl/bsdf.wgsl.d.ts.map +1 -0
- package/dist/wgsl/bsdf.wgsl.js +619 -0
- package/dist/wgsl/bsdf.wgsl.js.map +1 -0
- package/dist/wgsl/bvh.wgsl.d.ts +3 -0
- package/dist/wgsl/bvh.wgsl.d.ts.map +1 -0
- package/dist/wgsl/bvh.wgsl.js +218 -0
- package/dist/wgsl/bvh.wgsl.js.map +1 -0
- package/dist/wgsl/common.wgsl.d.ts +3 -0
- package/dist/wgsl/common.wgsl.d.ts.map +1 -0
- package/dist/wgsl/common.wgsl.js +193 -0
- package/dist/wgsl/common.wgsl.js.map +1 -0
- package/dist/wgsl/environment.wgsl.d.ts +10 -0
- package/dist/wgsl/environment.wgsl.d.ts.map +1 -0
- package/dist/wgsl/environment.wgsl.js +145 -0
- package/dist/wgsl/environment.wgsl.js.map +1 -0
- package/dist/wgsl/lights.wgsl.d.ts +10 -0
- package/dist/wgsl/lights.wgsl.d.ts.map +1 -0
- package/dist/wgsl/lights.wgsl.js +434 -0
- package/dist/wgsl/lights.wgsl.js.map +1 -0
- package/dist/wgsl/pathTrace.wgsl.d.ts +14 -0
- package/dist/wgsl/pathTrace.wgsl.d.ts.map +1 -0
- package/dist/wgsl/pathTrace.wgsl.js +1020 -0
- package/dist/wgsl/pathTrace.wgsl.js.map +1 -0
- package/dist/wgsl/present.wgsl.d.ts +7 -0
- package/dist/wgsl/present.wgsl.d.ts.map +1 -0
- package/dist/wgsl/present.wgsl.js +154 -0
- package/dist/wgsl/present.wgsl.js.map +1 -0
- package/dist/wgsl/random.wgsl.d.ts +9 -0
- package/dist/wgsl/random.wgsl.d.ts.map +1 -0
- package/dist/wgsl/random.wgsl.js +91 -0
- package/dist/wgsl/random.wgsl.js.map +1 -0
- package/dist/wgsl/sampling.wgsl.d.ts +3 -0
- package/dist/wgsl/sampling.wgsl.d.ts.map +1 -0
- package/dist/wgsl/sampling.wgsl.js +31 -0
- package/dist/wgsl/sampling.wgsl.js.map +1 -0
- package/dist/wgsl/shaderSupport.wgsl.d.ts +6 -0
- package/dist/wgsl/shaderSupport.wgsl.d.ts.map +1 -0
- package/dist/wgsl/shaderSupport.wgsl.js +120 -0
- package/dist/wgsl/shaderSupport.wgsl.js.map +1 -0
- package/dist/wgsl/surface.wgsl.d.ts +10 -0
- package/dist/wgsl/surface.wgsl.d.ts.map +1 -0
- package/dist/wgsl/surface.wgsl.js +794 -0
- package/dist/wgsl/surface.wgsl.js.map +1 -0
- package/dist/wgsl/texture.wgsl.d.ts +11 -0
- package/dist/wgsl/texture.wgsl.d.ts.map +1 -0
- package/dist/wgsl/texture.wgsl.js +132 -0
- package/dist/wgsl/texture.wgsl.js.map +1 -0
- package/package.json +77 -3
- package/src/accumulator.ts +64 -0
- package/src/bvh.ts +336 -0
- package/src/bvhBuilder.ts +178 -0
- package/src/bvhWorker.ts +13 -0
- package/src/camera.ts +77 -0
- package/src/canvas.ts +13 -0
- package/src/denoise.ts +514 -0
- package/src/device.ts +81 -0
- package/src/dispatchScheduler.ts +71 -0
- package/src/environment.ts +233 -0
- package/src/frameLayout.ts +59 -0
- package/src/geometry.ts +1067 -0
- package/src/index.ts +64 -0
- package/src/lights.ts +365 -0
- package/src/materialLayout.ts +408 -0
- package/src/math.ts +48 -0
- package/src/pathTracePass.ts +97 -0
- package/src/planarCapture.ts +225 -0
- package/src/presentPass.ts +99 -0
- package/src/renderer.ts +1145 -0
- package/src/sceneAssets.ts +118 -0
- package/src/sceneUpload.ts +185 -0
- package/src/shaderModule.ts +19 -0
- package/src/shaderProgram.ts +1370 -0
- package/src/structLayout.ts +93 -0
- package/src/texturePack.ts +152 -0
- package/src/upstream.ts +56 -0
- package/src/volumetric.ts +42 -0
- package/src/wgsl/bsdf.wgsl.ts +618 -0
- package/src/wgsl/bvh.wgsl.ts +217 -0
- package/src/wgsl/common.wgsl.ts +193 -0
- package/src/wgsl/environment.wgsl.ts +144 -0
- package/src/wgsl/lights.wgsl.ts +433 -0
- package/src/wgsl/pathTrace.wgsl.ts +1019 -0
- package/src/wgsl/present.wgsl.ts +153 -0
- package/src/wgsl/random.wgsl.ts +90 -0
- package/src/wgsl/sampling.wgsl.ts +30 -0
- package/src/wgsl/shaderSupport.wgsl.ts +119 -0
- package/src/wgsl/surface.wgsl.ts +793 -0
- package/src/wgsl/texture.wgsl.ts +131 -0
package/ARCHITECTURE.md
ADDED
|
@@ -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.
|