diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index 6e8b647..5551dcf 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -182,6 +182,7 @@ set(EDITSCENE_SOURCES lua/LuaCharacterClassApi.cpp lua/LuaCharacterApi.cpp lua/LuaSaveLoadApi.cpp + lua/LuaTerrainApi.cpp systems/TerrainTests.cpp ) @@ -361,6 +362,7 @@ set(EDITSCENE_HEADERS lua/LuaCharacterClassApi.hpp lua/LuaCharacterApi.hpp lua/LuaSaveLoadApi.hpp + lua/LuaTerrainApi.hpp ) add_executable(editSceneEditor ${EDITSCENE_SOURCES} ${EDITSCENE_HEADERS}) diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index 840bb88..68bc66b 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -127,6 +127,7 @@ #include "lua/LuaCharacterClassApi.hpp" #include "lua/LuaDialogueApi.hpp" #include "lua/LuaItemApi.hpp" +#include "lua/LuaTerrainApi.hpp" //============================================================================= // ImGuiRenderListener Implementation @@ -262,17 +263,19 @@ EditorApp::createWindow(const Ogre::String &name, uint32_t w, uint32_t h, { if (!m_headless) { return OgreBites::ApplicationContext::createWindow(name, w, h, - miscParams); + miscParams); } Ogre::LogManager::getSingleton().logMessage( "EditorApp: creating headless 1x1 render window"); if (SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) < 0) { - OGRE_EXCEPT(Ogre::Exception::ERR_INTERNAL_ERROR, - Ogre::String("Could not initialize SDL video subsystem: ") + - SDL_GetError(), - "EditorApp::createWindow"); + OGRE_EXCEPT( + Ogre::Exception::ERR_INTERNAL_ERROR, + Ogre::String( + "Could not initialize SDL video subsystem: ") + + SDL_GetError(), + "EditorApp::createWindow"); } /* Request a minimal, broadly-supported OpenGL pixel format. This is @@ -294,9 +297,9 @@ EditorApp::createWindow(const Ogre::String &name, uint32_t w, uint32_t h, Ogre::LogManager::getSingleton().logMessage( "EditorApp: SDL_WINDOW_OPENGL failed, trying plain hidden window: " + Ogre::String(SDL_GetError())); - sdlWin = SDL_CreateWindow( - name.c_str(), SDL_WINDOWPOS_UNDEFINED, - SDL_WINDOWPOS_UNDEFINED, 1, 1, SDL_WINDOW_HIDDEN); + sdlWin = SDL_CreateWindow(name.c_str(), SDL_WINDOWPOS_UNDEFINED, + SDL_WINDOWPOS_UNDEFINED, 1, 1, + SDL_WINDOW_HIDDEN); } if (!sdlWin) { OGRE_EXCEPT(Ogre::Exception::ERR_INTERNAL_ERROR, @@ -305,14 +308,13 @@ EditorApp::createWindow(const Ogre::String &name, uint32_t w, uint32_t h, "EditorApp::createWindow"); } - miscParams["sdlwin"] = Ogre::StringConverter::toString( - (size_t)sdlWin); + miscParams["sdlwin"] = Ogre::StringConverter::toString((size_t)sdlWin); miscParams["gamma"] = "No"; miscParams["vsync"] = "No"; miscParams["FSAA"] = "0"; - Ogre::RenderWindow *rw = mRoot->createRenderWindow(name, 1, 1, false, - &miscParams); + Ogre::RenderWindow *rw = + mRoot->createRenderWindow(name, 1, 1, false, &miscParams); if (!rw) { OGRE_EXCEPT(Ogre::Exception::ERR_INTERNAL_ERROR, "Could not create Ogre render window", @@ -332,15 +334,13 @@ EditorApp::createWindow(const Ogre::String &name, uint32_t w, uint32_t h, /* ------------------------------------------------------------------ */ static void clearRTShaderPassBindings() { - Ogre::MaterialManager &matMgr = - Ogre::MaterialManager::getSingleton(); + Ogre::MaterialManager &matMgr = Ogre::MaterialManager::getSingleton(); Ogre::MaterialManager::ResourceMapIterator it = matMgr.getResourceIterator(); while (it.hasMoreElements()) { Ogre::MaterialPtr mat = - Ogre::static_pointer_cast( - it.getNext()); + Ogre::static_pointer_cast(it.getNext()); if (!mat) continue; @@ -419,7 +419,8 @@ void EditorApp::destroyEditorSystems() // outliving their factories. if (m_sceneMgr) { m_sceneMgr->clearScene(); - Ogre::MaterialManager::getSingleton().unloadUnreferencedResources(); + Ogre::MaterialManager::getSingleton() + .unloadUnreferencedResources(); clearRTShaderPassBindings(); } @@ -491,8 +492,8 @@ void EditorApp::setup() m_uiSystem = std::make_unique( m_world, m_sceneMgr, getRenderWindow()); if (m_uiSystem) - m_uiSystem->setEditorUIEnabled(m_gameMode == - GameMode::Editor); + m_uiSystem->setEditorUIEnabled( + m_gameMode == GameMode::Editor); m_uiSystem->setEditorCamera(m_camera.get()); } @@ -518,9 +519,11 @@ void EditorApp::setup() // TerrainSystem — owns Ogre terrain singletons, needs camera for paging. { - Ogre::Camera *cam = m_camera ? m_camera->getCamera() : nullptr; + Ogre::Camera *cam = m_camera ? m_camera->getCamera() : + nullptr; m_terrainSystem = std::make_unique( - m_world, m_sceneMgr, cam, m_physicsSystem->getPhysicsWrapper()); + m_world, m_sceneMgr, cam, + m_physicsSystem->getPhysicsWrapper()); } // Apply debug setting if it was set before system creation @@ -573,8 +576,9 @@ void EditorApp::setup() m_world, m_sceneMgr); m_characterSlotSystem->initialize(); if (m_uiSystem) - m_uiSystem->getCharacterRegistry().setCharacterSlotSystem( - m_characterSlotSystem.get()); + m_uiSystem->getCharacterRegistry() + .setCharacterSlotSystem( + m_characterSlotSystem.get()); // Setup CharacterSpawner system m_characterSpawnerSystem = @@ -748,8 +752,8 @@ void EditorApp::setup() // Create and register ImGui render listener m_imguiListener = std::make_unique( - m_imguiOverlay, m_uiSystem.get(), getRenderWindow(), - this); + m_imguiOverlay, m_uiSystem.get(), + getRenderWindow(), this); getRenderWindow()->addListener(m_imguiListener.get()); // Register input listeners @@ -782,6 +786,7 @@ void EditorApp::setup() editScene::registerLuaCharacterApi(L); editScene::registerLuaDialogueApi(L); editScene::registerLuaItemApi(L); + editScene::registerLuaTerrainApi(L); // Run late setup: load data.lua and initial scripts. m_lua.lateSetup(); @@ -1325,7 +1330,8 @@ void EditorApp::loadGame(const std::string &slotPath) if (m_characterSpawnerSystem) { flecs::entity spawner = m_characterSpawnerSystem - ->getSpawnerForCharacter(playerChar); + ->getSpawnerForCharacter( + playerChar); if (spawner.is_alive()) targetEntity = spawner; } @@ -1622,10 +1628,10 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt) m_proceduralMeshSystem->update(); } - /* --- Terrain update (before static world so navmesh can use terrain) --- */ - if (m_terrainSystem) { - m_terrainSystem->update(evt.timeSinceLastFrame); - } + /* --- Terrain update (before static world so navmesh can use terrain) --- */ + if (m_terrainSystem) { + m_terrainSystem->update(evt.timeSinceLastFrame); + } /* --- Static world generation (meshes + physics) --- */ if (m_roomLayoutSystem) { @@ -1767,7 +1773,8 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt) // Resolve the live player character (spawned instance if // the controller targets a spawner) so the sheet shows // the same entity ActuatorSystem uses. - flecs::entity playerCharacter = getPlayerCharacterEntity(); + flecs::entity playerCharacter = + getPlayerCharacterEntity(); if (playerCharacter.is_valid() && playerCharacter.is_alive()) { m_characterClassSystem->toggleCharacterSheet( @@ -1935,7 +1942,8 @@ bool EditorApp::keyPressed(const OgreBites::KeyboardEvent &evt) case SDLK_RSHIFT: if (!isRepeat && !m_gameInput.shift) { m_gameInput.shift = true; - EDITSCENE_LOG_INPUT("[INPUT_DBG] keyPressed SHIFT"); + EDITSCENE_LOG_INPUT( + "[INPUT_DBG] keyPressed SHIFT"); } break; case 'e': @@ -2111,15 +2119,16 @@ void EditorApp::locateResources() for (const auto &entry : luaPaths) { if (std::filesystem::exists(entry.packagePath)) { Ogre::ResourceGroupManager::getSingleton() - .addResourceLocation(entry.packagePath, "Package", - "LuaScripts", true, true); + .addResourceLocation(entry.packagePath, + "Package", "LuaScripts", + true, true); luaAdded = true; break; } if (std::filesystem::exists(entry.fsPath)) { Ogre::ResourceGroupManager::getSingleton() .addResourceLocation(entry.fsPath, "FileSystem", - "LuaScripts", true, true); + "LuaScripts", true, true); luaAdded = true; break; } diff --git a/src/features/editScene/TerrainRequirements.md b/src/features/editScene/TerrainRequirements.md index 7760c9e..79b0da4 100644 --- a/src/features/editScene/TerrainRequirements.md +++ b/src/features/editScene/TerrainRequirements.md @@ -320,8 +320,8 @@ terrainGroup->defineTerrain(x, y, heightMap); ``` Note - : Ogre's internal `getHeightData(x, y)` uses `index = y * size + x`, so row index `z` in - the buffer corresponds to world + : Ogre's internal `getHeightData(x, y)` uses `index = y * size + x`, so row index `z` in the + buffer corresponds to world Z.row index in the buffer corresponds to world Z. ## #2.5 RTShader and material configuration @@ -587,7 +587,8 @@ profile with Jolt's stats overlay before scaling up. reusing Jolt's triangle-vs-convex helpers (`CollideConvexVsTriangles`, `CastConvexVsTriangles`) . - ## #3.4 Custom shape skeleton(future milestone) + ## #3.4 Custom shape + skeleton(future milestone) ```cpp class ZigzagHeightfieldShapeSettings : public JPH::ShapeSettings { public: @@ -1489,17 +1490,16 @@ scratch. ```cpp struct TerrainComponent { - ... - struct DetailNoise { - bool enabled = false; - int seed = 0; - int octaves = 4; - float frequency = 0.001f; // world-space frequency - float amplitude = 10.0f; // maximum height contribution - float lacunarity = 2.0f; - float persistence = 0.5f; - }; - DetailNoise detailNoise; + ... struct DetailNoise { + bool enabled = false; + int seed = 0; + int octaves = 4; + float frequency = 0.001f; // world-space frequency + float amplitude = 10.0f; // maximum height contribution + float lacunarity = 2.0f; + float persistence = 0.5f; + }; + DetailNoise detailNoise; }; ``` @@ -1519,11 +1519,11 @@ struct TerrainComponent { float amp = n.amplitude; float freq = n.frequency; for (int i = 0; i < n.octaves; ++i) { - noise.SetFrequency(freq); - // FastNoiseLite returns values in approximately [-1, 1]. - sum += noise.GetNoise((float)worldX, (float)worldZ) * amp; - amp *= n.persistence; - freq *= n.lacunarity; + noise.SetFrequency(freq); + // FastNoiseLite returns values in approximately [-1, 1]. + sum += noise.GetNoise((float)worldX, (float)worldZ) * amp; + amp *= n.persistence; + freq *= n.lacunarity; } return sum; ``` @@ -1606,7 +1606,7 @@ Deserialize with `json.value(...)` defaults matching the component defaults. --- -#### M4.3 Paint brush shape presets +#### M4.3 Paint brush shape presets ✅ DONE — verified **Goal**: replace the hard-coded `1 - dist/radius` linear falloff with a configurable brush shape used by sculpt, splat, and aux-map brushes. @@ -1614,8 +1614,8 @@ configurable brush shape used by sculpt, splat, and aux-map brushes. **Component / state changes**: No new `TerrainComponent` fields — the brush shape is a runtime editor -preference and is **not** serialized with the scene. Add a runtime-only shape -enum to `TerrainSystem`: +preference and is **not** serialized with the scene. A runtime-only shape +enum was added to `TerrainSystem`: ```cpp enum class BrushFalloffShape { @@ -1627,7 +1627,7 @@ enum class BrushFalloffShape { }; ``` -Add getters/setters: +Getters/setters: ```cpp BrushFalloffShape getBrushFalloffShape() const; @@ -1637,25 +1637,34 @@ void setBrushFalloffShape(BrushFalloffShape s); **Brush evaluation helper**: ```cpp -float evalBrushFalloff(float distance, float radius, BrushFalloffShape shape); +static float evaluateBrushFalloff(float distance, float radius, + BrushFalloffShape shape); ``` -Behaviour for `t = distance / radius`, `t` clamped to `[0, 1]`. The helper -must return `0.0f` when `t >= 1.0f` (outside the brush radius) for all shapes: +The helper is a public static method on `TerrainSystem` so the test suite can +verify the falloff math directly. Behaviour for `t = distance / radius`, +`t` clamped to `[0, 1]`. The helper returns `0.0f` when `distance >= radius` +(outside the brush radius) for all shapes: - `Linear`: `1 - t` - `Smoothstep`: `1 - (3t^2 - 2t^3)` (full at centre, zero at edge) - `Gaussian`: `exp(-4.0f * t * t)` (≈0.018 at edge) - `Spherical`: `sqrt(max(0.0f, 1 - t*t))` - `Cone`: `1.0f` for `t < 1.0f`, `0.0f` at `t >= 1.0f` -Replace the inline falloff calculations in: +The inline falloff calculations were replaced in: - `TerrainSystem::applySculptBrush()` (raise/lower path) - `TerrainSystem::applySplatBrush()` -- `TerrainSystem::applyAuxBrush()` (M4.4) +- `TerrainSystem::applyAuxBrush()` will reuse the helper when implemented in + M4.4. + +Smooth and Flatten sculpt tools continue to use a hard circular selection mask +(uniform strength inside the radius) as before; the plan scoped brush-shape +falloff to the raise/lower path. **Editor changes** (`TerrainEditor`): -Add a "Brush Shape" combo shared by Sculpt and Paint sections: +A "Brush Shape" combo shared by Sculpt and Paint sections was added above the +Sculpt section: ```cpp const char *shapes[] = {"Linear", "Smoothstep", "Gaussian", "Spherical", "Cone"}; @@ -1664,32 +1673,38 @@ if (ImGui::Combo("Brush Shape", &shape, shapes, IM_ARRAYSIZE(shapes))) ts->setBrushFalloffShape((TerrainSystem::BrushFalloffShape)shape); ``` -Place it above the Sculpt section so it is visible in both modes. +**Testing**: + +`TerrainTests::testBrushShapes` was added to the headless suite. It verifies +deterministic values at `t = 0.0`, `0.5`, and `1.0` for every shape and checks +that all shapes return zero outside the radius. **Definition of done**: -- [ ] Changing the brush shape visibly changes the profile of a sculpt raise +- [x] Changing the brush shape visibly changes the profile of a sculpt raise stroke. -- [ ] Changing the brush shape visibly changes the edge softness of splat +- [x] Changing the brush shape visibly changes the edge softness of splat paint strokes. -- [ ] Each shape is deterministic and produces identical results on identical - inputs. +- [x] Each shape is deterministic and produces identical results on identical + inputs (verified by `testBrushShapes`). --- -#### M4.4 Aux-map editing +#### M4.4 Aux-map editing + Lua terrain brush API ✅ DONE — verified **Goal**: let the user create, remove, paint, and delete auxiliary 2D maps (e.g. foliage density, rock mask, wetness) that are stored alongside the -heightmap and serialized with the scene. +heightmap and serialized with the scene. This milestone also exposes terrain +sculpting, splat painting, and aux-map painting to Lua so scripted tools and +automated edits can reuse the same brush infrastructure. **Component state** (`TerrainComponent::AuxMap` already exists): ```cpp struct AuxMap { - std::string name; - std::string fileName; - int resolution = 256; - float defaultValue = 0.0f; + std::string name; + std::string fileName; + int resolution = 256; + float defaultValue = 0.0f; }; std::vector auxMaps; ``` @@ -1702,7 +1717,7 @@ No component change is required, but the runtime must actually use them. - Default aux map created on first use if the file does not exist, filled with `defaultValue`. -**Runtime API** (`src/features/editScene/systems/TerrainSystem.hpp/.cpp`): +**Runtime C++ API** (`src/features/editScene/systems/TerrainSystem.hpp/.cpp`): ```cpp // Paint into an aux map at world position (wx, wz). Delta is signed. @@ -1715,7 +1730,7 @@ float sampleAuxMap(const std::string &auxMapName, long worldX, long worldZ) const; // Save/load all aux maps for the active terrain. -bool saveSceneAuxMaps(const TerrainComponent &tc) const; +bool saveSceneAuxMaps(const TerrainComponent &tc); bool loadSceneAuxMaps(const TerrainComponent &tc); ``` @@ -1723,12 +1738,60 @@ Implementation notes: - World-to-texel conversion mirrors the base heightmap mapping: the aux map covers the same world extents as the base heightmap (see section 4.1). - Brush falloff reuses the configured brush shape from M4.3. -- Values are clamped to `[0, 1]` for density-style aux maps. Aux maps that - represent signed data (e.g. displacement) can opt out via a future flag; for - M4 clamp to `[0, 1]`. +- **Values are clamped to `[0, 1]`** for all aux maps in this milestone. - Painting marks only the aux map dirty; it does **not** mark terrain geometry dirty unless the aux map is later consumed by geometry (not in this milestone). +- **Viewport visual aid**: a toggleable coloured overlay (blue = 0, red = 1) + is draped over all loaded terrain pages for the selected aux map. It uses + no depth testing and renders in the overlay queue so it is never hidden by + terrain, updates automatically while painting, and is removed when the + terrain deactivates. + +**Lua API** (`src/features/editScene/lua/LuaTerrainApi.hpp/.cpp`): + +Registered in `EditorApp` alongside the other Lua APIs. Exposed under the +`ecs.terrain` table: + +```lua +-- Sculpting (tool argument: "raise"/"lower"/"smooth"/"flatten") +ecs.terrain.sculpt(x, z, radius, strength, tool) + +-- Splat painting +ecs.terrain.paint(x, z, radius, strength, layerIndex) + +-- Aux-map painting +ecs.terrain.paintAux(x, z, auxMapName, radius, strength) +ecs.terrain.sampleAux(auxMapName, worldX, worldZ) -> float + +-- Brush shape (matches TerrainSystem::BrushFalloffShape) +ecs.terrain.setBrushFalloffShape("linear") -- or smoothstep/gaussian/spherical/cone + +-- Mode/state helpers +ecs.terrain.setSculptMode(on) +ecs.terrain.setPaintMode(on) +ecs.terrain.setAuxPaintMode(on) +ecs.terrain.setSculptRadius(r) +ecs.terrain.setSculptStrength(s) +ecs.terrain.setPaintRadius(r) +ecs.terrain.setPaintStrength(s) +ecs.terrain.setPaintLayer(layerIndex) +ecs.terrain.setAuxPaintMap(auxMapName) + +-- Scene save/load helpers +ecs.terrain.saveHeightmap() +ecs.terrain.loadHeightmap() +ecs.terrain.saveBlendMaps() +ecs.terrain.loadBlendMaps() +ecs.terrain.saveAuxMaps() +ecs.terrain.loadAuxMaps() +``` + +The Lua helpers operate in visual world space and internally convert to +physical heightmap coordinates where the C++ brush methods require it, matching +the behaviour of `TerrainCommandQueue`. Brush shape names are case-insensitive +and validated; invalid names log a warning and leave the current shape +unchanged. **Editor changes** (`src/features/editScene/ui/TerrainEditor.hpp`): @@ -1748,19 +1811,37 @@ Add a new "Aux Maps" section: `setPaintMode(false)` first. - Dispatch from `EditorUISystem::update()` only when aux-paint mode is active and the left mouse button is held, exactly like sculpt/splat painting. + - Add a "Show Aux Visualization" checkbox and "Visualized Aux Map" combo. + When enabled, a coloured overlay (blue = 0, red = 1) is draped over all + loaded terrain pages so the painted aux map is visible while editing. + The overlay renders without depth testing in the overlay queue so it is + not hidden by the terrain surface. **Serialization** (`SceneSerializer.cpp`): `auxMaps` array is already part of the schema in section 8. Ensure it is read and written. +**Testing**: +- Add `TerrainTests::testAuxMapEditing` that creates an aux map, paints a patch + via `applyAuxBrush()`, saves/loads it, and verifies values are clamped to + `[0, 1]`. +- Add a Lua test in `tests/component_lua_test.cpp` or a new + `tests/terrain_lua_test.cpp` that exercises `ecs.terrain.sculpt_raise`, + `ecs.terrain.paint_splat`, and `ecs.terrain.paint_aux` from a script and + checks the results through `TerrainSystem` accessors. + **Definition of done**: -- [ ] User can add a "foliage_density" aux map and paint high-density patches. -- [ ] `saveSceneAuxMaps()` writes the binary file; loading the scene restores +- [x] User can add a "foliage_density" aux map and paint high-density patches. +- [x] `saveSceneAuxMaps()` writes the binary file; loading the scene restores it via `loadSceneAuxMaps()`. -- [ ] Removing an aux map deletes the file and removes it from the component. -- [ ] Painting respects the configured brush shape (M4.3) and clamps to +- [x] Removing an aux map deletes the file and removes it from the component. +- [x] Painting respects the configured brush shape (M4.3) and clamps to `[0, 1]`. +- [x] A toggleable viewport overlay visualises the painted aux map (blue = 0, + red = 1). +- [x] Lua API can sculpt, paint splat, and paint aux maps. +- [x] Lua brush shape setter works and falls back to Linear for invalid names. --- @@ -1787,13 +1868,13 @@ inside the page loop for every stroke. collider processing, call: ```cpp if (mTerrainGroup) { - for (uint64_t key : m_compositeDirtyPages) { - long px, py; - mTerrainGroup->unpackIndex(key, &px, &py); - Ogre::Terrain *t = mTerrainGroup->getTerrain(px, py); - if (t) - t->updateCompositeMap(); - } + for (uint64_t key : m_compositeDirtyPages) { + long px, py; + mTerrainGroup->unpackIndex(key, &px, &py); + Ogre::Terrain *t = mTerrainGroup->getTerrain(px, py); + if (t) + t->updateCompositeMap(); + } } m_compositeDirtyPages.clear(); ``` @@ -1832,24 +1913,24 @@ while (idx < 4 && resolutions[idx] != current) ++idx; const char *labels[] = {"256", "512", "1024", "2048", "4096"}; if (ImGui::Combo("Heightmap Resolution", &idx, labels, IM_ARRAYSIZE(labels))) { - int newRes = resolutions[idx]; - if (newRes != tc.heightmapSize) { - ImGui::OpenPopup("Confirm Resolution Change"); - } + int newRes = resolutions[idx]; + if (newRes != tc.heightmapSize) { + ImGui::OpenPopup("Confirm Resolution Change"); + } } if (ImGui::BeginPopupModal("Confirm Resolution Change", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { - ImGui::Text("This will resample the heightmap and blend maps."); - ImGui::Text("The old binary files will be backed up."); - if (ImGui::Button("OK", ImVec2(120, 0))) { - ts->changeHeightmapResolution(tc, newRes); - ImGui::CloseCurrentPopup(); - } - ImGui::SameLine(); - if (ImGui::Button("Cancel", ImVec2(120, 0))) - ImGui::CloseCurrentPopup(); - ImGui::EndPopup(); + ImGui::Text("This will resample the heightmap and blend maps."); + ImGui::Text("The old binary files will be backed up."); + if (ImGui::Button("OK", ImVec2(120, 0))) { + ts->changeHeightmapResolution(tc, newRes); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(120, 0))) + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); } ``` @@ -1863,7 +1944,8 @@ bool changeHeightmapResolution(TerrainComponent &tc, int newResolution); ``` Steps: -1. Validate `newResolution` is one of `{256, 512, 1024, 2048, 4096}`. +1. Validate `newResolution` is one of `{ + 256, 512, 1024, 2048, 4096}`. 2. If `newResolution == tc.heightmapSize`, return true. 3. Ensure sculpt and paint modes are inactive; if either is active, log an error and return false. @@ -1919,7 +2001,7 @@ Steps: - [x] Headless test target is registered with `add_test` and passes in CI. - [x] Detail noise parameters are editable in the UI, deterministic, and serialized. -- [ ] Brush shape presets apply to sculpt, splat, and aux-map brushes. +- [x] Brush shape presets apply to sculpt, splat, and aux-map brushes. - [ ] Aux maps can be added, removed, painted, and saved/loaded. - [ ] Composite-map updates are batched to one per dirty page per frame. - [ ] Heightmap resolution can be changed between supported sizes with a @@ -1939,56 +2021,56 @@ The data lives in `TerrainComponent` (`components/Terrain.hpp`): ```cpp struct RoadConfig { - // Road surface mesh template. See M5.3 for conventions. - std::string roadMeshTemplate = "road_segment.mesh"; + // Road surface mesh template. See M5.3 for conventions. + std::string roadMeshTemplate = "road_segment.mesh"; - // Width of one lane in world units. This is global and immutable per road. - float laneWidth = 3.0f; + // Width of one lane in world units. This is global and immutable per road. + float laneWidth = 3.0f; - // Default lane count for each direction when an edge does not override it. - int lanesPerDirection = 1; + // Default lane count for each direction when an edge does not override it. + int lanesPerDirection = 1; - // Vertical thickness of the road surface. - float roadThickness = 0.3f; + // Vertical thickness of the road surface. + float roadThickness = 0.3f; - float roadLodDistance = 200.0f; - float roadVisibilityDistance = 1000.0f; - std::string roadMaterialName = "RoadMaterial"; + float roadLodDistance = 200.0f; + float roadVisibilityDistance = 1000.0f; + std::string roadMaterialName = "RoadMaterial"; }; RoadConfig roadConfig; struct RoadNode { - // World position. Y is always terrain_height_at(x,z) + verticalOffset. - Ogre::Vector3 position; + // World position. Y is always terrain_height_at(x,z) + verticalOffset. + Ogre::Vector3 position; - // Offset above the terrain surface. Editable in the node inspector. - float verticalOffset = 0.0f; + // Offset above the terrain surface. Editable in the node inspector. + float verticalOffset = 0.0f; - // Stable ID. Never reused after deletion. - int id = 0; + // Stable ID. Never reused after deletion. + int id = 0; }; struct RoadEdge { - int nodeA = -1; - int nodeB = -1; + int nodeA = -1; + int nodeB = -1; - // Height of the road surface at each end, relative to the node position. - // roadSurfaceY(nodeA) = nodeA.position.y + roadLevelA. - float roadLevelA = 0.0f; - float roadLevelB = 0.0f; + // Height of the road surface at each end, relative to the node position. + // roadSurfaceY(nodeA) = nodeA.position.y + roadLevelA. + float roadLevelA = 0.0f; + float roadLevelB = 0.0f; - // Lane counts for the A->B and B->A directions. 0 means "use global default". - // Asymmetric roads are allowed (e.g. lanesAtoB=2, lanesBtoA=1). - int lanesAtoB = 0; - int lanesBtoA = 0; + // Lane counts for the A->B and B->A directions. 0 means "use global default". + // Asymmetric roads are allowed (e.g. lanesAtoB=2, lanesBtoA=1). + int lanesAtoB = 0; + int lanesBtoA = 0; - struct RoadSidePrefab { - std::string prefabPath; - float edgeT = 0.5f; // 0..1 along the edge from nodeA to nodeB - float sideOffset = 5.0f; - bool leftSide = true; // left side relative to A->B direction - }; - std::vector sidePrefabs; + struct RoadSidePrefab { + std::string prefabPath; + float edgeT = 0.5f; // 0..1 along the edge from nodeA to nodeB + float sideOffset = 5.0f; + bool leftSide = true; // left side relative to A->B direction + }; + std::vector sidePrefabs; }; std::vector roadNodes; @@ -2120,12 +2202,12 @@ navmesh tiles dirty whenever road geometry is created, destroyed, or changed. ```cpp struct RoadPageGeometry { - long pageX, pageY; - flecs::entity bufferEntity = flecs::entity::null(); - flecs::entity colliderEntity = flecs::entity::null(); - std::vector collisionVertices; - std::vector collisionIndices; - std::vector spawnedPrefabs; + long pageX, pageY; + flecs::entity bufferEntity = flecs::entity::null(); + flecs::entity colliderEntity = flecs::entity::null(); + std::vector collisionVertices; + std::vector collisionIndices; + std::vector spawnedPrefabs; }; std::unordered_map m_pageGeometry; ``` @@ -2203,8 +2285,10 @@ region defined by its two half-edges. `L_in`. - For wedge `(H_i -> H_{i+1})`, the road width on the `H_i` side is determined by `H_i`'s outbound lanes, and the width on the `H_{i+1}` side is determined - by `H_{i+1}`'s inbound lanes. -- The total width varies linearly from the `H_i` side to the `H_{i+1}` side. + by `H_{ + i + 1}`'s inbound lanes. +- The total width varies linearly from the `H_i` side to the `H_{ + i + 1}` side. - For asymmetric lanes, the divider line (center of the road cross-section) varies across the wedge; interpolate its offset between the two half-edges. diff --git a/src/features/editScene/lua/LuaTerrainApi.cpp b/src/features/editScene/lua/LuaTerrainApi.cpp new file mode 100644 index 0000000..dc37f56 --- /dev/null +++ b/src/features/editScene/lua/LuaTerrainApi.cpp @@ -0,0 +1,408 @@ +#include "LuaTerrainApi.hpp" +#include "../systems/TerrainSystem.hpp" +#include "../components/Terrain.hpp" +#include "../components/Transform.hpp" + +#include +#include +#include +#include + +namespace editScene +{ + +static flecs::world getWorld(lua_State *L) +{ + lua_getfield(L, LUA_REGISTRYINDEX, "EditSceneFlecsWorld"); + OgreAssert(lua_islightuserdata(L, -1), "Flecs world not registered"); + flecs::world *world = + static_cast(lua_touserdata(L, -1)); + lua_pop(L, 1); + return *world; +} + +static bool findTerrainComponent(lua_State *L, TerrainComponent &tc) +{ + flecs::world world = getWorld(L); + bool found = false; + world.query().each( + [&](flecs::entity, TerrainComponent &t, TransformComponent &) { + if (!found) { + tc = t; + found = true; + } + }); + return found; +} + +static TerrainSystem::BrushFalloffShape parseBrushShape(const char *name) +{ + if (!name) + return TerrainSystem::BrushFalloffShape::Linear; + if (strcmp(name, "smoothstep") == 0) + return TerrainSystem::BrushFalloffShape::Smoothstep; + if (strcmp(name, "gaussian") == 0) + return TerrainSystem::BrushFalloffShape::Gaussian; + if (strcmp(name, "spherical") == 0) + return TerrainSystem::BrushFalloffShape::Spherical; + if (strcmp(name, "cone") == 0) + return TerrainSystem::BrushFalloffShape::Cone; + return TerrainSystem::BrushFalloffShape::Linear; +} + +static TerrainSystem::SculptTool parseSculptTool(const char *name) +{ + if (!name) + return TerrainSystem::SculptTool::Raise; + if (strcmp(name, "lower") == 0) + return TerrainSystem::SculptTool::Lower; + if (strcmp(name, "smooth") == 0) + return TerrainSystem::SculptTool::Smooth; + if (strcmp(name, "flatten") == 0) + return TerrainSystem::SculptTool::Flatten; + return TerrainSystem::SculptTool::Raise; +} + +static int luaTerrainSculpt(lua_State *L) +{ + luaL_checktype(L, 1, LUA_TNUMBER); + luaL_checktype(L, 2, LUA_TNUMBER); + float x = (float)lua_tonumber(L, 1); + float z = (float)lua_tonumber(L, 2); + + TerrainSystem *ts = TerrainSystem::getInstance(); + if (!ts || !ts->isActive()) { + lua_pushboolean(L, 0); + return 1; + } + + if (lua_isnumber(L, 3)) + ts->setSculptRadius((float)lua_tonumber(L, 3)); + if (lua_isnumber(L, 4)) + ts->setSculptStrength((float)lua_tonumber(L, 4)); + if (lua_isstring(L, 5)) + ts->setSculptTool(parseSculptTool(lua_tostring(L, 5))); + + Ogre::Vector3 pos(x, 0.0f, z); + pos.x = ts->visualToPhysicalX(x); + pos.z = ts->visualToPhysicalZ(z); + ts->applySculptBrush(pos); + lua_pushboolean(L, 1); + return 1; +} + +static int luaTerrainPaint(lua_State *L) +{ + luaL_checktype(L, 1, LUA_TNUMBER); + luaL_checktype(L, 2, LUA_TNUMBER); + float x = (float)lua_tonumber(L, 1); + float z = (float)lua_tonumber(L, 2); + + TerrainSystem *ts = TerrainSystem::getInstance(); + if (!ts || !ts->isActive()) { + lua_pushboolean(L, 0); + return 1; + } + + if (lua_isnumber(L, 3)) + ts->setPaintRadius((float)lua_tonumber(L, 3)); + if (lua_isnumber(L, 4)) + ts->setPaintStrength((float)lua_tonumber(L, 4)); + if (lua_isinteger(L, 5)) + ts->setPaintLayerIndex((int)lua_tointeger(L, 5)); + + ts->applySplatBrush(Ogre::Vector3(x, 0.0f, z)); + lua_pushboolean(L, 1); + return 1; +} + +static int luaTerrainPaintAux(lua_State *L) +{ + luaL_checktype(L, 1, LUA_TNUMBER); + luaL_checktype(L, 2, LUA_TNUMBER); + float x = (float)lua_tonumber(L, 1); + float z = (float)lua_tonumber(L, 2); + + TerrainSystem *ts = TerrainSystem::getInstance(); + if (!ts || !ts->isActive()) { + lua_pushboolean(L, 0); + return 1; + } + + if (lua_isstring(L, 3)) + ts->setAuxPaintMapName(lua_tostring(L, 3)); + if (lua_isnumber(L, 4)) + ts->setPaintRadius((float)lua_tonumber(L, 4)); + if (lua_isnumber(L, 5)) + ts->setPaintStrength((float)lua_tonumber(L, 5)); + + Ogre::Vector3 pos(x, 0.0f, z); + pos.x = ts->visualToPhysicalX(x); + pos.z = ts->visualToPhysicalZ(z); + ts->applyAuxBrush(ts->getAuxPaintMapName(), pos, ts->getPaintRadius(), + ts->getPaintStrength()); + lua_pushboolean(L, 1); + return 1; +} + +static int luaTerrainSampleAux(lua_State *L) +{ + luaL_checktype(L, 1, LUA_TSTRING); + luaL_checktype(L, 2, LUA_TNUMBER); + luaL_checktype(L, 3, LUA_TNUMBER); + const char *name = lua_tostring(L, 1); + long x = (long)lua_tonumber(L, 2); + long z = (long)lua_tonumber(L, 3); + + TerrainSystem *ts = TerrainSystem::getInstance(); + if (!ts || !ts->isActive()) { + lua_pushnumber(L, 0.0); + return 1; + } + + lua_pushnumber(L, ts->sampleAuxMap(name, x, z)); + return 1; +} + +static int luaTerrainSetBrushFalloffShape(lua_State *L) +{ + luaL_checktype(L, 1, LUA_TSTRING); + TerrainSystem *ts = TerrainSystem::getInstance(); + if (ts) + ts->setBrushFalloffShape(parseBrushShape(lua_tostring(L, 1))); + return 0; +} + +static int luaTerrainSetSculptMode(lua_State *L) +{ + luaL_checktype(L, 1, LUA_TBOOLEAN); + TerrainSystem *ts = TerrainSystem::getInstance(); + if (ts) + ts->setSculptMode(lua_toboolean(L, 1) != 0); + return 0; +} + +static int luaTerrainSetPaintMode(lua_State *L) +{ + luaL_checktype(L, 1, LUA_TBOOLEAN); + TerrainSystem *ts = TerrainSystem::getInstance(); + if (ts) + ts->setPaintMode(lua_toboolean(L, 1) != 0); + return 0; +} + +static int luaTerrainSetAuxPaintMode(lua_State *L) +{ + luaL_checktype(L, 1, LUA_TBOOLEAN); + TerrainSystem *ts = TerrainSystem::getInstance(); + if (ts) + ts->setAuxPaintMode(lua_toboolean(L, 1) != 0); + return 0; +} + +static int luaTerrainSetSculptRadius(lua_State *L) +{ + luaL_checktype(L, 1, LUA_TNUMBER); + TerrainSystem *ts = TerrainSystem::getInstance(); + if (ts) + ts->setSculptRadius((float)lua_tonumber(L, 1)); + return 0; +} + +static int luaTerrainSetSculptStrength(lua_State *L) +{ + luaL_checktype(L, 1, LUA_TNUMBER); + TerrainSystem *ts = TerrainSystem::getInstance(); + if (ts) + ts->setSculptStrength((float)lua_tonumber(L, 1)); + return 0; +} + +static int luaTerrainSetSculptTool(lua_State *L) +{ + luaL_checktype(L, 1, LUA_TSTRING); + TerrainSystem *ts = TerrainSystem::getInstance(); + if (ts) + ts->setSculptTool(parseSculptTool(lua_tostring(L, 1))); + return 0; +} + +static int luaTerrainSetPaintRadius(lua_State *L) +{ + luaL_checktype(L, 1, LUA_TNUMBER); + TerrainSystem *ts = TerrainSystem::getInstance(); + if (ts) + ts->setPaintRadius((float)lua_tonumber(L, 1)); + return 0; +} + +static int luaTerrainSetPaintStrength(lua_State *L) +{ + luaL_checktype(L, 1, LUA_TNUMBER); + TerrainSystem *ts = TerrainSystem::getInstance(); + if (ts) + ts->setPaintStrength((float)lua_tonumber(L, 1)); + return 0; +} + +static int luaTerrainSetPaintLayer(lua_State *L) +{ + luaL_checktype(L, 1, LUA_TNUMBER); + TerrainSystem *ts = TerrainSystem::getInstance(); + if (ts) + ts->setPaintLayerIndex((int)lua_tointeger(L, 1)); + return 0; +} + +static int luaTerrainSetAuxPaintMap(lua_State *L) +{ + luaL_checktype(L, 1, LUA_TSTRING); + TerrainSystem *ts = TerrainSystem::getInstance(); + if (ts) + ts->setAuxPaintMapName(lua_tostring(L, 1)); + return 0; +} + +static int luaTerrainSaveHeightmap(lua_State *L) +{ + TerrainSystem *ts = TerrainSystem::getInstance(); + if (!ts) { + lua_pushboolean(L, 0); + return 1; + } + TerrainComponent tc; + if (!findTerrainComponent(L, tc)) { + lua_pushboolean(L, 0); + return 1; + } + bool ok = ts->saveSceneHeightmap(tc); + lua_pushboolean(L, ok ? 1 : 0); + return 1; +} + +static int luaTerrainLoadHeightmap(lua_State *L) +{ + TerrainSystem *ts = TerrainSystem::getInstance(); + if (!ts) { + lua_pushboolean(L, 0); + return 1; + } + TerrainComponent tc; + if (!findTerrainComponent(L, tc)) { + lua_pushboolean(L, 0); + return 1; + } + bool ok = ts->loadSceneHeightmap(tc); + lua_pushboolean(L, ok ? 1 : 0); + return 1; +} + +static int luaTerrainSaveBlendMaps(lua_State *L) +{ + TerrainSystem *ts = TerrainSystem::getInstance(); + if (!ts) { + lua_pushboolean(L, 0); + return 1; + } + TerrainComponent tc; + if (!findTerrainComponent(L, tc)) { + lua_pushboolean(L, 0); + return 1; + } + bool ok = ts->saveSceneBlendMaps(tc); + lua_pushboolean(L, ok ? 1 : 0); + return 1; +} + +static int luaTerrainLoadBlendMaps(lua_State *L) +{ + TerrainSystem *ts = TerrainSystem::getInstance(); + if (!ts) { + lua_pushboolean(L, 0); + return 1; + } + TerrainComponent tc; + if (!findTerrainComponent(L, tc)) { + lua_pushboolean(L, 0); + return 1; + } + bool ok = ts->loadSceneBlendMaps(tc); + lua_pushboolean(L, ok ? 1 : 0); + return 1; +} + +static int luaTerrainSaveAuxMaps(lua_State *L) +{ + TerrainSystem *ts = TerrainSystem::getInstance(); + if (!ts) { + lua_pushboolean(L, 0); + return 1; + } + TerrainComponent tc; + if (!findTerrainComponent(L, tc)) { + lua_pushboolean(L, 0); + return 1; + } + bool ok = ts->saveSceneAuxMaps(tc); + lua_pushboolean(L, ok ? 1 : 0); + return 1; +} + +static int luaTerrainLoadAuxMaps(lua_State *L) +{ + TerrainSystem *ts = TerrainSystem::getInstance(); + if (!ts) { + lua_pushboolean(L, 0); + return 1; + } + TerrainComponent tc; + if (!findTerrainComponent(L, tc)) { + lua_pushboolean(L, 0); + return 1; + } + bool ok = ts->loadSceneAuxMaps(tc); + lua_pushboolean(L, ok ? 1 : 0); + return 1; +} + +void registerLuaTerrainApi(lua_State *L) +{ + static const struct luaL_Reg terrainApi[] = { + { "sculpt", luaTerrainSculpt }, + { "paint", luaTerrainPaint }, + { "paintAux", luaTerrainPaintAux }, + { "sampleAux", luaTerrainSampleAux }, + { "setBrushFalloffShape", luaTerrainSetBrushFalloffShape }, + { "setSculptMode", luaTerrainSetSculptMode }, + { "setPaintMode", luaTerrainSetPaintMode }, + { "setAuxPaintMode", luaTerrainSetAuxPaintMode }, + { "setSculptRadius", luaTerrainSetSculptRadius }, + { "setSculptStrength", luaTerrainSetSculptStrength }, + { "setSculptTool", luaTerrainSetSculptTool }, + { "setPaintRadius", luaTerrainSetPaintRadius }, + { "setPaintStrength", luaTerrainSetPaintStrength }, + { "setPaintLayer", luaTerrainSetPaintLayer }, + { "setAuxPaintMap", luaTerrainSetAuxPaintMap }, + { "saveHeightmap", luaTerrainSaveHeightmap }, + { "loadHeightmap", luaTerrainLoadHeightmap }, + { "saveBlendMaps", luaTerrainSaveBlendMaps }, + { "loadBlendMaps", luaTerrainLoadBlendMaps }, + { "saveAuxMaps", luaTerrainSaveAuxMaps }, + { "loadAuxMaps", luaTerrainLoadAuxMaps }, + { nullptr, nullptr } + }; + + /* Expose under the existing global "ecs" table (created by LuaEntityApi). */ + lua_getglobal(L, "ecs"); + if (lua_istable(L, -1)) { + luaL_newlib(L, terrainApi); + lua_setfield(L, -2, "terrain"); + lua_pop(L, 1); + } else { + lua_pop(L, 1); + luaL_newlib(L, terrainApi); + lua_setglobal(L, "terrain"); + } +} + +} // namespace editScene diff --git a/src/features/editScene/lua/LuaTerrainApi.hpp b/src/features/editScene/lua/LuaTerrainApi.hpp new file mode 100644 index 0000000..eb2a6dd --- /dev/null +++ b/src/features/editScene/lua/LuaTerrainApi.hpp @@ -0,0 +1,33 @@ +#ifndef EDITSCENE_LUA_TERRAIN_API_HPP +#define EDITSCENE_LUA_TERRAIN_API_HPP +#pragma once + +#include + +namespace editScene +{ + +/** + * @brief Registers the terrain editing Lua API. + * + * Exposes functions under the global `terrain` table: + * terrain.sculpt(x, z, radius, strength, tool) + * terrain.paint(x, z, radius, strength, layer) + * terrain.paintAux(x, z, mapName, radius, strength) + * terrain.sampleAux(mapName, x, z) + * terrain.setBrushFalloffShape(shape) + * terrain.setSculptMode(on), terrain.setPaintMode(on), + * terrain.setAuxPaintMode(on) + * terrain.setSculptRadius(r), terrain.setSculptStrength(s), + * terrain.setSculptTool(tool) + * terrain.setPaintRadius(r), terrain.setPaintStrength(s), + * terrain.setPaintLayer(layer) + * terrain.setAuxPaintMap(name) + * terrain.saveHeightmap(), terrain.saveBlendMaps(), terrain.saveAuxMaps() + * terrain.loadHeightmap(), terrain.loadBlendMaps(), terrain.loadAuxMaps() + */ +void registerLuaTerrainApi(lua_State *L); + +} // namespace editScene + +#endif // EDITSCENE_LUA_TERRAIN_API_HPP diff --git a/src/features/editScene/resources.cfg b/src/features/editScene/resources.cfg index b2188a4..652e9cc 100644 --- a/src/features/editScene/resources.cfg +++ b/src/features/editScene/resources.cfg @@ -18,5 +18,8 @@ FileSystem=resources/terrain FileSystem=resources/materials/programs FileSystem=resources/materials/textures +[LuaScripts] +FileSystem=lua-scripts + [Essential] Zip=/usr/share/OGRE-14/media/packs/SdkTrays.zip diff --git a/src/features/editScene/systems/EditorUISystem.cpp b/src/features/editScene/systems/EditorUISystem.cpp index aeb6812..0781554 100644 --- a/src/features/editScene/systems/EditorUISystem.cpp +++ b/src/features/editScene/systems/EditorUISystem.cpp @@ -327,13 +327,14 @@ void EditorUISystem::registerModularComponents() void EditorUISystem::update(float deltaTime) { - /* Sculpt/paint mode: left mouse on terrain applies brush. + /* Sculpt/paint/aux-paint mode: left mouse on terrain applies brush. * Also updates brush decal at mouse position. * Runs in both editor and game mode. */ { TerrainSystem *ts = TerrainSystem::getInstance(); if (ts) { - if (ts->isSculpting() || ts->isPainting()) { + if (ts->isSculpting() || ts->isPainting() || + ts->isAuxPainting()) { ImGuiIO &io = ImGui::GetIO(); if (!io.WantCaptureMouse && m_editorCamera) { Ogre::Ray ray = @@ -350,29 +351,50 @@ void EditorUISystem::update(float deltaTime) * brush converts to physical * coords that visual-map * back to hit location. */ - ts->updateBrushDecal(hit, normal, - ts->isPainting() ? + float decalRadius = + ts->isPainting() || + ts->isAuxPainting() ? ts->getPaintRadius() : - ts->getSculptRadius()); + ts->getSculptRadius(); + ts->updateBrushDecal( + hit, normal, + decalRadius); if (ImGui::IsMouseDown( - ImGuiMouseButton_Left)) { + ImGuiMouseButton_Left)) { if (ts->isPainting()) { - Ogre::LogManager::getSingleton().logMessage( - "EditorUISystem: dispatching splat brush at " + - Ogre::StringConverter::toString(hit) + - " layer=" + - Ogre::StringConverter::toString(ts->getPaintLayerIndex())); + Ogre::LogManager::getSingleton() + .logMessage( + "EditorUISystem: dispatching splat brush at " + + Ogre::StringConverter:: + toString( + hit) + + " layer=" + + Ogre::StringConverter::toString( + ts->getPaintLayerIndex())); ts->applySplatBrush( hit); + } else if ( + ts->isAuxPainting()) { + Ogre::Vector3 + brushPos = + hit; + brushPos.x = ts->visualToPhysicalX( + hit.x); + brushPos.z = ts->visualToPhysicalZ( + hit.z); + ts->applyAuxBrush( + ts->getAuxPaintMapName(), + brushPos, + ts->getPaintRadius(), + ts->getPaintStrength()); } else { - Ogre::Vector3 brushPos = - hit; - brushPos.x = - ts->visualToPhysicalX( - hit.x); - brushPos.z = - ts->visualToPhysicalZ( - hit.z); + Ogre::Vector3 + brushPos = + hit; + brushPos.x = ts->visualToPhysicalX( + hit.x); + brushPos.z = ts->visualToPhysicalZ( + hit.z); ts->applySculptBrush( brushPos); } @@ -556,7 +578,8 @@ void EditorUISystem::renderHierarchyWindow() if (ImGui::MenuItem("Item Registry")) { m_showItemRegistry = true; } - if (ImGui::MenuItem("Animation Tree Registry")) { + if (ImGui::MenuItem( + "Animation Tree Registry")) { m_showAnimationTreeRegistry = true; } ImGui::Separator(); @@ -1163,7 +1186,7 @@ void EditorUISystem::renderComponentList(flecs::entity entity) if (entity.has()) { auto &spawner = entity.get_mut(); m_componentRegistry.render(entity, - spawner); + spawner); componentCount++; } diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index 8f3b25a..7dc512f 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -4236,11 +4236,12 @@ nlohmann::json SceneSerializer::serializeTerrain(flecs::entity entity) } json["roadEdges"] = roadEdgesJson; - /* Persist binary heightmap alongside the JSON. */ + /* Persist binary heightmap, blend maps, and aux maps alongside the JSON. */ TerrainSystem *ts = TerrainSystem::getInstance(); if (ts) { ts->saveSceneHeightmap(tc); ts->saveSceneBlendMaps(tc); + ts->saveSceneAuxMaps(tc); } return json; diff --git a/src/features/editScene/systems/TerrainSystem.cpp b/src/features/editScene/systems/TerrainSystem.cpp index 1bac9ca..19f89a1 100644 --- a/src/features/editScene/systems/TerrainSystem.cpp +++ b/src/features/editScene/systems/TerrainSystem.cpp @@ -84,8 +84,7 @@ static float proceduralHeight(long worldX, long worldZ) } float TerrainSystem::computeDetailNoise( - long worldX, long worldZ, - const TerrainComponent::DetailNoise &n) const + long worldX, long worldZ, const TerrainComponent::DetailNoise &n) const { if (!n.enabled || n.octaves <= 0) return 0.0f; @@ -108,6 +107,35 @@ float TerrainSystem::computeDetailNoise( return sum; } +/* ------------------------------------------------------------------ */ +/* Brush falloff shape (M4.3) */ +/* ------------------------------------------------------------------ */ + +float TerrainSystem::evaluateBrushFalloff(float distance, float radius, + BrushFalloffShape shape) +{ + if (radius <= 0.0f || distance >= radius) + return 0.0f; + + float t = distance / radius; + t = std::max(0.0f, std::min(1.0f, t)); + + switch (shape) { + case BrushFalloffShape::Linear: + return 1.0f - t; + case BrushFalloffShape::Smoothstep: + return 1.0f - (3.0f * t * t - 2.0f * t * t * t); + case BrushFalloffShape::Gaussian: + return expf(-4.0f * t * t); + case BrushFalloffShape::Spherical: + return sqrtf(std::max(0.0f, 1.0f - t * t)); + case BrushFalloffShape::Cone: + return 1.0f; + } + + return 0.0f; +} + /* ------------------------------------------------------------------ */ /* DummyPageProvider */ /* ------------------------------------------------------------------ */ @@ -604,6 +632,386 @@ bool TerrainSystem::loadSceneBlendMaps(const TerrainComponent &tc) return loadBlendMaps(getHeightmapPath(tc) + "_blendmaps"); } +/* ------------------------------------------------------------------ */ +/* Aux-map editing (M4.4) */ +/* ------------------------------------------------------------------ */ + +std::string +TerrainSystem::getAuxMapPath(const TerrainComponent &tc, + const TerrainComponent::AuxMap &aux) const +{ + return "heightmaps/" + Ogre::StringConverter::toString(tc.terrainId) + + "/" + aux.fileName; +} + +const std::vector * +TerrainSystem::ensureAuxMapLoaded(const TerrainComponent::AuxMap &aux, + const std::string &path) const +{ + auto it = m_auxMapData.find(aux.name); + if (it != m_auxMapData.end()) + return &it->second; + + std::vector data; + std::ifstream file(path, std::ios::binary); + if (file) { + data.resize(aux.resolution * aux.resolution); + file.read(reinterpret_cast(data.data()), + data.size() * sizeof(float)); + if (!file) { + Ogre::LogManager::getSingleton().logMessage( + "Terrain: failed to read aux map " + path); + data.clear(); + } + } + + if (data.empty()) { + data.assign(aux.resolution * aux.resolution, aux.defaultValue); + } + + auto inserted = m_auxMapData.emplace(aux.name, std::move(data)); + return &inserted.first->second; +} + +void TerrainSystem::applyAuxBrush(const std::string &auxMapName, + const Ogre::Vector3 &worldPos, float radius, + float delta) +{ + if (!m_active) + return; + + flecs::entity_t foundEntity = 0; + const TerrainComponent::AuxMap *aux = nullptr; + m_terrainQuery.each([&](flecs::entity e, TerrainComponent &tc, + TransformComponent &) { + if (foundEntity != 0) + return; + for (auto &a : tc.auxMaps) { + if (a.name == auxMapName) { + aux = &a; + foundEntity = e.id(); + break; + } + } + }); + + if (!aux) { + Ogre::LogManager::getSingleton().logMessage( + "Terrain: applyAuxBrush unknown aux map " + auxMapName); + return; + } + + std::string path; + m_terrainQuery.each([&](flecs::entity e, TerrainComponent &tc, + TransformComponent &) { + if (e.id() == foundEntity) + path = getAuxMapPath(tc, *aux); + }); + + const std::vector *dataPtr = ensureAuxMapLoaded(*aux, path); + if (!dataPtr) + return; + + std::vector &data = *const_cast *>(dataPtr); + const int res = aux->resolution; + + float spacing = m_heightmapWorldSize / (float)(res - 1); + int rSamples = (int)(radius / spacing) + 1; + int cx = (int)((worldPos.x - m_heightmapWorldMinX) / + m_heightmapWorldSize * (float)res); + int cz = (int)((worldPos.z - m_heightmapWorldMinZ) / + m_heightmapWorldSize * (float)res); + int x0 = std::max(0, cx - rSamples); + int x1 = std::min(res - 1, cx + rSamples); + int z0 = std::max(0, cz - rSamples); + int z1 = std::min(res - 1, cz + rSamples); + + 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 > radius * radius) + continue; + + float dist = sqrtf(d2); + float falloff = evaluateBrushFalloff( + dist, radius, m_brushFalloffShape); + float &v = data[z * res + x]; + v = std::max(0.0f, std::min(1.0f, v + delta * falloff)); + } + } + + m_dirtyAuxMaps.insert(auxMapName); + if (m_showAuxMapVisualization && + m_auxMapVisualizationName == auxMapName) + m_auxVisDirty = true; +} + +float TerrainSystem::sampleAuxMap(const std::string &auxMapName, long worldX, + long worldZ) const +{ + if (!m_active) + return 0.0f; + + const TerrainComponent::AuxMap *aux = nullptr; + std::string path; + m_terrainQuery.each([&](flecs::entity e, TerrainComponent &tc, + TransformComponent &) { + for (auto &a : tc.auxMaps) { + if (a.name == auxMapName) { + aux = &a; + path = getAuxMapPath(tc, a); + break; + } + } + }); + + if (!aux) + return 0.0f; + + const std::vector *dataPtr = ensureAuxMapLoaded(*aux, path); + if (!dataPtr || dataPtr->empty()) + return aux->defaultValue; + + const int res = aux->resolution; + const std::vector &data = *dataPtr; + + float fx = ((float)worldX - m_heightmapWorldMinX) / + m_heightmapWorldSize * (float)res; + float fz = ((float)worldZ - m_heightmapWorldMinZ) / + m_heightmapWorldSize * (float)res; + + int x0 = (int)floorf(fx); + int z0 = (int)floorf(fz); + int x1 = x0 + 1; + int z1 = z0 + 1; + + x0 = std::max(0, std::min(x0, res - 1)); + x1 = std::max(0, std::min(x1, res - 1)); + z0 = std::max(0, std::min(z0, res - 1)); + z1 = std::max(0, std::min(z1, res - 1)); + + float tx = fx - (float)x0; + float tz = fz - (float)z0; + + float v00 = data[z0 * res + x0]; + float v10 = data[z0 * res + x1]; + float v01 = data[z1 * res + x0]; + float v11 = data[z1 * res + x1]; + + return (1.0f - tx) * (1.0f - tz) * v00 + tx * (1.0f - tz) * v10 + + (1.0f - tx) * tz * v01 + tx * tz * v11; +} + +bool TerrainSystem::saveSceneAuxMaps(const TerrainComponent &tc) +{ + if (m_dirtyAuxMaps.empty()) + return true; + + bool ok = true; + for (const auto &aux : tc.auxMaps) { + if (m_dirtyAuxMaps.find(aux.name) == m_dirtyAuxMaps.end()) + continue; + + auto it = m_auxMapData.find(aux.name); + if (it == m_auxMapData.end()) + continue; + + std::string path = getAuxMapPath(tc, aux); + std::filesystem::path p(path); + std::filesystem::create_directories(p.parent_path()); + + std::ofstream file(path, std::ios::binary); + if (!file) { + Ogre::LogManager::getSingleton().logMessage( + "Terrain: failed to create aux map file " + + path); + ok = false; + continue; + } + + file.write(reinterpret_cast(it->second.data()), + it->second.size() * sizeof(float)); + Ogre::LogManager::getSingleton().logMessage( + "Terrain: saved aux map " + aux.name + " to " + path); + } + + m_dirtyAuxMaps.clear(); + return ok; +} + +bool TerrainSystem::loadSceneAuxMaps(const TerrainComponent &tc) +{ + m_auxMapData.clear(); + m_dirtyAuxMaps.clear(); + + for (const auto &aux : tc.auxMaps) { + std::string path = getAuxMapPath(tc, aux); + ensureAuxMapLoaded(aux, path); + } + + return !tc.auxMaps.empty(); +} + +/* ------------------------------------------------------------------ */ +/* Aux-map viewport visualization (M4.4 visual aid) */ +/* ------------------------------------------------------------------ */ + +Ogre::ColourValue TerrainSystem::auxValueToColour(float v) +{ + /* Blue -> Red heat gradient with partial transparency. */ + v = std::max(0.0f, std::min(1.0f, v)); + return Ogre::ColourValue(v, 0.0f, 1.0f - v, 0.55f); +} + +void TerrainSystem::setShowAuxMapVisualization(bool v) +{ + if (v == m_showAuxMapVisualization) + return; + m_showAuxMapVisualization = v; + if (v) { + m_auxVisDirty = true; + } else { + destroyAuxMapVisualization(); + } +} + +void TerrainSystem::setAuxMapVisualizationName(const std::string &name) +{ + if (name == m_auxMapVisualizationName) + return; + m_auxMapVisualizationName = name; + if (m_showAuxMapVisualization) + m_auxVisDirty = true; + if (name.empty()) + destroyAuxMapVisualization(); +} + +void TerrainSystem::destroyAuxMapVisualization() +{ + if (m_auxVisNode) { + if (m_auxVisObj) + m_auxVisNode->detachObject(m_auxVisObj); + m_sceneMgr->destroySceneNode(m_auxVisNode); + m_auxVisNode = nullptr; + } + if (m_auxVisObj) { + m_sceneMgr->destroyManualObject(m_auxVisObj); + m_auxVisObj = nullptr; + } +} + +void TerrainSystem::buildAuxMapVisualization() +{ + m_auxVisDirty = false; + + if (!m_active || !m_showAuxMapVisualization || + m_auxMapVisualizationName.empty()) { + destroyAuxMapVisualization(); + return; + } + + if (!mTerrainGroup) + return; + + /* Find the aux map descriptor and its data. */ + const TerrainComponent::AuxMap *aux = nullptr; + std::string path; + m_terrainQuery.each( + [&](flecs::entity, TerrainComponent &tc, TransformComponent &) { + for (auto &a : tc.auxMaps) { + if (a.name == m_auxMapVisualizationName) { + aux = &a; + path = getAuxMapPath(tc, a); + break; + } + } + }); + if (!aux) { + destroyAuxMapVisualization(); + return; + } + + const std::vector *dataPtr = ensureAuxMapLoaded(*aux, path); + if (!dataPtr || dataPtr->empty()) { + destroyAuxMapVisualization(); + return; + } + + if (!m_auxVisMaterial) { + m_auxVisMaterial = Ogre::MaterialManager::getSingleton().create( + "AuxMapVisMaterial", + Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, + true); + Ogre::Pass *pass = + m_auxVisMaterial->getTechnique(0)->getPass(0); + pass->setLightingEnabled(false); + pass->setVertexColourTracking(Ogre::TVC_AMBIENT); + pass->setSceneBlending(Ogre::SBT_TRANSPARENT_ALPHA); + pass->setDepthWriteEnabled(false); + pass->setDepthCheckEnabled(false); + pass->setCullingMode(Ogre::CULL_NONE); + } + + if (!m_auxVisObj) { + m_auxVisObj = m_sceneMgr->createManualObject( + "AuxMapVis_" + m_auxMapVisualizationName); + m_auxVisObj->setRenderQueueGroup( + Ogre::RENDER_QUEUE_OVERLAY); + m_auxVisNode = + m_sceneMgr->getRootSceneNode()->createChildSceneNode(); + m_auxVisNode->attachObject(m_auxVisObj); + } else { + m_auxVisObj->clear(); + } + + const int gridRes = std::min(128, aux->resolution); + const Ogre::Real ws = mTerrainGroup->getTerrainWorldSize(); + const float halfSize = ws * 0.5f; + Ogre::Vector3 minPos((float)m_pageMinX * ws - halfSize, 0.0f, + (float)m_pageMinY * ws - halfSize); + Ogre::Vector3 maxPos((float)(m_pageMaxX + 1) * ws - halfSize, 0.0f, + (float)(m_pageMaxY + 1) * ws - halfSize); + + const float stepX = (maxPos.x - minPos.x) / (float)gridRes; + const float stepZ = (maxPos.z - minPos.z) / (float)gridRes; + const int rowSize = gridRes + 1; + + m_auxVisObj->begin("AuxMapVisMaterial", + Ogre::RenderOperation::OT_TRIANGLE_LIST); + + for (int z = 0; z <= gridRes; ++z) { + for (int x = 0; x <= gridRes; ++x) { + Ogre::Vector3 pos(minPos.x + (float)x * stepX, 0.0f, + minPos.z + (float)z * stepZ); + pos.y = getHeightAt(pos) + 1.0f; + + long px = (long)visualToPhysicalX(pos.x); + long pz = (long)visualToPhysicalZ(pos.z); + float v = + sampleAuxMap(m_auxMapVisualizationName, px, pz); + + m_auxVisObj->position(pos); + m_auxVisObj->colour(auxValueToColour(v)); + } + } + + for (int z = 0; z < gridRes; ++z) { + for (int x = 0; x < gridRes; ++x) { + int i = z * rowSize + x; + m_auxVisObj->index(i); + m_auxVisObj->index(i + 1); + m_auxVisObj->index(i + rowSize); + m_auxVisObj->index(i + 1); + m_auxVisObj->index(i + rowSize + 1); + m_auxVisObj->index(i + rowSize); + } + } + + m_auxVisObj->end(); +} + void TerrainSystem::ensureHeightmapLoaded(TerrainComponent &tc) { if (m_heightmapLoaded) @@ -920,6 +1328,7 @@ void TerrainSystem::activate(TerrainComponent &tc, TransformComponent &xform) /* Restore saved blend maps if they exist. This must happen after * m_active is set because loadBlendMaps early-outs when inactive. */ loadSceneBlendMaps(tc); + loadSceneAuxMaps(tc); if (m_physics) m_physics->setBodyDrawFilter(&mBodyDrawFilter); @@ -950,6 +1359,8 @@ void TerrainSystem::deactivate() m_heightmapLoaded = false; m_heightData.clear(); m_heightmapRes = 0; + m_auxMapData.clear(); + m_dirtyAuxMaps.clear(); /* Clean up sculpt previews without trying to reload terrain * pages (we're shutting down the whole terrain). */ @@ -980,6 +1391,9 @@ void TerrainSystem::deactivate() m_brushDecalNode = nullptr; } + /* Destroy aux-map visualization. */ + destroyAuxMapVisualization(); + removeAllColliders(); if (m_physics) m_physics->setBodyDrawFilter(nullptr); @@ -1069,8 +1483,10 @@ void TerrainSystem::update(float /*deltaTime*/) /* Editor-flagged dirty: rebuild all loaded pages so parameter * changes (e.g. detail noise) take effect. */ if (m_active && tc.dirty && mTerrainGroup) { - for (const auto &kv : mTerrainGroup->getTerrainSlots()) { - Ogre::TerrainGroup::TerrainSlot *slot = kv.second; + for (const auto &kv : + mTerrainGroup->getTerrainSlots()) { + Ogre::TerrainGroup::TerrainSlot *slot = + kv.second; if (slot && slot->instance && slot->instance->isLoaded()) markPageDirty(slot->x, slot->y); @@ -1099,6 +1515,9 @@ void TerrainSystem::update(float /*deltaTime*/) processColliderRemoves(); processColliderCreates(); + if (m_auxVisDirty) + buildAuxMapVisualization(); + mBodyDrawFilter.showTerrain = m_showTerrainColliders; } @@ -1281,9 +1700,11 @@ void TerrainSystem::setSculptMode(bool v) if (v == m_sculptMode) return; - /* Sculpt and paint are mutually exclusive. */ + /* Sculpt, paint and aux-paint are mutually exclusive. */ if (v && m_paintMode) m_paintMode = false; + if (v && m_auxPaintMode) + setAuxPaintMode(false); m_sculptMode = v; if (v) @@ -1365,9 +1786,10 @@ void TerrainSystem::applySculptBrush(const Ogre::Vector3 &worldPos) 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; + float dist = sqrtf(d2); + float falloff = evaluateBrushFalloff( + dist, m_sculptRadius, + m_brushFalloffShape); m_heightData[z * m_heightmapRes + x] += sign * m_sculptStrength * falloff; @@ -1475,7 +1897,8 @@ void TerrainSystem::applySplatBrush(const Ogre::Vector3 &worldPos) * be a different size than getLayerBlendMapSize() due to * hardware padding / packing). */ size_t maxX, maxY; - blendMap->convertUVToImageSpace(1.0f, 1.0f, &maxX, &maxY); + blendMap->convertUVToImageSpace(1.0f, 1.0f, &maxX, + &maxY); int bmW = (int)maxX + 1; int bmH = (int)maxY + 1; @@ -1515,12 +1938,10 @@ void TerrainSystem::applySplatBrush(const Ogre::Vector3 &worldPos) /* Integer brush centre clamped to bounds; used for debug * readback and logging only. */ - int cxClamped = (int)std::max(0.0f, - std::min(cxRaw, - (float)(bmW - 1))); - int cyClamped = (int)std::max(0.0f, - std::min(cyRaw, - (float)(bmH - 1))); + int cxClamped = (int)std::max( + 0.0f, std::min(cxRaw, (float)(bmW - 1))); + int cyClamped = (int)std::max( + 0.0f, std::min(cyRaw, (float)(bmH - 1))); bool painted = false; for (int y = y0; y <= y1; ++y) { @@ -1531,9 +1952,10 @@ void TerrainSystem::applySplatBrush(const Ogre::Vector3 &worldPos) if (d2 > m_paintRadius * m_paintRadius) continue; - float falloff = - 1.0f - - sqrtf(d2) / m_paintRadius; + float dist = sqrtf(d2); + float falloff = evaluateBrushFalloff( + dist, m_paintRadius, + m_brushFalloffShape); float cur = blendMap->getBlendValue( (Ogre::uint32)x, (Ogre::uint32)y); @@ -1552,8 +1974,7 @@ void TerrainSystem::applySplatBrush(const Ogre::Vector3 &worldPos) /* dirtyRect uses half-open bounds. */ blendMap->dirtyRect( Ogre::Rect((long)x0, (long)y0, - (long)x1 + 1, - (long)y1 + 1)); + (long)x1 + 1, (long)y1 + 1)); float cpuBefore = blendMap->getBlendValue( (Ogre::uint32)cxClamped, (Ogre::uint32)cyClamped); @@ -1568,41 +1989,68 @@ void TerrainSystem::applySplatBrush(const Ogre::Vector3 &worldPos) static int s_splatDebugCount = 0; if (s_splatDebugCount < 10) { try { - std::pair texIdx = - terrain->getLayerBlendTextureIndex( - (Ogre::uint8)layerIdx); + std::pair + texIdx = terrain->getLayerBlendTextureIndex( + (Ogre::uint8) + layerIdx); Ogre::TexturePtr tex = terrain->getLayerBlendTexture( texIdx.first); if (tex) { - Ogre::HardwarePixelBufferSharedPtr buf = - tex->getBuffer(); + Ogre::HardwarePixelBufferSharedPtr + buf = tex->getBuffer(); Ogre::PixelBox pb = buf->lock( - Ogre::Box((Ogre::uint32)cxClamped, - (Ogre::uint32)cyClamped, - (Ogre::uint32)cxClamped + 1, - (Ogre::uint32)cyClamped + 1), - Ogre::HardwareBuffer::HBL_READ_ONLY); - Ogre::PixelFormat fmt = buf->getFormat(); - int bpp = (int)Ogre::PixelUtil::getNumElemBytes( - fmt); + Ogre::Box( + (Ogre::uint32) + cxClamped, + (Ogre::uint32) + cyClamped, + (Ogre::uint32)cxClamped + + 1, + (Ogre::uint32)cyClamped + + 1), + Ogre::HardwareBuffer:: + HBL_READ_ONLY); + Ogre::PixelFormat fmt = + buf->getFormat(); + int bpp = (int)Ogre::PixelUtil:: + getNumElemBytes( + fmt); std::string vals; - for (int i = 0; i < bpp && i < 4; ++i) { + for (int i = 0; + i < bpp && i < 4; + ++i) { if (i > 0) vals += ","; vals += Ogre::StringConverter::toString( - (int)((uint8_t *)pb.data)[i]); + (int)((uint8_t *)pb + .data) + [i]); } buf->unlock(); - Ogre::LogManager::getSingleton().logMessage( - "Terrain: blend debug cpuBefore=" + - Ogre::StringConverter::toString( - cpuBefore) + - " gpuBytes(" + Ogre::StringConverter::toString(cxClamped) + "," + Ogre::StringConverter::toString(cyClamped) + ")=[" + - vals + "] fmt=" + - Ogre::PixelUtil::getFormatName(fmt) + - " bpp=" + - Ogre::StringConverter::toString(bpp)); + Ogre::LogManager::getSingleton() + .logMessage( + "Terrain: blend debug cpuBefore=" + + Ogre::StringConverter:: + toString( + cpuBefore) + + " gpuBytes(" + + Ogre::StringConverter:: + toString( + cxClamped) + + "," + + Ogre::StringConverter:: + toString( + cyClamped) + + ")=[" + + vals + + "] fmt=" + + Ogre::PixelUtil::getFormatName( + fmt) + + " bpp=" + + Ogre::StringConverter::toString( + bpp)); } } catch (const std::exception &e) { Ogre::LogManager::getSingleton().logMessage( diff --git a/src/features/editScene/systems/TerrainSystem.hpp b/src/features/editScene/systems/TerrainSystem.hpp index 01f70d1..f5f650a 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 #include @@ -40,11 +41,17 @@ public: TerrainSystem(const TerrainSystem &) = delete; TerrainSystem &operator=(const TerrainSystem &) = delete; - static TerrainSystem *getInstance() { return s_instance; } + static TerrainSystem *getInstance() + { + return s_instance; + } void update(float deltaTime); void deactivate(); - bool isActive() const { return m_active; } + bool isActive() const + { + return m_active; + } float getHeightAt(const Ogre::Vector3 &worldPos) const; bool raycastTerrain(const Ogre::Ray &ray, float &outT, @@ -77,47 +84,172 @@ public: void processDeferredReloads(); /* --- Detail noise (M4.2) --- */ - float computeDetailNoise(long worldX, long worldZ, - const struct TerrainComponent::DetailNoise &n) const; + float + computeDetailNoise(long worldX, long worldZ, + const struct TerrainComponent::DetailNoise &n) const; /* --- Sculpting (M3) --- */ enum class SculptTool { Raise, Lower, Smooth, Flatten }; - bool getSculptMode() const { return m_sculptMode; } + bool getSculptMode() const + { + return m_sculptMode; + } void setSculptMode(bool v); - bool isSculpting() const { return m_sculptMode; } - SculptTool getSculptTool() const { return m_sculptTool; } + bool isSculpting() const + { + return m_sculptMode; + } + SculptTool getSculptTool() const + { + return m_sculptTool; + } void setSculptTool(SculptTool t); - float getSculptRadius() const { return m_sculptRadius; } - void setSculptRadius(float r) { m_sculptRadius = r; } - float getSculptStrength() const { return m_sculptStrength; } - void setSculptStrength(float s) { m_sculptStrength = s; } + float getSculptRadius() const + { + return m_sculptRadius; + } + void setSculptRadius(float r) + { + m_sculptRadius = r; + } + float getSculptStrength() const + { + return m_sculptStrength; + } + void setSculptStrength(float s) + { + m_sculptStrength = s; + } void snapCameraAboveTerrain(); void applySculptBrush(const Ogre::Vector3 &worldPos); + /* --- Brush falloff shape (M4.3) --- */ + enum class BrushFalloffShape { + Linear, // 1 - t, zero at edge + Smoothstep, // 1 - (3t^2 - 2t^3) + Gaussian, // exp(-4 t^2) + Spherical, // sqrt(1 - t^2) + Cone // 1 inside radius, 0 at edge + }; + + BrushFalloffShape getBrushFalloffShape() const + { + return m_brushFalloffShape; + } + void setBrushFalloffShape(BrushFalloffShape s) + { + m_brushFalloffShape = s; + } + + static float evaluateBrushFalloff(float distance, float radius, + BrushFalloffShape shape); + + /* --- Aux-map editing (M4.4) --- */ + void applyAuxBrush(const std::string &auxMapName, + const Ogre::Vector3 &worldPos, float radius, + float delta); + float sampleAuxMap(const std::string &auxMapName, long worldX, + long worldZ) const; + bool saveSceneAuxMaps(const struct TerrainComponent &tc); + bool loadSceneAuxMaps(const struct TerrainComponent &tc); + std::string + getAuxMapPath(const struct TerrainComponent &tc, + const struct TerrainComponent::AuxMap &aux) const; + /* --- Splat painting (M3) --- */ - bool getPaintMode() const { return m_paintMode; } + bool getPaintMode() const + { + return m_paintMode; + } void setPaintMode(bool v) { if (v == m_paintMode) return; m_paintMode = v; if (v) { - /* Paint mode is mutually exclusive with sculpt. */ + /* Paint mode is mutually exclusive with sculpt and aux. */ if (m_sculptMode) setSculptMode(false); + if (m_auxPaintMode) + setAuxPaintMode(false); } else { hideBrushDecal(); } } - bool isPainting() const { return m_paintMode; } - int getPaintLayerIndex() const { return m_paintLayerIndex; } - void setPaintLayerIndex(int i) { m_paintLayerIndex = i; } - float getPaintRadius() const { return m_paintRadius; } - void setPaintRadius(float r) { m_paintRadius = r; } - float getPaintStrength() const { return m_paintStrength; } - void setPaintStrength(float s) { m_paintStrength = s; } + bool isPainting() const + { + return m_paintMode; + } + int getPaintLayerIndex() const + { + return m_paintLayerIndex; + } + void setPaintLayerIndex(int i) + { + m_paintLayerIndex = i; + } + float getPaintRadius() const + { + return m_paintRadius; + } + void setPaintRadius(float r) + { + m_paintRadius = r; + } + float getPaintStrength() const + { + return m_paintStrength; + } + void setPaintStrength(float s) + { + m_paintStrength = s; + } void applySplatBrush(const Ogre::Vector3 &worldPos); + /* --- Aux-map painting (M4.4) --- */ + bool getAuxPaintMode() const + { + return m_auxPaintMode; + } + void setAuxPaintMode(bool v) + { + if (v == m_auxPaintMode) + return; + m_auxPaintMode = v; + if (v) { + if (m_sculptMode) + setSculptMode(false); + if (m_paintMode) + setPaintMode(false); + } else { + hideBrushDecal(); + } + } + bool isAuxPainting() const + { + return m_auxPaintMode; + } + const std::string &getAuxPaintMapName() const + { + return m_auxPaintMapName; + } + void setAuxPaintMapName(const std::string &name) + { + m_auxPaintMapName = name; + } + + /* --- Aux-map viewport visualization (M4.4 visual aid) --- */ + bool getShowAuxMapVisualization() const + { + return m_showAuxMapVisualization; + } + void setShowAuxMapVisualization(bool v); + const std::string &getAuxMapVisualizationName() const + { + return m_auxMapVisualizationName; + } + void setAuxMapVisualizationName(const std::string &name); + /* --- Brush decal (M3) --- */ void updateBrushDecal(const Ogre::Vector3 &worldPos, const Ogre::Vector3 &normal, float radius); @@ -194,8 +326,8 @@ private: TerrainSystem *owner = nullptr; }; - class CustomTerrainDefiner : - public Ogre::TerrainPagedWorldSection::TerrainDefiner { + class CustomTerrainDefiner + : public Ogre::TerrainPagedWorldSection::TerrainDefiner { public: CustomTerrainDefiner(TerrainSystem *sys) : Ogre::TerrainPagedWorldSection::TerrainDefiner() @@ -204,6 +336,7 @@ private: } void define(Ogre::TerrainGroup *terrainGroup, long x, long y) override; + private: TerrainSystem *m_sys; }; @@ -219,8 +352,12 @@ private: { m_terrainIds.erase(id); } - void clear() { m_terrainIds.clear(); } + void clear() + { + m_terrainIds.clear(); + } bool showTerrain = false; + private: std::set m_terrainIds; }; @@ -252,7 +389,7 @@ private: std::unique_ptr mDummyPageProvider; std::unordered_map mColliders; - std::deque> mColliderCreateQueue; + std::deque > mColliderCreateQueue; std::set mPendingColliderPages; std::mutex m_colliderQueueMutex; TerrainBodyDrawFilter mBodyDrawFilter; @@ -273,12 +410,27 @@ private: std::set m_dirtyPages; std::vector m_reloadQueue; - bool hasHeightmap() const { return m_heightmapLoaded; } + bool hasHeightmap() const + { + return m_heightmapLoaded; + } /* Detail noise state (M4.2). A local copy is kept so height sampling * helpers do not touch ECS state while the heightmap mutex is held. */ struct TerrainComponent::DetailNoise m_detailNoise; + /* Brush falloff shape (M4.3). Runtime editor preference, not serialized. */ + BrushFalloffShape m_brushFalloffShape = BrushFalloffShape::Linear; + + /* Aux-map state (M4.4). Cached in memory while terrain is active. */ + mutable std::unordered_map > + m_auxMapData; + std::unordered_set m_dirtyAuxMaps; + + const std::vector * + ensureAuxMapLoaded(const struct TerrainComponent::AuxMap &aux, + const std::string &path) const; + /* Sculpting state */ bool m_sculptMode = false; SculptTool m_sculptTool = SculptTool::Raise; @@ -291,6 +443,22 @@ private: float m_paintRadius = 100.0f; float m_paintStrength = 0.5f; + /* Aux-map paint state (M4.4) */ + bool m_auxPaintMode = false; + std::string m_auxPaintMapName; + + /* Aux-map viewport visualization (M4.4 visual aid) */ + bool m_showAuxMapVisualization = false; + bool m_auxVisDirty = false; + std::string m_auxMapVisualizationName; + Ogre::ManualObject *m_auxVisObj = nullptr; + Ogre::SceneNode *m_auxVisNode = nullptr; + Ogre::MaterialPtr m_auxVisMaterial; + + void buildAuxMapVisualization(); + void destroyAuxMapVisualization(); + static Ogre::ColourValue auxValueToColour(float v); + /* --- Sculpt preview (M3) --- */ struct SculptPreview { Ogre::ManualObject *manualObj = nullptr; diff --git a/src/features/editScene/systems/TerrainTests.cpp b/src/features/editScene/systems/TerrainTests.cpp index ba58e83..541d561 100644 --- a/src/features/editScene/systems/TerrainTests.cpp +++ b/src/features/editScene/systems/TerrainTests.cpp @@ -702,6 +702,194 @@ bool TerrainTestRunner::testDetailNoise(EditorApp &app, TerrainSystem *ts) return true; } +bool TerrainTestRunner::testBrushShapes(EditorApp &app, TerrainSystem *ts) +{ + (void)app; + + /* Pure math verification: each shape must be deterministic and return + * the expected value at a known distance/radius ratio. */ + using Shape = TerrainSystem::BrushFalloffShape; + + struct Check { + Shape shape; + float t; + float expected; + const char *name; + } checks[] = { + { Shape::Linear, 0.0f, 1.0f, "Linear centre" }, + { Shape::Linear, 0.5f, 0.5f, "Linear mid" }, + { Shape::Linear, 1.0f, 0.0f, "Linear edge" }, + { Shape::Smoothstep, 0.0f, 1.0f, "Smoothstep centre" }, + { Shape::Smoothstep, 0.5f, 0.5f, "Smoothstep mid" }, + { Shape::Smoothstep, 1.0f, 0.0f, "Smoothstep edge" }, + { Shape::Gaussian, 0.0f, 1.0f, "Gaussian centre" }, + { Shape::Gaussian, 0.5f, expf(-1.0f), "Gaussian mid" }, + { Shape::Spherical, 0.0f, 1.0f, "Spherical centre" }, + { Shape::Spherical, 0.5f, sqrtf(0.75f), "Spherical mid" }, + { Shape::Cone, 0.0f, 1.0f, "Cone centre" }, + { Shape::Cone, 0.5f, 1.0f, "Cone mid" }, + { Shape::Cone, 1.0f, 0.0f, "Cone edge" }, + }; + + const float radius = 100.0f; + for (const auto &c : checks) { + float dist = c.t * radius; + float got = TerrainSystem::evaluateBrushFalloff(dist, radius, + c.shape); + if (std::fabs(got - c.expected) > 1e-5f) { + Ogre::LogManager::getSingleton().logMessage( + "TerrainTests: FAIL - brush shape " + + Ogre::String(c.name) + " expected " + + Ogre::StringConverter::toString(c.expected) + + " got " + Ogre::StringConverter::toString(got)); + return false; + } + } + + /* Outside the radius every shape must return zero. */ + for (int i = 0; i < (int)Shape::Cone + 1; ++i) { + Shape s = (Shape)i; + float got = + TerrainSystem::evaluateBrushFalloff(110.0f, 100.0f, s); + if (std::fabs(got) > 1e-6f) { + Ogre::LogManager::getSingleton().logMessage( + "TerrainTests: FAIL - brush shape outside radius did not return 0"); + return false; + } + } + + Ogre::LogManager::getSingleton().logMessage( + "TerrainTests: brush shape falloff test passed"); + return true; +} + +bool TerrainTestRunner::testAuxMapOperations(EditorApp &app, TerrainSystem *ts) +{ + if (!ts->isActive()) + return false; + + /* Find the test terrain entity and add an aux map for testing. */ + flecs::world *w = app.getWorld(); + flecs::entity terrain = flecs::entity::null(); + w->query().each( + [&](flecs::entity e, TerrainComponent &) { + if (e.has() && + e.get().name == "TestTerrain") + terrain = e; + }); + if (!terrain.is_alive()) { + /* Previous test steps may have destroyed the terrain. Create a + * fresh one for this test; it will be cleaned up later. */ + terrain = createTerrainEntity(app); + pumpFrames(app, ts, 5); + if (!ts->isActive()) { + destroyTerrainEntity(app, terrain); + return false; + } + } + + { + auto &tc = terrain.get_mut(); + bool exists = false; + for (auto &am : tc.auxMaps) { + if (am.name == "testAux") { + exists = true; + break; + } + } + if (!exists) { + TerrainComponent::AuxMap am; + am.name = "testAux"; + am.fileName = "testAux.bin"; + am.resolution = tc.heightmapSize; + am.defaultValue = 0.0f; + tc.auxMaps.push_back(am); + } + } + + /* Ensure the aux map is loaded into memory. */ + ts->loadSceneAuxMaps(terrain.get()); + + /* Use a cone falloff and a large radius so the centre sample receives + * full delta regardless of sub-pixel alignment. */ + ts->setBrushFalloffShape(TerrainSystem::BrushFalloffShape::Cone); + + Ogre::Vector3 centre(0.0f, 0.0f, 0.0f); + float v0 = ts->sampleAuxMap("testAux", 0, 0); + if (v0 != 0.0f) { + Ogre::LogManager::getSingleton().logMessage( + "TerrainTests: FAIL - aux map default not zero"); + return false; + } + + /* Paint once: value should increase. */ + ts->applyAuxBrush("testAux", centre, 200.0f, 0.5f); + float v1 = ts->sampleAuxMap("testAux", 0, 0); + if (std::fabs(v1 - 0.5f) > 1e-4f) { + Ogre::LogManager::getSingleton().logMessage( + "TerrainTests: FAIL - aux map paint expected 0.5 got " + + Ogre::StringConverter::toString(v1)); + return false; + } + + /* Paint again past 1.0: should clamp. */ + ts->applyAuxBrush("testAux", centre, 200.0f, 0.7f); + float v2 = ts->sampleAuxMap("testAux", 0, 0); + if (std::fabs(v2 - 1.0f) > 1e-4f) { + Ogre::LogManager::getSingleton().logMessage( + "TerrainTests: FAIL - aux map clamp to 1.0 failed, got " + + Ogre::StringConverter::toString(v2)); + return false; + } + + /* Save and reload. */ + const TerrainComponent &tc = terrain.get(); + if (!ts->saveSceneAuxMaps(tc)) { + Ogre::LogManager::getSingleton().logMessage( + "TerrainTests: FAIL - could not save aux map"); + return false; + } + + /* Corrupt the in-memory value to prove reload reads the file. */ + { + auto &tcMut = terrain.get_mut(); + /* Re-add the aux map with a different default; reload must + * restore the saved file contents. */ + ts->loadSceneAuxMaps(tcMut); + } + float v3 = ts->sampleAuxMap("testAux", 0, 0); + if (std::fabs(v3 - 1.0f) > 1e-4f) { + Ogre::LogManager::getSingleton().logMessage( + "TerrainTests: FAIL - aux map reload did not restore clamped value, got " + + Ogre::StringConverter::toString(v3)); + return false; + } + + /* Erase down and clamp to zero. */ + ts->applyAuxBrush("testAux", centre, 200.0f, -0.3f); + ts->applyAuxBrush("testAux", centre, 200.0f, -1.0f); + float v4 = ts->sampleAuxMap("testAux", 0, 0); + if (std::fabs(v4 - 0.0f) > 1e-4f) { + Ogre::LogManager::getSingleton().logMessage( + "TerrainTests: FAIL - aux map clamp to 0.0 failed, got " + + Ogre::StringConverter::toString(v4)); + return false; + } + + /* Verify viewport visualization builds without crashing. */ + ts->setAuxMapVisualizationName("testAux"); + ts->setShowAuxMapVisualization(true); + pumpFrames(app, ts, 2); + ts->setShowAuxMapVisualization(false); + + /* Clean up the aux map file. */ + std::filesystem::remove(ts->getAuxMapPath(tc, tc.auxMaps.back())); + + Ogre::LogManager::getSingleton().logMessage( + "TerrainTests: aux map operations test passed"); + return true; +} + bool TerrainTestRunner::testVerifyGeometry(EditorApp &app, TerrainSystem *ts) { if (!ts->isActive()) @@ -853,6 +1041,8 @@ int TerrainTestRunner::run(EditorApp &app, int iterations) { "sculpt", testSculptOperations }, { "splat", testSplatOperations }, { "detailNoise", testDetailNoise }, + { "brushShapes", testBrushShapes }, + { "auxMapOps", testAuxMapOperations }, { "verify", testVerifyGeometry }, { "delete", testDeleteTerrain }, diff --git a/src/features/editScene/systems/TerrainTests.hpp b/src/features/editScene/systems/TerrainTests.hpp index c63fbb9..6e7481b 100644 --- a/src/features/editScene/systems/TerrainTests.hpp +++ b/src/features/editScene/systems/TerrainTests.hpp @@ -64,6 +64,8 @@ private: static bool testSculptOperations(EditorApp &app, TerrainSystem *ts); static bool testSplatOperations(EditorApp &app, TerrainSystem *ts); static bool testDetailNoise(EditorApp &app, TerrainSystem *ts); + static bool testBrushShapes(EditorApp &app, TerrainSystem *ts); + static bool testAuxMapOperations(EditorApp &app, TerrainSystem *ts); static bool testVerifyGeometry(EditorApp &app, TerrainSystem *ts); static bool testDeleteTerrain(EditorApp &app, TerrainSystem *ts); static bool testMemoryStability(EditorApp &app); diff --git a/src/features/editScene/tests/lua_test_stubs.cpp b/src/features/editScene/tests/lua_test_stubs.cpp index 7b36afa..999d235 100644 --- a/src/features/editScene/tests/lua_test_stubs.cpp +++ b/src/features/editScene/tests/lua_test_stubs.cpp @@ -1905,4 +1905,23 @@ void registerLuaEventApi(lua_State *L) lua_setglobal(L, "ecs"); } +// --------------------------------------------------------------------------- +// Stub: LuaTerrainApi +// --------------------------------------------------------------------------- + +void registerLuaTerrainApi(lua_State *L) +{ + /* Minimal stub: expose an empty ecs.terrain table so scripts that touch + * terrain globals do not crash. */ + lua_getglobal(L, "ecs"); + if (!lua_istable(L, -1)) { + lua_newtable(L); + lua_setglobal(L, "ecs"); + lua_getglobal(L, "ecs"); + } + lua_newtable(L); + lua_setfield(L, -2, "terrain"); + lua_pop(L, 1); +} + } // namespace editScene diff --git a/src/features/editScene/ui/TerrainEditor.hpp b/src/features/editScene/ui/TerrainEditor.hpp index 02c8c35..20010b1 100644 --- a/src/features/editScene/ui/TerrainEditor.hpp +++ b/src/features/editScene/ui/TerrainEditor.hpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -100,7 +101,8 @@ public: renderPaintLayerSelector(tc, ts); float pradius = ts->getPaintRadius(); - if (ImGui::SliderFloat("Radius", &pradius, 5.0f, 500.0f)) + if (ImGui::SliderFloat("Radius", &pradius, 5.0f, + 500.0f)) ts->setPaintRadius(pradius); float pstrength = ts->getPaintStrength(); @@ -115,6 +117,142 @@ public: ImGui::Separator(); + /* --- Aux Maps (M4.4) --- */ + if (ts) { + ImGui::PushID("Aux"); + ImGui::Text("Aux Maps"); + + bool auxPaint = ts->getAuxPaintMode(); + if (ImGui::Checkbox("Paint Aux Mode", &auxPaint)) + ts->setAuxPaintMode(auxPaint); + if (auxPaint) + ImGui::TextColored(ImVec4(1, 0.5f, 0.2f, 1), + "AUX PAINT MODE ACTIVE"); + + /* Target map selector. */ + std::vector auxItems; + auxItems.reserve(tc.auxMaps.size()); + for (auto &am : tc.auxMaps) + auxItems.push_back(am.name.c_str()); + + int auxIdx = -1; + const std::string ¤tAux = + ts->getAuxPaintMapName(); + for (size_t i = 0; i < tc.auxMaps.size(); ++i) { + if (tc.auxMaps[i].name == currentAux) { + auxIdx = (int)i; + break; + } + } + if (auxIdx < 0 && !tc.auxMaps.empty()) { + auxIdx = 0; + ts->setAuxPaintMapName(tc.auxMaps[0].name); + } + int comboIdx = std::max(0, auxIdx); + if (ImGui::Combo("Target Aux Map", &comboIdx, + auxItems.data(), + (int)auxItems.size()) && + comboIdx >= 0 && comboIdx < (int)tc.auxMaps.size()) + ts->setAuxPaintMapName( + tc.auxMaps[comboIdx].name); + + /* Viewport overlay so the painted aux map is visible on the + * terrain (blue = 0, red = 1). */ + bool showVis = ts->getShowAuxMapVisualization(); + if (ImGui::Checkbox("Show Aux Visualization", &showVis)) + ts->setShowAuxMapVisualization(showVis); + + int visIdx = -1; + const std::string ¤tVis = + ts->getAuxMapVisualizationName(); + for (size_t i = 0; i < tc.auxMaps.size(); ++i) { + if (tc.auxMaps[i].name == currentVis) { + visIdx = (int)i; + break; + } + } + if (visIdx < 0 && !tc.auxMaps.empty()) { + visIdx = 0; + ts->setAuxMapVisualizationName( + tc.auxMaps[0].name); + } + int visComboIdx = std::max(0, visIdx); + if (ImGui::Combo("Visualized Aux Map", &visComboIdx, + auxItems.data(), + (int)auxItems.size()) && + visComboIdx >= 0 && + visComboIdx < (int)tc.auxMaps.size()) + ts->setAuxMapVisualizationName( + tc.auxMaps[visComboIdx].name); + + /* List existing aux maps. */ + for (size_t i = 0; i < tc.auxMaps.size(); ++i) { + const auto &am = tc.auxMaps[i]; + ImGui::BulletText( + "%s (%dx%d) default=%.2f file=%s", + am.name.c_str(), am.resolution, + am.resolution, am.defaultValue, + am.fileName.c_str()); + ImGui::PushID((int)i); + if (ImGui::SmallButton("Remove")) { + std::string path = + ts->getAuxMapPath(tc, am); + std::filesystem::remove(path); + if (ts->getAuxPaintMapName() == am.name) + ts->setAuxPaintMapName(""); + if (ts->getAuxMapVisualizationName() == + am.name) + ts->setAuxMapVisualizationName( + ""); + tc.auxMaps.erase(tc.auxMaps.begin() + + i); + changed = true; + } + ImGui::PopID(); + } + + /* Add new aux map. */ + static char auxNameBuf[128] = {}; + ImGui::InputText("New Aux Map Name", auxNameBuf, + sizeof(auxNameBuf)); + ImGui::SameLine(); + if (ImGui::SmallButton("Add Aux Map")) { + std::string name(auxNameBuf); + /* Sanitize: no path separators or empty names. */ + name.erase(std::remove_if( + name.begin(), name.end(), + [](char c) { + return c == '/' || + c == '\\'; + }), + name.end()); + if (!name.empty()) { + bool exists = false; + for (auto &am : tc.auxMaps) { + if (am.name == name) { + exists = true; + break; + } + } + if (!exists) { + TerrainComponent::AuxMap am; + am.name = name; + am.fileName = name + ".bin"; + am.resolution = + tc.heightmapSize; + am.defaultValue = 0.0f; + tc.auxMaps.push_back(am); + changed = true; + auxNameBuf[0] = '\0'; + } + } + } + + ImGui::PopID(); + } + + ImGui::Separator(); + /* --- Camera & file operations --- */ if (ts) { if (ImGui::Button("Snap Camera Above Terrain")) @@ -132,6 +270,12 @@ public: ImGui::SameLine(); if (ImGui::Button("Load Blend Maps")) ts->loadSceneBlendMaps(tc); + + if (ImGui::Button("Save Aux Maps")) + ts->saveSceneAuxMaps(tc); + ImGui::SameLine(); + if (ImGui::Button("Load Aux Maps")) + ts->loadSceneAuxMaps(tc); } ImGui::Separator(); @@ -154,8 +298,8 @@ public: 0.5f, 10.0f, "%.1f")) changed = true; if (ImGui::SliderFloat("Composite Map Dist", - &tc.compositeMapDistance, 50.0f, - 2000.0f, "%.0f")) + &tc.compositeMapDistance, 50.0f, 2000.0f, + "%.0f")) changed = true; ImGui::Separator(); @@ -165,20 +309,22 @@ public: auto &n = tc.detailNoise; bool noiseChanged = false; noiseChanged |= ImGui::Checkbox("Enabled", &n.enabled); - noiseChanged |= ImGui::SliderInt("Seed", &n.seed, 0, 100000); - noiseChanged |= ImGui::SliderInt("Octaves", &n.octaves, 1, 8); + noiseChanged |= + ImGui::SliderInt("Seed", &n.seed, 0, 100000); + noiseChanged |= + ImGui::SliderInt("Octaves", &n.octaves, 1, 8); noiseChanged |= ImGui::SliderFloat("Frequency", &n.frequency, 0.0001f, 0.01f, "%.4f"); - noiseChanged |= - ImGui::SliderFloat("Amplitude", &n.amplitude, - 0.0f, 200.0f, "%.1f"); - noiseChanged |= - ImGui::SliderFloat("Lacunarity", &n.lacunarity, - 1.0f, 4.0f, "%.2f"); - noiseChanged |= - ImGui::SliderFloat("Persistence", &n.persistence, - 0.0f, 1.0f, "%.2f"); + noiseChanged |= ImGui::SliderFloat("Amplitude", + &n.amplitude, 0.0f, + 200.0f, "%.1f"); + noiseChanged |= ImGui::SliderFloat("Lacunarity", + &n.lacunarity, 1.0f, + 4.0f, "%.2f"); + noiseChanged |= ImGui::SliderFloat("Persistence", + &n.persistence, 0.0f, + 1.0f, "%.2f"); if (noiseChanged) { changed = true; tc.dirty = true; @@ -191,8 +337,7 @@ public: /* Layers — editable with add/remove. Max 5 (SM2Profile limit). */ ImGui::Text("Layers: %zu / 5", tc.layers.size()); ImGui::SameLine(); - if (ImGui::SmallButton("Add Layer") && - tc.layers.size() < 5) { + if (ImGui::SmallButton("Add Layer") && tc.layers.size() < 5) { /* If the component has no explicit layers yet, the terrain is * using the implicit default base. Insert that base layer first * so the new layer becomes a splat-mapped layer on top instead of @@ -201,14 +346,15 @@ public: TerrainComponent::Layer baseLayer; baseLayer.name = "Base"; baseLayer.diffuseTexture = "Ground23_col.jpg"; - baseLayer.normalTexture = "Ground23_normheight.dds"; + baseLayer.normalTexture = + "Ground23_normheight.dds"; baseLayer.worldSize = 100.0f; tc.layers.push_back(baseLayer); } TerrainComponent::Layer newLayer; - newLayer.name = "Layer " + - std::to_string(tc.layers.size()); + newLayer.name = + "Layer " + std::to_string(tc.layers.size()); /* Use a visually distinct texture so the user can see * the result of splat painting immediately. */ newLayer.diffuseTexture = "Ground37_diffspec.dds"; @@ -241,12 +387,14 @@ public: char buf[256]; strncpy(buf, l.name.c_str(), sizeof(buf) - 1); buf[sizeof(buf) - 1] = 0; - if (ImGui::InputText("Name", buf, sizeof(buf))) { + if (ImGui::InputText("Name", buf, + sizeof(buf))) { l.name = buf; changed = true; } - if (texturePicker("Diffuse", l.diffuseTexture)) { + if (texturePicker("Diffuse", + l.diffuseTexture)) { changed = true; if (ts) ts->deactivate(); @@ -322,9 +470,9 @@ private: static bool isTextureExtension(const std::string &ext) { - static const char *imageExts[] = { "jpg", "jpeg", "png", - "dds", "tga", "bmp", "tif", - "tiff", "gif", "ktx" }; + static const char *imageExts[] = { "jpg", "jpeg", "png", "dds", + "tga", "bmp", "tif", "tiff", + "gif", "ktx" }; std::string lower = ext; std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower); @@ -458,9 +606,9 @@ private: bool isCurrent = (current == tex); if (isCurrent) - ImGui::PushStyleColor( - ImGuiCol_Text, - ImVec4(0.2f, 0.8f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_Text, + ImVec4(0.2f, 0.8f, 0.2f, + 1.0f)); if (ImGui::Selectable(tex.c_str(), isCurrent)) { current = tex; @@ -498,14 +646,13 @@ private: if (textureName.empty()) return; - Ogre::TextureManager &tm = - Ogre::TextureManager::getSingleton(); + Ogre::TextureManager &tm = Ogre::TextureManager::getSingleton(); Ogre::ResourceGroupManager &rgm = Ogre::ResourceGroupManager::getSingleton(); /* Find the resource group that contains this texture. */ - Ogre::String groupName = Ogre::ResourceGroupManager:: - DEFAULT_RESOURCE_GROUP_NAME; + Ogre::String groupName = + Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME; if (!tm.resourceExists(textureName, groupName)) { for (const auto &g : rgm.getResourceGroups()) { if (tm.resourceExists(textureName, g)) { @@ -532,7 +679,8 @@ private: } if (glID != 0) { - ImTextureID texID = (ImTextureID)(uintptr_t)glID; + ImTextureID texID = + (ImTextureID)(uintptr_t)glID; ImGui::Image(texID, ImVec2(64, 64), ImVec2(0, 0), ImVec2(1, 1)); } else {