From f7e7c56392ccabfca28d9e02ee05ff12c87e57af Mon Sep 17 00:00:00 2001 From: Bram Verhulst Date: Fri, 23 Oct 2026 20:53:56 +0200 Subject: [PATCH] feat: add click to select --- destrum/assets_src/shaders/instance_data.glsl | 2 +- destrum/assets_src/shaders/mesh.frag | 18 +++ destrum/assets_src/shaders/mesh.vert | 2 + destrum/assets_src/shaders/scene_data.glsl | 2 +- destrum/assets_src/shaders/selection.frag | 16 +++ destrum/include/destrum/App.h | 4 + .../destrum/Graphics/IndirectDrawList.h | 2 +- .../destrum/Graphics/Managers/MemoryManager.h | 5 + .../destrum/Graphics/MeshDrawCommand.h | 2 + .../destrum/Graphics/Pipelines/MeshPipeline.h | 7 +- destrum/include/destrum/Graphics/Renderer.h | 43 ++++++- destrum/src/App.cpp | 45 +++++++ .../src/Components/MeshRendererComponent.cpp | 6 +- destrum/src/Graphics/IndirectDrawList.cpp | 2 +- .../src/Graphics/Managers/MemoryManager.cpp | 12 ++ .../src/Graphics/Pipelines/MeshPipeline.cpp | 36 +++++- destrum/src/Graphics/Renderer.cpp | 119 +++++++++++++++++- lightkeeper/include/Lightkeeper.h | 3 + lightkeeper/src/Lightkeeper.cpp | 5 + lightkeeper/src/LightkeeperDebugUi.cpp | 74 +++++++++++ 20 files changed, 385 insertions(+), 20 deletions(-) create mode 100644 destrum/assets_src/shaders/selection.frag diff --git a/destrum/assets_src/shaders/instance_data.glsl b/destrum/assets_src/shaders/instance_data.glsl index fa51e92..9cd8c82 100644 --- a/destrum/assets_src/shaders/instance_data.glsl +++ b/destrum/assets_src/shaders/instance_data.glsl @@ -11,7 +11,7 @@ struct InstanceData { mat4 transform; VertexBuffer vertexBuffer; uint materialID; - uint padding; + uint objectID; }; layout (buffer_reference, scalar) readonly buffer InstanceBuffer { diff --git a/destrum/assets_src/shaders/mesh.frag b/destrum/assets_src/shaders/mesh.frag index 1497bb4..0d863e8 100644 --- a/destrum/assets_src/shaders/mesh.frag +++ b/destrum/assets_src/shaders/mesh.frag @@ -13,9 +13,23 @@ layout (location = 1) in vec2 inUV; layout (location = 2) in vec3 inNormal; layout (location = 3) in vec4 inTangent; layout (location = 4) flat in uint inMaterialID; +layout (location = 5) flat in uint inObjectID; layout (location = 0) out vec4 outFragColor; +// Stable per object, the same object keeps its color between frames and runs +vec3 selectionColor(uint objectID) +{ + if (objectID == 0u) { + return vec3(0.0); + } + uint hash = objectID * 2654435761u; + hash ^= hash >> 15; + hash *= 2246822519u; + hash ^= hash >> 13; + return vec3(hash & 255u, (hash >> 8) & 255u, (hash >> 16) & 255u) / 255.0; +} + vec3 calculateNormal(MaterialData material, uint samplerID) { vec3 n = normalize(inNormal); @@ -127,6 +141,10 @@ void main() fragColor = mix(fragColor, lightHeatmapColor(tileLightCount), 0.5); } + if (pcs.sceneData.debugSelectionView != 0) { + fragColor = selectionColor(inObjectID); + } + #if 0 // TANGENT DEBUG if (inTangent == vec4(0.0)) { diff --git a/destrum/assets_src/shaders/mesh.vert b/destrum/assets_src/shaders/mesh.vert index cbf1840..d0bb738 100644 --- a/destrum/assets_src/shaders/mesh.vert +++ b/destrum/assets_src/shaders/mesh.vert @@ -9,6 +9,7 @@ layout (location = 1) out vec2 outUV; layout (location = 2) out vec3 outNormal; layout (location = 3) out vec4 outTangent; layout (location = 4) flat out uint outMaterialID; +layout (location = 5) flat out uint outObjectID; // The depth pre-pass reuses this shader, positions have to match bit for bit // so the main pass can depth test against it with LESS_OR_EQUAL @@ -34,4 +35,5 @@ void main() // w is the bitangent sign, the fragment shader builds the TBN from the interpolated vectors outTangent = vec4(mat3(instance.transform) * v.tangent.xyz, v.tangent.w); outMaterialID = instance.materialID; + outObjectID = instance.objectID; } diff --git a/destrum/assets_src/shaders/scene_data.glsl b/destrum/assets_src/shaders/scene_data.glsl index ae30f81..7373f18 100644 --- a/destrum/assets_src/shaders/scene_data.glsl +++ b/destrum/assets_src/shaders/scene_data.glsl @@ -37,7 +37,7 @@ layout (buffer_reference, scalar) readonly buffer SceneDataBuffer { // directional shadows uint shadowMapId; float shadowNormalOffset; - uint padding; + uint debugSelectionView; mat4 lightViewProj; // spot shadows diff --git a/destrum/assets_src/shaders/selection.frag b/destrum/assets_src/shaders/selection.frag new file mode 100644 index 0000000..a4326a9 --- /dev/null +++ b/destrum/assets_src/shaders/selection.frag @@ -0,0 +1,16 @@ +#version 460 + +// Same interface as mesh.vert, only the object ID is used +layout (location = 0) in vec3 inPos; +layout (location = 1) in vec2 inUV; +layout (location = 2) in vec3 inNormal; +layout (location = 3) in vec4 inTangent; +layout (location = 4) flat in uint inMaterialID; +layout (location = 5) flat in uint inObjectID; + +layout (location = 0) out uint outObjectID; + +void main() +{ + outObjectID = inObjectID; +} diff --git a/destrum/include/destrum/App.h b/destrum/include/destrum/App.h index aea0fe0..a28062b 100644 --- a/destrum/include/destrum/App.h +++ b/destrum/include/destrum/App.h @@ -123,12 +123,16 @@ private: void drawDockSpace(); void drawViewportWindow(); void applyViewportResize(); + void requestSelectionPick(); + void applySelectionPick(); + [[nodiscard]] bool canPickInViewport() const { return usesEditorViewport() && (!playing || paused); } ViewportRect viewportRect{}; glm::ivec2 viewportDesiredSize{0}; bool viewportHovered{false}; bool viewportFocused{false}; bool viewportMouseClaimed{false}; + bool pickSelectsParent{false}; void drawPlayControls(); void applyPlayModeToggle(); diff --git a/destrum/include/destrum/Graphics/IndirectDrawList.h b/destrum/include/destrum/Graphics/IndirectDrawList.h index 308ef33..94d455c 100644 --- a/destrum/include/destrum/Graphics/IndirectDrawList.h +++ b/destrum/include/destrum/Graphics/IndirectDrawList.h @@ -22,7 +22,7 @@ struct GPUInstanceData { glm::mat4 transform; VkDeviceAddress vertexBuffer; std::uint32_t materialId; - std::uint32_t padding; + std::uint32_t objectId; }; static_assert(sizeof(GPUInstanceData) == 80); diff --git a/destrum/include/destrum/Graphics/Managers/MemoryManager.h b/destrum/include/destrum/Graphics/Managers/MemoryManager.h index c613824..b97064b 100644 --- a/destrum/include/destrum/Graphics/Managers/MemoryManager.h +++ b/destrum/include/destrum/Graphics/Managers/MemoryManager.h @@ -39,6 +39,11 @@ public: VkDeviceSize offset = 0, VkDeviceSize size = VK_WHOLE_SIZE) const; + void invalidateAllocation( + const GPUBuffer& buffer, + VkDeviceSize offset = 0, + VkDeviceSize size = VK_WHOLE_SIZE) const; + [[nodiscard]] VkDeviceAddress getBufferAddress(const GPUBuffer& buffer) const; private: diff --git a/destrum/include/destrum/Graphics/MeshDrawCommand.h b/destrum/include/destrum/Graphics/MeshDrawCommand.h index e4f2fd8..eb84a16 100644 --- a/destrum/include/destrum/Graphics/MeshDrawCommand.h +++ b/destrum/include/destrum/Graphics/MeshDrawCommand.h @@ -26,6 +26,8 @@ struct MeshDrawCommand { // skinned meshes only const SkinnedMesh* skinnedMesh{nullptr}; std::uint32_t jointMatricesStartIndex{0}; + + std::uint32_t objectId{0}; }; #endif //MESHDRAWCOMMAND_H diff --git a/destrum/include/destrum/Graphics/Pipelines/MeshPipeline.h b/destrum/include/destrum/Graphics/Pipelines/MeshPipeline.h index d71e7db..fe53b0d 100644 --- a/destrum/include/destrum/Graphics/Pipelines/MeshPipeline.h +++ b/destrum/include/destrum/Graphics/Pipelines/MeshPipeline.h @@ -24,7 +24,8 @@ public: GfxDevice& gfxDevice, RenderResources& resources, VkFormat drawImageFormat, - VkFormat depthImageFormat); + VkFormat depthImageFormat, + VkFormat selectionImageFormat); void drawDepthPrepass( VkCommandBuffer cmd, @@ -32,7 +33,8 @@ public: const RenderResources& resources, const GPUBuffer& sceneDataBuffer, const IndirectDrawList& drawList, - const IndirectDrawList::View& view); + const IndirectDrawList::View& view, + bool writeObjectIds); void draw( VkCommandBuffer cmd, @@ -71,6 +73,7 @@ private: VkPipelineLayout m_pipelineLayout{VK_NULL_HANDLE}; std::unique_ptr m_pipeline; std::unique_ptr m_depthPrepassPipeline; + std::unique_ptr m_selectionPrepassPipeline; bool m_renderWireframe{false}; }; diff --git a/destrum/include/destrum/Graphics/Renderer.h b/destrum/include/destrum/Graphics/Renderer.h index f154522..1957b67 100644 --- a/destrum/include/destrum/Graphics/Renderer.h +++ b/destrum/include/destrum/Graphics/Renderer.h @@ -2,6 +2,7 @@ #define RENDERER_H #include +#include #include #include @@ -21,6 +22,7 @@ #include #include #include +#include #include #include @@ -58,9 +60,9 @@ public: void draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& camera, const SceneData& sceneData); void cleanup(GfxDevice& gfxDevice); - void drawMesh(MeshID id, const glm::mat4& transform, MaterialID materialId); + void drawMesh(MeshID id, const glm::mat4& transform, MaterialID materialId, std::uint32_t objectId = 0); void drawSkinnedMesh(MeshID id, const glm::mat4& transform, MaterialID materialId, SkinnedMesh* skinnedMesh, - std::size_t jointMatricesStartIndex); + std::size_t jointMatricesStartIndex, std::uint32_t objectId = 0); void submitLight(const Light& light); const GPUImage& getDrawImage() const; @@ -101,6 +103,31 @@ public: return renderLightHeatmap; } + void setRenderSelectionView(bool selectionView) + { + renderSelectionView = selectionView; + } + + bool getRenderSelectionView() const + { + return renderSelectionView; + } + + void setSelectionPassEnabled(bool enabled) + { + selectionPassEnabled = enabled; + } + + void requestSelectionPick(const glm::ivec2& pixel) + { + requestedPick = pixel; + } + + [[nodiscard]] std::optional takeSelectionPick() + { + return std::exchange(pickResult, std::nullopt); + } + void setShadowDistance(float distance) { shadowMapPipeline->setShadowDistance(distance); @@ -126,6 +153,7 @@ public: private: void createDrawImage(GfxDevice& gfxDevice, const glm::ivec2& drawImageSize, bool firstCreate); void uploadLights(const Camera& camera); + void recordSelectionPick(VkCommandBuffer cmd, std::uint32_t frameIndex); [[nodiscard]] std::uint32_t assignShadowIndex(const Light& light, const Frustum& cameraFrustum); RenderResources* resources = nullptr; @@ -142,9 +170,18 @@ private: VkFormat drawImageFormat{VK_FORMAT_R16G16B16A16_SFLOAT}; VkFormat depthImageFormat{VK_FORMAT_D32_SFLOAT}; + VkFormat selectionImageFormat{VK_FORMAT_R32_UINT}; ImageID drawImageId{NULL_IMAGE_ID}; ImageID depthImageId{NULL_IMAGE_ID}; + ImageID selectionImageId{NULL_IMAGE_ID}; + + std::array pickBuffers{}; + std::array pickPending{}; + std::optional requestedPick; + std::optional pickResult; + bool selectionPassEnabled{false}; + bool renderSelectionView{false}; VkSampleCountFlagBits samples{VK_SAMPLE_COUNT_1_BIT}; @@ -173,7 +210,7 @@ private: std::uint32_t shadowMapId; float shadowNormalOffset; - std::uint32_t padding; + std::uint32_t debugSelectionView; glm::mat4 lightViewProj; std::uint32_t spotShadowAtlasId; diff --git a/destrum/src/App.cpp b/destrum/src/App.cpp index 26b865a..7ed7c63 100644 --- a/destrum/src/App.cpp +++ b/destrum/src/App.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -234,6 +235,7 @@ if (!isRunning) break; { ZoneScopedN("Debug ImGui"); drawWindowsMenu(); + applySelectionPick(); drawViewportWindow(); if (m_DebugWindows.inspector) @@ -380,6 +382,7 @@ if (!isRunning) break; { ZoneScopedN("customDraw"); + renderer.setSelectionPassEnabled(canPickInViewport()); customDraw(); } @@ -549,10 +552,52 @@ void App::drawViewportWindow() ImGui::SetCursorScreenPos(origin); drawViewportOverlay(); + requestSelectionPick(); ImGui::End(); } +void App::requestSelectionPick() +{ + if (!canPickInViewport() || !viewportHovered || viewportMouseClaimed || + !ImGui::IsMouseClicked(ImGuiMouseButton_Left)) + { + return; + } + + const ImVec2 mouse = ImGui::GetMousePos(); + const glm::vec2 uv = (glm::vec2{mouse.x, mouse.y} - viewportRect.min) / viewportRect.size; + if (uv.x < 0.0f || uv.y < 0.0f || uv.x >= 1.0f || uv.y >= 1.0f) + { + return; + } + + const VkExtent3D extent = renderer.getDrawImage().extent; + renderer.requestSelectionPick(glm::ivec2{uv * glm::vec2{static_cast(extent.width), static_cast(extent.height)}}); + pickSelectsParent = ImGui::GetIO().KeyAlt; +} + +void App::applySelectionPick() +{ + const auto picked = renderer.takeSelectionPick(); + auto& sceneManager = SceneManager::GetInstance(); + if (!picked || sceneManager.GetActiveScenes().empty()) + { + return; + } + + Scene& scene = sceneManager.GetCurrentScene(); + GameObject* object = scene.FindGameObject(*picked); + if (object != nullptr && pickSelectsParent) + { + if (const Transform* parent = object->GetTransform().GetParent(); parent != nullptr && parent->GetOwner() != nullptr) + { + object = parent->GetOwner(); + } + } + scene.SetSelectedObject(object != nullptr ? object->GetId() : InvalidObjectId); +} + void App::applyViewportResize() { if (viewportDesiredSize.x <= 0 || viewportDesiredSize.y <= 0) diff --git a/destrum/src/Components/MeshRendererComponent.cpp b/destrum/src/Components/MeshRendererComponent.cpp index 86d53ef..124fdd2 100644 --- a/destrum/src/Components/MeshRendererComponent.cpp +++ b/destrum/src/Components/MeshRendererComponent.cpp @@ -216,9 +216,11 @@ void MeshRendererComponent::Render(const RenderContext& ctx) { GetTransform().GetWorldMatrix(), materialID, m_skinnedMesh.get(), - jointMatricesStartIndex + jointMatricesStartIndex, + static_cast(GetGameObject()->GetId()) ); } else { - ctx.renderer.drawMesh(meshID, GetTransform().GetWorldMatrix(), materialID); + ctx.renderer.drawMesh(meshID, GetTransform().GetWorldMatrix(), materialID, + static_cast(GetGameObject()->GetId())); } } diff --git a/destrum/src/Graphics/IndirectDrawList.cpp b/destrum/src/Graphics/IndirectDrawList.cpp index b43532a..9e8e7cc 100644 --- a/destrum/src/Graphics/IndirectDrawList.cpp +++ b/destrum/src/Graphics/IndirectDrawList.cpp @@ -91,7 +91,7 @@ IndirectDrawList::View IndirectDrawList::addView( ? dc.skinnedMesh->skinnedVertexBuffer.address : mesh.vertexBuffer.address, .materialId = dc.materialId, - .padding = 0, + .objectId = dc.objectId, }); ++commands.back().instanceCount; ++view.instanceCount; diff --git a/destrum/src/Graphics/Managers/MemoryManager.cpp b/destrum/src/Graphics/Managers/MemoryManager.cpp index 7060664..9d0e1b4 100644 --- a/destrum/src/Graphics/Managers/MemoryManager.cpp +++ b/destrum/src/Graphics/Managers/MemoryManager.cpp @@ -113,3 +113,15 @@ void MemoryManager::flushAllocation( VK_CHECK(vmaFlushAllocation(allocator, buffer.allocation, offset, size)); } + +void MemoryManager::invalidateAllocation( + const GPUBuffer& buffer, + VkDeviceSize offset, + VkDeviceSize size) const +{ + if (!buffer.hostVisible || buffer.allocation == VK_NULL_HANDLE) { + return; + } + + VK_CHECK(vmaInvalidateAllocation(allocator, buffer.allocation, offset, size)); +} diff --git a/destrum/src/Graphics/Pipelines/MeshPipeline.cpp b/destrum/src/Graphics/Pipelines/MeshPipeline.cpp index b4985b7..a85e679 100644 --- a/destrum/src/Graphics/Pipelines/MeshPipeline.cpp +++ b/destrum/src/Graphics/Pipelines/MeshPipeline.cpp @@ -21,7 +21,8 @@ void MeshPipeline::init( GfxDevice& gfxDevice, RenderResources& resources, VkFormat drawImageFormat, - VkFormat depthImageFormat) + VkFormat depthImageFormat, + VkFormat selectionImageFormat) { try { @@ -31,6 +32,9 @@ void MeshPipeline::init( const auto fragShader = AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/mesh.frag"); + const auto selectionShader = + AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/selection.frag"); + const auto bufferRange = VkPushConstantRange{ .stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, .offset = 0, @@ -82,6 +86,27 @@ void MeshPipeline::init( ); } + { + PipelineConfigInfo pipelineConfig{}; + Pipeline::DefaultPipelineConfigInfo(pipelineConfig); + + pipelineConfig.name = "Mesh Selection Prepass Pipeline"; + pipelineConfig.pipelineLayout = m_pipelineLayout; + + pipelineConfig.vertexAttributeDescriptions = {}; + pipelineConfig.vertexBindingDescriptions = {}; + + pipelineConfig.colorAttachments = {selectionImageFormat}; + pipelineConfig.depthAttachment = depthImageFormat; + + m_selectionPrepassPipeline = std::make_unique( + gfxDevice, + vertexShader.string(), + selectionShader.string(), + pipelineConfig + ); + } + { PipelineConfigInfo pipelineConfig{}; Pipeline::DefaultPipelineConfigInfo(pipelineConfig); @@ -120,16 +145,18 @@ void MeshPipeline::drawDepthPrepass( const RenderResources& resources, const GPUBuffer& sceneDataBuffer, const IndirectDrawList& drawList, - const IndirectDrawList::View& view) + const IndirectDrawList::View& view, + bool writeObjectIds) { - if (!m_depthPrepassPipeline) + const auto& pipeline = writeObjectIds ? m_selectionPrepassPipeline : m_depthPrepassPipeline; + if (!pipeline) { return; } drawMeshes( cmd, - *m_depthPrepassPipeline, + *pipeline, renderExtent, resources, sceneDataBuffer, @@ -228,6 +255,7 @@ void MeshPipeline::cleanup(VkDevice device) { m_pipeline.reset(); m_depthPrepassPipeline.reset(); + m_selectionPrepassPipeline.reset(); if (m_pipelineLayout != VK_NULL_HANDLE && device != VK_NULL_HANDLE) { diff --git a/destrum/src/Graphics/Renderer.cpp b/destrum/src/Graphics/Renderer.cpp index 4818006..43fb18a 100644 --- a/destrum/src/Graphics/Renderer.cpp +++ b/destrum/src/Graphics/Renderer.cpp @@ -33,7 +33,15 @@ void GameRenderer::init(GfxDevice& gfxDevice, RenderResources& _resources, glm:: createDrawImage(gfxDevice, drawImageSize, true); meshPipeline = std::make_unique(); - meshPipeline->init(gfxDevice, _resources, drawImageFormat, depthImageFormat); + meshPipeline->init(gfxDevice, _resources, drawImageFormat, depthImageFormat, selectionImageFormat); + + for (GPUBuffer& pickBuffer : pickBuffers) + { + pickBuffer = gfxDevice.createBuffer( + sizeof(std::uint32_t), + VK_BUFFER_USAGE_TRANSFER_DST_BIT, + VMA_MEMORY_USAGE_GPU_TO_CPU); + } skyboxPipeline = std::make_unique(); skyboxPipeline->init(gfxDevice, _resources, drawImageFormat, depthImageFormat); @@ -65,6 +73,15 @@ void GameRenderer::init(GfxDevice& gfxDevice, RenderResources& _resources, glm:: void GameRenderer::beginDrawing(GfxDevice& gfxDevice) { + // This frame's fence was waited on, so a pick copied by its last use has landed + const std::uint32_t frameIndex = gfxDevice.getCurrentFrameIndex(); + if (pickPending[frameIndex]) + { + pickPending[frameIndex] = false; + gfxDevice.getMemoryManager().invalidateAllocation(pickBuffers[frameIndex]); + pickResult = *static_cast(pickBuffers[frameIndex].info.pMappedData); + } + meshDrawCommands.clear(); skinningPipeline->beginDrawing(gfxDevice.getCurrentFrameIndex()); lightCullingPipeline->beginDrawing(gfxDevice.getCurrentFrameIndex()); @@ -207,7 +224,7 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& .debugLightHeatmap = renderLightHeatmap ? 1u : 0u, .shadowMapId = static_cast(shadowMapPipeline->getShadowMapId()), .shadowNormalOffset = shadowMapPipeline->getNormalOffset(), - .padding = 0, + .debugSelectionView = renderSelectionView ? 1u : 0u, .lightViewProj = lightViewProj, .spotShadowAtlasId = static_cast(shadowMapPipeline->getSpotShadowAtlasId()), .spotShadowTilesPerRow = ShadowMapPipeline::SPOT_SHADOW_TILES_PER_ROW, @@ -288,12 +305,29 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Depth Prepass"); vkutil::ScopedCmdLabel label(cmd, "Depth Prepass", vkutil::labelColors::depth); + const auto& selectionImage = resources->getImage(selectionImageId); + if (selectionPassEnabled) + { + vkutil::transitionImage( + cmd, + selectionImage.image, + selectionImage.layout, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + selectionImage.setLayout(VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + } + auto depthPrepassInfo = vkutil::createRenderingInfo({ .renderExtent = drawImage.getExtent2D(), + .colorImageView = selectionPassEnabled ? selectionImage.imageView : VK_NULL_HANDLE, + .colorImageClearValue = glm::vec4{0.f}, .depthImageView = depthImage.imageView, .depthImageClearValue = 1.f, }); depthPrepassInfo.renderingInfo.pDepthAttachment = &depthPrepassInfo.depthAttachment; + if (selectionPassEnabled) + { + depthPrepassInfo.renderingInfo.pColorAttachments = &depthPrepassInfo.colorAttachment; + } vkCmdBeginRendering(cmd, &depthPrepassInfo.renderingInfo); @@ -303,9 +337,16 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& *resources, sceneDataBuffer.getBuffer(), drawList, - mainView); + mainView, + selectionPassEnabled); vkCmdEndRendering(cmd); + + if (selectionPassEnabled && requestedPick) + { + recordSelectionPick(cmd, gfxDevice.getCurrentFrameIndex()); + } + requestedPick.reset(); } { @@ -404,6 +445,51 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& } } +void GameRenderer::recordSelectionPick(VkCommandBuffer cmd, std::uint32_t frameIndex) +{ + vkutil::ScopedCmdLabel label(cmd, "Selection Pick", vkutil::labelColors::transfer); + + const auto& image = resources->getImage(selectionImageId); + const VkExtent2D extent = image.getExtent2D(); + const glm::ivec2 pixel = glm::clamp( + *requestedPick, + glm::ivec2{0}, + glm::ivec2{static_cast(extent.width) - 1, static_cast(extent.height) - 1}); + + vkutil::transitionImage(cmd, image.image, image.layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + image.setLayout(VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + + const VkBufferImageCopy region{ + .bufferOffset = 0, + .imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, + .imageOffset = {pixel.x, pixel.y, 0}, + .imageExtent = {1, 1, 1}, + }; + vkCmdCopyImageToBuffer( + cmd, + image.image, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + pickBuffers[frameIndex].buffer, + 1, + ®ion); + + const VkMemoryBarrier2 toHost{ + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2, + .srcStageMask = VK_PIPELINE_STAGE_2_COPY_BIT, + .srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT, + .dstStageMask = VK_PIPELINE_STAGE_2_HOST_BIT, + .dstAccessMask = VK_ACCESS_2_HOST_READ_BIT, + }; + const VkDependencyInfo dependency{ + .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, + .memoryBarrierCount = 1, + .pMemoryBarriers = &toHost, + }; + vkCmdPipelineBarrier2(cmd, &dependency); + + pickPending[frameIndex] = true; +} + void GameRenderer::draw2D( VkCommandBuffer cmd, GfxDevice& gfxDevice, @@ -440,6 +526,13 @@ void GameRenderer::cleanup(GfxDevice& gfxDevice) sceneDataBuffer.cleanup(gfxDevice); drawList.cleanup(gfxDevice); + for (GPUBuffer& pickBuffer : pickBuffers) + { + gfxDevice.destroyBuffer(pickBuffer); + } + pickPending = {}; + pickResult.reset(); + requestedPick.reset(); // if (drawImageId != NULL_IMAGE_ID) // gfxDevice.destroyImage(); @@ -449,6 +542,7 @@ void GameRenderer::cleanup(GfxDevice& gfxDevice) drawImageId = NULL_IMAGE_ID; depthImageId = NULL_IMAGE_ID; + selectionImageId = NULL_IMAGE_ID; resources = nullptr; meshPipeline.reset(); skyboxPipeline.reset(); @@ -467,7 +561,7 @@ void GameRenderer::cleanup(GfxDevice& gfxDevice) GameState::GetInstance().SetRenderer(nullptr); } -void GameRenderer::drawMesh(MeshID id, const glm::mat4& transform, MaterialID materialId) +void GameRenderer::drawMesh(MeshID id, const glm::mat4& transform, MaterialID materialId, std::uint32_t objectId) { const auto& mesh = resources->meshes().getMesh(id); const auto worldBoundingSphere = edge::calculateBoundingSphereWorld(transform, mesh.boundingSphere, false); @@ -478,6 +572,7 @@ void GameRenderer::drawMesh(MeshID id, const glm::mat4& transform, MaterialID ma .transformMatrix = transform, .worldBoundingSphere = worldBoundingSphere, .materialId = materialId, + .objectId = objectId, }); } @@ -485,7 +580,8 @@ void GameRenderer::drawSkinnedMesh(MeshID id, const glm::mat4& transform, MaterialID materialId, SkinnedMesh* skinnedMesh, - std::size_t jointMatricesStartIndex) + std::size_t jointMatricesStartIndex, + std::uint32_t objectId) { const auto& mesh = resources->meshes().getMesh(id); const auto worldBoundingSphere = edge::calculateBoundingSphereWorld(transform, mesh.boundingSphere, true); @@ -499,6 +595,7 @@ void GameRenderer::drawSkinnedMesh(MeshID id, .materialId = materialId, .skinnedMesh = skinnedMesh, .jointMatricesStartIndex = static_cast(jointMatricesStartIndex), + .objectId = objectId, }); } @@ -650,4 +747,16 @@ void GameRenderer::createDrawImage(GfxDevice& gfxDevice, depthImageId = resources->createImage(gfxDevice, createInfo, "depth image"); spdlog::info("Created depth image with id {}", depthImageId); } + + { + auto createInfo = vkutil::CreateImageInfo{ + .format = selectionImageFormat, + .usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, + .extent = drawImageExtent, + .samples = noMsaa, + }; + + resources->destroyImage(selectionImageId); + selectionImageId = resources->createImage(gfxDevice, createInfo, "selection image"); + } } diff --git a/lightkeeper/include/Lightkeeper.h b/lightkeeper/include/Lightkeeper.h index dc51b83..3b6ddf3 100644 --- a/lightkeeper/include/Lightkeeper.h +++ b/lightkeeper/include/Lightkeeper.h @@ -36,6 +36,8 @@ public: private: void setupSkybox(); void updateViewMode(); + void focusSelection(); + void updateFocus(float dt); [[nodiscard]] CameraComponent* getSceneCamera() const; [[nodiscard]] Camera& getViewCamera(); [[nodiscard]] bool isPlayerViewActive() const; @@ -57,6 +59,7 @@ private: Camera camera{glm::vec3(0.f, 0.f, -5.f), glm::vec3(0, 1, 0)}; std::optional editorCamera; + std::optional focusTarget; std::unique_ptr skyboxCubemap; MainSceneObjects mainScene{}; diff --git a/lightkeeper/src/Lightkeeper.cpp b/lightkeeper/src/Lightkeeper.cpp index 44561a5..e2cf5ef 100644 --- a/lightkeeper/src/Lightkeeper.cpp +++ b/lightkeeper/src/Lightkeeper.cpp @@ -65,6 +65,11 @@ void LightKeeper::customUpdate(float dt) // after SDL events have been consumed. if (!isPlayerViewActive() && getSceneCamera() == nullptr) { + if (InputManager::GetInstance().WasKeyPressed(SDL_SCANCODE_F)) + { + focusSelection(); + } + updateFocus(dt); camera.Update(dt); } if (InputManager::GetInstance().WasKeyPressed(SDL_SCANCODE_F11)) diff --git a/lightkeeper/src/LightkeeperDebugUi.cpp b/lightkeeper/src/LightkeeperDebugUi.cpp index 6167f47..fc5234f 100644 --- a/lightkeeper/src/LightkeeperDebugUi.cpp +++ b/lightkeeper/src/LightkeeperDebugUi.cpp @@ -19,8 +19,12 @@ #include +#include +#include #include #include +#include +#include #include #include #include @@ -162,6 +166,11 @@ void LightKeeper::drawDebugMenuBar() if (ImGui::MenuItem("Forward+ Light Heatmap", nullptr, &lightHeatmap)) { renderer.setRenderLightHeatmap(lightHeatmap); } + + bool selectionView = renderer.getRenderSelectionView(); + if (ImGui::MenuItem("Selection Buffer", nullptr, &selectionView)) { + renderer.setRenderSelectionView(selectionView); + } ImGui::EndMenu(); } @@ -217,6 +226,71 @@ void LightKeeper::onViewportAssetDropped(std::string_view path) } } +void LightKeeper::focusSelection() +{ + GameObject* selected = SceneManager::GetInstance().GetCurrentScene().GetSelectedObject(); + if (selected == nullptr) { + return; + } + + glm::vec3 boundsMin{std::numeric_limits::max()}; + glm::vec3 boundsMax{std::numeric_limits::lowest()}; + std::vector pending{selected}; + while (!pending.empty()) { + GameObject* object = pending.back(); + pending.pop_back(); + for (const Transform* child : object->GetTransform().GetChildren()) { + if (child != nullptr && child->GetOwner() != nullptr) { + pending.push_back(child->GetOwner()); + } + } + + const auto* meshRenderer = object->GetComponent(); + if (meshRenderer == nullptr || meshRenderer->GetMeshID() == NULL_MESH_ID) { + continue; + } + const GPUMesh& mesh = resources.meshes().getMesh(meshRenderer->GetMeshID()); + const glm::mat4 world = object->GetTransform().GetWorldMatrix(); + for (int corner = 0; corner < 8; ++corner) { + const glm::vec3 local{ + corner & 1 ? mesh.maxPos.x : mesh.minPos.x, + corner & 2 ? mesh.maxPos.y : mesh.minPos.y, + corner & 4 ? mesh.maxPos.z : mesh.minPos.z, + }; + const glm::vec3 point{world * glm::vec4{local, 1.0f}}; + boundsMin = glm::min(boundsMin, point); + boundsMax = glm::max(boundsMax, point); + } + } + + const bool hasBounds = boundsMin.x <= boundsMax.x; + const glm::vec3 center = hasBounds ? (boundsMin + boundsMax) * 0.5f : selected->GetTransform().GetWorldPosition(); + const float radius = hasBounds ? std::max(glm::length(boundsMax - boundsMin) * 0.5f, 0.1f) : 1.0f; + + // Fit the bounding sphere in the narrower of the two field of view angles + const float halfFovY = glm::radians(camera.GetFOVY()) * 0.5f; + const float halfFovX = std::atan(std::tan(halfFovY) * camera.GetAspectRatio()); + const float distance = radius / std::sin(std::min(halfFovX, halfFovY)) * 1.1f; + focusTarget = center - camera.GetForward() * distance; +} + +void LightKeeper::updateFocus(float dt) +{ + if (!focusTarget) { + return; + } + if (InputManager::GetInstance().IsMouseDown(SDL_BUTTON_RIGHT)) { + focusTarget.reset(); + return; + } + + camera.m_position = glm::mix(camera.m_position, *focusTarget, 1.0f - std::exp(-12.0f * dt)); + if (glm::distance(camera.m_position, *focusTarget) < 0.01f) { + camera.m_position = *focusTarget; + focusTarget.reset(); + } +} + void LightKeeper::drawViewportOverlay() { if (!showTransformGizmo || isPlayerViewActive()) {