The existing occlusion is tuned as AMBIENT occlusion: a wide radius answering "how open is the sky here". That is the right question, and it leaves every object in the frame hovering - because what says a log is ON the ground rather than in front of it is a hard, narrow darkening in the last few centimetres where the two meet, and at a metre and a half of radius that darkening is spread so thin it is not there. A second, tight pass: eight taps inside 40 cm, which at any normal distance is a handful of pixels and stays in cache, with a much tighter range check than the ambient one so a wall across the room does not darken the floor in front of it. Small by frame area, because contact occlusion is - 1.8% of the frame, 7% of the region around the things it grounds. GL 7.0 -> 7.1 s, VK 7.3 s over 400 frames. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
461 lines
21 KiB
Text
461 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_contact: int = 0x3FA00000 # 1.25: how hard a thing is darkened where it meets the ground
|
|
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)
|
|
var contact = post_contact
|
|
if r3d_env_has("R3D_NOCONTACT") { contact = F_ZERO }
|
|
u_f(gpu_uniform(post_p_ao, "u_contact"), contact)
|
|
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)
|
|
}
|