From 9186abbbe35aceef8d777b58dea2e396b893ab80 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Tue, 29 Sep 2026 21:14:22 +0300 Subject: [PATCH] render3d: the GPU blades grow by a painted density (a layer of the ground densities, grass_density_layer) where a map has one - a 5x5-tile window round the camera in a storage buffer the cull samples - and by today's rules where not; grass_rule_at is the rules on the CPU for the migration Co-Authored-By: Claude Opus 5.5 --- changes/render3d-blade-density.md | 9 ++ packages/ludic.render3d/env.ludic | 5 + packages/ludic.render3d/gpu_vk.ludic | 4 +- packages/ludic.render3d/grass_density.ludic | 100 ++++++++++++++++++ packages/ludic.render3d/grass_gpu.ludic | 13 ++- packages/ludic.render3d/grass_rule.ludic | 53 ++++++++++ packages/ludic.render3d/ground_density.ludic | 1 + packages/ludic.render3d/r3d.ludic | 2 + .../ludic.render3d/shaders/grass_cull.comp | 53 +++++++--- .../shaders/spv/grass_cull.comp.spv | Bin 29488 -> 32972 bytes 10 files changed, 219 insertions(+), 21 deletions(-) create mode 100644 changes/render3d-blade-density.md create mode 100644 packages/ludic.render3d/grass_density.ludic create mode 100644 packages/ludic.render3d/grass_rule.ludic diff --git a/changes/render3d-blade-density.md b/changes/render3d-blade-density.md new file mode 100644 index 00000000..83d3e461 --- /dev/null +++ b/changes/render3d-blade-density.md @@ -0,0 +1,9 @@ +bump: minor +type: feature +**`ludic.render3d`: the GPU blades grow where a painted density says.** `grass_density_layer(l)` names a layer of +the ground densities (`ground_density_open`: the same 2 m texel, 64 m tiles, bake and dev fallback) as the +blades'; `grass_cull.comp` then keeps a blade by that density, bilinear, where it used to apply its rules (the +slope, the water's margin, the snow line, the photograph). Without one, nothing changes. The cull reads a +window of 5 x 5 density tiles round the camera (160^2 bytes, a host-visible storage buffer, re-filled from +the CPU tile cache when the camera crosses a 64 m tile), never the whole map. `grass_rule_at(x, z)` is the +rules on the CPU, for a migration that paints them; `grass_density_texels()` the side a density PNG has. diff --git a/packages/ludic.render3d/env.ludic b/packages/ludic.render3d/env.ludic index 3265fd1a..5d56490e 100644 --- a/packages/ludic.render3d/env.ludic +++ b/packages/ludic.render3d/env.ludic @@ -856,6 +856,11 @@ export state Render3dState { gd_slot_of: words = null gd_hand: int = 0 gd_trample_cb: fn(float, float) -> float = null + gb_layer: int = -1 # the densities' layer the blades grow by; -1: the rules (grass_density.ludic) + gb_buf: int = 0 # its window round the camera, on the GPU + gb_win: words = null # ... and the window's bytes, four texels a word, made once + gb_tx: int = -1000000 # the window's first texel in the map, x and z (-1000000: not filled) + gb_tz: int = 0 tt_dir: string = "" # terrain_tiles_to: where a map's tiles are written ("" keeps the whole copies) tt_key: string = "" tt_file: pointer = null # the tiles' file, open for reading (terrain_tiles.ludic) diff --git a/packages/ludic.render3d/gpu_vk.ludic b/packages/ludic.render3d/gpu_vk.ludic index bcd4de09..2339ab4f 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(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) + render3d_st.gg_rb = words(8); render3d_st.gg_cb = words(1); render3d_st.gg_bufs = words(4) + render3d_st.gg_texs = words(7); render3d_st.gg_pr = words(76) 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_density.ludic b/packages/ludic.render3d/grass_density.ludic new file mode 100644 index 00000000..d25e6e3a --- /dev/null +++ b/packages/ludic.render3d/grass_density.ludic @@ -0,0 +1,100 @@ +# grass_density.ludic — the blades grow where the map's painted density says, when it has one: a layer of +# the ground densities (ground_density.ludic; a PNG like any ground layer's, the same 2 m texel and 64 m +# tiles), named by grass_density_layer. Without one grass_cull.comp keeps its rules (grass_rule_at here). +# +# The cull decides every blade on the GPU, so the density must be there too - but not all of it: 4096^2 +# bytes is 16 MB, and the blades stand within grass_reach (70 m at most). The GPU holds a window of +# GB_TILES x GB_TILES density tiles round the camera's (5 x 64 m: 160^2 bytes, 25 KB, twice while a re-fill +# swaps), re-filled from the CPU tile cache when the camera crosses into another tile. The camera stays in +# the middle tile, so the window reaches at least 128 m every way. A host-visible storage buffer, not a +# texture: filling a texture waits for the GPU (gpu_tex_fill), a buffer's re-fill swaps under the frame +# still reading it (gvk_buf_reserve), and the cull does its own bilinear in four reads. + +const GB_TILES: int = 5 +const GB_NONE: int = -1000000 + +# The layer of the ground densities the blades grow by (-1: none, the rules). Call it after +# ground_density_open, with the index the blades' PNG had in its list; closing the densities forgets it. +export function grass_density_layer(render3d_st: mut Render3dState, l: int) -> void { + render3d_st.gb_layer = l + render3d_st.gb_tx = GB_NONE +} + +# the side, in texels, a density PNG must have for this renderer (as every ground layer's) +export function grass_density_texels() -> int { return TERRAIN_RES / TT_TEX * GD_SIDE } + +# whether the blades read a density: a layer named, and the densities open with it +function gb_on(render3d_st: Render3dState) -> bool { + let l = render3d_st.gb_layer + if l < 0 or render3d_st.gd_file == null or render3d_st.gb_win == null or l >= render3d_st.gd_layers { return false } + return render3d_st.gd_ch[l] > 0 +} + +# the densities' texel in metres +function gb_texel(render3d_st: Render3dState) -> float { return 2.0 * float(render3d_st.TERRAIN_HALF) / float(render3d_st.gd_n * GD_SIDE) } + +# once, with the cull's buffers: the window's bytes and its buffer, at its full size (zeros) +@alloc_ok("start-up: the density window's bytes and buffer, once") +function gb_init(render3d_st: mut Render3dState) -> void { + let side = GB_TILES * GD_SIDE + render3d_st.gb_win = words(side * side / 4) + for i in 0 .. side * side / 4 { render3d_st.gb_win[i] = 0 } + render3d_st.gb_buf = gpu_buffer_new(render3d_st) + gpu_buffer_upload(render3d_st, render3d_st.gb_buf, side * side, data_of(render3d_st.gb_win), GPU_DYNAMIC) +} + +# before the cull: the window re-filled and sent when the camera is no longer in its middle tile +function gb_frame(render3d_st: mut Render3dState) -> void { + if not gb_on(render3d_st) { return } + let texel = gb_texel(render3d_st) + let tm = texel * float(GD_SIDE) + let half = float(render3d_st.TERRAIN_HALF) + let ctx = int(Math.floor((render3d_st.cam_pos[0] - render3d_st.ter_ox + half) / tm)) + let ctz = int(Math.floor((render3d_st.cam_pos[2] - render3d_st.ter_oz + half) / tm)) + let tx = (ctx - GB_TILES / 2) * GD_SIDE + let tz = (ctz - GB_TILES / 2) * GD_SIDE + if tx == render3d_st.gb_tx and tz == render3d_st.gb_tz { return } + for j in 0 .. GB_TILES { + for i in 0 .. GB_TILES { gb_fill_tile(render3d_st, i, j, ctx - GB_TILES / 2 + i, ctz - GB_TILES / 2 + j) } + } + let side = GB_TILES * GD_SIDE + gpu_buffer_upload(render3d_st, render3d_st.gb_buf, side * side, data_of(render3d_st.gb_win), GPU_DYNAMIC) + render3d_st.gb_tx = tx + render3d_st.gb_tz = tz +} + +# window tile (i, j) from the map's tile (mx, mz): its density bytes, four to a word; zeros off the map +function gb_fill_tile(render3d_st: mut Render3dState, i: int, j: int, mx: int, mz: int) -> void { + let l = render3d_st.gb_layer + let n = render3d_st.gd_n + let wq = GB_TILES * GD_SIDE / 4 + let inside = mx >= 0 and mz >= 0 and mx < n and mz < n + var at = 0 + var ch = 1 + if inside { + at = gd_slot(render3d_st, l, mz * n + mx) * GD_SIDE * GD_SIDE * 2 + ch = render3d_st.gd_ch[l] + } + let pool = render3d_st.gd_pool + for r in 0 .. GD_SIDE { + for q in 0 .. GD_SIDE / 4 { + var w = 0 + if inside { + let o = at + (r * GD_SIDE + q * 4) * ch + w = pool[o] | (pool[o + ch] << 8) | (pool[o + 2 * ch] << 16) | (pool[o + 3 * ch] << 24) + } + render3d_st.gb_win[(j * GD_SIDE + r) * wq + i * GD_SIDE / 4 + q] = w + } + } +} + +# grass_cull.comp's Params word 72: the window's world corner, its texel and side; a side of 0 is the rules +function gb_params(render3d_st: Render3dState, pr: words) -> void { + if not gb_on(render3d_st) or render3d_st.gb_tx == GB_NONE { return } + let texel = gb_texel(render3d_st) + let half = float(render3d_st.TERRAIN_HALF) + pr[72] = float_bits(render3d_st.ter_ox - half + float(render3d_st.gb_tx) * texel) + pr[73] = float_bits(render3d_st.ter_oz - half + float(render3d_st.gb_tz) * texel) + pr[74] = float_bits(texel) + pr[75] = float_bits(float(GB_TILES * GD_SIDE)) +} diff --git a/packages/ludic.render3d/grass_gpu.ludic b/packages/ludic.render3d/grass_gpu.ludic index 9e85da0c..853caa44 100644 --- a/packages/ludic.render3d/grass_gpu.ludic +++ b/packages/ludic.render3d/grass_gpu.ludic @@ -37,7 +37,7 @@ function gg_init__t(render3d_st: mut Render3dState) -> void { if not gpu_has_compute(render3d_st) or render3d_st.grass_prog == 0 { return } if r3d_env_has(render3d_st, "R3D_GRASS_GPU") and r3d_env(render3d_st, "R3D_GRASS_GPU") == "0" { return } render3d_st.gg_reset = gpu_compute(render3d_st, "grass_reset", 1) - render3d_st.gg_cull = gpu_compute_tex(render3d_st, "grass_cull", 3, 7) + render3d_st.gg_cull = gpu_compute_tex(render3d_st, "grass_cull", 4, 7) render3d_st.gg_prog = r3d_program(render3d_st, "grass_inst.vert", "model.frag", "#define FOLIAGE\n#define BLADE\n#define GBLADE\n#define GINST\n") if render3d_st.gg_reset == 0 or render3d_st.gg_cull == 0 or render3d_st.gg_prog == 0 { return } render3d_st.gg_tiles_buf = new []int @@ -57,6 +57,7 @@ function gg_init__t(render3d_st: mut Render3dState) -> void { render3d_st.gg_cmds = gpu_buffer_new(render3d_st) gpu_buffer_upload(render3d_st, render3d_st.gg_cmds, GG_BANDS * 20, data_of(rec), GPU_DYNAMIC) gpu_buffer_gpu_owned(render3d_st, render3d_st.gg_cmds) + gb_init(render3d_st) render3d_st.gg_on = true print("r3d: grass: blades are culled on the GPU") } @@ -132,13 +133,14 @@ function gg_cull_frame(render3d_st: mut Render3dState) -> void { # 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] gpu_buffer_upload(render3d_st, tb, render3d_st.gg_n * 16, data_of(render3d_st.gg_tv), GPU_DYNAMIC) + gb_frame(render3d_st) let pr = gg_params(render3d_st) let bufs = render3d_st.gg_bufs - bufs[0] = tb; bufs[1] = render3d_st.gg_out; bufs[2] = render3d_st.gg_cmds + bufs[0] = tb; bufs[1] = render3d_st.gg_out; bufs[2] = render3d_st.gg_cmds; bufs[3] = render3d_st.gb_buf 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), 288, bufs, texs, (render3d_st.gg_max + 63) / 64, render3d_st.gg_n) + gpu_dispatch_tex(render3d_st, render3d_st.gg_cull, data_of(pr), 304, 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) @@ -148,10 +150,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: 18 vec4s, into the block made once with the device +# grass_cull.comp's Params, std140: 19 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 .. 72 { pr[i] = 0 } + for i in 0 .. 76 { 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 @@ -171,6 +173,7 @@ function gg_params(render3d_st: mut Render3dState) -> words { # 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) } pr[48] = float_bits(float(render3d_st.tt_n)); pr[49] = float_bits(float(TT_TEX)); pr[50] = float_bits(float(render3d_st.tt_oside)); pr[51] = float_bits(float(TERRAIN_RES)) + gb_params(render3d_st, pr) return pr } diff --git a/packages/ludic.render3d/grass_rule.ludic b/packages/ludic.render3d/grass_rule.ludic new file mode 100644 index 00000000..55b2091c --- /dev/null +++ b/packages/ludic.render3d/grass_rule.ludic @@ -0,0 +1,53 @@ +# grass_rule.ludic — grass_cull.comp's bladeRules on the CPU: whether the ground at (x, z) grows blades, 0..1, +# for the migration that paints it into a map's blades density (keep the two in step). Every term here varies +# slower than a 2 m texel - the slope, the water's margin, the snow line, the photograph - so a painted texel +# holds it; what varies within one (each blade's hashes, the tussocks, the colour field) stays in the shader. + +# the ground's keep at (x, z), under the kind in use and the map's water and snow +export function grass_rule_at(render3d_st: mut Render3dState, x: float, z: float) -> float { + let k = grass_kind(render3d_st) + let e = terrain_texel(render3d_st) + let y = terrain_height(render3d_st, x, z) + let gx = (terrain_height(render3d_st, x + e, z) - terrain_height(render3d_st, x - e, z)) / (2.0 * e) + let gz = (terrain_height(render3d_st, x, z + e) - terrain_height(render3d_st, x, z - e)) / (2.0 * e) + let ny = 1.0 / Math.sqrt(1.0 + gx * gx + gz * gz) + let snow = render3d_st.ter_snow_line + var ok = (1.0 - gr_smooth(k.slope_lo, k.slope_hi, 1.0 - ny)) * gr_smooth(0.0, 0.6, y - gr_water(render3d_st, x, z) - 0.15) + ok = ok * gr_smooth(snow - k.snow_in, snow - k.snow_out, y) + if ok <= 0.0 or render3d_st.ter_ortho_tex == 0 { return ok } + # the photograph varies inside a texel: its term averaged over four points of it + var o = 0.0 + for s in 0 .. 4 { + let c = terrain_ortho(render3d_st, x + (float(s % 2) - 0.5) * e * 0.5, z + (float(s / 2) - 0.5) * e * 0.5) + let gb = gr_linear((c >> 8) & 255) - gr_linear(c & 255) + o = o + k.ortho_floor + k.ortho_gain * gr_smooth(0.0, k.ortho_green, gb) + } + return ok * o * 0.25 +} + +# the water line there: the sea's, or the carved lake's inside its ellipse (as gg_params hands them over) +function gr_water(render3d_st: Render3dState, x: float, z: float) -> float { + var lake = -100000.0 + if render3d_st.ter_lake_ex != 0.0 { lake = render3d_st.ter_lake_level } + var wl = lake + if render3d_st.ter_sea_set { wl = render3d_st.ter_sea_level } + if render3d_st.ter_lake_ex > 0.0 { + let qx = (x - render3d_st.ter_lake_cx) / render3d_st.ter_lake_ex + let qz = (z - render3d_st.ter_lake_cz) / render3d_st.ter_lake_ez + if qx * qx + qz * qz < 1.0 { wl = Math.max(wl, lake) } + } + return wl +} + +# GLSL's smoothstep, edges either way round as the shader's snow line has them +function gr_smooth(e0: float, e1: float, v: float) -> float { + let t = Math.clamp((v - e0) / (e1 - e0), 0.0, 1.0) + return t * t * (3.0 - 2.0 * t) +} + +# an sRGB byte as the shader reads it from the photograph's sRGB texture: linear +function gr_linear(c: int) -> float { + let v = float(c) / 255.0 + if v <= 0.04045 { return v / 12.92 } + return Math.pow((v + 0.055) / 1.055, 2.4) +} diff --git a/packages/ludic.render3d/ground_density.ludic b/packages/ludic.render3d/ground_density.ludic index 0672a2f9..e03cc013 100644 --- a/packages/ludic.render3d/ground_density.ludic +++ b/packages/ludic.render3d/ground_density.ludic @@ -68,6 +68,7 @@ function gd_cache_reset(render3d_st: mut Render3dState, tiles: int) -> void { export function ground_density_close(render3d_st: mut Render3dState) -> void { if render3d_st.gd_file != null { file_close(render3d_st.gd_file); render3d_st.gd_file = null } + render3d_st.gb_layer = -1 # a layer index means nothing in the next map's file (grass_density_layer) } # the slot holding layer l's tile t, read in when it is not (a clock, as the terrain's tiles) diff --git a/packages/ludic.render3d/r3d.ludic b/packages/ludic.render3d/r3d.ludic index 75b97723..f4e52a9a 100644 --- a/packages/ludic.render3d/r3d.ludic +++ b/packages/ludic.render3d/r3d.ludic @@ -60,6 +60,8 @@ import "grass_kind.ludic" import "grass_bind.ludic" import "grass.ludic" import "grass_gpu.ludic" +import "grass_density.ludic" +import "grass_rule.ludic" import "water.ludic" import "streamline.ludic" import "render.ludic" diff --git a/packages/ludic.render3d/shaders/grass_cull.comp b/packages/ludic.render3d/shaders/grass_cull.comp index e533d3c2..442882bb 100644 --- a/packages/ludic.render3d/shaders/grass_cull.comp +++ b/packages/ludic.render3d/shaders/grass_cull.comp @@ -27,20 +27,22 @@ layout(set = 0, binding = 0) uniform Params { 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) + vec4 bd; // the painted density's window (grass_density.ludic): world x/z of its corner, m a texel, texels a side (0: the rules) } pr; layout(set = 0, binding = 1) readonly buffer Tiles { vec4 tiles[]; }; // corner x/z, indices per cell, cells per side layout(set = 0, binding = 2) buffer Out { vec4 outv[]; }; // 4 per blade layout(set = 0, binding = 3) buffer Cmds { uint cmds[]; }; // VkDrawIndexedIndirectCommand per band -layout(set = 0, binding = 4) uniform sampler2D u_height; // the height (terrain's u_ts_height) -layout(set = 0, binding = 5) uniform sampler2D u_ortho; // the photograph -layout(set = 0, binding = 6) uniform sampler2D u_ter_normal; // the normal: x and z, y rebuilt +layout(set = 0, binding = 4) readonly buffer Dens { uint dens[]; }; // the density's window, four texels a word +layout(set = 0, binding = 5) uniform sampler2D u_height; // the height (terrain's u_ts_height) +layout(set = 0, binding = 6) uniform sampler2D u_ortho; // the photograph +layout(set = 0, binding = 7) uniform sampler2D u_ter_normal; // the normal: x and z, y rebuilt // the page pool (terrain_pages.ludic): while it is on, the three above are the coarse whole-map level // and the tiles round the camera are layers of these, addressed through the page table -layout(set = 0, binding = 7) uniform sampler2D u_tp_page; // slot + 1 per tile, 0 = coarse -layout(set = 0, binding = 8) uniform sampler2DArray u_tp_h; // (T + 2)^2 a layer, a texel of border -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 +layout(set = 0, binding = 8) uniform sampler2D u_tp_page; // slot + 1 per tile, 0 = coarse +layout(set = 0, binding = 9) uniform sampler2DArray u_tp_h; // (T + 2)^2 a layer, a texel of border +layout(set = 0, binding = 10) uniform sampler2DArray u_tp_nrm; +layout(set = 0, binding = 11) uniform sampler2DArray u_tp_ortho; // (S + 2)^2 a layer #define CELL pr.gk_ortho.w // the kind's cell (m) @@ -91,6 +93,34 @@ float heightSmooth(vec2 uv) { return (groundH(vec2(o0.x, o0.y)) * s0.x + groundH(vec2(o1.x, o0.y)) * s1.x) * s0.y + (groundH(vec2(o0.x, o1.y)) * s0.x + groundH(vec2(o1.x, o1.y)) * s1.x) * s1.y; } +// the painted density, 0..1, bilinear over texel centres as ground_density_at reads it on the CPU +float densTexel(ivec2 t, int side) { + t = clamp(t, ivec2(0), ivec2(side - 1)); + uint k = uint(t.y * side + t.x); + return float((dens[k >> 2u] >> ((k & 3u) * 8u)) & 0xFFu) * (1.0 / 255.0); +} +float bladeDensity(vec2 xz) { + int side = int(pr.bd.w + 0.5); + vec2 f = (xz - pr.bd.xy) / pr.bd.z - 0.5; + vec2 i0 = floor(f); + vec2 a = f - i0; + ivec2 i = ivec2(i0); + float lo = mix(densTexel(i, side), densTexel(i + ivec2(1, 0), side), a.x); + float hi = mix(densTexel(i + ivec2(0, 1), side), densTexel(i + ivec2(1, 1), side), a.x); + return mix(lo, hi, a.y); +} +// today's rules, where no density is painted: the slope, the water's margin, the snow line, the photograph +// (grass_rule_at in grass_density.ludic is this on the CPU, for the migration that paints them: keep in step) +float bladeRules(vec2 xz, vec2 huv, float y, vec3 gn) { + 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(pr.gk_ground.x, pr.gk_ground.y, 1.0 - gn.y)) * smoothstep(0.0, 0.6, y - wl - 0.15) * smoothstep(pr.dens.w - pr.gk_ground.z, pr.dens.w - pr.gk_ground.w, y); + if (pr.lev.z > 0.5) { + vec3 oc = groundO(huv); + ok *= pr.gk_ortho.x + pr.gk_ortho.y * smoothstep(0.0, pr.gk_ortho.z, oc.g - oc.b); + } + return ok; +} // grass.vert's bladeField: the region, the patchiness, the dry patches and the tussocks' shade vec3 bladeField(vec2 xz, float y) { float n = fbm(xz * 0.0018 + 4.0, 3) * 0.5 + 0.5; @@ -136,13 +166,8 @@ void main() { vec2 gxz = groundN(huv); vec3 gn = vec3(gxz.x, sqrt(max(1.0 - dot(gxz, gxz), 0.0)), gxz.y); 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(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 *= pr.gk_ortho.x + pr.gk_ortho.y * smoothstep(0.0, pr.gk_ortho.z, oc.g - oc.b); - } + // where it grows: the map's painted density when it has one, else the rules; the hashes stay procedural + float ok = pr.bd.w > 0.5 ? bladeDensity(xz) : bladeRules(xz, huv, ht.r, gn); if (h4 > ok) return; float h = heightSmooth(huv); float seed = hv.x * 0.7 + hv.y * 0.3; diff --git a/packages/ludic.render3d/shaders/spv/grass_cull.comp.spv b/packages/ludic.render3d/shaders/spv/grass_cull.comp.spv index 26165a32735285e0fa067e220f9f8ddbb64d6054..63deb1294f4d8379a1efa1ae29a2e6081daaa767 100644 GIT binary patch literal 32972 zcmZ{s2b^71^@ShIB%$|SQ|N>mdMKfW5+HPtI!Pu4l1xaWLqhMp_a=fOMM1DKiqZrG zMHEm36jYj2DGL0*@4mZ+Gwz_=EXL?W7_!jR+~9?{h|7_HReH^LeaipZC}5z zFHqPQ0gs#9HDOw3)4}%UkoHT1$Bvp*87Hhaw2HrEkG-w26u4{jboBPdit3}stu~|f zTLC_k(ua9lm3q{~jxn7(lI&_TC#*k|Xa=n8?TwYFM|Vz~*yL@El>vuWwzkG9)JIgf zPh$=0=~MQa*fss@d{;-CK}UUI#W|EW0mukh2M7IF`bjA?cI5F=fun(n(MUV36KkAGWZ1b94yu>W1b$56z( zQIBJ1_hGN^4X*m$VM6D`F;z^Lqw?DmTeI)B#$MpGcN`Na#yX06Y>i2?F{8rmjWemI zbWH7-RPzg4__Zzk)?U1=@jZA4#Xjz%o>t@gsl6!GAEcgIa0ez!+ zx=idiqT0)zAja~HnAkZJpCOkV8IwF#di42we7V#L-SdeUT~nuz?*iiA z9EWGwC>)*N{)}@93nX3>+}!_t8k>P<96EiIT-ERqGcZ)2zK0FD+htgbs3ws zES&c7E%w7(_@rLEPh(ouzIip%Mb-DQXzA~`7XOnge{!8X)va9hd1^18_QvTg{H$KQ zZ{vbyjLxZtPVSmIsiWuoyc|9KUeT*fpT_mgHd8Xgj`hY~KJAU0Tlj4){LU&ybBUbe z``}aAyy9Pio9ofmcmkZce+ryy`x)?29K`znMa&^Aeos84i9Z#u-^AVor`7)L;@W>M za96D_Ca(1M#>(JwZP#hxTeR@uy?9&W0C@AB)7}^j_V{#-+~;yXxfad4v(}u;qrhLE z!EKEb;F*K7!4v9y&jWW5$MzSg*Z5NKM0UQhE(4e2xT1w$*TS#w#oHRU!p9TXShx4G zwKwi);di(2dwTJ<#!ul#vBS*w5%7_9zAu8im@IYwb&YQJHwXCZ+<8`jH_unD=Uz6R z9a{Jxc*nFUojH%!f{&ScOf`-TTYNX_#oHP~;m!Tm-q@|OO&>j;8;o3!&1Zx5#s}ar6Q;SGIuN{L6d!wKuM8 z;aB(KZH;SfyFs(6{O}%(q;}&@H+HP-ryO*uKaaRk!r-gs7h2P(cw>2Ju z=NYNHm#w|=Fnn5P=a|gdlkkpujh}AueWr!~wuL_ruO3LMJ=E5C2|m8Q$NvRfJ;2oa z>)n>NU%>O=;u`hoQ|=kgH25eU`dp(m#C4oa!C!x;Xm1RKZ#$`a+vi1& z@5YWq%5&%-xSwdaG}ML_nr+lZ78;jkr5#piOkbsW0Vba5+ICiV`%J2~P@(xes^+t(b89^S z?k9X;lUCekQ|*pHyS%jf{HfjXXqT6EpD(pLf7<1x-RDW|j$OOFwEL{6-Fek6FYP`T zYImNr%S*e@eA-#?6!ap$L**XeLIOSJRB#!)klYk3*PPkgQhYu7K@%{5m`yF0;i z)ZBUcK84R#OA+@60Ouxq;enc~C;uOUmwe&rp7Hz${_MbOwo0ErhU=rAHa}_lJQSaw z!qxOsGycygw$)}k_hdK4&thcs7d@mI?IM_oKvyZm)ZpI!F7GyF}s`#t<^ zuxCv8``{NoIDet=kKkTga`X6t(wOzNznvfa(b)F*_l4guY}Gl!=Y>D;)n`3+?{)Vc zv+?!Xo%1E(1YF1KW?8VB`7Fo(`&^-Bj@p-(*Y*_}i&W1R@zJlf=wG$@AGY{?)@aOnD10sWzrH%D+AGfEdT`IYrTPDj z!M3fMQ0EiAEIn-k(a-)itqp4CEBC(d$9SW`WxU$d6W@4a5cM-&XYHV7Jh{&aeiGm3 zf^h9V2ZY&g8i%j_BlZA*v3%vuhQ&W?sI^e_VG2gZE~H2=GhuP1^!jdo!4pb*|TO1 z)$Upz4PTJjcqf7RSM9~*dJTftX*VU>ZD?}!Xy2{<)DpWJ&3zXB5`6xeo6GC)tFKzR zXWstV?qC4B7I+j{s` z@T;#nfU~w4XBgZz`e{p?9l%3|T+-8SIDG8b(|g+O2-ip5v8LTf@Y{QAvPJlQ@TZ;} z(GzF?iqF>*=K#3v_0yJi-vB@LEWaT%pHrD%?3UCvEXN2F&<-;vWlN{h$SV z`0?-|Lq6=`C&C+zmA3I3I2-J}(=*m<)8}fj*QU?Sa{bPyc5kYAZOVO4R`a~S5bSkH zRry~8AWWs}FZVfG&3Kp8*fz2IT&>-4d94oO5bsm-YCn3YH-`0$<8k=aS6$U}{X7NN zFZ1#o{G|mSAC`Ep!2NC%``hrRo_w;}H}>}d+~?%jKZes^&pOUYGEY4@u_xZ#@cHm? z-uw=bGtFmyx#QfN+Bs5loN}N4bDi5}D>(nEe$8WhNO`7>gnz{+H8=b8x!d=cCHEa>$$f`ea^F*y+;^4X&dJYP__GBci2cQa>+gHZwAcP} z!OicL7Vf*t(%*NN;rhSP!hLrcyZ!m@GJG)ngBI@l%hK-q%aZ&4vgE$MEV=J5!yS+B zFvHEycbFyj9cH-s_zpANe0_%*?)n{8aP7X!jNR`pzRL`^Ki_4B>+k!_lKW0G-1ffH z3^yO&Ylb@>-))8)-*=lO_uXc=?R~cyZhPNvhTGovntNHJI`?A`OY)k@%qj)T)Xc)!;RNnaP7YLj9u<~&v3`*yU%dP=ey66`|dN`_P+Zp zx$izp?)%Sh^YQ&>xb1!a8Lr*;pW)hl{~7Lg72koDe4Q5Vd(qPFd(Lp<`~EWA_`biC zZ_O#=J>yb}_mMl%w&0BM+`6pRx8~gPneKA9_onXJR{3#bxdKhwmYiIkQ*z_l&sAVG zpMCZBe7qX2J{uqTHDEQrdrrp3v-(=Nx-mWLYrfwCo9Fk5_Za+kuv)hmAGJH+>Yvd5 zJJfzY$EN;mikj!XIC1X;+gJE^!S)k=7ud0!O26iDH&`F__}l|7eeQ+pr+#;x@Ats= zrR_6vNG{eNvs29f`(X2D>ooK?PsegUwI9c#&wUg%$0GiMrw_4Oa(WP4j_ZeT$8|!W^h-A6arINI#Pd!m{#y!ZbCd=6!I z#_zTAFj&pv2bZ4yeu-B0|0}q2eoo!jBVc{hyNK_;e-x~qoF4<1F&>8-BXjZuSReJo z_%&F4B>kAQD|ua9m2Os!@g z&ez*u*Dd@Vus&JGcftQ_9sh-{EqmrYux-_|KJSB#ug$*SqE<^^AA;39d%drG1hy~7 zrjOU&$7t%g_Wli4v-qJ|ee1c|7ecTVu{ik5URKLTg-8?>^R?iqe2dCeGINP^v z|AVg0HlI<;WBV`IIN@J__4ygOct(B+)<@m5k&xBLvr*eu6gAI7an44y(l=-SeEAF#QkUEiWz?&G%Whpx@*Og0^1Yn%w*M3g9cNKKoUCMYw&gQQN#ma`yMfraj{x05+bwefb?n&DaBIw=y_q&Prf; z`dt;AGso{ba%1>CXAmW4&g$BTWqbX!>*x0;Z5h`ZV72fy!RGTL=F012FjyaTuamW@ z{dk>dTZ^LRbt2AnqL!HJfsL8D9s*WdpAw%9!1{!52sW>*=|O?9ZFjb063mtZvDDb{nw$Xp7&r;MV)>c4+!)bL@VnRrB2T*?xPlT6sq809Svo zUgzOpwbSs=bMTIEb=Q4oYCo>K`c4!z*Ik^xb^*8cH3CgNdums(n(O5=Y~onw8Fn|Y zeY=nJw~yqtJ6OAU?LqCwywpch)XYnqy!Hf}S9#v<1y|1;>lhl7n%uH_ME>LWSfeOEsbY;4ezZ@m?bgnb=l?XYG48FepXp$=0nCSMF7Kik%h=+yodLG3=Z4Q!Gr^u4 z-Ney*Eqn^Hx12_BbSAFL=9-h9n z4J4?(-X~9hYoA@mI}vP5^;|P2fz|yPqWf(&*pG9dZ5Bn%IS^;RsX4dKvo`DGcna7Y zXVI^7eJWVZdurBBtz0|j+4i3GXMnSRPY280zuL~!My+nF)2P+5x6THumHY1;xO(EB zTl<8cSM$vK`EY&IbAP%3?DeZ{J~DBBF9f@Wt5LLj?&P`nqT1%)6zP}zF4m7y?)^*P z>e>63g3Z|&+8pOu)avFufS6Z+^NevhSgy_az6zXYj4Q!%V_Z$`vu>U-u7UfEVSD|w z>vtKow)Ay9IDK6QmZz^9!RhM;u-q6oQFj-8-3+%c+v}%YziX+r}KJQ*Q&Sc`tR}-U0UGzSVX+<+~L3rr3PlKi>g+_SpBg!SbAqcY=rGqptm1)N1Zi z;~UGEiFG$PvF-xPW4jleJ$w&Xo;~n=@L`m4kKPAYx8Ltk%Z=r^@B^^xukC(n`KI-= z@&n-A@v(pVv8_JIUwfIq<1nuK_d&2@)b|!@d2BxfyEfrJ0_&6c|1nq}b?5)5)P9_Q zZ9k!?IcMUW8*0XMEq)Hp9Cd@`S&LtQ{o68i^LvO|Epzxwu-~K0Is6rxdY;oB0jv4l zBkdlA+pfI+9z#=4yC=YEk5gvXXXmfM#?xj#4^yi-7O%(OfOCyJ4VG*3dVCg~YvdWQ z+!(*5eu|Ro@poXa5!>shJvsdzto=EPZJ(r8%liBQJSQdB(I3HHN3Gv&UqIKE?=mlf zZL4nF=c&~a_fO!<3jXJsC$E>``l#FfC2FNU8w`2Dr;E7#_4=-QIc z>tOrH_lh^bgs*<@&6xV@lem8er~mSq{vYVt;`h(OZy*8F-<#;#+T!;)*u1o*um6D8rNrmIVExpci@#8-W$wNN zdtPQtU%@k`a?ZKr8rm|)bHHt@p6^j@V1J%%F>lwV9jvc5`}b!(YFUfE;6)5Wot*oD z)$`udb8#-Xx|(smpl`M0JrCHt{rP#h7W2Zj#c#gCuiVq~qif4NE&#TV@C9q092bJ? zqaL4yTYMIQPa&wL)Vu6mj}D2)Bg%^ebnQ#V(pVL^@o@H{uPKmu@9*2 z{=Tex-I%sX9|OVWtDj@^=Y(qMZxC25d?j%Cj&NmofBN+NEyuPBy0-MQD%iH_(N+Vi ze}ii!&zh^l9gokN_N`Cy(w02d1ebXXhMR}?nR2hLg|02%1J(xHR^9vgdenZrpKDu} zvOdN8x!At_*{CsnzR+KrKFMVWxXfh(cycNC#fIqGlFLS5+p6EelZrnh-59KHJLfa~ ztC#(ohyB}T6R(6!Nv26)f^XFFj4g;(CyDWZhQQI1}tS1ZT88(h5{_wMkFTYJX62UuIiJrbN}J^N3;`+&=F?hALE zdsAzR-+o|i;roM~udK-dV13lHcHaQ2zs>la^S!`oiE$`cZENOkFg}NYy$_YwLkF6= zZM+AJ0zbqxle2UTTx~QZaXP`qaqMX~7EL{T9N1^YylWW`w=d)AtKGHtJ;DU=A(U?7 znX_Y2vrWcwIM}gdK8^sZl{rj=`+S)1qLbiEb@jWP+LGJk7Qe3A&)?b97QZQA&;Rfv z;Y(36rm0~2u)V(8Gp1?ajLBRblbUTZrs?2vOf%qWIXAtx9|g~Mt(jnX@;L_VS{eIj zuzXYI)bCEmg7>DjjW+Wd#AF-?cK^*yoqk;R#yDTnfKGdnRn;YxlyxC z=KTzCIqzq})iO@!?=1LP#oC_@_e@V-=V+(+iFR(SmHD4nYi0iDquE}6?Z!_27l4z$ zd6~bOZ9HE#q4@iJ&a2PYe*7Kxjq#g{+TWA(?3<6;@w$iht@XU;`9`hzHz~PR4g{O` z{1ok3&x66*4x+^O5U{@XrTt6#Q%i34xgf=l@sgX@m$~`(QOT_Xte)IPfz`|{KBK{< z&p5c+SW2FY$AfLF%{kLwEiop5)f|KQ9Zt!3wdI-nh}xF%>N~Oab-dc+JGr(^qD~%D zz&=-5JVR}NBt>6s&XqiKABl+J5wyn0by9r$OeG6ReW{Q2g-__FhH^KI;o*1`* z%f7z_S2ITXz8!2^ZJE0}z^!xlZ8Y`F-FLv5yBopA(I+v!3pU1`6!YmzeHTSNG42K@ zhHGI=eLQEhmFLXTa6d~>awacX7r+|wxP#tb&z&uZOR-4ws~{G8fct^L{8!_>c|_|g6=YVFp}-D1>_ zP>d1&DA+OI%XRx0_2U$M)crmo{{_X^#u8^8o&cM_-1&Ku;#``)W0bqET(hSri%`^a zUws;E3~h<|8?f`>Ib*wLC~Eo|SDRY=p9Loe{eMew?$Z8uwWeR@RW0qG1KU11{T{5A zHPNP~{|~62r??(+$NUG1W4@?9_x=dhN8SFkskt7mjdN&i`^BhVptv4B)5~9~wHK*v zuT8C7Z`UIE{|S6w!T$_){CUUwGFTsVuLpUqhh@PlQv57W@p@Q}dIgGk_NR8wtG!Bb zyy1VZxp^4pHHz)>z3gwb_E+kBUwH$p{yN368B;AW{sC4C|7XpA!Q9x#TNJf7DT(<` zt-nnjzjwjL(w12N0vr30I@Wt&{nV552VnL0Ddy)G)e_@Fuv+^07_9aYB|iTK>yv&y z0qdvk{L3@{?$=c*eg;vT|AEvi)p&90l?%KI*mYTrI_vN`xL?5+oukP;KW+aXit#R^ zn4^2_zZCWCxi4$&3u=E4+qwRVqMo^#gScuQw23_@*w}Yc61xpeJ+a%tYGv#`aP`FQ z2UhcMdNXfxf$dXUV$ThBK9b`+aJ9sl53J_j0w>P=U}I@ZoCUznW#TLdS4*6Q!D{{u zapEijHkLN`hV!iE`R05r2KHJ~*Y3Kg*{Al!!D>_BuKN;jwVXRkfz|vwW%r4^+$U?$ z#?Rc8+_zQ-yC2r1c0agx{O`%RAMzb`C3t_z0Lnm$`Pl!;U~PHtwhCCybMF)KUk&WX zdw{l8DQfm7Hb;MNpxTer#;mRr^}R^D>tNpM$$JD?P42wy3U=&?yBk~|^^9$I zu=8s1Ily@siKec9V(kGoR-P61gsV-`zizh|T-}o2?b{n{JZ*`!57>3cn(qtO&rh`d zYE3!6@3B8bUv0*4FQ{cNIOYuv!l%6UH-Nk58&SLF?ui3w=UB4`4r<}!3$Fj+EqqeV zGxrC>?Z zqORS()v^vJg4MELP6DfCzsv^valdGrMNxB1V&k}9PNAG!b8|WkoSYnwJp1Jg@c-H` zXQFE}Psc1zo@ay2Q=8+LXTO{ScE8wLZr^HIm#e{Q*+18S)v|xC1N(9R zXuFo8=9t9BasS*vxxVJ+bQ3r^IUafT&#mD8l=UdLP?F0x!P?BzG0S877T7$sIevNe z&+TCMkNwzAd)D<1u(|2;^}05HxAfa++AV&p^l@#qIY!574(_4vQtqs|W4sIO7}M81 zwU4@S?xxP~vEB>TZt-K4zO-c@dG2nd1E2D~wI$qqhEeA}bsz1@ee_@p|78n*qJ=-z z!k=m3e`(?GweU|{xC1KlX>Z~FcY8~_|LxwAuiV1@@AsB=|NFfq_rKpO&%U~!eEL%c zPzF-6cYXlYmfypB0IZg0_8)=$xW~2qkn%H%^DR#NAA^nWzAfYb1g^&{d$lYKy|Ldj$$m{1|wQjI~U*nj60as6ac^Th+jIZy*U~|yd zJtL3pSC#%Xe*~^i-fuq&)<-?#dkpN{S@KT$aWs9k*^hfqE&co&td?ikC&B!yo?*4w z#{H?DHcx}gHot-MuX;|`W*hggdfGe-HcoQ=EjZ7#$@O<=>iVYLbKtVw@8PZOo<~#H zH}mrcu<^7d)*r!TtQX*|v0g+|*Eg|V0vk_T^8YjVPj$(2^viI4wK)e~w`%GCRj~c% zS^qV#nsMUuSFk?m`)^?V)YJFt;8*IBxqAbyu76_v9bESJ54hTitW|veS^K2FH{trJ z$LFmUpSR)qsptF1J79CrmOS4Dx6aYO(Dc>j9C? z$8i1Blgq!st+{-Hrmr@0@f=l4+|R(SVPbp^Zrv~cK~vW^?fwgPTv?kh!1}4j=S#5t z#pf%qe(L7(DYaVs=ip2=m+(2^_L2E-gX^Pid(V8e`1b)DGrTX{{?oo6TpxAYSN8~T z{O1OHO=m9Vfw#`ZylDDqOS}2N<{3Ue+}v`_ECAL=J-I9hHkP*Zw-7jITl^MA(^p&K zF9N=g{t|ytxPI!6%X^(#{1*q8<5~iqaiyOn(e%@nc1wYszwo8u&RP0h2CR>I;w%d` zmbQ#*IdJQ^mPgZ9TjH+(F2}VZTt9Wkd9{a*){YyfsF+7fF+ zu;VXt+z76oSQ~?FtDbh7fLqsWQ#AGXYzB6Y%Gg8U>KXs$VB4uXexJ`Oto`Jvna;wyk>FZ3Avyvu)ATruf}+AoVsiUYvSciqDNdO$Vz@qqtw><$f7X8$acD%pKtFksYbsBd*U;v~!Q-JLd^4{OlHfUcm>_?xKPZ zhF@B6?UxnY{;nvv_Uj6+{rZ9%@799re|y38zoX#V?=HCZdupEVc*nr|vnEGVyEd+$ z^Ls2@Tdv3Bz-r!8^WTL!9&cm7TUJHPJNlfbhm>8IKw^fkNI^M31OxSGWe zE2S1{=>a#`xEQ)vlo!$Nj68 zK5hV~5BIV>ecTAP58G&Sueir=0z2l*z{WA3o8jvDU7lOO&a*%FGFM~iV-8+(+LQA) z!DY_hf~(y|(cf!GEjfQ1oSeO$ho+u+xgVT)F;`>hV-B7j+Rec^{Q=my3V#5sPx^ZhY=5~| z+2)6E_4M~6aQZWzG4$~a)>fXuyVKs!&Xl|_-lf1JYV01^wZ=P9??&;Q9!c%Fto^6N zF3;&l3a#_9;)BpM!1FOShCT*QfBGy--;blI=L~%UtY-0pOV7FT zYcyl(lbBC}TVpmv#F46WIPX$Zu1KUQw%-tJcuetEQ!=1~t`3G1Z^=NN`)&EIJ-*17_ zxBLBdxPEy~dmC*3`ei=e1v_u=P!FU|4*!B{i{E>-pYvN@Z|}pkC9e;_wpCBt55fPd zkB`u`#qZpdrTYH zpYN$_ul`Xbw?1HVYo{*vdSAG<`1LFN%6&W+y0-YuUHFw_od;c8u8DcU=2FI*53U}c z`D>pt&H`xaS-%Crwo}h~Ed z7Tg$vTKHN8_qle%g4=$hf_twWT5#j-R&d+zS#aC$RdD?~3a+D=S5BIoOH7Wc_!-*_jkhe(U!Fx0M_QUmpw5M zuC8V*=U*-By9!t>`)*Zm_T5Tg|63IL#D5L2>l*(x;qhM$ygEgnfQ&F%Lur7N^;p1ExFj&jud^9i}tMH?%;9__kgR7 zr0DN8rY83KD%WOjxND=2w&c1GSX;R^`@+@LjODegmN_^Gtd_AI3|33Y*bYI<*bV?6 zNYOWQdnnksH5cdlFtD2KGJds;e>Aupeq*PUXB2(m-8|au3qlhN$~91QQ-Y3`q@thwf4;0G_YFcZ8}&jCG$1|E%P=7Je8tv z=4~dpoVTOkYPQR~sm1?jaQ3ig;BoMB9#4R)9Z$(To&-LzE}6$!aDBDe&unV7%;U*m v=TSZLcnVm(oX1n)>T2;j4Sp;|Th7$e!N*XvdCt2>&Hy_eZQd*8-YfqP8)bXd literal 29488 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