From ddf97e9b8af27d6b4175260c9bdd7cde4b204da5 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 5 May 2026 13:47:23 -0700 Subject: [PATCH] feat(editor): multi-select objects, time-of-day lighting, WOT loading - Multi-select: Ctrl+Shift+Click adds objects to selection, transforms (move/rotate/scale/delete) operate on all selected objects at once - Time-of-day slider (0-24h) with automatic sun angle, light color, ambient, fog, and sky color transitions (dawn/day/dusk/night) - View > Sky/Lighting menu: color pickers for light/ambient/fog, fog distance sliders, preset buttons (Dawn/Noon/Dusk/Night) - loadADT prefers WOT/WHM open format from custom_zones/output dirs - Selection count display when multiple objects selected - setSkyPreset now delegates to setTimeOfDay for consistency --- tools/editor/editor_app.cpp | 29 ++++++++-------- tools/editor/editor_app.hpp | 1 + tools/editor/editor_ui.cpp | 23 ++++++++++--- tools/editor/editor_viewport.cpp | 43 ++++++++++++++++++++--- tools/editor/editor_viewport.hpp | 13 +++++++ tools/editor/object_placer.cpp | 58 ++++++++++++++++++++++++++++---- tools/editor/object_placer.hpp | 10 ++++-- 7 files changed, 147 insertions(+), 30 deletions(-) diff --git a/tools/editor/editor_app.cpp b/tools/editor/editor_app.cpp index ad4f61c8..6098b72a 100644 --- a/tools/editor/editor_app.cpp +++ b/tools/editor/editor_app.cpp @@ -428,15 +428,25 @@ void EditorApp::processEvents() { showToast("Start point set — click terrain for end"); } } - // Ctrl+click = select object (any mode) + // Ctrl+click = select (Ctrl+Shift+click = add to selection) else if ((event.key.keysym.mod & KMOD_CTRL) || (SDL_GetModState() & KMOD_CTRL)) { + bool additive = (SDL_GetModState() & KMOD_SHIFT) != 0; auto ext = window_->getVkContext()->getSwapchainExtent(); rendering::Ray ray = camera_.getCamera().screenToWorldRay( static_cast(event.button.x), static_cast(event.button.y), static_cast(ext.width), static_cast(ext.height)); - objectPlacer_.selectAt(ray, 200.0f); + if (additive) { + int prevSel = objectPlacer_.getSelectedIndex(); + int hit = objectPlacer_.selectAt(ray, 200.0f); + if (hit >= 0) { + if (prevSel >= 0) objectPlacer_.addToSelection(prevSel); + objectPlacer_.addToSelection(hit); + } + } else { + objectPlacer_.selectAt(ray, 200.0f); + } } else if (mode_ == EditorMode::NPC) { auto ext = window_->getVkContext()->getSwapchainExtent(); rendering::Ray ray = camera_.getCamera().screenToWorldRay( @@ -1049,18 +1059,9 @@ void EditorApp::updateToasts(float dt) { void EditorApp::setSkyPreset(int preset) { switch (preset) { - case 0: // Day - viewport_.setClearColor(0.4f, 0.6f, 0.9f); - viewport_.setLightDir(glm::normalize(glm::vec3(0.5f, -1.0f, 0.8f))); - break; - case 1: // Dusk - viewport_.setClearColor(0.6f, 0.3f, 0.2f); - viewport_.setLightDir(glm::normalize(glm::vec3(0.8f, -0.3f, 0.1f))); - break; - case 2: // Night - viewport_.setClearColor(0.05f, 0.05f, 0.12f); - viewport_.setLightDir(glm::normalize(glm::vec3(0.2f, -0.5f, 0.8f))); - break; + case 0: viewport_.setTimeOfDay(12.0f); break; + case 1: viewport_.setTimeOfDay(18.0f); break; + case 2: viewport_.setTimeOfDay(22.0f); break; } } diff --git a/tools/editor/editor_app.hpp b/tools/editor/editor_app.hpp index ddc04838..b681c076 100644 --- a/tools/editor/editor_app.hpp +++ b/tools/editor/editor_app.hpp @@ -53,6 +53,7 @@ public: NpcPresets& getNpcPresets() { return npcPresets_; } QuestEditor& getQuestEditor() { return questEditor_; } AssetBrowser& getAssetBrowser() { return assetBrowser_; } + EditorViewport& getViewport() { return viewport_; } rendering::TerrainRenderer* getTerrainRenderer(); rendering::M2Renderer* getM2Renderer() { return viewport_.getM2Renderer(); } pipeline::AssetManager* getAssetManager() { return assetManager_.get(); } diff --git a/tools/editor/editor_ui.cpp b/tools/editor/editor_ui.cpp index 81fe53ba..e3df77c3 100644 --- a/tools/editor/editor_ui.cpp +++ b/tools/editor/editor_ui.cpp @@ -382,9 +382,21 @@ void EditorUI::renderMenuBar(EditorApp& app) { if (ImGui::MenuItem("Center on Terrain", "Home")) app.centerOnTerrain(); ImGui::Separator(); if (ImGui::BeginMenu("Sky / Lighting")) { - if (ImGui::MenuItem("Day")) app.setSkyPreset(0); - if (ImGui::MenuItem("Dusk")) app.setSkyPreset(1); - if (ImGui::MenuItem("Night")) app.setSkyPreset(2); + auto& vp = app.getViewport(); + float tod = vp.getTimeOfDay(); + if (ImGui::SliderFloat("Time of Day", &tod, 0.0f, 24.0f, "%.1fh")) + vp.setTimeOfDay(tod); + ImGui::Separator(); + if (ImGui::MenuItem("Dawn (6:30)")) vp.setTimeOfDay(6.5f); + if (ImGui::MenuItem("Noon (12:00)")) vp.setTimeOfDay(12.0f); + if (ImGui::MenuItem("Dusk (18:00)")) vp.setTimeOfDay(18.0f); + if (ImGui::MenuItem("Night (22:00)")) vp.setTimeOfDay(22.0f); + ImGui::Separator(); + ImGui::ColorEdit3("Light", &vp.getLightColor().x, ImGuiColorEditFlags_Float); + ImGui::ColorEdit3("Ambient", &vp.getAmbientColor().x, ImGuiColorEditFlags_Float); + ImGui::ColorEdit3("Fog", &vp.getFogColor().x, ImGuiColorEditFlags_Float); + ImGui::SliderFloat("Fog Near", &vp.getFogNear(), 100.0f, 10000.0f); + ImGui::SliderFloat("Fog Far", &vp.getFogFar(), 500.0f, 20000.0f); ImGui::EndMenu(); } ImGui::Separator(); @@ -1424,7 +1436,10 @@ void EditorUI::renderObjectPanel(EditorApp& app) { } if (auto* sel = placer.getSelected()) { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 0.9f, 0.3f, 1)); - ImGui::Text("Selected: %s", sel->path.c_str()); + if (placer.isMultiSelected()) + ImGui::Text("Selected: %zu objects", placer.selectionCount()); + else + ImGui::Text("Selected: %s", sel->path.c_str()); ImGui::PopStyleColor(); bool changed = false; diff --git a/tools/editor/editor_viewport.cpp b/tools/editor/editor_viewport.cpp index 905adb31..f27707f1 100644 --- a/tools/editor/editor_viewport.cpp +++ b/tools/editor/editor_viewport.cpp @@ -730,6 +730,41 @@ void EditorViewport::destroyPerFrameResources() { } } +void EditorViewport::setTimeOfDay(float t) { + timeOfDay_ = std::clamp(t, 0.0f, 24.0f); + float hour = timeOfDay_; + + // Sun angle: noon=overhead, 6am/6pm=horizon, night=below + float sunAngle = (hour - 6.0f) / 12.0f * 3.14159f; + lightDir_ = glm::normalize(glm::vec3(std::cos(sunAngle) * 0.5f, -1.0f, std::sin(sunAngle))); + + // Dawn/dusk warm tones, noon white, night blue + if (hour >= 6.0f && hour <= 8.0f) { + float t2 = (hour - 6.0f) / 2.0f; + lightColor_ = glm::mix(glm::vec3(1.0f, 0.5f, 0.2f), glm::vec3(1.0f, 0.95f, 0.85f), t2); + ambientColor_ = glm::mix(glm::vec3(0.15f, 0.1f, 0.2f), glm::vec3(0.3f, 0.3f, 0.35f), t2); + fogColor_ = glm::mix(glm::vec3(0.5f, 0.3f, 0.3f), glm::vec3(0.6f, 0.7f, 0.8f), t2); + } else if (hour >= 17.0f && hour <= 19.0f) { + float t2 = (hour - 17.0f) / 2.0f; + lightColor_ = glm::mix(glm::vec3(1.0f, 0.95f, 0.85f), glm::vec3(1.0f, 0.4f, 0.15f), t2); + ambientColor_ = glm::mix(glm::vec3(0.3f, 0.3f, 0.35f), glm::vec3(0.1f, 0.08f, 0.15f), t2); + fogColor_ = glm::mix(glm::vec3(0.6f, 0.7f, 0.8f), glm::vec3(0.4f, 0.25f, 0.3f), t2); + } else if (hour < 6.0f || hour > 19.0f) { + lightColor_ = glm::vec3(0.15f, 0.15f, 0.25f); + ambientColor_ = glm::vec3(0.05f, 0.05f, 0.1f); + fogColor_ = glm::vec3(0.1f, 0.1f, 0.15f); + } else { + lightColor_ = glm::vec3(1.0f, 0.95f, 0.85f); + ambientColor_ = glm::vec3(0.3f, 0.3f, 0.35f); + fogColor_ = glm::vec3(0.6f, 0.7f, 0.8f); + } + + // Sky/clear color follows fog + clearR_ = fogColor_.x * 0.7f; + clearG_ = fogColor_.y * 0.7f; + clearB_ = fogColor_.z * 0.7f; +} + void EditorViewport::updatePerFrameUBO() { uint32_t frame = vkCtx_->getCurrentFrame(); @@ -738,11 +773,11 @@ void EditorViewport::updatePerFrameUBO() { data.projection = camera_->getProjectionMatrix(); data.lightSpaceMatrix = glm::mat4(1.0f); data.lightDir = glm::vec4(lightDir_, 0.0f); - data.lightColor = glm::vec4(1.0f, 0.95f, 0.85f, 0.0f); - data.ambientColor = glm::vec4(0.3f, 0.3f, 0.35f, 0.0f); + data.lightColor = glm::vec4(lightColor_, 0.0f); + data.ambientColor = glm::vec4(ambientColor_, 0.0f); data.viewPos = glm::vec4(camera_->getPosition(), 0.0f); - data.fogColor = glm::vec4(0.6f, 0.7f, 0.8f, 0.0f); - data.fogParams = glm::vec4(5000.0f, 10000.0f, 0.0f, 0.0f); + data.fogColor = glm::vec4(fogColor_, 0.0f); + data.fogParams = glm::vec4(fogNear_, fogFar_, 0.0f, 0.0f); data.shadowParams = glm::vec4(0.0f, 0.0f, 0.0f, 0.0f); std::memcpy(perFrameUBOMapped_[frame], &data, sizeof(data)); diff --git a/tools/editor/editor_viewport.hpp b/tools/editor/editor_viewport.hpp index a047f736..b5098e62 100644 --- a/tools/editor/editor_viewport.hpp +++ b/tools/editor/editor_viewport.hpp @@ -62,6 +62,14 @@ public: void setLightDir(const glm::vec3& d) { lightDir_ = d; } glm::vec3 getLightDir() const { return lightDir_; } + void setTimeOfDay(float t); + float getTimeOfDay() const { return timeOfDay_; } + glm::vec3& getLightColor() { return lightColor_; } + glm::vec3& getAmbientColor() { return ambientColor_; } + glm::vec3& getFogColor() { return fogColor_; } + float& getFogNear() { return fogNear_; } + float& getFogFar() { return fogFar_; } + rendering::TerrainRenderer* getTerrainRenderer() { return terrainRenderer_.get(); } rendering::M2Renderer* getM2Renderer() { return m2Renderer_.get(); } @@ -95,6 +103,11 @@ private: bool wireframe_ = false; float clearR_ = 0.15f, clearG_ = 0.15f, clearB_ = 0.2f; glm::vec3 lightDir_ = glm::normalize(glm::vec3(0.5f, -1.0f, 0.3f)); + glm::vec3 lightColor_ = glm::vec3(1.0f, 0.95f, 0.85f); + glm::vec3 ambientColor_ = glm::vec3(0.3f, 0.3f, 0.35f); + glm::vec3 fogColor_ = glm::vec3(0.6f, 0.7f, 0.8f); + float fogNear_ = 5000.0f, fogFar_ = 10000.0f; + float timeOfDay_ = 12.0f; // Ghost preview state std::string ghostModelPath_; diff --git a/tools/editor/object_placer.cpp b/tools/editor/object_placer.cpp index bcb26932..61790f36 100644 --- a/tools/editor/object_placer.cpp +++ b/tools/editor/object_placer.cpp @@ -75,7 +75,32 @@ int ObjectPlacer::selectAt(const rendering::Ray& ray, float maxDist) { return bestIdx; } +void ObjectPlacer::addToSelection(int idx) { + if (idx < 0 || idx >= static_cast(objects_.size())) return; + for (int si : selectedIndices_) { if (si == idx) return; } + selectedIndices_.push_back(idx); + objects_[idx].selected = true; + selectedIdx_ = idx; +} + +void ObjectPlacer::toggleSelection(int idx) { + if (idx < 0 || idx >= static_cast(objects_.size())) return; + auto it = std::find(selectedIndices_.begin(), selectedIndices_.end(), idx); + if (it != selectedIndices_.end()) { + objects_[idx].selected = false; + selectedIndices_.erase(it); + selectedIdx_ = selectedIndices_.empty() ? -1 : selectedIndices_.back(); + } else { + addToSelection(idx); + } +} + void ObjectPlacer::clearSelection() { + for (int idx : selectedIndices_) { + if (idx >= 0 && idx < static_cast(objects_.size())) + objects_[idx].selected = false; + } + selectedIndices_.clear(); if (selectedIdx_ >= 0 && selectedIdx_ < static_cast(objects_.size())) objects_[selectedIdx_].selected = false; selectedIdx_ = -1; @@ -87,22 +112,43 @@ PlacedObject* ObjectPlacer::getSelected() { } void ObjectPlacer::moveSelected(const glm::vec3& delta) { - if (auto* obj = getSelected()) obj->position += delta; + if (selectedIndices_.size() > 1) { + for (int idx : selectedIndices_) objects_[idx].position += delta; + } else if (auto* obj = getSelected()) { + obj->position += delta; + } } void ObjectPlacer::rotateSelected(const glm::vec3& deltaDeg) { - if (auto* obj = getSelected()) obj->rotation += deltaDeg; + if (selectedIndices_.size() > 1) { + for (int idx : selectedIndices_) objects_[idx].rotation += deltaDeg; + } else if (auto* obj = getSelected()) { + obj->rotation += deltaDeg; + } } void ObjectPlacer::scaleSelected(float delta) { - if (auto* obj = getSelected()) + if (selectedIndices_.size() > 1) { + for (int idx : selectedIndices_) + objects_[idx].scale = std::max(0.1f, objects_[idx].scale + delta); + } else if (auto* obj = getSelected()) { obj->scale = std::max(0.1f, obj->scale + delta); + } } void ObjectPlacer::deleteSelected() { - if (selectedIdx_ < 0 || selectedIdx_ >= static_cast(objects_.size())) return; - objects_.erase(objects_.begin() + selectedIdx_); - selectedIdx_ = -1; + if (!selectedIndices_.empty()) { + std::sort(selectedIndices_.begin(), selectedIndices_.end(), std::greater()); + for (int idx : selectedIndices_) { + if (idx >= 0 && idx < static_cast(objects_.size())) + objects_.erase(objects_.begin() + idx); + } + selectedIndices_.clear(); + selectedIdx_ = -1; + } else if (selectedIdx_ >= 0 && selectedIdx_ < static_cast(objects_.size())) { + objects_.erase(objects_.begin() + selectedIdx_); + selectedIdx_ = -1; + } } void ObjectPlacer::scatter(const glm::vec3& center, float radius, int count, diff --git a/tools/editor/object_placer.hpp b/tools/editor/object_placer.hpp index de9ff95c..cd8a33e6 100644 --- a/tools/editor/object_placer.hpp +++ b/tools/editor/object_placer.hpp @@ -34,13 +34,18 @@ public: // Place object at world position void placeObject(const glm::vec3& position); - // Select object nearest to ray + // Select object nearest to ray (Shift adds to selection) int selectAt(const rendering::Ray& ray, float maxDist = 50.0f); + void addToSelection(int idx); + void toggleSelection(int idx); void clearSelection(); int getSelectedIndex() const { return selectedIdx_; } PlacedObject* getSelected(); + const std::vector& getSelectedIndices() const { return selectedIndices_; } + size_t selectionCount() const { return selectedIndices_.size(); } + bool isMultiSelected() const { return selectedIndices_.size() > 1; } - // Transform selected + // Transform selected (operates on all selected objects) void moveSelected(const glm::vec3& delta); void rotateSelected(const glm::vec3& deltaDeg); void scaleSelected(float delta); @@ -85,6 +90,7 @@ private: std::vector objects_; std::vector undoStack_; // indices of recently placed objects int selectedIdx_ = -1; + std::vector selectedIndices_; uint32_t uniqueIdCounter_ = 1; float placementRotY_ = 0.0f; float placementScale_ = 1.0f;