Compare commits

6 Commits
52 changed files with 2546 additions and 333 deletions
+1
View File
@@ -44,6 +44,7 @@
[submodule "destrum/third_party/imgui"] [submodule "destrum/third_party/imgui"]
path = destrum/third_party/imgui path = destrum/third_party/imgui
url = https://github.com/ocornut/imgui.git url = https://github.com/ocornut/imgui.git
branch = docking
[submodule "destrum/third_party/jolt"] [submodule "destrum/third_party/jolt"]
path = destrum/third_party/jolt path = destrum/third_party/jolt
url = https://github.com/jrouwe/JoltPhysics.git url = https://github.com/jrouwe/JoltPhysics.git
+1 -1
View File
@@ -119,7 +119,7 @@ cmake --install build --config Release
## Demo app ## Demo app
`lightkeeper` is the current game application. It launches **Star Catcher**, a small playable loop built from the engine's scene and rendering systems. Move the blue catcher with `A`/`D` or the arrow keys, catch orange stars, and press `R` to restart after three misses. Press `Escape` to quit. `lightkeeper` is the current demo application. It creates an SDL window, initializes the Destrum app, loads assets, sets up a scene, renders meshes, skyboxes, and a skinned animated character.
## Development notes ## Development notes
+1 -1
Submodule TheChef updated: 14f7dd9423...38230f5092
+4
View File
@@ -37,12 +37,16 @@ set(SRC_FILES
"src/Graphics/Managers/MemoryManager.cpp" "src/Graphics/Managers/MemoryManager.cpp"
"src/Graphics/Managers/FrameManager.cpp" "src/Graphics/Managers/FrameManager.cpp"
"src/Graphics/Managers/ImageManager.cpp" "src/Graphics/Managers/ImageManager.cpp"
"src/Graphics/Managers/LineRenderingManager.cpp"
"src/Graphics/Managers/QuadRenderingManager.cpp"
"src/Graphics/Resources/GPUImage.cpp" "src/Graphics/Resources/GPUImage.cpp"
"src/Graphics/Resources/NBuffer.cpp" "src/Graphics/Resources/NBuffer.cpp"
"src/Graphics/Resources/Cubemap.cpp" "src/Graphics/Resources/Cubemap.cpp"
"src/Graphics/Pipelines/MeshPipeline.cpp" "src/Graphics/Pipelines/MeshPipeline.cpp"
"src/Graphics/Pipelines/LineRenderingPass.cpp"
"src/Graphics/Pipelines/QuadRendererPass.cpp"
"src/Graphics/Pipelines/SkyboxPipeline.cpp" "src/Graphics/Pipelines/SkyboxPipeline.cpp"
"src/Graphics/Pipelines/SkinningPipeline.cpp" "src/Graphics/Pipelines/SkinningPipeline.cpp"
"src/Graphics/Pipelines/ImguiPass.cpp" "src/Graphics/Pipelines/ImguiPass.cpp"
+9
View File
@@ -0,0 +1,9 @@
#version 460
layout (location = 0) in vec4 inColor;
layout (location = 0) out vec4 outFragColor;
void main()
{
outFragColor = inColor;
}
+16
View File
@@ -0,0 +1,16 @@
#version 460
layout (location = 0) in vec4 inPosition;
layout (location = 1) in vec4 inColor;
layout (location = 0) out vec4 outColor;
layout (push_constant) uniform LinePushConstants {
mat4 viewProjection;
} pcs;
void main()
{
gl_Position = pcs.viewProjection * inPosition;
outColor = inColor;
}
+16
View File
@@ -0,0 +1,16 @@
#version 460
#extension GL_GOOGLE_include_directive : require
#extension GL_EXT_nonuniform_qualifier : enable
#include "bindless.glsl"
layout(location = 0) in vec2 inUV;
layout(location = 1) in vec4 inColor;
layout(location = 2) flat in uint inTexId;
layout(location = 0) out vec4 outFragColor;
void main() {
vec4 texColor = sampleTexture2DLinear(inTexId, inUV);
outFragColor = texColor * inColor;
}
+22
View File
@@ -0,0 +1,22 @@
#version 460
#extension GL_GOOGLE_include_directive : require
layout(location = 0) in vec2 inPosition;
layout(location = 1) in vec2 inUV;
layout(location = 2) in vec4 inColor;
layout(location = 3) in uint inTexId;
layout(location = 0) out vec2 outUV;
layout(location = 1) out vec4 outColor;
layout(location = 2) flat out uint outTexId;
layout(push_constant) uniform QuadPushConstants {
mat4 viewProjection;
} pcs;
void main() {
gl_Position = pcs.viewProjection * vec4(inPosition, 0.0, 1.0);
outUV = inUV;
outColor = inColor;
outTexId = inTexId;
}
@@ -0,0 +1,37 @@
#ifndef DESTRUM_LINERENDERINGMANAGER_H
#define DESTRUM_LINERENDERINGMANAGER_H
#include <vector>
#include <glm/vec3.hpp>
#include <glm/vec4.hpp>
#include <destrum/Singleton.h>
struct Line {
glm::vec3 start{};
glm::vec3 end{};
glm::vec4 color{1.0f};
};
class LineRenderingManager final : public Singleton<LineRenderingManager> {
public:
friend class Singleton<LineRenderingManager>;
void SubmitLine(const Line& line);
void SubmitLine(
const glm::vec3& start,
const glm::vec3& end,
const glm::vec4& color = glm::vec4{1.0f});
[[nodiscard]] const std::vector<Line>& GetLines() const { return lines; }
void ClearLines();
private:
LineRenderingManager() = default;
std::vector<Line> lines;
};
#endif // DESTRUM_LINERENDERINGMANAGER_H
@@ -0,0 +1,45 @@
#ifndef DESTRUM_QUADRENDERINGMANAGER_H
#define DESTRUM_QUADRENDERINGMANAGER_H
#include <vector>
#include <glm/glm.hpp>
#include <destrum/Graphics/ids.h>
#include <destrum/Singleton.h>
struct QuadSubmission {
glm::vec2 position;
glm::vec2 size;
float rotation;
glm::vec4 color;
ImageID textureId;
};
class QuadRenderingManager final : public Singleton<QuadRenderingManager> {
public:
friend class Singleton<QuadRenderingManager>;
void SubmitQuad(
const glm::vec2& position,
const glm::vec2& size,
const glm::vec4& color = glm::vec4{1.0f},
ImageID textureId = NULL_IMAGE_ID);
void SubmitQuad(
const glm::vec2& position,
const glm::vec2& size,
float rotation,
const glm::vec4& color = glm::vec4{1.0f},
ImageID textureId = NULL_IMAGE_ID);
[[nodiscard]] const std::vector<QuadSubmission>& GetQuads() const { return quads; }
void ClearQuads();
private:
QuadRenderingManager() = default;
std::vector<QuadSubmission> quads;
};
#endif // DESTRUM_QUADRENDERINGMANAGER_H
@@ -0,0 +1,42 @@
#ifndef DESTRUM_LINERENDERINGPASS_H
#define DESTRUM_LINERENDERINGPASS_H
#include <cstddef>
#include <memory>
#include <vulkan/vulkan.h>
#include <destrum/Graphics/Managers/LineRenderingManager.h>
#include <destrum/Graphics/Resources/NBuffer.h>
class Camera;
class GfxDevice;
struct GPUImage;
class Pipeline;
class LineRenderingPass final {
public:
void init(GfxDevice& gfxDevice, VkFormat drawImageFormat);
void draw(
VkCommandBuffer cmd,
GfxDevice& gfxDevice,
const GPUImage& target,
const Camera& camera,
LineRenderingManager& lineManager);
void cleanup(GfxDevice& gfxDevice);
private:
struct LineVertex {
glm::vec4 position;
glm::vec4 color;
};
void ensureVertexCapacity(GfxDevice& gfxDevice, std::size_t requiredVertices);
VkPipelineLayout pipelineLayout{VK_NULL_HANDLE};
std::unique_ptr<Pipeline> pipeline;
NBuffer vertexBuffer;
std::size_t vertexCapacity{0};
};
#endif // DESTRUM_LINERENDERINGPASS_H
@@ -0,0 +1,47 @@
#ifndef DESTRUM_QUADRENDERERPASS_H
#define DESTRUM_QUADRENDERERPASS_H
#include <cstddef>
#include <memory>
#include <glm/glm.hpp>
#include <vulkan/vulkan.h>
#include <destrum/Graphics/Resources/Buffer.h>
class GfxDevice;
class Pipeline;
struct GPUImage;
class RenderResources;
class QuadRendererPass final {
public:
void init(GfxDevice& gfxDevice, RenderResources& resources, VkFormat drawImageFormat);
void draw(
VkCommandBuffer cmd,
GfxDevice& gfxDevice,
RenderResources& resources,
const GPUImage& target,
const glm::mat4& projection);
void cleanup(GfxDevice& gfxDevice);
private:
struct QuadVertex {
glm::vec2 position;
glm::vec2 uv;
glm::vec4 color;
std::uint32_t textureId;
};
void ensureCapacity(GfxDevice& gfxDevice, std::size_t requiredQuads);
VkPipelineLayout pipelineLayout{VK_NULL_HANDLE};
std::unique_ptr<Pipeline> pipeline;
GPUBuffer vertexBuffer{};
GPUBuffer indexBuffer{};
std::size_t quadCapacity{0};
};
#endif // DESTRUM_QUADRENDERERPASS_H
@@ -1,10 +1,13 @@
#ifndef RENDERER_H #ifndef RENDERER_H
#define RENDERER_H #define RENDERER_H
#include <glm/mat4x4.hpp>
#include <glm/vec3.hpp> #include <glm/vec3.hpp>
#include <destrum/Graphics/Camera.h> #include <destrum/Graphics/Camera.h>
#include <destrum/Graphics/Pipelines/LineRenderingPass.h>
#include <destrum/Graphics/Pipelines/MeshPipeline.h> #include <destrum/Graphics/Pipelines/MeshPipeline.h>
#include <destrum/Graphics/Pipelines/QuadRendererPass.h>
#include <destrum/Graphics/ids.h> #include <destrum/Graphics/ids.h>
#include <destrum/Graphics/MeshDrawCommand.h> #include <destrum/Graphics/MeshDrawCommand.h>
#include <destrum/Graphics/Resources/NBuffer.h> #include <destrum/Graphics/Resources/NBuffer.h>
@@ -68,6 +71,11 @@ public:
return resources; return resources;
} }
void drawQuads(
VkCommandBuffer cmd,
GfxDevice& gfxDevice,
const glm::mat4& projection);
private: private:
void createDrawImage(GfxDevice& gfxDevice, const glm::ivec2& drawImageSize, bool firstCreate); void createDrawImage(GfxDevice& gfxDevice, const glm::ivec2& drawImageSize, bool firstCreate);
@@ -111,6 +119,8 @@ private:
std::unique_ptr<MeshPipeline> meshPipeline; std::unique_ptr<MeshPipeline> meshPipeline;
std::unique_ptr<SkyboxPipeline> skyboxPipeline; std::unique_ptr<SkyboxPipeline> skyboxPipeline;
std::unique_ptr<LineRenderingPass> lineRenderingPass;
std::unique_ptr<QuadRendererPass> quadRendererPass;
std::unique_ptr<SkinningPipeline> skinningPipeline; std::unique_ptr<SkinningPipeline> skinningPipeline;
bool initialized{false}; bool initialized{false};
+9 -5
View File
@@ -30,8 +30,8 @@ public:
int MouseX() const { return m_mouseX; } int MouseX() const { return m_mouseX; }
int MouseY() const { return m_mouseY; } int MouseY() const { return m_mouseY; }
int MouseDeltaX() const { return m_mouseDX; } int MouseDeltaX() const { return m_mouseRelDX; }
int MouseDeltaY() const { return m_mouseDY; } int MouseDeltaY() const { return m_mouseRelDY; }
int WheelX() const { return m_wheelX; } int WheelX() const { return m_wheelX; }
int WheelY() const { return m_wheelY; } int WheelY() const { return m_wheelY; }
@@ -81,6 +81,9 @@ public:
void SetAxisDeadzone(int deadzone) { m_axisDeadzone = deadzone; } // 0..32767 void SetAxisDeadzone(int deadzone) { m_axisDeadzone = deadzone; } // 0..32767
void SetMouseCaptured(bool captured);
[[nodiscard]] bool IsMouseCaptured() const { return m_MouseCaptured; }
private: private:
// Key states // Key states
std::unordered_set<SDL_Scancode> m_keysDown; std::unordered_set<SDL_Scancode> m_keysDown;
@@ -92,10 +95,9 @@ private:
std::unordered_set<Uint8> m_mousePressed; std::unordered_set<Uint8> m_mousePressed;
std::unordered_set<Uint8> m_mouseReleased; std::unordered_set<Uint8> m_mouseReleased;
// Mouse position + delta // Mouse position + relative delta (accumulated from SDL xrel/yrel)
int m_mouseX = 0, m_mouseY = 0; int m_mouseX = 0, m_mouseY = 0;
int m_prevMouseX = 0, m_prevMouseY = 0; int m_mouseRelDX = 0, m_mouseRelDY = 0;
int m_mouseDX = 0, m_mouseDY = 0;
int m_wheelX = 0, m_wheelY = 0; int m_wheelX = 0, m_wheelY = 0;
@@ -125,6 +127,8 @@ private:
int m_axisDeadzone = 8000; // typical deadzone int m_axisDeadzone = 8000; // typical deadzone
bool m_MouseCaptured = false;
private: private:
bool QueryBinding(const Binding& b, ButtonState state) const; bool QueryBinding(const Binding& b, ButtonState state) const;
@@ -59,6 +59,16 @@ public:
virtual void ResolveReferences(const ObjectMap&) { virtual void ResolveReferences(const ObjectMap&) {
} }
// Trigger callbacks called when this component's owner overlaps a sensor.
// Only called when the owner has a Collider set as a trigger (or overlaps one).
virtual void OnTriggerEnter(GameObject* other) {
(void)other;
}
virtual void OnTriggerExit(GameObject* other) {
(void)other;
}
bool HasStarted{false}; bool HasStarted{false};
protected: protected:
@@ -55,6 +55,8 @@ public:
float maxDistance, float maxDistance,
PhysicsRaycastHit& hit) const override; PhysicsRaycastHit& hit) const override;
std::vector<TriggerEvent> ConsumeTriggerEvents() override;
private: private:
class Impl; class Impl;
std::unique_ptr<Impl> m_Impl; std::unique_ptr<Impl> m_Impl;
@@ -21,6 +21,7 @@ class PhysicsSceneBridge final {
public: public:
explicit PhysicsSceneBridge(std::unique_ptr<PhysicsWorld> world); explicit PhysicsSceneBridge(std::unique_ptr<PhysicsWorld> world);
[[nodiscard]] bool IsValid() const { return m_World != nullptr; }
[[nodiscard]] PhysicsWorld& GetWorld() { return *m_World; } [[nodiscard]] PhysicsWorld& GetWorld() { return *m_World; }
[[nodiscard]] const PhysicsWorld& GetWorld() const { return *m_World; } [[nodiscard]] const PhysicsWorld& GetWorld() const { return *m_World; }
@@ -116,3 +116,9 @@ struct PhysicsBodyDesc {
bool useGravity{true}; bool useGravity{true};
bool allowSleep{true}; bool allowSleep{true};
}; };
struct TriggerEvent {
GameObject* owner{nullptr};
GameObject* other{nullptr};
bool entered{true};
};
@@ -1,5 +1,7 @@
#pragma once #pragma once
#include <vector>
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include <unordered_set> #include <unordered_set>
@@ -49,6 +51,9 @@ public:
float maxDistance, float maxDistance,
PhysicsRaycastHit& hit) const = 0; PhysicsRaycastHit& hit) const = 0;
// Trigger events accumulated during the last Step().
virtual std::vector<TriggerEvent> ConsumeTriggerEvents() { return {}; }
private: private:
std::unordered_set<Rigidbody*> m_RegisteredRigidbodies; std::unordered_set<Rigidbody*> m_RegisteredRigidbodies;
}; };
+4
View File
@@ -3,6 +3,8 @@
#include <functional> #include <functional>
#include <memory>
#include <destrum/Event.h> #include <destrum/Event.h>
#include <destrum/ObjectModel/ObjectId.h> #include <destrum/ObjectModel/ObjectId.h>
#include <destrum/Scene/SceneManager.h> #include <destrum/Scene/SceneManager.h>
@@ -12,6 +14,7 @@
class GameObject; class GameObject;
class SceneSerializer; class SceneSerializer;
class PhysicsWorld;
class Scene final { class Scene final {
friend Scene& SceneManager::CreateScene(const std::string& name); friend Scene& SceneManager::CreateScene(const std::string& name);
@@ -104,6 +107,7 @@ public:
PhysicsSceneBridge& GetPhysics() { return m_Physics; } PhysicsSceneBridge& GetPhysics() { return m_Physics; }
private: private:
explicit Scene(const std::string& name); explicit Scene(const std::string& name);
explicit Scene(const std::string& name, std::unique_ptr<PhysicsWorld> physicsWorld);
PhysicsSceneBridge m_Physics{std::make_unique<JoltPhysicsWorld>()}; PhysicsSceneBridge m_Physics{std::make_unique<JoltPhysicsWorld>()};
+5 -3
View File
@@ -16,6 +16,7 @@ public:
Scene& CreateScene(const std::string& name); Scene& CreateScene(const std::string& name);
Scene& GetCurrentScene() const; Scene& GetCurrentScene() const;
const std::vector<std::shared_ptr<Scene>>& GetActiveScenes() const { return m_activeScenes; }
void Update(float dt); void Update(float dt);
void FixedUpdate(float dt); void FixedUpdate(float dt);
@@ -34,7 +35,9 @@ public:
void Destroy(); void Destroy();
void SwitchScene(int index); void SwitchScene(int index);
int GetActiveSceneId() const { return m_scenes.empty() ? -1 : m_ActiveSceneIndex; } void AddActiveScene(int index);
void RemoveActiveScene(int index);
int GetActiveSceneId() const;
[[nodiscard]] const std::vector<std::shared_ptr<Scene>>& GetScenes() const { return m_scenes; } [[nodiscard]] const std::vector<std::shared_ptr<Scene>>& GetScenes() const { return m_scenes; }
@@ -47,8 +50,7 @@ private:
SceneManager() = default; SceneManager() = default;
int m_ActiveSceneIndex{0}; std::vector<std::shared_ptr<Scene>> m_activeScenes;
std::vector<std::shared_ptr<Scene>> m_scenes; std::vector<std::shared_ptr<Scene>> m_scenes;
}; };
@@ -22,6 +22,6 @@
X(Rigidbody) \ X(Rigidbody) \
X(BoxCollider) \ X(BoxCollider) \
X(SphereCollider) \ X(SphereCollider) \
X(CapsuleCollider) X(CapsuleCollider) \
#endif // DESTRUM_ENGINECOMPONENTLIST_H #endif // DESTRUM_ENGINECOMPONENTLIST_H
@@ -2,13 +2,37 @@
#define DESTRUM_SCENESERIALIZER_H #define DESTRUM_SCENESERIALIZER_H
#include <filesystem> #include <filesystem>
#include <functional>
#include <future>
#include <nlohmann/json.hpp>
class Scene; class Scene;
class SceneSerializer final { class SceneSerializer final {
public: public:
struct LoadResult {
bool success{false};
std::string errorMessage;
nlohmann::json root;
};
static bool Save(Scene& scene, const std::filesystem::path& path); static bool Save(Scene& scene, const std::filesystem::path& path);
static bool Load(Scene& scene, const std::filesystem::path& path);
static bool Load(Scene& scene, const std::filesystem::path& path,
std::function<void(float)> progressCallback = nullptr);
// Thread-safe: parse and validate a scene file on any thread.
// No engine state is touched during parsing.
static LoadResult LoadSceneFile(const std::filesystem::path& path);
// Main-thread-only: construct a scene from pre-validated JSON.
static bool ConstructScene(Scene& scene, LoadResult& result,
std::function<void(float)> progressCallback = nullptr);
// Parse the scene file on a background thread. Call LoadFromJson on the
// main thread once the future is ready.
static std::future<LoadResult> LoadSceneFileAsync(const std::filesystem::path& path);
}; };
#endif //DESTRUM_SCENESERIALIZER_H #endif //DESTRUM_SCENESERIALIZER_H
+8 -8
View File
@@ -131,6 +131,12 @@ void App::run()
imguiPass.handleEvent(event); imguiPass.handleEvent(event);
const bool mouseEvent =
event.type == SDL_MOUSEBUTTONDOWN ||
event.type == SDL_MOUSEBUTTONUP ||
event.type == SDL_MOUSEMOTION ||
event.type == SDL_MOUSEWHEEL;
if (event.type == SDL_QUIT) if (event.type == SDL_QUIT)
{ {
isRunning = false; isRunning = false;
@@ -149,19 +155,13 @@ void App::run()
} }
} }
const bool mouseEvent =
event.type == SDL_MOUSEBUTTONDOWN ||
event.type == SDL_MOUSEBUTTONUP ||
event.type == SDL_MOUSEMOTION ||
event.type == SDL_MOUSEWHEEL;
const bool keyboardEvent = const bool keyboardEvent =
event.type == SDL_KEYDOWN || event.type == SDL_KEYDOWN ||
event.type == SDL_KEYUP || event.type == SDL_KEYUP ||
event.type == SDL_TEXTINPUT; event.type == SDL_TEXTINPUT;
const bool capturedByImgui = const bool capturedByImgui =
(mouseEvent && imguiPass.wantsMouse()) || (mouseEvent && !InputManager::GetInstance().IsMouseCaptured() && imguiPass.wantsMouse()) ||
(keyboardEvent && imguiPass.wantsKeyboard()); (keyboardEvent && imguiPass.wantsKeyboard());
if (!capturedByImgui) if (!capturedByImgui)
@@ -176,7 +176,7 @@ void App::run()
} }
} }
if (!isRunning) break; if (!isRunning) break;
// Consume SDL events before updating the base camera so held and // Consume SDL events before updating the base camera so held and
// newly pressed inputs are applied in the same frame. // newly pressed inputs are applied in the same frame.
+1 -1
View File
@@ -114,7 +114,7 @@ void Camera::SetRotation(const glm::vec2& yawPitchRadians) {
SetRotation(yawPitchRadians.x, yawPitchRadians.y); SetRotation(yawPitchRadians.x, yawPitchRadians.y);
} }
void Camera::SetTarget(const glm::vec3& target) { void Camera::SetTarget(const glm::vec3&) {
} }
void Camera::ClearTarget() { void Camera::ClearTarget() {
@@ -0,0 +1,23 @@
#include <destrum/Graphics/Managers/LineRenderingManager.h>
void LineRenderingManager::SubmitLine(const Line& line)
{
lines.push_back(line);
}
void LineRenderingManager::SubmitLine(
const glm::vec3& start,
const glm::vec3& end,
const glm::vec4& color)
{
SubmitLine(Line{
.start = start,
.end = end,
.color = color,
});
}
void LineRenderingManager::ClearLines()
{
lines.clear();
}
@@ -0,0 +1,31 @@
#include <destrum/Graphics/Managers/QuadRenderingManager.h>
void QuadRenderingManager::SubmitQuad(
const glm::vec2& position,
const glm::vec2& size,
const glm::vec4& color,
ImageID textureId)
{
SubmitQuad(position, size, 0.0f, color, textureId);
}
void QuadRenderingManager::SubmitQuad(
const glm::vec2& position,
const glm::vec2& size,
float rotation,
const glm::vec4& color,
ImageID textureId)
{
quads.push_back(QuadSubmission{
.position = position,
.size = size,
.rotation = rotation,
.color = color,
.textureId = textureId,
});
}
void QuadRenderingManager::ClearQuads()
{
quads.clear();
}
@@ -124,6 +124,11 @@ void ImguiPass::beginFrame() {
ImGui_ImplVulkan_NewFrame(); ImGui_ImplVulkan_NewFrame();
ImGui_ImplSDL2_NewFrame(); ImGui_ImplSDL2_NewFrame();
if (SDL_GetRelativeMouseMode() == SDL_TRUE) {
ImGui::GetIO().MousePos = ImVec2(-FLT_MAX, -FLT_MAX);
}
ImGui::NewFrame(); ImGui::NewFrame();
frameBegun = true; frameBegun = true;
@@ -0,0 +1,232 @@
#include <destrum/Graphics/Pipelines/LineRenderingPass.h>
#include <algorithm>
#include <cstdint>
#include <limits>
#include <stdexcept>
#include <destrum/FS/AssetFS.h>
#include <destrum/Graphics/Camera.h>
#include <destrum/Graphics/GPUImage.h>
#include <destrum/Graphics/GfxDevice.h>
#include <destrum/Graphics/Pipeline.h>
#include <destrum/Graphics/Util.h>
#include "volk.h"
namespace {
constexpr std::size_t InitialVertexCapacity = 256;
}
void LineRenderingPass::init(GfxDevice& gfxDevice, VkFormat drawImageFormat)
{
try {
vertexBuffer.init(
gfxDevice,
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
InitialVertexCapacity * sizeof(LineVertex),
"line vertices");
vertexCapacity = InitialVertexCapacity;
const auto vertexShader =
AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/line.vert");
const auto fragmentShader =
AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/line.frag");
const auto pushConstantRange = VkPushConstantRange{
.stageFlags = VK_SHADER_STAGE_VERTEX_BIT,
.offset = 0,
.size = sizeof(glm::mat4),
};
const auto pipelineLayoutInfo = VkPipelineLayoutCreateInfo{
.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
.pushConstantRangeCount = 1,
.pPushConstantRanges = &pushConstantRange,
};
VK_CHECK(vkCreatePipelineLayout(
gfxDevice.getDevice(),
&pipelineLayoutInfo,
nullptr,
&pipelineLayout));
PipelineConfigInfo pipelineConfig{};
Pipeline::DefaultPipelineConfigInfo(pipelineConfig);
pipelineConfig.name = "Line Rendering Pipeline";
pipelineConfig.pipelineLayout = pipelineLayout;
pipelineConfig.inputAssemblyInfo.topology = VK_PRIMITIVE_TOPOLOGY_LINE_LIST;
pipelineConfig.rasterizationInfo.cullMode = VK_CULL_MODE_NONE;
pipelineConfig.depthStencilInfo.depthTestEnable = VK_FALSE;
pipelineConfig.depthStencilInfo.depthWriteEnable = VK_FALSE;
pipelineConfig.depthStencilInfo.depthCompareOp = VK_COMPARE_OP_ALWAYS;
pipelineConfig.colorAttachments = {drawImageFormat};
pipelineConfig.depthAttachment = VK_FORMAT_UNDEFINED;
pipelineConfig.vertexBindingDescriptions = {
VkVertexInputBindingDescription{
.binding = 0,
.stride = sizeof(LineVertex),
.inputRate = VK_VERTEX_INPUT_RATE_VERTEX,
}
};
pipelineConfig.vertexAttributeDescriptions = {
VkVertexInputAttributeDescription{
.location = 0,
.binding = 0,
.format = VK_FORMAT_R32G32B32A32_SFLOAT,
.offset = offsetof(LineVertex, position),
},
VkVertexInputAttributeDescription{
.location = 1,
.binding = 0,
.format = VK_FORMAT_R32G32B32A32_SFLOAT,
.offset = offsetof(LineVertex, color),
},
};
pipelineConfig.colorBlendAttachment.blendEnable = VK_TRUE;
pipelineConfig.colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
pipelineConfig.colorBlendAttachment.dstColorBlendFactor =
VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
pipelineConfig.colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD;
pipelineConfig.colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
pipelineConfig.colorBlendAttachment.dstAlphaBlendFactor =
VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
pipelineConfig.colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD;
pipeline = std::make_unique<Pipeline>(
gfxDevice,
vertexShader.string(),
fragmentShader.string(),
pipelineConfig);
} catch (...) {
cleanup(gfxDevice);
throw;
}
}
void LineRenderingPass::draw(
VkCommandBuffer cmd,
GfxDevice& gfxDevice,
const GPUImage& target,
const Camera& camera,
LineRenderingManager& lineManager)
{
const auto& lines = lineManager.GetLines();
if (!pipeline || lines.empty()) {
lineManager.ClearLines();
return;
}
if (lines.size() > std::numeric_limits<std::size_t>::max() / 2) {
throw std::overflow_error("Too many lines submitted for rendering");
}
std::vector<LineVertex> vertices;
vertices.reserve(lines.size() * 2);
for (const Line& line : lines) {
vertices.push_back(LineVertex{
.position = glm::vec4{line.start, 1.0f},
.color = line.color,
});
vertices.push_back(LineVertex{
.position = glm::vec4{line.end, 1.0f},
.color = line.color,
});
}
if (vertices.size() > std::numeric_limits<std::uint32_t>::max()) {
throw std::overflow_error("Too many line vertices submitted for rendering");
}
ensureVertexCapacity(gfxDevice, vertices.size());
vertexBuffer.uploadNewData(
cmd,
gfxDevice.getCurrentFrameIndex(),
vertices.data(),
vertices.size() * sizeof(LineVertex));
auto renderInfo = vkutil::createRenderingInfo({
.renderExtent = target.getExtent2D(),
.colorImageView = target.imageView,
});
vkCmdBeginRendering(cmd, &renderInfo.renderingInfo);
pipeline->bind(cmd);
const auto viewport = VkViewport{
.x = 0.0f,
.y = 0.0f,
.width = static_cast<float>(target.extent.width),
.height = static_cast<float>(target.extent.height),
.minDepth = 0.0f,
.maxDepth = 1.0f,
};
vkCmdSetViewport(cmd, 0, 1, &viewport);
const auto scissor = VkRect2D{
.offset = {},
.extent = target.getExtent2D(),
};
vkCmdSetScissor(cmd, 0, 1, &scissor);
vkCmdSetPolygonModeEXT(cmd, VK_POLYGON_MODE_FILL);
const glm::mat4 viewProjection = camera.GetViewProjectionMatrix();
vkCmdPushConstants(
cmd,
pipelineLayout,
VK_SHADER_STAGE_VERTEX_BIT,
0,
sizeof(viewProjection),
&viewProjection);
const VkBuffer vertexBufferHandle = vertexBuffer.getBuffer().buffer;
const VkDeviceSize vertexBufferOffset = 0;
vkCmdBindVertexBuffers(cmd, 0, 1, &vertexBufferHandle, &vertexBufferOffset);
vkCmdDraw(cmd, static_cast<std::uint32_t>(vertices.size()), 1, 0, 0);
vkCmdEndRendering(cmd);
lineManager.ClearLines();
}
void LineRenderingPass::cleanup(GfxDevice& gfxDevice)
{
pipeline.reset();
if (pipelineLayout != VK_NULL_HANDLE && gfxDevice.getDevice() != VK_NULL_HANDLE) {
vkDestroyPipelineLayout(gfxDevice.getDevice(), pipelineLayout, nullptr);
}
pipelineLayout = VK_NULL_HANDLE;
vertexBuffer.cleanup(gfxDevice);
vertexCapacity = 0;
}
void LineRenderingPass::ensureVertexCapacity(
GfxDevice& gfxDevice,
std::size_t requiredVertices)
{
if (requiredVertices <= vertexCapacity) {
return;
}
std::size_t newCapacity = std::max(vertexCapacity, InitialVertexCapacity);
while (newCapacity < requiredVertices) {
if (newCapacity > std::numeric_limits<std::size_t>::max() / 2) {
newCapacity = requiredVertices;
break;
}
newCapacity *= 2;
}
gfxDevice.waitIdle();
vertexBuffer.cleanup(gfxDevice);
vertexBuffer.init(
gfxDevice,
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
newCapacity * sizeof(LineVertex),
"line vertices");
vertexCapacity = newCapacity;
}
@@ -0,0 +1,334 @@
#include <destrum/Graphics/Pipelines/QuadRendererPass.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <limits>
#include <stdexcept>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <destrum/FS/AssetFS.h>
#include <destrum/Graphics/GfxDevice.h>
#include <destrum/Graphics/GPUImage.h>
#include <destrum/Graphics/Pipeline.h>
#include <destrum/Graphics/RenderResources.h>
#include <destrum/Graphics/Util.h>
#include <destrum/Graphics/Managers/QuadRenderingManager.h>
#include "volk.h"
namespace {
constexpr std::size_t InitialQuadCapacity = 256;
constexpr std::size_t VerticesPerQuad = 4;
constexpr std::size_t IndicesPerQuad = 6;
glm::vec2 rotatePoint(const glm::vec2& point, float cosA, float sinA) {
return glm::vec2{
point.x * cosA - point.y * sinA,
point.x * sinA + point.y * cosA,
};
}
}
void QuadRendererPass::init(GfxDevice& gfxDevice, RenderResources& resources, VkFormat drawImageFormat)
{
try {
const std::size_t vertexBytes = InitialQuadCapacity * VerticesPerQuad * sizeof(QuadVertex);
vertexBuffer = gfxDevice.createBuffer(
vertexBytes,
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
VMA_MEMORY_USAGE_AUTO_PREFER_HOST);
const std::size_t indexBytes = InitialQuadCapacity * IndicesPerQuad * sizeof(std::uint32_t);
indexBuffer = gfxDevice.createBuffer(
indexBytes,
VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
VMA_MEMORY_USAGE_AUTO_PREFER_HOST);
quadCapacity = InitialQuadCapacity;
const auto vertexShader =
AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/quad.vert");
const auto fragmentShader =
AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/quad.frag");
const auto pushConstantRange = VkPushConstantRange{
.stageFlags = VK_SHADER_STAGE_VERTEX_BIT,
.offset = 0,
.size = sizeof(glm::mat4),
};
const auto layouts = std::array{
resources.getBindlessDescSetLayout()
};
const auto pipelineLayoutInfo = VkPipelineLayoutCreateInfo{
.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
.setLayoutCount = static_cast<std::uint32_t>(layouts.size()),
.pSetLayouts = layouts.data(),
.pushConstantRangeCount = 1,
.pPushConstantRanges = &pushConstantRange,
};
VK_CHECK(vkCreatePipelineLayout(
gfxDevice.getDevice(),
&pipelineLayoutInfo,
nullptr,
&pipelineLayout));
PipelineConfigInfo pipelineConfig{};
Pipeline::DefaultPipelineConfigInfo(pipelineConfig);
pipelineConfig.name = "Quad Rendering Pipeline";
pipelineConfig.pipelineLayout = pipelineLayout;
pipelineConfig.inputAssemblyInfo.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
pipelineConfig.rasterizationInfo.cullMode = VK_CULL_MODE_NONE;
pipelineConfig.depthStencilInfo.depthTestEnable = VK_FALSE;
pipelineConfig.depthStencilInfo.depthWriteEnable = VK_FALSE;
pipelineConfig.depthStencilInfo.depthCompareOp = VK_COMPARE_OP_ALWAYS;
pipelineConfig.colorAttachments = {drawImageFormat};
pipelineConfig.depthAttachment = VK_FORMAT_UNDEFINED;
pipelineConfig.vertexBindingDescriptions = {
VkVertexInputBindingDescription{
.binding = 0,
.stride = sizeof(QuadVertex),
.inputRate = VK_VERTEX_INPUT_RATE_VERTEX,
}
};
pipelineConfig.vertexAttributeDescriptions = {
VkVertexInputAttributeDescription{
.location = 0,
.binding = 0,
.format = VK_FORMAT_R32G32_SFLOAT,
.offset = offsetof(QuadVertex, position),
},
VkVertexInputAttributeDescription{
.location = 1,
.binding = 0,
.format = VK_FORMAT_R32G32_SFLOAT,
.offset = offsetof(QuadVertex, uv),
},
VkVertexInputAttributeDescription{
.location = 2,
.binding = 0,
.format = VK_FORMAT_R32G32B32A32_SFLOAT,
.offset = offsetof(QuadVertex, color),
},
VkVertexInputAttributeDescription{
.location = 3,
.binding = 0,
.format = VK_FORMAT_R32_UINT,
.offset = offsetof(QuadVertex, textureId),
},
};
pipelineConfig.colorBlendAttachment.blendEnable = VK_TRUE;
pipelineConfig.colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
pipelineConfig.colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
pipelineConfig.colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD;
pipelineConfig.colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
pipelineConfig.colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
pipelineConfig.colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD;
pipeline = std::make_unique<Pipeline>(
gfxDevice,
vertexShader.string(),
fragmentShader.string(),
pipelineConfig);
} catch (...) {
cleanup(gfxDevice);
throw;
}
}
void QuadRendererPass::draw(
VkCommandBuffer cmd,
GfxDevice& gfxDevice,
RenderResources& resources,
const GPUImage& target,
const glm::mat4& projection)
{
auto& quadManager = QuadRenderingManager::GetInstance();
const auto& quads = quadManager.GetQuads();
if (!pipeline || quads.empty()) {
quadManager.ClearQuads();
return;
}
const std::size_t numQuads = quads.size();
if (numQuads > std::numeric_limits<std::size_t>::max() / VerticesPerQuad) {
throw std::overflow_error("Too many quads submitted for rendering");
}
const std::size_t numVertices = numQuads * VerticesPerQuad;
const std::size_t numIndices = numQuads * IndicesPerQuad;
if (numVertices > std::numeric_limits<std::uint32_t>::max()) {
throw std::overflow_error("Too many quad vertices submitted for rendering");
}
ensureCapacity(gfxDevice, numQuads);
const ImageID whiteTextureId = resources.getWhiteTextureID();
auto* vertexMapped = reinterpret_cast<std::uint8_t*>(vertexBuffer.info.pMappedData);
auto* indexMapped = reinterpret_cast<std::uint8_t*>(indexBuffer.info.pMappedData);
constexpr std::array<glm::vec2, 4> localCorners = {{
glm::vec2{-0.5f, -0.5f},
glm::vec2{ 0.5f, -0.5f},
glm::vec2{ 0.5f, 0.5f},
glm::vec2{-0.5f, 0.5f},
}};
constexpr std::array<glm::vec2, 4> uvs = {{
glm::vec2{0.0f, 1.0f},
glm::vec2{1.0f, 1.0f},
glm::vec2{1.0f, 0.0f},
glm::vec2{0.0f, 0.0f},
}};
auto* vertWrite = reinterpret_cast<QuadVertex*>(vertexMapped);
auto* idxWrite = reinterpret_cast<std::uint32_t*>(indexMapped);
std::uint32_t vertexBase = 0;
for (const auto& quad : quads) {
const float cosA = std::cos(quad.rotation);
const float sinA = std::sin(quad.rotation);
const glm::vec2 halfSize = quad.size * 0.5f;
const ImageID texId = (quad.textureId == NULL_IMAGE_ID) ? whiteTextureId : quad.textureId;
for (std::size_t v = 0; v < VerticesPerQuad; ++v) {
glm::vec2 localPos = localCorners[v] * halfSize;
glm::vec2 rotatedPos = rotatePoint(localPos, cosA, sinA);
glm::vec2 worldPos = quad.position + rotatedPos;
vertWrite[vertexBase + v] = QuadVertex{
.position = worldPos,
.uv = uvs[v],
.color = quad.color,
.textureId = static_cast<std::uint32_t>(texId),
};
}
const std::uint32_t idxOffset = vertexBase / 4 * 6;
idxWrite[idxOffset + 0] = vertexBase + 0;
idxWrite[idxOffset + 1] = vertexBase + 1;
idxWrite[idxOffset + 2] = vertexBase + 2;
idxWrite[idxOffset + 3] = vertexBase + 0;
idxWrite[idxOffset + 4] = vertexBase + 2;
idxWrite[idxOffset + 5] = vertexBase + 3;
vertexBase += VerticesPerQuad;
}
auto& memMgr = gfxDevice.getMemoryManager();
memMgr.flushAllocation(vertexBuffer, 0, numVertices * sizeof(QuadVertex));
memMgr.flushAllocation(indexBuffer, 0, numIndices * sizeof(std::uint32_t));
auto renderInfo = vkutil::createRenderingInfo({
.renderExtent = target.getExtent2D(),
.colorImageView = target.imageView,
});
vkCmdBeginRendering(cmd, &renderInfo.renderingInfo);
pipeline->bind(cmd);
resources.bindBindlessDescSet(cmd, pipelineLayout);
const auto viewport = VkViewport{
.x = 0.0f,
.y = 0.0f,
.width = static_cast<float>(target.extent.width),
.height = static_cast<float>(target.extent.height),
.minDepth = 0.0f,
.maxDepth = 1.0f,
};
vkCmdSetViewport(cmd, 0, 1, &viewport);
const auto scissor = VkRect2D{
.offset = {},
.extent = target.getExtent2D(),
};
vkCmdSetScissor(cmd, 0, 1, &scissor);
vkCmdSetPolygonModeEXT(cmd, VK_POLYGON_MODE_FILL);
vkCmdPushConstants(
cmd,
pipelineLayout,
VK_SHADER_STAGE_VERTEX_BIT,
0,
sizeof(glm::mat4),
&projection);
const VkBuffer vertexBufferHandle = vertexBuffer.buffer;
const VkDeviceSize vertexBufferOffset = 0;
vkCmdBindVertexBuffers(cmd, 0, 1, &vertexBufferHandle, &vertexBufferOffset);
const VkBuffer indexBufferHandle = indexBuffer.buffer;
vkCmdBindIndexBuffer(cmd, indexBufferHandle, 0, VK_INDEX_TYPE_UINT32);
vkCmdDrawIndexed(cmd, static_cast<std::uint32_t>(numIndices), 1, 0, 0, 0);
vkCmdEndRendering(cmd);
quadManager.ClearQuads();
}
void QuadRendererPass::cleanup(GfxDevice& gfxDevice)
{
pipeline.reset();
if (pipelineLayout != VK_NULL_HANDLE && gfxDevice.getDevice() != VK_NULL_HANDLE) {
vkDestroyPipelineLayout(gfxDevice.getDevice(), pipelineLayout, nullptr);
}
pipelineLayout = VK_NULL_HANDLE;
if (vertexBuffer.buffer != VK_NULL_HANDLE) {
gfxDevice.destroyBuffer(vertexBuffer);
}
if (indexBuffer.buffer != VK_NULL_HANDLE) {
gfxDevice.destroyBuffer(indexBuffer);
}
quadCapacity = 0;
}
void QuadRendererPass::ensureCapacity(
GfxDevice& gfxDevice,
std::size_t requiredQuads)
{
if (requiredQuads <= quadCapacity) {
return;
}
std::size_t newCapacity = std::max(quadCapacity, InitialQuadCapacity);
while (newCapacity < requiredQuads) {
if (newCapacity > std::numeric_limits<std::size_t>::max() / 2) {
newCapacity = requiredQuads;
break;
}
newCapacity *= 2;
}
gfxDevice.waitIdle();
if (vertexBuffer.buffer != VK_NULL_HANDLE) {
gfxDevice.destroyBuffer(vertexBuffer);
}
vertexBuffer = gfxDevice.createBuffer(
newCapacity * VerticesPerQuad * sizeof(QuadVertex),
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
VMA_MEMORY_USAGE_AUTO_PREFER_HOST);
if (indexBuffer.buffer != VK_NULL_HANDLE) {
gfxDevice.destroyBuffer(indexBuffer);
}
indexBuffer = gfxDevice.createBuffer(
newCapacity * IndicesPerQuad * sizeof(std::uint32_t),
VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
VMA_MEMORY_USAGE_AUTO_PREFER_HOST);
quadCapacity = newCapacity;
}
+56 -3
View File
@@ -1,10 +1,14 @@
#include <destrum/Graphics/Renderer.h> #include <destrum/Graphics/Renderer.h>
#include <destrum/Graphics/Managers/LineRenderingManager.h>
#include <destrum/Graphics/Managers/QuadRenderingManager.h>
#include <destrum/Graphics/Util.h> #include <destrum/Graphics/Util.h>
#include <algorithm> #include <algorithm>
#include <numeric> #include <numeric>
#include <glm/gtc/matrix_transform.hpp>
#include "volk.h" #include "volk.h"
#include "destrum/Util/GameState.h" #include "destrum/Util/GameState.h"
#include "spdlog/spdlog.h" #include "spdlog/spdlog.h"
@@ -30,12 +34,18 @@ void GameRenderer::init(GfxDevice& gfxDevice, RenderResources& _resources, glm::
meshPipeline = std::make_unique<MeshPipeline>(); meshPipeline = std::make_unique<MeshPipeline>();
meshPipeline->init(gfxDevice, _resources, drawImageFormat, depthImageFormat); meshPipeline->init(gfxDevice, _resources, drawImageFormat, depthImageFormat);
skyboxPipeline = std::make_unique<SkyboxPipeline>(); skyboxPipeline = std::make_unique<SkyboxPipeline>();
skyboxPipeline->init(gfxDevice, _resources, drawImageFormat, depthImageFormat); skyboxPipeline->init(gfxDevice, _resources, drawImageFormat, depthImageFormat);
skinningPipeline = std::make_unique<SkinningPipeline>(); skinningPipeline = std::make_unique<SkinningPipeline>();
skinningPipeline->init(gfxDevice); skinningPipeline->init(gfxDevice);
lineRenderingPass = std::make_unique<LineRenderingPass>();
lineRenderingPass->init(gfxDevice, drawImageFormat);
quadRendererPass = std::make_unique<QuadRendererPass>();
quadRendererPass->init(gfxDevice, _resources, drawImageFormat);
GameState::GetInstance().SetRenderer(this); GameState::GetInstance().SetRenderer(this);
initialized = true; initialized = true;
} catch (...) { } catch (...) {
@@ -49,6 +59,7 @@ void GameRenderer::beginDrawing(GfxDevice& gfxDevice)
flushMaterialUpdates(gfxDevice); flushMaterialUpdates(gfxDevice);
meshDrawCommands.clear(); meshDrawCommands.clear();
skinningPipeline->beginDrawing(gfxDevice.getCurrentFrameIndex()); skinningPipeline->beginDrawing(gfxDevice.getCurrentFrameIndex());
QuadRenderingManager::GetInstance().ClearQuads();
} }
void GameRenderer::endDrawing() void GameRenderer::endDrawing()
@@ -194,7 +205,41 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
vkCmdEndRendering(cmd); vkCmdEndRendering(cmd);
} }
// vkutil::cmdEndLabel(cmd);
{
TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "LineRenderingPass::draw");
lineRenderingPass->draw(
cmd,
gfxDevice,
drawImage,
camera,
LineRenderingManager::GetInstance());
}
{
TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "QuadRendererPass::draw");
quadRendererPass->draw(
cmd,
gfxDevice,
*resources,
drawImage,
glm::ortho(0.0f,
static_cast<float>(drawImage.extent.width),
static_cast<float>(drawImage.extent.height),
0.0f, -1.0f, 1.0f));
}
// vkutil::cmdEndLabel(cmd);
}
void GameRenderer::drawQuads(
VkCommandBuffer cmd,
GfxDevice& gfxDevice,
const glm::mat4& projection)
{
const auto& drawImage = resources->getImage(drawImageId);
quadRendererPass->draw(cmd, gfxDevice, *resources, drawImage, projection);
} }
void GameRenderer::cleanup(GfxDevice& gfxDevice) void GameRenderer::cleanup(GfxDevice& gfxDevice)
@@ -205,9 +250,15 @@ void GameRenderer::cleanup(GfxDevice& gfxDevice)
vkDeviceWaitIdle(device); vkDeviceWaitIdle(device);
} }
if (skinningPipeline) if (skinningPipeline)
skinningPipeline->cleanup(gfxDevice); skinningPipeline->cleanup(gfxDevice);
if (lineRenderingPass)
lineRenderingPass->cleanup(gfxDevice);
if (quadRendererPass)
quadRendererPass->cleanup(gfxDevice);
if (skyboxPipeline) if (skyboxPipeline)
skyboxPipeline->cleanup(device); skyboxPipeline->cleanup(device);
@@ -228,6 +279,8 @@ void GameRenderer::cleanup(GfxDevice& gfxDevice)
meshPipeline.reset(); meshPipeline.reset();
skyboxPipeline.reset(); skyboxPipeline.reset();
skinningPipeline.reset(); skinningPipeline.reset();
lineRenderingPass.reset();
quadRendererPass.reset();
pendingMaterialUploads.clear(); pendingMaterialUploads.clear();
meshDrawCommands.clear(); meshDrawCommands.clear();
sortedMeshDrawCommands.clear(); sortedMeshDrawCommands.clear();
+13 -5
View File
@@ -46,11 +46,11 @@ void InputManager::BeginFrame() {
m_textInput.clear(); m_textInput.clear();
// Update mouse delta based on last known position // Relative mouse accumulators are filled during SDL event processing
m_mouseDX = m_mouseX - m_prevMouseX; // and consumed by game code each frame. Reset them here (before events
m_mouseDY = m_mouseY - m_prevMouseY; // are processed) so this frame accumulates fresh deltas.
m_prevMouseX = m_mouseX; m_mouseRelDX = 0;
m_prevMouseY = m_mouseY; m_mouseRelDY = 0;
// Clear controller "edge" sets + snapshot axes // Clear controller "edge" sets + snapshot axes
for (auto& [id, pad]: m_pads) { for (auto& [id, pad]: m_pads) {
@@ -60,6 +60,12 @@ void InputManager::BeginFrame() {
} }
} }
void InputManager::SetMouseCaptured(bool captured) {
if (captured == m_MouseCaptured) return;
m_MouseCaptured = captured;
SDL_SetRelativeMouseMode(captured ? SDL_TRUE : SDL_FALSE);
}
void InputManager::AddController(int deviceIndex) { void InputManager::AddController(int deviceIndex) {
if (!SDL_IsGameController(deviceIndex)) return; if (!SDL_IsGameController(deviceIndex)) return;
@@ -134,6 +140,8 @@ bool InputManager::ProcessEvent(const SDL_Event& e) {
case SDL_MOUSEMOTION: { case SDL_MOUSEMOTION: {
m_mouseX = e.motion.x; m_mouseX = e.motion.x;
m_mouseY = e.motion.y; m_mouseY = e.motion.y;
m_mouseRelDX += e.motion.xrel;
m_mouseRelDY += e.motion.yrel;
} }
break; break;
+137
View File
@@ -26,9 +26,12 @@
#include <cstdarg> #include <cstdarg>
#include <cstdio> #include <cstdio>
#include <memory> #include <memory>
#include <mutex>
#include <stdexcept> #include <stdexcept>
#include <thread> #include <thread>
#include <unordered_map> #include <unordered_map>
#include <unordered_set>
#include <vector>
#include <glm/gtx/norm.hpp> #include <glm/gtx/norm.hpp>
@@ -276,6 +279,84 @@ namespace
throw std::runtime_error("Cannot create Jolt body without a valid shape."); throw std::runtime_error("Cannot create Jolt body without a valid shape.");
} }
} }
class SensorContactListener final : public ContactListener
{
public:
using BodyPair = std::pair<BodyID, BodyID>;
struct PairHash {
std::size_t operator()(const BodyPair& p) const {
return std::hash<uint32_t>{}(
p.first.GetIndexAndSequenceNumber() ^
(p.second.GetIndexAndSequenceNumber() << 7));
}
};
struct PairData {
GameObject* objA{nullptr};
GameObject* objB{nullptr};
bool sensorA{false};
bool sensorB{false};
};
ValidateResult OnContactValidate(const Body&, const Body&, RVec3Arg,
const CollideShapeResult&) override
{
return ValidateResult::AcceptAllContactsForThisBodyPair;
}
void OnContactAdded(const Body& body1, const Body& body2,
const ContactManifold&, ContactSettings& ioSettings) override
{
if (body1.IsSensor() || body2.IsSensor())
{
PairData data;
data.objA = reinterpret_cast<GameObject*>(body1.GetUserData());
data.objB = reinterpret_cast<GameObject*>(body2.GetUserData());
data.sensorA = body1.IsSensor();
data.sensorB = body2.IsSensor();
std::lock_guard<std::mutex> lock(m_Mutex);
m_Overlaps.insert_or_assign(MakePair(body1.GetID(), body2.GetID()), data);
}
}
void OnContactPersisted(const Body& body1, const Body& body2,
const ContactManifold&, ContactSettings&) override
{
}
void OnContactRemoved(const SubShapeIDPair& subShapePair) override
{
std::lock_guard<std::mutex> lock(m_Mutex);
m_Overlaps.erase(MakePair(subShapePair.GetBody1ID(), subShapePair.GetBody2ID()));
}
void Clear()
{
std::lock_guard<std::mutex> lock(m_Mutex);
m_Overlaps.clear();
}
std::unordered_map<BodyPair, PairData, PairHash> SwapOverlaps()
{
std::lock_guard<std::mutex> lock(m_Mutex);
return std::move(m_Overlaps);
}
private:
static BodyPair MakePair(BodyID a, BodyID b)
{
if (a.GetIndexAndSequenceNumber() < b.GetIndexAndSequenceNumber())
{
return {a, b};
}
return {b, a};
}
std::mutex m_Mutex;
std::unordered_map<BodyPair, PairData, PairHash> m_Overlaps;
};
} // namespace } // namespace
class JoltPhysicsWorld::Impl class JoltPhysicsWorld::Impl
@@ -307,6 +388,7 @@ public:
m_ObjectVsBroadPhaseLayerFilter, m_ObjectVsBroadPhaseLayerFilter,
m_ObjectLayerPairFilter); m_ObjectLayerPairFilter);
m_PhysicsSystem.SetContactListener(&m_SensorListener);
m_PhysicsSystem.SetGravity(ToJoltVec3(settings.gravity)); m_PhysicsSystem.SetGravity(ToJoltVec3(settings.gravity));
} }
@@ -426,6 +508,15 @@ public:
collisionSteps, collisionSteps,
m_TempAllocator.get(), m_TempAllocator.get(),
m_JobSystem.get()); m_JobSystem.get());
ProcessSensorOverlaps();
}
std::vector<TriggerEvent> ConsumeTriggerEvents()
{
std::vector<TriggerEvent> events;
m_TriggerEvents.swap(events);
return events;
} }
void SyncKinematicBodiesToPhysics() void SyncKinematicBodiesToPhysics()
@@ -626,6 +717,43 @@ private:
return {}; return {};
} }
void ProcessSensorOverlaps()
{
const auto currentOverlaps = m_SensorListener.SwapOverlaps();
for (const auto& [pair, data] : currentOverlaps)
{
if (m_PreviousSensorOverlaps.find(pair) == m_PreviousSensorOverlaps.end())
{
if (data.sensorA && data.objA)
{
m_TriggerEvents.push_back({data.objA, data.objB, true});
}
if (data.sensorB && data.objB)
{
m_TriggerEvents.push_back({data.objB, data.objA, true});
}
}
}
for (const auto& [pair, data] : m_PreviousSensorOverlaps)
{
if (currentOverlaps.find(pair) == currentOverlaps.end())
{
if (data.sensorA && data.objA)
{
m_TriggerEvents.push_back({data.objA, data.objB, false});
}
if (data.sensorB && data.objB)
{
m_TriggerEvents.push_back({data.objB, data.objA, false});
}
}
}
m_PreviousSensorOverlaps = std::move(currentOverlaps);
}
Settings m_Settings{}; Settings m_Settings{};
BPLayerInterfaceImpl m_BPLayerInterface{}; BPLayerInterfaceImpl m_BPLayerInterface{};
@@ -639,6 +767,10 @@ private:
std::unordered_map<std::uint32_t, BodyRecord> m_Bodies; std::unordered_map<std::uint32_t, BodyRecord> m_Bodies;
std::uint32_t m_NextHandle{0}; std::uint32_t m_NextHandle{0};
SensorContactListener m_SensorListener;
std::unordered_map<SensorContactListener::BodyPair, SensorContactListener::PairData, SensorContactListener::PairHash> m_PreviousSensorOverlaps;
std::vector<TriggerEvent> m_TriggerEvents;
}; };
JoltPhysicsWorld::JoltPhysicsWorld(const Settings& settings) JoltPhysicsWorld::JoltPhysicsWorld(const Settings& settings)
@@ -710,3 +842,8 @@ bool JoltPhysicsWorld::Raycast(const glm::vec3& origin,
{ {
return m_Impl->Raycast(origin, direction, maxDistance, hit); return m_Impl->Raycast(origin, direction, maxDistance, hit);
} }
std::vector<TriggerEvent> JoltPhysicsWorld::ConsumeTriggerEvents()
{
return m_Impl->ConsumeTriggerEvents();
}
+20 -1
View File
@@ -1,6 +1,7 @@
#include <destrum/Physics/PhysicsSceneBridge.h> #include <destrum/Physics/PhysicsSceneBridge.h>
#include <destrum/Components/Physics/Rigidbody.h> #include <destrum/Components/Physics/Rigidbody.h>
#include <destrum/ObjectModel/Component.h>
#include <destrum/ObjectModel/GameObject.h> #include <destrum/ObjectModel/GameObject.h>
PhysicsSceneBridge::PhysicsSceneBridge(std::unique_ptr<PhysicsWorld> world) PhysicsSceneBridge::PhysicsSceneBridge(std::unique_ptr<PhysicsWorld> world)
@@ -8,14 +9,16 @@ PhysicsSceneBridge::PhysicsSceneBridge(std::unique_ptr<PhysicsWorld> world)
} }
void PhysicsSceneBridge::RegisterGameObject(GameObject& object) { void PhysicsSceneBridge::RegisterGameObject(GameObject& object) {
if (!m_World) return;
if (auto* rb = object.GetComponent<Rigidbody>()) { if (auto* rb = object.GetComponent<Rigidbody>()) {
if (!rb->HasPhysicsBody() || rb->GetPhysicsWorld() != &GetWorld()) { if (!rb->HasPhysicsBody() || rb->GetPhysicsWorld() != m_World.get()) {
m_World->RegisterRigidbody(*rb); m_World->RegisterRigidbody(*rb);
} }
} }
} }
void PhysicsSceneBridge::UnregisterGameObject(GameObject& object) { void PhysicsSceneBridge::UnregisterGameObject(GameObject& object) {
if (!m_World) return;
if (auto* rb = object.GetComponent<Rigidbody>()) { if (auto* rb = object.GetComponent<Rigidbody>()) {
if (rb->GetPhysicsWorld() == m_World.get()) { if (rb->GetPhysicsWorld() == m_World.get()) {
m_World->UnregisterRigidbody(*rb); m_World->UnregisterRigidbody(*rb);
@@ -24,13 +27,29 @@ void PhysicsSceneBridge::UnregisterGameObject(GameObject& object) {
} }
void PhysicsSceneBridge::RefreshGameObject(GameObject& object) { void PhysicsSceneBridge::RefreshGameObject(GameObject& object) {
if (!m_World) return;
if (auto* rb = object.GetComponent<Rigidbody>()) { if (auto* rb = object.GetComponent<Rigidbody>()) {
m_World->RefreshRigidbody(*rb); m_World->RefreshRigidbody(*rb);
} }
} }
void PhysicsSceneBridge::FixedUpdate(float fixedDt) { void PhysicsSceneBridge::FixedUpdate(float fixedDt) {
if (!m_World) return;
m_World->SyncKinematicBodiesToPhysics(); m_World->SyncKinematicBodiesToPhysics();
m_World->Step(fixedDt); m_World->Step(fixedDt);
for (const auto& event : m_World->ConsumeTriggerEvents()) {
if (event.owner == nullptr) continue;
for (const auto& component : event.owner->GetComponents()) {
if (component && !component->IsBeingDestroyed() && component->isEnabled()) {
if (event.entered) {
component->OnTriggerEnter(event.other);
} else {
component->OnTriggerExit(event.other);
}
}
}
}
m_World->SyncDynamicBodiesToTransforms(); m_World->SyncDynamicBodiesToTransforms();
} }
+6 -1
View File
@@ -23,7 +23,12 @@ namespace {
} }
Scene::Scene(const std::string& name) Scene::Scene(const std::string& name)
: m_name(name), : Scene(name, std::make_unique<JoltPhysicsWorld>()) {
}
Scene::Scene(const std::string& name, std::unique_ptr<PhysicsWorld> physicsWorld)
: m_Physics(std::move(physicsWorld)),
m_name(name),
m_id(++m_idCounter) { m_id(++m_idCounter) {
} }
+72 -63
View File
@@ -8,50 +8,40 @@
#include <destrum/Util/DeltaTime.h> #include <destrum/Util/DeltaTime.h>
Scene& SceneManager::GetCurrentScene() const { Scene& SceneManager::GetCurrentScene() const {
if (m_scenes.empty()) { if (m_activeScenes.empty()) {
throw std::out_of_range("No scenes are available"); throw std::out_of_range("No active scenes are available");
} }
return *m_activeScenes.front();
if (m_ActiveSceneIndex < 0 || m_ActiveSceneIndex >= static_cast<int>(m_scenes.size())) {
throw std::out_of_range("Active scene index is invalid");
}
return *m_scenes[static_cast<std::size_t>(m_ActiveSceneIndex)];
} }
void SceneManager::Update(float dt) { void SceneManager::Update(float dt) {
if (m_scenes.empty()) return; for (const auto& scene : m_activeScenes) {
(void)GetCurrentScene(); scene->Update(dt);
const auto scene = m_scenes.at(static_cast<std::size_t>(m_ActiveSceneIndex)); }
scene->Update(dt);
} }
void SceneManager::FixedUpdate(float dt) { void SceneManager::FixedUpdate(float dt) {
if (m_scenes.empty()) return; for (const auto& scene : m_activeScenes) {
(void)GetCurrentScene(); scene->FixedUpdate(dt);
const auto scene = m_scenes.at(static_cast<std::size_t>(m_ActiveSceneIndex)); }
scene->FixedUpdate(dt);
} }
void SceneManager::LateUpdate(float dt) { void SceneManager::LateUpdate(float dt) {
if (m_scenes.empty()) return; for (const auto& scene : m_activeScenes) {
(void)GetCurrentScene(); scene->LateUpdate(dt);
const auto scene = m_scenes.at(static_cast<std::size_t>(m_ActiveSceneIndex)); }
scene->LateUpdate(dt);
} }
void SceneManager::Render(const RenderContext& ctx) { void SceneManager::Render(const RenderContext& ctx) {
if (m_scenes.empty()) return; for (const auto& scene : m_activeScenes) {
(void)GetCurrentScene(); scene->Render(ctx);
const auto scene = m_scenes.at(static_cast<std::size_t>(m_ActiveSceneIndex)); }
scene->Render(ctx);
} }
void SceneManager::RenderImgui() { void SceneManager::RenderImgui() {
if (m_scenes.empty()) return; for (const auto& scene : m_activeScenes) {
(void)GetCurrentScene(); scene->RenderImgui();
const auto scene = m_scenes.at(static_cast<std::size_t>(m_ActiveSceneIndex)); }
scene->RenderImgui();
} }
void SceneManager::HandleGameObjectDestroy() { void SceneManager::HandleGameObjectDestroy() {
@@ -61,7 +51,7 @@ void SceneManager::HandleGameObjectDestroy() {
} }
void SceneManager::DestroyGameObjects() { void SceneManager::DestroyGameObjects() {
for (const auto& scene: m_scenes) { for (const auto& scene : m_scenes) {
scene->DestroyGameObjects(); scene->DestroyGameObjects();
} }
} }
@@ -73,37 +63,22 @@ void SceneManager::UnloadAllScenes() {
} }
void SceneManager::HandleSceneDestroy() { void SceneManager::HandleSceneDestroy() {
const std::shared_ptr<Scene> activeScene =
m_ActiveSceneIndex >= 0 &&
m_ActiveSceneIndex < static_cast<int>(m_scenes.size())
? m_scenes[static_cast<std::size_t>(m_ActiveSceneIndex)]
: nullptr;
for (auto it = m_scenes.begin(); it != m_scenes.end();) { for (auto it = m_scenes.begin(); it != m_scenes.end();) {
if ((*it)->IsBeingUnloaded()) { if ((*it)->IsBeingUnloaded()) {
const auto activeIt = std::find(m_activeScenes.begin(), m_activeScenes.end(), *it);
if (activeIt != m_activeScenes.end()) {
m_activeScenes.erase(activeIt);
}
it = m_scenes.erase(it); it = m_scenes.erase(it);
} else { } else {
++it; ++it;
} }
} }
if (m_scenes.empty()) { // Remove any stale active scenes that are no longer in the scene list.
m_ActiveSceneIndex = 0; std::erase_if(m_activeScenes, [this](const auto& scene) {
return; return std::find(m_scenes.begin(), m_scenes.end(), scene) == m_scenes.end();
} });
if (activeScene != nullptr) {
const auto activeIt = std::find(m_scenes.begin(), m_scenes.end(), activeScene);
if (activeIt != m_scenes.end()) {
m_ActiveSceneIndex = static_cast<int>(std::distance(m_scenes.begin(), activeIt));
return;
}
}
m_ActiveSceneIndex = std::clamp(
m_ActiveSceneIndex,
0,
static_cast<int>(m_scenes.size()) - 1);
} }
void SceneManager::HandleScene() { void SceneManager::HandleScene() {
@@ -113,7 +88,6 @@ void SceneManager::HandleScene() {
void SceneManager::Destroy() { void SceneManager::Destroy() {
if (m_scenes.empty()) { if (m_scenes.empty()) {
m_ActiveSceneIndex = 0;
return; return;
} }
@@ -124,25 +98,60 @@ void SceneManager::Destroy() {
} }
void SceneManager::SwitchScene(int index) { void SceneManager::SwitchScene(int index) {
// InputManager::GetInstance().RemoveAllBindings();
if (index < 0 || index >= static_cast<int>(m_scenes.size())) { if (index < 0 || index >= static_cast<int>(m_scenes.size())) {
throw std::out_of_range("Scene index out of range"); throw std::out_of_range("Scene index out of range");
} }
if (index == m_ActiveSceneIndex) {
return;
}
m_scenes[static_cast<std::size_t>(m_ActiveSceneIndex)]->UnloadBindings(); for (const auto& scene : m_activeScenes) {
m_ActiveSceneIndex = index; scene->UnloadBindings();
m_scenes[static_cast<std::size_t>(m_ActiveSceneIndex)]->LoadBindings(); }
m_activeScenes.clear();
m_activeScenes.push_back(m_scenes[static_cast<std::size_t>(index)]);
m_activeScenes.back()->LoadBindings();
} }
Scene &SceneManager::CreateScene(const std::string &name) { void SceneManager::AddActiveScene(int index) {
if (index < 0 || index >= static_cast<int>(m_scenes.size())) {
throw std::out_of_range("Scene index out of range");
}
const auto& scene = m_scenes[static_cast<std::size_t>(index)];
if (std::find(m_activeScenes.begin(), m_activeScenes.end(), scene) == m_activeScenes.end()) {
scene->LoadBindings();
m_activeScenes.push_back(scene);
}
}
void SceneManager::RemoveActiveScene(int index) {
if (index < 0 || index >= static_cast<int>(m_scenes.size())) {
throw std::out_of_range("Scene index out of range");
}
const auto& scene = m_scenes[static_cast<std::size_t>(index)];
const auto it = std::find(m_activeScenes.begin(), m_activeScenes.end(), scene);
if (it != m_activeScenes.end()) {
(*it)->UnloadBindings();
m_activeScenes.erase(it);
}
}
int SceneManager::GetActiveSceneId() const {
if (m_scenes.empty() || m_activeScenes.empty()) {
return -1;
}
const auto it = std::find(m_scenes.begin(), m_scenes.end(), m_activeScenes.front());
if (it == m_scenes.end()) {
return -1;
}
return static_cast<int>(std::distance(m_scenes.begin(), it));
}
Scene& SceneManager::CreateScene(const std::string& name) {
const auto scene = std::shared_ptr<Scene>(new Scene(name)); const auto scene = std::shared_ptr<Scene>(new Scene(name));
m_scenes.push_back(scene); m_scenes.push_back(scene);
if (m_scenes.size() == 1) { if (m_scenes.size() == 1) {
m_ActiveSceneIndex = 0; m_activeScenes.push_back(scene);
} }
return *scene; return *scene;
} }
+158 -20
View File
@@ -16,8 +16,10 @@
#include <destrum/ObjectModel/GameObject.h> #include <destrum/ObjectModel/GameObject.h>
#include <destrum/ObjectModel/ObjectId.h> #include <destrum/ObjectModel/ObjectId.h>
#include <destrum/ObjectModel/Transform.h> #include <destrum/ObjectModel/Transform.h>
#include <destrum/Assets/AssetReference.h>
#include <destrum/Components/Physics/Collider.h> #include <destrum/Components/Physics/Collider.h>
#include <destrum/Components/Physics/Rigidbody.h> #include <destrum/Components/Physics/Rigidbody.h>
#include <destrum/FS/AssetFS.h>
#include <destrum/Serialization/ComponentFactory.h> #include <destrum/Serialization/ComponentFactory.h>
#include <destrum/Serialization/ComponentRegistry.h> #include <destrum/Serialization/ComponentRegistry.h>
@@ -201,6 +203,78 @@ namespace {
return true; return true;
} }
[[nodiscard]] bool AssetFileExists(const std::string& path) {
if (path.empty()) {
return true;
}
std::filesystem::path fullPath;
if (path.find("://") != std::string::npos) {
try {
fullPath = AssetFS::GetInstance().GetFullPath(path);
} catch (const std::exception&) {
return false;
}
} else {
fullPath = path;
}
return std::filesystem::exists(fullPath);
}
void ValidateAssetReferences(const json& root, const std::filesystem::path& scenePath) {
const auto report = [&](const std::string& objectName, const std::string& detail) {
std::cerr << "Scene asset warning (" << scenePath << "): " << objectName
<< " references \"" << detail << "\" which could not be found\n";
};
for (const auto& objectJson : root.at("objects")) {
const std::string objectName = objectJson.value("name", "GameObject");
if (!objectJson.contains("components")) {
continue;
}
for (const auto& componentJson : objectJson.at("components")) {
if (!componentJson.contains("data")) {
continue;
}
const auto& data = componentJson.at("data");
// MeshRendererComponent: meshKey / materialKey are asset cache keys.
for (const char* key : {"meshKey", "materialKey"}) {
if (data.contains(key) && data.at(key).is_string()) {
const std::string cacheKey = data.at(key).get<std::string>();
if (cacheKey.empty()) continue;
const auto asset = AssetReference::fromCacheKey(cacheKey);
if (asset && !AssetFileExists(asset->path)) {
report(objectName, cacheKey);
}
}
}
// Generic: any nested object with a "path" field is an AssetReference.
std::function<void(const json&)> walk = [&](const json& node) {
if (node.is_object()) {
if (node.contains("path") && node.at("path").is_string()) {
const std::string path = node.at("path").get<std::string>();
if (!path.empty() && !AssetFileExists(path)) {
report(objectName, path);
}
}
for (auto it = node.begin(); it != node.end(); ++it) {
walk(it.value());
}
} else if (node.is_array()) {
for (const auto& element : node) {
walk(element);
}
}
};
walk(data);
}
}
}
} }
bool SceneSerializer::Save(Scene& scene, const std::filesystem::path& path) { bool SceneSerializer::Save(Scene& scene, const std::filesystem::path& path) {
@@ -290,30 +364,45 @@ bool SceneSerializer::Save(Scene& scene, const std::filesystem::path& path) {
return true; return true;
} }
bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) { SceneSerializer::LoadResult SceneSerializer::LoadSceneFile(const std::filesystem::path& path) {
LoadResult result;
std::ifstream file(path);
if (!file.is_open()) {
result.errorMessage = "Failed to open scene file for reading: " + path.string();
std::cerr << result.errorMessage << '\n';
return result;
}
try {
file >> result.root;
if (!ValidateSceneJson(result.root)) {
result.errorMessage = "Invalid scene data: " + path.string();
std::cerr << result.errorMessage << '\n';
return result;
}
} catch (const std::exception& exception) {
result.errorMessage = "Failed to parse scene file: " + std::string(exception.what());
std::cerr << result.errorMessage << '\n';
return result;
}
ValidateAssetReferences(result.root, path);
result.success = true;
return result;
}
bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
std::function<void(float)> progressCallback) {
if (scene.IsIterating()) { if (scene.IsIterating()) {
std::cerr << "Cannot load a scene during an update or render phase: " std::cerr << "Cannot load a scene during an update or render phase: "
<< scene.GetName() << '\n'; << scene.GetName() << '\n';
return false; return false;
} }
RegisterEngineComponents(); if (!result.success) {
std::cerr << "Cannot construct scene from a failed load result: "
std::ifstream file(path); << result.errorMessage << '\n';
if (!file.is_open()) {
std::cerr << "Failed to open scene file for reading: " << path << '\n';
return false;
}
json root;
try {
file >> root;
if (!ValidateSceneJson(root)) {
std::cerr << "Invalid scene data: " << path << '\n';
return false;
}
} catch (const std::exception& exception) {
std::cerr << "Failed to parse scene file: " << exception.what() << '\n';
return false; return false;
} }
@@ -322,9 +411,16 @@ bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) {
return false; return false;
} }
RegisterEngineComponents();
if (progressCallback) progressCallback(0.35f);
const json& root = result.root;
// Build the replacement separately. The current scene is not touched // Build the replacement separately. The current scene is not touched
// until all objects, transforms, and components have loaded successfully. // until all objects, transforms, and components have loaded successfully.
Scene stagingScene(scene.GetName()); // No physics world is created for the staging scene - physics bodies are
// registered directly in the live scene's world once loading succeeds.
Scene stagingScene(scene.GetName(), nullptr);
std::unordered_map<ObjectId, GameObject*> idMap; std::unordered_map<ObjectId, GameObject*> idMap;
std::vector<const json*> objectJsonList; std::vector<const json*> objectJsonList;
std::vector<GameObject*> registeredObjects; std::vector<GameObject*> registeredObjects;
@@ -343,7 +439,16 @@ bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) {
objectJsonList.push_back(&objectJson); objectJsonList.push_back(&objectJson);
} }
stagingScene.CommitPendingAdditions(); if (progressCallback) progressCallback(0.4f);
// Move objects from pending to objects list without physics registration
// (staging scene has no physics world).
for (auto& pending : stagingScene.m_pendingAdditions) {
if (pending) {
stagingScene.m_objects.emplace_back(std::move(pending));
}
}
stagingScene.m_pendingAdditions.clear();
for (const json* objectJson : objectJsonList) { for (const json* objectJson : objectJsonList) {
const ObjectId id = objectJson->at("id").get<ObjectId>(); const ObjectId id = objectJson->at("id").get<ObjectId>();
@@ -392,6 +497,8 @@ bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) {
} }
} }
if (progressCallback) progressCallback(0.5f);
for (const json* objectJson : objectJsonList) { for (const json* objectJson : objectJsonList) {
GameObject* object = idMap.at(objectJson->at("id").get<ObjectId>()); GameObject* object = idMap.at(objectJson->at("id").get<ObjectId>());
if (!objectJson->contains("components")) { if (!objectJson->contains("components")) {
@@ -412,6 +519,8 @@ bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) {
} }
} }
if (progressCallback) progressCallback(0.6f);
for (const auto& objectPtr : stagingScene.GetObjects()) { for (const auto& objectPtr : stagingScene.GetObjects()) {
if (!objectPtr) { if (!objectPtr) {
continue; continue;
@@ -424,6 +533,8 @@ bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) {
} }
} }
if (progressCallback) progressCallback(0.7f);
// Register the replacement bodies in the live physics world before // Register the replacement bodies in the live physics world before
// touching the current scene. If any body fails, the old scene and // touching the current scene. If any body fails, the old scene and
// its physics state can remain intact. // its physics state can remain intact.
@@ -438,6 +549,8 @@ bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) {
registeredObjects.push_back(objectPtr.get()); registeredObjects.push_back(objectPtr.get());
} }
} }
if (progressCallback) progressCallback(0.8f);
} catch (const std::exception& exception) { } catch (const std::exception& exception) {
std::cerr << "Failed to load scene: " << exception.what() << '\n'; std::cerr << "Failed to load scene: " << exception.what() << '\n';
for (GameObject* object : registeredObjects) { for (GameObject* object : registeredObjects) {
@@ -449,6 +562,7 @@ bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) {
return false; return false;
} }
scene.UnloadBindings();
scene.RemoveAll(); scene.RemoveAll();
scene.m_objects = std::move(stagingScene.m_objects); scene.m_objects = std::move(stagingScene.m_objects);
scene.m_pendingAdditions = std::move(stagingScene.m_pendingAdditions); scene.m_pendingAdditions = std::move(stagingScene.m_pendingAdditions);
@@ -456,6 +570,8 @@ bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) {
scene.m_name = root.at("name").get<std::string>(); scene.m_name = root.at("name").get<std::string>();
} }
if (progressCallback) progressCallback(0.9f);
for (const auto& object : scene.m_objects) { for (const auto& object : scene.m_objects) {
if (object) { if (object) {
object->SetScene(&scene); object->SetScene(&scene);
@@ -467,5 +583,27 @@ bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) {
} }
} }
if (progressCallback) progressCallback(1.0f);
return true; return true;
} }
bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path,
std::function<void(float)> progressCallback) {
if (progressCallback) progressCallback(0.0f);
auto result = LoadSceneFile(path);
if (!result.success) {
if (progressCallback) progressCallback(1.0f);
return false;
}
if (progressCallback) progressCallback(0.3f);
return ConstructScene(scene, result, std::move(progressCallback));
}
std::future<SceneSerializer::LoadResult> SceneSerializer::LoadSceneFileAsync(
const std::filesystem::path& path) {
return std::async(std::launch::async, [path]() {
return LoadSceneFile(path);
});
}
+5
View File
@@ -6,6 +6,11 @@ set(GAME_SRC
src/main.cpp src/main.cpp
src/Lightkeeper.cpp src/Lightkeeper.cpp
src/components/GameComponentRegistry.cpp
src/components/FirstPersonController.cpp
src/components/PrintComponent.cpp
src/components/TriggerLoggerComponent.cpp
) )
add_executable(lightkeeper ${GAME_SRC}) add_executable(lightkeeper ${GAME_SRC})
+21 -22
View File
@@ -1,15 +1,16 @@
#ifndef LIGHTKEEPER_H #ifndef LIGHTKEEPER_H
#define LIGHTKEEPER_H #define LIGHTKEEPER_H
#include <random> #include <string>
#include <vector>
#include <destrum/App.h> #include <destrum/App.h>
#include <destrum/Scene/SceneManager.h>
#include <destrum/Graphics/RenderResources.h> #include <destrum/Graphics/RenderResources.h>
#include "destrum/Graphics/Resources/Cubemap.h" #include "destrum/Graphics/Resources/Cubemap.h"
#include "destrum/ObjectModel/GameObject.h" #include "destrum/ObjectModel/GameObject.h"
class FirstPersonController;
class LightKeeper final : public App { class LightKeeper final : public App {
public: public:
LightKeeper(); LightKeeper();
@@ -23,33 +24,31 @@ public:
void onWindowResize(int newWidth, int newHeight) override; void onWindowResize(int newWidth, int newHeight) override;
private: private:
struct FallingStar { void drawDebugMenuBar();
GameObject* object{}; void saveCurrentScene();
float speed{}; void loadScene();
}; void createEmptyScene();
void submitBoxColliderDebugLines();
void resetGame();
void respawnStar(FallingStar& star, float minimumHeight);
void drawHud();
Camera camera{glm::vec3(0.f, 0.f, -5.f), glm::vec3(0, 1, 0)}; Camera camera{glm::vec3(0.f, 0.f, -5.f), glm::vec3(0, 1, 0)};
MeshID sphereMesh{NULL_MESH_ID}; MeshID sphereMesh;
MaterialID playerMaterial{NULL_MATERIAL_ID}; MaterialID sphereMaterial;
MaterialID starMaterial{NULL_MATERIAL_ID};
ImageID texID;
std::unique_ptr<CubeMap> skyboxCubemap; std::unique_ptr<CubeMap> skyboxCubemap;
GameObject* player{nullptr}; GameObject* capybara = nullptr;
std::vector<FallingStar> stars; GameObject* m_Player = nullptr;
std::mt19937 randomEngine{0xD35A1234u}; FirstPersonController* m_FPSController = nullptr;
std::uniform_real_distribution<float> spawnX{-5.5f, 5.5f};
std::uniform_real_distribution<float> spawnHeight{0.0f, 2.0f};
std::uniform_real_distribution<float> spawnSpeed{2.8f, 4.2f};
int score{0}; char scenePath[512]{"scenes/debug_scene.json"};
int misses{0}; char newSceneName[128]{"EmptyScene"};
bool gameOver{false}; std::string sceneStatus;
bool renderBoxColliders{false};
bool renderQuads{true};
bool renderDebugLines{true};
}; };
#endif //LIGHTKEEPER_H #endif //LIGHTKEEPER_H
@@ -0,0 +1,48 @@
#ifndef LIGHTKEEPER_FIRSTPERSONCONTROLLER_H
#define LIGHTKEEPER_FIRSTPERSONCONTROLLER_H
#include <glm/glm.hpp>
#include <destrum/Input/InputManager.h>
#include <destrum/ObjectModel/Component.h>
class Rigidbody;
class CapsuleCollider;
class FirstPersonController final : public Component {
public:
explicit FirstPersonController(GameObject& parent);
std::string GetTypeName() const override { return "FirstPersonController"; }
void Start() override;
void Update(float dt) override;
void FixedUpdate(float fixedDt) override;
void ImGuiInspector() override;
nlohmann::json Serialize() const override;
void Deserialize(const nlohmann::json& data) override;
[[nodiscard]] float GetYaw() const { return m_Yaw; }
[[nodiscard]] float GetPitch() const { return m_Pitch; }
[[nodiscard]] float GetEyeHeight() const { return m_EyeHeight; }
void SetMovementSpeed(float speed) { m_MovementSpeed = speed; }
[[nodiscard]] float GetMovementSpeed() const { return m_MovementSpeed; }
void SetMouseSensitivity(float sens) { m_MouseSensitivity = sens; }
[[nodiscard]] float GetMouseSensitivity() const { return m_MouseSensitivity; }
[[nodiscard]] bool IsMouseCaptured() const { return InputManager::GetInstance().IsMouseCaptured(); }
void SetMouseCaptured(bool captured);
private:
Rigidbody* m_Rigidbody{nullptr};
CapsuleCollider* m_Capsule{nullptr};
float m_Yaw{0.0f};
float m_Pitch{0.0f};
float m_MovementSpeed{5.0f};
float m_MouseSensitivity{0.002f};
float m_EyeHeight{1.6f};
};
#endif
@@ -0,0 +1,13 @@
#ifndef LIGHTKEEPER_GAMECOMPONENTLIST_H
#define LIGHTKEEPER_GAMECOMPONENTLIST_H
#include <components/FirstPersonController.h>
#include <components/PrintComponent.h>
#include <components/TriggerLoggerComponent.h>
#define LIGHTKEEPER_GAME_COMPONENTS(X) \
X(FirstPersonController) \
X(PrintComponent) \
X(TriggerLoggerComponent)
#endif // LIGHTKEEPER_GAMECOMPONENTLIST_H
@@ -0,0 +1,6 @@
#ifndef LIGHTKEEPER_GAMECOMPONENTREGISTRY_H
#define LIGHTKEEPER_GAMECOMPONENTREGISTRY_H
void RegisterGameComponents();
#endif // LIGHTKEEPER_GAMECOMPONENTREGISTRY_H
@@ -0,0 +1,28 @@
#ifndef DESTRUM_PRINTCOMPONENT_H
#define DESTRUM_PRINTCOMPONENT_H
#include "destrum/ObjectModel/Component.h"
class PrintComponent: public Component {
public:
PrintComponent(GameObject& parent);
std::string GetTypeName() const override { return "PrintComponent"; }
void Start() override;
void Update(float dt) override;
void ImGuiInspector() override;
nlohmann::json Serialize() const override;
void Deserialize(const nlohmann::json&) override;
void SetMessage(std::string_view message)
{
m_message = message;
}
private:
std::string m_message{};
};
#endif //DESTRUM_PRINTCOMPONENT_H
@@ -0,0 +1,22 @@
#ifndef DESTRUM_TRIGGERLOGGERCOMPONENT_H
#define DESTRUM_TRIGGERLOGGERCOMPONENT_H
#include "destrum/ObjectModel/Component.h"
class TriggerLoggerComponent: public Component {
public:
TriggerLoggerComponent(GameObject& parent);
std::string GetTypeName() const override { return "TriggerLoggerComponent"; }
void OnTriggerEnter(GameObject* other) override;
void OnTriggerExit(GameObject* other) override;
nlohmann::json Serialize() const override;
void Deserialize(const nlohmann::json&) override;
int triggerEnterCount{0};
int triggerExitCount{0};
std::string lastOtherName;
};
#endif //DESTRUM_TRIGGERLOGGERCOMPONENT_H
+659 -192
View File
@@ -1,35 +1,60 @@
#include "Lightkeeper.h" #include "Lightkeeper.h"
#include <algorithm> #include <array>
#include <cmath> #include <cmath>
#include <string>
#include <SDL.h>
#include <glm/gtc/constants.hpp>
#include <glm/gtx/transform.hpp>
#include <imgui.h>
#include <destrum/Assets/AssetManager.h>
#include <destrum/Components/MeshRendererComponent.h>
#include <destrum/FS/AssetFS.h> #include <destrum/FS/AssetFS.h>
#include <destrum/ObjectModel/GameObject.h> #include <destrum/Assets/AssetManager.h>
#include <destrum/Scene/Scene.h> #include <destrum/Graphics/Managers/LineRenderingManager.h>
#include <destrum/Scene/SceneManager.h> #include <destrum/Graphics/Managers/QuadRenderingManager.h>
#include <destrum/Util/ModelDoc.h> #include "glm/gtx/transform.hpp"
#include <destrum/Util/ModelDocUtils.h>
#include "spdlog/spdlog.h" #include "spdlog/spdlog.h"
#include <destrum/Components/Physics/Rigidbody.h>
#include <destrum/Components/Physics/BoxCollider.h>
#include <destrum/Scene/Scene.h>
#include <destrum/Serialization/SceneSerializer.h>
#include "destrum/Components/MeshRendererComponent.h"
#include "destrum/Components/Rotator.h"
#include "destrum/Components/Spinner.h"
#include "destrum/Components/OrbitAndSpin.h"
#include "destrum/ObjectModel/GameObject.h"
#include "destrum/Util/ModelDoc.h"
#include "destrum/Components/Animator.h"
#include <components/FirstPersonController.h>
#include <components/TriggerLoggerComponent.h>
#include <filesystem>
#include <stdexcept>
#include <string>
#include <string_view>
#include "imgui.h"
#include "components/GameComponentRegistry.h"
#include "components/PrintComponent.h"
#include "destrum/Components/Physics/SphereCollider.h"
#include "destrum/Util/ModelDocUtils.h"
namespace { namespace {
constexpr int kStarCount = 3; std::filesystem::path ResolveScenePath(
constexpr int kMaxMisses = 3; std::string_view value,
constexpr float kPlayerY = -5.0f; const std::filesystem::path& exeDir) {
constexpr float kCatchY = -4.25f; if (value.empty()) {
constexpr float kPlayerSpeed = 7.0f; throw std::invalid_argument("Scene path cannot be empty");
constexpr float kPlayerHalfWidth = 1.15f; }
if (value.find("://") != std::string_view::npos) {
return AssetFS::GetInstance().GetFullPath(value);
}
const std::filesystem::path path{value};
return path.is_absolute() ? path : exeDir / path;
}
} }
LightKeeper::LightKeeper() = default; LightKeeper::LightKeeper() : App()
{
}
LightKeeper::~LightKeeper() LightKeeper::~LightKeeper()
{ {
@@ -43,185 +68,613 @@ LightKeeper::~LightKeeper()
void LightKeeper::customInit() void LightKeeper::customInit()
{ {
RegisterGameComponents();
resources.init(gfxDevice); resources.init(gfxDevice);
renderer.init(gfxDevice, resources, m_params.renderSize); renderer.init(gfxDevice, resources, m_params.renderSize);
camera.m_position = glm::vec3{0.0f, 0.0f, -10.0f}; const float aspectRatio = static_cast<float>(m_params.renderSize.x) / static_cast<float>(m_params.renderSize.y);
camera.SetRotation(glm::radians(90.0f), 0.0f); camera.setAspectRatio(aspectRatio);
camera.setAspectRatio(
static_cast<float>(m_params.renderSize.x) /
static_cast<float>(m_params.renderSize.y));
const auto skyboxID = ModelDoc::LoadOptions staticModelOptions{};
AssetFS::GetInstance().GetCookedPathForFile("game://starmap_2020_4k.exr"); staticModelOptions.meshImportMode = ModelDoc::MeshImportMode::MergedPerNode;
const auto vertShaderPath = staticModelOptions.loadMaterials = true;
AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/cubemap.vert"); staticModelOptions.loadSkeleton = false;
const auto fragShaderPath = staticModelOptions.loadAnimations = false;
AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/cubemap.frag");
camera.SetRotation(glm::radians(glm::vec2(90.f, 0.f)));
auto& scene = SceneManager::GetInstance().CreateScene("Main");
// scene.Add(testCube);
// 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().GetCookedPathForFile("game://starmap_2020_4k.exr");
//
// const auto skyboxID = AssetFS::GetInstance().GetFullPath("engine://textures/test-skybox.png");
//
const auto vertShaderPath = AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/cubemap.vert");
const auto fragShaderPath = AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/cubemap.frag");
//
skyboxCubemap = std::make_unique<CubeMap>(); skyboxCubemap = std::make_unique<CubeMap>();
skyboxCubemap->LoadCubeMap(gfxDevice, resources, skyboxID); skyboxCubemap->LoadCubeMap(gfxDevice, resources, skyboxID.generic_string());
skyboxCubemap->InitCubemapPipeline( skyboxCubemap->InitCubemapPipeline(gfxDevice, resources, vertShaderPath.generic_string(), fragShaderPath.generic_string());
gfxDevice,
resources,
vertShaderPath.generic_string(),
fragShaderPath.generic_string());
skyboxCubemap->CreateCubeMap(gfxDevice, resources); skyboxCubemap->CreateCubeMap(gfxDevice, resources);
renderer.setSkyboxTexture(skyboxCubemap->GetCubeMapImageID()); renderer.setSkyboxTexture(skyboxCubemap->GetCubeMapImageID());
ModelDoc::LoadOptions modelOptions{}; const auto planeObj = scene.CreateGameObject("GroundPlane");
modelOptions.meshImportMode = ModelDoc::MeshImportMode::MergedPerNode; const auto planeMeshComp = planeObj->AddComponent<MeshRendererComponent>();
modelOptions.loadMaterials = true;
modelOptions.loadSkeleton = false;
modelOptions.loadAnimations = false;
const auto sphereModel = AssetManager::GetInstance().LoadModel( auto planeModel = ModelDoc::LoadModel(
"engine://sphere.fbx", AssetFS::GetInstance().GetFullPath("game://plane.glb").generic_string(),
modelOptions); staticModelOptions
const auto& spherePrimitive = ModelDocUtils::GetFirstPrimitiveOrThrow( );
sphereModel, ModelDocUtils::LogModelDocSummary(planeModel, "plane.glb");
"engine://sphere.fbx");
const auto& planePrimitive = ModelDocUtils::GetFirstPrimitiveOrThrow(planeModel, "game://plane.glb");
const auto planeMeshID = resources.meshes().addMesh(gfxDevice, planePrimitive.mesh);
const auto planeTexturePath = ModelDocUtils::PickTexturePath(
planeModel,
planePrimitive,
AssetFS::GetInstance().GetFullPath("game://grass.png")
);
const auto planeTextureID = resources.loadImageFromFile(gfxDevice, planeTexturePath);
const auto planeMaterialID = resources.materials().addMaterial({
.baseColor = ModelDocUtils::GetImportedBaseColor(
planeModel, planePrimitive),
.textureFilteringMode = TextureFilteringMode::Nearest,
.diffuseTexture = planeTextureID,
.name = ModelDocUtils::GetImportedMaterialName(
planeModel, planePrimitive, "GroundPlaneMaterial"),
});
planeMeshComp->SetMeshID(planeMeshID);
planeMeshComp->SetMaterialID(planeMaterialID);
planeObj->GetTransform().SetWorldPosition(glm::vec3(0.f, -1.0f, 0.f));
planeObj->GetTransform().SetWorldScale(glm::vec3(10.f, 1.f, 10.f));
planeObj->AddComponent<BoxCollider>(glm::vec3{10.0f, 0.5f, 10.0f});
auto* floorRb = planeObj->AddComponent<Rigidbody>();
floorRb->SetType(RigidbodyType::Static);
scene.GetPhysics().RegisterGameObject(*planeObj);
const auto CharObj = scene.CreateGameObject("Character");
capybara = CharObj;
ModelDoc::LoadOptions characterModelOptions{};
characterModelOptions.meshImportMode = ModelDoc::MeshImportMode::MergedPerNode;
characterModelOptions.loadMaterials = true;
characterModelOptions.loadSkeleton = true;
characterModelOptions.loadAnimations = true;
// auto charModel = ModelDoc::LoadModel(
// AssetFS::GetInstance().GetFullPath("engine://cotw-capybara-male/source/capybara.fbx").generic_string(),
// characterModelOptions
// );
// ModelDocUtils::LogModelDocSummary(charModel, "capybara.fbx");
//
// const auto& charPrimitive = ModelDocUtils::GetFirstSkinnedPrimitiveOrFirstOrThrow(
// charModel,
// "engine://cotw-capybara-male/source/capybara.fbx"
// );
//
// const auto charMeshID = resources.meshes().addMesh(gfxDevice, charPrimitive.mesh);
//
// const auto charTexturePath = ModelDocUtils::PickTexturePath(
// charModel,
// charPrimitive,
// AssetFS::GetInstance().GetFullPath(
// "engine://cotw-capybara-male/textures/capybara_male_light_brown_dif.ddsc.DECA.RE.pngballs")
// );
//
// const auto charTextureID = resources.loadImageFromFile(gfxDevice, charTexturePath);
// // const auto charTextureID = gfxDevice.loadImageFromFile(AssetFS::GetInstance().GetFullPath("engine://char.jpg"));
// const auto charMaterialID = resources.materials().addMaterial({
// .baseColor = ModelDocUtils::GetImportedBaseColor(
// charModel, charPrimitive),
// .diffuseTexture = charTextureID,
// .name = ModelDocUtils::GetImportedMaterialName(
// charModel, charPrimitive, "CharacterMaterial"),
// });
//
// const auto charMeshComp = CharObj->AddComponent<MeshRendererComponent>();
// charMeshComp->SetMeshID(charMeshID);
// charMeshComp->SetMaterialID(charMaterialID);
//
// const auto animator = CharObj->AddComponent<Animator>();
// animator->setSkeleton(std::move(charModel.skeleton));
//
// std::string firstAnimationName;
//
// for (auto& clip : charModel.animations)
// {
// spdlog::info("Loaded animation: '{}' ({:.2f}s)", clip.name, clip.duration);
//
// if (firstAnimationName.empty())
// firstAnimationName = clip.name;
//
// animator->addClip(std::make_shared<SkeletalAnimation>(std::move(clip)));
// }
//
// if (!firstAnimationName.empty())
// {
// spdlog::info("Playing animation: '{}'", firstAnimationName);
// animator->play(firstAnimationName);
// }
//
// animator->play("capybara_canter_fwd_01|capybara_canter_fwd_01|run");
//
// CharObj->GetTransform().SetWorldPosition(glm::vec3(0.f, 0.f, 0.f));
// CharObj->GetTransform().SetWorldScale(0.01f, 0.01f, 0.01f);
// ModelDoc::LoadOptions options{};
// options.meshImportMode = ModelDoc::MeshImportMode::MergedPerNode;
//
// auto model = ModelDoc::LoadModel(
// AssetFS::GetInstance()
// .GetFullPath("engine://multiple_object_test.fbx")
// .generic_string(),
// options
// );
//
// ModelDocUtils::LogModelDocSummary(model, "engine://multiple_object_test.fbx");
//
// auto root = std::make_shared<GameObject>("MultiMeshModel");
// root->GetTransform().SetWorldPosition(glm::vec3(0.1f, 0.1f, 0.1f));
// scene.Add(root);
//
// for (std::size_t i = 0; i < model.primitives.size(); ++i) {
// const auto &primitive = model.primitives[i];
//
// const auto meshID = meshCache.addMesh(gfxDevice, primitive.mesh);
//
// const auto texturePath = ModelDocUtils::PickTexturePath(
// model,
// primitive,
// AssetFS::GetInstance().GetFullPath("engine://textures/white.png")
// );
//
// const auto textureID = gfxDevice.loadImageFromFile(texturePath);
//
// const auto materialID = materialCache.addMaterial(gfxDevice, {
// .baseColor = ModelDocUtils::GetImportedBaseColor(
// model, primitive),
// .diffuseTexture = textureID,
// .name = ModelDocUtils::GetImportedMaterialName(
// model,
// primitive,
// primitive.mesh.name + "_Material"
// ),
// });
//
// auto part = std::make_shared<GameObject>(
// primitive.mesh.name.empty()
// ? "Primitive_" + std::to_string(i)
// : primitive.mesh.name
// );
//
// auto meshComp = part->AddComponent<MeshRendererComponent>();
// meshComp->SetMeshID(meshID);
// meshComp->SetMaterialID(materialID);
//
// scene.Add(part);
// }
// {
// const auto CharObj = scene.CreateGameObject("Character");
//
// ModelDoc::LoadOptions characterOptions{};
// characterOptions.meshImportMode = ModelDoc::MeshImportMode::MergedPerNode;
// characterOptions.loadMaterials = true;
// characterOptions.loadSkeleton = true;
// characterOptions.loadAnimations = false;
//
// auto charModel = AssetManager::GetInstance().LoadModel("engine://characterMedium.fbx", characterOptions);
//
// const auto& charPrimitive =
// ModelDocUtils::GetFirstSkinnedPrimitiveOrFirstOrThrow(
// charModel,
// "engine://characterMedium.fbx"
// );
//
// const auto charMeshID = resources.meshes().addMesh(gfxDevice, charPrimitive.mesh);
//
// const auto charTextureID = resources.loadImageFromFile(gfxDevice,
// AssetFS::GetInstance().GetFullPath("engine://textures/criminalMaleA.png")
// );
//
// const auto charMaterialID = resources.materials().addMaterial({
// .baseColor = ModelDocUtils::GetImportedBaseColor(
// charModel, charPrimitive),
// .diffuseTexture = charTextureID,
// .name = ModelDocUtils::GetImportedMaterialName(
// charModel,
// charPrimitive,
// "CharacterMaterial"
// ),
// });
//
// const auto charMeshComp = CharObj->AddComponent<MeshRendererComponent>();
// charMeshComp->SetMeshID(charMeshID);
// charMeshComp->SetMaterialID(charMaterialID);
//
// const auto animator = CharObj->AddComponent<Animator>();
// animator->setSkeleton(charModel.skeleton);
//
// auto runClips = AssetManager::GetInstance().LoadAnimationClips(
// "engine://run.fbx",
// charModel.skeleton
// );
//
// for (auto& clip : runClips)
// {
// spdlog::info("Loaded animation: '{}' ({:.2f}s)", clip.name, clip.duration);
// animator->addClip(std::make_shared<SkeletalAnimation>(std::move(clip)));
// }
//
// if (!runClips.empty())
// {
// animator->play("Root|Run");
// }
//
// CharObj->GetTransform().SetWorldPosition(glm::vec3(0.f));
// CharObj->GetTransform().SetWorldPosition(glm::vec3(5, 0, 0));
// }
//
// auto cubeModel = ModelDoc::LoadModel(
// AssetFS::GetInstance()
// .GetFullPath("engine://cube.fbx")
// .generic_string(),
// staticModelOptions
// );
// ModelDocUtils::LogModelDocSummary(cubeModel, "cube.fbx");
//
// const auto& cubePrimitive = ModelDocUtils::GetFirstPrimitiveOrThrow(cubeModel, "game://cube.fbx");
//
//
// const auto eliasTextuerPath = ModelDocUtils::PickTexturePath(
// cubeModel,
// cubePrimitive,
// AssetFS::GetInstance().GetFullPath("game://grass.png")
// );
//
// const auto eliasTextueID = resources.loadImageFromFile(gfxDevice, eliasTextuerPath);
// const auto eliasMaterialID = resources.materials().addMaterial({
// .baseColor = ModelDocUtils::GetImportedBaseColor(
// planeModel, planePrimitive),
// .textureFilteringMode =
// TextureFilteringMode::Anisotropic,
// .diffuseTexture = eliasTextueID,
// .name = ModelDocUtils::GetImportedMaterialName(
// planeModel, planePrimitive, "GroundPlaneMaterial"),
// });
//
// const auto cubeMeshID = resources.meshes().addMesh(gfxDevice, cubePrimitive.mesh);
// //
// // for (int i{0}; i < 100; i++)
// // {
// // auto cube = std::make_shared<GameObject>("Cube");
// //
// // cube->AddComponent<BoxCollider>(glm::vec3{0.5f});
// // cube->AddComponent<Rigidbody>();
// //
// // auto meshComp = cube->AddComponent<MeshRendererComponent>();
// // meshComp->SetMeshID(cubeMeshID);
// // meshComp->SetMaterialID(eliasMaterialID);
// //
// // cube->GetTransform().SetWorldPosition(glm::vec3(0.0f, i, 0.0f));
// // cube->GetTransform().SetWorldScale(glm::vec3(0.005f));
// //
// // scene.Add(cube);
// // scene.GetPhysics().RegisterGameObject(*cube);
// // }
//
// // const int cubeCount = 10;
// // const float spacing = 1.0f;
// //
// // for (int x = 0; x < cubeCount; x++)
// // {
// // for (int y = 0; y < 3; y++)
// // {
// // for (int z = 0; z < cubeCount; z++)
// // {
// // auto cube = std::make_shared<GameObject>("Cube");
// //
// // cube->AddComponent<BoxCollider>(glm::vec3{0.5f});
// // cube->AddComponent<Rigidbody>();
// //
// // auto meshComp = cube->AddComponent<MeshRendererComponent>();
// // meshComp->SetMeshID(cubeMeshID);
// // meshComp->SetMaterialID(eliasMaterialID);
// //
// // cube->GetTransform().SetWorldPosition(glm::vec3(
// // (x - cubeCount / 2.0f) * spacing,
// // y * spacing + 0,
// // (z - cubeCount / 2.0f) * spacing
// // ));
// //
// // cube->GetTransform().SetWorldScale(glm::vec3(0.005f));
// //
// // scene.Add(cube);
// // scene.GetPhysics().RegisterGameObject(*cube);
// // }
// // }
// // }
// {
auto sphereModel = AssetManager::GetInstance().LoadModel("engine://sphere.fbx", staticModelOptions);
ModelDocUtils::LogModelDocSummary(sphereModel, "sphere.fbx");
//
const auto& spherePrimitive = ModelDocUtils::GetFirstPrimitiveOrThrow(sphereModel, "game://sphere.fbx");
//
//
// const auto sphereTexturePath = ModelDocUtils::PickTexturePath(
// sphereModel,
// spherePrimitive,
// AssetFS::GetInstance().GetFullPath("game://238882.png")
// );
//
// const auto sphereTextureID = resources.loadImageFromFile(gfxDevice, sphereTexturePath);
// sphereMaterial = resources.materials().addMaterial({
// .baseColor = ModelDocUtils::GetImportedBaseColor(planeModel, planePrimitive),
// .textureFilteringMode =TextureFilteringMode::Anisotropic,
// .diffuseTexture = sphereTextureID,
// .name = ModelDocUtils::GetImportedMaterialName(
// planeModel, planePrimitive,
// "GroundPlaneMaterial"),
// });
//
sphereMesh = resources.meshes().addMesh(gfxDevice, spherePrimitive.mesh); sphereMesh = resources.meshes().addMesh(gfxDevice, spherePrimitive.mesh);
//
// }
playerMaterial = resources.materials().addSimpleColorMaterial( sphereMaterial = resources.materials().addSimpleColorMaterial({0.5f, 0.3f, 0.8f}, "Blue");
{0.08f, 0.75f, 1.0f},
"Catcher Blue");
starMaterial = resources.materials().addSimpleColorMaterial(
{1.0f, 0.55f, 0.05f},
"Falling Star Orange");
auto& scene = SceneManager::GetInstance().CreateScene("Star Catcher");
player = scene.CreateGameObject("Catcher"); // Trigger zone: a static box with a TriggerLoggerComponent.
auto* playerRenderer = player->AddComponent<MeshRendererComponent>(); // Spheres spawned via the "Spawn ball" button fall through it.
playerRenderer->SetMeshID(sphereMesh); auto triggerObj = scene.CreateGameObject("TriggerBox");
playerRenderer->SetMaterialID(playerMaterial); auto triggerBox = triggerObj->AddComponent<BoxCollider>(glm::vec3{3.0f, 3.0f, 3.0f});
player->GetTransform().SetLocalPosition({0.0f, kPlayerY, 0.0f}); triggerBox->SetTrigger(true);
player->GetTransform().SetLocalScale(glm::vec3{0.012f, 0.0035f, 0.009f}); auto triggerRb = triggerObj->AddComponent<Rigidbody>();
triggerRb->SetType(RigidbodyType::Static);
triggerObj->AddComponent<TriggerLoggerComponent>();
triggerObj->GetTransform().SetWorldPosition({0.0f, 0.0f, 0.0f});
stars.reserve(kStarCount); auto playerObj = scene.CreateGameObject("Player");
for (int index = 0; index < kStarCount; ++index) { playerObj->GetTransform().SetWorldPosition(glm::vec3(0.0f, 2.0f, -5.0f));
auto* starObject = scene.CreateGameObject( auto* fps = playerObj->AddComponent<FirstPersonController>();
"Star " + std::to_string(index + 1)); m_Player = playerObj;
auto* starRenderer = starObject->AddComponent<MeshRendererComponent>(); m_FPSController = fps;
starRenderer->SetMeshID(sphereMesh); scene.GetPhysics().RegisterGameObject(*playerObj);
starRenderer->SetMaterialID(starMaterial);
starObject->GetTransform().SetLocalScale(glm::vec3{0.0045f});
stars.push_back({starObject, 0.0f});
}
resetGame(); const auto texPath = AssetFS::GetInstance().GetFullPath("engine://textures/kobe.png");
} texID = resources.loadImageFromFile(gfxDevice, texPath);
void LightKeeper::respawnStar(FallingStar& star, float minimumHeight)
{
star.speed = spawnSpeed(randomEngine) + static_cast<float>(score) * 0.08f;
star.object->SetActive(true);
star.object->GetTransform().SetLocalPosition({
spawnX(randomEngine),
minimumHeight + spawnHeight(randomEngine),
0.0f});
}
void LightKeeper::resetGame()
{
score = 0;
misses = 0;
gameOver = false;
if (player != nullptr) {
player->SetActive(true);
player->GetTransform().SetLocalPosition({0.0f, kPlayerY, 0.0f});
}
for (std::size_t index = 0; index < stars.size(); ++index) {
respawnStar(stars[index], 6.5f + static_cast<float>(index) * 0.85f);
}
} }
void LightKeeper::customUpdate(float dt) void LightKeeper::customUpdate(float dt)
{ {
auto& input = InputManager::GetInstance(); // LightKeeper owns a camera that hides App::camera; update this instance
// after SDL events have been consumed.
if (m_FPSController != nullptr && m_FPSController->IsMouseCaptured())
{
auto& playerTransform = m_Player->GetTransform();
const glm::vec3 playerPos = playerTransform.GetWorldPosition();
camera.SetRotation(m_FPSController->GetYaw(), m_FPSController->GetPitch());
camera.m_position = playerPos + glm::vec3(0.0f, m_FPSController->GetEyeHeight(), 0.0f);
camera.CalculateViewMatrix();
camera.CalculateProjectionMatrix();
}
else
{
camera.Update(dt);
}
SceneManager::GetInstance().Update(dt);
SceneManager::GetInstance().LateUpdate(dt);
if (input.WasKeyPressed(SDL_SCANCODE_ESCAPE)) { if (m_params.showDebugUi) {
isRunning = false; drawDebugMenuBar();
}
LineRenderingManager::GetInstance().SubmitLine({0, 0, 0}, {0, 10, 0}, {255, 0, 0, 255});
if (InputManager::GetInstance().WasKeyPressed(SDL_SCANCODE_1))
{
renderer.setRenderWireframe(!renderer.getRenderWireframe());
}
if (m_params.showDebugUi) {
ImGui::Begin("Test");
if (ImGui::Button("SPawn ball"))
{
auto sphere = SceneManager::GetInstance().GetCurrentScene().CreateGameObject("Sphere");
sphere->AddComponent<SphereCollider>(1.5f);
auto rb = sphere->AddComponent<Rigidbody>();
rb->SetMass(1000);
auto meshRenderComp = sphere->AddComponent<MeshRendererComponent>();
meshRenderComp->SetMaterialID(sphereMaterial);
meshRenderComp->SetMeshID(sphereMesh);
sphere->GetTransform().SetWorldPosition((std::rand() % 2) - 0.5f, 100, (std::rand() % 2) - 0.5f);
sphere->GetTransform().SetWorldScale(glm::vec3{0.015f});
SceneManager::GetInstance().GetCurrentScene().GetPhysics().RefreshGameObject(*sphere);
}
ImGui::End();
}
}
void LightKeeper::drawDebugMenuBar()
{
if (!ImGui::BeginMainMenuBar()) {
return; return;
} }
if (input.WasKeyPressed(SDL_SCANCODE_R)) { if (ImGui::BeginMenu("File")) {
resetGame(); ImGui::TextDisabled("Scene");
ImGui::SetNextItemWidth(320.0f);
ImGui::InputText("Scene path", scenePath, sizeof(scenePath));
ImGui::SetNextItemWidth(320.0f);
ImGui::InputText("New scene name", newSceneName, sizeof(newSceneName));
ImGui::Separator();
if (ImGui::MenuItem("New Empty Scene")) {
createEmptyScene();
}
if (ImGui::MenuItem("Load")) {
loadScene();
}
if (ImGui::MenuItem("Save")) {
saveCurrentScene();
}
if (!sceneStatus.empty()) {
ImGui::Separator();
ImGui::TextWrapped("%s", sceneStatus.c_str());
}
ImGui::EndMenu();
} }
if (!gameOver) { if (ImGui::BeginMenu("Render")) {
float moveDirection = 0.0f; ImGui::MenuItem("Box Colliders", nullptr, &renderBoxColliders);
if (input.IsKeyDown(SDL_SCANCODE_A) || input.IsKeyDown(SDL_SCANCODE_LEFT)) { ImGui::MenuItem("2D Quads", nullptr, &renderQuads);
moveDirection += 1.0f; ImGui::MenuItem("Debug Lines", nullptr, &renderDebugLines);
} ImGui::EndMenu();
if (input.IsKeyDown(SDL_SCANCODE_D) || input.IsKeyDown(SDL_SCANCODE_RIGHT)) {
moveDirection -= 1.0f;
}
glm::vec3 playerPosition = player->GetTransform().GetLocalPosition();
playerPosition.x = std::clamp(
playerPosition.x + moveDirection * kPlayerSpeed * dt,
-5.8f,
5.8f);
player->GetTransform().SetLocalPosition(playerPosition);
for (auto& star : stars) {
glm::vec3 starPosition = star.object->GetTransform().GetLocalPosition();
starPosition.y -= star.speed * dt;
star.object->GetTransform().SetLocalPosition(starPosition);
if (starPosition.y > kCatchY) {
continue;
}
const bool caught = std::abs(starPosition.x - playerPosition.x) <=
kPlayerHalfWidth;
if (caught) {
++score;
} else {
++misses;
if (misses >= kMaxMisses) {
gameOver = true;
}
}
if (!gameOver) {
respawnStar(star, 7.0f);
}
}
} }
SceneManager::GetInstance().Update(dt); ImGui::EndMainMenuBar();
SceneManager::GetInstance().LateUpdate(dt);
drawHud();
} }
void LightKeeper::drawHud() void LightKeeper::saveCurrentScene()
{ {
constexpr ImGuiWindowFlags flags = try {
ImGuiWindowFlags_NoDecoration | const auto path = ResolveScenePath(scenePath, m_params.exeDir);
ImGuiWindowFlags_AlwaysAutoResize | const auto parent = path.parent_path();
ImGuiWindowFlags_NoSavedSettings | if (!parent.empty()) {
ImGuiWindowFlags_NoNav; std::filesystem::create_directories(parent);
}
ImGui::SetNextWindowPos({24.0f, 24.0f}, ImGuiCond_Always); if (SceneSerializer::Save(
ImGui::SetNextWindowBgAlpha(0.65f); SceneManager::GetInstance().GetCurrentScene(),
ImGui::Begin("Star Catcher HUD", nullptr, flags); path)) {
ImGui::Text("STAR CATCHER"); sceneStatus = "Saved scene to " + path.string();
ImGui::Separator(); } else {
ImGui::Text("Score %03d", score); sceneStatus = "Failed to save scene to " + path.string();
ImGui::Text("Lives %d", kMaxMisses - misses); }
ImGui::TextDisabled("A/D or arrow keys to move"); } catch (const std::exception& exception) {
sceneStatus = std::string{"Save failed: "} + exception.what();
if (gameOver) { }
ImGui::Separator(); }
ImGui::Text("GAME OVER");
ImGui::Text("Press R to play again"); void LightKeeper::loadScene()
{
try {
const auto path = ResolveScenePath(scenePath, m_params.exeDir);
if (SceneSerializer::Load(
SceneManager::GetInstance().GetCurrentScene(),
path)) {
sceneStatus = "Loaded scene from " + path.string();
} else {
sceneStatus = "Failed to load scene from " + path.string();
}
} catch (const std::exception& exception) {
sceneStatus = std::string{"Load failed: "} + exception.what();
}
}
void LightKeeper::createEmptyScene()
{
try {
const std::string name = newSceneName[0] != '\0'
? newSceneName
: "EmptyScene";
auto& sceneManager = SceneManager::GetInstance();
const int newSceneIndex = sceneManager.GetSceneCount();
sceneManager.CreateScene(name);
sceneManager.SwitchScene(newSceneIndex);
sceneStatus = "Created and switched to scene '" + name + "'";
} catch (const std::exception& exception) {
sceneStatus = std::string{"New scene failed: "} + exception.what();
}
}
void LightKeeper::submitBoxColliderDebugLines()
{
if (!renderBoxColliders) {
return;
}
auto& sceneManager = SceneManager::GetInstance();
if (sceneManager.GetSceneCount() == 0) {
return;
}
Scene& scene = sceneManager.GetCurrentScene();
auto& lineManager = LineRenderingManager::GetInstance();
constexpr std::array<std::array<std::size_t, 2>, 12> edges{{
{{0, 1}}, {{1, 3}}, {{3, 2}}, {{2, 0}},
{{4, 5}}, {{5, 7}}, {{7, 6}}, {{6, 4}},
{{0, 4}}, {{1, 5}}, {{2, 6}}, {{3, 7}},
}};
for (const auto& objectPtr : scene.GetObjects()) {
if (!objectPtr || objectPtr->IsBeingDestroyed() ||
!objectPtr->IsActiveInHierarchy()) {
continue;
}
const auto* boxCollider = objectPtr->GetComponent<BoxCollider>();
if (boxCollider == nullptr) {
continue;
}
// Collider dimensions are already in world units. Physics does not
// apply the GameObject scale when constructing the shape.
const glm::vec3 center = boxCollider->GetCenterOffset();
const glm::vec3 halfExtents = boxCollider->GetHalfExtents();
const std::array<glm::vec3, 8> localCorners{
center + glm::vec3{-halfExtents.x, -halfExtents.y, -halfExtents.z},
center + glm::vec3{ halfExtents.x, -halfExtents.y, -halfExtents.z},
center + glm::vec3{-halfExtents.x, halfExtents.y, -halfExtents.z},
center + glm::vec3{ halfExtents.x, halfExtents.y, -halfExtents.z},
center + glm::vec3{-halfExtents.x, -halfExtents.y, halfExtents.z},
center + glm::vec3{ halfExtents.x, -halfExtents.y, halfExtents.z},
center + glm::vec3{-halfExtents.x, halfExtents.y, halfExtents.z},
center + glm::vec3{ halfExtents.x, halfExtents.y, halfExtents.z},
};
Transform& transform = objectPtr->GetTransform();
const glm::vec3 worldPosition = transform.GetWorldPosition();
const glm::quat worldRotation = transform.GetWorldRotation();
std::array<glm::vec3, 8> worldCorners{};
for (std::size_t index = 0; index < localCorners.size(); ++index) {
worldCorners[index] = worldPosition + worldRotation * localCorners[index];
}
const glm::vec4 color = boxCollider->IsTrigger()
? glm::vec4{1.0f, 0.75f, 0.1f, 1.0f}
: glm::vec4{0.1f, 0.8f, 1.0f, 1.0f};
for (const auto& edge : edges) {
lineManager.SubmitLine(
worldCorners[edge[0]],
worldCorners[edge[1]],
color);
}
} }
ImGui::End();
} }
void LightKeeper::customDraw() void LightKeeper::customDraw()
@@ -232,42 +685,59 @@ void LightKeeper::customDraw()
} }
renderer.beginDrawing(gfxDevice); renderer.beginDrawing(gfxDevice);
submitBoxColliderDebugLines();
const GameRenderer::SceneData sceneData{ QuadRenderingManager::GetInstance().SubmitQuad(
.camera = camera, glm::vec2{100.0f, 100.0f}, glm::vec2{200.0f, 200.0f}, 0.0f,
.ambientColor = glm::vec3{0.1f}, glm::vec4{1.0f}, texID);
.ambientIntensity = 0.5f,
.fogColor = glm::vec3{0.02f, 0.03f, 0.08f}, const RenderContext ctx{
.fogDensity = 0.01f};
const RenderContext context{
.renderer = renderer, .renderer = renderer,
.camera = camera, .camera = camera,
.sceneData = sceneData}; .sceneData = {
.camera = camera,
.ambientColor = glm::vec3(0.1f),
.ambientIntensity = 0.5f,
.fogColor = glm::vec3(0.5f),
.fogDensity = 0.01f
}
};
SceneManager::GetInstance().Render(context); SceneManager::GetInstance().Render(ctx);
renderer.endDrawing(); renderer.endDrawing();
renderer.draw(cmd, gfxDevice, camera, sceneData); const auto& drawImage = renderer.getDrawImage();
renderer.draw(
cmd, gfxDevice, camera, GameRenderer::SceneData{
camera, glm::vec3(0.1f), 0.5f, glm::vec3(0.5f), 0.01f
});
gfxDevice.endFrame( gfxDevice.endFrame(
cmd, cmd, drawImage, {
renderer.getDrawImage(), .clearColor = {{0.f, 0.f, 0.5f, 1.f}},
{
.clearColor = {{0.005f, 0.008f, 0.03f, 1.0f}},
.drawImageBlitRect = glm::ivec4{}, .drawImageBlitRect = glm::ivec4{},
.imguiPass = &imguiPass}); .imguiPass = &imguiPass,
});
} }
void LightKeeper::customCleanup() void LightKeeper::customCleanup()
{ {
// auto device = gfxDevice.getDevice().device;
// vkDeviceWaitIdle(device);
gfxDevice.waitIdle(); gfxDevice.waitIdle();
stars.clear(); SceneManager::GetInstance().Destroy();
player = nullptr;
if (skyboxCubemap) { if (skyboxCubemap)
{
skyboxCubemap->cleanup(gfxDevice); skyboxCubemap->cleanup(gfxDevice);
skyboxCubemap.reset(); skyboxCubemap.reset();
} }
renderer.cleanup(gfxDevice);
} }
void LightKeeper::customFixedUpdate(float dt) void LightKeeper::customFixedUpdate(float dt)
@@ -277,10 +747,7 @@ void LightKeeper::customFixedUpdate(float dt)
void LightKeeper::onWindowResize(int newWidth, int newHeight) void LightKeeper::onWindowResize(int newWidth, int newHeight)
{ {
if (newWidth <= 0 || newHeight <= 0) { renderer.resize(gfxDevice, glm::ivec2{newWidth, newHeight});
return; const float aspectRatio = static_cast<float>(newWidth) / static_cast<float>(newHeight);
} camera.setAspectRatio(aspectRatio);
renderer.resize(gfxDevice, {newWidth, newHeight});
camera.setAspectRatio(static_cast<float>(newWidth) / static_cast<float>(newHeight));
} }
@@ -0,0 +1,140 @@
#include <components/FirstPersonController.h>
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
#include <glm/gtx/norm.hpp>
#include <destrum/Components/Physics/CapsuleCollider.h>
#include <destrum/Components/Physics/Rigidbody.h>
#include <destrum/Input/InputManager.h>
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/Physics/PhysicsWorld.h>
#include <destrum/Scene/Scene.h>
#include <imgui.h>
FirstPersonController::FirstPersonController(GameObject& parent)
: Component(parent, "FirstPersonController")
{
}
void FirstPersonController::SetMouseCaptured(bool captured)
{
InputManager::GetInstance().SetMouseCaptured(captured);
}
void FirstPersonController::Start()
{
auto* obj = GetGameObject();
m_Capsule = obj->AddComponent<CapsuleCollider>(0.5f, 2.0f);
m_Rigidbody = obj->AddComponent<Rigidbody>();
m_Rigidbody->SetMass(80.0f);
m_Rigidbody->SetAllowSleep(false);
m_Rigidbody->SetUseGravity(true);
}
void FirstPersonController::Update(float)
{
const auto& input = InputManager::GetInstance();
if (input.WasKeyPressed(SDL_SCANCODE_F5))
{
SetMouseCaptured(!IsMouseCaptured());
}
if (!IsMouseCaptured())
{
if (input.WasMousePressed(SDL_BUTTON_LEFT) && !ImGui::GetIO().WantCaptureMouse)
{
SetMouseCaptured(true);
}
return;
}
if (input.WasKeyPressed(SDL_SCANCODE_ESCAPE))
{
SetMouseCaptured(false);
return;
}
if (input.WasKeyPressed(SDL_SCANCODE_SPACE) && m_Rigidbody->HasPhysicsBody())
{
m_Rigidbody->AddImpulse(glm::vec3(0.0f, 400.0f, 0.0f));
}
m_Yaw += static_cast<float>(input.MouseDeltaX()) * m_MouseSensitivity;
m_Pitch -= static_cast<float>(input.MouseDeltaY()) * m_MouseSensitivity;
m_Pitch = glm::clamp(m_Pitch, glm::radians(-89.0f), glm::radians(89.0f));
}
void FirstPersonController::FixedUpdate(float)
{
if (!IsMouseCaptured() || m_Rigidbody == nullptr || !m_Rigidbody->HasPhysicsBody())
return;
const auto& input = InputManager::GetInstance();
const float cosY = glm::cos(m_Yaw);
const float sinY = glm::sin(m_Yaw);
const glm::vec3 forward{cosY, 0.0f, sinY};
const glm::vec3 right{-sinY, 0.0f, cosY};
glm::vec3 moveDir(0.0f);
if (input.IsKeyDown(SDL_SCANCODE_W)) moveDir += forward;
if (input.IsKeyDown(SDL_SCANCODE_S)) moveDir -= forward;
if (input.IsKeyDown(SDL_SCANCODE_D)) moveDir += right;
if (input.IsKeyDown(SDL_SCANCODE_A)) moveDir -= right;
glm::vec3 velocity(0.0f);
if (glm::length2(moveDir) > 0.0f)
{
moveDir = glm::normalize(moveDir);
velocity = moveDir * m_MovementSpeed;
}
velocity.y = m_Rigidbody->GetLinearVelocity().y;
m_Rigidbody->SetLinearVelocity(velocity);
auto* world = m_Rigidbody->GetPhysicsWorld();
if (world != nullptr)
{
PhysicsTransform xform = world->GetBodyTransform(m_Rigidbody->GetBody());
xform.rotation = glm::quat(glm::vec3(0.0f, m_Yaw, 0.0f));
world->SetBodyTransform(m_Rigidbody->GetBody(), xform);
}
}
void FirstPersonController::ImGuiInspector()
{
Component::ImGuiInspector();
ImGui::DragFloat("Movement Speed", &m_MovementSpeed, 0.1f, 0.0f, 100.0f);
ImGui::DragFloat("Mouse Sensitivity", &m_MouseSensitivity, 0.0001f, 0.0f, 0.1f);
ImGui::DragFloat("Eye Height", &m_EyeHeight, 0.01f, 0.0f, 5.0f);
ImGui::Text("Yaw: %.1f deg", glm::degrees(m_Yaw));
ImGui::Text("Pitch: %.1f deg", glm::degrees(m_Pitch));
bool captured = IsMouseCaptured();
ImGui::Checkbox("Mouse Captured", &captured);
if (captured != IsMouseCaptured()) {
SetMouseCaptured(captured);
}
}
nlohmann::json FirstPersonController::Serialize() const
{
return {
{"movementSpeed", m_MovementSpeed},
{"mouseSensitivity", m_MouseSensitivity},
{"eyeHeight", m_EyeHeight},
{"yaw", m_Yaw},
{"pitch", m_Pitch},
};
}
void FirstPersonController::Deserialize(const nlohmann::json& data)
{
if (data.contains("movementSpeed")) m_MovementSpeed = data.at("movementSpeed").get<float>();
if (data.contains("mouseSensitivity")) m_MouseSensitivity = data.at("mouseSensitivity").get<float>();
if (data.contains("eyeHeight")) m_EyeHeight = data.at("eyeHeight").get<float>();
if (data.contains("yaw")) m_Yaw = data.at("yaw").get<float>();
if (data.contains("pitch")) m_Pitch = data.at("pitch").get<float>();
}
@@ -0,0 +1,20 @@
#include <components/GameComponentRegistry.h>
#include <destrum/Serialization/ComponentFactory.h>
#include <components/GameComponentList.h>
void RegisterGameComponents()
{
static bool registered = false;
if (registered) {
return;
}
registered = true;
#define LIGHTKEEPER_REGISTER_COMPONENT(ComponentType) \
ComponentFactory::Register<ComponentType>(#ComponentType);
LIGHTKEEPER_GAME_COMPONENTS(LIGHTKEEPER_REGISTER_COMPONENT)
#undef LIGHTKEEPER_REGISTER_COMPONENT
}
@@ -0,0 +1,34 @@
#include <components/PrintComponent.h>
#include "spdlog/spdlog.h"
PrintComponent::PrintComponent(GameObject& parent) : Component(parent, "PrintComponent")
{
}
void PrintComponent::Start()
{
}
void PrintComponent::Update(float dt)
{
spdlog::debug(m_message);
}
void PrintComponent::ImGuiInspector()
{
Component::ImGuiInspector();
}
nlohmann::json PrintComponent::Serialize() const
{
return {"message", m_message};
}
void PrintComponent::Deserialize(const nlohmann::json& basic_jsons)
{
if (basic_jsons.contains("message"))
{
m_message = basic_jsons.at("message").get<std::string>();
}
}
@@ -0,0 +1,45 @@
#include <components/TriggerLoggerComponent.h>
#include "spdlog/spdlog.h"
#include <destrum/ObjectModel/GameObject.h>
TriggerLoggerComponent::TriggerLoggerComponent(GameObject& parent)
: Component(parent, "TriggerLoggerComponent") {}
void TriggerLoggerComponent::OnTriggerEnter(GameObject* other) {
++triggerEnterCount;
lastOtherName = other ? other->GetName() : "null";
spdlog::info(
"TriggerLogger: ENTER {} -> {} (enters: {}, exits: {})",
GetGameObject()->GetName(),
lastOtherName,
triggerEnterCount,
triggerExitCount);
}
void TriggerLoggerComponent::OnTriggerExit(GameObject* other) {
++triggerExitCount;
lastOtherName = other ? other->GetName() : "null";
spdlog::info(
"TriggerLogger: EXIT {} -> {} (enters: {}, exits: {})",
GetGameObject()->GetName(),
lastOtherName,
triggerEnterCount,
triggerExitCount);
}
nlohmann::json TriggerLoggerComponent::Serialize() const {
return {
{"triggerEnterCount", triggerEnterCount},
{"triggerExitCount", triggerExitCount}
};
}
void TriggerLoggerComponent::Deserialize(const nlohmann::json& data) {
if (data.contains("triggerEnterCount")) {
triggerEnterCount = data.at("triggerEnterCount").get<int>();
}
if (data.contains("triggerExitCount")) {
triggerExitCount = data.at("triggerExitCount").get<int>();
}
}
+2 -3
View File
@@ -25,10 +25,9 @@ int main(int argc, char* argv[]) {
app.init({ app.init({
.windowSize = {1200, 800}, .windowSize = {1200, 800},
.renderSize = {1200, 800}, .renderSize = {1200, 800},
.appName = "Destrum Star Catcher", .appName = "Destrum Engine",
.windowTitle = "Star Catcher", .windowTitle = "Lightkeeper",
.exeDir = exeDir, .exeDir = exeDir,
.showDebugUi = false,
}); });
app.run(); app.run();
app.cleanup(); app.cleanup();
+86
View File
@@ -20,9 +20,11 @@
#include <destrum/Physics/JoltPhysicsWorld.h> #include <destrum/Physics/JoltPhysicsWorld.h>
#include <destrum/Components/Physics/SphereCollider.h> #include <destrum/Components/Physics/SphereCollider.h>
#include <destrum/Components/Physics/BoxCollider.h> #include <destrum/Components/Physics/BoxCollider.h>
#include <destrum/Components/Physics/Collider.h>
#include <destrum/Components/Animator.h> #include <destrum/Components/Animator.h>
#include <destrum/Components/Physics/Rigidbody.h> #include <destrum/Components/Physics/Rigidbody.h>
#include <destrum/Graphics/Material.h> #include <destrum/Graphics/Material.h>
#include <destrum/Graphics/Managers/LineRenderingManager.h>
namespace { namespace {
class TestComponent final : public Component { class TestComponent final : public Component {
@@ -189,6 +191,29 @@ namespace {
"RigidBody legacy alias must not be registered"); "RigidBody legacy alias must not be registered");
} }
void testLineRenderingManager()
{
auto& lineManager = LineRenderingManager::GetInstance();
lineManager.ClearLines();
lineManager.SubmitLine(
glm::vec3{0.0f},
glm::vec3{1.0f, 0.0f, 0.0f},
glm::vec4{1.0f, 0.0f, 0.0f, 1.0f});
lineManager.SubmitLine(Line{
.start = glm::vec3{1.0f},
.end = glm::vec3{2.0f},
.color = glm::vec4{0.0f, 1.0f, 0.0f, 1.0f},
});
check(lineManager.GetLines().size() == 2,
"line rendering manager must retain submitted lines");
lineManager.ClearLines();
check(lineManager.GetLines().empty(),
"line rendering manager must clear transient lines");
}
void testAssetPathValidation() void testAssetPathValidation()
{ {
const auto root = std::filesystem::temp_directory_path() / "destrum_asset_test"; const auto root = std::filesystem::temp_directory_path() / "destrum_asset_test";
@@ -459,6 +484,65 @@ namespace {
check(!AssetReference::fromCacheKey("CharacterMedium.fbx").has_value(), check(!AssetReference::fromCacheKey("CharacterMedium.fbx").has_value(),
"plain cache names must not be treated as file-backed assets"); "plain cache names must not be treated as file-backed assets");
} }
class TriggerTracker final : public Component {
public:
explicit TriggerTracker(GameObject& owner)
: Component(owner, "TriggerTracker") {}
void Update(float) override {}
std::string GetTypeName() const override { return "TriggerTracker"; }
void OnTriggerEnter(GameObject* other) override {
++enterCount;
lastOther = other;
enterObjectName = other ? other->GetName() : "null";
}
void OnTriggerExit(GameObject* other) override {
++exitCount;
}
int enterCount{0};
int exitCount{0};
GameObject* lastOther{nullptr};
std::string enterObjectName;
};
void testTriggerZone()
{
Scene& scene = SceneManager::GetInstance().CreateScene("trigger-test");
// Trigger box at origin (Static rigidbody + BoxCollider as trigger).
GameObject* triggerObj = scene.CreateGameObject("TriggerZone");
triggerObj->AddComponent<BoxCollider>(glm::vec3{3.0f, 3.0f, 3.0f});
triggerObj->GetComponent<BoxCollider>()->SetTrigger(true);
auto* triggerRb = triggerObj->AddComponent<Rigidbody>();
triggerRb->SetType(RigidbodyType::Static);
auto* tracker = triggerObj->AddComponent<TriggerTracker>();
// Falling sphere above the trigger.
GameObject* sphere = scene.CreateGameObject("FallingSphere");
sphere->AddComponent<SphereCollider>(0.5f);
sphere->AddComponent<Rigidbody>();
sphere->GetTransform().SetWorldPosition({0.0f, 8.0f, 0.0f});
scene.CommitPendingAdditions();
// Run physics for enough steps that the sphere falls into the trigger.
for (int step = 0; step < 120; ++step) {
scene.FixedUpdate(1.0f / 60.0f);
}
check(tracker->enterCount >= 1,
"falling sphere must trigger OnTriggerEnter at least once");
check(tracker->exitCount >= 1,
"falling sphere passing through trigger must exit");
check(tracker->enterObjectName == "FallingSphere",
"OnTriggerEnter must report the correct entering object");
SceneManager::GetInstance().Destroy();
}
} }
int main() int main()
@@ -469,6 +553,7 @@ int main()
testEventRemoval(); testEventRemoval();
testComponentRemoval(); testComponentRemoval();
testEngineComponentRegistration(); testEngineComponentRegistration();
testLineRenderingManager();
testAssetPathValidation(); testAssetPathValidation();
testLegacySceneLoad(); testLegacySceneLoad();
testSceneLoadRollback(); testSceneLoadRollback();
@@ -478,6 +563,7 @@ int main()
testAnimatorAssetReferences(); testAnimatorAssetReferences();
testSimpleColorMaterial(); testSimpleColorMaterial();
testAssetReferenceCacheKeys(); testAssetReferenceCacheKeys();
testTriggerZone();
std::cout << "destrum tests passed\n"; std::cout << "destrum tests passed\n";
return EXIT_SUCCESS; return EXIT_SUCCESS;
} }