@tinypoly/gpu-renderer 0.1.0 → 0.2.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/ARCHITECTURE.md +31 -3
- package/README.md +13 -4
- package/dist/denoise.d.ts +10 -1
- package/dist/denoise.d.ts.map +1 -1
- package/dist/denoise.js +4 -0
- package/dist/denoise.js.map +1 -1
- package/dist/device.d.ts.map +1 -1
- package/dist/device.js +8 -0
- package/dist/device.js.map +1 -1
- package/dist/frameLayout.d.ts.map +1 -1
- package/dist/frameLayout.js +6 -1
- package/dist/frameLayout.js.map +1 -1
- package/dist/imageResolve.d.ts +2 -1
- package/dist/imageResolve.d.ts.map +1 -1
- package/dist/imageResolve.js +1 -0
- package/dist/imageResolve.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lensPass.d.ts +4 -0
- package/dist/lensPass.d.ts.map +1 -1
- package/dist/lensPass.js +14 -1
- package/dist/lensPass.js.map +1 -1
- package/dist/lightTree.d.ts +10 -3
- package/dist/lightTree.d.ts.map +1 -1
- package/dist/lightTree.js +56 -17
- package/dist/lightTree.js.map +1 -1
- package/dist/lights.d.ts +20 -0
- package/dist/lights.d.ts.map +1 -1
- package/dist/lights.js +48 -1
- package/dist/lights.js.map +1 -1
- package/dist/oidnPass.d.ts +48 -0
- package/dist/oidnPass.d.ts.map +1 -0
- package/dist/oidnPass.js +255 -0
- package/dist/oidnPass.js.map +1 -0
- package/dist/pathTracePass.d.ts +2 -2
- package/dist/pathTracePass.d.ts.map +1 -1
- package/dist/pathTracePass.js +9 -9
- package/dist/pathTracePass.js.map +1 -1
- package/dist/presentPass.d.ts +2 -0
- package/dist/presentPass.d.ts.map +1 -1
- package/dist/presentPass.js +1 -0
- package/dist/presentPass.js.map +1 -1
- package/dist/renderer.d.ts +20 -0
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +138 -19
- package/dist/renderer.js.map +1 -1
- package/dist/sceneUpload.d.ts +5 -0
- package/dist/sceneUpload.d.ts.map +1 -1
- package/dist/sceneUpload.js +4 -0
- package/dist/sceneUpload.js.map +1 -1
- package/dist/settings.d.ts +29 -1
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +15 -6
- package/dist/settings.js.map +1 -1
- package/dist/wgsl/lens.wgsl.d.ts +1 -1
- package/dist/wgsl/lens.wgsl.d.ts.map +1 -1
- package/dist/wgsl/lens.wgsl.js +110 -17
- package/dist/wgsl/lens.wgsl.js.map +1 -1
- package/dist/wgsl/lights.wgsl.d.ts +1 -1
- package/dist/wgsl/lights.wgsl.d.ts.map +1 -1
- package/dist/wgsl/lights.wgsl.js +197 -33
- package/dist/wgsl/lights.wgsl.js.map +1 -1
- package/dist/wgsl/pathTrace.wgsl.d.ts +1 -1
- package/dist/wgsl/pathTrace.wgsl.d.ts.map +1 -1
- package/dist/wgsl/pathTrace.wgsl.js +173 -131
- package/dist/wgsl/pathTrace.wgsl.js.map +1 -1
- package/dist/wgsl/present.wgsl.d.ts +1 -1
- package/dist/wgsl/present.wgsl.d.ts.map +1 -1
- package/dist/wgsl/present.wgsl.js +11 -1
- package/dist/wgsl/present.wgsl.js.map +1 -1
- package/dist/wgsl/random.wgsl.d.ts +1 -1
- package/dist/wgsl/random.wgsl.d.ts.map +1 -1
- package/dist/wgsl/random.wgsl.js +28 -0
- package/dist/wgsl/random.wgsl.js.map +1 -1
- package/package.json +4 -2
- package/src/denoise.ts +15 -1
- package/src/device.ts +9 -0
- package/src/frameLayout.ts +6 -1
- package/src/imageResolve.ts +2 -0
- package/src/index.ts +2 -1
- package/src/lensPass.ts +17 -1
- package/src/lightTree.ts +67 -21
- package/src/lights.ts +65 -1
- package/src/oidnPass.ts +284 -0
- package/src/oidnWebgpu.d.ts +5 -0
- package/src/pathTracePass.ts +11 -9
- package/src/presentPass.ts +3 -0
- package/src/renderer.ts +161 -20
- package/src/sceneUpload.ts +9 -0
- package/src/settings.ts +40 -6
- package/src/wgsl/lens.wgsl.ts +110 -17
- package/src/wgsl/lights.wgsl.ts +197 -33
- package/src/wgsl/pathTrace.wgsl.ts +173 -131
- package/src/wgsl/present.wgsl.ts +11 -1
- package/src/wgsl/random.wgsl.ts +28 -0
- package/weights/LICENSE.txt +202 -0
- package/weights/rt_hdr_alb_nrm.tza +0 -0
|
@@ -42,7 +42,9 @@ fn backgroundRadiance(direction: vec3<f32>) -> vec3<f32> {
|
|
|
42
42
|
return frame.background.rgb;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
// A procedural sky paints the background
|
|
45
|
+
// A procedural sky paints the background with the strength it lights with, and its foreground over it. The CPU
|
|
46
|
+
// renderer leaves that strength out of what the camera sees, which a renderer exposing for the light cannot: the sky
|
|
47
|
+
// that lights a wall five times brighter than itself would show as a dark band behind it.
|
|
46
48
|
if (frame.skyProgram != NO_PROGRAM || frame.skyIntensity > 0.0) {
|
|
47
49
|
if (frame.skyBackground == 0u) {
|
|
48
50
|
return vec3<f32>(0.0);
|
|
@@ -54,10 +56,10 @@ fn backgroundRadiance(direction: vec3<f32>) -> vec3<f32> {
|
|
|
54
56
|
input.direction = direction;
|
|
55
57
|
input.diffuseColor = vec4<f32>(1.0);
|
|
56
58
|
|
|
57
|
-
return runProgram(frame.skyProgram, input).color.rgb;
|
|
59
|
+
return runProgram(frame.skyProgram, input).color.rgb * frame.skyIntensity;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
return environmentRadiance(direction)
|
|
62
|
+
return environmentRadiance(direction);
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
return environmentRadiance(direction);
|
|
@@ -67,7 +69,7 @@ fn backgroundRadiance(direction: vec3<f32>) -> vec3<f32> {
|
|
|
67
69
|
* How much of the sky a bounce ray that escaped keeps. Direct sampling aims at the bright parts of an image too, so the
|
|
68
70
|
* two share it; when direct sampling took all of it, as it does from a toon surface, the bounce keeps none.
|
|
69
71
|
*/
|
|
70
|
-
fn escapeWeight(direction: vec3<f32>, bsdfPdf: f32, lightsOwnDirect: bool) -> f32 {
|
|
72
|
+
fn escapeWeight(direction: vec3<f32>, bsdfPdf: f32, lightsOwnDirect: bool, scatteredAt: vec3<f32>) -> f32 {
|
|
71
73
|
if (frame.environmentPick <= 0.0) {
|
|
72
74
|
return 1.0;
|
|
73
75
|
}
|
|
@@ -80,7 +82,8 @@ fn escapeWeight(direction: vec3<f32>, bsdfPdf: f32, lightsOwnDirect: bool) -> f3
|
|
|
80
82
|
return 1.0;
|
|
81
83
|
}
|
|
82
84
|
|
|
83
|
-
|
|
85
|
+
// The sky was aimed at from the vertex the bounce left, through its windows as well when that vertex is inside.
|
|
86
|
+
return misWeight(bsdfPdf, frame.environmentPick * skyPdf(scatteredAt, direction, portalChance(scatteredAt)));
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
/** Scales a contribution down to the luminance ceiling, which trades a little bias for the fireflies it removes. */
|
|
@@ -456,55 +459,57 @@ fn traceRadiance(
|
|
|
456
459
|
for (var step = 0u; step < MAX_STEPS + MAX_WALK; step = step + 1u) {
|
|
457
460
|
let hit = traceScene(origin, direction, limit);
|
|
458
461
|
|
|
459
|
-
//
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
if (VOLUMES) {
|
|
469
|
-
mediumSample = sample2D(rng);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
if (BEVEL) {
|
|
473
|
-
bevelSample = vec4<f32>(sample2D(rng), sample2D(rng));
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// The walk left the material here, and the surface below is its way out rather than what the material shows.
|
|
477
|
-
var exiting = false;
|
|
462
|
+
// A step of the walk inside a material that ends in the medium: the light scatters there, keeping each colour's
|
|
463
|
+
// share of it. It needs three numbers, which a plain hash gives for a fraction of what the sequence costs.
|
|
464
|
+
if (SUBSURFACE_WALK && inMedium && hit.triangle == NO_HIT) {
|
|
465
|
+
let travelled = exp(-sigma * limit);
|
|
466
|
+
let density = dot(walkOdds, sigma * travelled);
|
|
467
|
+
// A new direction, the next distance (a colour and how far), and the roulette's chance.
|
|
468
|
+
let turn = hashSample2D(rng);
|
|
469
|
+
let next = hashSample2D(rng);
|
|
470
|
+
let chance = hashSample2D(rng).x;
|
|
478
471
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
// Nothing within the drawn distance: the light scatters there, keeping each colour's share of the medium.
|
|
482
|
-
let travelled = exp(-sigma * limit);
|
|
483
|
-
let density = dot(walkOdds, sigma * travelled);
|
|
472
|
+
throughput = throughput * sigma * walkAlbedo * travelled / max(density, 1e-20);
|
|
473
|
+
walkSteps = walkSteps + 1u;
|
|
484
474
|
|
|
485
|
-
|
|
486
|
-
walkSteps = walkSteps + 1u;
|
|
475
|
+
let survival = max3(throughput);
|
|
487
476
|
|
|
488
|
-
|
|
477
|
+
if (walkSteps >= MAX_WALK || survival <= 0.0) {
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
489
480
|
|
|
490
|
-
|
|
481
|
+
if (survival < 0.2) {
|
|
482
|
+
if (chance > survival) {
|
|
491
483
|
break;
|
|
492
484
|
}
|
|
493
485
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
break;
|
|
497
|
-
}
|
|
486
|
+
throughput = throughput / survival;
|
|
487
|
+
}
|
|
498
488
|
|
|
499
|
-
|
|
500
|
-
|
|
489
|
+
origin = origin + direction * limit;
|
|
490
|
+
direction = sampleHenyeyGreenstein(direction, walkG, turn);
|
|
491
|
+
limit = walkDistance(throughput, sigma, next, &walkOdds);
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
501
494
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
495
|
+
// Every vertex draws the same dimensions in the same order, whatever the path then does with them: from the
|
|
496
|
+
// sequence for the first vertices, from a hash past SEQUENCE_DEPTH bounces.
|
|
497
|
+
let deep = bounce >= SEQUENCE_DEPTH;
|
|
498
|
+
let coverage = pathSample2D(rng, deep);
|
|
499
|
+
let pick = pathSample2D(rng, deep);
|
|
500
|
+
let lightSample = pathSample2D(rng, deep);
|
|
501
|
+
let lobe = pathSample2D(rng, deep);
|
|
502
|
+
let bounceSample = pathSample2D(rng, deep);
|
|
503
|
+
var mediumSample = vec2<f32>(0.0);
|
|
504
|
+
|
|
505
|
+
if (VOLUMES) {
|
|
506
|
+
mediumSample = pathSample2D(rng, deep);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// The walk left the material here, and the surface below is its way out rather than what the material shows.
|
|
510
|
+
var exiting = false;
|
|
507
511
|
|
|
512
|
+
if (SUBSURFACE_WALK && inMedium) {
|
|
508
513
|
// The walk reached the surface before scattering again: the chance of travelling that far, per colour.
|
|
509
514
|
let travelled = exp(-sigma * hit.distance);
|
|
510
515
|
|
|
@@ -546,7 +551,7 @@ fn traceRadiance(
|
|
|
546
551
|
if (escapeMap != NO_TEXTURE) {
|
|
547
552
|
sky = sky + sampleEquirect(escapeMap, direction).rgb * escapeIntensity;
|
|
548
553
|
} else {
|
|
549
|
-
sky = sky + environmentRadiance(direction) * escapeWeight(direction, bsdfPdf, lightsOwnDirect);
|
|
554
|
+
sky = sky + environmentRadiance(direction) * escapeWeight(direction, bsdfPdf, lightsOwnDirect, lastPosition);
|
|
550
555
|
}
|
|
551
556
|
|
|
552
557
|
let contribution = throughput * sky;
|
|
@@ -631,8 +636,16 @@ fn traceRadiance(
|
|
|
631
636
|
|
|
632
637
|
surface = resolveSurfaceFrom(hit.triangle, bary, direction, base);
|
|
633
638
|
|
|
639
|
+
// Deep in the path, light reflects off a material that scatters inside as off a diffuse one of the same colour.
|
|
640
|
+
if (SUBSURFACE_WALK && bounce >= frame.subsurfaceDepth) {
|
|
641
|
+
surface.subsurface = 0.0;
|
|
642
|
+
}
|
|
643
|
+
|
|
634
644
|
if (BEVEL && frame.bevelEnabled != 0u && materials[m].bevel > 0.0) {
|
|
635
|
-
|
|
645
|
+
let disk = hashSample2D(rng);
|
|
646
|
+
let axis = hashSample2D(rng);
|
|
647
|
+
|
|
648
|
+
surface.normal = bevelNormal(m, point, surface, -direction, materials[m].bevel, disk, axis);
|
|
636
649
|
}
|
|
637
650
|
}
|
|
638
651
|
|
|
@@ -693,8 +706,14 @@ fn traceRadiance(
|
|
|
693
706
|
depth = length(point - rayOrigin);
|
|
694
707
|
}
|
|
695
708
|
|
|
709
|
+
// A window the camera ray goes straight through is not what the camera sees: what lies behind it is, so the
|
|
710
|
+
// denoiser takes its guides there, and the air its distance. Glass has next to no diffuse colour and one flat
|
|
711
|
+
// normal, which would leave everything seen through it unfiltered. The window's own guides stand until then, for
|
|
712
|
+
// the paths that reflect off it instead.
|
|
713
|
+
let window = surface.transmission > 0.0 && surface.thickness <= 0.0;
|
|
714
|
+
|
|
696
715
|
if (straight && !guided) {
|
|
697
|
-
guided =
|
|
716
|
+
guided = !window;
|
|
698
717
|
diffuseAlbedo = diffuseGuide(surface);
|
|
699
718
|
specularAlbedoGuide = select(specularAlbedo(surface, -direction), vec3<f32>(1.0), (surface.flags & MATERIAL_LIT) == 0u);
|
|
700
719
|
guideNormal = surface.normal;
|
|
@@ -735,7 +754,7 @@ fn traceRadiance(
|
|
|
735
754
|
}
|
|
736
755
|
}
|
|
737
756
|
|
|
738
|
-
if (!hazed && frame.atmosphereColor.w > 0.0) {
|
|
757
|
+
if (!hazed && !window && frame.atmosphereColor.w > 0.0) {
|
|
739
758
|
hazed = true;
|
|
740
759
|
// From the eye, where the camera ray only starts on the near plane; an orthographic view has no eye.
|
|
741
760
|
hazeDistance = length(point - select(frame.cameraToWorld[3].xyz, rayOrigin, frame.cameraKind == 1u));
|
|
@@ -755,7 +774,11 @@ fn traceRadiance(
|
|
|
755
774
|
}
|
|
756
775
|
|
|
757
776
|
// Three does not light a basic, points, line or sprite material, so nothing carries on from one.
|
|
758
|
-
|
|
777
|
+
// Past the last bounce a path still crosses a window straight on, which scatters nothing, to find the sky behind
|
|
778
|
+
// it: cut there, the daylight a room gets through its glazing would go missing from every last bounce.
|
|
779
|
+
let spent = bounce >= frame.bounces;
|
|
780
|
+
|
|
781
|
+
if ((surface.flags & MATERIAL_LIT) == 0u || (spent && !window)) {
|
|
759
782
|
break;
|
|
760
783
|
}
|
|
761
784
|
|
|
@@ -778,8 +801,105 @@ fn traceRadiance(
|
|
|
778
801
|
var nextOrigin = point + direction * RAY_EPSILON;
|
|
779
802
|
|
|
780
803
|
// Transmission replaces part of the diffuse layer, so that share of the time the surface is a dielectric instead.
|
|
781
|
-
// Choosing it in proportion to its weight leaves both halves unweighted
|
|
782
|
-
|
|
804
|
+
// Choosing it in proportion to its weight leaves both halves unweighted. The opaque half aims at a light; on a
|
|
805
|
+
// window that lets only part of the light through, it does so on every path, weighed by its share, rather than on
|
|
806
|
+
// the paths that chose it: a sunlit pane seen against a dim room would otherwise be lit on one path in five and
|
|
807
|
+
// dark on the rest, which is the grain glass shows.
|
|
808
|
+
let dielectric = surface.transmission > 0.0 && lobe.y < surface.transmission;
|
|
809
|
+
let windowLight = window && !spent && surface.transmission < 1.0 && surface.subsurface <= 0.0;
|
|
810
|
+
|
|
811
|
+
if (!dielectric || windowLight) {
|
|
812
|
+
var chance = 0.0;
|
|
813
|
+
|
|
814
|
+
if (!dielectric) {
|
|
815
|
+
// Light that enters the material: a random walk inside it from here, in place of the share of the diffuse
|
|
816
|
+
// layer that scatters inside. Chosen by chance, so neither branch is weighted against the other.
|
|
817
|
+
// The window's opaque share is a surface like any other, which a spent path stops at.
|
|
818
|
+
if (spent) {
|
|
819
|
+
break;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (SUBSURFACE_WALK) {
|
|
823
|
+
chance = subsurfaceChance(surface, view);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (SUBSURFACE_WALK && lobe.x < chance) {
|
|
827
|
+
let medium = materials[surface.material].subsurface;
|
|
828
|
+
|
|
829
|
+
throughput = throughput
|
|
830
|
+
* ((1.0 - surface.metalness) * surface.subsurface * coatTransmittance(surface, view) / chance);
|
|
831
|
+
sigma = vec3<f32>(1.0) / max(medium.rgb, vec3<f32>(1e-6));
|
|
832
|
+
walkAlbedo = subsurfaceAlbedo(surface.albedo);
|
|
833
|
+
walkG = materials[surface.material].subsurfaceAnisotropy;
|
|
834
|
+
walkSteps = 0u;
|
|
835
|
+
|
|
836
|
+
// Light that scattered inside is diffuse light, which is the signal it feeds.
|
|
837
|
+
if (!split) {
|
|
838
|
+
split = true;
|
|
839
|
+
splitBase = radiance;
|
|
840
|
+
splitDiffuse = true;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
inMedium = true;
|
|
844
|
+
primary = false;
|
|
845
|
+
straight = false;
|
|
846
|
+
bsdfPdf = 0.0;
|
|
847
|
+
lightsOwnDirect = false;
|
|
848
|
+
escapeMap = NO_TEXTURE;
|
|
849
|
+
bounce = bounce + 1u;
|
|
850
|
+
direction = cosineDirection(-surface.facing, bounceSample);
|
|
851
|
+
origin = point - surface.facing * RAY_EPSILON;
|
|
852
|
+
limit = walkDistance(throughput, sigma, pick, &walkOdds);
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
throughput = throughput / (1.0 - chance);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
let position = point + surface.normal * RAY_EPSILON;
|
|
860
|
+
var directDiffuse: vec3<f32>;
|
|
861
|
+
let lit = select(throughput, throughput * (1.0 - surface.transmission), windowLight);
|
|
862
|
+
let direct = lit * directLight(surface, position, view, pick.x, lightSample, rng, &directDiffuse,
|
|
863
|
+
select(0.0, volumeDensity, inVolume));
|
|
864
|
+
let added = select(direct, clampIndirect(direct), bounce > 0u);
|
|
865
|
+
let before = radiance;
|
|
866
|
+
|
|
867
|
+
radiance = radiance + added;
|
|
868
|
+
|
|
869
|
+
if (!dielectric) {
|
|
870
|
+
let scattered = sampleBsdf(surface, view, (lobe.x - chance) / (1.0 - chance), bounceSample);
|
|
871
|
+
|
|
872
|
+
// The first scatter splits the path: its direct light by lobe, and everything after it by the lobe it chose.
|
|
873
|
+
if (!split) {
|
|
874
|
+
split = true;
|
|
875
|
+
splitBase = before;
|
|
876
|
+
splitDirect = added;
|
|
877
|
+
splitDirectDiffuse = lit * directDiffuse * select(1.0, luminanceOf(added) / max(luminanceOf(direct), 1e-20),
|
|
878
|
+
bounce > 0u);
|
|
879
|
+
splitDiffuse = !scattered.valid || scattered.diffuse;
|
|
880
|
+
waitingForSpecular = scattered.valid && !scattered.diffuse;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (!scattered.valid) {
|
|
884
|
+
break;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
throughput = throughput * scattered.weight;
|
|
888
|
+
bsdfPdf = scattered.pdf;
|
|
889
|
+
lastPosition = position;
|
|
890
|
+
lastNormal = treeNormal(surface);
|
|
891
|
+
lightsOwnDirect = surface.kind == KIND_TOON;
|
|
892
|
+
escapeMap = surface.envMap;
|
|
893
|
+
escapeIntensity = surface.envMapIntensity;
|
|
894
|
+
next = scattered.direction;
|
|
895
|
+
nextOrigin = position;
|
|
896
|
+
primary = false;
|
|
897
|
+
straight = false;
|
|
898
|
+
bounce = bounce + 1u;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (dielectric) {
|
|
783
903
|
var ior = surface.ior;
|
|
784
904
|
|
|
785
905
|
// Three's dispersion: each colour refracts with its own index, spread around the material's. The path picks one
|
|
@@ -799,7 +919,8 @@ fn traceRadiance(
|
|
|
799
919
|
|
|
800
920
|
let crossing = sampleDielectric(surface, view, ior, lobe.y / surface.transmission, bounceSample);
|
|
801
921
|
|
|
802
|
-
|
|
922
|
+
// A reflection off the glass is a scattering, which a spent path no longer has.
|
|
923
|
+
if (!crossing.valid || (spent && !crossing.straight)) {
|
|
803
924
|
break;
|
|
804
925
|
}
|
|
805
926
|
|
|
@@ -834,85 +955,6 @@ fn traceRadiance(
|
|
|
834
955
|
if (!primary) {
|
|
835
956
|
bounce = bounce + 1u;
|
|
836
957
|
}
|
|
837
|
-
} else {
|
|
838
|
-
// Light that enters the material: a random walk inside it from here, in place of the share of the diffuse
|
|
839
|
-
// layer that scatters inside. Chosen by chance, so neither branch is weighted against the other.
|
|
840
|
-
var chance = 0.0;
|
|
841
|
-
|
|
842
|
-
if (SUBSURFACE_WALK) {
|
|
843
|
-
chance = subsurfaceChance(surface, view);
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
if (SUBSURFACE_WALK && lobe.x < chance) {
|
|
847
|
-
let medium = materials[surface.material].subsurface;
|
|
848
|
-
|
|
849
|
-
throughput = throughput
|
|
850
|
-
* ((1.0 - surface.metalness) * surface.subsurface * coatTransmittance(surface, view) / chance);
|
|
851
|
-
sigma = vec3<f32>(1.0) / max(medium.rgb, vec3<f32>(1e-6));
|
|
852
|
-
walkAlbedo = subsurfaceAlbedo(surface.albedo);
|
|
853
|
-
walkG = materials[surface.material].subsurfaceAnisotropy;
|
|
854
|
-
walkSteps = 0u;
|
|
855
|
-
|
|
856
|
-
// Light that scattered inside is diffuse light, which is the signal it feeds.
|
|
857
|
-
if (!split) {
|
|
858
|
-
split = true;
|
|
859
|
-
splitBase = radiance;
|
|
860
|
-
splitDiffuse = true;
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
inMedium = true;
|
|
864
|
-
primary = false;
|
|
865
|
-
straight = false;
|
|
866
|
-
bsdfPdf = 0.0;
|
|
867
|
-
lightsOwnDirect = false;
|
|
868
|
-
escapeMap = NO_TEXTURE;
|
|
869
|
-
bounce = bounce + 1u;
|
|
870
|
-
direction = cosineDirection(-surface.facing, bounceSample);
|
|
871
|
-
origin = point - surface.facing * RAY_EPSILON;
|
|
872
|
-
limit = walkDistance(throughput, sigma, pick, &walkOdds);
|
|
873
|
-
continue;
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
throughput = throughput / (1.0 - chance);
|
|
877
|
-
|
|
878
|
-
let position = point + surface.normal * RAY_EPSILON;
|
|
879
|
-
var directDiffuse: vec3<f32>;
|
|
880
|
-
let direct = throughput * directLight(surface, position, view, pick.x, lightSample, rng, &directDiffuse,
|
|
881
|
-
select(0.0, volumeDensity, inVolume));
|
|
882
|
-
let added = select(direct, clampIndirect(direct), bounce > 0u);
|
|
883
|
-
let before = radiance;
|
|
884
|
-
|
|
885
|
-
radiance = radiance + added;
|
|
886
|
-
|
|
887
|
-
let scattered = sampleBsdf(surface, view, (lobe.x - chance) / (1.0 - chance), bounceSample);
|
|
888
|
-
|
|
889
|
-
// The first scatter splits the path: its direct light by lobe, and everything after it by the lobe it chose.
|
|
890
|
-
if (!split) {
|
|
891
|
-
split = true;
|
|
892
|
-
splitBase = before;
|
|
893
|
-
splitDirect = added;
|
|
894
|
-
splitDirectDiffuse = throughput * directDiffuse * select(1.0, luminanceOf(added) / max(luminanceOf(direct), 1e-20),
|
|
895
|
-
bounce > 0u);
|
|
896
|
-
splitDiffuse = !scattered.valid || scattered.diffuse;
|
|
897
|
-
waitingForSpecular = scattered.valid && !scattered.diffuse;
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
if (!scattered.valid) {
|
|
901
|
-
break;
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
throughput = throughput * scattered.weight;
|
|
905
|
-
bsdfPdf = scattered.pdf;
|
|
906
|
-
lastPosition = position;
|
|
907
|
-
lastNormal = treeNormal(surface);
|
|
908
|
-
lightsOwnDirect = surface.kind == KIND_TOON;
|
|
909
|
-
escapeMap = surface.envMap;
|
|
910
|
-
escapeIntensity = surface.envMapIntensity;
|
|
911
|
-
next = scattered.direction;
|
|
912
|
-
nextOrigin = position;
|
|
913
|
-
primary = false;
|
|
914
|
-
straight = false;
|
|
915
|
-
bounce = bounce + 1u;
|
|
916
958
|
}
|
|
917
959
|
|
|
918
960
|
// A path that carries almost nothing is ended by chance rather than by a cut, which keeps the estimate unbiased.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pathTrace.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/pathTrace.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,UAAU,CAAC
|
|
1
|
+
{"version":3,"file":"pathTrace.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/pathTrace.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAy5CzC,CAAC"}
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
* vignette and grain. The operators are ports of `color.ts` in the CPU renderer, which are in turn Three's ACES, AgX
|
|
4
4
|
* and Khronos neutral curves, so the two renderers agree on the display transform even when they disagree on the light.
|
|
5
5
|
*/
|
|
6
|
-
export declare const PRESENT_WGSL = "\nstruct Present {\n resolution: vec2<u32>,\n exposure: f32,\n toneMapping: u32,\n vignette: f32,\n grain: f32,\n frameIndex: u32,\n // A transparent background: the canvas gets the coverage as alpha, premultiplied as it is configured.\n transparent: u32,\n // Whether the colour comes from the denoiser's output rather than straight from the accumulation.\n denoised: u32,\n};\n\n@group(0) @binding(0) var<uniform> present: Present;\n@group(0) @binding(1) var<storage, read> accumulation: array<vec4<f32>>;\n@group(0) @binding(2) var<storage, read> denoised: array<vec4<f32>>;\n\n@vertex\nfn vertexMain(@builtin(vertex_index) index: u32) -> @builtin(position) vec4<f32> {\n // One oversized triangle covers the viewport with no vertex buffer.\n let x = f32(index / 2u) * 4.0 - 1.0;\n let y = f32(index & 1u) * 4.0 - 1.0;\n\n return vec4<f32>(x, y, 0.0, 1.0);\n}\n\nfn toneMapAces(color: vec3<f32>) -> vec3<f32> {\n let toAces = mat3x3<f32>(\n vec3<f32>(0.59719, 0.076, 0.0284),\n vec3<f32>(0.35458, 0.90834, 0.13383),\n vec3<f32>(0.04823, 0.01566, 0.83777));\n let fromAces = mat3x3<f32>(\n vec3<f32>(1.60475, -0.10208, -0.00327),\n vec3<f32>(-0.53108, 1.10813, -0.07276),\n vec3<f32>(-0.07367, -0.00605, 1.07602));\n let v = toAces * (color / 0.6);\n\n return fromAces * ((v * (v + 0.0245786) - 0.000090537) / (v * (0.983729 * v + 0.432951) + 0.238081));\n}\n\nfn toneMapAgx(color: vec3<f32>) -> vec3<f32> {\n let toAgx = mat3x3<f32>(\n vec3<f32>(0.6274, 0.0691, 0.0164),\n vec3<f32>(0.3293, 0.9195, 0.088),\n vec3<f32>(0.0433, 0.0113, 0.8956));\n let inset = mat3x3<f32>(\n vec3<f32>(0.856627153315983, 0.137318972929847, 0.11189821299995),\n vec3<f32>(0.0951212405381588, 0.761241990602591, 0.0767994186031903),\n vec3<f32>(0.0482516061458583, 0.101439036467562, 0.811302368396859));\n let outset = mat3x3<f32>(\n vec3<f32>(1.1271005818144368, -0.1413297634984383, -0.14132976349843826),\n vec3<f32>(-0.11060664309660323, 1.157823702216272, -0.11060664309660294),\n vec3<f32>(-0.016493938717834573, -0.016493938717834257, 1.2519364065950405));\n let fromAgx = mat3x3<f32>(\n vec3<f32>(1.6605, -0.1246, -0.0182),\n vec3<f32>(-0.5876, 1.1329, -0.1006),\n vec3<f32>(-0.0728, -0.0083, 1.1187));\n let logged = inset * (toAgx * color);\n let x = clamp((log2(max(logged, vec3<f32>(1e-10))) + 12.47393) / 16.499999, vec3<f32>(0.0), vec3<f32>(1.0));\n let
|
|
6
|
+
export declare const PRESENT_WGSL = "\nstruct Present {\n resolution: vec2<u32>,\n exposure: f32,\n toneMapping: u32,\n vignette: f32,\n grain: f32,\n frameIndex: u32,\n // A transparent background: the canvas gets the coverage as alpha, premultiplied as it is configured.\n transparent: u32,\n // Whether the colour comes from the denoiser's output rather than straight from the accumulation.\n denoised: u32,\n // How much of AgX's Punchy look goes over its base curve.\n contrast: f32,\n};\n\n@group(0) @binding(0) var<uniform> present: Present;\n@group(0) @binding(1) var<storage, read> accumulation: array<vec4<f32>>;\n@group(0) @binding(2) var<storage, read> denoised: array<vec4<f32>>;\n\n@vertex\nfn vertexMain(@builtin(vertex_index) index: u32) -> @builtin(position) vec4<f32> {\n // One oversized triangle covers the viewport with no vertex buffer.\n let x = f32(index / 2u) * 4.0 - 1.0;\n let y = f32(index & 1u) * 4.0 - 1.0;\n\n return vec4<f32>(x, y, 0.0, 1.0);\n}\n\nfn toneMapAces(color: vec3<f32>) -> vec3<f32> {\n let toAces = mat3x3<f32>(\n vec3<f32>(0.59719, 0.076, 0.0284),\n vec3<f32>(0.35458, 0.90834, 0.13383),\n vec3<f32>(0.04823, 0.01566, 0.83777));\n let fromAces = mat3x3<f32>(\n vec3<f32>(1.60475, -0.10208, -0.00327),\n vec3<f32>(-0.53108, 1.10813, -0.07276),\n vec3<f32>(-0.07367, -0.00605, 1.07602));\n let v = toAces * (color / 0.6);\n\n return fromAces * ((v * (v + 0.0245786) - 0.000090537) / (v * (0.983729 * v + 0.432951) + 0.238081));\n}\n\nfn toneMapAgx(color: vec3<f32>) -> vec3<f32> {\n let toAgx = mat3x3<f32>(\n vec3<f32>(0.6274, 0.0691, 0.0164),\n vec3<f32>(0.3293, 0.9195, 0.088),\n vec3<f32>(0.0433, 0.0113, 0.8956));\n let inset = mat3x3<f32>(\n vec3<f32>(0.856627153315983, 0.137318972929847, 0.11189821299995),\n vec3<f32>(0.0951212405381588, 0.761241990602591, 0.0767994186031903),\n vec3<f32>(0.0482516061458583, 0.101439036467562, 0.811302368396859));\n let outset = mat3x3<f32>(\n vec3<f32>(1.1271005818144368, -0.1413297634984383, -0.14132976349843826),\n vec3<f32>(-0.11060664309660323, 1.157823702216272, -0.11060664309660294),\n vec3<f32>(-0.016493938717834573, -0.016493938717834257, 1.2519364065950405));\n let fromAgx = mat3x3<f32>(\n vec3<f32>(1.6605, -0.1246, -0.0182),\n vec3<f32>(-0.5876, 1.1329, -0.1006),\n vec3<f32>(-0.0728, -0.0083, 1.1187));\n let logged = inset * (toAgx * color);\n let x = clamp((log2(max(logged, vec3<f32>(1e-10))) + 12.47393) / 16.499999, vec3<f32>(0.0), vec3<f32>(1.0));\n let base = 15.5 * pow(x, vec3<f32>(6.0)) - 40.14 * pow(x, vec3<f32>(5.0)) + 31.96 * pow(x, vec3<f32>(4.0))\n - 6.868 * pow(x, vec3<f32>(3.0)) + 0.4298 * x * x + 0.1191 * x - 0.00232;\n // A look in the spirit of Blender's Punchy, a power of 1.35 and more saturation, but pivoted on middle grey: Punchy\n // raises the power around zero, which takes middle grey from half the display down to a third and darkens the whole\n // image. Here the highlights rise and the shadows fall while grey, which the automatic exposure aims at, stands.\n let pivot = 0.49673;\n let contrasted = pivot * pow(max(base, vec3<f32>(0.0)) / pivot, vec3<f32>(1.35));\n let luma = dot(contrasted, vec3<f32>(0.2126, 0.7152, 0.0722));\n let punchy = luma + 1.2 * (contrasted - luma);\n let curved = mix(base, punchy, clamp(present.contrast, 0.0, 1.0));\n\n return fromAgx * pow(max(outset * curved, vec3<f32>(0.0)), vec3<f32>(2.2));\n}\n\nfn toneMapNeutral(color: vec3<f32>) -> vec3<f32> {\n let low = min(color.r, min(color.g, color.b));\n let offset = select(0.04, low - 6.25 * low * low, low < 0.08);\n let shifted = color - offset;\n let peak = max(shifted.r, max(shifted.g, shifted.b));\n\n if (peak < 0.76) {\n return shifted;\n }\n\n let newPeak = 1.0 - 0.24 * 0.24 / (peak + 0.24 - 0.76);\n let g = 1.0 - 1.0 / (0.15 * (peak - newPeak) + 1.0);\n\n return shifted * (newPeak / peak) * (1.0 - g) + newPeak * g;\n}\n\nfn toneMap(color: vec3<f32>) -> vec3<f32> {\n let exposed = max(color * present.exposure, vec3<f32>(0.0));\n var mapped = exposed;\n\n if (present.toneMapping == 0u) {\n mapped = toneMapAces(exposed);\n } else if (present.toneMapping == 1u) {\n mapped = toneMapAgx(exposed);\n } else if (present.toneMapping == 2u) {\n mapped = toneMapNeutral(exposed);\n }\n\n return clamp(mapped, vec3<f32>(0.0), vec3<f32>(1.0));\n}\n\nfn linearToSrgb(color: vec3<f32>) -> vec3<f32> {\n let low = color * 12.92;\n let high = 1.055 * pow(max(color, vec3<f32>(0.0)), vec3<f32>(1.0 / 2.4)) - 0.055;\n\n return select(high, low, color <= vec3<f32>(0.0031308));\n}\n\nfn hash(value: u32) -> f32 {\n var state = value * 747796405u + 2891336453u;\n state = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;\n\n return f32((state >> 22u) ^ state) * 2.3283064365386963e-10;\n}\n\n@fragment\nfn fragmentMain(@builtin(position) position: vec4<f32>) -> @location(0) vec4<f32> {\n let pixel = vec2<u32>(position.xy);\n let index = pixel.y * present.resolution.x + pixel.x;\n let stored = accumulation[index];\n\n // Nothing has been traced into this pixel yet: it stays see-through, so whatever is under the canvas shows.\n if (stored.w <= 0.0) {\n return vec4<f32>(0.0);\n }\n\n var color = select(stored.rgb / stored.w, vec3<f32>(0.0), stored.w <= 0.0);\n\n if (present.denoised != 0u) {\n color = denoised[index].rgb;\n }\n var alpha = 1.0;\n\n if (present.transparent != 0u) {\n let coverage = accumulation[present.resolution.x * present.resolution.y + index].x;\n alpha = select(clamp(coverage / stored.w, 0.0, 1.0), 0.0, stored.w <= 0.0);\n // The radiance is premultiplied by the coverage; the tone curve applies to the colour itself.\n color = select(color / alpha, vec3<f32>(0.0), alpha <= 0.0);\n }\n\n let centred = position.xy / vec2<f32>(present.resolution) - 0.5;\n let falloff = 1.0 - present.vignette * dot(centred, centred) * 2.0;\n var output = linearToSrgb(toneMap(color * max(falloff, 0.0)));\n\n if (present.grain > 0.0) {\n let noise = hash(index * 9781u + present.frameIndex * 6151u) - 0.5;\n output = clamp(output + noise * present.grain * 0.1, vec3<f32>(0.0), vec3<f32>(1.0));\n }\n\n return vec4<f32>(output * alpha, alpha);\n}\n";
|
|
7
7
|
//# sourceMappingURL=present.wgsl.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"present.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/present.wgsl.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,eAAO,MAAM,YAAY,
|
|
1
|
+
{"version":3,"file":"present.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/present.wgsl.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,eAAO,MAAM,YAAY,ulMA6JxB,CAAC"}
|
|
@@ -15,6 +15,8 @@ struct Present {
|
|
|
15
15
|
transparent: u32,
|
|
16
16
|
// Whether the colour comes from the denoiser's output rather than straight from the accumulation.
|
|
17
17
|
denoised: u32,
|
|
18
|
+
// How much of AgX's Punchy look goes over its base curve.
|
|
19
|
+
contrast: f32,
|
|
18
20
|
};
|
|
19
21
|
|
|
20
22
|
@group(0) @binding(0) var<uniform> present: Present;
|
|
@@ -63,8 +65,16 @@ fn toneMapAgx(color: vec3<f32>) -> vec3<f32> {
|
|
|
63
65
|
vec3<f32>(-0.0728, -0.0083, 1.1187));
|
|
64
66
|
let logged = inset * (toAgx * color);
|
|
65
67
|
let x = clamp((log2(max(logged, vec3<f32>(1e-10))) + 12.47393) / 16.499999, vec3<f32>(0.0), vec3<f32>(1.0));
|
|
66
|
-
let
|
|
68
|
+
let base = 15.5 * pow(x, vec3<f32>(6.0)) - 40.14 * pow(x, vec3<f32>(5.0)) + 31.96 * pow(x, vec3<f32>(4.0))
|
|
67
69
|
- 6.868 * pow(x, vec3<f32>(3.0)) + 0.4298 * x * x + 0.1191 * x - 0.00232;
|
|
70
|
+
// A look in the spirit of Blender's Punchy, a power of 1.35 and more saturation, but pivoted on middle grey: Punchy
|
|
71
|
+
// raises the power around zero, which takes middle grey from half the display down to a third and darkens the whole
|
|
72
|
+
// image. Here the highlights rise and the shadows fall while grey, which the automatic exposure aims at, stands.
|
|
73
|
+
let pivot = 0.49673;
|
|
74
|
+
let contrasted = pivot * pow(max(base, vec3<f32>(0.0)) / pivot, vec3<f32>(1.35));
|
|
75
|
+
let luma = dot(contrasted, vec3<f32>(0.2126, 0.7152, 0.0722));
|
|
76
|
+
let punchy = luma + 1.2 * (contrasted - luma);
|
|
77
|
+
let curved = mix(base, punchy, clamp(present.contrast, 0.0, 1.0));
|
|
68
78
|
|
|
69
79
|
return fromAgx * pow(max(outset * curved, vec3<f32>(0.0)), vec3<f32>(2.2));
|
|
70
80
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"present.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/present.wgsl.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,UAAU,CAAC
|
|
1
|
+
{"version":3,"file":"present.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/present.wgsl.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6JtC,CAAC"}
|
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
* random numbers. Only the sample index, the pixel and a dimension counter feed the generator, so a pixel draws the same
|
|
6
6
|
* numbers whichever dispatch happens to cover it and the image does not depend on the tiling.
|
|
7
7
|
*/
|
|
8
|
-
export declare const RANDOM_WGSL = "\nfn pcgHash(value: u32) -> u32 {\n let state = value * 747796405u + 2891336453u;\n let word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;\n\n return (word >> 22u) ^ word;\n}\n\nstruct PathSampler {\n seed: u32,\n index: u32,\n dimension: u32,\n};\n\nfn makeSampler(pixel: vec2<u32>, index: u32) -> PathSampler {\n var rng: PathSampler;\n rng.seed = pcgHash(pixel.x * 1973u + pixel.y * 9277u + 1u);\n rng.index = index;\n rng.dimension = 0u;\n\n return rng;\n}\n\n/**\n * The Laine-Karras hash, which permutes the high bits in a way that nests: what Owen scrambling asks for. Every\n * multiplier has to be even, or a bit stops depending on itself and the map is no longer a permutation.\n */\nfn laineKarras(value: u32, seed: u32) -> u32 {\n var x = value + seed;\n x = x ^ (x * 0x6c50b47cu);\n x = x ^ (x * 0xb82f1e52u);\n x = x ^ (x * 0xc7afe784u);\n x = x ^ (x * 0x8d22f6e6u);\n\n return x;\n}\n\nfn nestedScramble(value: u32, seed: u32) -> u32 {\n return reverseBits(laineKarras(reverseBits(value), seed));\n}\n\n/**\n * The first two Sobol dimensions, as fixed point fractions. The second is the index times Pascal's triangle mod 2,\n * which is linear in the bits of the index, so it is read one byte at a time from the tables texturePack.ts writes\n * instead of folded in one bit at a time: four reads where the scrambled index made it thirty two steps.\n */\nfn sobol2(index: u32) -> vec2<u32> {\n let second = textures[SOBOL_TABLE + (index & 0xffu)]\n ^ textures[SOBOL_TABLE + 256u + ((index >> 8u) & 0xffu)]\n ^ textures[SOBOL_TABLE + 512u + ((index >> 16u) & 0xffu)]\n ^ textures[SOBOL_TABLE + 768u + (index >> 24u)];\n\n return vec2<u32>(reverseBits(index), second);\n}\n\nfn toUnit(value: u32) -> f32 {\n return min(f32(value) * 2.3283064365386963e-10, 0.99999994);\n}\n\n/**\n * The next pair of dimensions, in [0, 1). The sample index is shuffled per pair as well, which pads the two dimensional\n * sequence out to as many dimensions as a path needs without correlating them.\n */\nfn sample2D(rng: ptr<function, PathSampler>) -> vec2<f32> {\n let pair = (*rng).seed + (*rng).dimension * 2654435761u;\n (*rng).dimension = (*rng).dimension + 1u;\n\n let shuffled = nestedScramble((*rng).index, pcgHash(pair));\n let point = sobol2(shuffled);\n\n return vec2<f32>(\n toUnit(nestedScramble(point.x, pcgHash(pair + 0x9e3779b9u))),\n toUnit(nestedScramble(point.y, pcgHash(pair + 0x3c6ef372u))));\n}\n";
|
|
8
|
+
export declare const RANDOM_WGSL = "\nfn pcgHash(value: u32) -> u32 {\n let state = value * 747796405u + 2891336453u;\n let word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;\n\n return (word >> 22u) ^ word;\n}\n\nstruct PathSampler {\n seed: u32,\n index: u32,\n dimension: u32,\n};\n\nfn makeSampler(pixel: vec2<u32>, index: u32) -> PathSampler {\n var rng: PathSampler;\n rng.seed = pcgHash(pixel.x * 1973u + pixel.y * 9277u + 1u);\n rng.index = index;\n rng.dimension = 0u;\n\n return rng;\n}\n\n/**\n * The Laine-Karras hash, which permutes the high bits in a way that nests: what Owen scrambling asks for. Every\n * multiplier has to be even, or a bit stops depending on itself and the map is no longer a permutation.\n */\nfn laineKarras(value: u32, seed: u32) -> u32 {\n var x = value + seed;\n x = x ^ (x * 0x6c50b47cu);\n x = x ^ (x * 0xb82f1e52u);\n x = x ^ (x * 0xc7afe784u);\n x = x ^ (x * 0x8d22f6e6u);\n\n return x;\n}\n\nfn nestedScramble(value: u32, seed: u32) -> u32 {\n return reverseBits(laineKarras(reverseBits(value), seed));\n}\n\n/**\n * The first two Sobol dimensions, as fixed point fractions. The second is the index times Pascal's triangle mod 2,\n * which is linear in the bits of the index, so it is read one byte at a time from the tables texturePack.ts writes\n * instead of folded in one bit at a time: four reads where the scrambled index made it thirty two steps.\n */\nfn sobol2(index: u32) -> vec2<u32> {\n let second = textures[SOBOL_TABLE + (index & 0xffu)]\n ^ textures[SOBOL_TABLE + 256u + ((index >> 8u) & 0xffu)]\n ^ textures[SOBOL_TABLE + 512u + ((index >> 16u) & 0xffu)]\n ^ textures[SOBOL_TABLE + 768u + (index >> 24u)];\n\n return vec2<u32>(reverseBits(index), second);\n}\n\nfn toUnit(value: u32) -> f32 {\n return min(f32(value) * 2.3283064365386963e-10, 0.99999994);\n}\n\n/**\n * The next pair of dimensions from a plain hash instead of the scrambled sequence: for the deep dimensions of a random\n * walk, where stratification buys nothing and the sequence's table reads and scrambles cost a step more than its ray.\n * It takes a dimension of its own, so the sequence's draws after it stay where they would have been.\n */\nfn hashSample2D(rng: ptr<function, PathSampler>) -> vec2<f32> {\n let first = pcgHash((*rng).seed ^ pcgHash((*rng).index * 2654435761u + (*rng).dimension * 0x9e3779b9u));\n (*rng).dimension = (*rng).dimension + 1u;\n\n return vec2<f32>(toUnit(first), toUnit(pcgHash(first)));\n}\n\n/**\n * How many vertices of a path draw from the scrambled sequence. Its stratification shows on the camera ray and the\n * first bounces, where the light a pixel sees is decided; past them the light has been averaged by every bounce\n * before, and a plain hash gives numbers as good for a fraction of the cost.\n */\nconst SEQUENCE_DEPTH: u32 = 3u;\n\n/** The next pair of dimensions of a path's vertex: from the sequence while the path is shallow, from a hash after. */\nfn pathSample2D(rng: ptr<function, PathSampler>, deep: bool) -> vec2<f32> {\n if (deep) {\n return hashSample2D(rng);\n }\n\n return sample2D(rng);\n}\n\n/**\n * The next pair of dimensions, in [0, 1). The sample index is shuffled per pair as well, which pads the two dimensional\n * sequence out to as many dimensions as a path needs without correlating them.\n */\nfn sample2D(rng: ptr<function, PathSampler>) -> vec2<f32> {\n let pair = (*rng).seed + (*rng).dimension * 2654435761u;\n (*rng).dimension = (*rng).dimension + 1u;\n\n let shuffled = nestedScramble((*rng).index, pcgHash(pair));\n let point = sobol2(shuffled);\n\n return vec2<f32>(\n toUnit(nestedScramble(point.x, pcgHash(pair + 0x9e3779b9u))),\n toUnit(nestedScramble(point.y, pcgHash(pair + 0x3c6ef372u))));\n}\n";
|
|
9
9
|
//# sourceMappingURL=random.wgsl.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"random.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/random.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,
|
|
1
|
+
{"version":3,"file":"random.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/random.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,mqHAsGvB,CAAC"}
|
package/dist/wgsl/random.wgsl.js
CHANGED
|
@@ -64,6 +64,34 @@ fn toUnit(value: u32) -> f32 {
|
|
|
64
64
|
return min(f32(value) * 2.3283064365386963e-10, 0.99999994);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* The next pair of dimensions from a plain hash instead of the scrambled sequence: for the deep dimensions of a random
|
|
69
|
+
* walk, where stratification buys nothing and the sequence's table reads and scrambles cost a step more than its ray.
|
|
70
|
+
* It takes a dimension of its own, so the sequence's draws after it stay where they would have been.
|
|
71
|
+
*/
|
|
72
|
+
fn hashSample2D(rng: ptr<function, PathSampler>) -> vec2<f32> {
|
|
73
|
+
let first = pcgHash((*rng).seed ^ pcgHash((*rng).index * 2654435761u + (*rng).dimension * 0x9e3779b9u));
|
|
74
|
+
(*rng).dimension = (*rng).dimension + 1u;
|
|
75
|
+
|
|
76
|
+
return vec2<f32>(toUnit(first), toUnit(pcgHash(first)));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* How many vertices of a path draw from the scrambled sequence. Its stratification shows on the camera ray and the
|
|
81
|
+
* first bounces, where the light a pixel sees is decided; past them the light has been averaged by every bounce
|
|
82
|
+
* before, and a plain hash gives numbers as good for a fraction of the cost.
|
|
83
|
+
*/
|
|
84
|
+
const SEQUENCE_DEPTH: u32 = 3u;
|
|
85
|
+
|
|
86
|
+
/** The next pair of dimensions of a path's vertex: from the sequence while the path is shallow, from a hash after. */
|
|
87
|
+
fn pathSample2D(rng: ptr<function, PathSampler>, deep: bool) -> vec2<f32> {
|
|
88
|
+
if (deep) {
|
|
89
|
+
return hashSample2D(rng);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return sample2D(rng);
|
|
93
|
+
}
|
|
94
|
+
|
|
67
95
|
/**
|
|
68
96
|
* The next pair of dimensions, in [0, 1). The sample index is shuffled per pair as well, which pads the two dimensional
|
|
69
97
|
* sequence out to as many dimensions as a path needs without correlating them.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"random.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/random.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,UAAU,CAAC
|
|
1
|
+
{"version":3,"file":"random.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/random.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsGrC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tinypoly/gpu-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "WebGPU path tracer for Three.js scenes: BVH traversal, progressive accumulation and tone mapping in WGSL compute shaders.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"three",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"!dist/**/*.test.*",
|
|
39
39
|
"src",
|
|
40
40
|
"!src/**/*.test.ts",
|
|
41
|
+
"weights",
|
|
41
42
|
"ARCHITECTURE.md"
|
|
42
43
|
],
|
|
43
44
|
"engines": {
|
|
@@ -45,7 +46,8 @@
|
|
|
45
46
|
},
|
|
46
47
|
"dependencies": {
|
|
47
48
|
"@tinypoly/cpu-renderer": "^0.3.1",
|
|
48
|
-
"naga-wasm": "30.2.0"
|
|
49
|
+
"naga-wasm": "30.2.0",
|
|
50
|
+
"oidn-webgpu": "0.6.0"
|
|
49
51
|
},
|
|
50
52
|
"peerDependencies": {
|
|
51
53
|
"three": ">=0.185.0"
|
package/src/denoise.ts
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import { createShaderModule } from "./shaderModule.js";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Which denoiser filters the image: `"oidn"`, Intel's Open Image Denoise, a neural network that keeps texture and fine
|
|
5
|
+
* detail, or `"svgf"`, the edge-aware wavelet filter, faster and with the controls below.
|
|
6
|
+
*/
|
|
7
|
+
export type DenoiserAlgorithm = "oidn" | "svgf";
|
|
8
|
+
|
|
3
9
|
/** How the denoiser filters. Every field is optional where it is taken, and falls back to `DEFAULT_DENOISER`. */
|
|
4
10
|
export interface DenoiserSettings {
|
|
11
|
+
/** The denoiser that runs; OIDN falls back to SVGF where its network cannot load. */
|
|
12
|
+
algorithm: DenoiserAlgorithm;
|
|
13
|
+
/** Where OIDN's weights are fetched from; empty takes the ones shipped with the package. */
|
|
14
|
+
oidnWeights: string;
|
|
5
15
|
/** How much of the filtered image replaces the traced one, from 0 (none) to 1. */
|
|
6
16
|
strength: number;
|
|
7
|
-
/**
|
|
17
|
+
/** SVGF only from here: wavelet passes, 1 to 5, how far the filter reaches, from 2 to 62 pixels. */
|
|
8
18
|
passes: number;
|
|
9
19
|
/**
|
|
10
20
|
* How large a luminance difference between two pixels still counts as noise, in standard deviations of the pixel's
|
|
@@ -28,6 +38,8 @@ export interface DenoiserSettings {
|
|
|
28
38
|
}
|
|
29
39
|
|
|
30
40
|
export const DEFAULT_DENOISER: Readonly<DenoiserSettings> = Object.freeze({
|
|
41
|
+
algorithm: "oidn",
|
|
42
|
+
oidnWeights: "",
|
|
31
43
|
strength: 1,
|
|
32
44
|
passes: 3,
|
|
33
45
|
edge: 2,
|
|
@@ -42,6 +54,8 @@ export function mergeDenoiser(base: DenoiserSettings, input: Partial<DenoiserSet
|
|
|
42
54
|
const merged = { ...base, ...Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined)) };
|
|
43
55
|
|
|
44
56
|
return {
|
|
57
|
+
algorithm: merged.algorithm === "svgf" ? "svgf" : "oidn",
|
|
58
|
+
oidnWeights: typeof merged.oidnWeights === "string" ? merged.oidnWeights : "",
|
|
45
59
|
strength: Math.min(1, Math.max(0, Number(merged.strength) || 0)),
|
|
46
60
|
passes: Math.min(5, Math.max(1, Math.round(Number(merged.passes) || 1))),
|
|
47
61
|
edge: Math.max(0.01, Number(merged.edge) || DEFAULT_DENOISER.edge),
|
package/src/device.ts
CHANGED
|
@@ -63,10 +63,19 @@ export async function requestRenderDevice(options: RenderDeviceOptions = {}): Pr
|
|
|
63
63
|
if (!adapter)
|
|
64
64
|
throw new Error("GPU renderer: no WebGPU adapter was available.");
|
|
65
65
|
|
|
66
|
+
// Half floats and the adapter's widest workgroups are what the OIDN denoiser runs fastest with; the path tracer
|
|
67
|
+
// needs neither, and an adapter without them still gets a device.
|
|
68
|
+
const requiredFeatures: GPUFeatureName[] = adapter.features.has("shader-f16") ? ["shader-f16"] : [];
|
|
69
|
+
|
|
66
70
|
const device = await adapter.requestDevice({
|
|
71
|
+
requiredFeatures,
|
|
67
72
|
requiredLimits: {
|
|
68
73
|
maxStorageBufferBindingSize: adapter.limits.maxStorageBufferBindingSize,
|
|
69
74
|
maxBufferSize: adapter.limits.maxBufferSize,
|
|
75
|
+
maxComputeWorkgroupStorageSize: adapter.limits.maxComputeWorkgroupStorageSize,
|
|
76
|
+
maxComputeWorkgroupsPerDimension: adapter.limits.maxComputeWorkgroupsPerDimension,
|
|
77
|
+
maxComputeWorkgroupSizeX: adapter.limits.maxComputeWorkgroupSizeX,
|
|
78
|
+
maxComputeInvocationsPerWorkgroup: adapter.limits.maxComputeInvocationsPerWorkgroup,
|
|
70
79
|
},
|
|
71
80
|
});
|
|
72
81
|
|