20 : skeleton_(skeleton), database_(database) {
21 if (!skeleton_)
throw Exception(
"MotionMatcher: skeleton is null");
22 if (!database_)
throw Exception(
"MotionMatcher: database is null");
24 throw Exception(
"MotionMatcher: database skeleton mismatch");
27 inertia_ = std::make_unique<detail::PoseInertia>();
42 if (seconds < 0.f)
throw Exception(
"MotionMatcher.setSearchInterval: must be >= 0");
43 searchInterval_ = seconds;
47 if (seconds < 0.f)
throw Exception(
"MotionMatcher.setBlendTime: must be >= 0");
52 if (
w < 0.f)
throw Exception(
"MotionMatcher.setTrajectoryWeight: must be >= 0");
57 if (
w < 0.f)
throw Exception(
"MotionMatcher.setPoseWeight: must be >= 0");
62 if (
w < 0.f)
throw Exception(
"MotionMatcher.setVelocityWeight: must be >= 0");
67 if (frames < 0)
throw Exception(
"MotionMatcher.setIgnoreRadius: must be >= 0");
68 ignoreRadius_ = frames;
72 if (!std::isfinite(
minimum) || !std::isfinite(
maximum) ||
minimum <= 0.f || maximum < minimum || maximum > 10.f)
74 "play rate range requires finite 0 < minimum <= maximum <= 10",
75 "playRateRange", {},
"animation"));
82void MotionMatcher::updatePlayRate(
int frame) {
83 float estimated = 1.f;
84 if (database_->schema_ && frame >= 0) {
86 if (poseSpeed > 1e-4f) estimated = queryTrajectorySpeed_ / poseSpeed;
88 playRate_ = std::clamp(estimated, playRateMin_, playRateMax_);
92 if (!std::isfinite(seconds) || seconds < 0.f || seconds > 10.f)
94 "pose reselect history must be finite and between zero and ten seconds",
95 "poseReselectHistory", {},
"animation"));
96 poseReselectHistory_ = seconds;
97 if (seconds == 0.f) poseHistory_.clear();
102 if (matchedFrame_ < 0 || !database_->isBaked())
return -1;
108void MotionMatcher::buildQuery(std::vector<float>& query)
const {
110 if (!database_->
isBaked())
throw Exception(
"MotionMatcher: database not baked");
112 buildLocomotionQuery(query);
116 query.assign(
static_cast<size_t>(
n), 0.f);
119 const float cs = std::cos(desiredYaw_);
120 const float sn = std::sin(desiredYaw_);
121 query[0] = desiredVelX_ * cs - desiredVelZ_ * sn;
122 query[1] = desiredVelX_ * sn + desiredVelZ_ * cs;
125 const float horizons[3] = {0.33f, 0.66f, 1.0f};
126 for (
int h = 0;
h < 3; ++
h) {
127 query[
static_cast<size_t>(2 +
h * 2)] = query[0] * horizons[
h];
128 query[
static_cast<size_t>(2 +
h * 2 + 1)] = query[1] * horizons[
h];
129 if (hasTrajectory_) {
130 query[
static_cast<size_t>(2 +
h * 2)] = trajectory_[
h].
x * cs - trajectory_[
h].
z * sn;
131 query[
static_cast<size_t>(3 +
h * 2)] = trajectory_[
h].
x * sn + trajectory_[
h].
z * cs;
136 yawToForward(hasTrajectory_ ? trajectory_[2].
yaw - desiredYaw_ : 0.f, fx, fz);
142 cur.copyFrom(hasQueryPose_ ? &queryPose_ : (playing_ ? &matchedPose_ : &pose_));
143 cur.computeWorld(skeleton_);
145 const float rootX = cur.getWorldPositionX(
root);
146 const float rootZ = cur.getWorldPositionZ(
root);
148 const float qy = cur.getWorldRotationY(
root);
149 const float qw = cur.getWorldRotationW(
root);
150 const float yaw = std::atan2(2.f * (
qw *
qy), 1.f - 2.f * (
qy *
qy));
151 const float c2 = std::cos(
yaw);
152 const float s2 = std::sin(
yaw);
157 const float dx = cur.getWorldPositionX(
b) - rootX;
158 const float dz = cur.getWorldPositionZ(
b) - rootZ;
159 const float lx =
dx * c2 -
dz * s2;
160 const float lz =
dx * s2 +
dz * c2;
161 query[
static_cast<size_t>(base)] = lx;
162 query[
static_cast<size_t>(base + 1)] = cur.getWorldPositionY(
b);
163 query[
static_cast<size_t>(base + 2)] = lz;
169float MotionMatcher::cost(
const std::vector<float>& query,
const std::vector<float>& cand,
float upperBound)
const {
170 const int n =
static_cast<int>(
query.size());
172 const auto* schema = database_->schema_.get();
173 const float schemaWeight = schema ? schema->weightSums[0] * poseWeight_ +
174 schema->weightSums[1] * velWeight_ + schema->weightSums[2] * trajWeight_ : 0.f;
175 for (
int i = 0; i <
n; ++i) {
176 const float d =
query[
static_cast<size_t>(i)] - cand[
static_cast<size_t>(i)];
177 float w = poseWeight_;
181 w *= schema->weights[i] / std::max(1e-8f, schemaWeight);
184 if (i < 2 || (i >= 12 && i < 14) || (i >= 16 && i < 19))
w = velWeight_;
186 w /= std::max(1e-8f, 8.5f * velWeight_ + 10.6f * trajWeight_ + 5.f * poseWeight_);
192 if (sum >= upperBound)
return sum;
197void MotionMatcher::sampleMatched(AnimPose* out)
const {
198 if (matchedFrame_ < 0) {
208 if (!database_->
isBaked())
throw Exception(
"MotionMatcher.search: database not baked");
210 throw Exception(
"MotionMatcher.search: empty database");
213 std::vector<float> query;
218 int continuation = -1;
219 float nearest = std::numeric_limits<float>::infinity();
221 const auto* currentAsset = currentClip >= 0 ? database_->
getClip(currentClip) :
nullptr;
222 const bool ended = currentAsset && !currentAsset->
getLoop() && matchedTime_ >= currentAsset->getDuration();
223 if (playing_ && currentClip >= 0 && !
ended) {
224 const auto*
clip = database_->
getClip(currentClip);
226 const int count = filtered_ ?
static_cast<int>(candidateFrames_.size()) : database_->
getFrameCount();
227 for (
int candidate = 0; candidate <
count; ++candidate) {
228 const int i = filtered_ ? candidateFrames_[candidate] : candidate;
242 if (filtered_ && continuation >= 0) {
243 const float rate = currentAsset->getSampleRate() > 0.f ? currentAsset->getSampleRate() : 30.f;
244 if (nearest > .5f /
rate + 1e-5f) continuation = -1;
247 float bestCost = std::numeric_limits<float>::infinity();
248 if (continuation >= 0) {
252 if (database_->schema_) {
253 for (std::size_t i = 0; i < query.size(); ++i)
261 std::copy(query.begin() + 10, query.end(), feature.begin() + 10);
262 bestCost =
cost(query, feature, bestCost);
263 if (filtered_) bestCost += candidateBias_[continuation] + candidateContinuingBias_[continuation];
265 int best = continuation;
266 float switchCost = bestCost;
267 const int searchCount = filtered_ ?
static_cast<int>(candidateFrames_.size()) : database_->
getFrameCount();
268 for (
int candidate = 0; candidate < searchCount; ++candidate) {
269 const int i = filtered_ ? candidateFrames_[candidate] : candidate;
271 if (filtered_ && transitionBlocked_[i])
continue;
272 if (std::any_of(poseHistory_.begin(), poseHistory_.end(),
273 [i](
const auto& item) { return item.first == i; }))
continue;
276 if (playing_ && filtered_ && disableReselection_[currentClip] && database_->
getFrameClipIndex(i) == currentClip)
279 if (filtered_ && disableReselection_[currentClip])
continue;
280 const auto*
clip = database_->
getClip(currentClip);
283 const float rate =
clip->getSampleRate() > 0.f ?
clip->getSampleRate() : 30.f;
284 if (
distance <=
static_cast<float>(ignoreRadius_) /
rate + 1e-5f)
continue;
286 const float bias = filtered_ ? candidateBias_[i] : 0.f;
288 if (
c + 1e-5f < switchCost) {
294 if (
best == continuation && continuation >= 0) {
295 matchedFrame_ = continuation;
296 lastCost_ = bestCost;
297 updatePlayRate(continuation);
303 if (
best < 0)
return;
304 lastCost_ = bestCost;
305 updatePlayRate(
best);
307 matchedFrame_ =
best;
309 sampleMatched(&matchedPose_);
310 if (blendTime_ > 1e-6f && playing_) {
313 ->
sample(matchedTime_ - inertia_->historyInterval * playRate_, &previousTarget, skeleton_);
314 inertia_->begin(pose_, matchedPose_, previousTarget, blendTime_);
325void MotionMatcher::updateUnchecked(
float dt) {
326 if (dt < 0.f)
throw Exception(
"MotionMatcher.update: dt must be >= 0");
327 if (!database_->
isBaked())
return;
336 if (searchTimer_ >= searchInterval_ ||
ended) {
342 inertia_->remember(pose_, dt);
343 if (matchedFrame_ >= 0) {
344 matchedTime_ += dt * playRate_;
346 if (!
clip->getLoop()) matchedTime_ = std::min(matchedTime_,
clip->getDuration());
347 sampleMatched(&matchedPose_);
352 if (blendElapsed_ >= inertia_->duration) {
356 inertia_->apply(matchedPose_, blendElapsed_, pose_);
358 }
else if (matchedFrame_ >= 0) {
361 updatePoseHistory(dt);
364void MotionMatcher::updatePoseHistory(
float dt) {
365 if (poseReselectHistory_ <= 0.f || dt <= 0.f || matchedFrame_ < 0)
return;
366 for (
auto& item : poseHistory_) item.
second += dt;
367 std::erase_if(poseHistory_, [&](
const auto& item) {
return item.second > poseReselectHistory_; });
371 int nearestFrame = -1;
float nearest = std::numeric_limits<float>::infinity();
372 const int count = filtered_ ?
static_cast<int>(candidateFrames_.size()) : database_->getFrameCount();
373 for (
int candidate = 0; candidate <
count; ++candidate) {
374 const int i = filtered_ ? candidateFrames_[candidate] : candidate;
380 if (nearestFrame < 0)
return;
381 auto found = std::find_if(poseHistory_.begin(), poseHistory_.end(),
382 [nearestFrame](
const auto& item) { return item.first == nearestFrame; });
383 if (
found == poseHistory_.end()) poseHistory_.push_back({nearestFrame, 0.f});
384 else found->second = 0.f;
390 updateUnchecked(std::move(seconds).takeValue());
391 lastTick_ =
step.tick;
399 step.ignore(
"legacy MotionMatcher update");
eve::resource::CostSpec cost
std::map< Cell, int > best
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
EVENGINE_API_FOUNDATION public API.
void ignore(std::string_view reason={}) const noexcept
Explicitly discard this result after documenting the reason.
Move-only operation result carrying either a value or Status.
static Result success(T value)
Construct a successful result owning value.
static Result failure(Status status)
Construct a failed result from a structured status.
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
bool getLoop() const
Returns the loop.
void sample(float time, AnimPose *out, const AnimSkeleton *skeleton=nullptr) const
Sample local pose at time (seconds). If skeleton non-null, missing tracks fall back to bind pose; oth...
float wrapTime(float time) const
Wrap or clamp time according to loop flag.
Evaluated local (and optional world) pose for an AnimSkeleton. Script type: AnimPose.
void copyFrom(const AnimPose *other)
Copies from.
void resize(int boneCount)
Resize.
3D bone hierarchy + bind-pose local TRS for skeletal animation. Independent of ik::Skeleton3D (FABRIK...
int getBoneCount() const
Returns the bone count.
void applyBindPose(class AnimPose *pose) const
Fill pose locals with bind pose.
Baked motion-matching feature database from one or more AnimClips. Feature layout per frame: [0....
void normalizeFeature(std::vector< float > &feature) const
Normalize a query with statistics computed by bake().
AnimSkeleton * getSkeleton() const
Returns the skeleton.
bool hasFeatureLayout() const
Whether this database uses an explicitly configured variable layout.
float getFrameTime(int frameIndex) const
Returns the frame time.
bool isBaked() const
True when baked.
int getFeatureBoneCount() const
Returns the feature bone count.
AnimClip * getClip(int clipIndex) const
Returns the clip.
const Frame & frameAt(int index) const
int getFeatureSize() const
Returns the feature size.
int getRootBone() const
Returns the root bone.
bool hasLocomotionFeatures() const
Whether this database uses the 30-dimensional locomotion layout.
int getFeatureBone(int index) const
Returns the feature bone.
int getFrameCount() const
Returns the frame count.
int getFrameClipIndex(int frameIndex) const
Returns the frame clip index.
void search()
Force an immediate search (also called periodically from update).
void update(float dt)
Legacy seconds facade; explicitly forwards to advance().
void setBlendTime(float seconds)
Sets the blend time.
MotionMatcher(AnimSkeleton *skeleton, MotionDatabase *database)
Motion matcher.
void setDesiredYaw(float yaw)
Desired facing yaw (radians, Y-up).
eve::Result< void > setPoseReselectHistory(float seconds)
Set how long an already selected baked pose is excluded from new candidates.
void setDesiredVelocity(float x, float z)
Desired planar velocity in character/world XZ (units/sec).
void setVelocityWeight(float w)
Sets the velocity weight.
void setPoseWeight(float w)
Sets the pose weight.
void setTrajectoryWeight(float w)
Sets the trajectory weight.
AnimPose * getPose()
Returns the pose.
void setIgnoreRadius(int frames)
Keep continuous playback within this frame radius of the live playhead, including loop seams.
~MotionMatcher()
Motion matcher.
int getMatchedClipIndex() const
Returns the matched clip index.
eve::Result< void > setPlayRateRange(float minimum, float maximum)
Configure the inclusive playback-rate interval used to reconcile query and selected trajectory speed.
void setSearchInterval(float seconds)
Sets the search interval.
eve::Result< void > advance(const eve::SimulationStep &step)
Advance matching and pose blending by one scheduler step.
float locomotionWeight(int index)
Locomotion weight.
eve::Result< eve::SimulationStep > legacyStep(float seconds, bool hasLastTick, eve::SimulationTick lastTick, const char *owner)
Convert a legacy seconds call to the next local scheduler step.
eve::Result< float > secondsForStep(const eve::SimulationStep &step, bool hasLastTick, eve::SimulationTick lastTick, const char *owner)
Validate a scheduler step before an animation object mutates state.
constexpr int locomotionPoseOffset
void yawToForward(float yaw, float &fx, float &fz)
Rotate unit +Z by yaw (radians) around Y — used for planar facing.
One deterministic fixed-step emitted by SimulationClock.
std::vector< float > feature