Вопрос или проблема
Я работаю над небольшим проектом, инди-игрой. Я застрял, потому что мне нужно загружать скелетные анимации в свою игру и визуализировать их.
Для этого я хочу использовать библиотеку ozz animation. Я посмотрел на их пример “воспроизведение анимации”, потому что думаю, что это связано с тем, что я хочу сделать, однако после, казалось бы, внедрения этого в свой собственный проект, это не работает. Код вершинного шейдера и настройка Vulkan довольно простые, поэтому я не загружу их здесь. Но вот код для преобразования данных анимации ozz в мои собственные структуры данных для последующей визуализации:
#include <fstream>
#include <vector>
#include <ma_UtilityFunctions.h>
#include <ma_OzzModel.h>
#include <iostream>
#include <stdexcept>
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
#include <map>
#include <algorithm>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/quaternion.hpp>
void ma_OzzModel::LoadAnimation(const std::string& animation_file, const std::string& skeleton_file, const std::string& mesh_file, std::vector<ma_Vertex>& vertices, std::vector<uint16_t>& indices) {
Assimp::Importer importer;
const aiScene* scene = importer.ReadFile(mesh_file.c_str(),
aiProcess_Triangulate | aiProcess_FlipUVs | aiProcess_GenNormals);
std::cout << "meshfile: " << mesh_file << std::endl;
if (!scene || !scene->HasMeshes()) {
std::cout << "scene: " << !scene << std::endl;
std::cout << "Has meshes: " << !scene->HasMeshes() << std::endl;
std::cerr << "Не удалось загрузить модель: " << importer.GetErrorString() << std::endl;
return;
}
size_t numVertices = 0;
for(size_t k = 0; k < scene->mNumMeshes; k++) {
aiMesh* mesh = scene->mMeshes[k];
for (unsigned int i = 0; i < mesh->mNumFaces; ++i) {
const aiFace& face = mesh->mFaces[i];
//std::cout << "Face " << i << ": ";
for (unsigned int j = 0; j < face.mNumIndices; ++j) {
//std::cout << face.mIndices[j] << " ";
indices.push_back(face.mIndices[j]);
}
//std::cout << std::endl;
}
std::vector<std::vector<std::pair<unsigned int, float>>> boneData(mesh->mNumVertices);
// Собираем веса костей для каждой вершины
for (unsigned int j = 0; j < mesh->mNumBones; ++j) {
const aiBone* bone = mesh->mBones[j];
for (unsigned int k = 0; k < bone->mNumWeights; ++k) {
const aiVertexWeight& weight = bone->mWeights[k];
unsigned int vertexId = weight.mVertexId;
float boneWeight = weight.mWeight;
boneData[vertexId].emplace_back(j, boneWeight);
}
}
// Обрабатываем каждую вершину
for (unsigned int i = 0; i < mesh->mNumVertices; ++i) {
numVertices += mesh->mNumVertices;
vertices.resize(numVertices);
ma_Vertex& vertex = vertices[i];
vertex.pos = glm::vec3(mesh->mVertices[i].x, mesh->mVertices[i].y, mesh->mVertices[i].z);
auto& vertexBonePairs = boneData[i];
if(vertexBonePairs.size() > NUM_OF_BONES_PER_VERTEX) std::cout << "слишком много костей в вершине : vertexBonePairs имеет " << vertexBonePairs.size() << " костей" << std::endl;
for(size_t j = 0; j < std::min(NUM_OF_BONES_PER_VERTEX, vertexBonePairs.size()); j++) {
vertex.boneIndices[j] = vertexBonePairs[j].first;
vertex.boneWeights[j] = vertexBonePairs[j].second;
}
}
}
// Файл в ozz является ozz::io::File, который реализует ozz::io::Stream
// интерфейс и соответствует спецификациям стандартного файла.
// ozz::io::File следует идиоме программирования RAII, которая гарантирует, что
// файл всегда будет закрыт (деструктор ozz::io::FileStream).
ozz::io::File animationFile(animation_file.c_str(), "rb");
ozz::io::File skeletonFile(skeleton_file.c_str(), "rb");
// Проверяет статус файла, который может быть закрыт, если имя файла недействительно.
if (!animationFile.opened()) {
std::cerr << "Не удается открыть файл " << animation_file << "." << std::endl;
assert(0);
}
// Проверяет статус файла, который может быть закрыт, если имя файла недействительно.
if (!skeletonFile.opened()) {
std::cerr << "Не удается открыть файл " << skeleton_file << "." << std::endl;
assert(0);
}
ozz::io::IArchive archiveAnimation(&animationFile);
if (!archiveAnimation.TestTag<ozz::animation::Animation>()) {
std::cerr << "Архив не содержит ожидаемого типа объекта анимации." <<
std::endl;
assert(0);
}
ozz::io::IArchive archiveSkeleton(&skeletonFile);
if (!archiveSkeleton.TestTag<ozz::animation::Skeleton>()) {
std::cerr << "Архив не содержит ожидаемого типа объекта скелета." <<
std::endl;
assert(0);
}
archiveSkeleton >> skeleton;
archiveAnimation >> animation;
}
void ma_OzzModel::UpdateBoneTransforms(std::vector<glm::mat4>& matrices, float _dt) {
std::vector<ozz::math::SoaTransform> data;
context_.Resize(skeleton.num_joints());
data.resize(skeleton.num_joints());
// Обновляет базовое время анимации для главной анимации.
controller_.Update(animation, _dt);
// Настройка задания выборки.
ozz::animation::SamplingJob sampling_job;
sampling_job.animation = &animation;
sampling_job.context = &context_;
sampling_job.ratio = controller_.time_ratio();
sampling_job.output = ozz::make_span(data);
if (!sampling_job.Run()) {
throw std::runtime_error("Не удалось выполнить задание выборки");
}
matrices = ConvertAllSoATransformsToMatrices(data);
std::cout << "количество трансформаций костей: " << data.size() << std::endl;
}
std::vector<glm::mat4> ma_OzzModel::ConvertAllSoATransformsToMatrices(const std::vector<ozz::math::SoaTransform>& soa_transforms) {
std::vector<glm::mat4> allMatrices;
for (const auto& soa_transform : soa_transforms) {
glm::vec3 position(soa_transform.translation.x[0], soa_transform.translation.y[0], soa_transform.translation.z[0]);
glm::quat rotation(soa_transform.rotation.x[0], soa_transform.rotation.y[0], soa_transform.rotation.z[0], soa_transform.rotation.w[0]);
glm::vec3 scale(soa_transform.scale.x[0], soa_transform.scale.y[0], soa_transform.scale.z[0]);
glm::mat4 mat = glm::translate(glm::mat4(1.0f), position);
mat *= glm::mat4_cast(rotation);
mat = glm::scale(mat, scale);
allMatrices.push_back(mat);
}
return allMatrices;
}
Вот декларация моей структуры Vertex:
#pragma once
#include <vulkan/vulkan.h>
#include <array>
#include <glm/vec2.hpp> // Включите заголовок для vec2
#include <glm/vec3.hpp> // Включите заголовок для vec3
#include <vector>
const size_t NUM_OF_BONES_PER_VERTEX = 4;
struct ma_Vertex {
glm::vec3 pos;
unsigned int boneIndices[NUM_OF_BONES_PER_VERTEX];
float boneWeights[NUM_OF_BONES_PER_VERTEX];
ma_Vertex() {
pos.x = 0;
pos.y = 0;
pos.z = 0;
}
ma_Vertex(const glm::vec3& p) {
pos.x = p.x;
pos.y = p.y;
pos.z = p.z;
}
static VkVertexInputBindingDescription getBindingDescription();
static std::array<VkVertexInputAttributeDescription, 3> getAttributeDescriptions();
};
Когда я запускаю свое приложение, модель выглядит искаженной и изуродованной, когда я применяю трансформации костей (только в тех частях, которые влияют на кости). Сама модель получается правильной, когда я загружаю ее без применения трансформации костей
Увидеть ошибку в моем коде может быть сложно, так что я также ищу предложения, что я мог бы попробовать вместо этого, или ссылки на примеры или работающие реализации Vulkan этого, которые бы мне помогли.
Спасибо заранее за вашу помощь!
Ответ или решение
Ваш вопрос о реализации воспроизведения анимации с использованием библиотеки Ozz Animation и вашего рендерера на Vulkan очень интересен, и я постараюсь помочь вам разобраться с возникшей проблемой. Прежде всего, давайте проанализируем ваш код и потенциальные проблемы, которые могут вызывать искажения модели во время анимации.
Общая структура кода
-
Загрузка модели и анимации: Ваш код загружает модель с помощью Assimp и анимацию и скелет с помощью Ozz Animation. Это правильный подход.
- Неправильное преобразование вершин: Проблемы с искажением часто связаны с неверным применением матриц преобразования к вершинам, особенно при использовании скилинга, вращения и трансляции.
Возможные решения
-
Проверка нормалей: Убедитесь, что вы правильно обрабатываете нормали вашей модели. Если у вас есть инверсии нормалей, это может привести к визуальным артефактам.
-
Корректное использование матриц: При применении матриц преобразования убедитесь, что вы используете правильный порядок операций. В большинстве движков графики (включая Vulkan) порядок применяемых преобразований имеет значение.
Пример кода для применения матриц преобразования к вершинам:
for (auto& vertex : vertices) { glm::mat4 boneTransform = allMatrices[vertex.boneIndices[0]]; // Если используется несколько костей, то учитывайте это glm::vec4 position = boneTransform * glm::vec4(vertex.pos, 1.0f); vertex.pos = glm::vec3(position); }
-
Альфа-смешивание весов костей: Если вы используете множественное влияние костей на одну вершину, убедитесь, что весы костей нормализованы. Например, они должны складываться в единицу:
float totalWeight = 0.0f; glm::vec3 updatedPosition(0.0f); for (size_t j = 0; j < NUM_OF_BONES_PER_VERTEX; ++j) { if (vertex.boneWeights[j] > 0) { glm::mat4 boneTransform = allMatrices[vertex.boneIndices[j]]; updatedPosition += boneTransform * glm::vec4(vertex.pos, 1.0f) * vertex.boneWeights[j]; totalWeight += vertex.boneWeights[j]; } } if (totalWeight > 0.0f) { updatedPosition /= totalWeight; // Нормализация } vertex.pos = updatedPosition;
- Проверка структуры вашей вершинной модели: Убедитесь, что структура данных
ma_Vertex
корректна и все поля инициализированы. Если вы используете веса костей и индексы, убедитесь, что они правильно сопоставлены при загрузке данных из вашей модели.
Рекомендации для отладки
- Вывод падений и ошибок: Добавьте дополнительный вывод для отладки, чтобы убедиться в корректности загружаемых данных и transformations. Например, выводите все матрицы преобразований для каждой кости.
- Тестовые данные: Используйте простые тестовые модели (например, с минимальным числом костей и вершин) для изоляции проблемы.
- Подбор масштабов: Проверьте и убедитесь, что ваши значения масштабов находятся в правильных границах, так как большие или слишком маленькие значения могут привести к искажению.
Заключение
Ваш подход к интеграции Ozz Animation с Vulkan выглядит довольно правильным, но искажения могут быть вызваны неправильной обработкой преобразования вершин. Проверьте порядок применения матриц, нормализацию весов и убедитесь в корректности данных при их загрузке. Я надеюсь, что эти рекомендации помогут вам решить проблему и успешно внедрить анимацию в вашу игру. Если у вас останутся вопросы, не стесняйтесь обращаться!