Terrain colliders work now!
This commit is contained in:
@@ -362,7 +362,7 @@ void EditorApp::setup()
|
||||
{
|
||||
Ogre::Camera *cam = m_camera ? m_camera->getCamera() : nullptr;
|
||||
m_terrainSystem = std::make_unique<TerrainSystem>(
|
||||
m_world, m_sceneMgr, cam);
|
||||
m_world, m_sceneMgr, cam, m_physicsSystem->getPhysicsWrapper());
|
||||
}
|
||||
|
||||
// Apply debug setting if it was set before system creation
|
||||
|
||||
@@ -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<uint64_t, TerrainCollider>` 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).
|
||||
|
||||
@@ -564,6 +564,7 @@ class Physics {
|
||||
std::set<JPH::Character *> characters;
|
||||
std::set<JPH::BodyID> characterBodies;
|
||||
bool debugDraw;
|
||||
JPH::BodyDrawFilter *mBodyDrawFilter = nullptr;
|
||||
JPH::Vec3 gravity = JPH::Vec3(0.0f, -9.8f, 0.0f);
|
||||
std::unordered_map<uint32_t, JPH::Ref<JPH::GroupFilterTable> > 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());
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <Ogre.h>
|
||||
#include <OgreSingleton.h>
|
||||
#include <Jolt/Jolt.h>
|
||||
#include <Jolt/Physics/Body/BodyFilter.h>
|
||||
#include <Jolt/Physics/Collision/Shape/Shape.h>
|
||||
#include <Jolt/Physics/Collision/Shape/StaticCompoundShape.h>
|
||||
#include <Jolt/Physics/Collision/ObjectLayer.h>
|
||||
@@ -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,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "TerrainSystem.hpp"
|
||||
#include "../components/Terrain.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
#include "../physics/physics.h"
|
||||
|
||||
#include <OgreTerrain.h>
|
||||
#include <OgreTerrainGroup.h>
|
||||
@@ -8,37 +9,48 @@
|
||||
#include <OgreTerrainPagedWorldSection.h>
|
||||
#include <OgreTerrainMaterialGeneratorA.h>
|
||||
#include <OgreLogManager.h>
|
||||
|
||||
#include <Jolt/Jolt.h>
|
||||
#include <Jolt/Physics/Collision/Shape/MeshShape.h>
|
||||
#include <Jolt/Physics/Body/BodyCreationSettings.h>
|
||||
#include <Jolt/Physics/Collision/RayCast.h>
|
||||
#include <Jolt/Physics/Collision/CastResult.h>
|
||||
#include <Jolt/Physics/PhysicsSystem.h>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
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<TerrainComponent, TransformComponent>())
|
||||
{
|
||||
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<uint64_t> 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<DummyPageProvider>();
|
||||
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<CustomTerrainDefiner>(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;
|
||||
}
|
||||
|
||||
@@ -13,65 +13,87 @@
|
||||
#include <OgrePagedWorld.h>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <set>
|
||||
#include <deque>
|
||||
#include <cstdint>
|
||||
|
||||
// Forward declarations
|
||||
namespace JPH {
|
||||
class BodyID;
|
||||
}
|
||||
#include <Jolt/Jolt.h>
|
||||
#include <Jolt/Physics/Body/BodyFilter.h>
|
||||
#include <Jolt/Physics/Body/Body.h>
|
||||
#include <Jolt/Physics/Collision/Shape/Shape.h>
|
||||
|
||||
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<JPH::BodyID> 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<CustomTerrainDefiner> mTerrainDefiner;
|
||||
std::unique_ptr<DummyPageProvider> mDummyPageProvider;
|
||||
|
||||
// Query for the active terrain entity
|
||||
std::unordered_map<uint64_t, TerrainCollider> mColliders;
|
||||
std::deque<std::pair<long, long>> mColliderCreateQueue;
|
||||
TerrainBodyDrawFilter mBodyDrawFilter;
|
||||
|
||||
flecs::query<struct TerrainComponent, struct TransformComponent>
|
||||
m_terrainQuery;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_TERRAINSYSTEM_HPP
|
||||
#endif
|
||||
|
||||
@@ -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<TerrainComponent> {
|
||||
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).
|
||||
|
||||
Reference in New Issue
Block a user