16std::vector<AnimStateMachine*>& machines() {
17 static std::vector<AnimStateMachine*> instances;
21double stateNumber(
const StateValue&
v) {
return v.isInt() ?
static_cast<double>(
v.asInt()) :
v.asDouble(); }
26 const char* stateKind()
const override {
return "anim"; }
28 bool captureState(StateValue& out)
override {
30 for (AnimStateMachine*
m : machines()) {
32 if (
m->captureState(sv)) arr.pushBack(std::move(sv));
38 bool restoreState(
const StateValue& in, std::string* err)
override {
40 if (err) *err =
"anim: expected array of state machines";
43 const size_t n = std::min(in.arraySize(), machines().size());
44 for (
size_t i = 0; i <
n; ++i) {
45 if (!machines()[i]->restoreState(in.at(i), err))
return false;
50 bool resetToDefaults()
override {
51 for (AnimStateMachine*
m : machines())
m->resetToDefaults();
58 static AnimStateProvider provider;
66 if (!skeleton_)
throw Exception(
"AnimStateMachine: skeleton is null");
71 machines().push_back(
this);
75 machines().erase(std::remove(machines().begin(), machines().end(),
this), machines().end());
78void AnimStateMachine::requireState(
const std::string &
name)
const {
79 if (states_.find(
name) == states_.end()) {
80 throw Exception(
"AnimStateMachine: unknown state '%s'",
name.c_str());
84AnimStateMachine::Transition &AnimStateMachine::requireTransition(
int id) {
85 if (id < 0 || id >=
static_cast<int>(transitions_.size())) {
86 throw Exception(
"AnimStateMachine: invalid transition id %d",
id);
88 return transitions_[
static_cast<size_t>(
id)];
91AnimStateMachine::FloatOp AnimStateMachine::parseFloatOp(
const std::string &op) {
92 if (op ==
">")
return FloatOp::Gt;
93 if (op ==
">=")
return FloatOp::Ge;
94 if (op ==
"<")
return FloatOp::Lt;
95 if (op ==
"<=")
return FloatOp::Le;
96 if (op ==
"==")
return FloatOp::Eq;
97 if (op ==
"!=")
return FloatOp::Ne;
98 throw Exception(
"AnimStateMachine: unknown float op '%s'", op.c_str());
102 if (
name.empty())
throw Exception(
"AnimStateMachine.addState: name empty");
103 if (!clip)
throw Exception(
"AnimStateMachine.addState: clip is null");
104 if (states_.count(
name)) {
105 throw Exception(
"AnimStateMachine.addState: duplicate state '%s'",
name.c_str());
108 stateOrder_.push_back(
name);
113 currentState_ =
name;
117 states_[
name].clip->sample(0.f, &pose_, skeleton_);
121 return states_.count(
name) > 0;
125 float blendSeconds) {
128 if (blendSeconds < 0.f) {
129 throw Exception(
"AnimStateMachine.addTransition: blendSeconds must be >= 0");
134 tr.blendSeconds = blendSeconds;
135 transitions_.push_back(
tr);
136 return static_cast<int>(transitions_.size()) - 1;
140 const std::string &op,
float threshold) {
141 if (param.empty())
throw Exception(
"AnimStateMachine.addFloatCondition: param empty");
142 Transition &
tr = requireTransition(transitionId);
144 c.kind = CondKind::Float;
146 c.op = parseFloatOp(op);
147 c.threshold = threshold;
148 tr.conditions.push_back(
c);
149 floats_.emplace(param, 0.f);
153 if (param.empty())
throw Exception(
"AnimStateMachine.addBoolCondition: param empty");
154 Transition &
tr = requireTransition(transitionId);
156 c.kind = CondKind::Bool;
159 tr.conditions.push_back(
c);
160 bools_.emplace(param,
false);
164 if (param.empty())
throw Exception(
"AnimStateMachine.addTriggerCondition: param empty");
165 Transition &
tr = requireTransition(transitionId);
167 c.kind = CondKind::Trigger;
169 tr.conditions.push_back(
c);
170 triggers_.emplace(param,
false);
174 Transition &
tr = requireTransition(transitionId);
175 tr.exitTime =
clampf(normalizedTime, 0.f, 1.f);
176 tr.hasExitTime =
true;
180 requireTransition(transitionId).hasExitTime =
enabled;
186 auto it = floats_.find(
name);
187 return it == floats_.end() ? 0.f : it->second;
193 auto it = bools_.find(
name);
194 return it == bools_.end() ? false : it->second;
203bool AnimStateMachine::conditionsMet(
const Transition &
tr)
const {
204 for (
const Condition &
c :
tr.conditions) {
206 case CondKind::Float: {
210 case FloatOp::Gt:
ok =
v >
c.threshold;
break;
211 case FloatOp::Ge:
ok =
v >=
c.threshold;
break;
212 case FloatOp::Lt:
ok =
v <
c.threshold;
break;
213 case FloatOp::Le:
ok =
v <=
c.threshold;
break;
214 case FloatOp::Eq:
ok = std::fabs(
v -
c.threshold) < 1e-5f;
break;
215 case FloatOp::Ne:
ok = std::fabs(
v -
c.threshold) >= 1e-5f;
break;
217 if (!
ok)
return false;
220 case CondKind::Bool: {
221 if (
getBool(
c.param) !=
c.boolValue)
return false;
224 case CondKind::Trigger: {
225 auto it = triggers_.find(
c.param);
226 if (it == triggers_.end() || !it->second)
return false;
234bool AnimStateMachine::tryTransition() {
235 if (currentState_.empty() || blending_)
return false;
236 const State &cur = states_.at(currentState_);
237 const float dur = cur.clip ? cur.clip->getDuration() : 0.f;
238 const float norm = dur > 1e-8f ?
clampf(stateTime_ / dur, 0.f, 1.f) : 1.f;
240 for (
const Transition &
tr : transitions_) {
241 if (
tr.from != currentState_)
continue;
242 if (
tr.hasExitTime && norm <
tr.exitTime)
continue;
243 if (!conditionsMet(
tr))
continue;
246 for (
const Condition &
c :
tr.conditions) {
247 if (
c.kind == CondKind::Trigger) triggers_[
c.param] =
false;
251 blendDuration_ =
tr.blendSeconds;
254 if (blendDuration_ <= 1e-6f) {
255 currentState_ = nextState_;
257 states_.at(currentState_).clip->sample(0.f, &pose_, skeleton_);
261 currentState_ = nextState_;
263 states_.at(currentState_).clip->sample(0.f, &toPose_, skeleton_);
264 pose_.
blendFrom(&fromPose_, &toPose_, 0.f);
272 if (dt < 0.f)
throw Exception(
"AnimStateMachine.update: dt must be >= 0");
274 if (!stateOrder_.empty())
setEntry(stateOrder_.front());
277 if (currentState_.empty())
return;
282 const State &dst = states_.at(currentState_);
283 dst.clip->sample(stateTime_, &toPose_, skeleton_);
284 float t = blendDuration_ > 1e-8f ? blendElapsed_ / blendDuration_ : 1.f;
290 pose_.
blendFrom(&fromPose_, &toPose_, t);
293 if (!blending_) tryTransition();
298 const State &st = states_.at(currentState_);
299 st.clip->sample(stateTime_, &pose_, skeleton_);
315 out.
set(
"floats", std::move(floats));
319 out.
set(
"bools", std::move(bools));
323 out.
set(
"triggers", std::move(triggers));
329 if (err) *err =
"anim: state is not an object";
334 if (!current || !current->
isString() || !started || !started->
isBool()) {
335 if (err) *err =
"anim: missing currentState/started";
338 const std::string stateName = current->
asString();
339 const bool wasStarted = started->
asBool();
340 if (wasStarted && !stateName.empty() && !
hasState(stateName)) {
341 if (err) *err =
"anim: unknown state '" + stateName +
"'";
349 if (
const StateValue* floats = in.
find(
"floats"); floats && floats->isObject()) {
350 for (
const auto& key : floats->keys()) {
352 if (
v && (
v->isInt() ||
v->isFloat())) floats_[key] =
static_cast<float>(stateNumber(*
v));
356 for (
const auto& key : bools->keys()) {
358 if (
v &&
v->isBool()) bools_[key] =
v->asBool();
362 for (
const auto& key : triggers->keys()) {
364 if (
v &&
v->isBool()) triggers_[key] =
v->asBool();
368 currentState_ = stateName;
370 if (
const StateValue* st = in.
find(
"stateTime"); st && (st->isInt() || st->isFloat()))
371 stateTime_ =
static_cast<float>(stateNumber(*st));
372 if (
const StateValue* bd = in.
find(
"blendDuration"); bd && (bd->isInt() || bd->isFloat()))
373 blendDuration_ =
static_cast<float>(stateNumber(*bd));
374 if (
const StateValue* be = in.
find(
"blendElapsed"); be && (be->isInt() || be->isFloat()))
375 blendElapsed_ =
static_cast<float>(stateNumber(*be));
377 started_ = wasStarted;
379 if (started_ && !currentState_.empty()) {
380 states_.at(currentState_).clip->sample(stateTime_, &pose_, skeleton_);
392 blendDuration_ = 0.f;
394 if (!stateOrder_.empty()) {
399 currentState_.clear();
JSON-compatible state value tree used by state hot reload.
static StateValue object()
Empty object.
static StateValue boolean(bool v)
Boolean value.
static StateValue array()
Empty array.
bool asBool() const
Boolean payload; only valid when kind() == Kind::Bool.
const std::string & asString() const
String payload; only valid when kind() == Kind::String.
static StateValue number(double v)
Floating-point value.
const StateValue * find(const std::string &key) const
Look up key; nullptr when absent.
static StateValue string(std::string v)
String value.
void set(const std::string &key, StateValue v)
Insert or replace key; only valid on objects.
Keyframed skeletal animation clip (local TRS tracks per bone). Script type: AnimClip.
Evaluated local (and optional world) pose for an AnimSkeleton. Script type: AnimPose.
void blendFrom(const AnimPose *a, const AnimPose *b, float t)
void copyFrom(const AnimPose *other)
void resize(int boneCount)
3D bone hierarchy + bind-pose local TRS for skeletal animation. Independent of ik::Skeleton3D (FABRIK...
void applyBindPose(class AnimPose *pose) const
Fill pose locals with bind pose.
void setHasExitTime(int transitionId, bool enabled)
float getFloat(const std::string &name) const
int addTransition(const std::string &from, const std::string &to, float blendSeconds)
Add transition from → to with blend duration.
void addTriggerCondition(int transitionId, const std::string ¶m)
bool captureState(StateValue &out) const
Serialize runtime state (params, current/next state, blend).
bool hasState(const std::string &name) const
AnimStateMachine(AnimSkeleton *skeleton)
void resetTrigger(const std::string &name)
void addBoolCondition(int transitionId, const std::string ¶m, bool value)
void setTrigger(const std::string &name)
void addFloatCondition(int transitionId, const std::string ¶m, const std::string &op, float threshold)
Comparison op: ">", ">=", "<", "<=", "==", "!=".
void setBool(const std::string &name, bool value)
void setFloat(const std::string &name, float value)
bool restoreState(const StateValue &in, std::string *err=nullptr)
Restore runtime state captured by captureState().
void addState(const std::string &name, AnimClip *clip)
bool getBool(const std::string &name) const
void setEntry(const std::string &name)
void setExitTime(int transitionId, float normalizedTime)
Optional: require normalized local time in [0,1] >= exitTime before transition.
bool resetToDefaults()
Clear params and re-seat the entry state (restore fallback).
Runtime-state serialization for state hot reload.
float clampf(float v, float lo, float hi)