载入中...
搜索中...
未找到
MotionMatcher.cpp
浏览该文件的文档.
2
10
11#include "common/Exception.h"
12
13#include <algorithm>
14#include <cmath>
15#include <limits>
16
17namespace eve::animation {
18
20 : skeleton_(skeleton), database_(database) {
21 if (!skeleton_) throw Exception("MotionMatcher: skeleton is null");
22 if (!database_) throw Exception("MotionMatcher: database is null");
23 if (database_->getSkeleton() != skeleton_) {
24 throw Exception("MotionMatcher: database skeleton mismatch");
25 }
26 pose_.resize(skeleton_->getBoneCount());
27 inertia_ = std::make_unique<detail::PoseInertia>();
28 matchedPose_.resize(skeleton_->getBoneCount());
29 skeleton_->applyBindPose(&pose_);
30}
31
33
35 desiredVelX_ = x;
36 desiredVelZ_ = z;
37}
38
39void MotionMatcher::setDesiredYaw(float yaw) { desiredYaw_ = yaw; }
40
42 if (seconds < 0.f) throw Exception("MotionMatcher.setSearchInterval: must be >= 0");
43 searchInterval_ = seconds;
44}
45
46void MotionMatcher::setBlendTime(float seconds) {
47 if (seconds < 0.f) throw Exception("MotionMatcher.setBlendTime: must be >= 0");
48 blendTime_ = seconds;
49}
50
52 if (w < 0.f) throw Exception("MotionMatcher.setTrajectoryWeight: must be >= 0");
53 trajWeight_ = w;
54}
55
57 if (w < 0.f) throw Exception("MotionMatcher.setPoseWeight: must be >= 0");
58 poseWeight_ = w;
59}
60
62 if (w < 0.f) throw Exception("MotionMatcher.setVelocityWeight: must be >= 0");
63 velWeight_ = w;
64}
65
67 if (frames < 0) throw Exception("MotionMatcher.setIgnoreRadius: must be >= 0");
68 ignoreRadius_ = frames;
69}
70
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"));
76 playRateMin_ = minimum;
77 playRateMax_ = maximum;
78 playRate_ = std::clamp(playRate_, minimum, maximum);
80}
81
82void MotionMatcher::updatePlayRate(int frame) {
83 float estimated = 1.f;
84 if (database_->schema_ && frame >= 0) {
85 const float poseSpeed = database_->frameAt(frame).trajectorySpeed;
86 if (poseSpeed > 1e-4f) estimated = queryTrajectorySpeed_ / poseSpeed;
87 }
88 playRate_ = std::clamp(estimated, playRateMin_, playRateMax_);
89}
90
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();
99}
100
102 if (matchedFrame_ < 0 || !database_->isBaked()) return -1;
103 return database_->getFrameClipIndex(matchedFrame_);
104}
105
106AnimPose* MotionMatcher::getPose() { return &pose_; }
107
108void MotionMatcher::buildQuery(std::vector<float>& query) const {
109 if (database_->hasFeatureLayout()) { buildSchemaQuery(query); return; }
110 if (!database_->isBaked()) throw Exception("MotionMatcher: database not baked");
111 if (database_->hasLocomotionFeatures()) {
112 buildLocomotionQuery(query);
113 return;
114 }
115 const int n = database_->getFeatureSize();
116 query.assign(static_cast<size_t>(n), 0.f);
117
118 // Character-space desired velocity (assume current facing = desiredYaw for query).
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;
123
124 // Desired trajectory: integrate constant velocity for 0.33/0.66/1.0s in char space.
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;
132 }
133 }
134 // Facing at +1s: same as desired (relative facing 0 → forward +Z in char space).
135 float fx, fz;
136 yawToForward(hasTrajectory_ ? trajectory_[2].yaw - desiredYaw_ : 0.f, fx, fz);
137 query[8] = fx;
138 query[9] = fz;
139
140 // Pose features from current pose (character-relative).
141 AnimPose cur;
142 cur.copyFrom(hasQueryPose_ ? &queryPose_ : (playing_ ? &matchedPose_ : &pose_));
143 cur.computeWorld(skeleton_);
144 const int root = database_->getRootBone();
145 const float rootX = cur.getWorldPositionX(root);
146 const float rootZ = cur.getWorldPositionZ(root);
147 // Estimate current yaw from root rotation.
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);
153
154 int base = 10;
155 for (int i = 0; i < database_->getFeatureBoneCount(); ++i) {
156 const int b = database_->getFeatureBone(i);
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;
164 base += 3;
165 }
166 database_->normalizeFeature(query);
167}
168
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());
171 float sum = 0.f;
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_;
178 if (schema) {
179 w = schema->sources[i] == MotionFeatureSource::Pose ? poseWeight_ :
180 (schema->kinds[i] == MotionFeatureKind::Velocity ? velWeight_ : trajWeight_);
181 w *= schema->weights[i] / std::max(1e-8f, schemaWeight);
182 } else if (database_->hasLocomotionFeatures()) {
183 w = i < detail::locomotionPoseOffset ? trajWeight_ : poseWeight_;
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_);
187 } else if (i < 2)
188 w = velWeight_;
189 else if (i < 10)
190 w = trajWeight_;
191 sum += w * d * d;
192 if (sum >= upperBound) return sum;
193 }
194 return sum;
195}
196
197void MotionMatcher::sampleMatched(AnimPose* out) const {
198 if (matchedFrame_ < 0) {
199 skeleton_->applyBindPose(out);
200 return;
201 }
202 const int ci = database_->getFrameClipIndex(matchedFrame_);
203 AnimClip* clip = database_->getClip(ci);
204 clip->sample(matchedTime_, out, skeleton_);
205}
206
208 if (!database_->isBaked()) throw Exception("MotionMatcher.search: database not baked");
209 if (database_->getFrameCount() == 0) {
210 throw Exception("MotionMatcher.search: empty database");
211 }
212
213 std::vector<float> query;
214 buildQuery(query);
215
216 // Compare against the live playhead, not the original entry frame.
217 // Equal-cost searches must keep advancing instead of restarting a blend.
218 int continuation = -1;
219 float nearest = std::numeric_limits<float>::infinity();
220 const int currentClip = getMatchedClipIndex();
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);
225 const float time = clip->wrapTime(matchedTime_);
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;
229 if (database_->getFrameClipIndex(i) != currentClip) continue;
230 float distance = std::abs(database_->getFrameTime(i) - time);
231 if (clip->getLoop()) distance = std::min(distance, clip->getDuration() - distance);
232 if (distance < nearest) {
233 nearest = distance;
234 continuation = i;
235 }
236 }
237 // Candidate ranges are a live search context (for example a Chooser
238 // changing from grounded to airborne). A different valid interval in
239 // the same clip must not make an out-of-range playhead eligible as a
240 // continuing pose. At the bake rate, a live time is at most half a
241 // sample from its representing frame.
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;
245 }
246 }
247 float bestCost = std::numeric_limits<float>::infinity();
248 if (continuation >= 0) {
249 auto feature = database_->frameAt(continuation).feature;
250 // Continuing has no pose discontinuity. Use the evaluated playhead
251 // pose rather than its nearest baked sample (which is quantized).
252 if (database_->schema_) {
253 for (std::size_t i = 0; i < query.size(); ++i)
254 if (database_->schema_->queries[i] == MotionFeatureQuery::Continuing) query[i] = feature[i];
255 } else if (database_->hasLocomotionFeatures()) {
256 // Authored pose channels use UseContinuingPose; trajectory channels
257 // still use the character's actual/predicted movement.
258 std::copy(feature.begin() + detail::locomotionPoseOffset, feature.end(),
259 query.begin() + detail::locomotionPoseOffset);
260 } else
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];
264 }
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;
270 // Authored protected phases can be played through, but never jumped into.
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;
274 // DisableReselection belongs to the current asset, including its end.
275 // An expired one-shot cannot become its own fresh candidate again.
276 if (playing_ && filtered_ && disableReselection_[currentClip] && database_->getFrameClipIndex(i) == currentClip)
277 continue;
278 if (continuation >= 0 && database_->getFrameClipIndex(i) == currentClip) {
279 if (filtered_ && disableReselection_[currentClip]) continue;
280 const auto* clip = database_->getClip(currentClip);
281 float distance = std::abs(database_->getFrameTime(i) - clip->wrapTime(matchedTime_));
282 if (clip->getLoop()) distance = std::min(distance, clip->getDuration() - distance);
283 const float rate = clip->getSampleRate() > 0.f ? clip->getSampleRate() : 30.f;
284 if (distance <= static_cast<float>(ignoreRadius_) / rate + 1e-5f) continue;
285 }
286 const float bias = filtered_ ? candidateBias_[i] : 0.f;
287 const float c = cost(query, database_->frameAt(i).feature, switchCost - bias) + bias;
288 if (c + 1e-5f < switchCost) {
289 bestCost = c;
290 switchCost = c;
291 best = i;
292 }
293 }
294 if (best == continuation && continuation >= 0) {
295 matchedFrame_ = continuation;
296 lastCost_ = bestCost;
297 updatePlayRate(continuation);
298 return;
299 }
300
301 // A database containing only an exhausted, non-reselectable one-shot holds
302 // its terminal pose. It remains searchable after the caller changes candidates.
303 if (best < 0) return;
304 lastCost_ = bestCost;
305 updatePlayRate(best);
306 if (best != matchedFrame_ || ended) {
307 matchedFrame_ = best;
308 matchedTime_ = database_->getFrameTime(best);
309 sampleMatched(&matchedPose_);
310 if (blendTime_ > 1e-6f && playing_) {
311 AnimPose previousTarget;
312 database_->getClip(getMatchedClipIndex())
313 ->sample(matchedTime_ - inertia_->historyInterval * playRate_, &previousTarget, skeleton_);
314 inertia_->begin(pose_, matchedPose_, previousTarget, blendTime_);
315 blending_ = true;
316 blendElapsed_ = 0.f;
317 } else {
318 pose_.copyFrom(&matchedPose_);
319 blending_ = false;
320 }
321 playing_ = true;
322 }
323}
324
325void MotionMatcher::updateUnchecked(float dt) {
326 if (dt < 0.f) throw Exception("MotionMatcher.update: dt must be >= 0");
327 if (!database_->isBaked()) return;
328
329 if (!playing_) {
330 search();
331 searchTimer_ = 0.f;
332 } else {
333 searchTimer_ += dt;
334 const auto* clip = matchedFrame_ >= 0 ? database_->getClip(getMatchedClipIndex()) : nullptr;
335 const bool ended = clip && !clip->getLoop() && matchedTime_ >= clip->getDuration();
336 if (searchTimer_ >= searchInterval_ || ended) {
337 searchTimer_ = 0.f;
338 search();
339 }
340 }
341
342 inertia_->remember(pose_, dt);
343 if (matchedFrame_ >= 0) {
344 matchedTime_ += dt * playRate_;
345 const auto* clip = database_->getClip(getMatchedClipIndex());
346 if (!clip->getLoop()) matchedTime_ = std::min(matchedTime_, clip->getDuration());
347 sampleMatched(&matchedPose_);
348 }
349
350 if (blending_) {
351 blendElapsed_ += dt;
352 if (blendElapsed_ >= inertia_->duration) {
353 blending_ = false;
354 pose_.copyFrom(&matchedPose_);
355 } else {
356 inertia_->apply(matchedPose_, blendElapsed_, pose_);
357 }
358 } else if (matchedFrame_ >= 0) {
359 pose_.copyFrom(&matchedPose_);
360 }
361 updatePoseHistory(dt);
362}
363
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_; });
368 const int clipIndex = getMatchedClipIndex();
369 const auto* clip = database_->getClip(clipIndex);
370 const float time = clip->wrapTime(matchedTime_);
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;
375 if (database_->getFrameClipIndex(i) != clipIndex) continue;
376 float distance = std::abs(database_->getFrameTime(i) - time);
377 if (clip->getLoop()) distance = std::min(distance, clip->getDuration() - distance);
378 if (distance < nearest) {nearest = distance; nearestFrame = i;}
379 }
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;
385}
386
388 auto seconds = detail::secondsForStep(step, hasLastTick_, lastTick_, "MotionMatcher");
389 if (!seconds) return eve::Result<void>::failure(seconds.status());
390 updateUnchecked(std::move(seconds).takeValue());
391 lastTick_ = step.tick;
392 hasLastTick_ = true;
394}
395
396void MotionMatcher::update(float dt) {
397 auto step = detail::legacyStep(dt, hasLastTick_, lastTick_, "MotionMatcher");
398 if (!step) {
399 step.ignore("legacy MotionMatcher update");
400 return;
401 }
402 advance(std::move(step).takeValue()).ignore("legacy MotionMatcher update");
403}
404
405} // namespace eve::animation
float w
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
int root
Definition AnimSmr.cpp:119
eve::resource::CostSpec cost
float maximum[3]
float minimum[3]
glm::vec4 clip
glm::vec3 n
Definition Grass.cpp:63
std::int32_t second
std::int32_t c
int h
bool ended
MeleePoint3 b
Definition MeleeHit.cpp:41
float distance
float d
bool found
float dz
float dx
float bias
std::uint32_t count
std::map< Cell, int > best
float step
Definition TreeMesh.cpp:314
float qy
float qw
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.
Definition Diagnostic.h:125
EVENGINE_API_FOUNDATION public API.
Definition Exception.h:13
void ignore(std::string_view reason={}) const noexcept
Explicitly discard this result after documenting the reason.
Definition Result.h:537
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
bool getLoop() const
Returns the loop.
Definition AnimClip.h:168
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...
Definition AnimClip.cpp:575
float wrapTime(float time) const
Wrap or clamp time according to loop flag.
Definition AnimClip.cpp:471
Evaluated local (and optional world) pose for an AnimSkeleton. Script type: AnimPose.
Definition AnimPose.h:17
void copyFrom(const AnimPose *other)
Copies from.
Definition AnimPose.cpp:91
void resize(int boneCount)
Resize.
Definition AnimPose.cpp:78
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.
void yawToForward(float yaw, float &fx, float &fz)
Rotate unit +Z by yaw (radians) around Y — used for planar facing.
Definition AnimMath.h:144
I * query()
Queries .
Definition Capability.h:88
One deterministic fixed-step emitted by SimulationClock.
Definition Time.h:158