Added skeletal animations and other fixes
This commit is contained in:
70
destrum/include/destrum/Components/Animator.h
Normal file
70
destrum/include/destrum/Components/Animator.h
Normal file
@@ -0,0 +1,70 @@
|
||||
#ifndef ANIMATOR_H
|
||||
#define ANIMATOR_H
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <glm/mat4x4.hpp>
|
||||
|
||||
#include <destrum/ObjectModel/Component.h>
|
||||
#include <destrum/Graphics/Resources/Mesh.h>
|
||||
#include <destrum/Graphics/SkeletalAnimation.h>
|
||||
|
||||
class SkinningPipeline;
|
||||
|
||||
class Animator : public Component {
|
||||
public:
|
||||
explicit Animator(GameObject& parent);
|
||||
|
||||
// Component interface
|
||||
void Update() override;
|
||||
void ImGuiInspector() override;
|
||||
|
||||
// Animation control
|
||||
void addClip(std::shared_ptr<SkeletalAnimation> clip);
|
||||
void play(const std::string& name, float blendTime = 0.f);
|
||||
void stop();
|
||||
|
||||
bool isPlaying() const { return m_current.clip != nullptr; }
|
||||
const std::string& currentClipName() const { return m_currentClipName; }
|
||||
float currentTime() const { return m_current.time; }
|
||||
|
||||
// Called during draw command building
|
||||
std::size_t uploadJointMatrices(SkinningPipeline& pipeline,
|
||||
const Skeleton& skeleton,
|
||||
std::size_t frameIndex);
|
||||
Skeleton* getSkeleton() {
|
||||
return &m_skeleton;
|
||||
}
|
||||
|
||||
void setSkeleton(Skeleton skeleton) {
|
||||
m_skeleton = std::move(skeleton);
|
||||
}
|
||||
|
||||
private:
|
||||
struct PlaybackState {
|
||||
SkeletalAnimation* clip = nullptr;
|
||||
float time = 0.f;
|
||||
float speed = 1.f;
|
||||
};
|
||||
|
||||
Skeleton m_skeleton{};
|
||||
|
||||
PlaybackState m_current;
|
||||
PlaybackState m_previous;
|
||||
float m_blendT = 0.f;
|
||||
float m_blendDuration = 0.f;
|
||||
std::string m_currentClipName;
|
||||
|
||||
std::unordered_map<std::string, std::shared_ptr<SkeletalAnimation>> m_clips;
|
||||
|
||||
std::vector<glm::mat4> computeJointMatrices(const Skeleton& skeleton);
|
||||
|
||||
glm::vec3 sampleTranslation(const SkeletalAnimation::Track& track, float t);
|
||||
glm::quat sampleRotation (const SkeletalAnimation::Track& track, float t);
|
||||
glm::vec3 sampleScale (const SkeletalAnimation::Track& track, float t);
|
||||
};
|
||||
|
||||
#endif // ANIMATOR_H
|
||||
@@ -21,6 +21,8 @@ public:
|
||||
private:
|
||||
MeshID meshID{NULL_MESH_ID};
|
||||
MaterialID materialID{NULL_MATERIAL_ID};
|
||||
|
||||
std::unique_ptr<SkinnedMesh> m_skinnedMesh;
|
||||
};
|
||||
|
||||
#endif //MESHRENDERERCOMPONENT_H
|
||||
|
||||
@@ -96,6 +96,13 @@ public:
|
||||
this->CalculateProjectionMatrix();
|
||||
}
|
||||
|
||||
float GetZNear() const { return m_zNear; }
|
||||
void SetZNear(float zNear) { m_zNear = zNear; }
|
||||
float GetZFar() const { return m_zFar; }
|
||||
void SetZFar(float zFar) { m_zFar = zFar; }
|
||||
float GetFOVY() const { return fovAngle; }
|
||||
float GetAspectRatio() const { return m_aspectRatio; }
|
||||
|
||||
glm::vec3 m_position{2.f, 0, 0};
|
||||
|
||||
void Translate(const glm::vec3& translation) {
|
||||
|
||||
@@ -1,4 +1,48 @@
|
||||
#ifndef COMPUTEPIPELINE_H
|
||||
#define COMPUTEPIPELINE_H
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <destrum/Graphics/GfxDevice.h>
|
||||
|
||||
struct ComputePipelineConfigInfo {
|
||||
std::string name;
|
||||
|
||||
// Optional specialization constants (can be nullptr if unused)
|
||||
VkSpecializationInfo* specializationInfo{nullptr};
|
||||
|
||||
VkPipelineLayout pipelineLayout{VK_NULL_HANDLE};
|
||||
};
|
||||
|
||||
class ComputePipeline {
|
||||
public:
|
||||
ComputePipeline(GfxDevice& device,
|
||||
const std::string& compPath,
|
||||
const ComputePipelineConfigInfo& configInfo);
|
||||
~ComputePipeline();
|
||||
|
||||
ComputePipeline(const ComputePipeline& other) = delete;
|
||||
ComputePipeline(ComputePipeline&& other) noexcept = delete;
|
||||
ComputePipeline& operator=(const ComputePipeline& other) = delete;
|
||||
ComputePipeline& operator=(ComputePipeline&& other) noexcept = delete;
|
||||
|
||||
void bind(VkCommandBuffer buffer) const;
|
||||
|
||||
static void DefaultPipelineConfigInfo(ComputePipelineConfigInfo& configInfo);
|
||||
|
||||
private:
|
||||
static std::vector<char> readFile(const std::string& filename);
|
||||
|
||||
void CreateComputePipeline(const std::string& compPath,
|
||||
const ComputePipelineConfigInfo& configInfo);
|
||||
|
||||
void CreateShaderModule(const std::vector<char>& code, VkShaderModule* shaderModule) const;
|
||||
|
||||
GfxDevice& m_device;
|
||||
|
||||
VkPipeline m_computePipeline{VK_NULL_HANDLE};
|
||||
VkShaderModule m_compShaderModule{VK_NULL_HANDLE};
|
||||
};
|
||||
|
||||
#endif //COMPUTEPIPELINE_H
|
||||
|
||||
@@ -1,4 +1,80 @@
|
||||
#ifndef FRUSTUM_H
|
||||
#define FRUSTUM_H
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include "Camera.h"
|
||||
|
||||
struct Frustum {
|
||||
struct Plane {
|
||||
Plane() = default;
|
||||
Plane(const glm::vec3& p1, const glm::vec3& norm) :
|
||||
normal(glm::normalize(norm)), distance(glm::dot(normal, p1))
|
||||
{}
|
||||
|
||||
glm::vec3 normal{0.f, 1.f, 0.f};
|
||||
|
||||
// distance from the origin to the nearest point in the plane
|
||||
float distance{0.f};
|
||||
|
||||
float getSignedDistanceToPlane(const glm::vec3& point) const
|
||||
{
|
||||
return glm::dot(normal, point) - distance;
|
||||
}
|
||||
};
|
||||
|
||||
const Plane& getPlane(int i) const
|
||||
{
|
||||
switch (i) {
|
||||
case 0:
|
||||
return farFace;
|
||||
case 1:
|
||||
return nearFace;
|
||||
case 2:
|
||||
return leftFace;
|
||||
case 3:
|
||||
return rightFace;
|
||||
case 4:
|
||||
return topFace;
|
||||
case 5:
|
||||
return bottomFace;
|
||||
default:
|
||||
assert(false);
|
||||
return nearFace;
|
||||
}
|
||||
}
|
||||
|
||||
Plane farFace;
|
||||
Plane nearFace;
|
||||
|
||||
Plane leftFace;
|
||||
Plane rightFace;
|
||||
|
||||
Plane topFace;
|
||||
Plane bottomFace;
|
||||
};
|
||||
|
||||
struct Sphere {
|
||||
glm::vec3 center{};
|
||||
float radius{};
|
||||
};
|
||||
|
||||
struct AABB {
|
||||
glm::vec3 min;
|
||||
glm::vec3 max;
|
||||
|
||||
glm::vec3 calculateSize() const { return glm::abs(max - min); }
|
||||
};
|
||||
|
||||
namespace edge
|
||||
{
|
||||
std::array<glm::vec3, 8> calculateFrustumCornersWorldSpace(const Camera& camera);
|
||||
|
||||
Frustum createFrustumFromCamera(const Camera& camera);
|
||||
bool isInFrustum(const Frustum& frustum, const Sphere& s);
|
||||
bool isInFrustum(const Frustum& frustum, const AABB& aabb);
|
||||
Sphere calculateBoundingSphereWorld(const glm::mat4& transform, const Sphere& s, bool hasSkeleton);
|
||||
}
|
||||
|
||||
|
||||
#endif //FRUSTUM_H
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
|
||||
#include "Util.h"
|
||||
|
||||
class MeshCache;
|
||||
|
||||
namespace {
|
||||
using ImmediateExecuteFunction = std::function<void(VkCommandBuffer)>;
|
||||
}
|
||||
|
||||
@@ -4,13 +4,21 @@
|
||||
#include <string>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
struct MaterialData {
|
||||
glm::vec4 baseColor;
|
||||
glm::vec4 metalRoughnessEmissive;
|
||||
std::uint32_t diffuseTex;
|
||||
std::uint32_t normalTex;
|
||||
std::uint32_t metallicRoughnessTex;
|
||||
std::uint32_t emissiveTex;
|
||||
struct alignas(16) MaterialData {
|
||||
alignas(16) glm::vec4 baseColor;
|
||||
alignas(16) glm::vec4 metalRoughnessEmissive;
|
||||
uint32_t textureFilteringMode;
|
||||
uint32_t diffuseTex;
|
||||
uint32_t normalTex;
|
||||
uint32_t metallicRoughnessTex;
|
||||
uint32_t emissiveTex;
|
||||
uint32_t _pad0, _pad1, _pad2; // explicit to 64 bytes
|
||||
};
|
||||
|
||||
enum class TextureFilteringMode : std::uint32_t {
|
||||
Nearest,
|
||||
Linear,
|
||||
Anisotropic,
|
||||
};
|
||||
|
||||
struct Material {
|
||||
@@ -19,6 +27,8 @@ struct Material {
|
||||
float roughnessFactor{0.7f};
|
||||
float emissiveFactor{0.f};
|
||||
|
||||
TextureFilteringMode textureFilteringMode{TextureFilteringMode::Linear};
|
||||
|
||||
ImageID diffuseTexture{NULL_IMAGE_ID};
|
||||
// ImageId normalMapTexture{NULL_IMAGE_ID};
|
||||
// ImageId metallicRoughnessTexture{NULL_IMAGE_ID};
|
||||
|
||||
@@ -18,13 +18,14 @@ public:
|
||||
void cleanup(GfxDevice& gfxDevice);
|
||||
|
||||
MaterialID addMaterial(GfxDevice& gfxDevice, Material material);
|
||||
const Material& getMaterial(MaterialID id) const;
|
||||
MaterialID addSimpleTextureMaterial(GfxDevice& gfxDevice, ImageID textureID);
|
||||
[[nodiscard]] const Material& getMaterial(MaterialID id) const;
|
||||
|
||||
MaterialID getFreeMaterialId() const;
|
||||
MaterialID getPlaceholderMaterialId() const;
|
||||
[[nodiscard]] MaterialID getFreeMaterialId() const;
|
||||
[[nodiscard]] MaterialID getPlaceholderMaterialId() const;
|
||||
|
||||
const GPUBuffer& getMaterialDataBuffer() const { return materialDataBuffer; }
|
||||
VkDeviceAddress getMaterialDataBufferAddress() const { return materialDataBuffer.address; }
|
||||
[[nodiscard]] const GPUBuffer& getMaterialDataBuffer() const { return materialDataBuffer; }
|
||||
[[nodiscard]] VkDeviceAddress getMaterialDataBufferAddress() const { return materialDataBuffer.address; }
|
||||
|
||||
|
||||
Material& getMaterialMutable(MaterialID id);
|
||||
@@ -33,7 +34,7 @@ public:
|
||||
private:
|
||||
std::vector<Material> materials;
|
||||
|
||||
static const auto MAX_MATERIALS = 1000;
|
||||
static constexpr auto MAX_MATERIALS = 1000;
|
||||
GPUBuffer materialDataBuffer;
|
||||
|
||||
// material which is used for meshes without materials
|
||||
|
||||
@@ -13,11 +13,13 @@ public:
|
||||
|
||||
MeshID addMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh);
|
||||
const GPUMesh& getMesh(MeshID id) const;
|
||||
const CPUMesh& getCPUMesh(MeshID id) const;
|
||||
|
||||
private:
|
||||
void uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh& gpuMesh) const;
|
||||
|
||||
std::vector<GPUMesh> meshes;
|
||||
std::vector<CPUMesh> cpuMeshes;
|
||||
};
|
||||
|
||||
#endif //MESHCACHE_H
|
||||
|
||||
@@ -5,13 +5,15 @@
|
||||
|
||||
#include <destrum/Graphics/ids.h>
|
||||
|
||||
#include <destrum/Graphics/Frustum.h>
|
||||
|
||||
|
||||
struct MeshDrawCommand {
|
||||
MeshID meshId;
|
||||
glm::mat4 transformMatrix;
|
||||
|
||||
// for frustum culling
|
||||
// math::Sphere worldBoundingSphere;
|
||||
Sphere worldBoundingSphere;
|
||||
|
||||
// If set - mesh will be drawn with overrideMaterialId
|
||||
// instead of whatever material the mesh has
|
||||
@@ -20,8 +22,8 @@ struct MeshDrawCommand {
|
||||
bool castShadow{true};
|
||||
|
||||
// skinned meshes only
|
||||
// const SkinnedMesh* skinnedMesh{nullptr};
|
||||
// std::uint32_t jointMatricesStartIndex;
|
||||
const SkinnedMesh* skinnedMesh{nullptr};
|
||||
std::uint32_t jointMatricesStartIndex;
|
||||
};
|
||||
|
||||
#endif //MESHDRAWCOMMAND_H
|
||||
|
||||
@@ -1,4 +1,58 @@
|
||||
#ifndef SKINNINGPIPELINE_H
|
||||
#define SKINNINGPIPELINE_H
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
#include <array>
|
||||
#include <span>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include <destrum/Graphics/ComputePipeline.h>
|
||||
#include <destrum/Graphics/Resources/AppendableBuffer.h>
|
||||
|
||||
#include "destrum/Graphics/Resources/NBuffer.h"
|
||||
|
||||
struct MeshDrawCommand;
|
||||
class MeshCache;
|
||||
class GfxDevice;
|
||||
|
||||
class SkinningPipeline final {
|
||||
public:
|
||||
void init(GfxDevice& gfxDevice);
|
||||
void cleanup(GfxDevice& gfxDevice);
|
||||
|
||||
void doSkinning(
|
||||
VkCommandBuffer cmd,
|
||||
std::size_t frameIndex,
|
||||
const MeshCache& meshCache,
|
||||
const MeshDrawCommand& dc);
|
||||
|
||||
void beginDrawing(std::size_t frameIndex);
|
||||
std::size_t appendJointMatrices(
|
||||
std::span<const glm::mat4> jointMatrices,
|
||||
std::size_t frameIndex);
|
||||
|
||||
private:
|
||||
VkPipelineLayout m_pipelineLayout;
|
||||
std::unique_ptr<ComputePipeline> skinningPipeline;
|
||||
struct PushConstants {
|
||||
VkDeviceAddress jointMatricesBuffer;
|
||||
std::uint32_t jointMatricesStartIndex;
|
||||
std::uint32_t numVertices;
|
||||
VkDeviceAddress inputBuffer;
|
||||
VkDeviceAddress skinningData;
|
||||
VkDeviceAddress outputBuffer;
|
||||
};
|
||||
static constexpr std::size_t MAX_JOINT_MATRICES = 5000;
|
||||
|
||||
struct PerFrameData {
|
||||
AppendableBuffer<glm::mat4> jointMatricesBuffer;
|
||||
};
|
||||
|
||||
std::array<PerFrameData, FRAMES_IN_FLIGHT> framesData;
|
||||
// NBuffer framesData;
|
||||
|
||||
PerFrameData& getCurrentFrameData(std::size_t frameIndex);
|
||||
};
|
||||
|
||||
|
||||
#endif //SKINNINGPIPELINE_H
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <destrum/Graphics/Pipelines/SkinningPipeline.h>
|
||||
#include "Pipelines/SkyboxPipeline.h"
|
||||
|
||||
class GameRenderer {
|
||||
@@ -35,6 +36,11 @@ public:
|
||||
void cleanup(VkDevice device);
|
||||
|
||||
void drawMesh(MeshID id, const glm::mat4& transform, MaterialID materialId);
|
||||
void drawSkinnedMesh(MeshID id,
|
||||
const glm::mat4& transform,
|
||||
MaterialID materialId,
|
||||
SkinnedMesh* skinnedMesh,
|
||||
std::size_t jointMatricesStartIndex);
|
||||
const GPUImage& getDrawImage(const GfxDevice& gfx_device) const;
|
||||
|
||||
void resize(GfxDevice& gfxDevice, const glm::ivec2& newSize) {
|
||||
@@ -47,6 +53,13 @@ public:
|
||||
void setSkyboxTexture(ImageID skyboxImageId);
|
||||
|
||||
void flushMaterialUpdates(GfxDevice& gfxDevice);
|
||||
[[nodiscard]] MeshCache& GetMeshCache() const {
|
||||
return meshCache;
|
||||
}
|
||||
|
||||
[[nodiscard]] SkinningPipeline& getSkinningPipeline() const {
|
||||
return *skinningPipeline;
|
||||
}
|
||||
|
||||
private:
|
||||
void createDrawImage(GfxDevice& gfxDevice, const glm::ivec2& drawImageSize, bool firstCreate);
|
||||
@@ -91,6 +104,8 @@ private:
|
||||
|
||||
std::unique_ptr<MeshPipeline> meshPipeline;
|
||||
std::unique_ptr<SkyboxPipeline> skyboxPipeline;
|
||||
|
||||
std::unique_ptr<SkinningPipeline> skinningPipeline;
|
||||
};
|
||||
|
||||
#endif //RENDERER_H
|
||||
|
||||
30
destrum/include/destrum/Graphics/Resources/AnimationClip.h
Normal file
30
destrum/include/destrum/Graphics/Resources/AnimationClip.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#ifndef ANIMATIONCLIP_H
|
||||
#define ANIMATIONCLIP_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
struct JointKeyframes {
|
||||
std::uint32_t jointIndex; // matches your skeleton joint order
|
||||
|
||||
std::vector<float> positionTimes;
|
||||
std::vector<glm::vec3> positions;
|
||||
|
||||
std::vector<float> rotationTimes;
|
||||
std::vector<glm::quat> rotations;
|
||||
|
||||
std::vector<float> scaleTimes;
|
||||
std::vector<glm::vec3> scales;
|
||||
};
|
||||
|
||||
struct AnimationClip {
|
||||
std::string name;
|
||||
float duration; // seconds
|
||||
float ticksPerSecond;
|
||||
std::vector<JointKeyframes> channels; // one per animated joint
|
||||
};
|
||||
|
||||
#endif //ANIMATIONCLIP_H
|
||||
@@ -1,4 +1,40 @@
|
||||
#ifndef APPENDABLEBUFFER_H
|
||||
#define APPENDABLEBUFFER_H
|
||||
|
||||
#include <cassert>
|
||||
#include <cstring> // memcpy
|
||||
#include <span>
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
#include <destrum/Graphics/Resources/Buffer.h>
|
||||
|
||||
template<typename T>
|
||||
struct AppendableBuffer {
|
||||
void append(std::span<const T> elements)
|
||||
{
|
||||
assert(size + elements.size() <= capacity);
|
||||
auto arr = (T*)buffer.info.pMappedData;
|
||||
std::memcpy((void*)&arr[size], elements.data(), elements.size() * sizeof(T));
|
||||
size += elements.size();
|
||||
}
|
||||
|
||||
void append(const T& element)
|
||||
{
|
||||
assert(size + 1 <= capacity);
|
||||
auto arr = (T*)buffer.info.pMappedData;
|
||||
std::memcpy((void*)&arr[size], &element, sizeof(T));
|
||||
++size;
|
||||
}
|
||||
|
||||
void clear() { size = 0; }
|
||||
|
||||
VkBuffer getVkBuffer() const { return buffer.buffer; }
|
||||
|
||||
GPUBuffer buffer;
|
||||
std::size_t capacity{};
|
||||
std::size_t size{0};
|
||||
};
|
||||
|
||||
|
||||
#endif //APPENDABLEBUFFER_H
|
||||
|
||||
@@ -2,33 +2,48 @@
|
||||
#define MESH_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <glm/vec2.hpp>
|
||||
#include <glm/vec3.hpp>
|
||||
#include <glm/vec4.hpp>
|
||||
#include <glm/mat4x4.hpp>
|
||||
|
||||
#include <destrum/Graphics/Resources/Buffer.h>
|
||||
#include <destrum/Graphics/Frustum.h>
|
||||
#include <destrum/Graphics/Skeleton.h>
|
||||
|
||||
// ─── CPU Mesh ─────────────────────────────────────────────────────────────────
|
||||
struct CPUMesh {
|
||||
std::vector<std::uint32_t> indices;
|
||||
|
||||
struct Vertex {
|
||||
glm::vec3 position;
|
||||
float uv_x{};
|
||||
float uv_x{};
|
||||
glm::vec3 normal;
|
||||
float uv_y{};
|
||||
float uv_y{};
|
||||
glm::vec4 tangent;
|
||||
};
|
||||
std::vector<Vertex> vertices;
|
||||
|
||||
struct SkinningData {
|
||||
glm::vec<4, std::uint32_t> jointIds;
|
||||
glm::vec4 weights;
|
||||
};
|
||||
|
||||
std::vector<std::uint32_t> indices;
|
||||
std::vector<Vertex> vertices;
|
||||
std::vector<SkinningData> skinningData; // empty if no skeleton
|
||||
|
||||
std::string name;
|
||||
|
||||
glm::vec3 minPos;
|
||||
glm::vec3 maxPos;
|
||||
|
||||
std::optional<Skeleton> skeleton; // present only for skinned meshes
|
||||
};
|
||||
|
||||
// ─── GPU Mesh ─────────────────────────────────────────────────────────────────
|
||||
|
||||
struct GPUMesh {
|
||||
GPUBuffer vertexBuffer;
|
||||
@@ -40,78 +55,64 @@ struct GPUMesh {
|
||||
// AABB
|
||||
glm::vec3 minPos;
|
||||
glm::vec3 maxPos;
|
||||
// math::Sphere boundingSphere;
|
||||
Sphere boundingSphere;
|
||||
|
||||
bool hasSkeleton{false};
|
||||
GPUBuffer skinningDataBuffer; // valid only when hasSkeleton == true
|
||||
};
|
||||
|
||||
static std::vector<CPUMesh::Vertex> vertices = {
|
||||
// =======================
|
||||
// ─── Skinned output buffer (one per entity, not per mesh asset) ───────────────
|
||||
|
||||
struct SkinnedMesh {
|
||||
GPUBuffer skinnedVertexBuffer;
|
||||
};
|
||||
|
||||
// ─── Cube geometry (dev / test asset) ─────────────────────────────────────────
|
||||
|
||||
namespace CubeMesh {
|
||||
|
||||
inline std::vector<CPUMesh::Vertex> vertices = {
|
||||
// +Z (Front)
|
||||
// =======================
|
||||
{{-0.5f, -0.5f, 0.5f}, 0.0f, {0, 0, 1}, 0.0f, {1, 0, 0, 1}},
|
||||
{{ 0.5f, -0.5f, 0.5f}, 1.0f, {0, 0, 1}, 0.0f, {1, 0, 0, 1}},
|
||||
{{ 0.5f, 0.5f, 0.5f}, 1.0f, {0, 0, 1}, 1.0f, {1, 0, 0, 1}},
|
||||
{{-0.5f, 0.5f, 0.5f}, 0.0f, {0, 0, 1}, 1.0f, {1, 0, 0, 1}},
|
||||
|
||||
// =======================
|
||||
// -Z (Back)
|
||||
// =======================
|
||||
{{ 0.5f, -0.5f, -0.5f}, 0.0f, {0, 0, -1}, 0.0f, {-1, 0, 0, 1}},
|
||||
{{-0.5f, -0.5f, -0.5f}, 1.0f, {0, 0, -1}, 0.0f, {-1, 0, 0, 1}},
|
||||
{{-0.5f, 0.5f, -0.5f}, 1.0f, {0, 0, -1}, 1.0f, {-1, 0, 0, 1}},
|
||||
{{ 0.5f, 0.5f, -0.5f}, 0.0f, {0, 0, -1}, 1.0f, {-1, 0, 0, 1}},
|
||||
|
||||
// =======================
|
||||
// +X (Right)
|
||||
// =======================
|
||||
{{ 0.5f, -0.5f, 0.5f}, 0.0f, {1, 0, 0}, 0.0f, {0, 0, -1, 1}},
|
||||
{{ 0.5f, -0.5f, -0.5f}, 1.0f, {1, 0, 0}, 0.0f, {0, 0, -1, 1}},
|
||||
{{ 0.5f, 0.5f, -0.5f}, 1.0f, {1, 0, 0}, 1.0f, {0, 0, -1, 1}},
|
||||
{{ 0.5f, 0.5f, 0.5f}, 0.0f, {1, 0, 0}, 1.0f, {0, 0, -1, 1}},
|
||||
|
||||
// =======================
|
||||
// -X (Left)
|
||||
// =======================
|
||||
{{-0.5f, -0.5f, -0.5f}, 0.0f, {-1, 0, 0}, 0.0f, {0, 0, 1, 1}},
|
||||
{{-0.5f, -0.5f, 0.5f}, 1.0f, {-1, 0, 0}, 0.0f, {0, 0, 1, 1}},
|
||||
{{-0.5f, 0.5f, 0.5f}, 1.0f, {-1, 0, 0}, 1.0f, {0, 0, 1, 1}},
|
||||
{{-0.5f, 0.5f, -0.5f}, 0.0f, {-1, 0, 0}, 1.0f, {0, 0, 1, 1}},
|
||||
|
||||
// =======================
|
||||
// +Y (Top)
|
||||
// =======================
|
||||
{{-0.5f, 0.5f, 0.5f}, 0.0f, {0, 1, 0}, 0.0f, {1, 0, 0, 1}},
|
||||
{{ 0.5f, 0.5f, 0.5f}, 1.0f, {0, 1, 0}, 0.0f, {1, 0, 0, 1}},
|
||||
{{ 0.5f, 0.5f, -0.5f}, 1.0f, {0, 1, 0}, 1.0f, {1, 0, 0, 1}},
|
||||
{{-0.5f, 0.5f, -0.5f}, 0.0f, {0, 1, 0}, 1.0f, {1, 0, 0, 1}},
|
||||
|
||||
// =======================
|
||||
// -Y (Bottom)
|
||||
// =======================
|
||||
{{-0.5f, -0.5f, -0.5f}, 0.0f, {0, -1, 0}, 0.0f, {1, 0, 0, 1}},
|
||||
{{ 0.5f, -0.5f, -0.5f}, 1.0f, {0, -1, 0}, 0.0f, {1, 0, 0, 1}},
|
||||
{{ 0.5f, -0.5f, 0.5f}, 1.0f, {0, -1, 0}, 1.0f, {1, 0, 0, 1}},
|
||||
{{-0.5f, -0.5f, 0.5f}, 0.0f, {0, -1, 0}, 1.0f, {1, 0, 0, 1}},
|
||||
};
|
||||
|
||||
static std::vector<uint32_t> indices = {
|
||||
// Front (+Z)
|
||||
0, 2, 1, 0, 3, 2,
|
||||
|
||||
// Back (-Z)
|
||||
4, 6, 5, 4, 7, 6,
|
||||
|
||||
// Right (+X)
|
||||
8,10, 9, 8,11,10,
|
||||
|
||||
// Left (-X)
|
||||
12,14,13, 12,15,14,
|
||||
|
||||
// Top (+Y)
|
||||
16,18,17, 16,19,18,
|
||||
|
||||
// Bottom (-Y)
|
||||
20,22,21, 20,23,22
|
||||
inline std::vector<std::uint32_t> indices = {
|
||||
0, 2, 1, 0, 3, 2, // Front
|
||||
4, 6, 5, 4, 7, 6, // Back
|
||||
8,10, 9, 8,11,10, // Right
|
||||
12,14,13, 12,15,14, // Left
|
||||
16,18,17, 16,19,18, // Top
|
||||
20,22,21, 20,23,22, // Bottom
|
||||
};
|
||||
|
||||
} // namespace CubeMesh
|
||||
|
||||
#endif //MESH_H
|
||||
#endif // MESH_H
|
||||
35
destrum/include/destrum/Graphics/SkeletalAnimation.h
Normal file
35
destrum/include/destrum/Graphics/SkeletalAnimation.h
Normal file
@@ -0,0 +1,35 @@
|
||||
#ifndef SKELETALANIMATION_H
|
||||
#define SKELETALANIMATION_H
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
#include <glm/vec3.hpp>
|
||||
|
||||
struct SkeletalAnimation {
|
||||
struct Keyframe {
|
||||
float time;
|
||||
glm::vec3 translation;
|
||||
glm::quat rotation;
|
||||
glm::vec3 scale;
|
||||
};
|
||||
|
||||
struct Track {
|
||||
std::uint32_t jointIndex;
|
||||
std::vector<Keyframe> keyframes; // sorted by time
|
||||
};
|
||||
|
||||
std::vector<Track> tracks; // one per animated joint (not all joints need a track)
|
||||
float duration{0.f};
|
||||
bool looped{true};
|
||||
std::string name;
|
||||
|
||||
int startFrame{0};
|
||||
std::map<int, std::vector<std::string>> events;
|
||||
|
||||
const std::vector<std::string>& getEventsForFrame(int frame) const;
|
||||
};
|
||||
|
||||
#endif // SKELETALANIMATION_H
|
||||
55
destrum/include/destrum/Graphics/Skeleton.h
Normal file
55
destrum/include/destrum/Graphics/Skeleton.h
Normal file
@@ -0,0 +1,55 @@
|
||||
#ifndef SKELETON_H
|
||||
#define SKELETON_H
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <glm/mat4x4.hpp>
|
||||
#include <glm/vec3.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
|
||||
#include <destrum/Graphics/ids.h>
|
||||
|
||||
struct Joint {
|
||||
JointId id{NULL_JOINT_ID};
|
||||
glm::vec3 localTranslation{0.f, 0.f, 0.f};
|
||||
glm::quat localRotation{glm::identity<glm::quat>()};
|
||||
glm::vec3 localScale{1.f, 1.f, 1.f};
|
||||
};
|
||||
|
||||
struct Skeleton {
|
||||
struct JointNode {
|
||||
JointId id{NULL_JOINT_ID};
|
||||
std::vector<JointId> children;
|
||||
};
|
||||
|
||||
std::vector<JointNode> hierarchy;
|
||||
std::vector<glm::mat4> inverseBindMatrices;
|
||||
std::vector<Joint> joints;
|
||||
std::vector<std::string> jointNames;
|
||||
std::vector<int> parentIndex; // -1 = root, built once after loading
|
||||
};
|
||||
|
||||
inline void buildParentIndex(Skeleton& skeleton) {
|
||||
const std::size_t numJoints = skeleton.joints.size();
|
||||
skeleton.parentIndex.assign(numJoints, -1);
|
||||
|
||||
// Build id -> slot map so we don't assume hierarchy[i] == joints[i]
|
||||
std::unordered_map<JointId, std::size_t> idToSlot;
|
||||
idToSlot.reserve(numJoints);
|
||||
for (std::size_t j = 0; j < numJoints; ++j)
|
||||
idToSlot[skeleton.joints[j].id] = j;
|
||||
|
||||
for (const auto& node : skeleton.hierarchy) {
|
||||
auto parentIt = idToSlot.find(node.id);
|
||||
if (parentIt == idToSlot.end()) continue;
|
||||
|
||||
for (const JointId childId : node.children) {
|
||||
auto childIt = idToSlot.find(childId);
|
||||
if (childIt != idToSlot.end())
|
||||
skeleton.parentIndex[childIt->second] = static_cast<int>(parentIt->second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // SKELETON_H
|
||||
@@ -16,4 +16,8 @@ constexpr MaterialID NULL_MATERIAL_ID = std::numeric_limits<std::uint32_t>::max(
|
||||
using BindlessID = std::uint32_t;
|
||||
constexpr BindlessID NULL_BINDLESS_ID = std::numeric_limits<std::uint32_t>::max();
|
||||
|
||||
using JointId = std::uint16_t;
|
||||
static const JointId NULL_JOINT_ID = std::numeric_limits<JointId>::max();
|
||||
static const JointId ROOT_JOINT_ID = 0;
|
||||
|
||||
#endif //IDS_H
|
||||
|
||||
@@ -1,4 +1,86 @@
|
||||
#ifndef MATHUTILS_H
|
||||
#define MATHUTILS_H
|
||||
|
||||
#include <destrum/Graphics/Frustum.h>
|
||||
#include <span>
|
||||
#include <array>
|
||||
#include <glm/glm.hpp>
|
||||
#include <cassert>
|
||||
|
||||
#include "glm/gtx/norm.hpp"
|
||||
|
||||
inline Sphere calculateBoundingSphere(std::span<glm::vec3> positions)
|
||||
{
|
||||
assert(!positions.empty());
|
||||
auto calculateInitialSphere = [](const std::span<glm::vec3>& positions) -> Sphere {
|
||||
constexpr int dirCount = 13;
|
||||
static const std::array<glm::vec3, dirCount> direction = {{
|
||||
{1.f, 0.f, 0.f},
|
||||
{0.f, 1.f, 0.f},
|
||||
{0.f, 0.f, 1.f},
|
||||
{1.f, 1.f, 0.f},
|
||||
{1.f, 0.f, 1.f},
|
||||
{0.f, 1.f, 1.f},
|
||||
{1.f, -1.f, 0.f},
|
||||
{1.f, 0.f, -1.f},
|
||||
{0.f, 1.f, -1.f},
|
||||
{1.f, 1.f, 1.f},
|
||||
{1.f, -1.f, 1.f},
|
||||
{1.f, 1.f, -1.f},
|
||||
{1.f, -1.f, -1.f},
|
||||
}};
|
||||
|
||||
std::array<float, dirCount> dmin{};
|
||||
std::array<float, dirCount> dmax{};
|
||||
std::array<std::size_t, dirCount> imin{};
|
||||
std::array<std::size_t, dirCount> imax{};
|
||||
|
||||
// Find min and max dot products for each direction and record vertex indices.
|
||||
for (int j = 0; j < dirCount; ++j) {
|
||||
const auto& u = direction[j];
|
||||
dmin[j] = glm::dot(u, positions[0]);
|
||||
dmax[j] = dmin[j];
|
||||
for (std::size_t i = 1; i < positions.size(); ++i) {
|
||||
const auto d = glm::dot(u, positions[i]);
|
||||
if (d < dmin[j]) {
|
||||
dmin[j] = d;
|
||||
imin[j] = i;
|
||||
} else if (d > dmax[j]) {
|
||||
dmax[j] = d;
|
||||
imax[j] = i;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Find direction for which vertices at min and max extents are furthest apart.
|
||||
float d2 = glm::length2(positions[imax[0]] - positions[imin[0]]);
|
||||
int k = 0;
|
||||
for (int j = 1; j < dirCount; j++) {
|
||||
const auto m2 = glm::length2(positions[imax[j]] - positions[imin[j]]);
|
||||
if (m2 > d2) {
|
||||
d2 = m2;
|
||||
k = j;
|
||||
}
|
||||
}
|
||||
|
||||
const auto center = (positions[imin[k]] + positions[imax[k]]) * 0.5f;
|
||||
float radius = sqrt(d2) * 0.5f;
|
||||
return {center, radius};
|
||||
};
|
||||
|
||||
// Determine initial center and radius.
|
||||
auto s = calculateInitialSphere(positions);
|
||||
// Make pass through vertices and adjust sphere as necessary.
|
||||
for (std::size_t i = 0; i < positions.size(); i++) {
|
||||
const auto pv = positions[i] - s.center;
|
||||
float m2 = glm::length2(pv);
|
||||
if (m2 > s.radius * s.radius) {
|
||||
auto q = s.center - (pv * (s.radius / std::sqrt(m2)));
|
||||
s.center = (q + positions[i]) * 0.5f;
|
||||
s.radius = glm::length(q - s.center);
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
#endif //MATHUTILS_H
|
||||
|
||||
@@ -1,498 +1,521 @@
|
||||
#ifndef MODELLOADER_H
|
||||
#define MODELLOADER_H
|
||||
|
||||
// CPUMesh loader with tinygltf
|
||||
// - Loads first scene (or default scene), iterates nodes, extracts mesh primitives.
|
||||
// - Handles POSITION/NORMAL/TANGENT/TEXCOORD_0 and indices.
|
||||
// - Computes minPos/maxPos.
|
||||
// - Can return per-primitive meshes, or merged-per-gltf-mesh meshes.
|
||||
// - IMPORTANT FIX: Applies node transforms (TRS / matrix) so models don't appear flipped/rotated.
|
||||
// CPUMesh loader using Assimp
|
||||
// - Loads first scene, iterates nodes recursively, extracts mesh primitives.
|
||||
// - Handles POSITION / NORMAL / TANGENT / TEXCOORD_0 and indices.
|
||||
// - Handles bones/skinning data and skeletal animations.
|
||||
// - Computes minPos / maxPos (in world space).
|
||||
// - Can return per-primitive meshes (PerPrimitive) or one merged mesh per
|
||||
// Assimp mesh-node (MergedPerMesh).
|
||||
// - LoadSkinnedModel loads meshes + skeleton + animations in one call.
|
||||
// - For skinned meshes, vertex positions are kept in local/bind-pose space
|
||||
// (identity world transform) so the skinning shader can apply joint matrices
|
||||
// correctly at runtime.
|
||||
//
|
||||
// NOTE: Defining TINYGLTF_IMPLEMENTATION in a header can cause ODR / multiple-definition issues
|
||||
// if included in more than one translation unit. Best practice: put the TINYGLTF_IMPLEMENTATION
|
||||
// define in exactly one .cpp. I keep it here because your original file did, but you may want
|
||||
// to move it.
|
||||
// Link against: assimp (e.g. -lassimp)
|
||||
// Supports any format Assimp understands (.gltf, .glb, .fbx, .obj, …).
|
||||
|
||||
#define TINYGLTF_IMPLEMENTATION
|
||||
#define TINYGLTF_NO_STB_IMAGE_WRITE
|
||||
#include <tiny_gltf.h>
|
||||
#include <assimp/Importer.hpp>
|
||||
#include <assimp/scene.h>
|
||||
#include <assimp/postprocess.h>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/matrix_transform.hpp> // translate/scale
|
||||
#include <glm/gtc/quaternion.hpp> // quat
|
||||
#include <glm/gtx/quaternion.hpp> // mat4_cast
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
|
||||
#include <destrum/Graphics/Resources/Mesh.h>
|
||||
#include <destrum/Graphics/Skeleton.h>
|
||||
#include <destrum/Graphics/SkeletalAnimation.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <stdexcept>
|
||||
#include <iostream>
|
||||
#include <algorithm>
|
||||
|
||||
namespace ModelLoader {
|
||||
|
||||
// -------------------- helpers --------------------
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
static size_t ComponentSizeInBytes(int componentType) {
|
||||
switch (componentType) {
|
||||
case TINYGLTF_COMPONENT_TYPE_BYTE: return 1;
|
||||
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE: return 1;
|
||||
case TINYGLTF_COMPONENT_TYPE_SHORT: return 2;
|
||||
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT: return 2;
|
||||
case TINYGLTF_COMPONENT_TYPE_INT: return 4;
|
||||
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT: return 4;
|
||||
case TINYGLTF_COMPONENT_TYPE_FLOAT: return 4;
|
||||
case TINYGLTF_COMPONENT_TYPE_DOUBLE: return 8;
|
||||
default: throw std::runtime_error("Unknown glTF component type");
|
||||
}
|
||||
}
|
||||
// Convert an Assimp row-major 4×4 matrix to GLM column-major.
|
||||
static glm::mat4 ToGLM(const aiMatrix4x4& m) {
|
||||
return glm::transpose(glm::make_mat4(&m.a1));
|
||||
}
|
||||
|
||||
static int TypeNumComponents(int type) {
|
||||
switch (type) {
|
||||
case TINYGLTF_TYPE_SCALAR: return 1;
|
||||
case TINYGLTF_TYPE_VEC2: return 2;
|
||||
case TINYGLTF_TYPE_VEC3: return 3;
|
||||
case TINYGLTF_TYPE_VEC4: return 4;
|
||||
case TINYGLTF_TYPE_MAT2: return 4;
|
||||
case TINYGLTF_TYPE_MAT3: return 9;
|
||||
case TINYGLTF_TYPE_MAT4: return 16;
|
||||
default: throw std::runtime_error("Unknown glTF type");
|
||||
}
|
||||
}
|
||||
static void UpdateBounds(glm::vec3& mn, glm::vec3& mx, const glm::vec3& p) {
|
||||
mn.x = std::min(mn.x, p.x);
|
||||
mn.y = std::min(mn.y, p.y);
|
||||
mn.z = std::min(mn.z, p.z);
|
||||
mx.x = std::max(mx.x, p.x);
|
||||
mx.y = std::max(mx.y, p.y);
|
||||
mx.z = std::max(mx.z, p.z);
|
||||
}
|
||||
|
||||
// Returns pointer to the first element, plus stride in bytes.
|
||||
static std::pair<const std::uint8_t*, size_t>
|
||||
GetAccessorDataPtrAndStride(const tinygltf::Model& model, const tinygltf::Accessor& accessor) {
|
||||
if (accessor.bufferView < 0) {
|
||||
throw std::runtime_error("Accessor has no bufferView");
|
||||
}
|
||||
const tinygltf::BufferView& bv = model.bufferViews.at(accessor.bufferView);
|
||||
const tinygltf::Buffer& buf = model.buffers.at(bv.buffer);
|
||||
// ─── Post-process flags ───────────────────────────────────────────────────────
|
||||
|
||||
const size_t componentSize = ComponentSizeInBytes(accessor.componentType);
|
||||
const int numComps = TypeNumComponents(accessor.type);
|
||||
const size_t packedStride = componentSize * size_t(numComps);
|
||||
static constexpr unsigned int kImportFlags =
|
||||
aiProcess_Triangulate |
|
||||
aiProcess_CalcTangentSpace |
|
||||
aiProcess_JoinIdenticalVertices |
|
||||
aiProcess_GenSmoothNormals |
|
||||
aiProcess_FlipUVs |
|
||||
aiProcess_LimitBoneWeights; // guarantees <= 4 weights per vertex
|
||||
|
||||
size_t stride = bv.byteStride ? size_t(bv.byteStride) : packedStride;
|
||||
static const aiScene* LoadScene(Assimp::Importer& importer, const std::string& path) {
|
||||
const aiScene* scene = importer.ReadFile(path, kImportFlags);
|
||||
if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode)
|
||||
throw std::runtime_error(std::string("Assimp failed to load: ") + importer.GetErrorString());
|
||||
return scene;
|
||||
}
|
||||
|
||||
const size_t start = size_t(bv.byteOffset) + size_t(accessor.byteOffset);
|
||||
// Conservative bounds check; don't hard-fail on weird stride but catches obvious issues.
|
||||
if (start > buf.data.size()) {
|
||||
throw std::runtime_error("Accessor start is out of buffer bounds");
|
||||
}
|
||||
// ─── Primitive extraction ─────────────────────────────────────────────────────
|
||||
|
||||
const std::uint8_t* ptr = buf.data.data() + start;
|
||||
return {ptr, stride};
|
||||
}
|
||||
// Build one CPUMesh from a single aiMesh, applying the given world transform.
|
||||
// For skinned meshes, pass glm::mat4{1.f} so vertices stay in bind-pose space.
|
||||
// Skinning data is NOT populated here — call LoadSkinningData afterwards.
|
||||
static CPUMesh LoadAiMeshIntoCPUMesh(const aiMesh* mesh,
|
||||
const std::string& name,
|
||||
const glm::mat4& world) {
|
||||
CPUMesh out{};
|
||||
out.name = name;
|
||||
|
||||
template <typename T>
|
||||
static T ReadAs(const std::uint8_t* p) {
|
||||
T v{};
|
||||
std::memcpy(&v, p, sizeof(T));
|
||||
return v;
|
||||
}
|
||||
const size_t vertexCount = mesh->mNumVertices;
|
||||
out.vertices.resize(vertexCount);
|
||||
|
||||
static glm::vec3 ReadVec3Float(const std::uint8_t* base, size_t stride, size_t i) {
|
||||
const std::uint8_t* p = base + i * stride;
|
||||
const float x = ReadAs<float>(p + 0);
|
||||
const float y = ReadAs<float>(p + 4);
|
||||
const float z = ReadAs<float>(p + 8);
|
||||
return glm::vec3{x, y, z};
|
||||
}
|
||||
const glm::mat3 nrmMat = glm::transpose(glm::inverse(glm::mat3(world)));
|
||||
const glm::mat3 tanMat = glm::mat3(world);
|
||||
|
||||
static glm::vec2 ReadVec2Float(const std::uint8_t* base, size_t stride, size_t i) {
|
||||
const std::uint8_t* p = base + i * stride;
|
||||
const float x = ReadAs<float>(p + 0);
|
||||
const float y = ReadAs<float>(p + 4);
|
||||
return glm::vec2{x, y};
|
||||
}
|
||||
glm::vec3 mn{ std::numeric_limits<float>::infinity()};
|
||||
glm::vec3 mx{-std::numeric_limits<float>::infinity()};
|
||||
|
||||
static glm::vec4 ReadVec4Float(const std::uint8_t* base, size_t stride, size_t i) {
|
||||
const std::uint8_t* p = base + i * stride;
|
||||
const float x = ReadAs<float>(p + 0);
|
||||
const float y = ReadAs<float>(p + 4);
|
||||
const float z = ReadAs<float>(p + 8);
|
||||
const float w = ReadAs<float>(p + 12);
|
||||
return glm::vec4{x, y, z, w};
|
||||
}
|
||||
for (size_t i = 0; i < vertexCount; ++i) {
|
||||
CPUMesh::Vertex v{};
|
||||
|
||||
static std::uint32_t ReadIndexAsU32(const tinygltf::Model& model, const tinygltf::Accessor& accessor, size_t i) {
|
||||
auto [base, stride] = GetAccessorDataPtrAndStride(model, accessor);
|
||||
const std::uint8_t* p = base + i * stride;
|
||||
// Position (required)
|
||||
const aiVector3D& ap = mesh->mVertices[i];
|
||||
v.position = glm::vec3(world * glm::vec4(ap.x, ap.y, ap.z, 1.0f));
|
||||
UpdateBounds(mn, mx, v.position);
|
||||
|
||||
switch (accessor.componentType) {
|
||||
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE:
|
||||
return static_cast<std::uint32_t>(ReadAs<std::uint8_t>(p));
|
||||
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT:
|
||||
return static_cast<std::uint32_t>(ReadAs<std::uint16_t>(p));
|
||||
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT:
|
||||
return static_cast<std::uint32_t>(ReadAs<std::uint32_t>(p));
|
||||
default:
|
||||
throw std::runtime_error("Unsupported index componentType (expected u8/u16/u32)");
|
||||
}
|
||||
}
|
||||
|
||||
static void UpdateBounds(glm::vec3& mn, glm::vec3& mx, const glm::vec3& p) {
|
||||
mn.x = std::min(mn.x, p.x);
|
||||
mn.y = std::min(mn.y, p.y);
|
||||
mn.z = std::min(mn.z, p.z);
|
||||
mx.x = std::max(mx.x, p.x);
|
||||
mx.y = std::max(mx.y, p.y);
|
||||
mx.z = std::max(mx.z, p.z);
|
||||
}
|
||||
|
||||
// Build a node local matrix from either node.matrix or TRS.
|
||||
// glTF stores rotation as quaternion [x,y,z,w].
|
||||
static glm::mat4 NodeLocalMatrix(const tinygltf::Node& node) {
|
||||
if (node.matrix.size() == 16) {
|
||||
glm::mat4 m(1.0f);
|
||||
// glTF is column-major; GLM is column-major -> fill columns.
|
||||
for (int c = 0; c < 4; ++c)
|
||||
for (int r = 0; r < 4; ++r)
|
||||
m[c][r] = static_cast<float>(node.matrix[c * 4 + r]);
|
||||
return m;
|
||||
}
|
||||
|
||||
glm::vec3 t(0.0f);
|
||||
if (node.translation.size() == 3) {
|
||||
t = glm::vec3(
|
||||
static_cast<float>(node.translation[0]),
|
||||
static_cast<float>(node.translation[1]),
|
||||
static_cast<float>(node.translation[2])
|
||||
);
|
||||
}
|
||||
|
||||
glm::quat q(1.0f, 0.0f, 0.0f, 0.0f); // w,x,y,z
|
||||
if (node.rotation.size() == 4) {
|
||||
q = glm::quat(
|
||||
static_cast<float>(node.rotation[3]), // w
|
||||
static_cast<float>(node.rotation[0]), // x
|
||||
static_cast<float>(node.rotation[1]), // y
|
||||
static_cast<float>(node.rotation[2]) // z
|
||||
);
|
||||
}
|
||||
|
||||
glm::vec3 s(1.0f);
|
||||
if (node.scale.size() == 3) {
|
||||
s = glm::vec3(
|
||||
static_cast<float>(node.scale[0]),
|
||||
static_cast<float>(node.scale[1]),
|
||||
static_cast<float>(node.scale[2])
|
||||
);
|
||||
}
|
||||
|
||||
const glm::mat4 T = glm::translate(glm::mat4(1.0f), t);
|
||||
const glm::mat4 R = glm::toMat4(q);
|
||||
const glm::mat4 S = glm::scale(glm::mat4(1.0f), s);
|
||||
return T * R * S;
|
||||
}
|
||||
|
||||
// -------------------- primitive extraction --------------------
|
||||
|
||||
static CPUMesh LoadPrimitiveIntoCPUMesh(const tinygltf::Model& model,
|
||||
const tinygltf::Primitive& prim,
|
||||
const std::string& nameForMesh,
|
||||
const glm::mat4& world) {
|
||||
CPUMesh out{};
|
||||
out.name = nameForMesh;
|
||||
|
||||
// POSITION is required
|
||||
auto itPos = prim.attributes.find("POSITION");
|
||||
if (itPos == prim.attributes.end()) {
|
||||
throw std::runtime_error("Primitive has no POSITION attribute");
|
||||
}
|
||||
|
||||
const tinygltf::Accessor& accPos = model.accessors.at(itPos->second);
|
||||
if (accPos.componentType != TINYGLTF_COMPONENT_TYPE_FLOAT || accPos.type != TINYGLTF_TYPE_VEC3) {
|
||||
throw std::runtime_error("POSITION must be VEC3 float for this loader");
|
||||
}
|
||||
|
||||
const size_t vertexCount = size_t(accPos.count);
|
||||
|
||||
// Optional attributes
|
||||
const tinygltf::Accessor* accNormal = nullptr;
|
||||
const tinygltf::Accessor* accTangent = nullptr;
|
||||
const tinygltf::Accessor* accUV0 = nullptr;
|
||||
|
||||
if (auto it = prim.attributes.find("NORMAL"); it != prim.attributes.end()) {
|
||||
accNormal = &model.accessors.at(it->second);
|
||||
if (accNormal->componentType != TINYGLTF_COMPONENT_TYPE_FLOAT || accNormal->type != TINYGLTF_TYPE_VEC3)
|
||||
accNormal = nullptr;
|
||||
}
|
||||
if (auto it = prim.attributes.find("TANGENT"); it != prim.attributes.end()) {
|
||||
accTangent = &model.accessors.at(it->second);
|
||||
if (accTangent->componentType != TINYGLTF_COMPONENT_TYPE_FLOAT || accTangent->type != TINYGLTF_TYPE_VEC4)
|
||||
accTangent = nullptr;
|
||||
}
|
||||
if (auto it = prim.attributes.find("TEXCOORD_0"); it != prim.attributes.end()) {
|
||||
accUV0 = &model.accessors.at(it->second);
|
||||
if (accUV0->componentType != TINYGLTF_COMPONENT_TYPE_FLOAT || accUV0->type != TINYGLTF_TYPE_VEC2)
|
||||
accUV0 = nullptr;
|
||||
}
|
||||
|
||||
// Prepare pointers/strides
|
||||
auto [posBase, posStride] = GetAccessorDataPtrAndStride(model, accPos);
|
||||
|
||||
const std::uint8_t* nrmBase = nullptr;
|
||||
size_t nrmStride = 0;
|
||||
const std::uint8_t* tanBase = nullptr;
|
||||
size_t tanStride = 0;
|
||||
const std::uint8_t* uvBase = nullptr;
|
||||
size_t uvStride = 0;
|
||||
|
||||
if (accNormal) {
|
||||
auto p = GetAccessorDataPtrAndStride(model, *accNormal);
|
||||
nrmBase = p.first;
|
||||
nrmStride = p.second;
|
||||
if (size_t(accNormal->count) != vertexCount) accNormal = nullptr;
|
||||
}
|
||||
if (accTangent) {
|
||||
auto p = GetAccessorDataPtrAndStride(model, *accTangent);
|
||||
tanBase = p.first;
|
||||
tanStride = p.second;
|
||||
if (size_t(accTangent->count) != vertexCount) accTangent = nullptr;
|
||||
}
|
||||
if (accUV0) {
|
||||
auto p = GetAccessorDataPtrAndStride(model, *accUV0);
|
||||
uvBase = p.first;
|
||||
uvStride = p.second;
|
||||
if (size_t(accUV0->count) != vertexCount) accUV0 = nullptr;
|
||||
}
|
||||
|
||||
// Allocate vertices
|
||||
out.vertices.resize(vertexCount);
|
||||
|
||||
// Bounds init (in world space, because we transform positions)
|
||||
glm::vec3 mn{std::numeric_limits<float>::infinity()};
|
||||
glm::vec3 mx{-std::numeric_limits<float>::infinity()};
|
||||
|
||||
// Normal matrix
|
||||
const glm::mat3 nrmMat = glm::transpose(glm::inverse(glm::mat3(world)));
|
||||
const glm::mat3 tanMat = glm::mat3(world);
|
||||
|
||||
for (size_t i = 0; i < vertexCount; ++i) {
|
||||
CPUMesh::Vertex v{};
|
||||
|
||||
// Position
|
||||
const glm::vec3 pLocal = ReadVec3Float(posBase, posStride, i);
|
||||
v.position = glm::vec3(world * glm::vec4(pLocal, 1.0f));
|
||||
UpdateBounds(mn, mx, v.position);
|
||||
|
||||
// Normal
|
||||
if (accNormal) {
|
||||
const glm::vec3 nLocal = ReadVec3Float(nrmBase, nrmStride, i);
|
||||
v.normal = glm::normalize(nrmMat * nLocal);
|
||||
} else {
|
||||
v.normal = glm::vec3(0.0f, 1.0f, 0.0f);
|
||||
}
|
||||
|
||||
// UV
|
||||
if (accUV0) {
|
||||
glm::vec2 uv = ReadVec2Float(uvBase, uvStride, i);
|
||||
v.uv_x = uv.x;
|
||||
v.uv_y = uv.y;
|
||||
} else {
|
||||
v.uv_x = 0.0f;
|
||||
v.uv_y = 0.0f;
|
||||
}
|
||||
|
||||
// Tangent
|
||||
if (accTangent) {
|
||||
glm::vec4 t = ReadVec4Float(tanBase, tanStride, i);
|
||||
glm::vec3 t3 = glm::normalize(tanMat * glm::vec3(t));
|
||||
v.tangent = glm::vec4(t3, t.w); // keep handedness in w
|
||||
} else {
|
||||
v.tangent = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
out.vertices[i] = v;
|
||||
}
|
||||
|
||||
out.minPos = (vertexCount > 0) ? mn : glm::vec3(0.0f);
|
||||
out.maxPos = (vertexCount > 0) ? mx : glm::vec3(0.0f);
|
||||
|
||||
// Indices
|
||||
if (prim.indices >= 0) {
|
||||
const tinygltf::Accessor& accIdx = model.accessors.at(prim.indices);
|
||||
if (accIdx.type != TINYGLTF_TYPE_SCALAR) {
|
||||
throw std::runtime_error("Indices accessor must be SCALAR");
|
||||
}
|
||||
|
||||
out.indices.resize(size_t(accIdx.count));
|
||||
for (size_t i = 0; i < size_t(accIdx.count); ++i) {
|
||||
out.indices[i] = ReadIndexAsU32(model, accIdx, i);
|
||||
}
|
||||
// Normal
|
||||
if (mesh->HasNormals()) {
|
||||
const aiVector3D& an = mesh->mNormals[i];
|
||||
v.normal = glm::normalize(nrmMat * glm::vec3(an.x, an.y, an.z));
|
||||
} else {
|
||||
out.indices.resize(vertexCount);
|
||||
for (size_t i = 0; i < vertexCount; ++i) out.indices[i] = static_cast<std::uint32_t>(i);
|
||||
v.normal = glm::vec3(0.0f, 1.0f, 0.0f);
|
||||
}
|
||||
|
||||
// If your renderer expects opposite winding vs glTF, you can flip triangle order.
|
||||
// Keep what you had:
|
||||
// for (size_t i = 0; i + 2 < out.indices.size(); i += 3) {
|
||||
// std::swap(out.indices[i + 1], out.indices[i + 2]);
|
||||
// }
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// Merge "src" into "dst" (offset indices)
|
||||
static void AppendMesh(CPUMesh& dst, const CPUMesh& src) {
|
||||
const std::uint32_t baseVertex = static_cast<std::uint32_t>(dst.vertices.size());
|
||||
dst.vertices.insert(dst.vertices.end(), src.vertices.begin(), src.vertices.end());
|
||||
|
||||
dst.indices.reserve(dst.indices.size() + src.indices.size());
|
||||
for (std::uint32_t idx : src.indices) {
|
||||
dst.indices.push_back(baseVertex + idx);
|
||||
}
|
||||
|
||||
if (dst.vertices.size() == src.vertices.size()) {
|
||||
dst.minPos = src.minPos;
|
||||
dst.maxPos = src.maxPos;
|
||||
// UV channel 0
|
||||
if (mesh->HasTextureCoords(0)) {
|
||||
v.uv_x = mesh->mTextureCoords[0][i].x;
|
||||
v.uv_y = mesh->mTextureCoords[0][i].y;
|
||||
} else {
|
||||
dst.minPos = glm::vec3(
|
||||
std::min(dst.minPos.x, src.minPos.x),
|
||||
std::min(dst.minPos.y, src.minPos.y),
|
||||
std::min(dst.minPos.z, src.minPos.z)
|
||||
);
|
||||
dst.maxPos = glm::vec3(
|
||||
std::max(dst.maxPos.x, src.maxPos.x),
|
||||
std::max(dst.maxPos.y, src.maxPos.y),
|
||||
std::max(dst.maxPos.z, src.maxPos.z)
|
||||
);
|
||||
v.uv_x = 0.0f;
|
||||
v.uv_y = 0.0f;
|
||||
}
|
||||
|
||||
// Tangent + bitangent sign (stored in w as +1/-1 handedness)
|
||||
if (mesh->HasTangentsAndBitangents()) {
|
||||
const aiVector3D& at = mesh->mTangents[i];
|
||||
const aiVector3D& ab = mesh->mBitangents[i];
|
||||
glm::vec3 t3 = glm::normalize(tanMat * glm::vec3(at.x, at.y, at.z));
|
||||
glm::vec3 b3 = glm::normalize(tanMat * glm::vec3(ab.x, ab.y, ab.z));
|
||||
glm::vec3 n3 = v.normal;
|
||||
float sign = (glm::dot(glm::cross(n3, t3), b3) < 0.0f) ? -1.0f : 1.0f;
|
||||
v.tangent = glm::vec4(t3, sign);
|
||||
} else {
|
||||
v.tangent = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
out.vertices[i] = v;
|
||||
}
|
||||
|
||||
out.minPos = (vertexCount > 0) ? mn : glm::vec3(0.0f);
|
||||
out.maxPos = (vertexCount > 0) ? mx : glm::vec3(0.0f);
|
||||
|
||||
// Indices
|
||||
if (mesh->HasFaces()) {
|
||||
out.indices.reserve(size_t(mesh->mNumFaces) * 3);
|
||||
for (unsigned int f = 0; f < mesh->mNumFaces; ++f) {
|
||||
const aiFace& face = mesh->mFaces[f];
|
||||
for (unsigned int k = 0; k < face.mNumIndices; ++k)
|
||||
out.indices.push_back(static_cast<std::uint32_t>(face.mIndices[k]));
|
||||
}
|
||||
} else {
|
||||
out.indices.resize(vertexCount);
|
||||
for (size_t i = 0; i < vertexCount; ++i)
|
||||
out.indices[i] = static_cast<std::uint32_t>(i);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// Merge src into dst, offsetting src indices by the current dst vertex count.
|
||||
static void AppendMesh(CPUMesh& dst, const CPUMesh& src) {
|
||||
const std::uint32_t base = static_cast<std::uint32_t>(dst.vertices.size());
|
||||
dst.vertices.insert(dst.vertices.end(), src.vertices.begin(), src.vertices.end());
|
||||
|
||||
dst.indices.reserve(dst.indices.size() + src.indices.size());
|
||||
for (std::uint32_t idx : src.indices)
|
||||
dst.indices.push_back(base + idx);
|
||||
|
||||
dst.minPos = glm::vec3(
|
||||
std::min(dst.minPos.x, src.minPos.x),
|
||||
std::min(dst.minPos.y, src.minPos.y),
|
||||
std::min(dst.minPos.z, src.minPos.z)
|
||||
);
|
||||
dst.maxPos = glm::vec3(
|
||||
std::max(dst.maxPos.x, src.maxPos.x),
|
||||
std::max(dst.maxPos.y, src.maxPos.y),
|
||||
std::max(dst.maxPos.z, src.maxPos.z)
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Skeleton loading ─────────────────────────────────────────────────────────
|
||||
|
||||
static Skeleton LoadSkeleton(const aiScene* scene) {
|
||||
Skeleton skeleton;
|
||||
|
||||
// Pass 1: collect all bone names and their inverse bind matrices
|
||||
// from every mesh in the scene.
|
||||
std::unordered_map<std::string, glm::mat4> boneOffsets;
|
||||
for (unsigned int m = 0; m < scene->mNumMeshes; ++m) {
|
||||
const aiMesh* mesh = scene->mMeshes[m];
|
||||
for (unsigned int b = 0; b < mesh->mNumBones; ++b) {
|
||||
const aiBone* bone = mesh->mBones[b];
|
||||
std::string boneName(bone->mName.C_Str());
|
||||
if (!boneOffsets.count(boneName))
|
||||
boneOffsets[boneName] = ToGLM(bone->mOffsetMatrix);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------- public API --------------------
|
||||
if (boneOffsets.empty()) return skeleton; // static mesh, no skeleton
|
||||
|
||||
struct NodeStackItem {
|
||||
int nodeIdx;
|
||||
glm::mat4 parentWorld;
|
||||
};
|
||||
// Pass 2: walk the node tree in depth-first order (parent before child),
|
||||
// registering only nodes that correspond to actual bones.
|
||||
// This guarantees joints are ordered parent-before-child, which is required
|
||||
// by Animator::computeJointMatrices.
|
||||
JointId nextId = 0;
|
||||
|
||||
// Option A: return one CPUMesh per *primitive* encountered in the scene
|
||||
static std::vector<CPUMesh> LoadGLTF_CPUMeshes_PerPrimitive(const std::string& path) {
|
||||
tinygltf::TinyGLTF loader;
|
||||
tinygltf::Model model;
|
||||
std::string err, warn;
|
||||
struct StackItem { const aiNode* node; int parentIdx; };
|
||||
std::vector<StackItem> stack{{ scene->mRootNode, -1 }};
|
||||
|
||||
bool ok = false;
|
||||
const bool isGLB = (path.size() >= 4 && path.substr(path.size() - 4) == ".glb");
|
||||
if (isGLB) ok = loader.LoadBinaryFromFile(&model, &err, &warn, path);
|
||||
else ok = loader.LoadASCIIFromFile(&model, &err, &warn, path);
|
||||
while (!stack.empty()) {
|
||||
auto [node, parentIdx] = stack.back();
|
||||
stack.pop_back();
|
||||
|
||||
if (!warn.empty()) std::cerr << "tinygltf warn: " << warn << "\n";
|
||||
if (!ok) throw std::runtime_error("Failed to load glTF: " + err);
|
||||
std::string name(node->mName.C_Str());
|
||||
int myIdx = parentIdx; // default: pass parent through to children
|
||||
|
||||
int sceneIndex = model.defaultScene >= 0 ? model.defaultScene : 0;
|
||||
if (sceneIndex < 0 || sceneIndex >= int(model.scenes.size())) {
|
||||
throw std::runtime_error("glTF has no valid scene");
|
||||
if (boneOffsets.count(name)) {
|
||||
const JointId id = nextId++;
|
||||
myIdx = static_cast<int>(id);
|
||||
|
||||
Joint joint{};
|
||||
joint.id = id;
|
||||
skeleton.joints.push_back(joint);
|
||||
skeleton.jointNames.push_back(name);
|
||||
skeleton.inverseBindMatrices.push_back(boneOffsets[name]);
|
||||
|
||||
// Grow hierarchy to accommodate this id
|
||||
while (skeleton.hierarchy.size() <= id)
|
||||
skeleton.hierarchy.push_back({});
|
||||
|
||||
skeleton.hierarchy[id].id = id; // record the node's own id
|
||||
|
||||
if (parentIdx >= 0)
|
||||
skeleton.hierarchy[parentIdx].children.push_back(id);
|
||||
}
|
||||
|
||||
std::vector<CPUMesh> result;
|
||||
const tinygltf::Scene& scene = model.scenes.at(sceneIndex);
|
||||
// Push children in reverse so left-most child is processed first
|
||||
for (int c = static_cast<int>(node->mNumChildren) - 1; c >= 0; --c)
|
||||
stack.push_back({ node->mChildren[c], myIdx });
|
||||
}
|
||||
|
||||
std::vector<NodeStackItem> stack;
|
||||
stack.reserve(scene.nodes.size());
|
||||
for (int n : scene.nodes) stack.push_back({n, glm::mat4(1.0f)});
|
||||
buildParentIndex(skeleton);
|
||||
return skeleton;
|
||||
}
|
||||
|
||||
while (!stack.empty()) {
|
||||
NodeStackItem it = stack.back();
|
||||
stack.pop_back();
|
||||
// ─── Skinning data ────────────────────────────────────────────────────────────
|
||||
|
||||
const tinygltf::Node& node = model.nodes.at(it.nodeIdx);
|
||||
const glm::mat4 local = NodeLocalMatrix(node);
|
||||
const glm::mat4 world = it.parentWorld * local;
|
||||
// Populate cpuMesh.skinningData from the matching aiMesh.
|
||||
// Must be called after LoadAiMeshIntoCPUMesh so vertices are already sized.
|
||||
static void LoadSkinningData(CPUMesh& cpuMesh,
|
||||
const aiMesh* aiMesh,
|
||||
const Skeleton& skeleton) {
|
||||
if (!aiMesh->HasBones() || skeleton.joints.empty()) return;
|
||||
|
||||
for (int child : node.children) stack.push_back({child, world});
|
||||
cpuMesh.skinningData.assign(cpuMesh.vertices.size(),
|
||||
CPUMesh::SkinningData{ {0, 0, 0, 0}, {0.f, 0.f, 0.f, 0.f} });
|
||||
|
||||
if (node.mesh < 0) continue;
|
||||
const tinygltf::Mesh& mesh = model.meshes.at(node.mesh);
|
||||
std::vector<int> weightCount(cpuMesh.vertices.size(), 0);
|
||||
|
||||
for (size_t p = 0; p < mesh.primitives.size(); ++p) {
|
||||
const tinygltf::Primitive& prim = mesh.primitives[p];
|
||||
CPUMesh cpu = LoadPrimitiveIntoCPUMesh(
|
||||
model,
|
||||
prim,
|
||||
mesh.name.empty() ? ("mesh_" + std::to_string(node.mesh)) : mesh.name,
|
||||
world
|
||||
);
|
||||
cpu.name += "_prim" + std::to_string(p);
|
||||
result.push_back(std::move(cpu));
|
||||
for (unsigned int b = 0; b < aiMesh->mNumBones; ++b) {
|
||||
const aiBone* bone = aiMesh->mBones[b];
|
||||
std::string boneName(bone->mName.C_Str());
|
||||
|
||||
// Find which joint index this bone maps to
|
||||
std::uint32_t jointIdx = 0;
|
||||
bool found = false;
|
||||
for (std::size_t j = 0; j < skeleton.jointNames.size(); ++j) {
|
||||
if (skeleton.jointNames[j] == boneName) {
|
||||
jointIdx = static_cast<std::uint32_t>(j);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) continue;
|
||||
|
||||
return result;
|
||||
}
|
||||
for (unsigned int w = 0; w < bone->mNumWeights; ++w) {
|
||||
const unsigned int vertIdx = bone->mWeights[w].mVertexId;
|
||||
const float weight = bone->mWeights[w].mWeight;
|
||||
const int slot = weightCount[vertIdx];
|
||||
|
||||
// Option B: return one CPUMesh per *glTF mesh instance*, merging all primitives of that mesh into one CPUMesh.
|
||||
// Note: If the same glTF mesh is instanced by multiple nodes, you'll get one merged CPUMesh PER NODE instance.
|
||||
static std::vector<CPUMesh> LoadGLTF_CPUMeshes_MergedPerMesh(const std::string& path) {
|
||||
tinygltf::TinyGLTF loader;
|
||||
tinygltf::Model model;
|
||||
std::string err, warn;
|
||||
if (slot >= 4) continue; // aiProcess_LimitBoneWeights should prevent this
|
||||
|
||||
bool ok = false;
|
||||
const bool isGLB = (path.size() >= 4 && path.substr(path.size() - 4) == ".glb");
|
||||
if (isGLB) ok = loader.LoadBinaryFromFile(&model, &err, &warn, path);
|
||||
else ok = loader.LoadASCIIFromFile(&model, &err, &warn, path);
|
||||
|
||||
if (!warn.empty()) std::cerr << "tinygltf warn: " << warn << "\n";
|
||||
if (!ok) throw std::runtime_error("Failed to load glTF: " + err);
|
||||
|
||||
int sceneIndex = model.defaultScene >= 0 ? model.defaultScene : 0;
|
||||
if (sceneIndex < 0 || sceneIndex >= int(model.scenes.size())) {
|
||||
throw std::runtime_error("glTF has no valid scene");
|
||||
cpuMesh.skinningData[vertIdx].jointIds[slot] = jointIdx;
|
||||
cpuMesh.skinningData[vertIdx].weights[slot] = weight;
|
||||
++weightCount[vertIdx];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<CPUMesh> result;
|
||||
// ─── Animation loading ────────────────────────────────────────────────────────
|
||||
|
||||
const tinygltf::Scene& scene = model.scenes.at(sceneIndex);
|
||||
std::vector<NodeStackItem> stack;
|
||||
stack.reserve(scene.nodes.size());
|
||||
for (int n : scene.nodes) stack.push_back({n, glm::mat4(1.0f)});
|
||||
static std::vector<SkeletalAnimation> LoadAnimations(const aiScene* scene,
|
||||
const Skeleton& skeleton) {
|
||||
std::vector<SkeletalAnimation> result;
|
||||
if (!scene->HasAnimations()) return result;
|
||||
|
||||
while (!stack.empty()) {
|
||||
NodeStackItem it = stack.back();
|
||||
stack.pop_back();
|
||||
for (unsigned int a = 0; a < scene->mNumAnimations; ++a) {
|
||||
const aiAnimation* aiAnim = scene->mAnimations[a];
|
||||
const double tps = aiAnim->mTicksPerSecond > 0.0 ? aiAnim->mTicksPerSecond : 25.0;
|
||||
|
||||
const tinygltf::Node& node = model.nodes.at(it.nodeIdx);
|
||||
const glm::mat4 local = NodeLocalMatrix(node);
|
||||
const glm::mat4 world = it.parentWorld * local;
|
||||
SkeletalAnimation anim;
|
||||
anim.name = aiAnim->mName.C_Str();
|
||||
anim.duration = static_cast<float>(aiAnim->mDuration / tps);
|
||||
anim.looped = true;
|
||||
|
||||
for (int child : node.children) stack.push_back({child, world});
|
||||
for (unsigned int c = 0; c < aiAnim->mNumChannels; ++c) {
|
||||
const aiNodeAnim* ch = aiAnim->mChannels[c];
|
||||
std::string boneName(ch->mNodeName.C_Str());
|
||||
|
||||
if (node.mesh < 0) continue;
|
||||
const tinygltf::Mesh& mesh = model.meshes.at(node.mesh);
|
||||
|
||||
CPUMesh merged{};
|
||||
merged.name = mesh.name.empty() ? ("mesh_" + std::to_string(node.mesh)) : mesh.name;
|
||||
merged.minPos = glm::vec3(std::numeric_limits<float>::infinity());
|
||||
merged.maxPos = glm::vec3(-std::numeric_limits<float>::infinity());
|
||||
|
||||
bool any = false;
|
||||
for (const tinygltf::Primitive& prim : mesh.primitives) {
|
||||
CPUMesh part = LoadPrimitiveIntoCPUMesh(model, prim, merged.name, world);
|
||||
if (!any) {
|
||||
merged = std::move(part);
|
||||
any = true;
|
||||
} else {
|
||||
AppendMesh(merged, part);
|
||||
// Map bone name to joint index
|
||||
std::uint32_t jointIdx = 0;
|
||||
bool found = false;
|
||||
for (std::size_t j = 0; j < skeleton.jointNames.size(); ++j) {
|
||||
if (skeleton.jointNames[j] == boneName) {
|
||||
jointIdx = static_cast<std::uint32_t>(j);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) continue;
|
||||
|
||||
if (any) result.push_back(std::move(merged));
|
||||
SkeletalAnimation::Track track;
|
||||
track.jointIndex = jointIdx;
|
||||
|
||||
// FIX: position, rotation and scale channels can have different
|
||||
// keyframe counts and independent time axes. Build the track from
|
||||
// the position channel's timeline and pick the nearest rotation/
|
||||
// scale key for each sample rather than assuming index alignment.
|
||||
// This avoids corrupted keyframes when counts differ.
|
||||
const unsigned int numPos = ch->mNumPositionKeys;
|
||||
const unsigned int numRot = ch->mNumRotationKeys;
|
||||
const unsigned int numSca = ch->mNumScalingKeys;
|
||||
|
||||
// Use position keys to drive the timeline (most common case).
|
||||
// If there are no position keys fall back to rotation keys.
|
||||
const unsigned int numKeys = numPos > 0 ? numPos : numRot;
|
||||
|
||||
for (unsigned int k = 0; k < numKeys; ++k) {
|
||||
SkeletalAnimation::Keyframe kf;
|
||||
|
||||
// Time comes from whichever channel drives this loop
|
||||
if (numPos > 0) {
|
||||
kf.time = static_cast<float>(ch->mPositionKeys[k].mTime / tps);
|
||||
const auto& p = ch->mPositionKeys[k].mValue;
|
||||
kf.translation = { p.x, p.y, p.z };
|
||||
} else {
|
||||
kf.time = static_cast<float>(ch->mRotationKeys[k].mTime / tps);
|
||||
kf.translation = { 0.f, 0.f, 0.f };
|
||||
}
|
||||
|
||||
// Nearest rotation key at this index
|
||||
{
|
||||
const unsigned int ri = std::min(k, numRot - 1);
|
||||
const auto& r = ch->mRotationKeys[ri].mValue;
|
||||
kf.rotation = glm::quat(r.w, r.x, r.y, r.z); // glm: (w,x,y,z)
|
||||
}
|
||||
|
||||
// Nearest scale key at this index
|
||||
{
|
||||
const unsigned int si = std::min(k, numSca - 1);
|
||||
const auto& s = ch->mScalingKeys[si].mValue;
|
||||
kf.scale = { s.x, s.y, s.z };
|
||||
}
|
||||
|
||||
track.keyframes.push_back(kf);
|
||||
}
|
||||
|
||||
anim.tracks.push_back(std::move(track));
|
||||
}
|
||||
|
||||
return result;
|
||||
result.push_back(std::move(anim));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ─── Public API: static meshes ────────────────────────────────────────────────
|
||||
|
||||
// Option A: one CPUMesh per aiMesh reference encountered in the node tree.
|
||||
static std::vector<CPUMesh> LoadGLTF_CPUMeshes_PerPrimitive(const std::string& path) {
|
||||
Assimp::Importer importer;
|
||||
const aiScene* scene = LoadScene(importer, path);
|
||||
|
||||
std::vector<CPUMesh> result;
|
||||
|
||||
struct StackItem { const aiNode* node; glm::mat4 parentWorld; };
|
||||
std::vector<StackItem> stack{{ scene->mRootNode, glm::mat4(1.0f) }};
|
||||
|
||||
while (!stack.empty()) {
|
||||
auto [node, parentWorld] = stack.back();
|
||||
stack.pop_back();
|
||||
|
||||
const glm::mat4 world = parentWorld * ToGLM(node->mTransformation);
|
||||
|
||||
for (unsigned int c = 0; c < node->mNumChildren; ++c)
|
||||
stack.push_back({ node->mChildren[c], world });
|
||||
|
||||
for (unsigned int m = 0; m < node->mNumMeshes; ++m) {
|
||||
const aiMesh* mesh = scene->mMeshes[node->mMeshes[m]];
|
||||
std::string name = mesh->mName.length > 0
|
||||
? std::string(mesh->mName.C_Str())
|
||||
: ("mesh_" + std::to_string(node->mMeshes[m]));
|
||||
name += "_prim" + std::to_string(m);
|
||||
|
||||
result.push_back(LoadAiMeshIntoCPUMesh(mesh, name, world));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Option B: one CPUMesh per node, merging all aiMesh references of that node.
|
||||
static std::vector<CPUMesh> LoadGLTF_CPUMeshes_MergedPerMesh(const std::string& path) {
|
||||
Assimp::Importer importer;
|
||||
const aiScene* scene = LoadScene(importer, path);
|
||||
|
||||
std::vector<CPUMesh> result;
|
||||
|
||||
struct StackItem { const aiNode* node; glm::mat4 parentWorld; };
|
||||
std::vector<StackItem> stack{{ scene->mRootNode, glm::mat4(1.0f) }};
|
||||
|
||||
while (!stack.empty()) {
|
||||
auto [node, parentWorld] = stack.back();
|
||||
stack.pop_back();
|
||||
|
||||
const glm::mat4 world = parentWorld * ToGLM(node->mTransformation);
|
||||
|
||||
for (unsigned int c = 0; c < node->mNumChildren; ++c)
|
||||
stack.push_back({ node->mChildren[c], world });
|
||||
|
||||
if (node->mNumMeshes == 0) continue;
|
||||
|
||||
std::string mergedName = node->mName.length > 0
|
||||
? std::string(node->mName.C_Str())
|
||||
: std::string(scene->mMeshes[node->mMeshes[0]]->mName.C_Str());
|
||||
|
||||
CPUMesh merged{};
|
||||
merged.name = mergedName;
|
||||
merged.minPos = glm::vec3( std::numeric_limits<float>::infinity());
|
||||
merged.maxPos = glm::vec3(-std::numeric_limits<float>::infinity());
|
||||
|
||||
bool any = false;
|
||||
for (unsigned int m = 0; m < node->mNumMeshes; ++m) {
|
||||
const aiMesh* mesh = scene->mMeshes[node->mMeshes[m]];
|
||||
CPUMesh part = LoadAiMeshIntoCPUMesh(mesh, mergedName, world);
|
||||
if (!any) { merged = std::move(part); any = true; }
|
||||
else AppendMesh(merged, part);
|
||||
}
|
||||
|
||||
if (any) result.push_back(std::move(merged));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ─── Public API: skinned model ────────────────────────────────────────────────
|
||||
|
||||
struct SkinnedModel {
|
||||
std::vector<CPUMesh> meshes;
|
||||
Skeleton skeleton;
|
||||
std::vector<SkeletalAnimation> animations;
|
||||
};
|
||||
|
||||
// Loads meshes + skeleton + animations in a single Assimp pass.
|
||||
// Each CPUMesh has skinningData and skeleton populated.
|
||||
//
|
||||
// IMPORTANT: skinned mesh vertices are loaded with an identity world transform
|
||||
// so they remain in bind-pose space. The skinning shader applies joint matrices
|
||||
// at runtime — baking the node transform into positions would break that.
|
||||
//
|
||||
// Usage:
|
||||
// auto model = ModelLoader::LoadSkinnedModel("character.glb");
|
||||
// MeshId id = meshCache.uploadMesh(model.meshes[0]);
|
||||
// auto* anim = go.AddComponent<Animator>();
|
||||
// for (auto& clip : model.animations)
|
||||
// anim->addClip(std::make_shared<SkeletalAnimation>(std::move(clip)));
|
||||
// anim->play("Walk");
|
||||
static SkinnedModel LoadSkinnedModel(const std::string& path) {
|
||||
Assimp::Importer importer;
|
||||
const aiScene* scene = LoadScene(importer, path);
|
||||
|
||||
SkinnedModel model;
|
||||
model.skeleton = LoadSkeleton(scene);
|
||||
model.animations = LoadAnimations(scene, model.skeleton);
|
||||
|
||||
struct StackItem { const aiNode* node; glm::mat4 parentWorld; };
|
||||
std::vector<StackItem> stack{{ scene->mRootNode, glm::mat4(1.f) }};
|
||||
|
||||
while (!stack.empty()) {
|
||||
auto [node, parentWorld] = stack.back();
|
||||
stack.pop_back();
|
||||
|
||||
// We still traverse with world transforms so non-skinned siblings
|
||||
// work correctly, but skinned meshes are loaded with identity below.
|
||||
const glm::mat4 world = parentWorld * ToGLM(node->mTransformation);
|
||||
|
||||
for (unsigned int c = 0; c < node->mNumChildren; ++c)
|
||||
stack.push_back({ node->mChildren[c], world });
|
||||
|
||||
for (unsigned int m = 0; m < node->mNumMeshes; ++m) {
|
||||
const aiMesh* aiMesh = scene->mMeshes[node->mMeshes[m]];
|
||||
std::string name = aiMesh->mName.length > 0
|
||||
? std::string(aiMesh->mName.C_Str())
|
||||
: ("mesh_" + std::to_string(node->mMeshes[m]));
|
||||
|
||||
// FIX: pass identity matrix for skinned meshes so vertices stay in
|
||||
// bind-pose space. The inverse bind matrices and joint transforms
|
||||
// are already expressed in that space — baking the node world
|
||||
// transform into positions would offset everything incorrectly.
|
||||
const bool isSkinned = aiMesh->HasBones() && !model.skeleton.joints.empty();
|
||||
const glm::mat4 meshWorld = isSkinned ? glm::mat4{1.f} : world;
|
||||
|
||||
CPUMesh cpu = LoadAiMeshIntoCPUMesh(aiMesh, name, meshWorld);
|
||||
LoadSkinningData(cpu, aiMesh, model.skeleton);
|
||||
cpu.skeleton = model.skeleton;
|
||||
model.meshes.push_back(std::move(cpu));
|
||||
}
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
} // namespace ModelLoader
|
||||
|
||||
#endif // MODELLOADER_H
|
||||
#endif // MODELLOADER_H
|
||||
Reference in New Issue
Block a user