# ============================================================================ # render.ludic — the frame. Shadow cascades, the HDR scene pass (terrain, the # scene's objects, the sky), bloom, and the tonemapped composite to the screen. # The scene (what the game places in the world) hooks in through scene_draw / # scene_draw_casters, which the demo defines. # ============================================================================ var r3d_sky_prog: int = 0 var r3d_fog_density: float = 0.0 var r3d_fog_scale: float = 1.0 # float bits: a setting's multiplier over the density the day sets var r3d_fog_falloff: float = 0.0 var r3d_fog_base: float = 0.0 # the height fog is measured from (float bits); 0 = y = 0 # Both of these are the DAY's, set by daylight_set from the sun's own elevation. The inscatter # is how hard the air scatters the sun forward - nearly nothing at noon, and the glow a ridge # is silhouetted against at dusk. The desaturation is how fast distance takes a surface's own # colour away, which is the term that was missing entirely and is most of why a midday frame # had no depth in it at all. var r3d_fog_inscatter: float = 0.02 # 0.02, what the shader used to hard-code var r3d_fog_desat: float = 1.35 # 1.35 # How much of the visible sky's colour comes from the analytic model rather than from the # photograph (sky.frag). The day fades it in: at night the sky has its own tint, a star field # and a moon, and relighting on top of those would only wash them out. var r3d_sky_relight: float = 0.0 # What the ground reflects back up, low and high, and the height they cross at. The defaults # are Maroon Lake's - a green basin under a grey-rock treeline at about 480 m over the datum. var r3d_ground_alb_r: float = 0.3; var r3d_ground_alb_g: float = 0.34; var r3d_ground_alb_b: float = 0.14 var r3d_ground_hi_r: float = 0.289; var r3d_ground_hi_g: float = 0.28; var r3d_ground_hi_b: float = 0.26 var r3d_ground_hi_y: float = 480.0 # 480 var r3d_ground_hi_w: float = 150.0 # 150 var r3d_time: float = 0.0 var r3d_ready: bool = false # set before r3d_init to build the landscape from a real height map var r3d_dem_path: string = null var r3d_dem_min: float = 0.0 var r3d_dem_max: float = 0.0 var r3d_dem_base: float = 0.0 var r3d_dem_ox: float = 0.0 var r3d_dem_oz: float = 0.0 var r3d_ortho_path: string = null # A plate: no terrain, no grass, no water - the sky, the sun, the shadows and whatever the scene # draws (a lab's stage, ludic.lab). Set before the load; nothing of the landscape is made, so # its hundreds of megabytes of height field, materials and grass never exist. var r3d_plate: bool = false function r3d_plate_mode(on: bool) -> void { r3d_plate = on } # the sky's HDR image, when it is not the default photograph under r3d_assets var r3d_sky_path: string = null var r3d_debug: bool = false var r3d_debug_shadow: bool = false var r3d_debug_max: bool = false var r3d_cloud_shadow: float = 0.5 # 0.5 var r3d_clip_y: float = -2147483600.0 # -2^31: no clipping # R3D_NOPREPASS=1: light the foliage the old way, every card behind the front one included var r3d_prepass_env: int = -1 function r3d_prepass_off() -> bool { if r3d_prepass_env < 0 { r3d_prepass_env = 0; if r3d_env_has("R3D_NOPREPASS") { r3d_prepass_env = 1 } } return r3d_prepass_env == 1 } function fog_bind(prog: int) -> void { u_f(gpu_uniform(prog, "u_clip_y"), r3d_clip_y) u_f(gpu_uniform(prog, "u_spec_scale"), 1.0) u_f(gpu_uniform(prog, "u_fog_density"), r3d_fog_density) u_f(gpu_uniform(prog, "u_fog_height_falloff"), r3d_fog_falloff) u_f(gpu_uniform(prog, "u_fog_base"), r3d_fog_base) u_f(gpu_uniform(prog, "u_fog_inscatter"), r3d_fog_inscatter) u_f(gpu_uniform(prog, "u_fog_desat"), r3d_fog_desat) u_f3(gpu_uniform(prog, "u_ground_alb"), r3d_ground_alb_r, r3d_ground_alb_g, r3d_ground_alb_b) u_f3(gpu_uniform(prog, "u_ground_alb_hi"), r3d_ground_hi_r, r3d_ground_hi_g, r3d_ground_hi_b) u_f(gpu_uniform(prog, "u_ground_hi_y"), r3d_ground_hi_y) u_f(gpu_uniform(prog, "u_ground_hi_w"), r3d_ground_hi_w) var cs = r3d_cloud_shadow if r3d_env_has("R3D_NOCLOUD") { cs = 0.0 } u_f(gpu_uniform(prog, "u_cloud_shadow"), cs) u_f(gpu_uniform(prog, "u_time"), r3d_time) } # profiling switches (environment): R3D_NOSHADOW R3D_NOGI R3D_MSAA=n R3D_NOBLADES R3D_NOCARDS R3D_NOTREES R3D_NEAR=m var r3d_test_frame: int = 0 var r3d_test_resize: int = 0 var r3d_no_shadow: bool = false var r3d_no_trees: bool = false var r3d_no_refl: bool = false function r3d_env_flags() -> void { r3d_no_shadow = r3d_env_has("R3D_NOSHADOW") r3d_no_trees = r3d_env_has("R3D_NOTREES") r3d_no_refl = r3d_env_has("R3D_NOREFL") if r3d_env_has("R3D_DEBUG") { r3d_debug = true } if r3d_env_has("R3D_DBGSHADOW") { r3d_debug_shadow = true } if r3d_env_has("R3D_NOGI") { post_gi_strength = 0.0; post_ao_strength = 0.0; post_no_gi = true } if r3d_env_has("R3D_MSAA") { post_ms_samples = Text.to_int(r3d_env("R3D_MSAA")) } sc_skip_blade = r3d_env_has("R3D_NOBLADES") sc_skip_card = r3d_env_has("R3D_NOCARDS") sc_dbg_lod = r3d_env_has("R3D_LODDBG") if r3d_env_has("R3D_ANISO") { let a = Text.to_int(r3d_env("R3D_ANISO")) tex_anisotropy = 1.0 if a >= 2 { tex_anisotropy = 2.0 } if a >= 4 { tex_anisotropy = 4.0 } if a >= 8 { tex_anisotropy = 8.0 } if a >= 16 { tex_anisotropy = 16.0 } } } # Start the renderer in one call: the window, then every load step in order. A game that shows # a loading screen calls r3d_open, draws its screen, and runs r3d_load_step itself between frames. function r3d_init(w: int, h: int, title: string) -> bool { if not r3d_open(w, h, title) { return false } for i in 0 .. r3d_load_count() { if not r3d_load_step(i) { return false } } return true } # the window and the graphics backend; nothing is baked yet, but a frame can be presented function r3d_open(w: int, h: int, title: string) -> bool { r3d_env_flags() gpu_select() if not gpu_open(w, h, title) { print("r3d: no OpenGL context"); return false } if r3d_env_has("R3D_NOVSYNC") { gpu_vsync(0) } var renderer: string = gpu_renderer_name() print(`r3d: {gl_w}x{gl_h} on {renderer}`) prof_init() cam_init(float(gl_w) / float(gl_h)) return true } # The load, as steps a loading screen can show between: 0 the sky and its image-based light, # 1 - 4 the terrain (the height field and normals, its sun shadow, its materials, its patches and # programs), 5 the shadow maps and the screen targets, 6 the scattered cover and the actors, 7 the # grass and the sky pass. The order is r3d_init's. function r3d_load_count() -> int { return 4 + TERRAIN_INIT_STEPS } function r3d_load_step(i: int) -> bool { let t = i - 1 if i == 0 { var sky = r3d_assets + "/hdri/kloofendal_48d_partly_cloudy_puresky_4k.hdr" if r3d_sky_path != null { sky = r3d_sky_path } if not sky_load(sky) { return false } daylight_init() } else if t >= 0 and t < TERRAIN_INIT_STEPS { if r3d_plate { return true } if t == 0 { if r3d_dem_path != null { terrain_use_dem(r3d_dem_path, r3d_dem_min, r3d_dem_max, r3d_dem_base, r3d_dem_ox, r3d_dem_oz) } if r3d_ortho_path != null { terrain_use_ortho(r3d_ortho_path) } } terrain_init_step(t) } else if i == 1 + TERRAIN_INIT_STEPS { shadow_init() post_init(gl_w, gl_h) } else if i == 2 + TERRAIN_INIT_STEPS { scatter_init() actor_init() } else if i == 3 + TERRAIN_INIT_STEPS { if not r3d_plate { grass_init() } r3d_sky_prog = r3d_program("fullscreen.vert", "sky.frag", "") # The base is NOON's air, and it was tuned when nothing desaturated with distance - so it # was doing its whole job through colour and could not be raised without the valley going # blue. With the desaturation term carrying the depth, the air can be as thick as real air # at 2900 m and the far ridge recedes instead of tinting. r3d_fog_density = 0.00024 r3d_fog_falloff = 0.002 r3d_ready = true gpu_check("r3d init") } return true } function r3d_draw_sky() -> void { gpu_depth_func(GL_LEQUAL) gpu_depth_write(false) gpu_cull(false) let p = r3d_sky_prog gpu_use_program(p) r3d_bind_2d(p, "u_sky", 0, sky_tex) sky_bind_rot(p) sky_bind_lighting(p) u_mat4(gpu_uniform(p, "u_inv_vp"), cam_inv_vp) u_f(gpu_uniform(p, "u_sky_gain"), 0.95) u_f(gpu_uniform(p, "u_sky_relight"), r3d_sky_relight) u_f(gpu_uniform(p, "u_sky_sat"), 1.35) u_f(gpu_uniform(p, "u_time"), r3d_time) mesh_draw(sky_fullscreen) gpu_depth_write(true) gpu_depth_func(GL_LESS) } # The end of the game's frame, whichever backend draws it: a screenshot first if one is wanted, # then the present. A game calls these rather than Gl.swap / Gl.screenshot. function r3d_screenshot(path: string) -> bool { return gpu_screenshot(path) } function r3d_present() -> void { gpu_present() } # the drawable changed size: the camera's aspect and every screen-sized target follow function r3d_resize() -> void { cam_aspect = float(gl_w) / float(gl_h) cam_update() post_free() post_init(gl_w, gl_h) if water_refl != null { target_free(water_refl); water_refl = null } print(`r3d: resized to {gl_w}x{gl_h}`) } var r3d_cam_log: int = -1 function r3d_frame(time: float) -> void { gpu_glcheck_after("the time between frames") outline_frame() if not r3d_ready { return } if gpu_resize_check() { r3d_resize() } # R3D_RESIZE_AT=: rebuild every screen-sized buffer mid-run, as a window resize # or a fullscreen change does. Headless has no window to resize, and this path is where # a stale attachment or a texture freed twice shows up. r3d_test_frame += 1 if r3d_test_resize == 0 and r3d_env_has("R3D_RESIZE_AT") { r3d_test_resize = Text.to_int(r3d_env("R3D_RESIZE_AT")) } # R3D_RESIZE_AT=: from frame n on, rebuild every screen-sized buffer every few # frames at a different size, as dragging a window edge or entering fullscreen does. if r3d_test_resize > 0 and r3d_test_frame >= r3d_test_resize and r3d_test_frame % 4 == 0 { let step = (r3d_test_frame / 4) % 4 var w = 1920; var h = 1080 if step == 1 { w = 1440; h = 810 } if step == 2 { w = 2560; h = 1440 } if step == 3 { w = 1281; h = 721 } gl_w = w; gl_h = h r3d_resize() } r3d_time = time gsl_frame_start() # the frame's counters close here, before any of its own work: the window each of them # covers is exactly one frame, from this point to the same point next time prof_gen_frame() prof_mark_start() cam_begin_frame(post_frame, gl_w, gl_h) # R3D_CAM_LOG=1: the camera each frame, in millimetres and thousandths of a radian - to tell a # camera that moves while the hiker stands still from a picture that shakes on its own if r3d_cam_log < 0 { r3d_cam_log = 0; if r3d_env_has("R3D_CAM_LOG") { r3d_cam_log = 1 } } if r3d_cam_log == 1 { print(`cam {r3d_test_frame} pos {int(cam_pos[0] * 1000.0)} {int(cam_pos[1] * 1000.0)} {int(cam_pos[2] * 1000.0)} yaw {int(cam_yaw * 1000.0)} pitch {int(cam_pitch * 1000.0)} jitter {int(gsl_jitter_x * 1000000.0)} {int(gsl_jitter_y * 1000000.0)} render {post_w}x{post_h} reset {gsl_reset} evalok {gsl_eval_ok} fresh {gsl_fresh}`) } # the height-field shadow rebakes as the light moves in steps (daylight), or with the sky yaw when there is no clock if not r3d_plate and ((not day_on and ter_shadow_yaw != sky_yaw) or ter_shadow_gen != day_gen) { ter_shadow_gen = day_gen let t_bk = gl_now_us() terrain_bake_shadow() prof_bake_add(gl_now_us() - t_bk) } scatter_begin_frame() prof_cpu_mark("shadow rebake") stream_update_all() prof_cpu_mark("streaming") if not r3d_no_shadow { prof_begin("shadow"); shadow_pass(); prof_end() } prof_cpu_mark("shadow pass") if water_on and not r3d_no_refl and water_reflect_visible() { prof_begin("water reflection"); water_reflection_pass(); prof_end() } prof_cpu_mark("reflection") post_begin_scene() # the near tree foliage lays its depth down before anything is shaded, so the terrain # under the stands and the cards behind the front ones are rejected before lighting. # It has to come straight after post_begin_scene: terrain_sun_prepare binds its own # target, and depth drawn after it lands in the sun buffer, not the scene's. if not r3d_prepass_off() { prof_begin("foliage prepass") gpu_color_write(false) scatter_draw_depth() gpu_color_write(true) prof_end() sc_prepass = true } prof_cpu_mark("foliage prepass") if not r3d_plate { prof_begin("terrain sun") terrain_sun_prepare() prof_end() prof_begin("terrain") terrain_draw() prof_end() } prof_cpu_mark("terrain") prof_begin("scene (vegetation)") r3d_scene_draw() prof_end() sc_prepass = false prof_cpu_mark("vegetation") prof_begin("grass") grass_draw() prof_end() prof_cpu_mark("grass") prof_begin("sky") r3d_draw_sky() prof_end() prof_begin("resolve MSAA") post_resolve() prof_end() # transparent water over the resolved frame: it tests against the frame's own # depth and reads a copy of it for the depth tint and soft shores if water_on { prof_begin("water surface") post_capture_scene() target_bind(post_hdr) gpu_depth_test(true) gpu_depth_func(GL_LESS) water_draw(post_depth_copy.depth) prof_end() } post_color = post_hdr.color; post_color_w = post_w; post_color_h = post_h # DLSS super resolution: the lit frame up to the display's size, before anything reads it if gsl_dlss_live() { prof_begin("DLSS"); post_color = gsl_dlss_eval(); prof_end() } # Before bloom and before the tonemap: a shaft of light is a bright thing in the air and # should bloom like one. prof_begin("volumetric"); post_volumetric_pass(); prof_end() prof_begin("dof"); post_dof_pass(); prof_end() if not post_no_gi { prof_begin("SSAO/GI"); post_ssao_pass(); prof_end() } if r3d_debug_max { tex_max(post_hdr.color, post_hdr.w, post_hdr.h, "hdr") } prof_begin("bloom") post_bloom_pass() prof_end() prof_begin("tonemap+exposure") post_tonemap(post_color) prof_end() prof_cpu_mark("post") prof_begin("prev-colour copy") post_capture_prev() prof_end() prof_collect() gpu_check("frame") }