Как реализовать пример воспроизведения анимации Ozz с использованием собственного рендерера (я использую Vulkan)

Вопрос или проблема

Я работаю над небольшим проектом, инди-игрой. Я застрял, потому что мне нужно загружать скелетные анимации в свою игру и визуализировать их.

Для этого я хочу использовать библиотеку 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();

};

Когда я запускаю свое приложение, модель выглядит искаженной и изуродованной, когда я применяю трансформации костей (только в тех частях, которые влияют на кости). Сама модель получается правильной, когда я загружаю ее без применения трансформации костей
модель в blender
модель визуализирована в моем приложении
Увидеть ошибку в моем коде может быть сложно, так что я также ищу предложения, что я мог бы попробовать вместо этого, или ссылки на примеры или работающие реализации Vulkan этого, которые бы мне помогли.

Спасибо заранее за вашу помощь!

Ответ или решение

Ваш вопрос о реализации воспроизведения анимации с использованием библиотеки Ozz Animation и вашего рендерера на Vulkan очень интересен, и я постараюсь помочь вам разобраться с возникшей проблемой. Прежде всего, давайте проанализируем ваш код и потенциальные проблемы, которые могут вызывать искажения модели во время анимации.

Общая структура кода

  1. Загрузка модели и анимации: Ваш код загружает модель с помощью Assimp и анимацию и скелет с помощью Ozz Animation. Это правильный подход.

  2. Неправильное преобразование вершин: Проблемы с искажением часто связаны с неверным применением матриц преобразования к вершинам, особенно при использовании скилинга, вращения и трансляции.

Возможные решения

  1. Проверка нормалей: Убедитесь, что вы правильно обрабатываете нормали вашей модели. Если у вас есть инверсии нормалей, это может привести к визуальным артефактам.

  2. Корректное использование матриц: При применении матриц преобразования убедитесь, что вы используете правильный порядок операций. В большинстве движков графики (включая 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);
    }
  3. Альфа-смешивание весов костей: Если вы используете множественное влияние костей на одну вершину, убедитесь, что весы костей нормализованы. Например, они должны складываться в единицу:

    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;
  4. Проверка структуры вашей вершинной модели: Убедитесь, что структура данных ma_Vertex корректна и все поля инициализированы. Если вы используете веса костей и индексы, убедитесь, что они правильно сопоставлены при загрузке данных из вашей модели.

Рекомендации для отладки

  • Вывод падений и ошибок: Добавьте дополнительный вывод для отладки, чтобы убедиться в корректности загружаемых данных и transformations. Например, выводите все матрицы преобразований для каждой кости.
  • Тестовые данные: Используйте простые тестовые модели (например, с минимальным числом костей и вершин) для изоляции проблемы.
  • Подбор масштабов: Проверьте и убедитесь, что ваши значения масштабов находятся в правильных границах, так как большие или слишком маленькие значения могут привести к искажению.

Заключение

Ваш подход к интеграции Ozz Animation с Vulkan выглядит довольно правильным, но искажения могут быть вызваны неправильной обработкой преобразования вершин. Проверьте порядок применения матриц, нормализацию весов и убедитесь в корректности данных при их загрузке. Я надеюсь, что эти рекомендации помогут вам решить проблему и успешно внедрить анимацию в вашу игру. Если у вас останутся вопросы, не стесняйтесь обращаться!

Оцените материал
Добавить комментарий

Капча загружается...