This commit is contained in:
2026-03-21 23:10:48 +01:00
parent 8cbb794dba
commit 3153735d0c
18 changed files with 249 additions and 195 deletions

View File

@@ -14,27 +14,22 @@
class SkinningPipeline;
class Animator : public Component {
class Animator final: 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; }
[[nodiscard]] bool isPlaying() const { return m_current.clip != nullptr; }
[[nodiscard]] const std::string& currentClipName() const { return m_currentClipName; }
[[nodiscard]] 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);
std::size_t uploadJointMatrices(const RenderContext& ctx, const Skeleton& skeleton, std::size_t frameIndex);
Skeleton* getSkeleton() {
return &m_skeleton;
}

View File

@@ -32,12 +32,15 @@ private:
std::unique_ptr<Pipeline> pipeline;
ImageID skyboxTextureId{NULL_IMAGE_ID};
glm::mat4 skyboxRotation{1.f};
struct SkyboxPushConstants {
glm::mat4 invViewProj;
glm::vec4 cameraPos;
glm::mat4 invViewProj;
glm::vec4 skyboxRot[3];
glm::vec3 cameraPos;
std::uint32_t skyboxTextureId;
};
};
#endif //SKYBOXPIPELINE_H

View File

@@ -7,6 +7,7 @@
#include <glm/mat4x4.hpp>
#include <glm/vec3.hpp>
#include <glm/gtc/quaternion.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <destrum/Graphics/ids.h>
@@ -28,6 +29,8 @@ struct Skeleton {
std::vector<Joint> joints;
std::vector<std::string> jointNames;
std::vector<int> parentIndex; // -1 = root, built once after loading
glm::mat4 rootPreTransform{1.f};
};
inline void buildParentIndex(Skeleton& skeleton) {

View File

@@ -0,0 +1,31 @@
#ifndef DELTATIME_H
#define DELTATIME_H
#include <chrono>
#include <destrum/Singleton.h>
class Time final: public Singleton<Time> {
public:
Time(const Time& other) = delete;
Time(Time&& other) = delete;
Time& operator=(const Time& other) = delete;
Time& operator=(Time&& other) = delete;
[[nodiscard]] double FixedDeltaTime() const;
[[nodiscard]] double DeltaTime() const;
[[nodiscard]] std::chrono::nanoseconds SleepDuration() const;
void Update();
private:
friend class Singleton;
Time() = default;
static constexpr double m_FixedDeltaTime{1.0 / 60.0};
static constexpr double FPS{60};
double m_DeltaTime{};
std::chrono::high_resolution_clock::time_point m_PrevTime;
};
#endif //DELTATIME_H

View File

@@ -12,6 +12,10 @@
// - 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.
// - rootPreTransform on Skeleton captures any non-bone ancestor transforms
// (e.g. the Assimp-injected coordinate-system node for glTF/FBX) so that
// computeJointMatrices can prepend it to root joints. This fixes the
// 90-degree rotation + mirror that appears when those transforms are dropped.
//
// Link against: assimp (e.g. -lassimp)
// Supports any format Assimp understands (.gltf, .glb, .fbx, .obj, …).
@@ -198,19 +202,32 @@ static Skeleton LoadSkeleton(const aiScene* scene) {
// registering only nodes that correspond to actual bones.
// This guarantees joints are ordered parent-before-child, which is required
// by Animator::computeJointMatrices.
//
// Non-bone ancestor nodes (e.g. the Assimp coordinate-system root, or an
// "Armature" node in Blender exports) are NOT registered as joints, but
// their accumulated transform is carried forward so that the first real
// bone joint inherits it via skeleton.rootPreTransform. Without this the
// 90° rotation / mirror baked into those nodes is silently dropped, causing
// skinned meshes to appear rotated and/or mirrored relative to static ones.
JointId nextId = 0;
struct StackItem { const aiNode* node; int parentIdx; };
std::vector<StackItem> stack{{ scene->mRootNode, -1 }};
struct StackItem {
const aiNode* node;
int parentIdx;
glm::mat4 accWorld; // accumulated transform of non-bone ancestors
};
std::vector<StackItem> stack{{ scene->mRootNode, -1, glm::mat4{1.f} }};
while (!stack.empty()) {
auto [node, parentIdx] = stack.back();
auto [node, parentIdx, accWorld] = stack.back();
stack.pop_back();
std::string name(node->mName.C_Str());
int myIdx = parentIdx; // default: pass parent through to children
int myIdx = parentIdx;
glm::mat4 myAccWorld = accWorld; // only used while still outside the bone hierarchy
if (boneOffsets.count(name)) {
// ── This node IS a bone ──────────────────────────────────────────
const JointId id = nextId++;
myIdx = static_cast<int>(id);
@@ -220,19 +237,37 @@ static Skeleton LoadSkeleton(const aiScene* scene) {
skeleton.jointNames.push_back(name);
skeleton.inverseBindMatrices.push_back(boneOffsets[name]);
// Grow hierarchy to accommodate this id
// Grow hierarchy arrays to accommodate this id
while (skeleton.hierarchy.size() <= id)
skeleton.hierarchy.push_back({});
skeleton.hierarchy[id].id = id;
skeleton.hierarchy[id].id = id; // record the node's own id
if (parentIdx >= 0)
if (parentIdx >= 0) {
skeleton.hierarchy[parentIdx].children.push_back(id);
} else {
// First bone encountered with no bone parent: capture any
// non-bone ancestor transforms accumulated so far.
// Animator::computeJointMatrices prepends this to root joints.
skeleton.rootPreTransform = accWorld;
glm::mat4& m = skeleton.rootPreTransform;
// Normalize each basis vector to remove scale
m[0] = glm::vec4(glm::normalize(glm::vec3(m[0])), 0.f);
m[1] = glm::vec4(glm::normalize(glm::vec3(m[1])), 0.f);
m[2] = glm::vec4(glm::normalize(glm::vec3(m[2])), 0.f);
}
// Once we are inside the bone hierarchy the pre-transform is fully
// absorbed — children carry identity as their accWorld.
myAccWorld = glm::mat4{1.f};
} else {
// ── Not a bone: accumulate this node's local transform ───────────
myAccWorld = accWorld * ToGLM(node->mTransformation);
}
// 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 });
stack.push_back({ node->mChildren[c], myIdx, myAccWorld });
}
buildParentIndex(skeleton);
@@ -318,11 +353,11 @@ static std::vector<SkeletalAnimation> LoadAnimations(const aiScene* scene,
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.
// 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;
@@ -464,6 +499,11 @@ struct SkinnedModel {
// so they remain in bind-pose space. The skinning shader applies joint matrices
// at runtime — baking the node transform into positions would break that.
//
// The skeleton carries a rootPreTransform that captures any non-bone ancestor
// node transforms (coordinate-system correction nodes injected by Assimp for
// glTF/FBX, Blender "Armature" nodes, etc.). Animator::computeJointMatrices
// must prepend this to the global matrix of every root joint (parentIndex < 0).
//
// Usage:
// auto model = ModelLoader::LoadSkinnedModel("character.glb");
// MeshId id = meshCache.uploadMesh(model.meshes[0]);
@@ -499,7 +539,7 @@ static SkinnedModel LoadSkinnedModel(const std::string& path) {
? std::string(aiMesh->mName.C_Str())
: ("mesh_" + std::to_string(node->mMeshes[m]));
// FIX: pass identity matrix for skinned meshes so vertices stay in
// 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.