321 lines
13 KiB
C++
321 lines
13 KiB
C++
#include "animation/AnimClip.h"
|
|
#include "animation/PoseInertiaInternal.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 {
|
|
void locomotion(AnimClip& clip, float speed) {
|
|
clip.setDuration(0.8f);
|
|
clip.setLoop(true);
|
|
clip.addPositionKey(0, 0.f, 0.f, 0.f, 0.f);
|
|
clip.addPositionKey(0, 0.8f, 0.f, 0.f, speed * 0.8f);
|
|
clip.addPositionKey(1, 0.f, 0.f, 0.1f, -0.2f);
|
|
clip.addPositionKey(1, 0.4f, 0.f, 0.3f, 0.2f);
|
|
clip.addPositionKey(1, 0.8f, 0.f, 0.1f, -0.2f);
|
|
}
|
|
} // namespace
|
|
|
|
TEST_CASE("animation.motionMatching.candidateIntervalsAreAtomicAndConstrainContinuation") {
|
|
AnimSkeleton skeleton;
|
|
skeleton.addBone("root");
|
|
skeleton.addBone("foot", 0);
|
|
AnimClip idle("idle"), walk("walk");
|
|
locomotion(idle, 0.f);
|
|
locomotion(walk, 2.f);
|
|
MotionDatabase db(&skeleton);
|
|
db.addFeatureBone(1);
|
|
db.addClip(&idle);
|
|
db.addClip(&walk);
|
|
db.bake();
|
|
MotionMatcher matcher(&skeleton, &db);
|
|
std::vector<MotionSearchRange> first{{1, 0.2f, 0.5f, -0.2f, true}};
|
|
auto selected = matcher.setCandidateRanges(first);
|
|
REQUIRE(selected.ok());
|
|
REQUIRE(selected.value() > 0);
|
|
matcher.search();
|
|
CHECK(matcher.getMatchedClipIndex() == 1);
|
|
CHECK(matcher.getMatchedTime() >= 0.19999f);
|
|
CHECK(matcher.getMatchedTime() <= 0.50001f);
|
|
for (auto bad : std::vector<MotionSearchRange>{
|
|
{9, 0.f, 0.5f}, {0, 0.6f, 0.2f}, {0, 0.f, 9.f}, {0, 0.f, 0.1f, std::numeric_limits<float>::quiet_NaN()}}) {
|
|
auto failed = matcher.setCandidateRanges(std::span(&bad, 1));
|
|
CHECK(!failed.ok());
|
|
matcher.search();
|
|
CHECK(matcher.getMatchedClipIndex() == 1);
|
|
}
|
|
auto empty = matcher.setCandidateRanges({});
|
|
CHECK(!empty.ok());
|
|
std::vector<MotionSearchRange> second{{0, 0.f, 0.1f}};
|
|
auto changed = matcher.setCandidateRanges(second);
|
|
REQUIRE(changed.ok());
|
|
matcher.search();
|
|
CHECK(matcher.getMatchedClipIndex() == 0);
|
|
CHECK(matcher.getMatchedTime() <= 0.10001f);
|
|
}
|
|
|
|
TEST_CASE("animation.motionMatching.externalPoseAndTrajectoryInputsAreCopied") {
|
|
AnimSkeleton skeleton;
|
|
skeleton.addBone("root");
|
|
skeleton.addBone("foot", 0);
|
|
AnimClip left("left"), right("right");
|
|
for (auto* clip : {&left, &right}) {
|
|
clip->setDuration(1.f);
|
|
clip->setLoop(true);
|
|
}
|
|
left.addPositionKey(1, 0.f, -1.f, 0.f, 0.f);
|
|
right.addPositionKey(1, 0.f, 1.f, 0.f, 0.f);
|
|
MotionDatabase db(&skeleton);
|
|
db.addFeatureBone(1);
|
|
db.addClip(&left);
|
|
db.addClip(&right);
|
|
db.bake();
|
|
MotionMatcher matcher(&skeleton, &db);
|
|
matcher.setPoseWeight(1.f);
|
|
AnimPose input(2);
|
|
input.setLocalPosition(1, 1.f, 0.f, 0.f);
|
|
ASSERT(matcher.setQueryPose(input).ok());
|
|
AnimPose wrongSkeleton(1);
|
|
ASSERT(!matcher.setQueryPose(wrongSkeleton).ok());
|
|
input.setLocalPosition(1, -1.f, 0.f, 0.f);
|
|
matcher.search();
|
|
CHECK(matcher.getMatchedClipIndex() == 1);
|
|
std::array<MotionTrajectorySample, 3> trajectory{};
|
|
auto configured = matcher.setTrajectory(trajectory);
|
|
CHECK(configured.ok());
|
|
trajectory[1].yaw = std::numeric_limits<float>::infinity();
|
|
auto invalid = matcher.setTrajectory(trajectory);
|
|
CHECK(!invalid.ok());
|
|
auto shortInput = matcher.setTrajectory(std::span(trajectory).first(2));
|
|
CHECK(!shortInput.ok());
|
|
}
|
|
|
|
TEST_CASE("animation.motionMatching.constantRootTrackReceivesPlanarTravel") {
|
|
AnimSkeleton skeleton;
|
|
skeleton.addBone("root");
|
|
AnimClip clip("in-place");
|
|
clip.setDuration(1.f);
|
|
clip.addPositionKey(0, 0.f, 0.f, 0.2f, 0.f);
|
|
clip.applyPlanarRootMotion(0, 0.f, 1.7f);
|
|
AnimPose pose;
|
|
clip.sample(0.5f, &pose, &skeleton);
|
|
CHECK(std::abs(pose.getLocalPositionZ(0) - 0.85f) < 0.0001f);
|
|
CHECK(std::abs(pose.getLocalPositionY(0) - 0.2f) < 0.0001f);
|
|
}
|
|
|
|
TEST_CASE("animation.motionMatching.loopFeaturesPreserveForwardTravel") {
|
|
AnimSkeleton skeleton;
|
|
skeleton.addBone("root");
|
|
skeleton.addBone("foot", 0);
|
|
AnimClip walk("walk");
|
|
locomotion(walk, 1.7f);
|
|
MotionDatabase database(&skeleton);
|
|
database.addFeatureBone(1);
|
|
database.addClip(&walk);
|
|
database.bake();
|
|
for (int i = 0; i < database.getFrameCount(); ++i) {
|
|
CHECK(std::abs(database.frameAt(i).velZ - 1.7f) < 0.001f);
|
|
// Constant velocity means every phase has identical trajectory features,
|
|
// even when a one-second horizon crosses more than one 0.8s cycle.
|
|
for (int f = 2; f < 8; ++f) CHECK(std::abs(database.frameAt(i).feature[f]) < 0.01f);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("animation.motionMatching.continuesCyclesAndChangesSpeedAndFacing") {
|
|
AnimSkeleton skeleton;
|
|
skeleton.addBone("root");
|
|
skeleton.addBone("foot", 0);
|
|
AnimClip idle("idle"), walk("walk"), run("run");
|
|
locomotion(idle, 0.f);
|
|
locomotion(walk, 1.7f);
|
|
locomotion(run, 4.2f);
|
|
MotionDatabase database(&skeleton);
|
|
database.addFeatureBone(1);
|
|
database.addClip(&idle);
|
|
database.addClip(&walk);
|
|
database.addClip(&run);
|
|
database.bake();
|
|
MotionMatcher matcher(&skeleton, &database);
|
|
matcher.setSearchInterval(0.08f);
|
|
matcher.setBlendTime(0.14f);
|
|
matcher.setVelocityWeight(2.f);
|
|
matcher.setPoseWeight(0.15f);
|
|
for (int clip = 0; clip < 3; ++clip) {
|
|
const float speed = clip == 0 ? 0.f : (clip == 1 ? 1.7f : 4.2f);
|
|
for (int direction = 0; direction < 4; ++direction) {
|
|
const float yaw = direction * 1.57079632679f;
|
|
matcher.setDesiredYaw(yaw);
|
|
matcher.setDesiredVelocity(std::sin(yaw) * speed, std::cos(yaw) * speed);
|
|
matcher.search();
|
|
REQUIRE(matcher.getMatchedClipIndex() == clip);
|
|
float time = matcher.getMatchedTime();
|
|
for (int frame = 0; frame < 180; ++frame) {
|
|
matcher.update(1.f / 60.f);
|
|
CHECK(matcher.getMatchedClipIndex() == clip);
|
|
CHECK(std::abs(matcher.getMatchedTime() - time - 1.f / 60.f) < 0.0001f);
|
|
time = matcher.getMatchedTime();
|
|
}
|
|
}
|
|
}
|
|
matcher.setDesiredVelocity(0.f, 0.f);
|
|
matcher.search();
|
|
CHECK(matcher.getMatchedClipIndex() == 0);
|
|
}
|
|
|
|
|
|
TEST_CASE("animation.motionMatching.sparseFeaturesMatchFullPose") {
|
|
AnimSkeleton skeleton;
|
|
skeleton.addBone("ancestor");
|
|
skeleton.addBone("root", 0);
|
|
skeleton.addBone("foot", 1);
|
|
skeleton.addBone("tip", 2);
|
|
skeleton.addBone("unrelated", 0);
|
|
skeleton.setBindScale(0, 1.2f, 0.9f, 1.4f);
|
|
skeleton.setBindRotation(0, 0.f, std::sin(0.15f), 0.f, std::cos(0.15f));
|
|
skeleton.setBindPosition(2, 0.2f, 0.4f, -0.3f);
|
|
skeleton.setBindPosition(3, 0.1f, 0.2f, 0.3f);
|
|
AnimClip clip("hierarchy");
|
|
clip.setDuration(0.6f);
|
|
clip.setSampleRate(10.f);
|
|
clip.addPositionKey(0, 0.f, 0.f, 0.f, 0.f);
|
|
clip.addPositionKey(0, 0.6f, 0.2f, 0.1f, 0.3f);
|
|
clip.addPositionKey(1, 0.f, 0.f, 0.f, 0.f);
|
|
clip.addPositionKey(1, 0.6f, 0.3f, 0.2f, 0.9f);
|
|
clip.addRotationKey(1, 0.f, 0.f, 0.f, 0.f, 1.f);
|
|
clip.addRotationKey(1, 0.6f, 0.f, std::sin(0.2f), 0.f, std::cos(0.2f));
|
|
AnimPose pose;
|
|
auto world = [&](int bone, float time) {
|
|
clip.sample(time, &pose, &skeleton);
|
|
pose.computeWorld(&skeleton);
|
|
return pose.world(bone);
|
|
};
|
|
auto yaw = [](const TransformTRS& t) { return std::atan2(2.f * t.qw * t.qy, 1.f - 2.f * t.qy * t.qy); };
|
|
for (int copies : {1, 16})
|
|
for (bool loop : {false, true}) {
|
|
clip.setLoop(false);
|
|
const auto start = world(1, 0.f), end = world(1, clip.getDuration());
|
|
clip.setLoop(loop);
|
|
MotionDatabase db(&skeleton);
|
|
db.setRootBone(1);
|
|
db.addFeatureBone(3);
|
|
for (int copy = 0; copy < copies; ++copy) db.addClip(&clip);
|
|
db.bake();
|
|
for (int i = 0; i < db.getFrameCount(); ++i) {
|
|
const auto& frame = db.frameAt(i);
|
|
const float time = frame.time;
|
|
const auto root = world(1, time);
|
|
const float angle = yaw(root), cs = std::cos(angle), sn = std::sin(angle);
|
|
auto future = [&](float t) {
|
|
auto value = world(1, t);
|
|
const float cycles = loop ? std::floor(t / clip.getDuration()) : 0.f;
|
|
value.px += cycles * (end.px - start.px);
|
|
value.pz += cycles * (end.pz - start.pz);
|
|
return value;
|
|
};
|
|
const auto next = future(time + 0.1f);
|
|
const float vx = (next.px - root.px) / 0.1f;
|
|
const float vz = (next.pz - root.pz) / 0.1f;
|
|
CHECK(std::abs(frame.rootX - root.px) < 0.00001f);
|
|
CHECK(std::abs(frame.rootZ - root.pz) < 0.00001f);
|
|
CHECK(std::abs(frame.velX - vx) < 0.00001f);
|
|
CHECK(std::abs(frame.velZ - vz) < 0.00001f);
|
|
std::vector<float> expected(13);
|
|
expected[0] = vx * cs - vz * sn;
|
|
expected[1] = vx * sn + vz * cs;
|
|
int offset = 2;
|
|
for (float horizon : {0.33f, 0.66f, 1.f}) {
|
|
const auto f = future(time + horizon);
|
|
const float dx = f.px - root.px, dz = f.pz - root.pz;
|
|
expected[offset++] = dx * cs - dz * sn;
|
|
expected[offset++] = dx * sn + dz * cs;
|
|
}
|
|
yawToForward(yaw(world(1, time + 1.f)) - angle, expected[8], expected[9]);
|
|
const auto foot = world(3, time);
|
|
expected[10] = (foot.px - root.px) * cs - (foot.pz - root.pz) * sn;
|
|
expected[11] = foot.py;
|
|
expected[12] = (foot.px - root.px) * sn + (foot.pz - root.pz) * cs;
|
|
db.normalizeFeature(expected);
|
|
for (int f = 0; f < 13; ++f) CHECK(std::abs(expected[f] - frame.feature[f]) < 0.001f);
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("animation.motionMatching.inertiaPreservesVelocityAndInterruptedPose") {
|
|
detail::PoseInertia inertia;
|
|
AnimPose source(1), previous(1), target(1), previousTarget(1), output(1);
|
|
source.setLocalPosition(0, 2.f, 0.f, 0.f);
|
|
previous.setLocalPosition(0, 1.97f, 0.f, 0.f);
|
|
target.setLocalPosition(0, 7.f, 0.f, 0.f);
|
|
previousTarget.setLocalPosition(0, 6.99f, 0.f, 0.f);
|
|
inertia.remember(previous, .01f);
|
|
inertia.begin(source, target, previousTarget, .3f);
|
|
inertia.apply(target, 0.f, output);
|
|
CHECK(std::abs(output.getLocalPositionX(0)-2.f)<1e-5f);
|
|
target.setLocalPosition(0,7.0001f,0.f,0.f);
|
|
inertia.apply(target,.0001f,output);
|
|
CHECK(std::abs((output.getLocalPositionX(0)-2.f)/.0001f-3.f)<.02f);
|
|
inertia.apply(target,.1f,output);
|
|
AnimPose interrupted(1); interrupted.copyFrom(&output);
|
|
inertia.remember(source,.1f);
|
|
target.setLocalPosition(0,-4.f,0.f,0.f);
|
|
previousTarget.copyFrom(&target);
|
|
inertia.begin(interrupted,target,previousTarget,.2f);
|
|
inertia.apply(target,0.f,output);
|
|
CHECK(std::abs(output.getLocalPositionX(0)-interrupted.getLocalPositionX(0))<1e-5f);
|
|
inertia.apply(target,.2f,output);
|
|
CHECK(output.getLocalPositionX(0)==-4.f);
|
|
}
|
|
|
|
TEST_CASE("animation.motionMatching.inertiaQuaternionHemisphereAndConvergence") {
|
|
detail::PoseInertia inertia;
|
|
AnimPose source(1), target(1), previousTarget(1), output(1);
|
|
source.setLocalRotation(0,0.f,std::sin(.8f),0.f,std::cos(.8f));
|
|
target.setLocalRotation(0,0.f,0.f,0.f,-1.f);
|
|
previousTarget.copyFrom(&target);
|
|
inertia.begin(source,target,previousTarget,.2f);
|
|
inertia.apply(target,0.f,output);
|
|
CHECK(std::abs(std::abs(output.local(0).qy)-std::sin(.8f))<1e-5f);
|
|
for(int i=0;i<=20;++i) {
|
|
inertia.apply(target,i*.01f,output);
|
|
auto q=output.local(0);
|
|
CHECK(std::abs(q.qx*q.qx+q.qy*q.qy+q.qz*q.qz+q.qw*q.qw-1.f)<1e-5f);
|
|
}
|
|
CHECK(std::abs(output.local(0).qy)<1e-5f);
|
|
inertia.begin(source,target,previousTarget,0.f);
|
|
inertia.apply(target,0.f,output);
|
|
CHECK(output.local(0).qw==-1.f);
|
|
}
|
|
|
|
TEST_CASE("animation.motionMatching.inertiaRunsThroughMatcherSearchAndAdvance") {
|
|
AnimSkeleton skeleton;
|
|
skeleton.addBone("root"); skeleton.addBone("foot",0);
|
|
AnimClip first("first"), second("second");
|
|
for(auto* clip : {&first,&second}) {clip->setDuration(2.f);clip->setLoop(false);}
|
|
first.addPositionKey(1,0.f,0.f,0.f,0.f);
|
|
first.addPositionKey(1,2.f,6.f,0.f,0.f);
|
|
second.addPositionKey(1,0.f,7.f,0.f,0.f);
|
|
second.addPositionKey(1,2.f,9.f,0.f,0.f);
|
|
MotionDatabase db(&skeleton); db.addFeatureBone(1);db.addClip(&first);db.addClip(&second);db.bake();
|
|
MotionMatcher matcher(&skeleton,&db);matcher.setSearchInterval(10.f);matcher.setBlendTime(.3f);
|
|
std::vector<MotionSearchRange> candidates{{0,0.f,1.f}};
|
|
REQUIRE(matcher.setCandidateRanges(candidates).ok());matcher.search();
|
|
matcher.update(.01f);matcher.update(.01f);
|
|
float before=matcher.getPose()->getLocalPositionX(1);
|
|
candidates[0].clipIndex=1;
|
|
REQUIRE(matcher.setCandidateRanges(candidates).ok());matcher.search();
|
|
CHECK(std::abs(matcher.getPose()->getLocalPositionX(1)-before)<1e-5f);
|
|
matcher.update(.0001f);
|
|
CHECK(std::abs((matcher.getPose()->getLocalPositionX(1)-before)/.0001f-3.f)<.02f);
|
|
matcher.update(.3f);
|
|
AnimPose expected;
|
|
second.sample(matcher.getMatchedTime(),&expected,&skeleton);
|
|
CHECK(std::abs(matcher.getPose()->getLocalPositionX(1)-expected.getLocalPositionX(1))<1e-5f);
|
|
}
|