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 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-25 17:11:15 +03:00
parent ce80e64b24
commit 3a87d02696
15 changed files with 152 additions and 148 deletions

View file

@ -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

View file

@ -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 } }

View file

@ -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)

View file

@ -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

View file

@ -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
}

View file

@ -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)

View file

@ -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_<n>_{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)

View file

@ -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_<c>.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

View file

@ -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
}

View file

@ -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]) }
}

View file

@ -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)

View file

@ -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 {

View file

@ -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) }

View file

@ -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 <img> 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)
}

View file

@ -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)
}
}
}