From b355132861881d81bd2cfb8f54304ae5ee07533e Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Wed, 10 Jun 2026 20:53:15 +0100 Subject: [PATCH] Add dithering --- .../renderer/renderer-implementation.ts | 13 +++++++++++++ .../rendering/shaders/shading-fs-100.glsl | 18 ++++++++++++++++-- src/graphics/rendering/shaders/shading-fs.glsl | 15 +++++++++++++-- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/graphics/rendering/renderer/renderer-implementation.ts b/src/graphics/rendering/renderer/renderer-implementation.ts index d7fe550..19ce9e4 100644 --- a/src/graphics/rendering/renderer/renderer-implementation.ts +++ b/src/graphics/rendering/renderer/renderer-implementation.ts @@ -51,6 +51,7 @@ export class RendererImplementation implements Renderer { private _canvasSize: vec2; private blendFactor!: number; private canvasResizeObserver!: ResizeObserver; + private frameIndex = 0; private applyRuntimeSettings: { [key in keyof RuntimeSettings]: (value: any) => void; @@ -247,6 +248,18 @@ export class RendererImplementation implements Renderer { distanceNdcPixelSize: 2 / Math.max(...this.distanceFieldFrameBuffer.getSize()), shadingNdcPixelSize: 2 / Math.max(...this.lightingFrameBuffer.getSize()), + + // The motion-blur blend multiplies the lights pass' output by + // blendFactor before it reaches the 8-bit canvas, which would attenuate + // the anti-banding dither below the quantization step it has to mask. + // Pre-scale the dither to arrive at full strength after blending; the + // clamp keeps extreme motion-blur values from injecting visible grain. + ditherStrength: lightsSizeChanged ? 1 : 1 / Math.max(this.blendFactor, 0.125), + + // Scroll the dither pattern every frame (golden-ratio offset), so the + // motion-blur blend averages independently dithered frames instead of + // freezing into a banded fixed point. + ditherSeed: (this.frameIndex = (this.frameIndex + 1) & 63) * 5.588238, }; this.distancePass.render(this.uniformsProvider.getUniforms(common), [ diff --git a/src/graphics/rendering/shaders/shading-fs-100.glsl b/src/graphics/rendering/shaders/shading-fs-100.glsl index 7d1756d..a312675 100644 --- a/src/graphics/rendering/shaders/shading-fs-100.glsl +++ b/src/graphics/rendering/shaders/shading-fs-100.glsl @@ -1,6 +1,10 @@ #version 100 -precision lowp float; +#ifdef GL_FRAGMENT_PRECISION_HIGH + precision highp float; +#else + precision mediump float; +#endif #define INTENSITY_INSIDE_RATIO {intensityInsideRatio} #define SHADOW_TRACE_COUNT {shadowTraceCount} @@ -11,6 +15,8 @@ precision lowp float; {macroDefinitions} uniform float shadingNdcPixelSize; +uniform float ditherStrength; +uniform float ditherSeed; uniform vec2 squareToAspectRatioTimes2; uniform vec3 ambientLight; @@ -62,6 +68,14 @@ float shadowTransparency(float startingDistance, float lightCenterDistance, vec2 #endif #endif +float ditherNoise() { + float ign = fract(52.9829189 * fract( + dot(gl_FragCoord.xy + ditherSeed, vec2(0.06711056, 0.00583715)) + )); + float t = 2.0 * ign - 1.0; + return sign(t) * (1.0 - sqrt(1.0 - abs(t))); +} + void main() { vec4 rgbaColorAtPosition; float startingDistance = getDistance(uvCoordinates, rgbaColorAtPosition); @@ -144,7 +158,7 @@ void main() { float edge = clamp(startingDistance / shadingNdcPixelSize, 0.0, 1.0); vec3 antialiasedColor = mix(insideColor, outsideColor, edge); gl_FragColor = vec4( - antialiasedColor, + antialiasedColor + ditherNoise() * ditherStrength / 255.0, rgbaColorAtPosition.a ); } diff --git a/src/graphics/rendering/shaders/shading-fs.glsl b/src/graphics/rendering/shaders/shading-fs.glsl index de3f2fd..424c9d0 100644 --- a/src/graphics/rendering/shaders/shading-fs.glsl +++ b/src/graphics/rendering/shaders/shading-fs.glsl @@ -1,6 +1,7 @@ #version 300 es -precision lowp float; +precision highp float; +precision highp sampler2D; #define INTENSITY_INSIDE_RATIO {intensityInsideRatio} #define SHADOW_TRACE_COUNT {shadowTraceCount} @@ -13,6 +14,8 @@ precision lowp float; uniform float shadingNdcPixelSize; uniform float distanceNdcPixelSize; +uniform float ditherStrength; +uniform float ditherSeed; uniform vec2 squareToAspectRatioTimes2; uniform vec3 ambientLight; @@ -83,6 +86,14 @@ float shadowTransparency(float startingDistance, float lightCenterDistance, vec2 #endif #endif +float ditherNoise() { + float ign = fract(52.9829189 * fract( + dot(gl_FragCoord.xy + ditherSeed, vec2(0.06711056, 0.00583715)) + )); + float t = 2.0 * ign - 1.0; + return sign(t) * (1.0 - sqrt(1.0 - abs(t))); +} + out vec4 fragmentColor; void main() { vec4 rgbaColorAtPosition = getColor(uvCoordinates); @@ -167,7 +178,7 @@ void main() { float edge = clamp(startingDistance / shadingNdcPixelSize, 0.0, 1.0); vec3 antialiasedColor = mix(insideColor, outsideColor, edge); fragmentColor = vec4( - antialiasedColor, + antialiasedColor + ditherNoise() * ditherStrength / 255.0, rgbaColorAtPosition.a ); }