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>
224 lines
9 KiB
Text
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
|
|
}
|
|
}
|
|
}
|