Files
aego/modules/animation/service.go
T
sanspie 515f4ca1b3 Stage 11a: animation (D333–D340, D344, D354–D357)
Clips (.aeanim) and controllers (.aectrl) with importers, AnimationPlayer and Animator with crossfades, root motion and events on fixed steps, rewind support. Editor: Animation panel (dope sheet, curves, record, preview, trajectory), Animator panel, sprite sheet slicer and flipbooks, Programming workspace, export panel redesign. Motion 2D ledge climb. 11c: the component and Create palette.
2026-10-25 15:31:22 +03:00

531 lines
12 KiB
Go

package animation
import (
"cmp"
"errors"
"slices"
"aego/anim"
"aego/asset"
"aego/contracts/motion2d"
"aego/contracts/physics2d"
"aego/contracts/scriptevents"
"aego/contracts/scriptfields"
"aego/core/ids"
"aego/core/log"
"aego/core/rewind"
"aego/engine"
"aego/scene"
"aego/scene/propclaim"
"aego/scene/propref"
"aego/schema"
"aego/script/vm"
)
var errNoClip = errors.New("the clip is not loaded")
type binding struct {
ref propref.Ref
key propclaim.Key
slot propclaim.Slot
label string
node scene.NodeHandle
kind schema.Kind
track int32
auth anim.Authority
end anim.OnEnd
base anim.Value
last anim.Value
hrev uint64
held propclaim.Owner
hasBase bool
wrote bool
live bool
blocked bool
idle bool
pose bool
shown bool
}
type player struct {
node scene.NodeHandle
slot int32
clip *anim.Clip
id ids.AssetID
gen uint32
ids []int32
binds []binding
prev []binding
sampler anim.Sampler
vals []anim.Value
mark uint32
}
const (
pendEvent uint8 = iota
pendFinished
pendEntered
pendExited
)
type pending struct {
node scene.NodeHandle
name string
id int32
payload float32
cycle int64
kind uint8
anim bool
pres bool
}
type warnKey struct {
node scene.NodeHandle
code string
track int32
}
type Service struct {
scene *scene.Scene
world *engine.World
m *Module
props *propref.Resolver
cl *propclaim.Table
owner propclaim.Owner
log *log.ChannelLogger
q physics2d.Queries
rcv motion2d.Receiver
unsub func()
players map[scene.NodeHandle]*player
list []*player
owned map[propclaim.Key][]propref.Ref
warned map[warnKey]bool
pend []pending
cross []anim.Crossing
args [2]vm.Value
key *animKey
hh *rewind.Hasher
dirty bool
mark uint32
dt float32
alpha float32
fixed float32
unscaled float32
loadFn func(scene.NodeHandle, *AnimationPlayer)
bindFn func(scene.NodeHandle, *AnimationPlayer)
stepFn func(scene.NodeHandle, *AnimationPlayer)
presentFn func(scene.NodeHandle, *AnimationPlayer)
captureFn func(scene.NodeHandle, *AnimationPlayer)
hashFn func(scene.NodeHandle, *AnimationPlayer)
anims map[scene.NodeHandle]*animator
alist []*animator
aloadFn func(scene.NodeHandle, *Animator)
abindFn func(scene.NodeHandle, *Animator)
astepFn func(scene.NodeHandle, *Animator)
apresentFn func(scene.NodeHandle, *Animator)
acaptureFn func(scene.NodeHandle, *Animator)
ahashFn func(scene.NodeHandle, *Animator)
}
func NewService(s *scene.Scene, w *engine.World, m *Module) *Service {
svc := &Service{scene: s, world: w, m: m, dirty: true,
players: map[scene.NodeHandle]*player{}, anims: map[scene.NodeHandle]*animator{}, owned: map[propclaim.Key][]propref.Ref{}, warned: map[warnKey]bool{}}
svc.props = propref.New(s, svc.fields)
if w != nil {
svc.cl = propclaim.Ensure(w)
svc.owner = svc.cl.Register(ID)
}
svc.unsub = s.Events().Subscribe(svc.onEvent)
svc.loadFn, svc.bindFn, svc.stepFn, svc.presentFn = svc.loadOne, svc.bindOne, svc.stepOne, svc.presentOne
svc.captureFn, svc.hashFn = svc.captureOne, svc.hashOne
svc.aloadFn, svc.abindFn, svc.astepFn, svc.apresentFn = svc.aloadOne, svc.abindOne, svc.astepOne, svc.apresentOne
svc.acaptureFn, svc.ahashFn = svc.acaptureOne, svc.ahashOne
return svc
}
func (s *Service) Close() {
if s.unsub != nil {
s.unsub()
s.unsub = nil
}
s.cl.Drop(s.owner)
for _, p := range s.players {
s.unbind(p.binds)
}
for _, r := range s.anims {
s.unbind(r.binds)
}
clear(s.players)
clear(s.anims)
clear(s.list)
clear(s.alist)
s.list, s.alist = s.list[:0], s.alist[:0]
}
func (s *Service) onEvent(ev scene.Event) {
switch ev.Kind {
case scene.EvNodeCreated, scene.EvNodeDestroyed, scene.EvNodeReparented, scene.EvNodeRenamed,
scene.EvComponentAdded, scene.EvComponentRemoved, scene.EvSpaceChanged:
s.dirty = true
}
}
func (s *Service) fields() scriptfields.Fields {
if s.world == nil {
return nil
}
f, _ := engine.ServiceOf[scriptfields.Fields](s.world, scriptfields.ServiceID)
return f
}
func (s *Service) queries() physics2d.Queries {
if s.world == nil {
return nil
}
q, _ := engine.ServiceOf[physics2d.Queries](s.world, physics2d.ServiceID)
return q
}
func (s *Service) receiver() motion2d.Receiver {
if s.world == nil {
return nil
}
r, _ := engine.ServiceOf[motion2d.Receiver](s.world, motion2d.ServiceID)
return r
}
func (s *Service) sink() scriptevents.Sink {
if s.world == nil {
return nil
}
sk, _ := engine.ServiceOf[scriptevents.Sink](s.world, scriptevents.ServiceID)
return sk
}
func (s *Service) once(owner scene.NodeHandle, code string, track int32) bool {
k := warnKey{node: owner, code: code, track: track}
if s.warned[k] {
return false
}
s.warned[k] = true
return true
}
func (s *Service) warn(owner scene.NodeHandle, code, msg string) {
if s.log != nil {
s.log.Warn(code+": "+msg, log.F("node", s.scene.Path(owner)))
}
}
func trackName(t *anim.Track) string {
if t.Target == "" {
return t.Property
}
return t.Target + ": " + t.Property
}
func (m *Module) clip(res asset.Resolver, id ids.AssetID) *anim.Clip {
if res == nil || id.IsZero() {
return nil
}
dec, ok := res.(asset.Decoder)
src, ok2 := res.(asset.AnimSource)
if !ok || !ok2 {
return nil
}
v, err := dec.Decoded(id, "anim.clip", func() (any, error) {
data, ok := src.Anim(id)
if !ok {
return nil, errNoClip
}
c, err := anim.Unmarshal(data)
if err != nil {
return nil, err
}
return c, nil
})
if err != nil {
return nil
}
c, _ := v.(*anim.Clip)
return c
}
func compatible(a, b *anim.Clip) bool {
if b == nil || len(a.Tracks) != len(b.Tracks) {
return false
}
for i := range a.Tracks {
x, y := &a.Tracks[i], &b.Tracks[i]
if x.Target != y.Target || x.Property != y.Property || x.Kind != y.Kind {
return false
}
}
return true
}
func (s *Service) eventIDs(h scene.NodeHandle, clip *anim.Clip, dst []int32) []int32 {
if clip == nil {
return dst
}
for i := range clip.Events {
id, ok := s.m.EventID(clip.Events[i].Name)
if !ok {
id = -1
if s.once(h, CodeEvent, int32(i)) {
s.warn(h, CodeEvent, "event "+clip.Events[i].Name+" is not declared in Project Settings → Animation")
}
}
dst = append(dst, id)
}
return dst
}
func (s *Service) lookup(h scene.NodeHandle, c *AnimationPlayer) *player {
if i := int(c.st.slot) - 1; i >= 0 && i < len(s.list) {
if p := s.list[i]; p.node == h {
return p
}
}
p := s.players[h]
if p != nil {
c.st.slot = p.slot
}
return p
}
func (s *Service) create(h scene.NodeHandle, c *AnimationPlayer) *player {
p := &player{node: h, slot: int32(len(s.list) + 1)}
s.players[h] = p
s.list = append(s.list, p)
c.st.slot = p.slot
return p
}
func (s *Service) loadOne(h scene.NodeHandle, c *AnimationPlayer) {
p := s.lookup(h, c)
if p == nil {
p = s.create(h, c)
s.dirty = true
}
if c.PlayOnStart && !c.st.auto {
c.st.auto = true
c.st.cmd |= cmdPlay
}
res := s.scene.Resolver()
var gen uint32
if res != nil {
gen = res.Generation()
}
if p.id == c.Clip.ID && p.gen == gen {
return
}
clip := s.m.clip(res, c.Clip.ID)
old, same := p.clip, p.id == c.Clip.ID
p.id, p.gen = c.Clip.ID, gen
switch {
case clip == old:
return
case same && clip == nil:
return
case same && old != nil && !compatible(old, clip):
if s.once(h, CodeReload, -1) {
s.warn(h, CodeReload, "the clip was reimported with other tracks; the player stopped")
}
if c.st.playing {
c.st.playing, c.st.paused = false, false
s.pend = append(s.pend, pending{node: h, cycle: -1, kind: pendFinished})
}
case same && old != nil && old.Length > 0:
k := float64(clip.Length) / float64(old.Length)
c.st.time *= k
c.st.shown *= k
}
p.clip = clip
p.ids = s.eventIDs(h, clip, p.ids[:0])
s.dirty = true
}
func (s *Service) rebind() {
s.dirty = false
s.mark++
clear(s.owned)
scene.Store[AnimationPlayer](s.scene).EachAll(s.bindFn)
scene.Store[Animator](s.scene).EachAll(s.abindFn)
clear(s.list)
s.list = s.list[:0]
for h, p := range s.players {
if p.mark != s.mark {
s.unbind(p.binds)
delete(s.players, h)
continue
}
p.slot = int32(len(s.list) + 1)
s.list = append(s.list, p)
}
clear(s.alist)
s.alist = s.alist[:0]
for h, r := range s.anims {
if r.mark != s.mark {
s.unbind(r.binds)
delete(s.anims, h)
continue
}
r.slot = int32(len(s.alist) + 1)
s.alist = append(s.alist, r)
}
}
func (s *Service) unbind(binds []binding) {
for i := range binds {
if binds[i].shown {
s.scene.DropPresentationLocal(binds[i].node)
}
}
}
func sizeVals(v []anim.Value, n int) []anim.Value {
if cap(v) < n {
return make([]anim.Value, n)
}
return v[:n]
}
func bindOrder(a, b binding) int {
switch {
case a.node.Index != b.node.Index:
return cmp.Compare(a.node.Index, b.node.Index)
case a.node.Gen != b.node.Gen:
return cmp.Compare(a.node.Gen, b.node.Gen)
case a.pose != b.pose:
if a.pose {
return -1
}
return 1
}
return cmp.Compare(a.track, b.track)
}
func carry(dst *binding, prev []binding) {
for i := range prev {
o := &prev[i]
if o.track == dst.track && o.node == dst.node && o.ref.Same(&dst.ref) {
dst.base, dst.hasBase, dst.last, dst.wrote = o.base, o.hasBase, o.last, o.wrote
dst.live, dst.shown = o.live, o.shown
o.track = -1
return
}
}
}
func posed(binds []binding, node scene.NodeHandle) bool {
for i := range binds {
if binds[i].pose && binds[i].node == node {
return true
}
}
return false
}
func (s *Service) bindOne(h scene.NodeHandle, c *AnimationPlayer) {
if !s.scene.IsAlive(h) {
return
}
p := s.lookup(h, c)
if p == nil {
p = s.create(h, c)
}
p.mark = s.mark
p.prev, p.binds = p.binds, p.prev[:0]
if p.clip != nil {
p.sampler.Reset(p.clip)
for i := range p.clip.Tracks {
s.bindTrack(h, c, p, int32(i))
}
slices.SortFunc(p.binds, bindOrder)
for i := range p.binds {
carry(&p.binds[i], p.prev)
}
}
for i := range p.prev {
if o := &p.prev[i]; o.track >= 0 && o.shown && !posed(p.binds, o.node) {
s.scene.DropPresentationLocal(o.node)
}
}
p.vals = sizeVals(p.vals, len(p.binds))
}
func fits(k schema.Kind, r *propref.Ref) bool {
isFloat := k == schema.KindFloat32 || k == schema.KindFloat64
if r.Lane() >= 0 {
return isFloat
}
switch r.Shape() {
case propref.Discrete:
return k == r.Kind()
case propref.Float:
return isFloat
case propref.Int:
return k == schema.KindInt32 || k == schema.KindInt64
case propref.Vec2:
return k == schema.KindVec2
case propref.Vec3:
return k == schema.KindVec3
case propref.Vec4:
return k == schema.KindVec4 || k == schema.KindColor || k == schema.KindRect
case propref.Quat:
return k == schema.KindQuat
}
return false
}
func (s *Service) bindTrack(h scene.NodeHandle, c *AnimationPlayer, p *player, i int32) {
t := &p.clip.Tracks[i]
if t.Root {
if s.once(h, CodeRoot, i) {
s.warn(h, CodeRoot, "track "+trackName(t)+": root motion plays only in an Animator; the track is off")
}
return
}
node := s.scene.Find(h, t.Target)
if node.IsZero() || !s.scene.IsAlive(node) {
if s.once(h, CodeLost, i) {
s.warn(h, CodeLost, "track "+trackName(t)+": the target node is missing")
}
return
}
ref, err := s.props.ResolveAny(node, t.Property)
if err != nil {
if s.once(h, CodeTrack, i) {
s.warn(h, CodeTrack, "track "+trackName(t)+": "+err.Error())
}
return
}
if !fits(t.Kind, &ref) {
if s.once(h, CodeTrack, i) {
s.warn(h, CodeTrack, "track "+trackName(t)+" has kind "+t.Kind.String()+", which does not match the property")
}
return
}
if c.TimeDomain == DomainUnscaled && t.Authority == anim.AuthSimulation {
if s.once(h, CodeUnscaled, i) {
s.warn(h, CodeUnscaled, "track "+trackName(t)+" has authority=simulation in an unscaled player; it is off")
}
return
}
k := propclaim.KeyOf(node, &ref)
for j := range s.owned[k] {
if s.owned[k][j].Overlaps(&ref) {
if s.once(h, CodeConflict, i) {
s.warn(h, CodeConflict, "track "+trackName(t)+": another animation track drives this property; this one is off")
}
return
}
}
s.owned[k] = append(s.owned[k], ref)
p.binds = append(p.binds, binding{
ref: ref, key: k, label: trackName(t), node: node, kind: t.Kind, track: i, auth: t.Authority, end: t.OnEnd,
pose: ref.Owner() == propref.OwnTransform && !ref.Three(),
})
}