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>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-19 17:38:25 +03:00
parent 182cbf3cc3
commit ae52e9c4da
10 changed files with 222 additions and 6 deletions

View file

@ -0,0 +1,28 @@
bump: minor
type: feat
Anti-aliasing that exists, and a lens for the viewfinder.
THE SHIPPING DEFAULT HAD NO ANTI-ALIASING AT ALL. The temporal resolve was removed (for
good reasons - 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 MSAA defaults
to one sample. Every machine without DLSS - which is every Mac - drew a frame full of grass
blades and needle cards with nothing smoothing a single edge.
FXAA now, in the sharpen pass, because that pass already reads this pixel's neighbourhood
and runs last on the LDR image. It has no history, so it cannot drag or smear a reflection.
Measured on edge pixels: 25.5% less single-pixel staircase.
One trap worth recording. 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 exactly the edges FXAA just softened - measured, that first
version was 29% WORSE than no anti-aliasing at all.
And depth of field, for the photo mode: a disc of taps whose radius is the pixel's circle
of confusion, signed so the two sides of the focal plane differ and normalised by the focus
distance, because a lens focused at two metres throws a background out far harder than one
focused at two hundred. A tap only counts if it is at least as out of focus as the pixel
it is blurring into, which is what keeps a sharp foreground from haloing into a blurred
background. It runs between the scene and the bloom so a blurred highlight still blooms,
and the whole pass is skipped when the aperture is shut - in ordinary play there is no lens
and this never draws.

View file

@ -76,6 +76,17 @@ var post_color_w: int = 0 # its size: the display's when DLSS upscaled
var post_color_h: int = 0
var post_color: int = 0 # the HDR colour the rest of post reads # the resolved depth, copied so passes can read it while drawing into the frame
var post_p_sharp: int = 0
# Spatial anti-aliasing, in the sharpen pass because that pass already reads this pixel's
# neighbourhood and runs last on the LDR image. 1 on, 0 off; the game's setting drives it.
var post_fxaa: int = 0x3F800000
# ---- depth of field ------------------------------------------------------------------------
# Off in ordinary play - the pass is skipped whole, not run at zero radius. The game turns it on
# behind the viewfinder and says what to focus on.
var post_p_dof: int = 0
var post_dof: Target = null
var post_dof_focus: int = 0x41200000 # 10 m
var post_dof_aperture: int = 0 # 0 = no lens, and no pass
var post_dof_max: int = 0x41400000 # 12 px
var post_p_tone_hdr: int = 0 # the tonemap's HDR10 variant, made the first time HDR is on
var post_ldr_hdr: bool = false # post_ldr was made for HDR10 output (10-bit)
# the LDR image is 10-bit while the output is HDR10: PQ in 8 bits bands
@ -88,7 +99,7 @@ function post_free() -> void {
if post_hdr == null { return }
if post_ms_fbo != 0 { gpu_fb_free(post_ms_fbo); post_ms_fbo = 0 }
target_free(post_hdr); target_free(post_ao); target_free(post_ao_blur); target_free(post_ldr)
target_free(post_depth_copy); target_free(post_prev); target_free(post_scene); target_free(post_vol)
target_free(post_depth_copy); target_free(post_prev); target_free(post_scene); target_free(post_vol); target_free(post_dof)
for i in 0 .. len(post_bloom) { target_free(post_bloom[i]) }
post_hdr = null
}
@ -149,6 +160,8 @@ function post_init(w: int, h: int) -> void {
post_ldr = target_new(w, h, post_ldr_fmt(), GL_RGBA, GL_UNSIGNED_BYTE, false, GL_LINEAR)
post_ldr_hdr = gpu_hdr_active()
post_depth_copy = target_new(w, h, GL_R8, GL_RED, GL_UNSIGNED_BYTE, true, GL_NEAREST)
post_dof = target_new(w, h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR)
if post_p_dof == 0 { post_p_dof = r3d_program("fullscreen.vert", "dof.frag", "") }
post_vol = target_new(max(w / 2, 1), max(h / 2, 1), GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR)
if post_p_vol == 0 { post_p_vol = r3d_program("fullscreen.vert", "volumetric.frag", "") }
post_prev = target_new(w, h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR)
@ -335,6 +348,25 @@ function post_volumetric_pass() -> void {
gpu_blend(false)
}
# The lens, between the scene and the bloom: a blurred highlight should still bloom, and a
# bloom smeared by the lens afterwards would be a halo round nothing.
function post_dof_pass() -> void {
if post_dof_aperture == 0 { return }
gpu_depth_test(false)
gpu_blend(false)
target_bind(post_dof)
gpu_use_program(post_p_dof)
r3d_bind_2d(post_p_dof, "u_src", 0, post_color)
r3d_bind_2d(post_p_dof, "u_depth", 1, post_hdr.depth)
u_mat4(gpu_uniform(post_p_dof, "u_inv_proj"), cam_inv_proj)
u_f2(gpu_uniform(post_p_dof, "u_texel"), fr(1, post_w), fr(1, post_h))
u_f(gpu_uniform(post_p_dof, "u_focus"), post_dof_focus)
u_f(gpu_uniform(post_p_dof, "u_aperture"), post_dof_aperture)
u_f(gpu_uniform(post_p_dof, "u_max_coc"), post_dof_max)
mesh_draw(post_fs)
post_color = post_dof.color
}
function post_bloom_pass() -> void {
gpu_depth_test(false)
gpu_blend(false)
@ -413,6 +445,9 @@ function post_tonemap(color_tex: int) -> void {
r3d_bind_2d(post_p_sharp, "u_src", 0, post_ldr.color)
u_f2(gpu_uniform(post_p_sharp, "u_texel"), fr(1, post_ldr.w), fr(1, post_ldr.h))
u_f(gpu_uniform(post_p_sharp, "u_amount"), post_sharpen)
var fx = post_fxaa
if r3d_env_has("R3D_NOFXAA") { fx = F_ZERO }
u_f(gpu_uniform(post_p_sharp, "u_fxaa"), fx)
# R3D_NOGRAIN=1: no film grain, so two frames of a still camera can be compared for what else moves
var grain = post_grain
if r3d_env_has("R3D_NOGRAIN") { grain = F_ZERO }

View file

@ -288,6 +288,7 @@ function r3d_frame(time: int) -> void {
# Before bloom and before the tonemap: a shaft of light is a bright thing in the air and
# should bloom like one.
prof_begin("volumetric"); post_volumetric_pass(); prof_end()
prof_begin("dof"); post_dof_pass(); prof_end()
if not post_no_gi { prof_begin("SSAO/GI"); post_ssao_pass(); prof_end() }
if r3d_debug_max { tex_max(post_hdr.color, post_hdr.w, post_hdr.h, "hdr") }
prof_begin("bloom")

View file

@ -0,0 +1,51 @@
// Depth of field, for the viewfinder. The camera mode (the game's photo mode) has had a frame,
// a zoom and a grade and no LENS: everything from the bootlaces to the Bells was equally sharp,
// which is the one thing a photograph never is.
//
// A disc of taps whose radius is this pixel's circle of confusion. It runs at full resolution
// and costs nothing when the aperture is shut, because the whole pass is skipped - in ordinary
// play there is no lens and this never draws.
in vec2 v_uv;
out vec4 o_color;
uniform sampler2D u_src;
uniform sampler2D u_depth;
uniform mat4 u_inv_proj;
uniform vec2 u_texel;
uniform float u_focus; // metres to the plane in focus
uniform float u_aperture; // how fast the blur opens either side of it
uniform float u_max_coc; // in pixels, so a wide aperture cannot eat the whole frame
float viewZ(vec2 uv) {
float d = texture(u_depth, uv).r;
vec4 p = u_inv_proj * vec4(uv * 2.0 - 1.0, d * 2.0 - 1.0, 1.0);
return -(p.z / p.w);
}
void main() {
float z = viewZ(v_uv);
// The circle of confusion, signed so the two sides of the focal plane can be told apart, and
// normalised by the focus distance: a lens focused at two metres throws the background out
// far harder than one focused at two hundred, and a constant here made a landscape shot read
// as a macro.
float coc = (z - u_focus) / max(z, 0.1) * u_aperture;
float r = clamp(abs(coc), 0.0, 1.0) * u_max_coc;
if (r < 0.75) { o_color = vec4(texture(u_src, v_uv).rgb, 1.0); return; }
// a 16-point spiral, which reads as a lens rather than as a box blur
vec3 acc = texture(u_src, v_uv).rgb;
float wsum = 1.0;
for (int i = 0; i < 16; i++) {
float a = float(i) * 2.3999632; // golden angle
float rr = sqrt((float(i) + 0.5) / 16.0) * r;
vec2 off = vec2(cos(a), sin(a)) * rr * u_texel;
vec2 uv2 = v_uv + off;
// Do not drag a SHARP FOREGROUND pixel into a blurred background: that is the halo every
// naive depth-of-field has round a near object. A tap only counts if it is at least as far
// out of focus as this pixel is.
float z2 = viewZ(uv2);
float coc2 = abs((z2 - u_focus) / max(z2, 0.1) * u_aperture);
float w = smoothstep(0.0, 0.35, coc2 * u_max_coc / max(r, 1e-3));
acc += texture(u_src, uv2).rgb * w;
wsum += w;
}
o_color = vec4(acc / wsum, 1.0);
}

View file

@ -7,14 +7,64 @@ 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() {
vec3 c = texture(u_src, v_uv).rgb;
// 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);
float lb = (luma(n) + luma(s) + luma(e) + luma(w) + lc * 4.0) / 8.0;
float d = clamp((lc - lb) * u_amount, -0.08, 0.08);
vec3 col = c * (1.0 + d / max(lc, 1e-3));
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);

Binary file not shown.

Binary file not shown.

View file

@ -326,7 +326,7 @@ T 638623c2 u_sky 6
T 638623c2 u_tershadow 7
T 638623c2 u_ts_height 8
P 4cf96468 fullscreen.vert sharpen.frag
B 4cf96468 frag 1 840
B 4cf96468 frag 1 844
U 4cf96468 frag u_cascade_vp 0 mat4 5 64
U 4cf96468 frag u_cascade_split 320 float 5 16
U 4cf96468 frag u_cascade_range 400 float 5 16
@ -364,6 +364,7 @@ U 4cf96468 frag u_fog_desat 816 float 1 0
U 4cf96468 frag u_texel 824 vec2 1 0
U 4cf96468 frag u_amount 832 float 1 0
U 4cf96468 frag u_grain 836 float 1 0
U 4cf96468 frag u_fxaa 840 float 1 0
T 4cf96468 u_brdf 2
T 4cf96468 u_irradiance 3
T 4cf96468 u_prefilter 4
@ -3168,3 +3169,52 @@ T 74258189 u_prefilter 5
T 74258189 u_shadow 6
T 74258189 u_tershadow 7
T 74258189 u_ts_height 8
P 5161e6ba fullscreen.vert dof.frag
B 5161e6ba frag 1 916
U 5161e6ba frag u_cascade_vp 0 mat4 5 64
U 5161e6ba frag u_cascade_split 320 float 5 16
U 5161e6ba frag u_cascade_range 400 float 5 16
U 5161e6ba frag u_cascade_texel 480 float 5 16
U 5161e6ba frag u_sun_dir 560 vec3 1 0
U 5161e6ba frag u_sun_color 576 vec3 1 0
U 5161e6ba frag u_cam_pos 592 vec3 1 0
U 5161e6ba frag u_prefilter_levels 604 float 1 0
U 5161e6ba frag u_fog_density 608 float 1 0
U 5161e6ba frag u_fog_height_falloff 612 float 1 0
U 5161e6ba frag u_fog_base 616 float 1 0
U 5161e6ba frag u_clip_y 620 float 1 0
U 5161e6ba frag u_spec_scale 624 float 1 0
U 5161e6ba frag u_sky_rot 632 vec2 1 0
U 5161e6ba frag u_ground_alb 640 vec3 1 0
U 5161e6ba frag u_ground_alb_hi 656 vec3 1 0
U 5161e6ba frag u_ground_hi_y 668 float 1 0
U 5161e6ba frag u_ground_hi_w 672 float 1 0
U 5161e6ba frag u_ibl_scale 688 vec3 1 0
U 5161e6ba frag u_daylight 700 float 1 0
U 5161e6ba frag u_fire_pos 704 vec3 1 0
U 5161e6ba frag u_fire_color 720 vec3 1 0
U 5161e6ba frag u_hand_pos 736 vec3 1 0
U 5161e6ba frag u_hand_color 752 vec3 1 0
U 5161e6ba frag u_hand_dir 768 vec3 1 0
U 5161e6ba frag u_hand_cone 780 float 1 0
U 5161e6ba frag u_ts_origin 784 vec2 1 0
U 5161e6ba frag u_ts_half 792 float 1 0
U 5161e6ba frag u_ts_on 796 float 1 0
U 5161e6ba frag u_force_cascade 800 int 1 0
U 5161e6ba frag u_cloud_shadow 804 float 1 0
U 5161e6ba frag u_time 808 float 1 0
U 5161e6ba frag u_fog_inscatter 812 float 1 0
U 5161e6ba frag u_fog_desat 816 float 1 0
U 5161e6ba frag u_inv_proj 832 mat4 1 0
U 5161e6ba frag u_texel 896 vec2 1 0
U 5161e6ba frag u_focus 904 float 1 0
U 5161e6ba frag u_aperture 908 float 1 0
U 5161e6ba frag u_max_coc 912 float 1 0
T 5161e6ba u_brdf 2
T 5161e6ba u_depth 3
T 5161e6ba u_irradiance 4
T 5161e6ba u_prefilter 5
T 5161e6ba u_shadow 6
T 5161e6ba u_src 7
T 5161e6ba u_tershadow 8
T 5161e6ba u_ts_height 9

View file

@ -55,3 +55,4 @@ terrain.vert|terrain.frag|#define SUN_INLINE;#define NEAR_ONLY;
terrain.vert|tersun.frag|
water.vert|water.frag|
fullscreen.vert|volumetric.frag|
fullscreen.vert|dof.frag|