@tinypoly/gpu-renderer 0.1.1 → 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 +11 -3
- 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 +4 -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/lights.d.ts +18 -0
- package/dist/lights.d.ts.map +1 -1
- package/dist/lights.js +45 -0
- 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 +12 -0
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +100 -14
- package/dist/renderer.js.map +1 -1
- package/dist/sceneUpload.d.ts +4 -0
- package/dist/sceneUpload.d.ts.map +1 -1
- package/dist/sceneUpload.js +3 -0
- package/dist/sceneUpload.js.map +1 -1
- package/dist/settings.d.ts +23 -1
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +13 -4
- 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 +108 -1
- 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 +147 -17
- 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 +123 -91
- 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/package.json +4 -2
- package/src/denoise.ts +15 -1
- package/src/device.ts +9 -0
- package/src/frameLayout.ts +4 -1
- package/src/imageResolve.ts +2 -0
- package/src/index.ts +2 -1
- package/src/lensPass.ts +17 -1
- package/src/lights.ts +60 -0
- 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 +118 -15
- package/src/sceneUpload.ts +7 -0
- package/src/settings.ts +32 -4
- package/src/wgsl/lens.wgsl.ts +108 -1
- package/src/wgsl/lights.wgsl.ts +147 -17
- package/src/wgsl/pathTrace.wgsl.ts +123 -91
- package/src/wgsl/present.wgsl.ts +11 -1
- 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. */
|
|
@@ -548,7 +551,7 @@ fn traceRadiance(
|
|
|
548
551
|
if (escapeMap != NO_TEXTURE) {
|
|
549
552
|
sky = sky + sampleEquirect(escapeMap, direction).rgb * escapeIntensity;
|
|
550
553
|
} else {
|
|
551
|
-
sky = sky + environmentRadiance(direction) * escapeWeight(direction, bsdfPdf, lightsOwnDirect);
|
|
554
|
+
sky = sky + environmentRadiance(direction) * escapeWeight(direction, bsdfPdf, lightsOwnDirect, lastPosition);
|
|
552
555
|
}
|
|
553
556
|
|
|
554
557
|
let contribution = throughput * sky;
|
|
@@ -703,8 +706,14 @@ fn traceRadiance(
|
|
|
703
706
|
depth = length(point - rayOrigin);
|
|
704
707
|
}
|
|
705
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
|
+
|
|
706
715
|
if (straight && !guided) {
|
|
707
|
-
guided =
|
|
716
|
+
guided = !window;
|
|
708
717
|
diffuseAlbedo = diffuseGuide(surface);
|
|
709
718
|
specularAlbedoGuide = select(specularAlbedo(surface, -direction), vec3<f32>(1.0), (surface.flags & MATERIAL_LIT) == 0u);
|
|
710
719
|
guideNormal = surface.normal;
|
|
@@ -745,7 +754,7 @@ fn traceRadiance(
|
|
|
745
754
|
}
|
|
746
755
|
}
|
|
747
756
|
|
|
748
|
-
if (!hazed && frame.atmosphereColor.w > 0.0) {
|
|
757
|
+
if (!hazed && !window && frame.atmosphereColor.w > 0.0) {
|
|
749
758
|
hazed = true;
|
|
750
759
|
// From the eye, where the camera ray only starts on the near plane; an orthographic view has no eye.
|
|
751
760
|
hazeDistance = length(point - select(frame.cameraToWorld[3].xyz, rayOrigin, frame.cameraKind == 1u));
|
|
@@ -765,7 +774,11 @@ fn traceRadiance(
|
|
|
765
774
|
}
|
|
766
775
|
|
|
767
776
|
// Three does not light a basic, points, line or sprite material, so nothing carries on from one.
|
|
768
|
-
|
|
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)) {
|
|
769
782
|
break;
|
|
770
783
|
}
|
|
771
784
|
|
|
@@ -788,8 +801,105 @@ fn traceRadiance(
|
|
|
788
801
|
var nextOrigin = point + direction * RAY_EPSILON;
|
|
789
802
|
|
|
790
803
|
// Transmission replaces part of the diffuse layer, so that share of the time the surface is a dielectric instead.
|
|
791
|
-
// Choosing it in proportion to its weight leaves both halves unweighted
|
|
792
|
-
|
|
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) {
|
|
793
903
|
var ior = surface.ior;
|
|
794
904
|
|
|
795
905
|
// Three's dispersion: each colour refracts with its own index, spread around the material's. The path picks one
|
|
@@ -809,7 +919,8 @@ fn traceRadiance(
|
|
|
809
919
|
|
|
810
920
|
let crossing = sampleDielectric(surface, view, ior, lobe.y / surface.transmission, bounceSample);
|
|
811
921
|
|
|
812
|
-
|
|
922
|
+
// A reflection off the glass is a scattering, which a spent path no longer has.
|
|
923
|
+
if (!crossing.valid || (spent && !crossing.straight)) {
|
|
813
924
|
break;
|
|
814
925
|
}
|
|
815
926
|
|
|
@@ -844,85 +955,6 @@ fn traceRadiance(
|
|
|
844
955
|
if (!primary) {
|
|
845
956
|
bounce = bounce + 1u;
|
|
846
957
|
}
|
|
847
|
-
} else {
|
|
848
|
-
// Light that enters the material: a random walk inside it from here, in place of the share of the diffuse
|
|
849
|
-
// layer that scatters inside. Chosen by chance, so neither branch is weighted against the other.
|
|
850
|
-
var chance = 0.0;
|
|
851
|
-
|
|
852
|
-
if (SUBSURFACE_WALK) {
|
|
853
|
-
chance = subsurfaceChance(surface, view);
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
if (SUBSURFACE_WALK && lobe.x < chance) {
|
|
857
|
-
let medium = materials[surface.material].subsurface;
|
|
858
|
-
|
|
859
|
-
throughput = throughput
|
|
860
|
-
* ((1.0 - surface.metalness) * surface.subsurface * coatTransmittance(surface, view) / chance);
|
|
861
|
-
sigma = vec3<f32>(1.0) / max(medium.rgb, vec3<f32>(1e-6));
|
|
862
|
-
walkAlbedo = subsurfaceAlbedo(surface.albedo);
|
|
863
|
-
walkG = materials[surface.material].subsurfaceAnisotropy;
|
|
864
|
-
walkSteps = 0u;
|
|
865
|
-
|
|
866
|
-
// Light that scattered inside is diffuse light, which is the signal it feeds.
|
|
867
|
-
if (!split) {
|
|
868
|
-
split = true;
|
|
869
|
-
splitBase = radiance;
|
|
870
|
-
splitDiffuse = true;
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
inMedium = true;
|
|
874
|
-
primary = false;
|
|
875
|
-
straight = false;
|
|
876
|
-
bsdfPdf = 0.0;
|
|
877
|
-
lightsOwnDirect = false;
|
|
878
|
-
escapeMap = NO_TEXTURE;
|
|
879
|
-
bounce = bounce + 1u;
|
|
880
|
-
direction = cosineDirection(-surface.facing, bounceSample);
|
|
881
|
-
origin = point - surface.facing * RAY_EPSILON;
|
|
882
|
-
limit = walkDistance(throughput, sigma, pick, &walkOdds);
|
|
883
|
-
continue;
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
throughput = throughput / (1.0 - chance);
|
|
887
|
-
|
|
888
|
-
let position = point + surface.normal * RAY_EPSILON;
|
|
889
|
-
var directDiffuse: vec3<f32>;
|
|
890
|
-
let direct = throughput * directLight(surface, position, view, pick.x, lightSample, rng, &directDiffuse,
|
|
891
|
-
select(0.0, volumeDensity, inVolume));
|
|
892
|
-
let added = select(direct, clampIndirect(direct), bounce > 0u);
|
|
893
|
-
let before = radiance;
|
|
894
|
-
|
|
895
|
-
radiance = radiance + added;
|
|
896
|
-
|
|
897
|
-
let scattered = sampleBsdf(surface, view, (lobe.x - chance) / (1.0 - chance), bounceSample);
|
|
898
|
-
|
|
899
|
-
// The first scatter splits the path: its direct light by lobe, and everything after it by the lobe it chose.
|
|
900
|
-
if (!split) {
|
|
901
|
-
split = true;
|
|
902
|
-
splitBase = before;
|
|
903
|
-
splitDirect = added;
|
|
904
|
-
splitDirectDiffuse = throughput * directDiffuse * select(1.0, luminanceOf(added) / max(luminanceOf(direct), 1e-20),
|
|
905
|
-
bounce > 0u);
|
|
906
|
-
splitDiffuse = !scattered.valid || scattered.diffuse;
|
|
907
|
-
waitingForSpecular = scattered.valid && !scattered.diffuse;
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
if (!scattered.valid) {
|
|
911
|
-
break;
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
throughput = throughput * scattered.weight;
|
|
915
|
-
bsdfPdf = scattered.pdf;
|
|
916
|
-
lastPosition = position;
|
|
917
|
-
lastNormal = treeNormal(surface);
|
|
918
|
-
lightsOwnDirect = surface.kind == KIND_TOON;
|
|
919
|
-
escapeMap = surface.envMap;
|
|
920
|
-
escapeIntensity = surface.envMapIntensity;
|
|
921
|
-
next = scattered.direction;
|
|
922
|
-
nextOrigin = position;
|
|
923
|
-
primary = false;
|
|
924
|
-
straight = false;
|
|
925
|
-
bounce = bounce + 1u;
|
|
926
958
|
}
|
|
927
959
|
|
|
928
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"}
|
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
|
|
package/src/frameLayout.ts
CHANGED
|
@@ -35,6 +35,9 @@ export const FRAME_LAYOUT = layoutStruct("Frame", [
|
|
|
35
35
|
{ name: "localLightCount", type: "u32", doc: "How many leading lights sit in the scene, which the light tree picks." },
|
|
36
36
|
{ name: "localLightPick", type: "f32", doc: "The chance of picking one of them, the last one's cumulative probability." },
|
|
37
37
|
{ name: "lightTree", type: "u32", doc: "Where the light tree's nodes start in the texture buffer; zero picks by power alone." },
|
|
38
|
+
{ name: "portals", type: "u32", doc: "Where the tree of the windows the sky is sampled through starts in the texture buffer." },
|
|
39
|
+
{ name: "portalCount", type: "u32", doc: "How many window triangles there are; zero samples the sky by its brightness alone." },
|
|
40
|
+
{ name: "portalList", type: "u32", doc: "Where the windows of that tree's leaves are listed in the texture buffer." },
|
|
38
41
|
{ name: "lightList", type: "u32", doc: "Where the lights of the tree's leaves are listed in the texture buffer." },
|
|
39
42
|
{ name: "lightLeaves", type: "u32", doc: "Where the leaf of every local light starts in the texture buffer." },
|
|
40
43
|
{ name: "clampIndirect", type: "f32", doc: "Maximum luminance one indirect contribution may add; zero leaves it." },
|
|
@@ -55,7 +58,7 @@ export const FRAME_LAYOUT = layoutStruct("Frame", [
|
|
|
55
58
|
{ name: "skyProgram", type: "u32", doc: "A procedural sky's foreground program, or NO_PROGRAM." },
|
|
56
59
|
{ name: "skyData", type: "u32" },
|
|
57
60
|
{ name: "skyBackground", type: "u32", doc: "Whether a procedural sky paints the background (1) or leaves it black (0)." },
|
|
58
|
-
{ name: "skyIntensity", type: "f32", doc: "The strength a procedural sky lights with,
|
|
61
|
+
{ name: "skyIntensity", type: "f32", doc: "The strength a procedural sky lights with, and shows with as the background." },
|
|
59
62
|
{ name: "pixelCount", type: "u32", doc: "Where the alpha half of the accumulation buffer starts." },
|
|
60
63
|
{ name: "volumetricCount", type: "u32", doc: "Lights that scatter in the air, and where their records start." },
|
|
61
64
|
{ name: "volumetricData", type: "u32" },
|
package/src/imageResolve.ts
CHANGED
|
@@ -62,6 +62,7 @@ export interface ResolveSettings {
|
|
|
62
62
|
exposure: number;
|
|
63
63
|
toneMapping: RenderToneMapping;
|
|
64
64
|
vignette: number;
|
|
65
|
+
contrast: number;
|
|
65
66
|
transparent: boolean;
|
|
66
67
|
}
|
|
67
68
|
|
|
@@ -92,6 +93,7 @@ export class ImageResolvePass {
|
|
|
92
93
|
floats[4] = settings.vignette;
|
|
93
94
|
integers[7] = settings.transparent ? 1 : 0;
|
|
94
95
|
integers[8] = denoised ? 1 : 0;
|
|
96
|
+
floats[9] = settings.contrast;
|
|
95
97
|
this.device.queue.writeBuffer(this.uniforms, 0, this.values);
|
|
96
98
|
|
|
97
99
|
const bytes = width * height * 4;
|
package/src/index.ts
CHANGED
|
@@ -19,7 +19,8 @@ export type {
|
|
|
19
19
|
export { attachCanvas } from "./canvas.js";
|
|
20
20
|
|
|
21
21
|
export { DEFAULT_DENOISER } from "./denoise.js";
|
|
22
|
-
export
|
|
22
|
+
export { DEFAULT_OIDN_WEIGHTS } from "./oidnPass.js";
|
|
23
|
+
export type { DenoiserAlgorithm, DenoiserSettings } from "./denoise.js";
|
|
23
24
|
|
|
24
25
|
// The settings only this renderer reads, beside the CPU renderer's, and the defaults it renders with.
|
|
25
26
|
export {
|
package/src/lensPass.ts
CHANGED
|
@@ -27,6 +27,8 @@ export interface LensEffects {
|
|
|
27
27
|
glareAngle: number;
|
|
28
28
|
/** A linear sRGB matrix, row by row, or `null` for a neutral white balance. */
|
|
29
29
|
whiteBalance: number[] | null;
|
|
30
|
+
/** Whether the image is metered and exposed to middle grey before the display transform. */
|
|
31
|
+
autoExposure: boolean;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
/** The lens effects these settings ask for, or `null` when they ask for none and the pass can be skipped. */
|
|
@@ -38,7 +40,9 @@ export function lensEffects(settings: GpuRenderSettings): LensEffects | null {
|
|
|
38
40
|
const neutral = !Number.isFinite(camera.whiteBalance) || camera.whiteBalance === NEUTRAL_WHITE_BALANCE;
|
|
39
41
|
const blades = Math.min(16, Math.max(3, Math.round(Number(glare.blades) || 6)));
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
const autoExposure = camera.autoExposure === true;
|
|
44
|
+
|
|
45
|
+
if (intensity <= 0 && streaks <= 0 && aberration <= 0 && neutral && !autoExposure) return null;
|
|
42
46
|
|
|
43
47
|
return {
|
|
44
48
|
bloom: intensity,
|
|
@@ -50,6 +54,7 @@ export function lensEffects(settings: GpuRenderSettings): LensEffects | null {
|
|
|
50
54
|
glareLength: Math.min(1, Math.max(0, Number(glare.length) || 0)),
|
|
51
55
|
glareAngle: (Number(glare.rotation) || 0) * Math.PI / 180,
|
|
52
56
|
whiteBalance: neutral ? null : whiteBalanceMatrix(camera.whiteBalance),
|
|
57
|
+
autoExposure,
|
|
53
58
|
};
|
|
54
59
|
}
|
|
55
60
|
|
|
@@ -93,6 +98,8 @@ export class LensPass {
|
|
|
93
98
|
private readonly upsample: GPUComputePipeline;
|
|
94
99
|
private readonly composite: GPUComputePipeline;
|
|
95
100
|
private readonly streaks: GPUComputePipeline;
|
|
101
|
+
private readonly meter: GPUComputePipeline;
|
|
102
|
+
private readonly metered: GPUBuffer;
|
|
96
103
|
/** One uniform buffer per dispatch, so every one of them keeps its own values until the queue runs it. */
|
|
97
104
|
private readonly uniforms: GPUBuffer[] = [];
|
|
98
105
|
private readonly values = new ArrayBuffer(UNIFORM_BYTES);
|
|
@@ -116,6 +123,7 @@ export class LensPass {
|
|
|
116
123
|
{ binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: storage("storage") },
|
|
117
124
|
{ binding: 5, visibility: GPUShaderStage.COMPUTE, buffer: storage("storage") },
|
|
118
125
|
{ binding: 6, visibility: GPUShaderStage.COMPUTE, buffer: storage("storage") },
|
|
126
|
+
{ binding: 7, visibility: GPUShaderStage.COMPUTE, buffer: storage("storage") },
|
|
119
127
|
],
|
|
120
128
|
});
|
|
121
129
|
|
|
@@ -126,6 +134,8 @@ export class LensPass {
|
|
|
126
134
|
this.upsample = pipeline("upsample");
|
|
127
135
|
this.composite = pipeline("composite");
|
|
128
136
|
this.streaks = pipeline("streaks");
|
|
137
|
+
this.meter = pipeline("meter");
|
|
138
|
+
this.metered = device.createBuffer({ label: "lens meter", size: 16, usage: GPUBufferUsage.STORAGE });
|
|
129
139
|
}
|
|
130
140
|
|
|
131
141
|
/** The graded image of the last `encode`, one vec4 of linear colour per pixel. */
|
|
@@ -201,6 +211,7 @@ export class LensPass {
|
|
|
201
211
|
integers[32] = streak.width;
|
|
202
212
|
integers[33] = streak.height;
|
|
203
213
|
floats[34] = effects.glareAngle;
|
|
214
|
+
floats[35] = effects.autoExposure ? 1 : 0;
|
|
204
215
|
for (let row = 0; row < 3; row++) for (let column = 0; column < 3; column++)
|
|
205
216
|
floats[20 + row * 4 + column] = white[row * 3 + column];
|
|
206
217
|
fill();
|
|
@@ -222,6 +233,7 @@ export class LensPass {
|
|
|
222
233
|
{ binding: 4, resource: { buffer: this.up as GPUBuffer } },
|
|
223
234
|
{ binding: 5, resource: { buffer: this.graded as GPUBuffer } },
|
|
224
235
|
{ binding: 6, resource: { buffer: this.streaked as GPUBuffer } },
|
|
236
|
+
{ binding: 7, resource: { buffer: this.metered } },
|
|
225
237
|
],
|
|
226
238
|
}));
|
|
227
239
|
pass.dispatchWorkgroups(Math.ceil(target.width / WORKGROUP), Math.ceil(target.height / WORKGROUP));
|
|
@@ -278,6 +290,9 @@ export class LensPass {
|
|
|
278
290
|
}
|
|
279
291
|
}
|
|
280
292
|
|
|
293
|
+
// The meter is one workgroup, whatever the size: dispatched as a single tile of the grid it reads.
|
|
294
|
+
if (effects.autoExposure) dispatch(this.meter, { width: 1, height: 1 }, () => {});
|
|
295
|
+
|
|
281
296
|
dispatch(this.composite, { width, height }, () => {});
|
|
282
297
|
|
|
283
298
|
return this.graded as GPUBuffer;
|
|
@@ -293,6 +308,7 @@ export class LensPass {
|
|
|
293
308
|
|
|
294
309
|
destroy() {
|
|
295
310
|
this.destroyBuffers();
|
|
311
|
+
this.metered.destroy();
|
|
296
312
|
for (const buffer of this.uniforms) buffer.destroy();
|
|
297
313
|
this.uniforms.length = 0;
|
|
298
314
|
}
|