perf(animation): add parallel batch evaluation and bone LOD
This commit is contained in:
1 parent
a816043366
commit
a63cd414e3
12 files changed
+216
-2
No files matched your search
@@ -412,6 +412,8 @@ Root-motion 位移会补偿 loop 末尾到开头的跳变;旋转返回单位
|
||||
- 内建 GPU 蒙皮路径只需在 Mesh 创建后调用一次 `skin.bindGpuMesh(gfx, mesh)`,之后每帧在 `pose.computeWorld(skeleton)` 后调用 `skin.updateGpuMesh(mesh, pose)`;顶点保持 Bind Pose,Vulkan/WebGPU 顶点着色器读取四关节权重和最多 128 个骨骼矩阵完成变形,前向、阴影与 GBuffer 路径共享同一调色板。
|
||||
- 离线导入后可调用 `clip.compress(positionError, rotationErrorDegrees, scaleError)`,以逐轨道曲线误差为上限删除冗余关键帧;首尾关键帧、clip 属性和 gameplay notify 均保留。
|
||||
- `local targetClip = sourceClip.retarget(sourceSkeleton, targetSkeleton)` 按骨名匹配目标骨架,以源/目标 Bind Pose 的局部 TRS 差量重建轨道,并按对应骨段长度缩放位移。返回的新 clip 可继续压缩、进入状态机或 Animation Graph。
|
||||
- 批量角色求值使用 `AnimBatch`:通过 `anim.newBatch()` 创建,逐角色 `add(clip, skeleton, pose, time, lodLevel)`,最后 `evaluate(workerCount)`;另提供 `clear()`、`getCount()` 和 `getLastWorkerCount()`。`workerCount=0` 自动采用硬件并发度,任务以无锁原子索引分发,且拒绝同一 Pose 的并发写入。Pose 混合的平移/缩放在 x86/x64 使用 SSE 路径。
|
||||
- 骨骼 LOD 以 0 为最高细节。`skeleton.setBoneLodLimit(bone, highestLod)` 指定骨骼保留到哪一级,`skeleton.getBoneLodLimit(bone)` 可查询;`clip.sampleLod(...)` 和 `AnimBatch` 会让被裁剪轨道回退到目标 Bind Pose。根骨、碰撞骨和 IK 末端应保留较高 limit,手指与装饰骨通常只保留 LOD 0。
|
||||
- 参数约束、默认值和返回类型以对应模块头文件及 `addFunc` 绑定为准;本文 API 快查与当前源码同步生成。
|
||||
|
||||
**源码:** [`src/modules/animation/`](../../../src/modules/animation/)
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
#include "animation/AnimBatch.h"
|
||||
|
||||
#include "animation/AnimClip.h"
|
||||
#include "animation/AnimPose.h"
|
||||
#include "animation/AnimSkeleton.h"
|
||||
#include "common/Exception.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
|
||||
namespace eve::animation {
|
||||
|
||||
void AnimBatch::add(AnimClip* clip, AnimSkeleton* skeleton, AnimPose* pose, float time, int lodLevel) {
|
||||
if (!clip || !skeleton || !pose) throw Exception("AnimBatch.add: clip, skeleton and pose are required");
|
||||
if (lodLevel < 0) throw Exception("AnimBatch.add: lodLevel must be >= 0");
|
||||
for (const Job& job : jobs_) if (job.pose == pose) throw Exception("AnimBatch.add: pose is already queued");
|
||||
jobs_.push_back({clip, skeleton, pose, time, lodLevel});
|
||||
}
|
||||
|
||||
void AnimBatch::evaluate(int workerCount) {
|
||||
if (workerCount < 0) throw Exception("AnimBatch.evaluate: workerCount must be >= 0");
|
||||
if (jobs_.empty()) { lastWorkerCount_ = 0; return; }
|
||||
const unsigned hardware = std::max(1u, std::thread::hardware_concurrency());
|
||||
const size_t count = std::min(jobs_.size(), static_cast<size_t>(workerCount > 0 ? workerCount : hardware));
|
||||
lastWorkerCount_ = static_cast<int>(count);
|
||||
if (count == 1) {
|
||||
for (Job& job : jobs_) job.clip->sampleLod(job.time, job.pose, job.skeleton, job.lodLevel);
|
||||
return;
|
||||
}
|
||||
std::atomic<size_t> next{0};
|
||||
std::vector<std::thread> workers;
|
||||
workers.reserve(count);
|
||||
for (size_t worker = 0; worker < count; ++worker) {
|
||||
workers.emplace_back([&] {
|
||||
for (;;) {
|
||||
const size_t index = next.fetch_add(1, std::memory_order_relaxed);
|
||||
if (index >= jobs_.size()) break;
|
||||
Job& job = jobs_[index];
|
||||
job.clip->sampleLod(job.time, job.pose, job.skeleton, job.lodLevel);
|
||||
}
|
||||
});
|
||||
}
|
||||
for (std::thread& worker : workers) worker.join();
|
||||
}
|
||||
|
||||
} // namespace eve::animation
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace eve::animation {
|
||||
|
||||
class AnimClip;
|
||||
class AnimPose;
|
||||
class AnimSkeleton;
|
||||
|
||||
/** @brief Parallel batch evaluator for independent character clip poses. Script type: `AnimBatch`. */
|
||||
class AnimBatch {
|
||||
public:
|
||||
AnimBatch() = default;
|
||||
|
||||
/** @brief Queue one clip sample. The clip, skeleton and pose must outlive evaluate(). */
|
||||
void add(AnimClip* clip, AnimSkeleton* skeleton, AnimPose* pose, float time, int lodLevel = 0);
|
||||
/** @brief Remove all queued jobs without deleting referenced objects. */
|
||||
void clear() { jobs_.clear(); }
|
||||
int getCount() const { return static_cast<int>(jobs_.size()); }
|
||||
/** @brief Evaluate queued poses, using hardware concurrency when workerCount is zero. */
|
||||
void evaluate(int workerCount = 0);
|
||||
int getLastWorkerCount() const { return lastWorkerCount_; }
|
||||
|
||||
private:
|
||||
struct Job {
|
||||
AnimClip* clip = nullptr;
|
||||
AnimSkeleton* skeleton = nullptr;
|
||||
AnimPose* pose = nullptr;
|
||||
float time = 0.f;
|
||||
int lodLevel = 0;
|
||||
};
|
||||
std::vector<Job> jobs_;
|
||||
int lastWorkerCount_ = 0;
|
||||
};
|
||||
|
||||
} // namespace eve::animation
|
||||
@@ -297,6 +297,18 @@ void AnimClip::sample(float time, AnimPose* out, const AnimSkeleton* skeleton) c
|
||||
sampleClamped(wrapTime(time), out, skeleton);
|
||||
}
|
||||
|
||||
void AnimClip::sampleLod(float time, AnimPose* out, const AnimSkeleton* skeleton, int lodLevel) const {
|
||||
if (!out || !skeleton) throw Exception("AnimClip.sampleLod: pose or skeleton is null");
|
||||
if (lodLevel < 0) throw Exception("AnimClip.sampleLod: lodLevel must be >= 0");
|
||||
const int boneCount = skeleton->getBoneCount();
|
||||
out->resize(boneCount);
|
||||
time = clampf(wrapTime(time), 0.f, duration_);
|
||||
for (int i = 0; i < boneCount; ++i) {
|
||||
const TransformTRS& bind = skeleton->bindLocal(i);
|
||||
out->local(i) = lodLevel <= skeleton->getBoneLodLimit(i) ? sampleBone(i, time, bind) : bind;
|
||||
}
|
||||
}
|
||||
|
||||
void AnimClip::sampleClamped(float time, AnimPose* out, const AnimSkeleton* skeleton) const {
|
||||
if (!out) throw Exception("AnimClip.sample: pose is null");
|
||||
const int boneCount = skeleton ? skeleton->getBoneCount() : static_cast<int>(tracks_.size());
|
||||
|
||||
@@ -93,6 +93,8 @@ public:
|
||||
* fall back to bind pose; otherwise identity.
|
||||
*/
|
||||
void sample(float time, AnimPose* out, const AnimSkeleton* skeleton = nullptr) const;
|
||||
/** @brief Sample only bones enabled for lodLevel; culled tracks use target bind pose. */
|
||||
void sampleLod(float time, AnimPose* out, const AnimSkeleton* skeleton, int lodLevel) const;
|
||||
|
||||
/** @brief Wrap or clamp time according to loop flag. */
|
||||
float wrapTime(float time) const;
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
|
||||
#include "common/Exception.h"
|
||||
|
||||
#if defined(__SSE2__) || defined(_M_X64) || defined(_M_IX86_FP)
|
||||
#include <immintrin.h>
|
||||
#endif
|
||||
|
||||
namespace eve::animation {
|
||||
|
||||
namespace {
|
||||
@@ -124,8 +128,25 @@ void AnimPose::blendFrom(const AnimPose* a, const AnimPose* b, float t) {
|
||||
}
|
||||
resize(a->getBoneCount());
|
||||
for (int i = 0; i < getBoneCount(); ++i) {
|
||||
locals_[static_cast<size_t>(i)] =
|
||||
blendTRS(a->locals_[static_cast<size_t>(i)], b->locals_[static_cast<size_t>(i)], t);
|
||||
const TransformTRS& av = a->locals_[static_cast<size_t>(i)];
|
||||
const TransformTRS& bv = b->locals_[static_cast<size_t>(i)];
|
||||
TransformTRS& out = locals_[static_cast<size_t>(i)];
|
||||
#if defined(__SSE2__) || defined(_M_X64) || defined(_M_IX86_FP)
|
||||
const __m128 weight = _mm_set1_ps(clampf(t, 0.f, 1.f));
|
||||
const __m128 oneMinus = _mm_sub_ps(_mm_set1_ps(1.f), weight);
|
||||
alignas(16) float values[4];
|
||||
_mm_store_ps(values, _mm_add_ps(_mm_mul_ps(_mm_setr_ps(av.px, av.py, av.pz, 0.f), oneMinus),
|
||||
_mm_mul_ps(_mm_setr_ps(bv.px, bv.py, bv.pz, 0.f), weight)));
|
||||
out.px = values[0]; out.py = values[1]; out.pz = values[2];
|
||||
_mm_store_ps(values, _mm_add_ps(_mm_mul_ps(_mm_setr_ps(av.sx, av.sy, av.sz, 0.f), oneMinus),
|
||||
_mm_mul_ps(_mm_setr_ps(bv.sx, bv.sy, bv.sz, 0.f), weight)));
|
||||
out.sx = values[0]; out.sy = values[1]; out.sz = values[2];
|
||||
#else
|
||||
out.px = lerpf(av.px, bv.px, t); out.py = lerpf(av.py, bv.py, t); out.pz = lerpf(av.pz, bv.pz, t);
|
||||
out.sx = lerpf(av.sx, bv.sx, t); out.sy = lerpf(av.sy, bv.sy, t); out.sz = lerpf(av.sz, bv.sz, t);
|
||||
#endif
|
||||
slerpQuat(av.qx, av.qy, av.qz, av.qw, bv.qx, bv.qy, bv.qz, bv.qw, clampf(t, 0.f, 1.f),
|
||||
out.qx, out.qy, out.qz, out.qw);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,17 @@ void AnimSkeleton::setBindScale(int boneIndex, float x, float y, float z) {
|
||||
t.sz = z;
|
||||
}
|
||||
|
||||
void AnimSkeleton::setBoneLodLimit(int boneIndex, int highestLod) {
|
||||
requireBone(boneIndex);
|
||||
if (highestLod < 0) throw Exception("AnimSkeleton.setBoneLodLimit: highestLod must be >= 0");
|
||||
bones_[static_cast<size_t>(boneIndex)].lodLimit = highestLod;
|
||||
}
|
||||
|
||||
int AnimSkeleton::getBoneLodLimit(int boneIndex) const {
|
||||
requireBone(boneIndex);
|
||||
return bones_[static_cast<size_t>(boneIndex)].lodLimit;
|
||||
}
|
||||
|
||||
float AnimSkeleton::getBindPositionX(int boneIndex) const {
|
||||
requireBone(boneIndex);
|
||||
return bones_[static_cast<size_t>(boneIndex)].bind.px;
|
||||
|
||||
@@ -45,6 +45,14 @@ public:
|
||||
float getBindScaleY(int boneIndex) const;
|
||||
float getBindScaleZ(int boneIndex) const;
|
||||
|
||||
/**
|
||||
* @brief Set the highest animation LOD at which this bone is sampled (0 = full-detail only).
|
||||
* Descendants can use lower limits than gameplay-critical roots and effectors.
|
||||
*/
|
||||
void setBoneLodLimit(int boneIndex, int highestLod);
|
||||
/** @brief Highest LOD at which the bone is sampled; defaults to all LODs. */
|
||||
int getBoneLodLimit(int boneIndex) const;
|
||||
|
||||
const TransformTRS &bindLocal(int boneIndex) const;
|
||||
|
||||
/** @brief Fill pose locals with bind pose. */
|
||||
@@ -55,6 +63,7 @@ private:
|
||||
std::string name;
|
||||
int parent = -1;
|
||||
TransformTRS bind;
|
||||
int lodLimit = 0x7fffffff;
|
||||
};
|
||||
|
||||
void requireBone(int boneIndex) const;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "animation/Animation.h"
|
||||
#include "animation/AnimClip.h"
|
||||
#include "animation/AnimClipRegistry.h"
|
||||
#include "animation/AnimBatch.h"
|
||||
#include "animation/AnimImporter.h"
|
||||
#include "animation/AnimPlayer.h"
|
||||
#include "animation/AnimGraph.h"
|
||||
@@ -309,6 +310,7 @@ AnimSkeleton *Animation::newSkeleton() { return new AnimSkeleton(); }
|
||||
AnimClip *Animation::newClip(const std::string &name) { return new AnimClip(name); }
|
||||
|
||||
AnimPose *Animation::newPose(int boneCount) { return new AnimPose(boneCount); }
|
||||
AnimBatch *Animation::newBatch() { return new AnimBatch(); }
|
||||
|
||||
AnimPlayer *Animation::newPlayer(AnimSkeleton *skeleton) { return new AnimPlayer(skeleton); }
|
||||
AnimGraph *Animation::newGraph(AnimSkeleton *skeleton) { return new AnimGraph(skeleton); }
|
||||
@@ -522,6 +524,8 @@ void Animation::expose(ssq::Table &table) {
|
||||
sk.addFunc("setBindPosition", &AnimSkeleton::setBindPosition);
|
||||
sk.addFunc("setBindRotation", &AnimSkeleton::setBindRotation);
|
||||
sk.addFunc("setBindScale", &AnimSkeleton::setBindScale);
|
||||
sk.addFunc("setBoneLodLimit", &AnimSkeleton::setBoneLodLimit);
|
||||
sk.addFunc("getBoneLodLimit", &AnimSkeleton::getBoneLodLimit);
|
||||
sk.addFunc("getBindPositionX", &AnimSkeleton::getBindPositionX);
|
||||
sk.addFunc("getBindPositionY", &AnimSkeleton::getBindPositionY);
|
||||
sk.addFunc("getBindPositionZ", &AnimSkeleton::getBindPositionZ);
|
||||
@@ -558,6 +562,7 @@ void Animation::expose(ssq::Table &table) {
|
||||
clip.addFunc("compress", &AnimClip::compress);
|
||||
clip.addFunc("retarget", &AnimClip::retarget);
|
||||
clip.addFunc("sample", &AnimClip::sample);
|
||||
clip.addFunc("sampleLod", &AnimClip::sampleLod);
|
||||
clip.addFunc("wrapTime", &AnimClip::wrapTime);
|
||||
|
||||
auto pose = table.addClass<AnimPose>(
|
||||
@@ -591,6 +596,14 @@ void Animation::expose(ssq::Table &table) {
|
||||
pose.addFunc("getWorldRotationW", &AnimPose::getWorldRotationW);
|
||||
pose.addFunc("getWorldMatrixElement", &AnimPose::getWorldMatrixElement);
|
||||
|
||||
auto batch = table.addClass<AnimBatch>(
|
||||
"AnimBatch", std::function<AnimBatch *()>([]() -> AnimBatch * { return nullptr; }), true);
|
||||
batch.addFunc("add", &AnimBatch::add);
|
||||
batch.addFunc("clear", &AnimBatch::clear);
|
||||
batch.addFunc("getCount", &AnimBatch::getCount);
|
||||
batch.addFunc("evaluate", &AnimBatch::evaluate);
|
||||
batch.addFunc("getLastWorkerCount", &AnimBatch::getLastWorkerCount);
|
||||
|
||||
auto skin = table.addClass<AnimSkin>(
|
||||
"AnimSkin", std::function<AnimSkin *()>([]() -> AnimSkin * { return nullptr; }), true);
|
||||
skin.addFunc("getVertexCount", &AnimSkin::getVertexCount);
|
||||
@@ -1114,6 +1127,7 @@ void Animation::expose(ssq::Class &cls) {
|
||||
cls.addFunc("newSkeleton", &Animation::newSkeleton);
|
||||
cls.addFunc("newClip", &Animation::newClip);
|
||||
cls.addFunc("newPose", &Animation::newPose);
|
||||
cls.addFunc("newBatch", &Animation::newBatch);
|
||||
cls.addFunc("newPlayer", &Animation::newPlayer);
|
||||
cls.addFunc("newGraph", &Animation::newGraph);
|
||||
cls.addFunc("newStateMachine", &Animation::newStateMachine);
|
||||
|
||||
@@ -31,6 +31,7 @@ class ControlPose;
|
||||
class AnimSkin;
|
||||
class AnimLattice;
|
||||
class AnimTrail;
|
||||
class AnimBatch;
|
||||
class SpriteSheet;
|
||||
class SpriteClip;
|
||||
class SpriteAnim;
|
||||
@@ -105,6 +106,8 @@ public:
|
||||
AnimSkeleton *newSkeleton();
|
||||
AnimClip *newClip(const std::string &name = "");
|
||||
AnimPose *newPose(int boneCount = 0);
|
||||
/** @brief Create a parallel batch clip evaluator. */
|
||||
AnimBatch *newBatch();
|
||||
AnimPlayer *newPlayer(AnimSkeleton *skeleton);
|
||||
/** @brief Create a composable pose graph for the skeleton. */
|
||||
AnimGraph *newGraph(AnimSkeleton *skeleton);
|
||||
|
||||
@@ -162,6 +162,7 @@ file(GLOB all_test_cpp
|
||||
ik.cpp
|
||||
animation.cpp
|
||||
animation_retarget_compression.cpp
|
||||
animation_batch_lod.cpp
|
||||
animation_mixamo.cpp
|
||||
animation_skinned.cpp
|
||||
animation_lattice.cpp
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
#include "zeroerr/assert.h"
|
||||
#include "zeroerr/unittest.h"
|
||||
|
||||
#include "animation/AnimBatch.h"
|
||||
#include "animation/AnimClip.h"
|
||||
#include "animation/AnimPose.h"
|
||||
#include "animation/AnimSkeleton.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
using namespace eve::animation;
|
||||
|
||||
TEST_CASE("animation.batch.parallelEvaluationAndBoneLod") {
|
||||
AnimSkeleton skeleton;
|
||||
const int root = skeleton.addBone("root");
|
||||
const int spine = skeleton.addBone("spine", root);
|
||||
const int finger = skeleton.addBone("finger", spine);
|
||||
skeleton.setBindPosition(finger, 3.f, 0.f, 0.f);
|
||||
skeleton.setBoneLodLimit(finger, 0);
|
||||
|
||||
AnimClip clip("move");
|
||||
clip.setDuration(1.f);
|
||||
clip.addPositionKey(root, 0.f, 0.f, 0.f, 0.f);
|
||||
clip.addPositionKey(root, 1.f, 10.f, 0.f, 0.f);
|
||||
clip.addPositionKey(finger, 0.f, 3.f, 0.f, 0.f);
|
||||
clip.addPositionKey(finger, 1.f, 9.f, 0.f, 0.f);
|
||||
|
||||
std::vector<std::unique_ptr<AnimPose>> poses;
|
||||
AnimBatch batch;
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
poses.push_back(std::make_unique<AnimPose>());
|
||||
batch.add(&clip, &skeleton, poses.back().get(), 0.25f, 1);
|
||||
}
|
||||
batch.evaluate(4);
|
||||
|
||||
CHECK(batch.getLastWorkerCount() == 4);
|
||||
for (const auto& pose : poses) {
|
||||
CHECK(std::fabs(pose->getLocalPositionX(root) - 2.5f) < 1e-5f);
|
||||
CHECK(std::fabs(pose->getLocalPositionX(finger) - 3.f) < 1e-5f);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("animation.pose.simdBlendMatchesTrsBlend") {
|
||||
AnimPose a(1), b(1), out;
|
||||
a.setLocalPosition(0, 0.f, 2.f, 4.f);
|
||||
a.setLocalScale(0, 1.f, 2.f, 3.f);
|
||||
b.setLocalPosition(0, 10.f, 6.f, 0.f);
|
||||
b.setLocalScale(0, 3.f, 4.f, 5.f);
|
||||
out.blendFrom(&a, &b, 0.25f);
|
||||
CHECK(std::fabs(out.getLocalPositionX(0) - 2.5f) < 1e-6f);
|
||||
CHECK(std::fabs(out.getLocalPositionY(0) - 3.f) < 1e-6f);
|
||||
CHECK(std::fabs(out.getLocalScaleZ(0) - 3.5f) < 1e-6f);
|
||||
}
|
||||
Reference in new issue
Block a user