From 3a87d0269664c9de1ddf3ab17179eead82b4187e Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Fri, 25 Sep 2026 17:11:15 +0300 Subject: [PATCH] packages: ludic.ui's render3d backend threads Render3dState; render3d's settable vars (r3d_dem_path, post_*, grass_*, wt_wade_*, ...) are Render3dState's fields again, not lets Co-Authored-By: Claude Opus 5.5 --- packages/ludic.render3d/daylight.ludic | 4 +- packages/ludic.render3d/env.ludic | 53 ++++++++++++++++++++++++++ packages/ludic.render3d/grass.ludic | 11 ++---- packages/ludic.render3d/post.ludic | 36 +++++++---------- packages/ludic.render3d/programs.ludic | 7 ++-- packages/ludic.render3d/render.ludic | 33 +++++----------- packages/ludic.render3d/scatter.ludic | 12 ++---- packages/ludic.render3d/shadow.ludic | 23 +++++------ packages/ludic.render3d/sky.ludic | 6 +-- packages/ludic.render3d/stream.ludic | 4 +- packages/ludic.render3d/terrain.ludic | 6 +-- packages/ludic.render3d/water.ludic | 9 ++--- packages/ludic.ui/render3d.ludic | 44 ++++++++++----------- packages/ludic.ui/render3d_image.ludic | 42 ++++++++++---------- packages/ludic.ui/render3d_nine.ludic | 10 ++--- 15 files changed, 152 insertions(+), 148 deletions(-) diff --git a/packages/ludic.render3d/daylight.ludic b/packages/ludic.render3d/daylight.ludic index 9e5e1ad4..206bba2d 100644 --- a/packages/ludic.render3d/daylight.ludic +++ b/packages/ludic.render3d/daylight.ludic @@ -128,7 +128,7 @@ function daylight_set(render3d_st: mut Render3dState, hours: float) -> void { let lowsun = fog_rise * (1.0 - fog_high) var dens = render3d_st.day_fog_base * (1.0 + 1.5 * lowsun) dens = dens * (1.0 + 1.2 * oc) - render3d_st.r3d_fog_density = dens * render3d_st.day_fog_mul * r3d_fog_scale + render3d_st.r3d_fog_density = dens * render3d_st.day_fog_mul * render3d_st.r3d_fog_scale # how fast the haze thins with height: a settled morning's lies IN the valley, a noon sky is # thin all the way up, so the falloff rises as the sun drops render3d_st.r3d_fog_falloff = 0.002 * (1.0 + 1.6 * lowsun) @@ -193,7 +193,7 @@ function daylight_set(render3d_st: mut Render3dState, hours: float) -> void { # so a before-and-after can be shot from ONE binary at one hour, which is the only kind of # comparison worth looking at, and it joins R3D_NOCLOUD / R3D_NOSHADOW / R3D_NOGI. if r3d_env_has(render3d_st, "R3D_NOAIR") { - render3d_st.r3d_fog_density = render3d_st.day_fog_base * render3d_st.day_fog_mul * r3d_fog_scale + render3d_st.r3d_fog_density = render3d_st.day_fog_base * render3d_st.day_fog_mul * render3d_st.r3d_fog_scale render3d_st.r3d_fog_falloff = 0.002 render3d_st.r3d_fog_inscatter = 0.02 render3d_st.r3d_fog_desat = 0.0 diff --git a/packages/ludic.render3d/env.ludic b/packages/ludic.render3d/env.ludic index bf4cfbdc..047de807 100644 --- a/packages/ludic.render3d/env.ludic +++ b/packages/ludic.render3d/env.ludic @@ -797,6 +797,59 @@ export state Render3dState { wb_refl_always: int = -1 wb_dbg: int = -1 wb_build_us: long = 0 # how long the last water_cells_build took (R3D_REFL_DBG prints it) + grass_blade_tex: int = 0 + grass_blade_cols: int = 8 + grass_push_x: float = 0.0 + grass_push_z: float = 0.0 + grass_push_r: float = 0.0 + post_auto: bool = true + post_contact: float = 1.25 # 1.25: how hard a thing is darkened where it meets the ground + post_vol_steps: float = 24.0 # 24 + post_vol_far: float = 800.0 # 800 m + post_vol_g: float = 0.6 # 0.6: air throws light forward + post_vol_mist_h: float = 40.0 # 40 m + post_fxaa: float = 1.0 + post_dof_focus: float = 10.0 # 10 m + post_dof_aperture: float = 0.0 # 0 = no lens, and no pass + post_dof_max: float = 12.0 # 12 px + r3d_global_defs: string = "" + r3d_fog_scale: float = 1.0 # float bits: a setting's multiplier over the density the day sets + r3d_fog_base: float = 0.0 # the height fog is measured from (float bits); 0 = y = 0 + r3d_ground_alb_r: float = 0.3 + r3d_ground_alb_g: float = 0.34 + r3d_ground_alb_b: float = 0.14 + r3d_ground_hi_r: float = 0.289 + r3d_ground_hi_g: float = 0.28 + r3d_ground_hi_b: float = 0.26 + r3d_ground_hi_y: float = 480.0 # 480 + r3d_ground_hi_w: float = 150.0 # 150 + r3d_dem_path: string = null + r3d_dem_min: float = 0.0 + r3d_dem_max: float = 0.0 + r3d_dem_base: float = 0.0 + r3d_dem_ox: float = 0.0 + r3d_dem_oz: float = 0.0 + r3d_ortho_path: string = null + r3d_debug_max: bool = false + r3d_cloud_shadow: float = 0.5 # 0.5 + sc_a2c: bool = true + sc_bake_flower: bool = false + sc_dbg_blade: int = 0 + sc_skip_flower: bool = false + sh_enabled: bool = true + sh_skip_terrain: bool = false + sh_probe_x: float = 0.0 + sh_probe_y: float = 0.0 + sh_probe_z: float = 0.0 + sky_sun_boost: float = 2.3 # 2.3: the photograph's thin cloud dims its sun; a crisper day wants more + sky_start_yaw: float = 0.0 + STREAM_BUDGET_US: int = 2500 # microseconds of generation per frame; a setting may move it + stream_budget_left: int = 0 + ter_wet: float = 0.0 + ter_dem_blur: float = 0.0 # gaussian texels applied to the survey (0 for lidar; ~3 for 30 m data) + wt_wade_x: float = 0.0 + wt_wade_z: float = 0.0 + wt_wade_s: float = 0.0 } function r3d_dev(render3d_st: mut Render3dState) -> bool { if render3d_st.r3d_dev_v < 0 { if Os.has_env("R3D_DEV") and Os.env("R3D_DEV") != "0" { render3d_st.r3d_dev_v = 1 } else { render3d_st.r3d_dev_v = 0 } } diff --git a/packages/ludic.render3d/grass.ludic b/packages/ludic.render3d/grass.ludic index 59c3adc7..b1e2d321 100644 --- a/packages/ludic.render3d/grass.ludic +++ b/packages/ludic.render3d/grass.ludic @@ -17,15 +17,10 @@ const GRASS_CELL: int = 16 # valley the same blade. A real blade has a midrib, a colour that runs olive to straw, # browning where it has dried and a tip that is its own shape - none of which can be # written down, only photographed. -let grass_blade_tex: int = 0 -let grass_blade_cols: int = 8 # Where a body is standing, and how wide it pushes. The grass has never known the player was # in it: you walked through a meadow and every blade ignored you, which is the single most # noticeable thing missing from every step the game asks you to take. The game sets this each # frame; radius 0 means nobody is there. -let grass_push_x: float = 0.0 -let grass_push_z: float = 0.0 -let grass_push_r: float = 0.0 # Vulkan with multi-draw indirect: every visible tile is a record in one buffer, uploaded once a # frame, and each band draws its records GRASS_CHUNK at a time - one draw for up to 256 tiles. A # record's firstInstance is its place in its chunk times 65536; grass.vert's TILES variant reads that @@ -194,13 +189,13 @@ function grass_draw(render3d_st: mut Render3dState) -> void { u_f(render3d_st, gpu_uniform(render3d_st, p, "u_wind"), render3d_st.grass_wind) # copied into a local first: a global reaching a uniform call is the codegen fault # CLAUDE.md records against u_wade and u_flutter, and it costs a day every time - let btex = grass_blade_tex - let bcols = grass_blade_cols + let btex = render3d_st.grass_blade_tex + let bcols = render3d_st.grass_blade_cols u_f(render3d_st, gpu_uniform(render3d_st, p, "u_blade_cols"), float(bcols)) var bon = 0.0 if btex != 0 { bon = 1.0; r3d_bind_2d(render3d_st, p, "u_blade_tex", 12, btex) } u_f(render3d_st, gpu_uniform(render3d_st, p, "u_blade_tex_on"), bon) - u_f3(render3d_st, gpu_uniform(render3d_st, p, "u_push"), grass_push_x, grass_push_z, grass_push_r) + u_f3(render3d_st, gpu_uniform(render3d_st, p, "u_push"), render3d_st.grass_push_x, render3d_st.grass_push_z, render3d_st.grass_push_r) u_f(render3d_st, gpu_uniform(render3d_st, p, "u_rough_scale"), 1.0) u_v3(render3d_st, gpu_uniform(render3d_st, p, "u_tint"), render3d_st.sc_blade_tint) u_v3(render3d_st, gpu_uniform(render3d_st, p, "u_blade_base"), render3d_st.sc_blade_base) diff --git a/packages/ludic.render3d/post.ludic b/packages/ludic.render3d/post.ludic index 93e775c6..db96777e 100644 --- a/packages/ludic.render3d/post.ludic +++ b/packages/ludic.render3d/post.ludic @@ -6,7 +6,6 @@ const BLOOM_LEVELS: int = 6 -let post_auto: bool = true # ---- 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 - @@ -16,24 +15,15 @@ let post_auto: bool = true # 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. -let post_contact: float = 1.25 # 1.25: how hard a thing is darkened where it meets the ground # ---- 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. -let post_vol_steps: float = 24.0 # 24 -let post_vol_far: float = 800.0 # 800 m -let post_vol_g: float = 0.6 # 0.6: air throws light forward -let post_vol_mist_h: float = 40.0 # 40 m # 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. -let post_fxaa: float = 1.0 # ---- 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. -let post_dof_focus: float = 10.0 # 10 m -let post_dof_aperture: float = 0.0 # 0 = no lens, and no pass -let post_dof_max: float = 12.0 # 12 px # 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 } @@ -236,7 +226,7 @@ function post_ssao_pass(render3d_st: mut Render3dState) -> void { 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 = post_contact + 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) @@ -252,7 +242,7 @@ function post_ssao_pass(render3d_st: mut Render3dState) -> void { # 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 post_vol_steps <= 0.0 { return } + 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) @@ -267,13 +257,13 @@ function post_volumetric_pass(render3d_st: mut Render3dState) -> void { 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"), post_vol_steps) + 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"), post_vol_far) - u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_vol, "u_vol_g"), post_vol_g) + 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"), post_vol_mist_h) + 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 @@ -293,7 +283,7 @@ function post_volumetric_pass(render3d_st: mut Render3dState) -> void { # 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 post_dof_aperture == 0.0 { return } + 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) @@ -302,9 +292,9 @@ function post_dof_pass(render3d_st: mut Render3dState) -> void { 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"), post_dof_focus) - u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_dof, "u_aperture"), post_dof_aperture) - u_f(render3d_st, gpu_uniform(render3d_st, render3d_st.post_p_dof, "u_max_coc"), post_dof_max) + 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 } @@ -355,7 +345,7 @@ function post_tonemap(render3d_st: mut Render3dState, color_tex: int) -> void { 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 post_auto { post_measure(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) @@ -366,7 +356,7 @@ function post_tonemap(render3d_st: mut Render3dState, color_tex: int) -> void { 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 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) } + 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) @@ -387,7 +377,7 @@ function post_tonemap(render3d_st: mut Render3dState, color_tex: int) -> void { 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 = post_fxaa + 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 diff --git a/packages/ludic.render3d/programs.ludic b/packages/ludic.render3d/programs.ludic index fba173a7..c3140758 100644 --- a/packages/ludic.render3d/programs.ludic +++ b/packages/ludic.render3d/programs.ludic @@ -11,7 +11,6 @@ # toolchain and redistributable from their origin, so they are fetched into the project # (`ludic assets`) and read from there. # defines prepended to EVERY program (set before any is built): renderer-wide switches -let r3d_global_defs: string = "" function r3d_set_paths(render3d_st: mut Render3dState, root: string, assets: string) -> void { render3d_st.r3d_root = root; render3d_st.r3d_assets = assets } @@ -50,7 +49,7 @@ function r3d_shader_src(render3d_st: mut Render3dState, name: string, defines: s # ripples in the fragment one, and they have to be reading the same field or the meadow and # the lake disagree about the weather if render3d_st.r3d_wind_src == null { render3d_st.r3d_wind_src = r3d_shader_file(render3d_st, "wind.glsl") } - var s = "#version 410 core\n" + r3d_global_defs + defines + render3d_st.r3d_wind_src + var s = "#version 410 core\n" + render3d_st.r3d_global_defs + defines + render3d_st.r3d_wind_src if is_frag { s = s + render3d_st.r3d_noise_src + render3d_st.r3d_lighting_src } return s + r3d_shader_file(render3d_st, name) } @@ -64,14 +63,14 @@ function r3d_program_log(render3d_st: mut Render3dState, vs: string, fs: string, let f = file_open(r3d_env(render3d_st, "R3D_PROGRAMS_LOG"), "ab") if f == null { return } # one line per program: the defines' newlines become ';' so a variant is one line - let defs = Text.replace(`{r3d_global_defs}{defines}`, "\n", ";") + let defs = Text.replace(`{render3d_st.r3d_global_defs}{defines}`, "\n", ";") let line = `{vs}|{fs}|{defs}\n` file_write(f, line, len(line)) file_close(f) } function r3d_program(render3d_st: mut Render3dState, vs: string, fs: string, defines: string) -> int { r3d_program_log(render3d_st, vs, fs, defines) - let p = gpu_program(render3d_st, r3d_shader_src(render3d_st, vs, defines, false), r3d_shader_src(render3d_st, fs, defines, true), vs, fs, `{r3d_global_defs}{defines}`) + let p = gpu_program(render3d_st, r3d_shader_src(render3d_st, vs, defines, false), r3d_shader_src(render3d_st, fs, defines, true), vs, fs, `{render3d_st.r3d_global_defs}{defines}`) if p == 0 { print(`r3d: program failed: {vs} + {fs}`) } return p } diff --git a/packages/ludic.render3d/render.ludic b/packages/ludic.render3d/render.ludic index b0182c2f..57c45364 100644 --- a/packages/ludic.render3d/render.ludic +++ b/packages/ludic.render3d/render.ludic @@ -5,8 +5,6 @@ # scene_draw_casters, which the demo defines. # ============================================================================ -let r3d_fog_scale: float = 1.0 # float bits: a setting's multiplier over the density the day sets -let 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 @@ -17,26 +15,13 @@ let r3d_fog_base: float = 0.0 # the height fog is measured from (floa # and a moon, and relighting on top of those would only wash them out. # 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. -let r3d_ground_alb_r: float = 0.3; let r3d_ground_alb_g: float = 0.34; let r3d_ground_alb_b: float = 0.14 -let r3d_ground_hi_r: float = 0.289; let r3d_ground_hi_g: float = 0.28; let r3d_ground_hi_b: float = 0.26 -let r3d_ground_hi_y: float = 480.0 # 480 -let r3d_ground_hi_w: float = 150.0 # 150 # set before r3d_init to build the landscape from a real height map -let r3d_dem_path: string = null -let r3d_dem_min: float = 0.0 -let r3d_dem_max: float = 0.0 -let r3d_dem_base: float = 0.0 -let r3d_dem_ox: float = 0.0 -let r3d_dem_oz: float = 0.0 -let 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. function r3d_plate_mode(render3d_st: mut Render3dState, on: bool) -> void { render3d_st.r3d_plate = on } # the sky's HDR image, when it is not the default photograph under r3d_assets -let r3d_debug_max: bool = false -let r3d_cloud_shadow: float = 0.5 # 0.5 # R3D_NOPREPASS=1: light the foliage the old way, every card behind the front one included function r3d_prepass_off(render3d_st: mut Render3dState) -> bool { @@ -48,14 +33,14 @@ function fog_bind(render3d_st: mut Render3dState, prog: int) -> void { u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_spec_scale"), 1.0) u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_fog_density"), render3d_st.r3d_fog_density) u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_fog_height_falloff"), render3d_st.r3d_fog_falloff) - u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_fog_base"), r3d_fog_base) + u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_fog_base"), render3d_st.r3d_fog_base) u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_fog_inscatter"), render3d_st.r3d_fog_inscatter) u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_fog_desat"), render3d_st.r3d_fog_desat) - u_f3(render3d_st, gpu_uniform(render3d_st, prog, "u_ground_alb"), r3d_ground_alb_r, r3d_ground_alb_g, r3d_ground_alb_b) - u_f3(render3d_st, gpu_uniform(render3d_st, prog, "u_ground_alb_hi"), r3d_ground_hi_r, r3d_ground_hi_g, r3d_ground_hi_b) - u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_ground_hi_y"), r3d_ground_hi_y) - u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_ground_hi_w"), r3d_ground_hi_w) - var cs = r3d_cloud_shadow + u_f3(render3d_st, gpu_uniform(render3d_st, prog, "u_ground_alb"), render3d_st.r3d_ground_alb_r, render3d_st.r3d_ground_alb_g, render3d_st.r3d_ground_alb_b) + u_f3(render3d_st, gpu_uniform(render3d_st, prog, "u_ground_alb_hi"), render3d_st.r3d_ground_hi_r, render3d_st.r3d_ground_hi_g, render3d_st.r3d_ground_hi_b) + u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_ground_hi_y"), render3d_st.r3d_ground_hi_y) + u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_ground_hi_w"), render3d_st.r3d_ground_hi_w) + var cs = render3d_st.r3d_cloud_shadow if r3d_env_has(render3d_st, "R3D_NOCLOUD") { cs = 0.0 } u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_cloud_shadow"), cs) u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_time"), render3d_st.r3d_time) @@ -118,8 +103,8 @@ function r3d_load_step(render3d_st: mut Render3dState, i: int) -> bool { } else if t >= 0 and t < TERRAIN_INIT_STEPS { if render3d_st.r3d_plate { return true } if t == 0 { - if r3d_dem_path != null { terrain_use_dem(render3d_st, 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(render3d_st, r3d_ortho_path) } + if render3d_st.r3d_dem_path != null { terrain_use_dem(render3d_st, render3d_st.r3d_dem_path, render3d_st.r3d_dem_min, render3d_st.r3d_dem_max, render3d_st.r3d_dem_base, render3d_st.r3d_dem_ox, render3d_st.r3d_dem_oz) } + if render3d_st.r3d_ortho_path != null { terrain_use_ortho(render3d_st, render3d_st.r3d_ortho_path) } } terrain_init_step(render3d_st, t) } else if i == 1 + TERRAIN_INIT_STEPS { @@ -282,7 +267,7 @@ function r3d_frame(render3d_st: mut Render3dState, time: float) -> void { prof_begin(render3d_st, "volumetric"); post_volumetric_pass(render3d_st); prof_end(render3d_st) prof_begin(render3d_st, "dof"); post_dof_pass(render3d_st); prof_end(render3d_st) if not render3d_st.post_no_gi { prof_begin(render3d_st, "SSAO/GI"); post_ssao_pass(render3d_st); prof_end(render3d_st) } - if r3d_debug_max { tex_max(render3d_st, render3d_st.post_hdr.color, render3d_st.post_hdr.w, render3d_st.post_hdr.h, "hdr") } + if render3d_st.r3d_debug_max { tex_max(render3d_st, render3d_st.post_hdr.color, render3d_st.post_hdr.w, render3d_st.post_hdr.h, "hdr") } prof_begin(render3d_st, "bloom") post_bloom_pass(render3d_st) prof_end(render3d_st) diff --git a/packages/ludic.render3d/scatter.ludic b/packages/ludic.render3d/scatter.ludic index a63da77e..6a4edf88 100644 --- a/packages/ludic.render3d/scatter.ludic +++ b/packages/ludic.render3d/scatter.ludic @@ -331,7 +331,6 @@ function model_blade(render3d_st: mut Render3dState) -> Model { # shader that only runs the alpha test, then the lit pass shades them with no discard and # an equal depth test, so a pixel of needles is lit once rather than once for every card # stacked behind it. In a dense stand at 4K that overdraw was the largest pass in the frame. -let sc_a2c: bool = true function scatter_init(render3d_st: mut Render3dState) -> void { # R3D_DUMP_ATLAS: every impostor and card atlas the run bakes, to build/atlas__{color,alpha}.ppm @@ -407,7 +406,6 @@ function layer_add(l: Layer, x: float, y: float, z: float, scale: float, yaw: fl } # ---- impostors --------------------------------------------------------------------- -let sc_bake_flower: bool = false function impostor_bake(render3d_st: mut Render3dState, model: Model, tiles: int, tw: int, th: int) -> Impostor { let im = new Impostor im.tiles = tiles @@ -439,7 +437,7 @@ function impostor_bake(render3d_st: mut Render3dState, model: Model, tiles: int, let r = im.radius let hh = model.height * 0.5 var bake = render3d_st.sc_bake_prog - if sc_bake_flower { bake = render3d_st.sc_bake_flower_prog } + if render3d_st.sc_bake_flower { bake = render3d_st.sc_bake_flower_prog } gpu_use_program(render3d_st, bake) for t in 0 .. tiles { let a = 2.0 * PI * (float(t) / float(tiles)) @@ -910,7 +908,6 @@ function layer_program(render3d_st: Render3dState, l: Layer, shadow: bool, card: return render3d_st.sc_prog } -let sc_dbg_blade: int = 0 # R3D_LODDBG=1 tints each LOD level (red, green, blue, yellow) and impostors magenta # Can level k's casters (its instances lie between the previous level's distance and its own) put a # shadow on anything the cascade being rendered covers? A receiver in that slice of view depth @@ -999,7 +996,7 @@ function layer_draw_model(render3d_st: mut Render3dState, l: Layer, model: Model var mh = 0.0 if not card and l.foliage { mh = model.height } u_f(render3d_st, gpu_uniform(render3d_st, p, "u_model_h"), mh) - if not card and l.foliage and not shadow and sc_a2c { gpu_alpha_to_coverage(render3d_st, true) } + if not card and l.foliage and not shadow and render3d_st.sc_a2c { gpu_alpha_to_coverage(render3d_st, true) } if card { u_f(render3d_st, gpu_uniform(render3d_st, p, "u_card_w"), l.atlas.radius * 2.0); u_f(render3d_st, gpu_uniform(render3d_st, p, "u_card_h"), l.atlas.height) r3d_bind_2d(render3d_st, p, "u_diff", 0, l.atlas.albedo) @@ -1069,7 +1066,7 @@ function layer_draw_far(render3d_st: mut Render3dState, l: Layer, shadow: bool, if l.foliage { u_f(render3d_st, gpu_uniform(render3d_st, p, "u_spec_scale"), 0.05) } } gpu_cull(render3d_st, false) - if not shadow and sc_a2c { gpu_alpha_to_coverage(render3d_st, true) } + if not shadow and render3d_st.sc_a2c { gpu_alpha_to_coverage(render3d_st, true) } if l.g_on { scatter_attach(render3d_st, render3d_st.sc_card, l.g_dst) gpu_draw_mesh_indirect(render3d_st, render3d_st.sc_card, l.g_cmds, 16 * SC_REC_W, 1, 0, 0) @@ -1101,7 +1098,6 @@ function layer_draw_shadow(render3d_st: mut Render3dState, l: Layer, light_vp: f mesh_draw_instanced(render3d_st, render3d_st.sc_card, l.n_sh) } -let sc_skip_flower: bool = false # ---- the foliage depth prepass ----------------------------------------------------------- # Exactly the instances and positions layer_draw_model will light (the same LOD buckets, @@ -1157,7 +1153,7 @@ function scatter_draw(render3d_st: mut Render3dState) -> void { for i in 0 .. len(render3d_st.sc_layers) { let l = render3d_st.sc_layers[i] if render3d_st.sc_skip_blade and l.blade { continue } - if sc_skip_flower and l.flower { continue } + if render3d_st.sc_skip_flower and l.flower { continue } if render3d_st.sc_skip_card and l.card { continue } if render3d_st.r3d_no_trees and l.imp != null and not l.card { continue } layer_update(render3d_st, l) diff --git a/packages/ludic.render3d/shadow.ludic b/packages/ludic.render3d/shadow.ludic index e48fcc8e..d9b60358 100644 --- a/packages/ludic.render3d/shadow.ludic +++ b/packages/ludic.render3d/shadow.ludic @@ -150,8 +150,8 @@ function shadow_pass(render3d_st: mut Render3dState) -> void { gpu_clear(render3d_st, GL_DEPTH_BUFFER_BIT) let vp = shadow_cascade_vp(render3d_st, c) # shadows off (a video setting): the cascades stay cleared, so everything reads lit - if sh_enabled { - if not sh_skip_terrain { terrain_draw_shadow(vp) } + if render3d_st.sh_enabled { + if not render3d_st.sh_skip_terrain { terrain_draw_shadow(vp) } r3d_scene_casters(render3d_st, vp) } } @@ -161,15 +161,15 @@ function shadow_pass(render3d_st: mut Render3dState) -> void { if render3d_st.r3d_debug_shadow and not render3d_st.sh_printed2 { render3d_st.sh_printed2 = true let q = floats(3) - if sh_probe_x != 0.0 { + if render3d_st.sh_probe_x != 0.0 { let vp = shadow_cascade_vp(render3d_st, 2) - m4_xform_point(q, vp, sh_probe_x, sh_probe_y, sh_probe_z) + m4_xform_point(q, vp, render3d_st.sh_probe_x, render3d_st.sh_probe_y, render3d_st.sh_probe_z) print(`probe base ndc {fixed(q[0])} {fixed(q[1])} {fixed(q[2])}`) - m4_xform_point(q, vp, sh_probe_x, sh_probe_y + 15.0, sh_probe_z) + m4_xform_point(q, vp, render3d_st.sh_probe_x, render3d_st.sh_probe_y + 15.0, render3d_st.sh_probe_z) print(`probe top ndc {fixed(q[0])} {fixed(q[1])} {fixed(q[2])} -> map texel {int((q[0] * 0.5 + 0.5) * float(render3d_st.shadow_res))} {int((q[1] * 0.5 + 0.5) * float(render3d_st.shadow_res))}`) # where the top's shadow lands on the ground: walk down the sun ray - let gx = sh_probe_x + 0.0 - render3d_st.sun_dir[0] * (15.0 / render3d_st.sun_dir[1]) - let gz = sh_probe_z - render3d_st.sun_dir[2] * (15.0 / render3d_st.sun_dir[1]) + let gx = render3d_st.sh_probe_x + 0.0 - render3d_st.sun_dir[0] * (15.0 / render3d_st.sun_dir[1]) + let gz = render3d_st.sh_probe_z - render3d_st.sun_dir[2] * (15.0 / render3d_st.sun_dir[1]) m4_xform_point(q, vp, gx, terrain_height(render3d_st, gx, gz), gz) print(`shadow-of-top ground ndc {fixed(q[0])} {fixed(q[1])} {fixed(q[2])} at {fixed(gx)} {fixed(gz)}`) } @@ -183,11 +183,6 @@ function shadow_pass(render3d_st: mut Render3dState) -> void { free(q) } } -let sh_enabled: bool = true -let sh_skip_terrain: bool = false -let sh_probe_x: float = 0.0 -let sh_probe_y: float = 0.0 -let sh_probe_z: float = 0.0 # Debug: cascade depths as grey PPMs (build/dbg_shadow_.ppm) function shadow_dump(render3d_st: mut Render3dState) -> void { @@ -197,10 +192,10 @@ function shadow_dump(render3d_st: mut Render3dState) -> void { gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_COMPARE_MODE, GL_NONE) gpu_tex_read(render3d_st, GPU_TEX2D_ARRAY, GL_DEPTH_COMPONENT, GL_FLOAT, data_of(buf)) gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE) - if sh_probe_x != 0.0 { + if render3d_st.sh_probe_x != 0.0 { let vp = shadow_cascade_vp(render3d_st, 2) let q = floats(3) - m4_xform_point(q, vp, sh_probe_x, sh_probe_y + 12.0, sh_probe_z) + m4_xform_point(q, vp, render3d_st.sh_probe_x, render3d_st.sh_probe_y + 12.0, render3d_st.sh_probe_z) let tx = int((q[0] * 0.5 + 0.5) * float(render3d_st.shadow_res)) let ty = int((q[1] * 0.5 + 0.5) * float(render3d_st.shadow_res)) let want = q[2] * 0.5 + 0.5 diff --git a/packages/ludic.render3d/sky.ludic b/packages/ludic.render3d/sky.ludic index 3b8caf64..fc9c5296 100644 --- a/packages/ludic.render3d/sky.ludic +++ b/packages/ludic.render3d/sky.ludic @@ -7,11 +7,9 @@ const SKY_PREFILTER_LEVELS: int = 6 -let sky_sun_boost: float = 2.3 # 2.3: the photograph's thin cloud dims its sun; a crisper day wants more # The yaw to bake the light at the first time (radians, float bits). A game that turns the sky at # start sets it before r3d_init, so the image-based light is baked once at that yaw rather than at # 0 and then again - which is what sky_set_yaw at boot used to cost. -let sky_start_yaw: float = 0.0 # turn the HDRI so its sun sits at world azimuth `yaw` (radians, 0 = toward -z) function sky_set_yaw(render3d_st: mut Render3dState, yaw: float) -> void { @@ -50,11 +48,11 @@ function sky_load(render3d_st: mut Render3dState, path: string) -> bool { render3d_st.sky_rot_c = 1.0 # the sun's irradiance is what the IBL clip leaves out of the map; lighting it # directly with that keeps sun and sky in the photograph's own proportion - render3d_st.sun_color = v3_new(render3d_st.hdr_sun_r * sky_sun_boost, render3d_st.hdr_sun_g * sky_sun_boost, render3d_st.hdr_sun_b * sky_sun_boost) + render3d_st.sun_color = v3_new(render3d_st.hdr_sun_r * render3d_st.sky_sun_boost, render3d_st.hdr_sun_g * render3d_st.sky_sun_boost, render3d_st.hdr_sun_b * render3d_st.sky_sun_boost) print(`sun irradiance: {fixed(render3d_st.hdr_sun_r)} {fixed(render3d_st.hdr_sun_g)} {fixed(render3d_st.hdr_sun_b)} (Q16.16), clip {fixed(render3d_st.hdr_clip)}`) print(`sky: {render3d_st.sky_w}x{render3d_st.sky_h}, sun at texel {render3d_st.hdr_max_x},{render3d_st.hdr_max_y}`) render3d_st.sky_fullscreen = mesh_fullscreen(render3d_st) - if sky_start_yaw != 0.0 { sky_set_yaw(render3d_st, sky_start_yaw) } else { sky_precompute(render3d_st) } + if render3d_st.sky_start_yaw != 0.0 { sky_set_yaw(render3d_st, render3d_st.sky_start_yaw) } else { sky_precompute(render3d_st) } return true } diff --git a/packages/ludic.render3d/stream.ludic b/packages/ludic.render3d/stream.ludic index 5ce875b9..d5bde33a 100644 --- a/packages/ludic.render3d/stream.ludic +++ b/packages/ludic.render3d/stream.ludic @@ -131,13 +131,11 @@ function stream_band(s: Stream, d: float) -> int { # by band and kind. With a real microsecond clock the budget can just be the thing we # actually care about — how long this frame is allowed to spend growing ground cover. # Overshoot is bounded by one chunk, so keep chunks small on the dense near streams. -let STREAM_BUDGET_US: int = 2500 # microseconds of generation per frame; a setting may move it const STREAM_BUDGET: int = 8000 # kept for the work counter only # The worst frame is now bounded by one chunk, not by the budget: stream_fill emits a # whole chunk in one call, and the densest band-0 chunk is ~114k instances. Splitting a # chunk's generation across frames would need a resumable generator contract; that is # the next step if the residual hitch ever matters. -let stream_budget_left: int = 0 # Drop the half of the cache nobody has asked for in the longest time, and rebuild the # index over what is left. @@ -297,7 +295,7 @@ function stream_census(render3d_st: Render3dState) -> void { function stream_update_all(render3d_st: mut Render3dState) -> void { if render3d_st.stream_all == null { return } - render3d_st.stream_deadline = gl_now_us() + STREAM_BUDGET_US + render3d_st.stream_deadline = gl_now_us() + render3d_st.STREAM_BUDGET_US for i in 0 .. len(render3d_st.stream_all) { stream_update(render3d_st, render3d_st.stream_all[i], render3d_st.cam_pos[0], render3d_st.cam_pos[2]) } } diff --git a/packages/ludic.render3d/terrain.ludic b/packages/ludic.render3d/terrain.ludic index 1b20e945..ecf7866c 100644 --- a/packages/ludic.render3d/terrain.ludic +++ b/packages/ludic.render3d/terrain.ludic @@ -14,8 +14,6 @@ const CD_LEAVES: int = 256 # leaf patches per side (8192 / 32) # The far tier compiled on its own (FAR_ONLY). A patch that lies entirely beyond the # near/far split is drawn with it: same pixels, a shader small enough to run wide. # 0 dry .. 1 soaked. The game sets it from the weather and lets it dry out. -let ter_wet: float = 0.0 -let ter_dem_blur: float = 0.0 # gaussian texels applied to the survey (0 for lidar; ~3 for 30 m data) function terrain_set_carpet(render3d_st: mut Render3dState, tex: int) -> void { render3d_st.ter_carpet = tex } # Carve a lake bed below `level` inside the ellipse (cx, cz) ± (ex, ez); call before r3d_init. function terrain_lake(render3d_st: mut Render3dState, level: float, cx: float, cz: float, ex: float, ez: float) -> void { @@ -191,7 +189,7 @@ function terrain_generate(render3d_st: mut Render3dState) -> void { u_f(render3d_st, gpu_uniform(render3d_st, p, "u_sea_level"), ter_sea_gen(render3d_st)) u_f4(render3d_st, gpu_uniform(render3d_st, p, "u_isle"), render3d_st.ter_isle_cx, render3d_st.ter_isle_cz, render3d_st.ter_isle_r, render3d_st.ter_isle_fall) u_f(render3d_st, gpu_uniform(render3d_st, p, "u_isle_mode"), float(render3d_st.ter_isle_mode)) - u_f(render3d_st, gpu_uniform(render3d_st, p, "u_dem_blur"), float(ter_dem_blur)) + u_f(render3d_st, gpu_uniform(render3d_st, p, "u_dem_blur"), float(render3d_st.ter_dem_blur)) } mesh_draw(render3d_st, render3d_st.sky_fullscreen) # second pass: R = height, GBA = the smooth surface normal, baked once (ternormal.frag) @@ -423,7 +421,7 @@ function terrain_bind_prog(render3d_st: mut Render3dState, p: int) -> void { u_f(render3d_st, gpu_uniform(render3d_st, p, "u_half"), float(render3d_st.TERRAIN_HALF)) u_f(render3d_st, gpu_uniform(render3d_st, p, "u_texel"), 1.0 / float(TERRAIN_RES)) u_f(render3d_st, gpu_uniform(render3d_st, p, "u_snow_line"), render3d_st.ter_snow_line) - u_f(render3d_st, gpu_uniform(render3d_st, p, "u_wet"), ter_wet) + u_f(render3d_st, gpu_uniform(render3d_st, p, "u_wet"), render3d_st.ter_wet) var lake = -100000.0 if render3d_st.ter_lake_ex != 0.0 { lake = render3d_st.ter_lake_level } u_f(render3d_st, gpu_uniform(render3d_st, p, "u_lake_level"), lake) diff --git a/packages/ludic.render3d/water.ludic b/packages/ludic.render3d/water.ludic index 49c1e4c8..e85b01d0 100644 --- a/packages/ludic.render3d/water.ludic +++ b/packages/ludic.render3d/water.ludic @@ -7,9 +7,6 @@ # Where a body is standing in the water and how hard it is disturbing it. The game sets it; # strength 0 means nobody is in the water and the whole term is skipped. -let wt_wade_x: float = 0.0 -let wt_wade_z: float = 0.0 -let wt_wade_s: float = 0.0 # Several still-water planes, each at its own level over its own bounds: the sea round an # island and a lake a hundred metres above it cannot be one surface. Each draws the same way; # only one - the first added with `reflect` - gets the planar reflection pass, because every @@ -318,9 +315,9 @@ function water_draw(render3d_st: mut Render3dState, depth_tex: int) -> void { # right after gpu_use_program) nor the nested gpu_uniform call (hoisting that changes # nothing). Verified by picture, both backends. Read a global into a local before handing it # to a uniform call. - let wx = wt_wade_x - let wz = wt_wade_z - let ws = wt_wade_s + let wx = render3d_st.wt_wade_x + let wz = render3d_st.wt_wade_z + let ws = render3d_st.wt_wade_s u_f3(render3d_st, gpu_uniform(render3d_st, p, "u_wade"), wx, wz, ws) var ron = 0.0 if render3d_st.water_refl != null and render3d_st.wb_primary >= 0 { diff --git a/packages/ludic.ui/render3d.ludic b/packages/ludic.ui/render3d.ludic index 003f3280..c6cfe001 100644 --- a/packages/ludic.ui/render3d.ludic +++ b/packages/ludic.ui/render3d.ludic @@ -43,38 +43,38 @@ function r3u_scale(ui_st: UiState) -> float { return float(gl_height()) / 1080.0 function r3u_r(c: int) -> float { return float((c / 65536) % 256) / 255.0 } function r3u_g(c: int) -> float { return float((c / 256) % 256) / 255.0 } function r3u_b(c: int) -> float { return float(c % 256) / 255.0 } -function r3u_rect(x: float, y: float, w: float, h: float, c: int, a: float) -> void { ov_rect(int(x), int(y), int(w), int(h), r3u_r(c), r3u_g(c), r3u_b(c), a) } +function r3u_rect(render3d_st: mut Render3dState, x: float, y: float, w: float, h: float, c: int, a: float) -> void { ov_rect(render3d_st, int(x), int(y), int(w), int(h), r3u_r(c), r3u_g(c), r3u_b(c), a) } # a rounded rectangle: a cross of two rects and a disc in each corner -function r3u_round(x: float, y: float, w: float, h: float, rad0: float, c: int, a: float) -> void { +function r3u_round(render3d_st: mut Render3dState, x: float, y: float, w: float, h: float, rad0: float, c: int, a: float) -> void { let rad = Math.min(rad0, Math.min(w, h) / 2.0) let r = r3u_r(c) let g = r3u_g(c) let b = r3u_b(c) - ov_rect(int(x + rad), int(y), int(w - rad * 2.0), int(h), r, g, b, a) - ov_rect(int(x), int(y + rad), int(rad), int(h - rad * 2.0), r, g, b, a) - ov_rect(int(x + w - rad), int(y + rad), int(rad), int(h - rad * 2.0), r, g, b, a) - ov_disc(x + rad, y + rad, rad, 12, r, g, b, a) - ov_disc(x + w - rad, y + rad, rad, 12, r, g, b, a) - ov_disc(x + rad, y + h - rad, rad, 12, r, g, b, a) - ov_disc(x + w - rad, y + h - rad, rad, 12, r, g, b, a) + ov_rect(render3d_st, int(x + rad), int(y), int(w - rad * 2.0), int(h), r, g, b, a) + ov_rect(render3d_st, int(x), int(y + rad), int(rad), int(h - rad * 2.0), r, g, b, a) + ov_rect(render3d_st, int(x + w - rad), int(y + rad), int(rad), int(h - rad * 2.0), r, g, b, a) + ov_disc(render3d_st, x + rad, y + rad, rad, 12, r, g, b, a) + ov_disc(render3d_st, x + w - rad, y + rad, rad, 12, r, g, b, a) + ov_disc(render3d_st, x + rad, y + h - rad, rad, 12, r, g, b, a) + ov_disc(render3d_st, x + w - rad, y + h - rad, rad, 12, r, g, b, a) } # a rounded frame: four straight edges and a quarter arc at each corner -function r3u_ring(x: float, y: float, w: float, h: float, rad0: float, t: float, c: int, a: float) -> void { +function r3u_ring(render3d_st: mut Render3dState, x: float, y: float, w: float, h: float, rad0: float, t: float, c: int, a: float) -> void { let rad = Math.min(rad0, Math.min(w, h) / 2.0) let r = r3u_r(c) let g = r3u_g(c) let b = r3u_b(c) - ov_rect(int(x + rad), int(y), int(w - rad * 2.0), int(t), r, g, b, a) - ov_rect(int(x + rad), int(y + h - t), int(w - rad * 2.0), int(t), r, g, b, a) - ov_rect(int(x), int(y + rad), int(t), int(h - rad * 2.0), r, g, b, a) - ov_rect(int(x + w - t), int(y + rad), int(t), int(h - rad * 2.0), r, g, b, a) + ov_rect(render3d_st, int(x + rad), int(y), int(w - rad * 2.0), int(t), r, g, b, a) + ov_rect(render3d_st, int(x + rad), int(y + h - t), int(w - rad * 2.0), int(t), r, g, b, a) + ov_rect(render3d_st, int(x), int(y + rad), int(t), int(h - rad * 2.0), r, g, b, a) + ov_rect(render3d_st, int(x + w - t), int(y + rad), int(t), int(h - rad * 2.0), r, g, b, a) let q = 1.5707964 - ov_arc(x + rad, y + rad, rad - t / 2.0, t, q * 2.0, q, 8, r, g, b, a) - ov_arc(x + w - rad, y + rad, rad - t / 2.0, t, q * 3.0, q, 8, r, g, b, a) - ov_arc(x + w - rad, y + h - rad, rad - t / 2.0, t, 0.0, q, 8, r, g, b, a) - ov_arc(x + rad, y + h - rad, rad - t / 2.0, t, q, q, 8, r, g, b, a) + ov_arc(render3d_st, x + rad, y + rad, rad - t / 2.0, t, q * 2.0, q, 8, r, g, b, a) + ov_arc(render3d_st, x + w - rad, y + rad, rad - t / 2.0, t, q * 3.0, q, 8, r, g, b, a) + ov_arc(render3d_st, x + w - rad, y + h - rad, rad - t / 2.0, t, 0.0, q, 8, r, g, b, a) + ov_arc(render3d_st, x + rad, y + h - rad, rad - t / 2.0, t, q, q, 8, r, g, b, a) } -function r3u_text(x: float, y: float, s: string, size: float, c: int, a: float) -> void { ov_text(int(x), int(y), int(size), s, r3u_r(c), r3u_g(c), r3u_b(c), a) } -function r3u_measure(s: string, size: float) -> float { return float(ov_text_w(int(size), s)) } -function r3u_clip(x: float, y: float, w: float, h: float) -> void { ov_clip(int(x), int(y), int(w), int(h)) } -function r3u_unclip() -> void { ov_unclip() } +function r3u_text(render3d_st: mut Render3dState, x: float, y: float, s: string, size: float, c: int, a: float) -> void { ov_text(render3d_st, int(x), int(y), int(size), s, r3u_r(c), r3u_g(c), r3u_b(c), a) } +function r3u_measure(render3d_st: mut Render3dState, s: string, size: float) -> float { return float(ov_text_w(render3d_st, int(size), s)) } +function r3u_clip(render3d_st: mut Render3dState, x: float, y: float, w: float, h: float) -> void { ov_clip(render3d_st, int(x), int(y), int(w), int(h)) } +function r3u_unclip(render3d_st: mut Render3dState) -> void { ov_unclip(render3d_st) } diff --git a/packages/ludic.ui/render3d_image.ludic b/packages/ludic.ui/render3d_image.ludic index 165bc7e6..258e7119 100644 --- a/packages/ludic.ui/render3d_image.ludic +++ b/packages/ludic.ui/render3d_image.ludic @@ -1,37 +1,37 @@ # render3d_image.ludic - pictures for the render3d backend: a texture by path (its sRGB bytes as they # are, clamped at its edges, loaded again when the file changes) and a cell of a named atlas module ludic_ui -function r3u_tex(ui_st: mut UiState, path: string) -> int { +function r3u_tex(render3d_st: mut Render3dState, ui_st: mut UiState, path: string) -> int { for i in 0 .. len(ui_st.r3u_tex_paths) { - if ui_st.r3u_tex_paths[i] == path { return r3u_tex_fresh(ui_st, i) } + if ui_st.r3u_tex_paths[i] == path { return r3u_tex_fresh(render3d_st, ui_st, i) } } push(ui_st.r3u_tex_paths, path) - push(ui_st.r3u_tex_ids, r3u_tex_load(path)) + push(ui_st.r3u_tex_ids, r3u_tex_load(render3d_st, path)) push(ui_st.r3u_tex_sizes, Fs.size(path)) push(ui_st.r3u_tex_seen, r3u_now()) return ui_st.r3u_tex_ids[len(ui_st.r3u_tex_ids) - 1] } # a picture is looked at again every half second: a file that has changed (or that was not there, # and is now) is loaded again, so an of a photograph written over the old one shows the new one -function r3u_tex_fresh(ui_st: mut UiState, i: int) -> int { +function r3u_tex_fresh(render3d_st: mut Render3dState, ui_st: mut UiState, i: int) -> int { let now = r3u_now() if now - ui_st.r3u_tex_seen[i] < 0.5 and now >= ui_st.r3u_tex_seen[i] { return ui_st.r3u_tex_ids[i] } ui_st.r3u_tex_seen[i] = now let size = Fs.size(ui_st.r3u_tex_paths[i]) if size == ui_st.r3u_tex_sizes[i] and (ui_st.r3u_tex_ids[i] != 0 or size < 0) { return ui_st.r3u_tex_ids[i] } - if ui_st.r3u_tex_ids[i] != 0 { gpu_tex_free(ui_st.r3u_tex_ids[i]) } - ui_st.r3u_tex_ids[i] = r3u_tex_load(ui_st.r3u_tex_paths[i]) + if ui_st.r3u_tex_ids[i] != 0 { gpu_tex_free(render3d_st, ui_st.r3u_tex_ids[i]) } + ui_st.r3u_tex_ids[i] = r3u_tex_load(render3d_st, ui_st.r3u_tex_paths[i]) ui_st.r3u_tex_sizes[i] = size return ui_st.r3u_tex_ids[i] } # the interface is drawn in sRGB, and a picture's bytes are sRGB, so they are taken as they are # (not decoded to linear, which would darken it); clamped at its edges -function r3u_tex_load(path: string) -> int { - let t = tex_load(path, false) +function r3u_tex_load(render3d_st: mut Render3dState, path: string) -> int { + let t = tex_load(render3d_st, path, false) if t != 0 { - gpu_tex_bind(GPU_TEX2D, t) - gpu_tex_param(GPU_TEX2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) - gpu_tex_param(GPU_TEX2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) + gpu_tex_bind(render3d_st, GPU_TEX2D, t) + gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) + gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) } return t } @@ -44,7 +44,7 @@ export function ui_image_reload(ui_st: mut UiState, path: string) -> void { } } } -function r3u_image(ui_st: mut UiState, src: string, x: float, y: float, w: float, h: float, c: int, a: float) -> void { +function r3u_image(render3d_st: mut Render3dState, ui_st: mut UiState, src: string, x: float, y: float, w: float, h: float, c: int, a: float) -> void { var colon = -1 for i in 0 .. len(src) { if colon < 0 and src[i] == 58 { colon = i } @@ -53,22 +53,22 @@ function r3u_image(ui_st: mut UiState, src: string, x: float, y: float, w: float let prefix: string = src[0..colon] for k in 0 .. len(ui_st.r3u_atlas) { if ui_st.r3u_atlas[k] == prefix { - r3u_cell(ui_st, k, src[colon + 1..len(src)], x, y, w, h, c, a) + r3u_cell(render3d_st, ui_st, k, src[colon + 1..len(src)], x, y, w, h, c, a) return } } } - let t = r3u_tex(ui_st, src) - if t != 0 { ov_sub(t, int(x), int(y), int(w), int(h), 0.0, 0.0, 1.0, 1.0, r3u_r(c), r3u_g(c), r3u_b(c), a) } + let t = r3u_tex(render3d_st, ui_st, src) + if t != 0 { ov_sub(render3d_st, t, int(x), int(y), int(w), int(h), 0.0, 0.0, 1.0, 1.0, r3u_r(c), r3u_g(c), r3u_b(c), a) } } # a picture's own size, for object-fit; an atlas cell is drawn square -function r3u_image_w(ui_st: mut UiState, src: string) -> float { +function r3u_image_w(render3d_st: mut Render3dState, ui_st: mut UiState, src: string) -> float { if r3u_is_cell(ui_st, src) { return 1.0 } - return float(tex_width(r3u_tex(ui_st, src))) + return float(tex_width(render3d_st, r3u_tex(render3d_st, ui_st, src))) } -function r3u_image_h(ui_st: mut UiState, src: string) -> float { +function r3u_image_h(render3d_st: mut Render3dState, ui_st: mut UiState, src: string) -> float { if r3u_is_cell(ui_st, src) { return 1.0 } - return float(tex_height(r3u_tex(ui_st, src))) + return float(tex_height(render3d_st, r3u_tex(render3d_st, ui_st, src))) } function r3u_is_cell(ui_st: UiState, src: string) -> bool { for k in 0 .. len(ui_st.r3u_atlas) { @@ -77,7 +77,7 @@ function r3u_is_cell(ui_st: UiState, src: string) -> bool { } return false } -function r3u_cell(ui_st: mut UiState, k: int, name: string, x: float, y: float, w: float, h: float, c: int, a: float) -> void { +function r3u_cell(render3d_st: mut Render3dState, ui_st: mut UiState, k: int, name: string, x: float, y: float, w: float, h: float, c: int, a: float) -> void { let names = ui_st.r3u_atlas_names[k] var idx = -1 for i in 0 .. len(names) { @@ -95,5 +95,5 @@ function r3u_cell(ui_st: mut UiState, k: int, name: string, x: float, y: float, let u0 = float(cx) / float(cols) let v0 = float(cy) / float(rows) let s = Math.min(w, h) - ov_sub(ui_st.r3u_atlas_tex[k], int(x + (w - s) / 2.0), int(y + (h - s) / 2.0), int(s), int(s), u0, v0, u0 + 1.0 / float(cols), v0 + 1.0 / float(rows), r3u_r(c), r3u_g(c), r3u_b(c), a) + ov_sub(render3d_st, ui_st.r3u_atlas_tex[k], int(x + (w - s) / 2.0), int(y + (h - s) / 2.0), int(s), int(s), u0, v0, u0 + 1.0 / float(cols), v0 + 1.0 / float(rows), r3u_r(c), r3u_g(c), r3u_b(c), a) } diff --git a/packages/ludic.ui/render3d_nine.ludic b/packages/ludic.ui/render3d_nine.ludic index dbc0b743..641aced6 100644 --- a/packages/ludic.ui/render3d_nine.ludic +++ b/packages/ludic.ui/render3d_nine.ludic @@ -3,11 +3,11 @@ module ludic_ui # border-image: the texture's `slice`-pixel corners kept, the rest stretched, drawn `dst` wide (at # most half the box each way, ui_nine_cuts); the slice is a share of the texture's own width across # and its own height down, so it need not be square -function r3u_nine(ui_st: mut UiState, src: string, slice: float, x: float, y: float, w: float, h: float, dst: float, c: int, a: float) -> void { - let t = r3u_tex(ui_st, src) +function r3u_nine(render3d_st: mut Render3dState, ui_st: mut UiState, src: string, slice: float, x: float, y: float, w: float, h: float, dst: float, c: int, a: float) -> void { + let t = r3u_tex(render3d_st, ui_st, src) if t == 0 { return } - let su = slice / float(Math.max(tex_width(t), 1)) - let sv = slice / float(Math.max(tex_height(t), 1)) + let su = slice / float(Math.max(tex_width(render3d_st, t), 1)) + let sv = slice / float(Math.max(tex_height(render3d_st, t), 1)) let cut = ui_nine_cuts(x, y, w, h, dst) let xs = r3u_four(cut[0], cut[1], cut[2], cut[3]) let ys = r3u_four(cut[4], cut[5], cut[6], cut[7]) @@ -17,7 +17,7 @@ function r3u_nine(ui_st: mut UiState, src: string, slice: float, x: float, y: fl for i in 0 .. 3 { let x0 = int(xs[i]) let y0 = int(ys[j]) - ov_sub(t, x0, y0, int(xs[i + 1]) - x0, int(ys[j + 1]) - y0, us[i], vs[j], us[i + 1], vs[j + 1], r3u_r(c), r3u_g(c), r3u_b(c), a) + ov_sub(render3d_st, t, x0, y0, int(xs[i + 1]) - x0, int(ys[j + 1]) - y0, us[i], vs[j], us[i + 1], vs[j + 1], r3u_r(c), r3u_g(c), r3u_b(c), a) } } }