164 lines
7.4 KiB
C++
164 lines
7.4 KiB
C++
#include <array>
|
|
#include <limits>
|
|
#include "animation/AnimClip.h"
|
|
#include "animation/AnimSkeleton.h"
|
|
#include "animation/MotionDatabase.h"
|
|
#include "animation/MotionFeatureLayout.h"
|
|
#include "animation/MotionMatcher.h"
|
|
#include "zeroerr/assert.h"
|
|
#include "zeroerr/unittest.h"
|
|
|
|
using namespace eve::animation;
|
|
|
|
TEST_CASE("animation.motionSchema.velocitySelectionCopiesQueryAndRejectsInvalidReplacement") {
|
|
AnimSkeleton skeleton;
|
|
skeleton.addBone("root");
|
|
skeleton.addBone("foot", 0);
|
|
AnimClip forward("forward"), backward("backward");
|
|
for (auto* clip : {&forward, &backward}) {
|
|
const float direction = clip == &forward ? 1.f : -1.f;
|
|
clip->setDuration(1.f);
|
|
clip->addPositionKey(1, 0.f, 0.f, 0.f, -.5f * direction);
|
|
clip->addPositionKey(1, 1.f, 0.f, 0.f, .5f * direction);
|
|
}
|
|
MotionFeatureChannel channel;
|
|
channel.kind = MotionFeatureKind::Velocity;
|
|
channel.query = MotionFeatureQuery::Character;
|
|
channel.bone = 1;
|
|
channel.axes = 4;
|
|
MotionFeatureLayout layout{20, {channel}};
|
|
MotionDatabase db(&skeleton);
|
|
REQUIRE(db.setFeatureLayout(layout).ok());
|
|
layout.channels[0].axes = 7;
|
|
CHECK(db.getFeatureSize() == 1);
|
|
db.addClip(&forward);
|
|
db.addClip(&backward);
|
|
db.bake();
|
|
CHECK(db.getFrameCount() == 42);
|
|
CHECK(!db.setFeatureLayout(layout).ok());
|
|
for (int direction : {-1, 1}) {
|
|
MotionMatcher matcher(&skeleton, &db);
|
|
const std::array<MotionSearchRange, 2> ranges{{{0, .5f, .5f}, {1, .5f, .5f}}};
|
|
REQUIRE(matcher.setCandidateRanges(ranges).ok());
|
|
AnimPose current(2), previous(2);
|
|
previous.setLocalPosition(1, 0.f, 0.f, -direction / 60.f);
|
|
REQUIRE(matcher.setFeatureQuery(current, previous, 1.f / 60.f, {}).ok());
|
|
previous.setLocalPosition(1, 0.f, 0.f, 100.f);
|
|
CHECK(!matcher.setFeatureQuery(current, previous, 0.f, {}).ok());
|
|
CHECK(!matcher.setQueryPose(current).ok());
|
|
const std::array<MotionTrajectorySample, 3> basic{};
|
|
CHECK(!matcher.setTrajectory(basic).ok());
|
|
matcher.search();
|
|
CHECK(matcher.getMatchedClipIndex() == (direction == 1 ? 0 : 1));
|
|
}
|
|
}
|
|
|
|
TEST_CASE("animation.motionSchema.layoutFailurePreservesConfiguration") {
|
|
AnimSkeleton skeleton;
|
|
skeleton.addBone("root");
|
|
MotionDatabase db(&skeleton);
|
|
MotionFeatureChannel channel;
|
|
channel.kind = MotionFeatureKind::Velocity;
|
|
channel.normalizationGroup = "speed";
|
|
MotionFeatureLayout layout{30, {channel}};
|
|
REQUIRE(db.setFeatureLayout(layout).ok());
|
|
layout.channels.push_back(channel);
|
|
layout.channels.back().normalizeVelocity = true;
|
|
layout.channels.back().axes = 0;
|
|
CHECK(!db.setFeatureLayout(layout).ok());
|
|
CHECK(db.getFeatureSize() == 3);
|
|
layout.channels.pop_back();
|
|
layout.channels[0].weight = std::numeric_limits<float>::quiet_NaN();
|
|
CHECK(!db.setFeatureLayout(layout).ok());
|
|
CHECK(db.getFeatureSize() == 3);
|
|
CHECK(db.hasFeatureLayout());
|
|
}
|
|
|
|
TEST_CASE("animation.motionSchema.mixedVelocityPoolPreservesAuthoredLengthScale") {
|
|
AnimSkeleton skeleton;skeleton.addBone("root");
|
|
AnimClip clip("travel");clip.setDuration(1.f);
|
|
clip.addPositionKey(0,0.f,0.f,0.f,0.f);clip.addPositionKey(0,1.f,2.f,0.f,0.f);
|
|
MotionFeatureChannel raw;
|
|
raw.kind=MotionFeatureKind::Velocity;raw.axes=1;raw.characterSpaceVelocity=false;
|
|
raw.normalizationGroup="speed";raw.query=MotionFeatureQuery::Character;
|
|
auto normalized=raw;normalized.normalizeVelocity=true;
|
|
MotionDatabase db(&skeleton);
|
|
REQUIRE(db.setFeatureLayout({30,{raw,normalized},100.f}).ok());
|
|
db.addClip(&clip);db.bake();
|
|
const auto& feature=db.frameAt(15).feature;
|
|
CHECK(std::abs(feature[0]-1.f)<.0001f);
|
|
CHECK(std::abs(feature[1]+1.f)<.0001f);
|
|
MotionMatcher matcher(&skeleton,&db);
|
|
AnimPose current(1),previous(1);current.setLocalPosition(0,1.f,0.f,0.f);
|
|
previous.setLocalPosition(0,1.f-2.f/60.f,0.f,0.f);
|
|
REQUIRE(matcher.setFeatureQuery(current,previous,1.f/60.f,{}).ok());
|
|
matcher.search();CHECK(matcher.getLastSearchCost()<.000001f);
|
|
}
|
|
|
|
TEST_CASE("animation.motionSchema.playRateMatchesUnnormalizedTrajectorySpeedRatio") {
|
|
AnimSkeleton skeleton;skeleton.addBone("root");
|
|
AnimClip clip("travel");clip.setDuration(2.f);
|
|
clip.addPositionKey(0,0.f,0.f,0.f,0.f);clip.addPositionKey(0,2.f,2.f,0.f,0.f);
|
|
MotionFeatureChannel velocity;
|
|
velocity.kind=MotionFeatureKind::Velocity;velocity.source=MotionFeatureSource::Trajectory;
|
|
velocity.query=MotionFeatureQuery::Character;velocity.axes=1;velocity.sampleTime=0.f;
|
|
velocity.characterSpaceVelocity=false;velocity.normalizeVelocity=false;
|
|
MotionDatabase db(&skeleton);REQUIRE(db.setFeatureLayout({30,{velocity},100.f}).ok());
|
|
db.addClip(&clip);db.bake();
|
|
MotionMatcher matcher(&skeleton,&db);
|
|
const std::array<MotionSearchRange,1> ranges{{MotionSearchRange{0,0.f,0.f}}};
|
|
REQUIRE(matcher.setCandidateRanges(ranges).ok());
|
|
REQUIRE(matcher.setPlayRateRange(.85f,1.15f).ok());
|
|
const float oldMin=matcher.getPlayRateMinimum(),oldMax=matcher.getPlayRateMaximum();
|
|
CHECK(!matcher.setPlayRateRange(1.2f,.8f).ok());
|
|
CHECK(matcher.getPlayRateMinimum()==oldMin);CHECK(matcher.getPlayRateMaximum()==oldMax);
|
|
AnimPose pose(1);std::array<MotionFeatureTrajectorySample,1> samples{};samples[0].vx=2.f;
|
|
REQUIRE(matcher.setFeatureQuery(pose,pose,1.f/60.f,samples).ok());
|
|
matcher.search();CHECK(std::abs(matcher.getPlayRate()-1.15f)<.0001f);
|
|
matcher.update(.5f);CHECK(std::abs(matcher.getMatchedTime()-.575f)<.0001f);
|
|
}
|
|
|
|
TEST_CASE("animation.motionSchema.arbitraryTrajectoryTimesSelectTravelAcrossClipBoundaries") {
|
|
AnimSkeleton skeleton;
|
|
skeleton.addBone("root");
|
|
for (bool loop : {false, true}) {
|
|
AnimClip forward("forward"), backward("backward");
|
|
for (auto* clip : {&forward, &backward}) {
|
|
clip->setDuration(1.f);
|
|
clip->setLoop(loop);
|
|
clip->addPositionKey(0, 0.f, 0.f, 0.f, 0.f);
|
|
clip->addPositionKey(0, 1.f, clip == &forward ? 2.f : -2.f, 0.f, 0.f);
|
|
}
|
|
MotionFeatureChannel past;
|
|
past.source = MotionFeatureSource::Trajectory;
|
|
past.query = MotionFeatureQuery::Character;
|
|
past.axes = 1;
|
|
past.sampleTime = -.2f;
|
|
MotionFeatureChannel future = past;
|
|
future.sampleTime = 1.35f;
|
|
MotionDatabase db(&skeleton);
|
|
REQUIRE(db.setFeatureLayout({20, {past, future}}).ok());
|
|
db.addClip(&forward);
|
|
db.addClip(&backward);
|
|
db.bake();
|
|
CHECK(db.getFrameCount() == (loop ? 40 : 42));
|
|
for (int direction : {-1, 1}) {
|
|
MotionMatcher matcher(&skeleton, &db);
|
|
const std::array<MotionSearchRange, 2> ranges{{{0, 0.f, 0.f}, {1, 0.f, 0.f}}};
|
|
REQUIRE(matcher.setCandidateRanges(ranges).ok());
|
|
AnimPose pose(1);
|
|
std::array<MotionFeatureTrajectorySample, 2> samples{};
|
|
samples[0].seconds = -.2f;
|
|
samples[0].x = -.4f * direction;
|
|
samples[1].seconds = 1.35f;
|
|
samples[1].x = 2.7f * direction;
|
|
REQUIRE(matcher.setFeatureQuery(pose, pose, 1.f / 60.f, samples).ok());
|
|
CHECK(!matcher.setFeatureQuery(pose, pose, 1.f / 60.f, std::span(samples).first(1)).ok());
|
|
samples[1].seconds = samples[0].seconds;
|
|
CHECK(!matcher.setFeatureQuery(pose, pose, 1.f / 60.f, samples).ok());
|
|
matcher.search();
|
|
CHECK(matcher.getMatchedClipIndex() == (direction == 1 ? 0 : 1));
|
|
}
|
|
}
|
|
}
|