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
@@ -11,7 +11,7 @@ struct InstanceData {
mat4 transform; mat4 transform;
VertexBuffer vertexBuffer; VertexBuffer vertexBuffer;
uint materialID; uint materialID;
uint padding; uint objectID;
}; };
layout (buffer_reference, scalar) readonly buffer InstanceBuffer { layout (buffer_reference, scalar) readonly buffer InstanceBuffer {
+18
View File
@@ -13,9 +13,23 @@ layout (location = 1) in vec2 inUV;
layout (location = 2) in vec3 inNormal; layout (location = 2) in vec3 inNormal;
layout (location = 3) in vec4 inTangent; layout (location = 3) in vec4 inTangent;
layout (location = 4) flat in uint inMaterialID; layout (location = 4) flat in uint inMaterialID;
layout (location = 5) flat in uint inObjectID;
layout (location = 0) out vec4 outFragColor; 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 calculateNormal(MaterialData material, uint samplerID)
{ {
vec3 n = normalize(inNormal); vec3 n = normalize(inNormal);
@@ -127,6 +141,10 @@ void main()
fragColor = mix(fragColor, lightHeatmapColor(tileLightCount), 0.5); fragColor = mix(fragColor, lightHeatmapColor(tileLightCount), 0.5);
} }
if (pcs.sceneData.debugSelectionView != 0) {
fragColor = selectionColor(inObjectID);
}
#if 0 #if 0
// TANGENT DEBUG // TANGENT DEBUG
if (inTangent == vec4(0.0)) { if (inTangent == vec4(0.0)) {
+2
View File
@@ -9,6 +9,7 @@ layout (location = 1) out vec2 outUV;
layout (location = 2) out vec3 outNormal; layout (location = 2) out vec3 outNormal;
layout (location = 3) out vec4 outTangent; layout (location = 3) out vec4 outTangent;
layout (location = 4) flat out uint outMaterialID; 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 // 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 // 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 // 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); outTangent = vec4(mat3(instance.transform) * v.tangent.xyz, v.tangent.w);
outMaterialID = instance.materialID; outMaterialID = instance.materialID;
outObjectID = instance.objectID;
} }
+1 -1
View File
@@ -37,7 +37,7 @@ layout (buffer_reference, scalar) readonly buffer SceneDataBuffer {
// directional shadows // directional shadows
uint shadowMapId; uint shadowMapId;
float shadowNormalOffset; float shadowNormalOffset;
uint padding; uint debugSelectionView;
mat4 lightViewProj; mat4 lightViewProj;
// spot shadows // spot shadows
+16
View File
@@ -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;
}
+4
View File
@@ -123,12 +123,16 @@ private:
void drawDockSpace(); void drawDockSpace();
void drawViewportWindow(); void drawViewportWindow();
void applyViewportResize(); void applyViewportResize();
void requestSelectionPick();
void applySelectionPick();
[[nodiscard]] bool canPickInViewport() const { return usesEditorViewport() && (!playing || paused); }
ViewportRect viewportRect{}; ViewportRect viewportRect{};
glm::ivec2 viewportDesiredSize{0}; glm::ivec2 viewportDesiredSize{0};
bool viewportHovered{false}; bool viewportHovered{false};
bool viewportFocused{false}; bool viewportFocused{false};
bool viewportMouseClaimed{false}; bool viewportMouseClaimed{false};
bool pickSelectsParent{false};
void drawPlayControls(); void drawPlayControls();
void applyPlayModeToggle(); void applyPlayModeToggle();
@@ -22,7 +22,7 @@ struct GPUInstanceData {
glm::mat4 transform; glm::mat4 transform;
VkDeviceAddress vertexBuffer; VkDeviceAddress vertexBuffer;
std::uint32_t materialId; std::uint32_t materialId;
std::uint32_t padding; std::uint32_t objectId;
}; };
static_assert(sizeof(GPUInstanceData) == 80); static_assert(sizeof(GPUInstanceData) == 80);
@@ -39,6 +39,11 @@ public:
VkDeviceSize offset = 0, VkDeviceSize offset = 0,
VkDeviceSize size = VK_WHOLE_SIZE) const; 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; [[nodiscard]] VkDeviceAddress getBufferAddress(const GPUBuffer& buffer) const;
private: private:
@@ -26,6 +26,8 @@ struct MeshDrawCommand {
// skinned meshes only // skinned meshes only
const SkinnedMesh* skinnedMesh{nullptr}; const SkinnedMesh* skinnedMesh{nullptr};
std::uint32_t jointMatricesStartIndex{0}; std::uint32_t jointMatricesStartIndex{0};
std::uint32_t objectId{0};
}; };
#endif //MESHDRAWCOMMAND_H #endif //MESHDRAWCOMMAND_H
@@ -24,7 +24,8 @@ public:
GfxDevice& gfxDevice, GfxDevice& gfxDevice,
RenderResources& resources, RenderResources& resources,
VkFormat drawImageFormat, VkFormat drawImageFormat,
VkFormat depthImageFormat); VkFormat depthImageFormat,
VkFormat selectionImageFormat);
void drawDepthPrepass( void drawDepthPrepass(
VkCommandBuffer cmd, VkCommandBuffer cmd,
@@ -32,7 +33,8 @@ public:
const RenderResources& resources, const RenderResources& resources,
const GPUBuffer& sceneDataBuffer, const GPUBuffer& sceneDataBuffer,
const IndirectDrawList& drawList, const IndirectDrawList& drawList,
const IndirectDrawList::View& view); const IndirectDrawList::View& view,
bool writeObjectIds);
void draw( void draw(
VkCommandBuffer cmd, VkCommandBuffer cmd,
@@ -71,6 +73,7 @@ private:
VkPipelineLayout m_pipelineLayout{VK_NULL_HANDLE}; VkPipelineLayout m_pipelineLayout{VK_NULL_HANDLE};
std::unique_ptr<Pipeline> m_pipeline; std::unique_ptr<Pipeline> m_pipeline;
std::unique_ptr<Pipeline> m_depthPrepassPipeline; std::unique_ptr<Pipeline> m_depthPrepassPipeline;
std::unique_ptr<Pipeline> m_selectionPrepassPipeline;
bool m_renderWireframe{false}; bool m_renderWireframe{false};
}; };
+40 -3
View File
@@ -2,6 +2,7 @@
#define RENDERER_H #define RENDERER_H
#include <glm/mat4x4.hpp> #include <glm/mat4x4.hpp>
#include <glm/vec2.hpp>
#include <glm/vec3.hpp> #include <glm/vec3.hpp>
#include <destrum/Graphics/Camera.h> #include <destrum/Graphics/Camera.h>
@@ -21,6 +22,7 @@
#include <array> #include <array>
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <utility>
#include <vector> #include <vector>
#include <destrum/Graphics/Pipelines/SkinningPipeline.h> #include <destrum/Graphics/Pipelines/SkinningPipeline.h>
@@ -58,9 +60,9 @@ public:
void draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& camera, const SceneData& sceneData); void draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& camera, const SceneData& sceneData);
void cleanup(GfxDevice& gfxDevice); 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, 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); void submitLight(const Light& light);
const GPUImage& getDrawImage() const; const GPUImage& getDrawImage() const;
@@ -101,6 +103,31 @@ public:
return renderLightHeatmap; 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<std::uint32_t> takeSelectionPick()
{
return std::exchange(pickResult, std::nullopt);
}
void setShadowDistance(float distance) void setShadowDistance(float distance)
{ {
shadowMapPipeline->setShadowDistance(distance); shadowMapPipeline->setShadowDistance(distance);
@@ -126,6 +153,7 @@ public:
private: private:
void createDrawImage(GfxDevice& gfxDevice, const glm::ivec2& drawImageSize, bool firstCreate); void createDrawImage(GfxDevice& gfxDevice, const glm::ivec2& drawImageSize, bool firstCreate);
void uploadLights(const Camera& camera); void uploadLights(const Camera& camera);
void recordSelectionPick(VkCommandBuffer cmd, std::uint32_t frameIndex);
[[nodiscard]] std::uint32_t assignShadowIndex(const Light& light, const Frustum& cameraFrustum); [[nodiscard]] std::uint32_t assignShadowIndex(const Light& light, const Frustum& cameraFrustum);
RenderResources* resources = nullptr; RenderResources* resources = nullptr;
@@ -142,9 +170,18 @@ private:
VkFormat drawImageFormat{VK_FORMAT_R16G16B16A16_SFLOAT}; VkFormat drawImageFormat{VK_FORMAT_R16G16B16A16_SFLOAT};
VkFormat depthImageFormat{VK_FORMAT_D32_SFLOAT}; VkFormat depthImageFormat{VK_FORMAT_D32_SFLOAT};
VkFormat selectionImageFormat{VK_FORMAT_R32_UINT};
ImageID drawImageId{NULL_IMAGE_ID}; ImageID drawImageId{NULL_IMAGE_ID};
ImageID depthImageId{NULL_IMAGE_ID}; ImageID depthImageId{NULL_IMAGE_ID};
ImageID selectionImageId{NULL_IMAGE_ID};
std::array<GPUBuffer, FRAMES_IN_FLIGHT> pickBuffers{};
std::array<bool, FRAMES_IN_FLIGHT> pickPending{};
std::optional<glm::ivec2> requestedPick;
std::optional<std::uint32_t> pickResult;
bool selectionPassEnabled{false};
bool renderSelectionView{false};
VkSampleCountFlagBits samples{VK_SAMPLE_COUNT_1_BIT}; VkSampleCountFlagBits samples{VK_SAMPLE_COUNT_1_BIT};
@@ -173,7 +210,7 @@ private:
std::uint32_t shadowMapId; std::uint32_t shadowMapId;
float shadowNormalOffset; float shadowNormalOffset;
std::uint32_t padding; std::uint32_t debugSelectionView;
glm::mat4 lightViewProj; glm::mat4 lightViewProj;
std::uint32_t spotShadowAtlasId; std::uint32_t spotShadowAtlasId;
+45
View File
@@ -8,6 +8,7 @@
#include <destrum/FS/AssetFS.h> #include <destrum/FS/AssetFS.h>
#include <destrum/Graphics/Resources/FontCache.h> #include <destrum/Graphics/Resources/FontCache.h>
#include <destrum/ObjectModel/GameObject.h> #include <destrum/ObjectModel/GameObject.h>
#include <destrum/ObjectModel/Transform.h>
#include <destrum/Scene/Scene.h> #include <destrum/Scene/Scene.h>
#include <destrum/Scene/SceneManager.h> #include <destrum/Scene/SceneManager.h>
#include <destrum/Serialization/SceneSerializer.h> #include <destrum/Serialization/SceneSerializer.h>
@@ -234,6 +235,7 @@ if (!isRunning) break;
{ {
ZoneScopedN("Debug ImGui"); ZoneScopedN("Debug ImGui");
drawWindowsMenu(); drawWindowsMenu();
applySelectionPick();
drawViewportWindow(); drawViewportWindow();
if (m_DebugWindows.inspector) if (m_DebugWindows.inspector)
@@ -380,6 +382,7 @@ if (!isRunning) break;
{ {
ZoneScopedN("customDraw"); ZoneScopedN("customDraw");
renderer.setSelectionPassEnabled(canPickInViewport());
customDraw(); customDraw();
} }
@@ -549,10 +552,52 @@ void App::drawViewportWindow()
ImGui::SetCursorScreenPos(origin); ImGui::SetCursorScreenPos(origin);
drawViewportOverlay(); drawViewportOverlay();
requestSelectionPick();
ImGui::End(); 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() void App::applyViewportResize()
{ {
if (viewportDesiredSize.x <= 0 || viewportDesiredSize.y <= 0) if (viewportDesiredSize.x <= 0 || viewportDesiredSize.y <= 0)
@@ -216,9 +216,11 @@ void MeshRendererComponent::Render(const RenderContext& ctx) {
GetTransform().GetWorldMatrix(), GetTransform().GetWorldMatrix(),
materialID, materialID,
m_skinnedMesh.get(), m_skinnedMesh.get(),
jointMatricesStartIndex jointMatricesStartIndex,
static_cast<std::uint32_t>(GetGameObject()->GetId())
); );
} else { } 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 ? dc.skinnedMesh->skinnedVertexBuffer.address
: mesh.vertexBuffer.address, : mesh.vertexBuffer.address,
.materialId = dc.materialId, .materialId = dc.materialId,
.padding = 0, .objectId = dc.objectId,
}); });
++commands.back().instanceCount; ++commands.back().instanceCount;
++view.instanceCount; ++view.instanceCount;
@@ -113,3 +113,15 @@ void MemoryManager::flushAllocation(
VK_CHECK(vmaFlushAllocation(allocator, buffer.allocation, offset, size)); 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, GfxDevice& gfxDevice,
RenderResources& resources, RenderResources& resources,
VkFormat drawImageFormat, VkFormat drawImageFormat,
VkFormat depthImageFormat) VkFormat depthImageFormat,
VkFormat selectionImageFormat)
{ {
try try
{ {
@@ -31,6 +32,9 @@ void MeshPipeline::init(
const auto fragShader = const auto fragShader =
AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/mesh.frag"); AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/mesh.frag");
const auto selectionShader =
AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/selection.frag");
const auto bufferRange = VkPushConstantRange{ const auto bufferRange = VkPushConstantRange{
.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, .stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
.offset = 0, .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{}; PipelineConfigInfo pipelineConfig{};
Pipeline::DefaultPipelineConfigInfo(pipelineConfig); Pipeline::DefaultPipelineConfigInfo(pipelineConfig);
@@ -120,16 +145,18 @@ void MeshPipeline::drawDepthPrepass(
const RenderResources& resources, const RenderResources& resources,
const GPUBuffer& sceneDataBuffer, const GPUBuffer& sceneDataBuffer,
const IndirectDrawList& drawList, 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; return;
} }
drawMeshes( drawMeshes(
cmd, cmd,
*m_depthPrepassPipeline, *pipeline,
renderExtent, renderExtent,
resources, resources,
sceneDataBuffer, sceneDataBuffer,
@@ -228,6 +255,7 @@ void MeshPipeline::cleanup(VkDevice device)
{ {
m_pipeline.reset(); m_pipeline.reset();
m_depthPrepassPipeline.reset(); m_depthPrepassPipeline.reset();
m_selectionPrepassPipeline.reset();
if (m_pipelineLayout != VK_NULL_HANDLE && device != VK_NULL_HANDLE) 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); createDrawImage(gfxDevice, drawImageSize, true);
meshPipeline = std::make_unique<MeshPipeline>(); 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 = std::make_unique<SkyboxPipeline>();
skyboxPipeline->init(gfxDevice, _resources, drawImageFormat, depthImageFormat); skyboxPipeline->init(gfxDevice, _resources, drawImageFormat, depthImageFormat);
@@ -65,6 +73,15 @@ void GameRenderer::init(GfxDevice& gfxDevice, RenderResources& _resources, glm::
void GameRenderer::beginDrawing(GfxDevice& gfxDevice) 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(); meshDrawCommands.clear();
skinningPipeline->beginDrawing(gfxDevice.getCurrentFrameIndex()); skinningPipeline->beginDrawing(gfxDevice.getCurrentFrameIndex());
lightCullingPipeline->beginDrawing(gfxDevice.getCurrentFrameIndex()); lightCullingPipeline->beginDrawing(gfxDevice.getCurrentFrameIndex());
@@ -207,7 +224,7 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
.debugLightHeatmap = renderLightHeatmap ? 1u : 0u, .debugLightHeatmap = renderLightHeatmap ? 1u : 0u,
.shadowMapId = static_cast<std::uint32_t>(shadowMapPipeline->getShadowMapId()), .shadowMapId = static_cast<std::uint32_t>(shadowMapPipeline->getShadowMapId()),
.shadowNormalOffset = shadowMapPipeline->getNormalOffset(), .shadowNormalOffset = shadowMapPipeline->getNormalOffset(),
.padding = 0, .debugSelectionView = renderSelectionView ? 1u : 0u,
.lightViewProj = lightViewProj, .lightViewProj = lightViewProj,
.spotShadowAtlasId = static_cast<std::uint32_t>(shadowMapPipeline->getSpotShadowAtlasId()), .spotShadowAtlasId = static_cast<std::uint32_t>(shadowMapPipeline->getSpotShadowAtlasId()),
.spotShadowTilesPerRow = ShadowMapPipeline::SPOT_SHADOW_TILES_PER_ROW, .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"); TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Depth Prepass");
vkutil::ScopedCmdLabel label(cmd, "Depth Prepass", vkutil::labelColors::depth); 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({ auto depthPrepassInfo = vkutil::createRenderingInfo({
.renderExtent = drawImage.getExtent2D(), .renderExtent = drawImage.getExtent2D(),
.colorImageView = selectionPassEnabled ? selectionImage.imageView : VK_NULL_HANDLE,
.colorImageClearValue = glm::vec4{0.f},
.depthImageView = depthImage.imageView, .depthImageView = depthImage.imageView,
.depthImageClearValue = 1.f, .depthImageClearValue = 1.f,
}); });
depthPrepassInfo.renderingInfo.pDepthAttachment = &depthPrepassInfo.depthAttachment; depthPrepassInfo.renderingInfo.pDepthAttachment = &depthPrepassInfo.depthAttachment;
if (selectionPassEnabled)
{
depthPrepassInfo.renderingInfo.pColorAttachments = &depthPrepassInfo.colorAttachment;
}
vkCmdBeginRendering(cmd, &depthPrepassInfo.renderingInfo); vkCmdBeginRendering(cmd, &depthPrepassInfo.renderingInfo);
@@ -303,9 +337,16 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
*resources, *resources,
sceneDataBuffer.getBuffer(), sceneDataBuffer.getBuffer(),
drawList, drawList,
mainView); mainView,
selectionPassEnabled);
vkCmdEndRendering(cmd); 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( void GameRenderer::draw2D(
VkCommandBuffer cmd, VkCommandBuffer cmd,
GfxDevice& gfxDevice, GfxDevice& gfxDevice,
@@ -440,6 +526,13 @@ void GameRenderer::cleanup(GfxDevice& gfxDevice)
sceneDataBuffer.cleanup(gfxDevice); sceneDataBuffer.cleanup(gfxDevice);
drawList.cleanup(gfxDevice); drawList.cleanup(gfxDevice);
for (GPUBuffer& pickBuffer : pickBuffers)
{
gfxDevice.destroyBuffer(pickBuffer);
}
pickPending = {};
pickResult.reset();
requestedPick.reset();
// if (drawImageId != NULL_IMAGE_ID) // if (drawImageId != NULL_IMAGE_ID)
// gfxDevice.destroyImage(); // gfxDevice.destroyImage();
@@ -449,6 +542,7 @@ void GameRenderer::cleanup(GfxDevice& gfxDevice)
drawImageId = NULL_IMAGE_ID; drawImageId = NULL_IMAGE_ID;
depthImageId = NULL_IMAGE_ID; depthImageId = NULL_IMAGE_ID;
selectionImageId = NULL_IMAGE_ID;
resources = nullptr; resources = nullptr;
meshPipeline.reset(); meshPipeline.reset();
skyboxPipeline.reset(); skyboxPipeline.reset();
@@ -467,7 +561,7 @@ void GameRenderer::cleanup(GfxDevice& gfxDevice)
GameState::GetInstance().SetRenderer(nullptr); 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& mesh = resources->meshes().getMesh(id);
const auto worldBoundingSphere = edge::calculateBoundingSphereWorld(transform, mesh.boundingSphere, false); 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, .transformMatrix = transform,
.worldBoundingSphere = worldBoundingSphere, .worldBoundingSphere = worldBoundingSphere,
.materialId = materialId, .materialId = materialId,
.objectId = objectId,
}); });
} }
@@ -485,7 +580,8 @@ void GameRenderer::drawSkinnedMesh(MeshID id,
const glm::mat4& transform, const glm::mat4& transform,
MaterialID materialId, MaterialID materialId,
SkinnedMesh* skinnedMesh, SkinnedMesh* skinnedMesh,
std::size_t jointMatricesStartIndex) std::size_t jointMatricesStartIndex,
std::uint32_t objectId)
{ {
const auto& mesh = resources->meshes().getMesh(id); const auto& mesh = resources->meshes().getMesh(id);
const auto worldBoundingSphere = edge::calculateBoundingSphereWorld(transform, mesh.boundingSphere, true); const auto worldBoundingSphere = edge::calculateBoundingSphereWorld(transform, mesh.boundingSphere, true);
@@ -499,6 +595,7 @@ void GameRenderer::drawSkinnedMesh(MeshID id,
.materialId = materialId, .materialId = materialId,
.skinnedMesh = skinnedMesh, .skinnedMesh = skinnedMesh,
.jointMatricesStartIndex = static_cast<std::uint32_t>(jointMatricesStartIndex), .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"); depthImageId = resources->createImage(gfxDevice, createInfo, "depth image");
spdlog::info("Created depth image with id {}", depthImageId); 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");
}
} }
+3
View File
@@ -36,6 +36,8 @@ public:
private: private:
void setupSkybox(); void setupSkybox();
void updateViewMode(); void updateViewMode();
void focusSelection();
void updateFocus(float dt);
[[nodiscard]] CameraComponent* getSceneCamera() const; [[nodiscard]] CameraComponent* getSceneCamera() const;
[[nodiscard]] Camera& getViewCamera(); [[nodiscard]] Camera& getViewCamera();
[[nodiscard]] bool isPlayerViewActive() const; [[nodiscard]] bool isPlayerViewActive() const;
@@ -57,6 +59,7 @@ private:
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)};
std::optional<Camera> editorCamera; std::optional<Camera> editorCamera;
std::optional<glm::vec3> focusTarget;
std::unique_ptr<CubeMap> skyboxCubemap; std::unique_ptr<CubeMap> skyboxCubemap;
MainSceneObjects mainScene{}; MainSceneObjects mainScene{};
+5
View File
@@ -65,6 +65,11 @@ void LightKeeper::customUpdate(float dt)
// after SDL events have been consumed. // after SDL events have been consumed.
if (!isPlayerViewActive() && getSceneCamera() == nullptr) if (!isPlayerViewActive() && getSceneCamera() == nullptr)
{ {
if (InputManager::GetInstance().WasKeyPressed(SDL_SCANCODE_F))
{
focusSelection();
}
updateFocus(dt);
camera.Update(dt); camera.Update(dt);
} }
if (InputManager::GetInstance().WasKeyPressed(SDL_SCANCODE_F11)) if (InputManager::GetInstance().WasKeyPressed(SDL_SCANCODE_F11))
+74
View File
@@ -19,8 +19,12 @@
#include <components/FirstPersonController.h> #include <components/FirstPersonController.h>
#include <algorithm>
#include <cmath>
#include <cstdlib> #include <cstdlib>
#include <filesystem> #include <filesystem>
#include <limits>
#include <vector>
#include <stdexcept> #include <stdexcept>
#include <string> #include <string>
#include <string_view> #include <string_view>
@@ -162,6 +166,11 @@ void LightKeeper::drawDebugMenuBar()
if (ImGui::MenuItem("Forward+ Light Heatmap", nullptr, &lightHeatmap)) { if (ImGui::MenuItem("Forward+ Light Heatmap", nullptr, &lightHeatmap)) {
renderer.setRenderLightHeatmap(lightHeatmap); renderer.setRenderLightHeatmap(lightHeatmap);
} }
bool selectionView = renderer.getRenderSelectionView();
if (ImGui::MenuItem("Selection Buffer", nullptr, &selectionView)) {
renderer.setRenderSelectionView(selectionView);
}
ImGui::EndMenu(); 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<float>::max()};
glm::vec3 boundsMax{std::numeric_limits<float>::lowest()};
std::vector<GameObject*> 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<MeshRendererComponent>();
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() void LightKeeper::drawViewportOverlay()
{ {
if (!showTransformGizmo || isPlayerViewActive()) { if (!showTransformGizmo || isPlayerViewActive()) {