perf(render3d): the terrain's height and normal in R32F + RG16F, 128 MB where one RGBA32F was 256

The first generation pass's R32F height is kept as ter_height_tex (not copied into an RGBA32F),
so every reader of the height - placement, the read-back, physics, selftest16's 9 mm - sees the
same 32 bits. ternormal.frag writes the baked normal's x and z into ter_normal_tex (RG16F), and
the six places that read it (terrain.frag twice, tersun.frag, grass.vert, grass.mesh,
grass_cull.comp, which takes a third texture at binding 6) rebuild y. SPIR-V regenerated.

Maroon Lake's play, headless Vulkan: 2793 -> 2647 MB. The camp's frame: 0.066% of pixels differ by
more than 8 (mean 0.024/255), isolated grass blades at the slope gate. ludic-dev test 307/307.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-28 02:31:18 +03:00
parent f1049953c4
commit 1675eb451f
26 changed files with 77 additions and 36 deletions

View file

@ -17,6 +17,9 @@ uniform mat4 u_vp;
uniform vec3 u_cam_pos;
uniform float u_time;
uniform sampler2D u_ts_height;
// the baked terrain normal: x and z in RG16F, y rebuilt (a terrain normal always points up)
uniform sampler2D u_ter_normal;
vec3 terNormal(vec2 uv) { vec2 xz = textureLod(u_ter_normal, uv, 0.0).rg; return vec3(xz.x, sqrt(max(1.0 - dot(xz, xz), 0.0)), xz.y); }
uniform vec2 u_ts_origin;
uniform float u_ts_half;
uniform sampler2D u_ortho;
@ -95,7 +98,7 @@ void main() {
vec4 ht = textureLod(u_ts_height, huv, 0.0);
vec4 croot = u_vp * vec4(xz.x, ht.r, xz.y, 1.0);
if (croot.w < -1.0 || abs(croot.x) > croot.w * 1.25 + 1.5 || abs(croot.y) > croot.w * 1.4 + 1.5) continue;
vec3 gn = normalize(ht.gba);
vec3 gn = terNormal(huv);
float h3 = bladeHash(ci, j, 2), h4 = bladeHash(ci, j, 3);
float wl = u_sea_level;
if (u_lake.z > 0.0) { vec2 q = (xz - u_lake.xy) / u_lake.zw; if (dot(q, q) < 1.0) wl = max(wl, u_lake_level); }

View file

@ -15,6 +15,9 @@ uniform mat4 u_vp;
uniform vec3 u_cam_pos;
uniform float u_time;
uniform sampler2D u_ts_height;
// the baked terrain normal: x and z in RG16F, y rebuilt (a terrain normal always points up)
uniform sampler2D u_ter_normal;
vec3 terNormal(vec2 uv) { vec2 xz = textureLod(u_ter_normal, uv, 0.0).rg; return vec3(xz.x, sqrt(max(1.0 - dot(xz, xz), 0.0)), xz.y); }
uniform vec2 u_ts_origin;
uniform float u_ts_half;
uniform sampler2D u_ortho;
@ -133,7 +136,7 @@ void main() {
vec4 ht = texture(u_ts_height, huv);
vec4 croot = u_vp * vec4(xz.x, ht.r, xz.y, 1.0);
if (croot.w < -1.0 || abs(croot.x) > croot.w * 1.25 + 1.5 || abs(croot.y) > croot.w * 1.4 + 1.5) { cull(); return; }
vec3 gn = normalize(ht.gba);
vec3 gn = terNormal(huv);
float h3 = bladeHash(ci, j, 2), h4 = bladeHash(ci, j, 3);
// No blades under water: the sea's line everywhere, and the lake's inside its outline. One
// line for both left a lake above the sea with grass on its bed or a valley with none.

View file

@ -25,8 +25,9 @@ layout(set = 0, binding = 0) uniform Params {
layout(set = 0, binding = 1) readonly buffer Tiles { vec4 tiles[]; }; // corner x/z, indices per cell, cells per side
layout(set = 0, binding = 2) buffer Out { vec4 outv[]; }; // 4 per blade
layout(set = 0, binding = 3) buffer Cmds { uint cmds[]; }; // VkDrawIndexedIndirectCommand per band
layout(set = 0, binding = 4) uniform sampler2D u_height; // height, normal (terrain's u_ts_height)
layout(set = 0, binding = 4) uniform sampler2D u_height; // the height (terrain's u_ts_height)
layout(set = 0, binding = 5) uniform sampler2D u_ortho; // the photograph
layout(set = 0, binding = 6) uniform sampler2D u_ter_normal; // the normal: x and z, y rebuilt
const float CELL = 4.0; // grass.ludic GRASS_CELL
@ -92,7 +93,8 @@ void main() {
// the frustum, on a sphere round the blade (it stands at most 0.9 m tall)
vec3 root = vec3(xz.x, ht.r, xz.y);
for (int k = 0; k < 4; k++) { if (dot(pr.planes[k].xyz, root) + pr.planes[k].w < -1.0) return; }
vec3 gn = normalize(ht.gba);
vec2 gxz = textureLod(u_ter_normal, huv, 0.0).rg;
vec3 gn = vec3(gxz.x, sqrt(max(1.0 - dot(gxz, gxz), 0.0)), gxz.y);
float h3 = bladeHash(ci, j, 2), h4 = bladeHash(ci, j, 3);
float wl = pr.lev.y;
if (pr.lake.z > 0.0) { vec2 q = (xz - pr.lake.xy) / pr.lake.zw; if (dot(q, q) < 1.0) wl = max(wl, pr.lev.x); }

View file

@ -918,8 +918,9 @@ T d48d6a48 u_nrm 7
T d48d6a48 u_ortho 8
T d48d6a48 u_prefilter 9
T d48d6a48 u_shadow 10
T d48d6a48 u_tershadow 11
T d48d6a48 u_ts_height 12
T d48d6a48 u_ter_normal 11
T d48d6a48 u_tershadow 12
T d48d6a48 u_ts_height 13
P cb13f3c6 grass.vert model.frag #define FOLIAGE;#define BLADE;#define GBLADE;
B cb13f3c6 vert 0 324
U cb13f3c6 vert u_view 0 mat4 1 0
@ -1000,8 +1001,9 @@ T cb13f3c6 u_nrm 7
T cb13f3c6 u_ortho 8
T cb13f3c6 u_prefilter 9
T cb13f3c6 u_shadow 10
T cb13f3c6 u_tershadow 11
T cb13f3c6 u_ts_height 12
T cb13f3c6 u_ter_normal 11
T cb13f3c6 u_tershadow 12
T cb13f3c6 u_ts_height 13
P b8cff226 grass.vert model.frag #define FOLIAGE;#define BLADE;#define GBLADE;#define TILES;
B b8cff226 vert 0 4420
U b8cff226 vert u_view 0 mat4 1 0
@ -1083,8 +1085,9 @@ T b8cff226 u_nrm 7
T b8cff226 u_ortho 8
T b8cff226 u_prefilter 9
T b8cff226 u_shadow 10
T b8cff226 u_tershadow 11
T b8cff226 u_ts_height 12
T b8cff226 u_ter_normal 11
T b8cff226 u_tershadow 12
T b8cff226 u_ts_height 13
P 3733fc38 impostor.vert impostor.frag
B 3733fc38 vert 0 232
U 3733fc38 vert u_view 0 mat4 1 0
@ -3020,8 +3023,9 @@ T 2e2908c8 u_rock_n 13
T 2e2908c8 u_shadow 14
T 2e2908c8 u_snow_d 15
T 2e2908c8 u_sunshadow 16
T 2e2908c8 u_tershadow 17
T 2e2908c8 u_ts_height 18
T 2e2908c8 u_ter_normal 17
T 2e2908c8 u_tershadow 18
T 2e2908c8 u_ts_height 19
P 1f95f31b terrain.vert terrain.frag #define FAR_ONLY;
B 1f95f31b vert 0 204
U 1f95f31b vert u_half 0 float 1 0
@ -3097,8 +3101,9 @@ T 1f95f31b u_rock_n 13
T 1f95f31b u_shadow 14
T 1f95f31b u_snow_d 15
T 1f95f31b u_sunshadow 16
T 1f95f31b u_tershadow 17
T 1f95f31b u_ts_height 18
T 1f95f31b u_ter_normal 17
T 1f95f31b u_tershadow 18
T 1f95f31b u_ts_height 19
P f4382918 terrain.vert terrain.frag #define NEAR_ONLY;
B f4382918 vert 0 204
U f4382918 vert u_half 0 float 1 0
@ -3174,8 +3179,9 @@ T f4382918 u_rock_n 13
T f4382918 u_shadow 14
T f4382918 u_snow_d 15
T f4382918 u_sunshadow 16
T f4382918 u_tershadow 17
T f4382918 u_ts_height 18
T f4382918 u_ter_normal 17
T f4382918 u_tershadow 18
T f4382918 u_ts_height 19
P f2952361 terrain.vert terrain.frag #define SUN_INLINE;
B f2952361 vert 0 204
U f2952361 vert u_half 0 float 1 0
@ -3251,8 +3257,9 @@ T f2952361 u_rock_n 13
T f2952361 u_shadow 14
T f2952361 u_snow_d 15
T f2952361 u_sunshadow 16
T f2952361 u_tershadow 17
T f2952361 u_ts_height 18
T f2952361 u_ter_normal 17
T f2952361 u_tershadow 18
T f2952361 u_ts_height 19
P a0f2dbac terrain.vert terrain.frag #define SUN_INLINE;#define FAR_ONLY;
B a0f2dbac vert 0 204
U a0f2dbac vert u_half 0 float 1 0
@ -3328,8 +3335,9 @@ T a0f2dbac u_rock_n 13
T a0f2dbac u_shadow 14
T a0f2dbac u_snow_d 15
T a0f2dbac u_sunshadow 16
T a0f2dbac u_tershadow 17
T a0f2dbac u_ts_height 18
T a0f2dbac u_ter_normal 17
T a0f2dbac u_tershadow 18
T a0f2dbac u_ts_height 19
P 5e4589a1 terrain.vert terrain.frag #define SUN_INLINE;#define NEAR_ONLY;
B 5e4589a1 vert 0 204
U 5e4589a1 vert u_half 0 float 1 0
@ -3405,8 +3413,9 @@ T 5e4589a1 u_rock_n 13
T 5e4589a1 u_shadow 14
T 5e4589a1 u_snow_d 15
T 5e4589a1 u_sunshadow 16
T 5e4589a1 u_tershadow 17
T 5e4589a1 u_ts_height 18
T 5e4589a1 u_ter_normal 17
T 5e4589a1 u_tershadow 18
T 5e4589a1 u_ts_height 19
P bf3151c8 terrain.vert tersun.frag
B bf3151c8 vert 0 204
U bf3151c8 vert u_half 0 float 1 0
@ -3462,8 +3471,9 @@ T bf3151c8 u_height 3
T bf3151c8 u_irradiance 4
T bf3151c8 u_prefilter 5
T bf3151c8 u_shadow 6
T bf3151c8 u_tershadow 7
T bf3151c8 u_ts_height 8
T bf3151c8 u_ter_normal 7
T bf3151c8 u_tershadow 8
T bf3151c8 u_ts_height 9
P 46e2325c water.vert water.frag
B 46e2325c vert 0 152
U 46e2325c vert u_view 0 mat4 1 0

View file

@ -1,5 +1,5 @@
// Second generation pass: copy the height into R and bake the B-spline surface normal into
// GBA, once, at texel resolution. terrain.frag used to differentiate the bicubic height
// Second generation pass: bake the B-spline surface normal, once, at texel resolution: x and z
// into an RG16F beside the R32F height (y is rebuilt where it is read). terrain.frag used to differentiate the bicubic height
// per pixel — four bicubic reads, sixteen taps — for a quantity that never changes.
in vec2 v_uv;
out vec4 o;
@ -28,5 +28,5 @@ void main() {
float hd = heightSmooth(u_src, v_uv - vec2(0, step));
float hu = heightSmooth(u_src, v_uv + vec2(0, step));
vec3 n = normalize(vec3(hl - hr, 2.0 * world, hd - hu));
o = vec4(texture(u_src, v_uv).r, n);
o = vec4(n.x, n.z, 0.0, 0.0); // into RG16F: x and z, y rebuilt when read (always up)
}

View file

@ -7,6 +7,9 @@ out vec4 o_color;
#define gnoise(p) 0.1
#endif
uniform sampler2D u_height;
// the baked terrain normal: x and z in RG16F, y rebuilt (a terrain normal always points up)
uniform sampler2D u_ter_normal;
vec3 terNormal(vec2 uv) { vec2 xz = texture(u_ter_normal, uv).rg; return vec3(xz.x, sqrt(max(1.0 - dot(xz, xz), 0.0)), xz.y); }
uniform float u_half;
uniform float u_texel; // height-map texel size in uv
uniform sampler2D u_grass_d; uniform sampler2D u_grass_n; uniform sampler2D u_grass_a;
@ -76,7 +79,7 @@ float heightSmooth(sampler2D tex, vec2 uv) {
}
// baked at generation (ternormal.frag) into the height texture's GBA
vec3 terrainNormal(vec2 uv) {
return normalize(texture(u_height, uv).gba);
return terNormal(uv);
}
// stochastic (triangle-grid) sampling: three randomly offset / rotated taps blended by
@ -223,7 +226,7 @@ void triC(bool cheap, sampler2D d, sampler2D nm, sampler2D am, vec3 p, vec3 dpx,
uniform float u_far_split;
uniform float u_far_band;
float sunInline(vec3 p, float dist, float viewDepth) {
vec3 Ng = normalize(texture(u_height, v_huv).gba);
vec3 Ng = terNormal(v_huv);
if (dist > u_far_split + u_far_band) return sunShadowCheap(p, Ng, viewDepth);
if (dist < u_far_split - u_far_band) return sunShadow(p, Ng, viewDepth);
return mix(sunShadow(p, Ng, viewDepth), sunShadowCheap(p, Ng, viewDepth),

View file

@ -12,13 +12,16 @@ in vec3 v_wpos;
in vec2 v_huv;
out float o_sh;
uniform sampler2D u_height;
// the baked terrain normal: x and z in RG16F, y rebuilt (a terrain normal always points up)
uniform sampler2D u_ter_normal;
vec3 terNormal(vec2 uv) { vec2 xz = texture(u_ter_normal, uv).rg; return vec3(xz.x, sqrt(max(1.0 - dot(xz, xz), 0.0)), xz.y); }
uniform mat4 u_view;
uniform float u_far_split;
uniform float u_far_band;
void main() {
vec3 p = v_wpos;
if (p.y < u_clip_y) discard;
vec3 N = normalize(texture(u_height, v_huv).gba);
vec3 N = terNormal(v_huv);
float dist = length(p - u_cam_pos);
float viewDepth = -(u_view * vec4(p, 1.0)).z;
// The same tier choice the ground makes, cross-faded over the same band: the near tier