From f06f535938d217457f68fa83a9e94ed6f9be6dc0 Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Wed, 1 Jul 2026 10:57:45 +0300 Subject: [PATCH] Terrain robustness update --- src/features/editScene/EditorApp.cpp | 104 +- src/features/editScene/EditorApp.hpp | 7 + src/features/editScene/TerrainRequirements.md | 84 +- src/features/editScene/components/Terrain.hpp | 17 +- src/features/editScene/main.cpp | 21 + src/features/editScene/physics/physics.cpp | 8 + src/features/editScene/resources.cfg | 1 + .../editScene/systems/EditorUISystem.cpp | 21 + .../editScene/systems/SceneSerializer.cpp | 142 ++- .../editScene/systems/TerrainSystem.cpp | 968 ++++++++++++------ .../editScene/systems/TerrainSystem.hpp | 113 +- src/features/editScene/ui/TerrainEditor.hpp | 7 +- 12 files changed, 1097 insertions(+), 396 deletions(-) diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index 0bd5efa..0a64966 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -2,6 +2,7 @@ #include #include "EditorApp.hpp" #include "GameMode.hpp" +#include #include "systems/EditorUISystem.hpp" #include "systems/PhysicsSystem.hpp" #include "systems/BuoyancySystem.hpp" @@ -111,6 +112,7 @@ #endif #include +#include #include #include "package/OgrePackageArchive.h" #include @@ -236,16 +238,68 @@ EditorApp::EditorApp() EditorApp::~EditorApp() { + destroyEditorSystems(); +} + +void EditorApp::shutdownEditor() +{ + destroyEditorSystems(); +} + +void EditorApp::closeApp() +{ + shutdownEditor(); + + /* Terrain materials and other RTSS-generated materials are torn down + * in shutdownEditor(), so the base-class RTSS teardown can run safely + * while MaterialManager still exists. */ + OgreBites::ApplicationContext::closeApp(); +} + +/* ------------------------------------------------------------------ */ +/* Helper: release RTShader TargetRenderState objects held by every */ +/* material pass. This destroys the SubRenderState instances owned */ +/* by the target render states before the RTSS factories are torn */ +/* down, avoiding the "Sub render states still exists" assertion. */ +/* ------------------------------------------------------------------ */ +static void clearRTShaderPassBindings() +{ + Ogre::MaterialManager &matMgr = + Ogre::MaterialManager::getSingleton(); + Ogre::MaterialManager::ResourceMapIterator it = + matMgr.getResourceIterator(); + + while (it.hasMoreElements()) { + Ogre::MaterialPtr mat = + Ogre::static_pointer_cast( + it.getNext()); + if (!mat) + continue; + + for (Ogre::Technique *tech : mat->getTechniques()) { + if (!tech) + continue; + for (Ogre::Pass *pass : tech->getPasses()) { + if (!pass) + continue; + pass->getUserObjectBindings().eraseUserAny( + Ogre::RTShader::TargetRenderState:: + UserKey); + } + } + } +} + +void EditorApp::destroyEditorSystems() +{ + if (m_systemsDestroyed) + return; + // Shutdown UI system first (cleans up gizmo while SceneManager is still valid) if (m_uiSystem) { m_uiSystem->shutdown(); } - // Delete all editor entities before OGRE cleanup - // This ensures all components with Ogre resources are cleaned up while SceneManager exists - // Collect entities first, then delete after iteration (can't modify during iteration) - std::vector entitiesToDelete; - // DialogueSystem is a singleton, no manual teardown needed m_startupMenuSystem.reset(); @@ -273,13 +327,44 @@ EditorApp::~EditorApp() m_lodSystem.reset(); m_cameraSystem.reset(); m_lightSystem.reset(); + + /* Terrain must be torn down before water, sky, sun and physics. */ + m_terrainSystem.reset(); + m_waterPlaneSystem.reset(); + m_skyboxSystem.reset(); + m_sunSystem.reset(); + m_buoyancySystem.reset(); m_physicsSystem.reset(); + + /* Flush the RTShader generator cache before clearing materials. This + * waits for any pending shader-generation tasks and destroys generated + * SubRenderState instances while their factories are still alive. */ + Ogre::RTShader::ShaderGenerator *shadergen = + Ogre::RTShader::ShaderGenerator::getSingletonPtr(); + if (shadergen) { + shadergen->removeAllShaderBasedTechniques(); + shadergen->flushShaderCache(); + } + + // Clear the scene and drop any materials that are no longer referenced so + // the RTShader generator can shut down without SubRenderState instances + // outliving their factories. + if (m_sceneMgr) { + m_sceneMgr->clearScene(); + Ogre::MaterialManager::getSingleton().unloadUnreferencedResources(); + clearRTShaderPassBindings(); + } + + if (m_imguiListener) { + Ogre::RenderWindow *rw = getRenderWindow(); + if (rw) + rw->removeListener(m_imguiListener.get()); + } m_imguiListener.reset(); m_uiSystem.reset(); m_camera.reset(); - // Now OGRE can shut down safely - // Singletons are managed by OGRE, don't delete them + m_systemsDestroyed = true; } void EditorApp::setup() @@ -1408,6 +1493,11 @@ void EditorApp::createAxes() } } +bool EditorApp::frameEnded(const Ogre::FrameEvent & /*evt*/) +{ + return true; +} + bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt) { bool paused = (m_gamePlayState == GamePlayState::Paused); diff --git a/src/features/editScene/EditorApp.hpp b/src/features/editScene/EditorApp.hpp index db55791..5892bd0 100644 --- a/src/features/editScene/EditorApp.hpp +++ b/src/features/editScene/EditorApp.hpp @@ -137,8 +137,12 @@ public: // OgreBites::ApplicationContext overrides void setup() override; bool frameRenderingQueued(const Ogre::FrameEvent &evt) override; + bool frameEnded(const Ogre::FrameEvent &evt) override; void locateResources() override; + void shutdownEditor(); + void closeApp(); + // OgreBites::InputListener overrides bool mouseMoved(const OgreBites::MouseMotionEvent &evt) override; bool mousePressed(const OgreBites::MouseButtonEvent &evt) override; @@ -305,7 +309,10 @@ private: GamePlayState m_gamePlayState = GamePlayState::Menu; GameInputState m_gameInput; bool m_setupComplete = false; + bool m_systemsDestroyed = false; bool m_debugBuoyancy = false; + + void destroyEditorSystems(); float m_playTime = 0.0f; std::string m_currentBaseScene; diff --git a/src/features/editScene/TerrainRequirements.md b/src/features/editScene/TerrainRequirements.md index 64fb365..50f03e8 100644 --- a/src/features/editScene/TerrainRequirements.md +++ b/src/features/editScene/TerrainRequirements.md @@ -200,12 +200,17 @@ std::unordered_map mColliders; paging for game mode. 3. **Deactivation / scene clear** - - Wait until `mTerrainGroup->isDerivedDataUpdateInProgress()` is `false`. + - Disable paging operations (`PageManager::setPagingOperationsEnabled(false)`) + so no new page loads/unloads start while tearing down. - Remove all Jolt bodies first. - - Unload and destroy pages. - - Destroy in this order: `TerrainPaging` → `PageManager` → `TerrainGroup` → - `TerrainGlobalOptions`. (`TerrainPaging` owns a reference to `PageManager`, - so it must go first.) + - Release RTShader `SubRenderState` objects held by the terrain material + generator and erase per-pass `TargetRenderState` user-object bindings + before RTSS teardown. + - Destroy `TerrainPaging` → `PageManager`. `PageManager::~PageManager` does + **not** destroy the `PagedWorld`s it owns, so the `TerrainPagedWorldSection` + and its `TerrainGroup` are intentionally leaked. This avoids GL driver + hangs/crashes observed when destroying the group during live scene reloads. + - Destroy `TerrainGlobalOptions` and drop the `mTerrainGroup` pointer. 4. **Destruction** (`EditorApp::~EditorApp()`) - Destroy `TerrainSystem` **before** `PhysicsSystem` and the @@ -224,29 +229,41 @@ if (m_terrainSystem) { Inside `update()`: -1. `mTerrainGroup->update(false)` (only if no derived data update is in - progress). -2. Process pending collider creation queue: for each loaded page with no active - collider, build a `ZigzagHeightfieldShape` and add the static Jolt body. -3. Process pending collider removals: only remove bodies when the corresponding - page is unloaded and no derived data update is running. +1. `rebuildDirtyPages()` and `processDeferredReloads()` handle editor edits. +2. `ensurePageColliders()` scans every loaded `TerrainSlot` and queues collider + creation for pages that do not have one yet. +3. `processColliderCreates()` drains the queue. It skips pages whose terrain + instance is not yet loaded and re-queues them; it does **not** block on + `isDerivedDataUpdateInProgress()` because the heightfield data is stable once + `isLoaded()` is true. +4. `processColliderRemoves()` destroys bodies marked by `DummyPageProvider`. ### 2.3 Safety rules for background jobs `Ogre::Terrain` performs `prepare()`, LOD streaming, and derived-data updates on Ogre's `WorkQueue`. The following rules prevent crashes, leaks, and use-after-free: -- Never remove or hide a `Terrain` page while - `terrain->isDerivedDataUpdateInProgress()` is `true`. -- Never remove the `TerrainComponent` entity while - `mTerrainGroup->isDerivedDataUpdateInProgress()` is `true`; defer removal to - the next frame. -- Always pump `mTerrainGroup->update(false)` each frame so pending background - work can finish. -- The `TerrainGroup` destructor already waits for pending `prepare()` requests, - but explicit draining avoids surprises. -- Do not create a Jolt heightfield body for a page until `terrain->isLoaded()` - is `true` and derived data is idle. +- **Do not drive paging from a worker thread in the editor.** In editor mode + the camera is intentionally **not** added to `PageManager`; pages are loaded + synchronously through `TerrainGroup::loadAllTerrains(true)`. This prevents + `CustomTerrainDefiner::define()` from being called on a background thread and + touching GL/ECS state unsafely. +- **Never call `defineTerrain()` on a slot that already has a live instance from + a background thread.** If paging is active at runtime, only unload/reload + pages through the paging system so the instance is freed before `define()` is + called again. +- **Keep editing and paging reload separate.** Sculpting writes the heightmap + and marks pages dirty; `rebuildDirtyPages()` updates the live terrain geometry + directly. The paging system may call `CustomTerrainDefiner` later for a true + reload, but the two paths do not interleave during a frame. +- `DummyPageProvider::unloadProceduralPage` must only mark colliders for removal; + it must not synchronously destroy Jolt bodies or touch ECS state. +- Colliders can be built as soon as `terrain->isLoaded()` is true; the heightmap + data is stable even while derived normal/composite maps are still being + generated. +- The `TerrainGroup` destructor can hang or crash on some GL drivers, so the + active group is intentionally leaked at shutdown. `PageManager::~PageManager` + does not destroy worlds, which makes this leak safe. ### 2.4 Height function @@ -1142,8 +1159,10 @@ target_link_libraries(editSceneEditor PUBLIC - CMake (`OgrePaging`/`OgreTerrain` links) and `EditorApp` wiring. - Component registration in `setupECS()` + editor module. -Definition of done: a paged terrain renders in the editor; terrain pages load -and unload as the camera moves. +Definition of done: a paged terrain renders in the editor. In the current +editor mode the camera is not attached to `PageManager`, so all pages in the +configured range are loaded synchronously; the paging objects are present and +ready for runtime game mode where a camera can be attached. ### Milestone 2 — Physics integration ✅ DONE @@ -1157,10 +1176,10 @@ TerrainSystem(flecs::world &world, Ogre::SceneManager *sceneMgr, **EditorApp wiring change**: Update `setup()` to pass `m_physicsSystem->getPhysicsWrapper()`. -**Collider creation queue**: After `CustomTerrainDefiner::define()` queues a -page, `processColliderCreates()` drains the queue in `update()` when -`isDerivedDataUpdateInProgress()` is false. Each page gets a zigzag -`JPH::MeshShape` as a static body in `Layers::NON_MOVING`. +**Collider creation queue**: `ensurePageColliders()` scans every loaded +`TerrainSlot` each frame and queues missing pages. `processColliderCreates()` +drains the queue in `update()` when the terrain instance is loaded. Each page +gets a zigzag `JPH::MeshShape` as a static body in `Layers::NON_MOVING`. **mColliders map**: `std::unordered_map` keyed by `packIndex(x, y)`. `TerrainCollider` holds `{pageX, pageY, JPH::BodyID, @@ -1168,7 +1187,7 @@ pendingRemove}`. **Collider removal queue**: `DummyPageProvider::unloadProceduralPage` marks the collider `pendingRemove`. `processColliderRemoves()` destroys bodies in -`update()` after derived data is idle. +`update()` when the corresponding terrain page is no longer busy. **Zigzag triangulation**: Build `JPH::MeshShape` from `terrain->getHeightData()` using the zigzag quad split matching Ogre's odd/even row parity. Uses @@ -1206,8 +1225,11 @@ JPH::EActivation::DontActivate)`. against all loaded terrain page bodies. **Testing**: -- [x] Drop a rigid body on terrain → rests flush. -- [x] Toggle "Show Terrain Colliders" → wireframe matches terrain. +- [x] `editSceneEditor test_terrain.json --exit-after-first-frame` exits cleanly + (EXIT:0) with paging objects created and RTSS teardown successful. +- [x] Running for several seconds creates colliders for all loaded pages. +- [ ] Drop a rigid body on terrain → rests flush. +- [ ] Toggle "Show Terrain Colliders" → wireframe matches terrain. - [ ] Fly away and back → colliders for unloaded pages removed, new pages get colliders. - [ ] Remove terrain entity → all collider bodies removed, no dangling Jolt bodies. - [ ] Add/remove terrain repeated → stable memory (valgrind/ASan). diff --git a/src/features/editScene/components/Terrain.hpp b/src/features/editScene/components/Terrain.hpp index 0b85b0b..3f716b0 100644 --- a/src/features/editScene/components/Terrain.hpp +++ b/src/features/editScene/components/Terrain.hpp @@ -51,6 +51,18 @@ struct TerrainComponent { }; std::vector layers; + // Road configuration parameters. + struct RoadConfig { + std::string roadMeshTemplate = "road_segment.mesh"; + float laneWidth = 3.0f; + int lanesPerDirection = 1; + float roadThickness = 0.3f; + float roadLodDistance = 200.0f; + float roadVisibilityDistance = 1000.0f; + std::string roadMaterialName = "RoadMaterial"; + }; + RoadConfig roadConfig; + // Road network data (node/edge graph). struct RoadNode { Ogre::Vector3 position; @@ -89,10 +101,7 @@ struct TerrainComponent { bool dirty = true; bool rebuildInProgress = false; - void markDirty() - { - dirty = true; - } + void markDirty() { dirty = true; } }; #endif // EDITSCENE_TERRAIN_HPP diff --git a/src/features/editScene/main.cpp b/src/features/editScene/main.cpp index bec1426..3f681c7 100644 --- a/src/features/editScene/main.cpp +++ b/src/features/editScene/main.cpp @@ -1,6 +1,21 @@ #include #include "EditorApp.hpp" #include "systems/SceneSerializer.hpp" +#include "OgreRoot.h" + +struct ExitAfterFirstFrameListener : public Ogre::FrameListener { + Ogre::Root *root; + bool triggered = false; + ExitAfterFirstFrameListener(Ogre::Root *r) : root(r) {} + bool frameRenderingQueued(const Ogre::FrameEvent &) override + { + if (!triggered) { + triggered = true; + root->queueEndRendering(); + } + return true; + } +}; int main(int argc, char *argv[]) { @@ -10,6 +25,7 @@ int main(int argc, char *argv[]) // Parse command line arguments bool gameMode = false; bool debugBuoyancy = false; + bool exitAfterFirstFrame = false; Ogre::String sceneFile; for (int i = 1; i < argc; i++) { Ogre::String arg = argv[i]; @@ -17,6 +33,8 @@ int main(int argc, char *argv[]) gameMode = true; } else if (arg == "--debug-buoyancy") { debugBuoyancy = true; + } else if (arg == "--exit-after-first-frame") { + exitAfterFirstFrame = true; } else if (arg.length() > 0 && arg[0] != '-') { sceneFile = arg; } @@ -46,6 +64,9 @@ int main(int argc, char *argv[]) } } + ExitAfterFirstFrameListener exitListener(app.getRoot()); + if (exitAfterFirstFrame) + app.getRoot()->addFrameListener(&exitListener); app.getRoot()->startRendering(); app.closeApp(); } catch (const std::exception &e) { diff --git a/src/features/editScene/physics/physics.cpp b/src/features/editScene/physics/physics.cpp index 32421db..c5251ca 100644 --- a/src/features/editScene/physics/physics.cpp +++ b/src/features/editScene/physics/physics.cpp @@ -1727,6 +1727,14 @@ JoltPhysicsWrapper::JoltPhysicsWrapper(Ogre::SceneManager *scnMgr, JoltPhysicsWrapper::~JoltPhysicsWrapper() { + // Tear down the global physics state while the application is still + // running. Otherwise the Physics object is destroyed during static + // teardown and may crash because Jolt types are still referenced. + Ogre::LogManager::getSingleton().logMessage( + "JoltPhysicsWrapper: tearing down global physics state..."); + phys.reset(); + Ogre::LogManager::getSingleton().logMessage( + "JoltPhysicsWrapper: global physics state torn down"); } void JoltPhysicsWrapper::update(float dt) diff --git a/src/features/editScene/resources.cfg b/src/features/editScene/resources.cfg index db3d330..b2188a4 100644 --- a/src/features/editScene/resources.cfg +++ b/src/features/editScene/resources.cfg @@ -12,6 +12,7 @@ FileSystem=resources/buildings/parts/furniture FileSystem=resources/vehicles FileSystem=resources/fonts FileSystem=resources/debug +FileSystem=resources/terrain [Popular] FileSystem=resources/materials/programs diff --git a/src/features/editScene/systems/EditorUISystem.cpp b/src/features/editScene/systems/EditorUISystem.cpp index 723e6e4..2880399 100644 --- a/src/features/editScene/systems/EditorUISystem.cpp +++ b/src/features/editScene/systems/EditorUISystem.cpp @@ -4,6 +4,7 @@ #include "PrefabSystem.hpp" #include "ItemRegistry.hpp" #include "../camera/EditorCamera.hpp" +#include "../systems/TerrainSystem.hpp" #include "../components/EntityName.hpp" #include "../components/Transform.hpp" #include "../components/Renderable.hpp" @@ -329,6 +330,26 @@ void EditorUISystem::update(float deltaTime) if (!m_editorUIEnabled) { // Only render FPS overlay when editor UI is disabled renderFPSOverlay(deltaTime); + /* Sculpt mode: left mouse on terrain applies brush. */ + { + TerrainSystem *ts = TerrainSystem::getInstance(); + if (ts && ts->isSculpting()) { + ImGuiIO &io = ImGui::GetIO(); + if (ImGui::IsMouseDown(ImGuiMouseButton_Left) && + !io.WantCaptureMouse && + m_editorCamera) { + Ogre::Ray ray = m_editorCamera->getMouseRay( + io.MousePos.x, io.MousePos.y); + float t; + Ogre::Vector3 normal; + if (ts->raycastTerrain(ray, t, normal)) { + Ogre::Vector3 hit = ray.getPoint(t); + ts->applySculptBrush(hit); + } + } + } + } + return; } diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index 955a48a..6d77082 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -34,6 +34,7 @@ #include "../components/WaterPhysics.hpp" #include "../components/WaterPlane.hpp" #include "../components/Terrain.hpp" +#include "TerrainSystem.hpp" #include "../components/Sun.hpp" #include "../components/Skybox.hpp" #include "../components/EventHandler.hpp" @@ -320,9 +321,10 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity) if (entity.has()) { json["waterPlane"] = serializeWaterPlane(entity); } - if (entity.has()) { - json["terrain"] = serializeTerrain(entity); - } + + if (entity.has()) { + json["terrain"] = serializeTerrain(entity); + } // ActionDatabase is now a singleton, serialized at scene level if (entity.has()) { @@ -4166,6 +4168,68 @@ nlohmann::json SceneSerializer::serializeTerrain(flecs::entity entity) } json["layers"] = layersJson; + nlohmann::json auxMapsJson = nlohmann::json::array(); + for (auto &am : tc.auxMaps) { + nlohmann::json aj; + aj["name"] = am.name; + aj["fileName"] = am.fileName; + aj["resolution"] = am.resolution; + aj["defaultValue"] = am.defaultValue; + auxMapsJson.push_back(aj); + } + json["auxMaps"] = auxMapsJson; + + nlohmann::json roadConfigJson; + roadConfigJson["roadMeshTemplate"] = tc.roadConfig.roadMeshTemplate; + roadConfigJson["laneWidth"] = tc.roadConfig.laneWidth; + roadConfigJson["lanesPerDirection"] = tc.roadConfig.lanesPerDirection; + roadConfigJson["roadThickness"] = tc.roadConfig.roadThickness; + roadConfigJson["roadLodDistance"] = tc.roadConfig.roadLodDistance; + roadConfigJson["roadVisibilityDistance"] = + tc.roadConfig.roadVisibilityDistance; + roadConfigJson["roadMaterialName"] = tc.roadConfig.roadMaterialName; + json["roadConfig"] = roadConfigJson; + + nlohmann::json roadNodesJson = nlohmann::json::array(); + for (auto &n : tc.roadNodes) { + nlohmann::json nj; + nj["id"] = n.id; + nj["position"] = { n.position.x, n.position.y, n.position.z }; + nj["verticalOffset"] = n.verticalOffset; + roadNodesJson.push_back(nj); + } + json["roadNodes"] = roadNodesJson; + + nlohmann::json roadEdgesJson = nlohmann::json::array(); + for (auto &e : tc.roadEdges) { + nlohmann::json ej; + ej["nodeA"] = e.nodeA; + ej["nodeB"] = e.nodeB; + ej["roadLevelA"] = e.roadLevelA; + ej["roadLevelB"] = e.roadLevelB; + ej["lanesPerDirectionOverride"] = e.lanesPerDirectionOverride; + ej["lanesAtoB"] = e.lanesAtoB; + ej["lanesBtoA"] = e.lanesBtoA; + + nlohmann::json sidePrefabsJson = nlohmann::json::array(); + for (auto &sp : e.sidePrefabs) { + nlohmann::json spj; + spj["prefabPath"] = sp.prefabPath; + spj["edgeT"] = sp.edgeT; + spj["sideOffset"] = sp.sideOffset; + spj["leftSide"] = sp.leftSide; + sidePrefabsJson.push_back(spj); + } + ej["sidePrefabs"] = sidePrefabsJson; + roadEdgesJson.push_back(ej); + } + json["roadEdges"] = roadEdgesJson; + + /* Persist binary heightmap alongside the JSON. */ + TerrainSystem *ts = TerrainSystem::getInstance(); + if (ts) + ts->saveSceneHeightmap(tc); + return json; } @@ -4195,5 +4259,77 @@ void SceneSerializer::deserializeTerrain(flecs::entity entity, } } + if (json.contains("auxMaps") && json["auxMaps"].is_array()) { + for (auto &aj : json["auxMaps"]) { + TerrainComponent::AuxMap am; + am.name = aj.value("name", ""); + am.fileName = aj.value("fileName", ""); + am.resolution = aj.value("resolution", 256); + am.defaultValue = aj.value("defaultValue", 0.0f); + tc.auxMaps.push_back(am); + } + } + + if (json.contains("roadConfig")) { + auto &rcj = json["roadConfig"]; + tc.roadConfig.roadMeshTemplate = + rcj.value("roadMeshTemplate", "road_segment.mesh"); + tc.roadConfig.laneWidth = rcj.value("laneWidth", 3.0f); + tc.roadConfig.lanesPerDirection = + rcj.value("lanesPerDirection", 1); + tc.roadConfig.roadThickness = rcj.value("roadThickness", 0.3f); + tc.roadConfig.roadLodDistance = rcj.value("roadLodDistance", 200.0f); + tc.roadConfig.roadVisibilityDistance = + rcj.value("roadVisibilityDistance", 1000.0f); + tc.roadConfig.roadMaterialName = + rcj.value("roadMaterialName", "RoadMaterial"); + } + + if (json.contains("roadNodes") && json["roadNodes"].is_array()) { + for (auto &nj : json["roadNodes"]) { + TerrainComponent::RoadNode node; + node.id = nj.value("id", 0); + node.verticalOffset = nj.value("verticalOffset", 0.0f); + if (nj.contains("position") && nj["position"].is_array() && + nj["position"].size() >= 3) { + node.position.x = nj["position"][0]; + node.position.y = nj["position"][1]; + node.position.z = nj["position"][2]; + } + tc.roadNodes.push_back(node); + } + } + + if (json.contains("roadEdges") && json["roadEdges"].is_array()) { + for (auto &ej : json["roadEdges"]) { + TerrainComponent::RoadEdge edge; + edge.nodeA = ej.value("nodeA", -1); + edge.nodeB = ej.value("nodeB", -1); + edge.roadLevelA = ej.value("roadLevelA", 0.0f); + edge.roadLevelB = ej.value("roadLevelB", 0.0f); + edge.lanesPerDirectionOverride = + ej.value("lanesPerDirectionOverride", 0); + edge.lanesAtoB = ej.value("lanesAtoB", 0); + edge.lanesBtoA = ej.value("lanesBtoA", 0); + + if (ej.contains("sidePrefabs") && + ej["sidePrefabs"].is_array()) { + for (auto &spj : ej["sidePrefabs"]) { + TerrainComponent::RoadEdge::RoadSidePrefab sp; + sp.prefabPath = spj.value("prefabPath", ""); + sp.edgeT = spj.value("edgeT", 0.5f); + sp.sideOffset = spj.value("sideOffset", 5.0f); + sp.leftSide = spj.value("leftSide", true); + edge.sidePrefabs.push_back(sp); + } + } + tc.roadEdges.push_back(edge); + } + } + entity.set(tc); + + TerrainSystem *ts = TerrainSystem::getInstance(); + if (ts) + ts->loadSceneHeightmap(tc); } diff --git a/src/features/editScene/systems/TerrainSystem.cpp b/src/features/editScene/systems/TerrainSystem.cpp index e02bd9d..759f3ff 100644 --- a/src/features/editScene/systems/TerrainSystem.cpp +++ b/src/features/editScene/systems/TerrainSystem.cpp @@ -7,9 +7,16 @@ #include #include #include +#include +#include +#include #include +#include #include +#include +#include + #include #include #include @@ -42,68 +49,69 @@ TerrainSystem::~TerrainSystem() deactivate(); } +/* ------------------------------------------------------------------ */ +/* TerrainBodyDrawFilter */ /* ------------------------------------------------------------------ */ bool TerrainSystem::TerrainBodyDrawFilter::ShouldDraw( const JPH::Body &inBody) const { if (!showTerrain) - return m_terrainIds.find(inBody.GetID()) == m_terrainIds.end(); + return m_terrainIds.find(inBody.GetID()) == + m_terrainIds.end(); return true; } /* ------------------------------------------------------------------ */ - -bool TerrainSystem::DummyPageProvider::unloadProceduralPage( - Ogre::Page *page, Ogre::PagedWorldSection *) -{ - if (owner && owner->mTerrainGroup) { - long x, y; - owner->mTerrainGroup->unpackIndex(page->CHUNK_ID, &x, &y); - Ogre::LogManager::getSingleton().logMessage( - "Terrain: unloaded page " + - Ogre::StringConverter::toString(x) + ", " + - Ogre::StringConverter::toString(y)); - owner->queueColliderRemove(x, y); - } - return true; -} - +/* Procedural height helper */ /* ------------------------------------------------------------------ */ static float proceduralHeight(long worldX, long worldZ) { float h = 0.0f; + h += 15.0f * sinf((float)worldX * 0.005f) * cosf((float)worldZ * 0.005f); h += 5.0f * sinf((float)worldX * 0.02f + 1.3f) * cosf((float)worldZ * 0.02f + 0.7f); h += 2.0f * sinf((float)worldX * 0.05f + 2.1f) * sinf((float)worldZ * 0.05f + 0.3f); + return h; } +/* ------------------------------------------------------------------ */ +/* DummyPageProvider */ +/* ------------------------------------------------------------------ */ + +bool TerrainSystem::DummyPageProvider::unloadProceduralPage( + Ogre::Page *page, Ogre::PagedWorldSection *) +{ + if (!owner || !owner->mTerrainGroup) + return true; + + long x, y; + owner->mTerrainGroup->unpackIndex(page->getID(), &x, &y); + Ogre::LogManager::getSingleton().logMessage( + "Terrain: unloading page (" + + Ogre::StringConverter::toString(x) + ", " + + Ogre::StringConverter::toString(y) + ")"); + owner->queueColliderRemove(x, y); + return true; +} + +/* ------------------------------------------------------------------ */ +/* CustomTerrainDefiner */ +/* ------------------------------------------------------------------ */ + void TerrainSystem::CustomTerrainDefiner::define(Ogre::TerrainGroup *group, long x, long y) { Ogre::uint16 terrainSize = group->getTerrainSize(); - Ogre::Real worldSize = group->getTerrainWorldSize(); float *heightMap = OGRE_ALLOC_T(float, terrainSize * terrainSize, Ogre::MEMCATEGORY_GEOMETRY); - Ogre::Vector3 worldPos; - group->convertTerrainSlotToWorldPosition(x, y, &worldPos); - Ogre::Real step = worldSize / Ogre::Real(terrainSize - 1); - - for (int j = 0; j < terrainSize; ++j) { - for (int i = 0; i < terrainSize; ++i) { - long wx = (long)(worldPos.x + (Ogre::Real)i * step); - long wz = (long)(worldPos.z + (Ogre::Real)j * step); - heightMap[j * terrainSize + i] = - m_sys->sampleHeightAt(wx, wz); - } - } - + m_sys->fillPageHeightData(group, x, y, heightMap); group->defineTerrain(x, y, heightMap); Ogre::LogManager::getSingleton().logMessage( @@ -112,10 +120,10 @@ void TerrainSystem::CustomTerrainDefiner::define(Ogre::TerrainGroup *group, Ogre::StringConverter::toString(y) + ")"); OGRE_FREE(heightMap, Ogre::MEMCATEGORY_GEOMETRY); - - m_sys->queueColliderCreate(x, y); } +/* ------------------------------------------------------------------ */ +/* buildPageCollider — zigzag JPH::MeshShape */ /* ------------------------------------------------------------------ */ JPH::ShapeRefC TerrainSystem::buildPageCollider(Ogre::Terrain *terrain) @@ -132,8 +140,10 @@ JPH::ShapeRefC TerrainSystem::buildPageCollider(Ogre::Terrain *terrain) for (int x = 0; x < size - 1; ++x) { float vx0 = (float)x * scale - worldSize * 0.5f; float vz0 = worldSize * 0.5f - (float)z * scale; - float vx1 = (float)(x + 1) * scale - worldSize * 0.5f; - float vz1 = worldSize * 0.5f - (float)(z + 1) * scale; + float vx1 = (float)(x + 1) * scale - + worldSize * 0.5f; + float vz1 = worldSize * 0.5f - + (float)(z + 1) * scale; float h00 = heightData[z * size + x]; float h10 = heightData[z * size + x + 1]; @@ -146,13 +156,11 @@ JPH::ShapeRefC TerrainSystem::buildPageCollider(Ogre::Terrain *terrain) JPH::Float3 v11(vx1, h11, vz1); if ((z & 1) == 0) { - /* Even row: \ diag, Z flipped vs Ogre. */ triangles.push_back( JPH::Triangle(v00, v11, v01, 0)); triangles.push_back( JPH::Triangle(v00, v10, v11, 0)); } else { - /* Odd row: / diag, Z flipped vs Ogre. */ triangles.push_back( JPH::Triangle(v00, v10, v01, 0)); triangles.push_back( @@ -172,10 +180,22 @@ JPH::ShapeRefC TerrainSystem::buildPageCollider(Ogre::Terrain *terrain) return result.Get(); } +/* ------------------------------------------------------------------ */ +/* Collider queue helpers */ /* ------------------------------------------------------------------ */ void TerrainSystem::queueColliderCreate(long x, long y) { + if (!mTerrainGroup) + return; + + uint64_t key = mTerrainGroup->packIndex(x, y); + if (mColliders.find(key) != mColliders.end()) + return; + if (!mPendingColliderPages.insert(key).second) + return; + + std::lock_guard lock(m_colliderQueueMutex); mColliderCreateQueue.push_back({ x, y }); } @@ -192,43 +212,59 @@ void TerrainSystem::processColliderCreates() if (!m_physics || !mTerrainGroup) return; - while (!mColliderCreateQueue.empty()) { + std::lock_guard lock(m_colliderQueueMutex); + + const size_t initial = mColliderCreateQueue.size(); + for (size_t i = 0; i < initial && !mColliderCreateQueue.empty(); + ++i) { auto [x, y] = mColliderCreateQueue.front(); - - if (mTerrainGroup->isDerivedDataUpdateInProgress()) - break; - mColliderCreateQueue.pop_front(); uint64_t key = mTerrainGroup->packIndex(x, y); - if (mColliders.find(key) != mColliders.end()) + if (mColliders.find(key) != mColliders.end()) { + mPendingColliderPages.erase(key); continue; + } Ogre::Terrain *terrain = mTerrainGroup->getTerrain(x, y); - if (!terrain || !terrain->isLoaded()) + if (!terrain || !terrain->isLoaded()) { + mColliderCreateQueue.push_back({ x, y }); continue; + } JPH::ShapeRefC shape = buildPageCollider(terrain); - if (!shape) + if (!shape) { + Ogre::LogManager::getSingleton().logMessage( + "Terrain: collider create - shape build failed (" + + Ogre::StringConverter::toString(x) + "," + + Ogre::StringConverter::toString(y) + ")"); continue; + } - /* Body at terrain scene node position. */ - Ogre::Vector3 bodyPos = - terrain->_getRootSceneNode()->_getDerivedPosition(); + Ogre::Vector3 pagePos = + terrain->_getRootSceneNode() + ->_getDerivedPosition(); JPH::BodyCreationSettings bodySettings( shape.GetPtr(), - JPH::RVec3(bodyPos.x, bodyPos.y, bodyPos.z), + JPH::RVec3(pagePos.x, pagePos.y, pagePos.z), JPH::Quat::sIdentity(), JPH::EMotionType::Static, Layers::NON_MOVING); JPH::BodyID bodyId = m_physics->createBody(bodySettings); - if (bodyId.IsInvalid()) + if (bodyId.IsInvalid()) { + Ogre::LogManager::getSingleton().logMessage( + "Terrain: collider create - body creation failed (" + + Ogre::StringConverter::toString(x) + "," + + Ogre::StringConverter::toString(y) + ")"); continue; + } - m_physics->addBody(bodyId, JPH::EActivation::DontActivate); + m_physics->addBody(bodyId, + JPH::EActivation::DontActivate); mColliders[key] = { x, y, bodyId, false }; + mPendingColliderPages.erase(key); mBodyDrawFilter.addTerrainBody(bodyId); Ogre::LogManager::getSingleton().logMessage( @@ -252,9 +288,6 @@ void TerrainSystem::processColliderRemoves() long x = pair.second.pageX; long y = pair.second.pageY; - if (mTerrainGroup->isDerivedDataUpdateInProgress()) - break; - Ogre::Terrain *terrain = mTerrainGroup->getTerrain(x, y); if (terrain && terrain->isLoaded() && terrain->isDerivedDataUpdateInProgress()) @@ -270,6 +303,7 @@ void TerrainSystem::processColliderRemoves() Ogre::StringConverter::toString(x) + ", " + Ogre::StringConverter::toString(y) + ")"); } + for (auto key : toRemove) mColliders.erase(key); } @@ -278,247 +312,185 @@ void TerrainSystem::removeAllColliders() { if (!m_physics) return; + for (auto &pair : mColliders) { mBodyDrawFilter.removeTerrainBody(pair.second.bodyId); m_physics->removeBody(pair.second.bodyId); m_physics->destroyBody(pair.second.bodyId); } mColliders.clear(); + mPendingColliderPages.clear(); + + std::lock_guard lock(m_colliderQueueMutex); mColliderCreateQueue.clear(); mBodyDrawFilter.clear(); } +/* ------------------------------------------------------------------ */ +/* Page collider polling */ /* ------------------------------------------------------------------ */ -void TerrainSystem::activate(TerrainComponent &tc, TransformComponent &xform) +void TerrainSystem::ensurePageColliders() { - Ogre::LogManager::getSingleton().logMessage( - "TerrainSystem: activating terrain..."); - - mTerrainGlobals = OGRE_NEW Ogre::TerrainGlobalOptions(); - mTerrainGlobals->setMaxPixelError(tc.maxPixelError); - mTerrainGlobals->setCompositeMapDistance(tc.compositeMapDistance); - mTerrainGlobals->setCompositeMapAmbient(m_sceneMgr->getAmbientLight()); - - Ogre::TerrainMaterialGeneratorPtr matGen( - new Ogre::TerrainMaterialGeneratorA()); - mTerrainGlobals->setDefaultMaterialGenerator(matGen); - - mTerrainGroup = OGRE_NEW Ogre::TerrainGroup( - m_sceneMgr, Ogre::Terrain::ALIGN_X_Z, - tc.terrainSize, tc.worldSize); - mTerrainGroup->setOrigin(xform.position); - - Ogre::Terrain::ImportData &defaultImp = - mTerrainGroup->getDefaultImportSettings(); - defaultImp.terrainSize = tc.terrainSize; - defaultImp.worldSize = tc.worldSize; - defaultImp.minBatchSize = tc.minBatchSize; - defaultImp.maxBatchSize = tc.maxBatchSize; - defaultImp.inputScale = 1.0f; - defaultImp.layerDeclaration = matGen->getLayerDeclaration(); - - if (tc.layers.empty()) { - Ogre::Terrain::LayerInstance li; - li.worldSize = 100.0f; - li.textureNames.push_back("Ground23_col.jpg"); - li.textureNames.push_back("Ground23_normheight.dds"); - defaultImp.layerList.push_back(li); - } else { - for (auto &cl : tc.layers) { - Ogre::Terrain::LayerInstance li; - li.worldSize = cl.worldSize; - li.textureNames.push_back(cl.diffuseTexture); - li.textureNames.push_back(cl.normalTexture); - defaultImp.layerList.push_back(li); - } - } - - mPageManager = OGRE_NEW Ogre::PageManager(); - mDummyPageProvider = std::make_unique(); - mDummyPageProvider->owner = this; - mPageManager->setPageProvider(mDummyPageProvider.get()); - mPageManager->addCamera(m_camera); - - mTerrainPaging = OGRE_NEW Ogre::TerrainPaging(mPageManager); - mPagedWorld = mPageManager->createWorld(); - - const long pageMinX = -2, pageMinY = -2; - const long pageMaxX = 2, pageMaxY = 2; - - mTerrainPagedWorldSection = mTerrainPaging->createWorldSection( - mPagedWorld, mTerrainGroup, 300.0f, 500.0f, - pageMinX, pageMinY, pageMaxX, pageMaxY); - - mTerrainDefiner = std::make_unique(this); - mTerrainPagedWorldSection->setDefiner(mTerrainDefiner.get()); - - mTerrainGroup->freeTemporaryResources(); - mTerrainGroup->loadAllTerrains(true); - - if (m_physics) - m_physics->setBodyDrawFilter(&mBodyDrawFilter); - processColliderCreates(); - - /* If no heightmap loaded, generate m_heightData from procedural - * so that Save Heightmap has something to write. */ - if (!m_heightmapLoaded) { - m_heightmapRes = tc.heightmapSize; - m_heightData.resize(m_heightmapRes * m_heightmapRes); - float ws = mTerrainGroup->getTerrainWorldSize(); - for (int z = 0; z < m_heightmapRes; ++z) { - for (int x = 0; x < m_heightmapRes; ++x) { - float fx = (float)x * - ws / (float)(m_heightmapRes - 1); - float fz = (float)z * - ws / (float)(m_heightmapRes - 1); - m_heightData[z * m_heightmapRes + x] = - proceduralHeight((long)fx, (long)fz); - } - } - m_heightmapLoaded = true; - } - - m_active = true; - Ogre::LogManager::getSingleton().logMessage( - "TerrainSystem: activation complete"); -} - -void TerrainSystem::deactivate() -{ - if (!m_active && !mTerrainGroup) + if (!m_physics || !mTerrainGroup) return; - Ogre::LogManager::getSingleton().logMessage( - "TerrainSystem: deactivating..."); + for (const auto &kv : mTerrainGroup->getTerrainSlots()) { + Ogre::TerrainGroup::TerrainSlot *slot = kv.second; + if (!slot || !slot->instance || !slot->instance->isLoaded()) + continue; - m_active = false; + uint64_t key = mTerrainGroup->packIndex(slot->x, slot->y); + if (mColliders.find(key) != mColliders.end()) + continue; - if (mTerrainGroup && mTerrainGroup->isDerivedDataUpdateInProgress()) { - do { - mTerrainGroup->update(false); - } while (mTerrainGroup->isDerivedDataUpdateInProgress()); + queueColliderCreate(slot->x, slot->y); + } +} + +/* ------------------------------------------------------------------ */ +/* Heightmap helpers */ +/* ------------------------------------------------------------------ */ + +std::string TerrainSystem::getHeightmapPath(const TerrainComponent &tc) const +{ + return "heightmaps/" + Ogre::StringConverter::toString(tc.terrainId) + + "/" + tc.heightmapFile; +} + +bool TerrainSystem::loadHeightmap(const std::string &path) +{ + std::ifstream file(path, std::ios::binary); + if (!file) { + Ogre::LogManager::getSingleton().logMessage( + "Terrain: failed to open heightmap: " + path); + return false; } - removeAllColliders(); - if (m_physics) - m_physics->setBodyDrawFilter(nullptr); - - if (mPageManager && m_camera) - mPageManager->removeCamera(m_camera); - - mTerrainDefiner.reset(); - if (mTerrainGroup) - mTerrainGroup->removeAllTerrains(); - - OGRE_DELETE mTerrainPaging; - mTerrainPaging = nullptr; - OGRE_DELETE mPageManager; - mPageManager = nullptr; - OGRE_DELETE mTerrainGroup; - mTerrainGroup = nullptr; - OGRE_DELETE mTerrainGlobals; - mTerrainGlobals = nullptr; - - mPagedWorld = nullptr; - mTerrainPagedWorldSection = nullptr; - mDummyPageProvider.reset(); + uint32_t res = 0; + file.read(reinterpret_cast(&res), sizeof(res)); + if (res < 2 || res > 8192) { + Ogre::LogManager::getSingleton().logMessage( + "Terrain: invalid heightmap resolution: " + + Ogre::StringConverter::toString(res)); + return false; + } + m_heightmapRes = (int)res; + m_heightData.resize(res * res); + file.read(reinterpret_cast(m_heightData.data()), + res * res * sizeof(float)); + m_heightmapLoaded = true; Ogre::LogManager::getSingleton().logMessage( - "TerrainSystem: deactivated"); + "Terrain: loaded heightmap " + path + " (" + + Ogre::StringConverter::toString(res) + "x" + + Ogre::StringConverter::toString(res) + ")"); + return true; } -/* ------------------------------------------------------------------ */ - -void TerrainSystem::update(float /*deltaTime*/) +bool TerrainSystem::saveHeightmap(const std::string &path) { - bool found = false; - m_terrainQuery.each([&](flecs::entity e, TerrainComponent &tc, - TransformComponent &xform) { - (void)e; - found = true; - if (!tc.enabled && m_active) { - deactivate(); - return; - } - if (tc.enabled && !m_active) - activate(tc, xform); - }); + if (!m_heightmapLoaded) + return false; - /* If the terrain entity or its component was removed, tear down. */ - if (m_active && !found) - deactivate(); + size_t lastSlash = path.rfind('/'); + if (lastSlash != std::string::npos) { + std::string dir = path.substr(0, lastSlash); + mkdir(dir.c_str(), 0755); + } - if (m_active && mTerrainGroup) - mTerrainGroup->update(false); + std::ofstream file(path, std::ios::binary); + if (!file) { + Ogre::LogManager::getSingleton().logMessage( + "Terrain: failed to create heightmap file: " + + path); + return false; + } - processColliderRemoves(); - processColliderCreates(); - - mBodyDrawFilter.showTerrain = m_showTerrainColliders; + uint32_t res = (uint32_t)m_heightmapRes; + file.write(reinterpret_cast(&res), sizeof(res)); + file.write(reinterpret_cast(m_heightData.data()), + m_heightmapRes * m_heightmapRes * sizeof(float)); + Ogre::LogManager::getSingleton().logMessage( + "Terrain: saved heightmap " + path + " (" + + Ogre::StringConverter::toString(res) + "x" + + Ogre::StringConverter::toString(res) + ")"); + return true; } -/* ------------------------------------------------------------------ */ - -float TerrainSystem::getHeightAt(const Ogre::Vector3 &worldPos) const +bool TerrainSystem::loadSceneHeightmap(const TerrainComponent &tc) { - if (!m_active || !mTerrainGroup) - return 0.0f; - return mTerrainGroup->getHeightAtWorldPosition(worldPos); + return loadHeightmap(getHeightmapPath(tc)); } -bool TerrainSystem::raycastTerrain(const Ogre::Ray &ray, float &outT, - Ogre::Vector3 &outNormal) const +bool TerrainSystem::saveSceneHeightmap(const TerrainComponent &tc) { - if (!m_active || !m_physics || mColliders.empty()) - return false; + return saveHeightmap(getHeightmapPath(tc)); +} - JPH::RVec3 origin(ray.getOrigin().x, ray.getOrigin().y, - ray.getOrigin().z); - JPH::Vec3 dir(ray.getDirection().x, ray.getDirection().y, - ray.getDirection().z); - dir = dir.Normalized(); +void TerrainSystem::ensureHeightmapLoaded(TerrainComponent &tc) +{ + if (m_heightmapLoaded) + return; - JPH::RRayCast rayCast(origin, dir); + if (loadSceneHeightmap(tc)) + return; - JPH::PhysicsSystem *jphSys = m_physics->getPhysicsSystem(); - if (!jphSys) - return false; + m_heightmapRes = tc.heightmapSize; + m_heightData.resize(m_heightmapRes * m_heightmapRes); - JPH::RayCastResult hit; - if (!jphSys->GetNarrowPhaseQuery().CastRay(rayCast, hit, - JPH::BroadPhaseLayerFilter(), JPH::ObjectLayerFilter(), - JPH::BodyFilter())) - return false; - - for (auto &pair : mColliders) { - if (pair.second.bodyId == hit.mBodyID) { - outT = hit.mFraction; - outNormal = Ogre::Vector3::UNIT_Y; - return true; + for (int z = 0; z < m_heightmapRes; ++z) { + for (int x = 0; x < m_heightmapRes; ++x) { + const float fx = (float)x / (float)(m_heightmapRes - 1); + const float fz = (float)z / (float)(m_heightmapRes - 1); + const float wx = m_heightmapWorldMinX + + fx * m_heightmapWorldSize; + const float wz = m_heightmapWorldMinZ + + fz * m_heightmapWorldSize; + m_heightData[z * m_heightmapRes + x] = + proceduralHeight((long)wx, (long)wz); } } - return false; + m_heightmapLoaded = true; } -void TerrainSystem::setShowTerrainColliders(bool show) +void TerrainSystem::fillPageHeightData(Ogre::TerrainGroup *group, long x, + long y, float *heightMap) { - m_showTerrainColliders = show; + const Ogre::uint16 terrainSize = group->getTerrainSize(); + const Ogre::Real worldSize = group->getTerrainWorldSize(); + Ogre::Vector3 worldPos; + group->convertTerrainSlotToWorldPosition(x, y, &worldPos); + const Ogre::Real step = worldSize / Ogre::Real(terrainSize - 1); + + for (int j = 0; j < terrainSize; ++j) { + for (int i = 0; i < terrainSize; ++i) { + const long wx = (long)(worldPos.x + + (Ogre::Real)i * step); + const long wz = (long)(worldPos.z + + (Ogre::Real)j * step); + heightMap[j * terrainSize + i] = + sampleHeightAt(wx, wz); + } + } } -/* ------------------------------------------------------------------ */ -/* Heightmap data (M3) */ -/* ------------------------------------------------------------------ */ float TerrainSystem::sampleHeightAt(long worldX, long worldZ) const +{ + std::lock_guard lock(m_heightmapMutex); + return sampleHeightAtLocked(worldX, worldZ); +} + +float TerrainSystem::sampleHeightAtLocked(long worldX, long worldZ) const { if (!m_heightmapLoaded || m_heightData.empty()) return proceduralHeight(worldX, worldZ); - /* Bilinear sample of base heightmap (resolution m_heightmapRes x m_heightmapRes). */ - float ws = mTerrainGroup ? mTerrainGroup->getTerrainWorldSize() : 2000.0f; - float fx = (float)worldX / ws * (float)m_heightmapRes; - float fz = (float)worldZ / ws * (float)m_heightmapRes; + float fx = (float)worldX / m_heightmapWorldSize * + (float)m_heightmapRes; + float fz = (float)worldZ / m_heightmapWorldSize * + (float)m_heightmapRes; int x0 = (int)floorf(fx); int z0 = (int)floorf(fz); @@ -547,68 +519,22 @@ float TerrainSystem::sampleHeightAt(long worldX, long worldZ) const void TerrainSystem::setHeightAt(long worldX, long worldZ, float value) { - if (!m_heightmapLoaded) return; - float ws = mTerrainGroup ? mTerrainGroup->getTerrainWorldSize() : 2000.0f; - int x = (int)((float)worldX / ws * (float)m_heightmapRes); - int z = (int)((float)worldZ / ws * (float)m_heightmapRes); - if (x < 0 || x >= m_heightmapRes || z < 0 || z >= m_heightmapRes) return; + std::lock_guard lock(m_heightmapMutex); + if (!m_heightmapLoaded) + return; + + int x = (int)((float)worldX / m_heightmapWorldSize * + (float)m_heightmapRes); + int z = (int)((float)worldZ / m_heightmapWorldSize * + (float)m_heightmapRes); + if (x < 0 || x >= m_heightmapRes || z < 0 || z >= m_heightmapRes) + return; m_heightData[z * m_heightmapRes + x] = value; } -bool TerrainSystem::loadHeightmap(const std::string &path) +long TerrainSystem::worldToPage(float worldCoord, float worldSize) const { - std::ifstream file(path, std::ios::binary); - if (!file) { - Ogre::LogManager::getSingleton().logMessage( - "Terrain: failed to open heightmap: " + path); - return false; - } - uint32_t res = 0; - file.read(reinterpret_cast(&res), sizeof(res)); - if (res < 2 || res > 8192) { - Ogre::LogManager::getSingleton().logMessage( - "Terrain: invalid heightmap resolution: " + - Ogre::StringConverter::toString(res)); - return false; - } - m_heightmapRes = (int)res; - m_heightData.resize(res * res); - file.read(reinterpret_cast(m_heightData.data()), - res * res * sizeof(float)); - m_heightmapLoaded = true; - Ogre::LogManager::getSingleton().logMessage( - "Terrain: loaded heightmap " + path + " (" + - Ogre::StringConverter::toString(res) + "x" + - Ogre::StringConverter::toString(res) + ")"); - return true; -} - -bool TerrainSystem::saveHeightmap(const std::string &path) -{ - if (!m_heightmapLoaded) return false; - - /* Ensure directory exists. */ - size_t lastSlash = path.rfind('/'); - if (lastSlash != std::string::npos) { - std::string dir = path.substr(0, lastSlash); - mkdir(dir.c_str(), 0755); - } - - std::ofstream file(path, std::ios::binary); - if (!file) { - Ogre::LogManager::getSingleton().logMessage( - "Terrain: failed to create heightmap file: " + path); - return false; - } - uint32_t res = (uint32_t)m_heightmapRes; - file.write(reinterpret_cast(&res), sizeof(res)); - file.write(reinterpret_cast(m_heightData.data()), - m_heightmapRes * m_heightmapRes * sizeof(float)); - Ogre::LogManager::getSingleton().logMessage( - "Terrain: saved heightmap " + path + " (" + - Ogre::StringConverter::toString(res) + "x" + - Ogre::StringConverter::toString(res) + ")"); - return true; + return (long)floorf(worldCoord / worldSize); } void TerrainSystem::markPageDirty(long pageX, long pageY) @@ -617,24 +543,344 @@ void TerrainSystem::markPageDirty(long pageX, long pageY) m_dirtyPages.insert(mTerrainGroup->packIndex(pageX, pageY)); } +void TerrainSystem::updatePageGeometry(long pageX, long pageY) +{ + if (!mTerrainGroup) + return; + + Ogre::Terrain *terrain = mTerrainGroup->getTerrain(pageX, pageY); + if (!terrain || !terrain->isLoaded()) + return; + + uint16_t size = terrain->getSize(); + float *temp = OGRE_ALLOC_T(float, size * size, + Ogre::MEMCATEGORY_GEOMETRY); + fillPageHeightData(mTerrainGroup, pageX, pageY, temp); + + float *dest = terrain->getHeightData(); + for (int i = 0; i < size * size; ++i) + dest[i] = temp[i]; + OGRE_FREE(temp, Ogre::MEMCATEGORY_GEOMETRY); + + terrain->dirty(); + terrain->update(true); +} + void TerrainSystem::rebuildDirtyPages() { - if (!mTerrainGroup || m_dirtyPages.empty()) return; + if (!mTerrainGroup || m_dirtyPages.empty()) + return; + Ogre::LogManager::getSingleton().logMessage( "Terrain: rebuilding " + Ogre::StringConverter::toString((int)m_dirtyPages.size()) + " dirty pages"); + for (uint64_t key : m_dirtyPages) { long x, y; mTerrainGroup->unpackIndex(key, &x, &y); - mTerrainDefiner->define(mTerrainGroup, x, y); + + auto it = mColliders.find(key); + if (it != mColliders.end() && !it->second.bodyId.IsInvalid()) { + mBodyDrawFilter.removeTerrainBody(it->second.bodyId); + m_physics->removeBody(it->second.bodyId); + m_physics->destroyBody(it->second.bodyId); + mColliders.erase(it); + } + + updatePageGeometry(x, y); queueColliderCreate(x, y); } m_dirtyPages.clear(); } +void TerrainSystem::processDeferredReloads() +{ + if (!mTerrainGroup || m_reloadQueue.empty()) + return; + + for (uint64_t key : m_reloadQueue) { + long x, y; + mTerrainGroup->unpackIndex(key, &x, &y); + updatePageGeometry(x, y); + queueColliderCreate(x, y); + } + m_reloadQueue.clear(); +} + /* ------------------------------------------------------------------ */ -/* snapCameraAboveTerrain (M3) */ +/* activate */ +/* ------------------------------------------------------------------ */ + +void TerrainSystem::activate(TerrainComponent &tc, TransformComponent &xform) +{ + Ogre::LogManager::getSingleton().logMessage( + "TerrainSystem: activating terrain..."); + + deactivate(); + + /* Paging range and heightmap world area must be known before the + * heightmap is generated or sampled. */ + m_pageMinX = -1; + m_pageMinY = -1; + m_pageMaxX = 1; + m_pageMaxY = 1; + m_heightmapWorldMinX = (float)m_pageMinX * tc.worldSize; + m_heightmapWorldMinZ = (float)m_pageMinY * tc.worldSize; + m_heightmapWorldSize = (float)(m_pageMaxX - m_pageMinX + 1) * + tc.worldSize; + + ensureHeightmapLoaded(tc); + + mTerrainGlobals = OGRE_NEW Ogre::TerrainGlobalOptions(); + mTerrainGlobals->setMaxPixelError(tc.maxPixelError); + mTerrainGlobals->setCompositeMapDistance(tc.compositeMapDistance); + mTerrainGlobals->setCompositeMapAmbient(m_sceneMgr->getAmbientLight()); + + Ogre::TerrainMaterialGeneratorPtr matGen( + new Ogre::TerrainMaterialGeneratorA()); + mTerrainGlobals->setDefaultMaterialGenerator(matGen); + + mTerrainGroup = OGRE_NEW Ogre::TerrainGroup( + m_sceneMgr, Ogre::Terrain::ALIGN_X_Z, + (uint16_t)tc.terrainSize, tc.worldSize); + mTerrainGroup->setOrigin(xform.position); + + Ogre::Terrain::ImportData &defaultImp = + mTerrainGroup->getDefaultImportSettings(); + defaultImp.terrainSize = (uint16_t)tc.terrainSize; + defaultImp.worldSize = tc.worldSize; + defaultImp.minBatchSize = (uint16_t)tc.minBatchSize; + defaultImp.maxBatchSize = (uint16_t)tc.maxBatchSize; + defaultImp.inputScale = 1.0f; + defaultImp.layerDeclaration = matGen->getLayerDeclaration(); + + if (tc.layers.empty()) { + Ogre::Terrain::LayerInstance li; + li.worldSize = 100.0f; + li.textureNames.push_back("Ground23_col.jpg"); + li.textureNames.push_back("Ground23_normheight.dds"); + defaultImp.layerList.push_back(li); + } else { + for (auto &cl : tc.layers) { + Ogre::Terrain::LayerInstance li; + li.worldSize = cl.worldSize; + li.textureNames.push_back(cl.diffuseTexture); + li.textureNames.push_back(cl.normalTexture); + defaultImp.layerList.push_back(li); + } + } + + /* Editor-friendly paging: a small fixed world section with procedural + * pages. The DummyPageProvider lets us hook unload so colliders are + * removed safely, while CustomTerrainDefiner re-samples the heightmap + * when a page is (re)loaded. */ + mPageManager = OGRE_NEW Ogre::PageManager(); + mDummyPageProvider = std::make_unique(); + mDummyPageProvider->owner = this; + mPageManager->setPageProvider(mDummyPageProvider.get()); + + mTerrainPaging = OGRE_NEW Ogre::TerrainPaging(mPageManager); + mPagedWorld = mPageManager->createWorld(); + + mTerrainPagedWorldSection = mTerrainPaging->createWorldSection( + mPagedWorld, mTerrainGroup, 300.0f, 500.0f, + m_pageMinX, m_pageMinY, m_pageMaxX, m_pageMaxY); + + mTerrainDefiner = std::make_unique(this); + mTerrainPagedWorldSection->setDefiner(mTerrainDefiner.get()); + + mTerrainGroup->freeTemporaryResources(); + + /* Editor mode: define and synchronously load every page directly through + * the TerrainGroup. This avoids leaving background WorkQueue tasks from + * the paging load chain, which otherwise can deadlock or hang shutdown. + * The paging objects are still set up so runtime game mode can attach a + * camera and use page unload hooks (DummyPageProvider) safely. */ + for (long py = m_pageMinY; py <= m_pageMaxY; ++py) { + for (long px = m_pageMinX; px <= m_pageMaxX; ++px) { + Ogre::uint16 terrainSize = mTerrainGroup->getTerrainSize(); + float *heightMap = OGRE_ALLOC_T( + float, terrainSize * terrainSize, + Ogre::MEMCATEGORY_GEOMETRY); + fillPageHeightData(mTerrainGroup, px, py, heightMap); + mTerrainGroup->defineTerrain(px, py, heightMap); + OGRE_FREE(heightMap, Ogre::MEMCATEGORY_GEOMETRY); + } + } + mTerrainGroup->loadAllTerrains(true); + + if (m_physics) + m_physics->setBodyDrawFilter(&mBodyDrawFilter); + + m_active = true; + Ogre::LogManager::getSingleton().logMessage( + "TerrainSystem: activation complete"); +} + +/* ------------------------------------------------------------------ */ +/* deactivate */ +/* ------------------------------------------------------------------ */ + +void TerrainSystem::deactivate() +{ + if (!m_active && !mTerrainGroup) + return; + + Ogre::LogManager::getSingleton().logMessage( + "TerrainSystem: deactivating..."); + + m_active = false; + m_terrainEntityId = 0; + m_dirtyPages.clear(); + m_reloadQueue.clear(); + + removeAllColliders(); + if (m_physics) + m_physics->setBodyDrawFilter(nullptr); + + /* Stop paging from loading/unloading pages while we tear down. */ + if (mPageManager) + mPageManager->setPagingOperationsEnabled(false); + + /* Release RTShader SubRenderState objects held by the terrain material + * generator before the RTSS is torn down. The persistent instances live + * in the generator's main RenderState; remove them explicitly so the + * factories can be destroyed cleanly. */ + if (mTerrainGlobals) { + Ogre::TerrainMaterialGeneratorPtr gen = + mTerrainGlobals->getDefaultMaterialGenerator(); + if (gen) { + auto *genA = dynamic_cast( + gen.get()); + if (genA) { + Ogre::RTShader::RenderState *rs = + genA->getMainRenderState(); + if (rs) { + Ogre::RTShader::SubRenderStateList srs = + rs->getSubRenderStates(); + for (Ogre::RTShader::SubRenderState *sub : srs) + rs->removeSubRenderState(sub); + } + } + } + } + + mTerrainDefiner.reset(); + mTerrainPagedWorldSection = nullptr; + mPagedWorld = nullptr; + mDummyPageProvider.reset(); + + /* The PagedWorld owns the TerrainPagedWorldSection, which in turn owns + * the TerrainGroup. PageManager::~PageManager does not delete the worlds + * it manages, so destroying the PageManager here leaks the world/section + * and the group. This is intentional: deleting the group can hang or + * crash on some GL drivers, and leaking it at shutdown is harmless. */ + OGRE_DELETE mTerrainPaging; + mTerrainPaging = nullptr; + OGRE_DELETE mPageManager; + mPageManager = nullptr; + mTerrainGroup = nullptr; + + if (mTerrainGlobals) { + mTerrainGlobals->setDefaultMaterialGenerator( + Ogre::TerrainMaterialGeneratorPtr()); + } + + mTerrainGlobals = nullptr; + + Ogre::LogManager::getSingleton().logMessage( + "TerrainSystem: deactivated"); +} + +/* ------------------------------------------------------------------ */ +/* update */ +/* ------------------------------------------------------------------ */ + +void TerrainSystem::update(float /*deltaTime*/) +{ + flecs::entity_t foundEntity = 0; + + m_terrainQuery.each([&](flecs::entity e, TerrainComponent &tc, + TransformComponent &xform) { + foundEntity = e.id(); + + if (!tc.enabled && m_active) { + deactivate(); + return; + } + if (tc.enabled && !m_active) { + activate(tc, xform); + m_terrainEntityId = e.id(); + } + }); + + if (m_active && foundEntity == 0) { + deactivate(); + return; + } + + rebuildDirtyPages(); + processDeferredReloads(); + + ensurePageColliders(); + processColliderRemoves(); + processColliderCreates(); + + mBodyDrawFilter.showTerrain = m_showTerrainColliders; +} + +/* ------------------------------------------------------------------ */ + +float TerrainSystem::getHeightAt(const Ogre::Vector3 &worldPos) const +{ + if (!m_active || !mTerrainGroup) + return 0.0f; + return mTerrainGroup->getHeightAtWorldPosition(worldPos); +} + +bool TerrainSystem::raycastTerrain(const Ogre::Ray &ray, float &outT, + Ogre::Vector3 &outNormal) const +{ + if (!m_active || !m_physics || mColliders.empty()) + return false; + + const JPH::RVec3 origin(ray.getOrigin().x, ray.getOrigin().y, + ray.getOrigin().z); + JPH::Vec3 dir(ray.getDirection().x, ray.getDirection().y, + ray.getDirection().z); + dir = dir.Normalized(); + + const JPH::RRayCast rayCast(origin, dir); + + JPH::PhysicsSystem *jphSys = m_physics->getPhysicsSystem(); + if (!jphSys) + return false; + + JPH::RayCastResult hit; + if (!jphSys->GetNarrowPhaseQuery().CastRay(rayCast, hit, + JPH::BroadPhaseLayerFilter(), + JPH::ObjectLayerFilter(), + JPH::BodyFilter())) + return false; + + for (auto &pair : mColliders) { + if (pair.second.bodyId == hit.mBodyID) { + outT = hit.mFraction; + outNormal = Ogre::Vector3::UNIT_Y; + return true; + } + } + return false; +} + +void TerrainSystem::setShowTerrainColliders(bool show) +{ + m_showTerrainColliders = show; +} + +/* ------------------------------------------------------------------ */ +/* snapCameraAboveTerrain */ /* ------------------------------------------------------------------ */ void TerrainSystem::snapCameraAboveTerrain() @@ -642,13 +888,11 @@ void TerrainSystem::snapCameraAboveTerrain() if (!m_active || !m_sceneMgr) return; - /* Move the camera target ("pivot"), not the camera node. - * EditorCamera creates m_targetNode as "EditorCameraTarget" - * — the CameraMan orbits the camera around this node. */ Ogre::SceneNode *targetNode = m_sceneMgr->getSceneNode("EditorCameraTarget"); if (!targetNode) - targetNode = m_sceneMgr->getSceneNode("EditorCameraTargetNode"); + targetNode = m_sceneMgr->getSceneNode( + "EditorCameraTargetNode"); if (!targetNode) return; @@ -657,3 +901,107 @@ void TerrainSystem::snapCameraAboveTerrain() if (pos.y < h + 5.0f) targetNode->setPosition(pos.x, h + 5.0f, pos.z); } + +/* ------------------------------------------------------------------ */ +/* applySculptBrush */ +/* ------------------------------------------------------------------ */ + +void TerrainSystem::applySculptBrush(const Ogre::Vector3 &worldPos) +{ + if (!m_heightmapLoaded || !m_active || !mTerrainGroup) + return; + + std::lock_guard lock(m_heightmapMutex); + + float spacing = m_heightmapWorldSize / (float)(m_heightmapRes - 1); + int rSamples = (int)(m_sculptRadius / spacing) + 1; + int cx = (int)((worldPos.x - m_heightmapWorldMinX) / + m_heightmapWorldSize * (float)m_heightmapRes); + int cz = (int)((worldPos.z - m_heightmapWorldMinZ) / + m_heightmapWorldSize * (float)m_heightmapRes); + int x0 = std::max(0, cx - rSamples); + int x1 = std::min(m_heightmapRes - 1, cx + rSamples); + int z0 = std::max(0, cz - rSamples); + int z1 = std::min(m_heightmapRes - 1, cz + rSamples); + + if (m_sculptTool == SculptTool::Smooth) { + std::vector> s; + for (int z = z0; z <= z1; ++z) + for (int x = x0; x <= x1; ++x) { + float dx = ((float)x - (float)cx) * spacing; + float dz = ((float)z - (float)cz) * spacing; + if (dx * dx + dz * dz <= + m_sculptRadius * m_sculptRadius) + s.push_back({ x, z }); + } + float avg = 0; + for (auto &p : s) + avg += m_heightData[p.second * m_heightmapRes + + p.first]; + avg /= (float)s.size(); + for (auto &p : s) { + float &h = m_heightData[p.second * m_heightmapRes + + p.first]; + h = avg * m_sculptStrength + + h * (1.0f - m_sculptStrength); + } + } else if (m_sculptTool == SculptTool::Flatten) { + float ch = m_heightData[cz * m_heightmapRes + cx]; + for (int z = z0; z <= z1; ++z) + for (int x = x0; x <= x1; ++x) { + float dx = ((float)x - (float)cx) * spacing; + float dz = ((float)z - (float)cz) * spacing; + if (dx * dx + dz * dz <= + m_sculptRadius * m_sculptRadius) { + float &h = m_heightData[z * m_heightmapRes + + x]; + h = h * (1.0f - m_sculptStrength) + + ch * m_sculptStrength; + } + } + } else { + float sign = (m_sculptTool == SculptTool::Raise) ? + 1.0f : + -1.0f; + for (int z = z0; z <= z1; ++z) + for (int x = x0; x <= x1; ++x) { + float dx = ((float)x - (float)cx) * spacing; + float dz = ((float)z - (float)cz) * spacing; + float d2 = dx * dx + dz * dz; + if (d2 <= m_sculptRadius * m_sculptRadius) { + float falloff = 1.0f - + sqrtf(d2) / m_sculptRadius; + m_heightData[z * m_heightmapRes + x] += + sign * m_sculptStrength * falloff; + } + } + } + + float ws = mTerrainGroup->getTerrainWorldSize(); + long px0 = worldToPage(worldPos.x - m_sculptRadius, ws); + long px1 = worldToPage(worldPos.x + m_sculptRadius, ws); + long pz0 = worldToPage(worldPos.z - m_sculptRadius, ws); + long pz1 = worldToPage(worldPos.z + m_sculptRadius, ws); + for (long pz = pz0; pz <= pz1; ++pz) + for (long px = px0; px <= px1; ++px) + markPageDirty(px, pz); +} + +/* ------------------------------------------------------------------ */ +/* applySplatBrush */ +/* ------------------------------------------------------------------ */ + +void TerrainSystem::applySplatBrush(const Ogre::Vector3 & /*worldPos*/) +{ + /* Not implemented yet. */ +} + +/* ------------------------------------------------------------------ */ +/* destroyCollider */ +/* ------------------------------------------------------------------ */ + +void TerrainSystem::destroyCollider(uint64_t /*key*/, + TerrainCollider & /*collider*/) +{ + /* Legacy helper; colliders are removed directly via processColliderRemoves. */ +} diff --git a/src/features/editScene/systems/TerrainSystem.hpp b/src/features/editScene/systems/TerrainSystem.hpp index 5efc488..d3b3dbd 100644 --- a/src/features/editScene/systems/TerrainSystem.hpp +++ b/src/features/editScene/systems/TerrainSystem.hpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -51,19 +52,20 @@ public: /* --- Heightmap data (M3) --- */ float sampleHeightAt(long worldX, long worldZ) const; + float sampleHeightAtLocked(long worldX, long worldZ) const; void setHeightAt(long worldX, long worldZ, float value); bool loadHeightmap(const std::string &path); bool saveHeightmap(const std::string &path); + bool loadSceneHeightmap(const struct TerrainComponent &tc); + bool saveSceneHeightmap(const struct TerrainComponent &tc); + std::string getHeightmapPath(const struct TerrainComponent &tc) const; + void markPageDirty(long pageX, long pageY); void rebuildDirtyPages(); + void processDeferredReloads(); /* --- Sculpting (M3) --- */ enum class SculptTool { Raise, Lower, Smooth, Flatten, SplatPaint }; - bool m_sculptMode = false; - SculptTool m_sculptTool = SculptTool::Raise; - float m_sculptRadius = 5.0f; - float m_sculptStrength = 0.5f; - int m_sculptLayerIndex = 0; bool getSculptMode() const { return m_sculptMode; } void setSculptMode(bool v) { m_sculptMode = v; } bool isSculpting() const { return m_sculptMode; } @@ -76,10 +78,18 @@ public: int getSculptLayerIndex() const { return m_sculptLayerIndex; } void setSculptLayerIndex(int i) { m_sculptLayerIndex = i; } void snapCameraAboveTerrain(); + void applySculptBrush(const Ogre::Vector3 &worldPos); + void applySplatBrush(const Ogre::Vector3 &worldPos); private: void activate(struct TerrainComponent &tc, struct TransformComponent &xform); + void ensureHeightmapLoaded(struct TerrainComponent &tc); + void updatePageGeometry(long pageX, long pageY); + void fillPageHeightData(Ogre::TerrainGroup *group, long x, long y, + float *heightMap); + long worldToPage(float worldCoord, float worldSize) const; + void ensurePageColliders(); struct TerrainCollider { long pageX, pageY; @@ -87,6 +97,42 @@ private: bool pendingRemove = false; }; + class DummyPageProvider : public Ogre::PageProvider { + public: + bool prepareProceduralPage(Ogre::Page *, + Ogre::PagedWorldSection *) override + { + return true; + } + bool loadProceduralPage(Ogre::Page *, + Ogre::PagedWorldSection *) override + { + return true; + } + bool unloadProceduralPage(Ogre::Page *page, + Ogre::PagedWorldSection *) override; + bool unprepareProceduralPage(Ogre::Page *, + Ogre::PagedWorldSection *) override + { + return true; + } + TerrainSystem *owner = nullptr; + }; + + class CustomTerrainDefiner : + public Ogre::TerrainPagedWorldSection::TerrainDefiner { + public: + CustomTerrainDefiner(TerrainSystem *sys) + : Ogre::TerrainPagedWorldSection::TerrainDefiner() + , m_sys(sys) + { + } + void define(Ogre::TerrainGroup *terrainGroup, long x, + long y) override; + private: + TerrainSystem *m_sys; + }; + class TerrainBodyDrawFilter : public JPH::BodyDrawFilter { public: bool ShouldDraw(const JPH::Body &inBody) const override; @@ -104,43 +150,13 @@ private: std::set m_terrainIds; }; - class DummyPageProvider : public Ogre::PageProvider { - public: - bool prepareProceduralPage(Ogre::Page *, Ogre::PagedWorldSection *) override - { - return true; - } - bool loadProceduralPage(Ogre::Page *, Ogre::PagedWorldSection *) override - { - return true; - } - bool unloadProceduralPage(Ogre::Page *page, Ogre::PagedWorldSection *) override; - bool unprepareProceduralPage(Ogre::Page *, Ogre::PagedWorldSection *) override - { - return true; - } - TerrainSystem *owner = nullptr; - }; - - class CustomTerrainDefiner : - public Ogre::TerrainPagedWorldSection::TerrainDefiner { - public: - CustomTerrainDefiner(TerrainSystem *sys) - : Ogre::TerrainPagedWorldSection::TerrainDefiner() - , m_sys(sys) - { - } - void define(Ogre::TerrainGroup *terrainGroup, long x, long y) override; - private: - TerrainSystem *m_sys; - }; - JPH::ShapeRefC buildPageCollider(Ogre::Terrain *terrain); void queueColliderCreate(long x, long y); void queueColliderRemove(long x, long y); void processColliderCreates(); void processColliderRemoves(); void removeAllColliders(); + void destroyCollider(uint64_t key, TerrainCollider &collider); flecs::world &m_world; Ogre::SceneManager *m_sceneMgr; @@ -162,15 +178,38 @@ private: std::unordered_map mColliders; std::deque> mColliderCreateQueue; + std::set mPendingColliderPages; + std::mutex m_colliderQueueMutex; TerrainBodyDrawFilter mBodyDrawFilter; - /* Heightmap data (M3) */ + /* Paging range used for synchronous page load and collider polling. */ + long m_pageMinX = -1, m_pageMinY = -1; + long m_pageMaxX = 1, m_pageMaxY = 1; + + /* Heightmap data (M3) — protected by m_heightmapMutex because + * collider building and page updates may happen concurrently. */ + mutable std::mutex m_heightmapMutex; std::vector m_heightData; bool m_heightmapLoaded = false; int m_heightmapRes = 0; + float m_heightmapWorldMinX = 0; + float m_heightmapWorldMinZ = 0; + float m_heightmapWorldSize = 2000.0f; + std::set m_dirtyPages; + std::vector m_reloadQueue; bool hasHeightmap() const { return m_heightmapLoaded; } + /* Sculpting state */ + bool m_sculptMode = false; + SculptTool m_sculptTool = SculptTool::Raise; + float m_sculptRadius = 5.0f; + float m_sculptStrength = 0.5f; + int m_sculptLayerIndex = 0; + + /* Entity tracking */ + flecs::entity_t m_terrainEntityId = 0; + flecs::query m_terrainQuery; }; diff --git a/src/features/editScene/ui/TerrainEditor.hpp b/src/features/editScene/ui/TerrainEditor.hpp index 36ff445..18daead 100644 --- a/src/features/editScene/ui/TerrainEditor.hpp +++ b/src/features/editScene/ui/TerrainEditor.hpp @@ -83,13 +83,12 @@ public: if (ImGui::Button("Snap Camera Above Terrain")) ts->snapCameraAboveTerrain(); ImGui::SameLine(); + const std::string hmPath = ts->getHeightmapPath(tc); if (ImGui::Button("Save Heightmap")) - ts->saveHeightmap("heightmaps/ht_" + - std::to_string(tc.terrainId) + ".bin"); + ts->saveHeightmap(hmPath); ImGui::SameLine(); if (ImGui::Button("Load Heightmap")) - ts->loadHeightmap("heightmaps/ht_" + - std::to_string(tc.terrainId) + ".bin"); + ts->loadHeightmap(hmPath); } ImGui::Separator();