AUX Painting works
This commit is contained in:
@@ -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})
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ¤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 {
|
||||
|
||||
Reference in New Issue
Block a user