Terrain robustness update

This commit is contained in:
2026-07-01 10:57:45 +03:00
parent 4cd2c72fb0
commit f06f535938
12 changed files with 1097 additions and 396 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;
+53 -31
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).
+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
+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;
}
@@ -34,6 +34,7 @@
#include "../components/WaterPhysics.hpp"
#include "../components/WaterPlane.hpp"
#include "../components/Terrain.hpp"
#include "TerrainSystem.hpp"
#include "../components/Sun.hpp"
#include "../components/Skybox.hpp"
#include "../components/EventHandler.hpp"
@@ -320,9 +321,10 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity)
if (entity.has<WaterPlane>()) {
json["waterPlane"] = serializeWaterPlane(entity);
}
if (entity.has<TerrainComponent>()) {
json["terrain"] = serializeTerrain(entity);
}
if (entity.has<TerrainComponent>()) {
json["terrain"] = serializeTerrain(entity);
}
// ActionDatabase is now a singleton, serialized at scene level
if (entity.has<ActionDebug>()) {
@@ -4166,6 +4168,68 @@ nlohmann::json SceneSerializer::serializeTerrain(flecs::entity entity)
}
json["layers"] = layersJson;
nlohmann::json auxMapsJson = nlohmann::json::array();
for (auto &am : tc.auxMaps) {
nlohmann::json aj;
aj["name"] = am.name;
aj["fileName"] = am.fileName;
aj["resolution"] = am.resolution;
aj["defaultValue"] = am.defaultValue;
auxMapsJson.push_back(aj);
}
json["auxMaps"] = auxMapsJson;
nlohmann::json roadConfigJson;
roadConfigJson["roadMeshTemplate"] = tc.roadConfig.roadMeshTemplate;
roadConfigJson["laneWidth"] = tc.roadConfig.laneWidth;
roadConfigJson["lanesPerDirection"] = tc.roadConfig.lanesPerDirection;
roadConfigJson["roadThickness"] = tc.roadConfig.roadThickness;
roadConfigJson["roadLodDistance"] = tc.roadConfig.roadLodDistance;
roadConfigJson["roadVisibilityDistance"] =
tc.roadConfig.roadVisibilityDistance;
roadConfigJson["roadMaterialName"] = tc.roadConfig.roadMaterialName;
json["roadConfig"] = roadConfigJson;
nlohmann::json roadNodesJson = nlohmann::json::array();
for (auto &n : tc.roadNodes) {
nlohmann::json nj;
nj["id"] = n.id;
nj["position"] = { n.position.x, n.position.y, n.position.z };
nj["verticalOffset"] = n.verticalOffset;
roadNodesJson.push_back(nj);
}
json["roadNodes"] = roadNodesJson;
nlohmann::json roadEdgesJson = nlohmann::json::array();
for (auto &e : tc.roadEdges) {
nlohmann::json ej;
ej["nodeA"] = e.nodeA;
ej["nodeB"] = e.nodeB;
ej["roadLevelA"] = e.roadLevelA;
ej["roadLevelB"] = e.roadLevelB;
ej["lanesPerDirectionOverride"] = e.lanesPerDirectionOverride;
ej["lanesAtoB"] = e.lanesAtoB;
ej["lanesBtoA"] = e.lanesBtoA;
nlohmann::json sidePrefabsJson = nlohmann::json::array();
for (auto &sp : e.sidePrefabs) {
nlohmann::json spj;
spj["prefabPath"] = sp.prefabPath;
spj["edgeT"] = sp.edgeT;
spj["sideOffset"] = sp.sideOffset;
spj["leftSide"] = sp.leftSide;
sidePrefabsJson.push_back(spj);
}
ej["sidePrefabs"] = sidePrefabsJson;
roadEdgesJson.push_back(ej);
}
json["roadEdges"] = roadEdgesJson;
/* Persist binary heightmap alongside the JSON. */
TerrainSystem *ts = TerrainSystem::getInstance();
if (ts)
ts->saveSceneHeightmap(tc);
return json;
}
@@ -4195,5 +4259,77 @@ void SceneSerializer::deserializeTerrain(flecs::entity entity,
}
}
if (json.contains("auxMaps") && json["auxMaps"].is_array()) {
for (auto &aj : json["auxMaps"]) {
TerrainComponent::AuxMap am;
am.name = aj.value("name", "");
am.fileName = aj.value("fileName", "");
am.resolution = aj.value("resolution", 256);
am.defaultValue = aj.value("defaultValue", 0.0f);
tc.auxMaps.push_back(am);
}
}
if (json.contains("roadConfig")) {
auto &rcj = json["roadConfig"];
tc.roadConfig.roadMeshTemplate =
rcj.value("roadMeshTemplate", "road_segment.mesh");
tc.roadConfig.laneWidth = rcj.value("laneWidth", 3.0f);
tc.roadConfig.lanesPerDirection =
rcj.value("lanesPerDirection", 1);
tc.roadConfig.roadThickness = rcj.value("roadThickness", 0.3f);
tc.roadConfig.roadLodDistance = rcj.value("roadLodDistance", 200.0f);
tc.roadConfig.roadVisibilityDistance =
rcj.value("roadVisibilityDistance", 1000.0f);
tc.roadConfig.roadMaterialName =
rcj.value("roadMaterialName", "RoadMaterial");
}
if (json.contains("roadNodes") && json["roadNodes"].is_array()) {
for (auto &nj : json["roadNodes"]) {
TerrainComponent::RoadNode node;
node.id = nj.value("id", 0);
node.verticalOffset = nj.value("verticalOffset", 0.0f);
if (nj.contains("position") && nj["position"].is_array() &&
nj["position"].size() >= 3) {
node.position.x = nj["position"][0];
node.position.y = nj["position"][1];
node.position.z = nj["position"][2];
}
tc.roadNodes.push_back(node);
}
}
if (json.contains("roadEdges") && json["roadEdges"].is_array()) {
for (auto &ej : json["roadEdges"]) {
TerrainComponent::RoadEdge edge;
edge.nodeA = ej.value("nodeA", -1);
edge.nodeB = ej.value("nodeB", -1);
edge.roadLevelA = ej.value("roadLevelA", 0.0f);
edge.roadLevelB = ej.value("roadLevelB", 0.0f);
edge.lanesPerDirectionOverride =
ej.value("lanesPerDirectionOverride", 0);
edge.lanesAtoB = ej.value("lanesAtoB", 0);
edge.lanesBtoA = ej.value("lanesBtoA", 0);
if (ej.contains("sidePrefabs") &&
ej["sidePrefabs"].is_array()) {
for (auto &spj : ej["sidePrefabs"]) {
TerrainComponent::RoadEdge::RoadSidePrefab sp;
sp.prefabPath = spj.value("prefabPath", "");
sp.edgeT = spj.value("edgeT", 0.5f);
sp.sideOffset = spj.value("sideOffset", 5.0f);
sp.leftSide = spj.value("leftSide", true);
edge.sidePrefabs.push_back(sp);
}
}
tc.roadEdges.push_back(edge);
}
}
entity.set<TerrainComponent>(tc);
TerrainSystem *ts = TerrainSystem::getInstance();
if (ts)
ts->loadSceneHeightmap(tc);
}
File diff suppressed because it is too large Load Diff
@@ -15,6 +15,7 @@
#include <unordered_map>
#include <set>
#include <deque>
#include <mutex>
#include <cstdint>
#include <Jolt/Jolt.h>
@@ -51,19 +52,20 @@ public:
/* --- Heightmap data (M3) --- */
float sampleHeightAt(long worldX, long worldZ) const;
float sampleHeightAtLocked(long worldX, long worldZ) const;
void setHeightAt(long worldX, long worldZ, float value);
bool loadHeightmap(const std::string &path);
bool saveHeightmap(const std::string &path);
bool loadSceneHeightmap(const struct TerrainComponent &tc);
bool saveSceneHeightmap(const struct TerrainComponent &tc);
std::string getHeightmapPath(const struct TerrainComponent &tc) const;
void markPageDirty(long pageX, long pageY);
void rebuildDirtyPages();
void processDeferredReloads();
/* --- Sculpting (M3) --- */
enum class SculptTool { Raise, Lower, Smooth, Flatten, SplatPaint };
bool m_sculptMode = false;
SculptTool m_sculptTool = SculptTool::Raise;
float m_sculptRadius = 5.0f;
float m_sculptStrength = 0.5f;
int m_sculptLayerIndex = 0;
bool getSculptMode() const { return m_sculptMode; }
void setSculptMode(bool v) { m_sculptMode = v; }
bool isSculpting() const { return m_sculptMode; }
@@ -76,10 +78,18 @@ public:
int getSculptLayerIndex() const { return m_sculptLayerIndex; }
void setSculptLayerIndex(int i) { m_sculptLayerIndex = i; }
void snapCameraAboveTerrain();
void applySculptBrush(const Ogre::Vector3 &worldPos);
void applySplatBrush(const Ogre::Vector3 &worldPos);
private:
void activate(struct TerrainComponent &tc,
struct TransformComponent &xform);
void ensureHeightmapLoaded(struct TerrainComponent &tc);
void updatePageGeometry(long pageX, long pageY);
void fillPageHeightData(Ogre::TerrainGroup *group, long x, long y,
float *heightMap);
long worldToPage(float worldCoord, float worldSize) const;
void ensurePageColliders();
struct TerrainCollider {
long pageX, pageY;
@@ -87,6 +97,42 @@ private:
bool pendingRemove = false;
};
class DummyPageProvider : public Ogre::PageProvider {
public:
bool prepareProceduralPage(Ogre::Page *,
Ogre::PagedWorldSection *) override
{
return true;
}
bool loadProceduralPage(Ogre::Page *,
Ogre::PagedWorldSection *) override
{
return true;
}
bool unloadProceduralPage(Ogre::Page *page,
Ogre::PagedWorldSection *) override;
bool unprepareProceduralPage(Ogre::Page *,
Ogre::PagedWorldSection *) override
{
return true;
}
TerrainSystem *owner = nullptr;
};
class CustomTerrainDefiner :
public Ogre::TerrainPagedWorldSection::TerrainDefiner {
public:
CustomTerrainDefiner(TerrainSystem *sys)
: Ogre::TerrainPagedWorldSection::TerrainDefiner()
, m_sys(sys)
{
}
void define(Ogre::TerrainGroup *terrainGroup, long x,
long y) override;
private:
TerrainSystem *m_sys;
};
class TerrainBodyDrawFilter : public JPH::BodyDrawFilter {
public:
bool ShouldDraw(const JPH::Body &inBody) const override;
@@ -104,43 +150,13 @@ private:
std::set<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;
@@ -162,15 +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;
/* Heightmap data (M3) */
/* Paging range used for synchronous page load and collider polling. */
long m_pageMinX = -1, m_pageMinY = -1;
long m_pageMaxX = 1, m_pageMaxY = 1;
/* Heightmap data (M3) — protected by m_heightmapMutex because
* collider building and page updates may happen concurrently. */
mutable std::mutex m_heightmapMutex;
std::vector<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;
};
+3 -4
View File
@@ -83,13 +83,12 @@ public:
if (ImGui::Button("Snap Camera Above Terrain"))
ts->snapCameraAboveTerrain();
ImGui::SameLine();
const std::string hmPath = ts->getHeightmapPath(tc);
if (ImGui::Button("Save Heightmap"))
ts->saveHeightmap("heightmaps/ht_" +
std::to_string(tc.terrainId) + ".bin");
ts->saveHeightmap(hmPath);
ImGui::SameLine();
if (ImGui::Button("Load Heightmap"))
ts->loadHeightmap("heightmaps/ht_" +
std::to_string(tc.terrainId) + ".bin");
ts->loadHeightmap(hmPath);
}
ImGui::Separator();