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 <noreply@anthropic.com>
This commit is contained in:
parent
e6f4685bd0
commit
9186abbbe3
10 changed files with 219 additions and 21 deletions
9
changes/render3d-blade-density.md
Normal file
9
changes/render3d-blade-density.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
100
packages/ludic.render3d/grass_density.ludic
Normal file
100
packages/ludic.render3d/grass_density.ludic
Normal file
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
53
packages/ludic.render3d/grass_rule.ludic
Normal file
53
packages/ludic.render3d/grass_rule.ludic
Normal file
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue