Add loading of sk. meshes, skeletons and animations

This commit is contained in:
tovjemam 2025-08-26 19:06:19 +02:00
parent cb7f76c8f0
commit e34fb2a900
9 changed files with 498 additions and 11 deletions

147
src/assets/animation.cpp Normal file
View File

@ -0,0 +1,147 @@
#include "animation.hpp"
#include "skeleton.hpp"
#include <fstream>
#include <sstream>
#include <stdexcept>
std::shared_ptr<assets::Animation> 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<Animation> anim = std::make_shared<Animation>();
int last_frame = 0;
std::vector<size_t> 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;
}

42
src/assets/animation.hpp Normal file
View File

@ -0,0 +1,42 @@
#pragma once
#include <vector>
#include <memory>
#include <string>
#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<AnimationChannel> channels_;
std::vector<const Transform*> frame_refs_;
std::vector<Transform> 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<Animation> LoadFromFile(const std::string& filename, const Skeleton* skeleton);
};
}

View File

@ -7,18 +7,72 @@
std::map<std::string, std::weak_ptr<gfx::Texture>> assets::Mesh::s_texture_cache; std::map<std::string, std::weak_ptr<gfx::Texture>> assets::Mesh::s_texture_cache;
assets::Mesh::Mesh(std::span<MeshVertex> verts, std::span<MeshTriangle> tris, std::span<MeshMaterial> materials) : static int GetVertexAttrFlags(int mesh_flags)
va_(gfx::VA_POSITION | gfx::VA_NORMAL | gfx::VA_UV | gfx::VA_LIGHTMAP_UV, gfx::VF_CREATE_EBO) {
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 <class T>
static void BufferPut(std::vector<char>& buffer, const T& val)
{
const char* data = reinterpret_cast<const char*>(&val);
buffer.insert(buffer.end(), data, data + sizeof(T));
}
assets::Mesh::Mesh(std::span<MeshVertex> verts, std::span<MeshTriangle> tris, std::span<MeshMaterial> materials, int flags) :
va_(GetVertexAttrFlags(flags), gfx::VF_CREATE_EBO)
{ {
verts_.assign(verts.begin(), verts.end()); verts_.assign(verts.begin(), verts.end());
tris_.assign(tris.begin(), tris.end()); tris_.assign(tris.begin(), tris.end());
materials_.assign(materials.begin(), materials.end()); materials_.assign(materials.begin(), materials.end());
va_.SetVBOData(verts.data(), verts.size_bytes()); std::vector<char> 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<int32_t>(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<const GLuint*>(tris.data()), tris.size() * 3); va_.SetIndices(reinterpret_cast<const GLuint*>(tris.data()), tris.size() * 3);
} }
std::shared_ptr<assets::Mesh> assets::Mesh::LoadFromFile(const std::string& filename, bool load_collision) std::shared_ptr<assets::Mesh> assets::Mesh::LoadFromFile(const std::string& filename, bool load_collision, const Skeleton* skeleton)
{ {
std::shared_ptr<collision::TriangleMesh> collision_mesh; std::shared_ptr<collision::TriangleMesh> collision_mesh;
@ -32,9 +86,11 @@ std::shared_ptr<assets::Mesh> assets::Mesh::LoadFromFile(const std::string& file
throw std::runtime_error("Failed to open mesh file: " + filename); throw std::runtime_error("Failed to open mesh file: " + filename);
} }
int flags = 0;
std::vector<MeshVertex> verts; std::vector<MeshVertex> verts;
std::vector<MeshTriangle> tris; std::vector<MeshTriangle> tris;
std::vector<MeshMaterial> materials; std::vector<MeshMaterial> materials;
std::vector<int> bone_skel_ids;
std::string line; std::string line;
@ -54,7 +110,42 @@ std::shared_ptr<assets::Mesh> assets::Mesh::LoadFromFile(const std::string& file
iss >> vertex.pos.x >> vertex.pos.y >> vertex.pos.z; iss >> vertex.pos.x >> vertex.pos.y >> vertex.pos.z;
iss >> vertex.normal.x >> vertex.normal.y >> vertex.normal.z; iss >> vertex.normal.x >> vertex.normal.y >> vertex.normal.z;
iss >> vertex.uv.x >> vertex.uv.y; 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<int>(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 else if (command == "m") // Material switch
{ {
@ -91,6 +182,46 @@ std::shared_ptr<assets::Mesh> assets::Mesh::LoadFromFile(const std::string& file
collision_mesh->AddTriangle(tri_verts[0], tri_verts[1], tri_verts[2]); 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) if (materials.size() > 0)
@ -120,7 +251,7 @@ std::shared_ptr<assets::Mesh> assets::Mesh::LoadFromFile(const std::string& file
} }
// Create the mesh object // Create the mesh object
std::shared_ptr<Mesh> mesh = std::make_shared<Mesh>(verts, tris, materials); std::shared_ptr<Mesh> mesh = std::make_shared<Mesh>(verts, tris, materials, flags);
if (load_collision) if (load_collision)
{ {

View File

@ -9,15 +9,23 @@
#include "gfx/vertex_array.hpp" #include "gfx/vertex_array.hpp"
#include "gfx/texture.hpp" #include "gfx/texture.hpp"
#include "collision/trianglemesh.hpp" #include "collision/trianglemesh.hpp"
#include "skeleton.hpp"
namespace assets namespace assets
{ {
struct MeshVertexBoneInfluence
{
int bone_index;
float weight;
};
struct MeshVertex struct MeshVertex
{ {
glm::vec3 pos; glm::vec3 pos;
glm::vec3 normal; glm::vec3 normal;
glm::vec2 uv; glm::vec2 uv;
glm::vec2 lightmap_uv; glm::vec2 lightmap_uv;
MeshVertexBoneInfluence bones[4];
}; };
struct MeshTriangle struct MeshTriangle
@ -33,17 +41,24 @@ namespace assets
size_t num_tris = 0; size_t num_tris = 0;
}; };
enum MeshFlags
{
MF_NONE = 0,
MF_HAS_LIGHTMAP_UV = 1 << 0,
MF_IS_SKELETAL = 1 << 1,
};
class Mesh class Mesh
{ {
std::vector<MeshVertex> verts_; std::vector<MeshVertex> verts_;
std::vector<MeshTriangle> tris_; std::vector<MeshTriangle> tris_;
std::vector<MeshMaterial> materials_; std::vector<MeshMaterial> materials_;
gfx::VertexArray va_; gfx::VertexArray va_;
std::shared_ptr<collision::TriangleMesh> collision_mesh_; std::shared_ptr<collision::TriangleMesh> collision_mesh_;
public: public:
Mesh(std::span<MeshVertex> verts, std::span<MeshTriangle> tris, std::span<MeshMaterial> materials); Mesh(std::span<MeshVertex> verts, std::span<MeshTriangle> tris, std::span<MeshMaterial> materials, int flags);
void SetCollisionMesh(std::shared_ptr<collision::TriangleMesh> mesh) { void SetCollisionMesh(std::shared_ptr<collision::TriangleMesh> mesh) {
collision_mesh_ = std::move(mesh); collision_mesh_ = std::move(mesh);
@ -59,7 +74,7 @@ namespace assets
std::span<const MeshTriangle> GetTriangles() const { return tris_; } std::span<const MeshTriangle> GetTriangles() const { return tris_; }
std::span<const MeshMaterial> GetMaterials() const { return materials_; } std::span<const MeshMaterial> GetMaterials() const { return materials_; }
static std::shared_ptr<Mesh> LoadFromFile(const std::string& filename, bool load_collision); static std::shared_ptr<Mesh> LoadFromFile(const std::string& filename, bool load_collision, const Skeleton* skeleton = nullptr);
private: private:
static std::map<std::string, std::weak_ptr<gfx::Texture>> s_texture_cache; static std::map<std::string, std::weak_ptr<gfx::Texture>> s_texture_cache;

74
src/assets/skeleton.cpp Normal file
View File

@ -0,0 +1,74 @@
#include "skeleton.hpp"
#include <fstream>
#include <sstream>
#include <stdexcept>
void assets::Skeleton::AddBone(const std::string& name, const std::string& parent_name, const Transform& transform)
{
int index = static_cast<int>(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> 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> skeleton = std::make_shared<Skeleton>();
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;
}

39
src/assets/skeleton.hpp Normal file
View File

@ -0,0 +1,39 @@
#pragma once
#include <string>
#include <vector>
#include <map>
#include <memory>
#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<Bone> bones_;
std::map<std::string, int> 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<Skeleton> LoadFromFile(const std::string& filename);
};
}

View File

@ -1,5 +1,7 @@
#include "vertex_array.hpp" #include "vertex_array.hpp"
#include <cstdint>
// Struktura pro informace o jednotlivych vertex atributu // Struktura pro informace o jednotlivych vertex atributu
struct VertexAttribInfo { struct VertexAttribInfo {
GLuint index; GLuint index;
@ -15,7 +17,9 @@ static const VertexAttribInfo s_ATTR_INFO[] = {
{ 1, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 3 }, // VA_NORMAL { 1, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 3 }, // VA_NORMAL
{ 2, 4, GL_UNSIGNED_BYTE, GL_TRUE, 4 }, // VA_COLOR { 2, 4, GL_UNSIGNED_BYTE, GL_TRUE, 4 }, // VA_COLOR
{ 3, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2 }, // VA_UV { 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 // 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)) { if (attrs & (1 << i)) {
const auto& info = s_ATTR_INFO[i]; const auto& info = s_ATTR_INFO[i];
glEnableVertexAttribArray(info.index); 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; offset += info.byte_size;
} }
} }

View File

@ -18,6 +18,8 @@ namespace gfx
VA_COLOR = 1 << 2, VA_COLOR = 1 << 2,
VA_UV = 1 << 3, VA_UV = 1 << 3,
VA_LIGHTMAP_UV = 1 << 4, VA_LIGHTMAP_UV = 1 << 4,
VA_BONE_INDICES = 1 << 5,
VA_BONE_WEIGHTS = 1 << 6,
}; };
// Informace pro vytvoreni VertexArraye // Informace pro vytvoreni VertexArraye

24
src/utils/transform.hpp Normal file
View File

@ -0,0 +1,24 @@
#pragma once
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
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);
}
};