Terrain robustness update
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user