diff --git a/changes/render3d-grass-kind.md b/changes/render3d-grass-kind.md new file mode 100644 index 00000000..02b65734 --- /dev/null +++ b/changes/render3d-grass-kind.md @@ -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. diff --git a/packages/ludic.render3d/env.ludic b/packages/ludic.render3d/env.ludic index a6188f2f..e26b5ce8 100644 --- a/packages/ludic.render3d/env.ludic +++ b/packages/ludic.render3d/env.ludic @@ -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) diff --git a/packages/ludic.render3d/gpu_vk.ludic b/packages/ludic.render3d/gpu_vk.ludic index 4de95209..bcd4de09 100644 --- a/packages/ludic.render3d/gpu_vk.ludic +++ b/packages/ludic.render3d/gpu_vk.ludic @@ -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 } diff --git a/packages/ludic.render3d/grass.ludic b/packages/ludic.render3d/grass.ludic index 6059d5d3..de093b42 100644 --- a/packages/ludic.render3d/grass.ludic +++ b/packages/ludic.render3d/grass.ludic @@ -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) } diff --git a/packages/ludic.render3d/grass_bind.ludic b/packages/ludic.render3d/grass_bind.ludic new file mode 100644 index 00000000..7d09ac2b --- /dev/null +++ b/packages/ludic.render3d/grass_bind.ludic @@ -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)) +} diff --git a/packages/ludic.render3d/grass_gpu.ludic b/packages/ludic.render3d/grass_gpu.ludic index 732d0275..9e85da0c 100644 --- a/packages/ludic.render3d/grass_gpu.ludic +++ b/packages/ludic.render3d/grass_gpu.ludic @@ -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) } diff --git a/packages/ludic.render3d/grass_kind.ludic b/packages/ludic.render3d/grass_kind.ludic new file mode 100644 index 00000000..fe83e51d --- /dev/null +++ b/packages/ludic.render3d/grass_kind.ludic @@ -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 } diff --git a/packages/ludic.render3d/r3d.ludic b/packages/ludic.render3d/r3d.ludic index cf69746e..5e6f1f6b 100644 --- a/packages/ludic.render3d/r3d.ludic +++ b/packages/ludic.render3d/r3d.ludic @@ -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" diff --git a/packages/ludic.render3d/scatter.ludic b/packages/ludic.render3d/scatter.ludic index 616ab527..42d4ff93 100644 --- a/packages/ludic.render3d/scatter.ludic +++ b/packages/ludic.render3d/scatter.ludic @@ -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) diff --git a/packages/ludic.render3d/shaders/grass.mesh b/packages/ludic.render3d/shaders/grass.mesh index 38c1ea7a..a684c814 100644 --- a/packages/ludic.render3d/shaders/grass.mesh +++ b/packages/ludic.render3d/shaders/grass.mesh @@ -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) { diff --git a/packages/ludic.render3d/shaders/grass.vert b/packages/ludic.render3d/shaders/grass.vert index e3afa1c5..a8ec80b4 100644 --- a/packages/ludic.render3d/shaders/grass.vert +++ b/packages/ludic.render3d/shaders/grass.vert @@ -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 diff --git a/packages/ludic.render3d/shaders/grass_cull.comp b/packages/ludic.render3d/shaders/grass_cull.comp index 76d8445f..e533d3c2 100644 --- a/packages/ludic.render3d/shaders/grass_cull.comp +++ b/packages/ludic.render3d/shaders/grass_cull.comp @@ -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); } diff --git a/packages/ludic.render3d/shaders/grass_reset.comp b/packages/ludic.render3d/shaders/grass_reset.comp index f810b0bc..d059eb23 100644 --- a/packages/ludic.render3d/shaders/grass_reset.comp +++ b/packages/ludic.render3d/shaders/grass_reset.comp @@ -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; } } diff --git a/packages/ludic.render3d/shaders/model.frag b/packages/ludic.render3d/shaders/model.frag index 2518d569..6a14ba6f 100644 --- a/packages/ludic.render3d/shaders/model.frag +++ b/packages/ludic.render3d/shaders/model.frag @@ -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; diff --git a/packages/ludic.render3d/shaders/spv/b20fdcb5.frag.spv b/packages/ludic.render3d/shaders/spv/b20fdcb5.frag.spv index 8f5ad99d..e95e173b 100644 Binary files a/packages/ludic.render3d/shaders/spv/b20fdcb5.frag.spv and b/packages/ludic.render3d/shaders/spv/b20fdcb5.frag.spv differ diff --git a/packages/ludic.render3d/shaders/spv/b8cff226.frag.spv b/packages/ludic.render3d/shaders/spv/b8cff226.frag.spv index 4424e546..b8614580 100644 Binary files a/packages/ludic.render3d/shaders/spv/b8cff226.frag.spv and b/packages/ludic.render3d/shaders/spv/b8cff226.frag.spv differ diff --git a/packages/ludic.render3d/shaders/spv/b8cff226.vert.spv b/packages/ludic.render3d/shaders/spv/b8cff226.vert.spv index 70fc4234..7d614756 100644 Binary files a/packages/ludic.render3d/shaders/spv/b8cff226.vert.spv and b/packages/ludic.render3d/shaders/spv/b8cff226.vert.spv differ diff --git a/packages/ludic.render3d/shaders/spv/cb13f3c6.frag.spv b/packages/ludic.render3d/shaders/spv/cb13f3c6.frag.spv index 4424e546..b8614580 100644 Binary files a/packages/ludic.render3d/shaders/spv/cb13f3c6.frag.spv and b/packages/ludic.render3d/shaders/spv/cb13f3c6.frag.spv differ diff --git a/packages/ludic.render3d/shaders/spv/cb13f3c6.vert.spv b/packages/ludic.render3d/shaders/spv/cb13f3c6.vert.spv index dd6092db..5d803732 100644 Binary files a/packages/ludic.render3d/shaders/spv/cb13f3c6.vert.spv and b/packages/ludic.render3d/shaders/spv/cb13f3c6.vert.spv differ diff --git a/packages/ludic.render3d/shaders/spv/d48d6a48.frag.spv b/packages/ludic.render3d/shaders/spv/d48d6a48.frag.spv index 9c842354..7ab3908e 100644 Binary files a/packages/ludic.render3d/shaders/spv/d48d6a48.frag.spv and b/packages/ludic.render3d/shaders/spv/d48d6a48.frag.spv differ diff --git a/packages/ludic.render3d/shaders/spv/d48d6a48.vert.spv b/packages/ludic.render3d/shaders/spv/d48d6a48.vert.spv index 9cc1e17d..1e70bc50 100644 Binary files a/packages/ludic.render3d/shaders/spv/d48d6a48.vert.spv and b/packages/ludic.render3d/shaders/spv/d48d6a48.vert.spv differ diff --git a/packages/ludic.render3d/shaders/spv/f4a2b586.frag.spv b/packages/ludic.render3d/shaders/spv/f4a2b586.frag.spv index 8b479bb2..b52d05ff 100644 Binary files a/packages/ludic.render3d/shaders/spv/f4a2b586.frag.spv and b/packages/ludic.render3d/shaders/spv/f4a2b586.frag.spv differ diff --git a/packages/ludic.render3d/shaders/spv/grass_cull.comp.spv b/packages/ludic.render3d/shaders/spv/grass_cull.comp.spv index 30f2434f..26165a32 100644 Binary files a/packages/ludic.render3d/shaders/spv/grass_cull.comp.spv and b/packages/ludic.render3d/shaders/spv/grass_cull.comp.spv differ diff --git a/packages/ludic.render3d/shaders/spv/grass_reset.comp.spv b/packages/ludic.render3d/shaders/spv/grass_reset.comp.spv index 0d6a3302..ad53d657 100644 Binary files a/packages/ludic.render3d/shaders/spv/grass_reset.comp.spv and b/packages/ludic.render3d/shaders/spv/grass_reset.comp.spv differ diff --git a/packages/ludic.render3d/shaders/spv/manifest.txt b/packages/ludic.render3d/shaders/spv/manifest.txt index 4481d6c9..ac8d72d4 100644 --- a/packages/ludic.render3d/shaders/spv/manifest.txt +++ b/packages/ludic.render3d/shaders/spv/manifest.txt @@ -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