GrassKind (grass_kind.ludic) carries what grass.ludic, grass_gpu.ludic, grass.vert, grass_cull.comp and model.frag's BLADE path held as constants: the cell, s0 / d0 / radius, the row bands and rows, the blade profile (neck, root, swell, tip, bend), the heights, clumps, widths and arch, where it grows (slope, snow, the photograph's test), the root / tip / gone-to-seed colours, the far tufts, the root occlusion, the roughness, the sheen and the wind. Its defaults are those constants exactly, so a game that sets nothing draws as before. The shaders read them as u_gk_* (the cull as five more Params vec4s, 288 bytes); grass_reset.comp writes each band's index count so a kind of other rows reaches the draw in order. grass_quality holds the kind to a settings tier (never denser, never farther); R3D_GRASS_* still win. grass_profile_w / _bend are the one profile, for the meshes and a game's preview. grass.mesh takes only the cell: the rest of it is an older copy that already differs from grass.vert, left as it draws. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
343 lines
19 KiB
Text
343 lines
19 KiB
Text
# ============================================================================
|
|
# grass.ludic — procedural GPU ground cover with continuous density (no rings).
|
|
#
|
|
# The world is cut into 4 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.
|
|
# ============================================================================
|
|
|
|
# The cell (GRASS_CELL until the kinds) is the kind's now: render3d_st.grass_cell (grass_kind.ludic).
|
|
# A photographed blade, as an atlas of straightened blades side by side (the game sets
|
|
# this; the renderer does not name a game asset). 0 = the procedural gradient, which is
|
|
# what this was for a year: a two-tone ramp with a hard edge, and every blade in the
|
|
# valley the same blade. A real blade has a midrib, a colour that runs olive to straw,
|
|
# browning where it has dried and a tip that is its own shape - none of which can be
|
|
# written down, only photographed.
|
|
# 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.
|
|
# 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
|
|
# 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
|
|
function r3d_mesh_grass(render3d_st: mut Render3dState, on: bool) -> void {
|
|
render3d_st.grass_mesh_on = on
|
|
if r3d_env_has(render3d_st, "R3D_MESH_GRASS") { render3d_st.grass_mesh_on = Text.to_int(r3d_env(render3d_st, "R3D_MESH_GRASS")) != 0 }
|
|
}
|
|
function grass_mesh_live(render3d_st: Render3dState) -> bool { return render3d_st.grass_mesh_on and render3d_st.grass_merge and render3d_st.grass_mesh_prog != 0 and gpu_has_mesh(render3d_st) }
|
|
|
|
# a blade: `rows` rows of 2 vertices (x across, y along, z bend), attribute 2 = uv
|
|
function grass_blade_mesh(render3d_st: mut Render3dState, rows: int) -> Mesh {
|
|
let gk = grass_kind(render3d_st)
|
|
let m = gpu_mesh_new(render3d_st)
|
|
let v = gl_floats(rows * 2 * 5)
|
|
var k = 0
|
|
# The blade's PROFILE, and it is the whole difference between grass and a green spike.
|
|
# It used to be `1 - t^2.5` floored at 0.12 with a bend of 0.28t^2: widest at the very
|
|
# bottom, narrowing to a needle, and standing almost straight. That is the silhouette of
|
|
# a pine needle, and eighty of them to the square metre read as a bed of nails.
|
|
#
|
|
# A real blade is narrow where it leaves the sheath, WIDEST about a fifth of the way up,
|
|
# and then tapers the rest of the way to a fine point - and it arches over under its own
|
|
# weight. Both terms below say that. The tip is floored just off zero rather than at 0.12
|
|
# so the point is a point and not a cut-off stub, but not so low that the last quad is
|
|
# degenerate.
|
|
#
|
|
# The numbers are the kind's (grass_profile_w / _bend). grass.mesh carries A COPY of the
|
|
# default profile for the mesh-shader path; change both or the Windows blades stop matching.
|
|
for r in 0 .. rows {
|
|
let t = float(r) / float(rows - 1)
|
|
let taper = grass_profile_w(gk, t)
|
|
let bend = grass_profile_bend(gk, t)
|
|
for sd in 0 .. 2 {
|
|
var x = -0.5
|
|
if sd == 1 { x = 0.5 }
|
|
gl_put_bits(v, k, float_bits(x * taper)); gl_put_bits(v, k + 1, float_bits(t)); gl_put_bits(v, k + 2, float_bits(bend))
|
|
gl_put_bits(v, k + 3, float_bits(float(sd))); gl_put_bits(v, k + 4, float_bits(t))
|
|
k += 5
|
|
}
|
|
}
|
|
gpu_mesh_vertices(render3d_st, m, v, gl_bytes_of(rows * 2 * 5), GPU_STATIC)
|
|
gpu_mesh_attr(render3d_st, m, 0, 3, GPU_F32, 20, 0, false)
|
|
gpu_mesh_attr(render3d_st, 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(render3d_st, m, data_of(idx), nq * 6 * 4, 4)
|
|
free(idx)
|
|
m.count = nq * 6
|
|
gpu_mesh_done(render3d_st, m)
|
|
return m
|
|
}
|
|
|
|
# the GPU memory it makes is counted as VKM_GRASS (R3D_VKMEM)
|
|
function grass_init(render3d_st: mut Render3dState) -> void {
|
|
let was = render3d_st.gvk_tag
|
|
render3d_st.gvk_tag = VKM_GRASS
|
|
grass_init__t(render3d_st)
|
|
render3d_st.gvk_tag = was
|
|
}
|
|
@alloc_ok("start-up: the device, its tables, the programs, the passes and the world's first textures are made once, before play")
|
|
function grass_init__t(render3d_st: mut Render3dState) -> void {
|
|
render3d_st.grass_merge = gpu_has_mdi(render3d_st) and not r3d_env_has(render3d_st, "R3D_GRASS_TILES")
|
|
var defs = "#define FOLIAGE\n#define BLADE\n#define GBLADE\n"
|
|
if render3d_st.grass_merge {
|
|
defs = defs + "#define TILES\n"
|
|
render3d_st.grass_rec = words(GRASS_RECS * 5); render3d_st.grass_tv = floats(GRASS_RECS * 4); render3d_st.grass_chunk_tv = floats(GRASS_CHUNK * 4)
|
|
render3d_st.grass_band_start = words(4); render3d_st.grass_band_cells = words(4)
|
|
render3d_st.grass_band_mesh = new []Mesh
|
|
for b in 0 .. 4 { push(render3d_st.grass_band_mesh, null) }
|
|
render3d_st.grass_cmds = gpu_buffer_new(render3d_st)
|
|
}
|
|
render3d_st.grass_prog = r3d_program(render3d_st, "grass.vert", "model.frag", defs)
|
|
if render3d_st.grass_merge and gpu_has_mesh(render3d_st) { render3d_st.grass_mesh_prog = r3d_program(render3d_st, "grass.mesh", "model.frag", "#define FOLIAGE\n#define BLADE\n#define MESH\n") }
|
|
grass_meshes_build(render3d_st)
|
|
# Matched to the blade's width: a 1 cm blade at 0.11 m spacing covers a third of what a
|
|
# 2.8 cm blade did, and the meadow goes bare. The game's graphics settings override this
|
|
# (gfx_grass_spacing), but only once game_init has run - a plain headless render never
|
|
# gets there, so the two have to agree or a shot shows something no player will see.
|
|
# That is exactly how the last change measured as "no effect": the render was identical
|
|
# because this line, not the settings, was deciding.
|
|
# The spacing doubles every 18 m and the blades stop at 70 m: at 45 m and 1600 m a 1 cm blade
|
|
# was under a pixel from 20 m on, costing a full blade's vertices to shimmer (5 ms of 9.7 on GL).
|
|
# The spacing, its doubling distance and the reach are the kind's (0.066, 18 and 70 by default),
|
|
# held to the settings' tier (grass_quality) and then to R3D_GRASS_S0 / _D0 / _R (grass_derive).
|
|
grass_derive(render3d_st)
|
|
if r3d_env_has(render3d_st, "R3D_NOBLADES") { render3d_st.grass_on = false }
|
|
if r3d_env_has(render3d_st, "R3D_GRASS_DBG") { render3d_st.grass_dbg = Text.to_int(r3d_env(render3d_st, "R3D_GRASS_DBG")) }
|
|
gg_init(render3d_st)
|
|
}
|
|
|
|
# The three blades, at the kind's rows: five, four quads, near (the arch needs somewhere to bend, and
|
|
# at four rows a blade that leans over is three straight segments); three; and one quad far out.
|
|
# Made again only when a kind with other rows or another profile is set (grass_kind_set).
|
|
@alloc_ok("start-up, and a kind of another shape set at a world build: the blade meshes are made once")
|
|
function grass_meshes_build(render3d_st: mut Render3dState) -> void {
|
|
if render3d_st.grass_prog == 0 { return }
|
|
let k = grass_kind(render3d_st)
|
|
mesh_free(render3d_st, render3d_st.grass_mesh)
|
|
mesh_free(render3d_st, render3d_st.grass_mesh3)
|
|
mesh_free(render3d_st, render3d_st.grass_mesh2)
|
|
render3d_st.grass_mesh = grass_blade_mesh(render3d_st, k.rows_near)
|
|
render3d_st.grass_mesh3 = grass_blade_mesh(render3d_st, k.rows_mid)
|
|
render3d_st.grass_mesh2 = grass_blade_mesh(render3d_st, k.rows_far)
|
|
if render3d_st.gg_on { gg_meshes_build(render3d_st) }
|
|
}
|
|
|
|
# indices per 16 m cell that could exist at distance d (the count the shader computes)
|
|
# Under a fog wall the blades end at three quarters of it. A blade's tip stands against ground
|
|
# much farther off, and a dark blade 85% fogged read as a dark band against fully fogged ground
|
|
# behind it; ending here, they thin out (the cull's last 30%) while the fog is still coming in.
|
|
const GRASS_FOG_END: float = 0.75
|
|
function grass_reach(render3d_st: Render3dState) -> float {
|
|
let w = render3d_st.r3d_fog_wall * GRASS_FOG_END
|
|
if w > 0.0 and w < render3d_st.grass_radius { return w }
|
|
return render3d_st.grass_radius
|
|
}
|
|
|
|
function grass_count_at(render3d_st: Render3dState, d: float) -> int {
|
|
let spacing = render3d_st.grass_s0 * (1.0 + d / render3d_st.grass_d0)
|
|
let n = float(render3d_st.grass_cell * render3d_st.grass_cell) / (spacing * spacing)
|
|
return int(n) + 1
|
|
}
|
|
|
|
# one tile size over one distance band
|
|
function grass_tiles(render3d_st: mut Render3dState, size: int, d_min: float, d_max: float, blade: Mesh) -> void {
|
|
let p = render3d_st.grass_prog
|
|
let cells = size / render3d_st.grass_cell
|
|
if d_min >= grass_reach(render3d_st) { return }
|
|
if render3d_st.grass_merge {
|
|
render3d_st.grass_band_start[render3d_st.grass_band_n] = render3d_st.grass_n; render3d_st.grass_band_cells[render3d_st.grass_band_n] = cells
|
|
render3d_st.grass_band_mesh[render3d_st.grass_band_n] = blade; render3d_st.grass_band_n += 1
|
|
} else {
|
|
u_i(render3d_st, gpu_uniform(render3d_st, p, "u_tile_cells"), cells)
|
|
}
|
|
let sz = float(size)
|
|
let half = sz * 0.5
|
|
let reach = d_max + half * 1.5
|
|
let tx0 = int(Math.floor((render3d_st.cam_pos[0] - reach) / sz))
|
|
let tx1 = int(Math.floor((render3d_st.cam_pos[0] + reach) / sz))
|
|
let tz0 = int(Math.floor((render3d_st.cam_pos[2] - reach) / sz))
|
|
let tz1 = int(Math.floor((render3d_st.cam_pos[2] + reach) / sz))
|
|
let corner_r = half * 1.42
|
|
var tz = tz0
|
|
while tz <= tz1 {
|
|
var tx = tx0
|
|
while tx <= tx1 {
|
|
let ox = float(tx) * sz; let oz = float(tz) * sz
|
|
let cx = ox + half; let cz = oz + half
|
|
let dx = cx - render3d_st.cam_pos[0]; let dz = cz - render3d_st.cam_pos[2]
|
|
let dc = Math.sqrt(dx * dx + dz * dz)
|
|
# the tile's nearest and farthest points decide which band it belongs to
|
|
let dnear = Math.max(dc - corner_r, 0.0)
|
|
if dc < d_min or not (dnear < d_max) { tx += 1; continue }
|
|
let cy = terrain_height(render3d_st, cx, cz)
|
|
if cam_sphere_visible(render3d_st, cx, cy, cz, corner_r + 6.0) {
|
|
let per = grass_count_at(render3d_st, dnear)
|
|
if per > 0 and render3d_st.grass_merge {
|
|
if render3d_st.grass_n < GRASS_RECS {
|
|
var inst = per * cells * cells
|
|
if inst > 65535 { inst = 65535 }
|
|
let r = render3d_st.grass_n * 5
|
|
render3d_st.grass_rec[r] = blade.count; render3d_st.grass_rec[r + 1] = inst; render3d_st.grass_rec[r + 2] = 0; render3d_st.grass_rec[r + 3] = 0
|
|
render3d_st.grass_rec[r + 4] = ((render3d_st.grass_n - render3d_st.grass_band_start[render3d_st.grass_band_n - 1]) % GRASS_CHUNK) * 65536
|
|
let t = render3d_st.grass_n * 4
|
|
render3d_st.grass_tv[t] = ox; render3d_st.grass_tv[t + 1] = oz; render3d_st.grass_tv[t + 2] = float(per); render3d_st.grass_tv[t + 3] = 0.0
|
|
render3d_st.grass_n += 1
|
|
}
|
|
} else if per > 0 {
|
|
u_f2(render3d_st, gpu_uniform(render3d_st, p, "u_tile"), ox, oz)
|
|
u_i(render3d_st, gpu_uniform(render3d_st, p, "u_per_cell"), per)
|
|
mesh_draw_instanced(render3d_st, blade, per * cells * cells)
|
|
render3d_st.grass_draws += 1
|
|
}
|
|
}
|
|
tx += 1
|
|
}
|
|
tz += 1
|
|
}
|
|
}
|
|
|
|
function grass_live(render3d_st: Render3dState) -> bool { return not render3d_st.grass_env_off and (render3d_st.grass_on or render3d_st.grass_force) and render3d_st.grass_prog != 0 }
|
|
|
|
function grass_draw(render3d_st: mut Render3dState) -> void {
|
|
if not grass_live(render3d_st) or render3d_st.ter_reflect { return }
|
|
if render3d_st.gg_on { gg_draw(render3d_st); return }
|
|
var p = render3d_st.grass_prog
|
|
if grass_mesh_live(render3d_st) { p = render3d_st.grass_mesh_prog }
|
|
gpu_use_program(render3d_st, p)
|
|
grass_bind(render3d_st, p)
|
|
gpu_cull(render3d_st, false)
|
|
render3d_st.grass_draws = 0
|
|
render3d_st.grass_n = 0
|
|
render3d_st.grass_band_n = 0
|
|
gpu_mesh_bind(render3d_st, render3d_st.grass_mesh)
|
|
grass_bands(render3d_st)
|
|
if render3d_st.grass_merge { grass_flush(render3d_st) }
|
|
gpu_cull(render3d_st, true)
|
|
}
|
|
|
|
# the blades' uniforms, shared by the per-vertex path and the culled one (grass_gpu.ludic)
|
|
function grass_bind(render3d_st: mut Render3dState, p: int) -> void {
|
|
u_mat4(render3d_st, gpu_uniform(render3d_st, p, "u_view"), render3d_st.cam_view)
|
|
u_mat4(render3d_st, gpu_uniform(render3d_st, p, "u_proj"), render3d_st.cam_proj)
|
|
u_mat4(render3d_st, gpu_uniform(render3d_st, p, "u_vp"), render3d_st.cam_vp_clean)
|
|
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_wind"), grass_kind(render3d_st).wind)
|
|
grass_bind_kind(render3d_st, p)
|
|
# copied into a local first: a global reaching a uniform call is the codegen fault
|
|
# CLAUDE.md records against u_wade and u_flutter, and it costs a day every time
|
|
let btex = render3d_st.grass_blade_tex
|
|
let bcols = render3d_st.grass_blade_cols
|
|
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_blade_cols"), float(bcols))
|
|
var bon = 0.0
|
|
if btex != 0 { bon = 1.0; r3d_bind_2d(render3d_st, p, "u_blade_tex", 12, btex) }
|
|
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_blade_tex_on"), bon)
|
|
u_f3(render3d_st, gpu_uniform(render3d_st, p, "u_push"), render3d_st.grass_push_x, render3d_st.grass_push_z, render3d_st.grass_push_r)
|
|
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_rough_scale"), 1.0)
|
|
u_v3(render3d_st, gpu_uniform(render3d_st, p, "u_tint"), render3d_st.sc_blade_tint)
|
|
u_v3(render3d_st, gpu_uniform(render3d_st, p, "u_blade_base"), render3d_st.sc_blade_base)
|
|
u_v3(render3d_st, gpu_uniform(render3d_st, p, "u_blade_tip"), render3d_st.sc_blade_tip)
|
|
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_cull"), grass_reach(render3d_st))
|
|
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_model_h"), 0.0)
|
|
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_s0"), render3d_st.grass_s0)
|
|
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_d0"), render3d_st.grass_d0)
|
|
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_radius"), grass_reach(render3d_st))
|
|
var ph = render3d_st.post_h
|
|
if ph <= 0 { ph = gl_height() }
|
|
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_px"), 2.0 * Math.tan(render3d_st.cam_fov * 0.5) / float(ph))
|
|
u_i(render3d_st, gpu_uniform(render3d_st, p, "u_dbg"), render3d_st.grass_dbg)
|
|
var orthotex = render3d_st.ter_ortho_tex
|
|
var oon = 1.0
|
|
if orthotex == 0 { orthotex = render3d_st.ter_height_tex; oon = 0.0 }
|
|
r3d_bind_2d(render3d_st, p, "u_ortho", 4, orthotex)
|
|
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_ortho_on"), oon)
|
|
var lake = -100000.0
|
|
if render3d_st.ter_lake_ex != 0.0 { lake = render3d_st.ter_lake_level }
|
|
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_lake_level"), lake)
|
|
var sea = lake
|
|
if render3d_st.ter_sea_set { sea = render3d_st.ter_sea_level }
|
|
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_sea_level"), sea)
|
|
u_f4(render3d_st, gpu_uniform(render3d_st, p, "u_lake"), render3d_st.ter_lake_cx, render3d_st.ter_lake_cz, render3d_st.ter_lake_ex, render3d_st.ter_lake_ez)
|
|
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_snow_line"), render3d_st.ter_snow_line)
|
|
sky_bind_lighting(render3d_st, p)
|
|
shadow_bind(render3d_st, p)
|
|
fog_bind(render3d_st, p)
|
|
# 0.15 was enough to put a hard white highlight down the length of a blade whenever it
|
|
# caught the sun, and a white blade of grass is the one thing grass is never. Measured:
|
|
# at 0.15, 0.70% of a near-ground frame was over 210 of 255; at 0.0 it is 0.05%. A blade
|
|
# does have a faint sheen, so this is small rather than nothing - the foliage layers have
|
|
# used 0.05 all along and never showed the fault. The kind's `spec` (0.008).
|
|
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_spec_scale"), grass_kind(render3d_st).spec)
|
|
}
|
|
|
|
# the per-vertex path's tiles, in four bands
|
|
function grass_bands(render3d_st: mut Render3dState) -> void {
|
|
# four bands: the tile grows and the blade loses rows with distance (a band past the radius is
|
|
# skipped); a tile asks for what its NEAREST point needs, so a wide tile far out wastes the rest
|
|
# the kind's row bands, inside the 40 m where the tiles grow to 16 m
|
|
let mid = Math.min(grass_kind(render3d_st).band_mid, 40.0)
|
|
let near = Math.min(grass_kind(render3d_st).band_near, mid)
|
|
grass_tiles(render3d_st, 4, 0.0, near, render3d_st.grass_mesh)
|
|
grass_tiles(render3d_st, 4, near, mid, render3d_st.grass_mesh3)
|
|
grass_tiles(render3d_st, 8, mid, 40.0, render3d_st.grass_mesh2)
|
|
grass_tiles(render3d_st, 16, 40.0, grass_reach(render3d_st), render3d_st.grass_mesh2)
|
|
}
|
|
|
|
# the records gathered this frame: uploaded once, then each band GRASS_CHUNK records a draw
|
|
function grass_flush(render3d_st: mut Render3dState) -> void {
|
|
if render3d_st.grass_n == 0 { return }
|
|
var p = render3d_st.grass_prog
|
|
let mesh = grass_mesh_live(render3d_st)
|
|
if mesh { p = render3d_st.grass_mesh_prog } else { gpu_buffer_upload(render3d_st, render3d_st.grass_cmds, render3d_st.grass_n * 20, data_of(render3d_st.grass_rec), GPU_DYNAMIC) }
|
|
for b in 0 .. render3d_st.grass_band_n {
|
|
let s = render3d_st.grass_band_start[b]
|
|
var e = render3d_st.grass_n
|
|
if b + 1 < render3d_st.grass_band_n { e = render3d_st.grass_band_start[b + 1] }
|
|
if e > s { u_i(render3d_st, gpu_uniform(render3d_st, p, "u_tile_cells"), render3d_st.grass_band_cells[b]) }
|
|
let blade = render3d_st.grass_band_mesh[b]
|
|
if e > s and not mesh { gpu_mesh_bind(render3d_st, blade) }
|
|
var k = s
|
|
while k < e {
|
|
var m = e - k
|
|
if m > GRASS_CHUNK { m = GRASS_CHUNK }
|
|
for q in 0 .. m * 4 { render3d_st.grass_chunk_tv[q] = render3d_st.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 = render3d_st.grass_band_cells[b]
|
|
for q in 0 .. m {
|
|
var total = int(render3d_st.grass_tv[(k + q) * 4 + 2]) * cells * cells
|
|
if total > 65535 { total = 65535 }
|
|
if total > 0 {
|
|
for c in 0 .. 4 { render3d_st.grass_chunk_tv[c] = render3d_st.grass_tv[(k + q) * 4 + c] }
|
|
u_f4v(render3d_st, gpu_uniform(render3d_st, p, "u_tiles"), 1, render3d_st.grass_chunk_tv)
|
|
gpu_draw_mesh_tasks(render3d_st, (total + GRASS_MESH_BLADES - 1) / GRASS_MESH_BLADES, 1, 1)
|
|
render3d_st.grass_draws += 1
|
|
}
|
|
}
|
|
} else {
|
|
u_f4v(render3d_st, gpu_uniform(render3d_st, p, "u_tiles"), m, render3d_st.grass_chunk_tv)
|
|
gpu_draw_mesh_indirect(render3d_st, blade, render3d_st.grass_cmds, k * 20, m, 0, 0)
|
|
}
|
|
if not mesh { render3d_st.grass_draws += 1 }
|
|
k += m
|
|
}
|
|
}
|
|
}
|