我正在尝试使用 Assimp 库在 C++/Vulkan 中实现骨骼动画。我设法加载了骨架本身,一切似乎都井井有条。现在可以控制每个骨骼并指定相对于绑定变换的局部变换。一切似乎都运行良好。在代码中它看起来像这样
// Загрузка геометрии
auto ar2rGeometry = vk::helpers::LoadVulkanGeometryMesh(_vkRenderer,"Ar2r-Devil-Pinky.dae", true);
// Загрузка скелета
auto skeleton = vk::helpers::LoadVulkanMeshSkeleton("Ar2r-Devil-Pinky.dae");
// Добавление меша на сцену, его настройка и установка скелета
auto Ar2r = _vkRenderer->addMeshToScene(ar2rGeometry);
Ar2r->setPosition({0.0f, 0.0f, 0.0f}, false);
Ar2r->setScale({2.0f, 2.0f, 2.0f});
Ar2r->setSkeleton(std::move(skeleton));
// Доступ к костям скелета
auto torso = Ar2r->getSkeletonPtr()->getRootBone()->getChildrenBones()[0];
auto leg1 = Ar2r->getSkeletonPtr()->getRootBone()->getChildrenBones()[1];
auto leg2 = Ar2r->getSkeletonPtr()->getRootBone()->getChildrenBones()[2];
auto neck = torso->getChildrenBones()[0];
// Управление костями
torso->setLocalTransform(glm::rotate(glm::mat4(1.0f),glm::radians(45.0f),{1.0f, 0.0f, 0.0f}));
neck->setLocalTransform(glm::rotate(glm::mat4(1.0f),glm::radians(-20.0f),{1.0f, 0.0f, 0.0f}));
leg1->setLocalTransform(glm::rotate(glm::mat4(1.0f),glm::radians(30.0f),{1.0f, 0.0f, 0.0f}));
leg2->setLocalTransform(glm::rotate(glm::mat4(1.0f),glm::radians(-30.0f),{1.0f, 0.0f, 0.0f}));
骨骼的软件控制(直接来自代码)似乎工作得很好。结果,我可以使用骨骼使网格变形。
以防万一,我将附上计算分支矩阵(骨骼及其所有后代)的代码。这是 SkeletonBone 类的一个方法。
void calculateBranch(bool callUpdateCallbackFunction = true, unsigned calcFlags = CalcFlags::eFullTransform | CalcFlags::eBindTransform | CalcFlags::eInverseBindTransform)
{
// Если у кости есть родительская кость
if(pParentBone_ != nullptr)
{
// Общая initial (bind) трансформация для кости учитывает текущую и родительскую (что в свою очередь справедливо и для родительской)
if(calcFlags & CalcFlags::eBindTransform)
totalBindTransform_ = pParentBone_->totalBindTransform_ * this->localBindTransform_;
// Общая полная (с учетом задаваемой) трансформация кости (смещаем на localTransform_, затем на initial, затем на общую родительскую трансформацию)
if(calcFlags & CalcFlags::eFullTransform)
totalTransform_ = pParentBone_->totalTransform_ * this->localBindTransform_ * this->localTransform_;
}
// Если нет родительской кости - считать кость корневой
else
{
if(calcFlags & CalcFlags::eBindTransform)
totalBindTransform_ = this->localBindTransform_;
if(calcFlags & CalcFlags::eFullTransform)
totalTransform_ = this->localBindTransform_ * this->localTransform_;
}
// Инвертированная матрица bind трансформации
if(calcFlags & CalcFlags::eInverseBindTransform)
totalBindTransformInverse_ = glm::inverse(totalBindTransform_);
// Если есть указатель на объект скелета и индекс валиден
if(pSkeleton_ != nullptr && index_ < pSkeleton_->modelSpaceFinalTransforms_.size())
{
// Итоговая матрица трансформации для точек находящихся в пространстве модели
// Поскольку общая трансформация кости работает с вершинами находящимися в пространстве модели,
// они в начале должны быть переведены в пространство кости.
pSkeleton_->modelSpaceFinalTransforms_[index_] = totalTransform_ * totalBindTransformInverse_;
// Для ситуаций, если вершины задаются сразу в пространстве кости
pSkeleton_->boneSpaceFinalTransforms_[index_] = totalTransform_;
}
// Рекурсивно выполнить для дочерних элементов (если они есть)
if(!this->childrenBones_.empty()){
for(auto& childBone : this->childrenBones_){
childBone->calculateBranch(false, calcFlags);
}
}
// Если нужно вызвать функцию обновления UBO
if(callUpdateCallbackFunction && this->pSkeleton_ != nullptr && this->pSkeleton_->updateCallback_ != nullptr){
this->pSkeleton_->updateCallback_();
}
}
我还将附上用于加载骨架信息的函数的代码:
/**
* Загрузка скелета из файла 3D-моделей
* @param filename Имя файла в папке Models
* @return Объект скелета
*/
vk::scene::UniqueSkeleton LoadVulkanMeshSkeleton(const std::string &filename)
{
// Итоговый скелет
vk::scene::UniqueSkeleton skeleton = std::make_unique<vk::scene::Skeleton>();
// Полный путь к файлу
auto path = ::tools::ExeDir().append("..\\Models\\").append(filename);
// Импортер Assimp
Assimp::Importer importer;
// Получить сцену
const aiScene* scene = importer.ReadFile(path.c_str(),
aiProcess_Triangulate |
aiProcess_JoinIdenticalVertices |
//aiProcess_PreTransformVertices |
aiProcess_FlipWindingOrder |
aiProcess_PopulateArmatureData
);
// Если не удалось загрузить
if(scene == nullptr){
throw std::runtime_error(std::string("Can't load geometry from (").append(path).append(")").c_str());
}
// Если нет геометрических мешей
if(!scene->HasMeshes()){
throw std::runtime_error(std::string("Can't find any geometry meshes from (").append(path).append(")").c_str());
}
// Первый меш сцены
auto pFirstMesh = scene->mMeshes[0];
// Если у меша есть кости
if(pFirstMesh->HasBones())
{
// Инициализировать скелет
skeleton = std::make_unique<vk::scene::Skeleton>(pFirstMesh->mNumBones);
// Ассоциативный массив костей Assimp
std::unordered_map<std::string, aiBone*> bones{};
// Ассоциативный массив индексов костей
std::unordered_map<std::string, size_t> indices{};
// Пройтись по костям скелета и заполнить ассоциативные массив костей и индексов для доступа по именам
for(size_t i = 0; i < pFirstMesh->mNumBones; i++)
{
bones[pFirstMesh->mBones[i]->mName.C_Str()] = pFirstMesh->mBones[i];
indices[pFirstMesh->mBones[i]->mName.C_Str()] = i;
}
// Установить значение корневой кости скелета
auto rootBone = pFirstMesh->mBones[0];
skeleton->getRootBone()->setLocalBindTransform(ToGlmMat4(rootBone->mNode->mTransformation));
// Добавление дочерних костей
RecursivePopulateSkeleton(pFirstMesh->mBones[0]->mName.C_Str(), skeleton->getRootBone(), bones, indices, scene);
}
// Отдать скелет
return skeleton;
}
/**
* Рекурсивное заполнение данных скелета
* @param assimpBoneName Наименование кости assimp
* @param bone Текущая кость
* @param assimpBones Ассоциативный массив костей assimp (ключ - имя кости)
* @param assimpBoneIndices Ассоциативный массив индексов костей (ключ - имя кости)
*/
static inline void RecursivePopulateSkeleton(const std::string& assimpBoneName,
const vk::scene::SkeletonBonePtr& bone,
const std::unordered_map<std::string, aiBone*>& assimpBones,
const std::unordered_map<std::string, size_t>& assimpBoneIndices,
const aiScene* scene)
{
// Получить текущую кость assimp
auto assimpBone = assimpBones.at(assimpBoneName);
// Если у кости есть потомки
if(assimpBone->mNode->mNumChildren > 0)
{
// Пройтись по ним
for(size_t i = 0; i < assimpBone->mNode->mNumChildren; i++)
{
// Получить необходимые данные о потомке
auto childNode = assimpBone->mNode->mChildren[i];
// Если такого индекса кости не обнаружено - пропуск итерации
if(assimpBoneIndices.find(childNode->mName.C_Str()) == assimpBoneIndices.end())
continue;
// Индекс кости
auto childIndex = assimpBoneIndices.at(childNode->mName.C_Str());
// Добавить нового потомка в текущую кость
auto child = bone->addChildBone(childIndex, ToGlmMat4(childNode->mTransformation),glm::mat4(1.0f));
// Рекурсивно выполнить эту функцию для потомка
RecursivePopulateSkeleton(childNode->mName.C_Str(), child, assimpBones, assimpBoneIndices, scene);
}
}
}
正如我上面所写,加载骨架工作正常。作为绑定转换,在加载时,我在 Assimp 节点(节点)处使用 mTransformation 矩阵。结果,一切都正确加载,网格以绑定姿势显示。对于每个单独的骨骼,您可以设置一个额外的(相对于绑定的局部)动画,并且一切正常。
然后我决定尝试使用 Assimp 从 Collada 文件中加载动画信息。
从各种教程来看,关键帧中的变换应该在 LOCAL 骨骼空间(相对于父骨骼)。我尝试下载此信息。我是这样做的:
// Пройтись по набору анимаций сцены
for(size_t i = 0; i < scene->mNumAnimations; i++)
{
// Указатель на анимацию Assimp
auto pAiAnimation = scene->mAnimations[i];
// Кол-во ключевых кадров (считаем что у всех каналов одинаковое кол-во ключевых кадров)
auto keyframesCount = pAiAnimation->mChannels[0]->mNumRotationKeys;
// Продолжительность в тиках (1 тик - 1 м/с)
auto duration = static_cast<float>(pAiAnimation->mDuration);
// Создать анимацию
auto animation = std::make_shared<vk::scene::SkeletonAnimation>(duration);
// Пройтись по ключевым кадрам
for(size_t f = 0; f < keyframesCount; f++)
{
// Время кадра
auto frameTime = static_cast<float>(pAiAnimation->mChannels[0]->mRotationKeys[f].mTime);
// Создать кадр
vk::scene::SkeletonAnimation::Keyframe keyframe(frameTime,totalBones);
// Пройтись по всем костям
for(size_t j = 0; j < pAiAnimation->mNumChannels; j++)
{
// Указатель на канал (кость) Assimp
auto pAiBoneChannel = pAiAnimation->mChannels[j];
// Получить индекс кости
auto boneIndex = indices.at(pAiBoneChannel->mNodeName.C_Str());
// Установить трансформацию кости в кадре
keyframe.setBonePosition(boneIndex,{
ToGlmVec3(pAiBoneChannel->mPositionKeys[f].mValue),
ToGlmQuat(pAiBoneChannel->mRotationKeys[f].mValue),
ToGlmVec3(pAiBoneChannel->mScalingKeys[f].mValue)
});
}
// Добавить ключевой кадр
animation->addKeyFrame(keyframe);
}
// Добавить анимацию
animations.push_back(animation);
}
从 Assimp 转换为 GLM 的方法:
static inline glm::vec3 ToGlmVec3(const aiVector3D &v) { return glm::vec3(v.x, v.y, v.z); }
static inline glm::vec2 ToGlmVec2(const aiVector3D &v) { return glm::vec2(v.x, v.y); }
static inline glm::quat ToGlmQuat(const aiQuaternion &q) { return glm::quat(q.w, q.x, q.y, q.z); }
static inline glm::mat4 ToGlmMat4(const aiMatrix4x4 &m) { return glm::transpose(glm::make_mat4(&m.a1)); }
static inline glm::mat4 ToGlmMat4(const aiMatrix3x3 &m) { return glm::transpose(glm::make_mat3(&m.a1)); }
但这只是一个下载。接下来,我尝试简单地从第一帧中获取骨骼变换数据。
// Получить положения костей для кадра 0
auto bonePositions = this->skeletonAnimation_->getKeyFrames()[0].getBonePositions();
// Пройтись по положениям костей
for(size_t i = 0; i < bonePositions.size(); i++)
{
// Получить кость скелета
auto bone = this->getSkeletonPtr()->getBoneByIndex(i);
// Получить матрицы трансформации
auto scaleM = glm::scale(glm::mat4(1.0f),bonePositions[i].scaling);
auto rotM = glm::toMat4(bonePositions[i].rotation);
auto translateM = glm::transpose(glm::translate(glm::mat4(1.0f),bonePositions[i].location));
// Установить локальную трансформацию
bone->setLocalTransform(rotM, false);
}
// Пересчитать матрицы
this->getSkeletonPtr()->getRootBone()->calculateBranch(true);
正如你从代码中看到的那样,我决定只从旋转开始(事实上,只有它们在那里改变了)。结果,我得到了一些不足之处:
我尝试应用其他矩阵(偏移和缩放),但没有帮助。我试图将四元数转换为通常的欧拉角,看看值是什么——而那里的值是从哪里来的完全无法理解。
我的错误是什么?也许我对 Assimp 的功能一无所知?Assimp (mPositionKeys, mRotationKeys, mScalingKeys) 返回的数据在什么空间?



问题解决了。问题是 assimp 为动画提供的变换已经包含骨骼的局部绑定变换。也就是说,在您的代码中:
我预计会从 Assimp 收到
localTransform_,但实际上我收到了localBindTransform_ * localTransform_。这个问题是通过乘以变换的反向绑定矩阵来解决的。
也许有人会发现它很有用。