three-realtime-rt 0.5.0 → 0.6.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/README.md +63 -13
- package/package.json +2 -1
- package/src/CompositePass.js +3 -0
- package/src/CopyPass.js +3 -0
- package/src/DenoisePass.js +37 -7
- package/src/GBufferPass.js +6 -1
- package/src/GIReservoirPass.js +443 -62
- package/src/RTLightingPass.js +74 -8
- package/src/RealtimeRaytracer.js +237 -2
- package/src/RestirPass.js +12 -5
- package/src/SceneCompiler.js +219 -33
- package/src/TAAPass.js +6 -0
- package/src/VolumetricPass.js +3 -0
- package/src/index.d.ts +102 -2
- package/src/mrtCompat.js +37 -0
package/src/SceneCompiler.js
CHANGED
|
@@ -58,6 +58,15 @@ export class CompiledScene {
|
|
|
58
58
|
this.emissiveTriCount = 0;
|
|
59
59
|
this.triangleCount = 0;
|
|
60
60
|
|
|
61
|
+
// Dynamic emissive area lights: the final (post-cap) emissive triangle list
|
|
62
|
+
// and the subset that belongs to dynamic emitters (row + merged-position
|
|
63
|
+
// offset), refreshed in place each updateDynamic(). lastEmissiveRefreshMs is
|
|
64
|
+
// the CPU cost of the most recent refresh (0 when there are none).
|
|
65
|
+
this.emissiveTris = [];
|
|
66
|
+
this._dynamicEmissive = [];
|
|
67
|
+
this.hasDynamicEmissive = false;
|
|
68
|
+
this.lastEmissiveRefreshMs = 0;
|
|
69
|
+
|
|
61
70
|
this._m3 = new THREE.Matrix3();
|
|
62
71
|
this._normalFrame = 0;
|
|
63
72
|
this._dynBuildVolume = null; // world-volume of the dynamic set at build time
|
|
@@ -251,6 +260,64 @@ export class CompiledScene {
|
|
|
251
260
|
if (this.hasDeforming || this.hasSkinned || this._normalFrame++ % 8 === 0) {
|
|
252
261
|
this.dynamicAttrTex.updateFrom(this.dynamicPackedAttr);
|
|
253
262
|
}
|
|
263
|
+
|
|
264
|
+
// Refresh dynamic emissive area lights from the freshly baked world-space
|
|
265
|
+
// merged positions (rows in the scene-data texture + the power CDF).
|
|
266
|
+
if (this.hasDynamicEmissive) this._refreshDynamicEmissive();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Re-derive dynamic emitters' world-space NEE triangles from the merged
|
|
271
|
+
* dynamic positions this frame, rewrite their rows in the scene-data texture
|
|
272
|
+
* (row 1) and rebuild the power CDF (row 66), then flag the texture for
|
|
273
|
+
* re-upload. The whole (small) texture goes back up — measured in
|
|
274
|
+
* lastEmissiveRefreshMs. Called from updateDynamic (i.e. only when the dynamic
|
|
275
|
+
* set actually updated); an emissive change on an emitter that did NOT move —
|
|
276
|
+
* e.g. recolouring material.emissive — is frozen at compile time and needs a
|
|
277
|
+
* compileScene() (updateLights only rescans analytic THREE lights).
|
|
278
|
+
*/
|
|
279
|
+
_refreshDynamicEmissive() {
|
|
280
|
+
const list = this._dynamicEmissive;
|
|
281
|
+
if (list.length === 0) return;
|
|
282
|
+
const now = typeof performance !== "undefined" ? performance : Date;
|
|
283
|
+
const t0 = now.now();
|
|
284
|
+
const tex = this.materialsTex;
|
|
285
|
+
const data = tex.image.data;
|
|
286
|
+
const row = tex.image.width * 4; // texture width in floats
|
|
287
|
+
const pos = this.dynamicMerged.getAttribute("position").array;
|
|
288
|
+
const tris = this.emissiveTris;
|
|
289
|
+
for (let k = 0; k < list.length; k++) {
|
|
290
|
+
const de = list[k];
|
|
291
|
+
const off = de.off;
|
|
292
|
+
const ax = pos[off], ay = pos[off + 1], az = pos[off + 2];
|
|
293
|
+
const e1x = pos[off + 3] - ax, e1y = pos[off + 4] - ay, e1z = pos[off + 5] - az;
|
|
294
|
+
const e2x = pos[off + 6] - ax, e2y = pos[off + 7] - ay, e2z = pos[off + 8] - az;
|
|
295
|
+
let nx = e1y * e2z - e1z * e2y;
|
|
296
|
+
let ny = e1z * e2x - e1x * e2z;
|
|
297
|
+
let nz = e1x * e2y - e1y * e2x;
|
|
298
|
+
const len = Math.hypot(nx, ny, nz);
|
|
299
|
+
const area = len * 0.5;
|
|
300
|
+
const il = len > 1e-10 ? 1 / len : 0; // keep a degenerate frame from NaN-ing
|
|
301
|
+
nx *= il; ny *= il; nz *= il;
|
|
302
|
+
const emit = de.emit;
|
|
303
|
+
// Keep the JS-side tri object current so writeEmissiveCdf sees fresh areas.
|
|
304
|
+
const t = tris[de.row];
|
|
305
|
+
t.v0[0] = ax; t.v0[1] = ay; t.v0[2] = az;
|
|
306
|
+
t.e1[0] = e1x; t.e1[1] = e1y; t.e1[2] = e1z;
|
|
307
|
+
t.e2[0] = e2x; t.e2[1] = e2y; t.e2[2] = e2z;
|
|
308
|
+
t.n[0] = nx; t.n[1] = ny; t.n[2] = nz;
|
|
309
|
+
t.area = area;
|
|
310
|
+
// Row 1 texel (16 floats) — layout must match buildSceneDataTexture.
|
|
311
|
+
const o = row + de.row * 16;
|
|
312
|
+
data[o + 0] = ax; data[o + 1] = ay; data[o + 2] = az; data[o + 3] = area;
|
|
313
|
+
data[o + 4] = e1x; data[o + 5] = e1y; data[o + 6] = e1z; data[o + 7] = emit[0];
|
|
314
|
+
data[o + 8] = e2x; data[o + 9] = e2y; data[o + 10] = e2z; data[o + 11] = emit[1];
|
|
315
|
+
data[o + 12] = nx; data[o + 13] = ny; data[o + 14] = nz; data[o + 15] = emit[2];
|
|
316
|
+
}
|
|
317
|
+
// Areas (and therefore pick probabilities) may have changed — rebuild the CDF.
|
|
318
|
+
writeEmissiveCdf(data, row, tris);
|
|
319
|
+
tex.needsUpdate = true;
|
|
320
|
+
this.lastEmissiveRefreshMs = now.now() - t0;
|
|
254
321
|
}
|
|
255
322
|
|
|
256
323
|
dispose() {
|
|
@@ -333,12 +400,91 @@ function resolveMeshMaterials(mesh, count, registerMaterial) {
|
|
|
333
400
|
return { matIdx, ranges };
|
|
334
401
|
}
|
|
335
402
|
|
|
403
|
+
// Average-colour cache for emissive maps: texture -> [r,g,b] linear, or null when
|
|
404
|
+
// the image is unreadable (CORS-tainted / not yet decoded). Keyed by the THREE
|
|
405
|
+
// texture so the two collect sites (material row + NEE tris) share one solve.
|
|
406
|
+
const _emissiveMapAvgCache = new Map();
|
|
407
|
+
let _emissiveMapWarned = false;
|
|
408
|
+
|
|
409
|
+
// sRGB -> linear for one 0..1 channel (three decodes SRGBColorSpace maps this way
|
|
410
|
+
// before lighting; average in linear so the cast colour matches the lit look).
|
|
411
|
+
function srgbToLinear(c) {
|
|
412
|
+
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// CPU average of an emissiveMap: draw the texture image into a small (16x16)
|
|
416
|
+
// offscreen canvas and mean the texels. Returns linear [r,g,b] in 0..1, or null
|
|
417
|
+
// if the image can't be read (CORS-tainted, or no decoded image yet) — the
|
|
418
|
+
// caller then falls back to the map-zeroes-the-light behaviour, once, with a
|
|
419
|
+
// console.info explaining why. Result is cached per texture.
|
|
420
|
+
function averageEmissiveMap(map) {
|
|
421
|
+
if (_emissiveMapAvgCache.has(map)) return _emissiveMapAvgCache.get(map);
|
|
422
|
+
let result = null;
|
|
423
|
+
try {
|
|
424
|
+
const img = map.image;
|
|
425
|
+
const w = img && (img.width || img.videoWidth || 0);
|
|
426
|
+
const h = img && (img.height || img.videoHeight || 0);
|
|
427
|
+
if (img && w > 0 && h > 0 && typeof document !== "undefined") {
|
|
428
|
+
const N = 16;
|
|
429
|
+
const canvas = document.createElement("canvas");
|
|
430
|
+
canvas.width = N;
|
|
431
|
+
canvas.height = N;
|
|
432
|
+
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
433
|
+
ctx.drawImage(img, 0, 0, N, N); // downsample
|
|
434
|
+
const d = ctx.getImageData(0, 0, N, N).data; // throws if the image is tainted
|
|
435
|
+
const toLinear = map.colorSpace !== THREE.NoColorSpace && map.colorSpace !== THREE.LinearSRGBColorSpace;
|
|
436
|
+
let r = 0, g = 0, b = 0;
|
|
437
|
+
const n = d.length / 4;
|
|
438
|
+
for (let i = 0; i < d.length; i += 4) {
|
|
439
|
+
if (toLinear) {
|
|
440
|
+
r += srgbToLinear(d[i] / 255);
|
|
441
|
+
g += srgbToLinear(d[i + 1] / 255);
|
|
442
|
+
b += srgbToLinear(d[i + 2] / 255);
|
|
443
|
+
} else {
|
|
444
|
+
r += d[i] / 255; g += d[i + 1] / 255; b += d[i + 2] / 255;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
result = [r / n, g / n, b / n];
|
|
448
|
+
}
|
|
449
|
+
} catch (e) {
|
|
450
|
+
result = null; // CORS-tainted (SecurityError) or unreadable
|
|
451
|
+
}
|
|
452
|
+
if (result === null && !_emissiveMapWarned) {
|
|
453
|
+
_emissiveMapWarned = true;
|
|
454
|
+
console.info(
|
|
455
|
+
"three-realtime-rt: an emissiveMap could not be read on the CPU (CORS-tainted " +
|
|
456
|
+
"or not yet decoded), so its mesh casts no area light — it is still drawn " +
|
|
457
|
+
"per-pixel in the G-buffer. Serve the texture same-origin (or set " +
|
|
458
|
+
"image.crossOrigin) to enable the average-colour approximation."
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
_emissiveMapAvgCache.set(map, result);
|
|
462
|
+
return result;
|
|
463
|
+
}
|
|
464
|
+
|
|
336
465
|
// Effective emissive colour (already scaled by intensity), or null if the
|
|
337
|
-
// material doesn't emit.
|
|
466
|
+
// material doesn't emit. A plain emissive colour is used directly; an
|
|
467
|
+
// emissiveMap is approximated by its AVERAGE colour (avg(map) x emissive x
|
|
468
|
+
// emissiveIntensity) so a textured emitter casts (approximately) correct light —
|
|
469
|
+
// the G-buffer still renders it per-pixel, so it LOOKS patterned. An unreadable
|
|
470
|
+
// map falls back to null (visible only), matching the old exclusion.
|
|
338
471
|
function emissiveColor(mat) {
|
|
339
|
-
if (
|
|
472
|
+
if (!mat.emissive) return null;
|
|
340
473
|
const i = mat.emissiveIntensity ?? 1;
|
|
341
474
|
if (i <= 0 || mat.emissive.r + mat.emissive.g + mat.emissive.b <= 0) return null;
|
|
475
|
+
if (mat.emissiveMap != null) {
|
|
476
|
+
const avg = averageEmissiveMap(mat.emissiveMap);
|
|
477
|
+
if (avg == null) return null; // unreadable -> current behaviour (visible only)
|
|
478
|
+
const emit = [mat.emissive.r * i * avg[0], mat.emissive.g * i * avg[1], mat.emissive.b * i * avg[2]];
|
|
479
|
+
// A map that averages to (near) black casts no meaningful light — treat it
|
|
480
|
+
// as visible-only, like a black emissive colour. This keeps an incidental
|
|
481
|
+
// textured emissive (a high-poly model whose emissiveMap is mostly dark with
|
|
482
|
+
// a few tiny glowing texels — e.g. DamagedHelmet) out of the NEE list rather
|
|
483
|
+
// than flooding it with the whole mesh; a real neon sign clears this easily.
|
|
484
|
+
const lum = 0.2126 * emit[0] + 0.7152 * emit[1] + 0.0722 * emit[2];
|
|
485
|
+
if (lum < 1e-3) return null;
|
|
486
|
+
return emit;
|
|
487
|
+
}
|
|
342
488
|
return [mat.emissive.r * i, mat.emissive.g * i, mat.emissive.b * i];
|
|
343
489
|
}
|
|
344
490
|
|
|
@@ -387,22 +533,9 @@ function buildSceneDataTexture(materials, emissiveTris) {
|
|
|
387
533
|
data[o + i] = (bn[src + i] + 0.5) / 256.0;
|
|
388
534
|
}
|
|
389
535
|
}
|
|
390
|
-
// Emissive power CDF (
|
|
391
|
-
//
|
|
392
|
-
|
|
393
|
-
const w = emissiveTris.map(
|
|
394
|
-
(t) => t.area * (0.2126 * t.emit[0] + 0.7152 * t.emit[1] + 0.0722 * t.emit[2])
|
|
395
|
-
);
|
|
396
|
-
const total = w.reduce((a, b) => a + b, 0);
|
|
397
|
-
const cdfRow = (2 + BLUE_NOISE_SIZE) * row;
|
|
398
|
-
let cum = 0;
|
|
399
|
-
for (let i = 0; i < emissiveTris.length; i++) {
|
|
400
|
-
const p = total > 0 ? w[i] / total : 1 / emissiveTris.length;
|
|
401
|
-
cum += p;
|
|
402
|
-
data[cdfRow + i * 4 + 0] = i === emissiveTris.length - 1 ? 1.0 : cum;
|
|
403
|
-
data[cdfRow + i * 4 + 1] = p;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
536
|
+
// Emissive power CDF (row 66). Factored out so updateDynamic can rebuild it
|
|
537
|
+
// in place when a dynamic emitter's area/position changed this frame.
|
|
538
|
+
writeEmissiveCdf(data, row, emissiveTris);
|
|
406
539
|
const tex = new THREE.DataTexture(data, width, height, THREE.RGBAFormat, THREE.FloatType);
|
|
407
540
|
tex.minFilter = THREE.NearestFilter;
|
|
408
541
|
tex.magFilter = THREE.NearestFilter;
|
|
@@ -410,11 +543,40 @@ function buildSceneDataTexture(materials, emissiveTris) {
|
|
|
410
543
|
return tex;
|
|
411
544
|
}
|
|
412
545
|
|
|
546
|
+
// Write the emissive power CDF (row 66 of the scene-data texture). Weight =
|
|
547
|
+
// area x emitted luminance; degenerate totals fall back to the uniform pick.
|
|
548
|
+
// The layout matches the row-66 comment in buildSceneDataTexture. `row` is the
|
|
549
|
+
// texture width in floats (width * 4). Reused by updateDynamic's in-place refresh.
|
|
550
|
+
function writeEmissiveCdf(data, row, emissiveTris) {
|
|
551
|
+
if (emissiveTris.length === 0) return;
|
|
552
|
+
const cdfRow = (2 + BLUE_NOISE_SIZE) * row;
|
|
553
|
+
let total = 0;
|
|
554
|
+
const w = new Array(emissiveTris.length);
|
|
555
|
+
for (let i = 0; i < emissiveTris.length; i++) {
|
|
556
|
+
const t = emissiveTris[i];
|
|
557
|
+
w[i] = t.area * (0.2126 * t.emit[0] + 0.7152 * t.emit[1] + 0.0722 * t.emit[2]);
|
|
558
|
+
total += w[i];
|
|
559
|
+
}
|
|
560
|
+
let cum = 0;
|
|
561
|
+
for (let i = 0; i < emissiveTris.length; i++) {
|
|
562
|
+
const p = total > 0 ? w[i] / total : 1 / emissiveTris.length;
|
|
563
|
+
cum += p;
|
|
564
|
+
data[cdfRow + i * 4 + 0] = i === emissiveTris.length - 1 ? 1.0 : cum;
|
|
565
|
+
data[cdfRow + i * 4 + 1] = p;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
413
569
|
// Collect world-space triangles of an emissive mesh for the NEE light list.
|
|
414
570
|
// `geo` is already non-indexed and world-baked by extractMeshGeometry. An
|
|
415
571
|
// optional [vStart, vCount) vertex range restricts collection to one material
|
|
416
572
|
// group (ranges are triangle-aligned, so this stays whole-triangle).
|
|
417
|
-
|
|
573
|
+
//
|
|
574
|
+
// `dynBase` >= 0 tags each triangle as belonging to a DYNAMIC emitter: dynBase is
|
|
575
|
+
// the float offset of this mesh's segment in the merged dynamic position array
|
|
576
|
+
// (segStart * 3), so `dynOff` records where the triangle's v0/e1/e2 live there.
|
|
577
|
+
// updateDynamic re-derives the world-space triangle from those merged positions
|
|
578
|
+
// each frame (the merged array already holds the freshly transformed vertices).
|
|
579
|
+
function collectEmissiveTriangles(geo, emit, out, vStart = 0, vCount = -1, dynBase = -1) {
|
|
418
580
|
const pos = geo.getAttribute("position").array;
|
|
419
581
|
const begin = vStart * 3;
|
|
420
582
|
const end = vCount < 0 ? pos.length : Math.min(pos.length, (vStart + vCount) * 3);
|
|
@@ -426,14 +588,19 @@ function collectEmissiveTriangles(geo, emit, out, vStart = 0, vCount = -1) {
|
|
|
426
588
|
const cz = e1[0] * e2[1] - e1[1] * e2[0];
|
|
427
589
|
const len = Math.hypot(cx, cy, cz);
|
|
428
590
|
if (len < 1e-10) continue; // degenerate
|
|
429
|
-
|
|
591
|
+
const tri = {
|
|
430
592
|
v0: [pos[i], pos[i + 1], pos[i + 2]],
|
|
431
593
|
e1,
|
|
432
594
|
e2,
|
|
433
595
|
n: [cx / len, cy / len, cz / len],
|
|
434
596
|
area: len * 0.5,
|
|
435
597
|
emit,
|
|
436
|
-
}
|
|
598
|
+
};
|
|
599
|
+
if (dynBase >= 0) {
|
|
600
|
+
tri.dyn = true;
|
|
601
|
+
tri.dynOff = dynBase + i; // float offset of v0 in the merged dynamic positions
|
|
602
|
+
}
|
|
603
|
+
out.push(tri);
|
|
437
604
|
}
|
|
438
605
|
}
|
|
439
606
|
|
|
@@ -523,19 +690,20 @@ export function compileScene(scene, options = {}) {
|
|
|
523
690
|
// the shared table) and get the ranges for per-group emissive collection.
|
|
524
691
|
const { matIdx, ranges } = resolveMeshMaterials(obj, extracted.count, registerMaterial);
|
|
525
692
|
extracted.geo.setAttribute("materialIndex", new THREE.BufferAttribute(matIdx, 1));
|
|
526
|
-
//
|
|
527
|
-
//
|
|
528
|
-
//
|
|
529
|
-
//
|
|
530
|
-
//
|
|
531
|
-
|
|
693
|
+
// Emissive meshes become NEE area lights (sampled directly with shadow rays
|
|
694
|
+
// instead of waiting for a GI ray to stumble into them). Each emissive GROUP
|
|
695
|
+
// contributes its own range. STATIC emitters are collected in world space
|
|
696
|
+
// once; DYNAMIC emitters are ALSO collected now but tagged with their merged
|
|
697
|
+
// segment offset (segStart * 3) so updateDynamic() can re-derive their
|
|
698
|
+
// world-space triangles each frame from the freshly baked merged positions.
|
|
699
|
+
if (isDynamic) {
|
|
700
|
+
const segStart = dynVertexOffset; // this segment's vertex base in the merged array
|
|
701
|
+
dynamicGeoms.push(extracted.geo);
|
|
532
702
|
for (const r of ranges) {
|
|
533
703
|
const emit = emissiveColor(r.material);
|
|
534
|
-
if (emit)
|
|
704
|
+
if (emit)
|
|
705
|
+
collectEmissiveTriangles(extracted.geo, emit, emissiveTris, r.start, r.vcount, segStart * 3);
|
|
535
706
|
}
|
|
536
|
-
}
|
|
537
|
-
if (isDynamic) {
|
|
538
|
-
dynamicGeoms.push(extracted.geo);
|
|
539
707
|
// A SkinnedMesh is auto-detected (no userData flag): it is CPU-skinned from
|
|
540
708
|
// its live skeleton pose every frame. Opt-in CPU deformation (water/cloth)
|
|
541
709
|
// instead requires userData.rtDeforming and reads live geometry. The two are
|
|
@@ -564,6 +732,10 @@ export function compileScene(scene, options = {}) {
|
|
|
564
732
|
dynVertexOffset += extracted.count;
|
|
565
733
|
} else {
|
|
566
734
|
staticGeoms.push(extracted.geo);
|
|
735
|
+
for (const r of ranges) {
|
|
736
|
+
const emit = emissiveColor(r.material);
|
|
737
|
+
if (emit) collectEmissiveTriangles(extracted.geo, emit, emissiveTris, r.start, r.vcount);
|
|
738
|
+
}
|
|
567
739
|
}
|
|
568
740
|
});
|
|
569
741
|
|
|
@@ -602,13 +774,27 @@ export function compileScene(scene, options = {}) {
|
|
|
602
774
|
if (emissiveTris.length > MAX_EMISSIVE_TRIS) {
|
|
603
775
|
console.warn(
|
|
604
776
|
`three-realtime-rt: ${emissiveTris.length} emissive triangles exceed the ` +
|
|
605
|
-
`NEE cap of ${MAX_EMISSIVE_TRIS}
|
|
606
|
-
`
|
|
777
|
+
`NEE cap of ${MAX_EMISSIVE_TRIS} (shared across static + dynamic emitters); ` +
|
|
778
|
+
`keeping the largest by area (measured at compile time). Dropped triangles ` +
|
|
779
|
+
`no longer act as lights — prefer low-poly emitter meshes, especially for ` +
|
|
780
|
+
`dynamic ones (their tris are refreshed every frame).`
|
|
607
781
|
);
|
|
608
782
|
emissiveTris.sort((a, b) => b.area - a.area);
|
|
609
783
|
emissiveTris.length = MAX_EMISSIVE_TRIS;
|
|
610
784
|
}
|
|
611
785
|
compiled.emissiveTriCount = emissiveTris.length;
|
|
786
|
+
// Keep the final (post-cap) emissive list so updateDynamic can rebuild the
|
|
787
|
+
// power CDF, and record which surviving rows belong to dynamic emitters (with
|
|
788
|
+
// their merged-position offset) so their world-space triangles can be
|
|
789
|
+
// refreshed per frame. Rows stay index-stable after this point, so the
|
|
790
|
+
// reservoir passes' triangle ids remain valid across refreshes.
|
|
791
|
+
compiled.emissiveTris = emissiveTris;
|
|
792
|
+
compiled._dynamicEmissive = [];
|
|
793
|
+
for (let r = 0; r < emissiveTris.length; r++) {
|
|
794
|
+
const t = emissiveTris[r];
|
|
795
|
+
if (t.dyn) compiled._dynamicEmissive.push({ row: r, off: t.dynOff, emit: t.emit });
|
|
796
|
+
}
|
|
797
|
+
compiled.hasDynamicEmissive = compiled._dynamicEmissive.length > 0;
|
|
612
798
|
compiled.materialsTex = buildSceneDataTexture(materials, emissiveTris);
|
|
613
799
|
syncLights(scene, compiled);
|
|
614
800
|
|
package/src/TAAPass.js
CHANGED
|
@@ -128,6 +128,9 @@ export class TAAPass {
|
|
|
128
128
|
this._reset = true;
|
|
129
129
|
|
|
130
130
|
this.material = new THREE.ShaderMaterial({
|
|
131
|
+
// Stable program name for compile-failure self-diagnosis; a link failure
|
|
132
|
+
// disables the optional `taa` feature (image stays lit, just aliased).
|
|
133
|
+
name: "rt:taa",
|
|
131
134
|
glslVersion: THREE.GLSL3,
|
|
132
135
|
vertexShader: fullscreenVert,
|
|
133
136
|
fragmentShader: taaFrag,
|
|
@@ -147,6 +150,9 @@ export class TAAPass {
|
|
|
147
150
|
});
|
|
148
151
|
|
|
149
152
|
this.copyMaterial = new THREE.ShaderMaterial({
|
|
153
|
+
// The TAA resolve-to-screen / history-carry blit; folded into the `taa`
|
|
154
|
+
// feature classification (a failure makes TAA unusable → disable taa).
|
|
155
|
+
name: "rt:taa-copy",
|
|
150
156
|
glslVersion: THREE.GLSL3,
|
|
151
157
|
vertexShader: fullscreenVert,
|
|
152
158
|
fragmentShader: copyFrag,
|
package/src/VolumetricPass.js
CHANGED
|
@@ -277,6 +277,9 @@ export class VolumetricPass {
|
|
|
277
277
|
this.targetB = this._makeTarget(width, height);
|
|
278
278
|
|
|
279
279
|
this.material = new THREE.ShaderMaterial({
|
|
280
|
+
// Stable program name for compile-failure self-diagnosis; a link failure
|
|
281
|
+
// disables the optional `volumetric` feature (image stays lit, no god rays).
|
|
282
|
+
name: "rt:volumetric",
|
|
280
283
|
glslVersion: THREE.GLSL3,
|
|
281
284
|
vertexShader: fullscreenVert,
|
|
282
285
|
fragmentShader: volumetricFrag,
|
package/src/index.d.ts
CHANGED
|
@@ -173,6 +173,24 @@ export interface RealtimeRaytracerOptions {
|
|
|
173
173
|
restirGI?: boolean;
|
|
174
174
|
/** EXPERIMENTAL — temporal M-cap for the ReSTIR GI reservoir (default 20). */
|
|
175
175
|
restirGIMCap?: number;
|
|
176
|
+
/**
|
|
177
|
+
* EXPERIMENTAL — ReSTIR GI (v2) spatial-reuse taps per frame, taken after the
|
|
178
|
+
* temporal merge from the previous frame's reservoirs (reconnection-Jacobian
|
|
179
|
+
* reweighted, with a final visibility ray to prevent leaks). Clamped to 0..4;
|
|
180
|
+
* `0` reproduces v1 temporal-only behaviour. Default 2.
|
|
181
|
+
*/
|
|
182
|
+
restirGISpatialTaps?: number;
|
|
183
|
+
/**
|
|
184
|
+
* EXPERIMENTAL — ReSTIR GI reservoir-sample validation period. Every frame a
|
|
185
|
+
* rotating 1-in-N subset of pixels re-aims its single candidate ray at the
|
|
186
|
+
* reservoir's stored hit (instead of a fresh cosine bounce) and re-shades it;
|
|
187
|
+
* the reservoir is killed when the geometry moved or the re-shaded target
|
|
188
|
+
* collapsed to near-black (a light switched off), and left untouched otherwise.
|
|
189
|
+
* Reuses the existing candidate trace (no extra bounce rays); fixes stale bounce
|
|
190
|
+
* light (a switched-off light stops haunting the reservoir) without drifting a
|
|
191
|
+
* static scene. `0` disables it (byte-identical to before the feature). Default 8.
|
|
192
|
+
*/
|
|
193
|
+
restirGIValidate?: number;
|
|
176
194
|
/**
|
|
177
195
|
* Global fallback index of refraction for transmissive surfaces. A
|
|
178
196
|
* `MeshPhysicalMaterial.ior` overrides this per material for fully-transmissive
|
|
@@ -181,6 +199,15 @@ export interface RealtimeRaytracerOptions {
|
|
|
181
199
|
* the default when no per-material ior is carried.
|
|
182
200
|
*/
|
|
183
201
|
ior?: number;
|
|
202
|
+
/**
|
|
203
|
+
* Chromatic dispersion strength for glass, `0..0.5` (clamped), default `0`
|
|
204
|
+
* (off). Splits refracted white light into a spectrum via stochastic spectral
|
|
205
|
+
* sampling — each frame every glass pixel estimates one colour channel through
|
|
206
|
+
* a channel-shifted ior, and temporal accumulation blends the three into a
|
|
207
|
+
* rainbow (no extra rays). Needs accumulation to converge, so it shimmers
|
|
208
|
+
* slightly in motion. Global control only (no per-material dispersion).
|
|
209
|
+
*/
|
|
210
|
+
dispersion?: number;
|
|
184
211
|
/** History length cap: higher = smoother but slower to react. */
|
|
185
212
|
maxHistory?: number;
|
|
186
213
|
/** Clamp on indirect luminance to suppress fireflies. 0 disables. */
|
|
@@ -250,6 +277,45 @@ export interface VolumetricState {
|
|
|
250
277
|
zones: FogZone[];
|
|
251
278
|
}
|
|
252
279
|
|
|
280
|
+
/** One optional feature auto-disabled after its pass program failed to link. */
|
|
281
|
+
export interface DisabledPass {
|
|
282
|
+
/** The failed pass program's stable name (e.g. `"rt:taa"`, `"rt:denoise"`). */
|
|
283
|
+
pass: string;
|
|
284
|
+
/**
|
|
285
|
+
* The instance toggle that was set false to keep the image lit: one of
|
|
286
|
+
* `"restir"`, `"restirGI"`, `"denoise"`, `"volumetric"`, `"taa"`, `"specular"`.
|
|
287
|
+
*/
|
|
288
|
+
feature: string;
|
|
289
|
+
/** First line of the driver's shader/link info log for the failure. */
|
|
290
|
+
reason: string;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Compile-health summary for the ray tracing pipeline, on
|
|
295
|
+
* {@link RealtimeRaytracer.status}. A pass whose program fails to LINK renders
|
|
296
|
+
* black without throwing (three logs and sets `program.diagnostics.runnable`
|
|
297
|
+
* false, but rendering proceeds), so this is how an integrator distinguishes a
|
|
298
|
+
* working RT image from a broken one — and renders an honest raster fallback
|
|
299
|
+
* with a reason instead of guessing. Populated over the first several rendered
|
|
300
|
+
* frames (link status is checked lazily / can be deferred by the driver).
|
|
301
|
+
*/
|
|
302
|
+
export interface RaytracerStatus {
|
|
303
|
+
/**
|
|
304
|
+
* True while every ray tracing pass is running as intended. Flips to false the
|
|
305
|
+
* moment ANY `rt:*` pass program fails to link — whether a core pass (see
|
|
306
|
+
* `coreFailure`) or an optional feature that was auto-disabled (see `disabled`).
|
|
307
|
+
*/
|
|
308
|
+
ok: boolean;
|
|
309
|
+
/** Optional features turned off to keep the image lit after their pass failed to link. */
|
|
310
|
+
disabled: DisabledPass[];
|
|
311
|
+
/**
|
|
312
|
+
* Set when a CORE pass (gbuffer / lighting / composite) failed to link. Such a
|
|
313
|
+
* pass has no fallback, so the image is black — but now diagnosed. The string
|
|
314
|
+
* is a `"rt:<pass>: <driver log>"` summary; null when no core pass has failed.
|
|
315
|
+
*/
|
|
316
|
+
coreFailure: string | null;
|
|
317
|
+
}
|
|
318
|
+
|
|
253
319
|
/** Options accepted by {@link RealtimeRaytracer.compileScene} and {@link compileScene}. */
|
|
254
320
|
export interface CompileSceneOptions {
|
|
255
321
|
/**
|
|
@@ -287,11 +353,19 @@ export class CompiledScene {
|
|
|
287
353
|
triangleCount: number;
|
|
288
354
|
/** Number of lights scanned into the compiled light tables. */
|
|
289
355
|
lightCount: number;
|
|
290
|
-
/** Number of emissive triangles registered as NEE area lights. */
|
|
356
|
+
/** Number of emissive triangles registered as NEE area lights (static + dynamic). */
|
|
291
357
|
emissiveTriCount: number;
|
|
292
358
|
/** World-space diagonal of the static level (used to auto-scale ray epsilon). */
|
|
293
359
|
sceneDiagonal: number;
|
|
294
|
-
/**
|
|
360
|
+
/** True when any dynamic emitter contributes NEE rows refreshed each frame. */
|
|
361
|
+
hasDynamicEmissive: boolean;
|
|
362
|
+
/** CPU cost (ms) of the most recent dynamic-emissive refresh (0 if none). */
|
|
363
|
+
lastEmissiveRefreshMs: number;
|
|
364
|
+
/**
|
|
365
|
+
* Re-bake moving meshes' current world transforms and refit the dynamic BVH.
|
|
366
|
+
* Also refreshes any dynamic emitters' NEE area-light rows + power CDF from the
|
|
367
|
+
* freshly baked world positions.
|
|
368
|
+
*/
|
|
295
369
|
updateDynamic(): void;
|
|
296
370
|
/** Release GPU resources held by this compiled scene. */
|
|
297
371
|
dispose(): void;
|
|
@@ -335,6 +409,22 @@ export class RealtimeRaytracer {
|
|
|
335
409
|
specMRTSupported: boolean;
|
|
336
410
|
/** The current compiled scene, or null before the first compile / when unsupported. */
|
|
337
411
|
compiled: CompiledScene | null;
|
|
412
|
+
/**
|
|
413
|
+
* First/most-severe pass compile-failure summary (`"rt:<pass>: <driver log>"`),
|
|
414
|
+
* or null while every ray tracing pass compiles clean. A quick honest signal
|
|
415
|
+
* for "should I show the RT image?"; see {@link status} for the structured form.
|
|
416
|
+
* Core failures (gbuffer/lighting/composite) outrank auto-disabled features here.
|
|
417
|
+
*/
|
|
418
|
+
compileError: string | null;
|
|
419
|
+
/**
|
|
420
|
+
* Structured compile-health of the pipeline (see {@link RaytracerStatus}).
|
|
421
|
+
* `status.ok` is true on the healthy path and false once any pass fails to
|
|
422
|
+
* link; `status.disabled` lists auto-disabled optional features; and
|
|
423
|
+
* `status.coreFailure` names an unrecoverable core-pass failure. Populated over
|
|
424
|
+
* the first several rendered frames. When `supported` is false, `status.ok` is
|
|
425
|
+
* false too (the RT pipeline is not operational).
|
|
426
|
+
*/
|
|
427
|
+
status: RaytracerStatus;
|
|
338
428
|
/** Accumulated frame counter. */
|
|
339
429
|
frame: number;
|
|
340
430
|
/** Debug view: 0 composite, 1 albedo, 2 normal, 3 irradiance, 4 worldPos, 5 emissive, 6 specular, 7 bvh cost. */
|
|
@@ -388,6 +478,12 @@ export class RealtimeRaytracer {
|
|
|
388
478
|
* per material for fully-transmissive glass (range [1.0, 1.98]).
|
|
389
479
|
*/
|
|
390
480
|
ior: number;
|
|
481
|
+
/**
|
|
482
|
+
* Chromatic dispersion strength for glass, `0..0.5` (clamped on upload),
|
|
483
|
+
* default `0`. Stochastic spectral sampling: splits refracted white light into
|
|
484
|
+
* a rainbow via temporal accumulation, no extra rays. Global control only.
|
|
485
|
+
*/
|
|
486
|
+
dispersion: number;
|
|
391
487
|
/** One stochastic direct shadow ray per pixel per frame instead of one per light. */
|
|
392
488
|
stochasticLights: boolean;
|
|
393
489
|
/** Adaptive quality governor toggle. */
|
|
@@ -415,6 +511,10 @@ export class RealtimeRaytracer {
|
|
|
415
511
|
restirGI: boolean;
|
|
416
512
|
/** EXPERIMENTAL — temporal M-cap for the ReSTIR GI reservoir. */
|
|
417
513
|
restirGIMCap: number;
|
|
514
|
+
/** EXPERIMENTAL — ReSTIR GI (v2) spatial-reuse taps per frame (0..4, 0 = v1). */
|
|
515
|
+
restirGISpatialTaps: number;
|
|
516
|
+
/** EXPERIMENTAL — ReSTIR GI reservoir-sample validation period (0 = off, default 8). */
|
|
517
|
+
restirGIValidate: number;
|
|
418
518
|
/** Procedural-sky state. */
|
|
419
519
|
sky: SkyState;
|
|
420
520
|
/** Distance-fog state. */
|
package/src/mrtCompat.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Multiple-render-target constructor that works across the three versions in
|
|
5
|
+
* this library's peer range (three >=0.155.0).
|
|
6
|
+
*
|
|
7
|
+
* three r172 removed `WebGLMultipleRenderTargets`. Its replacement is the base
|
|
8
|
+
* `WebGLRenderTarget` with a `count` option (added r162) for N color
|
|
9
|
+
* attachments, which it exposes as the ARRAY `.textures` — while `.texture`
|
|
10
|
+
* became a single texture (an alias for `textures[0]`). This library predates
|
|
11
|
+
* that split and indexes the attachment array as `.texture[i]` throughout the
|
|
12
|
+
* passes, so on new three we shadow the instance `.texture` accessor with the
|
|
13
|
+
* `.textures` array. That keeps every existing `.texture[i]` / `for (... of
|
|
14
|
+
* .texture)` call site correct with no per-site changes, and does not disturb
|
|
15
|
+
* three's WebGL backend (which binds/reads/reads-back via `.textures`, and only
|
|
16
|
+
* touches the single `.texture` on cube/PMREM targets, never on these 2D MRTs).
|
|
17
|
+
*
|
|
18
|
+
* On old three (<= r171) `WebGLMultipleRenderTargets` already IS an
|
|
19
|
+
* array-`.texture` MRT, so it is used unchanged and the result is byte-identical
|
|
20
|
+
* to before this shim existed.
|
|
21
|
+
*
|
|
22
|
+
* Signature matches the old constructor: (width, height, count, options).
|
|
23
|
+
*/
|
|
24
|
+
export function makeMRT(width, height, count, options = {}) {
|
|
25
|
+
if (THREE.WebGLMultipleRenderTargets) {
|
|
26
|
+
return new THREE.WebGLMultipleRenderTargets(width, height, count, options);
|
|
27
|
+
}
|
|
28
|
+
const rt = new THREE.WebGLRenderTarget(width, height, { ...options, count });
|
|
29
|
+
// Re-point `.texture` at the attachment array (old-three semantics). Shadows
|
|
30
|
+
// the prototype get/set texture() accessor on this instance only.
|
|
31
|
+
Object.defineProperty(rt, "texture", {
|
|
32
|
+
value: rt.textures,
|
|
33
|
+
writable: true,
|
|
34
|
+
configurable: true,
|
|
35
|
+
});
|
|
36
|
+
return rt;
|
|
37
|
+
}
|