Merge branch 'lang/foundations' into lang/ecs
This commit is contained in:
commit
4a285903e1
83 changed files with 2224 additions and 313 deletions
|
|
@ -30,6 +30,7 @@ section. The rules are in [ludic.base](ludic.base/README.md).
|
||||||
| [ludic.needs](ludic.needs/README.md) | a body's warmth, food, water and energy, and the countdown to a collapse |
|
| [ludic.needs](ludic.needs/README.md) | a body's warmth, food, water and energy, and the countdown to a collapse |
|
||||||
| [ludic.npc](ludic.npc/README.md) | the other people in a place: a routine by the hour, walking round what is in the way, facing a player who comes near, lines as data, a guest's copy |
|
| [ludic.npc](ludic.npc/README.md) | the other people in a place: a routine by the hour, walking round what is in the way, facing a player who comes near, lines as data, a guest's copy |
|
||||||
| [ludic.physics](ludic.physics/README.md) | rigid bodies, removable still shapes, queries and buoyancy over Jolt Physics (a native library, phase 16) |
|
| [ludic.physics](ludic.physics/README.md) | rigid bodies, removable still shapes, queries and buoyancy over Jolt Physics (a native library, phase 16) |
|
||||||
|
| [ludic.nav](ludic.nav/README.md) | a navmesh per kind of walker and the ways across it, over Recast & Detour (a native library, phase 17) |
|
||||||
| [ludic.photo](ludic.photo/README.md) | a camera's photographs: what is in the frame, a grade on size, framing, light and the moment, the roll, its worth, the best of each subject |
|
| [ludic.photo](ludic.photo/README.md) | a camera's photographs: what is in the frame, a grade on size, framing, light and the moment, the roll, its worth, the best of each subject |
|
||||||
| [ludic.save](ludic.save/README.md) | versioned save files: a migration chain the game declares, torn writes told apart, a backup, a newer file refused and read-only |
|
| [ludic.save](ludic.save/README.md) | versioned save files: a migration chain the game declares, torn writes told apart, a backup, a newer file refused and read-only |
|
||||||
| [ludic.settings](ludic.settings/README.md) | a game's settings as data: one store, a fact per change, ranges, a safe set |
|
| [ludic.settings](ludic.settings/README.md) | a game's settings as data: one store, a fact per change, ranges, a safe set |
|
||||||
|
|
|
||||||
|
|
@ -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_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_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_post_hand_in(b, s)`, `jobs_post_set(b, s, ...)`, `jobs_set_total(b, n)` | a post's verbs |
|
||||||
| `jobs_facts() -> Queue<JobsFact>` | `{ 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<JobsFact>` | `{ 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 |
|
| `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
|
## Tests
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ export const JOBS_F_OFFERED: int = 0
|
||||||
export const JOBS_F_PROGRESSED: int = 1
|
export const JOBS_F_PROGRESSED: int = 1
|
||||||
export const JOBS_F_COMPLETED: int = 2
|
export const JOBS_F_COMPLETED: int = 2
|
||||||
export const JOBS_F_HANDED_IN: int = 3
|
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
|
# `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
|
# post handed in is replaced before anyone reads the fact
|
||||||
|
|
|
||||||
25
packages/ludic.jobs/giveup.ludic
Normal file
25
packages/ludic.jobs/giveup.ludic
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ import "defs.ludic"
|
||||||
import "ports.ludic"
|
import "ports.ludic"
|
||||||
import "state.ludic"
|
import "state.ludic"
|
||||||
import "jobs.ludic"
|
import "jobs.ludic"
|
||||||
|
import "giveup.ludic"
|
||||||
import "count.ludic"
|
import "count.ludic"
|
||||||
import "lists.ludic"
|
import "lists.ludic"
|
||||||
import "boards.ludic"
|
import "boards.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
|
# 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 {
|
export function jobs_offered(jobs_st: JobsState, i: int) -> bool {
|
||||||
let d = Jobs[i]
|
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)
|
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 {
|
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__st[i] = JOBS_ACTIVE
|
||||||
jobs_st.jobs__hv[i] = 0
|
jobs_st.jobs__hv[i] = 0
|
||||||
jobs__recheck_one(jobs_st, i)
|
jobs__recheck_one(jobs_st, i)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ function jobs__reset_scope(jobs_st: mut JobsState, sc: int) -> void {
|
||||||
if Jobs[i].scope != sc { continue }
|
if Jobs[i].scope != sc { continue }
|
||||||
jobs_st.jobs__st[i] = JOBS_OFFERED
|
jobs_st.jobs__st[i] = JOBS_OFFERED
|
||||||
jobs_st.jobs__hv[i] = 0
|
jobs_st.jobs__hv[i] = 0
|
||||||
|
jobs_st.jobs__back[i] = 0
|
||||||
}
|
}
|
||||||
for b in 0 .. BOARD_COUNT {
|
for b in 0 .. BOARD_COUNT {
|
||||||
if JobBoards[b].scope != sc { continue }
|
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 v = Value.object()
|
||||||
let js = Value.object()
|
let js = Value.object()
|
||||||
for i in 0 .. JOB_COUNT {
|
for i in 0 .. JOB_COUNT {
|
||||||
if Jobs[i].scope != sc or jobs_st.jobs__st[i] == JOBS_OFFERED { continue }
|
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]])
|
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)
|
Value.put(v, "jobs", js)
|
||||||
let bs = Value.object()
|
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")
|
let js = Value.get(v, "jobs")
|
||||||
for i in 0 .. JOB_COUNT {
|
for i in 0 .. JOB_COUNT {
|
||||||
let p = sv_ints(js, Jobs[i].key)
|
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 }
|
if Value.has(v, "boards") == 0 { return }
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ export state JobsState {
|
||||||
jobs__st: []int = jobs__ints(JOB_COUNT, JOBS_OFFERED)
|
jobs__st: []int = jobs__ints(JOB_COUNT, JOBS_OFFERED)
|
||||||
jobs__hv: []int = jobs__ints(JOB_COUNT, 0)
|
jobs__hv: []int = jobs__ints(JOB_COUNT, 0)
|
||||||
jobs__seen: []bool = jobs__bools(JOB_COUNT) # an offer already said
|
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__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__done_n: []int = jobs__ints(JOBS_GROUPS, 0) # handed in, per group
|
||||||
jobs__off: []int = jobs__offsets()
|
jobs__off: []int = jobs__offsets()
|
||||||
|
|
|
||||||
132
packages/ludic.jobs/tests/giveup_test.ludic
Normal file
132
packages/ludic.jobs/tests/giveup_test.ludic
Normal file
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
67
packages/ludic.nav/README.md
Normal file
67
packages/ludic.nav/README.md
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
# ludic.nav
|
||||||
|
|
||||||
|
A walkable mesh of the world and the ways across it, over
|
||||||
|
[Recast & Detour](https://github.com/recastnavigation/recastnavigation) (zlib) built here from a
|
||||||
|
pinned tag (phase 17). Uses `ludic.base` and nothing else.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
import "ludic.nav"
|
||||||
|
```
|
||||||
|
|
||||||
|
## The rules it keeps
|
||||||
|
|
||||||
|
- **One mesh per kind of walker.** `NAV_PERSON` (0.35 m wide, the hiker's 0.55 m step),
|
||||||
|
`NAV_LARGE` (an elk, a bear, a horse) and `NAV_SMALL` (a hare, a marmot) each have their own,
|
||||||
|
because what a hare slips between a bear walks round. A `NavConfig` says what a mesh is built
|
||||||
|
for: its voxels and its walker's height, radius, step and steepest slope.
|
||||||
|
- **Built from ground, or loaded from a bake.** A `NavGround` is triangles - each with an area
|
||||||
|
byte, 0 not walkable and 1..62 a kind of ground - and what nothing stands in: cylinders (a trunk,
|
||||||
|
a post) and convex footprints (a boulder). Ground steeper than the slope is cleared whatever its
|
||||||
|
area. `nav_build` makes one mesh of it; a map is square tiles (`nav_tiled`, then `nav_tile_build`
|
||||||
|
for each tile from the ground under it and a few metres round), and a tile must be a whole number
|
||||||
|
of cells across or its seams never join - the shim refuses one that is not. A game bakes its maps
|
||||||
|
once and loads the bytes (`nav_save_file` / `nav_load_file`, one format for one tile or many): a
|
||||||
|
map is never built at start-up. Polygon refs are 64-bit, so a map may have 16384 tiles.
|
||||||
|
- **A path is corners.** `nav_path` answers the way from one point to another as the corners
|
||||||
|
where it turns, the first the start and the last the end. When the end cannot be reached the
|
||||||
|
path stops as near it as the mesh allows and `nav_partial` says so - a walker goes there and
|
||||||
|
gives up, never through a river. Either end off the mesh is `-1`.
|
||||||
|
- **A wander's goal is somewhere it can go.** `nav_random_near` answers a point about r away that a
|
||||||
|
walker standing at the start can reach - never across a river without a ford - from a seed the
|
||||||
|
caller draws from its own `Rng`, so the same seed is the same point on every machine.
|
||||||
|
- **A kind of ground has a cost** (`nav_area_cost`): a trail cheaper than a meadow, scree dearer.
|
||||||
|
- **The same question gets the same answer.** Detour is deterministic for the same mesh and the
|
||||||
|
same points, so co-op's order of dice is untouched by asking it.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| --- | --- |
|
||||||
|
| `NAV_PERSON`, `NAV_LARGE`, `NAV_SMALL`, `NAV_CORNERS` | the kinds of walker, and a path's most corners (256) |
|
||||||
|
| `NavConfig { cell, cell_h, height, radius, climb, slope }` | what a mesh is built for (a person by default, 0.25 m cells) |
|
||||||
|
| `NavGround { v, nv, t, nt, area, cyl, nc, foot, nf }` | what it is built from: triangles, an area byte each, cylinders (`x, y, z, r, h`) and footprints (`n`, n points `x z`, `ymin`, `ymax`) |
|
||||||
|
| `nav_build(kind, ground, config) -> bool`, `nav_polygons(kind)` | one mesh over all of it |
|
||||||
|
| `nav_tiled(kind, ox, oz, tile, max_tiles, max_polys) -> bool`, `nav_tile_build(kind, tx, tz, ground, config) -> int` | a map of square tiles, a tile at a time (its polygons, 0 none, -1 failed) |
|
||||||
|
| `nav_save_file(kind, path)`, `nav_load_file(kind, path)`, `nav_reset()` | a baked mesh written and read; every mesh let go |
|
||||||
|
| `nav_nearest(kind, x, y, z) -> bool`, `nav_near_x/y/z()` | the nearest walkable point within a couple of metres |
|
||||||
|
| `nav_path(kind, sx, sy, sz, ex, ey, ez) -> int`, `nav_corners()`, `nav_corner_x/y/z(i)`, `nav_partial(kind)` | a way as corners |
|
||||||
|
| `nav_straight(kind, sx, sy, sz, ex, ez) -> bool` | does the straight line stay walkable |
|
||||||
|
| `nav_random_near(kind, x, y, z, r, seed) -> bool`, `nav_near_x/y/z()` | a reachable point about r away, from the caller's seed |
|
||||||
|
| `nav_area_cost(kind, area, cost)` | how dear a kind of ground is to cross |
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ludic test packages/ludic.nav
|
||||||
|
```
|
||||||
|
|
||||||
|
A 40 m meadow built by hand: a path round a post and a boulder's footprint, over a river's ford,
|
||||||
|
stopping at the bank of a river with none, the nearest point, random points that never cross the
|
||||||
|
river, and a saved mesh answering as the built one did. And a 128 m meadow as four tiles: a path
|
||||||
|
across the seams, a tile under water with no polygons, and the set saved and loaded.
|
||||||
|
|
||||||
|
## The native library
|
||||||
|
|
||||||
|
`native/build.sh` fetches Recast & Detour v1.6.0, checks its SHA-256, and builds Recast, Detour
|
||||||
|
and the shim (`native/shim/nav_shim.cpp`) into `lib/<target>/` - the same script on the Mac and on
|
||||||
|
the PC (Git Bash, the LLVM installer's clang). `native/LICENSE-recastnavigation` ships with it.
|
||||||
66
packages/ludic.nav/build.ludic
Normal file
66
packages/ludic.nav/build.ludic
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
# build.ludic - a kind's mesh: whole from its ground, or tile by tile (a map), or from a bake's file
|
||||||
|
function nv_ground_ok(g: NavGround) -> bool {
|
||||||
|
if g.v == null or g.t == null or g.area == null { return false }
|
||||||
|
if len(g.v) < g.nv * 3 or len(g.t) < g.nt * 3 or len(g.area) < g.nt { return false }
|
||||||
|
if g.nc > 0 and (g.cyl == null or len(g.cyl) < g.nc * 5) { return false }
|
||||||
|
return g.nf == 0 or (g.foot != null and len(g.foot) >= g.nf)
|
||||||
|
}
|
||||||
|
# the configuration in the state's own buffer: a map is thousands of tile builds, and nothing
|
||||||
|
# allocated per build is ever given back
|
||||||
|
function nv_cfg(nav_st: mut NavState, c: NavConfig) -> []float {
|
||||||
|
let cfg = nav_st.nv_cfg
|
||||||
|
cfg[0] = c.cell
|
||||||
|
cfg[1] = c.cell_h
|
||||||
|
cfg[2] = c.height
|
||||||
|
cfg[3] = c.radius
|
||||||
|
cfg[4] = c.climb
|
||||||
|
cfg[5] = c.slope
|
||||||
|
cfg[6] = Math.max(c.detail, 0.9)
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
function nv_or_none(nav_st: NavState, xs: []float, n: int) -> []float {
|
||||||
|
if xs == null or n == 0 { return nav_st.nv_empty }
|
||||||
|
return xs
|
||||||
|
}
|
||||||
|
|
||||||
|
# one mesh over the whole of g
|
||||||
|
export function nav_build(nav_st: mut NavState, kind: int, g: NavGround, c: NavConfig) -> bool {
|
||||||
|
if not nv_ground_ok(g) { return false }
|
||||||
|
return nv_set(nav_st, kind, nvc_build(g.v, g.nv, g.t, g.nt, g.area, nv_or_none(nav_st, g.cyl, g.nc), g.nc, nv_or_none(nav_st, g.foot, g.nf), g.nf, nv_cfg(nav_st, c)))
|
||||||
|
}
|
||||||
|
|
||||||
|
# an empty mesh of square tiles `tile` metres across from (ox, oz), for a map built a tile at a time
|
||||||
|
export function nav_tiled(nav_st: mut NavState, kind: int, ox: float, oz: float, tile: float, max_tiles: int, max_polys: int) -> bool {
|
||||||
|
return nv_set(nav_st, kind, nvc_tiled(ox, oz, tile, max_tiles, max_polys))
|
||||||
|
}
|
||||||
|
# tile (tx, tz) from ground g covering it and a few metres round it: its polygons, 0 none, -1 failed
|
||||||
|
export function nav_tile_build(nav_st: mut NavState, kind: int, tx: int, tz: int, g: NavGround, c: NavConfig) -> int {
|
||||||
|
let h = nv_mesh(nav_st, kind)
|
||||||
|
if h == null or not nv_ground_ok(g) { return -1 }
|
||||||
|
return nvc_tile_build(h, tx, tz, g.v, g.nv, g.t, g.nt, g.area, nv_or_none(nav_st, g.cyl, g.nc), g.nc, nv_or_none(nav_st, g.foot, g.nf), g.nf, nv_cfg(nav_st, c))
|
||||||
|
}
|
||||||
|
|
||||||
|
# the polygons a kind's mesh has (0: none)
|
||||||
|
export function nav_polygons(nav_st: NavState, kind: int) -> int {
|
||||||
|
let h = nv_mesh(nav_st, kind)
|
||||||
|
if h == null { return 0 }
|
||||||
|
return nvc_polys(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
# a kind's mesh written to a file, and read back. Reading holds the file's bytes for good (a slice
|
||||||
|
# is never given back), so a mesh is loaded once per map, never per frame
|
||||||
|
export function nav_save_file(nav_st: NavState, kind: int, path: string) -> bool {
|
||||||
|
let h = nv_mesh(nav_st, kind)
|
||||||
|
if h == null { return false }
|
||||||
|
let n = nvc_save(h, null, 0)
|
||||||
|
if n <= 0 { return false }
|
||||||
|
let buf = buffer(n)
|
||||||
|
if nvc_save(h, buf, n) != n { return false }
|
||||||
|
return Fs.write_bytes(path, buf, n)
|
||||||
|
}
|
||||||
|
export function nav_load_file(nav_st: mut NavState, kind: int, path: string) -> bool {
|
||||||
|
if not Fs.exists(path) { return false }
|
||||||
|
let buf = Fs.read_bytes(path)
|
||||||
|
if buf == null or len(buf) == 0 { return false }
|
||||||
|
return nv_set(nav_st, kind, nvc_load(buf, len(buf)))
|
||||||
|
}
|
||||||
10
packages/ludic.nav/index.ludic
Normal file
10
packages/ludic.nav/index.ludic
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# ludic.nav - a navmesh per kind of walker (a person, a large animal, a small one): built from
|
||||||
|
# triangles and the cylinders nothing stands in, or loaded from the bytes a bake saved; asked for
|
||||||
|
# the nearest walkable point, a path as corners, and whether a straight line stays walkable.
|
||||||
|
module ludic_nav uses ludic_base
|
||||||
|
numbers float
|
||||||
|
import "ludic.base"
|
||||||
|
import "native.ludic"
|
||||||
|
import "state.ludic"
|
||||||
|
import "build.ludic"
|
||||||
|
import "query.ludic"
|
||||||
BIN
packages/ludic.nav/lib/macos-arm64/libludicnav.dylib
(Stored with Git LFS)
Executable file
BIN
packages/ludic.nav/lib/macos-arm64/libludicnav.dylib
(Stored with Git LFS)
Executable file
Binary file not shown.
BIN
packages/ludic.nav/lib/windows-x64/ludicnav.dll
(Stored with Git LFS)
Normal file
BIN
packages/ludic.nav/lib/windows-x64/ludicnav.dll
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
packages/ludic.nav/lib/windows-x64/ludicnav.lib
(Stored with Git LFS)
Normal file
BIN
packages/ludic.nav/lib/windows-x64/ludicnav.lib
(Stored with Git LFS)
Normal file
Binary file not shown.
15
packages/ludic.nav/native.ludic
Normal file
15
packages/ludic.nav/native.ludic
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# native.ludic - the shim's symbols (native/shim/nav_shim.cpp). A navmesh is a handle the package
|
||||||
|
# keeps; points come back through a float buffer it owns. None of this is exported.
|
||||||
|
extern function nvc_build(v: pointer, nv: int, t: pointer, nt: int, area: pointer, cyl: pointer, nc: int, foot: pointer, nf: int, c: pointer) -> pointer = "nav_build"
|
||||||
|
extern function nvc_tiled(ox: float, oz: float, tile: float, max_tiles: int, max_polys: int) -> pointer = "nav_tiled"
|
||||||
|
extern function nvc_tile_build(h: pointer, tx: int, tz: int, v: pointer, nv: int, t: pointer, nt: int, area: pointer, cyl: pointer, nc: int, foot: pointer, nf: int, c: pointer) -> int = "nav_tile_build"
|
||||||
|
extern function nvc_save(h: pointer, buf: pointer, cap: int) -> int = "nav_save"
|
||||||
|
extern function nvc_load(buf: pointer, size: int) -> pointer = "nav_load"
|
||||||
|
extern function nvc_free(h: pointer) -> void = "nav_free"
|
||||||
|
extern function nvc_polys(h: pointer) -> int = "nav_polys"
|
||||||
|
extern function nvc_nearest(h: pointer, x: float, y: float, z: float, out: pointer) -> int = "nav_nearest"
|
||||||
|
extern function nvc_area_cost(h: pointer, area: int, cost: float) -> void = "nav_area_cost"
|
||||||
|
extern function nvc_path(h: pointer, sx: float, sy: float, sz: float, ex: float, ey: float, ez: float, out: pointer, max: int) -> int = "nav_path"
|
||||||
|
extern function nvc_partial(h: pointer) -> int = "nav_partial"
|
||||||
|
extern function nvc_raycast(h: pointer, sx: float, sy: float, sz: float, ex: float, ez: float) -> int = "nav_raycast"
|
||||||
|
extern function nvc_random_near(h: pointer, x: float, y: float, z: float, r: float, seed: int, out: pointer) -> int = "nav_random_near"
|
||||||
18
packages/ludic.nav/native/LICENSE-recastnavigation
Normal file
18
packages/ludic.nav/native/LICENSE-recastnavigation
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
Copyright (c) 2009 Mikko Mononen memon@inside.org
|
||||||
|
|
||||||
|
This software is provided 'as-is', without any express or implied
|
||||||
|
warranty. In no event will the authors be held liable for any damages
|
||||||
|
arising from the use of this software.
|
||||||
|
|
||||||
|
Permission is granted to anyone to use this software for any purpose,
|
||||||
|
including commercial applications, and to alter it and redistribute it
|
||||||
|
freely, subject to the following restrictions:
|
||||||
|
|
||||||
|
1. The origin of this software must not be misrepresented; you must not
|
||||||
|
claim that you wrote the original software. If you use this software
|
||||||
|
in a product, an acknowledgment in the product documentation would be
|
||||||
|
appreciated but is not required.
|
||||||
|
2. Altered source versions must be plainly marked as such, and must not be
|
||||||
|
misrepresented as being the original software.
|
||||||
|
3. This notice may not be removed or altered from any source distribution.
|
||||||
|
|
||||||
28
packages/ludic.nav/native/build.sh
Executable file
28
packages/ludic.nav/native/build.sh
Executable file
|
|
@ -0,0 +1,28 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# builds lib/<target>/ for ludic.nav: Recast & Detour at a pinned tag, and the shim over them.
|
||||||
|
# Recast builds the mesh, Detour answers it; DetourTileCache and DetourCrowd come with phases 17.8
|
||||||
|
# and 18. Polygon refs are 64-bit (DT_POLYREF64): an 8 km map in 64 m tiles is 16384 tiles, which
|
||||||
|
# 32-bit refs would leave 256 polygons each. Objects go to build/native; a few seconds on eight cores.
|
||||||
|
set -eu
|
||||||
|
PKG="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
. "$PKG/../../tools/native/lib.sh"
|
||||||
|
RC_TAG=v1.6.0
|
||||||
|
RC_SHA=d48ca0121962fa0639502c0f56c4e3ae72f98e55d88727225444f500775c0074
|
||||||
|
SRC="$PKG/build/src/recast-$RC_TAG"
|
||||||
|
native_fetch "$SRC" "https://github.com/recastnavigation/recastnavigation/archive/refs/tags/$RC_TAG.tar.gz" "$RC_SHA"
|
||||||
|
|
||||||
|
OBJ="$PKG/build/native/$(native_target)"
|
||||||
|
mkdir -p "$OBJ"
|
||||||
|
CXX="$(native_cxx)"
|
||||||
|
INC="-I$SRC/Recast/Include -I$SRC/Detour/Include"
|
||||||
|
FLAGS="$(native_cflags) -std=c++17 -ffp-contract=off -fno-exceptions -fno-rtti -DNDEBUG -DDT_POLYREF64 $INC"
|
||||||
|
case "$(native_target)" in windows-*) FLAGS="$FLAGS -D_CRT_SECURE_NO_WARNINGS" ;; esac
|
||||||
|
JOBS="$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)"
|
||||||
|
|
||||||
|
( cd "$SRC" && ls Recast/Source/*.cpp Detour/Source/*.cpp ) | while read -r f; do
|
||||||
|
o="$OBJ/$(basename "$f" .cpp).o"
|
||||||
|
[ "$o" -nt "$SRC/$f" ] || echo "$f $o"
|
||||||
|
done | xargs -P "$JOBS" -n 2 sh -c '"$0" '"$FLAGS"' -c "'"$SRC"'/$1" -o "$2"' "$CXX"
|
||||||
|
"$CXX" $FLAGS -I"$PKG/native/shim" -c "$PKG/native/shim/nav_shim.cpp" -o "$OBJ/nav_shim.o"
|
||||||
|
|
||||||
|
native_link "$PKG" ludicnav "$OBJ"/*.o
|
||||||
129
packages/ludic.nav/native/shim/nav_build.inl
Normal file
129
packages/ludic.nav/native/shim/nav_build.inl
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
// nav_build.inl - Recast's pipeline, shared by a whole mesh and a tile: rasterize the triangles,
|
||||||
|
// filter, mark what nothing stands in, erode, regions, contours, polygons, detail, Detour's data.
|
||||||
|
// c[] is the configuration: cell size, cell height, agent height, radius, max climb, max slope, and
|
||||||
|
// the detail mesh's sample spacing (at least 0.9 m, Recast's own limit; 0 grew without bound)
|
||||||
|
|
||||||
|
// cylinders (x, y, z, r, h each) no agent may stand in: trunks, posts, tents
|
||||||
|
static void nav_mark_cylinders(rcContext &ctx, rcCompactHeightfield &chf, const float *cyl, int nc) {
|
||||||
|
for (int i = 0; i < nc; ++i) {
|
||||||
|
float pos[3] = {cyl[i * 5], cyl[i * 5 + 1], cyl[i * 5 + 2]};
|
||||||
|
rcMarkCylinderArea(&ctx, pos, cyl[i * 5 + 3], cyl[i * 5 + 4], RC_NULL_AREA, chf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// convex footprints no agent may stand in (a boulder): each is n, n points (x, z), ymin, ymax
|
||||||
|
static void nav_mark_footprints(rcContext &ctx, rcCompactHeightfield &chf, const float *f, int nf) {
|
||||||
|
float pts[3 * 64];
|
||||||
|
int i = 0;
|
||||||
|
while (i < nf) {
|
||||||
|
int n = (int)f[i];
|
||||||
|
if (n < 3 || n > 64 || i + 1 + n * 2 + 2 > nf) return;
|
||||||
|
float y0 = f[i + 1 + n * 2], y1 = f[i + 2 + n * 2];
|
||||||
|
for (int k = 0; k < n; ++k) { pts[k * 3] = f[i + 1 + k * 2]; pts[k * 3 + 1] = y0; pts[k * 3 + 2] = f[i + 2 + k * 2]; }
|
||||||
|
rcMarkConvexPolyArea(&ctx, pts, n, y0, y1, RC_NULL_AREA, chf);
|
||||||
|
i += 3 + n * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void nav_config(rcConfig &cfg, const float *c) {
|
||||||
|
memset(&cfg, 0, sizeof(cfg));
|
||||||
|
cfg.cs = c[0];
|
||||||
|
cfg.ch = c[1];
|
||||||
|
cfg.walkableSlopeAngle = c[5];
|
||||||
|
cfg.walkableHeight = (int)ceilf(c[2] / cfg.ch);
|
||||||
|
cfg.walkableClimb = (int)floorf(c[4] / cfg.ch);
|
||||||
|
cfg.walkableRadius = (int)ceilf(c[3] / cfg.cs);
|
||||||
|
cfg.maxEdgeLen = (int)(12.0f / cfg.cs);
|
||||||
|
cfg.maxSimplificationError = 1.3f;
|
||||||
|
cfg.minRegionArea = 8 * 8;
|
||||||
|
cfg.mergeRegionArea = 20 * 20;
|
||||||
|
cfg.maxVertsPerPoly = 6;
|
||||||
|
cfg.detailSampleDist = c[6] > 0.9f ? c[6] : 0.9f;
|
||||||
|
cfg.detailSampleMaxError = cfg.ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NavIn {
|
||||||
|
const float *v; int nv; const int *t; int nt; const unsigned char *area;
|
||||||
|
const float *cyl; int nc; const float *foot; int nf; const float *c;
|
||||||
|
};
|
||||||
|
|
||||||
|
// the pipeline over cfg's box; the tile's Detour data into data / size. 0 polygons is not an error
|
||||||
|
static bool nav_pipeline(rcConfig &cfg, const NavIn &in, int tx, int tz, unsigned char **data, int *size, int *polys) {
|
||||||
|
rcContext ctx(false);
|
||||||
|
std::vector<unsigned char> areas(in.area, in.area + in.nt);
|
||||||
|
rcClearUnwalkableTriangles(&ctx, cfg.walkableSlopeAngle, in.v, in.nv, in.t, in.nt, areas.data());
|
||||||
|
rcHeightfield *hf = rcAllocHeightfield();
|
||||||
|
rcCompactHeightfield *chf = rcAllocCompactHeightfield();
|
||||||
|
rcContourSet *cs = rcAllocContourSet();
|
||||||
|
rcPolyMesh *pm = rcAllocPolyMesh();
|
||||||
|
rcPolyMeshDetail *dm = rcAllocPolyMeshDetail();
|
||||||
|
bool ok = hf && chf && cs && pm && dm &&
|
||||||
|
rcCreateHeightfield(&ctx, *hf, cfg.width, cfg.height, cfg.bmin, cfg.bmax, cfg.cs, cfg.ch) &&
|
||||||
|
rcRasterizeTriangles(&ctx, in.v, in.nv, in.t, areas.data(), in.nt, *hf, cfg.walkableClimb);
|
||||||
|
if (ok) {
|
||||||
|
rcFilterLowHangingWalkableObstacles(&ctx, cfg.walkableClimb, *hf);
|
||||||
|
rcFilterLedgeSpans(&ctx, cfg.walkableHeight, cfg.walkableClimb, *hf);
|
||||||
|
rcFilterWalkableLowHeightSpans(&ctx, cfg.walkableHeight, *hf);
|
||||||
|
ok = rcBuildCompactHeightfield(&ctx, cfg.walkableHeight, cfg.walkableClimb, *hf, *chf);
|
||||||
|
}
|
||||||
|
if (ok) {
|
||||||
|
nav_mark_cylinders(ctx, *chf, in.cyl, in.nc);
|
||||||
|
nav_mark_footprints(ctx, *chf, in.foot, in.nf);
|
||||||
|
ok = rcErodeWalkableArea(&ctx, cfg.walkableRadius, *chf) && rcBuildDistanceField(&ctx, *chf) &&
|
||||||
|
rcBuildRegions(&ctx, *chf, cfg.borderSize, cfg.minRegionArea, cfg.mergeRegionArea) &&
|
||||||
|
rcBuildContours(&ctx, *chf, cfg.maxSimplificationError, cfg.maxEdgeLen, *cs) &&
|
||||||
|
rcBuildPolyMesh(&ctx, *cs, cfg.maxVertsPerPoly, *pm) &&
|
||||||
|
rcBuildPolyMeshDetail(&ctx, *pm, *chf, cfg.detailSampleDist, cfg.detailSampleMaxError, *dm);
|
||||||
|
}
|
||||||
|
*polys = ok ? pm->npolys : 0;
|
||||||
|
if (ok && pm->npolys > 0) {
|
||||||
|
for (int i = 0; i < pm->npolys; ++i) pm->flags[i] = pm->areas[i] ? 1 : 0;
|
||||||
|
dtNavMeshCreateParams p;
|
||||||
|
memset(&p, 0, sizeof(p));
|
||||||
|
p.verts = pm->verts; p.vertCount = pm->nverts; p.polys = pm->polys; p.polyAreas = pm->areas;
|
||||||
|
p.polyFlags = pm->flags; p.polyCount = pm->npolys; p.nvp = pm->nvp;
|
||||||
|
p.detailMeshes = dm->meshes; p.detailVerts = dm->verts; p.detailVertsCount = dm->nverts;
|
||||||
|
p.detailTris = dm->tris; p.detailTriCount = dm->ntris;
|
||||||
|
p.walkableHeight = in.c[2]; p.walkableRadius = in.c[3]; p.walkableClimb = in.c[4];
|
||||||
|
p.tileX = tx; p.tileY = tz;
|
||||||
|
rcVcopy(p.bmin, pm->bmin); rcVcopy(p.bmax, pm->bmax);
|
||||||
|
p.cs = cfg.cs; p.ch = cfg.ch; p.buildBvTree = true;
|
||||||
|
ok = dtCreateNavMeshData(&p, data, size);
|
||||||
|
}
|
||||||
|
rcFreeHeightField(hf); rcFreeCompactHeightfield(chf); rcFreeContourSet(cs); rcFreePolyMesh(pm); rcFreePolyMeshDetail(dm);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
// one mesh over all the triangles given: v nv points (x, y, z); t nt triangles; area a byte each,
|
||||||
|
// 0 not walkable and 1..62 a kind of ground; the slope clears what is too steep whatever its area
|
||||||
|
NAV_SHIM void *nav_build(const float *v, int nv, const int *t, int nt, const unsigned char *area,
|
||||||
|
const float *cyl, int nc, const float *foot, int nf, const float *c) {
|
||||||
|
rcConfig cfg;
|
||||||
|
nav_config(cfg, c);
|
||||||
|
rcCalcBounds(v, nv, cfg.bmin, cfg.bmax);
|
||||||
|
rcCalcGridSize(cfg.bmin, cfg.bmax, cfg.cs, &cfg.width, &cfg.height);
|
||||||
|
NavIn in = {v, nv, t, nt, area, cyl, nc, foot, nf, c};
|
||||||
|
unsigned char *data = nullptr;
|
||||||
|
int size = 0, polys = 0;
|
||||||
|
if (!nav_pipeline(cfg, in, 0, 0, &data, &size, &polys) || polys == 0) return nullptr;
|
||||||
|
return nav_from(data, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
NAV_SHIM void nav_free(void *h) {
|
||||||
|
Nav *n = static_cast<Nav *>(h);
|
||||||
|
if (!n) return;
|
||||||
|
dtFreeNavMeshQuery(n->query);
|
||||||
|
dtFreeNavMesh(n->mesh);
|
||||||
|
delete n;
|
||||||
|
}
|
||||||
|
|
||||||
|
// every polygon in every tile
|
||||||
|
NAV_SHIM int nav_polys(void *h) {
|
||||||
|
const dtNavMesh *m = static_cast<Nav *>(h)->mesh;
|
||||||
|
int total = 0;
|
||||||
|
for (int i = 0; i < m->getMaxTiles(); ++i) {
|
||||||
|
const dtMeshTile *t = m->getTile(i);
|
||||||
|
if (t && t->header) total += t->header->polyCount;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
72
packages/ludic.nav/native/shim/nav_query.inl
Normal file
72
packages/ludic.nav/native/shim/nav_query.inl
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
// nav_query.inl - what a navmesh answers: the nearest point on it, a path as corner points, and
|
||||||
|
// whether a straight line stays on it. Points go out through out[] (x, y, z each).
|
||||||
|
|
||||||
|
static dtPolyRef nav_poly_at(Nav *n, const float *p, float *on) {
|
||||||
|
dtPolyRef r = 0;
|
||||||
|
if (dtStatusFailed(n->query->findNearestPoly(p, n->ext, &n->filter, &r, on))) return 0;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the nearest point on the mesh to (x, y, z) into out; 0 when none is within reach
|
||||||
|
NAV_SHIM int nav_nearest(void *h, float x, float y, float z, float *out) {
|
||||||
|
float p[3] = {x, y, z};
|
||||||
|
return nav_poly_at(static_cast<Nav *>(h), p, out) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// a cost per kind of ground (area 1..62): the filter every query uses
|
||||||
|
NAV_SHIM void nav_area_cost(void *h, int area, float cost) {
|
||||||
|
Nav *n = static_cast<Nav *>(h);
|
||||||
|
if (area > 0 && area < DT_MAX_AREAS) n->filter.setAreaCost(area, cost);
|
||||||
|
}
|
||||||
|
|
||||||
|
// the corners of a path from one point to another, at most max of them, into out: the count, or
|
||||||
|
// -1 when either end is off the mesh. One that cannot reach the end stops as near it as the mesh
|
||||||
|
// allows, and nav_partial says so
|
||||||
|
NAV_SHIM int nav_path(void *h, float sx, float sy, float sz, float ex, float ey, float ez, float *out, int max) {
|
||||||
|
Nav *n = static_cast<Nav *>(h);
|
||||||
|
float s[3] = {sx, sy, sz}, e[3] = {ex, ey, ez}, so[3], eo[3];
|
||||||
|
dtPolyRef a = nav_poly_at(n, s, so), b = nav_poly_at(n, e, eo);
|
||||||
|
n->partial = 0;
|
||||||
|
if (!a || !b) return -1;
|
||||||
|
int np = 0;
|
||||||
|
dtStatus st = n->query->findPath(a, b, so, eo, &n->filter, n->polys, &np, NAV_MAX_POLYS);
|
||||||
|
if (dtStatusFailed(st) || np == 0) return -1;
|
||||||
|
if (dtStatusDetail(st, DT_PARTIAL_RESULT) || n->polys[np - 1] != b) n->partial = 1;
|
||||||
|
float end[3];
|
||||||
|
dtVcopy(end, eo);
|
||||||
|
if (n->polys[np - 1] != b) n->query->closestPointOnPoly(n->polys[np - 1], eo, end, nullptr);
|
||||||
|
int count = 0;
|
||||||
|
if (dtStatusFailed(n->query->findStraightPath(so, end, n->polys, np, out, nullptr, nullptr, &count, max, 0))) return -1;
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
NAV_SHIM int nav_partial(void *h) { return static_cast<Nav *>(h)->partial; }
|
||||||
|
|
||||||
|
// does the straight line from one point to another stay on the mesh? 1 yes, 0 no, -1 off it
|
||||||
|
NAV_SHIM int nav_raycast(void *h, float sx, float sy, float sz, float ex, float ez) {
|
||||||
|
Nav *n = static_cast<Nav *>(h);
|
||||||
|
float s[3] = {sx, sy, sz}, so[3], e[3] = {ex, sy, ez}, t = 0, nrm[3];
|
||||||
|
dtPolyRef a = nav_poly_at(n, s, so);
|
||||||
|
if (!a) return -1;
|
||||||
|
int np = 0;
|
||||||
|
if (dtStatusFailed(n->query->raycast(a, so, e, &n->filter, &t, nrm, n->polys, &np, NAV_MAX_POLYS))) return -1;
|
||||||
|
return t >= 1.0f ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// a point about r from (x, y, z) that a walker there can reach, into out; 0 when there is none.
|
||||||
|
// Detour asks a function for its dice: this one is seeded by the caller, so the same seed from the
|
||||||
|
// package's Rng gives the same point on every machine
|
||||||
|
static unsigned int nav_rs = 1;
|
||||||
|
static float nav_frand() {
|
||||||
|
nav_rs = nav_rs * 1664525u + 1013904223u;
|
||||||
|
return (float)(nav_rs >> 8) * (1.0f / 16777216.0f);
|
||||||
|
}
|
||||||
|
NAV_SHIM int nav_random_near(void *h, float x, float y, float z, float r, int seed, float *out) {
|
||||||
|
Nav *n = static_cast<Nav *>(h);
|
||||||
|
float p[3] = {x, y, z}, on[3];
|
||||||
|
dtPolyRef start = nav_poly_at(n, p, on);
|
||||||
|
if (!start) return 0;
|
||||||
|
nav_rs = (unsigned int)seed * 2654435761u + 1u;
|
||||||
|
dtPolyRef ref = 0;
|
||||||
|
if (dtStatusFailed(n->query->findRandomPointAroundCircle(start, on, r, &n->filter, nav_frand, &ref, out))) return 0;
|
||||||
|
return ref ? 1 : 0;
|
||||||
|
}
|
||||||
52
packages/ludic.nav/native/shim/nav_shim.cpp
Normal file
52
packages/ludic.nav/native/shim/nav_shim.cpp
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
// nav_shim.cpp - ludic.nav's door into Recast & Detour (phase 17). The shim rules of
|
||||||
|
// packages/README.md: int, float and opaque handles cross; no struct by value, no callback into
|
||||||
|
// Ludic; what a query finds is written into a buffer the package owns; errors are return codes.
|
||||||
|
#include <Recast.h>
|
||||||
|
#include <DetourNavMesh.h>
|
||||||
|
#include <DetourNavMeshBuilder.h>
|
||||||
|
#include <DetourNavMeshQuery.h>
|
||||||
|
#include <DetourCommon.h>
|
||||||
|
#include <cstring>
|
||||||
|
#include <cmath>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#define NAV_SHIM extern "C" __declspec(dllexport)
|
||||||
|
#else
|
||||||
|
#define NAV_SHIM extern "C" __attribute__((visibility("default")))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
const int NAV_MAX_POLYS = 512; // polygons a path may cross; a longer one is cut there
|
||||||
|
const int NAV_MAX_NODES = 4096; // the search's open list
|
||||||
|
|
||||||
|
// a navmesh and the one query that walks it, with the scratch a path needs
|
||||||
|
struct Nav {
|
||||||
|
dtNavMesh *mesh = nullptr;
|
||||||
|
dtNavMeshQuery *query = nullptr;
|
||||||
|
dtQueryFilter filter;
|
||||||
|
float ext[3] = {2.0f, 4.0f, 2.0f}; // how far to look for the mesh around a point
|
||||||
|
dtPolyRef polys[NAV_MAX_POLYS];
|
||||||
|
int partial = 0; // the last path stopped short of its end
|
||||||
|
};
|
||||||
|
|
||||||
|
// a navmesh with its query, from params (a set of tiles) or from one tile's data
|
||||||
|
Nav *nav_open(dtNavMesh *mesh) {
|
||||||
|
Nav *n = new Nav();
|
||||||
|
n->mesh = mesh;
|
||||||
|
n->query = dtAllocNavMeshQuery();
|
||||||
|
if (!n->query || dtStatusFailed(n->query->init(n->mesh, NAV_MAX_NODES))) { dtFreeNavMeshQuery(n->query); dtFreeNavMesh(n->mesh); delete n; return nullptr; }
|
||||||
|
n->filter.setIncludeFlags(0xffff);
|
||||||
|
n->filter.setExcludeFlags(0);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
Nav *nav_from(unsigned char *data, int size) {
|
||||||
|
dtNavMesh *mesh = dtAllocNavMesh();
|
||||||
|
if (!mesh || dtStatusFailed(mesh->init(data, size, DT_TILE_FREE_DATA))) { dtFree(data); dtFreeNavMesh(mesh); return nullptr; }
|
||||||
|
return nav_open(mesh);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
#include "nav_build.inl"
|
||||||
|
#include "nav_tiles.inl"
|
||||||
|
#include "nav_query.inl"
|
||||||
102
packages/ludic.nav/native/shim/nav_tiles.inl
Normal file
102
packages/ludic.nav/native/shim/nav_tiles.inl
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
// nav_tiles.inl - a map as square tiles: a tiled mesh, one tile built from the triangles under it
|
||||||
|
// and a border round it, and every tile written and read back as one set of bytes
|
||||||
|
|
||||||
|
// an empty mesh of tiles `tile` metres across, the first at (ox, oz)
|
||||||
|
NAV_SHIM void *nav_tiled(float ox, float oz, float tile, int max_tiles, int max_polys) {
|
||||||
|
dtNavMeshParams p;
|
||||||
|
memset(&p, 0, sizeof(p));
|
||||||
|
p.orig[0] = ox; p.orig[1] = 0.0f; p.orig[2] = oz;
|
||||||
|
p.tileWidth = tile; p.tileHeight = tile;
|
||||||
|
p.maxTiles = max_tiles; p.maxPolys = max_polys;
|
||||||
|
dtNavMesh *mesh = dtAllocNavMesh();
|
||||||
|
if (!mesh || dtStatusFailed(mesh->init(&p))) { dtFreeNavMesh(mesh); return nullptr; }
|
||||||
|
return nav_open(mesh);
|
||||||
|
}
|
||||||
|
|
||||||
|
// tile (tx, tz) built from triangles that cover it and a border of the walker's radius and three
|
||||||
|
// cells round it, replacing whatever was there; its polygons, 0 for none, -1 when it failed
|
||||||
|
NAV_SHIM int nav_tile_build(void *h, int tx, int tz, const float *v, int nv, const int *t, int nt,
|
||||||
|
const unsigned char *area, const float *cyl, int nc, const float *foot, int nf, const float *c) {
|
||||||
|
Nav *n = static_cast<Nav *>(h);
|
||||||
|
const dtNavMeshParams *mp = n->mesh->getParams();
|
||||||
|
rcConfig cfg;
|
||||||
|
nav_config(cfg, c);
|
||||||
|
cfg.tileSize = (int)(mp->tileWidth / cfg.cs + 0.5f);
|
||||||
|
// a tile a cell short of its width leaves a seam its neighbour's polygons never meet
|
||||||
|
if (fabsf(cfg.tileSize * cfg.cs - mp->tileWidth) > 0.001f) return -1;
|
||||||
|
cfg.borderSize = cfg.walkableRadius + 3;
|
||||||
|
cfg.width = cfg.height = cfg.tileSize + cfg.borderSize * 2;
|
||||||
|
float lo[3], hi[3];
|
||||||
|
rcCalcBounds(v, nv, lo, hi);
|
||||||
|
cfg.bmin[0] = mp->orig[0] + tx * mp->tileWidth - cfg.borderSize * cfg.cs;
|
||||||
|
cfg.bmin[2] = mp->orig[2] + tz * mp->tileHeight - cfg.borderSize * cfg.cs;
|
||||||
|
cfg.bmax[0] = mp->orig[0] + (tx + 1) * mp->tileWidth + cfg.borderSize * cfg.cs;
|
||||||
|
cfg.bmax[2] = mp->orig[2] + (tz + 1) * mp->tileHeight + cfg.borderSize * cfg.cs;
|
||||||
|
cfg.bmin[1] = lo[1] - 1.0f;
|
||||||
|
cfg.bmax[1] = hi[1] + c[2] + 1.0f;
|
||||||
|
NavIn in = {v, nv, t, nt, area, cyl, nc, foot, nf, c};
|
||||||
|
unsigned char *data = nullptr;
|
||||||
|
int size = 0, polys = 0;
|
||||||
|
n->mesh->removeTile(n->mesh->getTileRefAt(tx, tz, 0), nullptr, nullptr);
|
||||||
|
if (!nav_pipeline(cfg, in, tx, tz, &data, &size, &polys)) return -1;
|
||||||
|
if (polys == 0) return 0;
|
||||||
|
if (dtStatusFailed(n->mesh->addTile(data, size, DT_TILE_FREE_DATA, 0, nullptr))) { dtFree(data); return -1; }
|
||||||
|
return polys;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the set's bytes: "NAVT", a version, the mesh's params, then each tile's ref, size and data.
|
||||||
|
// First call with buf null for the size, then with a buffer that big
|
||||||
|
static const int NAV_MAGIC = 'N' << 24 | 'A' << 16 | 'V' << 8 | 'T';
|
||||||
|
static const int NAV_VERSION = 1;
|
||||||
|
static void nav_put(unsigned char *buf, int &at, const void *p, int n) {
|
||||||
|
if (buf) memcpy(buf + at, p, n);
|
||||||
|
at += n;
|
||||||
|
}
|
||||||
|
NAV_SHIM int nav_save(void *h, unsigned char *buf, int cap) {
|
||||||
|
const dtNavMesh *m = static_cast<Nav *>(h)->mesh;
|
||||||
|
const dtNavMeshParams *p = m->getParams();
|
||||||
|
int at = 0, count = 0;
|
||||||
|
for (int i = 0; i < m->getMaxTiles(); ++i) {
|
||||||
|
const dtMeshTile *t = m->getTile(i);
|
||||||
|
if (t && t->header && t->dataSize) count++;
|
||||||
|
}
|
||||||
|
nav_put(buf, at, &NAV_MAGIC, 4); nav_put(buf, at, &NAV_VERSION, 4); nav_put(buf, at, &count, 4);
|
||||||
|
nav_put(buf, at, p->orig, 12); nav_put(buf, at, &p->tileWidth, 4); nav_put(buf, at, &p->tileHeight, 4);
|
||||||
|
nav_put(buf, at, &p->maxTiles, 4); nav_put(buf, at, &p->maxPolys, 4);
|
||||||
|
for (int i = 0; i < m->getMaxTiles(); ++i) {
|
||||||
|
const dtMeshTile *t = m->getTile(i);
|
||||||
|
if (!t || !t->header || !t->dataSize) continue;
|
||||||
|
unsigned long long ref = (unsigned long long)m->getTileRef(t);
|
||||||
|
if (buf && at + 12 + t->dataSize > cap) return -1;
|
||||||
|
nav_put(buf, at, &ref, 8); nav_put(buf, at, &t->dataSize, 4); nav_put(buf, at, t->data, t->dataSize);
|
||||||
|
}
|
||||||
|
return at;
|
||||||
|
}
|
||||||
|
|
||||||
|
// a mesh from bytes nav_save wrote; null for anything else
|
||||||
|
NAV_SHIM void *nav_load(const unsigned char *buf, int size) {
|
||||||
|
int at = 0, magic = 0, version = 0, count = 0;
|
||||||
|
if (size < 40) return nullptr;
|
||||||
|
memcpy(&magic, buf, 4); memcpy(&version, buf + 4, 4); memcpy(&count, buf + 8, 4);
|
||||||
|
if (magic != NAV_MAGIC || version != NAV_VERSION) return nullptr;
|
||||||
|
dtNavMeshParams p;
|
||||||
|
memset(&p, 0, sizeof(p));
|
||||||
|
memcpy(p.orig, buf + 12, 12); memcpy(&p.tileWidth, buf + 24, 4); memcpy(&p.tileHeight, buf + 28, 4);
|
||||||
|
memcpy(&p.maxTiles, buf + 32, 4); memcpy(&p.maxPolys, buf + 36, 4);
|
||||||
|
at = 40;
|
||||||
|
dtNavMesh *mesh = dtAllocNavMesh();
|
||||||
|
if (!mesh || dtStatusFailed(mesh->init(&p))) { dtFreeNavMesh(mesh); return nullptr; }
|
||||||
|
for (int i = 0; i < count; ++i) {
|
||||||
|
unsigned long long ref = 0;
|
||||||
|
int n = 0;
|
||||||
|
if (at + 12 > size) break;
|
||||||
|
memcpy(&ref, buf + at, 8); memcpy(&n, buf + at + 8, 4);
|
||||||
|
at += 12;
|
||||||
|
if (n <= 0 || at + n > size) break;
|
||||||
|
unsigned char *data = static_cast<unsigned char *>(dtAlloc(n, DT_ALLOC_PERM));
|
||||||
|
memcpy(data, buf + at, n);
|
||||||
|
at += n;
|
||||||
|
if (dtStatusFailed(mesh->addTile(data, n, DT_TILE_FREE_DATA, (dtTileRef)ref, nullptr))) dtFree(data);
|
||||||
|
}
|
||||||
|
return nav_open(mesh);
|
||||||
|
}
|
||||||
7
packages/ludic.nav/package.ludic
Normal file
7
packages/ludic.nav/package.ludic
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# ludic.nav - a walkable mesh of the world and the ways across it, over Recast & Detour (phase 17),
|
||||||
|
# built here from a pinned tag (native/build.sh). Uses ludic.base and nothing else.
|
||||||
|
package "ludic.nav"
|
||||||
|
version "0.1.0"
|
||||||
|
kind source
|
||||||
|
native "macos-arm64" "lib/macos-arm64/libludicnav.dylib"
|
||||||
|
native "windows-x64" "lib/windows-x64/ludicnav.dll"
|
||||||
51
packages/ludic.nav/query.ludic
Normal file
51
packages/ludic.nav/query.ludic
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
# query.ludic - what a kind's mesh answers. A path is its corners, read back one at a time
|
||||||
|
# (nav_corner_x / _y / _z); the nearest point the same (nav_near_x / _y / _z)
|
||||||
|
|
||||||
|
# the nearest walkable point to (x, y, z) within a couple of metres; false when there is none
|
||||||
|
export function nav_nearest(nav_st: NavState, kind: int, x: float, y: float, z: float) -> bool {
|
||||||
|
let h = nv_mesh(nav_st, kind)
|
||||||
|
if h == null { return false }
|
||||||
|
return nvc_nearest(h, x, y, z, nav_st.nv_near) == 1
|
||||||
|
}
|
||||||
|
export function nav_near_x(nav_st: NavState) -> float { return nav_st.nv_near[0] }
|
||||||
|
export function nav_near_y(nav_st: NavState) -> float { return nav_st.nv_near[1] }
|
||||||
|
export function nav_near_z(nav_st: NavState) -> float { return nav_st.nv_near[2] }
|
||||||
|
|
||||||
|
# a point about r from (x, y, z) that a walker standing there can reach (a wander's goal), read back
|
||||||
|
# as the nearest is; the seed is the caller's dice, so the same seed is the same point everywhere
|
||||||
|
export function nav_random_near(nav_st: NavState, kind: int, x: float, y: float, z: float, r: float, seed: int) -> bool {
|
||||||
|
let h = nv_mesh(nav_st, kind)
|
||||||
|
if h == null { return false }
|
||||||
|
return nvc_random_near(h, x, y, z, r, seed, nav_st.nv_near) == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# a way from one point to another: its corners, first the start and last the end (or as near the end
|
||||||
|
# as the mesh reaches - nav_partial says which); -1 when either end is off the mesh
|
||||||
|
export function nav_path(nav_st: mut NavState, kind: int, sx: float, sy: float, sz: float, ex: float, ey: float, ez: float) -> int {
|
||||||
|
let h = nv_mesh(nav_st, kind)
|
||||||
|
nav_st.nv_count = 0
|
||||||
|
if h == null { return -1 }
|
||||||
|
let n = nvc_path(h, sx, sy, sz, ex, ey, ez, nav_st.nv_out, NAV_CORNERS)
|
||||||
|
if n > 0 { nav_st.nv_count = n }
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
export function nav_partial(nav_st: NavState, kind: int) -> bool {
|
||||||
|
let h = nv_mesh(nav_st, kind)
|
||||||
|
return h != null and nvc_partial(h) == 1
|
||||||
|
}
|
||||||
|
export function nav_corners(nav_st: NavState) -> int { return nav_st.nv_count }
|
||||||
|
export function nav_corner_x(nav_st: NavState, i: int) -> float { return nav_st.nv_out[i * 3] }
|
||||||
|
export function nav_corner_y(nav_st: NavState, i: int) -> float { return nav_st.nv_out[i * 3 + 1] }
|
||||||
|
export function nav_corner_z(nav_st: NavState, i: int) -> float { return nav_st.nv_out[i * 3 + 2] }
|
||||||
|
|
||||||
|
# does the straight line from (sx, sy, sz) to (ex, ez) stay walkable?
|
||||||
|
export function nav_straight(nav_st: NavState, kind: int, sx: float, sy: float, sz: float, ex: float, ez: float) -> bool {
|
||||||
|
let h = nv_mesh(nav_st, kind)
|
||||||
|
return h != null and nvc_raycast(h, sx, sy, sz, ex, ez) == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# how dear a kind of ground is to cross (1 is plain; a trail cheaper, scree dearer)
|
||||||
|
export function nav_area_cost(nav_st: NavState, kind: int, area: int, cost: float) -> void {
|
||||||
|
let h = nv_mesh(nav_st, kind)
|
||||||
|
if h != null { nvc_area_cost(h, area, cost) }
|
||||||
|
}
|
||||||
68
packages/ludic.nav/state.ludic
Normal file
68
packages/ludic.nav/state.ludic
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
# state.ludic - the kinds of walker, what a mesh is built for, and what the package holds: a mesh
|
||||||
|
# per kind, and the last path's corners and nearest point, read back through the verbs
|
||||||
|
export const NAV_PERSON: int = 0 # 0.35 m wide, steps what a hiker steps (CHAR_STEP)
|
||||||
|
export const NAV_LARGE: int = 1 # an elk, a bear, a horse
|
||||||
|
export const NAV_SMALL: int = 2 # a hare, a marmot
|
||||||
|
export const NAV_KINDS: int = 3
|
||||||
|
export const NAV_CORNERS: int = 256 # a path's corners at most; a longer one is cut there
|
||||||
|
|
||||||
|
# what a mesh is built for: its voxels (cell across, cell high) and its walker (height, radius,
|
||||||
|
# the step it takes whatever its gradient, the steepest ground it walks, degrees). A tile must be a
|
||||||
|
# whole number of cells across, or its seams never join: 0.25 m is 256 to a 64 m tile
|
||||||
|
export property NavConfig {
|
||||||
|
cell: float = 0.25
|
||||||
|
cell_h: float = 0.2
|
||||||
|
height: float = 1.8
|
||||||
|
radius: float = 0.35
|
||||||
|
climb: float = 0.55
|
||||||
|
slope: float = 45.0
|
||||||
|
detail: float = 1.5 # the detail mesh's samples, metres apart (at least 0.9: 0 took one small test to 3.5 GB)
|
||||||
|
}
|
||||||
|
|
||||||
|
# what a mesh is built from: v holds nv points (x, y, z) and t nt triangles (three indices each);
|
||||||
|
# area a byte per triangle, 0 not walkable and 1..62 a kind of ground; cyl nc cylinders (x, y, z,
|
||||||
|
# r, h) and foot nf floats of convex footprints (n, n points x z, ymin, ymax) nothing stands in
|
||||||
|
export property NavGround {
|
||||||
|
v: []float = null
|
||||||
|
nv: int = 0
|
||||||
|
t: []int = null
|
||||||
|
nt: int = 0
|
||||||
|
area: []byte = null
|
||||||
|
cyl: []float = null
|
||||||
|
nc: int = 0
|
||||||
|
foot: []float = null
|
||||||
|
nf: int = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export state NavState {
|
||||||
|
nv_meshes: []pointer = nv_none()
|
||||||
|
nv_out: []float = floats(NAV_CORNERS * 3)
|
||||||
|
nv_count: int = 0
|
||||||
|
nv_near: []float = floats(3)
|
||||||
|
nv_cfg: []float = floats(7) # the configuration handed to the shim, filled in place per build
|
||||||
|
nv_empty: []float = floats(1) # what the shim is handed for no cylinders or no footprints
|
||||||
|
}
|
||||||
|
function nv_none() -> []pointer {
|
||||||
|
let m = new []pointer
|
||||||
|
for k in 0 .. NAV_KINDS { push(m, null) }
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
function nv_mesh(nav_st: NavState, kind: int) -> pointer {
|
||||||
|
if kind < 0 or kind >= NAV_KINDS { return null }
|
||||||
|
return nav_st.nv_meshes[kind]
|
||||||
|
}
|
||||||
|
function nv_set(nav_st: mut NavState, kind: int, h: pointer) -> bool {
|
||||||
|
if kind < 0 or kind >= NAV_KINDS { return false }
|
||||||
|
if nav_st.nv_meshes[kind] != null { nvc_free(nav_st.nv_meshes[kind]) }
|
||||||
|
nav_st.nv_meshes[kind] = h
|
||||||
|
return h != null
|
||||||
|
}
|
||||||
|
|
||||||
|
# one kind's mesh let go (a bake writes each kind and drops it before the next)
|
||||||
|
export function nav_drop(nav_st: mut NavState, kind: int) -> void { nv_set(nav_st, kind, null) }
|
||||||
|
|
||||||
|
# every mesh let go: a new map, or the world closed
|
||||||
|
export function nav_reset(nav_st: mut NavState) -> void {
|
||||||
|
for k in 0 .. NAV_KINDS { nv_set(nav_st, k, null) }
|
||||||
|
nav_st.nv_count = 0
|
||||||
|
}
|
||||||
61
packages/ludic.nav/tests/fakes/meadow.ludic
Normal file
61
packages/ludic.nav/tests/fakes/meadow.ludic
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
# fakes/meadow.ludic - ground built by hand for ludic.nav's tests: n metre squares a side, flat at
|
||||||
|
# y = 0; river 1 crosses x 18..22 with a ford at z 30..34, river 2 without one, river 0 is none
|
||||||
|
function ground(n: int, river: int) -> NavGround {
|
||||||
|
let g = new NavGround
|
||||||
|
g.nv = (n + 1) * (n + 1)
|
||||||
|
g.v = floats(g.nv * 3)
|
||||||
|
for j in 0 .. n + 1 {
|
||||||
|
for i in 0 .. n + 1 {
|
||||||
|
let o = (j * (n + 1) + i) * 3
|
||||||
|
g.v[o] = float(i)
|
||||||
|
g.v[o + 2] = float(j)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.nt = n * n * 2
|
||||||
|
g.t = new []int
|
||||||
|
g.area = buffer(g.nt)
|
||||||
|
for j in 0 .. n {
|
||||||
|
for i in 0 .. n {
|
||||||
|
let a = j * (n + 1) + i
|
||||||
|
push(g.t, a)
|
||||||
|
push(g.t, a + n + 1)
|
||||||
|
push(g.t, a + 1)
|
||||||
|
push(g.t, a + 1)
|
||||||
|
push(g.t, a + n + 1)
|
||||||
|
push(g.t, a + n + 2)
|
||||||
|
var k = 1
|
||||||
|
if river > 0 and i >= 18 and i < 22 and not (river == 1 and j >= 30 and j < 34) { k = 0 }
|
||||||
|
g.area[(j * n + i) * 2] = k
|
||||||
|
g.area[(j * n + i) * 2 + 1] = k
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
# a post r wide at (x, z), from under the ground to over a walker's head
|
||||||
|
function post_at(x: float, z: float, r: float) -> []float {
|
||||||
|
let c = floats(5)
|
||||||
|
c[0] = x
|
||||||
|
c[1] = -1.0
|
||||||
|
c[2] = z
|
||||||
|
c[3] = r
|
||||||
|
c[4] = 4.0
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
# a square footprint 2h across at (x, z), as a boulder's is written: n, the corners, ymin, ymax
|
||||||
|
function square_at(x: float, z: float, h: float) -> []float {
|
||||||
|
let f = floats(11)
|
||||||
|
f[0] = 4.0
|
||||||
|
f[1] = x - h
|
||||||
|
f[2] = z - h
|
||||||
|
f[3] = x + h
|
||||||
|
f[4] = z - h
|
||||||
|
f[5] = x + h
|
||||||
|
f[6] = z + h
|
||||||
|
f[7] = x - h
|
||||||
|
f[8] = z + h
|
||||||
|
f[9] = -1.0
|
||||||
|
f[10] = 3.0
|
||||||
|
return f
|
||||||
|
}
|
||||||
103
packages/ludic.nav/tests/nav_test.ludic
Normal file
103
packages/ludic.nav/tests/nav_test.ludic
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
# nav_test.ludic - ludic.nav on ground built by hand: a 40 m meadow of metre squares, a post in its
|
||||||
|
# middle, a river across it (area 0) with a ford or without one. A path goes round the post and over
|
||||||
|
# the ford, stops at the bank when there is none, and a saved mesh answers as the built one did.
|
||||||
|
import "ludic.nav"
|
||||||
|
import "ludic.base"
|
||||||
|
import "fakes/meadow.ludic"
|
||||||
|
program NavTest {
|
||||||
|
numbers float
|
||||||
|
|
||||||
|
function meadow(nav_st: mut NavState, kind: int, river: int, post: bool) -> bool {
|
||||||
|
let g = ground(40, river)
|
||||||
|
if post { g.cyl = post_at(20.0, 20.0, 2.0) }
|
||||||
|
if post { g.nc = 1 }
|
||||||
|
return nav_build(nav_st, kind, g, new NavConfig)
|
||||||
|
}
|
||||||
|
function last_x(nav_st: NavState) -> float { return nav_corner_x(nav_st, nav_corners(nav_st) - 1) }
|
||||||
|
function last_z(nav_st: NavState) -> float { return nav_corner_z(nav_st, nav_corners(nav_st) - 1) }
|
||||||
|
|
||||||
|
test "a path goes round a post, and the straight line through it is not walkable" (nav_st: mut NavState) {
|
||||||
|
expect(meadow(nav_st, NAV_PERSON, 0, true))
|
||||||
|
expect(nav_polygons(nav_st, NAV_PERSON) > 0)
|
||||||
|
let n = nav_path(nav_st, NAV_PERSON, 5.0, 0.0, 20.0, 35.0, 0.0, 20.0)
|
||||||
|
expect(n >= 3)
|
||||||
|
expect(not nav_partial(nav_st, NAV_PERSON))
|
||||||
|
expect(Math.abs(last_x(nav_st) - 35.0) < 0.1)
|
||||||
|
for i in 0 .. n {
|
||||||
|
let dx = nav_corner_x(nav_st, i) - 20.0
|
||||||
|
let dz = nav_corner_z(nav_st, i) - 20.0
|
||||||
|
expect(dx * dx + dz * dz > 2.0 * 2.0)
|
||||||
|
}
|
||||||
|
expect(not nav_straight(nav_st, NAV_PERSON, 5.0, 0.0, 20.0, 35.0, 20.0))
|
||||||
|
expect(nav_straight(nav_st, NAV_PERSON, 5.0, 0.0, 5.0, 35.0, 5.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
test "a river is crossed at its ford" (nav_st: mut NavState) {
|
||||||
|
expect(meadow(nav_st, NAV_PERSON, 1, false))
|
||||||
|
let n = nav_path(nav_st, NAV_PERSON, 5.0, 0.0, 10.0, 35.0, 0.0, 10.0)
|
||||||
|
expect(n >= 3)
|
||||||
|
expect(not nav_partial(nav_st, NAV_PERSON))
|
||||||
|
var ford = false
|
||||||
|
for i in 0 .. n { if nav_corner_z(nav_st, i) > 29.0 { ford = true } }
|
||||||
|
expect(ford)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "without a ford the path stops at the near bank and says so" (nav_st: mut NavState) {
|
||||||
|
expect(meadow(nav_st, NAV_PERSON, 2, false))
|
||||||
|
let n = nav_path(nav_st, NAV_PERSON, 5.0, 0.0, 10.0, 35.0, 0.0, 10.0)
|
||||||
|
expect(n >= 1)
|
||||||
|
expect(nav_partial(nav_st, NAV_PERSON))
|
||||||
|
expect(last_x(nav_st) < 18.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "the nearest walkable point, and nothing off the mesh" (nav_st: mut NavState) {
|
||||||
|
expect(meadow(nav_st, NAV_PERSON, 0, false))
|
||||||
|
expect(nav_nearest(nav_st, NAV_PERSON, 10.0, 1.0, 10.0))
|
||||||
|
expect(Math.abs(nav_near_y(nav_st)) < 0.3)
|
||||||
|
expect(Math.abs(nav_near_x(nav_st) - 10.0) < 0.1)
|
||||||
|
expect(not nav_nearest(nav_st, NAV_PERSON, -50.0, 0.0, -50.0))
|
||||||
|
expect_eq(nav_path(nav_st, NAV_PERSON, -50.0, 0.0, -50.0, 10.0, 0.0, 10.0), -1)
|
||||||
|
expect_eq(nav_path(nav_st, NAV_LARGE, 5.0, 0.0, 5.0, 10.0, 0.0, 10.0), -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "a random point near is always one a walker there can reach, and a seed is always the same point" (nav_st: mut NavState) {
|
||||||
|
expect(meadow(nav_st, NAV_PERSON, 2, false))
|
||||||
|
for seed in 0 .. 50 {
|
||||||
|
expect(nav_random_near(nav_st, NAV_PERSON, 5.0, 0.0, 10.0, 30.0, seed))
|
||||||
|
expect(nav_near_x(nav_st) < 18.0)
|
||||||
|
}
|
||||||
|
nav_random_near(nav_st, NAV_PERSON, 5.0, 0.0, 10.0, 30.0, 7)
|
||||||
|
let x7 = nav_near_x(nav_st)
|
||||||
|
let z7 = nav_near_z(nav_st)
|
||||||
|
nav_random_near(nav_st, NAV_PERSON, 5.0, 0.0, 10.0, 30.0, 8)
|
||||||
|
expect(Math.abs(nav_near_x(nav_st) - x7) + Math.abs(nav_near_z(nav_st) - z7) > 0.01)
|
||||||
|
nav_random_near(nav_st, NAV_PERSON, 5.0, 0.0, 10.0, 30.0, 7)
|
||||||
|
expect_eq(nav_near_x(nav_st), x7)
|
||||||
|
expect_eq(nav_near_z(nav_st), z7)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "a boulder's footprint is walked round" (nav_st: mut NavState) {
|
||||||
|
let g = ground(40, 0)
|
||||||
|
g.foot = square_at(20.0, 20.0, 3.0)
|
||||||
|
g.nf = len(g.foot)
|
||||||
|
expect(nav_build(nav_st, NAV_PERSON, g, new NavConfig))
|
||||||
|
expect(not nav_straight(nav_st, NAV_PERSON, 5.0, 0.0, 20.0, 35.0, 20.0))
|
||||||
|
expect(nav_path(nav_st, NAV_PERSON, 5.0, 0.0, 20.0, 35.0, 0.0, 20.0) >= 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "a mesh saved and loaded answers as the built one did" (nav_st: mut NavState) {
|
||||||
|
expect(meadow(nav_st, NAV_PERSON, 1, true))
|
||||||
|
let path = Os.temp_dir() + "/ludic_nav_test.navmesh"
|
||||||
|
expect(nav_save_file(nav_st, NAV_PERSON, path))
|
||||||
|
expect(nav_load_file(nav_st, NAV_SMALL, path))
|
||||||
|
let a = nav_path(nav_st, NAV_PERSON, 5.0, 0.0, 10.0, 35.0, 0.0, 10.0)
|
||||||
|
let ax = last_x(nav_st)
|
||||||
|
let az = last_z(nav_st)
|
||||||
|
let b = nav_path(nav_st, NAV_SMALL, 5.0, 0.0, 10.0, 35.0, 0.0, 10.0)
|
||||||
|
expect_eq(a, b)
|
||||||
|
expect_near(last_x(nav_st), ax, 0.0001)
|
||||||
|
expect_near(last_z(nav_st), az, 0.0001)
|
||||||
|
nav_reset(nav_st)
|
||||||
|
expect_eq(nav_polygons(nav_st, NAV_SMALL), 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
66
packages/ludic.nav/tests/tiles_test.ludic
Normal file
66
packages/ludic.nav/tests/tiles_test.ludic
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
# tiles_test.ludic - ludic.nav's map in tiles: a 128 m meadow as four 64 m tiles, one of them under
|
||||||
|
# water. A path crosses the tiles' seams, the water's tile has no polygons and nothing inside it is
|
||||||
|
# a way's end, and the set saved and loaded answers as the built one did.
|
||||||
|
import "ludic.nav"
|
||||||
|
import "ludic.base"
|
||||||
|
import "fakes/meadow.ludic"
|
||||||
|
program TilesTest {
|
||||||
|
numbers float
|
||||||
|
|
||||||
|
# four tiles over ground(128), the far corner's squares (x and z past 64) under water when wet
|
||||||
|
function map(nav_st: mut NavState, wet: bool) -> int {
|
||||||
|
let g = ground(128, 0)
|
||||||
|
if wet {
|
||||||
|
for j in 64 .. 128 { for i in 64 .. 128 { g.area[(j * 128 + i) * 2] = 0 } }
|
||||||
|
for j in 64 .. 128 { for i in 64 .. 128 { g.area[(j * 128 + i) * 2 + 1] = 0 } }
|
||||||
|
}
|
||||||
|
g.cyl = post_at(96.0, 20.0, 2.0)
|
||||||
|
g.nc = 1
|
||||||
|
expect(nav_tiled(nav_st, NAV_PERSON, 0.0, 0.0, 64.0, 16, 4096))
|
||||||
|
var empty = 0
|
||||||
|
for tz in 0 .. 2 {
|
||||||
|
for tx in 0 .. 2 {
|
||||||
|
let p = nav_tile_build(nav_st, NAV_PERSON, tx, tz, g, new NavConfig)
|
||||||
|
expect(p >= 0)
|
||||||
|
if p == 0 { empty += 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
function last_x(nav_st: NavState) -> float { return nav_corner_x(nav_st, nav_corners(nav_st) - 1) }
|
||||||
|
function last_z(nav_st: NavState) -> float { return nav_corner_z(nav_st, nav_corners(nav_st) - 1) }
|
||||||
|
|
||||||
|
test "a path crosses the tiles' seams and goes round the post in the far tile" (nav_st: mut NavState) {
|
||||||
|
expect_eq(map(nav_st, false), 0)
|
||||||
|
expect(nav_polygons(nav_st, NAV_PERSON) > 0)
|
||||||
|
let n = nav_path(nav_st, NAV_PERSON, 10.0, 0.0, 20.0, 120.0, 0.0, 20.0)
|
||||||
|
expect(n >= 3)
|
||||||
|
expect(not nav_partial(nav_st, NAV_PERSON))
|
||||||
|
expect(Math.abs(last_x(nav_st) - 120.0) < 0.1)
|
||||||
|
let d = nav_path(nav_st, NAV_PERSON, 10.0, 0.0, 10.0, 120.0, 0.0, 120.0)
|
||||||
|
expect(d >= 2)
|
||||||
|
expect(not nav_partial(nav_st, NAV_PERSON))
|
||||||
|
}
|
||||||
|
|
||||||
|
test "a tile under water has no polygons: a way into it is none, the shore beside it is reached" (nav_st: mut NavState) {
|
||||||
|
expect_eq(map(nav_st, true), 1)
|
||||||
|
expect(not nav_nearest(nav_st, NAV_PERSON, 100.0, 0.0, 100.0))
|
||||||
|
expect_eq(nav_path(nav_st, NAV_PERSON, 10.0, 0.0, 10.0, 70.0, 0.0, 70.0), -1)
|
||||||
|
expect(nav_path(nav_st, NAV_PERSON, 10.0, 0.0, 10.0, 62.0, 0.0, 70.0) >= 2)
|
||||||
|
expect(not nav_partial(nav_st, NAV_PERSON))
|
||||||
|
expect(Math.abs(last_z(nav_st) - 70.0) < 0.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "the tiles saved and loaded answer as the built ones did" (nav_st: mut NavState) {
|
||||||
|
map(nav_st, true)
|
||||||
|
let path = Os.temp_dir() + "/ludic_nav_tiles.navmesh"
|
||||||
|
expect(nav_save_file(nav_st, NAV_PERSON, path))
|
||||||
|
expect(nav_load_file(nav_st, NAV_LARGE, path))
|
||||||
|
expect_eq(nav_polygons(nav_st, NAV_LARGE), nav_polygons(nav_st, NAV_PERSON))
|
||||||
|
let a = nav_path(nav_st, NAV_PERSON, 10.0, 0.0, 20.0, 120.0, 0.0, 20.0)
|
||||||
|
let ax = last_x(nav_st)
|
||||||
|
let b = nav_path(nav_st, NAV_LARGE, 10.0, 0.0, 20.0, 120.0, 0.0, 20.0)
|
||||||
|
expect_eq(a, b)
|
||||||
|
expect_near(last_x(nav_st), ax, 0.0001)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -29,7 +29,7 @@ import "ludic.physics"
|
||||||
| | |
|
| | |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `phys_open(st, max_bodies, threads) -> bool`, `phys_close(st)`, `phys_is_open(st)` | a world, and everything in it let go |
|
| `phys_open(st, max_bodies, threads) -> bool`, `phys_close(st)`, `phys_is_open(st)` | a world, and everything in it let go |
|
||||||
| `phys_box`, `phys_sphere`, `phys_capsule`, `phys_cylinder`, `phys_dome(r, h)`, `phys_offset(shape, x, y, z, yaw)` | shapes, each an int (`-1` refused); a dome is a boulder standing on its origin |
|
| `phys_box`, `phys_sphere`, `phys_capsule`, `phys_cylinder`, `phys_dome(r, h)`, `phys_convex(points, n)`, `phys_scaled(shape, k)`, `phys_offset(shape, x, y, z, yaw)` | shapes, each an int (`-1` refused); a dome is a boulder standing on its origin; a convex hull of n points (x, y, z) is a rock's own shape, made once and scaled for every rock of that shape; a body's yaw turns it as a renderer turns an instance (x' = cos x + sin z, z' = -sin x + cos z) |
|
||||||
| `phys_heightfield(st, h, n, ox, oz, cell)`, `phys_mesh(st, v, nv, tri, nt)` | the ground (`PHYS_HOLE` is none) and a dock or cabin |
|
| `phys_heightfield(st, h, n, ox, oz, cell)`, `phys_mesh(st, v, nv, tri, nt)` | the ground (`PHYS_HOLE` is none) and a dock or cabin |
|
||||||
| `phys_ground_add`, `phys_static_add`, `phys_kinematic_add`, `phys_body_add(..., mass)` | bodies: the ground, still things, what the game moves (the player), what falls and floats |
|
| `phys_ground_add`, `phys_static_add`, `phys_kinematic_add`, `phys_body_add(..., mass)` | bodies: the ground, still things, what the game moves (the player), what falls and floats |
|
||||||
| `phys_settle(st)` | after adding many still things, before the first query |
|
| `phys_settle(st)` | after adding many still things, before the first query |
|
||||||
|
|
|
||||||
BIN
packages/ludic.physics/lib/macos-arm64/libludicjolt.dylib
(Stored with Git LFS)
BIN
packages/ludic.physics/lib/macos-arm64/libludicjolt.dylib
(Stored with Git LFS)
Binary file not shown.
BIN
packages/ludic.physics/lib/windows-x64/ludicjolt.dll
(Stored with Git LFS)
BIN
packages/ludic.physics/lib/windows-x64/ludicjolt.dll
(Stored with Git LFS)
Binary file not shown.
BIN
packages/ludic.physics/lib/windows-x64/ludicjolt.lib
(Stored with Git LFS)
BIN
packages/ludic.physics/lib/windows-x64/ludicjolt.lib
(Stored with Git LFS)
Binary file not shown.
|
|
@ -14,6 +14,8 @@ extern function jph_shape_capsule(half_h: float, r: float) -> pointer = "jph_sha
|
||||||
extern function jph_shape_cylinder(half_h: float, r: float) -> pointer = "jph_shape_cylinder"
|
extern function jph_shape_cylinder(half_h: float, r: float) -> pointer = "jph_shape_cylinder"
|
||||||
extern function jph_shape_dome(r: float, h: float) -> pointer = "jph_shape_dome"
|
extern function jph_shape_dome(r: float, h: float) -> pointer = "jph_shape_dome"
|
||||||
extern function jph_shape_offset(s: pointer, x: float, y: float, z: float, yaw: float) -> pointer = "jph_shape_offset"
|
extern function jph_shape_offset(s: pointer, x: float, y: float, z: float, yaw: float) -> pointer = "jph_shape_offset"
|
||||||
|
extern function jph_shape_convex(v: pointer, n: int, radius: float) -> pointer = "jph_shape_convex"
|
||||||
|
extern function jph_shape_scaled(s: pointer, k: float) -> pointer = "jph_shape_scaled"
|
||||||
extern function jph_shape_heightfield(h: pointer, n: int, ox: float, oz: float, cell: float) -> pointer = "jph_shape_heightfield"
|
extern function jph_shape_heightfield(h: pointer, n: int, ox: float, oz: float, cell: float) -> pointer = "jph_shape_heightfield"
|
||||||
extern function jph_shape_mesh(v: pointer, nv: int, tri: pointer, nt: int) -> pointer = "jph_shape_mesh"
|
extern function jph_shape_mesh(v: pointer, nv: int, tri: pointer, nt: int) -> pointer = "jph_shape_mesh"
|
||||||
extern function jph_shape_free(s: pointer) -> void = "jph_shape_free"
|
extern function jph_shape_free(s: pointer) -> void = "jph_shape_free"
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,19 @@ JPH_SHIM void *jph_shape_dome(float r, float h) {
|
||||||
return keep(RotatedTranslatedShapeSettings(Vec3(0.0f, h - big, 0.0f), Quat::sIdentity(), s).Create());
|
return keep(RotatedTranslatedShapeSettings(Vec3(0.0f, h - big, 0.0f), Quat::sIdentity(), s).Create());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// a rock: the convex hull of n points (x, y, z) about its origin, its edges rounded by radius
|
||||||
|
JPH_SHIM void *jph_shape_convex(const float *v, int n, float radius) {
|
||||||
|
Array<Vec3> pts;
|
||||||
|
pts.reserve(n);
|
||||||
|
for (int i = 0; i < n; ++i) pts.push_back(Vec3(v[3 * i], v[3 * i + 1], v[3 * i + 2]));
|
||||||
|
return keep(ConvexHullShapeSettings(pts, radius).Create());
|
||||||
|
}
|
||||||
|
|
||||||
|
// another shape, k times its size on every axis (one hull, every boulder of that shape)
|
||||||
|
JPH_SHIM void *jph_shape_scaled(void *s, float k) {
|
||||||
|
return keep(ScaledShapeSettings(static_cast<Shape *>(s), Vec3::sReplicate(k)).Create());
|
||||||
|
}
|
||||||
|
|
||||||
// a shape moved and turned about y, inside its body
|
// a shape moved and turned about y, inside its body
|
||||||
JPH_SHIM void *jph_shape_offset(void *s, float x, float y, float z, float yaw) {
|
JPH_SHIM void *jph_shape_offset(void *s, float x, float y, float z, float yaw) {
|
||||||
return keep(RotatedTranslatedShapeSettings(Vec3(x, y, z), yaw_q(yaw), static_cast<Shape *>(s)).Create());
|
return keep(RotatedTranslatedShapeSettings(Vec3(x, y, z), yaw_q(yaw), static_cast<Shape *>(s)).Create());
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@
|
||||||
#include <Jolt/Physics/Collision/Shape/HeightFieldShape.h>
|
#include <Jolt/Physics/Collision/Shape/HeightFieldShape.h>
|
||||||
#include <Jolt/Physics/Collision/Shape/MeshShape.h>
|
#include <Jolt/Physics/Collision/Shape/MeshShape.h>
|
||||||
#include <Jolt/Physics/Collision/Shape/RotatedTranslatedShape.h>
|
#include <Jolt/Physics/Collision/Shape/RotatedTranslatedShape.h>
|
||||||
|
#include <Jolt/Physics/Collision/Shape/ConvexHullShape.h>
|
||||||
|
#include <Jolt/Physics/Collision/Shape/ScaledShape.h>
|
||||||
#include <Jolt/Physics/Collision/RayCast.h>
|
#include <Jolt/Physics/Collision/RayCast.h>
|
||||||
#include <Jolt/Physics/Collision/CastResult.h>
|
#include <Jolt/Physics/Collision/CastResult.h>
|
||||||
#include <Jolt/Physics/Collision/ShapeCast.h>
|
#include <Jolt/Physics/Collision/ShapeCast.h>
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,18 @@ export function phys_cylinder(physics_st: mut PhysicsState, half_h: float, r: fl
|
||||||
# a boulder: a dome r across and h tall, standing on its origin
|
# a boulder: a dome r across and h tall, standing on its origin
|
||||||
export function phys_dome(physics_st: mut PhysicsState, r: float, h: float) -> int { return ph_keep(physics_st, jph_shape_dome(r, h)) }
|
export function phys_dome(physics_st: mut PhysicsState, r: float, h: float) -> int { return ph_keep(physics_st, jph_shape_dome(r, h)) }
|
||||||
|
|
||||||
|
# a rock: the convex hull of n points (x, y, z) about its origin, its edges rounded by 2 cm
|
||||||
|
export function phys_convex(physics_st: mut PhysicsState, v: []float, n: int) -> int {
|
||||||
|
if n < 4 or len(v) < n * 3 { return -1 }
|
||||||
|
return ph_keep(physics_st, jph_shape_convex(v, n, 0.02))
|
||||||
|
}
|
||||||
|
# another shape k times its size, so one hull serves every boulder of its shape
|
||||||
|
export function phys_scaled(physics_st: mut PhysicsState, shape: int, k: float) -> int {
|
||||||
|
let s = ph_shape(physics_st, shape)
|
||||||
|
if s == null { return -1 }
|
||||||
|
return ph_keep(physics_st, jph_shape_scaled(s, k))
|
||||||
|
}
|
||||||
|
|
||||||
# another shape moved and turned about y inside its body (a post whose foot is its origin)
|
# another shape moved and turned about y inside its body (a post whose foot is its origin)
|
||||||
export function phys_offset(physics_st: mut PhysicsState, shape: int, x: float, y: float, z: float, yaw: float) -> int {
|
export function phys_offset(physics_st: mut PhysicsState, shape: int, x: float, y: float, z: float, yaw: float) -> int {
|
||||||
let s = ph_shape(physics_st, shape)
|
let s = ph_shape(physics_st, shape)
|
||||||
|
|
|
||||||
44
packages/ludic.physics/tests/hull_shape_test.ludic
Normal file
44
packages/ludic.physics/tests/hull_shape_test.ludic
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
# hull_shape_test.ludic - a rock's shape: a convex hull of points, scaled for one boulder and
|
||||||
|
# turned by its yaw the way a renderer turns an instance (x' = cos x + sin z, z' = -sin x + cos z)
|
||||||
|
import "ludic.physics"
|
||||||
|
import "ludic.base"
|
||||||
|
program HullShapeTest {
|
||||||
|
numbers float
|
||||||
|
|
||||||
|
# a cube two metres across, its centre two metres along x from its origin
|
||||||
|
function cube(physics_st: mut PhysicsState) -> int {
|
||||||
|
let v = floats(8 * 3)
|
||||||
|
for i in 0 .. 8 {
|
||||||
|
v[i * 3] = 2.0 + float(i % 2) * 2.0 - 1.0
|
||||||
|
v[i * 3 + 1] = float((i / 2) % 2) * 2.0 - 1.0
|
||||||
|
v[i * 3 + 2] = float(i / 4) * 2.0 - 1.0
|
||||||
|
}
|
||||||
|
return phys_convex(physics_st, v, 8)
|
||||||
|
}
|
||||||
|
function down(physics_st: mut PhysicsState, x: float, z: float) -> PhysHit { return phys_ray(physics_st, x, 10.0, z, 0.0, -20.0, 0.0, PHYS_M_ALL) }
|
||||||
|
|
||||||
|
test "a hull stands where its points are, and too few points are no shape" (physics_st: mut PhysicsState) {
|
||||||
|
expect(phys_open(physics_st, 16, 1))
|
||||||
|
phys_static_add(physics_st, cube(physics_st), 0.0, 0.0, 0.0, 0.0)
|
||||||
|
phys_settle(physics_st)
|
||||||
|
let h = down(physics_st, 2.0, 0.0)
|
||||||
|
expect(h.body >= 0)
|
||||||
|
expect(Math.abs(h.y - 1.0) < 0.01)
|
||||||
|
expect(down(physics_st, 0.0, 0.0).body < 0)
|
||||||
|
expect_eq(phys_convex(physics_st, floats(9), 3), -1)
|
||||||
|
phys_close(physics_st)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "scaled twice as big, and turned a quarter the renderer's way" (physics_st: mut PhysicsState) {
|
||||||
|
expect(phys_open(physics_st, 16, 1))
|
||||||
|
phys_static_add(physics_st, phys_scaled(physics_st, cube(physics_st), 2.0), 0.0, 0.0, 0.0, 1.5707963)
|
||||||
|
phys_settle(physics_st)
|
||||||
|
# (4, 0, 0) turned by +90 degrees about y is (0, 0, -4): x' = cos x + sin z, z' = -sin x + cos z
|
||||||
|
let h = down(physics_st, 0.0, -4.0)
|
||||||
|
expect(h.body >= 0)
|
||||||
|
expect(Math.abs(h.y - 2.0) < 0.01)
|
||||||
|
expect(down(physics_st, 4.0, 0.0).body < 0)
|
||||||
|
expect(down(physics_st, 0.0, 4.0).body < 0)
|
||||||
|
phys_close(physics_st)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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_fence: bytes = null # the presented frame still on the GPU (one frame in flight)
|
||||||
gvk_frame_pending: bool = false
|
gvk_frame_pending: bool = false
|
||||||
gvk_frame_pending_cb: pointer = null
|
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_has_hqr: bool = false
|
||||||
gvk_ts_period: float = 0.0 # float bits: nanoseconds per timestamp tick
|
gvk_ts_period: float = 0.0 # float bits: nanoseconds per timestamp tick
|
||||||
gvk_qpool: long = 0
|
gvk_qpool: long = 0
|
||||||
|
|
@ -208,6 +214,7 @@ export state Render3dState {
|
||||||
gvk_cp_layout: []long = null
|
gvk_cp_layout: []long = null
|
||||||
gvk_cp_dsl: []long = null
|
gvk_cp_dsl: []long = null
|
||||||
gvk_cp_nbuf: []int = null
|
gvk_cp_nbuf: []int = null
|
||||||
|
gvk_cp_ntex: []int = null
|
||||||
gvk_pipe_keys: []string = null
|
gvk_pipe_keys: []string = null
|
||||||
gvk_pipe: []long = null
|
gvk_pipe: []long = null
|
||||||
gvk_zero_vbuf: int = 0
|
gvk_zero_vbuf: int = 0
|
||||||
|
|
@ -357,8 +364,22 @@ export state Render3dState {
|
||||||
grass_n: int = 0
|
grass_n: int = 0
|
||||||
grass_cmds: int = 0
|
grass_cmds: int = 0
|
||||||
grass_band_start: words = null # per band: its first record
|
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
|
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_on: bool = false
|
||||||
grass_mesh_prog: int = 0
|
grass_mesh_prog: int = 0
|
||||||
r3d_draw_cb: fn() = null
|
r3d_draw_cb: fn() = null
|
||||||
|
|
@ -582,6 +603,9 @@ export state Render3dState {
|
||||||
sc_cast_dy: float = 0.0
|
sc_cast_dy: float = 0.0
|
||||||
sc_cast_k: float = 0.0
|
sc_cast_k: float = 0.0
|
||||||
sc_skip_blade: bool = false
|
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
|
sc_skip_card: bool = false
|
||||||
cb_state: int = 12345
|
cb_state: int = 12345
|
||||||
shadow_res: int = 2048
|
shadow_res: int = 2048
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
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) }
|
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)
|
# 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) } }
|
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
|
# the host-visible contents of a buffer, for a readback after gpu_finish; null on OpenGL
|
||||||
|
|
|
||||||
|
|
@ -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, VK_KHR_SURFACE_EXTENSION_NAME); n_iext += 1
|
||||||
Vk.put_ptr(iext_names, n_iext * 8, surf_ext); 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
|
# 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)
|
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 }
|
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
|
# 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
|
# 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 -
|
# 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
|
# waits for it first (gvk_frame_wait), and a buffer it reads is busy (gvk_buf_busy). The frame's
|
||||||
# on macOS; R3D_VK_INFLIGHT=0/1 says otherwise.
|
# 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 {
|
function gvk_inflight_on(render3d_st: mut Render3dState) -> bool {
|
||||||
if render3d_st.gvk_inflight < 0 {
|
if render3d_st.gvk_inflight < 0 {
|
||||||
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) }
|
if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkQueueSubmit", r) }
|
||||||
render3d_st.gvk_frame_pending = true
|
render3d_st.gvk_frame_pending = true
|
||||||
render3d_st.gvk_frame_pending_cb = cb
|
render3d_st.gvk_frame_pending_cb = cb
|
||||||
|
render3d_st.gvk_frame_pending_no = render3d_st.gvk_frame_no
|
||||||
return true
|
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)
|
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 = false
|
||||||
render3d_st.gvk_frame_pending_cb = null
|
render3d_st.gvk_frame_pending_cb = null
|
||||||
gvk_retire_flush(render3d_st)
|
gvk_retire_upto(render3d_st, render3d_st.gvk_frame_pending_no)
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---- teardown -----------------------------------------------------------------------------
|
# ---- 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)
|
out[0] = int(float(ticks) * render3d_st.gvk_ts_period)
|
||||||
return true
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -120,24 +120,28 @@ function gvk_program(render3d_st: mut Render3dState, p: int, key: string, spv_di
|
||||||
|
|
||||||
# ---- compute ------------------------------------------------------------------------------
|
# ---- compute ------------------------------------------------------------------------------
|
||||||
# A compute program is <name>.comp.spv beside the manifest, with bindings fixed by convention:
|
# A compute program is <name>.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
|
# 0 the parameter block (a uniform buffer, copied into the frame's ring at the dispatch),
|
||||||
# 1 .. n storage buffers. Handles start at 1.
|
# 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
|
let zero: long = 0
|
||||||
if render3d_st.gvk_cp_pipe == null {
|
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
|
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`)
|
let module = gvk_module(render3d_st, `{render3d_st.gvk_spv_dir}/{name}.comp.spv`)
|
||||||
if module == 0 { return 0 }
|
if module == 0 { return 0 }
|
||||||
let nb = n_bufs + 1
|
let nb = n_bufs + 1 + n_tex
|
||||||
let bw = VkDescriptorSetLayoutBinding_sizeof
|
let bw = VkDescriptorSetLayoutBinding_sizeof
|
||||||
let binds = bytes(bw * nb)
|
let binds = bytes(bw * nb)
|
||||||
Vk.zero(binds, bw * nb)
|
Vk.zero(binds, bw * nb)
|
||||||
for k in 0 .. nb {
|
for k in 0 .. nb {
|
||||||
var kind = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER
|
var kind = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER
|
||||||
if k == 0 { kind = VK_DESCRIPTOR_TYPE_UNIFORM_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_binding, k)
|
||||||
Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_descriptorType, kind)
|
Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_descriptorType, kind)
|
||||||
Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_descriptorCount, 1)
|
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)
|
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 }
|
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_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
|
return len(render3d_st.gvk_cp_pipe) - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Record a dispatch of compute program c into the frame, between passes: params (n_params
|
# 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.
|
# 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
|
let zero: long = 0
|
||||||
if render3d_st.gvk_cp_pipe == null or c <= 0 or c >= len(render3d_st.gvk_cp_pipe) { return }
|
if render3d_st.gvk_cp_pipe == null or c <= 0 or c >= len(render3d_st.gvk_cp_pipe) { return }
|
||||||
gvk_pass_end(render3d_st)
|
gvk_pass_end(render3d_st)
|
||||||
let cb = gvk_frame_cb(render3d_st)
|
let cb = gvk_frame_cb(render3d_st)
|
||||||
let nb = render3d_st.gvk_cp_nbuf[c]
|
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)
|
let dsai = bytes(VkDescriptorSetAllocateInfo_sizeof)
|
||||||
Vk.zero(dsai, VkDescriptorSetAllocateInfo_sizeof)
|
Vk.zero(dsai, VkDescriptorSetAllocateInfo_sizeof)
|
||||||
Vk.put_i32(dsai, VkDescriptorSetAllocateInfo_sType, VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO)
|
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 }
|
if at < 0 { print("r3d: vulkan: the frame's uniform ring is full"); return }
|
||||||
let ww = VkWriteDescriptorSet_sizeof
|
let ww = VkWriteDescriptorSet_sizeof
|
||||||
let bw = VkDescriptorBufferInfo_sizeof
|
let bw = VkDescriptorBufferInfo_sizeof
|
||||||
let writes = bytes(ww * (nb + 1))
|
let iw = VkDescriptorImageInfo_sizeof
|
||||||
Vk.zero(writes, ww * (nb + 1))
|
let writes = bytes(ww * (nb + 1 + nt))
|
||||||
|
Vk.zero(writes, ww * (nb + 1 + nt))
|
||||||
let infos = bytes(bw * (nb + 1))
|
let infos = bytes(bw * (nb + 1))
|
||||||
Vk.zero(infos, 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 {
|
for k in 0 .. nb + 1 {
|
||||||
var buf = render3d_st.gvk_ring_buf
|
var buf = render3d_st.gvk_ring_buf
|
||||||
var off: long = 0
|
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_i32(writes, k * ww + VkWriteDescriptorSet_descriptorType, kind)
|
||||||
Vk.put_ptr(writes, k * ww + VkWriteDescriptorSet_pBufferInfo, mem_off(infos, k * bw))
|
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_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_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)
|
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
|
# 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_2_BIT) != 0 { render3d_st.gvk_msaa_max = 2 }
|
||||||
if (counts & VK_SAMPLE_COUNT_4_BIT) != 0 { render3d_st.gvk_msaa_max = 4 }
|
if (counts & VK_SAMPLE_COUNT_4_BIT) != 0 { render3d_st.gvk_msaa_max = 4 }
|
||||||
render3d_st.gvk_ring_buf = gvk_buf_new(render3d_st)
|
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)
|
let sizes = bytes(VkDescriptorPoolSize_sizeof * 3)
|
||||||
Vk.put_i32(sizes, VkDescriptorPoolSize_type, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER)
|
Vk.put_i32(sizes, VkDescriptorPoolSize_type, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER)
|
||||||
Vk.put_i32(sizes, VkDescriptorPoolSize_descriptorCount, 32768)
|
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_i32(dpci, VkDescriptorPoolCreateInfo_poolSizeCount, 3)
|
||||||
Vk.put_ptr(dpci, VkDescriptorPoolCreateInfo_pPoolSizes, sizes)
|
Vk.put_ptr(dpci, VkDescriptorPoolCreateInfo_pPoolSizes, sizes)
|
||||||
let out = bytes(8)
|
let out = bytes(8)
|
||||||
let r = Vk.create_descriptor_pool(render3d_st.gvk_dev, dpci, null, out)
|
render3d_st.gvk_dpools = new []long
|
||||||
if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateDescriptorPool", r) }
|
for k in 0 .. 2 {
|
||||||
render3d_st.gvk_dpool = gvk_handle(out)
|
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 }
|
if not gvk_kpool_make(render3d_st) { return false }
|
||||||
render3d_st.gvk_white = gvk_tex_new(render3d_st)
|
render3d_st.gvk_white = gvk_tex_new(render3d_st)
|
||||||
let px = bytes(4)
|
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 {
|
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)
|
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
|
# 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 {
|
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
|
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)
|
mem_copy(mem_off(render3d_st.gvk_buf_map[render3d_st.gvk_ring_buf], at), blk, n)
|
||||||
render3d_st.gvk_ring_off = at + n
|
render3d_st.gvk_ring_off = at + n
|
||||||
return at
|
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 {
|
function gvk_frame_cb(render3d_st: mut Render3dState) -> pointer {
|
||||||
if render3d_st.gvk_cb == null {
|
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)
|
gvk_frame_reset(render3d_st)
|
||||||
render3d_st.gvk_cb = gvk_once_begin(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 }
|
if render3d_st.gvk_cb == null { return }
|
||||||
render3d_st.gvk_n_flush += 1
|
render3d_st.gvk_n_flush += 1
|
||||||
gvk_pass_end(render3d_st)
|
gvk_pass_end(render3d_st)
|
||||||
|
gvk_labels_close(render3d_st)
|
||||||
gvk_once_end(render3d_st, render3d_st.gvk_cb)
|
gvk_once_end(render3d_st, render3d_st.gvk_cb)
|
||||||
render3d_st.gvk_cb = null
|
render3d_st.gvk_cb = null
|
||||||
render3d_st.gvk_frame_no += 1
|
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 }
|
if render3d_st.gvk_cb == null { return }
|
||||||
gvk_pass_end(render3d_st)
|
gvk_pass_end(render3d_st)
|
||||||
|
gvk_labels_close(render3d_st)
|
||||||
gvk_once_end(render3d_st, render3d_st.gvk_cb)
|
gvk_once_end(render3d_st, render3d_st.gvk_cb)
|
||||||
render3d_st.gvk_cb = null
|
render3d_st.gvk_cb = null
|
||||||
render3d_st.gvk_frame_no += 1
|
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 {
|
function gvk_present_window(render3d_st: mut Render3dState) -> void {
|
||||||
let cb = gvk_frame_cb(render3d_st)
|
let cb = gvk_frame_cb(render3d_st)
|
||||||
gvk_pass_end(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
|
# 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.
|
# hands back once a second. The frame still runs, paced at about 60 Hz, so the game keeps time.
|
||||||
if win_visible() == 0 {
|
if win_visible() == 0 {
|
||||||
|
|
|
||||||
|
|
@ -560,6 +560,9 @@ function gvk_buf_release(render3d_st: mut Render3dState, b: int) -> void {
|
||||||
if gvk_buf_busy(render3d_st, b) {
|
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
|
# 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])
|
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 {
|
} else {
|
||||||
Vk.destroy_buffer(render3d_st.gvk_dev, render3d_st.gvk_buf[b], null)
|
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]))
|
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
|
# 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 {
|
function gvk_buf_busy(render3d_st: Render3dState, b: int) -> bool {
|
||||||
let u = render3d_st.gvk_buf_used[b]
|
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
|
# everything submitted is done: every retired buffer can go
|
||||||
function gvk_retire_flush(render3d_st: mut Render3dState) -> void {
|
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 }
|
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) {
|
for i in 0 .. len(render3d_st.gvk_retired_buf) {
|
||||||
Vk.destroy_buffer(render3d_st.gvk_dev, render3d_st.gvk_retired_buf[i], null)
|
var f = 0
|
||||||
gvk_mem_free(render3d_st, gvk_mem_id(render3d_st.gvk_retired_mem[i]))
|
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_buf = keep_buf
|
||||||
render3d_st.gvk_retired_mem = new []long
|
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,
|
# A buffer a compute pass writes: a draw later in the same frame reads what the GPU put there,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# grass.ludic — procedural GPU ground cover with continuous density (no rings).
|
# 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,
|
# (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
|
# 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
|
# 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.
|
# 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
|
# 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
|
# 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
|
# 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 {
|
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")
|
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 {
|
if render3d_st.grass_merge {
|
||||||
defs = defs + "#define TILES\n"
|
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_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_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_cmds = gpu_buffer_new(render3d_st)
|
||||||
}
|
}
|
||||||
render3d_st.grass_prog = r3d_program(render3d_st, "grass.vert", "model.frag", defs)
|
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
|
# 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
|
# 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_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
|
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
|
# 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
|
# 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.
|
# 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
|
# That is exactly how the last change measured as "no effect": the render was identical
|
||||||
# because this line, not the settings, was deciding.
|
# 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_s0 = 0.066
|
||||||
render3d_st.grass_d0 = 45.0
|
render3d_st.grass_d0 = 18.0
|
||||||
render3d_st.grass_radius = 1600.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_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_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")) }
|
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)
|
# 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
|
# 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 p = render3d_st.grass_prog
|
||||||
let cells = size / GRASS_CELL
|
let cells = size / GRASS_CELL
|
||||||
|
if d_min >= render3d_st.grass_radius { return }
|
||||||
if render3d_st.grass_merge {
|
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 {
|
} else {
|
||||||
u_i(render3d_st, gpu_uniform(render3d_st, p, "u_tile_cells"), cells)
|
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
|
var inst = per * cells * cells
|
||||||
if inst > 65535 { inst = 65535 }
|
if inst > 65535 { inst = 65535 }
|
||||||
let r = render3d_st.grass_n * 5
|
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
|
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
|
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
|
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 {
|
} else if per > 0 {
|
||||||
u_f2(render3d_st, gpu_uniform(render3d_st, p, "u_tile"), ox, oz)
|
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)
|
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
|
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 {
|
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
|
var p = render3d_st.grass_prog
|
||||||
if grass_mesh_live(render3d_st) { p = render3d_st.grass_mesh_prog }
|
if grass_mesh_live(render3d_st) { p = render3d_st.grass_mesh_prog }
|
||||||
gpu_use_program(render3d_st, p)
|
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_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_proj"), render3d_st.cam_proj)
|
||||||
u_mat4(render3d_st, gpu_uniform(render3d_st, p, "u_vp"), render3d_st.cam_vp_clean)
|
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_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_d0"), render3d_st.grass_d0)
|
||||||
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_radius"), render3d_st.grass_radius)
|
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)
|
u_i(render3d_st, gpu_uniform(render3d_st, p, "u_dbg"), render3d_st.grass_dbg)
|
||||||
var orthotex = render3d_st.ter_ortho_tex
|
var orthotex = render3d_st.ter_ortho_tex
|
||||||
var oon = 1.0
|
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
|
# 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.
|
# used 0.05 all along and never showed the fault.
|
||||||
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_spec_scale"), 0.008)
|
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
|
# the per-vertex path's tiles, in four bands
|
||||||
render3d_st.grass_band_n = 0
|
function grass_bands(render3d_st: mut Render3dState) -> void {
|
||||||
gpu_mesh_bind(render3d_st, render3d_st.grass_mesh)
|
# four bands: the tile grows and the blade loses rows with distance (a band past the radius is
|
||||||
grass_tiles(render3d_st, 16, 0.0, 300.0)
|
# skipped); a tile asks for what its NEAREST point needs, so a wide tile far out wastes the rest
|
||||||
grass_tiles(render3d_st, 64, 300.0, 1200.0)
|
grass_tiles(render3d_st, 4, 0.0, 6.0, render3d_st.grass_mesh)
|
||||||
grass_tiles(render3d_st, 256, 1200.0, render3d_st.grass_radius)
|
grass_tiles(render3d_st, 4, 6.0, 16.0, render3d_st.grass_mesh3)
|
||||||
if render3d_st.grass_merge { grass_flush(render3d_st) }
|
grass_tiles(render3d_st, 8, 16.0, 40.0, render3d_st.grass_mesh2)
|
||||||
gpu_cull(render3d_st, true)
|
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
|
# 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
|
var e = render3d_st.grass_n
|
||||||
if b + 1 < render3d_st.grass_band_n { e = render3d_st.grass_band_start[b + 1] }
|
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]) }
|
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
|
var k = s
|
||||||
while k < e {
|
while k < e {
|
||||||
var m = e - k
|
var m = e - k
|
||||||
|
|
@ -273,7 +305,7 @@ function grass_flush(render3d_st: mut Render3dState) -> void {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
u_f4v(render3d_st, gpu_uniform(render3d_st, p, "u_tiles"), m, render3d_st.grass_chunk_tv)
|
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 }
|
if not mesh { render3d_st.grass_draws += 1 }
|
||||||
k += m
|
k += m
|
||||||
|
|
|
||||||
154
packages/ludic.render3d/grass_gpu.ludic
Normal file
154
packages/ludic.render3d/grass_gpu.ludic
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -46,6 +46,7 @@ function prof_slot(render3d_st: mut Render3dState, name: pointer) -> int {
|
||||||
|
|
||||||
function prof_begin(render3d_st: mut Render3dState, name: pointer) -> void {
|
function prof_begin(render3d_st: mut Render3dState, name: pointer) -> void {
|
||||||
ds_set_pass(render3d_st, name)
|
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 not render3d_st.prof_on { return }
|
||||||
if render3d_st.prof_active >= 0 { return } # GL_TIME_ELAPSED queries cannot nest
|
if render3d_st.prof_active >= 0 { return } # GL_TIME_ELAPSED queries cannot nest
|
||||||
let s = prof_slot(render3d_st, name)
|
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 {
|
function prof_end(render3d_st: mut Render3dState) -> void {
|
||||||
ds_set_pass(render3d_st, render3d_st.ds_outside)
|
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 not render3d_st.prof_on { return }
|
||||||
if render3d_st.prof_active < 0 { return }
|
if render3d_st.prof_active < 0 { return }
|
||||||
gpu_query_end(render3d_st)
|
gpu_query_end(render3d_st)
|
||||||
|
|
|
||||||
|
|
@ -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") }
|
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
|
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 }
|
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)
|
return s + r3d_shader_file(render3d_st, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import "scatter.ludic"
|
||||||
import "actor.ludic"
|
import "actor.ludic"
|
||||||
import "stream.ludic"
|
import "stream.ludic"
|
||||||
import "grass.ludic"
|
import "grass.ludic"
|
||||||
|
import "grass_gpu.ludic"
|
||||||
import "water.ludic"
|
import "water.ludic"
|
||||||
import "streamline.ludic"
|
import "streamline.ludic"
|
||||||
import "render.ludic"
|
import "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_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")) }
|
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_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_skip_card = r3d_env_has(render3d_st, "R3D_NOCARDS")
|
||||||
render3d_st.sc_dbg_lod = r3d_env_has(render3d_st, "R3D_LODDBG")
|
render3d_st.sc_dbg_lod = r3d_env_has(render3d_st, "R3D_LODDBG")
|
||||||
if r3d_env_has(render3d_st, "R3D_ANISO") {
|
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")
|
prof_cpu_mark(render3d_st, "shadow rebake")
|
||||||
stream_update_all(render3d_st)
|
stream_update_all(render3d_st)
|
||||||
prof_cpu_mark(render3d_st, "streaming")
|
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) }
|
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")
|
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) }
|
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) }
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ property Layer {
|
||||||
last_cam: floats,
|
last_cam: floats,
|
||||||
rough: float = 0.0,
|
rough: float = 0.0,
|
||||||
blade: bool = false,
|
blade: bool = false,
|
||||||
|
grass: bool = false, # ground cover the GPU blades replace: skipped while they draw (and under R3D_NOGRASS)
|
||||||
flower: bool = false,
|
flower: bool = false,
|
||||||
card: bool = false,
|
card: bool = false,
|
||||||
cheap: bool = false, # distant cover: no shadows, no wind, flat lighting
|
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) {
|
for i in 0 .. len(render3d_st.sc_layers) {
|
||||||
let l = render3d_st.sc_layers[i]
|
let l = render3d_st.sc_layers[i]
|
||||||
if not l.foliage or l.blade or l.flower { continue }
|
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 }
|
if render3d_st.r3d_no_trees and l.imp != null { continue }
|
||||||
layer_update(render3d_st, l)
|
layer_update(render3d_st, l)
|
||||||
if l.n_lods > 1 and l.g_on {
|
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)
|
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 {
|
function scatter_draw(render3d_st: mut Render3dState) -> void {
|
||||||
for i in 0 .. len(render3d_st.sc_layers) {
|
for i in 0 .. len(render3d_st.sc_layers) {
|
||||||
let l = render3d_st.sc_layers[i]
|
let l = render3d_st.sc_layers[i]
|
||||||
if render3d_st.sc_skip_blade and l.blade { continue }
|
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_flower and l.flower { continue }
|
||||||
if render3d_st.sc_skip_card and l.card { 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 }
|
if render3d_st.r3d_no_trees and l.imp != null and not l.card { continue }
|
||||||
layer_update(render3d_st, l)
|
layer_update(render3d_st, l)
|
||||||
layer_draw_near(render3d_st, l, false, null, false)
|
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]
|
let l = render3d_st.sc_layers[i]
|
||||||
if render3d_st.sc_skip_blade and l.blade { continue }
|
if render3d_st.sc_skip_blade and l.blade { continue }
|
||||||
if render3d_st.sc_skip_card and l.card { 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 }
|
if render3d_st.r3d_no_trees and l.imp != null and not l.card { continue }
|
||||||
layer_update(render3d_st, l)
|
layer_update(render3d_st, l)
|
||||||
if l.imp != null {
|
if l.imp != null {
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,5 @@
|
||||||
# spv/<name>.comp.spv. Binding 0 is the uniform block of parameters, 1.. storage buffers.
|
# spv/<name>.comp.spv. Binding 0 is the uniform block of parameters, 1.. storage buffers.
|
||||||
probe|probe.comp
|
probe|probe.comp
|
||||||
scatter_cull|scatter_cull.comp
|
scatter_cull|scatter_cull.comp
|
||||||
|
grass_reset|grass_reset.comp
|
||||||
|
grass_cull|grass_cull.comp
|
||||||
|
|
|
||||||
|
|
@ -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_s0;
|
||||||
uniform float u_d0;
|
uniform float u_d0;
|
||||||
uniform float u_radius;
|
uniform float u_radius;
|
||||||
|
uniform float u_px; // one pixel in radians (grass.vert)
|
||||||
uniform int u_dbg;
|
uniform int u_dbg;
|
||||||
out vec3 v_wpos[];
|
out vec3 v_wpos[];
|
||||||
out vec3 v_nrm[];
|
out vec3 v_nrm[];
|
||||||
|
|
@ -40,7 +41,7 @@ out vec2 v_rot[];
|
||||||
out float v_hull[];
|
out float v_hull[];
|
||||||
out float v_quake[]; // grass does not quake; written so model.frag can read it
|
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); }
|
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; }
|
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) {
|
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 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));
|
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); }
|
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 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 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;
|
float sway = (sin(ph) * 0.6 + sin(ph * 2.3 + 1.0) * 0.4 + gust) * u_wind;
|
||||||
|
|
|
||||||
|
|
@ -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_s0; // blade spacing at the camera (m)
|
||||||
uniform float u_d0; // distance at which the spacing has doubled (m)
|
uniform float u_d0; // distance at which the spacing has doubled (m)
|
||||||
uniform float u_radius; // no blades past this
|
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
|
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_wpos;
|
||||||
out vec3 v_nrm;
|
out vec3 v_nrm;
|
||||||
|
|
@ -42,9 +43,10 @@ out float v_seed;
|
||||||
out vec2 v_rot;
|
out vec2 v_rot;
|
||||||
out float v_hull;
|
out float v_hull;
|
||||||
out float v_quake; // only aspens quake; written so model.frag can read it
|
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;
|
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); }
|
// 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
|
// 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
|
// 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.
|
// 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
|
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;
|
+ (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() {
|
void main() {
|
||||||
#ifdef TILES
|
#ifdef TILES
|
||||||
|
|
@ -158,6 +175,10 @@ void main() {
|
||||||
// that had been cut, which is the one thing an alpine meadow is not.
|
// that had been cut, which is the one thing an alpine meadow is not.
|
||||||
float hh = h3 * h3;
|
float hh = h3 * h3;
|
||||||
float tall = mix(0.09, 0.60, hh) * mix(0.8, 1.2, hash1(cid * 0.1)) * (1.0 + 0.35 * smoothstep(1.0, 12.0, grow)) * life;
|
float tall = mix(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
|
// 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
|
// 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
|
// 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.
|
// and too flat" note in this stage.
|
||||||
float bw = 0.010 * mix(1.0, 0.45 * grow, smoothstep(1.0, 4.0, grow));
|
float bw = 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); }
|
if (far) { bw = max(bw, spacing * 0.35); tall = min(tall, spacing * 0.3); }
|
||||||
|
// a blade under a pixel is a vertex bill that shimmers: widen it to one and a bit, and let the
|
||||||
|
// thinning (spacing) keep the coverage
|
||||||
|
bw = max(bw, dist * u_px * 1.1);
|
||||||
vec3 p = vec3(a_pos.x * bw, a_pos.y * tall, a_pos.z * tall * (0.6 + 0.8 * h4));
|
vec3 p = vec3(a_pos.x * bw, a_pos.y * tall, a_pos.z * tall * (0.6 + 0.8 * h4));
|
||||||
vec3 n = vec3(0.0, 0.3, 1.0);
|
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
|
// 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_seed = seed;
|
||||||
v_rot = vec2(s, c_);
|
v_rot = vec2(s, c_);
|
||||||
v_quake = 0.0;
|
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
|
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);
|
gl_Position = u_proj * u_view * vec4(w, 1.0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
123
packages/ludic.render3d/shaders/grass_cull.comp
Normal file
123
packages/ludic.render3d/shaders/grass_cull.comp
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
71
packages/ludic.render3d/shaders/grass_inst.vert
Normal file
71
packages/ludic.render3d/shaders/grass_inst.vert
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
8
packages/ludic.render3d/shaders/grass_reset.comp
Normal file
8
packages/ludic.render3d/shaders/grass_reset.comp
Normal file
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,9 @@ uniform mat4 u_view;
|
||||||
uniform vec3 u_tint;
|
uniform vec3 u_tint;
|
||||||
uniform float u_rough_scale;
|
uniform float u_rough_scale;
|
||||||
uniform float u_emissive; // self-lit (a flame): albedo added back after shading
|
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
|
#ifdef BLADE
|
||||||
uniform vec3 u_blade_base;
|
uniform vec3 u_blade_base;
|
||||||
uniform vec3 u_blade_tip;
|
uniform vec3 u_blade_tip;
|
||||||
|
|
@ -49,6 +52,11 @@ void main() {
|
||||||
#endif
|
#endif
|
||||||
vec3 N = normalize(v_nrm);
|
vec3 N = normalize(v_nrm);
|
||||||
if (!gl_FrontFacing && v_hull >= 0.0) N = -N;
|
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
|
#ifdef CARD
|
||||||
// a baked card: albedo with coverage (premultiplied mips), normal in the card's own frame
|
// a baked card: albedo with coverage (premultiplied mips), normal in the card's own frame
|
||||||
vec4 ca = texture(u_diff, v_uv);
|
vec4 ca = texture(u_diff, v_uv);
|
||||||
|
|
@ -80,7 +88,16 @@ void main() {
|
||||||
#ifdef BLADE
|
#ifdef BLADE
|
||||||
// a procedural blade: dark at the root, lighter at the tip; some blades gone to seed
|
// 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);
|
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));
|
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);
|
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
|
// 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);
|
vec3 g = photo * (1.0 / 0.3736);
|
||||||
alb *= mix(vec3(1.0), min(g, vec3(1.15)), 0.85);
|
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
|
// 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
|
// 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));
|
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);
|
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
|
// 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
|
// 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 -
|
// walking-distance shot the blades came out at 24-31 of 255 against a ground of 143 -
|
||||||
|
|
@ -227,6 +244,13 @@ void main() {
|
||||||
#else
|
#else
|
||||||
float shadow = mix(1.0, sunShadow(v_wpos + N * 0.1 + vec3(0.0, 0.2, 0.0), N, viewDepth), 0.55);
|
float shadow = mix(1.0, sunShadow(v_wpos + N * 0.1 + vec3(0.0, 0.2, 0.0), N, viewDepth), 0.55);
|
||||||
#endif
|
#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
|
#else
|
||||||
float shadow = sunShadow(v_wpos, N, viewDepth);
|
float shadow = sunShadow(v_wpos, N, viewDepth);
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -271,6 +295,9 @@ void main() {
|
||||||
float NoV = max(dot(n, vv), 0.0);
|
float NoV = max(dot(n, vv), 0.0);
|
||||||
float sheen = pow(NoH, 26.0) * 0.55 + pow(1.0 - NoV, 4.0) * 0.05;
|
float sheen = pow(NoH, 26.0) * 0.55 + pow(1.0 - NoV, 4.0) * 0.05;
|
||||||
sheen *= smoothstep(0.10, 0.42, NoV);
|
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);
|
col += u_sun_color * sheen * shadow * cloudShadow(v_wpos) * 0.4 * (0.45 + 0.55 * arm.r);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
packages/ludic.render3d/shaders/spv/b20fdcb5.frag.spv
Normal file
BIN
packages/ludic.render3d/shaders/spv/b20fdcb5.frag.spv
Normal file
Binary file not shown.
BIN
packages/ludic.render3d/shaders/spv/b20fdcb5.vert.spv
Normal file
BIN
packages/ludic.render3d/shaders/spv/b20fdcb5.vert.spv
Normal file
Binary file not shown.
BIN
packages/ludic.render3d/shaders/spv/b8cff226.frag.spv
Normal file
BIN
packages/ludic.render3d/shaders/spv/b8cff226.frag.spv
Normal file
Binary file not shown.
BIN
packages/ludic.render3d/shaders/spv/b8cff226.vert.spv
Normal file
BIN
packages/ludic.render3d/shaders/spv/b8cff226.vert.spv
Normal file
Binary file not shown.
BIN
packages/ludic.render3d/shaders/spv/cb13f3c6.frag.spv
Normal file
BIN
packages/ludic.render3d/shaders/spv/cb13f3c6.frag.spv
Normal file
Binary file not shown.
BIN
packages/ludic.render3d/shaders/spv/cb13f3c6.vert.spv
Normal file
BIN
packages/ludic.render3d/shaders/spv/cb13f3c6.vert.spv
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
packages/ludic.render3d/shaders/spv/grass_cull.comp.spv
Normal file
BIN
packages/ludic.render3d/shaders/spv/grass_cull.comp.spv
Normal file
Binary file not shown.
BIN
packages/ludic.render3d/shaders/spv/grass_reset.comp.spv
Normal file
BIN
packages/ludic.render3d/shaders/spv/grass_reset.comp.spv
Normal file
Binary file not shown.
|
|
@ -843,7 +843,7 @@ T 74258189 u_shadow 6
|
||||||
T 74258189 u_tershadow 7
|
T 74258189 u_tershadow 7
|
||||||
T 74258189 u_ts_height 8
|
T 74258189 u_ts_height 8
|
||||||
P d48d6a48 grass.mesh model.frag #define FOLIAGE;#define BLADE;#define MESH;
|
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_view 0 mat4 1 0
|
||||||
U d48d6a48 vert u_proj 64 mat4 1 0
|
U d48d6a48 vert u_proj 64 mat4 1 0
|
||||||
U d48d6a48 vert u_vp 128 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_s0 4368 float 1 0
|
||||||
U d48d6a48 vert u_d0 4372 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_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
|
B d48d6a48 frag 1 964
|
||||||
U d48d6a48 frag u_cascade_vp 0 mat4 5 64
|
U d48d6a48 frag u_cascade_vp 0 mat4 5 64
|
||||||
U d48d6a48 frag u_cascade_split 320 float 5 16
|
U d48d6a48 frag u_cascade_split 320 float 5 16
|
||||||
|
|
@ -919,169 +920,171 @@ T d48d6a48 u_prefilter 9
|
||||||
T d48d6a48 u_shadow 10
|
T d48d6a48 u_shadow 10
|
||||||
T d48d6a48 u_tershadow 11
|
T d48d6a48 u_tershadow 11
|
||||||
T d48d6a48 u_ts_height 12
|
T d48d6a48 u_ts_height 12
|
||||||
P 7ed7de52 grass.vert model.frag #define FOLIAGE;#define BLADE;
|
P cb13f3c6 grass.vert model.frag #define FOLIAGE;#define BLADE;#define GBLADE;
|
||||||
B 7ed7de52 vert 0 320
|
B cb13f3c6 vert 0 324
|
||||||
U 7ed7de52 vert u_view 0 mat4 1 0
|
U cb13f3c6 vert u_view 0 mat4 1 0
|
||||||
U 7ed7de52 vert u_proj 64 mat4 1 0
|
U cb13f3c6 vert u_proj 64 mat4 1 0
|
||||||
U 7ed7de52 vert u_vp 128 mat4 1 0
|
U cb13f3c6 vert u_vp 128 mat4 1 0
|
||||||
U 7ed7de52 vert u_cam_pos 192 vec3 1 0
|
U cb13f3c6 vert u_cam_pos 192 vec3 1 0
|
||||||
U 7ed7de52 vert u_time 204 float 1 0
|
U cb13f3c6 vert u_time 204 float 1 0
|
||||||
U 7ed7de52 vert u_ts_origin 208 vec2 1 0
|
U cb13f3c6 vert u_ts_origin 208 vec2 1 0
|
||||||
U 7ed7de52 vert u_ts_half 216 float 1 0
|
U cb13f3c6 vert u_ts_half 216 float 1 0
|
||||||
U 7ed7de52 vert u_ortho_on 220 float 1 0
|
U cb13f3c6 vert u_ortho_on 220 float 1 0
|
||||||
U 7ed7de52 vert u_lake_level 224 float 1 0
|
U cb13f3c6 vert u_lake_level 224 float 1 0
|
||||||
U 7ed7de52 vert u_sea_level 228 float 1 0
|
U cb13f3c6 vert u_sea_level 228 float 1 0
|
||||||
U 7ed7de52 vert u_lake 240 vec4 1 0
|
U cb13f3c6 vert u_lake 240 vec4 1 0
|
||||||
U 7ed7de52 vert u_snow_line 256 float 1 0
|
U cb13f3c6 vert u_snow_line 256 float 1 0
|
||||||
U 7ed7de52 vert u_wind 260 float 1 0
|
U cb13f3c6 vert u_wind 260 float 1 0
|
||||||
U 7ed7de52 vert u_push 272 vec3 1 0
|
U cb13f3c6 vert u_push 272 vec3 1 0
|
||||||
U 7ed7de52 vert u_tile 288 vec2 1 0
|
U cb13f3c6 vert u_tile 288 vec2 1 0
|
||||||
U 7ed7de52 vert u_tile_cells 296 int 1 0
|
U cb13f3c6 vert u_tile_cells 296 int 1 0
|
||||||
U 7ed7de52 vert u_per_cell 300 int 1 0
|
U cb13f3c6 vert u_per_cell 300 int 1 0
|
||||||
U 7ed7de52 vert u_s0 304 float 1 0
|
U cb13f3c6 vert u_s0 304 float 1 0
|
||||||
U 7ed7de52 vert u_d0 308 float 1 0
|
U cb13f3c6 vert u_d0 308 float 1 0
|
||||||
U 7ed7de52 vert u_radius 312 float 1 0
|
U cb13f3c6 vert u_radius 312 float 1 0
|
||||||
U 7ed7de52 vert u_dbg 316 int 1 0
|
U cb13f3c6 vert u_px 316 float 1 0
|
||||||
I 7ed7de52 a_pos 0 vec3
|
U cb13f3c6 vert u_dbg 320 int 1 0
|
||||||
I 7ed7de52 a_uv 2 vec2
|
I cb13f3c6 a_pos 0 vec3
|
||||||
B 7ed7de52 frag 1 964
|
I cb13f3c6 a_uv 2 vec2
|
||||||
U 7ed7de52 frag u_cascade_vp 0 mat4 5 64
|
B cb13f3c6 frag 1 964
|
||||||
U 7ed7de52 frag u_cascade_split 320 float 5 16
|
U cb13f3c6 frag u_cascade_vp 0 mat4 5 64
|
||||||
U 7ed7de52 frag u_cascade_range 400 float 5 16
|
U cb13f3c6 frag u_cascade_split 320 float 5 16
|
||||||
U 7ed7de52 frag u_cascade_texel 480 float 5 16
|
U cb13f3c6 frag u_cascade_range 400 float 5 16
|
||||||
U 7ed7de52 frag u_sun_dir 560 vec3 1 0
|
U cb13f3c6 frag u_cascade_texel 480 float 5 16
|
||||||
U 7ed7de52 frag u_sun_color 576 vec3 1 0
|
U cb13f3c6 frag u_sun_dir 560 vec3 1 0
|
||||||
U 7ed7de52 frag u_cam_pos 592 vec3 1 0
|
U cb13f3c6 frag u_sun_color 576 vec3 1 0
|
||||||
U 7ed7de52 frag u_prefilter_levels 604 float 1 0
|
U cb13f3c6 frag u_cam_pos 592 vec3 1 0
|
||||||
U 7ed7de52 frag u_fog_density 608 float 1 0
|
U cb13f3c6 frag u_prefilter_levels 604 float 1 0
|
||||||
U 7ed7de52 frag u_fog_height_falloff 612 float 1 0
|
U cb13f3c6 frag u_fog_density 608 float 1 0
|
||||||
U 7ed7de52 frag u_fog_base 616 float 1 0
|
U cb13f3c6 frag u_fog_height_falloff 612 float 1 0
|
||||||
U 7ed7de52 frag u_clip_y 620 float 1 0
|
U cb13f3c6 frag u_fog_base 616 float 1 0
|
||||||
U 7ed7de52 frag u_spec_scale 624 float 1 0
|
U cb13f3c6 frag u_clip_y 620 float 1 0
|
||||||
U 7ed7de52 frag u_sky_rot 632 vec2 1 0
|
U cb13f3c6 frag u_spec_scale 624 float 1 0
|
||||||
U 7ed7de52 frag u_ground_alb 640 vec3 1 0
|
U cb13f3c6 frag u_sky_rot 632 vec2 1 0
|
||||||
U 7ed7de52 frag u_ground_alb_hi 656 vec3 1 0
|
U cb13f3c6 frag u_ground_alb 640 vec3 1 0
|
||||||
U 7ed7de52 frag u_ground_hi_y 668 float 1 0
|
U cb13f3c6 frag u_ground_alb_hi 656 vec3 1 0
|
||||||
U 7ed7de52 frag u_ground_hi_w 672 float 1 0
|
U cb13f3c6 frag u_ground_hi_y 668 float 1 0
|
||||||
U 7ed7de52 frag u_ibl_scale 688 vec3 1 0
|
U cb13f3c6 frag u_ground_hi_w 672 float 1 0
|
||||||
U 7ed7de52 frag u_daylight 700 float 1 0
|
U cb13f3c6 frag u_ibl_scale 688 vec3 1 0
|
||||||
U 7ed7de52 frag u_fire_pos 704 vec3 1 0
|
U cb13f3c6 frag u_daylight 700 float 1 0
|
||||||
U 7ed7de52 frag u_fire_color 720 vec3 1 0
|
U cb13f3c6 frag u_fire_pos 704 vec3 1 0
|
||||||
U 7ed7de52 frag u_hand_pos 736 vec3 1 0
|
U cb13f3c6 frag u_fire_color 720 vec3 1 0
|
||||||
U 7ed7de52 frag u_hand_color 752 vec3 1 0
|
U cb13f3c6 frag u_hand_pos 736 vec3 1 0
|
||||||
U 7ed7de52 frag u_hand_dir 768 vec3 1 0
|
U cb13f3c6 frag u_hand_color 752 vec3 1 0
|
||||||
U 7ed7de52 frag u_hand_cone 780 float 1 0
|
U cb13f3c6 frag u_hand_dir 768 vec3 1 0
|
||||||
U 7ed7de52 frag u_hand_reach 784 float 1 0
|
U cb13f3c6 frag u_hand_cone 780 float 1 0
|
||||||
U 7ed7de52 frag u_ts_origin 792 vec2 1 0
|
U cb13f3c6 frag u_hand_reach 784 float 1 0
|
||||||
U 7ed7de52 frag u_ts_half 800 float 1 0
|
U cb13f3c6 frag u_ts_origin 792 vec2 1 0
|
||||||
U 7ed7de52 frag u_ts_on 804 float 1 0
|
U cb13f3c6 frag u_ts_half 800 float 1 0
|
||||||
U 7ed7de52 frag u_force_cascade 808 int 1 0
|
U cb13f3c6 frag u_ts_on 804 float 1 0
|
||||||
U 7ed7de52 frag u_cloud_shadow 812 float 1 0
|
U cb13f3c6 frag u_force_cascade 808 int 1 0
|
||||||
U 7ed7de52 frag u_time 816 float 1 0
|
U cb13f3c6 frag u_cloud_shadow 812 float 1 0
|
||||||
U 7ed7de52 frag u_fog_inscatter 820 float 1 0
|
U cb13f3c6 frag u_time 816 float 1 0
|
||||||
U 7ed7de52 frag u_fog_desat 824 float 1 0
|
U cb13f3c6 frag u_fog_inscatter 820 float 1 0
|
||||||
U 7ed7de52 frag u_model_h 828 float 1 0
|
U cb13f3c6 frag u_fog_desat 824 float 1 0
|
||||||
U 7ed7de52 frag u_view 832 mat4 1 0
|
U cb13f3c6 frag u_model_h 828 float 1 0
|
||||||
U 7ed7de52 frag u_tint 896 vec3 1 0
|
U cb13f3c6 frag u_view 832 mat4 1 0
|
||||||
U 7ed7de52 frag u_rough_scale 908 float 1 0
|
U cb13f3c6 frag u_tint 896 vec3 1 0
|
||||||
U 7ed7de52 frag u_emissive 912 float 1 0
|
U cb13f3c6 frag u_rough_scale 908 float 1 0
|
||||||
U 7ed7de52 frag u_blade_base 928 vec3 1 0
|
U cb13f3c6 frag u_emissive 912 float 1 0
|
||||||
U 7ed7de52 frag u_blade_tip 944 vec3 1 0
|
U cb13f3c6 frag u_blade_base 928 vec3 1 0
|
||||||
U 7ed7de52 frag u_blade_cols 956 float 1 0
|
U cb13f3c6 frag u_blade_tip 944 vec3 1 0
|
||||||
U 7ed7de52 frag u_blade_tex_on 960 float 1 0
|
U cb13f3c6 frag u_blade_cols 956 float 1 0
|
||||||
T 7ed7de52 u_arm 2
|
U cb13f3c6 frag u_blade_tex_on 960 float 1 0
|
||||||
T 7ed7de52 u_blade_tex 3
|
T cb13f3c6 u_arm 2
|
||||||
T 7ed7de52 u_brdf 4
|
T cb13f3c6 u_blade_tex 3
|
||||||
T 7ed7de52 u_diff 5
|
T cb13f3c6 u_brdf 4
|
||||||
T 7ed7de52 u_irradiance 6
|
T cb13f3c6 u_diff 5
|
||||||
T 7ed7de52 u_nrm 7
|
T cb13f3c6 u_irradiance 6
|
||||||
T 7ed7de52 u_ortho 8
|
T cb13f3c6 u_nrm 7
|
||||||
T 7ed7de52 u_prefilter 9
|
T cb13f3c6 u_ortho 8
|
||||||
T 7ed7de52 u_shadow 10
|
T cb13f3c6 u_prefilter 9
|
||||||
T 7ed7de52 u_tershadow 11
|
T cb13f3c6 u_shadow 10
|
||||||
T 7ed7de52 u_ts_height 12
|
T cb13f3c6 u_tershadow 11
|
||||||
P 7f096322 grass.vert model.frag #define FOLIAGE;#define BLADE;#define TILES;
|
T cb13f3c6 u_ts_height 12
|
||||||
B 7f096322 vert 0 4416
|
P b8cff226 grass.vert model.frag #define FOLIAGE;#define BLADE;#define GBLADE;#define TILES;
|
||||||
U 7f096322 vert u_view 0 mat4 1 0
|
B b8cff226 vert 0 4420
|
||||||
U 7f096322 vert u_proj 64 mat4 1 0
|
U b8cff226 vert u_view 0 mat4 1 0
|
||||||
U 7f096322 vert u_vp 128 mat4 1 0
|
U b8cff226 vert u_proj 64 mat4 1 0
|
||||||
U 7f096322 vert u_cam_pos 192 vec3 1 0
|
U b8cff226 vert u_vp 128 mat4 1 0
|
||||||
U 7f096322 vert u_time 204 float 1 0
|
U b8cff226 vert u_cam_pos 192 vec3 1 0
|
||||||
U 7f096322 vert u_ts_origin 208 vec2 1 0
|
U b8cff226 vert u_time 204 float 1 0
|
||||||
U 7f096322 vert u_ts_half 216 float 1 0
|
U b8cff226 vert u_ts_origin 208 vec2 1 0
|
||||||
U 7f096322 vert u_ortho_on 220 float 1 0
|
U b8cff226 vert u_ts_half 216 float 1 0
|
||||||
U 7f096322 vert u_lake_level 224 float 1 0
|
U b8cff226 vert u_ortho_on 220 float 1 0
|
||||||
U 7f096322 vert u_sea_level 228 float 1 0
|
U b8cff226 vert u_lake_level 224 float 1 0
|
||||||
U 7f096322 vert u_lake 240 vec4 1 0
|
U b8cff226 vert u_sea_level 228 float 1 0
|
||||||
U 7f096322 vert u_snow_line 256 float 1 0
|
U b8cff226 vert u_lake 240 vec4 1 0
|
||||||
U 7f096322 vert u_wind 260 float 1 0
|
U b8cff226 vert u_snow_line 256 float 1 0
|
||||||
U 7f096322 vert u_push 272 vec3 1 0
|
U b8cff226 vert u_wind 260 float 1 0
|
||||||
U 7f096322 vert u_tile 288 vec2 1 0
|
U b8cff226 vert u_push 272 vec3 1 0
|
||||||
U 7f096322 vert u_tile_cells 296 int 1 0
|
U b8cff226 vert u_tile 288 vec2 1 0
|
||||||
U 7f096322 vert u_per_cell 300 int 1 0
|
U b8cff226 vert u_tile_cells 296 int 1 0
|
||||||
U 7f096322 vert u_tiles 304 vec4 256 16
|
U b8cff226 vert u_per_cell 300 int 1 0
|
||||||
U 7f096322 vert u_s0 4400 float 1 0
|
U b8cff226 vert u_tiles 304 vec4 256 16
|
||||||
U 7f096322 vert u_d0 4404 float 1 0
|
U b8cff226 vert u_s0 4400 float 1 0
|
||||||
U 7f096322 vert u_radius 4408 float 1 0
|
U b8cff226 vert u_d0 4404 float 1 0
|
||||||
U 7f096322 vert u_dbg 4412 int 1 0
|
U b8cff226 vert u_radius 4408 float 1 0
|
||||||
I 7f096322 a_pos 0 vec3
|
U b8cff226 vert u_px 4412 float 1 0
|
||||||
I 7f096322 a_uv 2 vec2
|
U b8cff226 vert u_dbg 4416 int 1 0
|
||||||
B 7f096322 frag 1 964
|
I b8cff226 a_pos 0 vec3
|
||||||
U 7f096322 frag u_cascade_vp 0 mat4 5 64
|
I b8cff226 a_uv 2 vec2
|
||||||
U 7f096322 frag u_cascade_split 320 float 5 16
|
B b8cff226 frag 1 964
|
||||||
U 7f096322 frag u_cascade_range 400 float 5 16
|
U b8cff226 frag u_cascade_vp 0 mat4 5 64
|
||||||
U 7f096322 frag u_cascade_texel 480 float 5 16
|
U b8cff226 frag u_cascade_split 320 float 5 16
|
||||||
U 7f096322 frag u_sun_dir 560 vec3 1 0
|
U b8cff226 frag u_cascade_range 400 float 5 16
|
||||||
U 7f096322 frag u_sun_color 576 vec3 1 0
|
U b8cff226 frag u_cascade_texel 480 float 5 16
|
||||||
U 7f096322 frag u_cam_pos 592 vec3 1 0
|
U b8cff226 frag u_sun_dir 560 vec3 1 0
|
||||||
U 7f096322 frag u_prefilter_levels 604 float 1 0
|
U b8cff226 frag u_sun_color 576 vec3 1 0
|
||||||
U 7f096322 frag u_fog_density 608 float 1 0
|
U b8cff226 frag u_cam_pos 592 vec3 1 0
|
||||||
U 7f096322 frag u_fog_height_falloff 612 float 1 0
|
U b8cff226 frag u_prefilter_levels 604 float 1 0
|
||||||
U 7f096322 frag u_fog_base 616 float 1 0
|
U b8cff226 frag u_fog_density 608 float 1 0
|
||||||
U 7f096322 frag u_clip_y 620 float 1 0
|
U b8cff226 frag u_fog_height_falloff 612 float 1 0
|
||||||
U 7f096322 frag u_spec_scale 624 float 1 0
|
U b8cff226 frag u_fog_base 616 float 1 0
|
||||||
U 7f096322 frag u_sky_rot 632 vec2 1 0
|
U b8cff226 frag u_clip_y 620 float 1 0
|
||||||
U 7f096322 frag u_ground_alb 640 vec3 1 0
|
U b8cff226 frag u_spec_scale 624 float 1 0
|
||||||
U 7f096322 frag u_ground_alb_hi 656 vec3 1 0
|
U b8cff226 frag u_sky_rot 632 vec2 1 0
|
||||||
U 7f096322 frag u_ground_hi_y 668 float 1 0
|
U b8cff226 frag u_ground_alb 640 vec3 1 0
|
||||||
U 7f096322 frag u_ground_hi_w 672 float 1 0
|
U b8cff226 frag u_ground_alb_hi 656 vec3 1 0
|
||||||
U 7f096322 frag u_ibl_scale 688 vec3 1 0
|
U b8cff226 frag u_ground_hi_y 668 float 1 0
|
||||||
U 7f096322 frag u_daylight 700 float 1 0
|
U b8cff226 frag u_ground_hi_w 672 float 1 0
|
||||||
U 7f096322 frag u_fire_pos 704 vec3 1 0
|
U b8cff226 frag u_ibl_scale 688 vec3 1 0
|
||||||
U 7f096322 frag u_fire_color 720 vec3 1 0
|
U b8cff226 frag u_daylight 700 float 1 0
|
||||||
U 7f096322 frag u_hand_pos 736 vec3 1 0
|
U b8cff226 frag u_fire_pos 704 vec3 1 0
|
||||||
U 7f096322 frag u_hand_color 752 vec3 1 0
|
U b8cff226 frag u_fire_color 720 vec3 1 0
|
||||||
U 7f096322 frag u_hand_dir 768 vec3 1 0
|
U b8cff226 frag u_hand_pos 736 vec3 1 0
|
||||||
U 7f096322 frag u_hand_cone 780 float 1 0
|
U b8cff226 frag u_hand_color 752 vec3 1 0
|
||||||
U 7f096322 frag u_hand_reach 784 float 1 0
|
U b8cff226 frag u_hand_dir 768 vec3 1 0
|
||||||
U 7f096322 frag u_ts_origin 792 vec2 1 0
|
U b8cff226 frag u_hand_cone 780 float 1 0
|
||||||
U 7f096322 frag u_ts_half 800 float 1 0
|
U b8cff226 frag u_hand_reach 784 float 1 0
|
||||||
U 7f096322 frag u_ts_on 804 float 1 0
|
U b8cff226 frag u_ts_origin 792 vec2 1 0
|
||||||
U 7f096322 frag u_force_cascade 808 int 1 0
|
U b8cff226 frag u_ts_half 800 float 1 0
|
||||||
U 7f096322 frag u_cloud_shadow 812 float 1 0
|
U b8cff226 frag u_ts_on 804 float 1 0
|
||||||
U 7f096322 frag u_time 816 float 1 0
|
U b8cff226 frag u_force_cascade 808 int 1 0
|
||||||
U 7f096322 frag u_fog_inscatter 820 float 1 0
|
U b8cff226 frag u_cloud_shadow 812 float 1 0
|
||||||
U 7f096322 frag u_fog_desat 824 float 1 0
|
U b8cff226 frag u_time 816 float 1 0
|
||||||
U 7f096322 frag u_model_h 828 float 1 0
|
U b8cff226 frag u_fog_inscatter 820 float 1 0
|
||||||
U 7f096322 frag u_view 832 mat4 1 0
|
U b8cff226 frag u_fog_desat 824 float 1 0
|
||||||
U 7f096322 frag u_tint 896 vec3 1 0
|
U b8cff226 frag u_model_h 828 float 1 0
|
||||||
U 7f096322 frag u_rough_scale 908 float 1 0
|
U b8cff226 frag u_view 832 mat4 1 0
|
||||||
U 7f096322 frag u_emissive 912 float 1 0
|
U b8cff226 frag u_tint 896 vec3 1 0
|
||||||
U 7f096322 frag u_blade_base 928 vec3 1 0
|
U b8cff226 frag u_rough_scale 908 float 1 0
|
||||||
U 7f096322 frag u_blade_tip 944 vec3 1 0
|
U b8cff226 frag u_emissive 912 float 1 0
|
||||||
U 7f096322 frag u_blade_cols 956 float 1 0
|
U b8cff226 frag u_blade_base 928 vec3 1 0
|
||||||
U 7f096322 frag u_blade_tex_on 960 float 1 0
|
U b8cff226 frag u_blade_tip 944 vec3 1 0
|
||||||
T 7f096322 u_arm 2
|
U b8cff226 frag u_blade_cols 956 float 1 0
|
||||||
T 7f096322 u_blade_tex 3
|
U b8cff226 frag u_blade_tex_on 960 float 1 0
|
||||||
T 7f096322 u_brdf 4
|
T b8cff226 u_arm 2
|
||||||
T 7f096322 u_diff 5
|
T b8cff226 u_blade_tex 3
|
||||||
T 7f096322 u_irradiance 6
|
T b8cff226 u_brdf 4
|
||||||
T 7f096322 u_nrm 7
|
T b8cff226 u_diff 5
|
||||||
T 7f096322 u_ortho 8
|
T b8cff226 u_irradiance 6
|
||||||
T 7f096322 u_prefilter 9
|
T b8cff226 u_nrm 7
|
||||||
T 7f096322 u_shadow 10
|
T b8cff226 u_ortho 8
|
||||||
T 7f096322 u_tershadow 11
|
T b8cff226 u_prefilter 9
|
||||||
T 7f096322 u_ts_height 12
|
T b8cff226 u_shadow 10
|
||||||
|
T b8cff226 u_tershadow 11
|
||||||
|
T b8cff226 u_ts_height 12
|
||||||
P 3733fc38 impostor.vert impostor.frag
|
P 3733fc38 impostor.vert impostor.frag
|
||||||
B 3733fc38 vert 0 232
|
B 3733fc38 vert 0 232
|
||||||
U 3733fc38 vert u_view 0 mat4 1 0
|
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_shadow 8
|
||||||
T 46e2325c u_tershadow 9
|
T 46e2325c u_tershadow 9
|
||||||
T 46e2325c u_ts_height 10
|
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
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@ fullscreen.vert|tonemap.frag|
|
||||||
fullscreen.vert|tonemap.frag|#define HDR10;
|
fullscreen.vert|tonemap.frag|#define HDR10;
|
||||||
fullscreen.vert|volumetric.frag|
|
fullscreen.vert|volumetric.frag|
|
||||||
grass.mesh|model.frag|#define FOLIAGE;#define BLADE;#define MESH;
|
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 GBLADE;
|
||||||
grass.vert|model.frag|#define FOLIAGE;#define BLADE;#define TILES;
|
grass.vert|model.frag|#define FOLIAGE;#define BLADE;#define GBLADE;#define TILES;
|
||||||
impostor.vert|impostor.frag|
|
impostor.vert|impostor.frag|
|
||||||
impostor.vert|impostor.frag|#define SHADOW_PASS;
|
impostor.vert|impostor.frag|#define SHADOW_PASS;
|
||||||
model.vert|bake.frag|
|
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|terrain.frag|#define SUN_INLINE;#define NEAR_ONLY;
|
||||||
terrain.vert|tersun.frag|
|
terrain.vert|tersun.frag|
|
||||||
water.vert|water.frag|
|
water.vert|water.frag|
|
||||||
|
grass_inst.vert|model.frag|#define FOLIAGE;#define BLADE;#define GBLADE;#define GINST;
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,11 @@ through the game).
|
||||||
the oars pulled against each other - and reads back where the hull is and how fast it goes. The
|
the oars pulled against each other - and reads back where the hull is and how fast it goes. The
|
||||||
water floats it, the wind (`VehicleWorld.wind`) drifts every boat, ridden or not, and the lake bed
|
water floats it, the wind (`VehicleWorld.wind`) drifts every boat, ridden or not, and the lake bed
|
||||||
stops it at the shore; the swell at the sail limit takes the outward share of a stroke.
|
stops it at the shore; the swell at the sail limit takes the outward share of a stroke.
|
||||||
|
- **A horse walks on legs in the game's physics** (`VehicleLegs`; a Jolt walker in Maroon Lake). The
|
||||||
|
package decides its pace - a walk, a canter on stamina, hay for every metre - and never steps it
|
||||||
|
into water; the legs meet the ground, its steps and slopes, and whatever stands in its way. What
|
||||||
|
they cover along its heading is its speed, so a fence brings it to a stand and a trunk it glances
|
||||||
|
pushes it aside. A horse called to its bay or led round where it is tied is stood there (`stand`).
|
||||||
- **A horse's steering follows signed speed**: backing up with the stick left swings the nose right.
|
- **A horse's steering follows signed speed**: backing up with the stick left swings the nose right.
|
||||||
- A stale rider locks a vehicle for ever, so getting off is `vehicle_dismount` or `vehicle_drop`.
|
- A stale rider locks a vehicle for ever, so getting off is `vehicle_dismount` or `vehicle_drop`.
|
||||||
|
|
||||||
|
|
@ -35,12 +40,13 @@ through the game).
|
||||||
|
|
||||||
`VehicleHull { put(nid, x, y, z, yaw), row(nid, forward, turn), drift(nid, fx, fz), read(nid) -> bool,
|
`VehicleHull { put(nid, x, y, z, yaw), row(nid, forward, turn), drift(nid, fx, fz), read(nid) -> bool,
|
||||||
x, y, z, yaw, speed, gone(nid) }` is required: the boat's body, named by the vehicle's nid.
|
x, y, z, yaw, speed, gone(nid) }` is required: the boat's body, named by the vehicle's nid.
|
||||||
|
`VehicleLegs { walk(nid, x, y, z, vx, vz, dt) -> bool, x, y, z, stand(nid, x, y, z), gone(nid) }` is the
|
||||||
|
horse's: unbound, the ground is walked as it is and only a rise steeper than it can take stops it.
|
||||||
|
|
||||||
```ludic
|
```ludic
|
||||||
export port VehicleWorld {
|
export port VehicleWorld {
|
||||||
ground: fn(float, float) -> float # required
|
ground: fn(float, float) -> float # required
|
||||||
water: fn(float, float) -> float # the surface (unbound: never wet)
|
water: fn(float, float) -> float # the surface (unbound: never wet)
|
||||||
blocked: fn(float, float, float) -> bool # (x, z, r): a collider in a horse's way
|
|
||||||
swell: fn(float, float, float, float) -> float # (x, z, fx, fz): a stroke's share lost
|
swell: fn(float, float, float, float) -> float # (x, z, fx, fz): a stroke's share lost
|
||||||
wind: fn() -> float # 0..1
|
wind: fn() -> float # 0..1
|
||||||
wind_yaw: fn() -> float # the yaw the wind pushes toward
|
wind_yaw: fn() -> float # the yaw the wind pushes toward
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,14 @@ function ve_add(vehicles_st: VehiclesState, kind: int, x: float, z: float, yaw:
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
# a boat's hull put where the vehicle now is, at rest
|
# a boat's hull or a horse's legs put where the vehicle now is, at rest
|
||||||
function ve_settle(v: Vehicle) -> void { if v.kind == VEHICLE_BOAT { VehicleHull.put(v.nid, v.x, v.y, v.z, v.yaw) } }
|
function ve_settle(v: Vehicle) -> void {
|
||||||
|
if v.kind == VEHICLE_BOAT { VehicleHull.put(v.nid, v.x, v.y, v.z, v.yaw) } else { VehicleLegs.stand(v.nid, v.x, v.y, v.z) }
|
||||||
|
}
|
||||||
|
|
||||||
# gone from the world: its model and, for a boat, its hull
|
# gone from the world: its model and its hull or legs
|
||||||
function ve_gone(v: Vehicle) -> void {
|
function ve_gone(v: Vehicle) -> void {
|
||||||
if v.kind == VEHICLE_BOAT { VehicleHull.gone(v.nid) }
|
if v.kind == VEHICLE_BOAT { VehicleHull.gone(v.nid) } else { VehicleLegs.gone(v.nid) }
|
||||||
VehicleWorld.gone(v)
|
VehicleWorld.gone(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,40 +75,3 @@ export function vehicles_release(vehicles_st: mut VehiclesState, pid: int) -> vo
|
||||||
}
|
}
|
||||||
vehicles_st.ve_list = keep
|
vehicles_st.ve_list = keep
|
||||||
}
|
}
|
||||||
|
|
||||||
# hired or rented for the trip
|
|
||||||
export function vehicle_own(v: Vehicle) -> void { if v != null { v.owned = true } }
|
|
||||||
|
|
||||||
# hay for a horse, up to a full belly
|
|
||||||
export function vehicle_feed(vehicles_st: VehiclesState, v: Vehicle, amount: float) -> void {
|
|
||||||
if v == null { return }
|
|
||||||
v.food = Math.min(v.food + amount, 100.0)
|
|
||||||
ve_say(vehicles_st, VEHICLE_FED, v, amount)
|
|
||||||
}
|
|
||||||
|
|
||||||
# a horse nobody rides goes where its animal goes, round the home it is tied at (home_x, home_z)
|
|
||||||
export function vehicle_follow(v: Vehicle, x: float, y: float, z: float, yaw: float) -> void {
|
|
||||||
if v == null or v.rider >= 0 { return }
|
|
||||||
v.x = x
|
|
||||||
v.y = y
|
|
||||||
v.z = z
|
|
||||||
v.yaw = yaw
|
|
||||||
}
|
|
||||||
|
|
||||||
# a saved trip's word on this player's own: hired, and how fed
|
|
||||||
export function vehicle_restore(vehicles_st: mut VehiclesState, kind: int, owned: bool, food: float) -> void {
|
|
||||||
if kind == VEHICLE_HORSE {
|
|
||||||
vehicles_st.ve_kept_horse = owned
|
|
||||||
vehicles_st.ve_kept_food = food
|
|
||||||
} else { vehicles_st.ve_kept_boat = owned }
|
|
||||||
let v = vehicle_find(vehicles_st, kind)
|
|
||||||
if v != null { ve_apply_kept(vehicles_st, v) }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function ve_apply_kept(vehicles_st: VehiclesState, v: Vehicle) -> void {
|
|
||||||
if v.kind == VEHICLE_HORSE {
|
|
||||||
v.owned = vehicles_st.ve_kept_horse
|
|
||||||
v.food = vehicles_st.ve_kept_food
|
|
||||||
} else { v.owned = vehicles_st.ve_kept_boat }
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# horse.ludic - a horse under the stick: a walk, a canter on stamina, hay for every metre; it stops at
|
# horse.ludic - a horse under the stick: a walk, a canter on stamina, hay for every metre; it will
|
||||||
# water, at a rise it cannot take and at a still thing in its way
|
# not step into water, and its legs (VehicleLegs) meet the ground and whatever stands in its way
|
||||||
const VE_HORSE_WALK: float = 4.5
|
const VE_HORSE_WALK: float = 4.5
|
||||||
const VE_HORSE_GALLOP: float = 11.0
|
const VE_HORSE_GALLOP: float = 11.0
|
||||||
const VE_HORSE_ACC: float = 4.0
|
const VE_HORSE_ACC: float = 4.0
|
||||||
|
|
@ -15,26 +15,31 @@ function ve_ride_horse(vehicles_st: VehiclesState, v: Vehicle, ix: float, iz: fl
|
||||||
if v.speed < 0.0 { rate = -rate }
|
if v.speed < 0.0 { rate = -rate }
|
||||||
v.yaw = v.yaw - ix * VE_HORSE_TURN * (dt * rate)
|
v.yaw = v.yaw - ix * VE_HORSE_TURN * (dt * rate)
|
||||||
}
|
}
|
||||||
|
let fx = -Math.sin(v.yaw)
|
||||||
|
let fz = -Math.cos(v.yaw)
|
||||||
let d = v.speed * dt
|
let d = v.speed * dt
|
||||||
ve_travel(vehicles_st, v, v.x - Math.sin(v.yaw) * d, v.z - Math.cos(v.yaw) * d, d)
|
let nh = VehicleWorld.ground(v.x + fx * d, v.z + fz * d)
|
||||||
ve_stamina(v, d, dt)
|
if nh < VehicleWorld.water(v.x + fx * d, v.z + fz * d) + 0.4 {
|
||||||
}
|
|
||||||
|
|
||||||
# a horse only on dry ground, up a walkable rise, clear of what is in its way
|
|
||||||
function ve_travel(vehicles_st: VehiclesState, v: Vehicle, nx: float, nz: float, d: float) -> void {
|
|
||||||
let nh = VehicleWorld.ground(nx, nz)
|
|
||||||
var ok = true
|
|
||||||
if nh < VehicleWorld.water(nx, nz) + 0.4 { ok = false }
|
|
||||||
if nh - v.y > Math.abs(d) * 0.9 { ok = false }
|
|
||||||
if VehicleWorld.blocked(nx, nz, 0.8) { ok = false }
|
|
||||||
if not ok {
|
|
||||||
v.speed = 0.0
|
v.speed = 0.0
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
v.x = nx
|
ve_legs_walk(vehicles_st, v, fx * v.speed, fz * v.speed, dt)
|
||||||
v.z = nz
|
ve_stamina(v, d, dt)
|
||||||
v.y = nh
|
}
|
||||||
if Math.abs(d) > 0.0 { ve_say(vehicles_st, VEHICLE_MOVED, v, Math.abs(d)) }
|
|
||||||
|
# the legs carry it from where it is; what they covered along its heading is its speed now, never
|
||||||
|
# more than was asked, so a trunk or a rise it cannot take brings it to a stand
|
||||||
|
function ve_legs_walk(vehicles_st: VehiclesState, v: Vehicle, vx: float, vz: float, dt: float) -> void {
|
||||||
|
if not VehicleLegs.walk(v.nid, v.x, v.y, v.z, vx, vz, dt) { return }
|
||||||
|
let dx = VehicleLegs.x() - v.x
|
||||||
|
let dz = VehicleLegs.z() - v.z
|
||||||
|
v.x = VehicleLegs.x()
|
||||||
|
v.y = VehicleLegs.y()
|
||||||
|
v.z = VehicleLegs.z()
|
||||||
|
let got = (dx * -Math.sin(v.yaw) + dz * -Math.cos(v.yaw)) / dt
|
||||||
|
if v.speed > 0.0 { v.speed = Math.clamp(got, 0.0, v.speed) } else { v.speed = Math.clamp(got, v.speed, 0.0) }
|
||||||
|
let moved = Math.sqrt(dx * dx + dz * dz)
|
||||||
|
if moved > 0.0 { ve_say(vehicles_st, VEHICLE_MOVED, v, moved) }
|
||||||
}
|
}
|
||||||
|
|
||||||
# a gallop spends stamina and a walk gives it back; every metre costs hay
|
# a gallop spends stamina and a walk gives it back; every metre costs hay
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,9 @@ import "ports.ludic"
|
||||||
import "state.ludic"
|
import "state.ludic"
|
||||||
import "places.ludic"
|
import "places.ludic"
|
||||||
import "call.ludic"
|
import "call.ludic"
|
||||||
|
import "keep.ludic"
|
||||||
import "mount.ludic"
|
import "mount.ludic"
|
||||||
import "ride.ludic"
|
import "ride.ludic"
|
||||||
import "horse.ludic"
|
import "horse.ludic"
|
||||||
|
import "legs.ludic"
|
||||||
import "system.ludic"
|
import "system.ludic"
|
||||||
|
|
|
||||||
39
packages/ludic.vehicles/keep.ludic
Normal file
39
packages/ludic.vehicles/keep.ludic
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
# keep.ludic - what a player does with their own between rides: hires it, feeds the horse, lets it
|
||||||
|
# wander where it is tied, and what a saved trip says of them
|
||||||
|
# hired or rented for the trip
|
||||||
|
export function vehicle_own(v: Vehicle) -> void { if v != null { v.owned = true } }
|
||||||
|
|
||||||
|
# hay for a horse, up to a full belly
|
||||||
|
export function vehicle_feed(vehicles_st: VehiclesState, v: Vehicle, amount: float) -> void {
|
||||||
|
if v == null { return }
|
||||||
|
v.food = Math.min(v.food + amount, 100.0)
|
||||||
|
ve_say(vehicles_st, VEHICLE_FED, v, amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
# a horse nobody rides goes where its animal goes, round the home it is tied at (home_x, home_z)
|
||||||
|
export function vehicle_follow(v: Vehicle, x: float, y: float, z: float, yaw: float) -> void {
|
||||||
|
if v == null or v.rider >= 0 { return }
|
||||||
|
v.x = x
|
||||||
|
v.y = y
|
||||||
|
v.z = z
|
||||||
|
v.yaw = yaw
|
||||||
|
VehicleLegs.stand(v.nid, x, y, z)
|
||||||
|
}
|
||||||
|
|
||||||
|
# a saved trip's word on this player's own: hired, and how fed
|
||||||
|
export function vehicle_restore(vehicles_st: mut VehiclesState, kind: int, owned: bool, food: float) -> void {
|
||||||
|
if kind == VEHICLE_HORSE {
|
||||||
|
vehicles_st.ve_kept_horse = owned
|
||||||
|
vehicles_st.ve_kept_food = food
|
||||||
|
} else { vehicles_st.ve_kept_boat = owned }
|
||||||
|
let v = vehicle_find(vehicles_st, kind)
|
||||||
|
if v != null { ve_apply_kept(vehicles_st, v) }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function ve_apply_kept(vehicles_st: VehiclesState, v: Vehicle) -> void {
|
||||||
|
if v.kind == VEHICLE_HORSE {
|
||||||
|
v.owned = vehicles_st.ve_kept_horse
|
||||||
|
v.food = vehicles_st.ve_kept_food
|
||||||
|
} else { v.owned = vehicles_st.ve_kept_boat }
|
||||||
|
}
|
||||||
30
packages/ludic.vehicles/legs.ludic
Normal file
30
packages/ludic.vehicles/legs.ludic
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
# legs.ludic - a horse's legs in the world's physics (Maroon Lake: a Jolt walker), named by the
|
||||||
|
# vehicle's nid. Gravity, steps, slopes and whatever stands in its way are theirs; unbound, the
|
||||||
|
# ground is walked as it is, a rise steeper than it can take stops it and nothing else is in the way.
|
||||||
|
export port VehicleLegs {
|
||||||
|
walk: fn(int, float, float, float, float, float, float) -> bool = fn ve__walk # (nid, x, y, z, vx, vz, dt): its pose into x, y, z
|
||||||
|
x: fn() -> float = fn ve__lx
|
||||||
|
y: fn() -> float = fn ve__ly
|
||||||
|
z: fn() -> float = fn ve__lz
|
||||||
|
stand: fn(int, float, float, float) -> void = fn ve__stand # (nid, x, y, z): tied or led there
|
||||||
|
gone: fn(int) -> void = fn ve__legs_gone
|
||||||
|
}
|
||||||
|
|
||||||
|
function ve__walk(vehicles_st: mut VehiclesState, nid: int, x: float, y: float, z: float, vx: float, vz: float, dt: float) -> bool {
|
||||||
|
vehicles_st.ve_lx = x
|
||||||
|
vehicles_st.ve_ly = y
|
||||||
|
vehicles_st.ve_lz = z
|
||||||
|
let nx = x + vx * dt
|
||||||
|
let nz = z + vz * dt
|
||||||
|
let nh = VehicleWorld.ground(nx, nz)
|
||||||
|
if nh - y > Math.sqrt(vx * vx + vz * vz) * dt * 0.9 { return true }
|
||||||
|
vehicles_st.ve_lx = nx
|
||||||
|
vehicles_st.ve_ly = nh
|
||||||
|
vehicles_st.ve_lz = nz
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
function ve__lx(vehicles_st: VehiclesState) -> float { return vehicles_st.ve_lx }
|
||||||
|
function ve__ly(vehicles_st: VehiclesState) -> float { return vehicles_st.ve_ly }
|
||||||
|
function ve__lz(vehicles_st: VehiclesState) -> float { return vehicles_st.ve_lz }
|
||||||
|
function ve__stand(nid: int, x: float, y: float, z: float) -> void { }
|
||||||
|
function ve__legs_gone(nid: int) -> void { }
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
export port VehicleWorld {
|
export port VehicleWorld {
|
||||||
ground: fn(float, float) -> float
|
ground: fn(float, float) -> float
|
||||||
water: fn(float, float) -> float = fn ve__dry # the surface over a point
|
water: fn(float, float) -> float = fn ve__dry # the surface over a point
|
||||||
blocked: fn(float, float, float) -> bool = fn ve__open # (x, z, r): a collider in a horse's way
|
|
||||||
swell: fn(float, float, float, float) -> float = fn ve__calm # (x, z, fx, fz): 0..1 of a stroke lost
|
swell: fn(float, float, float, float) -> float = fn ve__calm # (x, z, fx, fz): 0..1 of a stroke lost
|
||||||
wind: fn() -> float = fn ve__zero # 0..1
|
wind: fn() -> float = fn ve__zero # 0..1
|
||||||
wind_yaw: fn() -> float = fn ve__zero # the yaw the wind pushes toward
|
wind_yaw: fn() -> float = fn ve__zero # the yaw the wind pushes toward
|
||||||
|
|
@ -56,10 +55,12 @@ export state VehiclesState {
|
||||||
ve_rail_yaw: float = 0.0
|
ve_rail_yaw: float = 0.0
|
||||||
ve_have_dock: bool = false
|
ve_have_dock: bool = false
|
||||||
ve_swell_said: float = 0.0
|
ve_swell_said: float = 0.0
|
||||||
|
ve_lx: float = 0.0 # the unbound legs' pose
|
||||||
|
ve_ly: float = 0.0
|
||||||
|
ve_lz: float = 0.0
|
||||||
ve_fact_q: Queue<VehicleFact> = ve_fact_q__new()
|
ve_fact_q: Queue<VehicleFact> = ve_fact_q__new()
|
||||||
}
|
}
|
||||||
function ve__dry(x: float, z: float) -> float { return -10000.0 }
|
function ve__dry(x: float, z: float) -> float { return -10000.0 }
|
||||||
function ve__open(x: float, z: float, r: float) -> bool { return false }
|
|
||||||
function ve__calm(x: float, z: float, fx: float, fz: float) -> float { return 0.0 }
|
function ve__calm(x: float, z: float, fx: float, fz: float) -> float { return 0.0 }
|
||||||
function ve__zero() -> float { return 0.0 }
|
function ve__zero() -> float { return 0.0 }
|
||||||
function ve__next(vehicles_st: mut VehiclesState) -> int {
|
function ve__next(vehicles_st: mut VehiclesState) -> int {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,10 @@
|
||||||
# read back and pushed by the wind in vehicles_tick, which the game calls where the world is run
|
# read back and pushed by the wind in vehicles_tick, which the game calls where the world is run
|
||||||
export function vehicles_reset(vehicles_st: mut VehiclesState) -> void {
|
export function vehicles_reset(vehicles_st: mut VehiclesState) -> void {
|
||||||
if vehicles_st.ve_list != null {
|
if vehicles_st.ve_list != null {
|
||||||
for i in 0 .. len(vehicles_st.ve_list) { if vehicles_st.ve_list[i].kind == VEHICLE_BOAT { VehicleHull.gone(vehicles_st.ve_list[i].nid) } }
|
for i in 0 .. len(vehicles_st.ve_list) {
|
||||||
|
let v = vehicles_st.ve_list[i]
|
||||||
|
if v.kind == VEHICLE_BOAT { VehicleHull.gone(v.nid) } else { VehicleLegs.gone(v.nid) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
vehicles_st.ve_list = new []Vehicle
|
vehicles_st.ve_list = new []Vehicle
|
||||||
vehicles_st.ve_cur = null
|
vehicles_st.ve_cur = null
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# vehicles_test.ludic - a fake shore: land at x < 10, a lake past it deepening a metre a metre, a
|
# vehicles_test.ludic - a fake shore: land at x < 10, a lake past it deepening a metre a metre, a
|
||||||
# pier head at the shore facing out and a trough on the meadow, a trunk in a horse's way. Calling to
|
# pier head at the shore facing out and a trough on the meadow, a trunk in a horse's way. Calling to
|
||||||
# berths and bays, the owner and the rider, mounting a hungry horse, riding both, the swell, the wind,
|
# berths and bays, the owner and the rider, mounting a hungry horse, riding both, the swell, the wind,
|
||||||
# getting off near a shore and far from one, a player leaving, and the save. The boat's hull is a
|
# getting off near a shore and far from one, a player leaving, and the save. The boat's hull and the
|
||||||
# real ludic.physics body on the same lake.
|
# horse's legs are real ludic.physics bodies on the same ground, the trunk a post in it.
|
||||||
import "ludic.vehicles"
|
import "ludic.vehicles"
|
||||||
import "ludic.base"
|
import "ludic.base"
|
||||||
import "ludic.physics"
|
import "ludic.physics"
|
||||||
|
|
@ -17,7 +17,6 @@ program VehiclesTest {
|
||||||
if x > 9.5 { return 0.0 }
|
if x > 9.5 { return 0.0 }
|
||||||
return -10000.0
|
return -10000.0
|
||||||
}
|
}
|
||||||
function fk_blocked(x: float, z: float, r: float) -> bool { return Math.abs(x) < 1.0 + r and Math.abs(z + 60.0) < 1.0 + r }
|
|
||||||
state VehiclesTestState {
|
state VehiclesTestState {
|
||||||
swell: float = 0.0
|
swell: float = 0.0
|
||||||
wind: float = 0.0
|
wind: float = 0.0
|
||||||
|
|
@ -38,6 +37,11 @@ program VehiclesTest {
|
||||||
hyaw: float = 0.0
|
hyaw: float = 0.0
|
||||||
hspeed: float = 0.0
|
hspeed: float = 0.0
|
||||||
hulls: int = 0
|
hulls: int = 0
|
||||||
|
legs: int = -1
|
||||||
|
lx: float = 0.0
|
||||||
|
ly: float = 0.0
|
||||||
|
lz: float = 0.0
|
||||||
|
stood: int = 0
|
||||||
}
|
}
|
||||||
function fk_swell(vehicles_test_st: VehiclesTestState, x: float, z: float, fx: float, fz: float) -> float {
|
function fk_swell(vehicles_test_st: VehiclesTestState, x: float, z: float, fx: float, fz: float) -> float {
|
||||||
if fx > 0.0 { return vehicles_test_st.swell }
|
if fx > 0.0 { return vehicles_test_st.swell }
|
||||||
|
|
@ -79,11 +83,28 @@ program VehiclesTest {
|
||||||
function fk_lake(physics_st: mut PhysicsState) -> void {
|
function fk_lake(physics_st: mut PhysicsState) -> void {
|
||||||
if phys_is_open(physics_st) { return }
|
if phys_is_open(physics_st) { return }
|
||||||
phys_open(physics_st, 64, 1)
|
phys_open(physics_st, 64, 1)
|
||||||
let n = 96
|
let n = 160
|
||||||
let h = floats(n * n)
|
let h = floats(n * n)
|
||||||
for j in 0 .. n { for i in 0 .. n { h[j * n + i] = fk_ground(float(i) - 20.0, float(j) - 48.0) } }
|
for j in 0 .. n { for i in 0 .. n { h[j * n + i] = fk_ground(float(i) - 20.0, float(j) - 100.0) } }
|
||||||
phys_ground_add(physics_st, phys_heightfield(physics_st, h, n, -20.0, -48.0, 1.0))
|
phys_ground_add(physics_st, phys_heightfield(physics_st, h, n, -20.0, -100.0, 1.0))
|
||||||
|
phys_static_add(physics_st, phys_box(physics_st, 15.0, 5.0, 1.0), 0.0, 0.0, -60.0, 0.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# the legs: the horse's own walker on that ground, made on its first step
|
||||||
|
function fk_walk(physics_st: mut PhysicsState, vehicles_test_st: mut VehiclesTestState, nid: int, x: float, y: float, z: float, vx: float, vz: float, dt: float) -> bool {
|
||||||
|
fk_lake(physics_st)
|
||||||
|
if vehicles_test_st.legs < 0 { vehicles_test_st.legs = phys_walker_add(physics_st, 0.8, 1.7, x, y, z, 40.0, 450.0) }
|
||||||
|
phys_walker_move(physics_st, vehicles_test_st.legs, x, y, z, vx, vz, 0.0, 0.45, 40.0, dt)
|
||||||
|
let p = phys_walker_pose(physics_st, vehicles_test_st.legs)
|
||||||
|
vehicles_test_st.lx = p.x
|
||||||
|
vehicles_test_st.ly = p.y
|
||||||
|
vehicles_test_st.lz = p.z
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
function fk_lx(vehicles_test_st: VehiclesTestState) -> float { return vehicles_test_st.lx }
|
||||||
|
function fk_ly(vehicles_test_st: VehiclesTestState) -> float { return vehicles_test_st.ly }
|
||||||
|
function fk_lz(vehicles_test_st: VehiclesTestState) -> float { return vehicles_test_st.lz }
|
||||||
|
function fk_stand(vehicles_test_st: mut VehiclesTestState, nid: int, x: float, y: float, z: float) -> void { vehicles_test_st.stood += 1 }
|
||||||
function fk_put(physics_st: mut PhysicsState, vehicles_test_st: mut VehiclesTestState, nid: int, x: float, y: float, z: float, yaw: float) -> void {
|
function fk_put(physics_st: mut PhysicsState, vehicles_test_st: mut VehiclesTestState, nid: int, x: float, y: float, z: float, yaw: float) -> void {
|
||||||
fk_lake(physics_st)
|
fk_lake(physics_st)
|
||||||
if vehicles_test_st.hull >= 0 and vehicles_test_st.hull_nid == nid {
|
if vehicles_test_st.hull >= 0 and vehicles_test_st.hull_nid == nid {
|
||||||
|
|
@ -120,8 +141,9 @@ program VehiclesTest {
|
||||||
function fk_hspeed(vehicles_test_st: VehiclesTestState) -> float { return vehicles_test_st.hspeed }
|
function fk_hspeed(vehicles_test_st: VehiclesTestState) -> float { return vehicles_test_st.hspeed }
|
||||||
bind VehicleHull { put: fn fk_put, row: fn fk_row, drift: fn fk_drift, read: fn fk_read, x: fn fk_hx, y: fn fk_hy, z: fn fk_hz, yaw: fn fk_hyaw, speed: fn fk_hspeed, gone: fn fk_hull_gone }
|
bind VehicleHull { put: fn fk_put, row: fn fk_row, drift: fn fk_drift, read: fn fk_read, x: fn fk_hx, y: fn fk_hy, z: fn fk_hz, yaw: fn fk_hyaw, speed: fn fk_hspeed, gone: fn fk_hull_gone }
|
||||||
bind PhysWater { height: fn fk_water }
|
bind PhysWater { height: fn fk_water }
|
||||||
|
bind VehicleLegs { walk: fn fk_walk, x: fn fk_lx, y: fn fk_ly, z: fn fk_lz, stand: fn fk_stand }
|
||||||
|
|
||||||
bind VehicleWorld { ground: fn fk_ground, water: fn fk_water, blocked: fn fk_blocked, swell: fn fk_swell, wind: fn fk_wind, made: fn fk_made, gone: fn fk_gone, placed: fn fk_placed }
|
bind VehicleWorld { ground: fn fk_ground, water: fn fk_water, swell: fn fk_swell, wind: fn fk_wind, made: fn fk_made, gone: fn fk_gone, placed: fn fk_placed }
|
||||||
bind VehicleRider { board: fn fk_board, ride_at: fn fk_ride, step_off: fn fk_off, safe_spot: fn fk_safe, safe_x: fn fk_sx, safe_z: fn fk_sz }
|
bind VehicleRider { board: fn fk_board, ride_at: fn fk_ride, step_off: fn fk_off, safe_spot: fn fk_safe, safe_x: fn fk_sx, safe_z: fn fk_sz }
|
||||||
|
|
||||||
const EAST: float = -1.5707963
|
const EAST: float = -1.5707963
|
||||||
|
|
@ -207,7 +229,7 @@ program VehiclesTest {
|
||||||
expect(vehicle_in_place(vehicles_st, VEHICLE_HORSE, 0) == null)
|
expect(vehicle_in_place(vehicles_st, VEHICLE_HORSE, 0) == null)
|
||||||
}
|
}
|
||||||
|
|
||||||
test "a horse walks, gallops on stamina, stops at the water and at a trunk" (physics_st: mut PhysicsState, vehicles_st: mut VehiclesState, vehicles_test_st: mut VehiclesTestState) {
|
test "a horse walks, gallops on stamina, is pushed aside by a trunk it glances, stops at a fence and the water; called, it stands at its bay" (physics_st: mut PhysicsState, vehicles_st: mut VehiclesState, vehicles_test_st: mut VehiclesTestState) {
|
||||||
fresh(vehicles_st, vehicles_test_st)
|
fresh(vehicles_st, vehicles_test_st)
|
||||||
vehicle_call_for(vehicles_st, 0, VEHICLE_HORSE, 0)
|
vehicle_call_for(vehicles_st, 0, VEHICLE_HORSE, 0)
|
||||||
let h = vehicle_find(vehicles_st, VEHICLE_HORSE)
|
let h = vehicle_find(vehicles_st, VEHICLE_HORSE)
|
||||||
|
|
@ -221,10 +243,14 @@ program VehiclesTest {
|
||||||
expect(h.z < -15.0)
|
expect(h.z < -15.0)
|
||||||
expect_near(vehicles_test_st.rz, h.z, 0.001)
|
expect_near(vehicles_test_st.rz, h.z, 0.001)
|
||||||
expect_near(vehicles_test_st.ry, 1.35, 0.001)
|
expect_near(vehicles_test_st.ry, 1.35, 0.001)
|
||||||
# the trunk at z = -60 stops it
|
# a trunk half a metre off its line pushes it aside and lets it by; the fence at z = -60 stops it
|
||||||
|
let x0 = h.x
|
||||||
|
phys_static_add(physics_st, phys_cylinder(physics_st, 3.0, 0.3), x0 + 0.5, 0.0, -35.0, 0.0)
|
||||||
ride(physics_st, vehicles_st, 0.0, 1.0, true, 6.0)
|
ride(physics_st, vehicles_st, 0.0, 1.0, true, 6.0)
|
||||||
expect(h.z > -60.0 + 1.7)
|
expect(h.z > -59.0 - 0.85)
|
||||||
expect_near(h.speed, 0.0, 0.001)
|
expect(h.z < -50.0)
|
||||||
|
expect(Math.abs(h.x - x0) > 0.2)
|
||||||
|
expect_near(h.speed, 0.0, 0.05)
|
||||||
# and the lake to the east does too
|
# and the lake to the east does too
|
||||||
vehicle_drop(vehicles_st)
|
vehicle_drop(vehicles_st)
|
||||||
h.x = 7.0
|
h.x = 7.0
|
||||||
|
|
@ -233,6 +259,11 @@ program VehiclesTest {
|
||||||
vehicle_mount(vehicles_st, h)
|
vehicle_mount(vehicles_st, h)
|
||||||
ride(physics_st, vehicles_st, 0.0, 1.0, false, 4.0)
|
ride(physics_st, vehicles_st, 0.0, 1.0, false, 4.0)
|
||||||
expect(h.x < 9.6)
|
expect(h.x < 9.6)
|
||||||
|
# a horse brought to its bay is stood there
|
||||||
|
vehicle_drop(vehicles_st)
|
||||||
|
let before = vehicles_test_st.stood
|
||||||
|
expect(vehicle_call_for(vehicles_st, 0, VEHICLE_HORSE, 1))
|
||||||
|
expect_eq(vehicles_test_st.stood, before + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
test "a boat is rowed out, loses its stroke to the swell, turns, is stopped by the shore's bed, drifts on the wind" (physics_st: mut PhysicsState, vehicles_st: mut VehiclesState, vehicles_test_st: mut VehiclesTestState) {
|
test "a boat is rowed out, loses its stroke to the swell, turns, is stopped by the shore's bed, drifts on the wind" (physics_st: mut PhysicsState, vehicles_st: mut VehiclesState, vehicles_test_st: mut VehiclesTestState) {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
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,
|
# 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
|
# 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
|
# 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 -
|
# 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.
|
# 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, "|")
|
let cp = Text.split(cl, "|")
|
||||||
if len(cp) < 2 { err(`shaders: bad compute line: {cl}\n`); return 1 }
|
if len(cp) < 2 { err(`shaders: bad compute line: {cl}\n`); return 1 }
|
||||||
let glsl = `{tmp}/{cp[0]}.comp.glsl`
|
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 spv = `{outdir}/{cp[0]}.comp.spv`
|
||||||
let logf = `{tmp}/{cp[0]}.comp.log`
|
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`) {
|
if not shq(`{shd_q(shd_bin + "/glslangValidator")} -V -S comp {shd_q(glsl)} -o {shd_q(spv)} > {shd_q(logf)} 2>&1`) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue