render3d + lab: bakes as PNGs, and impostors, the sky's light and the carpet read from their bakes
ludic.lab: lab_png_write / lab_png_write_from (raw 8-bit, 1-4 channels, stored deflate) in png_write.ludic, importable alone with its own LabPngState; png_convert.ludic's previews of float textures (R32F min..max, RG16F x255, HDR x/(1+x) + sRGB); lab_ppm_to_png on the same encoder. render3d: bake_load.ludic - impostor_from_baked / impostor_source / impostor_refill (a fog re-open reads the bake), sky_baked_in and sky_precompute trying the bake at the start yaw (sky_compute is the convolution, and sky_ibl_bytes always uses it), carpet_from_baked / carpet_bytes / carpet_finish, bake_part_count / _len / _off. Compile-only: nothing run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f79390838a
commit
6c690c5db6
15 changed files with 396 additions and 107 deletions
108
packages/ludic.render3d/bake_load.ludic
Normal file
108
packages/ludic.render3d/bake_load.ludic
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
# bake_load.ludic — the game names where a bake of the renderer's work lives, and the renderer takes the
|
||||
# textures from it when it is current and makes them as it always has when it is not: an impostor's
|
||||
# atlases (and again when a fog opens past its cards), the sky's image-based light, the grass carpet.
|
||||
|
||||
# ---- a bundle's parts, for whoever writes a preview of one (bake_pack's layout) ----
|
||||
function bake_part_count(data: []byte) -> int {
|
||||
if data == null or len(data) < 4 { return 0 }
|
||||
return Vk.get_i32(data_of(data), 0)
|
||||
}
|
||||
function bake_part_len(data: []byte, i: int) -> int { return Vk.get_i32(data_of(data), 4 + i * 4) }
|
||||
# where part i's bytes begin in the bundle
|
||||
function bake_part_off(data: []byte, i: int) -> int {
|
||||
var at = 4 + bake_part_count(data) * 4
|
||||
for k in 0 .. i { at += bake_part_len(data, k) }
|
||||
return at
|
||||
}
|
||||
|
||||
# ---- an impostor ----
|
||||
# where an impostor's bake lives (the game's name for it), whether or not its atlases came from it
|
||||
function impostor_source(im: Impostor, path: string, key: string, version: int) -> void {
|
||||
im.src_path = intern(path)
|
||||
im.src_key = intern(key)
|
||||
im.src_version = version
|
||||
}
|
||||
|
||||
# an impostor of this shape from its bake, or null (missing, stale, or not this shape) for the caller to paint
|
||||
@alloc_ok("a bake read once at a map load")
|
||||
function impostor_from_baked(render3d_st: mut Render3dState, model: Model, tiles: int, tw: int, th: int, path: string, key: string, version: int) -> Impostor {
|
||||
let b = r3d_baked_read(path, key, version)
|
||||
if b == null { return null }
|
||||
let im = impostor_from_bytes(render3d_st, model, tiles, tw, th, b)
|
||||
free(b)
|
||||
if im == null { return null }
|
||||
impostor_source(im, path, key, version)
|
||||
im.src_ok = true
|
||||
return im
|
||||
}
|
||||
# the atlases filled from the bake again (a fog opening past the cards); false, and none kept, when it cannot be
|
||||
@alloc_ok("a bake read once when a fog opens")
|
||||
function impostor_refill(render3d_st: mut Render3dState, im: Impostor) -> bool {
|
||||
let b = r3d_baked_read(im.src_path, im.src_key, im.src_version)
|
||||
if b == null { return false }
|
||||
let ok = impostor_fill(render3d_st, im, b)
|
||||
free(b)
|
||||
if not ok {
|
||||
gpu_tex_free(render3d_st, im.albedo); gpu_tex_free(render3d_st, im.normal)
|
||||
im.albedo = 0; im.normal = 0
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
# ---- the sky's image-based light ----
|
||||
# the directory the sky's bakes are in, under the key and version they were baked as: before r3d_init.
|
||||
# A bake is one HDRI at the start yaw at one prefilter width; any other turn of the sky is convolved
|
||||
function sky_baked_in(render3d_st: mut Render3dState, dir: string, key: string, version: int) -> void {
|
||||
render3d_st.sky_bake_dir = intern(dir)
|
||||
render3d_st.sky_bake_key = intern(key)
|
||||
render3d_st.sky_bake_version = version
|
||||
}
|
||||
# the HDRI's name without its directory or ".hdr": what the sky's bakes are named by
|
||||
@alloc_ok("a bake's name, made at a load")
|
||||
function sky_hdri_name(render3d_st: Render3dState) -> string {
|
||||
let p = render3d_st.r3d_sky_path
|
||||
if p == null { return SKY_DEFAULT_HDRI }
|
||||
var from = 0
|
||||
for i in 0 .. len(p) { if p[i] == 47 { from = i + 1 } }
|
||||
var to = len(p)
|
||||
if to - from > 4 and p[to - 4] == 46 { to -= 4 }
|
||||
return p[from .. to]
|
||||
}
|
||||
# <dir>/<hdri>_y<start yaw in whole degrees>_w<prefilter width>.bin, the file one bake is
|
||||
@alloc_ok("a bake's name, made at a load")
|
||||
function sky_bake_file(render3d_st: Render3dState, w: int) -> string {
|
||||
let deg = int(Math.round(Math.rad_to_deg(render3d_st.sky_start_yaw)))
|
||||
return `{render3d_st.sky_bake_dir}/{sky_hdri_name(render3d_st)}_y{deg}_w{w}.bin`
|
||||
}
|
||||
# the IBL set from its bake when the sky is at the start yaw and a bake of this width is current
|
||||
@alloc_ok("a bake read once at a load or a new light quality")
|
||||
function sky_ibl_try_baked(render3d_st: mut Render3dState) -> bool {
|
||||
if len(render3d_st.sky_bake_dir) == 0 or render3d_st.sky_yaw != render3d_st.sky_start_yaw { return false }
|
||||
let path = sky_bake_file(render3d_st, render3d_st.sky_prefilter_w)
|
||||
let b = r3d_baked_read(path, render3d_st.sky_bake_key, render3d_st.sky_bake_version)
|
||||
var ok = false
|
||||
if b != null { ok = sky_ibl_from_bytes(render3d_st, b) }
|
||||
if b != null { free(b) }
|
||||
if not ok and not render3d_st.sky_bake_said { sky_say_missing(path) }
|
||||
if not ok { render3d_st.sky_bake_said = true }
|
||||
return ok
|
||||
}
|
||||
function sky_say_missing(path: string) -> void { print(`bake: {path} missing or stale - the sky's light made at run time (ludic bake)`) }
|
||||
|
||||
# ---- the grass carpet ----
|
||||
function carpet_bytes(render3d_st: mut Render3dState, tex: int) -> []byte { return bake_pack(render3d_st, bake_list1(tex)) }
|
||||
# the carpet texture (RGBA8, res square) from its bake, finished as carpet_bake finishes it; 0 when missing
|
||||
@alloc_ok("a bake read once at a map load")
|
||||
function carpet_from_baked(render3d_st: mut Render3dState, path: string, key: string, version: int, res: int) -> int {
|
||||
let b = r3d_baked_read(path, key, version)
|
||||
if b == null { return 0 }
|
||||
let was = render3d_st.gvk_tag
|
||||
render3d_st.gvk_tag = VKM_SCATTER
|
||||
let tex = tex_target(render3d_st, res, res, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, GL_LINEAR)
|
||||
let ok = bake_unpack(render3d_st, b, bake_list1(tex))
|
||||
free(b)
|
||||
if ok { carpet_finish(render3d_st, tex) } else { gpu_tex_free(render3d_st, tex) }
|
||||
render3d_st.gvk_tag = was
|
||||
if ok { return tex }
|
||||
return 0
|
||||
}
|
||||
|
|
@ -36,6 +36,12 @@ function bake_unpack(render3d_st: mut Render3dState, data: []byte, texs: words)
|
|||
return true
|
||||
}
|
||||
|
||||
@alloc_ok("a bake's list of textures, once")
|
||||
function bake_list1(a: int) -> words {
|
||||
let w = words(1)
|
||||
w[0] = a
|
||||
return w
|
||||
}
|
||||
@alloc_ok("a bake's list of textures, once")
|
||||
function bake_list2(a: int, b: int) -> words {
|
||||
let w = words(2)
|
||||
|
|
@ -52,13 +58,15 @@ function bake_list3(a: int, b: int, c: int) -> words {
|
|||
# ---- an impostor: its albedo and normal atlases (level 0; mips are made after) ----
|
||||
function impostor_bytes(render3d_st: mut Render3dState, im: Impostor) -> []byte { return bake_pack(render3d_st, bake_list2(im.albedo, im.normal)) }
|
||||
|
||||
# an impostor with the bake's shape (model, tiles and tile size as impostor_bake takes them) from bytes
|
||||
# an impostor with the bake's shape (model, tiles and tile size as impostor_bake takes them) from bytes;
|
||||
# null, and nothing kept, when the bytes are not that shape
|
||||
function impostor_from_bytes(render3d_st: mut Render3dState, model: Model, tiles: int, tw: int, th: int, data: []byte) -> Impostor {
|
||||
let im = new Impostor
|
||||
im.tiles = tiles; im.radius = model.radius * 1.02; im.height = model.height
|
||||
im.model = model; im.tw = tw; im.th = th; im.flower = render3d_st.sc_bake_flower
|
||||
impostor_fill(render3d_st, im, data)
|
||||
return im
|
||||
if impostor_fill(render3d_st, im, data) { return im }
|
||||
gpu_tex_free(render3d_st, im.albedo); gpu_tex_free(render3d_st, im.normal)
|
||||
return null
|
||||
}
|
||||
|
||||
# an impostor's atlases made and filled from bytes (at a load, or when a fog opens past its cards)
|
||||
|
|
@ -96,7 +104,7 @@ function terrain_shadow_from_bytes(render3d_st: mut Render3dState, data: []byte)
|
|||
# ---- the sky's image-based lighting: irradiance, the prefiltered levels, the BRDF table ----
|
||||
# for the HDRI already loaded and the yaw already set (the yaw is part of what a bake is keyed on)
|
||||
function sky_ibl_bytes(render3d_st: mut Render3dState) -> []byte {
|
||||
sky_precompute(render3d_st)
|
||||
sky_compute(render3d_st) # never from an older bake
|
||||
return bake_pack(render3d_st, bake_list3(render3d_st.sky_irradiance, render3d_st.sky_prefilter, render3d_st.sky_brdf))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -705,6 +705,10 @@ export state Render3dState {
|
|||
sky_p_pre: int = 0
|
||||
sky_p_brdf: int = 0
|
||||
sky_prefilter_w: int = 512
|
||||
sky_bake_dir: string = "" # where the sky's IBL bakes are (sky_baked_in, bake_load.ludic); "" = none
|
||||
sky_bake_key: string = ""
|
||||
sky_bake_version: int = 0
|
||||
sky_bake_said: bool = false # a missing bake said once
|
||||
STREAM_MAX_CHUNKS: int = 4096
|
||||
stream_all: []Stream = null
|
||||
stream_cap_read: bool = false
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# fog_impostors.ludic — a layer's impostor atlases are let go while the fog wall is nearer than its
|
||||
# cards begin (the layer's `near`), and baked again when the fog opens past it. Inside the wall every
|
||||
# cards begin (the layer's `near`), and baked again (or read again from its bake) when the fog opens past it. Inside the wall every
|
||||
# instance is a mesh, and the meshes' LOD2 cast its shadows, so a card is neither drawn nor cast.
|
||||
|
||||
# does the wall let any of this layer's instances be drawn as a card?
|
||||
|
|
@ -22,7 +22,8 @@ function fog_impostor(render3d_st: mut Render3dState, l: Layer) -> void {
|
|||
let t0 = gl_now_us()
|
||||
let was = render3d_st.gvk_tag
|
||||
render3d_st.gvk_tag = VKM_IMPOSTOR
|
||||
impostor_paint(render3d_st, im)
|
||||
# a baked impostor is read again rather than drawn again (bake_load.ludic)
|
||||
if not (im.src_ok and impostor_refill(render3d_st, im)) { impostor_paint(render3d_st, im) }
|
||||
render3d_st.gvk_tag = was
|
||||
render3d_st.fog_imp_rebake_us = render3d_st.fog_imp_rebake_us + (gl_now_us() - t0)
|
||||
render3d_st.fog_imp_rebaked += 1
|
||||
|
|
|
|||
|
|
@ -59,4 +59,5 @@ import "water.ludic"
|
|||
import "streamline.ludic"
|
||||
import "render.ludic"
|
||||
import "bake_parts.ludic"
|
||||
import "bake_load.ludic"
|
||||
import "safe_api.ludic"
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ function r3d_load_step(render3d_st: mut Render3dState, i: int) -> bool {
|
|||
var sky = render3d_st.r3d_sky_path
|
||||
var made: string = null
|
||||
if sky == null {
|
||||
made = render3d_st.r3d_assets + "/hdri/kloofendal_48d_partly_cloudy_puresky_4k.hdr"
|
||||
made = render3d_st.r3d_assets + "/hdri/" + SKY_DEFAULT_HDRI + ".hdr"
|
||||
sky = made
|
||||
}
|
||||
let got = sky_load(render3d_st, sky)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,11 @@ property Impostor {
|
|||
model: Model, # what it was baked from, to bake it again (fog_impostors.ludic)
|
||||
tw: int = 0,
|
||||
th: int = 0,
|
||||
flower: bool = false
|
||||
flower: bool = false,
|
||||
src_path: string = "", # its bake file, where the game names one (bake_load.ludic): what a fog opening reads
|
||||
src_key: string = "",
|
||||
src_version: int = 0,
|
||||
src_ok: bool = false # its atlases came from that bake, so a re-open reads it rather than painting
|
||||
}
|
||||
property Layer {
|
||||
model: Model,
|
||||
|
|
@ -1230,14 +1234,18 @@ function carpet_bake__t(render3d_st: mut Render3dState, layers: []Layer, count:
|
|||
gpu_fb_free(render3d_st, fbo)
|
||||
gpu_rb_free(render3d_st, rb)
|
||||
gpu_buffer_free(render3d_st, buf)
|
||||
carpet_finish(render3d_st, tex)
|
||||
gpu_check(render3d_st, "carpet bake")
|
||||
return tex
|
||||
}
|
||||
# the carpet tile made ready to wear: repeat-wrapped and mipmapped (a bake's too, bake_load.ludic)
|
||||
function carpet_finish(render3d_st: mut Render3dState, tex: int) -> void {
|
||||
gpu_tex_bind(render3d_st, GPU_TEX2D, tex)
|
||||
gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
|
||||
gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_T, GL_REPEAT)
|
||||
gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
|
||||
gpu_tex_paramf(render3d_st, GPU_TEX2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, render3d_st.tex_anisotropy)
|
||||
gpu_tex_mips(render3d_st, GPU_TEX2D)
|
||||
gpu_check(render3d_st, "carpet bake")
|
||||
return tex
|
||||
}
|
||||
|
||||
# ---- another map at run time ------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
# ============================================================================
|
||||
|
||||
const SKY_PREFILTER_LEVELS: int = 6
|
||||
const SKY_DEFAULT_HDRI: string = "kloofendal_48d_partly_cloudy_puresky_4k" # under <assets>/hdri/, when no path is given
|
||||
|
||||
# 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
|
||||
|
|
@ -90,7 +91,13 @@ function sky_set_quality(render3d_st: mut Render3dState, w: int) -> void {
|
|||
if render3d_st.sky_irradiance != 0 { sky_precompute(render3d_st) }
|
||||
}
|
||||
|
||||
# the IBL set for the sky as it is turned now: from its bake where there is a current one (bake_load.ludic)
|
||||
function sky_precompute(render3d_st: mut Render3dState) -> void {
|
||||
if sky_ibl_try_baked(render3d_st) { return }
|
||||
sky_compute(render3d_st)
|
||||
}
|
||||
# the IBL set convolved from the HDRI on the GPU
|
||||
function sky_compute(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", "") }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue