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

Submodule TheChef updated: 3a7b137165...df0da96c38

View File

@@ -47,6 +47,8 @@ set(SRC_FILES
"src/FS/AssetFS.cpp" "src/FS/AssetFS.cpp"
"src/FS/Manifest.cpp" "src/FS/Manifest.cpp"
"src/Util/DeltaTime.cpp"
) )
add_library(destrum ${SRC_FILES}) add_library(destrum ${SRC_FILES})

Binary file not shown.

Binary file not shown.

View File

@@ -4,26 +4,33 @@
#include "bindless.glsl" #include "bindless.glsl"
layout(location = 0) in vec2 uv; // from fullscreen triangle: 0..2 range layout(location = 0) in vec2 uv;
layout(location = 0) out vec4 outColor; layout(location = 0) out vec4 outColor;
layout(push_constant) uniform SkyboxPC { layout(push_constant) uniform SkyboxPC {
mat4 invViewProj; // inverse(Proj * View) (your current setup) mat4 invViewProj;
vec4 cameraPos; // xyz = camera world position vec4 skyboxRot[3];
uint skyboxTextureId; // index into textureCubes[] vec3 cameraPos;
uint skyboxTextureId;
} pcs; } pcs;
void main() void main()
{ {
vec2 ndcXY = uv * 2.0 - 1.0; vec2 ndcXY = uv * 2.0 - 1.0;
vec4 ndc = vec4(ndcXY, 1.0, 1.0); vec4 ndc = vec4(ndcXY, 1.0, 1.0);
vec4 world = pcs.invViewProj * ndc; vec4 world = pcs.invViewProj * ndc;
vec3 worldPos = world.xyz / world.w; vec3 worldPos = world.xyz / world.w;
vec3 dir = normalize(worldPos - pcs.cameraPos.xyz); vec3 dir = normalize(worldPos - pcs.cameraPos);
dir.y *= -1.0; dir.y *= -1.0;
mat3 skyboxRot = mat3(
pcs.skyboxRot[0].xyz,
pcs.skyboxRot[1].xyz,
pcs.skyboxRot[2].xyz
);
dir = skyboxRot * dir;
outColor = sampleTextureCubeLinear(pcs.skyboxTextureId, dir); outColor = sampleTextureCubeLinear(pcs.skyboxTextureId, dir);
} }

View File

@@ -14,27 +14,22 @@
class SkinningPipeline; class SkinningPipeline;
class Animator : public Component { class Animator final: public Component {
public: public:
explicit Animator(GameObject& parent); explicit Animator(GameObject& parent);
// Component interface
void Update() override; void Update() override;
void ImGuiInspector() override; void ImGuiInspector() override;
// Animation control
void addClip(std::shared_ptr<SkeletalAnimation> clip); void addClip(std::shared_ptr<SkeletalAnimation> clip);
void play(const std::string& name, float blendTime = 0.f); void play(const std::string& name, float blendTime = 0.f);
void stop(); void stop();
bool isPlaying() const { return m_current.clip != nullptr; } [[nodiscard]] bool isPlaying() const { return m_current.clip != nullptr; }
const std::string& currentClipName() const { return m_currentClipName; } [[nodiscard]] const std::string& currentClipName() const { return m_currentClipName; }
float currentTime() const { return m_current.time; } [[nodiscard]] float currentTime() const { return m_current.time; }
// Called during draw command building std::size_t uploadJointMatrices(const RenderContext& ctx, const Skeleton& skeleton, std::size_t frameIndex);
std::size_t uploadJointMatrices(SkinningPipeline& pipeline,
const Skeleton& skeleton,
std::size_t frameIndex);
Skeleton* getSkeleton() { Skeleton* getSkeleton() {
return &m_skeleton; return &m_skeleton;
} }

View File

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

View File

@@ -7,6 +7,7 @@
#include <glm/mat4x4.hpp> #include <glm/mat4x4.hpp>
#include <glm/vec3.hpp> #include <glm/vec3.hpp>
#include <glm/gtc/quaternion.hpp> #include <glm/gtc/quaternion.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <destrum/Graphics/ids.h> #include <destrum/Graphics/ids.h>
@@ -28,6 +29,8 @@ struct Skeleton {
std::vector<Joint> joints; std::vector<Joint> joints;
std::vector<std::string> jointNames; std::vector<std::string> jointNames;
std::vector<int> parentIndex; // -1 = root, built once after loading std::vector<int> parentIndex; // -1 = root, built once after loading
glm::mat4 rootPreTransform{1.f};
}; };
inline void buildParentIndex(Skeleton& skeleton) { 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 // - For skinned meshes, vertex positions are kept in local/bind-pose space
// (identity world transform) so the skinning shader can apply joint matrices // (identity world transform) so the skinning shader can apply joint matrices
// correctly at runtime. // 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) // Link against: assimp (e.g. -lassimp)
// Supports any format Assimp understands (.gltf, .glb, .fbx, .obj, …). // 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. // registering only nodes that correspond to actual bones.
// This guarantees joints are ordered parent-before-child, which is required // This guarantees joints are ordered parent-before-child, which is required
// by Animator::computeJointMatrices. // 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; JointId nextId = 0;
struct StackItem { const aiNode* node; int parentIdx; }; struct StackItem {
std::vector<StackItem> stack{{ scene->mRootNode, -1 }}; 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()) { while (!stack.empty()) {
auto [node, parentIdx] = stack.back(); auto [node, parentIdx, accWorld] = stack.back();
stack.pop_back(); stack.pop_back();
std::string name(node->mName.C_Str()); 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)) { if (boneOffsets.count(name)) {
// ── This node IS a bone ──────────────────────────────────────────
const JointId id = nextId++; const JointId id = nextId++;
myIdx = static_cast<int>(id); myIdx = static_cast<int>(id);
@@ -220,19 +237,37 @@ static Skeleton LoadSkeleton(const aiScene* scene) {
skeleton.jointNames.push_back(name); skeleton.jointNames.push_back(name);
skeleton.inverseBindMatrices.push_back(boneOffsets[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) while (skeleton.hierarchy.size() <= id)
skeleton.hierarchy.push_back({}); 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); 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 // Push children in reverse so left-most child is processed first
for (int c = static_cast<int>(node->mNumChildren) - 1; c >= 0; --c) 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); buildParentIndex(skeleton);
@@ -318,11 +353,11 @@ static std::vector<SkeletalAnimation> LoadAnimations(const aiScene* scene,
SkeletalAnimation::Track track; SkeletalAnimation::Track track;
track.jointIndex = jointIdx; track.jointIndex = jointIdx;
// FIX: position, rotation and scale channels can have different // Position, rotation and scale channels can have different keyframe
// keyframe counts and independent time axes. Build the track from // counts and independent time axes. Build the track from the position
// the position channel's timeline and pick the nearest rotation/ // channel's timeline and pick the nearest rotation/scale key for each
// scale key for each sample rather than assuming index alignment. // sample rather than assuming index alignment. This avoids corrupted
// This avoids corrupted keyframes when counts differ. // keyframes when counts differ.
const unsigned int numPos = ch->mNumPositionKeys; const unsigned int numPos = ch->mNumPositionKeys;
const unsigned int numRot = ch->mNumRotationKeys; const unsigned int numRot = ch->mNumRotationKeys;
const unsigned int numSca = ch->mNumScalingKeys; 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 // so they remain in bind-pose space. The skinning shader applies joint matrices
// at runtime — baking the node transform into positions would break that. // 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: // Usage:
// auto model = ModelLoader::LoadSkinnedModel("character.glb"); // auto model = ModelLoader::LoadSkinnedModel("character.glb");
// MeshId id = meshCache.uploadMesh(model.meshes[0]); // MeshId id = meshCache.uploadMesh(model.meshes[0]);
@@ -499,7 +539,7 @@ static SkinnedModel LoadSkinnedModel(const std::string& path) {
? std::string(aiMesh->mName.C_Str()) ? std::string(aiMesh->mName.C_Str())
: ("mesh_" + std::to_string(node->mMeshes[m])); : ("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 // bind-pose space. The inverse bind matrices and joint transforms
// are already expressed in that space — baking the node world // are already expressed in that space — baking the node world
// transform into positions would offset everything incorrectly. // transform into positions would offset everything incorrectly.

View File

@@ -3,6 +3,7 @@
#include <destrum/App.h> #include <destrum/App.h>
#include <destrum/FS/AssetFS.h> #include <destrum/FS/AssetFS.h>
#include <destrum/Util/DeltaTime.h>
#include "glm/gtx/transform.hpp" #include "glm/gtx/transform.hpp"
#include "spdlog/spdlog.h" #include "spdlog/spdlog.h"
@@ -35,41 +36,33 @@ void App::init(const AppParams& params) {
gfxDevice.init(window, params.appName, false); gfxDevice.init(window, params.appName, false);
InputManager::GetInstance().Init(); InputManager::GetInstance().Init();
Time::GetInstance().Update();
customInit(); customInit();
} }
void App::run() { void App::run() {
using clock = std::chrono::steady_clock; Time::GetInstance().Update(); // initialize delta timing
const float targetHz = 60.0f; const float fixedDt = static_cast<float>(Time::GetInstance().FixedDeltaTime());
const float dt = 1.0f / targetHz;
const float maxFrameTime = 0.25f; // clamp big hitches
const int maxSteps = 5; // prevent spiral of death const int maxSteps = 5; // prevent spiral of death
auto prevTime = clock::now();
float accumulator = 0.0f; float accumulator = 0.0f;
isRunning = true; isRunning = true;
while (isRunning) { while (isRunning) {
const auto frameStart = clock::now(); // ---- Update timing ---
float frameTimeSec = std::chrono::duration<float>(frameStart - prevTime).count(); Time::GetInstance().Update();
prevTime = frameStart; float dt = static_cast<float>(Time::GetInstance().DeltaTime());
if (frameTimeSec > 0.07f && frameTimeSec < 5.f) { if (dt > 0.25f) dt = 0.25f;
spdlog::warn("Frame drop detected, time: {:.4f}s", frameTimeSec);
}
if (frameTimeSec > maxFrameTime) frameTimeSec = maxFrameTime; if (dt > 0.0f) {
if (frameTimeSec < 0.0f) frameTimeSec = 0.0f; float newFPS = 1.0f / dt;
accumulator += frameTimeSec;
if (frameTimeSec > 0.0f) {
const float newFPS = 1.0f / frameTimeSec;
avgFPS = std::lerp(avgFPS, newFPS, 0.1f); avgFPS = std::lerp(avgFPS, newFPS, 0.1f);
} }
accumulator += dt;
InputManager::GetInstance().BeginFrame(); InputManager::GetInstance().BeginFrame();
camera.Update(dt); camera.Update(dt);
@@ -92,63 +85,43 @@ void App::run() {
} }
} }
if (!isRunning) break; if (!isRunning) break;
customUpdate(dt); customUpdate(dt);
// ---- Swapchain resize check once per frame ---- int steps = 0;
while (accumulator >= fixedDt && steps < maxSteps) {
// physics.Update(fixedDt);
SDL_SetWindowTitle(
window,
fmt::format("{} - FPS: {:.2f}", m_params.windowTitle, avgFPS).c_str()
);
accumulator -= fixedDt;
steps++;
}
if (steps == maxSteps) accumulator = 0.0f;
const float alpha = accumulator / fixedDt;
if (!gfxDevice.needsSwapchainRecreate()) {
customDraw();
}
if (gfxDevice.needsSwapchainRecreate()) { if (gfxDevice.needsSwapchainRecreate()) {
spdlog::info("Recreating swapchain to size: {}x{}", m_params.windowSize.x, m_params.windowSize.y); spdlog::info("Recreating swapchain to size: {}x{}", m_params.windowSize.x, m_params.windowSize.y);
gfxDevice.recreateSwapchain(m_params.windowSize.x, m_params.windowSize.y); gfxDevice.recreateSwapchain(m_params.windowSize.x, m_params.windowSize.y);
onWindowResize(m_params.windowSize.x, m_params.windowSize.y); onWindowResize(m_params.windowSize.x, m_params.windowSize.y);
} }
// ---- Fixed updates (no event polling inside) ----
int steps = 0;
while (accumulator >= dt && steps < maxSteps) {
//Set window title to fps
SDL_SetWindowTitle(
window,
fmt::format("{} - FPS: {:.2f}", m_params.windowTitle, avgFPS).c_str());
accumulator -= dt;
steps++;
}
// If we hit the step cap, drop leftover time to recover smoothly
if (steps == maxSteps) accumulator = 0.0f;
// Optional interpolation factor for rendering
const float alpha = accumulator / dt;
// ---- Render ----
if (!gfxDevice.needsSwapchainRecreate()) {
// glm::mat4 objMatrix = glm::translate(glm::mat4(1.f), glm::vec3(0.f, -3.0f, 0.f));
//
// renderer.beginDrawing(gfxDevice);
// renderer.drawMesh(testMeshID, glm::mat4(1.f), testMaterialID);
// renderer.drawMesh(testMeshID, objMatrix, 0);
// renderer.endDrawing();
//
// const auto cmd = gfxDevice.beginFrame();
// const auto& drawImage = renderer.getDrawImage(gfxDevice);
//
// renderer.draw(cmd, gfxDevice, camera, GameRenderer::SceneData{
// camera, glm::vec3(0.1f), 0.5f, glm::vec3(0.5f), 0.01f
// });
//
// gfxDevice.endFrame(cmd, drawImage, {
// .clearColor = {{0.f, 0.f, 0.5f, 1.f}},
// .drawImageBlitRect = glm::ivec4{}
// });
customDraw();
}
// ---- Frame cap (if you still want it) ----
if (frameLimit) { if (frameLimit) {
const auto targetEnd = frameStart + std::chrono::duration_cast<clock::duration>( auto sleepTime = Time::GetInstance().SleepDuration();
std::chrono::duration<float>(dt) if (sleepTime.count() > 0) {
); std::this_thread::sleep_for(sleepTime);
std::this_thread::sleep_until(targetEnd); }
}
}
}
}
gfxDevice.waitIdle(); gfxDevice.waitIdle();
} }

View File

@@ -4,21 +4,17 @@
#include <glm/gtc/matrix_transform.hpp> #include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/quaternion.hpp> #include <glm/gtc/quaternion.hpp>
#include <destrum/Util/DeltaTime.h>
#include "spdlog/spdlog.h" #include "spdlog/spdlog.h"
Animator::Animator(GameObject& parent) Animator::Animator(GameObject& parent)
: Component(parent, "Animator") {} : Component(parent, "Animator") {}
// ─── Component interface ──────────────────────────────────────────────────────
void Animator::Update() { void Animator::Update() {
if (!m_current.clip) return; if (!m_current.clip) return;
// Time delta comes from your engine's time system const float dt = Time::GetInstance().DeltaTime();
// const float dt = Time::GetDeltaTime();
const float dt = 0.016f; // ~60 FPS
m_current.time += dt * m_current.speed; m_current.time += dt * m_current.speed;
if (m_current.clip->looped) if (m_current.clip->looped)
@@ -37,11 +33,6 @@ void Animator::Update() {
m_previous = {}; m_previous = {};
} }
} }
spdlog::info("Playing '{}': time = {:.2f}s, blend = {:.2f}",
m_currentClipName.empty() ? "(none)" : m_currentClipName.c_str(),
m_current.time,
m_blendT);
} }
void Animator::ImGuiInspector() { void Animator::ImGuiInspector() {
@@ -65,8 +56,6 @@ void Animator::ImGuiInspector() {
// } // }
} }
// ─── Animation control ────────────────────────────────────────────────────────
void Animator::addClip(std::shared_ptr<SkeletalAnimation> clip) { void Animator::addClip(std::shared_ptr<SkeletalAnimation> clip) {
m_clips[clip->name] = std::move(clip); m_clips[clip->name] = std::move(clip);
} }
@@ -94,17 +83,11 @@ void Animator::stop() {
m_currentClipName = {}; m_currentClipName = {};
} }
// ─── Joint matrix upload ────────────────────────────────────────────────────── std::size_t Animator::uploadJointMatrices(const RenderContext& ctx, const Skeleton& skeleton, std::size_t frameIndex) {
std::size_t Animator::uploadJointMatrices(SkinningPipeline& pipeline,
const Skeleton& skeleton,
std::size_t frameIndex) {
auto matrices = computeJointMatrices(skeleton); auto matrices = computeJointMatrices(skeleton);
return pipeline.appendJointMatrices(matrices, frameIndex); return ctx.renderer.getSkinningPipeline().appendJointMatrices(matrices, frameIndex);
} }
// ─── Private: pose evaluation ─────────────────────────────────────────────────
std::vector<glm::mat4> Animator::computeJointMatrices(const Skeleton& skeleton) { std::vector<glm::mat4> Animator::computeJointMatrices(const Skeleton& skeleton) {
const std::size_t numJoints = skeleton.joints.size(); const std::size_t numJoints = skeleton.joints.size();
std::vector<glm::mat4> global(numJoints, glm::mat4{1.f}); std::vector<glm::mat4> global(numJoints, glm::mat4{1.f});
@@ -123,6 +106,8 @@ std::vector<glm::mat4> Animator::computeJointMatrices(const Skeleton& skeleton)
glm::quat rot = glm::identity<glm::quat>(); glm::quat rot = glm::identity<glm::quat>();
glm::vec3 sc = {1.f, 1.f, 1.f}; glm::vec3 sc = {1.f, 1.f, 1.f};
// Sample current clip if one is playing
if (m_current.clip) {
if (const auto* track = findTrack(m_current.clip, i)) { if (const auto* track = findTrack(m_current.clip, i)) {
tr = sampleTranslation(*track, m_current.time); tr = sampleTranslation(*track, m_current.time);
rot = sampleRotation (*track, m_current.time); rot = sampleRotation (*track, m_current.time);
@@ -136,13 +121,16 @@ std::vector<glm::mat4> Animator::computeJointMatrices(const Skeleton& skeleton)
sc = glm::mix (sampleScale (*prev, m_previous.time), sc, m_blendT); sc = glm::mix (sampleScale (*prev, m_previous.time), sc, m_blendT);
} }
} }
}
// If no clip: tr/rot/sc stay as identity → rest pose
glm::mat4 local = glm::translate(glm::mat4{1.f}, tr) glm::mat4 local = glm::translate(glm::mat4{1.f}, tr)
* glm::mat4_cast(rot) * glm::mat4_cast(rot)
* glm::scale(glm::mat4{1.f}, sc); * glm::scale(glm::mat4{1.f}, sc);
const int parent = skeleton.parentIndex[i]; const int parent = skeleton.parentIndex[i];
global[i] = (parent < 0) ? local : global[parent] * local; global[i] = (parent < 0) ? skeleton.rootPreTransform * local
: global[parent] * local;
result[i] = global[i] * skeleton.inverseBindMatrices[i]; result[i] = global[i] * skeleton.inverseBindMatrices[i];
} }

View File

@@ -38,8 +38,8 @@ void MeshRendererComponent::Render(const RenderContext& ctx) {
std::uint32_t frameIdx = GameState::GetInstance().Gfx().getCurrentFrameIndex(); std::uint32_t frameIdx = GameState::GetInstance().Gfx().getCurrentFrameIndex();
const std::size_t jointMatricesStartIndex = animator->uploadJointMatrices( const std::size_t jointMatricesStartIndex = animator->uploadJointMatrices(
ctx.renderer.getSkinningPipeline(), ctx,
*skeleton, // skeleton stored on GPUMesh (or pass from CPUMesh) *skeleton,
frameIdx frameIdx
); );

View File

@@ -10,15 +10,13 @@
Camera::Camera(const glm::vec3& position, const glm::vec3& up) Camera::Camera(const glm::vec3& position, const glm::vec3& up)
: m_position{position}, m_up{up} { : m_position{position}, m_up{up} {
// Initialize yaw to -90 degrees so the camera faces -Z by default
m_yaw = -glm::half_pi<float>(); m_yaw = -glm::half_pi<float>();
m_pitch = 0.0f; m_pitch = 0.0f;
} }
void Camera::Update(float deltaTime) { void Camera::Update(float deltaTime) {
auto& input = InputManager::GetInstance(); const auto& input = InputManager::GetInstance();
// --- tuning ---
float moveSpeed = m_movementSpeed; float moveSpeed = m_movementSpeed;
if (input.IsKeyDown(SDL_SCANCODE_LSHIFT) || input.IsKeyDown(SDL_SCANCODE_RSHIFT)) { if (input.IsKeyDown(SDL_SCANCODE_LSHIFT) || input.IsKeyDown(SDL_SCANCODE_RSHIFT)) {
moveSpeed *= 2.0f; moveSpeed *= 2.0f;
@@ -29,10 +27,7 @@ void Camera::Update(float deltaTime) {
moveSpeed *= 3.0f; moveSpeed *= 3.0f;
} }
// ========================= constexpr float keyLookSpeed = glm::radians(120.0f);
// Look Input (Keyboard & Controller)
// =========================
const float keyLookSpeed = glm::radians(120.0f);
if (input.IsKeyDown(SDL_SCANCODE_UP)) m_pitch += keyLookSpeed * deltaTime; if (input.IsKeyDown(SDL_SCANCODE_UP)) m_pitch += keyLookSpeed * deltaTime;
if (input.IsKeyDown(SDL_SCANCODE_DOWN)) m_pitch -= keyLookSpeed * deltaTime; if (input.IsKeyDown(SDL_SCANCODE_DOWN)) m_pitch -= keyLookSpeed * deltaTime;
if (input.IsKeyDown(SDL_SCANCODE_LEFT)) m_yaw -= keyLookSpeed * deltaTime; if (input.IsKeyDown(SDL_SCANCODE_LEFT)) m_yaw -= keyLookSpeed * deltaTime;
@@ -46,13 +41,8 @@ void Camera::Update(float deltaTime) {
m_pitch -= ry * padLookSpeed * deltaTime; // Inverted to match stick convention m_pitch -= ry * padLookSpeed * deltaTime; // Inverted to match stick convention
} }
// Clamp pitch to prevent flipping over the top
m_pitch = glm::clamp(m_pitch, -glm::half_pi<float>() + 0.01f, glm::half_pi<float>() - 0.01f); m_pitch = glm::clamp(m_pitch, -glm::half_pi<float>() + 0.01f, glm::half_pi<float>() - 0.01f);
// =========================
// Update Basis Vectors
// =========================
// Standard Spherical to Cartesian coordinates (Y-Up, Right-Handed)
glm::vec3 front; glm::vec3 front;
front.x = cos(m_yaw) * cos(m_pitch); front.x = cos(m_yaw) * cos(m_pitch);
front.y = sin(m_pitch); front.y = sin(m_pitch);
@@ -62,9 +52,6 @@ void Camera::Update(float deltaTime) {
m_right = glm::normalize(glm::cross(m_forward, glm::vec3(0, 1, 0))); // World Up m_right = glm::normalize(glm::cross(m_forward, glm::vec3(0, 1, 0))); // World Up
m_up = glm::normalize(glm::cross(m_right, m_forward)); m_up = glm::normalize(glm::cross(m_right, m_forward));
// =========================
// Movement Input
// =========================
glm::vec3 move(0.0f); glm::vec3 move(0.0f);
if (input.IsKeyDown(SDL_SCANCODE_W)) move += m_forward; if (input.IsKeyDown(SDL_SCANCODE_W)) move += m_forward;
if (input.IsKeyDown(SDL_SCANCODE_S)) move -= m_forward; if (input.IsKeyDown(SDL_SCANCODE_S)) move -= m_forward;
@@ -105,18 +92,11 @@ void Camera::CalculateViewMatrix() {
} }
void Camera::CalculateProjectionMatrix() { void Camera::CalculateProjectionMatrix() {
// RH_ZO: Right-Handed, Zero-to-One depth (Vulkan/D3D standard)
m_projectionMatrix = glm::perspectiveRH_ZO(glm::radians(fovAngle), m_aspectRatio, m_zNear, m_zFar); m_projectionMatrix = glm::perspectiveRH_ZO(glm::radians(fovAngle), m_aspectRatio, m_zNear, m_zFar);
// CRITICAL VULKAN FIX: Flip Y-axis
// This keeps the world upright and fixes winding order issues
m_projectionMatrix[1][1] *= -1; m_projectionMatrix[1][1] *= -1;
} }
// ---------------------------------------------------------
// Helpers to keep orientation consistent
// ---------------------------------------------------------
void Camera::SetRotation(float yawRadians, float pitchRadians) { void Camera::SetRotation(float yawRadians, float pitchRadians) {
m_yaw = yawRadians; m_yaw = yawRadians;
m_pitch = glm::clamp(pitchRadians, -glm::half_pi<float>() + 0.001f, glm::half_pi<float>() - 0.001f); m_pitch = glm::clamp(pitchRadians, -glm::half_pi<float>() + 0.001f, glm::half_pi<float>() - 0.001f);

View File

@@ -1,7 +1,10 @@
#include <destrum/Graphics/Pipelines/SkyboxPipeline.h> #include <destrum/Graphics/Pipelines/SkyboxPipeline.h>
#include <destrum/FS/AssetFS.h> #include <destrum/FS/AssetFS.h>
#include <glm/glm.hpp>
#include <destrum/Util/DeltaTime.h>
#include "glm/ext/matrix_transform.hpp"
#include "spdlog/spdlog.h" #include "spdlog/spdlog.h"
SkyboxPipeline::SkyboxPipeline(): pipelineLayout{nullptr} { SkyboxPipeline::SkyboxPipeline(): pipelineLayout{nullptr} {
@@ -72,12 +75,19 @@ void SkyboxPipeline::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camer
return; return;
} }
//rotate skybox slowly for visual interest
const float rotationSpeed = 0.01f; // radians per second
//Rotate over the Y axis
skyboxRotation = glm::rotate(skyboxRotation, rotationSpeed * static_cast<float>(Time::GetInstance().DeltaTime()), glm::vec3(0.f, 1.f, 0.f));
pipeline->bind(cmd); pipeline->bind(cmd);
gfxDevice.bindBindlessDescSet(cmd, pipelineLayout); gfxDevice.bindBindlessDescSet(cmd, pipelineLayout);
const glm::mat3 r = glm::mat3(skyboxRotation);
const auto pcs = SkyboxPushConstants{ const auto pcs = SkyboxPushConstants{
.invViewProj = glm::inverse(camera.GetViewProjectionMatrix()), .invViewProj = glm::inverse(camera.GetViewProjectionMatrix()),
.cameraPos = glm::vec4{camera.GetPosition(), 1.f}, .skyboxRot = { glm::vec4(r[0], 0.f), glm::vec4(r[1], 0.f), glm::vec4(r[2], 0.f) },
.cameraPos = camera.GetPosition(),
.skyboxTextureId = static_cast<std::uint32_t>(skyboxTextureId), .skyboxTextureId = static_cast<std::uint32_t>(skyboxTextureId),
}; };
vkCmdPushConstants(cmd, pipelineLayout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(SkyboxPushConstants), &pcs); vkCmdPushConstants(cmd, pipelineLayout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(SkyboxPushConstants), &pcs);

View File

@@ -5,18 +5,16 @@
#include <destrum/Graphics/GfxDevice.h> #include <destrum/Graphics/GfxDevice.h>
#include <destrum/Graphics/Init.h> #include <destrum/Graphics/Init.h>
#include <vulkan/vulkan.h>
#include <vulkan/vk_enum_string_helper.h> #include <vulkan/vk_enum_string_helper.h>
#include "volk.h" #include "volk.h"
void Swapchain::initSync(VkDevice device) void Swapchain::initSync(VkDevice device) {
{ constexpr auto fenceCreateInfo = VkFenceCreateInfo{
const auto fenceCreateInfo = VkFenceCreateInfo{
.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
.flags = VK_FENCE_CREATE_SIGNALED_BIT, .flags = VK_FENCE_CREATE_SIGNALED_BIT,
}; };
const auto semaphoreCreateInfo = VkSemaphoreCreateInfo{ constexpr auto semaphoreCreateInfo = VkSemaphoreCreateInfo{
.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO,
}; };
for (std::uint32_t i = 0; i < FRAMES_IN_FLIGHT; ++i) { for (std::uint32_t i = 0; i < FRAMES_IN_FLIGHT; ++i) {
@@ -57,7 +55,7 @@ void Swapchain::createSwapchain(GfxDevice* gfxDevice, VkFormat format, std::uint
.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO
}; };
for (auto& sem : imageRenderSemaphores) { for (auto& sem: imageRenderSemaphores) {
VK_CHECK(vkCreateSemaphore(m_gfxDevice->getDevice(), &sci, nullptr, &sem)); VK_CHECK(vkCreateSemaphore(m_gfxDevice->getDevice(), &sci, nullptr, &sem));
} }

View File

@@ -0,0 +1,24 @@
#include <destrum/Util/DeltaTime.h>
#include <iostream>
double Time::FixedDeltaTime() const {
return m_FixedDeltaTime;
}
double Time::DeltaTime() const {
return m_DeltaTime;
}
std::chrono::nanoseconds Time::SleepDuration() const {
constexpr auto msPerFrame = std::chrono::milliseconds(static_cast<int>(1000.f / FPS));
const std::chrono::nanoseconds sleepTime = (m_PrevTime + msPerFrame - std::chrono::high_resolution_clock::now());
return sleepTime;
}
void Time::Update() {
const auto currentTime = std::chrono::high_resolution_clock::now();
m_DeltaTime = std::chrono::duration<double>(currentTime - m_PrevTime).count();
m_PrevTime = currentTime;
}

View File

@@ -44,10 +44,11 @@ void LightKeeper::customInit() {
auto& scene = SceneManager::GetInstance().CreateScene("Main"); auto& scene = SceneManager::GetInstance().CreateScene("Main");
auto testCube = std::make_shared<GameObject>("TestCube"); // auto testCube = std::make_shared<GameObject>("TestCube");
auto meshComp = testCube->AddComponent<MeshRendererComponent>(); // auto meshComp = testCube->AddComponent<MeshRendererComponent>();
meshComp->SetMeshID(testMeshID); // meshComp->SetMeshID(testMeshID);
meshComp->SetMaterialID(testMaterialID);const int count = 100; // meshComp->SetMaterialID(testMaterialID);
const int count = 100;
const float radius = 5.0f; const float radius = 5.0f;
const float orbitRadius = 5.0f; const float orbitRadius = 5.0f;
@@ -67,9 +68,9 @@ void LightKeeper::customInit() {
// //
// scene.Add(childCube); // scene.Add(childCube);
} }
testCube->AddComponent<Spinner>(glm::vec3(0, 1, 0), glm::radians(10.0f)); // spin around Y, rad/sec // testCube->AddComponent<Spinner>(glm::vec3(0, 1, 0), glm::radians(10.0f)); // spin around Y, rad/sec
//rotate 180 around X axis //rotate 180 around X axis
testCube->GetTransform().SetLocalRotation(glm::quat(glm::vec3(glm::radians(180.0f), 0.0f, 0.0f))); // testCube->GetTransform().SetLocalRotation(glm::quat(glm::vec3(glm::radians(180.0f), 0.0f, 0.0f)));
// //
auto globeRoot = std::make_shared<GameObject>("GlobeRoot"); auto globeRoot = std::make_shared<GameObject>("GlobeRoot");
globeRoot->GetTransform().SetWorldPosition(glm::vec3(0.0f)); globeRoot->GetTransform().SetWorldPosition(glm::vec3(0.0f));
@@ -78,7 +79,7 @@ void LightKeeper::customInit() {
scene.Add(testCube); // scene.Add(testCube);
// const auto skyboxID = AssetFS::GetInstance().GetFullPath("engine://textures/skybox.jpg"); // const auto skyboxID = AssetFS::GetInstance().GetFullPath("engine://textures/skybox.jpg");
// const auto skyboxID = AssetFS::GetInstance().GetFullPath("engine://textures/mars.jpg"); // const auto skyboxID = AssetFS::GetInstance().GetFullPath("engine://textures/mars.jpg");
@@ -96,7 +97,7 @@ void LightKeeper::customInit() {
renderer.setSkyboxTexture(skyboxCubemap->GetCubeMapImageID()); renderer.setSkyboxTexture(skyboxCubemap->GetCubeMapImageID());
//
const auto planeObj = std::make_shared<GameObject>("GroundPlane"); const auto planeObj = std::make_shared<GameObject>("GroundPlane");
const auto planeMeshComp = planeObj->AddComponent<MeshRendererComponent>(); const auto planeMeshComp = planeObj->AddComponent<MeshRendererComponent>();
const auto planeModel = ModelLoader::LoadGLTF_CPUMeshes_MergedPerMesh(AssetFS::GetInstance().GetFullPath("game://plane.glb").generic_string()); const auto planeModel = ModelLoader::LoadGLTF_CPUMeshes_MergedPerMesh(AssetFS::GetInstance().GetFullPath("game://plane.glb").generic_string());
@@ -121,13 +122,12 @@ void LightKeeper::customInit() {
const auto CharObj = std::make_shared<GameObject>("Character"); const auto CharObj = std::make_shared<GameObject>("Character");
auto charModel = ModelLoader::LoadSkinnedModel( auto charModel = ModelLoader::LoadSkinnedModel(
AssetFS::GetInstance().GetFullPath("engine://char.fbx").generic_string() AssetFS::GetInstance().GetFullPath("engine://char2.fbx").generic_string()
); );
const auto charMeshID = meshCache.addMesh(gfxDevice, charModel.meshes[0]); const auto charMeshID = meshCache.addMesh(gfxDevice, charModel.meshes[0]);
const auto charTextureID = gfxDevice.loadImageFromFile( const auto charTextureID = gfxDevice.loadImageFromFile(AssetFS::GetInstance().GetFullPath("engine://char.jpg"));
AssetFS::GetInstance().GetFullPath("engine://char.jpg"));
const auto charMaterialID = materialCache.addMaterial(gfxDevice, { const auto charMaterialID = materialCache.addMaterial(gfxDevice, {
.baseColor = glm::vec3(1.f), .baseColor = glm::vec3(1.f),
.diffuseTexture = charTextureID, .diffuseTexture = charTextureID,
@@ -149,13 +149,13 @@ void LightKeeper::customInit() {
spdlog::info("Loaded animation: '{}' ({:.2f}s)", clip.name, clip.duration); spdlog::info("Loaded animation: '{}' ({:.2f}s)", clip.name, clip.duration);
if (!charModel.animations.empty()) if (!charModel.animations.empty())
// animator->play(charModel.animations[0].name); // animator->play("Armature|Armature|mixamo.com");
// animator->play("Armature|main"); // // animator->play(charModel.animations[0].name);
animator->play("Armature|mixamo.com"); animator->play("Armature|Armature|Armature|main");
// or: animator->play("Run", 0.2f); // 0.2s cross-fade // or: animator->play("Run", 0.2f); // 0.2s cross-fade
CharObj->GetTransform().SetWorldPosition(glm::vec3(0.f, 0.f, 0.f)); CharObj->GetTransform().SetWorldPosition(glm::vec3(0.f, 0.f, 0.f));
CharObj->GetTransform().SetWorldScale(0.01f, 0.01f, 0.01f); // CharObj->GetTransform().SetWorldScale(0.01f, 0.01f, 0.01f);
scene.Add(CharObj); scene.Add(CharObj);
} }