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

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

138 lines
8.3 KiB
Text

# ============================================================================
# sky.ludic — the HDRI sky and its image-based lighting: the equirect radiance
# map (RGB16F, mipped), the sun found in it, a diffuse-convolved irradiance map,
# a GGX-prefiltered map per roughness level (a 2D array), and the split-sum
# BRDF lookup. All convolved on the GPU at load.
# ============================================================================
const SKY_PREFILTER_LEVELS: int = 6
# 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.
# 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 {
sky_set_rot(render3d_st, yaw)
# world sun = rotY(sun_hdri, -yaw): the lookup rotates a world direction by +yaw
let s = render3d_st.sun_hdri
v3_set(render3d_st.sun_dir, render3d_st.sky_rot_c * s[0] - render3d_st.sky_rot_s * s[2], s[1], render3d_st.sky_rot_s * s[0] + render3d_st.sky_rot_c * s[2])
# the light is already baked at this yaw: turning to it again bakes nothing
if render3d_st.sky_baked and yaw == render3d_st.sky_baked_yaw { return }
sky_precompute(render3d_st)
}
# turn only the visible sky image (cheap, per frame): the light and the convolved
# maps stay where they are — daylight.ludic moves those on its own terms
function sky_set_rot(render3d_st: mut Render3dState, yaw: float) -> void {
render3d_st.sky_yaw = yaw
render3d_st.sky_rot_s = Math.sin(yaw); render3d_st.sky_rot_c = Math.cos(yaw)
}
function sky_bind_rot(render3d_st: mut Render3dState, prog: int) -> void { u_f2(render3d_st, gpu_uniform(render3d_st, prog, "u_sky_rot"), render3d_st.sky_rot_s, render3d_st.sky_rot_c) }
# direction for an equirect uv (matches equirectUV in lighting.glsl)
function sky_dir_from_uv(o: floats, u: float, v: float) -> void {
let phi = (u - 0.5) * (2.0 * PI)
let theta = v * PI
let st = Math.sin(theta)
v3_set(o, st * Math.sin(phi), Math.cos(theta), -(st * Math.cos(phi)))
}
# the GPU memory it makes is counted as VKM_SKY (R3D_VKMEM)
function sky_load(render3d_st: mut Render3dState, path: string) -> bool {
let was = render3d_st.gvk_tag
render3d_st.gvk_tag = VKM_SKY
let r = sky_load__t(render3d_st, path)
render3d_st.gvk_tag = was
return r
}
@alloc_ok("start-up: the device, its tables, the programs, the passes and the world's first textures are made once, before play")
function sky_load__t(render3d_st: mut Render3dState, path: string) -> bool {
render3d_st.sky_tex = tex_load_hdr(render3d_st, path)
if render3d_st.sky_tex == 0 { return false }
render3d_st.sky_w = render3d_st.tex_w; render3d_st.sky_h = render3d_st.tex_h
render3d_st.sun_dir = floats(3)
render3d_st.sun_hdri = floats(3)
sky_dir_from_uv(render3d_st.sun_hdri, float(render3d_st.hdr_max_x * 2 + 1) / float(render3d_st.sky_w * 2), float(render3d_st.hdr_max_y * 2 + 1) / float(render3d_st.sky_h * 2))
v3_copy(render3d_st.sun_dir, render3d_st.sun_hdri)
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 * 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 render3d_st.sky_start_yaw != 0.0 { sky_set_yaw(render3d_st, render3d_st.sky_start_yaw) } else { sky_precompute(render3d_st) }
return true
}
function sky_convolve(render3d_st: mut Render3dState, prog: int, target_tex: int, layer: int, w: int, h: int, rough: float) -> void {
let fbo = gpu_fb_new(render3d_st)
gpu_fb_bind(render3d_st, fbo)
if layer < 0 { gpu_fb_color(render3d_st, 0, target_tex) }
else { gpu_fb_color_layer(render3d_st, 0, target_tex, layer) }
gpu_viewport(render3d_st, 0, 0, w, h)
gpu_use_program(render3d_st, prog)
r3d_bind_2d(render3d_st, prog, "u_sky", 0, render3d_st.sky_tex)
sky_bind_rot(render3d_st, prog)
u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_sun_clip"), render3d_st.hdr_clip)
u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_rough"), rough)
u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_sky_w"), float(render3d_st.sky_w))
mesh_draw(render3d_st, render3d_st.sky_fullscreen)
gpu_fb_bind(render3d_st, 0)
gpu_fb_free(render3d_st, fbo)
}
# the prefiltered specular's width per level (its height is half): 256, 512 or 1024. The image
# the reflections and the rough sheen are lit from; sky_set_quality bakes it again at a new size.
function sky_set_quality(render3d_st: mut Render3dState, w: int) -> void {
if w < 64 or w == render3d_st.sky_prefilter_w { return }
render3d_st.sky_prefilter_w = w
if render3d_st.sky_irradiance != 0 { sky_precompute(render3d_st) }
}
function sky_precompute(render3d_st: mut Render3dState) -> void {
gpu_depth_test(render3d_st, false)
if render3d_st.sky_irradiance != 0 {
gpu_tex_free(render3d_st, render3d_st.sky_irradiance)
gpu_tex_free(render3d_st, render3d_st.sky_prefilter)
gpu_tex_free(render3d_st, render3d_st.sky_brdf)
}
# irradiance: 128 x 64 equirect
if render3d_st.sky_p_irr == 0 { render3d_st.sky_p_irr = r3d_program(render3d_st, "fullscreen.vert", "ibl_irradiance.frag", ""); render3d_st.sky_p_pre = r3d_program(render3d_st, "fullscreen.vert", "ibl_prefilter.frag", ""); render3d_st.sky_p_brdf = r3d_program(render3d_st, "fullscreen.vert", "ibl_brdf.frag", "") }
let p_irr = render3d_st.sky_p_irr
render3d_st.sky_irradiance = tex_target(render3d_st, 128, 64, GL_RGB16F, GL_RGB, GL_FLOAT, GL_LINEAR)
gpu_tex_bind(render3d_st, GPU_TEX2D, render3d_st.sky_irradiance)
gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
sky_convolve(render3d_st, p_irr, render3d_st.sky_irradiance, -1, 128, 64, 0.0)
# prefiltered specular: 6 roughness levels, sky_prefilter_w x half each, as a 2D array
let p_pre = render3d_st.sky_p_pre
render3d_st.sky_prefilter = gpu_tex_new(render3d_st)
gpu_tex_bind(render3d_st, GPU_TEX2D_ARRAY, render3d_st.sky_prefilter)
gpu_tex_image3d(render3d_st, GL_RGB16F, render3d_st.sky_prefilter_w, render3d_st.sky_prefilter_w / 2, SKY_PREFILTER_LEVELS, GL_RGB, GL_FLOAT, null)
gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_WRAP_S, GL_REPEAT)
gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
for l in 0 .. SKY_PREFILTER_LEVELS {
sky_convolve(render3d_st, p_pre, render3d_st.sky_prefilter, l, render3d_st.sky_prefilter_w, render3d_st.sky_prefilter_w / 2, float(l) / float(SKY_PREFILTER_LEVELS - 1))
}
# BRDF LUT
let p_brdf = render3d_st.sky_p_brdf
render3d_st.sky_brdf = tex_target(render3d_st, 256, 256, GL_RG16F, GL_RG, GL_FLOAT, GL_LINEAR)
sky_convolve(render3d_st, p_brdf, render3d_st.sky_brdf, -1, 256, 256, 0.0)
render3d_st.sky_baked = true
render3d_st.sky_baked_yaw = render3d_st.sky_yaw
gpu_check(render3d_st, "sky precompute")
}
# bind the IBL set + sun for a lit program (units 12..14)
function sky_bind_lighting(render3d_st: mut Render3dState, prog: int) -> void {
r3d_bind_2d(render3d_st, prog, "u_irradiance", 12, render3d_st.sky_irradiance)
r3d_bind_tex(render3d_st, prog, "u_prefilter", 13, GPU_TEX2D_ARRAY, render3d_st.sky_prefilter)
r3d_bind_2d(render3d_st, prog, "u_brdf", 14, render3d_st.sky_brdf)
u_v3(render3d_st, gpu_uniform(render3d_st, prog, "u_sun_dir"), render3d_st.sun_dir)
u_v3(render3d_st, gpu_uniform(render3d_st, prog, "u_sun_color"), render3d_st.sun_color)
u_v3(render3d_st, gpu_uniform(render3d_st, prog, "u_cam_pos"), render3d_st.cam_pos)
u_f(render3d_st, gpu_uniform(render3d_st, prog, "u_prefilter_levels"), float(SKY_PREFILTER_LEVELS))
daylight_bind(render3d_st, prog)
}