feat: add click to select

This commit is contained in:
2026-10-23 20:53:56 +02:00
parent f055bdcbf1
commit f7e7c56392
20 changed files with 385 additions and 20 deletions
+45
View File
@@ -8,6 +8,7 @@
#include <destrum/FS/AssetFS.h>
#include <destrum/Graphics/Resources/FontCache.h>
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/ObjectModel/Transform.h>
#include <destrum/Scene/Scene.h>
#include <destrum/Scene/SceneManager.h>
#include <destrum/Serialization/SceneSerializer.h>
@@ -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<float>(extent.width), static_cast<float>(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)
@@ -216,9 +216,11 @@ void MeshRendererComponent::Render(const RenderContext& ctx) {
GetTransform().GetWorldMatrix(),
materialID,
m_skinnedMesh.get(),
jointMatricesStartIndex
jointMatricesStartIndex,
static_cast<std::uint32_t>(GetGameObject()->GetId())
);
} else {
ctx.renderer.drawMesh(meshID, GetTransform().GetWorldMatrix(), materialID);
ctx.renderer.drawMesh(meshID, GetTransform().GetWorldMatrix(), materialID,
static_cast<std::uint32_t>(GetGameObject()->GetId()));
}
}
+1 -1
View File
@@ -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;
@@ -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));
}
@@ -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<Pipeline>(
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)
{
+114 -5
View File
@@ -33,7 +33,15 @@ void GameRenderer::init(GfxDevice& gfxDevice, RenderResources& _resources, glm::
createDrawImage(gfxDevice, drawImageSize, true);
meshPipeline = std::make_unique<MeshPipeline>();
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>();
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<const std::uint32_t*>(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<std::uint32_t>(shadowMapPipeline->getShadowMapId()),
.shadowNormalOffset = shadowMapPipeline->getNormalOffset(),
.padding = 0,
.debugSelectionView = renderSelectionView ? 1u : 0u,
.lightViewProj = lightViewProj,
.spotShadowAtlasId = static_cast<std::uint32_t>(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<int>(extent.width) - 1, static_cast<int>(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,
&region);
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<std::uint32_t>(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");
}
}