diff --git a/changes/quality-settings-live.md b/changes/quality-settings-live.md new file mode 100644 index 00000000..9573140d --- /dev/null +++ b/changes/quality-settings-live.md @@ -0,0 +1,8 @@ +bump: minor +type: feat +**Texture filtering and shadow resolution as settings a game can change while it runs.** + +- `r3d_set_anisotropy(level)` sets the anisotropic filtering of every mipmapped texture already + loaded, not only of later uploads, on OpenGL and Vulkan. +- `shadow_set_res(size)` remakes the cascades' depth layers at 1024, 2048 or 4096 (`shadow_res` + replaces the `SHADOW_RES` constant); the lighting reads the texel size from the map. diff --git a/packages/ludic.render3d/shaders/lighting.glsl b/packages/ludic.render3d/shaders/lighting.glsl index 00c70b51..1023455a 100644 --- a/packages/ludic.render3d/shaders/lighting.glsl +++ b/packages/ludic.render3d/shaders/lighting.glsl @@ -109,7 +109,7 @@ float shadowSample(int c, vec3 wpos, float biasWorld) { vec3 p = lp.xyz / lp.w * 0.5 + 0.5; if (p.x < 0.0 || p.x > 1.0 || p.y < 0.0 || p.y > 1.0 || p.z > 1.0) return 1.0; float bias = biasWorld / cascadeRange(c); - float texel = 1.0 / 2048.0; + float texel = 1.0 / float(textureSize(u_shadow, 0).x); // the shadow resolution setting float r = ign(gl_FragCoord.xy) * 6.2831853; float cs = cos(r), sn = sin(r); mat2 rot = mat2(cs, sn, -sn, cs); diff --git a/packages/ludic.render3d/shaders/spv/01089da8.frag.spv b/packages/ludic.render3d/shaders/spv/01089da8.frag.spv index 9451ae2b..004f4e20 100644 Binary files a/packages/ludic.render3d/shaders/spv/01089da8.frag.spv and b/packages/ludic.render3d/shaders/spv/01089da8.frag.spv differ diff --git a/packages/ludic.render3d/shaders/spv/1ce5c5d1.frag.spv b/packages/ludic.render3d/shaders/spv/1ce5c5d1.frag.spv index f6fdbfb9..9e4564cd 100644 Binary files a/packages/ludic.render3d/shaders/spv/1ce5c5d1.frag.spv and b/packages/ludic.render3d/shaders/spv/1ce5c5d1.frag.spv differ diff --git a/packages/ludic.render3d/shaders/spv/1fde9cd2.frag.spv b/packages/ludic.render3d/shaders/spv/1fde9cd2.frag.spv index 9451ae2b..004f4e20 100644 Binary files a/packages/ludic.render3d/shaders/spv/1fde9cd2.frag.spv and b/packages/ludic.render3d/shaders/spv/1fde9cd2.frag.spv differ diff --git a/packages/ludic.render3d/shaders/spv/3733fc38.frag.spv b/packages/ludic.render3d/shaders/spv/3733fc38.frag.spv index 880bcafc..8d524af4 100644 Binary files a/packages/ludic.render3d/shaders/spv/3733fc38.frag.spv and b/packages/ludic.render3d/shaders/spv/3733fc38.frag.spv differ diff --git a/packages/ludic.render3d/shaders/spv/3b183d8f.frag.spv b/packages/ludic.render3d/shaders/spv/3b183d8f.frag.spv index 4c0138cc..2003961f 100644 Binary files a/packages/ludic.render3d/shaders/spv/3b183d8f.frag.spv and b/packages/ludic.render3d/shaders/spv/3b183d8f.frag.spv differ diff --git a/packages/ludic.render3d/shaders/spv/46e2325c.frag.spv b/packages/ludic.render3d/shaders/spv/46e2325c.frag.spv index db73cadd..8d7aed6e 100644 Binary files a/packages/ludic.render3d/shaders/spv/46e2325c.frag.spv and b/packages/ludic.render3d/shaders/spv/46e2325c.frag.spv differ diff --git a/packages/ludic.render3d/shaders/spv/4dda406a.frag.spv b/packages/ludic.render3d/shaders/spv/4dda406a.frag.spv index 0a18eccb..14b18c85 100644 Binary files a/packages/ludic.render3d/shaders/spv/4dda406a.frag.spv and b/packages/ludic.render3d/shaders/spv/4dda406a.frag.spv differ diff --git a/packages/ludic.render3d/shaders/spv/79b77ed9.frag.spv b/packages/ludic.render3d/shaders/spv/79b77ed9.frag.spv index 34ac5ebe..208e5110 100644 Binary files a/packages/ludic.render3d/shaders/spv/79b77ed9.frag.spv and b/packages/ludic.render3d/shaders/spv/79b77ed9.frag.spv differ diff --git a/packages/ludic.render3d/shaders/spv/7ed7de52.frag.spv b/packages/ludic.render3d/shaders/spv/7ed7de52.frag.spv index ab4e5104..f83e77c0 100644 Binary files a/packages/ludic.render3d/shaders/spv/7ed7de52.frag.spv and b/packages/ludic.render3d/shaders/spv/7ed7de52.frag.spv differ diff --git a/packages/ludic.render3d/shaders/spv/a5ad521e.frag.spv b/packages/ludic.render3d/shaders/spv/a5ad521e.frag.spv index a609ec5c..a06e86ba 100644 Binary files a/packages/ludic.render3d/shaders/spv/a5ad521e.frag.spv and b/packages/ludic.render3d/shaders/spv/a5ad521e.frag.spv differ diff --git a/packages/ludic.render3d/shaders/spv/b4587cd4.frag.spv b/packages/ludic.render3d/shaders/spv/b4587cd4.frag.spv index ade925a9..ef976ce4 100644 Binary files a/packages/ludic.render3d/shaders/spv/b4587cd4.frag.spv and b/packages/ludic.render3d/shaders/spv/b4587cd4.frag.spv differ diff --git a/packages/ludic.render3d/shaders/spv/b9fc2041.frag.spv b/packages/ludic.render3d/shaders/spv/b9fc2041.frag.spv index 9451ae2b..004f4e20 100644 Binary files a/packages/ludic.render3d/shaders/spv/b9fc2041.frag.spv and b/packages/ludic.render3d/shaders/spv/b9fc2041.frag.spv differ diff --git a/packages/ludic.render3d/shaders/spv/bf3151c8.frag.spv b/packages/ludic.render3d/shaders/spv/bf3151c8.frag.spv index 4ed612f5..c23d11f0 100644 Binary files a/packages/ludic.render3d/shaders/spv/bf3151c8.frag.spv and b/packages/ludic.render3d/shaders/spv/bf3151c8.frag.spv differ diff --git a/packages/ludic.render3d/shaders/spv/f4a2b586.frag.spv b/packages/ludic.render3d/shaders/spv/f4a2b586.frag.spv index 8724ddee..db1dd14e 100644 Binary files a/packages/ludic.render3d/shaders/spv/f4a2b586.frag.spv and b/packages/ludic.render3d/shaders/spv/f4a2b586.frag.spv differ diff --git a/packages/ludic.render3d/shadow.ludic b/packages/ludic.render3d/shadow.ludic index 1339874e..41748097 100644 --- a/packages/ludic.render3d/shadow.ludic +++ b/packages/ludic.render3d/shadow.ludic @@ -4,7 +4,8 @@ # of the camera frustum and snapped to its own texel grid (no swimming). # ============================================================================ -const SHADOW_RES: int = 2048 +# the size of each cascade's depth layer; shadow_set_res changes it at run time +var shadow_res: int = 2048 const SHADOW_CASCADES: int = 5 var sh_tex: int = 0 @@ -21,19 +22,7 @@ var sh_corner: words = null var sh_cascade: int = 0 # the cascade being rendered (for casters that skip far ones) function shadow_init() -> void { - sh_tex = gpu_tex_new() - gpu_tex_bind(GPU_TEX2D_ARRAY, sh_tex) - gpu_tex_image3d(GL_DEPTH_COMPONENT32F, SHADOW_RES, SHADOW_RES, SHADOW_CASCADES, GL_DEPTH_COMPONENT, GL_FLOAT, null) - gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR) - gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR) - gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER) - gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER) - gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE) - gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL) - let border = gl_floats(4) - gl_put(border, 0, 1.0); gl_put(border, 1, 1.0); gl_put(border, 2, 1.0); gl_put(border, 3, 1.0) - gpu_tex_border(GPU_TEX2D_ARRAY, border) - free(border) + shadow_make_tex() sh_fbo = gpu_fb_new() gpu_fb_bind(sh_fbo) gpu_fb_no_color() @@ -48,6 +37,33 @@ function shadow_init() -> void { sh_corner = words(3) } +# A shadow resolution setting: 1024, 2048 or 4096 per cascade. The depth layers are made again at +# the new size; the pass attaches a layer per cascade every frame, and the lighting reads the +# texel size from the map itself, so nothing else has to follow. +function shadow_set_res(r: int) -> void { + if r < 256 or r == shadow_res { return } + shadow_res = r + if sh_tex == 0 { return } + gpu_tex_free(sh_tex) + shadow_make_tex() +} + +function shadow_make_tex() -> void { + sh_tex = gpu_tex_new() + gpu_tex_bind(GPU_TEX2D_ARRAY, sh_tex) + gpu_tex_image3d(GL_DEPTH_COMPONENT32F, shadow_res, shadow_res, SHADOW_CASCADES, GL_DEPTH_COMPONENT, GL_FLOAT, null) + gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR) + gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR) + gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER) + gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER) + gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE) + gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL) + let border = gl_floats(4) + gl_put(border, 0, 1.0); gl_put(border, 1, 1.0); gl_put(border, 2, 1.0); gl_put(border, 3, 1.0) + gpu_tex_border(GPU_TEX2D_ARRAY, border) + free(border) +} + # light view-projection for the camera-frustum slice [near, far] function shadow_fit(c: int, near: int, far: int) -> void { # Fit the slice in VIEW space, not world space. The bounding sphere of a frustum @@ -92,7 +108,7 @@ function shadow_fit(c: int, near: int, far: int) -> void { let up = v3_new(F_ZERO, F_ONE, F_ZERO) m4_look_at(sh_tmp_view, eye, center, up) # snap the ortho window to the shadow texel grid - let texel = f_div(f_mul(radius, F_TWO), fi(SHADOW_RES)) + let texel = f_div(f_mul(radius, F_TWO), fi(shadow_res)) m4_xform_point(sh_corner, sh_tmp_view, center[0], center[1], center[2]) let ox = f_sub(f_mul(f_floor(f_div(sh_corner[0], texel)), texel), sh_corner[0]) let oy = f_sub(f_mul(f_floor(f_div(sh_corner[1], texel)), texel), sh_corner[1]) @@ -114,7 +130,7 @@ function shadow_cascade_vp(c: int) -> words { return mem_off(sh_vp, c * 64) } function shadow_pass() -> void { var near = cam_near gpu_fb_bind(sh_fbo) - gpu_viewport(0, 0, SHADOW_RES, SHADOW_RES) + gpu_viewport(0, 0, shadow_res, shadow_res) gpu_depth_test(true) gpu_depth_func(GL_LESS) gpu_depth_bias(2.0, 4.0) @@ -144,7 +160,7 @@ function shadow_pass() -> void { m4_xform_point(q, vp, sh_probe_x, sh_probe_y, sh_probe_z) print(`probe base ndc {f_fx(q[0])} {f_fx(q[1])} {f_fx(q[2])}`) m4_xform_point(q, vp, sh_probe_x, f_add(sh_probe_y, fi(15)), sh_probe_z) - print(`probe top ndc {f_fx(q[0])} {f_fx(q[1])} {f_fx(q[2])} -> map texel {f_to_int(f_mul(f_add(f_mul(q[0], F_HALF), F_HALF), fi(SHADOW_RES)))} {f_to_int(f_mul(f_add(f_mul(q[1], F_HALF), F_HALF), fi(SHADOW_RES)))}`) + print(`probe top ndc {f_fx(q[0])} {f_fx(q[1])} {f_fx(q[2])} -> map texel {f_to_int(f_mul(f_add(f_mul(q[0], F_HALF), F_HALF), fi(shadow_res)))} {f_to_int(f_mul(f_add(f_mul(q[1], F_HALF), F_HALF), fi(shadow_res)))}`) # where the top's shadow lands on the ground: walk down the sun ray let gx = f_sub(f_add(sh_probe_x, F_ZERO), f_mul(sun_dir[0], f_div(fi(15), sun_dir[1]))) let gz = f_sub(sh_probe_z, f_mul(sun_dir[2], f_div(fi(15), sun_dir[1]))) @@ -172,7 +188,7 @@ var sh_probe_z: int = 0 # Debug: cascade depths as grey PPMs (build/dbg_shadow_.ppm) function shadow_dump() -> void { - let n = SHADOW_RES * SHADOW_RES + let n = shadow_res * shadow_res let buf = words(n * SHADOW_CASCADES) gpu_tex_bind(GPU_TEX2D_ARRAY, sh_tex) gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_COMPARE_MODE, GL_NONE) @@ -182,13 +198,13 @@ function shadow_dump() -> void { let vp = shadow_cascade_vp(2) let q = words(3) m4_xform_point(q, vp, sh_probe_x, f_add(sh_probe_y, fi(12)), sh_probe_z) - let tx = f_to_int(f_mul(f_add(f_mul(q[0], F_HALF), F_HALF), fi(SHADOW_RES))) - let ty = f_to_int(f_mul(f_add(f_mul(q[1], F_HALF), F_HALF), fi(SHADOW_RES))) + let tx = f_to_int(f_mul(f_add(f_mul(q[0], F_HALF), F_HALF), fi(shadow_res))) + let ty = f_to_int(f_mul(f_add(f_mul(q[1], F_HALF), F_HALF), fi(shadow_res))) let want = f_add(f_mul(q[2], F_HALF), F_HALF) print(`probe (12 m up) texel {tx} {ty} card depth {f_fx(f_mul(want, fi(1000)))}/1000`) for dy in 0 .. 5 { let yy = ty - 40 + dy * 20 - print(` row {yy}: {f_fx(f_mul(buf[2 * n + yy * SHADOW_RES + tx - 20], fi(1000)))} {f_fx(f_mul(buf[2 * n + yy * SHADOW_RES + tx], fi(1000)))} {f_fx(f_mul(buf[2 * n + yy * SHADOW_RES + tx + 20], fi(1000)))} /1000`) + print(` row {yy}: {f_fx(f_mul(buf[2 * n + yy * shadow_res + tx - 20], fi(1000)))} {f_fx(f_mul(buf[2 * n + yy * shadow_res + tx], fi(1000)))} {f_fx(f_mul(buf[2 * n + yy * shadow_res + tx + 20], fi(1000)))} /1000`) } free(q) } @@ -203,10 +219,10 @@ function shadow_dump() -> void { let f = file_open(`build/dbg_shadow_{c}.ppm`, "wb") let hdr = `P6\n{sm} {sm}\n255\n` file_write(f, hdr, len(hdr)) - let st = SHADOW_RES / sm + let st = shadow_res / sm for y in 0 .. sm { for x in 0 .. sm { - let d = buf[c * n + (y * st) * SHADOW_RES + x * st] + let d = buf[c * n + (y * st) * shadow_res + x * st] let g = f_to_int(f_mul(f_clamp(f_div(f_sub(d, lo), f_max(f_sub(hi, lo), fl(0.0001))), F_ZERO, F_ONE), fi(255))) row[x * 3] = g; row[x * 3 + 1] = g; row[x * 3 + 2] = g } diff --git a/packages/ludic.render3d/texture.ludic b/packages/ludic.render3d/texture.ludic index 272b4bcd..ec267126 100644 --- a/packages/ludic.render3d/texture.ludic +++ b/packages/ludic.render3d/texture.ludic @@ -18,6 +18,30 @@ var tex_depth: int = 0 # bits per sample (8 or 16) var tex_file_len: int = 0 var tex_anisotropy: fixed = 16.0 +# The anisotropic filtering level for every mipmapped texture, loaded or not: 1 (off), 2, 4, 8 +# or 16. Textures uploaded before a change are updated in place - on OpenGL by setting the +# parameter again, on Vulkan by rewriting the record the sampler cache reads - so a settings +# screen can offer it live instead of on the next start. +function r3d_set_anisotropy(level: int) -> void { + var a: fixed = 1.0 + if level >= 2 { a = 2.0 } + if level >= 4 { a = 4.0 } + if level >= 8 { a = 8.0 } + if level >= 16 { a = 16.0 } + if a == tex_anisotropy { return } + tex_anisotropy = a + if gpu_tx == null { return } + let keep = gpu_bound_2d + for t in 1 .. gpu_tx_cap { + let o = t * GPU_TX_W + # a 2D texture with mipmaps that was given a level when it was uploaded + if gpu_tx[o] != GPU_TEX2D or gpu_tx[o + 10] != 1 or gpu_tx[o + 11] == 0 { continue } + gpu_tex_bind(GPU_TEX2D, t) + gpu_tex_paramf(GPU_TEX2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, a) + } + if keep > 0 { gpu_tex_bind(GPU_TEX2D, keep) } +} + function r3d_read_file(path: pointer) -> pointer { let f = file_open(path, "rb") if f == null { return null }