grass_flush uploaded u_tiles (vec4[256]) with u_fv(4n floats). On Vulkan an
array element is copied at the size given and placed at the array's stride,
so each tile got one float: nonsense corners and zero blades a cell. The
meadow had no grass on the Vulkan renderer since cdfffa6 while the draw
counts looked right. u_f4v uploads n vec4s; OpenGL was never affected.
PC camp: chunked path matches R3D_GRASS_TILES=1, 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_f4v(gpu_uniform(p, "u_tiles"), m, grass_chunk_tv)
|
|
gpu_draw_mesh_indirect(grass_mesh, grass_cmds, k * 20, m, 0, 0)
|
|
grass_draws += 1
|
|
k += m
|
|
}
|
|
}
|
|
}
|