ludic/packages/ludic.render3d/post.ludic
Orkuncakilkaya 6289564382 render3d: R3D_VKMEM=<frame> says where the GPU memory is, by owner, and the renderer's big CPU arrays
Every allocation carries the owner its maker was wrapped in (models, terrain, impostor atlases,
scatter, grass, shadow maps, frame targets, sky, water, game textures, made in a frame), and the
report prints each owner's images and buffers, the 25 largest images and the scatter layers',
streams' and terrain's CPU copies. Nothing drawn changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 15:58:59 +03:00

398 lines
26 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
# ---- 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.
# ---- 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.
# 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.
# ---- 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.
# the LDR image is 10-bit while the output is HDR10: PQ in 8 bits bands
function post_ldr_fmt(render3d_st: Render3dState) -> int { if gpu_hdr_active(render3d_st) { return GL_RGB10_A2 }; return GL_RGBA8 }
# the screen-sized targets go away before post_init makes them at a new size
function post_free(render3d_st: mut Render3dState) -> void {
if render3d_st.post_hdr == null { return }
if render3d_st.post_ms_fbo != 0 { gpu_fb_free(render3d_st, render3d_st.post_ms_fbo); render3d_st.post_ms_fbo = 0 }
target_free(render3d_st, render3d_st.post_hdr); target_free(render3d_st, render3d_st.post_ao); target_free(render3d_st, render3d_st.post_ao_blur); target_free(render3d_st, render3d_st.post_ldr)
target_free(render3d_st, render3d_st.post_depth_copy); target_free(render3d_st, render3d_st.post_prev); target_free(render3d_st, render3d_st.post_scene); target_free(render3d_st, render3d_st.post_vol); target_free(render3d_st, render3d_st.post_dof)
for i in 0 .. len(render3d_st.post_bloom) { target_free(render3d_st, render3d_st.post_bloom[i]) }
render3d_st.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(render3d_st: Render3dState) -> bool { return gpu_msaa_max(render3d_st) > 1 }
function post_set_msaa(render3d_st: mut Render3dState, n: int) -> void {
var want = n
if want < 1 { want = 1 }
if not post_msaa_live(render3d_st) { want = 1 }
if want > gpu_msaa_max(render3d_st) and gpu_msaa_max(render3d_st) >= 1 { want = gpu_msaa_max(render3d_st) }
if want == render3d_st.post_ms_samples { return }
render3d_st.post_ms_samples = want
if render3d_st.post_hdr != null {
let w = render3d_st.post_w; let h = render3d_st.post_h
post_free(render3d_st)
post_init(render3d_st, w, h)
}
}
# the GPU memory it makes is counted as VKM_TARGET (R3D_VKMEM)
function post_init(render3d_st: mut Render3dState, w: int, h: int) -> void {
let was = render3d_st.gvk_tag
render3d_st.gvk_tag = VKM_TARGET
post_init__t(render3d_st, w, h)
render3d_st.gvk_tag = was
}
@alloc_ok("a world being set up (a stream, a layer, water, post, a bake): the map loading or swapping, not a frame")
function post_init__t(render3d_st: mut Render3dState, w: int, h: int) -> void {
render3d_st.post_w = w; render3d_st.post_h = h
# the exposure's two 1x1 targets: made once, here, rather than by the first frame that measured
if render3d_st.post_adapt_t == null {
render3d_st.post_adapt_t = new []Target
for k in 0 .. 2 { push(render3d_st.post_adapt_t, target_new(render3d_st, 1, 1, GL_R32F, GL_RED, GL_FLOAT, false, GL_NEAREST)) }
render3d_st.post_adapt_reset = true
}
render3d_st.post_hdr = target_new(render3d_st, w, h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, true, GL_LINEAR)
if render3d_st.post_ms_samples > 1 {
render3d_st.post_ms_fbo = gpu_fb_new(render3d_st)
gpu_fb_bind(render3d_st, render3d_st.post_ms_fbo)
let rbc = gpu_rb_new(render3d_st)
gpu_rb_storage(render3d_st, rbc, GL_RGBA16F, w, h, render3d_st.post_ms_samples)
gpu_fb_color_rb(render3d_st, 0, rbc)
let rbd = gpu_rb_new(render3d_st)
gpu_rb_storage(render3d_st, rbd, GL_DEPTH_COMPONENT32F, w, h, render3d_st.post_ms_samples)
gpu_fb_depth_rb(render3d_st, rbd)
let st = gpu_fb_status(render3d_st)
if st != GL_FRAMEBUFFER_COMPLETE { print(`r3d: msaa framebuffer incomplete {st}`); render3d_st.post_ms_fbo = 0 }
gpu_fb_bind(render3d_st, 0)
}
render3d_st.post_bloom = new []Target
var bw = w / 2; var bh = h / 2
for i in 0 .. BLOOM_LEVELS {
push(render3d_st.post_bloom, target_new(render3d_st, max(bw, 1), max(bh, 1), GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR))
bw = bw / 2; bh = bh / 2
}
if render3d_st.post_p_down == 0 {
render3d_st.post_p_down = r3d_program(render3d_st, "fullscreen.vert", "bloom_down.frag", "")
render3d_st.post_p_up = r3d_program(render3d_st, "fullscreen.vert", "bloom_up.frag", "")
render3d_st.post_p_tone = r3d_program(render3d_st, "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.
render3d_st.post_ao = target_new(render3d_st, w, h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR)
render3d_st.post_ao_blur = target_new(render3d_st, w, h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR)
if render3d_st.post_p_ao == 0 { render3d_st.post_p_ao = r3d_program(render3d_st, "fullscreen.vert", "ssgi.frag", ""); render3d_st.post_p_ao_blur = r3d_program(render3d_st, "fullscreen.vert", "ssao_blur.frag", "") }
render3d_st.post_ldr = target_new(render3d_st, w, h, post_ldr_fmt(render3d_st), GL_RGBA, GL_UNSIGNED_BYTE, false, GL_LINEAR)
render3d_st.post_ldr_hdr = gpu_hdr_active(render3d_st)
render3d_st.post_depth_copy = target_new(render3d_st, w, h, GL_R8, GL_RED, GL_UNSIGNED_BYTE, true, GL_NEAREST)
render3d_st.post_dof = target_new(render3d_st, w, h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR)
if render3d_st.post_p_dof == 0 { render3d_st.post_p_dof = r3d_program(render3d_st, "fullscreen.vert", "dof.frag", "") }
render3d_st.post_vol = target_new(render3d_st, max(w / 2, 1), max(h / 2, 1), GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR)
if render3d_st.post_p_vol == 0 { render3d_st.post_p_vol = r3d_program(render3d_st, "fullscreen.vert", "volumetric.frag", "") }
render3d_st.post_prev = target_new(render3d_st, w, h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR)
render3d_st.post_scene = target_new(render3d_st, w, h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR)
if render3d_st.post_p_sharp == 0 { render3d_st.post_p_sharp = r3d_program(render3d_st, "fullscreen.vert", "sharpen.frag", "") }
render3d_st.post_sharpen = 1.2
render3d_st.post_grain = float_bits(0.025)
render3d_st.post_ao_radius = 0.7
render3d_st.post_ao_intensity = 1.4
render3d_st.post_ao_strength = 0.8
render3d_st.post_fs = mesh_fullscreen(render3d_st)
render3d_st.post_exposure = 0.36
render3d_st.post_bloom_strength = float_bits(0.06)
render3d_st.post_vignette = 0.35
render3d_st.post_saturation = 1.04
render3d_st.post_contrast = 1.12
render3d_st.post_key = 0.19
render3d_st.post_lum = words(4)
var m = 1; var sz = max(w, h)
while sz > 1 { sz = sz / 2; m += 1 }
render3d_st.post_mips = m
render3d_st.post_adapt = 0.0
}
# 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.
function post_measure(render3d_st: mut Render3dState) -> void {
gpu_tex_bind(render3d_st, GPU_TEX2D, render3d_st.post_hdr.color)
gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
gpu_tex_mips(render3d_st, GPU_TEX2D)
if render3d_st.post_p_adapt == 0 { render3d_st.post_p_adapt = r3d_program(render3d_st, "fullscreen.vert", "adapt.frag", "") }
let next = 1 - render3d_st.post_adapt_i
target_bind(render3d_st, render3d_st.post_adapt_t[next])
gpu_depth_test(render3d_st, false)
gpu_use_program(render3d_st, render3d_st.post_p_adapt)
r3d_bind_2d(render3d_st, render3d_st.post_p_adapt, "u_scene", 0, render3d_st.post_hdr.color)
r3d_bind_2d(render3d_st, render3d_st.post_p_adapt, "u_prev", 1, render3d_st.post_adapt_t[render3d_st.post_adapt_i].color)
u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_adapt, "u_lod"), float(render3d_st.post_mips - 1))
u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_adapt, "u_key"), render3d_st.post_key)
u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_adapt, "u_max"), render3d_st.post_exposure_max)
u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_adapt, "u_rate"), 0.08)
var reset = 0.0
if render3d_st.post_adapt_reset { reset = 1.0; render3d_st.post_adapt_reset = false }
u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_adapt, "u_reset"), reset)
mesh_draw(render3d_st, render3d_st.post_fs)
render3d_st.post_adapt_i = next
gpu_tex_bind(render3d_st, GPU_TEX2D, render3d_st.post_hdr.color)
gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
}
function post_begin_scene(render3d_st: mut Render3dState) -> void {
target_bind(render3d_st, render3d_st.post_hdr)
if render3d_st.post_ms_fbo != 0 { gpu_fb_bind(render3d_st, render3d_st.post_ms_fbo); gpu_multisample(render3d_st, true) }
gpu_depth_test(render3d_st, true)
gpu_depth_func(render3d_st, GL_LESS)
gpu_depth_write(render3d_st, true)
gpu_cull(render3d_st, true)
gpu_cull_face(render3d_st, GL_BACK)
gpu_clear_color(render3d_st, 0.0, 0.0, 0.0, 1.0)
gpu_clear(render3d_st, GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
}
# resolve the multisampled scene into the plain HDR target (colour + depth)
function post_resolve(render3d_st: mut Render3dState) -> void {
if render3d_st.post_ms_fbo != 0 {
gpu_fb_bind_read(render3d_st, render3d_st.post_ms_fbo)
gpu_fb_bind_draw(render3d_st, render3d_st.post_hdr.fbo)
gpu_blit(render3d_st, render3d_st.post_w, render3d_st.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(render3d_st, render3d_st.post_hdr.fbo)
gpu_fb_bind_draw(render3d_st, render3d_st.post_depth_copy.fbo)
gpu_blit(render3d_st, render3d_st.post_w, render3d_st.post_h, GL_DEPTH_BUFFER_BIT)
gpu_fb_bind(render3d_st, 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(render3d_st: mut Render3dState) -> void {
gpu_fb_bind_read(render3d_st, render3d_st.post_hdr.fbo)
gpu_fb_bind_draw(render3d_st, render3d_st.post_scene.fbo)
gpu_blit(render3d_st, render3d_st.post_w, render3d_st.post_h, GL_COLOR_BUFFER_BIT)
gpu_fb_bind(render3d_st, render3d_st.post_hdr.fbo)
gpu_viewport(render3d_st, 0, 0, render3d_st.post_w, render3d_st.post_h)
}
# Keep a copy of the finished scene colour: the SSGI bounce reads last frame's colour.
function post_capture_prev(render3d_st: mut Render3dState) -> void {
gpu_fb_bind_read(render3d_st, render3d_st.post_hdr.fbo)
gpu_fb_bind_draw(render3d_st, render3d_st.post_prev.fbo)
gpu_blit(render3d_st, render3d_st.post_w, render3d_st.post_h, GL_COLOR_BUFFER_BIT)
gpu_fb_bind(render3d_st, 0)
render3d_st.post_frame += 1
}
function post_ssao_pass(render3d_st: mut Render3dState) -> void {
gpu_depth_test(render3d_st, false)
gpu_blend(render3d_st, false)
target_bind(render3d_st, render3d_st.post_ao)
gpu_use_program(render3d_st, render3d_st.post_p_ao)
r3d_bind_2d(render3d_st, render3d_st.post_p_ao, "u_depth", 0, render3d_st.post_hdr.depth)
r3d_bind_2d(render3d_st, render3d_st.post_p_ao, "u_prev_color", 1, render3d_st.post_prev.color)
u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_ao, "u_frame"), float(render3d_st.post_frame % 64))
u_mat4(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_ao, "u_inv_proj"), render3d_st.cam_inv_proj)
u_mat4(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_ao, "u_proj"), render3d_st.cam_proj)
u_f2(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_ao, "u_texel"), 1.0 / float(render3d_st.post_w), 1.0 / float(render3d_st.post_h))
u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_ao, "u_radius"), render3d_st.post_ao_radius)
u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_ao, "u_intensity"), render3d_st.post_ao_intensity)
var contact = render3d_st.post_contact
if r3d_env_has(render3d_st, "R3D_NOCONTACT") { contact = 0.0 }
u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_ao, "u_contact"), contact)
mesh_draw(render3d_st, render3d_st.post_fs)
target_bind(render3d_st, render3d_st.post_ao_blur)
gpu_use_program(render3d_st, render3d_st.post_p_ao_blur)
r3d_bind_2d(render3d_st, render3d_st.post_p_ao_blur, "u_ao", 0, render3d_st.post_ao.color)
r3d_bind_2d(render3d_st, render3d_st.post_p_ao_blur, "u_depth", 1, render3d_st.post_hdr.depth)
u_f2(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_ao_blur, "u_texel"), 1.0 / float(render3d_st.post_ao.w), 1.0 / float(render3d_st.post_ao.h))
mesh_draw(render3d_st, render3d_st.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(render3d_st: mut Render3dState) -> void {
if render3d_st.post_vol_steps <= 0.0 { return }
if r3d_env_has(render3d_st, "R3D_NOVOL") { return }
gpu_depth_test(render3d_st, false)
gpu_blend(render3d_st, false)
target_bind(render3d_st, render3d_st.post_vol)
gpu_use_program(render3d_st, render3d_st.post_p_vol)
r3d_bind_2d(render3d_st, render3d_st.post_p_vol, "u_depth", 0, render3d_st.post_hdr.depth)
shadow_bind(render3d_st, render3d_st.post_p_vol)
sky_bind_lighting(render3d_st, render3d_st.post_p_vol)
sky_bind_rot(render3d_st, render3d_st.post_p_vol)
fog_bind(render3d_st, render3d_st.post_p_vol)
u_mat4(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_vol, "u_inv_vp"), render3d_st.cam_inv_vp)
u_v3(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_vol, "u_cam_pos"), render3d_st.cam_pos)
u_v3(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_vol, "u_sun_dir"), render3d_st.sun_dir)
u_v3(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_vol, "u_sun_color"), render3d_st.sun_color)
u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_vol, "u_vol_steps"), render3d_st.post_vol_steps)
u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_vol, "u_vol_density"), render3d_st.post_vol_density)
u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_vol, "u_vol_falloff"), render3d_st.post_vol_falloff)
u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_vol, "u_vol_far"), render3d_st.post_vol_far)
u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_vol, "u_vol_g"), render3d_st.post_vol_g)
u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_vol, "u_vol_mist"), render3d_st.post_vol_mist)
u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_vol, "u_vol_mist_h"), render3d_st.post_vol_mist_h)
mesh_draw(render3d_st, render3d_st.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(render3d_st, render3d_st.post_hdr)
gpu_blend(render3d_st, true)
gpu_blend_func(render3d_st, GL_ONE, GL_ONE)
gpu_use_program(render3d_st, render3d_st.post_p_up)
r3d_bind_2d(render3d_st, render3d_st.post_p_up, "u_src", 0, render3d_st.post_vol.color)
u_f2(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_up, "u_texel"), 1.0 / float(render3d_st.post_vol.w), 1.0 / float(render3d_st.post_vol.h))
u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_up, "u_radius"), 1.0)
mesh_draw(render3d_st, render3d_st.post_fs)
gpu_blend(render3d_st, 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(render3d_st: mut Render3dState) -> void {
if render3d_st.post_dof_aperture == 0.0 { return }
gpu_depth_test(render3d_st, false)
gpu_blend(render3d_st, false)
target_bind(render3d_st, render3d_st.post_dof)
gpu_use_program(render3d_st, render3d_st.post_p_dof)
r3d_bind_2d(render3d_st, render3d_st.post_p_dof, "u_src", 0, render3d_st.post_color)
r3d_bind_2d(render3d_st, render3d_st.post_p_dof, "u_depth", 1, render3d_st.post_hdr.depth)
u_mat4(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_dof, "u_inv_proj"), render3d_st.cam_inv_proj)
u_f2(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_dof, "u_texel"), 1.0 / float(render3d_st.post_w), 1.0 / float(render3d_st.post_h))
u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_dof, "u_focus"), render3d_st.post_dof_focus)
u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_dof, "u_aperture"), render3d_st.post_dof_aperture)
u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_dof, "u_max_coc"), render3d_st.post_dof_max)
mesh_draw(render3d_st, render3d_st.post_fs)
render3d_st.post_color = render3d_st.post_dof.color
}
function post_bloom_pass(render3d_st: mut Render3dState) -> void {
gpu_depth_test(render3d_st, false)
gpu_blend(render3d_st, false)
var src = render3d_st.post_color
var sw = render3d_st.post_color_w; var sh = render3d_st.post_color_h
gpu_use_program(render3d_st, render3d_st.post_p_down)
for i in 0 .. BLOOM_LEVELS {
let t = render3d_st.post_bloom[i]
target_bind(render3d_st, t)
r3d_bind_2d(render3d_st, render3d_st.post_p_down, "u_src", 0, src)
u_f2(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_down, "u_texel"), 1.0 / float(sw), 1.0 / float(sh))
var th = -1.0
if i == 0 { th = 1.2 }
u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_down, "u_threshold"), th)
mesh_draw(render3d_st, render3d_st.post_fs)
src = t.color; sw = t.w; sh = t.h
}
gpu_use_program(render3d_st, render3d_st.post_p_up)
gpu_blend(render3d_st, true)
gpu_blend_func(render3d_st, GL_ONE, GL_ONE)
var i = BLOOM_LEVELS - 1
while i > 0 {
let from = render3d_st.post_bloom[i]
let to = render3d_st.post_bloom[i - 1]
target_bind(render3d_st, to)
r3d_bind_2d(render3d_st, render3d_st.post_p_up, "u_src", 0, from.color)
u_f2(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_up, "u_texel"), 1.0 / float(from.w), 1.0 / float(from.h))
u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_up, "u_radius"), 1.0)
mesh_draw(render3d_st, render3d_st.post_fs)
i -= 1
}
gpu_blend(render3d_st, false)
}
function post_tonemap(render3d_st: mut Render3dState, color_tex: int) -> void {
var prog = render3d_st.post_p_tone
if gpu_hdr_active(render3d_st) {
if render3d_st.post_p_tone_hdr == 0 { render3d_st.post_p_tone_hdr = r3d_program(render3d_st, "fullscreen.vert", "tonemap.frag", "#define HDR10\n") }
prog = render3d_st.post_p_tone_hdr
}
if render3d_st.post_ldr_hdr != gpu_hdr_active(render3d_st) {
let w = render3d_st.post_ldr.w; let h = render3d_st.post_ldr.h
target_free(render3d_st, render3d_st.post_ldr)
render3d_st.post_ldr = target_new(render3d_st, w, h, post_ldr_fmt(render3d_st), GL_RGBA, GL_UNSIGNED_BYTE, false, GL_LINEAR)
render3d_st.post_ldr_hdr = gpu_hdr_active(render3d_st)
}
if render3d_st.post_auto { post_measure(render3d_st) }
target_bind(render3d_st, render3d_st.post_ldr)
gpu_depth_test(render3d_st, false)
gpu_use_program(render3d_st, prog)
r3d_bind_2d(render3d_st, prog, "u_hdr", 0, color_tex)
r3d_bind_2d(render3d_st, prog, "u_bloom", 1, render3d_st.post_bloom[0].color)
r3d_bind_2d(render3d_st, prog, "u_ao", 2, render3d_st.post_ao_blur.color)
u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_ao_strength"), render3d_st.post_ao_strength)
u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_gi_strength"), render3d_st.post_gi_strength)
u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_exposure"), render3d_st.post_exposure)
var auto = 0.0
if render3d_st.post_auto and render3d_st.post_adapt_t != null { auto = 1.0; r3d_bind_2d(render3d_st, prog, "u_adapt", 3, render3d_st.post_adapt_t[render3d_st.post_adapt_i].color) }
u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_auto"), auto)
u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_bloom_strength"), float_from_bits(render3d_st.post_bloom_strength))
u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_vignette"), render3d_st.post_vignette)
u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_saturation"), render3d_st.post_saturation)
u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_contrast"), render3d_st.post_contrast)
u_f3(render3d_st, gpu_uniform(render3d_st, prog, "u_wb"), render3d_st.post_wb_r, render3d_st.post_wb_g, render3d_st.post_wb_b)
u_f3(render3d_st, gpu_uniform(render3d_st, prog, "u_lift"), render3d_st.post_lift_r, render3d_st.post_lift_g, render3d_st.post_lift_b)
u_f3(render3d_st, gpu_uniform(render3d_st, prog, "u_gain"), render3d_st.post_gain_r, render3d_st.post_gain_g, render3d_st.post_gain_b)
# the HDR10 variant's display calibration (the SDR program has none of these, and -1 sets nothing)
u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_hdr_peak"), r3d_hdr_peak_nits(render3d_st))
u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_hdr_paper"), r3d_hdr_paper_nits(render3d_st))
u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_hdr_black"), r3d_hdr_black_nits(render3d_st))
mesh_draw(render3d_st, render3d_st.post_fs)
# sharpen + grain onto the screen
gpu_fb_bind(render3d_st, gpu_screen_fb(render3d_st))
gpu_viewport(render3d_st, 0, 0, gl_width(), gl_height())
gpu_use_program(render3d_st, render3d_st.post_p_sharp)
r3d_bind_2d(render3d_st, render3d_st.post_p_sharp, "u_src", 0, render3d_st.post_ldr.color)
u_f2(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_sharp, "u_texel"), 1.0 / float(render3d_st.post_ldr.w), 1.0 / float(render3d_st.post_ldr.h))
u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_sharp, "u_amount"), render3d_st.post_sharpen)
var fx = render3d_st.post_fxaa
if r3d_env_has(render3d_st, "R3D_NOFXAA") { fx = 0.0 }
u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.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 = render3d_st.post_grain
if r3d_env_has(render3d_st, "R3D_NOGRAIN") { grain = float_bits(0.0) }
u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_sharp, "u_grain"), float_from_bits(grain))
u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_sharp, "u_time"), render3d_st.r3d_time)
mesh_draw(render3d_st, render3d_st.post_fs)
}