@driftengine/core 4.3.0 → 4.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.json +282 -0
- package/README.md +1 -1
- package/dist/cinematic/cameraPath.d.ts +53 -0
- package/dist/cinematic/cameraPath.js +118 -0
- package/dist/environment/daylight.d.ts +77 -0
- package/dist/environment/daylight.js +77 -0
- package/dist/geometry/meshConcat.d.ts +9 -0
- package/dist/geometry/meshConcat.js +101 -0
- package/dist/index.d.ts +12 -2
- package/dist/index.js +14 -1
- package/dist/render/ambientOcclusionPass.js +5 -1
- package/dist/render/backend/createRenderer.d.ts +13 -1
- package/dist/render/backend/createRenderer.js +6 -5
- package/dist/render/backend/webgl2/renderer.d.ts +147 -1
- package/dist/render/backend/webgl2/renderer.js +402 -23
- package/dist/render/backend/webgpu/buffers.js +46 -8
- package/dist/render/backend/webgpu/depthPass.d.ts +81 -3
- package/dist/render/backend/webgpu/depthPass.js +75 -14
- package/dist/render/backend/webgpu/exposurePass.d.ts +39 -0
- package/dist/render/backend/webgpu/exposurePass.js +190 -0
- package/dist/render/backend/webgpu/fieldCompose.d.ts +11 -0
- package/dist/render/backend/webgpu/fieldCompose.js +13 -0
- package/dist/render/backend/webgpu/flatPass.d.ts +3 -1
- package/dist/render/backend/webgpu/flatPass.js +12 -5
- package/dist/render/backend/webgpu/particlePass.d.ts +8 -0
- package/dist/render/backend/webgpu/particlePass.js +1 -0
- package/dist/render/backend/webgpu/postPass.d.ts +13 -3
- package/dist/render/backend/webgpu/probeBake.d.ts +59 -3
- package/dist/render/backend/webgpu/probeBake.js +97 -7
- package/dist/render/backend/webgpu/probeTargets.d.ts +65 -0
- package/dist/render/backend/webgpu/probeTargets.js +180 -0
- package/dist/render/backend/webgpu/renderer.d.ts +174 -11
- package/dist/render/backend/webgpu/renderer.js +682 -252
- package/dist/render/backend/webgpu/scatterPass.js +7 -7
- package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
- package/dist/render/backend/webgpu/skinPaletteRing.d.ts +4 -0
- package/dist/render/backend/webgpu/skinPaletteRing.js +9 -0
- package/dist/render/backend/webgpu/skyPass.js +5 -1
- package/dist/render/backend/webgpu/uniformRing.d.ts +31 -1
- package/dist/render/backend/webgpu/uniformRing.js +53 -3
- package/dist/render/bloomChain.d.ts +6 -0
- package/dist/render/bloomChain.js +8 -0
- package/dist/render/clusteredLights.d.ts +19 -11
- package/dist/render/clusteredLights.js +96 -45
- package/dist/render/cutoutCaster.d.ts +23 -0
- package/dist/render/cutoutCaster.js +20 -0
- package/dist/render/driftLight/bake.d.ts +34 -0
- package/dist/render/driftLight/bake.js +95 -0
- package/dist/render/driftLight/falloff.d.ts +16 -0
- package/dist/render/driftLight/falloff.js +23 -0
- package/dist/render/driftLight/layout.d.ts +59 -0
- package/dist/render/driftLight/layout.js +138 -0
- package/dist/render/driftLight/lightField.d.ts +108 -0
- package/dist/render/driftLight/lightField.js +179 -0
- package/dist/render/driftLight/uniforms.d.ts +28 -0
- package/dist/render/driftLight/uniforms.js +41 -0
- package/dist/render/driftLight/visibility.d.ts +18 -0
- package/dist/render/driftLight/visibility.js +47 -0
- package/dist/render/emptyTexture.d.ts +6 -0
- package/dist/render/emptyTexture.js +19 -0
- package/dist/render/exposurePass.d.ts +48 -0
- package/dist/render/exposurePass.js +172 -0
- package/dist/render/flameLight.d.ts +71 -0
- package/dist/render/flameLight.js +63 -0
- package/dist/render/gi/bounceLights.d.ts +34 -0
- package/dist/render/gi/bounceLights.js +66 -0
- package/dist/render/gi/fieldReach.d.ts +12 -0
- package/dist/render/gi/fieldReach.js +62 -0
- package/dist/render/gi/globalField.d.ts +8 -0
- package/dist/render/gi/globalField.js +31 -0
- package/dist/render/gi/probeTrace.d.ts +3 -1
- package/dist/render/gi/probeTrace.js +3 -1
- package/dist/render/gpuCapability.js +19 -4
- package/dist/render/lightBudget.d.ts +11 -0
- package/dist/render/lightBudget.js +11 -0
- package/dist/render/pointLightSelection.d.ts +16 -0
- package/dist/render/pointLightSelection.js +178 -83
- package/dist/render/pointShadowBudget.d.ts +12 -2
- package/dist/render/pointShadowBudget.js +16 -10
- package/dist/render/probeGrid.d.ts +11 -1
- package/dist/render/probeGrid.js +5 -2
- package/dist/render/probeSweeps.d.ts +45 -0
- package/dist/render/probeSweeps.js +77 -0
- package/dist/render/reflectionProbe.d.ts +43 -1
- package/dist/render/reflectionProbe.js +17 -3
- package/dist/render/renderQuality.d.ts +3 -1
- package/dist/render/rendererHarness.d.ts +9 -0
- package/dist/render/rendererHarness.js +18 -3
- package/dist/render/sceneTarget.d.ts +20 -0
- package/dist/render/sceneTarget.js +62 -1
- package/dist/render/shaders/ambientOcclusion.d.ts +63 -6
- package/dist/render/shaders/ambientOcclusion.js +188 -71
- package/dist/render/shaders/cutoutCoverage.d.ts +31 -0
- package/dist/render/shaders/cutoutCoverage.js +40 -0
- package/dist/render/shaders/depth.d.ts +25 -0
- package/dist/render/shaders/depth.js +69 -3
- package/dist/render/shaders/exposure.d.ts +52 -0
- package/dist/render/shaders/exposure.js +124 -0
- package/dist/render/shaders/filmLook.d.ts +47 -0
- package/dist/render/shaders/filmLook.js +98 -0
- package/dist/render/shaders/flat/directionalShadow.js +35 -4
- package/dist/render/shaders/flat/driftLight.d.ts +29 -0
- package/dist/render/shaders/flat/driftLight.js +75 -0
- package/dist/render/shaders/flat/index.js +5 -0
- package/dist/render/shaders/flat/lobes.js +13 -0
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +68 -9
- package/dist/render/shaders/flat/preamble.js +23 -5
- package/dist/render/shaders/flat/probeGrid.js +46 -3
- package/dist/render/shaders/generated/ambientOcclusion.wgsl.d.ts +8 -3
- package/dist/render/shaders/generated/ambientOcclusion.wgsl.js +8 -3
- package/dist/render/shaders/generated/depth.wgsl.d.ts +100 -0
- package/dist/render/shaders/generated/depth.wgsl.js +100 -0
- package/dist/render/shaders/generated/exposure.wgsl.d.ts +42 -0
- package/dist/render/shaders/generated/exposure.wgsl.js +48 -0
- package/dist/render/shaders/generated/flat.wgsl.d.ts +1800 -1344
- package/dist/render/shaders/generated/flat.wgsl.js +1816 -1360
- package/dist/render/shaders/generated/localExposure.wgsl.d.ts +18 -0
- package/dist/render/shaders/generated/localExposure.wgsl.js +24 -0
- package/dist/render/shaders/generated/rush.wgsl.d.ts +40 -10
- package/dist/render/shaders/generated/rush.wgsl.js +40 -10
- package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/temporalResolve.wgsl.js +1 -1
- package/dist/render/shaders/gi/driftLight.wgsl.d.ts +16 -0
- package/dist/render/shaders/gi/driftLight.wgsl.js +49 -0
- package/dist/render/shaders/gi/probeBake.wgsl.d.ts +29 -4
- package/dist/render/shaders/gi/probeBake.wgsl.js +190 -20
- package/dist/render/shaders/globalMedium.js +4 -0
- package/dist/render/shaders/localExposure.d.ts +35 -0
- package/dist/render/shaders/localExposure.js +138 -0
- package/dist/render/shaders/prefilter.d.ts +1 -1
- package/dist/render/shaders/prefilter.js +6 -0
- package/dist/render/shaders/rush.js +33 -5
- package/dist/render/shaders/tangentFrame.d.ts +1 -1
- package/dist/render/shaders/tangentFrame.js +15 -3
- package/dist/render/shaders/temporalResolve.js +39 -1
- package/dist/render/shadowCasters.d.ts +5 -3
- package/dist/render/shadowMap.js +13 -0
- package/dist/render/surfaceTexture.d.ts +5 -0
- package/package.json +6 -6
- package/scripts/gpuCompute.mjs +24 -3
- package/scripts/png.mjs +23 -12
- package/src/cinematic/cameraPath.ts +178 -0
- package/src/environment/daylight.ts +152 -0
- package/src/geometry/meshConcat.ts +100 -0
- package/src/index.ts +32 -0
- package/src/render/ambientOcclusionPass.ts +5 -1
- package/src/render/backend/createRenderer.ts +22 -6
- package/src/render/backend/webgl2/renderer.ts +439 -20
- package/src/render/backend/webgpu/buffers.ts +51 -8
- package/src/render/backend/webgpu/depthPass.ts +78 -10
- package/src/render/backend/webgpu/exposurePass.ts +234 -0
- package/src/render/backend/webgpu/fieldCompose.ts +15 -0
- package/src/render/backend/webgpu/flatPass.ts +10 -2
- package/src/render/backend/webgpu/particlePass.ts +9 -0
- package/src/render/backend/webgpu/probeBake.ts +143 -9
- package/src/render/backend/webgpu/probeTargets.ts +211 -0
- package/src/render/backend/webgpu/renderer.ts +808 -273
- package/src/render/backend/webgpu/scatterPass.ts +7 -7
- package/src/render/backend/webgpu/skinPaletteRing.ts +10 -0
- package/src/render/backend/webgpu/skyPass.ts +5 -1
- package/src/render/backend/webgpu/uniformRing.ts +49 -3
- package/src/render/bloomChain.ts +9 -0
- package/src/render/clusteredLights.ts +108 -63
- package/src/render/cutoutCaster.ts +44 -0
- package/src/render/driftLight/bake.ts +125 -0
- package/src/render/driftLight/falloff.ts +26 -0
- package/src/render/driftLight/layout.ts +177 -0
- package/src/render/driftLight/lightField.ts +227 -0
- package/src/render/driftLight/uniforms.ts +61 -0
- package/src/render/driftLight/visibility.ts +58 -0
- package/src/render/emptyTexture.ts +41 -0
- package/src/render/exposurePass.ts +186 -0
- package/src/render/flameLight.ts +135 -0
- package/src/render/gi/bounceLights.ts +68 -0
- package/src/render/gi/fieldReach.ts +74 -0
- package/src/render/gi/globalField.ts +39 -0
- package/src/render/gi/probeTrace.ts +3 -1
- package/src/render/gpuCapability.ts +18 -5
- package/src/render/lightBudget.ts +12 -0
- package/src/render/pointLightSelection.ts +191 -79
- package/src/render/pointShadowBudget.ts +16 -8
- package/src/render/probeGrid.ts +13 -2
- package/src/render/probeSweeps.ts +80 -0
- package/src/render/reflectionProbe.ts +54 -2
- package/src/render/renderQuality.ts +3 -1
- package/src/render/rendererHarness.ts +25 -3
- package/src/render/sceneTarget.ts +75 -1
- package/src/render/shaders/ambientOcclusion.ts +205 -71
- package/src/render/shaders/cutoutCoverage.ts +43 -0
- package/src/render/shaders/depth.ts +80 -3
- package/src/render/shaders/exposure.ts +128 -0
- package/src/render/shaders/filmLook.ts +111 -0
- package/src/render/shaders/flat/directionalShadow.ts +35 -4
- package/src/render/shaders/flat/driftLight.ts +75 -0
- package/src/render/shaders/flat/index.ts +5 -0
- package/src/render/shaders/flat/lobes.ts +13 -0
- package/src/render/shaders/flat/main.ts +68 -9
- package/src/render/shaders/flat/preamble.ts +23 -5
- package/src/render/shaders/flat/probeGrid.ts +46 -3
- package/src/render/shaders/generated/ambientOcclusion.wgsl.ts +8 -3
- package/src/render/shaders/generated/depth.wgsl.ts +103 -0
- package/src/render/shaders/generated/exposure.wgsl.ts +51 -0
- package/src/render/shaders/generated/flat.wgsl.ts +1816 -1360
- package/src/render/shaders/generated/localExposure.wgsl.ts +26 -0
- package/src/render/shaders/generated/rush.wgsl.ts +40 -10
- package/src/render/shaders/generated/temporalResolve.wgsl.ts +1 -1
- package/src/render/shaders/gi/driftLight.wgsl.ts +49 -0
- package/src/render/shaders/gi/probeBake.wgsl.ts +191 -20
- package/src/render/shaders/globalMedium.ts +4 -0
- package/src/render/shaders/localExposure.ts +154 -0
- package/src/render/shaders/prefilter.ts +6 -0
- package/src/render/shaders/rush.ts +33 -5
- package/src/render/shaders/tangentFrame.ts +15 -3
- package/src/render/shaders/temporalResolve.ts +39 -1
- package/src/render/shadowCasters.ts +5 -3
- package/src/render/shadowMap.ts +14 -0
- package/src/render/surfaceTexture.ts +5 -0
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { glSceneDepthFormat } from './depthConvention.ts';
|
|
2
|
+
import { clampGrain, clampVignette, grainSeed } from './shaders/filmLook.ts';
|
|
2
3
|
import { AmbientOcclusionPass } from './ambientOcclusionPass.ts';
|
|
4
|
+
import { ExposurePass } from './exposurePass.ts';
|
|
5
|
+
import { clampAutoExposure } from './shaders/exposure.ts';
|
|
6
|
+
import { clampLocalExposure } from './shaders/localExposure.ts';
|
|
3
7
|
import { BloomPass } from './bloomPass.ts';
|
|
4
8
|
import {
|
|
5
9
|
type ColourGradeLut,
|
|
@@ -91,6 +95,16 @@ export class SceneTarget {
|
|
|
91
95
|
private gradeSource: ColourGradeLut | null = null;
|
|
92
96
|
private gradeSize = GRADE_PLACEHOLDER_SIZE;
|
|
93
97
|
private gradeStrength = 0;
|
|
98
|
+
/** The lens and the print, held like the grade. See `filmLook.ts`. */
|
|
99
|
+
private vignette = 0;
|
|
100
|
+
private grain = 0;
|
|
101
|
+
private grainSeed = 0;
|
|
102
|
+
/** Eye adaptation: how far, and this frame's time step. See `Renderer.setAutoExposure`. */
|
|
103
|
+
private autoExposure = 0;
|
|
104
|
+
private autoExposureDt = 0;
|
|
105
|
+
/** How far each region is brought toward the frame. See `Renderer.setLocalExposure`. */
|
|
106
|
+
private localExposure = 0;
|
|
107
|
+
private exposure: ExposurePass | null = null;
|
|
94
108
|
private width = 0;
|
|
95
109
|
private height = 0;
|
|
96
110
|
|
|
@@ -606,6 +620,33 @@ export class SceneTarget {
|
|
|
606
620
|
* `null`, or a strength at or below zero, is exactly the frame that existed before the effect
|
|
607
621
|
* did: the shader's branch is on a uniform, so no fetch happens at all.
|
|
608
622
|
*/
|
|
623
|
+
/** The lens's corner falloff, held until changed. See `Renderer.setVignette`. */
|
|
624
|
+
setVignette(strength: number): void {
|
|
625
|
+
this.vignette = clampVignette(strength);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/** The print's grain and this frame's seed, held until changed. See `Renderer.setFilmGrain`. */
|
|
629
|
+
setFilmGrain(strength: number, seed: number): void {
|
|
630
|
+
this.grain = clampGrain(strength);
|
|
631
|
+
this.grainSeed = grainSeed(seed);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** How far the eye adapts, and the time since the last frame. See `Renderer.setAutoExposure`. */
|
|
635
|
+
setAutoExposure(strength: number, dtSec: number): void {
|
|
636
|
+
this.autoExposure = clampAutoExposure(strength);
|
|
637
|
+
this.autoExposureDt = dtSec;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/** How far each region is brought toward the frame. See `Renderer.setLocalExposure`. */
|
|
641
|
+
setLocalExposure(strength: number): void {
|
|
642
|
+
this.localExposure = clampLocalExposure(strength);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/** A new shot: the eye meters it afresh rather than easing in from the last one. */
|
|
646
|
+
cutExposure(): void {
|
|
647
|
+
this.exposure?.cut();
|
|
648
|
+
}
|
|
649
|
+
|
|
609
650
|
setColourGrade(lut: ColourGradeLut | null, strength: number): void {
|
|
610
651
|
this.gradeStrength = lut === null ? 0 : Math.max(0, Math.min(1, strength));
|
|
611
652
|
if (lut === null || lut === this.gradeSource) return;
|
|
@@ -851,6 +892,17 @@ export class SceneTarget {
|
|
|
851
892
|
bloomTexture = this.bloom.run(scene, this.width, this.height, bloom.threshold);
|
|
852
893
|
}
|
|
853
894
|
|
|
895
|
+
/*
|
|
896
|
+
* Eye adaptation, metered from the same resolved scene bloom reads, and after it for the same
|
|
897
|
+
* reason: the composite consumes it. Off at zero strength, where nothing is allocated or drawn.
|
|
898
|
+
*/
|
|
899
|
+
let heldExposure: WebGLTexture | null = null;
|
|
900
|
+
const local = this.localExposure > 0;
|
|
901
|
+
if ((this.autoExposure > 0 || local) && scene !== null) {
|
|
902
|
+
this.exposure ??= new ExposurePass(gl);
|
|
903
|
+
heldExposure = this.exposure.run(scene, this.autoExposureDt, local);
|
|
904
|
+
}
|
|
905
|
+
|
|
854
906
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
855
907
|
gl.viewport(0, 0, this.width, this.height);
|
|
856
908
|
gl.disable(gl.DEPTH_TEST);
|
|
@@ -946,6 +998,25 @@ export class SceneTarget {
|
|
|
946
998
|
gl.uniform1i(this.uniforms['uGradeLut'] ?? null, 4);
|
|
947
999
|
gl.uniform1f(this.uniforms['uGradeStrength'] ?? null, this.gradeStrength);
|
|
948
1000
|
gl.uniform1f(this.uniforms['uGradeSize'] ?? null, this.gradeSize);
|
|
1001
|
+
|
|
1002
|
+
/* The held brightness on unit 5, and the scene as the placeholder where there is none, for
|
|
1003
|
+
the reason the occlusion's placeholder gives. Zero strength is the whole of the off path. */
|
|
1004
|
+
gl.activeTexture(gl.TEXTURE5);
|
|
1005
|
+
gl.bindTexture(gl.TEXTURE_2D, heldExposure ?? scene);
|
|
1006
|
+
gl.uniform1i(this.uniforms['uExposureHeld'] ?? null, 5);
|
|
1007
|
+
gl.uniform1f(
|
|
1008
|
+
this.uniforms['uAutoExposure'] ?? null,
|
|
1009
|
+
heldExposure === null ? 0 : this.autoExposure,
|
|
1010
|
+
);
|
|
1011
|
+
/* Local exposure's grid on unit 6, with the same placeholder and the same off path. */
|
|
1012
|
+
const localGrid = local && heldExposure !== null ? (this.exposure?.localTexture ?? null) : null;
|
|
1013
|
+
gl.activeTexture(gl.TEXTURE6);
|
|
1014
|
+
gl.bindTexture(gl.TEXTURE_2D, localGrid ?? scene);
|
|
1015
|
+
gl.uniform1i(this.uniforms['uExposureLocal'] ?? null, 6);
|
|
1016
|
+
gl.uniform1f(
|
|
1017
|
+
this.uniforms['uLocalExposure'] ?? null,
|
|
1018
|
+
localGrid === null ? 0 : this.localExposure,
|
|
1019
|
+
);
|
|
949
1020
|
gl.activeTexture(gl.TEXTURE0);
|
|
950
1021
|
|
|
951
1022
|
/* No unit and no texture: a flat colour, not a sample. Uploaded unconditionally — two
|
|
@@ -954,6 +1025,8 @@ export class SceneTarget {
|
|
|
954
1025
|
mix it does not need. */
|
|
955
1026
|
gl.uniform3fv(this.uniforms['uVeilColor'] ?? null, veilColor);
|
|
956
1027
|
gl.uniform1f(this.uniforms['uVeilAlpha'] ?? null, veilAlpha);
|
|
1028
|
+
gl.uniform1f(this.uniforms['uVignette'] ?? null, this.vignette);
|
|
1029
|
+
gl.uniform2f(this.uniforms['uGrain'] ?? null, this.grain, this.grainSeed);
|
|
957
1030
|
|
|
958
1031
|
gl.bindVertexArray(this.vao);
|
|
959
1032
|
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
@@ -970,7 +1043,7 @@ export class SceneTarget {
|
|
|
970
1043
|
* own framebuffers on the next frame, and that is how this was found: 120 pixels of a frame
|
|
971
1044
|
* that should have been identical, and a warning per draw explaining why.
|
|
972
1045
|
*/
|
|
973
|
-
for (let unit = 0; unit <=
|
|
1046
|
+
for (let unit = 0; unit <= 5; unit++) {
|
|
974
1047
|
gl.activeTexture(gl.TEXTURE0 + unit);
|
|
975
1048
|
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
976
1049
|
}
|
|
@@ -999,6 +1072,7 @@ export class SceneTarget {
|
|
|
999
1072
|
dispose(): void {
|
|
1000
1073
|
const { gl } = this;
|
|
1001
1074
|
this.ao?.dispose();
|
|
1075
|
+
this.exposure?.dispose();
|
|
1002
1076
|
this.ao = null;
|
|
1003
1077
|
this.bloom?.dispose();
|
|
1004
1078
|
this.bloom = null;
|
|
@@ -26,13 +26,23 @@ import { glslIsFarDepth, glslSceneDepthToNdc } from '../depthConvention.ts';
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
* The estimate. Writes one channel
|
|
29
|
+
* The estimate. Writes one channel at `AO_STORE`: that is open sky, 0 is fully enclosed, and the
|
|
30
|
+
* second axis of the blur decodes it.
|
|
30
31
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
32
|
+
* **Horizons, not neighbours: ground-truth ambient occlusion (Jimenez et al. 2016).** Each pixel
|
|
33
|
+
* walks two lines across the screen, both ways, and keeps the highest thing it can see along each
|
|
34
|
+
* half: the horizon. The sky left between the two horizons of a line, weighted by the cosine to the
|
|
35
|
+
* surface's own normal, is that line's visibility in closed form, and two lines a pixel with the
|
|
36
|
+
* rotation tile below turning them is the hemisphere. It replaced a sum over twelve neighbours that
|
|
37
|
+
* counted anything within the radius as a blocker, however thin: cloth hanging a hand in front of a
|
|
38
|
+
* column darkened the column behind it, which read as a halo round every curtain in a courtyard.
|
|
39
|
+
*
|
|
40
|
+
* **What stops the halo is two rules about the horizon**, both the paper's. A sample fades out of
|
|
41
|
+
* the horizon as it approaches the radius, so a far wall does not shade a floor; and once the walk
|
|
42
|
+
* has passed the highest sample, the horizon sinks back toward what it sees next, so a thin thing
|
|
43
|
+
* in front raises it only while the walk is behind that thing. What that gives up is the shade under
|
|
44
|
+
* a thick object seen edge on, which reads as a thin one; the thickness is a constant because depth
|
|
45
|
+
* alone cannot say how deep anything is.
|
|
36
46
|
*/
|
|
37
47
|
export const AO_FRAG = `#version 300 es
|
|
38
48
|
precision highp float;
|
|
@@ -69,16 +79,18 @@ uniform float uRadius;
|
|
|
69
79
|
|
|
70
80
|
out float fragColor;
|
|
71
81
|
|
|
72
|
-
/**
|
|
73
|
-
const int
|
|
82
|
+
/** Lines across the screen a pixel walks, each both ways. Fixed, so the cost is fixed. */
|
|
83
|
+
const int AO_SLICES = 2;
|
|
84
|
+
/** Depth samples each way along a line: two lines, two ways, six steps is 24 a pixel. */
|
|
85
|
+
const int AO_STEPS = 6;
|
|
74
86
|
/**
|
|
75
|
-
*
|
|
87
|
+
* How far the horizon sinks back toward each later sample once the walk has passed the highest one.
|
|
76
88
|
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
89
|
+
* The paper's thickness heuristic. At zero anything within the radius shades for good, which is the
|
|
90
|
+
* halo; at one a horizon lasts one sample and a real corner loses its shade. A fifth keeps a corner,
|
|
91
|
+
* whose samples keep rising, and lets a curtain's shade drop within the few samples past its edge.
|
|
80
92
|
*/
|
|
81
|
-
const float
|
|
93
|
+
const float AO_THIN = 0.2;
|
|
82
94
|
/**
|
|
83
95
|
* How far the sampling may reach across the frame, whatever the depth says.
|
|
84
96
|
*
|
|
@@ -87,25 +99,6 @@ const float AO_GOLDEN_ANGLE = 2.39996323;
|
|
|
87
99
|
* being contact shading and becomes an expensive wide darkening. 6% of the frame.
|
|
88
100
|
*/
|
|
89
101
|
const float AO_MAX_REACH_UV = 0.06;
|
|
90
|
-
/**
|
|
91
|
-
* Self-occlusion guard, as a fraction of the pixel's own view depth.
|
|
92
|
-
*
|
|
93
|
-
* Depth is quantised, so a flat surface samples itself very slightly in front of itself and
|
|
94
|
-
* every flat surface in the world comes back faintly dirty. The error grows with distance,
|
|
95
|
-
* which is why the guard is proportional to depth rather than a constant.
|
|
96
|
-
*/
|
|
97
|
-
const float AO_BIAS = 0.004;
|
|
98
|
-
/**
|
|
99
|
-
* The floor under the 1 / (v·v) weighting, in square metres.
|
|
100
|
-
*
|
|
101
|
-
* Not a guard against dividing by zero, which is what a value near the float epsilon would
|
|
102
|
-
* be. The nearest taps land about a centimetre away, where v·v is 1e-4 and the weight is ten
|
|
103
|
-
* thousand — so a depth quantised to a millimetre yields several units of occlusion out of
|
|
104
|
-
* nothing at all. Measured that way first, with 1e-4 here: a flat floor came back as static.
|
|
105
|
-
* A hundredth of a square metre is a tenth of a metre of separation, which is the scale
|
|
106
|
-
* below which two samples are the same surface rather than one occluding the other.
|
|
107
|
-
*/
|
|
108
|
-
const float AO_EPSILON = 0.01;
|
|
109
102
|
/**
|
|
110
103
|
* How many times further one neighbour may be than the other before the pair straddles an edge.
|
|
111
104
|
*
|
|
@@ -126,6 +119,8 @@ const float AO_EDGE_RATIO = 8.0;
|
|
|
126
119
|
* and far below the step at any edge worth shading — a kerb against a deck is metres of it.
|
|
127
120
|
*/
|
|
128
121
|
const float AO_EDGE_FLOOR = 0.05;
|
|
122
|
+
/** Stored at half, so a turn the closed form credits past one reaches the blur whole. See AO_STORE in the module. */
|
|
123
|
+
const float AO_STORE = 0.5;
|
|
129
124
|
|
|
130
125
|
/** View-space position of whatever is at this pixel, from its depth alone. */
|
|
131
126
|
vec3 viewPosition(vec2 uv, float depth) {
|
|
@@ -134,6 +129,17 @@ vec3 viewPosition(vec2 uv, float depth) {
|
|
|
134
129
|
return view.xyz / view.w;
|
|
135
130
|
}
|
|
136
131
|
|
|
132
|
+
/**
|
|
133
|
+
* The cosine-weighted sky between two horizon angles h0 < 0 < h1, measured from the eye, for a
|
|
134
|
+
* normal at the angle given in the same plane: the paper's closed form. 1 for an open plane facing
|
|
135
|
+
* the eye.
|
|
136
|
+
*/
|
|
137
|
+
float gtaoArc(float h0, float h1, float normal) {
|
|
138
|
+
float c = cos(normal);
|
|
139
|
+
float s2 = 2.0 * sin(normal);
|
|
140
|
+
return 0.25 * (-cos(2.0 * h0 - normal) + c + h0 * s2) + 0.25 * (-cos(2.0 * h1 - normal) + c + h1 * s2);
|
|
141
|
+
}
|
|
142
|
+
|
|
137
143
|
void main() {
|
|
138
144
|
/*
|
|
139
145
|
* One output pixel, in UV, along each axis.
|
|
@@ -215,15 +221,16 @@ void main() {
|
|
|
215
221
|
vec3 n = normalize(cross(dpdx, dpdy));
|
|
216
222
|
/*
|
|
217
223
|
* Turned to face the eye. Which sign the cross product comes out with is a fact about the
|
|
218
|
-
* screen-space winding and the handedness of the projection, and a normal pointing away
|
|
219
|
-
*
|
|
220
|
-
*
|
|
224
|
+
* screen-space winding and the handedness of the projection, and a normal pointing away from the
|
|
225
|
+
* camera puts every horizon behind the surface. In view space the eye is the origin, so a normal
|
|
226
|
+
* facing it satisfies n·p < 0.
|
|
221
227
|
*/
|
|
222
228
|
if (dot(n, p) > 0.0) n = -n;
|
|
229
|
+
vec3 toEye = normalize(-p);
|
|
223
230
|
|
|
224
231
|
/*
|
|
225
|
-
* The radius as a fraction of the frame. A metre subtends less of the screen the further
|
|
226
|
-
*
|
|
232
|
+
* The radius as a fraction of the frame. A metre subtends less of the screen the further away it
|
|
233
|
+
* is, which is the entire reason this is computed per pixel rather than handed in.
|
|
227
234
|
*/
|
|
228
235
|
vec2 reach = min(
|
|
229
236
|
uRadius * uProjScale * 0.5 / max(-p.z, 1e-3),
|
|
@@ -231,41 +238,77 @@ void main() {
|
|
|
231
238
|
);
|
|
232
239
|
|
|
233
240
|
/*
|
|
234
|
-
* A rotation that repeats every four pixels: sixteen
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
* the noise rather than complete the kernel.
|
|
241
|
+
* A rotation that repeats every four pixels: sixteen turns laid out over a 4x4 tile, spanning the
|
|
242
|
+
* quarter turn between one line and the next, so the eight-wide blur that follows averages every
|
|
243
|
+
* turn of both lines. The first step's distance is staggered by the same tile, so neighbouring
|
|
244
|
+
* pixels do not all sample the same rings either.
|
|
239
245
|
*/
|
|
240
246
|
float tile = mod(gl_FragCoord.x, 4.0) + 4.0 * mod(gl_FragCoord.y, 4.0);
|
|
241
|
-
float turn = tile * (
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
float t = (float(i) + 0.5) / float(AO_TAPS);
|
|
246
|
-
float angle = turn + float(i) * AO_GOLDEN_ANGLE;
|
|
247
|
-
/* The square root spreads the taps evenly over the disk; without it they crowd the
|
|
248
|
-
centre, where they are measuring the pixel's own depth. */
|
|
249
|
-
vec2 uv = vUv + vec2(cos(angle), sin(angle)) * sqrt(t) * reach;
|
|
247
|
+
float turn = tile * (1.5707963 / 16.0);
|
|
248
|
+
float stagger = fract(tile * 0.618034);
|
|
249
|
+
/* One output pixel along each axis, in UV. */
|
|
250
|
+
vec2 pixel = vec2(length(stepX), length(stepY));
|
|
250
251
|
|
|
252
|
+
float visible = 0.0;
|
|
253
|
+
for (int slice = 0; slice < AO_SLICES; slice++) {
|
|
254
|
+
float angle = turn + float(slice) * (3.1415927 / float(AO_SLICES));
|
|
251
255
|
/*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
256
|
+
* **Even in pixels, not in UV.** A UV step across a wide frame is longer than one down it, so
|
|
257
|
+
* turns spread evenly in UV crowd toward the horizontal once they reach the screen, and on a
|
|
258
|
+
* floor the horizontal lines are the ones that see least of its normal: every flat floor and
|
|
259
|
+
* ceiling came back at 0.82 open, measured, where the construction gives 0.97. A pixel is as
|
|
260
|
+
* wide as it is tall in view space, so a direction even in pixels is even in the world. It is
|
|
261
|
+
* scaled here into UV, and the reach is taken down the frame, whose UV is the same pixels.
|
|
257
262
|
*/
|
|
258
|
-
|
|
259
|
-
|
|
263
|
+
vec2 screen = vec2(cos(angle) * pixel.x / pixel.y, sin(angle));
|
|
264
|
+
/*
|
|
265
|
+
* The line in view space, taken through the same reconstruction the samples use: a step along
|
|
266
|
+
* the screen at this pixel's own depth, so the samples lie in the plane the arc is measured in
|
|
267
|
+
* whatever the frame's aspect and whichever way a backend's clip space counts up.
|
|
268
|
+
*/
|
|
269
|
+
vec3 along = normalize(viewPosition(vUv + screen * 1e-3, depth) - p);
|
|
270
|
+
vec3 across = normalize(along - toEye * dot(along, toEye));
|
|
271
|
+
vec3 axis = cross(across, toEye);
|
|
272
|
+
/* The normal projected into the line's plane, and its angle from the eye toward +across. */
|
|
273
|
+
vec3 projected = n - axis * dot(n, axis);
|
|
274
|
+
float projectedLength = length(projected);
|
|
275
|
+
if (projectedLength < 1e-4) continue;
|
|
276
|
+
float normalAngle = atan(dot(projected, across), dot(projected, toEye));
|
|
260
277
|
|
|
261
|
-
|
|
262
|
-
float
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
278
|
+
/* The highest cosine seen each way. */
|
|
279
|
+
float horizon0 = -1.0;
|
|
280
|
+
float horizon1 = -1.0;
|
|
281
|
+
for (int k = 0; k < AO_STEPS; k++) {
|
|
282
|
+
float t = (float(k) + stagger) / float(AO_STEPS);
|
|
283
|
+
/* Squared, so the steps crowd the pixel, where contact shade lives. */
|
|
284
|
+
vec2 stride = screen * reach.y * (t * t + 0.5 / float(AO_STEPS * AO_STEPS));
|
|
285
|
+
for (int side = 0; side < 2; side++) {
|
|
286
|
+
vec2 uv = side == 0 ? vUv - stride : vUv + stride;
|
|
287
|
+
/*
|
|
288
|
+
* textureLod, not texture, and it is the rule rather than a preference: this sits inside a
|
|
289
|
+
* loop whose iterations a compiler cannot prove uniform. The depth texture has one level and
|
|
290
|
+
* NEAREST filters, so the fetch is identical. See AGENTS.md, 2026-08-07.
|
|
291
|
+
*/
|
|
292
|
+
float sampled = textureLod(uDepth, uv, 0.0).r;
|
|
293
|
+
if (${glslIsFarDepth('sampled')}) continue;
|
|
294
|
+
vec3 v = viewPosition(uv, sampled) - p;
|
|
295
|
+
float distance2 = dot(v, v);
|
|
296
|
+
if (distance2 < 1e-8) continue;
|
|
297
|
+
float cosine = dot(v, toEye) * inversesqrt(distance2);
|
|
298
|
+
/* Faded to nothing at the radius: a far wall is not an occluder of this surface. */
|
|
299
|
+
float fade = clamp(1.0 - distance2 / (uRadius * uRadius), 0.0, 1.0);
|
|
300
|
+
cosine = mix(-1.0, cosine, fade);
|
|
301
|
+
if (side == 0) horizon0 = cosine > horizon0 ? cosine : mix(horizon0, cosine, AO_THIN);
|
|
302
|
+
else horizon1 = cosine > horizon1 ? cosine : mix(horizon1, cosine, AO_THIN);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/* Angles from the eye, negative toward -across, and never below the surface's own tangent. */
|
|
306
|
+
float h0 = max(-acos(clamp(horizon0, -1.0, 1.0)), normalAngle - 1.5707963);
|
|
307
|
+
float h1 = min(acos(clamp(horizon1, -1.0, 1.0)), normalAngle + 1.5707963);
|
|
308
|
+
visible += projectedLength * gtaoArc(h0, h1, normalAngle);
|
|
266
309
|
}
|
|
267
310
|
|
|
268
|
-
fragColor = clamp(
|
|
311
|
+
fragColor = clamp(visible / float(AO_SLICES) * AO_STORE, 0.0, 1.0);
|
|
269
312
|
}
|
|
270
313
|
`;
|
|
271
314
|
|
|
@@ -295,12 +338,14 @@ uniform vec2 uStep;
|
|
|
295
338
|
* asks that question five times per pixel.
|
|
296
339
|
*/
|
|
297
340
|
uniform vec4 uDepthToViewZ;
|
|
341
|
+
/** One across, carrying the estimate's AO_STORE scale; its inverse down, decoding it. */
|
|
342
|
+
uniform float uAoScale;
|
|
298
343
|
|
|
299
344
|
out float fragColor;
|
|
300
345
|
|
|
301
346
|
/**
|
|
302
|
-
* How far
|
|
303
|
-
*
|
|
347
|
+
* How far a neighbour may sit from the depth this pixel's own surface predicts there, as a
|
|
348
|
+
* fraction of that depth, before it stops being the same surface.
|
|
304
349
|
*
|
|
305
350
|
* Relative rather than absolute, because the same 5 cm step is a cliff on a dashboard and
|
|
306
351
|
* nothing at all on a far wall.
|
|
@@ -313,8 +358,19 @@ float viewZ(float depth) {
|
|
|
313
358
|
}
|
|
314
359
|
|
|
315
360
|
void main() {
|
|
316
|
-
float
|
|
317
|
-
float
|
|
361
|
+
float depth = textureLod(uDepth, vUv, 0.0).r;
|
|
362
|
+
float own = textureLod(uAo, vUv, 0.0).r;
|
|
363
|
+
/* Nothing to average over the sky, and no surface to predict one from. */
|
|
364
|
+
if (${glslIsFarDepth('depth')}) {
|
|
365
|
+
fragColor = min(own * uAoScale, 1.0);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
float centre = viewZ(depth);
|
|
369
|
+
/* Each tap is compared with this pixel's plane, not with its depth. See blurWeight in the module. */
|
|
370
|
+
float inverse = 1.0 / centre;
|
|
371
|
+
float stepAfter = 1.0 / viewZ(textureLod(uDepth, vUv + uStep, 0.0).r) - inverse;
|
|
372
|
+
float stepBefore = inverse - 1.0 / viewZ(textureLod(uDepth, vUv - uStep, 0.0).r);
|
|
373
|
+
float slope = abs(stepAfter) < abs(stepBefore) ? stepAfter : stepBefore;
|
|
318
374
|
float sum = 0.0;
|
|
319
375
|
float weight = 0.0;
|
|
320
376
|
/*
|
|
@@ -342,13 +398,91 @@ void main() {
|
|
|
342
398
|
for (int i = -3; i <= 4; i++) {
|
|
343
399
|
vec2 uv = vUv + uStep * float(i);
|
|
344
400
|
float z = viewZ(textureLod(uDepth, uv, 0.0).r);
|
|
345
|
-
/* One if this neighbour is on
|
|
346
|
-
float w = max(0.0, 1.0 - abs(z -
|
|
401
|
+
/* One if this neighbour is on this pixel's surface, falling to zero as it leaves it. */
|
|
402
|
+
float w = max(0.0, 1.0 - abs(z * (inverse + slope * float(i)) - 1.0) / AO_BLUR_DEPTH_TOLERANCE);
|
|
347
403
|
sum += textureLod(uAo, uv, 0.0).r * w;
|
|
348
404
|
weight += w;
|
|
349
405
|
}
|
|
350
406
|
/* An isolated pixel — a thin rail against a far wall — keeps its own estimate rather than
|
|
351
407
|
dividing by nothing. */
|
|
352
|
-
|
|
408
|
+
float blurred = weight > 0.0 ? sum / weight : own;
|
|
409
|
+
fragColor = min(blurred * uAoScale, 1.0);
|
|
353
410
|
}
|
|
354
411
|
`;
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* The estimate's closed form, written out once more so a test can pin it to hand-derived values:
|
|
415
|
+
* the cosine-weighted sky between horizon angles `h0 < 0 < h1` for a normal at `normal`, all in
|
|
416
|
+
* radians from the eye within one line's plane.
|
|
417
|
+
*/
|
|
418
|
+
export function gtaoArc(h0: number, h1: number, normal: number): number {
|
|
419
|
+
const c = Math.cos(normal);
|
|
420
|
+
const s2 = 2 * Math.sin(normal);
|
|
421
|
+
return (
|
|
422
|
+
0.25 * (-Math.cos(2 * h0 - normal) + c + h0 * s2) +
|
|
423
|
+
0.25 * (-Math.cos(2 * h1 - normal) + c + h1 * s2)
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** One step of a horizon: up to a higher sample, or a fifth of the way down toward a lower one. */
|
|
428
|
+
export function nextHorizon(horizon: number, cosine: number): number {
|
|
429
|
+
return cosine > horizon ? cosine : horizon + (cosine - horizon) * AO_THIN;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** The shader's thickness heuristic. See `AO_THIN` inside `AO_FRAG`. */
|
|
433
|
+
export const AO_THIN = 0.2;
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* What the estimate is multiplied by before it is stored, and divided by once the blur is done.
|
|
437
|
+
*
|
|
438
|
+
* **Half, because an unblurred estimate is allowed past one.** A pixel's two lines see a plane
|
|
439
|
+
* turned away from the eye from two directions, and the closed form credits one of them more than
|
|
440
|
+
* the whole sky and the other less: it is the average over every turn that is exactly one, and the
|
|
441
|
+
* average is what the blur is for. Stored as it came, an eight-bit target clipped every turn above
|
|
442
|
+
* one before the blur could average it, and every surface seen edge on came back a few percent
|
|
443
|
+
* shaded with nothing on it. A line reaches π/2 at most, so half fits the whole range.
|
|
444
|
+
*
|
|
445
|
+
* What it gives up is one bit of the intermediate: steps of 1/128 rather than 1/256 until the
|
|
446
|
+
* second axis decodes, which the eight taps a pixel averages carry below what a frame can show.
|
|
447
|
+
*/
|
|
448
|
+
export const AO_STORE = 0.5;
|
|
449
|
+
|
|
450
|
+
/** How far a neighbour may sit from this pixel's surface, as a fraction of its distance. */
|
|
451
|
+
export const AO_BLUR_DEPTH_TOLERANCE = 0.02;
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* The blur's weight for a neighbour `offset` pixels along the axis, from view depths: this pixel's
|
|
455
|
+
* own, the two either side of it, and the neighbour's. The shader's lines written out again.
|
|
456
|
+
*
|
|
457
|
+
* **The neighbour is compared with this pixel's plane, not with this pixel's depth.** Comparing
|
|
458
|
+
* depths refused every neighbour of a surface seen edge on: a floor forty metres out changes its
|
|
459
|
+
* depth by four percent a row, twice the tolerance, so the pass down the frame kept the centre alone
|
|
460
|
+
* and left the rotation tile's rows standing — four-pixel stripes across every ramp and far floor,
|
|
461
|
+
* reported as strips on a track. On a plane 1/z is affine in the screen, so one step of it predicts
|
|
462
|
+
* the plane's depth at every tap exactly, and a tap is refused only for leaving the plane.
|
|
463
|
+
*
|
|
464
|
+
* The step is taken from whichever neighbour is closer to the centre in 1/z. At a silhouette that is
|
|
465
|
+
* the one on this pixel's own side, so the far surface cannot bend the prediction toward it; on a
|
|
466
|
+
* plane the two agree to a quantisation step, which moves a weight by nothing a frame shows. What it
|
|
467
|
+
* gives up is a crease: past the fold between a floor and a wall the prediction runs on along the
|
|
468
|
+
* floor, so the wall's taps fade out over the tolerance rather than being kept.
|
|
469
|
+
*
|
|
470
|
+
* **Written here rather than in the shader**, because a comment inside the GLSL is part of the string
|
|
471
|
+
* a consumer ships.
|
|
472
|
+
*/
|
|
473
|
+
export function blurWeight(
|
|
474
|
+
centre: number,
|
|
475
|
+
before: number,
|
|
476
|
+
after: number,
|
|
477
|
+
neighbour: number,
|
|
478
|
+
offset: number,
|
|
479
|
+
): number {
|
|
480
|
+
const inverse = 1 / centre;
|
|
481
|
+
const stepAfter = 1 / after - inverse;
|
|
482
|
+
const stepBefore = inverse - 1 / before;
|
|
483
|
+
const slope = Math.abs(stepAfter) < Math.abs(stepBefore) ? stepAfter : stepBefore;
|
|
484
|
+
return Math.max(
|
|
485
|
+
0,
|
|
486
|
+
1 - Math.abs(neighbour * (inverse + slope * offset) - 1) / AO_BLUR_DEPTH_TOLERANCE,
|
|
487
|
+
);
|
|
488
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A cutout that keeps its coverage down the mip chain.
|
|
3
|
+
*
|
|
4
|
+
* **Averaging alpha thins everything that is thinner than a texel.** Each level of a mip chain is
|
|
5
|
+
* the mean of four texels of the one above, so a needle one texel wide on a clear background is 0.5
|
|
6
|
+
* at the next level, 0.25 at the one after, and gone against a cutoff of 0.5 two levels down. A
|
|
7
|
+
* tree that is dense up close then stands bare a few metres off, which on a bought courtyard was a
|
|
8
|
+
* cypress drawn as bare branches at 1280 pixels across and in full leaf at 2000.
|
|
9
|
+
*
|
|
10
|
+
* **So the test scales alpha up by the level it is sampling**: a quarter more per level, the figure
|
|
11
|
+
* Ben Golus measured on foliage in "Anti-aliased Alpha Test" (2017). Only the test sees the scaled
|
|
12
|
+
* value; what a translucent draw blends with is the texture's own alpha, and a material with no
|
|
13
|
+
* cutoff is untouched. What it gives up is exactness: coverage is held roughly rather than measured
|
|
14
|
+
* per image, which Ignacio Castaño's method would do at upload from a readback this engine does not
|
|
15
|
+
* make. What would make it wrong is an image whose mips were built to keep coverage already, which
|
|
16
|
+
* this would then thicken.
|
|
17
|
+
*
|
|
18
|
+
* The level is estimated from the texture coordinate's screen derivatives, the isotropic formula;
|
|
19
|
+
* a sampler with anisotropic filtering reads a lower level at a grazing angle, so there a cutout
|
|
20
|
+
* comes out slightly fuller than its own mip would have made it.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** How much more alpha each mip level down is credited with in the test. */
|
|
24
|
+
export const CUTOUT_MIP_SCALE = 0.25;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* `cutoutAlpha(alpha, texels)`: the alpha the cutout test should see, for a sample at `texels`, the
|
|
28
|
+
* texture coordinate times the texture's size. Called in uniform control flow, since it takes
|
|
29
|
+
* derivatives.
|
|
30
|
+
*/
|
|
31
|
+
export const CUTOUT_COVERAGE_GLSL = `
|
|
32
|
+
float cutoutAlpha(float alpha, vec2 texels) {
|
|
33
|
+
vec2 dx = dFdx(texels);
|
|
34
|
+
vec2 dy = dFdy(texels);
|
|
35
|
+
float level = max(0.0, 0.5 * log2(max(dot(dx, dx), dot(dy, dy))));
|
|
36
|
+
return alpha * (1.0 + level * ${CUTOUT_MIP_SCALE.toFixed(2)});
|
|
37
|
+
}
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
/** The same, given the level, for a test to pin: what the shader compares against the cutoff. */
|
|
41
|
+
export function cutoutAlpha(alpha: number, level: number): number {
|
|
42
|
+
return alpha * (1 + Math.max(0, level) * CUTOUT_MIP_SCALE);
|
|
43
|
+
}
|