AUX Painting works

This commit is contained in:
2026-07-12 13:50:07 +03:00
parent 9a12e84fec
commit ffbc85d4e2
14 changed files with 1810 additions and 272 deletions
+2
View File
@@ -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})
+44 -35
View File
@@ -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<Ogre::Material>(
it.getNext());
Ogre::static_pointer_cast<Ogre::Material>(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<EditorUISystem>(
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<TerrainSystem>(
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<ImGuiRenderListener>(
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;
}
+198 -114
View File
@@ -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<AuxMap> 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<RoadSidePrefab> 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<RoadSidePrefab> sidePrefabs;
};
std::vector<RoadNode> 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<Ogre::Vector3> collisionVertices;
std::vector<uint32_t> collisionIndices;
std::vector<flecs::entity> spawnedPrefabs;
long pageX, pageY;
flecs::entity bufferEntity = flecs::entity::null();
flecs::entity colliderEntity = flecs::entity::null();
std::vector<Ogre::Vector3> collisionVertices;
std::vector<uint32_t> collisionIndices;
std::vector<flecs::entity> spawnedPrefabs;
};
std::unordered_map<uint64_t, RoadPageGeometry> 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.
@@ -0,0 +1,408 @@
#include "LuaTerrainApi.hpp"
#include "../systems/TerrainSystem.hpp"
#include "../components/Terrain.hpp"
#include "../components/Transform.hpp"
#include <OgreLogManager.h>
#include <OgreStringConverter.h>
#include <flecs.h>
#include <cstring>
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<flecs::world *>(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<TerrainComponent, TransformComponent>().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
@@ -0,0 +1,33 @@
#ifndef EDITSCENE_LUA_TERRAIN_API_HPP
#define EDITSCENE_LUA_TERRAIN_API_HPP
#pragma once
#include <lua.hpp>
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
+3
View File
@@ -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
@@ -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<CharacterSpawnerComponent>()) {
auto &spawner = entity.get_mut<CharacterSpawnerComponent>();
m_componentRegistry.render<CharacterSpawnerComponent>(entity,
spawner);
spawner);
componentCount++;
}
@@ -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;
+492 -44
View File
@@ -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<float> *
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<float> data;
std::ifstream file(path, std::ios::binary);
if (file) {
data.resize(aux.resolution * aux.resolution);
file.read(reinterpret_cast<char *>(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<float> *dataPtr = ensureAuxMapLoaded(*aux, path);
if (!dataPtr)
return;
std::vector<float> &data = *const_cast<std::vector<float> *>(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<float> *dataPtr = ensureAuxMapLoaded(*aux, path);
if (!dataPtr || dataPtr->empty())
return aux->defaultValue;
const int res = aux->resolution;
const std::vector<float> &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<const char *>(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<float> *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<Ogre::uint8, Ogre::uint8> texIdx =
terrain->getLayerBlendTextureIndex(
(Ogre::uint8)layerIdx);
std::pair<Ogre::uint8,
Ogre::uint8>
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(
+193 -25
View File
@@ -15,6 +15,7 @@
#include <OgreMaterial.h>
#include <memory>
#include <unordered_map>
#include <unordered_set>
#include <set>
#include <deque>
#include <mutex>
@@ -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<JPH::BodyID> m_terrainIds;
};
@@ -252,7 +389,7 @@ private:
std::unique_ptr<DummyPageProvider> mDummyPageProvider;
std::unordered_map<uint64_t, TerrainCollider> mColliders;
std::deque<std::pair<long, long>> mColliderCreateQueue;
std::deque<std::pair<long, long> > mColliderCreateQueue;
std::set<uint64_t> mPendingColliderPages;
std::mutex m_colliderQueueMutex;
TerrainBodyDrawFilter mBodyDrawFilter;
@@ -273,12 +410,27 @@ private:
std::set<uint64_t> m_dirtyPages;
std::vector<uint64_t> 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<std::string, std::vector<float> >
m_auxMapData;
std::unordered_set<std::string> m_dirtyAuxMaps;
const std::vector<float> *
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;
@@ -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<TerrainComponent>().each(
[&](flecs::entity e, TerrainComponent &) {
if (e.has<EntityNameComponent>() &&
e.get<EntityNameComponent>().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<TerrainComponent>();
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<TerrainComponent>());
/* 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<TerrainComponent>();
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<TerrainComponent>();
/* 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 },
@@ -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);
@@ -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
+180 -32
View File
@@ -13,6 +13,7 @@
#include <imgui.h>
#include <algorithm>
#include <cstring>
#include <filesystem>
#include <string>
#include <vector>
@@ -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<const char *> auxItems;
auxItems.reserve(tc.auxMaps.size());
for (auto &am : tc.auxMaps)
auxItems.push_back(am.name.c_str());
int auxIdx = -1;
const std::string &currentAux =
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 &currentVis =
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 {