Files
EVEngine/test/animation_motion_transition_blocks.cpp

231 lines
9.3 KiB
C++

#include "animation/AnimClip.h"
#include "animation/AnimSkeleton.h"
#include "animation/MotionDatabase.h"
#include "animation/MotionMatcher.h"
#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include <cmath>
#include <limits>
using namespace eve::animation;
namespace {
struct BlockFixture {
AnimSkeleton skeleton;
AnimClip first{"first"}, second{"second"};
MotionDatabase database{&skeleton};
BlockFixture() {
skeleton.addBone("root");
for (auto* clip : {&first, &second}) {
clip->setDuration(1.f);
clip->setLoop(false);
clip->addPositionKey(0, 0.f, 0.f, 0.f, 0.f);
database.addClip(clip);
}
database.bake();
}
};
}
TEST_CASE("animation.motionMatching.transitionBlocksPreventEntryButPreserveContinuation") {
BlockFixture fixture;
MotionMatcher continuing(&fixture.skeleton, &fixture.database);
continuing.setSearchInterval(10.f);
std::vector<MotionSearchRange> seed{{0, 0.f, 0.f}};
REQUIRE(continuing.setCandidateRanges(seed).ok());
continuing.search();
continuing.update(.3f);
const float time = continuing.getMatchedTime();
REQUIRE(time > .1f);
std::vector<MotionSearchRange> ranges{{0, 0.f, 1.f, -100.f, false, {{.1f, .8f}}},
{1, 0.f, 1.f, 0.f}};
REQUIRE(continuing.setCandidateRanges(ranges).ok());
continuing.search();
CHECK(continuing.getMatchedClipIndex() == 0);
CHECK(std::abs(continuing.getMatchedTime() - time) < 1e-6f);
// A fresh search would strongly prefer clip 0's bias, but its selected
// middle section is protected. This does not remove it from continuation.
ranges[0].start = .2f;
ranges[0].end = .7f;
MotionMatcher fresh(&fixture.skeleton, &fixture.database);
REQUIRE(fresh.setCandidateRanges(ranges).ok());
fresh.search();
CHECK(fresh.getMatchedClipIndex() == 1);
REQUIRE(continuing.setCandidateRanges(ranges).ok());
continuing.search();
CHECK(continuing.getMatchedClipIndex() == 0);
// Ordinary membership still applies even to a protected ongoing pose.
REQUIRE(continuing.setCandidateRanges(std::span(ranges).last(1)).ok());
continuing.search();
CHECK(continuing.getMatchedClipIndex() == 1);
}
TEST_CASE("animation.motionMatching.transitionBlockBoundariesAreCopiedAndUnioned") {
BlockFixture fixture;
const float start = fixture.database.getFrameTime(3);
const float end = fixture.database.getFrameTime(4);
REQUIRE(end > start);
std::vector<MotionSearchRange> ranges{{0, start, start, -100.f, false, {{start, end}}},
{0, start, start, -200.f},
{1, 0.f, 0.f}};
MotionMatcher matcher(&fixture.skeleton, &fixture.database);
REQUIRE(matcher.setCandidateRanges(ranges).ok());
ranges[0].transitionBlocks.clear();
matcher.search();
CHECK(matcher.getMatchedClipIndex() == 1);
// Exact end is eligible; exact start was blocked, including an overlapping
// duplicate candidate range with a better score and no block of its own.
ranges = {{0, end, end, -100.f, false, {{start, end}}}};
REQUIRE(matcher.setCandidateRanges(ranges).ok());
matcher.search();
CHECK(matcher.getMatchedClipIndex() == 0);
CHECK(matcher.getMatchedTime() == end);
}
TEST_CASE("animation.motionMatching.transitionBlockFailureIsAtomicAndAllBlockedCanRecover") {
BlockFixture fixture;
MotionMatcher matcher(&fixture.skeleton, &fixture.database);
std::vector<MotionSearchRange> blocked{{0, 0.f, 1.f, 0.f, false, {{0.f, 2.f}}}};
REQUIRE(matcher.setCandidateRanges(blocked).ok());
matcher.search();
CHECK(matcher.getMatchedFrame() == -1);
for (const auto& invalid : {MotionTransitionBlock{-1.f, 1.f}, {1.f, 1.f}, {2.f, 1.f},
{0.f, std::numeric_limits<float>::infinity()},
{std::numeric_limits<float>::quiet_NaN(), 1.f}}) {
std::vector<MotionSearchRange> attempted{{1, 0.f, 1.f, 0.f, false, {invalid}}};
REQUIRE(!matcher.setCandidateRanges(attempted).ok());
matcher.search();
CHECK(matcher.getMatchedFrame() == -1);
}
std::vector<MotionSearchRange> recovered{{1, 0.f, 1.f}};
REQUIRE(matcher.setCandidateRanges(recovered).ok());
matcher.search();
CHECK(matcher.getMatchedClipIndex() == 1);
}
TEST_CASE("animation.motionMatching.poseReselectHistoryExpiresAndKeepsContinuationEligible") {
BlockFixture fixture;
MotionMatcher matcher(&fixture.skeleton, &fixture.database);
matcher.setSearchInterval(10.f);
REQUIRE(matcher.setPoseReselectHistory(.3f).ok());
REQUIRE(!matcher.setPoseReselectHistory(std::numeric_limits<float>::quiet_NaN()).ok());
CHECK(std::abs(matcher.getPoseReselectHistory() - .3f) < 1e-6f);
std::vector<MotionSearchRange> seed{{0, 0.f, 0.f}};
REQUIRE(matcher.setCandidateRanges(seed).ok());
matcher.update(.1f);
std::vector<MotionSearchRange> ranges{{0, .1f, .1f, -100.f, false, {}, 200.f},
{1, 0.f, 1.f, 0.f}};
REQUIRE(matcher.setCandidateRanges(ranges).ok());
matcher.search();
CHECK(matcher.getMatchedClipIndex() == 1);
matcher.update(.31f);
ranges[1].continuingCostBias = 200.f;
REQUIRE(matcher.setCandidateRanges(ranges).ok());
matcher.search();
CHECK(matcher.getMatchedClipIndex() == 0);
}
TEST_CASE("animation.motionMatching.anyStrictCostImprovementCanSwitch") {
BlockFixture fixture;
MotionMatcher matcher(&fixture.skeleton, &fixture.database);
matcher.setSearchInterval(10.f);
std::vector<MotionSearchRange> seed{{0, 0.f, 0.f}};
REQUIRE(matcher.setCandidateRanges(seed).ok());
matcher.search();
std::vector<MotionSearchRange> ranges{{0, 0.f, 1.f, 0.f, false, {}, 1.f},
{1, 0.f, 1.f, .95f}};
REQUIRE(matcher.setCandidateRanges(ranges).ok());
matcher.search();
CHECK(matcher.getMatchedClipIndex() == 1);
}
TEST_CASE("animation.motionMatching.replacementRangesInvalidateDistantSameClipContinuation") {
BlockFixture fixture;
MotionMatcher matcher(&fixture.skeleton, &fixture.database);
matcher.setSearchInterval(10.f);
std::vector<MotionSearchRange> seed{{0, 0.f, 0.f}};
REQUIRE(matcher.setCandidateRanges(seed).ok());
matcher.search();
matcher.update(.3f);
REQUIRE(matcher.getMatchedClipIndex() == 0);
REQUIRE(matcher.getMatchedTime() < .5f);
// The same asset is still present, but only at a distant phase. It cannot
// masquerade as continuation of the current out-of-range playhead.
std::vector<MotionSearchRange> ranges{{0, .8f, 1.f, 10.f}, {1, 0.f, 1.f, 0.f}};
REQUIRE(matcher.setCandidateRanges(ranges).ok());
matcher.search();
CHECK(matcher.getMatchedClipIndex() == 1);
}
TEST_CASE("animation.motionMatching.authoredCandidateCostOverrideUsesLastActiveWindow") {
BlockFixture fixture;
MotionMatcher matcher(&fixture.skeleton, &fixture.database);
const float time = fixture.database.getFrameTime(3);
std::vector<MotionSearchRange> ranges{
{0, time, time, -10.f, false, {}, 0.f, {{0.f, 1.f, -2.f}, {time, time + .01f, 4.f}}},
{1, time, time, 0.f}};
REQUIRE(matcher.setCandidateRanges(ranges).ok());
matcher.search();
CHECK(matcher.getMatchedClipIndex() == 1);
// The override end is exclusive, so its base -10 cost wins exactly there.
const float end = fixture.database.getFrameTime(4);
ranges[0].start = end;
ranges[0].end = end;
ranges[0].costOverrides = {{0.f, end, 4.f}};
ranges[1].start = end;
ranges[1].end = end;
MotionMatcher boundary(&fixture.skeleton, &fixture.database);
REQUIRE(boundary.setCandidateRanges(ranges).ok());
boundary.search();
CHECK(boundary.getMatchedClipIndex() == 0);
}
TEST_CASE("animation.motionMatching.continuingCostBiasAndOverrideControlRetention") {
BlockFixture fixture;
MotionMatcher matcher(&fixture.skeleton, &fixture.database);
matcher.setSearchInterval(10.f);
std::vector<MotionSearchRange> seed{{0, 0.f, 0.f}};
REQUIRE(matcher.setCandidateRanges(seed).ok());
matcher.search();
matcher.update(.3f);
REQUIRE(matcher.getMatchedClipIndex() == 0);
std::vector<MotionSearchRange> ranges{
{0, 0.f, 1.f, 0.f, false, {}, -5.f},
{1, 0.f, 1.f, -1.f}};
REQUIRE(matcher.setCandidateRanges(ranges).ok());
matcher.search();
CHECK(matcher.getMatchedClipIndex() == 0);
// The source notify replaces the database continuing bias at this pose.
ranges[0].continuingCostOverrides = {{.2f, .8f, 5.f}};
REQUIRE(matcher.setCandidateRanges(ranges).ok());
matcher.search();
CHECK(matcher.getMatchedClipIndex() == 1);
}
TEST_CASE("animation.motionMatching.duplicateRangeRetainsCostPairFromWinningCandidate") {
BlockFixture fixture;
MotionMatcher matcher(&fixture.skeleton, &fixture.database);
matcher.setSearchInterval(10.f);
std::vector<MotionSearchRange> seed{{0, 0.f, 0.f}};
REQUIRE(matcher.setCandidateRanges(seed).ok());
matcher.search();
matcher.update(.3f);
std::vector<MotionSearchRange> ranges{
{0, 0.f, 1.f, -2.f, false, {}, 10.f},
{0, 0.f, 1.f, -1.f, false, {}, -10.f},
{1, 0.f, 1.f, 0.f}};
REQUIRE(matcher.setCandidateRanges(ranges).ok());
matcher.search();
CHECK(matcher.getMatchedClipIndex() == 1);
}