feat(render3d): the meadow's blades are culled on the GPU - under 1.5 ms of a MoltenVK frame for all the grass

- grass_cull.comp decides each candidate blade once a frame (place, ground, density, water,
  slope, frustum, colour field) and writes survivors into three bands by distance, one indirect
  draw each; grass_inst.vert only bends and places the vertices. OpenGL keeps grass.vert's
  per-vertex path; R3D_GRASS_GPU=0 compares
- compute programs take sampled textures after their buffers (gpu_compute_tex / gpu_dispatch_tex),
  and every dispatch now records a compute-to-draw memory barrier
- the blades as a sward: 4 m cells, bands with five, three and one-quad blades, spacing doubling
  every 18 m to 70 m, never narrower than a pixel; lit facing the sun and leaning to the sky,
  shadow looked up above the ground (it read the terrain as its caster), a colour ramp that
  leaves only the sheath dark, clumps, dry patches and a tussock shade worked out per blade
- scatter layers flagged grass are skipped while the blades draw (and under R3D_NOGRASS);
  R3D_BLADES, R3D_NOBLADES win over the game's setting; R3D_GRASS_S0/D0 for measuring

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-27 15:38:43 +03:00
parent f815f3e3cf
commit cc384efee0
34 changed files with 798 additions and 212 deletions

View file

@ -234,7 +234,10 @@ function cmd_shaders() -> int {
let depth_remap = "\n#undef main\nvoid main() {\n ludic_gl_main();\n gl_Position.z = (gl_Position.z + gl_Position.w) * 0.5;\n}\n"
# invariant: the foliage's depth prepass and its lit pass (depth EQUAL) are different variants,
# and without it the compiler may compute their positions a hair apart and fail the test
var vsrc = "#version 460\n" + defines + shd_wind + "invariant gl_Position;\n#define main ludic_gl_main\n" + shd_file(parts[0]) + depth_remap
# as programs.ludic: a blade's vertex stage gets the noise its colour field needs
var vnoise = ""
if Text.contains(defines, "#define GBLADE") { vnoise = shd_noise }
var vsrc = "#version 460\n" + defines + shd_wind + vnoise + "invariant gl_Position;\n#define main ludic_gl_main\n" + shd_file(parts[0]) + depth_remap
# A mesh shader stands in for the vertex stage (a variant whose first file is *.mesh): it writes an
# array of positions, so it cannot be wrapped for the depth remap or take invariant gl_Position -
# it remaps depth itself. Its SPIR-V keeps the .vert name, so the manifest and loader are unchanged.
@ -304,7 +307,8 @@ function cmd_shaders() -> int {
let cp = Text.split(cl, "|")
if len(cp) < 2 { err(`shaders: bad compute line: {cl}\n`); return 1 }
let glsl = `{tmp}/{cp[0]}.comp.glsl`
if not write_file(glsl, "#version 460\n" + shd_file(cp[1])) { err(`shaders: cannot write {glsl}\n`); return 1 }
# the noise functions come first, as in a fragment stage (grass_cull uses them; the rest ignore them)
if not write_file(glsl, "#version 460\n" + shd_noise + shd_file(cp[1])) { err(`shaders: cannot write {glsl}\n`); return 1 }
let spv = `{outdir}/{cp[0]}.comp.spv`
let logf = `{tmp}/{cp[0]}.comp.log`
if not shq(`{shd_q(shd_bin + "/glslangValidator")} -V -S comp {shd_q(glsl)} -o {shd_q(spv)} > {shd_q(logf)} 2>&1`) {