From 6e24e08818770977d449b5df5297719e7207174e Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 5 May 2026 04:10:46 -0700 Subject: [PATCH] feat(editor): brush radius circle indicator on terrain - Yellow circle renders at cursor position showing brush radius - Visible in Sculpt, Paint, and Water modes - Built from 48-segment quad strip slightly above terrain surface - Renders through the water pipeline (alpha-blended, depth-tested) - Disappears when cursor leaves terrain or in Object/NPC modes - Brush VB cleaned up properly on shutdown --- tools/editor/editor_app.cpp | 8 ++++ tools/editor/editor_viewport.cpp | 78 ++++++++++++++++++++++++++++++++ tools/editor/editor_viewport.hpp | 7 +++ tools/editor/editor_water.hpp | 2 + 4 files changed, 95 insertions(+) diff --git a/tools/editor/editor_app.cpp b/tools/editor/editor_app.cpp index 1c029fa7..8bd82458 100644 --- a/tools/editor/editor_app.cpp +++ b/tools/editor/editor_app.cpp @@ -386,6 +386,13 @@ void EditorApp::updateTerrainEditing(float dt) { viewport_.clearGhostPreview(); } + // Brush circle indicator for sculpt/paint/water modes + if (mode_ == EditorMode::Sculpt || mode_ == EditorMode::Paint || mode_ == EditorMode::Water) { + viewport_.setBrushIndicator(hitPos, terrainEditor_.brush().settings().radius, true); + } else { + viewport_.setBrushIndicator(hitPos, 0, false); + } + if (painting_ && terrainEditor_.brush().settings().mode == BrushMode::Flatten) { static bool flattenSet = false; if (!flattenSet) { @@ -396,6 +403,7 @@ void EditorApp::updateTerrainEditing(float dt) { } } else { terrainEditor_.brush().setActive(false); + viewport_.setBrushIndicator({}, 0, false); viewport_.clearGhostPreview(); } } diff --git a/tools/editor/editor_viewport.cpp b/tools/editor/editor_viewport.cpp index 3ab9b912..84e71b5b 100644 --- a/tools/editor/editor_viewport.cpp +++ b/tools/editor/editor_viewport.cpp @@ -6,6 +6,7 @@ #include "pipeline/wmo_loader.hpp" #include "core/logger.hpp" #include +#include #include #include @@ -56,6 +57,7 @@ void EditorViewport::shutdown() { if (!vkCtx_) return; vkDeviceWaitIdle(vkCtx_->getDevice()); + if (brushVB_) { vmaDestroyBuffer(vkCtx_->getAllocator(), brushVB_, brushVBAlloc_); brushVB_ = VK_NULL_HANDLE; } gizmo_.shutdown(); markerRenderer_.shutdown(); waterRenderer_.shutdown(); @@ -250,6 +252,60 @@ void EditorViewport::rebuildObjects(const std::vector& objects, vkCtx_->pollUploadBatches(); } +void EditorViewport::setBrushIndicator(const glm::vec3& center, float radius, bool active) { + brushVisible_ = active; + if (!active) return; + + // Rebuild circle vertex buffer + if (brushVB_) { + vmaDestroyBuffer(vkCtx_->getAllocator(), brushVB_, brushVBAlloc_); + brushVB_ = VK_NULL_HANDLE; + } + + constexpr int SEGMENTS = 48; + struct BV { float pos[3]; float color[4]; }; + std::vector verts; + + for (int i = 0; i < SEGMENTS; i++) { + float a0 = static_cast(i) / SEGMENTS * 6.2831853f; + float a1 = static_cast(i + 1) / SEGMENTS * 6.2831853f; + float x0 = center.x + std::cos(a0) * radius; + float y0 = center.y + std::sin(a0) * radius; + float x1 = center.x + std::cos(a1) * radius; + float y1 = center.y + std::sin(a1) * radius; + float z = center.z + 1.0f; // slightly above terrain + + float w = 0.6f; // line width via thin quad + float dx0 = std::cos(a0), dy0 = std::sin(a0); + float dx1 = std::cos(a1), dy1 = std::sin(a1); + + BV v; + v.color[0] = 1.0f; v.color[1] = 1.0f; v.color[2] = 0.3f; v.color[3] = 0.7f; + + // Thin quad for each segment + v.pos[0] = x0 - dy0*w; v.pos[1] = y0 + dx0*w; v.pos[2] = z; verts.push_back(v); + v.pos[0] = x0 + dy0*w; v.pos[1] = y0 - dx0*w; v.pos[2] = z; verts.push_back(v); + v.pos[0] = x1 - dy1*w; v.pos[1] = y1 + dx1*w; v.pos[2] = z; verts.push_back(v); + + v.pos[0] = x1 - dy1*w; v.pos[1] = y1 + dx1*w; v.pos[2] = z; verts.push_back(v); + v.pos[0] = x0 + dy0*w; v.pos[1] = y0 - dx0*w; v.pos[2] = z; verts.push_back(v); + v.pos[0] = x1 + dy1*w; v.pos[1] = y1 - dx1*w; v.pos[2] = z; verts.push_back(v); + } + + brushVertCount_ = static_cast(verts.size()); + VkBufferCreateInfo bufInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; + bufInfo.size = verts.size() * sizeof(BV); + bufInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; + VmaAllocationCreateInfo allocInfo{}; + allocInfo.usage = VMA_MEMORY_USAGE_CPU_TO_GPU; + allocInfo.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT; + VmaAllocationInfo mapInfo{}; + if (vmaCreateBuffer(vkCtx_->getAllocator(), &bufInfo, &allocInfo, + &brushVB_, &brushVBAlloc_, &mapInfo) == VK_SUCCESS) { + std::memcpy(mapInfo.pMappedData, verts.data(), verts.size() * sizeof(BV)); + } +} + void EditorViewport::update(float deltaTime) { if (m2Renderer_) m2Renderer_->update(deltaTime, camera_->getPosition(), camera_->getViewProjectionMatrix()); @@ -329,6 +385,28 @@ void EditorViewport::render(VkCommandBuffer cmd) { wmoRenderer_->render(cmd, perFrameSet, *camera_); waterRenderer_.render(cmd, perFrameSet); + + // Brush indicator circle + if (brushVisible_ && brushVB_ && brushVertCount_ > 0) { + // Reuse gizmo pipeline (same vertex format, no depth test, alpha blend) + if (gizmo_.getMode() == TransformMode::None && !gizmo_.isActive()) { + // Use water pipeline for brush (it has alpha blend + depth test) + // Actually just render through the water pipeline + } + // Render brush circle using the water renderer's pipeline setup + // (same pos+color vertex format) + auto* waterPipeline = waterRenderer_.getPipeline(); + auto* waterLayout = waterRenderer_.getPipelineLayout(); + if (waterPipeline && waterLayout) { + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, waterPipeline); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, waterLayout, + 0, 1, &perFrameSet, 0, nullptr); + VkDeviceSize off = 0; + vkCmdBindVertexBuffers(cmd, 0, 1, &brushVB_, &off); + vkCmdDraw(cmd, brushVertCount_, 1, 0, 0); + } + } + gizmo_.render(cmd, perFrameSet); } diff --git a/tools/editor/editor_viewport.hpp b/tools/editor/editor_viewport.hpp index 1022c060..7cdebd0a 100644 --- a/tools/editor/editor_viewport.hpp +++ b/tools/editor/editor_viewport.hpp @@ -50,6 +50,7 @@ public: void clearGhostPreview(); TransformGizmo& getGizmo() { return gizmo_; } + void setBrushIndicator(const glm::vec3& center, float radius, bool active); void setWireframe(bool enabled); bool isWireframe() const { return wireframe_; } @@ -90,6 +91,12 @@ private: uint32_t ghostModelId_ = 0; uint32_t ghostInstanceId_ = 0; bool ghostActive_ = false; + + // Brush indicator + VkBuffer brushVB_ = VK_NULL_HANDLE; + VmaAllocation brushVBAlloc_ = VK_NULL_HANDLE; + uint32_t brushVertCount_ = 0; + bool brushVisible_ = false; }; } // namespace editor diff --git a/tools/editor/editor_water.hpp b/tools/editor/editor_water.hpp index a2e0b178..9b951ab4 100644 --- a/tools/editor/editor_water.hpp +++ b/tools/editor/editor_water.hpp @@ -23,6 +23,8 @@ public: void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet); void clear(); + VkPipeline getPipeline() const { return pipeline_; } + VkPipelineLayout getPipelineLayout() const { return pipelineLayout_; } private: bool createPipeline();