diff --git a/destrum/CMakeLists.txt b/destrum/CMakeLists.txt index ffaf8c5..58f43f9 100644 --- a/destrum/CMakeLists.txt +++ b/destrum/CMakeLists.txt @@ -37,12 +37,14 @@ set(SRC_FILES "src/Graphics/Managers/MemoryManager.cpp" "src/Graphics/Managers/FrameManager.cpp" "src/Graphics/Managers/ImageManager.cpp" + "src/Graphics/Managers/LineRenderingManager.cpp" "src/Graphics/Resources/GPUImage.cpp" "src/Graphics/Resources/NBuffer.cpp" "src/Graphics/Resources/Cubemap.cpp" "src/Graphics/Pipelines/MeshPipeline.cpp" + "src/Graphics/Pipelines/LineRenderingPass.cpp" "src/Graphics/Pipelines/SkyboxPipeline.cpp" "src/Graphics/Pipelines/SkinningPipeline.cpp" "src/Graphics/Pipelines/ImguiPass.cpp" diff --git a/destrum/assets_src/shaders/line.frag b/destrum/assets_src/shaders/line.frag new file mode 100644 index 0000000..a5fec46 --- /dev/null +++ b/destrum/assets_src/shaders/line.frag @@ -0,0 +1,9 @@ +#version 460 + +layout (location = 0) in vec4 inColor; +layout (location = 0) out vec4 outFragColor; + +void main() +{ + outFragColor = inColor; +} diff --git a/destrum/assets_src/shaders/line.vert b/destrum/assets_src/shaders/line.vert new file mode 100644 index 0000000..f23eb69 --- /dev/null +++ b/destrum/assets_src/shaders/line.vert @@ -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; +} diff --git a/destrum/include/destrum/Graphics/Managers/LineRenderingManager.h b/destrum/include/destrum/Graphics/Managers/LineRenderingManager.h new file mode 100644 index 0000000..17a8f4a --- /dev/null +++ b/destrum/include/destrum/Graphics/Managers/LineRenderingManager.h @@ -0,0 +1,37 @@ +#ifndef DESTRUM_LINERENDERINGMANAGER_H +#define DESTRUM_LINERENDERINGMANAGER_H + +#include + +#include +#include + +#include + +struct Line { + glm::vec3 start{}; + glm::vec3 end{}; + glm::vec4 color{1.0f}; +}; + +class LineRenderingManager final : public Singleton { +public: + friend class Singleton; + + 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& GetLines() const { return lines; } + + void ClearLines(); + +private: + LineRenderingManager() = default; + + std::vector lines; +}; + +#endif // DESTRUM_LINERENDERINGMANAGER_H diff --git a/destrum/include/destrum/Graphics/Pipelines/LineRenderingPass.h b/destrum/include/destrum/Graphics/Pipelines/LineRenderingPass.h new file mode 100644 index 0000000..1a62a82 --- /dev/null +++ b/destrum/include/destrum/Graphics/Pipelines/LineRenderingPass.h @@ -0,0 +1,42 @@ +#ifndef DESTRUM_LINERENDERINGPASS_H +#define DESTRUM_LINERENDERINGPASS_H + +#include +#include + +#include + +#include +#include + +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; + NBuffer vertexBuffer; + std::size_t vertexCapacity{0}; +}; + +#endif // DESTRUM_LINERENDERINGPASS_H diff --git a/destrum/include/destrum/Graphics/Renderer.h b/destrum/include/destrum/Graphics/Renderer.h index ec899b5..727a3d4 100644 --- a/destrum/include/destrum/Graphics/Renderer.h +++ b/destrum/include/destrum/Graphics/Renderer.h @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -111,6 +112,7 @@ private: std::unique_ptr meshPipeline; std::unique_ptr skyboxPipeline; + std::unique_ptr lineRenderingPass; std::unique_ptr skinningPipeline; bool initialized{false}; diff --git a/destrum/src/Graphics/Managers/LineRenderingManager.cpp b/destrum/src/Graphics/Managers/LineRenderingManager.cpp new file mode 100644 index 0000000..07f9bcf --- /dev/null +++ b/destrum/src/Graphics/Managers/LineRenderingManager.cpp @@ -0,0 +1,23 @@ +#include + +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(); +} diff --git a/destrum/src/Graphics/Pipelines/LineRenderingPass.cpp b/destrum/src/Graphics/Pipelines/LineRenderingPass.cpp new file mode 100644 index 0000000..02c6e23 --- /dev/null +++ b/destrum/src/Graphics/Pipelines/LineRenderingPass.cpp @@ -0,0 +1,232 @@ +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#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( + 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::max() / 2) { + throw std::overflow_error("Too many lines submitted for rendering"); + } + + std::vector 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::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(target.extent.width), + .height = static_cast(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(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::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; +} diff --git a/destrum/src/Graphics/Renderer.cpp b/destrum/src/Graphics/Renderer.cpp index d22537b..de69a17 100644 --- a/destrum/src/Graphics/Renderer.cpp +++ b/destrum/src/Graphics/Renderer.cpp @@ -1,5 +1,6 @@ #include +#include #include #include @@ -36,6 +37,9 @@ void GameRenderer::init(GfxDevice& gfxDevice, RenderResources& _resources, glm:: skinningPipeline = std::make_unique(); skinningPipeline->init(gfxDevice); + lineRenderingPass = std::make_unique(); + lineRenderingPass->init(gfxDevice, drawImageFormat); + GameState::GetInstance().SetRenderer(this); initialized = true; } catch (...) { @@ -194,6 +198,17 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& vkCmdEndRendering(cmd); } + + { + TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "LineRenderingPass::draw"); + + lineRenderingPass->draw( + cmd, + gfxDevice, + drawImage, + camera, + LineRenderingManager::GetInstance()); + } // vkutil::cmdEndLabel(cmd); } @@ -208,6 +223,9 @@ void GameRenderer::cleanup(GfxDevice& gfxDevice) if (skinningPipeline) skinningPipeline->cleanup(gfxDevice); + if (lineRenderingPass) + lineRenderingPass->cleanup(gfxDevice); + if (skyboxPipeline) skyboxPipeline->cleanup(device); @@ -228,6 +246,7 @@ void GameRenderer::cleanup(GfxDevice& gfxDevice) meshPipeline.reset(); skyboxPipeline.reset(); skinningPipeline.reset(); + lineRenderingPass.reset(); pendingMaterialUploads.clear(); meshDrawCommands.clear(); sortedMeshDrawCommands.clear(); diff --git a/lightkeeper/include/Lightkeeper.h b/lightkeeper/include/Lightkeeper.h index 0428234..6174113 100644 --- a/lightkeeper/include/Lightkeeper.h +++ b/lightkeeper/include/Lightkeeper.h @@ -21,7 +21,14 @@ public: void customFixedUpdate(float dt) override; void onWindowResize(int newWidth, int newHeight) override; + private: + void drawDebugMenuBar(); + void saveCurrentScene(); + void loadScene(); + void createEmptyScene(); + void submitBoxColliderDebugLines(); + Camera camera{glm::vec3(0.f, 0.f, -5.f), glm::vec3(0, 1, 0)}; MeshID sphereMesh; @@ -34,6 +41,7 @@ private: char scenePath[512]{"scenes/debug_scene.json"}; char newSceneName[128]{"EmptyScene"}; std::string sceneStatus; + bool renderBoxColliders{false}; }; #endif //LIGHTKEEPER_H diff --git a/lightkeeper/src/Lightkeeper.cpp b/lightkeeper/src/Lightkeeper.cpp index b5a52a6..94e8330 100644 --- a/lightkeeper/src/Lightkeeper.cpp +++ b/lightkeeper/src/Lightkeeper.cpp @@ -1,7 +1,9 @@ #include "Lightkeeper.h" +#include #include #include +#include #include "glm/gtx/transform.hpp" #include "spdlog/spdlog.h" #include @@ -446,90 +448,199 @@ void LightKeeper::customUpdate(float dt) SceneManager::GetInstance().Update(dt); SceneManager::GetInstance().LateUpdate(dt); + if (m_params.showDebugUi) { + 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()); } - ImGui::Begin("Test"); - if (ImGui::Button("SPawn ball")) - { - auto sphere = SceneManager::GetInstance().GetCurrentScene().CreateGameObject("Sphere"); - sphere->AddComponent(1.5f); - auto rb = sphere->AddComponent(); - rb->SetMass(1000); + if (m_params.showDebugUi) { + ImGui::Begin("Test"); + if (ImGui::Button("SPawn ball")) + { + auto sphere = SceneManager::GetInstance().GetCurrentScene().CreateGameObject("Sphere"); + sphere->AddComponent(1.5f); + auto rb = sphere->AddComponent(); + rb->SetMass(1000); - auto meshRenderComp = sphere->AddComponent(); - meshRenderComp->SetMaterialID(sphereMaterial); - meshRenderComp->SetMeshID(sphereMesh); + auto meshRenderComp = sphere->AddComponent(); + 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}); + 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); + SceneManager::GetInstance().GetCurrentScene().GetPhysics().RefreshGameObject(*sphere); + } + ImGui::End(); } - ImGui::End(); +} - ImGui::Begin("Scene Debug"); - ImGui::InputText("Scene path", scenePath, sizeof(scenePath)); - ImGui::InputText("New scene name", newSceneName, sizeof(newSceneName)); +void LightKeeper::drawDebugMenuBar() +{ + if (!ImGui::BeginMainMenuBar()) { + return; + } - if (ImGui::Button("Save Current Scene")) { - try { - const auto path = ResolveScenePath(scenePath, m_params.exeDir); - const auto parent = path.parent_path(); - if (!parent.empty()) { - std::filesystem::create_directories(parent); - } + if (ImGui::BeginMenu("File")) { + ImGui::TextDisabled("Scene"); - if (SceneSerializer::Save( - SceneManager::GetInstance().GetCurrentScene(), - path)) { - sceneStatus = "Saved scene to " + path.string(); - } else { - sceneStatus = "Failed to save scene to " + path.string(); - } - } catch (const std::exception& exception) { - sceneStatus = std::string{"Save failed: "} + exception.what(); + 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 (ImGui::BeginMenu("Render")) { + ImGui::MenuItem("Box Colliders", nullptr, &renderBoxColliders); + ImGui::EndMenu(); + } + + ImGui::EndMainMenuBar(); +} + +void LightKeeper::saveCurrentScene() +{ + try { + const auto path = ResolveScenePath(scenePath, m_params.exeDir); + const auto parent = path.parent_path(); + if (!parent.empty()) { + std::filesystem::create_directories(parent); + } + + if (SceneSerializer::Save( + SceneManager::GetInstance().GetCurrentScene(), + path)) { + sceneStatus = "Saved scene to " + path.string(); + } else { + sceneStatus = "Failed to save scene to " + path.string(); + } + } catch (const std::exception& exception) { + sceneStatus = std::string{"Save failed: "} + exception.what(); + } +} + +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, 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(); + 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 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 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::SameLine(); - if (ImGui::Button("Load Scene")) { - 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(); - } - } - - if (ImGui::Button("New Empty Scene")) { - 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(); - } - } - - if (!sceneStatus.empty()) { - ImGui::TextWrapped("%s", sceneStatus.c_str()); - } - ImGui::End(); } void LightKeeper::customDraw() @@ -540,6 +651,7 @@ void LightKeeper::customDraw() } renderer.beginDrawing(gfxDevice); + submitBoxColliderDebugLines(); const RenderContext ctx{ .renderer = renderer, diff --git a/tests/destrum_tests.cpp b/tests/destrum_tests.cpp index 25eb481..36687df 100644 --- a/tests/destrum_tests.cpp +++ b/tests/destrum_tests.cpp @@ -23,6 +23,7 @@ #include #include #include +#include namespace { class TestComponent final : public Component { @@ -189,6 +190,29 @@ namespace { "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() { const auto root = std::filesystem::temp_directory_path() / "destrum_asset_test"; @@ -469,6 +493,7 @@ int main() testEventRemoval(); testComponentRemoval(); testEngineComponentRegistration(); + testLineRenderingManager(); testAssetPathValidation(); testLegacySceneLoad(); testSceneLoadRollback();