render3d: a grass kind - the GPU blades' per-kind numbers are a GrassKind record the game hands over (grass_kind_set), not constants

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>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 19:03:33 +03:00
parent d9a73d63bd
commit e908672034
25 changed files with 335 additions and 75 deletions

View file

@ -406,7 +406,10 @@ export state Render3dState {
grass_prog: int = 0
grass_mesh: Mesh = null
grass_on: bool = true
grass_wind: float = 0.0
grass_kind: GrassKind = null # the kind the blades are (grass_kind.ludic); its wind, sheen and shape
grass_cell: int = 4 # the kind's hash cell in metres (1, 2 or 4)
grass_q_s0: float = 0.0 # the settings' tier: no closer spacing than this (grass_quality)
grass_q_r: float = 0.0 # ... and no blades past this (0: the kind's own radius)
grass_s0: float = 0.0 # float bits: blade spacing at the camera (m)
grass_d0: float = 0.0 # the distance at which the spacing has doubled (m)
grass_radius: float = 0.0 # no blades past this (m)

View file

@ -814,8 +814,8 @@ function gvk_startup_state(render3d_st: mut Render3dState) -> void {
render3d_st.gvk_pc_pass = new []int; render3d_st.gvk_pc_pipe = new []long
render3d_st.gvk_pc_last = words(4096)
for i in 0 .. 4096 { render3d_st.gvk_pc_last[i] = -1 }
render3d_st.gg_rb = words(4); render3d_st.gg_cb = words(1); render3d_st.gg_bufs = words(3)
render3d_st.gg_texs = words(7); render3d_st.gg_pr = words(52)
render3d_st.gg_rb = words(8); render3d_st.gg_cb = words(1); render3d_st.gg_bufs = words(3)
render3d_st.gg_texs = words(7); render3d_st.gg_pr = words(72)
if render3d_st.gpu_unit_2d == null {
render3d_st.gpu_unit_2d = words(32)
for i in 0 .. 32 { render3d_st.gpu_unit_2d[i] = -1 }

View file

@ -10,7 +10,7 @@
# draw count down — the cells and their hashes are the same in every tile size.
# ============================================================================
const GRASS_CELL: int = 4
# 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
@ -39,6 +39,7 @@ function grass_mesh_live(render3d_st: Render3dState) -> bool { return render3d_s
# 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
@ -53,14 +54,12 @@ function grass_blade_mesh(render3d_st: mut Render3dState, rows: int) -> Mesh {
# so the point is a point and not a cut-off stub, but not so low that the last quad is
# degenerate.
#
# grass.mesh carries A COPY of these two lines for the mesh-shader path; change both or
# the Windows blades stop matching the ones everywhere else.
# 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 grow = Math.min(t / 0.22, 1.0)
let wide = (0.50 + 0.33 * grow) * (1.0 - t * (t * t))
let taper = Math.max(wide, 0.05)
let bend = t * t * 0.52
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 }
@ -108,12 +107,7 @@ function grass_init__t(render3d_st: mut Render3dState) -> void {
}
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") }
# five rows, four quads: the arch above needs somewhere to bend, and at four rows a
# blade that leans over is three straight segments and shows every join
render3d_st.grass_mesh = grass_blade_mesh(render3d_st, 5)
render3d_st.grass_mesh3 = grass_blade_mesh(render3d_st, 3)
render3d_st.grass_mesh2 = grass_blade_mesh(render3d_st, 2)
render3d_st.grass_wind = 2.4
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
@ -122,17 +116,30 @@ function grass_init__t(render3d_st: mut Render3dState) -> void {
# 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).
render3d_st.grass_s0 = 0.066
render3d_st.grass_d0 = 18.0
render3d_st.grass_radius = 70.0
# 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_R") { render3d_st.grass_radius = float(Text.to_int(r3d_env(render3d_st, "R3D_GRASS_R"))) }
if r3d_env_has(render3d_st, "R3D_GRASS_S0") { render3d_st.grass_s0 = float(Text.to_int(r3d_env(render3d_st, "R3D_GRASS_S0"))) / 1000.0 }
if r3d_env_has(render3d_st, "R3D_GRASS_D0") { render3d_st.grass_d0 = float(Text.to_int(r3d_env(render3d_st, "R3D_GRASS_D0"))) }
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
@ -146,14 +153,14 @@ function grass_reach(render3d_st: Render3dState) -> float {
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(GRASS_CELL * GRASS_CELL) / (spacing * spacing)
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 / GRASS_CELL
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
@ -231,7 +238,8 @@ 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"), render3d_st.grass_wind)
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
@ -274,17 +282,20 @@ function grass_bind(render3d_st: mut Render3dState, p: int) -> void {
# 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.
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_spec_scale"), 0.008)
# 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
grass_tiles(render3d_st, 4, 0.0, 6.0, render3d_st.grass_mesh)
grass_tiles(render3d_st, 4, 6.0, 16.0, render3d_st.grass_mesh3)
grass_tiles(render3d_st, 8, 16.0, 40.0, render3d_st.grass_mesh2)
# 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)
}

View file

@ -0,0 +1,36 @@
# ============================================================================
# grass_bind.ludic — the kind's numbers into the blade programs: the per-vertex path (grass.vert),
# the cull's Params (grass_cull.comp, gg_params) and model.frag's BLADE colouring, which the
# scatter's blade layers share. Four floats a uniform, named u_gk_*.
# ============================================================================
# the vertex stage's (grass.vert; grass.mesh reads only the cell) and the fragment stage's
function grass_bind_kind(render3d_st: mut Render3dState, p: int) -> void {
let k = grass_kind(render3d_st)
u_f4(render3d_st, gpu_uniform(render3d_st, p, "u_gk_tall"), k.tall_min, k.tall_max, k.cell_lo, k.cell_hi)
u_f4(render3d_st, gpu_uniform(render3d_st, p, "u_gk_grow"), k.tall_far, k.width, k.width_far, k.clump_freq)
u_f4(render3d_st, gpu_uniform(render3d_st, p, "u_gk_clump"), k.clump_lo, k.clump_hi, k.arch, k.arch_var)
u_f4(render3d_st, gpu_uniform(render3d_st, p, "u_gk_ground"), k.slope_lo, k.slope_hi, k.snow_in, k.snow_out)
u_f4(render3d_st, gpu_uniform(render3d_st, p, "u_gk_ortho"), k.ortho_floor, k.ortho_gain, k.ortho_green, float(render3d_st.grass_cell))
grass_bind_look(render3d_st, p)
}
# model.frag's BLADE path: the blades gone to seed, the far tufts, the root's occlusion, the roughness
function grass_bind_look(render3d_st: mut Render3dState, p: int) -> void {
let k = grass_kind(render3d_st)
u_f4(render3d_st, gpu_uniform(render3d_st, p, "u_gk_seed"), k.seed_r, k.seed_g, k.seed_b, k.seed_mix)
u_f4(render3d_st, gpu_uniform(render3d_st, p, "u_gk_look"), k.seed_lo, k.seed_hi, k.tuft_mix, k.tuft_shade)
u_f4(render3d_st, gpu_uniform(render3d_st, p, "u_gk_mat"), k.ao_root, k.rough, 0.0, 0.0)
}
# grass_cull.comp's Params from word 52: the same four vec4s as u_gk_tall .. u_gk_ortho, then the
# bands' outer distances into word 36 (they were GG_NEAR and GG_MID)
function grass_params_kind(render3d_st: mut Render3dState, pr: words) -> void {
let k = grass_kind(render3d_st)
pr[36] = float_bits(k.band_near); pr[37] = float_bits(k.band_mid)
pr[52] = float_bits(k.tall_min); pr[53] = float_bits(k.tall_max); pr[54] = float_bits(k.cell_lo); pr[55] = float_bits(k.cell_hi)
pr[56] = float_bits(k.tall_far); pr[57] = float_bits(k.width); pr[58] = float_bits(k.width_far); pr[59] = float_bits(k.clump_freq)
pr[60] = float_bits(k.clump_lo); pr[61] = float_bits(k.clump_hi); pr[62] = float_bits(k.arch); pr[63] = float_bits(k.arch_var)
pr[64] = float_bits(k.slope_lo); pr[65] = float_bits(k.slope_hi); pr[66] = float_bits(k.snow_in); pr[67] = float_bits(k.snow_out)
pr[68] = float_bits(k.ortho_floor); pr[69] = float_bits(k.ortho_gain); pr[70] = float_bits(k.ortho_green); pr[71] = float_bits(float(render3d_st.grass_cell))
}

View file

@ -11,8 +11,7 @@
const GG_TILES: int = 4096
const GG_BANDS: int = 3
const GG_NEAR: float = 6.0 # band 0, five rows
const GG_MID: float = 16.0 # band 1, three rows; band 2 is one quad
# band 0 is the kind's rows_near out to its band_near, band 1 rows_mid to band_mid, band 2 rows_far
function gg_cap(b: int) -> int {
if b == 0 { return 16384 }
@ -48,9 +47,9 @@ function gg_init__t(render3d_st: mut Render3dState) -> void {
gpu_buffer_upload(render3d_st, render3d_st.gg_out, (gg_base(2) + gg_cap(2)) * 64, null, GPU_DYNAMIC)
gpu_buffer_gpu_owned(render3d_st, render3d_st.gg_out)
render3d_st.gg_mesh = new []Mesh
push(render3d_st.gg_mesh, gg_blade(render3d_st, 5))
push(render3d_st.gg_mesh, gg_blade(render3d_st, 3))
push(render3d_st.gg_mesh, gg_blade(render3d_st, 2))
for b in 0 .. GG_BANDS { push(render3d_st.gg_mesh, null) }
gg_meshes_build(render3d_st)
# each band's index count is grass_reset.comp's to write, every frame, from the blade it draws
let rec = words(GG_BANDS * 5)
for b in 0 .. GG_BANDS {
rec[b * 5] = render3d_st.gg_mesh[b].count; rec[b * 5 + 1] = 0; rec[b * 5 + 2] = 0; rec[b * 5 + 3] = 0; rec[b * 5 + 4] = gg_base(b)
@ -62,6 +61,17 @@ function gg_init__t(render3d_st: mut Render3dState) -> void {
print("r3d: grass: blades are culled on the GPU")
}
# the three instanced blades at the kind's rows, the old ones let go (grass_meshes_build)
@alloc_ok("start-up, and a kind of another shape set at a world build: the blade meshes are made once")
function gg_meshes_build(render3d_st: mut Render3dState) -> void {
if render3d_st.gg_mesh == null { return }
let k = grass_kind(render3d_st)
for b in 0 .. GG_BANDS { mesh_free(render3d_st, render3d_st.gg_mesh[b]) }
render3d_st.gg_mesh[0] = gg_blade(render3d_st, k.rows_near)
render3d_st.gg_mesh[1] = gg_blade(render3d_st, k.rows_mid)
render3d_st.gg_mesh[2] = gg_blade(render3d_st, k.rows_far)
}
# a blade mesh reading its instance record (four vec4s) from the cull's output
function gg_blade(render3d_st: mut Render3dState, rows: int) -> Mesh {
let m = grass_blade_mesh(render3d_st, rows)
@ -74,7 +84,7 @@ function gg_blade(render3d_st: mut Render3dState, rows: int) -> Mesh {
# one tile size over one distance band: its visible tiles into gg_tv (corner, indices per cell, cells)
function gg_tiles(render3d_st: mut Render3dState, size: int, d_min: float, d_max: float) -> void {
if d_min >= grass_reach(render3d_st) { return }
let cells = size / GRASS_CELL
let cells = size / render3d_st.grass_cell
let sz = float(size)
let half = sz * 0.5
let reach = d_max + half * 1.5
@ -113,9 +123,11 @@ function gg_cull_frame(render3d_st: mut Render3dState) -> void {
# made once, with the device (gvk_startup_state)
let rb = render3d_st.gg_rb
rb[0] = GG_BANDS; rb[1] = 0; rb[2] = 0; rb[3] = 0
for b in 0 .. 4 { rb[4 + b] = 0 }
for b in 0 .. GG_BANDS { rb[4 + b] = render3d_st.gg_mesh[b].count }
let cb = render3d_st.gg_cb
cb[0] = render3d_st.gg_cmds
gpu_dispatch(render3d_st, render3d_st.gg_reset, data_of(rb), 16, cb, 1)
gpu_dispatch(render3d_st, render3d_st.gg_reset, data_of(rb), 32, cb, 1)
if render3d_st.gg_n == 0 { return }
# the tile list alternates buffers by frame, so the frame still on the GPU keeps its own
let tb = render3d_st.gg_tiles_buf[render3d_st.r3d_test_frame % 2]
@ -126,7 +138,7 @@ function gg_cull_frame(render3d_st: mut Render3dState) -> void {
let texs = render3d_st.gg_texs
texs[0] = render3d_st.ter_height_tex; texs[1] = render3d_st.ter_ortho_tex; texs[2] = render3d_st.ter_normal_tex
gg_page_texs(render3d_st, texs)
gpu_dispatch_tex(render3d_st, render3d_st.gg_cull, data_of(pr), 208, bufs, texs, (render3d_st.gg_max + 63) / 64, render3d_st.gg_n)
gpu_dispatch_tex(render3d_st, render3d_st.gg_cull, data_of(pr), 288, bufs, texs, (render3d_st.gg_max + 63) / 64, render3d_st.gg_n)
}
# the page pool's four, after the three whole-map ones (the stand-ins while paging is off)
@ -136,10 +148,10 @@ function gg_page_texs(render3d_st: mut Render3dState, texs: words) -> void {
if render3d_st.tp_on { texs[3] = render3d_st.tp_page_tex; texs[4] = render3d_st.tp_h_tex; texs[5] = render3d_st.tp_n_tex; texs[6] = render3d_st.tp_o_tex }
}
# grass_cull.comp's Params, std140: 13 vec4s, into the block made once in gg_cull_frame
# grass_cull.comp's Params, std140: 18 vec4s, into the block made once with the device
function gg_params(render3d_st: mut Render3dState) -> words {
let pr = render3d_st.gg_pr
for i in 0 .. 52 { pr[i] = 0 }
for i in 0 .. 72 { pr[i] = 0 }
if render3d_st.cam_planes != null { for i in 0 .. 16 { pr[i] = float_bits(render3d_st.cam_planes[i]) } }
pr[16] = float_bits(render3d_st.cam_pos[0]); pr[17] = float_bits(render3d_st.cam_pos[1]); pr[18] = float_bits(render3d_st.cam_pos[2]); pr[19] = float_bits(grass_reach(render3d_st))
var ph = render3d_st.post_h
@ -154,7 +166,7 @@ function gg_params(render3d_st: mut Render3dState) -> words {
if render3d_st.ter_ortho_tex != 0 { oon = 1.0 }
pr[28] = float_bits(lake); pr[29] = float_bits(sea); pr[30] = float_bits(oon)
pr[32] = float_bits(render3d_st.ter_ox); pr[33] = float_bits(render3d_st.ter_oz); pr[34] = float_bits(float(render3d_st.TERRAIN_HALF))
pr[36] = float_bits(GG_NEAR); pr[37] = float_bits(GG_MID)
grass_params_kind(render3d_st, pr)
for b in 0 .. GG_BANDS { pr[40 + b] = gg_base(b); pr[44 + b] = gg_cap(b) }
# the page pool (terrain_pages.ludic): on in lev.w, and its dims as the terrain's shaders take them
if render3d_st.tp_on { pr[31] = float_bits(1.0) }

View file

@ -0,0 +1,136 @@
# ============================================================================
# grass_kind.ludic — what one kind of GPU blade IS, handed in by the game as data.
#
# Every number here was a constant in grass.ludic, grass_gpu.ludic or the blade shaders (grass.vert,
# grass_cull.comp, model.frag's BLADE path) until the game's meadow became a row of its own. The
# defaults are those constants exactly, so a game that sets nothing draws what it always drew.
# grass_kind_set hands a kind over; the graphics settings only ever thin it (grass_quality).
# ============================================================================
property GrassKind {
cell: float = 4.0 # the hash cell (m): 1, 2 or 4, so the 4, 8 and 16 m tiles hold whole cells
s0: float = 0.066 # blade spacing at the camera (m)
d0: float = 18.0 # the distance at which the spacing has doubled (m)
radius: float = 70.0 # no blades past this (m)
band_near: float = 6.0 # full-row blades inside this (m)
band_mid: float = 16.0 # mid-row blades inside this; one quad past it
rows_near: int = 5
rows_mid: int = 3
rows_far: int = 2
# the profile (grass_blade_mesh): narrow at the sheath, widest at `neck`, tapering to a point, arched
neck: float = 0.22 # where the blade is widest, as a share of its length
root_w: float = 0.50 # its width at the sheath, of the blade's width
swell_w: float = 0.33 # what it gains by the neck
tip_w: float = 0.05 # the point, floored so the last quad is not degenerate
bend: float = 0.52 # how far the tip arches, of the blade's height
# height (m), biased short: tall_min..tall_max over h^2, times the cell's own factor
tall_min: float = 0.09
tall_max: float = 0.60
cell_lo: float = 0.8
cell_hi: float = 1.2
tall_far: float = 0.35 # height gained where the spacing has grown twelvefold
clump_freq: float = 0.85 # the tussocks' noise frequency (per m)
clump_lo: float = 0.45 # a tussock's height factor, low to high
clump_hi: float = 1.40
width: float = 0.010 # blade width at the camera (m)
width_far: float = 0.45 # how it widens with the spacing
arch: float = 0.6 # the arch: arch + arch_var * a per-blade hash
arch_var: float = 0.8
# where it grows
slope_lo: float = 0.30 # 1 - normal.y where blades start to thin ...
slope_hi: float = 0.55 # ... and are gone
snow_in: float = 80.0 # blades thin from this far under the snow line ...
snow_out: float = 200.0 # ... and are full this far under it
ortho_floor: float = 0.40 # density on ground the photograph calls bare ...
ortho_gain: float = 0.60 # ... and what vegetated ground adds to it
ortho_green: float = 0.025 # the photograph's g - b at which ground counts as vegetated
# colour (linear albedo): root to tip; the blades gone to seed; the far tufts
base_r: float = 0.045
base_g: float = 0.06
base_b: float = 0.025
tip_r: float = 0.22
tip_g: float = 0.27
tip_b: float = 0.13
seed_r: float = 0.40
seed_g: float = 0.36
seed_b: float = 0.13
seed_lo: float = 0.7 # a blade's seed hash past this starts to go to seed ...
seed_hi: float = 0.8 # ... and past this has
seed_mix: float = 0.75
tuft_mix: float = 0.45 # a far tuft: this far from root to tip colour ...
tuft_shade: float = 0.72 # ... and this dark
ao_root: float = 0.55 # the blade's own occlusion at the root
rough: float = 0.85
spec: float = 0.008 # its sheen (u_spec_scale)
wind: float = 2.4
}
# the kind in use, made with its defaults the first time it is asked for (render3d's start-up)
@alloc_ok("start-up: the default kind is made once")
function grass_kind(render3d_st: mut Render3dState) -> GrassKind {
if render3d_st.grass_kind == null { render3d_st.grass_kind = new GrassKind }
return render3d_st.grass_kind
}
# The game's kind (a row of its data). The renderer keeps the record, so hand a new one or call this
# again after changing one; the blade meshes are rebuilt only when the rows or the profile changed.
function grass_kind_set(render3d_st: mut Render3dState, k: GrassKind) -> void {
if k == null { return }
let was = grass_kind(render3d_st)
render3d_st.grass_kind = k
if not grass_shape_same(was, k) { grass_meshes_build(render3d_st) }
grass_derive(render3d_st)
grass_kind_colours(render3d_st)
}
# The graphics settings' tier: no closer spacing than s0_min and no farther than r_max (0: no limit).
# A tier thins the kind; it never makes a sparse kind denser.
function grass_quality(render3d_st: mut Render3dState, s0_min: float, r_max: float) -> void {
render3d_st.grass_q_s0 = s0_min
render3d_st.grass_q_r = r_max
grass_derive(render3d_st)
}
# the spacing, the doubling distance and the reach the tiles and the shaders use: the kind's, held
# to the tier, then the developer's switches
function grass_derive(render3d_st: mut Render3dState) -> void {
let k = grass_kind(render3d_st)
render3d_st.grass_s0 = Math.max(k.s0, render3d_st.grass_q_s0)
render3d_st.grass_d0 = k.d0
render3d_st.grass_radius = k.radius
if render3d_st.grass_q_r > 0.0 { render3d_st.grass_radius = Math.min(k.radius, render3d_st.grass_q_r) }
if r3d_env_has(render3d_st, "R3D_GRASS_R") { render3d_st.grass_radius = float(Text.to_int(r3d_env(render3d_st, "R3D_GRASS_R"))) }
if r3d_env_has(render3d_st, "R3D_GRASS_S0") { render3d_st.grass_s0 = float(Text.to_int(r3d_env(render3d_st, "R3D_GRASS_S0"))) / 1000.0 }
if r3d_env_has(render3d_st, "R3D_GRASS_D0") { render3d_st.grass_d0 = float(Text.to_int(r3d_env(render3d_st, "R3D_GRASS_D0"))) }
render3d_st.grass_cell = grass_cell_of(k)
}
# the cell in whole metres, held to one the 4 m tile divides
function grass_cell_of(k: GrassKind) -> int {
let c = int(k.cell + 0.5)
if c == 1 or c == 2 { return c }
return 4
}
# the root and tip colour the blades (and the scatter's blade layers) start from; the game's season
# may move them afterwards, as it always has
function grass_kind_colours(render3d_st: mut Render3dState) -> void {
let k = grass_kind(render3d_st)
if render3d_st.sc_blade_base != null { v3_set(render3d_st.sc_blade_base, k.base_r, k.base_g, k.base_b) }
if render3d_st.sc_blade_tip != null { v3_set(render3d_st.sc_blade_tip, k.tip_r, k.tip_g, k.tip_b) }
}
# whether two kinds build the same blade meshes
function grass_shape_same(a: GrassKind, b: GrassKind) -> bool {
if a.rows_near != b.rows_near or a.rows_mid != b.rows_mid or a.rows_far != b.rows_far { return false }
return a.neck == b.neck and a.root_w == b.root_w and a.swell_w == b.swell_w and a.tip_w == b.tip_w and a.bend == b.bend
}
# The blade's profile at t (0 sheath .. 1 tip): its width, of the blade's width, and its arch, of
# its height. The one source for the meshes and the game's preview; grass.mesh keeps a copy.
function grass_profile_w(k: GrassKind, t: float) -> float {
let grow = Math.min(t / k.neck, 1.0)
let wide = (k.root_w + k.swell_w * grow) * (1.0 - t * (t * t))
return Math.max(wide, k.tip_w)
}
function grass_profile_bend(k: GrassKind, t: float) -> float { return t * t * k.bend }

View file

@ -53,6 +53,8 @@ import "stream.ludic"
import "place_rec.ludic"
import "stream_baked.ludic"
import "fog_streams.ludic"
import "grass_kind.ludic"
import "grass_bind.ludic"
import "grass.ludic"
import "grass_gpu.ludic"
import "water.ludic"

View file

@ -226,8 +226,10 @@ function scatter_init(render3d_st: mut Render3dState) -> void {
render3d_st.sc_prog_card_shadow = r3d_program(render3d_st, "model.vert", "model.frag", "#define SHADOW_PASS\n#define WIND\n#define CARD\n")
render3d_st.sc_prog_card_cheap = r3d_program(render3d_st, "model.vert", "model.frag", "#define CARD\n#define CHEAP\n")
# a dry alpine meadow: brown-olive roots, straw with a little green at the tips
render3d_st.sc_blade_base = v3_new(0.045, 0.06, 0.025)
render3d_st.sc_blade_tip = v3_new(0.22, 0.27, 0.13)
# (the default GrassKind's base and tip, grass_kind.ludic)
render3d_st.sc_blade_base = v3_new(0.0, 0.0, 0.0)
render3d_st.sc_blade_tip = v3_new(0.0, 0.0, 0.0)
grass_kind_colours(render3d_st)
render3d_st.sc_blade_tint = v3_new(1.0, 1.0, 1.0)
render3d_st.sc_prog_shadow = r3d_program(render3d_st, "model.vert", "shadow.frag", "#define SHADOW_PASS\n")
render3d_st.sc_prog_shadow_wind = r3d_program(render3d_st, "model.vert", "shadow.frag", "#define SHADOW_PASS\n#define WIND\n")
@ -951,6 +953,7 @@ function layer_draw_model(render3d_st: mut Render3dState, l: Layer, model: Model
}
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_rough_scale"), l.rough)
if l.blade { 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) }
if l.blade { grass_bind_look(render3d_st, p) }
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_cull"), r3d_reach(render3d_st, l.cull))
sky_bind_lighting(render3d_st, p)
shadow_bind(render3d_st, p)

View file

@ -116,6 +116,7 @@ uniform float u_d0;
uniform float u_radius;
uniform float u_px; // one pixel in radians (grass.vert)
uniform int u_dbg;
uniform vec4 u_gk_ortho; // the grass kind's (grass_bind_kind): w its cell (m); the rest is grass.vert's
out vec3 v_wpos[];
out vec3 v_nrm[];
out vec2 v_uv[];
@ -124,7 +125,9 @@ out vec2 v_rot[];
out float v_hull[];
out float v_quake[]; // grass does not quake; written so model.frag can read it
const float CELL = 4.0; // grass.ludic GRASS_CELL: 4 m, so a near tile asks for what its blades need
// The kind's cell, as the CPU's tiles count it. Nothing else of the kind reaches this path yet: its
// shape, sizes and density test are an older copy of grass.vert's, kept as they draw today.
#define CELL u_gk_ortho.w
float hash1(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }
uint pcg(uint v) { uint s = v * 747796405u + 2891336453u; uint w = ((s >> ((s >> 28u) + 4u)) ^ s) * 277803737u; return (w >> 22u) ^ w; }
float bladeHash(ivec2 cell, int j, int k) {

View file

@ -119,6 +119,12 @@ uniform float u_d0; // distance at which the spacing has doubled (m)
uniform float u_radius; // no blades past this
uniform float u_px; // one pixel's height in radians: no blade is drawn narrower than a pixel
uniform int u_dbg; // R3D_GRASS_DBG: 1 lift blades 0.3 m, 2 light as ground everywhere, 3 both
// the grass kind (grass_kind.ludic, grass_bind_kind); grass_cull.comp's Params carry the same four
uniform vec4 u_gk_tall; // tall_min, tall_max, cell_lo, cell_hi
uniform vec4 u_gk_grow; // tall_far, width, width_far, clump_freq
uniform vec4 u_gk_clump; // clump_lo, clump_hi, arch, arch_var
uniform vec4 u_gk_ground; // slope_lo, slope_hi, snow_in, snow_out
uniform vec4 u_gk_ortho; // ortho_floor, ortho_gain, ortho_green, cell (m)
out vec3 v_wpos;
out vec3 v_nrm;
out vec2 v_uv;
@ -128,7 +134,7 @@ out float v_hull;
out float v_quake; // only aspens quake; written so model.frag can read it
out vec3 v_tint; // the blade's colour field at its root (model.frag's regionTint and patchiness, once a blade)
const float CELL = 4.0; // grass.ludic GRASS_CELL: 4 m, so a near tile asks for what its blades need
#define CELL u_gk_ortho.w // the kind's cell (4 m by default, so a near tile asks for what its blades need)
// hash1 and fbm come from noise.glsl, which a GBLADE vertex stage is given (programs.ludic)
// An integer hash (PCG) for the per-blade values. The sine hash advanced linearly with
// the blade index, so a cell's blades fell into diagonal rows, and the rows read as
@ -207,7 +213,7 @@ void main() {
// line for both left a lake above the sea with grass on its bed or a valley with none.
float wl = u_sea_level;
if (u_lake.z > 0.0) { vec2 q = (xz - u_lake.xy) / u_lake.zw; if (dot(q, q) < 1.0) wl = max(wl, u_lake_level); }
float ok = (1.0 - smoothstep(0.30, 0.55, 1.0 - gn.y)) * smoothstep(0.0, 0.6, ht.r - wl - 0.15) * smoothstep(u_snow_line - 80.0, u_snow_line - 200.0, ht.r);
float ok = (1.0 - smoothstep(u_gk_ground.x, u_gk_ground.y, 1.0 - gn.y)) * smoothstep(0.0, 0.6, ht.r - wl - 0.15) * smoothstep(u_snow_line - u_gk_ground.z, u_snow_line - u_gk_ground.w, ht.r);
if (u_ortho_on > 0.5) {
vec3 oc = textureLod(u_ortho, huv, tpOrthoLod(1.5)).rgb;
// Is this ground vegetated, by the photograph? The test used to be green DOMINANCE -
@ -218,7 +224,7 @@ void main() {
// and -0.002 six hundred metres away, flipping between full density and a quarter over
// open meadow; g - b reads +0.076 and +0.014 and separates plant from rock and snow
// just as well, because rock and snow are neutral and vegetation is not.
ok *= 0.40 + 0.60 * smoothstep(0.0, 0.025, oc.g - oc.b);
ok *= u_gk_ortho.x + u_gk_ortho.y * smoothstep(0.0, u_gk_ortho.z, oc.g - oc.b);
}
if (h4 > ok) { cull(); return; }
// the root on the drawn surface: the CDLOD mesh follows the B-spline to within
@ -242,22 +248,22 @@ void main() {
// are what carry the silhouette against the light. An even 0.18-0.42 spread read as a lawn
// that had been cut, which is the one thing an alpine meadow is not.
float hh = h3 * h3;
float tall = mix(0.09, 0.60, hh) * mix(0.8, 1.2, hash1(cid * 0.1)) * (1.0 + 0.35 * smoothstep(1.0, 12.0, grow)) * life;
float tall = mix(u_gk_tall.x, u_gk_tall.y, hh) * mix(u_gk_tall.z, u_gk_tall.w, hash1(cid * 0.1)) * (1.0 + u_gk_grow.x * smoothstep(1.0, 12.0, grow)) * life;
// CLUMPS: grass grows in tussocks, taller and shorter patches a metre or so across, and that
// relief is the texture a meadow has from above - an even height reads as felt
float clump = gnoise(xz * 0.85 + 3.7) * 0.5 + 0.5;
tall *= mix(0.45, 1.40, clump * clump * (3.0 - 2.0 * clump));
float clump = gnoise(xz * u_gk_grow.w + 3.7) * 0.5 + 0.5;
tall *= mix(u_gk_clump.x, u_gk_clump.y, clump * clump * (3.0 - 2.0 * clump));
// 2.8 cm was FIVE TIMES a blade of meadow grass, which is 3-6 mm. At 2 m from the
// camera that is a broad dark scimitar lying along the ground rather than a blade
// standing in a sward, and no amount of profile or colour work fixes a blade that is
// the wrong size. Measured, not guessed: it is the one number behind every "too wide
// and too flat" note in this stage.
float bw = 0.010 * mix(1.0, 0.45 * grow, smoothstep(1.0, 4.0, grow));
float bw = u_gk_grow.y * mix(1.0, u_gk_grow.z * grow, smoothstep(1.0, 4.0, grow));
if (far) { bw = max(bw, spacing * 0.35); tall = min(tall, spacing * 0.3); }
// a blade under a pixel is a vertex bill that shimmers: widen it to one and a bit, and let the
// thinning (spacing) keep the coverage
bw = max(bw, dist * u_px * 1.1);
vec3 p = vec3(a_pos.x * bw, a_pos.y * tall, a_pos.z * tall * (0.6 + 0.8 * h4));
vec3 p = vec3(a_pos.x * bw, a_pos.y * tall, a_pos.z * tall * (u_gk_clump.z + u_gk_clump.w * h4));
vec3 n = vec3(0.0, 0.3, 1.0);
// The shared field (wind.glsl), so the gust that crosses this meadow is the same gust that
// reaches the trees behind it a moment later. The two sines that used to live here were a

View file

@ -21,6 +21,12 @@ layout(set = 0, binding = 0) uniform Params {
uvec4 base; // each band's first record in Out
uvec4 cap; // ... and how many it holds
vec4 tp; // the page pool (u_tp_dims): tiles a side, height texels a tile, photograph texels a tile, height texels a side
// the grass kind (grass_kind.ludic, grass_params_kind): grass.vert's u_gk_* by the same names
vec4 gk_tall; // tall_min, tall_max, cell_lo, cell_hi
vec4 gk_grow; // tall_far, width, width_far, clump_freq
vec4 gk_clump; // clump_lo, clump_hi, arch, arch_var
vec4 gk_ground; // slope_lo, slope_hi, snow_in, snow_out
vec4 gk_ortho; // ortho_floor, ortho_gain, ortho_green, cell (m)
} pr;
layout(set = 0, binding = 1) readonly buffer Tiles { vec4 tiles[]; }; // corner x/z, indices per cell, cells per side
@ -36,7 +42,7 @@ layout(set = 0, binding = 8) uniform sampler2DArray u_tp_h; // (T +
layout(set = 0, binding = 9) uniform sampler2DArray u_tp_nrm;
layout(set = 0, binding = 10) uniform sampler2DArray u_tp_ortho; // (S + 2)^2 a layer
const float CELL = 4.0; // grass.ludic GRASS_CELL
#define CELL pr.gk_ortho.w // the kind's cell (m)
uint pcg(uint v) { uint s = v * 747796405u + 2891336453u; uint w = ((s >> ((s >> 28u) + 4u)) ^ s) * 277803737u; return (w >> 22u) ^ w; }
float bladeHash(ivec2 cell, int j, int k) {
@ -132,10 +138,10 @@ void main() {
float h3 = bladeHash(ci, j, 2), h4 = bladeHash(ci, j, 3);
float wl = pr.lev.y;
if (pr.lake.z > 0.0) { vec2 q = (xz - pr.lake.xy) / pr.lake.zw; if (dot(q, q) < 1.0) wl = max(wl, pr.lev.x); }
float ok = (1.0 - smoothstep(0.30, 0.55, 1.0 - gn.y)) * smoothstep(0.0, 0.6, ht.r - wl - 0.15) * smoothstep(pr.dens.w - 80.0, pr.dens.w - 200.0, ht.r);
float ok = (1.0 - smoothstep(pr.gk_ground.x, pr.gk_ground.y, 1.0 - gn.y)) * smoothstep(0.0, 0.6, ht.r - wl - 0.15) * smoothstep(pr.dens.w - pr.gk_ground.z, pr.dens.w - pr.gk_ground.w, ht.r);
if (pr.lev.z > 0.5) {
vec3 oc = groundO(huv);
ok *= 0.40 + 0.60 * smoothstep(0.0, 0.025, oc.g - oc.b);
ok *= pr.gk_ortho.x + pr.gk_ortho.y * smoothstep(0.0, pr.gk_ortho.z, oc.g - oc.b);
}
if (h4 > ok) return;
float h = heightSmooth(huv);
@ -143,10 +149,10 @@ void main() {
float ang = bladeHash(ci, j, 6) * 6.2831853;
float grow = spacing / pr.dens.x;
float hh = h3 * h3;
float tall = mix(0.09, 0.60, hh) * mix(0.8, 1.2, hash1(cid * 0.1)) * (1.0 + 0.35 * smoothstep(1.0, 12.0, grow)) * life;
float clump = gnoise(xz * 0.85 + 3.7) * 0.5 + 0.5;
tall *= mix(0.45, 1.40, clump * clump * (3.0 - 2.0 * clump));
float bw = 0.010 * mix(1.0, 0.45 * grow, smoothstep(1.0, 4.0, grow));
float tall = mix(pr.gk_tall.x, pr.gk_tall.y, hh) * mix(pr.gk_tall.z, pr.gk_tall.w, hash1(cid * 0.1)) * (1.0 + pr.gk_grow.x * smoothstep(1.0, 12.0, grow)) * life;
float clump = gnoise(xz * pr.gk_grow.w + 3.7) * 0.5 + 0.5;
tall *= mix(pr.gk_clump.x, pr.gk_clump.y, clump * clump * (3.0 - 2.0 * clump));
float bw = pr.gk_grow.y * mix(1.0, pr.gk_grow.z * grow, smoothstep(1.0, 4.0, grow));
bw = max(bw, dist * pr.dens.z * 1.1);
uint b = dist < pr.bands.x ? 0u : (dist < pr.bands.y ? 1u : 2u);
uint slot = atomicAdd(cmds[b * 5u + 1u], 1u);
@ -154,6 +160,6 @@ void main() {
uint o = (pr.base[b] + slot) * 4u;
outv[o] = vec4(xz.x, h - 0.02, xz.y, seed);
outv[o + 1u] = vec4(sin(ang), cos(ang), tall, bw);
outv[o + 2u] = vec4(bladeField(xz, h), 0.6 + 0.8 * h4);
outv[o + 2u] = vec4(bladeField(xz, h), pr.gk_clump.z + pr.gk_clump.w * h4);
outv[o + 3u] = vec4(gn, dist);
}

View file

@ -1,8 +1,10 @@
// grass_reset.comp - zero each band's instance count before grass_cull.comp counts into it. On the
// GPU, in order, because the frame before may still be drawing from the same records.
// GPU, in order, because the frame before may still be drawing from the same records. Each band's
// index count is written here too, from the blade the CPU draws it with, so a grass kind of other
// rows (grass_kind_set) reaches the draw in the frame's own order.
layout(local_size_x = 1) in;
layout(set = 0, binding = 0) uniform Params { uvec4 n; } pr;
layout(set = 0, binding = 0) uniform Params { uvec4 n; uvec4 idx; } pr;
layout(set = 0, binding = 1) buffer Cmds { uint cmds[]; };
void main() {
for (uint b = 0u; b < pr.n.x; b++) { cmds[b * 5u + 1u] = 0u; }
for (uint b = 0u; b < pr.n.x; b++) { cmds[b * 5u] = pr.idx[b]; cmds[b * 5u + 1u] = 0u; }
}

View file

@ -31,6 +31,10 @@ uniform vec3 u_blade_tip;
uniform sampler2D u_blade_tex; // straightened photographic blades, side by side
uniform float u_blade_cols; // how many are in it
uniform float u_blade_tex_on;
// the grass kind (grass_kind.ludic, grass_bind_look): the scatter's blade layers bind it too
uniform vec4 u_gk_seed; // gone to seed: colour, and how far a seeded blade goes to it
uniform vec4 u_gk_look; // seed_lo, seed_hi (the share gone to seed), tuft_mix, tuft_shade
uniform vec4 u_gk_mat; // ao_root, roughness
#endif
#ifdef CARD
uniform float u_cull; // the layer's cull distance (m); 0 = none
@ -105,8 +109,8 @@ void main() {
vec3 field = v_tint;
#endif
alb = mix(u_blade_base, u_blade_tip, smoothstep(0.0, 0.55, t)) * field;
float dry = smoothstep(0.7, 0.8, fract(v_seed * 3.17));
alb = mix(alb, vec3(0.40, 0.36, 0.13) * (0.45 + 0.55 * t), dry * 0.75);
float dry = smoothstep(u_gk_look.x, u_gk_look.y, fract(v_seed * 3.17));
alb = mix(alb, u_gk_seed.rgb * (0.45 + 0.55 * t), dry * u_gk_seed.w);
// A REAL BLADE, if the game gave us one. Each blade picks a column of the atlas from
// its own seed and runs v from sheath to tip, so a meadow is eight different plants
// rather than one plant ten thousand times. The photograph carries the midrib, the
@ -135,7 +139,7 @@ void main() {
alb *= mix(vec3(1.0), min(g, vec3(1.15)), 0.85);
}
// a far tuft is a patch of the meadow, darker than a lit blade tip and never straw
if (v_hull < 0.0) alb = mix(u_blade_base, u_blade_tip, 0.45) * field * 0.72;
if (v_hull < 0.0) alb = mix(u_blade_base, u_blade_tip, u_gk_look.z) * field * u_gk_look.w;
// a rounded cross-section reads softer than a flat card
vec3 side = normalize(cross(N, vec3(0.0, 1.0, 0.0)) + vec3(1e-4));
n = (v_hull < 0.0) ? N : normalize(N + side * (v_uv.x * 2.0 - 1.0) * 0.6);
@ -149,7 +153,7 @@ void main() {
// an occlusion term; grass albedo is capped near 0.5 and this was dividing it by five.
// A blade is a thin thing standing in open air: shaded at the root by its neighbours,
// not buried.
arm = vec3(mix(0.55, 1.0, t), 0.85, 0.0);
arm = vec3(mix(u_gk_mat.x, 1.0, t), u_gk_mat.y, 0.0);
#elif defined(CARD)
// thin grass is lit from either side: face the card toward the sun before shading
if (dot(N, u_sun_dir) < 0.0) N = -N;

View file

@ -926,7 +926,7 @@ T 74258189 u_tershadow 7
T 74258189 u_tershadow_aux 8
T 74258189 u_ts_height 9
P d48d6a48 grass.mesh model.frag #define FOLIAGE;#define BLADE;#define MESH;
B d48d6a48 vert 0 4420
B d48d6a48 vert 0 4448
U d48d6a48 vert u_view 0 mat4 1 0
U d48d6a48 vert u_proj 64 mat4 1 0
U d48d6a48 vert u_vp 128 mat4 1 0
@ -949,7 +949,8 @@ U d48d6a48 vert u_d0 4404 float 1 0
U d48d6a48 vert u_radius 4408 float 1 0
U d48d6a48 vert u_px 4412 float 1 0
U d48d6a48 vert u_dbg 4416 int 1 0
B d48d6a48 frag 1 1028
U d48d6a48 vert u_gk_ortho 4432 vec4 1 0
B d48d6a48 frag 1 1088
U d48d6a48 frag u_cascade_vp 0 mat4 5 64
U d48d6a48 frag u_cascade_split 320 float 5 16
U d48d6a48 frag u_cascade_range 400 float 5 16
@ -998,6 +999,9 @@ U d48d6a48 frag u_blade_base 992 vec3 1 0
U d48d6a48 frag u_blade_tip 1008 vec3 1 0
U d48d6a48 frag u_blade_cols 1020 float 1 0
U d48d6a48 frag u_blade_tex_on 1024 float 1 0
U d48d6a48 frag u_gk_seed 1040 vec4 1 0
U d48d6a48 frag u_gk_look 1056 vec4 1 0
U d48d6a48 frag u_gk_mat 1072 vec4 1 0
T d48d6a48 u_arm 2
T d48d6a48 u_blade_tex 3
T d48d6a48 u_brdf 4
@ -1015,7 +1019,7 @@ T d48d6a48 u_tp_nrm 15
T d48d6a48 u_tp_page 16
T d48d6a48 u_ts_height 17
P cb13f3c6 grass.vert model.frag #define FOLIAGE;#define BLADE;#define GBLADE;
B cb13f3c6 vert 0 356
B cb13f3c6 vert 0 448
U cb13f3c6 vert u_view 0 mat4 1 0
U cb13f3c6 vert u_proj 64 mat4 1 0
U cb13f3c6 vert u_vp 128 mat4 1 0
@ -1040,9 +1044,14 @@ U cb13f3c6 vert u_d0 340 float 1 0
U cb13f3c6 vert u_radius 344 float 1 0
U cb13f3c6 vert u_px 348 float 1 0
U cb13f3c6 vert u_dbg 352 int 1 0
U cb13f3c6 vert u_gk_tall 368 vec4 1 0
U cb13f3c6 vert u_gk_grow 384 vec4 1 0
U cb13f3c6 vert u_gk_clump 400 vec4 1 0
U cb13f3c6 vert u_gk_ground 416 vec4 1 0
U cb13f3c6 vert u_gk_ortho 432 vec4 1 0
I cb13f3c6 a_pos 0 vec3
I cb13f3c6 a_uv 2 vec2
B cb13f3c6 frag 1 1028
B cb13f3c6 frag 1 1088
U cb13f3c6 frag u_cascade_vp 0 mat4 5 64
U cb13f3c6 frag u_cascade_split 320 float 5 16
U cb13f3c6 frag u_cascade_range 400 float 5 16
@ -1091,6 +1100,9 @@ U cb13f3c6 frag u_blade_base 992 vec3 1 0
U cb13f3c6 frag u_blade_tip 1008 vec3 1 0
U cb13f3c6 frag u_blade_cols 1020 float 1 0
U cb13f3c6 frag u_blade_tex_on 1024 float 1 0
U cb13f3c6 frag u_gk_seed 1040 vec4 1 0
U cb13f3c6 frag u_gk_look 1056 vec4 1 0
U cb13f3c6 frag u_gk_mat 1072 vec4 1 0
T cb13f3c6 u_arm 2
T cb13f3c6 u_blade_tex 3
T cb13f3c6 u_brdf 4
@ -1108,7 +1120,7 @@ T cb13f3c6 u_tp_nrm 15
T cb13f3c6 u_tp_page 16
T cb13f3c6 u_ts_height 17
P b8cff226 grass.vert model.frag #define FOLIAGE;#define BLADE;#define GBLADE;#define TILES;
B b8cff226 vert 0 4452
B b8cff226 vert 0 4544
U b8cff226 vert u_view 0 mat4 1 0
U b8cff226 vert u_proj 64 mat4 1 0
U b8cff226 vert u_vp 128 mat4 1 0
@ -1134,9 +1146,14 @@ U b8cff226 vert u_d0 4436 float 1 0
U b8cff226 vert u_radius 4440 float 1 0
U b8cff226 vert u_px 4444 float 1 0
U b8cff226 vert u_dbg 4448 int 1 0
U b8cff226 vert u_gk_tall 4464 vec4 1 0
U b8cff226 vert u_gk_grow 4480 vec4 1 0
U b8cff226 vert u_gk_clump 4496 vec4 1 0
U b8cff226 vert u_gk_ground 4512 vec4 1 0
U b8cff226 vert u_gk_ortho 4528 vec4 1 0
I b8cff226 a_pos 0 vec3
I b8cff226 a_uv 2 vec2
B b8cff226 frag 1 1028
B b8cff226 frag 1 1088
U b8cff226 frag u_cascade_vp 0 mat4 5 64
U b8cff226 frag u_cascade_split 320 float 5 16
U b8cff226 frag u_cascade_range 400 float 5 16
@ -1185,6 +1202,9 @@ U b8cff226 frag u_blade_base 992 vec3 1 0
U b8cff226 frag u_blade_tip 1008 vec3 1 0
U b8cff226 frag u_blade_cols 1020 float 1 0
U b8cff226 frag u_blade_tex_on 1024 float 1 0
U b8cff226 frag u_gk_seed 1040 vec4 1 0
U b8cff226 frag u_gk_look 1056 vec4 1 0
U b8cff226 frag u_gk_mat 1072 vec4 1 0
T b8cff226 u_arm 2
T b8cff226 u_blade_tex 3
T b8cff226 u_brdf 4
@ -2015,7 +2035,7 @@ I f4a2b586 a_pos 0 vec3
I f4a2b586 i_pos 3 vec4
I f4a2b586 a_nrm 1 vec3
I f4a2b586 a_uv 2 vec2
B f4a2b586 frag 1 1028
B f4a2b586 frag 1 1088
U f4a2b586 frag u_cascade_vp 0 mat4 5 64
U f4a2b586 frag u_cascade_split 320 float 5 16
U f4a2b586 frag u_cascade_range 400 float 5 16
@ -2064,6 +2084,9 @@ U f4a2b586 frag u_blade_base 992 vec3 1 0
U f4a2b586 frag u_blade_tip 1008 vec3 1 0
U f4a2b586 frag u_blade_cols 1020 float 1 0
U f4a2b586 frag u_blade_tex_on 1024 float 1 0
U f4a2b586 frag u_gk_seed 1040 vec4 1 0
U f4a2b586 frag u_gk_look 1056 vec4 1 0
U f4a2b586 frag u_gk_mat 1072 vec4 1 0
T f4a2b586 u_arm 2
T f4a2b586 u_blade_tex 3
T f4a2b586 u_brdf 4
@ -3909,7 +3932,7 @@ I b20fdcb5 i_ground 6 vec4
I b20fdcb5 a_pos 0 vec3
I b20fdcb5 i_tint 5 vec4
I b20fdcb5 a_uv 2 vec2
B b20fdcb5 frag 1 1028
B b20fdcb5 frag 1 1088
U b20fdcb5 frag u_cascade_vp 0 mat4 5 64
U b20fdcb5 frag u_cascade_split 320 float 5 16
U b20fdcb5 frag u_cascade_range 400 float 5 16
@ -3958,6 +3981,9 @@ U b20fdcb5 frag u_blade_base 992 vec3 1 0
U b20fdcb5 frag u_blade_tip 1008 vec3 1 0
U b20fdcb5 frag u_blade_cols 1020 float 1 0
U b20fdcb5 frag u_blade_tex_on 1024 float 1 0
U b20fdcb5 frag u_gk_seed 1040 vec4 1 0
U b20fdcb5 frag u_gk_look 1056 vec4 1 0
U b20fdcb5 frag u_gk_mat 1072 vec4 1 0
T b20fdcb5 u_arm 2
T b20fdcb5 u_blade_tex 3
T b20fdcb5 u_brdf 4