feat(render3d): load steps for a loading screen, four quality switches, and the ring's uniform usage
- r3d_open opens the window with nothing baked; r3d_load_count / r3d_load_step run the rest (sky and light, terrain, shadow and screen targets, cover and actors, grass) so a game can present a loading frame between them. r3d_init is the same calls in a row. - sky_set_quality(width): the prefiltered sky light at 256 / 512 / 1024, baked again. - post_set_msaa(samples): multisampling on OpenGL, remade in place (post_msaa_live is false on Vulkan); post_free frees the multisampled framebuffer too. - STREAM_BUDGET_US is a variable; r3d_fog_scale multiplies the fog the day sets. - Vulkan buffers carry UNIFORM_BUFFER usage: the frame's ring is bound as uniform buffers and was created without it (VUID-VkWriteDescriptorSet-descriptorType-00330, found on MoltenVK). OpenGL frames at the five viewpoints unchanged; the game's 59 self-tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
b9eda646fc
commit
f2b976c976
8 changed files with 89 additions and 21 deletions
11
changes/quality-switches.md
Normal file
11
changes/quality-switches.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
**More of the renderer's quality is a switch a game can offer.**
|
||||
|
||||
- `sky_set_quality(width)`: the prefiltered sky light at 256, 512 or 1024 wide (half as tall),
|
||||
baked again at once.
|
||||
- `post_set_msaa(samples)`: 1, 2 or 4 samples for the scene on OpenGL, remaking the screen targets in
|
||||
place (`post_msaa_live()` is false on Vulkan, which has no sample counts yet); `post_free` now
|
||||
frees the multisampled framebuffer too.
|
||||
- `STREAM_BUDGET_US` is a variable: the microseconds a frame may spend generating streamed cover.
|
||||
- `r3d_fog_scale`: a multiplier over the fog density the daylight and the weather set.
|
||||
7
changes/r3d-load-steps.md
Normal file
7
changes/r3d-load-steps.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
**A loading screen can show the renderer's start-up as it happens.** `r3d_open(w, h, title)` opens
|
||||
the window and the graphics backend with nothing baked, and `r3d_load_count()` / `r3d_load_step(i)`
|
||||
run the rest - the sky and its light, the terrain, the shadow and screen targets, the scattered cover
|
||||
and actors, the grass - one step at a time, so a game can draw and present a frame between them.
|
||||
`r3d_init` is those same calls in a row and is unchanged for everything that uses it.
|
||||
|
|
@ -143,7 +143,7 @@ function daylight_set(hours: int) -> void {
|
|||
let dim = f_sub(F_ONE, f_mul(fl(0.55), oc))
|
||||
for i in 0 .. 3 { day_ibl[i] = f_mul(f_lerp(day_ibl[i], grey, f_mul(fl(0.7), oc)), dim) }
|
||||
if day_fog_base == 0 { day_fog_base = r3d_fog_density }
|
||||
r3d_fog_density = f_mul(day_fog_base, day_fog_mul)
|
||||
r3d_fog_density = f_mul(f_mul(day_fog_base, day_fog_mul), r3d_fog_scale)
|
||||
# exposure: auto-exposure must not turn the night into day
|
||||
# the ceiling has to move with the moon or auto-exposure eats the difference between a
|
||||
# full-moon night and a new-moon one
|
||||
|
|
|
|||
|
|
@ -585,7 +585,7 @@ function gvk_buf_reserve(b: int, n: int) -> bool {
|
|||
Vk.zero(bci, VkBufferCreateInfo_sizeof)
|
||||
Vk.put_i32(bci, VkBufferCreateInfo_sType, VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO)
|
||||
Vk.put_i64(bci, VkBufferCreateInfo_size, size_l)
|
||||
Vk.put_i32(bci, VkBufferCreateInfo_usage, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT)
|
||||
Vk.put_i32(bci, VkBufferCreateInfo_usage, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT)
|
||||
Vk.put_i32(bci, VkBufferCreateInfo_sharingMode, VK_SHARING_MODE_EXCLUSIVE)
|
||||
let out = bytes(8)
|
||||
var r = Vk.create_buffer(gvk_dev, bci, null, out)
|
||||
|
|
|
|||
|
|
@ -50,11 +50,28 @@ var post_grain: int = 0
|
|||
# the screen-sized targets go away before post_init makes them at a new size
|
||||
function post_free() -> void {
|
||||
if post_hdr == null { return }
|
||||
if post_ms_fbo != 0 { gpu_fb_free(post_ms_fbo); post_ms_fbo = 0 }
|
||||
target_free(post_hdr); target_free(post_ao); target_free(post_ao_blur); target_free(post_ldr)
|
||||
target_free(post_depth_copy); target_free(post_prev); target_free(post_scene)
|
||||
for i in 0 .. len(post_bloom) { target_free(post_bloom[i]) }
|
||||
post_hdr = null
|
||||
}
|
||||
# Multisampled scene: 1 (temporal AA alone), 2 or 4. OpenGL remakes the scene targets at once;
|
||||
# the Vulkan backend has no sample counts yet and keeps drawing single-sampled (post_msaa_live says so).
|
||||
function post_msaa_live() -> bool { return gpu_is_gl() }
|
||||
function post_set_msaa(n: int) -> void {
|
||||
var want = n
|
||||
if want < 1 { want = 1 }
|
||||
if not post_msaa_live() { want = 1 }
|
||||
if want == post_ms_samples { return }
|
||||
post_ms_samples = want
|
||||
if post_hdr != null {
|
||||
let w = post_w; let h = post_h
|
||||
post_free()
|
||||
post_init(w, h)
|
||||
}
|
||||
}
|
||||
|
||||
function post_init(w: int, h: int) -> void {
|
||||
post_w = w; post_h = h
|
||||
post_hdr = target_new(w, h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, true, GL_LINEAR)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
var r3d_sky_prog: int = 0
|
||||
var r3d_fog_density: int = 0
|
||||
var r3d_fog_scale: int = 0x3F800000 # float bits: a setting's multiplier over the density the day sets
|
||||
var r3d_fog_falloff: int = 0
|
||||
var r3d_fog_base: int = 0 # the height fog is measured from (float bits); 0 = y = 0
|
||||
var r3d_time: int = 0
|
||||
|
|
@ -70,7 +71,16 @@ function r3d_env_flags() -> void {
|
|||
if a >= 16 { tex_anisotropy = 16.0 }
|
||||
}
|
||||
}
|
||||
# Start the renderer in one call: the window, then every load step in order. A game that shows
|
||||
# a loading screen calls r3d_open, draws its screen, and runs r3d_load_step itself between frames.
|
||||
function r3d_init(w: int, h: int, title: string) -> bool {
|
||||
if not r3d_open(w, h, title) { return false }
|
||||
for i in 0 .. r3d_load_count() { if not r3d_load_step(i) { return false } }
|
||||
return true
|
||||
}
|
||||
|
||||
# the window and the graphics backend; nothing is baked yet, but a frame can be presented
|
||||
function r3d_open(w: int, h: int, title: string) -> bool {
|
||||
r3d_env_flags()
|
||||
gpu_select()
|
||||
if not gpu_open(w, h, title) { print("r3d: no OpenGL context"); return false }
|
||||
|
|
@ -79,21 +89,35 @@ function r3d_init(w: int, h: int, title: string) -> bool {
|
|||
print(`r3d: {gl_w}x{gl_h} on {renderer}`)
|
||||
prof_init()
|
||||
cam_init(fr(gl_w, gl_h))
|
||||
if not sky_load(r3d_assets + "/hdri/kloofendal_48d_partly_cloudy_puresky_4k.hdr") { return false }
|
||||
daylight_init()
|
||||
if r3d_dem_path != null { terrain_use_dem(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(r3d_ortho_path) }
|
||||
terrain_init()
|
||||
shadow_init()
|
||||
post_init(gl_w, gl_h)
|
||||
scatter_init()
|
||||
actor_init()
|
||||
grass_init()
|
||||
r3d_sky_prog = r3d_program("fullscreen.vert", "sky.frag", "")
|
||||
r3d_fog_density = fl(0.00014)
|
||||
r3d_fog_falloff = fl(0.002)
|
||||
r3d_ready = true
|
||||
gpu_check("r3d init")
|
||||
return true
|
||||
}
|
||||
|
||||
# The load, as steps a loading screen can show between: 0 the sky and its image-based light,
|
||||
# 1 the terrain (height, normals, shadow, materials), 2 the shadow maps and the screen targets,
|
||||
# 3 the scattered cover and the actors, 4 the grass and the sky pass. The order is r3d_init's.
|
||||
function r3d_load_count() -> int { return 5 }
|
||||
function r3d_load_step(i: int) -> bool {
|
||||
if i == 0 {
|
||||
if not sky_load(r3d_assets + "/hdri/kloofendal_48d_partly_cloudy_puresky_4k.hdr") { return false }
|
||||
daylight_init()
|
||||
} else if i == 1 {
|
||||
if r3d_dem_path != null { terrain_use_dem(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(r3d_ortho_path) }
|
||||
terrain_init()
|
||||
} else if i == 2 {
|
||||
shadow_init()
|
||||
post_init(gl_w, gl_h)
|
||||
} else if i == 3 {
|
||||
scatter_init()
|
||||
actor_init()
|
||||
} else if i == 4 {
|
||||
grass_init()
|
||||
r3d_sky_prog = r3d_program("fullscreen.vert", "sky.frag", "")
|
||||
r3d_fog_density = fl(0.00014)
|
||||
r3d_fog_falloff = fl(0.002)
|
||||
r3d_ready = true
|
||||
gpu_check("r3d init")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -85,6 +85,15 @@ function sky_convolve(prog: int, target_tex: int, layer: int, w: int, h: int, ro
|
|||
gpu_fb_free(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.
|
||||
var sky_prefilter_w: int = 512
|
||||
function sky_set_quality(w: int) -> void {
|
||||
if w < 64 or w == sky_prefilter_w { return }
|
||||
sky_prefilter_w = w
|
||||
if sky_irradiance != 0 { sky_precompute() }
|
||||
}
|
||||
|
||||
function sky_precompute() -> void {
|
||||
gpu_depth_test(false)
|
||||
if sky_irradiance != 0 {
|
||||
|
|
@ -99,17 +108,17 @@ function sky_precompute() -> void {
|
|||
gpu_tex_bind(GPU_TEX2D, sky_irradiance)
|
||||
gpu_tex_param(GPU_TEX2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
|
||||
sky_convolve(p_irr, sky_irradiance, -1, 128, 64, F_ZERO)
|
||||
# prefiltered specular: 6 roughness levels, 512 x 256 each, as a 2D array
|
||||
# prefiltered specular: 6 roughness levels, sky_prefilter_w x half each, as a 2D array
|
||||
let p_pre = sky_p_pre
|
||||
sky_prefilter = gpu_tex_new()
|
||||
gpu_tex_bind(GPU_TEX2D_ARRAY, sky_prefilter)
|
||||
gpu_tex_image3d(GL_RGB16F, 512, 256, SKY_PREFILTER_LEVELS, GL_RGB, GL_FLOAT, null)
|
||||
gpu_tex_image3d(GL_RGB16F, sky_prefilter_w, sky_prefilter_w / 2, SKY_PREFILTER_LEVELS, GL_RGB, GL_FLOAT, null)
|
||||
gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_WRAP_S, GL_REPEAT)
|
||||
gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
|
||||
gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
|
||||
gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
|
||||
for l in 0 .. SKY_PREFILTER_LEVELS {
|
||||
sky_convolve(p_pre, sky_prefilter, l, 512, 256, fr(l, SKY_PREFILTER_LEVELS - 1))
|
||||
sky_convolve(p_pre, sky_prefilter, l, sky_prefilter_w, sky_prefilter_w / 2, fr(l, SKY_PREFILTER_LEVELS - 1))
|
||||
}
|
||||
# BRDF LUT
|
||||
let p_brdf = sky_p_brdf
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ function stream_band(s: Stream, d: int) -> 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.
|
||||
const STREAM_BUDGET_US: int = 2500 # microseconds of generation per frame
|
||||
var STREAM_BUDGET_US: int = 2500 # microseconds of generation per frame; a setting may move it
|
||||
var stream_deadline: long = 0
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue