three-realtime-rt 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -6
- package/package.json +1 -1
- package/src/DenoisePass.js +26 -2
- package/src/GIReservoirPass.js +431 -57
- package/src/RTLightingPass.js +62 -7
- package/src/RealtimeRaytracer.js +37 -0
- package/src/SceneCompiler.js +219 -33
- package/src/index.d.ts +47 -2
package/README.md
CHANGED
|
@@ -364,7 +364,7 @@ material of a group* row).
|
|
|
364
364
|
| `roughness` | ✅ | Drives shadow / GI softness, reflection sharpness **and the GGX specular lobe width** (see *dielectric specular* below). |
|
|
365
365
|
| `metalness` | ✅ | Metallic pixels trace a reflection ray whose analytic-light glints are shadowed; `F0 = mix(0.04, albedo, metalness)`. |
|
|
366
366
|
| `emissive` | ✅ | A *static* emissive mesh becomes a real **area light** (NEE) — casts soft light + shadows, including a GGX highlight. |
|
|
367
|
-
| `emissiveMap` |
|
|
367
|
+
| `emissiveMap` | ✅ average-colour approximation | A map-masked emissive **glows on screen** with its full per-pixel pattern (G-buffer, untouched) **and now also casts light**: the CPU averages the map (`avg(map) × emissive × emissiveIntensity`) and feeds that single colour into the NEE area-light table. Needs a non-black `emissive` (white keeps the cast hue equal to the map average) and a readable image (a CORS-tainted / not-yet-decoded map falls back to visible-only, with a one-time `console.info`). A map that averages to **near-black** (e.g. a model's mostly-dark emissiveMap with a few tiny glowing texels) casts nothing — treated as visible-only so it doesn't flood the NEE list with a whole high-poly mesh. Texel-accurate (spatially-varying) emission is future work. |
|
|
368
368
|
| `transmission` (Physical) | ✅ | Glass: Fresnel reflection (with analytic-light glints) + two-interface refraction. |
|
|
369
369
|
| `transparent` + `opacity` | ✅ | Alpha blend: the surface is composited over the geometry behind it (a straight-through traced ray), weighted by scalar `opacity` and **tinted by `color`/`map`**. The behind-radiance rides the **specular buffer** and the opacity blend happens at **composite** (where the pane's albedo lives), so **needs `specular: true`** — with the specular buffer off, blend surfaces degrade to opaque. Single layer — nearest transparent surface wins, overlapping panes don't inter-sort. Kept out of the BVH, so it casts no shadow. Toggle with `transparency`. |
|
|
370
370
|
| `opacity` on an opaque material | ❌ | Only read when `transparent: true`; an opaque material always writes at full coverage. |
|
|
@@ -383,7 +383,7 @@ material of a group* row).
|
|
|
383
383
|
|-------|-----------|-------|
|
|
384
384
|
| `PointLight` | ✅ | `light.userData.rtRadius` (default `0.15`) sets soft-shadow size. |
|
|
385
385
|
| `DirectionalLight` | ✅ | `light.userData.rtRadius` (default `0.02`) sets sun softness; keep its direction in sync with `sky.sunDir`. |
|
|
386
|
-
| Emissive meshes | ✅ static | Sampled directly as area lights. **Dynamic** emitters
|
|
386
|
+
| Emissive meshes | ✅ static + dynamic | Sampled directly as area lights (NEE). **Dynamic** emitters (a mesh in `dynamicMeshes`) are refreshed every `updateDynamic()`: their world-space triangles are re-derived from the freshly baked/skinned/deformed positions and their NEE rows + power CDF rewritten — a moving glowing orb sweeps real light across the floor. Keep them **low-poly** (refreshed per frame) and note the 256-tri cap is shared with static emitters. |
|
|
387
387
|
| `SpotLight` | ✅ | Cone + penumbra respected; soft shadows via `rtRadius`; visible light cones in volumetric fog. |
|
|
388
388
|
| `RectAreaLight` | ❌ | Use an emissive mesh instead. |
|
|
389
389
|
| `HemisphereLight` / `AmbientLight` | ❌ | Ignored — the procedural `sky` (or `envColor`) provides ambient. |
|
|
@@ -397,8 +397,9 @@ material of a group* row).
|
|
|
397
397
|
bright ones, and let `fireflyClamp` do its job.
|
|
398
398
|
- Up to **32** point/directional lights (`MAX_LIGHTS`); further lights are dropped.
|
|
399
399
|
- Moving, toggling, recolouring or dimming a light → `rt.updateLights(scene)` (cheap, no recompile).
|
|
400
|
-
-
|
|
401
|
-
-
|
|
400
|
+
- **Textured emitters** (`emissiveMap`) cast their **average** colour (`avg(map) × emissive × emissiveIntensity`), not per-texel — the sign still *looks* patterned in the G-buffer but lights with one averaged hue. Texel-accurate emission is future work.
|
|
401
|
+
- **Moving** a dynamic emitter → `rt.updateDynamic()` refreshes its area-light rows + CDF from the new geometry. But an emitter's **emission itself** (its `emissive` colour / `emissiveIntensity`, or the map's average) is frozen at compile time: **changing what it emits — static or dynamic — needs `rt.compileScene(...)` again** (`updateDynamic`/`updateLights` do not rescan emissive meshes). A dynamic emitter that never moves also needs a recompile to reflect an emission change.
|
|
402
|
+
- Emissive area lights are capped at **256 triangles** (shared across static + dynamic; largest by compile-time area kept, with a console warning) — prefer low-poly emitter meshes, especially dynamic ones.
|
|
402
403
|
|
|
403
404
|
### Geometry & occlusion
|
|
404
405
|
|
|
@@ -412,7 +413,7 @@ material of a group* row).
|
|
|
412
413
|
|
|
413
414
|
- **1-bounce GI + direct light.** No multi-bounce diffuse, no caustics, no specular-chain paths.
|
|
414
415
|
- **Reflections** are a single traced bounce into a **diffuse-shaded** view of the world — no recursive mirror-in-mirror; a metal shows its surroundings, not a second full render.
|
|
415
|
-
- **Refraction** is two-interface (front + back). IOR is **per material** (`MeshPhysicalMaterial.ior`, encoded in the G-buffer for fully-transmissive glass, range [1.0, 1.98]); `rt.ior` is the global fallback.
|
|
416
|
+
- **Refraction** is two-interface (front + back). IOR is **per material** (`MeshPhysicalMaterial.ior`, encoded in the G-buffer for fully-transmissive glass, range [1.0, 1.98]); `rt.ior` is the global fallback. Optional **chromatic dispersion** (`rt.dispersion`, off by default) splits the refracted term into a spectrum by stochastic spectral sampling (one channel per glass pixel per frame, blended by temporal accumulation — no extra rays); it is a **global** control, not yet per-material.
|
|
416
417
|
- **Volumetric** is **single-scatter** god rays, not multiple-scattering fog.
|
|
417
418
|
- **Transparency** is a **single-layer deferred blend**: a `transparent` surface writes as the nearest layer of the G-buffer and the lighting pass composites it over the geometry behind by tracing one straight-through ray. The behind-radiance is fully lit (direct + 1-bounce GI) and tinted by the pane's albedo. Overlapping transparent surfaces do **not** inter-sort (only the nearest is kept), and there is no per-pixel back-to-front over-blend of many layers as in raster three.js. Turn it off with `transparency: false` (blend surfaces then render fully opaque).
|
|
418
419
|
|
|
@@ -440,8 +441,9 @@ material of a group* row).
|
|
|
440
441
|
| `refraction` | `true` | Traced two-interface refraction for `MeshPhysicalMaterial.transmission` surfaces. |
|
|
441
442
|
| `transparency` | `true` | Alpha-blended transparency: composite `transparent` meshes over the geometry behind them (single-layer, weighted by `opacity`, tinted by albedo). Needs the specular buffer (`specular: true`). Off = they render fully opaque. |
|
|
442
443
|
| `restir` | `true` | ReSTIR direct lighting: per-pixel reservoirs with temporal + spatial reuse, one visibility ray regardless of light count. Flat cost in light count; cuts emissive area-light noise. |
|
|
443
|
-
| `restirGI` | `false` | **Experimental.** ReSTIR GI (
|
|
444
|
+
| `restirGI` | `false` | **Experimental.** ReSTIR GI (v2, fused spatiotemporal): per-pixel reservoirs reuse the 1-bounce indirect sample across frames at the reprojected same-surface point, then take `restirGISpatialTaps` spatial taps (default `2`, `0` = v1 temporal-only) of the previous frame's reservoirs — each reweighted by the reconnection solid-angle→area Jacobian and validated by a final visibility ray so light does not leak through walls. Runs in a standalone pass with its own sampler budget; the lighting pass then skips its inline GI trace and the resolved GI is added at the à-trous denoise stage — so it only takes effect when `gi` **and** `denoise` are also on. Its mean matches the inline GI path; the spatial taps cut per-frame variance. `restirGIMCap` (default `20`) tunes the temporal M-cap. `restirGIValidate` (default `8`, `0` = off) sets the reservoir-sample validation period: each frame a rotating 1-in-N subset of pixels re-aims its single candidate ray at the reservoir's stored hit and re-shades it; if the geometry moved or the re-shaded target collapsed to near-black (a light switched off) the reservoir is killed so fresh candidates rebuild, otherwise it is left untouched. This reuses the existing candidate trace (no extra bounce rays) and is what makes a switched-off light stop haunting the reservoir instead of fading slowly, while a static scene stays put. |
|
|
444
445
|
| `ior` | `1.5` | **Global fallback** index of refraction for `refraction`. A `MeshPhysicalMaterial.ior` overrides it per material (fully-transmissive glass, range [1.0, 1.98]); this value applies to partial-transmission glass and as the default. |
|
|
446
|
+
| `dispersion` | `0` | Chromatic dispersion for glass, `0..0.5` (clamped). Splits refracted white light into a spectrum — a diamond throws a rainbow. Uses **stochastic spectral sampling**: each frame every glass pixel estimates one colour channel (R/G/B) through a channel-shifted ior and traces the *same single* refraction path, so it costs **no extra rays** and adds no traced-ray call site (it fits the Metal call-site budget). The three per-channel estimates are blended by the temporal accumulator, so the rainbow **only converges with accumulation on** and shimmers slightly in motion. **Global control** for now: three r160's `MeshPhysicalMaterial.dispersion` is not yet read per-material (no free G-buffer channel) — per-material dispersion is future work. |
|
|
445
447
|
| `volumetric` | *off* | Physically-based god rays: single-scatter fog, one BVH-shadowed light sample per lighting pixel per frame, temporally accumulated. `{ enabled, density, maxDist, zones }`, where `zones` is an optional array of up to 8 AABBs `{ min:[x,y,z], max:[x,y,z], density }` that add localized fog on top of (or instead of) the global `density`. |
|
|
446
448
|
| `stochasticLights` | `true` | One direct shadow ray per pixel per frame (random source) instead of one per light. The governor turns it off once it has scaled resolution up on strong hardware. |
|
|
447
449
|
| `temporalReprojection` | `true` | Keep samples across camera/object motion. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "three-realtime-rt",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Turn-on ray traced lighting for three.js: a hybrid deferred renderer with BVH-traced soft shadows, one-bounce global illumination, temporal reprojection, an a-trous denoiser and TAA.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/DenoisePass.js
CHANGED
|
@@ -40,9 +40,28 @@ float luminance(vec3 c) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
// Irradiance tap with the optional GI add folded into rgb (alpha untouched).
|
|
43
|
+
// METAL DIFFUSE WEIGHT: the GI add is DIFFUSE indirect irradiance. Metals have
|
|
44
|
+
// essentially no diffuse response — their indirect light rides the traced
|
|
45
|
+
// REFLECTION path — so their diffuse weight is (1 - metalness). RTLightingPass
|
|
46
|
+
// applies this implicitly to the inline GI: sampleIrr = mix(direct + indirect,
|
|
47
|
+
// reflRad, metal), scaling inline indirect by (1 - metal) on metals. The
|
|
48
|
+
// external ReSTIR GI add is injected HERE, downstream of that mix, so it never
|
|
49
|
+
// picked up the weight — a metalness-0.85 surface (the gold torus knot) received
|
|
50
|
+
// full-strength diffuse GI, ~6.6x too much. That excess is not just too bright:
|
|
51
|
+
// it is the ReSTIR resolve's residual per-pixel variance at full amplitude,
|
|
52
|
+
// which reads as bright gold speckles on the curved metal (worst on iOS/Metal,
|
|
53
|
+
// where the firefly stack has the least headroom). Re-apply the same
|
|
54
|
+
// (1 - metalness) diffuse weight to the add so the two GI paths are energy-
|
|
55
|
+
// consistent on metals and the speckle amplitude drops with the mean. Packed
|
|
56
|
+
// metal word (GBufferPass): metalness lives in [0,1]; glass [2,4) and alpha
|
|
57
|
+
// blend [4,5] are non-metal (weight 1, unchanged from before).
|
|
43
58
|
vec4 sampleIrr(vec2 uv) {
|
|
44
59
|
vec4 c = texture(uIrradiance, uv);
|
|
45
|
-
if (uHasAdd)
|
|
60
|
+
if (uHasAdd) {
|
|
61
|
+
float mw = texture(uGNormalMetal, uv).w;
|
|
62
|
+
float metalT = mw < 2.0 ? clamp(mw, 0.0, 1.0) : 0.0;
|
|
63
|
+
c.rgb += texture(uAddTex, uv).rgb * (1.0 - metalT);
|
|
64
|
+
}
|
|
46
65
|
return c;
|
|
47
66
|
}
|
|
48
67
|
|
|
@@ -90,7 +109,12 @@ void main() {
|
|
|
90
109
|
// business being brighter than its entire neighbourhood — clamp its
|
|
91
110
|
// luminance to the brightest neighbour. Converged pixels are exempt, so
|
|
92
111
|
// real small highlights survive.
|
|
93
|
-
|
|
112
|
+
// With an additive GI input (uHasAdd) the despeckle must ALWAYS run on the
|
|
113
|
+
// first iteration: count is the LIGHTING buffer's history depth and says
|
|
114
|
+
// nothing about the GI term, which is re-resolved fresh every frame — a GI
|
|
115
|
+
// firefly at a "converged" pixel would otherwise skip this clamp entirely
|
|
116
|
+
// and survive to the screen (observed as white speckles on iOS).
|
|
117
|
+
if (uStep < 1.5 && (count < 8.0 || uHasAdd)) {
|
|
94
118
|
float maxL = 0.0;
|
|
95
119
|
float found = 0.0;
|
|
96
120
|
for (int dy = -1; dy <= 1; dy++) {
|