ludic/packages/ludic.render3d/grass.ludic
Orkuncakilkaya cdfffa62bb render3d: grass in chunks of 256 tiles a draw, flower casters only into the cascades they reach
Grass (Vulkan with multi-draw indirect): every visible tile is a record in one buffer, uploaded once a
frame, and each band draws its records 256 at a time. A record's firstInstance is its place in the
chunk times 65536; grass.vert's TILES variant reads that place's corner and indices per cell from
u_tiles. R3D_GRASS_TILES=1 keeps a draw per tile. OpenGL is unchanged.

Casters: a LOD level with no impostor is drawn into a shadow cascade only when its distance band,
widened by six times its height, the camera's height over the ground and the frustum's corner reach,
can touch that cascade's receivers. The flowers' mesh levels (6 - 30 m) leave the three outer
cascades. R3D_CAST_ALL=1 draws every level everywhere. OpenGL frames byte-identical at all five
viewpoints; alpha-tested shadow draws at a 460 -> 244.

gpu_has_mdi() guards both this and the GPU-culled trees' multi-record draws.

Camp: Mac Vulkan 2645 -> 2191 (grass) -> 2034 draws; PC 2657 -> 2046, self-tests 59/59, validation 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 16:58:53 +03:00

224 lines
9 KiB
Text

# ============================================================================
# grass.ludic — procedural GPU ground cover with continuous density (no rings).
#
# The world is cut into 16 m cells; blade j of a cell always stands in the same place
# (shaders/grass.vert). Draws are per tile: the CPU walks tiles around the camera,
# frustum-culls them, and feeds each visible tile as many blade indices per cell as its
# NEAREST point could need; the vertex stage then keeps only the indices that exist at
# each blade's own distance, so density is one smooth function of distance everywhere.
# Tiles are 16 m near, 64 m in the middle distance and 256 m far, purely to keep the
# draw count down — the cells and their hashes are the same in every tile size.
# ============================================================================
const GRASS_CELL: int = 16
var grass_prog: int = 0
var grass_mesh: Mesh = null
var grass_on: bool = true
var grass_wind: int = 0
var grass_s0: int = 0 # float bits: blade spacing at the camera (m)
var grass_d0: int = 0 # the distance at which the spacing has doubled (m)
var grass_radius: int = 0 # no blades past this (m)
var grass_draws: int = 0
var grass_dbg: int = 0
# Vulkan with multi-draw indirect: every visible tile is a record in one buffer, uploaded once a
# frame, and each band draws its records GRASS_CHUNK at a time - one draw for up to 256 tiles. A
# record's firstInstance is its place in its chunk times 65536; grass.vert's TILES variant reads that
# place's corner and indices per cell from u_tiles. R3D_GRASS_TILES=1 keeps a draw per tile.
const GRASS_CHUNK: int = 256
const GRASS_RECS: int = 8192
var grass_merge: bool = false
var grass_rec: words = null # VkDrawIndexedIndirectCommand records, 5 words each
var grass_tv: words = null # per record: corner x, corner z, indices per cell, 0 (float bits)
var grass_chunk_tv: words = null
var grass_n: int = 0
var grass_cmds: int = 0
var grass_band_start: words = null # per band: its first record
var grass_band_cells: words = null # ... and its 16 m cells per tile side
var grass_band_n: int = 0
# a blade: `rows` rows of 2 vertices (x across, y along, z bend), attribute 2 = uv
function grass_blade_mesh(rows: int) -> Mesh {
let m = gpu_mesh_new()
let v = gl_floats(rows * 2 * 5)
var k = 0
for r in 0 .. rows {
let t = fr(r, rows - 1)
let taper = f_max(f_sub(F_ONE, f_mul(t, f_mul(t, f_sqrt(t)))), fl(0.12))
let bend = f_mul(f_mul(t, t), fl(0.28))
for sd in 0 .. 2 {
var x = f_neg(F_HALF)
if sd == 1 { x = F_HALF }
gl_put_bits(v, k, f_mul(x, taper)); gl_put_bits(v, k + 1, t); gl_put_bits(v, k + 2, bend)
gl_put_bits(v, k + 3, fi(sd)); gl_put_bits(v, k + 4, t)
k += 5
}
}
gpu_mesh_vertices(m, v, gl_bytes_of(rows * 2 * 5), GPU_STATIC)
gpu_mesh_attr(m, 0, 3, GPU_F32, 20, 0, false)
gpu_mesh_attr(m, 2, 2, GPU_F32, 20, 12, false)
free(v)
let nq = rows - 1
let idx = words(nq * 6)
for q in 0 .. nq {
let b = q * 2
idx[q * 6] = b; idx[q * 6 + 1] = b + 1; idx[q * 6 + 2] = b + 2
idx[q * 6 + 3] = b + 1; idx[q * 6 + 4] = b + 3; idx[q * 6 + 5] = b + 2
}
gpu_mesh_indices(m, idx, nq * 6 * 4, 4)
free(idx)
m.count = nq * 6
gpu_mesh_done(m)
return m
}
function grass_init() -> void {
grass_merge = gpu_has_mdi() and not Os.has_env("R3D_GRASS_TILES")
var defs = "#define FOLIAGE\n#define BLADE\n"
if grass_merge {
defs = defs + "#define TILES\n"
grass_rec = words(GRASS_RECS * 5); grass_tv = words(GRASS_RECS * 4); grass_chunk_tv = words(GRASS_CHUNK * 4)
grass_band_start = words(4); grass_band_cells = words(4)
grass_cmds = gpu_buffer_new()
}
grass_prog = r3d_program("grass.vert", "model.frag", defs)
grass_mesh = grass_blade_mesh(4)
grass_wind = fl(2.4)
grass_s0 = fl(0.11)
grass_d0 = fi(45)
grass_radius = fi(1600)
if Os.has_env("R3D_NOBLADES") { grass_on = false }
if Os.has_env("R3D_GRASS_R") { grass_radius = fi(Text.to_int(Os.env("R3D_GRASS_R"))) }
if Os.has_env("R3D_GRASS_DBG") { grass_dbg = Text.to_int(Os.env("R3D_GRASS_DBG")) }
}
# indices per 16 m cell that could exist at distance d (the count the shader computes)
function grass_count_at(d: int) -> int {
let spacing = f_mul(grass_s0, f_add(F_ONE, f_div(d, grass_d0)))
let n = f_div(fi(GRASS_CELL * GRASS_CELL), f_mul(spacing, spacing))
return f_to_int(n) + 1
}
# one tile size over one distance band
function grass_tiles(size: int, d_min: int, d_max: int) -> void {
let p = grass_prog
let cells = size / GRASS_CELL
if grass_merge {
grass_band_start[grass_band_n] = grass_n; grass_band_cells[grass_band_n] = cells; grass_band_n += 1
} else {
u_i(gpu_uniform(p, "u_tile_cells"), cells)
}
let sz = fi(size)
let half = f_mul(sz, F_HALF)
let reach = f_add(d_max, f_mul(half, fl(1.5)))
let tx0 = f_to_int(f_floor(f_div(f_sub(cam_pos[0], reach), sz)))
let tx1 = f_to_int(f_floor(f_div(f_add(cam_pos[0], reach), sz)))
let tz0 = f_to_int(f_floor(f_div(f_sub(cam_pos[2], reach), sz)))
let tz1 = f_to_int(f_floor(f_div(f_add(cam_pos[2], reach), sz)))
let corner_r = f_mul(half, fl(1.42))
var tz = tz0
while tz <= tz1 {
var tx = tx0
while tx <= tx1 {
let ox = f_mul(fi(tx), sz); let oz = f_mul(fi(tz), sz)
let cx = f_add(ox, half); let cz = f_add(oz, half)
let dx = f_sub(cx, cam_pos[0]); let dz = f_sub(cz, cam_pos[2])
let dc = f_sqrt(f_add(f_mul(dx, dx), f_mul(dz, dz)))
# the tile's nearest and farthest points decide which band it belongs to
let dnear = f_max(f_sub(dc, corner_r), F_ZERO)
if f_ls(dc, d_min) or not f_ls(dnear, d_max) { tx += 1; continue }
let cy = terrain_height(cx, cz)
if cam_sphere_visible(cx, cy, cz, f_add(corner_r, fi(6))) {
let per = grass_count_at(dnear)
if per > 0 and grass_merge {
if grass_n < GRASS_RECS {
var inst = per * cells * cells
if inst > 65535 { inst = 65535 }
let r = grass_n * 5
grass_rec[r] = grass_mesh.count; grass_rec[r + 1] = inst; grass_rec[r + 2] = 0; grass_rec[r + 3] = 0
grass_rec[r + 4] = ((grass_n - grass_band_start[grass_band_n - 1]) % GRASS_CHUNK) * 65536
let t = grass_n * 4
grass_tv[t] = ox; grass_tv[t + 1] = oz; grass_tv[t + 2] = fi(per); grass_tv[t + 3] = F_ZERO
grass_n += 1
}
} else if per > 0 {
u_f2(gpu_uniform(p, "u_tile"), ox, oz)
u_i(gpu_uniform(p, "u_per_cell"), per)
mesh_draw_instanced(grass_mesh, per * cells * cells)
grass_draws += 1
}
}
tx += 1
}
tz += 1
}
}
function grass_draw() -> void {
if not grass_on or ter_reflect or grass_prog == 0 { return }
let p = grass_prog
gpu_use_program(p)
u_mat4(gpu_uniform(p, "u_view"), cam_view)
u_mat4(gpu_uniform(p, "u_proj"), cam_proj)
u_mat4(gpu_uniform(p, "u_vp"), cam_vp_clean)
u_f(gpu_uniform(p, "u_wind"), grass_wind)
u_f(gpu_uniform(p, "u_rough_scale"), F_ONE)
u_v3(gpu_uniform(p, "u_tint"), sc_blade_tint)
u_v3(gpu_uniform(p, "u_blade_base"), sc_blade_base)
u_v3(gpu_uniform(p, "u_blade_tip"), sc_blade_tip)
u_f(gpu_uniform(p, "u_cull"), grass_radius)
u_f(gpu_uniform(p, "u_model_h"), F_ZERO)
u_f(gpu_uniform(p, "u_s0"), grass_s0)
u_f(gpu_uniform(p, "u_d0"), grass_d0)
u_f(gpu_uniform(p, "u_radius"), grass_radius)
u_i(gpu_uniform(p, "u_dbg"), grass_dbg)
var orthotex = ter_ortho_tex
var oon = F_ONE
if orthotex == 0 { orthotex = ter_height_tex; oon = F_ZERO }
r3d_bind_2d(p, "u_ortho", 4, orthotex)
u_f(gpu_uniform(p, "u_ortho_on"), oon)
var lake = fl(-100000.0)
if ter_lake_ex != 0 { lake = ter_lake_level }
u_f(gpu_uniform(p, "u_lake_level"), lake)
var sea = lake
if ter_sea_set { sea = ter_sea_level }
u_f(gpu_uniform(p, "u_sea_level"), sea)
u_f4(gpu_uniform(p, "u_lake"), ter_lake_cx, ter_lake_cz, ter_lake_ex, ter_lake_ez)
u_f(gpu_uniform(p, "u_snow_line"), ter_snow_line)
sky_bind_lighting(p)
shadow_bind(p)
fog_bind(p)
u_f(gpu_uniform(p, "u_spec_scale"), fl(0.15))
gpu_cull(false)
grass_draws = 0
grass_n = 0
grass_band_n = 0
gpu_mesh_bind(grass_mesh)
grass_tiles(16, F_ZERO, fi(300))
grass_tiles(64, fi(300), fi(1200))
grass_tiles(256, fi(1200), grass_radius)
if grass_merge { grass_flush() }
gpu_cull(true)
}
# the records gathered this frame: uploaded once, then each band GRASS_CHUNK records a draw
function grass_flush() -> void {
if grass_n == 0 { return }
let p = grass_prog
gpu_buffer_upload(grass_cmds, grass_n * 20, grass_rec, GPU_DYNAMIC)
for b in 0 .. grass_band_n {
let s = grass_band_start[b]
var e = grass_n
if b + 1 < grass_band_n { e = grass_band_start[b + 1] }
if e > s { u_i(gpu_uniform(p, "u_tile_cells"), grass_band_cells[b]) }
var k = s
while k < e {
var m = e - k
if m > GRASS_CHUNK { m = GRASS_CHUNK }
for q in 0 .. m * 4 { grass_chunk_tv[q] = grass_tv[k * 4 + q] }
u_fv(gpu_uniform(p, "u_tiles"), m * 4, grass_chunk_tv)
gpu_draw_mesh_indirect(grass_mesh, grass_cmds, k * 20, m, 0, 0)
grass_draws += 1
k += m
}
}
}