From e34fb2a900c306f189c9f6895ce1d116d39d7c15 Mon Sep 17 00:00:00 2001 From: tovjemam Date: Tue, 26 Aug 2025 19:06:19 +0200 Subject: [PATCH] Add loading of sk. meshes, skeletons and animations --- src/assets/animation.cpp | 147 +++++++++++++++++++++++++++++++++++++++ src/assets/animation.hpp | 42 +++++++++++ src/assets/mesh.cpp | 143 +++++++++++++++++++++++++++++++++++-- src/assets/mesh.hpp | 21 +++++- src/assets/skeleton.cpp | 74 ++++++++++++++++++++ src/assets/skeleton.hpp | 39 +++++++++++ src/gfx/vertex_array.cpp | 17 ++++- src/gfx/vertex_array.hpp | 2 + src/utils/transform.hpp | 24 +++++++ 9 files changed, 498 insertions(+), 11 deletions(-) create mode 100644 src/assets/animation.cpp create mode 100644 src/assets/animation.hpp create mode 100644 src/assets/skeleton.cpp create mode 100644 src/assets/skeleton.hpp create mode 100644 src/utils/transform.hpp diff --git a/src/assets/animation.cpp b/src/assets/animation.cpp new file mode 100644 index 0000000..37d2ef9 --- /dev/null +++ b/src/assets/animation.cpp @@ -0,0 +1,147 @@ +#include "animation.hpp" +#include "skeleton.hpp" + +#include +#include +#include + +std::shared_ptr assets::Animation::LoadFromFile(const std::string& filename, const Skeleton* skeleton) +{ + std::ifstream ifs(filename, std::ios::binary); + + if (!ifs.is_open()) + { + throw std::runtime_error("Failed to open animation file: " + filename); + } + + std::shared_ptr anim = std::make_shared(); + + int last_frame = 0; + std::vector frame_indices; + + auto FillFrameRefs = [&](int end_frame) + { + int channel_start = ((int)anim->channels_.size() - 1) * (int)anim->num_frames_; + int target_size = channel_start + end_frame; + size_t num_frame_refs = frame_indices.size(); + + if (num_frame_refs >= target_size) + { + return; // Already filled + } + + if (num_frame_refs % anim->num_frames_ == 0) + { + throw std::runtime_error("Cannot fill frames of channel that has 0 frames: " + filename); + } + + size_t last_frame_idx = frame_indices.back(); + frame_indices.resize(target_size, last_frame_idx); + }; + + std::string line; + + while (std::getline(ifs, line)) + { + if (line.empty() || line[0] == '#') // Skip empty lines and comments + continue; + + std::istringstream iss(line); + + std::string command; + iss >> command; + + if (command == "f") + { + if (anim->num_frames_ == 0) + { + throw std::runtime_error("Frame data specified before number of frames in animation file: " + filename); + } + + if (anim->channels_.empty()) + { + throw std::runtime_error("Frame data specified before any channels in animation file: " + filename); + } + + int frame_index; + iss >> frame_index; + + if (frame_index < 0 || frame_index >= (int)anim->num_frames_) + { + throw std::runtime_error("Frame index out of bounds in animation file: " + filename); + } + + if (frame_index < last_frame) + { + throw std::runtime_error("Frame indices must be in ascending order in animation file: " + filename); + } + + last_frame = frame_index; + + Transform t; + iss >> t.position.x >> t.position.y >> t.position.z; + glm::vec3 angles_deg; + iss >> angles_deg.x >> angles_deg.y >> angles_deg.z; + t.SetAngles(angles_deg); + iss >> t.scale; + + size_t idx = anim->frames_.size(); + anim->frames_.push_back(t); + + FillFrameRefs(frame_index); // Fill to current frame + frame_indices.push_back(idx); + } + else if (command == "ch") + { + std::string name; + iss >> name; + + int bone_index = skeleton->GetBoneIndex(name); + + if (bone_index < 0) + { + throw std::runtime_error("Bone referenced in animation not found in provided skeleton: " + name); + } + + FillFrameRefs(anim->num_frames_); // Fill to end for last channel + + AnimationChannel& channel = anim->channels_.emplace_back(); + channel.bone_index = bone_index; + channel.frames = nullptr; // Will be set up later + + last_frame = 0; + + } + else if (command == "frames") + { + iss >> anim->num_frames_; + } + else if (command == "fps") + { + iss >> anim->tps_; + } + } + + if (anim->channels_.empty()) + { + throw std::runtime_error("No channels found in animation file: " + filename); + } + + FillFrameRefs(anim->num_frames_); // Fill to end for last channel + + // Set up frame pointers + anim->frame_refs_.resize(frame_indices.size()); + for (size_t i = 0; i < frame_indices.size(); ++i) + { + anim->frame_refs_[i] = &anim->frames_[frame_indices[i]]; + } + + // Set up channel frame pointers + for (size_t i = 0; i < anim->channels_.size(); ++i) + { + AnimationChannel& channel = anim->channels_[i]; + channel.frames = &anim->frame_refs_[i * anim->num_frames_]; + } + + return anim; +} diff --git a/src/assets/animation.hpp b/src/assets/animation.hpp new file mode 100644 index 0000000..8388bbd --- /dev/null +++ b/src/assets/animation.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include + +#include "utils/transform.hpp" + +namespace assets +{ + class Skeleton; + + struct AnimationChannel + { + int bone_index; + const Transform* const* frames; + }; + + class Animation + { + size_t num_frames_ = 0; + float tps_ = 24.0f; + + std::vector channels_; + std::vector frame_refs_; + std::vector frames_; + + public: + Animation() = default; + + size_t GetNumFrames() const { return num_frames_; } + float GetTPS() const { return tps_; } + + size_t GetNumChannels() const { return channels_.size(); } + const AnimationChannel& GetChannel(int index) const { return channels_[index]; } + + static std::shared_ptr LoadFromFile(const std::string& filename, const Skeleton* skeleton); + + }; + + +} \ No newline at end of file diff --git a/src/assets/mesh.cpp b/src/assets/mesh.cpp index 1764928..cac229a 100644 --- a/src/assets/mesh.cpp +++ b/src/assets/mesh.cpp @@ -7,18 +7,72 @@ std::map> assets::Mesh::s_texture_cache; -assets::Mesh::Mesh(std::span verts, std::span tris, std::span materials) : - va_(gfx::VA_POSITION | gfx::VA_NORMAL | gfx::VA_UV | gfx::VA_LIGHTMAP_UV, gfx::VF_CREATE_EBO) +static int GetVertexAttrFlags(int mesh_flags) +{ + int attrs = gfx::VA_POSITION | gfx::VA_NORMAL | gfx::VA_UV; + + if (mesh_flags & assets::MF_HAS_LIGHTMAP_UV) + { + attrs |= gfx::VA_LIGHTMAP_UV; + } + + if (mesh_flags & assets::MF_IS_SKELETAL) + { + attrs |= gfx::VA_BONE_INDICES | gfx::VA_BONE_WEIGHTS; + } + + return attrs; +} + +template +static void BufferPut(std::vector& buffer, const T& val) +{ + const char* data = reinterpret_cast(&val); + buffer.insert(buffer.end(), data, data + sizeof(T)); +} + +assets::Mesh::Mesh(std::span verts, std::span tris, std::span materials, int flags) : + va_(GetVertexAttrFlags(flags), gfx::VF_CREATE_EBO) { verts_.assign(verts.begin(), verts.end()); tris_.assign(tris.begin(), tris.end()); materials_.assign(materials.begin(), materials.end()); - va_.SetVBOData(verts.data(), verts.size_bytes()); + std::vector buffer; + + for (const MeshVertex& vert : verts) + { + BufferPut(buffer, vert.pos); + BufferPut(buffer, vert.normal); + BufferPut(buffer, vert.uv); + + if (flags & MF_HAS_LIGHTMAP_UV) + { + BufferPut(buffer, vert.lightmap_uv); + } + + if (flags & MF_IS_SKELETAL) + { + // Bone indices as 4 ints + int32_t bone_indices[4] = { 0, 0, 0, 0 }; + float bone_weights[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; + + for (int i = 0; i < 4; ++i) + { + bone_indices[i] = static_cast(vert.bones[i].bone_index); + bone_weights[i] = vert.bones[i].weight; + } + + BufferPut(buffer, bone_indices); + BufferPut(buffer, bone_weights); + } + } + + va_.SetVBOData(buffer.data(), buffer.size()); va_.SetIndices(reinterpret_cast(tris.data()), tris.size() * 3); } -std::shared_ptr assets::Mesh::LoadFromFile(const std::string& filename, bool load_collision) +std::shared_ptr assets::Mesh::LoadFromFile(const std::string& filename, bool load_collision, const Skeleton* skeleton) { std::shared_ptr collision_mesh; @@ -32,9 +86,11 @@ std::shared_ptr assets::Mesh::LoadFromFile(const std::string& file throw std::runtime_error("Failed to open mesh file: " + filename); } + int flags = 0; std::vector verts; std::vector tris; std::vector materials; + std::vector bone_skel_ids; std::string line; @@ -54,7 +110,42 @@ std::shared_ptr assets::Mesh::LoadFromFile(const std::string& file iss >> vertex.pos.x >> vertex.pos.y >> vertex.pos.z; iss >> vertex.normal.x >> vertex.normal.y >> vertex.normal.z; iss >> vertex.uv.x >> vertex.uv.y; - iss >> vertex.lightmap_uv.x >> vertex.lightmap_uv.y; + + if (flags & MF_HAS_LIGHTMAP_UV) + { + iss >> vertex.lightmap_uv.x >> vertex.lightmap_uv.y; + } + + if (flags & MF_IS_SKELETAL) + { + int num_bones; + iss >> num_bones; + + if (num_bones > 4) + num_bones = 4; + + for (int i = 0; i < num_bones; ++i) + { + int bone_idx; + float weight; + iss >> bone_idx >> weight; + + if (bone_idx < 0 || bone_idx >= static_cast(bone_skel_ids.size())) + { + throw std::runtime_error("Bone index out of bounds in mesh file: " + filename); + } + + vertex.bones[i].bone_index = bone_skel_ids[bone_idx]; + vertex.bones[i].weight = weight; + } + + // Fill remaining influences with zeroes + for (int i = num_bones; i < 4; ++i) + { + vertex.bones[i].bone_index = -1; + vertex.bones[i].weight = 0.0f; + } + } } else if (command == "m") // Material switch { @@ -91,6 +182,46 @@ std::shared_ptr assets::Mesh::LoadFromFile(const std::string& file collision_mesh->AddTriangle(tri_verts[0], tri_verts[1], tri_verts[2]); } } + else if (command == "d") + { + std::string bone_name; + iss >> bone_name; + int idx = skeleton->GetBoneIndex(bone_name); + + if (idx < 0) + { + throw std::runtime_error("Bone referenced in mesh not found in provided skeleton: " + bone_name); + } + + bone_skel_ids.push_back(idx); + } + else if (command == "luv") + { + flags |= MF_HAS_LIGHTMAP_UV; + + if (verts.size() > 0) + { + throw std::runtime_error("Lightmap UV flag must be specified before vertex definitions in mesh file: " + filename); + } + } + else if (command == "skeletal") + { + flags |= MF_IS_SKELETAL; + + if (!skeleton) + { + throw std::runtime_error("Loading a skeletal mesh with no skeleton provided: " + filename); + } + + if (verts.size() > 0) + { + throw std::runtime_error("Skeletal flag must be specified before vertex definitions in mesh file: " + filename); + } + } + else + { + throw std::runtime_error("Unknown command in mesh file: " + command); + } } if (materials.size() > 0) @@ -120,7 +251,7 @@ std::shared_ptr assets::Mesh::LoadFromFile(const std::string& file } // Create the mesh object - std::shared_ptr mesh = std::make_shared(verts, tris, materials); + std::shared_ptr mesh = std::make_shared(verts, tris, materials, flags); if (load_collision) { diff --git a/src/assets/mesh.hpp b/src/assets/mesh.hpp index 116dd88..83236b5 100644 --- a/src/assets/mesh.hpp +++ b/src/assets/mesh.hpp @@ -9,15 +9,23 @@ #include "gfx/vertex_array.hpp" #include "gfx/texture.hpp" #include "collision/trianglemesh.hpp" +#include "skeleton.hpp" namespace assets { + struct MeshVertexBoneInfluence + { + int bone_index; + float weight; + }; + struct MeshVertex { glm::vec3 pos; glm::vec3 normal; glm::vec2 uv; glm::vec2 lightmap_uv; + MeshVertexBoneInfluence bones[4]; }; struct MeshTriangle @@ -33,17 +41,24 @@ namespace assets size_t num_tris = 0; }; + enum MeshFlags + { + MF_NONE = 0, + MF_HAS_LIGHTMAP_UV = 1 << 0, + MF_IS_SKELETAL = 1 << 1, + }; + class Mesh { std::vector verts_; std::vector tris_; std::vector materials_; - + gfx::VertexArray va_; std::shared_ptr collision_mesh_; public: - Mesh(std::span verts, std::span tris, std::span materials); + Mesh(std::span verts, std::span tris, std::span materials, int flags); void SetCollisionMesh(std::shared_ptr mesh) { collision_mesh_ = std::move(mesh); @@ -59,7 +74,7 @@ namespace assets std::span GetTriangles() const { return tris_; } std::span GetMaterials() const { return materials_; } - static std::shared_ptr LoadFromFile(const std::string& filename, bool load_collision); + static std::shared_ptr LoadFromFile(const std::string& filename, bool load_collision, const Skeleton* skeleton = nullptr); private: static std::map> s_texture_cache; diff --git a/src/assets/skeleton.cpp b/src/assets/skeleton.cpp new file mode 100644 index 0000000..fde68c3 --- /dev/null +++ b/src/assets/skeleton.cpp @@ -0,0 +1,74 @@ +#include "skeleton.hpp" + +#include +#include +#include + +void assets::Skeleton::AddBone(const std::string& name, const std::string& parent_name, const Transform& transform) +{ + int index = static_cast(bones_.size()); + + Bone& bone = bones_.emplace_back(); + bone.name = name; + bone.parent_idx = GetBoneIndex(parent_name); + bone.bind_transform = transform; + bone.inv_bind_matrix = glm::inverse(transform.ToMatrix()); + + bone_map_[bone.name] = index; +} + +int assets::Skeleton::GetBoneIndex(const std::string& name) const +{ + auto it = bone_map_.find(name); + if (it != bone_map_.end()) { + return it->second; + } + + return -1; +} + +std::shared_ptr assets::Skeleton::LoadFromFile(const std::string& filename) +{ + std::ifstream ifs(filename, std::ios::binary); + if (!ifs.is_open()) { + throw std::runtime_error("Failed to open skeleton file: " + filename); + } + + std::shared_ptr skeleton = std::make_shared(); + + std::string line; + + while (std::getline(ifs, line)) + { + if (line.empty() || line[0] == '#') // Skip empty lines and comments + continue; + + std::istringstream iss(line); + + std::string command; + iss >> command; + + if (command != "b") + continue; + + Transform t; + glm::vec3 angles; + std::string bone_name, parent_name; + + iss >> bone_name >> parent_name; + iss >> t.position.x >> t.position.y >> t.position.z; + iss >> angles.x >> angles.y >> angles.z; + iss >> t.scale; + + if (iss.fail()) + { + throw std::runtime_error("Failed to parse bone definition in file: " + filename); + } + + t.SetAngles(angles); + + skeleton->AddBone(bone_name, parent_name, t); + } + + return skeleton; +} diff --git a/src/assets/skeleton.hpp b/src/assets/skeleton.hpp new file mode 100644 index 0000000..5ad5dc7 --- /dev/null +++ b/src/assets/skeleton.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include + +#include "utils/transform.hpp" +#include "animation.hpp" + +namespace assets +{ + struct Bone + { + int parent_idx; + std::string name; + Transform bind_transform; + glm::mat4 inv_bind_matrix; + }; + + class Skeleton + { + std::vector bones_; + std::map bone_map_; + + public: + Skeleton() = default; + + void AddBone(const std::string& name, const std::string& parent_name, const Transform& transform); + + int GetBoneIndex(const std::string& name) const; + + const Bone& GetBone(int index) const { return bones_[index]; } + + static std::shared_ptr LoadFromFile(const std::string& filename); + }; + + +} \ No newline at end of file diff --git a/src/gfx/vertex_array.cpp b/src/gfx/vertex_array.cpp index 7c49d38..538e1f3 100644 --- a/src/gfx/vertex_array.cpp +++ b/src/gfx/vertex_array.cpp @@ -1,5 +1,7 @@ #include "vertex_array.hpp" +#include + // Struktura pro informace o jednotlivych vertex atributu struct VertexAttribInfo { GLuint index; @@ -15,7 +17,9 @@ static const VertexAttribInfo s_ATTR_INFO[] = { { 1, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 3 }, // VA_NORMAL { 2, 4, GL_UNSIGNED_BYTE, GL_TRUE, 4 }, // VA_COLOR { 3, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2 }, // VA_UV - { 4, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2 } // VA_LIGHTMAP_UV + { 4, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2 }, // VA_LIGHTMAP_UV + { 5, 4, GL_INT, GL_FALSE, sizeof(int32_t) * 4 }, // VA_BONE_INDICES + { 6, 4, GL_FLOAT, GL_FALSE, sizeof(float) * 4 }, // VA_BONE_WEIGHTS }; // Pocet typu vertex atributu @@ -45,7 +49,16 @@ gfx::VertexArray::VertexArray(int attrs, int flags) : m_usage(GL_STATIC_DRAW), m if (attrs & (1 << i)) { const auto& info = s_ATTR_INFO[i]; glEnableVertexAttribArray(info.index); - glVertexAttribPointer(info.index, info.size, info.type, info.normalized, stride, (const void*)offset); + + if (info.type != GL_INT) + { + glVertexAttribPointer(info.index, info.size, info.type, info.normalized, stride, (const void*)offset); + } + else + { + glVertexAttribIPointer(info.index, info.size, info.type, stride, (const void*)offset); + } + offset += info.byte_size; } } diff --git a/src/gfx/vertex_array.hpp b/src/gfx/vertex_array.hpp index 4a6efbf..4718a9a 100644 --- a/src/gfx/vertex_array.hpp +++ b/src/gfx/vertex_array.hpp @@ -18,6 +18,8 @@ namespace gfx VA_COLOR = 1 << 2, VA_UV = 1 << 3, VA_LIGHTMAP_UV = 1 << 4, + VA_BONE_INDICES = 1 << 5, + VA_BONE_WEIGHTS = 1 << 6, }; // Informace pro vytvoreni VertexArraye diff --git a/src/utils/transform.hpp b/src/utils/transform.hpp new file mode 100644 index 0000000..b66bab2 --- /dev/null +++ b/src/utils/transform.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +struct Transform +{ + glm::vec3 position; + glm::quat rotation; + float scale; + + glm::mat4 ToMatrix() const { + // TODO: Optimize matrix construction + glm::mat4 translation_mat = glm::translate(glm::mat4(1.0f), position); + glm::mat4 rotation_mat = glm::mat4_cast(rotation); + glm::mat4 scaling_mat = glm::scale(glm::mat4(1.0f), glm::vec3(scale)); + return translation_mat * rotation_mat * scaling_mat; + } + + void SetAngles(const glm::vec3& angles_deg) { + glm::vec3 angles_rad = glm::radians(angles_deg); + rotation = glm::quat(angles_rad); + } +}; \ No newline at end of file