ludic/packages/ludic.render3d/grass_kind.ludic
Orkuncakilkaya e908672034 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>
2026-09-29 19:15:39 +03:00

136 lines
7 KiB
Text

# ============================================================================
# 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 }