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>
457 lines
21 KiB
Text
457 lines
21 KiB
Text
# ============================================================================
|
|
# post.ludic — the HDR frame and what happens to it: a 16-bit float scene
|
|
# target, a mip-chain bloom (13-tap down, tent up), and the tonemap composite
|
|
# (exposure, ACES, vignette, saturation, contrast, dither) to the screen.
|
|
# ============================================================================
|
|
|
|
const BLOOM_LEVELS: int = 6
|
|
|
|
var post_hdr: Target = null
|
|
var post_ms_fbo: int = 0 # 4x multisampled scene target, resolved into post_hdr
|
|
var post_ms_samples: int = 1 # temporal AA carries the edges; R3D_MSAA=n to compare
|
|
var post_bloom: []Target = null
|
|
var post_p_down: int = 0
|
|
var post_p_up: int = 0
|
|
var post_p_tone: int = 0
|
|
var post_fs: Mesh = null
|
|
var post_exposure: int = 0
|
|
var post_bloom_strength: int = 0
|
|
var post_vignette: int = 0
|
|
var post_saturation: int = 0
|
|
var post_contrast: int = 0
|
|
var post_w: int = 0
|
|
var post_h: int = 0
|
|
var post_auto: bool = true
|
|
var post_key: int = 0 # target mean luminance after exposure (float bits)
|
|
var post_lum: words = null
|
|
var post_mips: int = 0
|
|
var post_adapt: int = 0 # smoothed exposure (float bits)
|
|
var post_exposure_max: int = 0x41A00000 # 20: the ceiling auto-exposure may reach (night lowers it)
|
|
# ---- the grade ------------------------------------------------------------------------------
|
|
# White balance, the shadows' floor and the highlights' gain. These were nine literals bound at
|
|
# the draw, so the game had the same colour at seven in the morning as at one in the afternoon -
|
|
# every knob a grade needs, and no hand on any of them. daylight.ludic owns them now and writes
|
|
# them from the sun's own elevation; the values here are what they used to be hard-coded to, so a
|
|
# program that never starts a day looks exactly as it did.
|
|
# They are plain ints rather than a v3 on purpose: a grade is written by daylight_set, which can
|
|
# run before post_init has allocated anything, and nine ints cannot be null.
|
|
var post_wb_r: int = 0x3F828F5C # 1.02
|
|
var post_wb_g: int = 0x3F800000 # 1.0
|
|
var post_wb_b: int = 0x3F7851EC # 0.97
|
|
var post_lift_r: int = 0x3B83126F # 0.004
|
|
var post_lift_g: int = 0x3B83126F # 0.004
|
|
var post_lift_b: int = 0x3C449BA6 # 0.012
|
|
var post_gain_r: int = 0x3F7D70A4 # 0.99
|
|
var post_gain_g: int = 0x3F7EB852 # 0.995
|
|
var post_gain_b: int = 0x3F800000 # 1.0
|
|
|
|
var post_ao: Target = null
|
|
var post_ao_blur: Target = null
|
|
var post_p_ao: int = 0
|
|
var post_p_ao_blur: int = 0
|
|
var post_ao_radius: int = 0
|
|
var post_ao_intensity: int = 0
|
|
var post_ao_strength: int = 0
|
|
var post_gi_strength: int = 0x3ECCCCCD # 0.4
|
|
var post_no_gi: bool = false
|
|
var post_ldr: Target = null
|
|
var post_depth_copy: Target = null
|
|
# ---- volumetric light ----------------------------------------------------------------------
|
|
# Half resolution on purpose: in-scattered light is smooth, a shaft has no sharp edge, and the
|
|
# march is the whole cost of the pass. post_vol_steps is the quality dial; 0 switches it off
|
|
# and the pass is skipped entirely rather than run at one step.
|
|
var post_vol: Target = null
|
|
var post_p_vol: int = 0
|
|
var post_vol_steps: int = 0x41C00000 # 24
|
|
var post_vol_density: int = 0x3B03126F # 0.002
|
|
var post_vol_falloff: int = 0x3BA3D70A # 0.005
|
|
var post_vol_far: int = 0x44480000 # 800 m
|
|
var post_vol_g: int = 0x3F19999A # 0.6: air throws light forward
|
|
var post_vol_mist: int = 0 # the day sets these two
|
|
var post_vol_mist_h: int = 0x42200000 # 40 m
|
|
var post_prev: Target = null # last frame's scene colour, for the SSGI bounce only
|
|
var post_scene: Target = null # this frame's scene colour before the water, for refraction
|
|
var post_frame: int = 0
|
|
var post_color_w: int = 0 # its size: the display's when DLSS upscaled it
|
|
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
|
|
function post_ldr_fmt() -> int { if gpu_hdr_active() { return GL_RGB10_A2 }; return GL_RGBA8 }
|
|
var post_sharpen: int = 0
|
|
var post_grain: int = 0
|
|
|
|
# the screen-sized targets go away before post_init makes them at a new size
|
|
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_dof)
|
|
for i in 0 .. len(post_bloom) { target_free(post_bloom[i]) }
|
|
post_hdr = null
|
|
}
|
|
# Multisampled scene: 1 (temporal AA alone), 2 or 4, remade at once. Vulkan draws it into
|
|
# multisampled renderbuffers and resolves them in a pass; a device that cannot take the count asked
|
|
# for gets the most it can (gpu_msaa_max).
|
|
function post_msaa_live() -> bool { return gpu_is_gl() or gpu_msaa_max() > 1 }
|
|
function post_set_msaa(n: int) -> void {
|
|
var want = n
|
|
if want < 1 { want = 1 }
|
|
if not post_msaa_live() { want = 1 }
|
|
if want > gpu_msaa_max() and gpu_msaa_max() >= 1 { want = gpu_msaa_max() }
|
|
if want == post_ms_samples { return }
|
|
post_ms_samples = want
|
|
if post_hdr != null {
|
|
let w = post_w; let h = post_h
|
|
post_free()
|
|
post_init(w, h)
|
|
}
|
|
}
|
|
|
|
function post_init(w: int, h: int) -> void {
|
|
post_w = w; post_h = h
|
|
post_hdr = target_new(w, h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, true, GL_LINEAR)
|
|
if post_ms_samples > 1 {
|
|
post_ms_fbo = gpu_fb_new()
|
|
gpu_fb_bind(post_ms_fbo)
|
|
let rbc = gpu_rb_new()
|
|
gpu_rb_storage(rbc, GL_RGBA16F, w, h, post_ms_samples)
|
|
gpu_fb_color_rb(0, rbc)
|
|
let rbd = gpu_rb_new()
|
|
gpu_rb_storage(rbd, GL_DEPTH_COMPONENT32F, w, h, post_ms_samples)
|
|
gpu_fb_depth_rb(rbd)
|
|
let st = gpu_fb_status()
|
|
if st != GL_FRAMEBUFFER_COMPLETE { print(`r3d: msaa framebuffer incomplete {st}`); post_ms_fbo = 0 }
|
|
gpu_fb_bind(0)
|
|
}
|
|
post_bloom = new []Target
|
|
var bw = w / 2; var bh = h / 2
|
|
for i in 0 .. BLOOM_LEVELS {
|
|
push(post_bloom, target_new(max(bw, 1), max(bh, 1), GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR))
|
|
bw = bw / 2; bh = bh / 2
|
|
}
|
|
if post_p_down == 0 {
|
|
post_p_down = r3d_program("fullscreen.vert", "bloom_down.frag", "")
|
|
post_p_up = r3d_program("fullscreen.vert", "bloom_up.frag", "")
|
|
post_p_tone = r3d_program("fullscreen.vert", "tonemap.frag", "")
|
|
}
|
|
# Full resolution, not half. The occlusion is reconstructed from depth differences,
|
|
# so on a surface seen at a grazing angle its gradient is steep in screen space; at
|
|
# half resolution that aliased into wide, screen-crossing bands which the bilinear
|
|
# upsample in the tonemapper then stretched over the whole ground. They read as thin
|
|
# transparent black bars, appear only where there is depth (never on the sky), and
|
|
# are nothing to do with the shadow map or the reflection.
|
|
post_ao = target_new(w, h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR)
|
|
post_ao_blur = target_new(w, h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR)
|
|
if post_p_ao == 0 { post_p_ao = r3d_program("fullscreen.vert", "ssgi.frag", ""); post_p_ao_blur = r3d_program("fullscreen.vert", "ssao_blur.frag", "") }
|
|
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)
|
|
post_scene = target_new(w, h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR)
|
|
if post_p_sharp == 0 { post_p_sharp = r3d_program("fullscreen.vert", "sharpen.frag", "") }
|
|
post_sharpen = fl(1.2)
|
|
post_grain = fl(0.025)
|
|
post_ao_radius = fl(0.7)
|
|
post_ao_intensity = fl(1.4)
|
|
post_ao_strength = fl(0.8)
|
|
post_fs = mesh_fullscreen()
|
|
post_exposure = fl(0.36)
|
|
post_bloom_strength = fl(0.06)
|
|
post_vignette = fl(0.35)
|
|
post_saturation = fl(1.04)
|
|
post_contrast = fl(1.12)
|
|
post_key = fl(0.19)
|
|
post_lum = words(4)
|
|
var m = 1; var sz = max(w, h)
|
|
while sz > 1 { sz = sz / 2; m += 1 }
|
|
post_mips = m
|
|
post_adapt = F_ZERO
|
|
}
|
|
|
|
# Mean scene luminance from the HDR mip chain -> exposure = key / mean, eased over
|
|
# frames. The value comes back through a pixel buffer one frame late: a direct
|
|
# glGetTexImage waits for the GPU to finish the whole frame, which serialised the
|
|
# CPU and the GPU. With the fly-camera demo that cost little (the CPU had nothing
|
|
# else to do); with the game's animals, HUD and rules on the CPU it doubled the frame
|
|
# (60 ms -> 28 ms when the read went asynchronous, measured 2026-09-09).
|
|
# ... and even that asynchronous read blocked on Apple's GL (glGetTexImage into a pixel
|
|
# buffer still synchronised the texture: 50% of the CPU's frame waiting, sampled), so
|
|
# the adaptation now stays on the GPU: a 1x1 pass (adapt.frag) eases last frame's value
|
|
# toward key / mean and the tonemapper samples it. The CPU never waits for the picture.
|
|
var post_adapt_t: []Target = null
|
|
var post_adapt_i: int = 0
|
|
var post_p_adapt: int = 0
|
|
var post_adapt_reset: bool = true
|
|
function post_measure() -> void {
|
|
gpu_tex_bind(GPU_TEX2D, post_hdr.color)
|
|
gpu_tex_param(GPU_TEX2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
|
|
gpu_tex_mips(GPU_TEX2D)
|
|
if post_adapt_t == null {
|
|
post_adapt_t = new []Target
|
|
for k in 0 .. 2 { push(post_adapt_t, target_new(1, 1, GL_R32F, GL_RED, GL_FLOAT, false, GL_NEAREST)) }
|
|
post_adapt_reset = true
|
|
}
|
|
if post_p_adapt == 0 { post_p_adapt = r3d_program("fullscreen.vert", "adapt.frag", "") }
|
|
let next = 1 - post_adapt_i
|
|
target_bind(post_adapt_t[next])
|
|
gpu_depth_test(false)
|
|
gpu_use_program(post_p_adapt)
|
|
r3d_bind_2d(post_p_adapt, "u_scene", 0, post_hdr.color)
|
|
r3d_bind_2d(post_p_adapt, "u_prev", 1, post_adapt_t[post_adapt_i].color)
|
|
u_f(gpu_uniform(post_p_adapt, "u_lod"), fi(post_mips - 1))
|
|
u_f(gpu_uniform(post_p_adapt, "u_key"), post_key)
|
|
u_f(gpu_uniform(post_p_adapt, "u_max"), post_exposure_max)
|
|
u_f(gpu_uniform(post_p_adapt, "u_rate"), fl(0.08))
|
|
var reset = F_ZERO
|
|
if post_adapt_reset { reset = F_ONE; post_adapt_reset = false }
|
|
u_f(gpu_uniform(post_p_adapt, "u_reset"), reset)
|
|
mesh_draw(post_fs)
|
|
post_adapt_i = next
|
|
gpu_tex_bind(GPU_TEX2D, post_hdr.color)
|
|
gpu_tex_param(GPU_TEX2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
|
|
}
|
|
|
|
function post_begin_scene() -> void {
|
|
target_bind(post_hdr)
|
|
if post_ms_fbo != 0 { gpu_fb_bind(post_ms_fbo); gpu_multisample(true) }
|
|
gpu_depth_test(true)
|
|
gpu_depth_func(GL_LESS)
|
|
gpu_depth_write(true)
|
|
gpu_cull(true)
|
|
gpu_cull_face(GL_BACK)
|
|
gpu_clear_color(0.0, 0.0, 0.0, 1.0)
|
|
gpu_clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
|
|
}
|
|
|
|
# resolve the multisampled scene into the plain HDR target (colour + depth)
|
|
function post_resolve() -> void {
|
|
if post_ms_fbo != 0 {
|
|
gpu_fb_bind_read(post_ms_fbo)
|
|
gpu_fb_bind_draw(post_hdr.fbo)
|
|
gpu_blit(post_w, post_h, GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
|
|
}
|
|
# the depth copy every pass after this may read while the frame is still being drawn into
|
|
gpu_fb_bind_read(post_hdr.fbo)
|
|
gpu_fb_bind_draw(post_depth_copy.fbo)
|
|
gpu_blit(post_w, post_h, GL_DEPTH_BUFFER_BIT)
|
|
gpu_fb_bind(0)
|
|
}
|
|
|
|
# There is no temporal anti-aliasing. It was reprojecting every pixel through the
|
|
# scene depth, which on water is the surface plane while the pixel's content is the
|
|
# reflection behind it — so the mirror image was fetched from the wrong place and, at
|
|
# 0.92 history, dragged several frames behind the camera as it turned. Geometry edges
|
|
# and the alpha-tested vegetation are covered by the 4x MSAA + alpha-to-coverage the
|
|
# scene already renders with, and the projection is no longer jittered, so nothing is
|
|
# left needing a temporal resolve.
|
|
|
|
# The lake bed, as drawn, before any water goes over it. Water reads this to refract and
|
|
# then absorb it, which is what makes the surface read as a body of water rather than a
|
|
# sheet laid over the ground: the bottom is seen THROUGH the water, tinted and dimmed by
|
|
# how far the light travelled, instead of being the dry terrain showing through an alpha.
|
|
function post_capture_scene() -> void {
|
|
gpu_fb_bind_read(post_hdr.fbo)
|
|
gpu_fb_bind_draw(post_scene.fbo)
|
|
gpu_blit(post_w, post_h, GL_COLOR_BUFFER_BIT)
|
|
gpu_fb_bind(post_hdr.fbo)
|
|
gpu_viewport(0, 0, post_w, post_h)
|
|
}
|
|
|
|
# Keep a copy of the finished scene colour: the SSGI bounce reads last frame's colour.
|
|
function post_capture_prev() -> void {
|
|
gpu_fb_bind_read(post_hdr.fbo)
|
|
gpu_fb_bind_draw(post_prev.fbo)
|
|
gpu_blit(post_w, post_h, GL_COLOR_BUFFER_BIT)
|
|
gpu_fb_bind(0)
|
|
post_frame += 1
|
|
}
|
|
|
|
function post_ssao_pass() -> void {
|
|
gpu_depth_test(false)
|
|
gpu_blend(false)
|
|
target_bind(post_ao)
|
|
gpu_use_program(post_p_ao)
|
|
r3d_bind_2d(post_p_ao, "u_depth", 0, post_hdr.depth)
|
|
r3d_bind_2d(post_p_ao, "u_prev_color", 1, post_prev.color)
|
|
u_f(gpu_uniform(post_p_ao, "u_frame"), fi(post_frame % 64))
|
|
u_mat4(gpu_uniform(post_p_ao, "u_inv_proj"), cam_inv_proj)
|
|
u_mat4(gpu_uniform(post_p_ao, "u_proj"), cam_proj)
|
|
u_f2(gpu_uniform(post_p_ao, "u_texel"), fr(1, post_w), fr(1, post_h))
|
|
u_f(gpu_uniform(post_p_ao, "u_radius"), post_ao_radius)
|
|
u_f(gpu_uniform(post_p_ao, "u_intensity"), post_ao_intensity)
|
|
mesh_draw(post_fs)
|
|
target_bind(post_ao_blur)
|
|
gpu_use_program(post_p_ao_blur)
|
|
r3d_bind_2d(post_p_ao_blur, "u_ao", 0, post_ao.color)
|
|
r3d_bind_2d(post_p_ao_blur, "u_depth", 1, post_hdr.depth)
|
|
u_f2(gpu_uniform(post_p_ao_blur, "u_texel"), fr(1, post_ao.w), fr(1, post_ao.h))
|
|
mesh_draw(post_fs)
|
|
}
|
|
|
|
# The march, then the composite. It is added to the scene BEFORE bloom on purpose: a shaft of
|
|
# light is a bright thing in the air and should bloom like one, and compositing it after the
|
|
# bloom pyramid would give hard-edged rays with no glow at all.
|
|
function post_volumetric_pass() -> void {
|
|
if post_vol_steps <= 0 { return }
|
|
if r3d_env_has("R3D_NOVOL") { return }
|
|
gpu_depth_test(false)
|
|
gpu_blend(false)
|
|
target_bind(post_vol)
|
|
gpu_use_program(post_p_vol)
|
|
r3d_bind_2d(post_p_vol, "u_depth", 0, post_hdr.depth)
|
|
shadow_bind(post_p_vol)
|
|
sky_bind_lighting(post_p_vol)
|
|
sky_bind_rot(post_p_vol)
|
|
fog_bind(post_p_vol)
|
|
u_mat4(gpu_uniform(post_p_vol, "u_inv_vp"), cam_inv_vp)
|
|
u_v3(gpu_uniform(post_p_vol, "u_cam_pos"), cam_pos)
|
|
u_v3(gpu_uniform(post_p_vol, "u_sun_dir"), sun_dir)
|
|
u_v3(gpu_uniform(post_p_vol, "u_sun_color"), sun_color)
|
|
u_f(gpu_uniform(post_p_vol, "u_vol_steps"), post_vol_steps)
|
|
u_f(gpu_uniform(post_p_vol, "u_vol_density"), post_vol_density)
|
|
u_f(gpu_uniform(post_p_vol, "u_vol_falloff"), post_vol_falloff)
|
|
u_f(gpu_uniform(post_p_vol, "u_vol_far"), post_vol_far)
|
|
u_f(gpu_uniform(post_p_vol, "u_vol_g"), post_vol_g)
|
|
u_f(gpu_uniform(post_p_vol, "u_vol_mist"), post_vol_mist)
|
|
u_f(gpu_uniform(post_p_vol, "u_vol_mist_h"), post_vol_mist_h)
|
|
mesh_draw(post_fs)
|
|
# Composite into the HDR scene with the bloom pyramid's own upsample - a 3x3 tent under
|
|
# ONE/ONE blending, which is exactly what is wanted here and already exists, rather than a
|
|
# blit shader written for one caller. Into post_hdr, not post_scene: post_scene is the copy
|
|
# the water refracts, so adding shafts there would put them UNDER the lake.
|
|
target_bind(post_hdr)
|
|
gpu_blend(true)
|
|
gpu_blend_func(GL_ONE, GL_ONE)
|
|
gpu_use_program(post_p_up)
|
|
r3d_bind_2d(post_p_up, "u_src", 0, post_vol.color)
|
|
u_f2(gpu_uniform(post_p_up, "u_texel"), fr(1, post_vol.w), fr(1, post_vol.h))
|
|
u_f(gpu_uniform(post_p_up, "u_radius"), F_ONE)
|
|
mesh_draw(post_fs)
|
|
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)
|
|
var src = post_color
|
|
var sw = post_color_w; var sh = post_color_h
|
|
gpu_use_program(post_p_down)
|
|
for i in 0 .. BLOOM_LEVELS {
|
|
let t = post_bloom[i]
|
|
target_bind(t)
|
|
r3d_bind_2d(post_p_down, "u_src", 0, src)
|
|
u_f2(gpu_uniform(post_p_down, "u_texel"), fr(1, sw), fr(1, sh))
|
|
var th = f_neg1()
|
|
if i == 0 { th = fl(1.2) }
|
|
u_f(gpu_uniform(post_p_down, "u_threshold"), th)
|
|
mesh_draw(post_fs)
|
|
src = t.color; sw = t.w; sh = t.h
|
|
}
|
|
gpu_use_program(post_p_up)
|
|
gpu_blend(true)
|
|
gpu_blend_func(GL_ONE, GL_ONE)
|
|
var i = BLOOM_LEVELS - 1
|
|
while i > 0 {
|
|
let from = post_bloom[i]
|
|
let to = post_bloom[i - 1]
|
|
target_bind(to)
|
|
r3d_bind_2d(post_p_up, "u_src", 0, from.color)
|
|
u_f2(gpu_uniform(post_p_up, "u_texel"), fr(1, from.w), fr(1, from.h))
|
|
u_f(gpu_uniform(post_p_up, "u_radius"), F_ONE)
|
|
mesh_draw(post_fs)
|
|
i -= 1
|
|
}
|
|
gpu_blend(false)
|
|
}
|
|
|
|
function post_tonemap(color_tex: int) -> void {
|
|
var prog = post_p_tone
|
|
if gpu_hdr_active() {
|
|
if post_p_tone_hdr == 0 { post_p_tone_hdr = r3d_program("fullscreen.vert", "tonemap.frag", "#define HDR10\n") }
|
|
prog = post_p_tone_hdr
|
|
}
|
|
if post_ldr_hdr != gpu_hdr_active() {
|
|
let w = post_ldr.w; let h = post_ldr.h
|
|
target_free(post_ldr)
|
|
post_ldr = target_new(w, h, post_ldr_fmt(), GL_RGBA, GL_UNSIGNED_BYTE, false, GL_LINEAR)
|
|
post_ldr_hdr = gpu_hdr_active()
|
|
}
|
|
if post_auto { post_measure() }
|
|
target_bind(post_ldr)
|
|
gpu_depth_test(false)
|
|
gpu_use_program(prog)
|
|
r3d_bind_2d(prog, "u_hdr", 0, color_tex)
|
|
r3d_bind_2d(prog, "u_bloom", 1, post_bloom[0].color)
|
|
r3d_bind_2d(prog, "u_ao", 2, post_ao_blur.color)
|
|
u_f(gpu_uniform(prog, "u_ao_strength"), post_ao_strength)
|
|
u_f(gpu_uniform(prog, "u_gi_strength"), post_gi_strength)
|
|
u_f(gpu_uniform(prog, "u_exposure"), post_exposure)
|
|
var auto = F_ZERO
|
|
if post_auto and post_adapt_t != null { auto = F_ONE; r3d_bind_2d(prog, "u_adapt", 3, post_adapt_t[post_adapt_i].color) }
|
|
u_f(gpu_uniform(prog, "u_auto"), auto)
|
|
u_f(gpu_uniform(prog, "u_bloom_strength"), post_bloom_strength)
|
|
u_f(gpu_uniform(prog, "u_vignette"), post_vignette)
|
|
u_f(gpu_uniform(prog, "u_saturation"), post_saturation)
|
|
u_f(gpu_uniform(prog, "u_contrast"), post_contrast)
|
|
u_f3(gpu_uniform(prog, "u_wb"), post_wb_r, post_wb_g, post_wb_b)
|
|
u_f3(gpu_uniform(prog, "u_lift"), post_lift_r, post_lift_g, post_lift_b)
|
|
u_f3(gpu_uniform(prog, "u_gain"), post_gain_r, post_gain_g, post_gain_b)
|
|
# the HDR10 variant's display calibration (the SDR program has none of these, and -1 sets nothing)
|
|
u_f(gpu_uniform(prog, "u_hdr_peak"), r3d_hdr_peak_nits())
|
|
u_f(gpu_uniform(prog, "u_hdr_paper"), r3d_hdr_paper_nits())
|
|
u_f(gpu_uniform(prog, "u_hdr_black"), r3d_hdr_black_nits())
|
|
mesh_draw(post_fs)
|
|
# sharpen + grain onto the screen
|
|
gpu_fb_bind(gpu_screen_fb())
|
|
gpu_viewport(0, 0, gl_w, gl_h)
|
|
gpu_use_program(post_p_sharp)
|
|
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 }
|
|
u_f(gpu_uniform(post_p_sharp, "u_grain"), grain)
|
|
u_f(gpu_uniform(post_p_sharp, "u_time"), r3d_time)
|
|
mesh_draw(post_fs)
|
|
}
|