Compare commits

...

3 Commits

Author SHA1 Message Date
slapin 31864e3da5 Terrain colliders work now! 2026-06-27 02:22:03 +03:00
slapin 3b00e0cc47 Updated AnimationTree component to use Animation Tree Registry 2026-06-27 00:14:09 +03:00
slapin 4b46b1a8e0 Update terrain plan 2026-06-27 00:13:04 +03:00
8 changed files with 439 additions and 131 deletions
+10
View File
@@ -229,6 +229,8 @@ The editor/game mode executable adds its own ECS modules:
| EditorUISystem | ImGui property panels and scene management |
| ProceduralTextureSystem | Runtime procedural texture generation |
| ProceduralMaterialSystem | Runtime procedural material creation |
| AnimationTreeRegistry | Global named animation tree definitions |
| AnimationTreeSystem | Runtime evaluation of registry-referenced animation trees |
#### PlayerControllerSystem
@@ -276,6 +278,14 @@ One-shot actions (`E`, `F`, `I`, `Escape`) are still matched by `keysym.sym`.
The spawner uses the entity's `TransformComponent` for spawn position/rotation. When the spawner itself moves/rotates/scales in the editor, the spawned character is updated to match; when the spawner is stationary, the character is left alone so it can move on its own. Spawned characters are created without `EditorMarkerComponent` and removed from the editor UI cache so they remain visible but are not selectable/editable in the editor. Registry changes that bump the character version trigger an immediate respawn. Scene serialization stores plain `spawnDistance`/`despawnDistance` values for readability while the component keeps squared values for comparisons.
#### AnimationTreeComponent
- `treeName` - Name of the animation tree in `AnimationTreeRegistry`.
- `enabled` - Whether the tree is evaluated.
- `useRootMotion` - Whether root motion from the animation drives the character's linear velocity.
The actual tree definition (node hierarchy, state machines, animations) lives in the `AnimationTreeRegistry`, persisted to `animation_tree.json`. Trees are authored in the registry editor (**Tools -> Animation Tree Registry**), where each entry can also select a skeleton source from the `Characters` resource group (one representative mesh per skeleton) to populate animation-name dropdowns. Old scenes and prefabs that stored the tree inline or referenced an `AnimationTreeTemplate` are migrated automatically on load.
### Physics System
Uses Jolt Physics with custom wrapper (legacy - `src/physics/physics.h`, current - `src/features/editScene/physics/physics.h`):
+1 -1
View File
@@ -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
+75 -13
View File
@@ -432,10 +432,9 @@ m_world.component<TerrainComponent>();
**In `EditorApp::setup()`** — construct after physics but before water/sun/skybox
(terrain must exist before those systems try to interact with it):
```cpp
m_terrainSystem = std::make_unique<TerrainSystem>(
m_world, m_sceneMgr, m_physicsSystem.get());
```
Ogre::Camera *cam = m_camera ? m_camera->getCamera() : nullptr;
m_terrainSystem = std::make_unique<TerrainSystem>(
m_world, m_sceneMgr, cam, m_physicsSystem->getPhysicsWrapper());
**In `EditorApp::frameRenderingQueued()`** — call update **before**
static-world generation and **after** visual mesh setup. Insert right after
@@ -1012,6 +1011,8 @@ Minimum controls:
- Add/remove/split/join/select nodes.
- Inspector for node position, offset, edge levels.
- "Comply terrain to roads" button.
- Physics debug: "Show Terrain Colliders" checkbox (off by default,
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.
@@ -1144,16 +1145,77 @@ 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
- Implement `ZigzagHeightfieldShape` as a `JPH::MeshShape` per page with zigzag
triangulation matching Ogre.
- Queue collider creation after page load and derived-data completion.
- Maintain `mColliders` map; safe remove/re-add on scene clear or component
removal.
- Add raycast helper to `TerrainSystem` for gameplay/editor tools.
### Milestone 2 — Physics integration ✅ DONE
Definition of done: rigid bodies and characters collide flush with the visible
terrain; removing/re-adding the terrain entity is leak-free.
**Changes to TerrainSystem constructor**: Now takes `JoltPhysicsWrapper*` so
it can create/destroy static terrain collider bodies directly in the Jolt world.
```cpp
TerrainSystem(flecs::world &world, Ogre::SceneManager *sceneMgr,
Ogre::Camera *camera, JoltPhysicsWrapper *physics);
```
**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`.
**mColliders map**: `std::unordered_map<uint64_t, TerrainCollider>` keyed by
`packIndex(x, y)`. `TerrainCollider` holds `{pageX, pageY, JPH::BodyID,
pendingRemove}`.
**Collider removal queue**: `DummyPageProvider::unloadProceduralPage` marks the
collider `pendingRemove`. `processColliderRemoves()` destroys bodies in
`update()` after derived data is idle.
**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**:
- `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**:
- `getHeightAt()` uses Ogre terrain for height queries.
- `raycastTerrain()` casts `JPH::RRayCast` via `NarrowPhaseQuery::CastRay`
against all loaded terrain page bodies.
**Testing**:
- [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.
- [x] Colliders are NOT visible in debug draw by default.
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).
+14 -1
View File
@@ -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());
+2
View File
@@ -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,
+252 -75
View File
@@ -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).