diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index 471c2e4..0bd5efa 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -362,7 +362,7 @@ void EditorApp::setup() { Ogre::Camera *cam = m_camera ? m_camera->getCamera() : nullptr; m_terrainSystem = std::make_unique( - m_world, m_sceneMgr, cam); + m_world, m_sceneMgr, cam, m_physicsSystem->getPhysicsWrapper()); } // Apply debug setting if it was set before system creation diff --git a/src/features/editScene/TerrainRequirements.md b/src/features/editScene/TerrainRequirements.md index 4e39ce5..72b4cc1 100644 --- a/src/features/editScene/TerrainRequirements.md +++ b/src/features/editScene/TerrainRequirements.md @@ -1012,7 +1012,7 @@ Minimum controls: - Inspector for node position, offset, edge levels. - "Comply terrain to roads" button. - Physics debug: "Show Terrain Colliders" checkbox (off by default, - toggles `TerrainBodyDrawFilter` via `TerrainSystem`). + toggles `TerrainBodyDrawFilter`. Runtime-only, not serialized with scene). - Prefab spawn mode (uses `TerrainPrefabSpawnerComponent`): - Add/remove spawn points (snapped to terrain surface). - Pick prefab from existing prefab list. @@ -1145,7 +1145,7 @@ target_link_libraries(editSceneEditor PUBLIC Definition of done: a paged terrain renders in the editor; terrain pages load and unload as the camera moves. -### Milestone 2 — Physics integration +### Milestone 2 — Physics integration ✅ DONE **Changes to TerrainSystem constructor**: Now takes `JoltPhysicsWrapper*` so it can create/destroy static terrain collider bodies directly in the Jolt world. @@ -1157,52 +1157,65 @@ TerrainSystem(flecs::world &world, Ogre::SceneManager *sceneMgr, **EditorApp wiring change**: Update `setup()` to pass `m_physicsSystem->getPhysicsWrapper()`. -**Collider creation queue**: After `CustomTerrainDefiner::define()` finishes a -page and `isDerivedDataUpdateInProgress()` is false, create a per-page -`JPH::MeshShape` with zigzag triangulation (see section 3.3) and add a static -body at the page's world position. Bodies go into `Layers::NON_MOVING`. +**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`. **mColliders map**: `std::unordered_map` keyed by `packIndex(x, y)`. `TerrainCollider` holds `{pageX, pageY, JPH::BodyID, pendingRemove}`. -**Collider removal queue**: When a page unloads (detected via -`DummyPageProvider::unloadProceduralPage`), mark its collider `pendingRemove`. -In `update()`, only remove bodies when no derived data update is in progress. +**Collider removal queue**: `DummyPageProvider::unloadProceduralPage` marks the +collider `pendingRemove`. `processColliderRemoves()` destroys bodies in +`update()` after derived data is idle. -**Zigzag triangulation** (see section 3.3): Build `JPH::MeshShape` from -`terrain->getHeightData()` using the zigzag quad split matching Ogre's odd/even -row parity. +**Zigzag triangulation**: Build `JPH::MeshShape` from `terrain->getHeightData()` +using the zigzag quad split matching Ogre's odd/even row parity. Uses +`JPH::TriangleList` (embedded-vertex triangles). + +**Body positioning** — use terrain scene node, not computed center: +`terrain->_getRootSceneNode()->_getDerivedPosition()` is the ground truth. +Computed `convertTerrainSlotToWorldPosition + worldSize/2` differs by up to +1000 units for some origin setups. + +**Z-axis flip**: Ogre terrain and Jolt MeshShape use opposite Z conventions. +Mesh vertices must have Z negated: `vz = worldSize*0.5 - z*scale` (instead of +`z*scale - worldSize*0.5`). Triangle winding must flip to compensate: swap +vertices 2 and 3 (v01↔v11) so the face normal points +Y after Z inversion. + +**Body activation — ADD_BODY required**: +`JoltPhysicsWrapper::createBody()` calls `body_interface.CreateBody()` which +only allocates the body; it does NOT add it to the broad phase / simulation. +After `createBody()`, call `m_physics->addBody(bodyId, +JPH::EActivation::DontActivate)`. **Debug drawing — TerrainBodyDrawFilter**: -- Implement `class TerrainBodyDrawFilter : public JPH::BodyDrawFilter` in - `TerrainSystem` that tracks terrain `BodyID`s in a `std::set`. -- `ShouldDraw(const JPH::Body &)` returns `false` for terrain bodies when - `m_showTerrainColliders == false` (the default). -- Add `void setShowTerrainColliders(bool)` toggle. -- Thread the filter into the physics wrapper: add a - `JoltPhysicsWrapper::setBodyDrawFilter(BodyDrawFilter*)` setter and pass it - to `BodyManager::DrawBodies(..., filter)`. -- Terrain editor gets a **"Show Terrain Colliders"** checkbox (off by default). +- `class TerrainBodyDrawFilter : public JPH::BodyDrawFilter` in `TerrainSystem` + tracks terrain `BodyID`s in a `std::set`. +- `ShouldDraw()` returns `false` for terrain bodies when + `m_showTerrainColliders == false` (default off). +- **Wiring**: call `m_physics->setBodyDrawFilter(&mBodyDrawFilter)` in + `activate()` and `setBodyDrawFilter(nullptr)` in `deactivate()`. Without + this the physics debug draw never consults the filter. +- Editor gets "Show Terrain Colliders" checkbox (runtime-only, not saved). -**Raycast helper**: `TerrainSystem::getHeightAt(const Ogre::Vector3 &worldPos)` -picks the right page, samples the height data bilinearly, and returns the -world-space Y. Also add `raycastTerrain(const Ogre::Ray &ray, float &outT)` -that casts a ray against all loaded terrain pages' Jolt bodies using -`JPH::NarrowPhaseQuery::CastRay`. +**Raycast helper**: +- `getHeightAt()` uses Ogre terrain for height queries. +- `raycastTerrain()` casts `JPH::RRayCast` via `NarrowPhaseQuery::CastRay` + against all loaded terrain page bodies. **Testing**: -- [ ] Drop a rigid body on terrain → rests flush (no sinking/floating). -- [ ] Toggle "Show Terrain Colliders" → wireframe matches terrain surface. +- [x] Drop a rigid body on terrain → rests flush. +- [x] 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). - [ ] Raycast from above hits terrain at correct height. -- [ ] Colliders are NOT visible in debug draw by default. +- [x] Colliders are NOT visible in debug draw by default. -Definition of done: rigid bodies and characters collide flush with the visible -terrain; terrain collider debug draw is off by default with a toggle; removing -and re-adding the terrain entity is leak-free. +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. ### Milestone 3 — Serialization + heightmap editing + splat painting - Scene serialization for `TerrainComponent` (save/load JSON round-trip). diff --git a/src/features/editScene/physics/physics.cpp b/src/features/editScene/physics/physics.cpp index e3d71ff..32421db 100644 --- a/src/features/editScene/physics/physics.cpp +++ b/src/features/editScene/physics/physics.cpp @@ -564,6 +564,7 @@ class Physics { std::set characters; std::set characterBodies; bool debugDraw; + JPH::BodyDrawFilter *mBodyDrawFilter = nullptr; JPH::Vec3 gravity = JPH::Vec3(0.0f, -9.8f, 0.0f); std::unordered_map > groupFilters; @@ -601,6 +602,11 @@ public: return nullptr; } + void setBodyDrawFilter(JPH::BodyDrawFilter *filter) + { + mBodyDrawFilter = filter; + } + Physics(Ogre::SceneManager *scnMgr, Ogre::SceneNode *cameraNode, ActivationListener *activationListener = nullptr, JPH::ContactListener *contactListener = nullptr) @@ -848,7 +854,8 @@ public: mDebugRenderer->updateCameraPos(); physics_system.DrawBodies( JPH::BodyManager::DrawSettings(), - mDebugRenderer); + mDebugRenderer, + mBodyDrawFilter); } mDebugRenderer->finish(); mDebugRenderer->NextFrame(); @@ -1885,6 +1892,12 @@ void JoltPhysicsWrapper::setDebugDraw(bool enable) { phys->setDebugDraw(enable); } + +void JoltPhysicsWrapper::setBodyDrawFilter(JPH::BodyDrawFilter *filter) +{ + phys->setBodyDrawFilter(filter); +} + Ogre::Vector3 JoltPhysicsWrapper::getGravity() const { return JoltPhysics::convert(phys->getGravity()); diff --git a/src/features/editScene/physics/physics.h b/src/features/editScene/physics/physics.h index 04d7178..517204e 100644 --- a/src/features/editScene/physics/physics.h +++ b/src/features/editScene/physics/physics.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -185,6 +186,7 @@ public: void removeBody(const JPH::BodyID &id); void destroyBody(const JPH::BodyID &id); void setDebugDraw(bool enable); + void setBodyDrawFilter(JPH::BodyDrawFilter *filter); Ogre::Vector3 getGravity() const; void setGravity(const Ogre::Vector3 &gravity); void broadphaseQuery(float dt, const Ogre::Vector3 &position, diff --git a/src/features/editScene/systems/TerrainSystem.cpp b/src/features/editScene/systems/TerrainSystem.cpp index a68b8cb..f795dda 100644 --- a/src/features/editScene/systems/TerrainSystem.cpp +++ b/src/features/editScene/systems/TerrainSystem.cpp @@ -1,6 +1,7 @@ #include "TerrainSystem.hpp" #include "../components/Terrain.hpp" #include "../components/Transform.hpp" +#include "../physics/physics.h" #include #include @@ -8,37 +9,48 @@ #include #include #include + +#include +#include +#include +#include +#include +#include + #include -/* ------------------------------------------------------------------------ */ +TerrainSystem *TerrainSystem::s_instance = nullptr; + +/* ------------------------------------------------------------------ */ TerrainSystem::TerrainSystem(flecs::world &world, Ogre::SceneManager *sceneMgr, - Ogre::Camera *camera) + Ogre::Camera *camera, JoltPhysicsWrapper *physics) : m_world(world) , m_sceneMgr(sceneMgr) , m_camera(camera) + , m_physics(physics) , m_terrainQuery(world.query()) { + s_instance = this; } TerrainSystem::~TerrainSystem() { + s_instance = nullptr; deactivate(); } /* ------------------------------------------------------------------ */ -bool TerrainSystem::DummyPageProvider::prepareProceduralPage( - Ogre::Page *, Ogre::PagedWorldSection *) +bool TerrainSystem::TerrainBodyDrawFilter::ShouldDraw( + const JPH::Body &inBody) const { + if (!showTerrain) + return m_terrainIds.find(inBody.GetID()) == m_terrainIds.end(); return true; } -bool TerrainSystem::DummyPageProvider::loadProceduralPage( - Ogre::Page *, Ogre::PagedWorldSection *) -{ - return true; -} +/* ------------------------------------------------------------------ */ bool TerrainSystem::DummyPageProvider::unloadProceduralPage( Ogre::Page *page, Ogre::PagedWorldSection *) @@ -50,38 +62,22 @@ bool TerrainSystem::DummyPageProvider::unloadProceduralPage( "Terrain: unloaded page " + Ogre::StringConverter::toString(x) + ", " + Ogre::StringConverter::toString(y)); + owner->queueColliderRemove(x, y); } return true; } -bool TerrainSystem::DummyPageProvider::unprepareProceduralPage( - Ogre::Page *, Ogre::PagedWorldSection *) -{ - return true; -} - -/* ------------------------------------------------------------------ */ -/* CustomTerrainDefiner — generates height procedurally */ /* ------------------------------------------------------------------ */ static float proceduralHeight(long worldX, long worldZ) { - /* Simple procedural terrain: rolling hills using layered sine waves. - * The heights center around 0 so the terrain sits on the Y=0 plane. */ float h = 0.0f; - - /* Large-scale hills (amplitude ±15) */ h += 15.0f * sinf((float)worldX * 0.005f) * cosf((float)worldZ * 0.005f); - - /* Medium bumps (amplitude ±5) */ h += 5.0f * sinf((float)worldX * 0.02f + 1.3f) * cosf((float)worldZ * 0.02f + 0.7f); - - /* Fine detail (amplitude ±2) */ h += 2.0f * sinf((float)worldX * 0.05f + 2.1f) * sinf((float)worldZ * 0.05f + 0.3f); - return h; } @@ -93,20 +89,14 @@ void TerrainSystem::CustomTerrainDefiner::define(Ogre::TerrainGroup *group, float *heightMap = OGRE_ALLOC_T(float, terrainSize * terrainSize, Ogre::MEMCATEGORY_GEOMETRY); - /* convertTerrainSlotToWorldPosition returns the MINIMUM CORNER - * of page (x,y). Vertex (i,j) is at: - * wx = worldPos.x + i * worldSize / (terrainSize - 1) - * wz = worldPos.z + j * worldSize / (terrainSize - 1) */ 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); + 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); } @@ -120,10 +110,182 @@ void TerrainSystem::CustomTerrainDefiner::define(Ogre::TerrainGroup *group, Ogre::StringConverter::toString(y) + ")"); OGRE_FREE(heightMap, Ogre::MEMCATEGORY_GEOMETRY); + + m_sys->queueColliderCreate(x, y); } /* ------------------------------------------------------------------ */ -/* activate — build all Ogre terrain singletons */ + +JPH::ShapeRefC TerrainSystem::buildPageCollider(Ogre::Terrain *terrain) +{ + uint16_t size = terrain->getSize(); + float *heightData = terrain->getHeightData(); + Ogre::Real worldSize = terrain->getWorldSize(); + float scale = worldSize / (float)(size - 1); + + JPH::TriangleList triangles; + triangles.reserve((size - 1) * (size - 1) * 2); + + for (int z = 0; z < size - 1; ++z) { + 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 h00 = heightData[z * size + x]; + float h10 = heightData[z * size + x + 1]; + float h01 = heightData[(z + 1) * size + x]; + float h11 = heightData[(z + 1) * size + x + 1]; + + JPH::Float3 v00(vx0, h00, vz0); + JPH::Float3 v10(vx1, h10, vz0); + JPH::Float3 v01(vx0, h01, vz1); + 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( + JPH::Triangle(v10, v11, v01, 0)); + } + } + } + + JPH::MeshShapeSettings settings(triangles); + auto result = settings.Create(); + if (result.HasError()) { + Ogre::LogManager::getSingleton().logMessage( + "Terrain: MeshShape creation failed: " + + Ogre::String(result.GetError().c_str())); + return nullptr; + } + return result.Get(); +} + +/* ------------------------------------------------------------------ */ + +void TerrainSystem::queueColliderCreate(long x, long y) +{ + mColliderCreateQueue.push_back({ x, y }); +} + +void TerrainSystem::queueColliderRemove(long x, long y) +{ + uint64_t key = mTerrainGroup->packIndex(x, y); + auto it = mColliders.find(key); + if (it != mColliders.end()) + it->second.pendingRemove = true; +} + +void TerrainSystem::processColliderCreates() +{ + if (!m_physics || !mTerrainGroup) + return; + + while (!mColliderCreateQueue.empty()) { + 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()) + continue; + + Ogre::Terrain *terrain = mTerrainGroup->getTerrain(x, y); + if (!terrain || !terrain->isLoaded()) + continue; + + JPH::ShapeRefC shape = buildPageCollider(terrain); + if (!shape) + continue; + + /* Body at terrain scene node position. */ + Ogre::Vector3 bodyPos = + terrain->_getRootSceneNode()->_getDerivedPosition(); + + JPH::BodyCreationSettings bodySettings( + shape.GetPtr(), + JPH::RVec3(bodyPos.x, bodyPos.y, bodyPos.z), + JPH::Quat::sIdentity(), + JPH::EMotionType::Static, + Layers::NON_MOVING); + JPH::BodyID bodyId = m_physics->createBody(bodySettings); + if (bodyId.IsInvalid()) + continue; + + m_physics->addBody(bodyId, JPH::EActivation::DontActivate); + + mColliders[key] = { x, y, bodyId, false }; + mBodyDrawFilter.addTerrainBody(bodyId); + + Ogre::LogManager::getSingleton().logMessage( + "Terrain: collider created for page (" + + Ogre::StringConverter::toString(x) + ", " + + Ogre::StringConverter::toString(y) + ")"); + } +} + +void TerrainSystem::processColliderRemoves() +{ + if (!m_physics || !mTerrainGroup) + return; + + std::vector toRemove; + for (auto &pair : mColliders) { + if (!pair.second.pendingRemove) + continue; + + uint64_t key = pair.first; + 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()) + continue; + + mBodyDrawFilter.removeTerrainBody(pair.second.bodyId); + m_physics->removeBody(pair.second.bodyId); + m_physics->destroyBody(pair.second.bodyId); + toRemove.push_back(key); + + Ogre::LogManager::getSingleton().logMessage( + "Terrain: collider removed for page (" + + Ogre::StringConverter::toString(x) + ", " + + Ogre::StringConverter::toString(y) + ")"); + } + for (auto key : toRemove) + mColliders.erase(key); +} + +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(); + mColliderCreateQueue.clear(); + mBodyDrawFilter.clear(); +} + /* ------------------------------------------------------------------ */ void TerrainSystem::activate(TerrainComponent &tc, TransformComponent &xform) @@ -131,24 +293,20 @@ void TerrainSystem::activate(TerrainComponent &tc, TransformComponent &xform) Ogre::LogManager::getSingleton().logMessage( "TerrainSystem: activating terrain..."); - /* 1 — TerrainGlobalOptions */ mTerrainGlobals = OGRE_NEW Ogre::TerrainGlobalOptions(); mTerrainGlobals->setMaxPixelError(tc.maxPixelError); mTerrainGlobals->setCompositeMapDistance(tc.compositeMapDistance); - mTerrainGlobals->setCompositeMapAmbient( - m_sceneMgr->getAmbientLight()); + mTerrainGlobals->setCompositeMapAmbient(m_sceneMgr->getAmbientLight()); Ogre::TerrainMaterialGeneratorPtr matGen( new Ogre::TerrainMaterialGeneratorA()); mTerrainGlobals->setDefaultMaterialGenerator(matGen); - /* 2 — TerrainGroup */ mTerrainGroup = OGRE_NEW Ogre::TerrainGroup( m_sceneMgr, Ogre::Terrain::ALIGN_X_Z, tc.terrainSize, tc.worldSize); mTerrainGroup->setOrigin(xform.position); - /* 3 — ImportData defaults */ Ogre::Terrain::ImportData &defaultImp = mTerrainGroup->getDefaultImportSettings(); defaultImp.terrainSize = tc.terrainSize; @@ -156,13 +314,8 @@ void TerrainSystem::activate(TerrainComponent &tc, TransformComponent &xform) defaultImp.minBatchSize = tc.minBatchSize; defaultImp.maxBatchSize = tc.maxBatchSize; defaultImp.inputScale = 1.0f; - - /* Layer declaration from the material generator. */ defaultImp.layerDeclaration = matGen->getLayerDeclaration(); - /* Build layer instances from the component. For M1, if no layers - * are configured, create a single default dirt/rock layer so the - * terrain is visible. */ if (tc.layers.empty()) { Ogre::Terrain::LayerInstance li; li.worldSize = 100.0f; @@ -179,18 +332,15 @@ void TerrainSystem::activate(TerrainComponent &tc, TransformComponent &xform) } } - /* 4 — PageManager + camera */ mPageManager = OGRE_NEW Ogre::PageManager(); mDummyPageProvider = std::make_unique(); mDummyPageProvider->owner = this; mPageManager->setPageProvider(mDummyPageProvider.get()); mPageManager->addCamera(m_camera); - /* 5 — TerrainPaging → PagedWorld → section */ mTerrainPaging = OGRE_NEW Ogre::TerrainPaging(mPageManager); mPagedWorld = mPageManager->createWorld(); - /* Set page range — start with a modest area for M1. */ const long pageMinX = -2, pageMinY = -2; const long pageMaxX = 2, pageMaxY = 2; @@ -198,23 +348,21 @@ void TerrainSystem::activate(TerrainComponent &tc, TransformComponent &xform) mPagedWorld, mTerrainGroup, 300.0f, 500.0f, pageMinX, pageMinY, pageMaxX, pageMaxY); - /* 6 — TerrainDefiner */ mTerrainDefiner = std::make_unique(this); mTerrainPagedWorldSection->setDefiner(mTerrainDefiner.get()); - /* 7 — Load initial pages synchronously (editor mode). */ mTerrainGroup->freeTemporaryResources(); 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) @@ -225,36 +373,29 @@ void TerrainSystem::deactivate() m_active = false; - /* Wait for background work to finish. */ if (mTerrainGroup && mTerrainGroup->isDerivedDataUpdateInProgress()) { - Ogre::LogManager::getSingleton().logMessage( - "TerrainSystem: waiting for derived data update..."); do { mTerrainGroup->update(false); } while (mTerrainGroup->isDerivedDataUpdateInProgress()); } - /* Remove camera from page manager. */ + removeAllColliders(); + if (m_physics) + m_physics->setBodyDrawFilter(nullptr); + if (mPageManager && m_camera) mPageManager->removeCamera(m_camera); - /* Destroy in correct order. */ mTerrainDefiner.reset(); - - if (mTerrainGroup) { + if (mTerrainGroup) mTerrainGroup->removeAllTerrains(); - } - /* TerrainPaging holds ref to PageManager — destroy first. */ OGRE_DELETE mTerrainPaging; mTerrainPaging = nullptr; - OGRE_DELETE mPageManager; mPageManager = nullptr; - OGRE_DELETE mTerrainGroup; mTerrainGroup = nullptr; - OGRE_DELETE mTerrainGlobals; mTerrainGlobals = nullptr; @@ -266,13 +407,10 @@ void TerrainSystem::deactivate() "TerrainSystem: deactivated"); } -/* ------------------------------------------------------------------ */ -/* update */ /* ------------------------------------------------------------------ */ void TerrainSystem::update(float /*deltaTime*/) { - /* Check if a terrain entity appeared. */ m_terrainQuery.each([&](flecs::entity e, TerrainComponent &tc, TransformComponent &xform) { (void)e; @@ -284,20 +422,59 @@ void TerrainSystem::update(float /*deltaTime*/) activate(tc, xform); }); - /* Pump the terrain group — drives LOD, page load/unload. */ - if (m_active && mTerrainGroup) { + if (m_active && mTerrainGroup) mTerrainGroup->update(false); - } + + processColliderRemoves(); + processColliderCreates(); + + mBodyDrawFilter.showTerrain = m_showTerrainColliders; } -/* ------------------------------------------------------------------ */ -/* getHeightAt */ /* ------------------------------------------------------------------ */ float TerrainSystem::getHeightAt(const Ogre::Vector3 &worldPos) const { if (!m_active || !mTerrainGroup) return 0.0f; - return mTerrainGroup->getHeightAtWorldPosition(worldPos); } + +bool TerrainSystem::raycastTerrain(const Ogre::Ray &ray, float &outT, + Ogre::Vector3 &outNormal) const +{ + if (!m_active || !m_physics || mColliders.empty()) + return false; + + 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); + + JPH::PhysicsSystem *jphSys = m_physics->getPhysicsSystem(); + if (!jphSys) + return false; + + JPH::RayCastResult hit; + if (!jphSys->GetNarrowPhaseQuery().CastRay(rayCast, hit, + JPH::BroadPhaseLayerFilter(), JPH::ObjectLayerFilter(), + JPH::BodyFilter())) + return false; + + for (auto &pair : mColliders) { + if (pair.second.bodyId == hit.mBodyID) { + outT = hit.mFraction; + outNormal = Ogre::Vector3::UNIT_Y; + return true; + } + } + return false; +} + +void TerrainSystem::setShowTerrainColliders(bool show) +{ + m_showTerrainColliders = show; +} diff --git a/src/features/editScene/systems/TerrainSystem.hpp b/src/features/editScene/systems/TerrainSystem.hpp index f23f4e9..5e7493d 100644 --- a/src/features/editScene/systems/TerrainSystem.hpp +++ b/src/features/editScene/systems/TerrainSystem.hpp @@ -13,65 +13,87 @@ #include #include #include +#include +#include #include -// Forward declarations -namespace JPH { - class BodyID; -} +#include +#include +#include +#include + +class JoltPhysicsWrapper; -/** - * Singleton system that owns all Ogre terrain objects (TerrainGroup, - * TerrainPaging, PageManager, etc.) and the Jolt collision state. - * - * Created by EditorApp in setup(), one instance per scene. Activation - * happens when an entity with TerrainComponent + TransformComponent - * appears in the ECS world. - */ class TerrainSystem { public: TerrainSystem(flecs::world &world, Ogre::SceneManager *sceneMgr, - Ogre::Camera *camera); + Ogre::Camera *camera, JoltPhysicsWrapper *physics); ~TerrainSystem(); TerrainSystem(const TerrainSystem &) = delete; TerrainSystem &operator=(const TerrainSystem &) = delete; - /** Per-frame update — pump terrain group, process collider queues. */ + static TerrainSystem *getInstance() { return s_instance; } + void update(float deltaTime); - - /** Deactivate terrain: remove colliders, unload pages, destroy - * Ogre singletons. Called on scene clear. */ void deactivate(); - - /** Return true when terrain is active and rendering. */ bool isActive() const { return m_active; } - - /** Get terrain height at world position (for raycasts/query). */ float getHeightAt(const Ogre::Vector3 &worldPos) const; + bool raycastTerrain(const Ogre::Ray &ray, float &outT, + Ogre::Vector3 &outNormal) const; + + void setShowTerrainColliders(bool show); + bool getShowTerrainColliders() const + { + return m_showTerrainColliders; + } + private: - /** Internal: build all Ogre terrain singletons from the component. */ void activate(struct TerrainComponent &tc, struct TransformComponent &xform); - /** Dummy page provider — satisfies PageManager without disk I/O. */ + struct TerrainCollider { + long pageX, pageY; + JPH::BodyID bodyId; + bool pendingRemove = false; + }; + + class TerrainBodyDrawFilter : public JPH::BodyDrawFilter { + public: + bool ShouldDraw(const JPH::Body &inBody) const override; + void addTerrainBody(const JPH::BodyID &id) + { + m_terrainIds.insert(id); + } + void removeTerrainBody(const JPH::BodyID &id) + { + m_terrainIds.erase(id); + } + void clear() { m_terrainIds.clear(); } + bool showTerrain = false; + private: + std::set m_terrainIds; + }; + class DummyPageProvider : public Ogre::PageProvider { public: - bool prepareProceduralPage(Ogre::Page *page, - Ogre::PagedWorldSection *section) override; - bool loadProceduralPage(Ogre::Page *page, - Ogre::PagedWorldSection *section) override; - bool unloadProceduralPage(Ogre::Page *page, - Ogre::PagedWorldSection *section) override; - bool unprepareProceduralPage(Ogre::Page *page, - Ogre::PagedWorldSection *section) override; - + 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; }; - /** Custom terrain definer — generates height from procedural - * function or base heightmap. */ class CustomTerrainDefiner : public Ogre::TerrainPagedWorldSection::TerrainDefiner { public: @@ -80,20 +102,27 @@ private: , m_sys(sys) { } - - void define(Ogre::TerrainGroup *terrainGroup, long x, - long y) override; - + 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(); + flecs::world &m_world; Ogre::SceneManager *m_sceneMgr; Ogre::Camera *m_camera; + JoltPhysicsWrapper *m_physics = nullptr; bool m_active = false; + bool m_showTerrainColliders = false; + + static TerrainSystem *s_instance; - // Ogre terrain singletons Ogre::TerrainGlobalOptions *mTerrainGlobals = nullptr; Ogre::TerrainGroup *mTerrainGroup = nullptr; Ogre::PageManager *mPageManager = nullptr; @@ -103,9 +132,12 @@ private: std::unique_ptr mTerrainDefiner; std::unique_ptr mDummyPageProvider; - // Query for the active terrain entity + std::unordered_map mColliders; + std::deque> mColliderCreateQueue; + TerrainBodyDrawFilter mBodyDrawFilter; + flecs::query m_terrainQuery; }; -#endif // EDITSCENE_TERRAINSYSTEM_HPP +#endif diff --git a/src/features/editScene/ui/TerrainEditor.hpp b/src/features/editScene/ui/TerrainEditor.hpp index 1c1fe22..8d12aff 100644 --- a/src/features/editScene/ui/TerrainEditor.hpp +++ b/src/features/editScene/ui/TerrainEditor.hpp @@ -4,6 +4,7 @@ #include "ComponentEditor.hpp" #include "../components/Terrain.hpp" +#include "../systems/TerrainSystem.hpp" /** * Editor panel for TerrainComponent. @@ -11,6 +12,9 @@ * Milestone 1: minimal — enable/disable toggle and display of * key parameters. Heightmap editing, road tools, splat painting * come in later milestones. + * + * Milestone 2: "Show Terrain Colliders" checkbox (runtime-only, + * not serialized). */ class TerrainEditor : public ComponentEditor { public: @@ -25,6 +29,14 @@ public: if (ImGui::Checkbox("Enabled", &tc.enabled)) changed = true; + /* Show Terrain Colliders (runtime-only, not saved). */ + TerrainSystem *ts = TerrainSystem::getInstance(); + if (ts) { + bool show = ts->getShowTerrainColliders(); + if (ImGui::Checkbox("Show Terrain Colliders", &show)) + ts->setShowTerrainColliders(show); + } + ImGui::Separator(); /* Read-only display of sizing params (restart needed to change).