# ============================================================================ # 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) } } function post_init(render3d_st: mut Render3dState, w: int, h: int) -> void { render3d_st.post_w = w; render3d_st.post_h = h 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_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 } 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) }