diff --git a/packages/ludic.jobs/README.md b/packages/ludic.jobs/README.md index 7561670a..809b3028 100644 --- a/packages/ludic.jobs/README.md +++ b/packages/ludic.jobs/README.md @@ -64,7 +64,8 @@ export port JobsPay { reward: fn(int, int, int) -> void } # (scope, money, rep | `jobs_morning()`, `jobs_roll(lo, hi)`, `jobs_post(b, s, kind, param, need, money, rep)`, `jobs_config_seed(seed)` | posting | | `jobs_slots(b)`, `jobs_posted(b)`, `jobs_post_kind / _param / _need / _have / _money / _rep / _state / _day(b, s)`, `jobs_posts_done(b)`, `jobs_total(b)` | the boards | | `jobs_post_hand_in(b, s)`, `jobs_post_set(b, s, ...)`, `jobs_set_total(b, n)` | a post's verbs | -| `jobs_facts() -> Queue` | `{ what: JOBS_F_OFFERED / _PROGRESSED / _COMPLETED / _HANDED_IN, job, board, slot, kind, param, have, need, money, rep, scope }` | +| `jobs_give_up(i)`, `jobs_back_on(i)`, `jobs_post_give_up(b, s)` | letting one go: a written job is offered again from the next day (the day back is saved with it), a post comes down and the morning replaces it; an auto board's posts are never taken, so never given up | +| `jobs_facts() -> Queue` | `{ what: JOBS_F_OFFERED / _PROGRESSED / _COMPLETED / _HANDED_IN / _GIVEN_UP, job, board, slot, kind, param, have, need, money, rep, scope }` | | `jobs_system()`, `jobs_party_system()` | `"jobs"` (yours, the tick) and `"jobs.party"` (the party's), each saving its scope's jobs and boards by key | ## Tests diff --git a/packages/ludic.jobs/defs.ludic b/packages/ludic.jobs/defs.ludic index 4da15821..657fc49a 100644 --- a/packages/ludic.jobs/defs.ludic +++ b/packages/ludic.jobs/defs.ludic @@ -49,6 +49,7 @@ export const JOBS_F_OFFERED: int = 0 export const JOBS_F_PROGRESSED: int = 1 export const JOBS_F_COMPLETED: int = 2 export const JOBS_F_HANDED_IN: int = 3 +export const JOBS_F_GIVEN_UP: int = 4 # taken and let go: what the game placed for it comes down # `job` a written job, or -1 and `board` / `slot` a post; what it counted rides along, since a # post handed in is replaced before anyone reads the fact diff --git a/packages/ludic.jobs/giveup.ludic b/packages/ludic.jobs/giveup.ludic new file mode 100644 index 00000000..95b9c972 --- /dev/null +++ b/packages/ludic.jobs/giveup.ludic @@ -0,0 +1,25 @@ +# giveup.ludic - letting a job go. A written job goes back on offer, but not until tomorrow, so +# giving up is never a reroll; a post comes down and the morning puts up another in its place. +# Either way the fact says so, and the game takes down what it had placed for it. +export function jobs_give_up(jobs_st: mut JobsState, i: int) -> bool { + if not jobs_ok(i) or not jobs_under_way(jobs_st, i) { return false } + jobs__fact(jobs_st, JOBS_F_GIVEN_UP, i, -1, -1, jobs_st.jobs__hv[i], Jobs[i].need) + jobs_st.jobs__st[i] = JOBS_OFFERED + jobs_st.jobs__hv[i] = 0 + jobs_st.jobs__back[i] = JobsWorld.day() + 1 + return true +} + +# the day a written job given up may be taken again, 0 never given up +export function jobs_back_on(jobs_st: JobsState, i: int) -> int { return jobs_st.jobs__back[i] } + +# a post taken and not yet paid; an auto board's posts are not taken, so they are not given up +export function jobs_post_give_up(jobs_st: mut JobsState, b: int, s: int) -> bool { + if not jobs_board_ok(b) or s < 0 or s >= JobBoards[b].slots or JobBoards[b].auto { return false } + let k = jobs_st.jobs__off[b] + s + let st = jobs_st.jobs__ps[k] + if jobs_st.jobs__pk[k] < 0 or (st != JOBS_ACTIVE and st != JOBS_READY) { return false } + jobs__fact(jobs_st, JOBS_F_GIVEN_UP, -1, b, s, jobs_st.jobs__ph[k], jobs_st.jobs__pn[k]) + jobs__clear(jobs_st, k) + return true +} diff --git a/packages/ludic.jobs/index.ludic b/packages/ludic.jobs/index.ludic index b29ea912..6ff8a86a 100644 --- a/packages/ludic.jobs/index.ludic +++ b/packages/ludic.jobs/index.ludic @@ -7,6 +7,7 @@ import "defs.ludic" import "ports.ludic" import "state.ludic" import "jobs.ludic" +import "giveup.ludic" import "count.ludic" import "lists.ludic" import "boards.ludic" diff --git a/packages/ludic.jobs/jobs.ludic b/packages/ludic.jobs/jobs.ludic index 2ec1c933..2be089f8 100644 --- a/packages/ludic.jobs/jobs.ludic +++ b/packages/ludic.jobs/jobs.ludic @@ -20,12 +20,12 @@ export function jobs_under_way(jobs_st: JobsState, i: int) -> bool { # on offer: not taken, the scope's standing and the story far enough, and the game allowing it export function jobs_offered(jobs_st: JobsState, i: int) -> bool { let d = Jobs[i] - if jobs_state(jobs_st, i) != JOBS_OFFERED { return false } + if jobs_state(jobs_st, i) != JOBS_OFFERED or JobsWorld.day() < jobs_st.jobs__back[i] { return false } return JobsWorld.rep(d.scope) >= d.minrep and JobsWorld.stage() >= d.minstage and JobsWorld.open(i) } export function jobs_take(jobs_st: mut JobsState, i: int) -> bool { - if not jobs_ok(i) or jobs_state(jobs_st, i) != JOBS_OFFERED { return false } + if not jobs_ok(i) or jobs_state(jobs_st, i) != JOBS_OFFERED or JobsWorld.day() < jobs_st.jobs__back[i] { return false } jobs_st.jobs__st[i] = JOBS_ACTIVE jobs_st.jobs__hv[i] = 0 jobs__recheck_one(jobs_st, i) diff --git a/packages/ludic.jobs/save.ludic b/packages/ludic.jobs/save.ludic index 46a15a0e..fedc10a3 100644 --- a/packages/ludic.jobs/save.ludic +++ b/packages/ludic.jobs/save.ludic @@ -5,6 +5,7 @@ function jobs__reset_scope(jobs_st: mut JobsState, sc: int) -> void { if Jobs[i].scope != sc { continue } jobs_st.jobs__st[i] = JOBS_OFFERED jobs_st.jobs__hv[i] = 0 + jobs_st.jobs__back[i] = 0 } for b in 0 .. BOARD_COUNT { if JobBoards[b].scope != sc { continue } @@ -19,8 +20,8 @@ function jobs__save_scope(jobs_st: JobsState, sc: int) -> Val { let v = Value.object() let js = Value.object() for i in 0 .. JOB_COUNT { - if Jobs[i].scope != sc or jobs_st.jobs__st[i] == JOBS_OFFERED { continue } - sv_put_ints(js, Jobs[i].key, [jobs_st.jobs__st[i], jobs_st.jobs__hv[i]]) + if Jobs[i].scope != sc or (jobs_st.jobs__st[i] == JOBS_OFFERED and jobs_st.jobs__back[i] == 0) { continue } + sv_put_ints(js, Jobs[i].key, [jobs_st.jobs__st[i], jobs_st.jobs__hv[i], jobs_st.jobs__back[i]]) } Value.put(v, "jobs", js) let bs = Value.object() @@ -48,7 +49,9 @@ function jobs__load_scope(jobs_st: mut JobsState, sc: int, v: Val) -> void { let js = Value.get(v, "jobs") for i in 0 .. JOB_COUNT { let p = sv_ints(js, Jobs[i].key) - if Jobs[i].scope == sc and len(p) == 2 { jobs_set(jobs_st, i, p[0], p[1]) } + if Jobs[i].scope != sc or len(p) < 2 { continue } + jobs_set(jobs_st, i, p[0], p[1]) + if len(p) > 2 { jobs_st.jobs__back[i] = p[2] } } } if Value.has(v, "boards") == 0 { return } diff --git a/packages/ludic.jobs/state.ludic b/packages/ludic.jobs/state.ludic index c3c2b7d8..370701f2 100644 --- a/packages/ludic.jobs/state.ludic +++ b/packages/ludic.jobs/state.ludic @@ -4,6 +4,7 @@ export state JobsState { jobs__st: []int = jobs__ints(JOB_COUNT, JOBS_OFFERED) jobs__hv: []int = jobs__ints(JOB_COUNT, 0) jobs__seen: []bool = jobs__bools(JOB_COUNT) # an offer already said + jobs__back: []int = jobs__ints(JOB_COUNT, 0) # given up: the first day it may be taken again jobs__primed: bool = false # the first tick after a reset learns the offers quietly jobs__done_n: []int = jobs__ints(JOBS_GROUPS, 0) # handed in, per group jobs__off: []int = jobs__offsets() diff --git a/packages/ludic.jobs/tests/giveup_test.ludic b/packages/ludic.jobs/tests/giveup_test.ludic new file mode 100644 index 00000000..96b83c66 --- /dev/null +++ b/packages/ludic.jobs/tests/giveup_test.ludic @@ -0,0 +1,132 @@ +# giveup_test.ludic - letting a job go: a written job back on offer tomorrow, not today; a post taken +# down and replaced in the morning; a daily never taken; the day back saved with the job +import "ludic.jobs" +import "ludic.base" +program JobsGiveUpTest { + numbers float + const K_FISH: int = 0 + const K_PHOTO: int = 1 + const K_WOOD: int = 2 + + def Jobs catch { group: 0, kind: K_FISH, need: 3, money: 30, rep: 5 } + def Jobs deer { group: 0, kind: K_PHOTO, param: 4, need: 1, money: 20, rep: 2, minrep: 10 } + def Jobs ten { group: 0, kind: K_FISH, need: 10, money: 70, rep: 12, scope: JOBS_PARTY } + def Jobs still { group: 1, need: 0, money: 60, rep: 12, minstage: 2 } + def JobBoards daily { slots: 3, keep: 1, auto: true, refill: false } + def JobBoards ada { slots: 2, keep: 7, scope: JOBS_PARTY } + + state JobsTestState { + rep: int = 0 + stage: int = 0 + day: int = 1 + guest: bool = false + fish: int = 0 # the state's count of fish + paid: int = 0 + paid_rep: int = 0 + paid_scope: int = -1 + } + + function world_rep(jobs_test_st: JobsTestState, sc: int) -> int { return jobs_test_st.rep } + function world_stage(jobs_test_st: JobsTestState) -> int { return jobs_test_st.stage } + function world_day(jobs_test_st: JobsTestState) -> int { return jobs_test_st.day } + function world_owns(jobs_test_st: JobsTestState, sc: int) -> bool { return sc == JOBS_SELF or not jobs_test_st.guest } + function world_evidence(jobs_test_st: JobsTestState, kind: int, param: int) -> int { + if kind == K_FISH { return jobs_test_st.fish } + return -1 + } + function world_roll(jobs_st: mut JobsState, b: int, s: int) -> void { + let need = jobs_roll(jobs_st, 2, 4) + if b == BOARD_DAILY { jobs_post(jobs_st, b, s, K_WOOD, -1, need, 7, 2) } else { jobs_post(jobs_st, b, s, K_FISH, -1, need, 9 * need, 3) } + } + function pay(jobs_test_st: mut JobsTestState, sc: int, money: int, r: int) -> void { + jobs_test_st.paid += money + jobs_test_st.paid_rep += r + jobs_test_st.paid_scope = sc + } + + bind JobsWorld { rep: fn world_rep, stage: fn world_stage, day: fn world_day, owns: fn world_owns, evidence: fn world_evidence, roll: fn world_roll } + bind JobsPay { reward: fn pay } + + function fresh(jobs_st: mut JobsState, jobs_test_st: mut JobsTestState) -> void { + jobs_test_st.rep = 0 + jobs_test_st.stage = 0 + jobs_test_st.day = 1 + jobs_test_st.guest = false + jobs_test_st.fish = 0 + jobs_test_st.paid = 0 + jobs_test_st.paid_rep = 0 + jobs_reset(jobs_st) + jobs_party_reset(jobs_st) + } + + function count_of(jobs_st: JobsState, what: int) -> int { + let fs = q_drain(jobs_facts(jobs_st)) + var n = 0 + for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } } + return n + } + + test "a job given up is offered again tomorrow, not today" (jobs_st: mut JobsState, jobs_test_st: mut JobsTestState) { + fresh(jobs_st, jobs_test_st) + expect(jobs_take(jobs_st, JOB_CATCH)) + jobs_count(jobs_st, K_FISH, 0, 2) + count_of(jobs_st, JOBS_F_GIVEN_UP) + expect(jobs_give_up(jobs_st, JOB_CATCH)) + expect_eq(count_of(jobs_st, JOBS_F_GIVEN_UP), 1) + expect_eq(jobs_state(jobs_st, JOB_CATCH), JOBS_OFFERED) + expect_eq(jobs_have(jobs_st, JOB_CATCH), 0) + expect(not jobs_offered(jobs_st, JOB_CATCH)) + expect(not jobs_take(jobs_st, JOB_CATCH)) + expect(not jobs_give_up(jobs_st, JOB_CATCH)) + jobs_test_st.day = 2 + expect(jobs_offered(jobs_st, JOB_CATCH)) + expect(jobs_take(jobs_st, JOB_CATCH)) + expect_eq(jobs_test_st.paid, 0) + } + + test "a ready job can be let go too, and nothing is paid" (jobs_st: mut JobsState, jobs_test_st: mut JobsTestState) { + fresh(jobs_st, jobs_test_st) + jobs_take(jobs_st, JOB_CATCH) + jobs_count(jobs_st, K_FISH, 0, 3) + expect_eq(jobs_state(jobs_st, JOB_CATCH), JOBS_READY) + expect(jobs_give_up(jobs_st, JOB_CATCH)) + expect(not jobs_hand_in(jobs_st, JOB_CATCH)) + expect_eq(jobs_test_st.paid, 0) + } + + test "a post given up comes down, and the morning puts up another" (jobs_st: mut JobsState, jobs_test_st: mut JobsTestState) { + fresh(jobs_st, jobs_test_st) + jobs_morning(jobs_st) + count_of(jobs_st, JOBS_F_OFFERED) + expect(jobs_post_give_up(jobs_st, BOARD_ADA, 0)) + expect_eq(count_of(jobs_st, JOBS_F_GIVEN_UP), 1) + expect_eq(jobs_post_kind(jobs_st, BOARD_ADA, 0), -1) + expect(not jobs_post_give_up(jobs_st, BOARD_ADA, 0)) + expect(jobs_post_kind(jobs_st, BOARD_ADA, 1) >= 0) + jobs_test_st.day = 2 + jobs_morning(jobs_st) + expect(jobs_post_kind(jobs_st, BOARD_ADA, 0) >= 0) + expect_eq(jobs_post_day(jobs_st, BOARD_ADA, 0), 2) + } + + test "a daily is not taken, so it is not given up" (jobs_st: mut JobsState, jobs_test_st: mut JobsTestState) { + fresh(jobs_st, jobs_test_st) + jobs_morning(jobs_st) + expect(not jobs_post_give_up(jobs_st, BOARD_DAILY, 0)) + expect(jobs_post_kind(jobs_st, BOARD_DAILY, 0) >= 0) + } + + test "the day back is saved with the job" (jobs_st: mut JobsState, jobs_test_st: mut JobsTestState) { + fresh(jobs_st, jobs_test_st) + jobs_take(jobs_st, JOB_CATCH) + jobs_give_up(jobs_st, JOB_CATCH) + let body = jobs_save(jobs_st) + fresh(jobs_st, jobs_test_st) + expect(jobs_offered(jobs_st, JOB_CATCH)) + jobs_load(jobs_st, body, 1) + expect_eq(jobs_back_on(jobs_st, JOB_CATCH), 2) + expect(not jobs_offered(jobs_st, JOB_CATCH)) + jobs_test_st.day = 2 + expect(jobs_offered(jobs_st, JOB_CATCH)) + } +} diff --git a/packages/ludic.render3d/env.ludic b/packages/ludic.render3d/env.ludic index 96c07802..fc07c7f6 100644 --- a/packages/ludic.render3d/env.ludic +++ b/packages/ludic.render3d/env.ludic @@ -191,7 +191,13 @@ export state Render3dState { gvk_frame_fence: bytes = null # the presented frame still on the GPU (one frame in flight) gvk_frame_pending: bool = false gvk_frame_pending_cb: pointer = null - gvk_inflight: int = -1 # -1 until asked: 1 leaves the presented frame in flight + gvk_frame_pending_no: int = -1 # which frame it is (gvk_frame_no when it was submitted) + gvk_dpools: []long = null # a descriptor pool per frame slot (frame_no & 1) + gvk_ring_end: int = 0 # the end of this frame's half of the ring + gvk_retired_frame: []int = null # the last frame that read each retired buffer + gvk_labels: bool = false # R3D_VK_LABELS: each profiled pass is a debug label (Metal System Trace) + gvk_inflight: int = -1 + gvk_label_depth: int = 0 # -1 until asked: 1 leaves the presented frame in flight gvk_has_hqr: bool = false gvk_ts_period: float = 0.0 # float bits: nanoseconds per timestamp tick gvk_qpool: long = 0 @@ -208,6 +214,7 @@ export state Render3dState { gvk_cp_layout: []long = null gvk_cp_dsl: []long = null gvk_cp_nbuf: []int = null + gvk_cp_ntex: []int = null gvk_pipe_keys: []string = null gvk_pipe: []long = null gvk_zero_vbuf: int = 0 @@ -357,8 +364,22 @@ export state Render3dState { grass_n: int = 0 grass_cmds: int = 0 grass_band_start: words = null # per band: its first record - grass_band_cells: words = null # ... and its 16 m cells per tile side + grass_band_cells: words = null # ... and its cells per tile side grass_band_n: int = 0 + gg_on: bool = false # the blades are culled on the GPU (grass_gpu.ludic) + gg_reset: int = 0 + gg_cull: int = 0 + gg_prog: int = 0 + gg_tiles_buf: []int = null + gg_tv: floats = null + gg_out: int = 0 + gg_cmds: int = 0 + gg_mesh: []Mesh = null + gg_n: int = 0 + gg_max: int = 0 + grass_band_mesh: []Mesh = null # per band: the blade it draws (rows fall with distance) + grass_mesh3: Mesh = null # three rows: the middle distance + grass_mesh2: Mesh = null # two rows, one quad: far enough that the arch is under a pixel grass_mesh_on: bool = false grass_mesh_prog: int = 0 r3d_draw_cb: fn() = null @@ -582,6 +603,9 @@ export state Render3dState { sc_cast_dy: float = 0.0 sc_cast_k: float = 0.0 sc_skip_blade: bool = false + sc_skip_grass: bool = false + grass_force: bool = false # R3D_BLADES: the blades on whatever the game set + grass_env_off: bool = false # R3D_NOBLADES: the blades off whatever the game set sc_skip_card: bool = false cb_state: int = 12345 shadow_res: int = 2048 diff --git a/packages/ludic.render3d/gpu.ludic b/packages/ludic.render3d/gpu.ludic index 90489d07..49fea526 100644 --- a/packages/ludic.render3d/gpu.ludic +++ b/packages/ludic.render3d/gpu.ludic @@ -513,6 +513,14 @@ function gpu_compute(render3d_st: mut Render3dState, name: string, n_bufs: int) function gpu_dispatch(render3d_st: mut Render3dState, c: int, params: pointer, n_params: int, bufs: words, groups: int) -> void { if render3d_st.gpu_kind == GPU_VK and c > 0 { gvk_dispatch(render3d_st, c, params, n_params, bufs, groups, 1, 1) } } +# a compute program that also samples n_tex textures (bound after its buffers), and its 2-D dispatch +function gpu_compute_tex(render3d_st: mut Render3dState, name: string, n_bufs: int, n_tex: int) -> int { + if render3d_st.gpu_kind != GPU_VK { return 0 } + return gvk_compute_new_tex(render3d_st, name, n_bufs, n_tex) +} +function gpu_dispatch_tex(render3d_st: mut Render3dState, c: int, params: pointer, n_params: int, bufs: words, texs: words, gx: int, gy: int) -> void { + if render3d_st.gpu_kind == GPU_VK and c > 0 { gvk_dispatch_tex(render3d_st, c, params, n_params, bufs, texs, gx, gy, 1) } +} # a buffer a compute pass writes (never reallocated under a draw that reads it) function gpu_buffer_gpu_owned(render3d_st: mut Render3dState, buf: int) -> void { if render3d_st.gpu_kind == GPU_VK { gvk_buf_gpu_owned(render3d_st, buf) } } # the host-visible contents of a buffer, for a readback after gpu_finish; null on OpenGL diff --git a/packages/ludic.render3d/gpu_vk.ludic b/packages/ludic.render3d/gpu_vk.ludic index 2793f781..87f2b6d5 100644 --- a/packages/ludic.render3d/gpu_vk.ludic +++ b/packages/ludic.render3d/gpu_vk.ludic @@ -85,6 +85,9 @@ function gvk_init(render3d_st: mut Render3dState) -> bool { Vk.put_ptr(iext_names, n_iext * 8, VK_KHR_SURFACE_EXTENSION_NAME); n_iext += 1 Vk.put_ptr(iext_names, n_iext * 8, surf_ext); n_iext += 1 } + # R3D_VK_LABELS: name each pass for a GPU capture (MoltenVK makes them Metal debug groups) + render3d_st.gvk_labels = r3d_env_has(render3d_st, "R3D_VK_LABELS") and gvk_ext_in(iexts, nie, VK_EXT_DEBUG_UTILS_EXTENSION_NAME) and Vk.has("vkCmdBeginDebugUtilsLabelEXT") == 1 + if render3d_st.gvk_labels { Vk.put_ptr(iext_names, n_iext * 8, VK_EXT_DEBUG_UTILS_EXTENSION_NAME); n_iext += 1 } # HDR output: an instance only lists the HDR colour spaces when it asks for them render3d_st.gvk_has_colorspace = render3d_st.gvk_has_surface and gvk_ext_in(iexts, nie, VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME) if render3d_st.gvk_has_colorspace { Vk.put_ptr(iext_names, n_iext * 8, VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME); n_iext += 1 } @@ -544,8 +547,11 @@ function gvk_once_end(render3d_st: mut Render3dState, cb: pointer) -> bool { # CPU while the GPU draws: waiting at the present serialised the two, which cost MoltenVK a third # of its frame rate against OpenGL. Anything that would reuse what that frame reads - the next # frame's command buffer and ring, a one-off submit, a descriptor pool reset, a swapchain rebuild - -# waits for it first (gvk_frame_wait), and a buffer it reads is busy (gvk_buf_busy). On by default -# on macOS; R3D_VK_INFLIGHT=0/1 says otherwise. +# waits for it first (gvk_frame_wait), and a buffer it reads is busy (gvk_buf_busy). The frame's +# uniform ring and descriptor pool are double-buffered by frame_no & 1, so the next frame records +# while this one draws: waiting at the next frame's start left the GPU idle for the whole of its +# recording and MoltenVK's encoding (a third of a frame). On by default on macOS; R3D_VK_INFLIGHT=0/1 +# says otherwise. function gvk_inflight_on(render3d_st: mut Render3dState) -> bool { if render3d_st.gvk_inflight < 0 { render3d_st.gvk_inflight = 0 @@ -571,6 +577,7 @@ function gvk_frame_submit(render3d_st: mut Render3dState, cb: pointer) -> bool { if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkQueueSubmit", r) } render3d_st.gvk_frame_pending = true render3d_st.gvk_frame_pending_cb = cb + render3d_st.gvk_frame_pending_no = render3d_st.gvk_frame_no return true } @@ -583,7 +590,7 @@ function gvk_frame_wait(render3d_st: mut Render3dState) -> void { Vk.free_command_buffers(render3d_st.gvk_dev, render3d_st.gvk_pool, 1, cbs) render3d_st.gvk_frame_pending = false render3d_st.gvk_frame_pending_cb = null - gvk_retire_flush(render3d_st) + gvk_retire_upto(render3d_st, render3d_st.gvk_frame_pending_no) } # ---- teardown ----------------------------------------------------------------------------- @@ -640,3 +647,30 @@ function gvk_query_result(render3d_st: Render3dState, id: int, out: words) -> bo out[0] = int(float(ticks) * render3d_st.gvk_ts_period) return true } + +# ---- debug labels (R3D_VK_LABELS) ------------------------------------------------------------ +function gvk_label_begin(render3d_st: mut Render3dState, name: pointer) -> void { + # only inside a frame: a pass the load profiles comes before the frame's pools exist + if render3d_st.gpu_kind != GPU_VK or render3d_st.gvk_cb == null { return } + let cb = render3d_st.gvk_cb + let li = bytes(VkDebugUtilsLabelEXT_sizeof) + Vk.zero(li, VkDebugUtilsLabelEXT_sizeof) + Vk.put_i32(li, VkDebugUtilsLabelEXT_sType, VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT) + Vk.put_ptr(li, VkDebugUtilsLabelEXT_pLabelName, name) + Vk.cmd_begin_debug_utils_label_ext(cb, li) + render3d_st.gvk_label_depth += 1 +} +function gvk_label_end(render3d_st: mut Render3dState) -> void { + if render3d_st.gpu_kind != GPU_VK or render3d_st.gvk_label_depth <= 0 or render3d_st.gvk_cb == null { return } + Vk.cmd_end_debug_utils_label_ext(render3d_st.gvk_cb) + render3d_st.gvk_label_depth -= 1 +} +# the frame's command buffer ends with every label it opened closed (called where it ends: a +# pointer compares by what it points at, so once_end cannot ask which buffer it was handed) +function gvk_labels_close(render3d_st: mut Render3dState) -> void { + if render3d_st.gvk_cb == null { render3d_st.gvk_label_depth = 0; return } + while render3d_st.gvk_label_depth > 0 { + Vk.cmd_end_debug_utils_label_ext(render3d_st.gvk_cb) + render3d_st.gvk_label_depth -= 1 + } +} diff --git a/packages/ludic.render3d/gpu_vk_draw.ludic b/packages/ludic.render3d/gpu_vk_draw.ludic index 8efb6e85..b5bb3acb 100644 --- a/packages/ludic.render3d/gpu_vk_draw.ludic +++ b/packages/ludic.render3d/gpu_vk_draw.ludic @@ -120,24 +120,28 @@ function gvk_program(render3d_st: mut Render3dState, p: int, key: string, spv_di # ---- compute ------------------------------------------------------------------------------ # A compute program is .comp.spv beside the manifest, with bindings fixed by convention: -# 0 the parameter block (a uniform buffer, copied into the frame's ring at the dispatch) and -# 1 .. n storage buffers. Handles start at 1. +# 0 the parameter block (a uniform buffer, copied into the frame's ring at the dispatch), +# 1 .. n storage buffers, then n+1 .. n+m sampled textures (linear, clamped). Handles start at 1. -function gvk_compute_new(render3d_st: mut Render3dState, name: string, n_bufs: int) -> int { +function gvk_compute_new(render3d_st: mut Render3dState, name: string, n_bufs: int) -> int { return gvk_compute_new_tex(render3d_st, name, n_bufs, 0) } + +function gvk_compute_new_tex(render3d_st: mut Render3dState, name: string, n_bufs: int, n_tex: int) -> int { let zero: long = 0 if render3d_st.gvk_cp_pipe == null { render3d_st.gvk_cp_pipe = new []long; render3d_st.gvk_cp_layout = new []long; render3d_st.gvk_cp_dsl = new []long; render3d_st.gvk_cp_nbuf = new []int - push(render3d_st.gvk_cp_pipe, zero); push(render3d_st.gvk_cp_layout, zero); push(render3d_st.gvk_cp_dsl, zero); push(render3d_st.gvk_cp_nbuf, 0) + render3d_st.gvk_cp_ntex = new []int + push(render3d_st.gvk_cp_pipe, zero); push(render3d_st.gvk_cp_layout, zero); push(render3d_st.gvk_cp_dsl, zero); push(render3d_st.gvk_cp_nbuf, 0); push(render3d_st.gvk_cp_ntex, 0) } let module = gvk_module(render3d_st, `{render3d_st.gvk_spv_dir}/{name}.comp.spv`) if module == 0 { return 0 } - let nb = n_bufs + 1 + let nb = n_bufs + 1 + n_tex let bw = VkDescriptorSetLayoutBinding_sizeof let binds = bytes(bw * nb) Vk.zero(binds, bw * nb) for k in 0 .. nb { var kind = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER if k == 0 { kind = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER } + if k > n_bufs { kind = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER } Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_binding, k) Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_descriptorType, kind) Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_descriptorCount, 1) @@ -172,17 +176,25 @@ function gvk_compute_new(render3d_st: mut Render3dState, name: string, n_bufs: i r = Vk.create_compute_pipelines(render3d_st.gvk_dev, zero, 1, cpci, null, out) if r != VK_SUCCESS { gvk_fail(render3d_st, `vkCreateComputePipelines {name}`, r); return 0 } push(render3d_st.gvk_cp_pipe, gvk_handle(out)); push(render3d_st.gvk_cp_layout, layout); push(render3d_st.gvk_cp_dsl, Vk.get_i64(dsl, 0)); push(render3d_st.gvk_cp_nbuf, n_bufs) + push(render3d_st.gvk_cp_ntex, n_tex) return len(render3d_st.gvk_cp_pipe) - 1 } # Record a dispatch of compute program c into the frame, between passes: params (n_params # bytes, std140) as binding 0 and bufs[0 .. n) as bindings 1 .. n. -function gvk_dispatch(render3d_st: mut Render3dState, c: int, params: pointer, n_params: int, bufs: words, gx: int, gy: int, gz: int) -> void { +function gvk_dispatch(render3d_st: mut Render3dState, c: int, params: pointer, n_params: int, bufs: words, gx: int, gy: int, gz: int) -> void { gvk_dispatch_tex(render3d_st, c, params, n_params, bufs, null, gx, gy, gz) } + +# ... and texs[0 .. m) as the sampled textures after the buffers. What it writes is made visible to +# the indirect draws, vertex attributes and shaders that follow (Metal's own hazard tracking was all +# that ordered the tree culling's output before this) +function gvk_dispatch_tex(render3d_st: mut Render3dState, c: int, params: pointer, n_params: int, bufs: words, texs: words, gx: int, gy: int, gz: int) -> void { let zero: long = 0 if render3d_st.gvk_cp_pipe == null or c <= 0 or c >= len(render3d_st.gvk_cp_pipe) { return } gvk_pass_end(render3d_st) let cb = gvk_frame_cb(render3d_st) let nb = render3d_st.gvk_cp_nbuf[c] + var nt = 0 + if render3d_st.gvk_cp_ntex != null and c < len(render3d_st.gvk_cp_ntex) { nt = render3d_st.gvk_cp_ntex[c] } let dsai = bytes(VkDescriptorSetAllocateInfo_sizeof) Vk.zero(dsai, VkDescriptorSetAllocateInfo_sizeof) Vk.put_i32(dsai, VkDescriptorSetAllocateInfo_sType, VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO) @@ -199,10 +211,13 @@ function gvk_dispatch(render3d_st: mut Render3dState, c: int, params: pointer, n if at < 0 { print("r3d: vulkan: the frame's uniform ring is full"); return } let ww = VkWriteDescriptorSet_sizeof let bw = VkDescriptorBufferInfo_sizeof - let writes = bytes(ww * (nb + 1)) - Vk.zero(writes, ww * (nb + 1)) + let iw = VkDescriptorImageInfo_sizeof + let writes = bytes(ww * (nb + 1 + nt)) + Vk.zero(writes, ww * (nb + 1 + nt)) let infos = bytes(bw * (nb + 1)) Vk.zero(infos, bw * (nb + 1)) + let iis = bytes(iw * (nt + 1)) + Vk.zero(iis, iw * (nt + 1)) for k in 0 .. nb + 1 { var buf = render3d_st.gvk_ring_buf var off: long = 0 @@ -220,10 +235,31 @@ function gvk_dispatch(render3d_st: mut Render3dState, c: int, params: pointer, n Vk.put_i32(writes, k * ww + VkWriteDescriptorSet_descriptorType, kind) Vk.put_ptr(writes, k * ww + VkWriteDescriptorSet_pBufferInfo, mem_off(infos, k * bw)) } - Vk.update_descriptor_sets(render3d_st.gvk_dev, nb + 1, writes, 0, null) + let smp = gvk_sampler(render3d_st, GL_LINEAR, GL_LINEAR, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE, 0, 0) + for t in 0 .. nt { + var tex = render3d_st.gvk_white + if texs != null and t < len(texs) and texs[t] > 0 { tex = texs[t] } + Vk.put_i64(iis, t * iw + VkDescriptorImageInfo_sampler, smp) + Vk.put_i64(iis, t * iw + VkDescriptorImageInfo_imageView, render3d_st.gvk_tex_view[tex]) + Vk.put_i32(iis, t * iw + VkDescriptorImageInfo_imageLayout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) + let k = nb + 1 + t + Vk.put_i32(writes, k * ww + VkWriteDescriptorSet_sType, VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET) + Vk.put_i64(writes, k * ww + VkWriteDescriptorSet_dstSet, set) + Vk.put_i32(writes, k * ww + VkWriteDescriptorSet_dstBinding, k) + Vk.put_i32(writes, k * ww + VkWriteDescriptorSet_descriptorCount, 1) + Vk.put_i32(writes, k * ww + VkWriteDescriptorSet_descriptorType, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER) + Vk.put_ptr(writes, k * ww + VkWriteDescriptorSet_pImageInfo, mem_off(iis, t * iw)) + } + Vk.update_descriptor_sets(render3d_st.gvk_dev, nb + 1 + nt, writes, 0, null) Vk.cmd_bind_pipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, render3d_st.gvk_cp_pipe[c]) Vk.cmd_bind_descriptor_sets(cb, VK_PIPELINE_BIND_POINT_COMPUTE, render3d_st.gvk_cp_layout[c], 0, 1, sets, 0, null) Vk.cmd_dispatch(cb, gx, gy, gz) + let mb = bytes(VkMemoryBarrier_sizeof) + Vk.zero(mb, VkMemoryBarrier_sizeof) + Vk.put_i32(mb, VkMemoryBarrier_sType, VK_STRUCTURE_TYPE_MEMORY_BARRIER) + Vk.put_i32(mb, VkMemoryBarrier_srcAccessMask, VK_ACCESS_SHADER_WRITE_BIT) + Vk.put_i32(mb, VkMemoryBarrier_dstAccessMask, VK_ACCESS_INDIRECT_COMMAND_READ_BIT | VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT) + Vk.cmd_pipeline_barrier(cb, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 1, mb, 0, null, 0, null) } # R3D_VK_PROBE=1: one dispatch over a GPU-owned buffer, read back - the compute path end to end @@ -650,7 +686,8 @@ function gvk_frame_init(render3d_st: mut Render3dState) -> bool { if (counts & VK_SAMPLE_COUNT_2_BIT) != 0 { render3d_st.gvk_msaa_max = 2 } if (counts & VK_SAMPLE_COUNT_4_BIT) != 0 { render3d_st.gvk_msaa_max = 4 } render3d_st.gvk_ring_buf = gvk_buf_new(render3d_st) - if not gvk_buf_reserve(render3d_st, render3d_st.gvk_ring_buf, GVK_RING_BYTES) { return false } + # one half per frame slot: the cached sets bind this one buffer, and each draw's offset says the half + if not gvk_buf_reserve(render3d_st, render3d_st.gvk_ring_buf, GVK_RING_BYTES * 2) { return false } let sizes = bytes(VkDescriptorPoolSize_sizeof * 3) Vk.put_i32(sizes, VkDescriptorPoolSize_type, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER) Vk.put_i32(sizes, VkDescriptorPoolSize_descriptorCount, 32768) @@ -665,9 +702,13 @@ function gvk_frame_init(render3d_st: mut Render3dState) -> bool { Vk.put_i32(dpci, VkDescriptorPoolCreateInfo_poolSizeCount, 3) Vk.put_ptr(dpci, VkDescriptorPoolCreateInfo_pPoolSizes, sizes) let out = bytes(8) - let r = Vk.create_descriptor_pool(render3d_st.gvk_dev, dpci, null, out) - if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateDescriptorPool", r) } - render3d_st.gvk_dpool = gvk_handle(out) + render3d_st.gvk_dpools = new []long + for k in 0 .. 2 { + let r = Vk.create_descriptor_pool(render3d_st.gvk_dev, dpci, null, out) + if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateDescriptorPool", r) } + push(render3d_st.gvk_dpools, gvk_handle(out)) + } + render3d_st.gvk_dpool = render3d_st.gvk_dpools[0] if not gvk_kpool_make(render3d_st) { return false } render3d_st.gvk_white = gvk_tex_new(render3d_st) let px = bytes(4) @@ -676,14 +717,17 @@ function gvk_frame_init(render3d_st: mut Render3dState) -> bool { } function gvk_frame_reset(render3d_st: mut Render3dState) -> void { - render3d_st.gvk_ring_off = 0 + let slot = render3d_st.gvk_frame_no & 1 + render3d_st.gvk_ring_off = slot * GVK_RING_BYTES + render3d_st.gvk_ring_end = (slot + 1) * GVK_RING_BYTES + render3d_st.gvk_dpool = render3d_st.gvk_dpools[slot] Vk.reset_descriptor_pool(render3d_st.gvk_dev, render3d_st.gvk_dpool, 0) } # a block into the ring; its offset, or -1 when the frame has used the whole ring function gvk_ring_put(render3d_st: mut Render3dState, blk: pointer, n: int) -> int { let at = (render3d_st.gvk_ring_off + render3d_st.gvk_ring_align - 1) / render3d_st.gvk_ring_align * render3d_st.gvk_ring_align - if at + n > GVK_RING_BYTES { return -1 } + if at + n > render3d_st.gvk_ring_end { return -1 } mem_copy(mem_off(render3d_st.gvk_buf_map[render3d_st.gvk_ring_buf], at), blk, n) render3d_st.gvk_ring_off = at + n return at @@ -954,7 +998,8 @@ function gvk_screen_make(render3d_st: mut Render3dState, w: int, h: int) -> bool function gvk_frame_cb(render3d_st: mut Render3dState) -> pointer { if render3d_st.gvk_cb == null { - gvk_frame_wait(render3d_st) + # the frame in flight uses the other half of the ring and the other pool, unless it has this slot + if render3d_st.gvk_frame_pending and (render3d_st.gvk_frame_pending_no & 1) == (render3d_st.gvk_frame_no & 1) { gvk_frame_wait(render3d_st) } gvk_frame_reset(render3d_st) render3d_st.gvk_cb = gvk_once_begin(render3d_st) } @@ -1270,6 +1315,7 @@ function gvk_flush(render3d_st: mut Render3dState) -> void { if render3d_st.gvk_cb == null { return } render3d_st.gvk_n_flush += 1 gvk_pass_end(render3d_st) + gvk_labels_close(render3d_st) gvk_once_end(render3d_st, render3d_st.gvk_cb) render3d_st.gvk_cb = null render3d_st.gvk_frame_no += 1 @@ -1290,6 +1336,7 @@ function gvk_present(render3d_st: mut Render3dState) -> void { } if render3d_st.gvk_cb == null { return } gvk_pass_end(render3d_st) + gvk_labels_close(render3d_st) gvk_once_end(render3d_st, render3d_st.gvk_cb) render3d_st.gvk_cb = null render3d_st.gvk_frame_no += 1 @@ -1669,6 +1716,7 @@ function gvk_swap_make(render3d_st: mut Render3dState, w: int, h: int) -> bool { function gvk_present_window(render3d_st: mut Render3dState) -> void { let cb = gvk_frame_cb(render3d_st) gvk_pass_end(render3d_st) + gvk_labels_close(render3d_st) # a covered window is not drawn to: its layer would hold the frame for a drawable the compositor # hands back once a second. The frame still runs, paced at about 60 Hz, so the game keeps time. if win_visible() == 0 { diff --git a/packages/ludic.render3d/gpu_vk_res.ludic b/packages/ludic.render3d/gpu_vk_res.ludic index 298cb72c..4d25a78a 100644 --- a/packages/ludic.render3d/gpu_vk_res.ludic +++ b/packages/ludic.render3d/gpu_vk_res.ludic @@ -560,6 +560,9 @@ function gvk_buf_release(render3d_st: mut Render3dState, b: int) -> void { if gvk_buf_busy(render3d_st, b) { # a draw recorded this frame (or the frame in flight) still reads it: destroy it once the frame has been submitted push(render3d_st.gvk_retired_buf, render3d_st.gvk_buf[b]); push(render3d_st.gvk_retired_mem, render3d_st.gvk_buf_mem[b]) + if render3d_st.gvk_retired_frame == null { render3d_st.gvk_retired_frame = new []int } + while len(render3d_st.gvk_retired_frame) < len(render3d_st.gvk_retired_buf) - 1 { push(render3d_st.gvk_retired_frame, 0) } + push(render3d_st.gvk_retired_frame, render3d_st.gvk_buf_used[b]) } else { Vk.destroy_buffer(render3d_st.gvk_dev, render3d_st.gvk_buf[b], null) gvk_mem_free(render3d_st, gvk_mem_id(render3d_st.gvk_buf_mem[b])) @@ -569,18 +572,32 @@ function gvk_buf_release(render3d_st: mut Render3dState, b: int) -> void { # read by a draw recorded this frame, or by the presented frame still on the GPU function gvk_buf_busy(render3d_st: Render3dState, b: int) -> bool { let u = render3d_st.gvk_buf_used[b] - return u == render3d_st.gvk_frame_no or (render3d_st.gvk_frame_pending and u == render3d_st.gvk_frame_no - 1) + return u == render3d_st.gvk_frame_no or (render3d_st.gvk_frame_pending and u == render3d_st.gvk_frame_pending_no) } -# the frame's submitted work is done: storage retired during it can go -function gvk_retire_flush(render3d_st: mut Render3dState) -> void { +# everything submitted is done: every retired buffer can go +function gvk_retire_flush(render3d_st: mut Render3dState) -> void { gvk_retire_upto(render3d_st, render3d_st.gvk_frame_no) } + +# frame `done` and every frame before it are finished: storage last read by them can go, and what +# the frame being recorded read stays +function gvk_retire_upto(render3d_st: mut Render3dState, done: int) -> void { if render3d_st.gvk_retired_buf == null { return } + let keep_buf = new []long + let keep_mem = new []long + let keep_frame = new []int for i in 0 .. len(render3d_st.gvk_retired_buf) { - Vk.destroy_buffer(render3d_st.gvk_dev, render3d_st.gvk_retired_buf[i], null) - gvk_mem_free(render3d_st, gvk_mem_id(render3d_st.gvk_retired_mem[i])) + var f = 0 + if render3d_st.gvk_retired_frame != null and i < len(render3d_st.gvk_retired_frame) { f = render3d_st.gvk_retired_frame[i] } + if f > done { + push(keep_buf, render3d_st.gvk_retired_buf[i]); push(keep_mem, render3d_st.gvk_retired_mem[i]); push(keep_frame, f) + } else { + Vk.destroy_buffer(render3d_st.gvk_dev, render3d_st.gvk_retired_buf[i], null) + gvk_mem_free(render3d_st, gvk_mem_id(render3d_st.gvk_retired_mem[i])) + } } - render3d_st.gvk_retired_buf = new []long - render3d_st.gvk_retired_mem = new []long + render3d_st.gvk_retired_buf = keep_buf + render3d_st.gvk_retired_mem = keep_mem + render3d_st.gvk_retired_frame = keep_frame } # A buffer a compute pass writes: a draw later in the same frame reads what the GPU put there, diff --git a/packages/ludic.render3d/grass.ludic b/packages/ludic.render3d/grass.ludic index b1e2d321..27641155 100644 --- a/packages/ludic.render3d/grass.ludic +++ b/packages/ludic.render3d/grass.ludic @@ -1,7 +1,7 @@ # ============================================================================ # grass.ludic — procedural GPU ground cover with continuous density (no rings). # -# The world is cut into 16 m cells; blade j of a cell always stands in the same place +# The world is cut into 4 m cells; blade j of a cell always stands in the same place # (shaders/grass.vert). Draws are per tile: the CPU walks tiles around the camera, # frustum-culls them, and feeds each visible tile as many blade indices per cell as its # NEAREST point could need; the vertex stage then keeps only the indices that exist at @@ -10,7 +10,7 @@ # draw count down — the cells and their hashes are the same in every tile size. # ============================================================================ -const GRASS_CELL: int = 16 +const GRASS_CELL: int = 4 # 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 @@ -89,11 +89,13 @@ function grass_blade_mesh(render3d_st: mut Render3dState, rows: int) -> Mesh { function grass_init(render3d_st: mut Render3dState) -> void { render3d_st.grass_merge = gpu_has_mdi(render3d_st) and not r3d_env_has(render3d_st, "R3D_GRASS_TILES") - var defs = "#define FOLIAGE\n#define BLADE\n" + var defs = "#define FOLIAGE\n#define BLADE\n#define GBLADE\n" if render3d_st.grass_merge { defs = defs + "#define TILES\n" render3d_st.grass_rec = words(GRASS_RECS * 5); render3d_st.grass_tv = floats(GRASS_RECS * 4); render3d_st.grass_chunk_tv = floats(GRASS_CHUNK * 4) render3d_st.grass_band_start = words(4); render3d_st.grass_band_cells = words(4) + render3d_st.grass_band_mesh = new []Mesh + for b in 0 .. 4 { push(render3d_st.grass_band_mesh, null) } render3d_st.grass_cmds = gpu_buffer_new(render3d_st) } render3d_st.grass_prog = r3d_program(render3d_st, "grass.vert", "model.frag", defs) @@ -101,6 +103,8 @@ function grass_init(render3d_st: mut Render3dState) -> void { # 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 # 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 @@ -108,12 +112,17 @@ function grass_init(render3d_st: mut Render3dState) -> void { # gets there, so the two have to agree or a shot shows something no player will see. # That is exactly how the last change measured as "no effect": the render was identical # 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 = 45.0 - render3d_st.grass_radius = 1600.0 + render3d_st.grass_d0 = 18.0 + render3d_st.grass_radius = 70.0 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) } # indices per 16 m cell that could exist at distance d (the count the shader computes) @@ -124,11 +133,13 @@ function grass_count_at(render3d_st: Render3dState, d: float) -> int { } # one tile size over one distance band -function grass_tiles(render3d_st: mut Render3dState, size: int, d_min: float, d_max: float) -> void { +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 + if d_min >= render3d_st.grass_radius { 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; render3d_st.grass_band_n += 1 + 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 + render3d_st.grass_band_mesh[render3d_st.grass_band_n] = blade; render3d_st.grass_band_n += 1 } else { u_i(render3d_st, gpu_uniform(render3d_st, p, "u_tile_cells"), cells) } @@ -159,7 +170,7 @@ function grass_tiles(render3d_st: mut Render3dState, size: int, d_min: float, d_ var inst = per * cells * cells if inst > 65535 { inst = 65535 } let r = render3d_st.grass_n * 5 - render3d_st.grass_rec[r] = render3d_st.grass_mesh.count; render3d_st.grass_rec[r + 1] = inst; render3d_st.grass_rec[r + 2] = 0; render3d_st.grass_rec[r + 3] = 0 + render3d_st.grass_rec[r] = blade.count; render3d_st.grass_rec[r + 1] = inst; render3d_st.grass_rec[r + 2] = 0; render3d_st.grass_rec[r + 3] = 0 render3d_st.grass_rec[r + 4] = ((render3d_st.grass_n - render3d_st.grass_band_start[render3d_st.grass_band_n - 1]) % GRASS_CHUNK) * 65536 let t = render3d_st.grass_n * 4 render3d_st.grass_tv[t] = ox; render3d_st.grass_tv[t + 1] = oz; render3d_st.grass_tv[t + 2] = float(per); render3d_st.grass_tv[t + 3] = 0.0 @@ -168,7 +179,7 @@ function grass_tiles(render3d_st: mut Render3dState, size: int, d_min: float, d_ } else if per > 0 { u_f2(render3d_st, gpu_uniform(render3d_st, p, "u_tile"), ox, oz) u_i(render3d_st, gpu_uniform(render3d_st, p, "u_per_cell"), per) - mesh_draw_instanced(render3d_st, render3d_st.grass_mesh, per * cells * cells) + mesh_draw_instanced(render3d_st, blade, per * cells * cells) render3d_st.grass_draws += 1 } } @@ -178,11 +189,27 @@ function grass_tiles(render3d_st: mut Render3dState, size: int, d_min: float, d_ } } +function grass_live(render3d_st: Render3dState) -> bool { return not render3d_st.grass_env_off and (render3d_st.grass_on or render3d_st.grass_force) and render3d_st.grass_prog != 0 } + function grass_draw(render3d_st: mut Render3dState) -> void { - if not render3d_st.grass_on or render3d_st.ter_reflect or render3d_st.grass_prog == 0 { return } + if not grass_live(render3d_st) or render3d_st.ter_reflect { return } + if render3d_st.gg_on { gg_draw(render3d_st); return } var p = render3d_st.grass_prog if grass_mesh_live(render3d_st) { p = render3d_st.grass_mesh_prog } gpu_use_program(render3d_st, p) + grass_bind(render3d_st, p) + gpu_cull(render3d_st, false) + render3d_st.grass_draws = 0 + render3d_st.grass_n = 0 + render3d_st.grass_band_n = 0 + gpu_mesh_bind(render3d_st, render3d_st.grass_mesh) + grass_bands(render3d_st) + if render3d_st.grass_merge { grass_flush(render3d_st) } + gpu_cull(render3d_st, true) +} + +# the blades' uniforms, shared by the per-vertex path and the culled one (grass_gpu.ludic) +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) @@ -205,6 +232,9 @@ function grass_draw(render3d_st: mut Render3dState) -> void { u_f(render3d_st, gpu_uniform(render3d_st, p, "u_s0"), render3d_st.grass_s0) u_f(render3d_st, gpu_uniform(render3d_st, p, "u_d0"), render3d_st.grass_d0) u_f(render3d_st, gpu_uniform(render3d_st, p, "u_radius"), render3d_st.grass_radius) + var ph = render3d_st.post_h + if ph <= 0 { ph = gl_height() } + u_f(render3d_st, gpu_uniform(render3d_st, p, "u_px"), 2.0 * Math.tan(render3d_st.cam_fov * 0.5) / float(ph)) u_i(render3d_st, gpu_uniform(render3d_st, p, "u_dbg"), render3d_st.grass_dbg) var orthotex = render3d_st.ter_ortho_tex var oon = 1.0 @@ -228,16 +258,16 @@ function grass_draw(render3d_st: mut Render3dState) -> void { # 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) - gpu_cull(render3d_st, false) - render3d_st.grass_draws = 0 - render3d_st.grass_n = 0 - render3d_st.grass_band_n = 0 - gpu_mesh_bind(render3d_st, render3d_st.grass_mesh) - grass_tiles(render3d_st, 16, 0.0, 300.0) - grass_tiles(render3d_st, 64, 300.0, 1200.0) - grass_tiles(render3d_st, 256, 1200.0, render3d_st.grass_radius) - if render3d_st.grass_merge { grass_flush(render3d_st) } - gpu_cull(render3d_st, true) +} + +# 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) + grass_tiles(render3d_st, 16, 40.0, render3d_st.grass_radius, render3d_st.grass_mesh2) } # the records gathered this frame: uploaded once, then each band GRASS_CHUNK records a draw @@ -251,6 +281,8 @@ function grass_flush(render3d_st: mut Render3dState) -> void { var e = render3d_st.grass_n if b + 1 < render3d_st.grass_band_n { e = render3d_st.grass_band_start[b + 1] } if e > s { u_i(render3d_st, gpu_uniform(render3d_st, p, "u_tile_cells"), render3d_st.grass_band_cells[b]) } + let blade = render3d_st.grass_band_mesh[b] + if e > s and not mesh { gpu_mesh_bind(render3d_st, blade) } var k = s while k < e { var m = e - k @@ -273,7 +305,7 @@ function grass_flush(render3d_st: mut Render3dState) -> void { } } else { u_f4v(render3d_st, gpu_uniform(render3d_st, p, "u_tiles"), m, render3d_st.grass_chunk_tv) - gpu_draw_mesh_indirect(render3d_st, render3d_st.grass_mesh, render3d_st.grass_cmds, k * 20, m, 0, 0) + gpu_draw_mesh_indirect(render3d_st, blade, render3d_st.grass_cmds, k * 20, m, 0, 0) } if not mesh { render3d_st.grass_draws += 1 } k += m diff --git a/packages/ludic.render3d/grass_gpu.ludic b/packages/ludic.render3d/grass_gpu.ludic new file mode 100644 index 00000000..845ea613 --- /dev/null +++ b/packages/ludic.render3d/grass_gpu.ludic @@ -0,0 +1,154 @@ +# ============================================================================ +# grass_gpu.ludic — the meadow's blades culled on the GPU (Vulkan), drawn from what survived. +# +# grass.vert decided every blade per VERTEX: its hashes, the ground under it, whether anything +# grows there, its colour field - ten times for a five-row blade, and in full for every index a +# tile asked for and threw away. That was 3.5 ms of a MoltenVK frame. Here the CPU still walks the +# visible tiles (as grass_tiles does), but grass_cull.comp decides each candidate once and writes +# the survivors into three bands by distance, one indirect draw each; grass_inst.vert only bends +# and places the vertices. OpenGL keeps the per-vertex path. R3D_GRASS_GPU=0 does too, to compare. +# ============================================================================ + +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 + +function gg_cap(b: int) -> int { + if b == 0 { return 16384 } + if b == 1 { return 49152 } + return 163840 +} +function gg_base(b: int) -> int { + if b == 0 { return 0 } + if b == 1 { return 16384 } + return 65536 +} + +# once, after grass_init: the programs, the buffers and the three instanced blades +function gg_init(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, 2) + 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 + for k in 0 .. 2 { push(render3d_st.gg_tiles_buf, gpu_buffer_new(render3d_st)) } + render3d_st.gg_tv = floats(GG_TILES * 4) + render3d_st.gg_out = gpu_buffer_new(render3d_st) + 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)) + 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) + } + 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) + render3d_st.gg_on = true + print("r3d: grass: blades are culled on the GPU") +} + +# 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) + gpu_mesh_bind_instances(render3d_st, m, render3d_st.gg_out) + for k in 0 .. 4 { gpu_mesh_attr_inst(render3d_st, m, 3 + k, 4, GPU_F32, 64, k * 16) } + gpu_mesh_done(render3d_st, m) + return m +} + +# 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 >= render3d_st.grass_radius { return } + let cells = size / GRASS_CELL + let sz = float(size) + let half = sz * 0.5 + let reach = d_max + half * 1.5 + let corner_r = half * 1.42 + let tx0 = int(Math.floor((render3d_st.cam_pos[0] - reach) / sz)) + let tx1 = int(Math.floor((render3d_st.cam_pos[0] + reach) / sz)) + let tz0 = int(Math.floor((render3d_st.cam_pos[2] - reach) / sz)) + let tz1 = int(Math.floor((render3d_st.cam_pos[2] + reach) / sz)) + for tz in tz0 .. tz1 + 1 { + for tx in tx0 .. tx1 + 1 { + let ox = float(tx) * sz; let oz = float(tz) * sz + let dx = ox + half - render3d_st.cam_pos[0]; let dz = oz + half - render3d_st.cam_pos[2] + let dc = Math.sqrt(dx * dx + dz * dz) + let dnear = Math.max(dc - corner_r, 0.0) + if dc < d_min or not (dnear < d_max) or render3d_st.gg_n >= GG_TILES { continue } + if not cam_sphere_visible(render3d_st, ox + half, terrain_height(render3d_st, ox + half, oz + half), oz + half, corner_r + 6.0) { continue } + let per = grass_count_at(render3d_st, dnear) + var total = per * cells * cells + if total > 65535 { total = 65535 } + let t = render3d_st.gg_n * 4 + render3d_st.gg_tv[t] = ox; render3d_st.gg_tv[t + 1] = oz; render3d_st.gg_tv[t + 2] = float(total / (cells * cells)); render3d_st.gg_tv[t + 3] = float(cells) + if total > render3d_st.gg_max { render3d_st.gg_max = total } + render3d_st.gg_n += 1 + } + } +} + +# before the frame's first pass: the tiles, then the reset and the cull +function gg_cull_frame(render3d_st: mut Render3dState) -> void { + if not render3d_st.gg_on or not grass_live(render3d_st) { return } + render3d_st.gg_n = 0 + render3d_st.gg_max = 0 + gg_tiles(render3d_st, 4, 0.0, 16.0) + gg_tiles(render3d_st, 8, 16.0, 40.0) + gg_tiles(render3d_st, 16, 40.0, render3d_st.grass_radius) + let rb = words(4) + rb[0] = GG_BANDS; rb[1] = 0; rb[2] = 0; rb[3] = 0 + let cb = words(1) + cb[0] = render3d_st.gg_cmds + gpu_dispatch(render3d_st, render3d_st.gg_reset, data_of(rb), 16, 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] + gpu_buffer_upload(render3d_st, tb, render3d_st.gg_n * 16, data_of(render3d_st.gg_tv), GPU_DYNAMIC) + let pr = gg_params(render3d_st) + let bufs = words(3) + bufs[0] = tb; bufs[1] = render3d_st.gg_out; bufs[2] = render3d_st.gg_cmds + let texs = words(2) + texs[0] = render3d_st.ter_height_tex; texs[1] = render3d_st.ter_ortho_tex + gpu_dispatch_tex(render3d_st, render3d_st.gg_cull, data_of(pr), 192, bufs, texs, (render3d_st.gg_max + 63) / 64, render3d_st.gg_n) +} + +# grass_cull.comp's Params, std140: 12 vec4s +function gg_params(render3d_st: Render3dState) -> words { + let pr = words(48) + for i in 0 .. 48 { 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(render3d_st.grass_radius) + var ph = render3d_st.post_h + if ph <= 0 { ph = gl_height() } + pr[20] = float_bits(render3d_st.grass_s0); pr[21] = float_bits(render3d_st.grass_d0); pr[22] = float_bits(2.0 * Math.tan(render3d_st.cam_fov * 0.5) / float(ph)); pr[23] = float_bits(render3d_st.ter_snow_line) + pr[24] = float_bits(render3d_st.ter_lake_cx); pr[25] = float_bits(render3d_st.ter_lake_cz); pr[26] = float_bits(render3d_st.ter_lake_ex); pr[27] = float_bits(render3d_st.ter_lake_ez) + var lake = -100000.0 + if render3d_st.ter_lake_ex != 0.0 { lake = render3d_st.ter_lake_level } + var sea = lake + if render3d_st.ter_sea_set { sea = render3d_st.ter_sea_level } + var oon = 0.0 + 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) + for b in 0 .. GG_BANDS { pr[40 + b] = gg_base(b); pr[44 + b] = gg_cap(b) } + return pr +} + +# the survivors: one indirect draw per band, from the records grass_cull.comp counted into +function gg_draw(render3d_st: mut Render3dState) -> void { + let p = render3d_st.gg_prog + gpu_use_program(render3d_st, p) + grass_bind(render3d_st, p) + u_f(render3d_st, gpu_uniform(render3d_st, p, "u_time"), render3d_st.r3d_time) + gpu_cull(render3d_st, false) + for b in 0 .. GG_BANDS { gpu_draw_mesh_indirect(render3d_st, render3d_st.gg_mesh[b], render3d_st.gg_cmds, b * 20, 1, 0, 0) } + gpu_cull(render3d_st, true) +} diff --git a/packages/ludic.render3d/prof.ludic b/packages/ludic.render3d/prof.ludic index 32bd1391..6fe9fc53 100644 --- a/packages/ludic.render3d/prof.ludic +++ b/packages/ludic.render3d/prof.ludic @@ -46,6 +46,7 @@ function prof_slot(render3d_st: mut Render3dState, name: pointer) -> int { function prof_begin(render3d_st: mut Render3dState, name: pointer) -> void { ds_set_pass(render3d_st, name) + if render3d_st.gvk_labels { gvk_label_begin(render3d_st, name) } if not render3d_st.prof_on { return } if render3d_st.prof_active >= 0 { return } # GL_TIME_ELAPSED queries cannot nest let s = prof_slot(render3d_st, name) @@ -56,6 +57,7 @@ function prof_begin(render3d_st: mut Render3dState, name: pointer) -> void { function prof_end(render3d_st: mut Render3dState) -> void { ds_set_pass(render3d_st, render3d_st.ds_outside) + if render3d_st.gvk_labels { gvk_label_end(render3d_st) } if not render3d_st.prof_on { return } if render3d_st.prof_active < 0 { return } gpu_query_end(render3d_st) diff --git a/packages/ludic.render3d/programs.ludic b/packages/ludic.render3d/programs.ludic index c3140758..ac82e165 100644 --- a/packages/ludic.render3d/programs.ludic +++ b/packages/ludic.render3d/programs.ludic @@ -51,6 +51,8 @@ function r3d_shader_src(render3d_st: mut Render3dState, name: string, defines: s if render3d_st.r3d_wind_src == null { render3d_st.r3d_wind_src = r3d_shader_file(render3d_st, "wind.glsl") } var s = "#version 410 core\n" + render3d_st.r3d_global_defs + defines + render3d_st.r3d_wind_src if is_frag { s = s + render3d_st.r3d_noise_src + render3d_st.r3d_lighting_src } + # a blade's colour field is worked out once per vertex, not once per pixel (grass.vert) + else if Text.contains(defines, "#define GBLADE") { s = s + render3d_st.r3d_noise_src } return s + r3d_shader_file(render3d_st, name) } diff --git a/packages/ludic.render3d/r3d.ludic b/packages/ludic.render3d/r3d.ludic index 135ce808..55fa9d89 100644 --- a/packages/ludic.render3d/r3d.ludic +++ b/packages/ludic.render3d/r3d.ludic @@ -31,6 +31,7 @@ import "scatter.ludic" import "actor.ludic" import "stream.ludic" import "grass.ludic" +import "grass_gpu.ludic" import "water.ludic" import "streamline.ludic" import "render.ludic" diff --git a/packages/ludic.render3d/render.ludic b/packages/ludic.render3d/render.ludic index 57c45364..fb98638f 100644 --- a/packages/ludic.render3d/render.ludic +++ b/packages/ludic.render3d/render.ludic @@ -56,6 +56,9 @@ function r3d_env_flags(render3d_st: mut Render3dState) -> void { if r3d_env_has(render3d_st, "R3D_NOGI") { render3d_st.post_gi_strength = 0.0; render3d_st.post_ao_strength = 0.0; render3d_st.post_no_gi = true } if r3d_env_has(render3d_st, "R3D_MSAA") { render3d_st.post_ms_samples = Text.to_int(r3d_env(render3d_st, "R3D_MSAA")) } render3d_st.sc_skip_blade = r3d_env_has(render3d_st, "R3D_NOBLADES") + render3d_st.sc_skip_grass = r3d_env_has(render3d_st, "R3D_NOGRASS") + render3d_st.grass_force = r3d_env_has(render3d_st, "R3D_BLADES") + render3d_st.grass_env_off = r3d_env_has(render3d_st, "R3D_NOBLADES") render3d_st.sc_skip_card = r3d_env_has(render3d_st, "R3D_NOCARDS") render3d_st.sc_dbg_lod = r3d_env_has(render3d_st, "R3D_LODDBG") if r3d_env_has(render3d_st, "R3D_ANISO") { @@ -206,6 +209,7 @@ function r3d_frame(render3d_st: mut Render3dState, time: float) -> void { prof_cpu_mark(render3d_st, "shadow rebake") stream_update_all(render3d_st) prof_cpu_mark(render3d_st, "streaming") + gg_cull_frame(render3d_st) # before any pass: a dispatch inside one would split it if not render3d_st.r3d_no_shadow { prof_begin(render3d_st, "shadow"); shadow_pass(render3d_st); prof_end(render3d_st) } prof_cpu_mark(render3d_st, "shadow pass") if render3d_st.water_on and not render3d_st.r3d_no_refl and water_reflect_visible(render3d_st) { prof_begin(render3d_st, "water reflection"); water_reflection_pass(render3d_st); prof_end(render3d_st) } diff --git a/packages/ludic.render3d/scatter.ludic b/packages/ludic.render3d/scatter.ludic index 6a4edf88..2f24fe8d 100644 --- a/packages/ludic.render3d/scatter.ludic +++ b/packages/ludic.render3d/scatter.ludic @@ -36,6 +36,7 @@ property Layer { last_cam: floats, rough: float = 0.0, blade: bool = false, + grass: bool = false, # ground cover the GPU blades replace: skipped while they draw (and under R3D_NOGRASS) flower: bool = false, card: bool = false, cheap: bool = false, # distant cover: no shadows, no wind, flat lighting @@ -1134,6 +1135,7 @@ function scatter_draw_depth(render3d_st: mut Render3dState) -> void { for i in 0 .. len(render3d_st.sc_layers) { let l = render3d_st.sc_layers[i] if not l.foliage or l.blade or l.flower { continue } + if l.grass and sc_grass_replaced(render3d_st) { continue } if render3d_st.r3d_no_trees and l.imp != null { continue } layer_update(render3d_st, l) if l.n_lods > 1 and l.g_on { @@ -1149,12 +1151,15 @@ function scatter_draw_depth(render3d_st: mut Render3dState) -> void { } gpu_cull(render3d_st, true) } +# a layer the game flagged `grass` is ground cover the GPU blades stand in for: off while they draw +function sc_grass_replaced(render3d_st: Render3dState) -> bool { return render3d_st.sc_skip_grass or ((render3d_st.grass_on or render3d_st.grass_force) and not render3d_st.grass_env_off and render3d_st.grass_prog != 0) } function scatter_draw(render3d_st: mut Render3dState) -> void { for i in 0 .. len(render3d_st.sc_layers) { let l = render3d_st.sc_layers[i] if render3d_st.sc_skip_blade and l.blade { continue } if render3d_st.sc_skip_flower and l.flower { continue } if render3d_st.sc_skip_card and l.card { continue } + if l.grass and sc_grass_replaced(render3d_st) { continue } if render3d_st.r3d_no_trees and l.imp != null and not l.card { continue } layer_update(render3d_st, l) layer_draw_near(render3d_st, l, false, null, false) @@ -1172,6 +1177,7 @@ function scatter_draw_casters(render3d_st: mut Render3dState, light_vp: floats) let l = render3d_st.sc_layers[i] if render3d_st.sc_skip_blade and l.blade { continue } if render3d_st.sc_skip_card and l.card { continue } + if l.grass and sc_grass_replaced(render3d_st) { continue } if render3d_st.r3d_no_trees and l.imp != null and not l.card { continue } layer_update(render3d_st, l) if l.imp != null { diff --git a/packages/ludic.render3d/shaders/compute.list b/packages/ludic.render3d/shaders/compute.list index 10bd7bd4..fafa1bcb 100644 --- a/packages/ludic.render3d/shaders/compute.list +++ b/packages/ludic.render3d/shaders/compute.list @@ -2,3 +2,5 @@ # spv/.comp.spv. Binding 0 is the uniform block of parameters, 1.. storage buffers. probe|probe.comp scatter_cull|scatter_cull.comp +grass_reset|grass_reset.comp +grass_cull|grass_cull.comp diff --git a/packages/ludic.render3d/shaders/grass.mesh b/packages/ludic.render3d/shaders/grass.mesh index 60f50ce0..b172254d 100644 --- a/packages/ludic.render3d/shaders/grass.mesh +++ b/packages/ludic.render3d/shaders/grass.mesh @@ -31,6 +31,7 @@ uniform vec4 u_tiles[256]; // per tile of the dispatch: corner x, corner z, in uniform float u_s0; uniform float u_d0; uniform float u_radius; +uniform float u_px; // one pixel in radians (grass.vert) uniform int u_dbg; out vec3 v_wpos[]; out vec3 v_nrm[]; @@ -40,7 +41,7 @@ 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 = 16.0; +const float CELL = 4.0; // grass.ludic GRASS_CELL: 4 m, so a near tile asks for what its blades need 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) { @@ -115,6 +116,7 @@ void main() { float tall = mix(0.18, 0.42, h3) * mix(0.8, 1.2, hash1(cid * 0.1)) * (1.0 + 0.35 * smoothstep(1.0, 12.0, grow)) * life; float bw = 0.028 * mix(1.0, 0.45 * grow, smoothstep(1.0, 4.0, grow)); if (far) { bw = max(bw, spacing * 0.35); tall = min(tall, spacing * 0.3); } + bw = max(bw, dist * u_px * 1.1); float gust = sin(xz.x * 0.09 + u_time * 1.1) * 0.5 + sin(xz.y * 0.13 - u_time * 0.8 + xz.x * 0.05) * 0.5; float ph = u_time * 1.7 + seed * 6.2831 + xz.x * 0.05 + xz.y * 0.07; float sway = (sin(ph) * 0.6 + sin(ph * 2.3 + 1.0) * 0.4 + gust) * u_wind; diff --git a/packages/ludic.render3d/shaders/grass.vert b/packages/ludic.render3d/shaders/grass.vert index 03475942..32f75c16 100644 --- a/packages/ludic.render3d/shaders/grass.vert +++ b/packages/ludic.render3d/shaders/grass.vert @@ -34,6 +34,7 @@ uniform vec4 u_tiles[256]; // per tile of the draw: corner x, corner z, indice uniform float u_s0; // blade spacing at the camera (m) 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 out vec3 v_wpos; out vec3 v_nrm; @@ -42,9 +43,10 @@ out float v_seed; out vec2 v_rot; 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 = 16.0; -float hash1(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); } +const float CELL = 4.0; // grass.ludic GRASS_CELL: 4 m, 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 // streaks across the meadow with an edge wherever they thinned out. @@ -68,7 +70,22 @@ float heightSmooth(sampler2D tex, vec2 uv) { return (texture(tex, vec2(o0.x, o0.y)).r * s0.x + texture(tex, vec2(o1.x, o0.y)).r * s1.x) * s0.y + (texture(tex, vec2(o0.x, o1.y)).r * s0.x + texture(tex, vec2(o1.x, o1.y)).r * s1.x) * s1.y; } -void cull() { gl_Position = vec4(0.0, 0.0, 2.0, 1.0); v_wpos = vec3(0.0); v_nrm = vec3(0.0, 1.0, 0.0); v_uv = vec2(0.0); v_seed = 0.0; v_rot = vec2(0.0, 1.0); v_hull = 1.0; v_quake = 0.0; } +void cull() { gl_Position = vec4(0.0, 0.0, 2.0, 1.0); v_wpos = vec3(0.0); v_nrm = vec3(0.0, 1.0, 0.0); v_uv = vec2(0.0); v_seed = 0.0; v_rot = vec2(0.0, 1.0); v_hull = 1.0; v_quake = 0.0; v_tint = vec3(1.0); } +// lighting.glsl's regionTint(wpos, 0.35) times model.frag's patchiness, at the root: three fbm +// fields a blade pixel used to pay for, when the whole blade stands on one spot +vec3 bladeField(vec2 xz, float y) { + float n = fbm(xz * 0.0018 + 4.0, 3) * 0.5 + 0.5; + float aspen = smoothstep(0.52, 0.7, n) * smoothstep(520.0, 250.0, y); + float dry = smoothstep(0.35, 0.15, fbm(xz * 0.004 + 9.0, 3) * 0.5 + 0.5); + vec3 t = mix(vec3(1.0), vec3(1.25, 1.3, 0.85), aspen * 0.35); + t = mix(t, vec3(1.15, 1.05, 0.7), dry * 0.35 * 0.6); + float patchy = fbm(xz * 0.045, 3) * 0.5 + 0.5; + t *= mix(vec3(0.7, 0.8, 0.55), vec3(1.1, 1.05, 0.85), patchy); + // dry and green patches a few metres across, and the tussocks' own shade of the same green + float dryp = smoothstep(0.2, 0.8, gnoise(xz * 0.33 + 17.0) * 0.5 + 0.5); + t *= mix(vec3(0.92, 1.0, 0.95), vec3(1.30, 1.10, 0.62), dryp); + return t * (0.72 + 0.56 * (gnoise(xz * 1.9 + 41.0) * 0.5 + 0.5)); +} void main() { #ifdef TILES @@ -158,6 +175,10 @@ void main() { // 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; + // 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)); // 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 @@ -165,6 +186,9 @@ void main() { // and too flat" note in this stage. float bw = 0.010 * mix(1.0, 0.45 * 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 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 @@ -217,6 +241,7 @@ void main() { v_seed = seed; v_rot = vec2(s, c_); v_quake = 0.0; + v_tint = bladeField(xz, h); v_hull = (dist > 2.0 || far || (u_dbg & 2) != 0) ? -1.0 : 1.0; // no back-face flip, no rounding past arm's reach gl_Position = u_proj * u_view * vec4(w, 1.0); } diff --git a/packages/ludic.render3d/shaders/grass_cull.comp b/packages/ludic.render3d/shaders/grass_cull.comp new file mode 100644 index 00000000..4f4567ad --- /dev/null +++ b/packages/ludic.render3d/shaders/grass_cull.comp @@ -0,0 +1,123 @@ +// grass_cull.comp - the meadow's blades, decided once a frame on the GPU (Vulkan). +// +// grass.vert used to do all of this per VERTEX: every blade's hashes, its place, the ground under +// it (a four-tap bicubic height), the photograph's say on whether anything grows there, the water, +// the slope, the snow line and its colour field - ten times over for a five-row blade, and in full +// for every index a tile asked for and then threw away. Here each candidate is one invocation: a +// workgroup row per visible tile (grass.ludic walks them as before), a thread per candidate blade. +// A blade that survives is written once, as four vec4s, into its band's region, and the band's +// indirect draw count goes up by one; grass_inst.vert only bends and places the vertices. +// The rules are grass.vert's, kept in step with it: the same hashes give the same meadow. +layout(local_size_x = 64) in; + +layout(set = 0, binding = 0) uniform Params { + vec4 planes[4]; // the side frustum planes: xyz in, w distance + vec4 cam; // xyz the camera, w no blades past this (u_radius) + vec4 dens; // x s0, y d0, z one pixel in radians, w the snow line + vec4 lake; // the carved lake: centre x/z, half extents (z = 0: none) + vec4 lev; // x the lake's level, y the sea's, z the photograph on (1), w unused + vec4 ts; // the height texture: origin x/z, half extent, unused + vec4 bands; // outer distance of bands 0 and 1 (x, y); band 2 is the rest + uvec4 base; // each band's first record in Out + uvec4 cap; // ... and how many it holds +} 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; // height, normal (terrain's u_ts_height) +layout(set = 0, binding = 5) uniform sampler2D u_ortho; // the photograph + +const float CELL = 4.0; // grass.ludic GRASS_CELL + +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) { + uint h = pcg(uint(cell.x + 32768) * 73856093u ^ uint(cell.y + 32768) * 19349663u ^ uint(j) * 83492791u ^ uint(k) * 2654435761u); + return float(h) * (1.0 / 4294967295.0); +} +float heightSmooth(vec2 uv) { + vec2 res = vec2(textureSize(u_height, 0)); + vec2 t = uv * res - 0.5; + vec2 f = fract(t); + vec2 i = floor(t); + vec2 w0 = (1.0 - f) * (1.0 - f) * (1.0 - f) / 6.0; + vec2 w1 = (4.0 - 6.0 * f * f + 3.0 * f * f * f) / 6.0; + vec2 w3 = f * f * f / 6.0; + vec2 w2 = 1.0 - w0 - w1 - w3; + vec2 s0 = w0 + w1, s1 = w2 + w3; + vec2 o0 = (i - 1.0 + w1 / s0 + 0.5) / res; + vec2 o1 = (i + 1.0 + w3 / s1 + 0.5) / res; + return (textureLod(u_height, vec2(o0.x, o0.y), 0.0).r * s0.x + textureLod(u_height, vec2(o1.x, o0.y), 0.0).r * s1.x) * s0.y + + (textureLod(u_height, vec2(o0.x, o1.y), 0.0).r * s0.x + textureLod(u_height, vec2(o1.x, o1.y), 0.0).r * s1.x) * s1.y; +} +// 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; + float aspen = smoothstep(0.52, 0.7, n) * smoothstep(520.0, 250.0, y); + float dry = smoothstep(0.35, 0.15, fbm(xz * 0.004 + 9.0, 3) * 0.5 + 0.5); + vec3 t = mix(vec3(1.0), vec3(1.25, 1.3, 0.85), aspen * 0.35); + t = mix(t, vec3(1.15, 1.05, 0.7), dry * 0.35 * 0.6); + float patchy = fbm(xz * 0.045, 3) * 0.5 + 0.5; + t *= mix(vec3(0.7, 0.8, 0.55), vec3(1.1, 1.05, 0.85), patchy); + float dryp = smoothstep(0.2, 0.8, gnoise(xz * 0.33 + 17.0) * 0.5 + 0.5); + t *= mix(vec3(0.92, 1.0, 0.95), vec3(1.30, 1.10, 0.62), dryp); + return t * (0.72 + 0.56 * (gnoise(xz * 1.9 + 41.0) * 0.5 + 0.5)); +} + +void main() { + vec4 tile = tiles[gl_WorkGroupID.y]; + int per_cell = int(tile.z + 0.5); + int cells = int(tile.w + 0.5); + int i = int(gl_GlobalInvocationID.x); + if (per_cell <= 0 || i >= per_cell * cells * cells) return; + int c = i / per_cell; + int j = i - c * per_cell; + vec2 cell = tile.xy + vec2(float(c % cells), float(c / cells)) * CELL; + vec2 cid = floor(cell / CELL + 0.5); + ivec2 ci = ivec2(cid); + vec2 hv = vec2(bladeHash(ci, j, 0), bladeHash(ci, j, 1)); + vec2 xz = cell + hv * CELL; + float dist = length(xz - pr.cam.xz); + float radius = pr.cam.w; + if (dist >= radius) return; + float spacing = pr.dens.x * (1.0 + dist / pr.dens.y); + float count = CELL * CELL / (spacing * spacing) * (1.0 - smoothstep(radius * 0.7, radius, dist)); + float keep = count / float(max(per_cell, 1)); + float r = bladeHash(ci, j, 5); + if (r > keep) return; + float life = 1.0 - smoothstep(keep * 0.75, keep, r); + vec2 huv = (xz - pr.ts.xy) / (2.0 * pr.ts.z) + 0.5; + if (huv.x < 0.0 || huv.x > 1.0 || huv.y < 0.0 || huv.y > 1.0) return; + vec4 ht = textureLod(u_height, huv, 0.0); + // the frustum, on a sphere round the blade (it stands at most 0.9 m tall) + vec3 root = vec3(xz.x, ht.r, xz.y); + for (int k = 0; k < 4; k++) { if (dot(pr.planes[k].xyz, root) + pr.planes[k].w < -1.0) return; } + vec3 gn = normalize(ht.gba); + 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); + if (pr.lev.z > 0.5) { + vec3 oc = textureLod(u_ortho, huv, 1.5).rgb; + ok *= 0.40 + 0.60 * smoothstep(0.0, 0.025, oc.g - oc.b); + } + if (h4 > ok) return; + float h = heightSmooth(huv); + float seed = hv.x * 0.7 + hv.y * 0.3; + 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)); + 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); + if (slot >= pr.cap[b]) { atomicAdd(cmds[b * 5u + 1u], 0xFFFFFFFFu); return; } + 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 + 3u] = vec4(gn, dist); +} diff --git a/packages/ludic.render3d/shaders/grass_inst.vert b/packages/ludic.render3d/shaders/grass_inst.vert new file mode 100644 index 00000000..c501ed90 --- /dev/null +++ b/packages/ludic.render3d/shaders/grass_inst.vert @@ -0,0 +1,71 @@ +// grass_inst.vert - a blade grass_cull.comp has already decided on (Vulkan). Everything about the +// blade - where it stands, its facing, height, width, colour field and the ground under it - is its +// instance record; this only bends, sways, pushes and stands the vertices, as grass.vert's second +// half does, so the two draw the same meadow. +layout(location = 0) in vec3 a_pos; // x: -0.5..0.5 across, y: 0..1 along the blade, z: bend +layout(location = 2) in vec2 a_uv; +layout(location = 3) in vec4 i_root; // xyz the root, w the seed +layout(location = 4) in vec4 i_shape; // sin and cos of the yaw, height, width +layout(location = 5) in vec4 i_tint; // the colour field, and how far the tip arches +layout(location = 6) in vec4 i_ground; // the ground's normal, and the distance it was decided at +uniform mat4 u_view; +uniform mat4 u_proj; +uniform float u_time; +uniform float u_wind; +uniform vec3 u_push; // x, z, radius: a body standing in the grass +uniform int u_dbg; // R3D_GRASS_DBG (2: light as ground everywhere) +out vec3 v_wpos; +out vec3 v_nrm; +out vec2 v_uv; +out float v_seed; +out vec2 v_rot; +out float v_hull; +out float v_quake; +out vec3 v_tint; + +void main() { + vec2 xz = i_root.xz; + float seed = i_root.w; + float s = i_shape.x, c_ = i_shape.y, tall = i_shape.z, bw = i_shape.w; + vec3 gn = i_ground.xyz; + float dist = i_ground.w; + vec3 p = vec3(a_pos.x * bw, a_pos.y * tall, a_pos.z * tall * i_tint.w); + vec3 n = vec3(0.0, 0.3, 1.0); + float ph = u_time * 1.7 + seed * 6.2831 + xz.x * 0.05 + xz.y * 0.07; + float sway = windSway(xz, u_time, ph, 0.0) * u_wind; + float hgt = max(p.y, 0.0); + p.x += sway * hgt * hgt * 0.35; + p.z += sway * hgt * hgt * 0.15 * cos(ph * 0.7); + p = vec3(c_ * p.x + s * p.z, p.y, -s * p.x + c_ * p.z); + if (u_push.z > 0.0) { + vec2 away = xz - u_push.xy; + float pd = length(away); + float push = smoothstep(u_push.z, u_push.z * 0.2, pd); + if (push > 0.0) { + vec2 pdir = (pd > 1e-3) ? away / pd : vec2(1.0, 0.0); + float bh = max(p.y, 0.0); + p.x += pdir.x * push * bh * 1.05; + p.z += pdir.y * push * bh * 1.05; + p.y -= push * bh * 0.40; + } + } + n = normalize(vec3(c_ * n.x + s * n.z, n.y, -s * n.x + c_ * n.z)); + vec3 k = cross(vec3(0.0, 1.0, 0.0), gn); + float sk = length(k), ck = gn.y; + if (sk > 1e-4) { + k /= sk; + p = p * ck + cross(k, p) * sk + k * dot(k, p) * (1.0 - ck); + n = normalize(n * ck + cross(k, n) * sk + k * dot(k, n) * (1.0 - ck)); + } + n = normalize(mix(n, gn, smoothstep(2.0, 12.0, dist))); + vec3 w = i_root.xyz + p; + v_wpos = w; + v_nrm = n; + v_uv = a_uv; + v_seed = seed; + v_rot = vec2(s, c_); + v_quake = 0.0; + v_tint = i_tint.rgb; + v_hull = (dist > 2.0 || (u_dbg & 2) != 0) ? -1.0 : 1.0; + gl_Position = u_proj * u_view * vec4(w, 1.0); +} diff --git a/packages/ludic.render3d/shaders/grass_reset.comp b/packages/ludic.render3d/shaders/grass_reset.comp new file mode 100644 index 00000000..f810b0bc --- /dev/null +++ b/packages/ludic.render3d/shaders/grass_reset.comp @@ -0,0 +1,8 @@ +// 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. +layout(local_size_x = 1) in; +layout(set = 0, binding = 0) uniform Params { uvec4 n; } 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; } +} diff --git a/packages/ludic.render3d/shaders/model.frag b/packages/ludic.render3d/shaders/model.frag index 41257a24..d6356ab3 100644 --- a/packages/ludic.render3d/shaders/model.frag +++ b/packages/ludic.render3d/shaders/model.frag @@ -15,6 +15,9 @@ uniform mat4 u_view; uniform vec3 u_tint; uniform float u_rough_scale; uniform float u_emissive; // self-lit (a flame): albedo added back after shading +#ifdef GBLADE +in vec3 v_tint; // grass.vert's colour field (the mesh-shader and scatter blades work it out here) +#endif #ifdef BLADE uniform vec3 u_blade_base; uniform vec3 u_blade_tip; @@ -49,6 +52,11 @@ void main() { #endif vec3 N = normalize(v_nrm); if (!gl_FrontFacing && v_hull >= 0.0) N = -N; +#ifdef BLADE + // A blade is thin and two-sided: the sun lights whichever face it reaches (the other glows + // through, below). Faced away from the sun, half the meadow was lit by the sky alone. + if (dot(N, u_sun_dir) < 0.0) N = -N; +#endif #ifdef CARD // a baked card: albedo with coverage (premultiplied mips), normal in the card's own frame vec4 ca = texture(u_diff, v_uv); @@ -80,7 +88,16 @@ void main() { #ifdef BLADE // a procedural blade: dark at the root, lighter at the tip; some blades gone to seed float t = clamp(v_uv.y, 0.0, 1.0); - alb = mix(u_blade_base, u_blade_tip, t * t) * regionTint(v_wpos, 0.35); + // most of a blade is its own green, only the sheath darker: t*t held the lower two thirds at the + // root colour (0.14, darker than the ground), and a meadow of those reads as dark wire +#ifndef GBLADE + vec3 field = regionTint(v_wpos, 0.35); + float patchy = fbm(v_wpos.xz * 0.045, 3) * 0.5 + 0.5; + field *= mix(vec3(0.7, 0.8, 0.55), vec3(1.1, 1.05, 0.85), patchy); +#else + 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); // A REAL BLADE, if the game gave us one. Each blade picks a column of the atlas from @@ -110,13 +127,13 @@ void main() { vec3 g = photo * (1.0 / 0.3736); alb *= mix(vec3(1.0), min(g, vec3(1.15)), 0.85); } - float patchy = fbm(v_wpos.xz * 0.045, 3) * 0.5 + 0.5; - alb *= mix(vec3(0.7, 0.8, 0.55), vec3(1.1, 1.05, 0.85), patchy); // 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) * regionTint(v_wpos, 0.35) * mix(vec3(0.7, 0.8, 0.55), vec3(1.1, 1.05, 0.85), patchy) * 0.72; + if (v_hull < 0.0) alb = mix(u_blade_base, u_blade_tip, 0.45) * field * 0.72; // 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); + // and a sward is lit as a mass leaning to the sky: a level normal took the light at a graze + n = normalize(n + vec3(0.0, 0.45, 0.0)); // A blade's own ambient occlusion. This was mix(0.2, 1.0, t*t): 80% occluded at the // sheath and still 60% at half height, because t*t holds the curve down. Measured in a // walking-distance shot the blades came out at 24-31 of 255 against a ground of 143 - @@ -227,6 +244,13 @@ void main() { #else float shadow = mix(1.0, sunShadow(v_wpos + N * 0.1 + vec3(0.0, 0.2, 0.0), N, viewDepth), 0.55); #endif +#elif defined(BLADE) + // A blade stands within a shadow texel of the ground under it, and biased along its own + // (level) normal it read the terrain as its caster: the whole meadow in shadow, dark wire. + // Looked up a hand's width up, along the ground's up, trees and rocks still shade it. + // past arm's length a blade is a pixel or two wide: one tap is what a soft filter averages to + float shadow = (dist < 8.0) ? sunShadow(v_wpos + vec3(0.0, 0.25, 0.0), vec3(0.0, 1.0, 0.0), viewDepth) + : sunShadowCheap(v_wpos + vec3(0.0, 0.25, 0.0), vec3(0.0, 1.0, 0.0), viewDepth); #else float shadow = sunShadow(v_wpos, N, viewDepth); #endif @@ -271,6 +295,9 @@ void main() { float NoV = max(dot(n, vv), 0.0); float sheen = pow(NoH, 26.0) * 0.55 + pow(1.0 - NoV, 4.0) * 0.05; sheen *= smoothstep(0.10, 0.42, NoV); +#ifdef BLADE + sheen *= 0.2; // a sward seen from above is tips edge-on to the sun: the leaf's glint frosted them white +#endif col += u_sun_color * sheen * shadow * cloudShadow(v_wpos) * 0.4 * (0.45 + 0.55 * arm.r); } #endif diff --git a/packages/ludic.render3d/shaders/spv/7ed7de52.frag.spv b/packages/ludic.render3d/shaders/spv/7ed7de52.frag.spv deleted file mode 100644 index b54d682d..00000000 Binary files a/packages/ludic.render3d/shaders/spv/7ed7de52.frag.spv and /dev/null differ diff --git a/packages/ludic.render3d/shaders/spv/7ed7de52.vert.spv b/packages/ludic.render3d/shaders/spv/7ed7de52.vert.spv deleted file mode 100644 index 5fd3b45c..00000000 Binary files a/packages/ludic.render3d/shaders/spv/7ed7de52.vert.spv and /dev/null differ diff --git a/packages/ludic.render3d/shaders/spv/7f096322.frag.spv b/packages/ludic.render3d/shaders/spv/7f096322.frag.spv deleted file mode 100644 index b54d682d..00000000 Binary files a/packages/ludic.render3d/shaders/spv/7f096322.frag.spv and /dev/null differ diff --git a/packages/ludic.render3d/shaders/spv/7f096322.vert.spv b/packages/ludic.render3d/shaders/spv/7f096322.vert.spv deleted file mode 100644 index eb02b0d0..00000000 Binary files a/packages/ludic.render3d/shaders/spv/7f096322.vert.spv and /dev/null differ diff --git a/packages/ludic.render3d/shaders/spv/b20fdcb5.frag.spv b/packages/ludic.render3d/shaders/spv/b20fdcb5.frag.spv new file mode 100644 index 00000000..35eaa795 Binary files /dev/null and b/packages/ludic.render3d/shaders/spv/b20fdcb5.frag.spv differ diff --git a/packages/ludic.render3d/shaders/spv/b20fdcb5.vert.spv b/packages/ludic.render3d/shaders/spv/b20fdcb5.vert.spv new file mode 100644 index 00000000..3744bcf9 Binary files /dev/null and b/packages/ludic.render3d/shaders/spv/b20fdcb5.vert.spv differ diff --git a/packages/ludic.render3d/shaders/spv/b8cff226.frag.spv b/packages/ludic.render3d/shaders/spv/b8cff226.frag.spv new file mode 100644 index 00000000..a7862f95 Binary files /dev/null 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 new file mode 100644 index 00000000..12eafb74 Binary files /dev/null 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 new file mode 100644 index 00000000..a7862f95 Binary files /dev/null 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 new file mode 100644 index 00000000..4858c099 Binary files /dev/null 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 b54d682d..39de29f8 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 7b7b8aa1..0749c351 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 071188aa..21e807a8 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 new file mode 100644 index 00000000..4b9c9260 Binary files /dev/null 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 new file mode 100644 index 00000000..0d6a3302 Binary files /dev/null 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 c344f908..78000865 100644 --- a/packages/ludic.render3d/shaders/spv/manifest.txt +++ b/packages/ludic.render3d/shaders/spv/manifest.txt @@ -843,7 +843,7 @@ T 74258189 u_shadow 6 T 74258189 u_tershadow 7 T 74258189 u_ts_height 8 P d48d6a48 grass.mesh model.frag #define FOLIAGE;#define BLADE;#define MESH; -B d48d6a48 vert 0 4384 +B d48d6a48 vert 0 4388 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 @@ -862,7 +862,8 @@ U d48d6a48 vert u_tiles 272 vec4 256 16 U d48d6a48 vert u_s0 4368 float 1 0 U d48d6a48 vert u_d0 4372 float 1 0 U d48d6a48 vert u_radius 4376 float 1 0 -U d48d6a48 vert u_dbg 4380 int 1 0 +U d48d6a48 vert u_px 4380 float 1 0 +U d48d6a48 vert u_dbg 4384 int 1 0 B d48d6a48 frag 1 964 U d48d6a48 frag u_cascade_vp 0 mat4 5 64 U d48d6a48 frag u_cascade_split 320 float 5 16 @@ -919,169 +920,171 @@ T d48d6a48 u_prefilter 9 T d48d6a48 u_shadow 10 T d48d6a48 u_tershadow 11 T d48d6a48 u_ts_height 12 -P 7ed7de52 grass.vert model.frag #define FOLIAGE;#define BLADE; -B 7ed7de52 vert 0 320 -U 7ed7de52 vert u_view 0 mat4 1 0 -U 7ed7de52 vert u_proj 64 mat4 1 0 -U 7ed7de52 vert u_vp 128 mat4 1 0 -U 7ed7de52 vert u_cam_pos 192 vec3 1 0 -U 7ed7de52 vert u_time 204 float 1 0 -U 7ed7de52 vert u_ts_origin 208 vec2 1 0 -U 7ed7de52 vert u_ts_half 216 float 1 0 -U 7ed7de52 vert u_ortho_on 220 float 1 0 -U 7ed7de52 vert u_lake_level 224 float 1 0 -U 7ed7de52 vert u_sea_level 228 float 1 0 -U 7ed7de52 vert u_lake 240 vec4 1 0 -U 7ed7de52 vert u_snow_line 256 float 1 0 -U 7ed7de52 vert u_wind 260 float 1 0 -U 7ed7de52 vert u_push 272 vec3 1 0 -U 7ed7de52 vert u_tile 288 vec2 1 0 -U 7ed7de52 vert u_tile_cells 296 int 1 0 -U 7ed7de52 vert u_per_cell 300 int 1 0 -U 7ed7de52 vert u_s0 304 float 1 0 -U 7ed7de52 vert u_d0 308 float 1 0 -U 7ed7de52 vert u_radius 312 float 1 0 -U 7ed7de52 vert u_dbg 316 int 1 0 -I 7ed7de52 a_pos 0 vec3 -I 7ed7de52 a_uv 2 vec2 -B 7ed7de52 frag 1 964 -U 7ed7de52 frag u_cascade_vp 0 mat4 5 64 -U 7ed7de52 frag u_cascade_split 320 float 5 16 -U 7ed7de52 frag u_cascade_range 400 float 5 16 -U 7ed7de52 frag u_cascade_texel 480 float 5 16 -U 7ed7de52 frag u_sun_dir 560 vec3 1 0 -U 7ed7de52 frag u_sun_color 576 vec3 1 0 -U 7ed7de52 frag u_cam_pos 592 vec3 1 0 -U 7ed7de52 frag u_prefilter_levels 604 float 1 0 -U 7ed7de52 frag u_fog_density 608 float 1 0 -U 7ed7de52 frag u_fog_height_falloff 612 float 1 0 -U 7ed7de52 frag u_fog_base 616 float 1 0 -U 7ed7de52 frag u_clip_y 620 float 1 0 -U 7ed7de52 frag u_spec_scale 624 float 1 0 -U 7ed7de52 frag u_sky_rot 632 vec2 1 0 -U 7ed7de52 frag u_ground_alb 640 vec3 1 0 -U 7ed7de52 frag u_ground_alb_hi 656 vec3 1 0 -U 7ed7de52 frag u_ground_hi_y 668 float 1 0 -U 7ed7de52 frag u_ground_hi_w 672 float 1 0 -U 7ed7de52 frag u_ibl_scale 688 vec3 1 0 -U 7ed7de52 frag u_daylight 700 float 1 0 -U 7ed7de52 frag u_fire_pos 704 vec3 1 0 -U 7ed7de52 frag u_fire_color 720 vec3 1 0 -U 7ed7de52 frag u_hand_pos 736 vec3 1 0 -U 7ed7de52 frag u_hand_color 752 vec3 1 0 -U 7ed7de52 frag u_hand_dir 768 vec3 1 0 -U 7ed7de52 frag u_hand_cone 780 float 1 0 -U 7ed7de52 frag u_hand_reach 784 float 1 0 -U 7ed7de52 frag u_ts_origin 792 vec2 1 0 -U 7ed7de52 frag u_ts_half 800 float 1 0 -U 7ed7de52 frag u_ts_on 804 float 1 0 -U 7ed7de52 frag u_force_cascade 808 int 1 0 -U 7ed7de52 frag u_cloud_shadow 812 float 1 0 -U 7ed7de52 frag u_time 816 float 1 0 -U 7ed7de52 frag u_fog_inscatter 820 float 1 0 -U 7ed7de52 frag u_fog_desat 824 float 1 0 -U 7ed7de52 frag u_model_h 828 float 1 0 -U 7ed7de52 frag u_view 832 mat4 1 0 -U 7ed7de52 frag u_tint 896 vec3 1 0 -U 7ed7de52 frag u_rough_scale 908 float 1 0 -U 7ed7de52 frag u_emissive 912 float 1 0 -U 7ed7de52 frag u_blade_base 928 vec3 1 0 -U 7ed7de52 frag u_blade_tip 944 vec3 1 0 -U 7ed7de52 frag u_blade_cols 956 float 1 0 -U 7ed7de52 frag u_blade_tex_on 960 float 1 0 -T 7ed7de52 u_arm 2 -T 7ed7de52 u_blade_tex 3 -T 7ed7de52 u_brdf 4 -T 7ed7de52 u_diff 5 -T 7ed7de52 u_irradiance 6 -T 7ed7de52 u_nrm 7 -T 7ed7de52 u_ortho 8 -T 7ed7de52 u_prefilter 9 -T 7ed7de52 u_shadow 10 -T 7ed7de52 u_tershadow 11 -T 7ed7de52 u_ts_height 12 -P 7f096322 grass.vert model.frag #define FOLIAGE;#define BLADE;#define TILES; -B 7f096322 vert 0 4416 -U 7f096322 vert u_view 0 mat4 1 0 -U 7f096322 vert u_proj 64 mat4 1 0 -U 7f096322 vert u_vp 128 mat4 1 0 -U 7f096322 vert u_cam_pos 192 vec3 1 0 -U 7f096322 vert u_time 204 float 1 0 -U 7f096322 vert u_ts_origin 208 vec2 1 0 -U 7f096322 vert u_ts_half 216 float 1 0 -U 7f096322 vert u_ortho_on 220 float 1 0 -U 7f096322 vert u_lake_level 224 float 1 0 -U 7f096322 vert u_sea_level 228 float 1 0 -U 7f096322 vert u_lake 240 vec4 1 0 -U 7f096322 vert u_snow_line 256 float 1 0 -U 7f096322 vert u_wind 260 float 1 0 -U 7f096322 vert u_push 272 vec3 1 0 -U 7f096322 vert u_tile 288 vec2 1 0 -U 7f096322 vert u_tile_cells 296 int 1 0 -U 7f096322 vert u_per_cell 300 int 1 0 -U 7f096322 vert u_tiles 304 vec4 256 16 -U 7f096322 vert u_s0 4400 float 1 0 -U 7f096322 vert u_d0 4404 float 1 0 -U 7f096322 vert u_radius 4408 float 1 0 -U 7f096322 vert u_dbg 4412 int 1 0 -I 7f096322 a_pos 0 vec3 -I 7f096322 a_uv 2 vec2 -B 7f096322 frag 1 964 -U 7f096322 frag u_cascade_vp 0 mat4 5 64 -U 7f096322 frag u_cascade_split 320 float 5 16 -U 7f096322 frag u_cascade_range 400 float 5 16 -U 7f096322 frag u_cascade_texel 480 float 5 16 -U 7f096322 frag u_sun_dir 560 vec3 1 0 -U 7f096322 frag u_sun_color 576 vec3 1 0 -U 7f096322 frag u_cam_pos 592 vec3 1 0 -U 7f096322 frag u_prefilter_levels 604 float 1 0 -U 7f096322 frag u_fog_density 608 float 1 0 -U 7f096322 frag u_fog_height_falloff 612 float 1 0 -U 7f096322 frag u_fog_base 616 float 1 0 -U 7f096322 frag u_clip_y 620 float 1 0 -U 7f096322 frag u_spec_scale 624 float 1 0 -U 7f096322 frag u_sky_rot 632 vec2 1 0 -U 7f096322 frag u_ground_alb 640 vec3 1 0 -U 7f096322 frag u_ground_alb_hi 656 vec3 1 0 -U 7f096322 frag u_ground_hi_y 668 float 1 0 -U 7f096322 frag u_ground_hi_w 672 float 1 0 -U 7f096322 frag u_ibl_scale 688 vec3 1 0 -U 7f096322 frag u_daylight 700 float 1 0 -U 7f096322 frag u_fire_pos 704 vec3 1 0 -U 7f096322 frag u_fire_color 720 vec3 1 0 -U 7f096322 frag u_hand_pos 736 vec3 1 0 -U 7f096322 frag u_hand_color 752 vec3 1 0 -U 7f096322 frag u_hand_dir 768 vec3 1 0 -U 7f096322 frag u_hand_cone 780 float 1 0 -U 7f096322 frag u_hand_reach 784 float 1 0 -U 7f096322 frag u_ts_origin 792 vec2 1 0 -U 7f096322 frag u_ts_half 800 float 1 0 -U 7f096322 frag u_ts_on 804 float 1 0 -U 7f096322 frag u_force_cascade 808 int 1 0 -U 7f096322 frag u_cloud_shadow 812 float 1 0 -U 7f096322 frag u_time 816 float 1 0 -U 7f096322 frag u_fog_inscatter 820 float 1 0 -U 7f096322 frag u_fog_desat 824 float 1 0 -U 7f096322 frag u_model_h 828 float 1 0 -U 7f096322 frag u_view 832 mat4 1 0 -U 7f096322 frag u_tint 896 vec3 1 0 -U 7f096322 frag u_rough_scale 908 float 1 0 -U 7f096322 frag u_emissive 912 float 1 0 -U 7f096322 frag u_blade_base 928 vec3 1 0 -U 7f096322 frag u_blade_tip 944 vec3 1 0 -U 7f096322 frag u_blade_cols 956 float 1 0 -U 7f096322 frag u_blade_tex_on 960 float 1 0 -T 7f096322 u_arm 2 -T 7f096322 u_blade_tex 3 -T 7f096322 u_brdf 4 -T 7f096322 u_diff 5 -T 7f096322 u_irradiance 6 -T 7f096322 u_nrm 7 -T 7f096322 u_ortho 8 -T 7f096322 u_prefilter 9 -T 7f096322 u_shadow 10 -T 7f096322 u_tershadow 11 -T 7f096322 u_ts_height 12 +P cb13f3c6 grass.vert model.frag #define FOLIAGE;#define BLADE;#define GBLADE; +B cb13f3c6 vert 0 324 +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 +U cb13f3c6 vert u_cam_pos 192 vec3 1 0 +U cb13f3c6 vert u_time 204 float 1 0 +U cb13f3c6 vert u_ts_origin 208 vec2 1 0 +U cb13f3c6 vert u_ts_half 216 float 1 0 +U cb13f3c6 vert u_ortho_on 220 float 1 0 +U cb13f3c6 vert u_lake_level 224 float 1 0 +U cb13f3c6 vert u_sea_level 228 float 1 0 +U cb13f3c6 vert u_lake 240 vec4 1 0 +U cb13f3c6 vert u_snow_line 256 float 1 0 +U cb13f3c6 vert u_wind 260 float 1 0 +U cb13f3c6 vert u_push 272 vec3 1 0 +U cb13f3c6 vert u_tile 288 vec2 1 0 +U cb13f3c6 vert u_tile_cells 296 int 1 0 +U cb13f3c6 vert u_per_cell 300 int 1 0 +U cb13f3c6 vert u_s0 304 float 1 0 +U cb13f3c6 vert u_d0 308 float 1 0 +U cb13f3c6 vert u_radius 312 float 1 0 +U cb13f3c6 vert u_px 316 float 1 0 +U cb13f3c6 vert u_dbg 320 int 1 0 +I cb13f3c6 a_pos 0 vec3 +I cb13f3c6 a_uv 2 vec2 +B cb13f3c6 frag 1 964 +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 +U cb13f3c6 frag u_cascade_texel 480 float 5 16 +U cb13f3c6 frag u_sun_dir 560 vec3 1 0 +U cb13f3c6 frag u_sun_color 576 vec3 1 0 +U cb13f3c6 frag u_cam_pos 592 vec3 1 0 +U cb13f3c6 frag u_prefilter_levels 604 float 1 0 +U cb13f3c6 frag u_fog_density 608 float 1 0 +U cb13f3c6 frag u_fog_height_falloff 612 float 1 0 +U cb13f3c6 frag u_fog_base 616 float 1 0 +U cb13f3c6 frag u_clip_y 620 float 1 0 +U cb13f3c6 frag u_spec_scale 624 float 1 0 +U cb13f3c6 frag u_sky_rot 632 vec2 1 0 +U cb13f3c6 frag u_ground_alb 640 vec3 1 0 +U cb13f3c6 frag u_ground_alb_hi 656 vec3 1 0 +U cb13f3c6 frag u_ground_hi_y 668 float 1 0 +U cb13f3c6 frag u_ground_hi_w 672 float 1 0 +U cb13f3c6 frag u_ibl_scale 688 vec3 1 0 +U cb13f3c6 frag u_daylight 700 float 1 0 +U cb13f3c6 frag u_fire_pos 704 vec3 1 0 +U cb13f3c6 frag u_fire_color 720 vec3 1 0 +U cb13f3c6 frag u_hand_pos 736 vec3 1 0 +U cb13f3c6 frag u_hand_color 752 vec3 1 0 +U cb13f3c6 frag u_hand_dir 768 vec3 1 0 +U cb13f3c6 frag u_hand_cone 780 float 1 0 +U cb13f3c6 frag u_hand_reach 784 float 1 0 +U cb13f3c6 frag u_ts_origin 792 vec2 1 0 +U cb13f3c6 frag u_ts_half 800 float 1 0 +U cb13f3c6 frag u_ts_on 804 float 1 0 +U cb13f3c6 frag u_force_cascade 808 int 1 0 +U cb13f3c6 frag u_cloud_shadow 812 float 1 0 +U cb13f3c6 frag u_time 816 float 1 0 +U cb13f3c6 frag u_fog_inscatter 820 float 1 0 +U cb13f3c6 frag u_fog_desat 824 float 1 0 +U cb13f3c6 frag u_model_h 828 float 1 0 +U cb13f3c6 frag u_view 832 mat4 1 0 +U cb13f3c6 frag u_tint 896 vec3 1 0 +U cb13f3c6 frag u_rough_scale 908 float 1 0 +U cb13f3c6 frag u_emissive 912 float 1 0 +U cb13f3c6 frag u_blade_base 928 vec3 1 0 +U cb13f3c6 frag u_blade_tip 944 vec3 1 0 +U cb13f3c6 frag u_blade_cols 956 float 1 0 +U cb13f3c6 frag u_blade_tex_on 960 float 1 0 +T cb13f3c6 u_arm 2 +T cb13f3c6 u_blade_tex 3 +T cb13f3c6 u_brdf 4 +T cb13f3c6 u_diff 5 +T cb13f3c6 u_irradiance 6 +T cb13f3c6 u_nrm 7 +T cb13f3c6 u_ortho 8 +T cb13f3c6 u_prefilter 9 +T cb13f3c6 u_shadow 10 +T cb13f3c6 u_tershadow 11 +T cb13f3c6 u_ts_height 12 +P b8cff226 grass.vert model.frag #define FOLIAGE;#define BLADE;#define GBLADE;#define TILES; +B b8cff226 vert 0 4420 +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 +U b8cff226 vert u_cam_pos 192 vec3 1 0 +U b8cff226 vert u_time 204 float 1 0 +U b8cff226 vert u_ts_origin 208 vec2 1 0 +U b8cff226 vert u_ts_half 216 float 1 0 +U b8cff226 vert u_ortho_on 220 float 1 0 +U b8cff226 vert u_lake_level 224 float 1 0 +U b8cff226 vert u_sea_level 228 float 1 0 +U b8cff226 vert u_lake 240 vec4 1 0 +U b8cff226 vert u_snow_line 256 float 1 0 +U b8cff226 vert u_wind 260 float 1 0 +U b8cff226 vert u_push 272 vec3 1 0 +U b8cff226 vert u_tile 288 vec2 1 0 +U b8cff226 vert u_tile_cells 296 int 1 0 +U b8cff226 vert u_per_cell 300 int 1 0 +U b8cff226 vert u_tiles 304 vec4 256 16 +U b8cff226 vert u_s0 4400 float 1 0 +U b8cff226 vert u_d0 4404 float 1 0 +U b8cff226 vert u_radius 4408 float 1 0 +U b8cff226 vert u_px 4412 float 1 0 +U b8cff226 vert u_dbg 4416 int 1 0 +I b8cff226 a_pos 0 vec3 +I b8cff226 a_uv 2 vec2 +B b8cff226 frag 1 964 +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 +U b8cff226 frag u_cascade_texel 480 float 5 16 +U b8cff226 frag u_sun_dir 560 vec3 1 0 +U b8cff226 frag u_sun_color 576 vec3 1 0 +U b8cff226 frag u_cam_pos 592 vec3 1 0 +U b8cff226 frag u_prefilter_levels 604 float 1 0 +U b8cff226 frag u_fog_density 608 float 1 0 +U b8cff226 frag u_fog_height_falloff 612 float 1 0 +U b8cff226 frag u_fog_base 616 float 1 0 +U b8cff226 frag u_clip_y 620 float 1 0 +U b8cff226 frag u_spec_scale 624 float 1 0 +U b8cff226 frag u_sky_rot 632 vec2 1 0 +U b8cff226 frag u_ground_alb 640 vec3 1 0 +U b8cff226 frag u_ground_alb_hi 656 vec3 1 0 +U b8cff226 frag u_ground_hi_y 668 float 1 0 +U b8cff226 frag u_ground_hi_w 672 float 1 0 +U b8cff226 frag u_ibl_scale 688 vec3 1 0 +U b8cff226 frag u_daylight 700 float 1 0 +U b8cff226 frag u_fire_pos 704 vec3 1 0 +U b8cff226 frag u_fire_color 720 vec3 1 0 +U b8cff226 frag u_hand_pos 736 vec3 1 0 +U b8cff226 frag u_hand_color 752 vec3 1 0 +U b8cff226 frag u_hand_dir 768 vec3 1 0 +U b8cff226 frag u_hand_cone 780 float 1 0 +U b8cff226 frag u_hand_reach 784 float 1 0 +U b8cff226 frag u_ts_origin 792 vec2 1 0 +U b8cff226 frag u_ts_half 800 float 1 0 +U b8cff226 frag u_ts_on 804 float 1 0 +U b8cff226 frag u_force_cascade 808 int 1 0 +U b8cff226 frag u_cloud_shadow 812 float 1 0 +U b8cff226 frag u_time 816 float 1 0 +U b8cff226 frag u_fog_inscatter 820 float 1 0 +U b8cff226 frag u_fog_desat 824 float 1 0 +U b8cff226 frag u_model_h 828 float 1 0 +U b8cff226 frag u_view 832 mat4 1 0 +U b8cff226 frag u_tint 896 vec3 1 0 +U b8cff226 frag u_rough_scale 908 float 1 0 +U b8cff226 frag u_emissive 912 float 1 0 +U b8cff226 frag u_blade_base 928 vec3 1 0 +U b8cff226 frag u_blade_tip 944 vec3 1 0 +U b8cff226 frag u_blade_cols 956 float 1 0 +U b8cff226 frag u_blade_tex_on 960 float 1 0 +T b8cff226 u_arm 2 +T b8cff226 u_blade_tex 3 +T b8cff226 u_brdf 4 +T b8cff226 u_diff 5 +T b8cff226 u_irradiance 6 +T b8cff226 u_nrm 7 +T b8cff226 u_ortho 8 +T b8cff226 u_prefilter 9 +T b8cff226 u_shadow 10 +T b8cff226 u_tershadow 11 +T b8cff226 u_ts_height 12 P 3733fc38 impostor.vert impostor.frag B 3733fc38 vert 0 232 U 3733fc38 vert u_view 0 mat4 1 0 @@ -3522,3 +3525,72 @@ T 46e2325c u_scene 7 T 46e2325c u_shadow 8 T 46e2325c u_tershadow 9 T 46e2325c u_ts_height 10 +P b20fdcb5 grass_inst.vert model.frag #define FOLIAGE;#define BLADE;#define GBLADE;#define GINST; +B b20fdcb5 vert 0 160 +U b20fdcb5 vert u_view 0 mat4 1 0 +U b20fdcb5 vert u_proj 64 mat4 1 0 +U b20fdcb5 vert u_time 128 float 1 0 +U b20fdcb5 vert u_wind 132 float 1 0 +U b20fdcb5 vert u_push 144 vec3 1 0 +U b20fdcb5 vert u_dbg 156 int 1 0 +I b20fdcb5 i_root 3 vec4 +I b20fdcb5 i_shape 4 vec4 +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 964 +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 +U b20fdcb5 frag u_cascade_texel 480 float 5 16 +U b20fdcb5 frag u_sun_dir 560 vec3 1 0 +U b20fdcb5 frag u_sun_color 576 vec3 1 0 +U b20fdcb5 frag u_cam_pos 592 vec3 1 0 +U b20fdcb5 frag u_prefilter_levels 604 float 1 0 +U b20fdcb5 frag u_fog_density 608 float 1 0 +U b20fdcb5 frag u_fog_height_falloff 612 float 1 0 +U b20fdcb5 frag u_fog_base 616 float 1 0 +U b20fdcb5 frag u_clip_y 620 float 1 0 +U b20fdcb5 frag u_spec_scale 624 float 1 0 +U b20fdcb5 frag u_sky_rot 632 vec2 1 0 +U b20fdcb5 frag u_ground_alb 640 vec3 1 0 +U b20fdcb5 frag u_ground_alb_hi 656 vec3 1 0 +U b20fdcb5 frag u_ground_hi_y 668 float 1 0 +U b20fdcb5 frag u_ground_hi_w 672 float 1 0 +U b20fdcb5 frag u_ibl_scale 688 vec3 1 0 +U b20fdcb5 frag u_daylight 700 float 1 0 +U b20fdcb5 frag u_fire_pos 704 vec3 1 0 +U b20fdcb5 frag u_fire_color 720 vec3 1 0 +U b20fdcb5 frag u_hand_pos 736 vec3 1 0 +U b20fdcb5 frag u_hand_color 752 vec3 1 0 +U b20fdcb5 frag u_hand_dir 768 vec3 1 0 +U b20fdcb5 frag u_hand_cone 780 float 1 0 +U b20fdcb5 frag u_hand_reach 784 float 1 0 +U b20fdcb5 frag u_ts_origin 792 vec2 1 0 +U b20fdcb5 frag u_ts_half 800 float 1 0 +U b20fdcb5 frag u_ts_on 804 float 1 0 +U b20fdcb5 frag u_force_cascade 808 int 1 0 +U b20fdcb5 frag u_cloud_shadow 812 float 1 0 +U b20fdcb5 frag u_time 816 float 1 0 +U b20fdcb5 frag u_fog_inscatter 820 float 1 0 +U b20fdcb5 frag u_fog_desat 824 float 1 0 +U b20fdcb5 frag u_model_h 828 float 1 0 +U b20fdcb5 frag u_view 832 mat4 1 0 +U b20fdcb5 frag u_tint 896 vec3 1 0 +U b20fdcb5 frag u_rough_scale 908 float 1 0 +U b20fdcb5 frag u_emissive 912 float 1 0 +U b20fdcb5 frag u_blade_base 928 vec3 1 0 +U b20fdcb5 frag u_blade_tip 944 vec3 1 0 +U b20fdcb5 frag u_blade_cols 956 float 1 0 +U b20fdcb5 frag u_blade_tex_on 960 float 1 0 +T b20fdcb5 u_arm 2 +T b20fdcb5 u_blade_tex 3 +T b20fdcb5 u_brdf 4 +T b20fdcb5 u_diff 5 +T b20fdcb5 u_irradiance 6 +T b20fdcb5 u_nrm 7 +T b20fdcb5 u_prefilter 8 +T b20fdcb5 u_shadow 9 +T b20fdcb5 u_tershadow 10 +T b20fdcb5 u_ts_height 11 diff --git a/packages/ludic.render3d/shaders/variants.list b/packages/ludic.render3d/shaders/variants.list index fee8e402..340d0ad3 100644 --- a/packages/ludic.render3d/shaders/variants.list +++ b/packages/ludic.render3d/shaders/variants.list @@ -20,8 +20,8 @@ fullscreen.vert|tonemap.frag| fullscreen.vert|tonemap.frag|#define HDR10; fullscreen.vert|volumetric.frag| grass.mesh|model.frag|#define FOLIAGE;#define BLADE;#define MESH; -grass.vert|model.frag|#define FOLIAGE;#define BLADE; -grass.vert|model.frag|#define FOLIAGE;#define BLADE;#define TILES; +grass.vert|model.frag|#define FOLIAGE;#define BLADE;#define GBLADE; +grass.vert|model.frag|#define FOLIAGE;#define BLADE;#define GBLADE;#define TILES; impostor.vert|impostor.frag| impostor.vert|impostor.frag|#define SHADOW_PASS; model.vert|bake.frag| @@ -59,3 +59,4 @@ terrain.vert|terrain.frag|#define SUN_INLINE;#define FAR_ONLY; terrain.vert|terrain.frag|#define SUN_INLINE;#define NEAR_ONLY; terrain.vert|tersun.frag| water.vert|water.frag| +grass_inst.vert|model.frag|#define FOLIAGE;#define BLADE;#define GBLADE;#define GINST; diff --git a/tools/ludic-cli/shaders.ludic b/tools/ludic-cli/shaders.ludic index 16389dce..a27cf2ff 100644 --- a/tools/ludic-cli/shaders.ludic +++ b/tools/ludic-cli/shaders.ludic @@ -234,7 +234,10 @@ function cmd_shaders() -> int { let depth_remap = "\n#undef main\nvoid main() {\n ludic_gl_main();\n gl_Position.z = (gl_Position.z + gl_Position.w) * 0.5;\n}\n" # invariant: the foliage's depth prepass and its lit pass (depth EQUAL) are different variants, # and without it the compiler may compute their positions a hair apart and fail the test - var vsrc = "#version 460\n" + defines + shd_wind + "invariant gl_Position;\n#define main ludic_gl_main\n" + shd_file(parts[0]) + depth_remap + # as programs.ludic: a blade's vertex stage gets the noise its colour field needs + var vnoise = "" + if Text.contains(defines, "#define GBLADE") { vnoise = shd_noise } + var vsrc = "#version 460\n" + defines + shd_wind + vnoise + "invariant gl_Position;\n#define main ludic_gl_main\n" + shd_file(parts[0]) + depth_remap # A mesh shader stands in for the vertex stage (a variant whose first file is *.mesh): it writes an # array of positions, so it cannot be wrapped for the depth remap or take invariant gl_Position - # it remaps depth itself. Its SPIR-V keeps the .vert name, so the manifest and loader are unchanged. @@ -304,7 +307,8 @@ function cmd_shaders() -> int { let cp = Text.split(cl, "|") if len(cp) < 2 { err(`shaders: bad compute line: {cl}\n`); return 1 } let glsl = `{tmp}/{cp[0]}.comp.glsl` - if not write_file(glsl, "#version 460\n" + shd_file(cp[1])) { err(`shaders: cannot write {glsl}\n`); return 1 } + # the noise functions come first, as in a fragment stage (grass_cull uses them; the rest ignore them) + if not write_file(glsl, "#version 460\n" + shd_noise + shd_file(cp[1])) { err(`shaders: cannot write {glsl}\n`); return 1 } let spv = `{outdir}/{cp[0]}.comp.spv` let logf = `{tmp}/{cp[0]}.comp.log` if not shq(`{shd_q(shd_bin + "/glslangValidator")} -V -S comp {shd_q(glsl)} -o {shd_q(spv)} > {shd_q(logf)} 2>&1`) {