ludic.anim: animation sets - the game's src/animset moved in as it stood (records, AnimPlayer, animset_bind / request / param / tick and the accessors); AnimSets is an open @ByKey registry a game fills with def AnimSets from; a test on a rig of hand-made clips
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d9a73d63bd
commit
5517873275
10 changed files with 442 additions and 1 deletions
|
|
@ -58,6 +58,18 @@ A sampled rotation agrees with `anim_mix` to within 1e-4 a component (ozz keeps
|
|||
and a channel gives the absolute local L, so D = G_p (L R_rest^-1) G_p^-1.
|
||||
- The scratch is made once and grown: a call is per actor per frame.
|
||||
|
||||
## Animation sets
|
||||
|
||||
Which clip plays when, as data: `AnimSets` is an open registry (`@ByKey`, `ASET_*`) a game fills with
|
||||
`def AnimSets from "assets/data/anim_sets.lres"`. A set is a graph - states (nodes) each with an
|
||||
ordered list of choices, the first whose `need` bands all hold and whose clip the rig carries playing
|
||||
(`rate`, or `rate_by` a parameter over the `ground` a cycle covers, held to `rate_min` / `rate_max`),
|
||||
and transitions (from a state or `"*"`, with a fade; with `@frame when: fn() -> bool`, taken on their
|
||||
own the frame it answers true). The code binds an `AnimPlayer` to a set and a rig
|
||||
(`animset_bind`), sets parameters (`animset_param`), asks for a state (`animset_request`, taken once
|
||||
however often it is asked) and ticks it into a skin (`animset_tick`). A one-shot (`loop: false`)
|
||||
holds its last pose and hands on to `next`. Clip names are made once per rig; a tick makes nothing.
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
|
|
@ -65,4 +77,6 @@ ludic test packages/ludic.anim
|
|||
```
|
||||
|
||||
A two-bone skin made from a glTF document with no GPU: sampling, wrapping, the cross-fade,
|
||||
translations off by default, and clips read out of a `.bin` written to the temp directory.
|
||||
translations off by default, and clips read out of a `.bin` written to the temp directory. And an
|
||||
animation set on a rig of hand-made clips (`animset_test`, `sets.lres`): need bands, the rate clamp,
|
||||
a one-shot's `next`, a `when` edge, and a request taken once.
|
||||
|
|
|
|||
|
|
@ -10,3 +10,9 @@ import "mix.ludic"
|
|||
import "pose.ludic"
|
||||
import "bones.ludic"
|
||||
import "ozz.ludic"
|
||||
import "set_records.ludic"
|
||||
import "set_player.ludic"
|
||||
import "set_bind.ludic"
|
||||
import "set_pick.ludic"
|
||||
import "set_follow.ludic"
|
||||
import "set_tick.ludic"
|
||||
|
|
|
|||
53
packages/ludic.anim/set_bind.ludic
Normal file
53
packages/ludic.anim/set_bind.ludic
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# set_bind.ludic - a player given a set and a rig: the clip names are made once per rig, and
|
||||
# looked up again (without making anything) until the model has loaded and read its clips
|
||||
@alloc_ok("a rig's clip names and tables, made once when the rig changes")
|
||||
function as_names(pl: AnimPlayer, set: int, rig: string) -> void {
|
||||
let s = AnimSets[set]
|
||||
pl.set = set
|
||||
pl.rig = intern(rig)
|
||||
pl.names = new []string
|
||||
pl.clips = new []Clip
|
||||
pl.base = new []int
|
||||
pl.vals = new []float
|
||||
for i in 0 .. len(s.states) {
|
||||
push(pl.base, len(pl.names))
|
||||
let d = s.states[i]
|
||||
for c in 0 .. len(d.choices) {
|
||||
push(pl.names, intern(rig + "_" + d.choices[c].clip))
|
||||
push(pl.clips, null)
|
||||
}
|
||||
}
|
||||
if s.params != null { for i in 0 .. len(s.params) { push(pl.vals, 0.0) } }
|
||||
}
|
||||
|
||||
# Bind (or look again). A new rig is a different skeleton: nothing of the last one's cross-fade
|
||||
# may carry over. Ready once the entry state's default - its last choice - is found.
|
||||
export function animset_bind(anim_st: AnimState, pl: AnimPlayer, set: int, rig: string) -> bool {
|
||||
if animset_bound(pl, set, rig) and pl.ready { return true }
|
||||
if not animset_bound(pl, set, rig) {
|
||||
as_names(pl, set, rig)
|
||||
pl.ready = false
|
||||
}
|
||||
for i in 0 .. len(pl.names) { pl.clips[i] = anim_find(anim_st, pl.names[i]) }
|
||||
let s = AnimSets[set]
|
||||
pl.state = as_entry(s)
|
||||
pl.want = pl.state
|
||||
pl.asked = ""
|
||||
pl.clip = null
|
||||
pl.prev = null
|
||||
let d = s.states[pl.state]
|
||||
let n = len(d.choices)
|
||||
pl.ready = n > 0 and pl.clips[pl.base[pl.state] + n - 1] != null
|
||||
return pl.ready
|
||||
}
|
||||
|
||||
function as_entry(s: AnimSet) -> int {
|
||||
let i = as_state(s, s.entry)
|
||||
if i < 0 { return 0 }
|
||||
return i
|
||||
}
|
||||
|
||||
export function as_state(s: AnimSet, key: string) -> int {
|
||||
for i in 0 .. len(s.states) { if s.states[i].key == key { return i } }
|
||||
return -1
|
||||
}
|
||||
40
packages/ludic.anim/set_follow.ludic
Normal file
40
packages/ludic.anim/set_follow.ludic
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# set_follow.ludic - moving between states: a `when` edge out of the current state that answers
|
||||
# true, else the state the code asked for, faded by the edge between them or the target's own blend
|
||||
function as_follow(pl: AnimPlayer) -> void {
|
||||
pl.fresh = false
|
||||
let s = AnimSets[pl.set]
|
||||
var to = pl.want
|
||||
var blend = -1.0
|
||||
if s.transitions != null {
|
||||
for i in 0 .. len(s.transitions) {
|
||||
let tr = s.transitions[i]
|
||||
if tr.when == null or not as_leaves(s, tr, pl.state) { continue }
|
||||
if tr.when() {
|
||||
to = as_state(s, tr.to)
|
||||
blend = tr.blend
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if to < 0 or to == pl.state { return }
|
||||
if blend < 0.0 { blend = as_blend(s, pl.state, to) }
|
||||
pl.state = to
|
||||
pl.want = to
|
||||
pl.fresh = true
|
||||
pl.fade_next = blend
|
||||
}
|
||||
|
||||
function as_leaves(s: AnimSet, tr: AnimTransition, from: int) -> bool {
|
||||
return tr.from == "*" or tr.from == s.states[from].key
|
||||
}
|
||||
|
||||
# the edge the code's request crosses, if the graph has one; else the target's own fade
|
||||
function as_blend(s: AnimSet, from: int, to: int) -> float {
|
||||
if s.transitions != null {
|
||||
for i in 0 .. len(s.transitions) {
|
||||
let tr = s.transitions[i]
|
||||
if tr.when == null and tr.to == s.states[to].key and as_leaves(s, tr, from) { return tr.blend }
|
||||
}
|
||||
}
|
||||
return s.states[to].blend
|
||||
}
|
||||
56
packages/ludic.anim/set_pick.ludic
Normal file
56
packages/ludic.anim/set_pick.ludic
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# set_pick.ludic - the code's side: a parameter set, a state asked for; and the choice a state
|
||||
# makes from them. Nothing here makes anything: it runs every frame.
|
||||
export function animset_param(pl: AnimPlayer, name: string, v: float) -> void {
|
||||
let i = as_param_at(pl, name)
|
||||
if i >= 0 { pl.vals[i] = v }
|
||||
}
|
||||
|
||||
# a state by name, taken once: the code may ask every frame, and a `when` edge that moved the
|
||||
# player on since is not undone by the same request again
|
||||
export function animset_request(pl: AnimPlayer, key: string) -> void {
|
||||
if pl.set < 0 or key == pl.asked { return }
|
||||
let i = as_state(AnimSets[pl.set], key)
|
||||
if i < 0 { return }
|
||||
pl.asked = key
|
||||
pl.want = i
|
||||
}
|
||||
|
||||
function as_param_at(pl: AnimPlayer, name: string) -> int {
|
||||
if pl.set < 0 { return -1 }
|
||||
let s = AnimSets[pl.set]
|
||||
if s.params == null { return -1 }
|
||||
for i in 0 .. len(s.params) { if s.params[i] == name { return i } }
|
||||
return -1
|
||||
}
|
||||
|
||||
function as_value(pl: AnimPlayer, name: string) -> float {
|
||||
let i = as_param_at(pl, name)
|
||||
if i < 0 { return 0.0 }
|
||||
return pl.vals[i]
|
||||
}
|
||||
|
||||
function as_holds(pl: AnimPlayer, c: AnimChoice) -> bool {
|
||||
if c.need == null { return true }
|
||||
for k in 0 .. len(c.need) {
|
||||
let n = c.need[k]
|
||||
let v = as_value(pl, n.param)
|
||||
if not (v > n.over and v < n.under) { return false }
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
# the state's first choice that holds and whose clip the rig has; -1 none
|
||||
function as_pick(pl: AnimPlayer, d: AnimStateDef) -> int {
|
||||
if d.choices == null { return -1 }
|
||||
let b = pl.base[pl.state]
|
||||
for c in 0 .. len(d.choices) {
|
||||
if pl.clips[b + c] != null and as_holds(pl, d.choices[c]) { return c }
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
function as_rate(pl: AnimPlayer, d: AnimStateDef, c: AnimChoice) -> float {
|
||||
var r = c.rate
|
||||
if c.rate_by != "" and c.ground > 0.0 { r = as_value(pl, c.rate_by) / c.ground }
|
||||
return Math.clamp(r, d.rate_min, d.rate_max)
|
||||
}
|
||||
29
packages/ludic.anim/set_player.ludic
Normal file
29
packages/ludic.anim/set_player.ludic
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# set_player.ludic - one actor playing a set: which rig its clips were found for, the state it
|
||||
# is in and the one the code last asked for, and the two clips being cross-faded
|
||||
export property AnimPlayer {
|
||||
set: int = -1
|
||||
rig: string = "" # the clips' prefix it was bound with ("hiker_m")
|
||||
names: []string = null # every choice's full clip name, state by state, made once per rig
|
||||
clips: []Clip = null # the same, found; null where the rig has no such clip
|
||||
base: []int = null # where each state's choices start in names and clips
|
||||
vals: []float = null # the set's parameters, in its order
|
||||
ready: bool = false # the entry state's default clip is found: the model has loaded
|
||||
state: int = 0
|
||||
want: int = 0
|
||||
asked: string = "" # the last state asked for: a request is taken once, not every frame
|
||||
fresh: bool = false # the state changed this frame: its first clip starts at zero
|
||||
fade_next: float = -1.0 # the fade a state change asked for
|
||||
clip: Clip = null
|
||||
t: float = 0.0
|
||||
prev: Clip = null
|
||||
pt: float = 0.0
|
||||
mix: float = 1.0
|
||||
fade: float = 0.2
|
||||
rate: float = 1.0
|
||||
}
|
||||
|
||||
export function animset_clip(pl: AnimPlayer) -> Clip { return pl.clip }
|
||||
export function animset_time(pl: AnimPlayer) -> float { return pl.t }
|
||||
export function animset_rate(pl: AnimPlayer) -> float { return pl.rate }
|
||||
export function animset_ready(pl: AnimPlayer) -> bool { return pl.ready }
|
||||
export function animset_bound(pl: AnimPlayer, set: int, rig: string) -> bool { return pl.set == set and pl.rig == rig and pl.names != null }
|
||||
55
packages/ludic.anim/set_records.ludic
Normal file
55
packages/ludic.anim/set_records.ludic
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# set_records.ludic - a rig's animation graph as the studio draws it: states are nodes,
|
||||
# transitions edges, and inside a state an ordered list of choices (a 1D blend by bands)
|
||||
|
||||
# a band's test on one parameter the code sets each frame: holds while over < value < under
|
||||
export property AnimCond {
|
||||
param: string = ""
|
||||
over: float = -1000000.0
|
||||
under: float = 1000000.0
|
||||
}
|
||||
|
||||
# one clip a state may play: `clip` follows the rig's prefix (`walk` is `<rig>_walk`) and plays
|
||||
# when every `need` holds (none: the state's default)
|
||||
export property AnimChoice {
|
||||
clip: string = ""
|
||||
need: []AnimCond = null
|
||||
rate: float = 1.0
|
||||
rate_by: string = "" # or the rate is this parameter over `ground`,
|
||||
ground: float = 0.0 # the m/s one cycle covers, so the feet cover the ground the body does
|
||||
}
|
||||
|
||||
# a node: the first choice that holds AND whose clip the rig carries plays. `blend` is the fade
|
||||
# between its own choices and into it; `keep_phase` carries a cycle's phase across such a change
|
||||
export property AnimStateDef {
|
||||
key: string = ""
|
||||
choices: []AnimChoice = null
|
||||
rate_min: float = 0.0
|
||||
rate_max: float = 1000000.0
|
||||
blend: float = 0.2
|
||||
keep_phase: bool = true
|
||||
loop: bool = true
|
||||
next: string = "" # a one-shot's state once it has played through (loop: false)
|
||||
}
|
||||
|
||||
# an edge: from a state (or "*", any) to another, with its fade. With no `when` it is the fade taken
|
||||
# when the code asks for `to`; with one, it is taken on its own the frame the function answers true
|
||||
export property AnimTransition {
|
||||
from: string = ""
|
||||
to: string = ""
|
||||
blend: float = 0.2
|
||||
@frame when: fn() -> bool = null
|
||||
}
|
||||
|
||||
# a graph for a kind of rig: its parameters, its states (the first is where it starts unless
|
||||
# `entry` says) and its transitions
|
||||
export property AnimSet {
|
||||
key: string = ""
|
||||
entry: string = ""
|
||||
params: []string = null
|
||||
states: []AnimStateDef = null
|
||||
transitions: []AnimTransition = null
|
||||
}
|
||||
|
||||
# open: a game fills it (`def AnimSets from "assets/data/anim_sets.lres"`), a set's place its ASET_ number
|
||||
@ByKey
|
||||
export open registry AnimSets of AnimSet as ASET
|
||||
52
packages/ludic.anim/set_tick.ludic
Normal file
52
packages/ludic.anim/set_tick.ludic
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# set_tick.ludic - a frame of a player: follow the graph, pick the clip, cross-fade on a change,
|
||||
# advance both clips by the same step and play them into the skin (ludic.anim)
|
||||
export function animset_tick(anim_st: mut AnimState, pl: AnimPlayer, sk: Skin, dt: float) -> bool {
|
||||
if not pl.ready or sk == null { return false }
|
||||
as_follow(pl)
|
||||
let d = AnimSets[pl.set].states[pl.state]
|
||||
let k = as_pick(pl, d)
|
||||
if k < 0 { return false }
|
||||
pl.rate = as_rate(pl, d, d.choices[k])
|
||||
as_switch(pl, d, pl.clips[pl.base[pl.state] + k])
|
||||
let adv = dt * pl.rate
|
||||
pl.t = pl.t + adv
|
||||
pl.pt = pl.pt + adv
|
||||
as_one_shot(pl, d)
|
||||
if pl.mix < 1.0 { pl.mix = Math.min(1.0, pl.mix + dt / pl.fade) }
|
||||
# anim_play blends TOWARD its second clip, so the weight is how much of the old one is left
|
||||
return anim_play(anim_st, sk, pl.clip, pl.t, pl.prev, pl.pt, 1.0 - pl.mix)
|
||||
}
|
||||
|
||||
# a new clip fades in from the old one. Within a state it carries the PHASE across: two leg cycles
|
||||
# restarted at zero put the far leg forward for the length of the fade, which reads as a stumble.
|
||||
function as_switch(pl: AnimPlayer, d: AnimStateDef, want: Clip) -> void {
|
||||
var fade = d.blend
|
||||
if pl.fade_next > 0.0 { fade = pl.fade_next }
|
||||
pl.fade_next = -1.0
|
||||
if pl.clip == null {
|
||||
pl.clip = want
|
||||
pl.t = 0.0
|
||||
pl.prev = null
|
||||
pl.mix = 1.0
|
||||
return
|
||||
}
|
||||
if want == pl.clip { return }
|
||||
var ph = 0.0
|
||||
if d.keep_phase and not pl.fresh and pl.clip.dur > 0.0 { ph = pl.t % pl.clip.dur / pl.clip.dur }
|
||||
pl.prev = pl.clip
|
||||
pl.pt = pl.t
|
||||
pl.clip = want
|
||||
pl.t = ph * want.dur
|
||||
pl.fade = fade
|
||||
pl.mix = 0.0
|
||||
if not (fade > 0.0) { pl.mix = 1.0 }
|
||||
}
|
||||
|
||||
# a clip that does not loop holds its last pose, and hands over to its `next` state when played
|
||||
function as_one_shot(pl: AnimPlayer, d: AnimStateDef) -> void {
|
||||
if d.loop or pl.clip.dur <= 0.0 or pl.t < pl.clip.dur { return }
|
||||
pl.t = pl.clip.dur * 0.999
|
||||
if d.next == "" { return }
|
||||
let i = as_state(AnimSets[pl.set], d.next)
|
||||
if i >= 0 { pl.want = i }
|
||||
}
|
||||
116
packages/ludic.anim/tests/animset_test.ludic
Normal file
116
packages/ludic.anim/tests/animset_test.ludic
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
# animset_test.ludic - an animation set played on a fake rig (clips made by hand, a two-bone skin
|
||||
# from a glTF document with no GPU): a choice by its need bands, rate_by held to the state's range,
|
||||
# a one-shot handing on to `next`, a `when` edge, and a request taken once
|
||||
import "ludic.render3d/r3d.ludic"
|
||||
import "ludic.anim"
|
||||
program AnimSetTest {
|
||||
numbers float
|
||||
# never called: naming Input links the engine's runtime, which render3d's textures stand on
|
||||
function links_runtime() -> bool { return Input.key_pressed(0) }
|
||||
|
||||
def AnimSets from "sets.lres"
|
||||
state ToyState {
|
||||
hop: bool = false
|
||||
}
|
||||
function toy_hops(toy_st: ToyState) -> bool { return toy_st.hop }
|
||||
|
||||
function skin(render3d_st: mut Render3dState) -> Skin {
|
||||
render3d_st.gltf_doc = Json.parse("{\"nodes\": [{\"name\": \"root\", \"children\": [1]}, {\"name\": \"bone\", \"translation\": [0, 1, 0]}], \"skins\": [{\"joints\": [0, 1]}]}")
|
||||
return skin_load(render3d_st, 0)
|
||||
}
|
||||
# one rotation channel on the bone, `dur` seconds long
|
||||
function clip(name: string, dur: float) -> Clip {
|
||||
let c = new Clip
|
||||
c.name = name
|
||||
c.n = 1
|
||||
c.cnode = words(1)
|
||||
c.cpath = words(1)
|
||||
c.ckeys = words(1)
|
||||
c.coff = words(1)
|
||||
c.cvoff = words(1)
|
||||
c.cnode[0] = 1
|
||||
c.cpath[0] = AC_ROT
|
||||
c.ckeys[0] = 2
|
||||
c.times = floats(2)
|
||||
c.times[1] = dur
|
||||
c.vals = floats(8)
|
||||
c.vals[3] = 1.0
|
||||
c.vals[7] = 1.0
|
||||
c.dur = dur
|
||||
return c
|
||||
}
|
||||
# the rig "rig": every clip the set names, bound and ready
|
||||
function rig(anim_st: mut AnimState) -> AnimPlayer {
|
||||
anim_clear(anim_st)
|
||||
anim_add(anim_st, clip("rig_idle", 1.0))
|
||||
anim_add(anim_st, clip("rig_walk", 1.0))
|
||||
anim_add(anim_st, clip("rig_run", 1.0))
|
||||
anim_add(anim_st, clip("rig_wave", 0.5))
|
||||
anim_add(anim_st, clip("rig_hop", 1.0))
|
||||
let pl = new AnimPlayer
|
||||
expect(animset_bind(anim_st, pl, ASET_TOY, "rig"))
|
||||
return pl
|
||||
}
|
||||
function playing(pl: AnimPlayer) -> string { return animset_clip(pl).name }
|
||||
|
||||
test "a state plays its first choice whose need bands hold" (anim_st: mut AnimState, render3d_st: mut Render3dState) {
|
||||
let sk = skin(render3d_st)
|
||||
let pl = rig(anim_st)
|
||||
expect(animset_tick(anim_st, pl, sk, 0.016))
|
||||
expect(playing(pl) == "rig_idle")
|
||||
animset_param(pl, "speed", 1.0)
|
||||
expect(animset_tick(anim_st, pl, sk, 0.016))
|
||||
expect(playing(pl) == "rig_walk")
|
||||
animset_param(pl, "speed", 5.0)
|
||||
expect(animset_tick(anim_st, pl, sk, 0.016))
|
||||
expect(playing(pl) == "rig_run")
|
||||
animset_param(pl, "speed", 4.0)
|
||||
expect(animset_tick(anim_st, pl, sk, 0.016))
|
||||
expect(playing(pl) == "rig_walk")
|
||||
}
|
||||
test "rate_by is the parameter over the ground a cycle covers, held to the state's range" (anim_st: mut AnimState, render3d_st: mut Render3dState) {
|
||||
let sk = skin(render3d_st)
|
||||
let pl = rig(anim_st)
|
||||
animset_param(pl, "speed", 3.0)
|
||||
animset_tick(anim_st, pl, sk, 0.016)
|
||||
expect_eq(animset_rate(pl), 0.75)
|
||||
animset_param(pl, "speed", 1.0)
|
||||
animset_tick(anim_st, pl, sk, 0.016)
|
||||
expect_eq(animset_rate(pl), 0.5)
|
||||
animset_param(pl, "speed", 9.0)
|
||||
animset_tick(anim_st, pl, sk, 0.016)
|
||||
expect_eq(animset_rate(pl), 2.0)
|
||||
}
|
||||
test "a one-shot plays through and hands on to its next state" (anim_st: mut AnimState, render3d_st: mut Render3dState) {
|
||||
let sk = skin(render3d_st)
|
||||
let pl = rig(anim_st)
|
||||
animset_request(pl, "wave")
|
||||
animset_tick(anim_st, pl, sk, 0.1)
|
||||
expect(playing(pl) == "rig_wave")
|
||||
animset_tick(anim_st, pl, sk, 0.3)
|
||||
animset_tick(anim_st, pl, sk, 0.3)
|
||||
expect(playing(pl) == "rig_wave")
|
||||
animset_tick(anim_st, pl, sk, 0.016)
|
||||
expect(playing(pl) == "rig_idle")
|
||||
}
|
||||
test "a when edge is taken the frame its function says so" (anim_st: mut AnimState, render3d_st: mut Render3dState, toy_st: mut ToyState) {
|
||||
let sk = skin(render3d_st)
|
||||
let pl = rig(anim_st)
|
||||
toy_st.hop = false
|
||||
animset_tick(anim_st, pl, sk, 0.016)
|
||||
expect(playing(pl) == "rig_idle")
|
||||
toy_st.hop = true
|
||||
animset_tick(anim_st, pl, sk, 0.016)
|
||||
expect(playing(pl) == "rig_hop")
|
||||
toy_st.hop = false
|
||||
}
|
||||
test "a request asked every frame is taken once, not re-applied after the graph moves on" (anim_st: mut AnimState, render3d_st: mut Render3dState) {
|
||||
let sk = skin(render3d_st)
|
||||
let pl = rig(anim_st)
|
||||
for i in 0 .. 12 {
|
||||
animset_request(pl, "wave")
|
||||
animset_tick(anim_st, pl, sk, 0.1)
|
||||
}
|
||||
expect(playing(pl) == "rig_idle")
|
||||
}
|
||||
}
|
||||
20
packages/ludic.anim/tests/sets.lres
Normal file
20
packages/ludic.anim/tests/sets.lres
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# sets.lres - the test's one set: a ground state picking by speed bands, a one-shot that hands back,
|
||||
# and a hop the code never asks for, taken when toy_hops says so
|
||||
toy {
|
||||
entry: "ground"
|
||||
params: ["speed"]
|
||||
states: [
|
||||
{
|
||||
key: "ground"
|
||||
rate_min: 0.5, rate_max: 2.0, blend: 0.2
|
||||
choices: [
|
||||
{ clip: "run", rate_by: "speed", ground: 2.0, need: [{ param: "speed", over: 4.0 }] },
|
||||
{ clip: "walk", rate_by: "speed", ground: 4.0, need: [{ param: "speed", over: 0.5 }] },
|
||||
{ clip: "idle" }
|
||||
]
|
||||
},
|
||||
{ key: "wave", loop: false, next: "ground", choices: [{ clip: "wave" }] },
|
||||
{ key: "hop", choices: [{ clip: "hop" }] }
|
||||
]
|
||||
transitions: [{ from: "ground", to: "hop", blend: 0.1, when: fn toy_hops }]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue