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

@ -0,0 +1,10 @@
bump: minor
type: feature
**`ludic.render3d`: a grass kind.** The GPU blades' per-kind numbers - the cell, the spacing and its
doubling distance, the reach, the row bands and rows, the blade's profile, heights, tussocks, width and
arch, where it grows (slope, snow line, the photograph's green), its root, tip and gone-to-seed colours,
the far tufts, the root's occlusion, roughness, sheen and wind - are a `GrassKind` record a game hands
over with `grass_kind_set`, not constants in `grass.ludic` and the blade shaders. The defaults are the
old constants, so a game that sets nothing draws what it drew. `grass_quality(s0_min, r_max)` holds a
kind to a settings tier; `grass_profile_w` / `grass_profile_bend` are the one blade profile, for the
meshes and a game's own preview of a kind.

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