ludic/packages/ludic.render3d/shaders/sharpen.frag
Orkuncakilkaya ae52e9c4da feat(render3d): anti-aliasing that exists, and a lens for the viewfinder
The shipping default had NO anti-aliasing at all: the temporal resolve was removed, the
setting's first option went on saying "Temporal", and MSAA defaults to one sample, so
every machine without DLSS drew grass and needle cards with nothing smoothing an edge.

FXAA in the sharpen pass, which already reads the neighbourhood and runs last on the LDR
image. No history, so it cannot drag or smear a reflection. 25.5% less single-pixel
staircase on edge pixels.

The trap, recorded because it inverted the result: the unsharp delta must be computed
from the RAW image and only then applied to the anti-aliased colour. Centre from FXAA and
neighbours from the raw texture measures half smoothing and half signal, so the mask
sharpens precisely what FXAA softened - 29% WORSE than no anti-aliasing at all.

Depth of field for the photo mode: a disc whose radius is the circle of confusion,
normalised by focus distance so a landscape shot is not a macro, with taps rejected
unless they are at least as out of focus as the pixel they blur into - which is what
stops a sharp foreground haloing into a blurred background. Between the scene and the
bloom, so a blurred highlight still blooms. Skipped whole when the aperture is shut.

400 frames in ordinary play: GL 7.1 s, VK 7.2 s - the lens does not draw there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 17:38:25 +03:00

71 lines
3.8 KiB
GLSL

// luma unsharp mask + film grain, on the final LDR image
in vec2 v_uv;
out vec4 o_color;
uniform sampler2D u_src;
uniform vec2 u_texel;
uniform float u_amount;
uniform float u_grain;
float hash(vec2 p) { return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453); }
float luma(vec3 c) { return dot(c, vec3(0.299, 0.587, 0.114)); }
// ---- anti-aliasing -------------------------------------------------------------------------
// THE SHIPPING DEFAULT HAS BEEN NO ANTI-ALIASING AT ALL. The temporal resolve was removed
// (post.ludic says why, and it was right to go: it reprojected water through the surface plane
// and dragged the mirror image behind the camera), the setting's first option went on saying
// "Temporal", and post_ms_samples defaults to 1 - so a frame full of grass blades and needle
// cards shimmered on every machine without DLSS, which is every Mac.
//
// This is a spatial pass with NO history: it cannot drag, it cannot smear a reflection, and it
// costs four taps on a pass that was already sampling its own neighbourhood for the unsharp
// mask. It does not fix sub-pixel grass shimmer - nothing spatial can - but it fixes every
// geometric edge, which is what the eye follows when you walk.
vec3 fxaa(vec2 uv, vec2 texel) {
vec3 rgbM = texture(u_src, uv).rgb;
float lM = luma(rgbM);
float lN = luma(texture(u_src, uv + vec2(0.0, texel.y)).rgb);
float lS = luma(texture(u_src, uv - vec2(0.0, texel.y)).rgb);
float lE = luma(texture(u_src, uv + vec2(texel.x, 0.0)).rgb);
float lW = luma(texture(u_src, uv - vec2(texel.x, 0.0)).rgb);
float lMin = min(lM, min(min(lN, lS), min(lE, lW)));
float lMax = max(lM, max(max(lN, lS), max(lE, lW)));
float range = lMax - lMin;
// a contrast floor, or film grain and dithering get "anti-aliased" into mush
if (range < max(0.0312, lMax * 0.125)) return rgbM;
// which way the edge runs, and which side of it is the step
bool isHorz = abs(lN + lS - 2.0 * lM) >= abs(lE + lW - 2.0 * lM);
vec2 dir = isHorz ? vec2(0.0, texel.y) : vec2(texel.x, 0.0);
float l1 = isHorz ? lN : lE;
float l2 = isHorz ? lS : lW;
if (abs(l1 - lM) < abs(l2 - lM)) dir = -dir;
vec3 a = texture(u_src, uv + dir * 0.5).rgb;
vec3 b = texture(u_src, uv + dir * 1.5).rgb;
vec3 blended = (a + b) * 0.5;
// Never let the blend leave the neighbourhood's own luma range. Without this guard a thin
// bright line - a blade against the sky, a rim on a trunk - reaches two pixels past where it
// belongs and reads as a halo, which is the failure everyone recognises as "this game has
// FXAA on".
float lB = luma(blended);
if (lB < lMin || lB > lMax) return mix(rgbM, a, 0.5);
return mix(rgbM, blended, 0.75);
}
uniform float u_fxaa;
void main() {
// The unsharp mask's delta is computed from the RAW image and only then applied to the
// anti-aliased colour. Taking the centre from the FXAA result and the neighbours from the
// raw texture measures a difference that is half smoothing and half signal, so the mask
// sharpens precisely the edges FXAA just softened - measured, that made the frame's
// single-pixel reversal energy 29% WORSE than no anti-aliasing at all.
vec3 raw = texture(u_src, v_uv).rgb;
vec3 c = (u_fxaa > 0.5) ? fxaa(v_uv, u_texel) : raw;
vec3 n = texture(u_src, v_uv + vec2(0, u_texel.y)).rgb, s = texture(u_src, v_uv - vec2(0, u_texel.y)).rgb;
vec3 e = texture(u_src, v_uv + vec2(u_texel.x, 0)).rgb, w = texture(u_src, v_uv - vec2(u_texel.x, 0)).rgb;
float lraw = luma(raw);
float lb = (luma(n) + luma(s) + luma(e) + luma(w) + lraw * 4.0) / 8.0;
float d = clamp((lraw - lb) * u_amount, -0.08, 0.08);
float lc = luma(c);
vec3 col = c * (1.0 + d / max(lraw, 1e-3));
float g = (hash(v_uv * 1731.0 + fract(u_time)) - 0.5) * u_grain;
col += g * (0.6 + 0.4 * (1.0 - lc));
o_color = vec4(clamp(col, 0.0, 1.0), 1.0);
}