You walked through a meadow and every blade ignored you, which is the most noticeable thing missing from every step the game asks you to take. u_push (x, z, radius) bends blades away from a body and DOWN - a trodden stem is shorter as well as leaning, and leaving the height alone made them splay outward like a fan instead of being walked through. Applied after the blade's own yaw has put it into world axes and before it is tipped onto the ground normal, so the push is a world direction rather than something in the blade's private frame. Radius 0 means nobody is there, so nothing is paid for when it does not apply. Measured: GL 6.9 s, VK 7.3 s over 400 frames, unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
263 lines
11 KiB
Text
263 lines
11 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
|
|
# Where a body is standing, and how wide it pushes. The grass has never known the player was
|
|
# in it: you walked through a meadow and every blade ignored you, which is the single most
|
|
# noticeable thing missing from every step the game asks you to take. The game sets this each
|
|
# frame; radius 0 means nobody is there.
|
|
var grass_push_x: int = 0
|
|
var grass_push_z: int = 0
|
|
var grass_push_r: 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
|
|
# Mesh-shader grass (Settings, Video, Advanced): the chunked path's records, but each chunk is one
|
|
# mesh dispatch - work group y a tile, x a batch of GRASS_MESH_BLADES of its blades - so a blade the
|
|
# tests reject emits nothing instead of eight degenerate vertices. R3D_MESH_GRASS=1 / 0 overrides.
|
|
const GRASS_MESH_BLADES: int = 16
|
|
var grass_mesh_on: bool = false
|
|
var grass_mesh_prog: int = 0
|
|
function r3d_mesh_grass(on: bool) -> void {
|
|
grass_mesh_on = on
|
|
if r3d_env_has("R3D_MESH_GRASS") { grass_mesh_on = Text.to_int(r3d_env("R3D_MESH_GRASS")) != 0 }
|
|
}
|
|
function grass_mesh_live() -> bool { return grass_mesh_on and grass_merge and grass_mesh_prog != 0 and gpu_has_mesh() }
|
|
|
|
# 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 r3d_env_has("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)
|
|
if grass_merge and gpu_has_mesh() { grass_mesh_prog = r3d_program("grass.mesh", "model.frag", "#define FOLIAGE\n#define BLADE\n#define MESH\n") }
|
|
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 r3d_env_has("R3D_NOBLADES") { grass_on = false }
|
|
if r3d_env_has("R3D_GRASS_R") { grass_radius = fi(Text.to_int(r3d_env("R3D_GRASS_R"))) }
|
|
if r3d_env_has("R3D_GRASS_DBG") { grass_dbg = Text.to_int(r3d_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 }
|
|
var p = grass_prog
|
|
if grass_mesh_live() { p = grass_mesh_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_f3(gpu_uniform(p, "u_push"), grass_push_x, grass_push_z, grass_push_r)
|
|
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 }
|
|
var p = grass_prog
|
|
let mesh = grass_mesh_live()
|
|
if mesh { p = grass_mesh_prog } else { 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] }
|
|
if mesh {
|
|
# one dispatch per tile, as many blade batches as that tile has: sized for a chunk's largest
|
|
# tile, the far tiles beside a near one ran thousands of empty invocations (9.8 ms against the
|
|
# chunked path's 2 at 4K on an RTX 3070 Ti)
|
|
let cells = grass_band_cells[b]
|
|
for q in 0 .. m {
|
|
var total = f_to_int(grass_tv[(k + q) * 4 + 2]) * cells * cells
|
|
if total > 65535 { total = 65535 }
|
|
if total > 0 {
|
|
for c in 0 .. 4 { grass_chunk_tv[c] = grass_tv[(k + q) * 4 + c] }
|
|
u_f4v(gpu_uniform(p, "u_tiles"), 1, grass_chunk_tv)
|
|
gpu_draw_mesh_tasks((total + GRASS_MESH_BLADES - 1) / GRASS_MESH_BLADES, 1, 1)
|
|
grass_draws += 1
|
|
}
|
|
}
|
|
} else {
|
|
u_f4v(gpu_uniform(p, "u_tiles"), m, grass_chunk_tv)
|
|
gpu_draw_mesh_indirect(grass_mesh, grass_cmds, k * 20, m, 0, 0)
|
|
}
|
|
if not mesh { grass_draws += 1 }
|
|
k += m
|
|
}
|
|
}
|
|
}
|