ludic/packages/ludic.render3d/sky.ludic
Orkuncakilkaya 883ea8d48a render3d: the deterministic work as bytes a bake keeps, and the textures made from them
Each pair runs the work as the renderer always has and reads the result back, or makes the same
textures and fills them from bytes instead: impostor_bytes / impostor_from_bytes (and impostor_fill,
for a fog that opens past a layer's cards), terrain_shadow_bytes / terrain_shadow_from_bytes,
sky_ibl_bytes / sky_ibl_from_bytes (sky_precompute split into sky_ibl_make and the convolutions).
A bundle is a word count, a word length per part and the parts (bake_pack / bake_unpack), checked
against the textures' shapes before any byte is used. gpu_vk_readback.ludic reads a texture's level 0,
every layer, as stored; r3d_baked_read reads a bake's file (ludic.base's LBAK header: key and version
must match) since render3d cannot import ludic.base. Nothing calls them yet: behaviour unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:19:13 +03:00

141 lines
8.4 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)
sky_ibl_make(render3d_st)
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", "") }
sky_convolve(render3d_st, render3d_st.sky_p_irr, render3d_st.sky_irradiance, -1, 128, 64, 0.0)
for l in 0 .. SKY_PREFILTER_LEVELS {
sky_convolve(render3d_st, render3d_st.sky_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))
}
sky_convolve(render3d_st, render3d_st.sky_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")
}
# the IBL set's textures, made empty (the last ones let go): what sky_precompute convolves into, and
# what sky_ibl_from_bytes fills from a bake
function sky_ibl_make(render3d_st: mut Render3dState) -> void {
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
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)
# prefiltered specular: 6 roughness levels, sky_prefilter_w x half each, as a 2D array
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)
# BRDF LUT
render3d_st.sky_brdf = tex_target(render3d_st, 256, 256, GL_RG16F, GL_RG, GL_FLOAT, GL_LINEAR)
}
# 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)
}