Compare commits

..

3 Commits

Author SHA1 Message Date
slapin f06f535938 Terrain robustness update 2026-07-01 10:57:45 +03:00
slapin 4cd2c72fb0 Now terrains serialization works 2026-06-27 04:53:33 +03:00
slapin 2c095de9f2 Start of terrain sculpting implementation 2026-06-27 03:18:11 +03:00
14 changed files with 1307 additions and 193 deletions
+97 -7
View File
@@ -2,6 +2,7 @@
#include <filesystem>
#include "EditorApp.hpp"
#include "GameMode.hpp"
#include <OgreMaterialManager.h>
#include "systems/EditorUISystem.hpp"
#include "systems/PhysicsSystem.hpp"
#include "systems/BuoyancySystem.hpp"
@@ -111,6 +112,7 @@
#endif
#include <OgreRTShaderSystem.h>
#include <OgreShaderRenderState.h>
#include <OgreArchiveManager.h>
#include "package/OgrePackageArchive.h"
#include <imgui.h>
@@ -236,16 +238,68 @@ EditorApp::EditorApp()
EditorApp::~EditorApp()
{
destroyEditorSystems();
}
void EditorApp::shutdownEditor()
{
destroyEditorSystems();
}
void EditorApp::closeApp()
{
shutdownEditor();
/* Terrain materials and other RTSS-generated materials are torn down
* in shutdownEditor(), so the base-class RTSS teardown can run safely
* while MaterialManager still exists. */
OgreBites::ApplicationContext::closeApp();
}
/* ------------------------------------------------------------------ */
/* Helper: release RTShader TargetRenderState objects held by every */
/* material pass. This destroys the SubRenderState instances owned */
/* by the target render states before the RTSS factories are torn */
/* down, avoiding the "Sub render states still exists" assertion. */
/* ------------------------------------------------------------------ */
static void clearRTShaderPassBindings()
{
Ogre::MaterialManager &matMgr =
Ogre::MaterialManager::getSingleton();
Ogre::MaterialManager::ResourceMapIterator it =
matMgr.getResourceIterator();
while (it.hasMoreElements()) {
Ogre::MaterialPtr mat =
Ogre::static_pointer_cast<Ogre::Material>(
it.getNext());
if (!mat)
continue;
for (Ogre::Technique *tech : mat->getTechniques()) {
if (!tech)
continue;
for (Ogre::Pass *pass : tech->getPasses()) {
if (!pass)
continue;
pass->getUserObjectBindings().eraseUserAny(
Ogre::RTShader::TargetRenderState::
UserKey);
}
}
}
}
void EditorApp::destroyEditorSystems()
{
if (m_systemsDestroyed)
return;
// Shutdown UI system first (cleans up gizmo while SceneManager is still valid)
if (m_uiSystem) {
m_uiSystem->shutdown();
}
// Delete all editor entities before OGRE cleanup
// This ensures all components with Ogre resources are cleaned up while SceneManager exists
// Collect entities first, then delete after iteration (can't modify during iteration)
std::vector<flecs::entity> entitiesToDelete;
// DialogueSystem is a singleton, no manual teardown needed
m_startupMenuSystem.reset();
@@ -273,13 +327,44 @@ EditorApp::~EditorApp()
m_lodSystem.reset();
m_cameraSystem.reset();
m_lightSystem.reset();
/* Terrain must be torn down before water, sky, sun and physics. */
m_terrainSystem.reset();
m_waterPlaneSystem.reset();
m_skyboxSystem.reset();
m_sunSystem.reset();
m_buoyancySystem.reset();
m_physicsSystem.reset();
/* Flush the RTShader generator cache before clearing materials. This
* waits for any pending shader-generation tasks and destroys generated
* SubRenderState instances while their factories are still alive. */
Ogre::RTShader::ShaderGenerator *shadergen =
Ogre::RTShader::ShaderGenerator::getSingletonPtr();
if (shadergen) {
shadergen->removeAllShaderBasedTechniques();
shadergen->flushShaderCache();
}
// Clear the scene and drop any materials that are no longer referenced so
// the RTShader generator can shut down without SubRenderState instances
// outliving their factories.
if (m_sceneMgr) {
m_sceneMgr->clearScene();
Ogre::MaterialManager::getSingleton().unloadUnreferencedResources();
clearRTShaderPassBindings();
}
if (m_imguiListener) {
Ogre::RenderWindow *rw = getRenderWindow();
if (rw)
rw->removeListener(m_imguiListener.get());
}
m_imguiListener.reset();
m_uiSystem.reset();
m_camera.reset();
// Now OGRE can shut down safely
// Singletons are managed by OGRE, don't delete them
m_systemsDestroyed = true;
}
void EditorApp::setup()
@@ -1408,6 +1493,11 @@ void EditorApp::createAxes()
}
}
bool EditorApp::frameEnded(const Ogre::FrameEvent & /*evt*/)
{
return true;
}
bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
{
bool paused = (m_gamePlayState == GamePlayState::Paused);
+7
View File
@@ -137,8 +137,12 @@ public:
// OgreBites::ApplicationContext overrides
void setup() override;
bool frameRenderingQueued(const Ogre::FrameEvent &evt) override;
bool frameEnded(const Ogre::FrameEvent &evt) override;
void locateResources() override;
void shutdownEditor();
void closeApp();
// OgreBites::InputListener overrides
bool mouseMoved(const OgreBites::MouseMotionEvent &evt) override;
bool mousePressed(const OgreBites::MouseButtonEvent &evt) override;
@@ -305,7 +309,10 @@ private:
GamePlayState m_gamePlayState = GamePlayState::Menu;
GameInputState m_gameInput;
bool m_setupComplete = false;
bool m_systemsDestroyed = false;
bool m_debugBuoyancy = false;
void destroyEditorSystems();
float m_playTime = 0.0f;
std::string m_currentBaseScene;
+148 -54
View File
@@ -200,12 +200,17 @@ std::unordered_map<uint64_t, TerrainCollider> mColliders;
paging for game mode.
3. **Deactivation / scene clear**
- Wait until `mTerrainGroup->isDerivedDataUpdateInProgress()` is `false`.
- Disable paging operations (`PageManager::setPagingOperationsEnabled(false)`)
so no new page loads/unloads start while tearing down.
- Remove all Jolt bodies first.
- Unload and destroy pages.
- Destroy in this order: `TerrainPaging``PageManager``TerrainGroup`
`TerrainGlobalOptions`. (`TerrainPaging` owns a reference to `PageManager`,
so it must go first.)
- Release RTShader `SubRenderState` objects held by the terrain material
generator and erase per-pass `TargetRenderState` user-object bindings
before RTSS teardown.
- Destroy `TerrainPaging``PageManager`. `PageManager::~PageManager` does
**not** destroy the `PagedWorld`s it owns, so the `TerrainPagedWorldSection`
and its `TerrainGroup` are intentionally leaked. This avoids GL driver
hangs/crashes observed when destroying the group during live scene reloads.
- Destroy `TerrainGlobalOptions` and drop the `mTerrainGroup` pointer.
4. **Destruction** (`EditorApp::~EditorApp()`)
- Destroy `TerrainSystem` **before** `PhysicsSystem` and the
@@ -224,29 +229,41 @@ if (m_terrainSystem) {
Inside `update()`:
1. `mTerrainGroup->update(false)` (only if no derived data update is in
progress).
2. Process pending collider creation queue: for each loaded page with no active
collider, build a `ZigzagHeightfieldShape` and add the static Jolt body.
3. Process pending collider removals: only remove bodies when the corresponding
page is unloaded and no derived data update is running.
1. `rebuildDirtyPages()` and `processDeferredReloads()` handle editor edits.
2. `ensurePageColliders()` scans every loaded `TerrainSlot` and queues collider
creation for pages that do not have one yet.
3. `processColliderCreates()` drains the queue. It skips pages whose terrain
instance is not yet loaded and re-queues them; it does **not** block on
`isDerivedDataUpdateInProgress()` because the heightfield data is stable once
`isLoaded()` is true.
4. `processColliderRemoves()` destroys bodies marked by `DummyPageProvider`.
### 2.3 Safety rules for background jobs
`Ogre::Terrain` performs `prepare()`, LOD streaming, and derived-data updates on
Ogre's `WorkQueue`. The following rules prevent crashes, leaks, and use-after-free:
- Never remove or hide a `Terrain` page while
`terrain->isDerivedDataUpdateInProgress()` is `true`.
- Never remove the `TerrainComponent` entity while
`mTerrainGroup->isDerivedDataUpdateInProgress()` is `true`; defer removal to
the next frame.
- Always pump `mTerrainGroup->update(false)` each frame so pending background
work can finish.
- The `TerrainGroup` destructor already waits for pending `prepare()` requests,
but explicit draining avoids surprises.
- Do not create a Jolt heightfield body for a page until `terrain->isLoaded()`
is `true` and derived data is idle.
- **Do not drive paging from a worker thread in the editor.** In editor mode
the camera is intentionally **not** added to `PageManager`; pages are loaded
synchronously through `TerrainGroup::loadAllTerrains(true)`. This prevents
`CustomTerrainDefiner::define()` from being called on a background thread and
touching GL/ECS state unsafely.
- **Never call `defineTerrain()` on a slot that already has a live instance from
a background thread.** If paging is active at runtime, only unload/reload
pages through the paging system so the instance is freed before `define()` is
called again.
- **Keep editing and paging reload separate.** Sculpting writes the heightmap
and marks pages dirty; `rebuildDirtyPages()` updates the live terrain geometry
directly. The paging system may call `CustomTerrainDefiner` later for a true
reload, but the two paths do not interleave during a frame.
- `DummyPageProvider::unloadProceduralPage` must only mark colliders for removal;
it must not synchronously destroy Jolt bodies or touch ECS state.
- Colliders can be built as soon as `terrain->isLoaded()` is true; the heightmap
data is stable even while derived normal/composite maps are still being
generated.
- The `TerrainGroup` destructor can hang or crash on some GL drivers, so the
active group is intentionally leaked at shutdown. `PageManager::~PageManager`
does not destroy worlds, which makes this leak safe.
### 2.4 Height function
@@ -1142,8 +1159,10 @@ target_link_libraries(editSceneEditor PUBLIC
- CMake (`OgrePaging`/`OgreTerrain` links) and `EditorApp` wiring.
- Component registration in `setupECS()` + editor module.
Definition of done: a paged terrain renders in the editor; terrain pages load
and unload as the camera moves.
Definition of done: a paged terrain renders in the editor. In the current
editor mode the camera is not attached to `PageManager`, so all pages in the
configured range are loaded synchronously; the paging objects are present and
ready for runtime game mode where a camera can be attached.
### Milestone 2 — Physics integration ✅ DONE
@@ -1157,10 +1176,10 @@ TerrainSystem(flecs::world &world, Ogre::SceneManager *sceneMgr,
**EditorApp wiring change**: Update `setup()` to pass `m_physicsSystem->getPhysicsWrapper()`.
**Collider creation queue**: After `CustomTerrainDefiner::define()` queues a
page, `processColliderCreates()` drains the queue in `update()` when
`isDerivedDataUpdateInProgress()` is false. Each page gets a zigzag
`JPH::MeshShape` as a static body in `Layers::NON_MOVING`.
**Collider creation queue**: `ensurePageColliders()` scans every loaded
`TerrainSlot` each frame and queues missing pages. `processColliderCreates()`
drains the queue in `update()` when the terrain instance is loaded. Each page
gets a zigzag `JPH::MeshShape` as a static body in `Layers::NON_MOVING`.
**mColliders map**: `std::unordered_map<uint64_t, TerrainCollider>` keyed by
`packIndex(x, y)`. `TerrainCollider` holds `{pageX, pageY, JPH::BodyID,
@@ -1168,7 +1187,7 @@ pendingRemove}`.
**Collider removal queue**: `DummyPageProvider::unloadProceduralPage` marks the
collider `pendingRemove`. `processColliderRemoves()` destroys bodies in
`update()` after derived data is idle.
`update()` when the corresponding terrain page is no longer busy.
**Zigzag triangulation**: Build `JPH::MeshShape` from `terrain->getHeightData()`
using the zigzag quad split matching Ogre's odd/even row parity. Uses
@@ -1206,8 +1225,11 @@ JPH::EActivation::DontActivate)`.
against all loaded terrain page bodies.
**Testing**:
- [x] Drop a rigid body on terrain → rests flush.
- [x] Toggle "Show Terrain Colliders" → wireframe matches terrain.
- [x] `editSceneEditor test_terrain.json --exit-after-first-frame` exits cleanly
(EXIT:0) with paging objects created and RTSS teardown successful.
- [x] Running for several seconds creates colliders for all loaded pages.
- [ ] Drop a rigid body on terrain → rests flush.
- [ ] Toggle "Show Terrain Colliders" → wireframe matches terrain.
- [ ] Fly away and back → colliders for unloaded pages removed, new pages get colliders.
- [ ] Remove terrain entity → all collider bodies removed, no dangling Jolt bodies.
- [ ] Add/remove terrain repeated → stable memory (valgrind/ASan).
@@ -1217,30 +1239,102 @@ JPH::EActivation::DontActivate)`.
Definition of done: ✅ rigid bodies collide flush with visible terrain; debug
draw off by default with toggle; removing and re-adding terrain is leak-free.
**Terrain cleanup on entity/component removal** (post-M2 fix):
`TerrainSystem::update()` tracks whether any matching terrain entity exists.
If `m_active` but no entity carries `TerrainComponent`, `deactivate()` is
called. Without this, deleting the entity or removing the component leaves
terrain and colliders orphaned.
### Milestone 3 — Serialization + heightmap editing + splat painting
- Scene serialization for `TerrainComponent` (save/load JSON round-trip).
**Terrain cleanup on entity/component removal** (added post-M2):
`TerrainSystem::update()` now tracks whether any matching terrain entity exists.
If `m_active` is true but the query returns no entity (because the terrain
entity was deleted or its TerrainComponent was removed), `deactivate()` is
called automatically. Without this, removing the component or deleting the
entity would leave the terrain and colliders orphaned in the scene.
- Binary heightmap file save/load alongside scene JSON.
- Brush tools (raise/lower/smooth/flatten) with curve-based brush profile.
- Splat paint brush (select layer, set opacity, paint blend values via
`TerrainLayerBlendMap`).
- Sparse 256x256 fixup chunk cache and file I/O (lazy creation, sentinel-based
override semantics).
- Mark affected pages dirty and rebuild them via CustomTerrainDefiner
re-sampling.
- Heightmap editor UI (brush radius, strength, type selector).
- `AuxMap` infrastructure (create, load, save, edit with brushes).
- Fixup chunk cleanup (clear all, delete individual chunks by world position).
**Scene serialization**:
- `SceneSerializer::serializeEntity()` / `deserializeEntity()` support for
`TerrainComponent` (save/load JSON round-trip with all parameters, layers,
road config, aux maps).
- Binary heightmap file save/load alongside scene JSON
(`heightmaps/<terrainId>/`).
Definition of done: user can sculpt the terrain in the editor; save scene,
reload, terrain restored with edited heightmap and layer paint; visual and
collision update after sculpting.
**Heightmap data manager** (`HeightmapData` class inside `TerrainSystem`):
- Loads/stores the `terrainSize × terrainSize` float array from/to binary
files.
- Provides `getHeightAt(worldX, worldZ)` combining base heightmap + fixup
chunks with bilinear sampling.
- `setHeightAt(worldX, worldZ, value)` writes to base heightmap or a fixup
chunk (permanent base edit vs road compliance fixup).
- `CustomTerrainDefiner` calls `HeightmapData::getHeightAt()` instead of
procedural — falls back to procedural if no file loaded yet.
**Fixup chunk cache and I/O**:
- Sparse 256×256 fixup chunks, lazy-created, sentinel-based override.
- `loadFixupChunk()` / `saveFixupChunk()` / `clearAllFixups()`.
- "Clear all fixups" button; individual chunk delete by world-position
right-click.
**3D world point selection and sculpting UX**:
The TerrainEditor panel gains a **Sculpting** section below the terrain
properties, grouping all brush controls together:
```
┌─ Sculpting ─────────────────────────────────────┐
│ [✓] Sculpt Mode │
│ Tool: [ Raise ▾ ] Radius: [====] 5.0 │
│ Strength: [====] 0.5 Curve: [====] │
│ Layer: [ Layer 1 ▾ ] (splat paint only) │
└──────────────────────────────────────────────────┘
```
**Sculpt Mode checkbox**: When checked, left-click on the terrain applies the
selected tool. When unchecked, clicks behave normally (entity selection,
camera). ESC also exits sculpt mode.
**Tool selector**: Combo box — Raise, Lower, Smooth, Flatten, Splat Paint.
Switching the tool immediately changes behavior; no separate "activate" step.
**Painting**: In sculpt mode, holding left mouse on the terrain continuously
applies the current tool at the brush position (determined by raycast against
terrain via `TerrainSystem::raycastTerrain()`). Releasing the mouse stops
painting. The brush follows the cursor — drag to paint a stroke. A
translucent circle decal on the terrain surface shows brush position and
radius.
**Camera interaction while sculpting**:
- Right mouse always controls camera (orbit/pan), even in sculpt mode.
- Middle mouse or Ctrl+Left also moves camera in sculpt mode, so the user
can reposition without toggling the mode off.
**Brush tools**:
- **Raise**: adds height (strength × radius curve) at brush center.
- **Lower**: subtracts height.
- **Smooth**: averages heights within brush radius.
- **Flatten**: pulls heights toward the height under brush center (plateau).
- **Splat Paint**: paints selected layer's blend weight via
`TerrainLayerBlendMap::setBlendValue()` + `dirtyRect()` + `update()`.
Layer selector appears when this tool is active.
**Brush application and dirty page marking**:
- During a stroke, `HeightmapData::setHeightAt()` is called for each affected
sample within the brush radius, weighted by the radius curve.
- At mouse release (end of stroke), determine which terrain pages overlap the
modified area and mark them dirty.
- `CustomTerrainDefiner` re-samples `HeightmapData::getHeightAtWorld()` on the
next `define()` call for dirty pages.
- If a modified page already has a physics collider, queue its removal and
recreation with updated heights.
**Heightmap file I/O and AuxMap controls**:
- "Save Heightmap" / "Load Heightmap" buttons in the TerrainEditor panel.
- AuxMap management: create, rename, delete, select active map for editing.
AuxMap editing uses the same brush tools but writes to the selected aux map
instead of the base heightmap.
- "Clear All Fixups" button — deletes all fixup chunks and marks affected
pages dirty for rebuild.
**Definition of done**: user can sculpt terrain with visual brush feedback;
save scene, reload, terrain restored with edited heightmap and layer paint;
visual and collision update after sculpting; camera can be snapped above
terrain on demand.
### Milestone 4 — Procedural roads
- Node/edge editing UI (add, remove, split, join, select, move).
@@ -1249,8 +1343,8 @@ collision update after sculpting.
`Procedural::TriangleBuffer`.
- End cap generation for endpoints.
- Seam suppression via vertex shifting.
- LOD + visibility distance per road mesh segment.
- Road physics colliders (JPH::MeshShape per segment).
- LOD + visibility distance per road mesh.
- Road physics colliders (JPH::MeshShape per road mesh).
- "Comply terrain to roads" writing fixup chunks with smooth falloff.
Definition of done: user can draw a road network; road geometry appears with
+13 -4
View File
@@ -51,6 +51,18 @@ struct TerrainComponent {
};
std::vector<Layer> layers;
// Road configuration parameters.
struct RoadConfig {
std::string roadMeshTemplate = "road_segment.mesh";
float laneWidth = 3.0f;
int lanesPerDirection = 1;
float roadThickness = 0.3f;
float roadLodDistance = 200.0f;
float roadVisibilityDistance = 1000.0f;
std::string roadMaterialName = "RoadMaterial";
};
RoadConfig roadConfig;
// Road network data (node/edge graph).
struct RoadNode {
Ogre::Vector3 position;
@@ -89,10 +101,7 @@ struct TerrainComponent {
bool dirty = true;
bool rebuildInProgress = false;
void markDirty()
{
dirty = true;
}
void markDirty() { dirty = true; }
};
#endif // EDITSCENE_TERRAIN_HPP
@@ -4,6 +4,7 @@
#include "EntityName.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/TerrainEditor.hpp"
#include <chrono>
REGISTER_COMPONENT_GROUP("Terrain", "Environment", TerrainComponent,
TerrainEditor)
@@ -13,8 +14,14 @@ REGISTER_COMPONENT_GROUP("Terrain", "Environment", TerrainComponent,
std::make_unique<TerrainEditor>(),
/* Adder */
[](flecs::entity e) {
if (!e.has<TerrainComponent>())
e.set<TerrainComponent>({});
if (!e.has<TerrainComponent>()) {
TerrainComponent tc;
tc.terrainId = (uint64_t)std::chrono::
system_clock::now()
.time_since_epoch()
.count();
e.set<TerrainComponent>(tc);
}
},
/* Remover */
[](flecs::entity e) {
+21
View File
@@ -1,6 +1,21 @@
#include <iostream>
#include "EditorApp.hpp"
#include "systems/SceneSerializer.hpp"
#include "OgreRoot.h"
struct ExitAfterFirstFrameListener : public Ogre::FrameListener {
Ogre::Root *root;
bool triggered = false;
ExitAfterFirstFrameListener(Ogre::Root *r) : root(r) {}
bool frameRenderingQueued(const Ogre::FrameEvent &) override
{
if (!triggered) {
triggered = true;
root->queueEndRendering();
}
return true;
}
};
int main(int argc, char *argv[])
{
@@ -10,6 +25,7 @@ int main(int argc, char *argv[])
// Parse command line arguments
bool gameMode = false;
bool debugBuoyancy = false;
bool exitAfterFirstFrame = false;
Ogre::String sceneFile;
for (int i = 1; i < argc; i++) {
Ogre::String arg = argv[i];
@@ -17,6 +33,8 @@ int main(int argc, char *argv[])
gameMode = true;
} else if (arg == "--debug-buoyancy") {
debugBuoyancy = true;
} else if (arg == "--exit-after-first-frame") {
exitAfterFirstFrame = true;
} else if (arg.length() > 0 && arg[0] != '-') {
sceneFile = arg;
}
@@ -46,6 +64,9 @@ int main(int argc, char *argv[])
}
}
ExitAfterFirstFrameListener exitListener(app.getRoot());
if (exitAfterFirstFrame)
app.getRoot()->addFrameListener(&exitListener);
app.getRoot()->startRendering();
app.closeApp();
} catch (const std::exception &e) {
@@ -1727,6 +1727,14 @@ JoltPhysicsWrapper::JoltPhysicsWrapper(Ogre::SceneManager *scnMgr,
JoltPhysicsWrapper::~JoltPhysicsWrapper()
{
// Tear down the global physics state while the application is still
// running. Otherwise the Physics object is destroyed during static
// teardown and may crash because Jolt types are still referenced.
Ogre::LogManager::getSingleton().logMessage(
"JoltPhysicsWrapper: tearing down global physics state...");
phys.reset();
Ogre::LogManager::getSingleton().logMessage(
"JoltPhysicsWrapper: global physics state torn down");
}
void JoltPhysicsWrapper::update(float dt)
+1
View File
@@ -12,6 +12,7 @@ FileSystem=resources/buildings/parts/furniture
FileSystem=resources/vehicles
FileSystem=resources/fonts
FileSystem=resources/debug
FileSystem=resources/terrain
[Popular]
FileSystem=resources/materials/programs
@@ -4,6 +4,7 @@
#include "PrefabSystem.hpp"
#include "ItemRegistry.hpp"
#include "../camera/EditorCamera.hpp"
#include "../systems/TerrainSystem.hpp"
#include "../components/EntityName.hpp"
#include "../components/Transform.hpp"
#include "../components/Renderable.hpp"
@@ -329,6 +330,26 @@ void EditorUISystem::update(float deltaTime)
if (!m_editorUIEnabled) {
// Only render FPS overlay when editor UI is disabled
renderFPSOverlay(deltaTime);
/* Sculpt mode: left mouse on terrain applies brush. */
{
TerrainSystem *ts = TerrainSystem::getInstance();
if (ts && ts->isSculpting()) {
ImGuiIO &io = ImGui::GetIO();
if (ImGui::IsMouseDown(ImGuiMouseButton_Left) &&
!io.WantCaptureMouse &&
m_editorCamera) {
Ogre::Ray ray = m_editorCamera->getMouseRay(
io.MousePos.x, io.MousePos.y);
float t;
Ogre::Vector3 normal;
if (ts->raycastTerrain(ray, t, normal)) {
Ogre::Vector3 hit = ray.getPoint(t);
ts->applySculptBrush(hit);
}
}
}
}
return;
}
@@ -33,6 +33,8 @@
#include "../components/BuoyancyInfo.hpp"
#include "../components/WaterPhysics.hpp"
#include "../components/WaterPlane.hpp"
#include "../components/Terrain.hpp"
#include "TerrainSystem.hpp"
#include "../components/Sun.hpp"
#include "../components/Skybox.hpp"
#include "../components/EventHandler.hpp"
@@ -320,6 +322,10 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity)
json["waterPlane"] = serializeWaterPlane(entity);
}
if (entity.has<TerrainComponent>()) {
json["terrain"] = serializeTerrain(entity);
}
// ActionDatabase is now a singleton, serialized at scene level
if (entity.has<ActionDebug>()) {
json["actionDebug"] = serializeActionDebug(entity);
@@ -552,6 +558,7 @@ void SceneSerializer::deserializeEntity(const nlohmann::json &json,
if (json.contains("waterPlane")) {
deserializeWaterPlane(entity, json["waterPlane"]);
}
if (json.contains("terrain")) deserializeTerrain(entity, json["terrain"]);
// ActionDatabase is now a singleton, deserialized at scene level
if (json.contains("actionDebug")) {
@@ -858,6 +865,7 @@ void SceneSerializer::deserializeEntityComponents(
if (json.contains("waterPlane")) {
deserializeWaterPlane(entity, json["waterPlane"]);
}
if (json.contains("terrain")) deserializeTerrain(entity, json["terrain"]);
// ActionDatabase is now a singleton, deserialized at scene level
if (json.contains("actionDebug")) {
@@ -4129,3 +4137,199 @@ void SceneSerializer::deserializeInventory(flecs::entity entity,
}
}
}
/* ------------------------------------------------------------------ */
/* TerrainComponent serialization */
/* ------------------------------------------------------------------ */
nlohmann::json SceneSerializer::serializeTerrain(flecs::entity entity)
{
const TerrainComponent &tc = entity.get<TerrainComponent>();
nlohmann::json json;
json["enabled"] = tc.enabled;
json["terrainSize"] = tc.terrainSize;
json["terrainId"] = tc.terrainId;
json["heightmapSize"] = tc.heightmapSize;
json["worldSize"] = tc.worldSize;
json["maxPixelError"] = tc.maxPixelError;
json["compositeMapDistance"] = tc.compositeMapDistance;
json["minBatchSize"] = tc.minBatchSize;
json["maxBatchSize"] = tc.maxBatchSize;
json["heightmapFile"] = tc.heightmapFile;
nlohmann::json layersJson = nlohmann::json::array();
for (auto &l : tc.layers) {
nlohmann::json lj;
lj["diffuseTexture"] = l.diffuseTexture;
lj["normalTexture"] = l.normalTexture;
lj["worldSize"] = l.worldSize;
layersJson.push_back(lj);
}
json["layers"] = layersJson;
nlohmann::json auxMapsJson = nlohmann::json::array();
for (auto &am : tc.auxMaps) {
nlohmann::json aj;
aj["name"] = am.name;
aj["fileName"] = am.fileName;
aj["resolution"] = am.resolution;
aj["defaultValue"] = am.defaultValue;
auxMapsJson.push_back(aj);
}
json["auxMaps"] = auxMapsJson;
nlohmann::json roadConfigJson;
roadConfigJson["roadMeshTemplate"] = tc.roadConfig.roadMeshTemplate;
roadConfigJson["laneWidth"] = tc.roadConfig.laneWidth;
roadConfigJson["lanesPerDirection"] = tc.roadConfig.lanesPerDirection;
roadConfigJson["roadThickness"] = tc.roadConfig.roadThickness;
roadConfigJson["roadLodDistance"] = tc.roadConfig.roadLodDistance;
roadConfigJson["roadVisibilityDistance"] =
tc.roadConfig.roadVisibilityDistance;
roadConfigJson["roadMaterialName"] = tc.roadConfig.roadMaterialName;
json["roadConfig"] = roadConfigJson;
nlohmann::json roadNodesJson = nlohmann::json::array();
for (auto &n : tc.roadNodes) {
nlohmann::json nj;
nj["id"] = n.id;
nj["position"] = { n.position.x, n.position.y, n.position.z };
nj["verticalOffset"] = n.verticalOffset;
roadNodesJson.push_back(nj);
}
json["roadNodes"] = roadNodesJson;
nlohmann::json roadEdgesJson = nlohmann::json::array();
for (auto &e : tc.roadEdges) {
nlohmann::json ej;
ej["nodeA"] = e.nodeA;
ej["nodeB"] = e.nodeB;
ej["roadLevelA"] = e.roadLevelA;
ej["roadLevelB"] = e.roadLevelB;
ej["lanesPerDirectionOverride"] = e.lanesPerDirectionOverride;
ej["lanesAtoB"] = e.lanesAtoB;
ej["lanesBtoA"] = e.lanesBtoA;
nlohmann::json sidePrefabsJson = nlohmann::json::array();
for (auto &sp : e.sidePrefabs) {
nlohmann::json spj;
spj["prefabPath"] = sp.prefabPath;
spj["edgeT"] = sp.edgeT;
spj["sideOffset"] = sp.sideOffset;
spj["leftSide"] = sp.leftSide;
sidePrefabsJson.push_back(spj);
}
ej["sidePrefabs"] = sidePrefabsJson;
roadEdgesJson.push_back(ej);
}
json["roadEdges"] = roadEdgesJson;
/* Persist binary heightmap alongside the JSON. */
TerrainSystem *ts = TerrainSystem::getInstance();
if (ts)
ts->saveSceneHeightmap(tc);
return json;
}
void SceneSerializer::deserializeTerrain(flecs::entity entity,
const nlohmann::json &json)
{
TerrainComponent tc;
tc.enabled = json.value("enabled", true);
tc.terrainSize = json.value("terrainSize", 65);
tc.terrainId = json.value("terrainId", (uint64_t)0);
tc.heightmapSize = json.value("heightmapSize", 256);
tc.worldSize = json.value("worldSize", 2000.0f);
tc.maxPixelError = json.value("maxPixelError", 1.0f);
tc.compositeMapDistance = json.value("compositeMapDistance", 300.0f);
tc.minBatchSize = json.value("minBatchSize", 17);
tc.maxBatchSize = json.value("maxBatchSize", 65);
tc.heightmapFile = json.value("heightmapFile", "heightmap.bin");
if (json.contains("layers") && json["layers"].is_array()) {
for (auto &lj : json["layers"]) {
TerrainComponent::Layer layer;
layer.diffuseTexture = lj.value("diffuseTexture", "");
layer.normalTexture = lj.value("normalTexture", "");
layer.worldSize = lj.value("worldSize", 100.0f);
tc.layers.push_back(layer);
}
}
if (json.contains("auxMaps") && json["auxMaps"].is_array()) {
for (auto &aj : json["auxMaps"]) {
TerrainComponent::AuxMap am;
am.name = aj.value("name", "");
am.fileName = aj.value("fileName", "");
am.resolution = aj.value("resolution", 256);
am.defaultValue = aj.value("defaultValue", 0.0f);
tc.auxMaps.push_back(am);
}
}
if (json.contains("roadConfig")) {
auto &rcj = json["roadConfig"];
tc.roadConfig.roadMeshTemplate =
rcj.value("roadMeshTemplate", "road_segment.mesh");
tc.roadConfig.laneWidth = rcj.value("laneWidth", 3.0f);
tc.roadConfig.lanesPerDirection =
rcj.value("lanesPerDirection", 1);
tc.roadConfig.roadThickness = rcj.value("roadThickness", 0.3f);
tc.roadConfig.roadLodDistance = rcj.value("roadLodDistance", 200.0f);
tc.roadConfig.roadVisibilityDistance =
rcj.value("roadVisibilityDistance", 1000.0f);
tc.roadConfig.roadMaterialName =
rcj.value("roadMaterialName", "RoadMaterial");
}
if (json.contains("roadNodes") && json["roadNodes"].is_array()) {
for (auto &nj : json["roadNodes"]) {
TerrainComponent::RoadNode node;
node.id = nj.value("id", 0);
node.verticalOffset = nj.value("verticalOffset", 0.0f);
if (nj.contains("position") && nj["position"].is_array() &&
nj["position"].size() >= 3) {
node.position.x = nj["position"][0];
node.position.y = nj["position"][1];
node.position.z = nj["position"][2];
}
tc.roadNodes.push_back(node);
}
}
if (json.contains("roadEdges") && json["roadEdges"].is_array()) {
for (auto &ej : json["roadEdges"]) {
TerrainComponent::RoadEdge edge;
edge.nodeA = ej.value("nodeA", -1);
edge.nodeB = ej.value("nodeB", -1);
edge.roadLevelA = ej.value("roadLevelA", 0.0f);
edge.roadLevelB = ej.value("roadLevelB", 0.0f);
edge.lanesPerDirectionOverride =
ej.value("lanesPerDirectionOverride", 0);
edge.lanesAtoB = ej.value("lanesAtoB", 0);
edge.lanesBtoA = ej.value("lanesBtoA", 0);
if (ej.contains("sidePrefabs") &&
ej["sidePrefabs"].is_array()) {
for (auto &spj : ej["sidePrefabs"]) {
TerrainComponent::RoadEdge::RoadSidePrefab sp;
sp.prefabPath = spj.value("prefabPath", "");
sp.edgeT = spj.value("edgeT", 0.5f);
sp.sideOffset = spj.value("sideOffset", 5.0f);
sp.leftSide = spj.value("leftSide", true);
edge.sidePrefabs.push_back(sp);
}
}
tc.roadEdges.push_back(edge);
}
}
entity.set<TerrainComponent>(tc);
TerrainSystem *ts = TerrainSystem::getInstance();
if (ts)
ts->loadSceneHeightmap(tc);
}
@@ -206,6 +206,8 @@ private:
// WaterPlane serialization
nlohmann::json serializeWaterPlane(flecs::entity entity);
nlohmann::json serializeTerrain(flecs::entity entity);
void deserializeTerrain(flecs::entity entity, const nlohmann::json &json);
void deserializeWaterPlane(flecs::entity entity,
const nlohmann::json &json);
+614 -93
View File
@@ -7,9 +7,16 @@
#include <OgreTerrainGroup.h>
#include <OgreTerrainPaging.h>
#include <OgreTerrainPagedWorldSection.h>
#include <OgrePageManager.h>
#include <OgrePage.h>
#include <OgrePagedWorld.h>
#include <OgreTerrainMaterialGeneratorA.h>
#include <OgreMaterial.h>
#include <OgreLogManager.h>
#include <OgreShaderRenderState.h>
#include <OgreShaderSubRenderState.h>
#include <Jolt/Jolt.h>
#include <Jolt/Physics/Collision/Shape/MeshShape.h>
#include <Jolt/Physics/Body/BodyCreationSettings.h>
@@ -18,6 +25,8 @@
#include <Jolt/Physics/PhysicsSystem.h>
#include <cmath>
#include <fstream>
#include <sys/stat.h>
TerrainSystem *TerrainSystem::s_instance = nullptr;
@@ -40,68 +49,69 @@ TerrainSystem::~TerrainSystem()
deactivate();
}
/* ------------------------------------------------------------------ */
/* TerrainBodyDrawFilter */
/* ------------------------------------------------------------------ */
bool TerrainSystem::TerrainBodyDrawFilter::ShouldDraw(
const JPH::Body &inBody) const
{
if (!showTerrain)
return m_terrainIds.find(inBody.GetID()) == m_terrainIds.end();
return m_terrainIds.find(inBody.GetID()) ==
m_terrainIds.end();
return true;
}
/* ------------------------------------------------------------------ */
bool TerrainSystem::DummyPageProvider::unloadProceduralPage(
Ogre::Page *page, Ogre::PagedWorldSection *)
{
if (owner && owner->mTerrainGroup) {
long x, y;
owner->mTerrainGroup->unpackIndex(page->CHUNK_ID, &x, &y);
Ogre::LogManager::getSingleton().logMessage(
"Terrain: unloaded page " +
Ogre::StringConverter::toString(x) + ", " +
Ogre::StringConverter::toString(y));
owner->queueColliderRemove(x, y);
}
return true;
}
/* Procedural height helper */
/* ------------------------------------------------------------------ */
static float proceduralHeight(long worldX, long worldZ)
{
float h = 0.0f;
h += 15.0f * sinf((float)worldX * 0.005f) *
cosf((float)worldZ * 0.005f);
h += 5.0f * sinf((float)worldX * 0.02f + 1.3f) *
cosf((float)worldZ * 0.02f + 0.7f);
h += 2.0f * sinf((float)worldX * 0.05f + 2.1f) *
sinf((float)worldZ * 0.05f + 0.3f);
return h;
}
/* ------------------------------------------------------------------ */
/* DummyPageProvider */
/* ------------------------------------------------------------------ */
bool TerrainSystem::DummyPageProvider::unloadProceduralPage(
Ogre::Page *page, Ogre::PagedWorldSection *)
{
if (!owner || !owner->mTerrainGroup)
return true;
long x, y;
owner->mTerrainGroup->unpackIndex(page->getID(), &x, &y);
Ogre::LogManager::getSingleton().logMessage(
"Terrain: unloading page (" +
Ogre::StringConverter::toString(x) + ", " +
Ogre::StringConverter::toString(y) + ")");
owner->queueColliderRemove(x, y);
return true;
}
/* ------------------------------------------------------------------ */
/* CustomTerrainDefiner */
/* ------------------------------------------------------------------ */
void TerrainSystem::CustomTerrainDefiner::define(Ogre::TerrainGroup *group,
long x, long y)
{
Ogre::uint16 terrainSize = group->getTerrainSize();
Ogre::Real worldSize = group->getTerrainWorldSize();
float *heightMap = OGRE_ALLOC_T(float, terrainSize * terrainSize,
Ogre::MEMCATEGORY_GEOMETRY);
Ogre::Vector3 worldPos;
group->convertTerrainSlotToWorldPosition(x, y, &worldPos);
Ogre::Real step = worldSize / Ogre::Real(terrainSize - 1);
for (int j = 0; j < terrainSize; ++j) {
for (int i = 0; i < terrainSize; ++i) {
long wx = (long)(worldPos.x + (Ogre::Real)i * step);
long wz = (long)(worldPos.z + (Ogre::Real)j * step);
heightMap[j * terrainSize + i] =
proceduralHeight(wx, wz);
}
}
m_sys->fillPageHeightData(group, x, y, heightMap);
group->defineTerrain(x, y, heightMap);
Ogre::LogManager::getSingleton().logMessage(
@@ -110,10 +120,10 @@ void TerrainSystem::CustomTerrainDefiner::define(Ogre::TerrainGroup *group,
Ogre::StringConverter::toString(y) + ")");
OGRE_FREE(heightMap, Ogre::MEMCATEGORY_GEOMETRY);
m_sys->queueColliderCreate(x, y);
}
/* ------------------------------------------------------------------ */
/* buildPageCollider — zigzag JPH::MeshShape */
/* ------------------------------------------------------------------ */
JPH::ShapeRefC TerrainSystem::buildPageCollider(Ogre::Terrain *terrain)
@@ -130,8 +140,10 @@ JPH::ShapeRefC TerrainSystem::buildPageCollider(Ogre::Terrain *terrain)
for (int x = 0; x < size - 1; ++x) {
float vx0 = (float)x * scale - worldSize * 0.5f;
float vz0 = worldSize * 0.5f - (float)z * scale;
float vx1 = (float)(x + 1) * scale - worldSize * 0.5f;
float vz1 = worldSize * 0.5f - (float)(z + 1) * scale;
float vx1 = (float)(x + 1) * scale -
worldSize * 0.5f;
float vz1 = worldSize * 0.5f -
(float)(z + 1) * scale;
float h00 = heightData[z * size + x];
float h10 = heightData[z * size + x + 1];
@@ -144,13 +156,11 @@ JPH::ShapeRefC TerrainSystem::buildPageCollider(Ogre::Terrain *terrain)
JPH::Float3 v11(vx1, h11, vz1);
if ((z & 1) == 0) {
/* Even row: \ diag, Z flipped vs Ogre. */
triangles.push_back(
JPH::Triangle(v00, v11, v01, 0));
triangles.push_back(
JPH::Triangle(v00, v10, v11, 0));
} else {
/* Odd row: / diag, Z flipped vs Ogre. */
triangles.push_back(
JPH::Triangle(v00, v10, v01, 0));
triangles.push_back(
@@ -170,10 +180,22 @@ JPH::ShapeRefC TerrainSystem::buildPageCollider(Ogre::Terrain *terrain)
return result.Get();
}
/* ------------------------------------------------------------------ */
/* Collider queue helpers */
/* ------------------------------------------------------------------ */
void TerrainSystem::queueColliderCreate(long x, long y)
{
if (!mTerrainGroup)
return;
uint64_t key = mTerrainGroup->packIndex(x, y);
if (mColliders.find(key) != mColliders.end())
return;
if (!mPendingColliderPages.insert(key).second)
return;
std::lock_guard<std::mutex> lock(m_colliderQueueMutex);
mColliderCreateQueue.push_back({ x, y });
}
@@ -190,43 +212,59 @@ void TerrainSystem::processColliderCreates()
if (!m_physics || !mTerrainGroup)
return;
while (!mColliderCreateQueue.empty()) {
std::lock_guard<std::mutex> lock(m_colliderQueueMutex);
const size_t initial = mColliderCreateQueue.size();
for (size_t i = 0; i < initial && !mColliderCreateQueue.empty();
++i) {
auto [x, y] = mColliderCreateQueue.front();
if (mTerrainGroup->isDerivedDataUpdateInProgress())
break;
mColliderCreateQueue.pop_front();
uint64_t key = mTerrainGroup->packIndex(x, y);
if (mColliders.find(key) != mColliders.end())
if (mColliders.find(key) != mColliders.end()) {
mPendingColliderPages.erase(key);
continue;
}
Ogre::Terrain *terrain = mTerrainGroup->getTerrain(x, y);
if (!terrain || !terrain->isLoaded())
if (!terrain || !terrain->isLoaded()) {
mColliderCreateQueue.push_back({ x, y });
continue;
}
JPH::ShapeRefC shape = buildPageCollider(terrain);
if (!shape)
if (!shape) {
Ogre::LogManager::getSingleton().logMessage(
"Terrain: collider create - shape build failed (" +
Ogre::StringConverter::toString(x) + "," +
Ogre::StringConverter::toString(y) + ")");
continue;
}
/* Body at terrain scene node position. */
Ogre::Vector3 bodyPos =
terrain->_getRootSceneNode()->_getDerivedPosition();
Ogre::Vector3 pagePos =
terrain->_getRootSceneNode()
->_getDerivedPosition();
JPH::BodyCreationSettings bodySettings(
shape.GetPtr(),
JPH::RVec3(bodyPos.x, bodyPos.y, bodyPos.z),
JPH::RVec3(pagePos.x, pagePos.y, pagePos.z),
JPH::Quat::sIdentity(),
JPH::EMotionType::Static,
Layers::NON_MOVING);
JPH::BodyID bodyId = m_physics->createBody(bodySettings);
if (bodyId.IsInvalid())
if (bodyId.IsInvalid()) {
Ogre::LogManager::getSingleton().logMessage(
"Terrain: collider create - body creation failed (" +
Ogre::StringConverter::toString(x) + "," +
Ogre::StringConverter::toString(y) + ")");
continue;
}
m_physics->addBody(bodyId, JPH::EActivation::DontActivate);
m_physics->addBody(bodyId,
JPH::EActivation::DontActivate);
mColliders[key] = { x, y, bodyId, false };
mPendingColliderPages.erase(key);
mBodyDrawFilter.addTerrainBody(bodyId);
Ogre::LogManager::getSingleton().logMessage(
@@ -250,9 +288,6 @@ void TerrainSystem::processColliderRemoves()
long x = pair.second.pageX;
long y = pair.second.pageY;
if (mTerrainGroup->isDerivedDataUpdateInProgress())
break;
Ogre::Terrain *terrain = mTerrainGroup->getTerrain(x, y);
if (terrain && terrain->isLoaded() &&
terrain->isDerivedDataUpdateInProgress())
@@ -268,6 +303,7 @@ void TerrainSystem::processColliderRemoves()
Ogre::StringConverter::toString(x) + ", " +
Ogre::StringConverter::toString(y) + ")");
}
for (auto key : toRemove)
mColliders.erase(key);
}
@@ -276,16 +312,304 @@ void TerrainSystem::removeAllColliders()
{
if (!m_physics)
return;
for (auto &pair : mColliders) {
mBodyDrawFilter.removeTerrainBody(pair.second.bodyId);
m_physics->removeBody(pair.second.bodyId);
m_physics->destroyBody(pair.second.bodyId);
}
mColliders.clear();
mPendingColliderPages.clear();
std::lock_guard<std::mutex> lock(m_colliderQueueMutex);
mColliderCreateQueue.clear();
mBodyDrawFilter.clear();
}
/* ------------------------------------------------------------------ */
/* Page collider polling */
/* ------------------------------------------------------------------ */
void TerrainSystem::ensurePageColliders()
{
if (!m_physics || !mTerrainGroup)
return;
for (const auto &kv : mTerrainGroup->getTerrainSlots()) {
Ogre::TerrainGroup::TerrainSlot *slot = kv.second;
if (!slot || !slot->instance || !slot->instance->isLoaded())
continue;
uint64_t key = mTerrainGroup->packIndex(slot->x, slot->y);
if (mColliders.find(key) != mColliders.end())
continue;
queueColliderCreate(slot->x, slot->y);
}
}
/* ------------------------------------------------------------------ */
/* Heightmap helpers */
/* ------------------------------------------------------------------ */
std::string TerrainSystem::getHeightmapPath(const TerrainComponent &tc) const
{
return "heightmaps/" + Ogre::StringConverter::toString(tc.terrainId) +
"/" + tc.heightmapFile;
}
bool TerrainSystem::loadHeightmap(const std::string &path)
{
std::ifstream file(path, std::ios::binary);
if (!file) {
Ogre::LogManager::getSingleton().logMessage(
"Terrain: failed to open heightmap: " + path);
return false;
}
uint32_t res = 0;
file.read(reinterpret_cast<char *>(&res), sizeof(res));
if (res < 2 || res > 8192) {
Ogre::LogManager::getSingleton().logMessage(
"Terrain: invalid heightmap resolution: " +
Ogre::StringConverter::toString(res));
return false;
}
m_heightmapRes = (int)res;
m_heightData.resize(res * res);
file.read(reinterpret_cast<char *>(m_heightData.data()),
res * res * sizeof(float));
m_heightmapLoaded = true;
Ogre::LogManager::getSingleton().logMessage(
"Terrain: loaded heightmap " + path + " (" +
Ogre::StringConverter::toString(res) + "x" +
Ogre::StringConverter::toString(res) + ")");
return true;
}
bool TerrainSystem::saveHeightmap(const std::string &path)
{
if (!m_heightmapLoaded)
return false;
size_t lastSlash = path.rfind('/');
if (lastSlash != std::string::npos) {
std::string dir = path.substr(0, lastSlash);
mkdir(dir.c_str(), 0755);
}
std::ofstream file(path, std::ios::binary);
if (!file) {
Ogre::LogManager::getSingleton().logMessage(
"Terrain: failed to create heightmap file: " +
path);
return false;
}
uint32_t res = (uint32_t)m_heightmapRes;
file.write(reinterpret_cast<const char *>(&res), sizeof(res));
file.write(reinterpret_cast<const char *>(m_heightData.data()),
m_heightmapRes * m_heightmapRes * sizeof(float));
Ogre::LogManager::getSingleton().logMessage(
"Terrain: saved heightmap " + path + " (" +
Ogre::StringConverter::toString(res) + "x" +
Ogre::StringConverter::toString(res) + ")");
return true;
}
bool TerrainSystem::loadSceneHeightmap(const TerrainComponent &tc)
{
return loadHeightmap(getHeightmapPath(tc));
}
bool TerrainSystem::saveSceneHeightmap(const TerrainComponent &tc)
{
return saveHeightmap(getHeightmapPath(tc));
}
void TerrainSystem::ensureHeightmapLoaded(TerrainComponent &tc)
{
if (m_heightmapLoaded)
return;
if (loadSceneHeightmap(tc))
return;
m_heightmapRes = tc.heightmapSize;
m_heightData.resize(m_heightmapRes * m_heightmapRes);
for (int z = 0; z < m_heightmapRes; ++z) {
for (int x = 0; x < m_heightmapRes; ++x) {
const float fx = (float)x / (float)(m_heightmapRes - 1);
const float fz = (float)z / (float)(m_heightmapRes - 1);
const float wx = m_heightmapWorldMinX +
fx * m_heightmapWorldSize;
const float wz = m_heightmapWorldMinZ +
fz * m_heightmapWorldSize;
m_heightData[z * m_heightmapRes + x] =
proceduralHeight((long)wx, (long)wz);
}
}
m_heightmapLoaded = true;
}
void TerrainSystem::fillPageHeightData(Ogre::TerrainGroup *group, long x,
long y, float *heightMap)
{
const Ogre::uint16 terrainSize = group->getTerrainSize();
const Ogre::Real worldSize = group->getTerrainWorldSize();
Ogre::Vector3 worldPos;
group->convertTerrainSlotToWorldPosition(x, y, &worldPos);
const Ogre::Real step = worldSize / Ogre::Real(terrainSize - 1);
for (int j = 0; j < terrainSize; ++j) {
for (int i = 0; i < terrainSize; ++i) {
const long wx = (long)(worldPos.x +
(Ogre::Real)i * step);
const long wz = (long)(worldPos.z +
(Ogre::Real)j * step);
heightMap[j * terrainSize + i] =
sampleHeightAt(wx, wz);
}
}
}
float TerrainSystem::sampleHeightAt(long worldX, long worldZ) const
{
std::lock_guard<std::mutex> lock(m_heightmapMutex);
return sampleHeightAtLocked(worldX, worldZ);
}
float TerrainSystem::sampleHeightAtLocked(long worldX, long worldZ) const
{
if (!m_heightmapLoaded || m_heightData.empty())
return proceduralHeight(worldX, worldZ);
float fx = (float)worldX / m_heightmapWorldSize *
(float)m_heightmapRes;
float fz = (float)worldZ / m_heightmapWorldSize *
(float)m_heightmapRes;
int x0 = (int)floorf(fx);
int z0 = (int)floorf(fz);
int x1 = x0 + 1;
int z1 = z0 + 1;
int r = m_heightmapRes;
x0 = std::max(0, std::min(x0, r - 1));
x1 = std::max(0, std::min(x1, r - 1));
z0 = std::max(0, std::min(z0, r - 1));
z1 = std::max(0, std::min(z1, r - 1));
float tx = fx - (float)x0;
float tz = fz - (float)z0;
float h00 = m_heightData[z0 * r + x0];
float h10 = m_heightData[z0 * r + x1];
float h01 = m_heightData[z1 * r + x0];
float h11 = m_heightData[z1 * r + x1];
return (1.0f - tx) * (1.0f - tz) * h00 +
tx * (1.0f - tz) * h10 +
(1.0f - tx) * tz * h01 +
tx * tz * h11;
}
void TerrainSystem::setHeightAt(long worldX, long worldZ, float value)
{
std::lock_guard<std::mutex> lock(m_heightmapMutex);
if (!m_heightmapLoaded)
return;
int x = (int)((float)worldX / m_heightmapWorldSize *
(float)m_heightmapRes);
int z = (int)((float)worldZ / m_heightmapWorldSize *
(float)m_heightmapRes);
if (x < 0 || x >= m_heightmapRes || z < 0 || z >= m_heightmapRes)
return;
m_heightData[z * m_heightmapRes + x] = value;
}
long TerrainSystem::worldToPage(float worldCoord, float worldSize) const
{
return (long)floorf(worldCoord / worldSize);
}
void TerrainSystem::markPageDirty(long pageX, long pageY)
{
if (mTerrainGroup)
m_dirtyPages.insert(mTerrainGroup->packIndex(pageX, pageY));
}
void TerrainSystem::updatePageGeometry(long pageX, long pageY)
{
if (!mTerrainGroup)
return;
Ogre::Terrain *terrain = mTerrainGroup->getTerrain(pageX, pageY);
if (!terrain || !terrain->isLoaded())
return;
uint16_t size = terrain->getSize();
float *temp = OGRE_ALLOC_T(float, size * size,
Ogre::MEMCATEGORY_GEOMETRY);
fillPageHeightData(mTerrainGroup, pageX, pageY, temp);
float *dest = terrain->getHeightData();
for (int i = 0; i < size * size; ++i)
dest[i] = temp[i];
OGRE_FREE(temp, Ogre::MEMCATEGORY_GEOMETRY);
terrain->dirty();
terrain->update(true);
}
void TerrainSystem::rebuildDirtyPages()
{
if (!mTerrainGroup || m_dirtyPages.empty())
return;
Ogre::LogManager::getSingleton().logMessage(
"Terrain: rebuilding " +
Ogre::StringConverter::toString((int)m_dirtyPages.size()) +
" dirty pages");
for (uint64_t key : m_dirtyPages) {
long x, y;
mTerrainGroup->unpackIndex(key, &x, &y);
auto it = mColliders.find(key);
if (it != mColliders.end() && !it->second.bodyId.IsInvalid()) {
mBodyDrawFilter.removeTerrainBody(it->second.bodyId);
m_physics->removeBody(it->second.bodyId);
m_physics->destroyBody(it->second.bodyId);
mColliders.erase(it);
}
updatePageGeometry(x, y);
queueColliderCreate(x, y);
}
m_dirtyPages.clear();
}
void TerrainSystem::processDeferredReloads()
{
if (!mTerrainGroup || m_reloadQueue.empty())
return;
for (uint64_t key : m_reloadQueue) {
long x, y;
mTerrainGroup->unpackIndex(key, &x, &y);
updatePageGeometry(x, y);
queueColliderCreate(x, y);
}
m_reloadQueue.clear();
}
/* ------------------------------------------------------------------ */
/* activate */
/* ------------------------------------------------------------------ */
void TerrainSystem::activate(TerrainComponent &tc, TransformComponent &xform)
@@ -293,6 +617,21 @@ void TerrainSystem::activate(TerrainComponent &tc, TransformComponent &xform)
Ogre::LogManager::getSingleton().logMessage(
"TerrainSystem: activating terrain...");
deactivate();
/* Paging range and heightmap world area must be known before the
* heightmap is generated or sampled. */
m_pageMinX = -1;
m_pageMinY = -1;
m_pageMaxX = 1;
m_pageMaxY = 1;
m_heightmapWorldMinX = (float)m_pageMinX * tc.worldSize;
m_heightmapWorldMinZ = (float)m_pageMinY * tc.worldSize;
m_heightmapWorldSize = (float)(m_pageMaxX - m_pageMinX + 1) *
tc.worldSize;
ensureHeightmapLoaded(tc);
mTerrainGlobals = OGRE_NEW Ogre::TerrainGlobalOptions();
mTerrainGlobals->setMaxPixelError(tc.maxPixelError);
mTerrainGlobals->setCompositeMapDistance(tc.compositeMapDistance);
@@ -304,15 +643,15 @@ void TerrainSystem::activate(TerrainComponent &tc, TransformComponent &xform)
mTerrainGroup = OGRE_NEW Ogre::TerrainGroup(
m_sceneMgr, Ogre::Terrain::ALIGN_X_Z,
tc.terrainSize, tc.worldSize);
(uint16_t)tc.terrainSize, tc.worldSize);
mTerrainGroup->setOrigin(xform.position);
Ogre::Terrain::ImportData &defaultImp =
mTerrainGroup->getDefaultImportSettings();
defaultImp.terrainSize = tc.terrainSize;
defaultImp.terrainSize = (uint16_t)tc.terrainSize;
defaultImp.worldSize = tc.worldSize;
defaultImp.minBatchSize = tc.minBatchSize;
defaultImp.maxBatchSize = tc.maxBatchSize;
defaultImp.minBatchSize = (uint16_t)tc.minBatchSize;
defaultImp.maxBatchSize = (uint16_t)tc.maxBatchSize;
defaultImp.inputScale = 1.0f;
defaultImp.layerDeclaration = matGen->getLayerDeclaration();
@@ -332,37 +671,57 @@ void TerrainSystem::activate(TerrainComponent &tc, TransformComponent &xform)
}
}
/* Editor-friendly paging: a small fixed world section with procedural
* pages. The DummyPageProvider lets us hook unload so colliders are
* removed safely, while CustomTerrainDefiner re-samples the heightmap
* when a page is (re)loaded. */
mPageManager = OGRE_NEW Ogre::PageManager();
mDummyPageProvider = std::make_unique<DummyPageProvider>();
mDummyPageProvider->owner = this;
mPageManager->setPageProvider(mDummyPageProvider.get());
mPageManager->addCamera(m_camera);
mTerrainPaging = OGRE_NEW Ogre::TerrainPaging(mPageManager);
mPagedWorld = mPageManager->createWorld();
const long pageMinX = -2, pageMinY = -2;
const long pageMaxX = 2, pageMaxY = 2;
mTerrainPagedWorldSection = mTerrainPaging->createWorldSection(
mPagedWorld, mTerrainGroup, 300.0f, 500.0f,
pageMinX, pageMinY, pageMaxX, pageMaxY);
m_pageMinX, m_pageMinY, m_pageMaxX, m_pageMaxY);
mTerrainDefiner = std::make_unique<CustomTerrainDefiner>(this);
mTerrainPagedWorldSection->setDefiner(mTerrainDefiner.get());
mTerrainGroup->freeTemporaryResources();
/* Editor mode: define and synchronously load every page directly through
* the TerrainGroup. This avoids leaving background WorkQueue tasks from
* the paging load chain, which otherwise can deadlock or hang shutdown.
* The paging objects are still set up so runtime game mode can attach a
* camera and use page unload hooks (DummyPageProvider) safely. */
for (long py = m_pageMinY; py <= m_pageMaxY; ++py) {
for (long px = m_pageMinX; px <= m_pageMaxX; ++px) {
Ogre::uint16 terrainSize = mTerrainGroup->getTerrainSize();
float *heightMap = OGRE_ALLOC_T(
float, terrainSize * terrainSize,
Ogre::MEMCATEGORY_GEOMETRY);
fillPageHeightData(mTerrainGroup, px, py, heightMap);
mTerrainGroup->defineTerrain(px, py, heightMap);
OGRE_FREE(heightMap, Ogre::MEMCATEGORY_GEOMETRY);
}
}
mTerrainGroup->loadAllTerrains(true);
if (m_physics)
m_physics->setBodyDrawFilter(&mBodyDrawFilter);
processColliderCreates();
m_active = true;
Ogre::LogManager::getSingleton().logMessage(
"TerrainSystem: activation complete");
}
/* ------------------------------------------------------------------ */
/* deactivate */
/* ------------------------------------------------------------------ */
void TerrainSystem::deactivate()
{
if (!m_active && !mTerrainGroup)
@@ -372,65 +731,99 @@ void TerrainSystem::deactivate()
"TerrainSystem: deactivating...");
m_active = false;
if (mTerrainGroup && mTerrainGroup->isDerivedDataUpdateInProgress()) {
do {
mTerrainGroup->update(false);
} while (mTerrainGroup->isDerivedDataUpdateInProgress());
}
m_terrainEntityId = 0;
m_dirtyPages.clear();
m_reloadQueue.clear();
removeAllColliders();
if (m_physics)
m_physics->setBodyDrawFilter(nullptr);
if (mPageManager && m_camera)
mPageManager->removeCamera(m_camera);
/* Stop paging from loading/unloading pages while we tear down. */
if (mPageManager)
mPageManager->setPagingOperationsEnabled(false);
/* Release RTShader SubRenderState objects held by the terrain material
* generator before the RTSS is torn down. The persistent instances live
* in the generator's main RenderState; remove them explicitly so the
* factories can be destroyed cleanly. */
if (mTerrainGlobals) {
Ogre::TerrainMaterialGeneratorPtr gen =
mTerrainGlobals->getDefaultMaterialGenerator();
if (gen) {
auto *genA = dynamic_cast<Ogre::TerrainMaterialGeneratorA *>(
gen.get());
if (genA) {
Ogre::RTShader::RenderState *rs =
genA->getMainRenderState();
if (rs) {
Ogre::RTShader::SubRenderStateList srs =
rs->getSubRenderStates();
for (Ogre::RTShader::SubRenderState *sub : srs)
rs->removeSubRenderState(sub);
}
}
}
}
mTerrainDefiner.reset();
if (mTerrainGroup)
mTerrainGroup->removeAllTerrains();
mTerrainPagedWorldSection = nullptr;
mPagedWorld = nullptr;
mDummyPageProvider.reset();
/* The PagedWorld owns the TerrainPagedWorldSection, which in turn owns
* the TerrainGroup. PageManager::~PageManager does not delete the worlds
* it manages, so destroying the PageManager here leaks the world/section
* and the group. This is intentional: deleting the group can hang or
* crash on some GL drivers, and leaking it at shutdown is harmless. */
OGRE_DELETE mTerrainPaging;
mTerrainPaging = nullptr;
OGRE_DELETE mPageManager;
mPageManager = nullptr;
OGRE_DELETE mTerrainGroup;
mTerrainGroup = nullptr;
OGRE_DELETE mTerrainGlobals;
mTerrainGlobals = nullptr;
mPagedWorld = nullptr;
mTerrainPagedWorldSection = nullptr;
mDummyPageProvider.reset();
if (mTerrainGlobals) {
mTerrainGlobals->setDefaultMaterialGenerator(
Ogre::TerrainMaterialGeneratorPtr());
}
mTerrainGlobals = nullptr;
Ogre::LogManager::getSingleton().logMessage(
"TerrainSystem: deactivated");
}
/* ------------------------------------------------------------------ */
/* update */
/* ------------------------------------------------------------------ */
void TerrainSystem::update(float /*deltaTime*/)
{
bool found = false;
flecs::entity_t foundEntity = 0;
m_terrainQuery.each([&](flecs::entity e, TerrainComponent &tc,
TransformComponent &xform) {
(void)e;
found = true;
foundEntity = e.id();
if (!tc.enabled && m_active) {
deactivate();
return;
}
if (tc.enabled && !m_active)
if (tc.enabled && !m_active) {
activate(tc, xform);
m_terrainEntityId = e.id();
}
});
/* If the terrain entity or its component was removed, tear down. */
if (m_active && !found)
if (m_active && foundEntity == 0) {
deactivate();
return;
}
if (m_active && mTerrainGroup)
mTerrainGroup->update(false);
rebuildDirtyPages();
processDeferredReloads();
ensurePageColliders();
processColliderRemoves();
processColliderCreates();
@@ -452,13 +845,13 @@ bool TerrainSystem::raycastTerrain(const Ogre::Ray &ray, float &outT,
if (!m_active || !m_physics || mColliders.empty())
return false;
JPH::RVec3 origin(ray.getOrigin().x, ray.getOrigin().y,
ray.getOrigin().z);
const JPH::RVec3 origin(ray.getOrigin().x, ray.getOrigin().y,
ray.getOrigin().z);
JPH::Vec3 dir(ray.getDirection().x, ray.getDirection().y,
ray.getDirection().z);
dir = dir.Normalized();
JPH::RRayCast rayCast(origin, dir);
const JPH::RRayCast rayCast(origin, dir);
JPH::PhysicsSystem *jphSys = m_physics->getPhysicsSystem();
if (!jphSys)
@@ -466,8 +859,9 @@ bool TerrainSystem::raycastTerrain(const Ogre::Ray &ray, float &outT,
JPH::RayCastResult hit;
if (!jphSys->GetNarrowPhaseQuery().CastRay(rayCast, hit,
JPH::BroadPhaseLayerFilter(), JPH::ObjectLayerFilter(),
JPH::BodyFilter()))
JPH::BroadPhaseLayerFilter(),
JPH::ObjectLayerFilter(),
JPH::BodyFilter()))
return false;
for (auto &pair : mColliders) {
@@ -484,3 +878,130 @@ void TerrainSystem::setShowTerrainColliders(bool show)
{
m_showTerrainColliders = show;
}
/* ------------------------------------------------------------------ */
/* snapCameraAboveTerrain */
/* ------------------------------------------------------------------ */
void TerrainSystem::snapCameraAboveTerrain()
{
if (!m_active || !m_sceneMgr)
return;
Ogre::SceneNode *targetNode =
m_sceneMgr->getSceneNode("EditorCameraTarget");
if (!targetNode)
targetNode = m_sceneMgr->getSceneNode(
"EditorCameraTargetNode");
if (!targetNode)
return;
Ogre::Vector3 pos = targetNode->getPosition();
float h = getHeightAt(pos);
if (pos.y < h + 5.0f)
targetNode->setPosition(pos.x, h + 5.0f, pos.z);
}
/* ------------------------------------------------------------------ */
/* applySculptBrush */
/* ------------------------------------------------------------------ */
void TerrainSystem::applySculptBrush(const Ogre::Vector3 &worldPos)
{
if (!m_heightmapLoaded || !m_active || !mTerrainGroup)
return;
std::lock_guard<std::mutex> lock(m_heightmapMutex);
float spacing = m_heightmapWorldSize / (float)(m_heightmapRes - 1);
int rSamples = (int)(m_sculptRadius / spacing) + 1;
int cx = (int)((worldPos.x - m_heightmapWorldMinX) /
m_heightmapWorldSize * (float)m_heightmapRes);
int cz = (int)((worldPos.z - m_heightmapWorldMinZ) /
m_heightmapWorldSize * (float)m_heightmapRes);
int x0 = std::max(0, cx - rSamples);
int x1 = std::min(m_heightmapRes - 1, cx + rSamples);
int z0 = std::max(0, cz - rSamples);
int z1 = std::min(m_heightmapRes - 1, cz + rSamples);
if (m_sculptTool == SculptTool::Smooth) {
std::vector<std::pair<int, int>> s;
for (int z = z0; z <= z1; ++z)
for (int x = x0; x <= x1; ++x) {
float dx = ((float)x - (float)cx) * spacing;
float dz = ((float)z - (float)cz) * spacing;
if (dx * dx + dz * dz <=
m_sculptRadius * m_sculptRadius)
s.push_back({ x, z });
}
float avg = 0;
for (auto &p : s)
avg += m_heightData[p.second * m_heightmapRes +
p.first];
avg /= (float)s.size();
for (auto &p : s) {
float &h = m_heightData[p.second * m_heightmapRes +
p.first];
h = avg * m_sculptStrength +
h * (1.0f - m_sculptStrength);
}
} else if (m_sculptTool == SculptTool::Flatten) {
float ch = m_heightData[cz * m_heightmapRes + cx];
for (int z = z0; z <= z1; ++z)
for (int x = x0; x <= x1; ++x) {
float dx = ((float)x - (float)cx) * spacing;
float dz = ((float)z - (float)cz) * spacing;
if (dx * dx + dz * dz <=
m_sculptRadius * m_sculptRadius) {
float &h = m_heightData[z * m_heightmapRes +
x];
h = h * (1.0f - m_sculptStrength) +
ch * m_sculptStrength;
}
}
} else {
float sign = (m_sculptTool == SculptTool::Raise) ?
1.0f :
-1.0f;
for (int z = z0; z <= z1; ++z)
for (int x = x0; x <= x1; ++x) {
float dx = ((float)x - (float)cx) * spacing;
float dz = ((float)z - (float)cz) * spacing;
float d2 = dx * dx + dz * dz;
if (d2 <= m_sculptRadius * m_sculptRadius) {
float falloff = 1.0f -
sqrtf(d2) / m_sculptRadius;
m_heightData[z * m_heightmapRes + x] +=
sign * m_sculptStrength * falloff;
}
}
}
float ws = mTerrainGroup->getTerrainWorldSize();
long px0 = worldToPage(worldPos.x - m_sculptRadius, ws);
long px1 = worldToPage(worldPos.x + m_sculptRadius, ws);
long pz0 = worldToPage(worldPos.z - m_sculptRadius, ws);
long pz1 = worldToPage(worldPos.z + m_sculptRadius, ws);
for (long pz = pz0; pz <= pz1; ++pz)
for (long px = px0; px <= px1; ++px)
markPageDirty(px, pz);
}
/* ------------------------------------------------------------------ */
/* applySplatBrush */
/* ------------------------------------------------------------------ */
void TerrainSystem::applySplatBrush(const Ogre::Vector3 & /*worldPos*/)
{
/* Not implemented yet. */
}
/* ------------------------------------------------------------------ */
/* destroyCollider */
/* ------------------------------------------------------------------ */
void TerrainSystem::destroyCollider(uint64_t /*key*/,
TerrainCollider & /*collider*/)
{
/* Legacy helper; colliders are removed directly via processColliderRemoves. */
}
+105 -31
View File
@@ -15,6 +15,7 @@
#include <unordered_map>
#include <set>
#include <deque>
#include <mutex>
#include <cstdint>
#include <Jolt/Jolt.h>
@@ -49,9 +50,46 @@ public:
return m_showTerrainColliders;
}
/* --- Heightmap data (M3) --- */
float sampleHeightAt(long worldX, long worldZ) const;
float sampleHeightAtLocked(long worldX, long worldZ) const;
void setHeightAt(long worldX, long worldZ, float value);
bool loadHeightmap(const std::string &path);
bool saveHeightmap(const std::string &path);
bool loadSceneHeightmap(const struct TerrainComponent &tc);
bool saveSceneHeightmap(const struct TerrainComponent &tc);
std::string getHeightmapPath(const struct TerrainComponent &tc) const;
void markPageDirty(long pageX, long pageY);
void rebuildDirtyPages();
void processDeferredReloads();
/* --- Sculpting (M3) --- */
enum class SculptTool { Raise, Lower, Smooth, Flatten, SplatPaint };
bool getSculptMode() const { return m_sculptMode; }
void setSculptMode(bool v) { m_sculptMode = v; }
bool isSculpting() const { return m_sculptMode; }
SculptTool getSculptTool() const { return m_sculptTool; }
void setSculptTool(SculptTool t) { m_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; }
int getSculptLayerIndex() const { return m_sculptLayerIndex; }
void setSculptLayerIndex(int i) { m_sculptLayerIndex = i; }
void snapCameraAboveTerrain();
void applySculptBrush(const Ogre::Vector3 &worldPos);
void applySplatBrush(const Ogre::Vector3 &worldPos);
private:
void activate(struct TerrainComponent &tc,
struct TransformComponent &xform);
void ensureHeightmapLoaded(struct TerrainComponent &tc);
void updatePageGeometry(long pageX, long pageY);
void fillPageHeightData(Ogre::TerrainGroup *group, long x, long y,
float *heightMap);
long worldToPage(float worldCoord, float worldSize) const;
void ensurePageColliders();
struct TerrainCollider {
long pageX, pageY;
@@ -59,6 +97,42 @@ private:
bool pendingRemove = false;
};
class DummyPageProvider : public Ogre::PageProvider {
public:
bool prepareProceduralPage(Ogre::Page *,
Ogre::PagedWorldSection *) override
{
return true;
}
bool loadProceduralPage(Ogre::Page *,
Ogre::PagedWorldSection *) override
{
return true;
}
bool unloadProceduralPage(Ogre::Page *page,
Ogre::PagedWorldSection *) override;
bool unprepareProceduralPage(Ogre::Page *,
Ogre::PagedWorldSection *) override
{
return true;
}
TerrainSystem *owner = nullptr;
};
class CustomTerrainDefiner :
public Ogre::TerrainPagedWorldSection::TerrainDefiner {
public:
CustomTerrainDefiner(TerrainSystem *sys)
: Ogre::TerrainPagedWorldSection::TerrainDefiner()
, m_sys(sys)
{
}
void define(Ogre::TerrainGroup *terrainGroup, long x,
long y) override;
private:
TerrainSystem *m_sys;
};
class TerrainBodyDrawFilter : public JPH::BodyDrawFilter {
public:
bool ShouldDraw(const JPH::Body &inBody) const override;
@@ -76,43 +150,13 @@ private:
std::set<JPH::BodyID> m_terrainIds;
};
class DummyPageProvider : public Ogre::PageProvider {
public:
bool prepareProceduralPage(Ogre::Page *, Ogre::PagedWorldSection *) override
{
return true;
}
bool loadProceduralPage(Ogre::Page *, Ogre::PagedWorldSection *) override
{
return true;
}
bool unloadProceduralPage(Ogre::Page *page, Ogre::PagedWorldSection *) override;
bool unprepareProceduralPage(Ogre::Page *, Ogre::PagedWorldSection *) override
{
return true;
}
TerrainSystem *owner = nullptr;
};
class CustomTerrainDefiner :
public Ogre::TerrainPagedWorldSection::TerrainDefiner {
public:
CustomTerrainDefiner(TerrainSystem *sys)
: Ogre::TerrainPagedWorldSection::TerrainDefiner()
, m_sys(sys)
{
}
void define(Ogre::TerrainGroup *terrainGroup, long x, long y) override;
private:
TerrainSystem *m_sys;
};
JPH::ShapeRefC buildPageCollider(Ogre::Terrain *terrain);
void queueColliderCreate(long x, long y);
void queueColliderRemove(long x, long y);
void processColliderCreates();
void processColliderRemoves();
void removeAllColliders();
void destroyCollider(uint64_t key, TerrainCollider &collider);
flecs::world &m_world;
Ogre::SceneManager *m_sceneMgr;
@@ -134,8 +178,38 @@ private:
std::unordered_map<uint64_t, TerrainCollider> mColliders;
std::deque<std::pair<long, long>> mColliderCreateQueue;
std::set<uint64_t> mPendingColliderPages;
std::mutex m_colliderQueueMutex;
TerrainBodyDrawFilter mBodyDrawFilter;
/* Paging range used for synchronous page load and collider polling. */
long m_pageMinX = -1, m_pageMinY = -1;
long m_pageMaxX = 1, m_pageMaxY = 1;
/* Heightmap data (M3) — protected by m_heightmapMutex because
* collider building and page updates may happen concurrently. */
mutable std::mutex m_heightmapMutex;
std::vector<float> m_heightData;
bool m_heightmapLoaded = false;
int m_heightmapRes = 0;
float m_heightmapWorldMinX = 0;
float m_heightmapWorldMinZ = 0;
float m_heightmapWorldSize = 2000.0f;
std::set<uint64_t> m_dirtyPages;
std::vector<uint64_t> m_reloadQueue;
bool hasHeightmap() const { return m_heightmapLoaded; }
/* Sculpting state */
bool m_sculptMode = false;
SculptTool m_sculptTool = SculptTool::Raise;
float m_sculptRadius = 5.0f;
float m_sculptStrength = 0.5f;
int m_sculptLayerIndex = 0;
/* Entity tracking */
flecs::entity_t m_terrainEntityId = 0;
flecs::query<struct TerrainComponent, struct TransformComponent>
m_terrainQuery;
};
+57 -2
View File
@@ -39,8 +39,63 @@ public:
ImGui::Separator();
/* Read-only display of sizing params (restart needed to change).
* In M2+ these get Restart Terrain buttons. */
/* --- Sculpting (M3) --- */
if (ts) {
ImGui::Text("Sculpting");
bool sculpt = ts->getSculptMode();
if (ImGui::Checkbox("Sculpt Mode", &sculpt))
ts->setSculptMode(sculpt);
if (sculpt)
ImGui::TextColored(ImVec4(0, 1, 0, 1),
"SCULPT MODE ACTIVE");
ImGui::SameLine();
ImGui::TextDisabled("(ESC to exit)");
const char *tools[] = { "Raise", "Lower", "Smooth",
"Flatten", "Splat Paint" };
int tool = (int)ts->getSculptTool();
if (ImGui::Combo("Tool", &tool, tools,
IM_ARRAYSIZE(tools)))
ts->setSculptTool((TerrainSystem::SculptTool)tool);
float radius = ts->getSculptRadius();
if (ImGui::SliderFloat("Radius", &radius, 0.5f, 50.0f))
ts->setSculptRadius(radius);
float strength = ts->getSculptStrength();
if (ImGui::SliderFloat("Strength", &strength, 0.01f,
1.0f))
ts->setSculptStrength(strength);
if (ts->getSculptTool() ==
TerrainSystem::SculptTool::SplatPaint) {
int li = ts->getSculptLayerIndex();
if (ImGui::InputInt("Layer Index", &li, 1, 1))
ts->setSculptLayerIndex(
std::max(0, li));
}
}
ImGui::Separator();
/* --- Camera & file operations --- */
if (ts) {
if (ImGui::Button("Snap Camera Above Terrain"))
ts->snapCameraAboveTerrain();
ImGui::SameLine();
const std::string hmPath = ts->getHeightmapPath(tc);
if (ImGui::Button("Save Heightmap"))
ts->saveHeightmap(hmPath);
ImGui::SameLine();
if (ImGui::Button("Load Heightmap"))
ts->loadHeightmap(hmPath);
}
ImGui::Separator();
/* Read-only display of sizing params */
ImGui::Text("Terrain ID: %llu",
(unsigned long long)tc.terrainId);
ImGui::Text("Page size: %d x %d vertices", tc.terrainSize,
tc.terrainSize);
ImGui::Text("World size: %.0f units", tc.worldSize);