Planning Terrain
This commit is contained in:
@@ -0,0 +1,653 @@
|
||||
# Terrain System Implementation Plan
|
||||
|
||||
## Goal
|
||||
Add a robust, editor-friendly `Ogre::Terrain`-based terrain feature to the
|
||||
`editScene` editor/game executable using the current ECS architecture in
|
||||
`src/features/editScene`.
|
||||
|
||||
## Non-Goals (for this plan)
|
||||
- Replacing the legacy `src/gamedata/TerrainModule.cpp` (it is kept as
|
||||
reference only).
|
||||
- Multi-player networking.
|
||||
- Runtime terrain streaming on a separate thread outside Ogre's work queue.
|
||||
|
||||
## High-Level Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ editScene executable │
|
||||
│ │
|
||||
│ ┌────────────────────┐ ┌────────────────────┐ ┌─────────────────┐ │
|
||||
│ │ TerrainComponent │────▶│ TerrainSystem │────▶│ Ogre::Terrain │ │
|
||||
│ │ (parameters only) │ │ (singleton owner) │ │ Ogre::TerrainGroup│
|
||||
│ └────────────────────┘ └────────────────────┘ │ Ogre::TerrainPaging│
|
||||
│ │ │ │ Ogre::PageManager │
|
||||
│ │ ▼ └─────────────────┘ │
|
||||
│ │ ┌────────────────────┐ │
|
||||
│ │ │ CustomTerrainDefiner │ │
|
||||
│ │ │ - height function f(int XZ) -> float │ │
|
||||
│ │ │ - 256x256 base heightmap │ │
|
||||
│ │ │ - sparse 256x256 fixup chunks │ │
|
||||
│ │ └────────────────────┘ │
|
||||
│ │ │ │
|
||||
│ │ ▼ │
|
||||
│ │ ┌────────────────────┐ │
|
||||
│ │ │ ZigzagHeightfieldShape │ │
|
||||
│ │ │ - Jolt custom collision shape │ │
|
||||
│ │ │ - matches Ogre::Terrain row zigzag │ │
|
||||
│ │ └────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────┐ │
|
||||
│ │ TerrainEditor │ - heightmap brushes │
|
||||
│ │ (ImGui panel) │ - road node graph add/split/join/select │
|
||||
│ │ │ - terrain restart for size changes │
|
||||
│ └────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Files to Create / Modify
|
||||
|
||||
| Type | Path | Purpose |
|
||||
|------|------|---------|
|
||||
| Component | `src/features/editScene/components/Terrain.hpp` | Parameters-only component |
|
||||
| Module | `src/features/editScene/components/TerrainModule.cpp` | ECS component registration |
|
||||
| Editor | `src/features/editScene/ui/TerrainEditor.hpp` | ImGui property/editor panel |
|
||||
| System | `src/features/editScene/systems/TerrainSystem.hpp` | System declaration |
|
||||
| System | `src/features/editScene/systems/TerrainSystem.cpp` | System implementation |
|
||||
| Physics | `src/features/editScene/physics/ZigzagHeightfieldShape.hpp` | Custom Jolt shape header |
|
||||
| Physics | `src/features/editScene/physics/ZigzagHeightfieldShape.cpp` | Custom Jolt shape implementation |
|
||||
| App wiring | `src/features/editScene/EditorApp.hpp/.cpp` | Construct/update/destroy system |
|
||||
| Serialization | `src/features/editScene/systems/SceneSerializer.hpp/.cpp` | Save/load terrain data |
|
||||
| Build | `src/features/editScene/CMakeLists.txt` | Add files + `OgrePaging`/`OgreTerrain` links |
|
||||
|
||||
## 1. TerrainComponent (parameters only)
|
||||
|
||||
```cpp
|
||||
struct TerrainComponent {
|
||||
bool enabled = true;
|
||||
|
||||
// Page/tile vertex resolution. Must be 2^n+1 and <= 65 (Ogre limit).
|
||||
int terrainSize = 65;
|
||||
|
||||
// Base binary heightmap resolution. Default 256x256.
|
||||
int heightmapSize = 256;
|
||||
|
||||
// World-space size of one Ogre terrain page.
|
||||
float worldSize = 40000000.0f;
|
||||
|
||||
// Maximum geometric error in pixels before a LOD tile splits.
|
||||
float maxPixelError = 1.0f;
|
||||
|
||||
// Distance at which the cheap composite map is used.
|
||||
float compositeMapDistance = 300.0f;
|
||||
|
||||
// Minimum and maximum batch LOD sizes. Must be 2^n+1 and <= terrainSize.
|
||||
int minBatchSize = 17;
|
||||
int maxBatchSize = 65;
|
||||
|
||||
// Base heightmap binary file path (project-relative or absolute).
|
||||
std::string heightmapFile = "heightmap.bin";
|
||||
|
||||
// Layer texture settings (diffuse+normal pairs).
|
||||
struct Layer {
|
||||
std::string diffuseTexture;
|
||||
std::string normalTexture;
|
||||
float worldSize = 100.0f;
|
||||
};
|
||||
std::vector<Layer> layers;
|
||||
|
||||
// Road network data (node/edge graph).
|
||||
struct RoadNode {
|
||||
Ogre::Vector3 position; // default: terrain surface + offset
|
||||
float verticalOffset = 0.0f;
|
||||
int id = 0;
|
||||
};
|
||||
struct RoadEdge {
|
||||
int nodeA = -1;
|
||||
int nodeB = -1;
|
||||
float roadLevelA = 0.0f;
|
||||
float roadLevelB = 0.0f;
|
||||
};
|
||||
std::vector<RoadNode> roadNodes;
|
||||
std::vector<RoadEdge> roadEdges;
|
||||
|
||||
// Prefab spawn points on the terrain.
|
||||
struct PrefabSpawn {
|
||||
std::string prefabName;
|
||||
Ogre::Vector3 position;
|
||||
Ogre::Quaternion rotation;
|
||||
float spawnDistance = 100.0f;
|
||||
float despawnDistance = 200.0f;
|
||||
};
|
||||
std::vector<PrefabSpawn> prefabSpawns;
|
||||
|
||||
// Runtime-only: managed by TerrainSystem. Not serialized.
|
||||
bool dirty = true;
|
||||
bool rebuildInProgress = false;
|
||||
|
||||
void markDirty() { dirty = true; }
|
||||
};
|
||||
```
|
||||
|
||||
Rules:
|
||||
- Only serializable parameters and a transient `dirty` flag live here.
|
||||
- `terrainSize`, `heightmapSize`, `worldSize` changes require a full terrain
|
||||
restart in editor mode.
|
||||
- Runtime objects (`Ogre::Terrain*`, Jolt bodies, scene nodes) must live in
|
||||
`TerrainSystem`, not in the component.
|
||||
|
||||
## 2. TerrainSystem (singleton owner)
|
||||
|
||||
One `TerrainSystem` instance is created by `EditorApp`. It owns all Ogre
|
||||
terrain singletons and the Jolt collision state for the active scene.
|
||||
|
||||
Owned objects:
|
||||
|
||||
```cpp
|
||||
Ogre::TerrainGlobalOptions* mTerrainGlobals = nullptr;
|
||||
Ogre::TerrainGroup* mTerrainGroup = nullptr;
|
||||
Ogre::PageManager* mPageManager = nullptr;
|
||||
Ogre::TerrainPaging* mTerrainPaging = nullptr;
|
||||
Ogre::PagedWorld* mPagedWorld = nullptr;
|
||||
Ogre::TerrainPagedWorldSection* mTerrainPagedWorldSection = nullptr;
|
||||
std::unique_ptr<CustomTerrainDefiner> mTerrainDefiner;
|
||||
```
|
||||
|
||||
Per-page physics state:
|
||||
|
||||
```cpp
|
||||
struct TerrainCollider {
|
||||
long x, y;
|
||||
JPH::BodyID bodyId;
|
||||
bool pendingRemove = false;
|
||||
};
|
||||
std::unordered_map<uint64_t, TerrainCollider> mColliders;
|
||||
```
|
||||
|
||||
### 2.1 Lifecycle
|
||||
|
||||
1. **Construction** (`EditorApp::setup()`)
|
||||
- Create `TerrainSystem` after `PhysicsSystem` but before static geometry
|
||||
systems so raycasts and navmesh generation can use terrain.
|
||||
|
||||
2. **Activation** (when an entity with `TerrainComponent` + `TransformComponent`
|
||||
is found)
|
||||
- Build `TerrainGlobalOptions`, `TerrainGroup`, `PageManager`,
|
||||
`TerrainPaging`, `PagedWorld`, `TerrainPagedWorldSection`.
|
||||
- Configure default `Ogre::Terrain::ImportData` from component values.
|
||||
- Set the custom `CustomTerrainDefiner`.
|
||||
- Call `loadAllTerrains(true)` for editor-mode synchronous setup, or rely on
|
||||
paging for game mode.
|
||||
|
||||
3. **Deactivation / scene clear**
|
||||
- Wait until `mTerrainGroup->isDerivedDataUpdateInProgress()` is `false`.
|
||||
- Remove all Jolt bodies first.
|
||||
- Unload and destroy pages.
|
||||
- Destroy `TerrainPaging`/`PageManager`/`TerrainGroup`/`TerrainGlobalOptions`
|
||||
in that order.
|
||||
|
||||
4. **Destruction** (`EditorApp::~EditorApp()`)
|
||||
- Destroy `TerrainSystem` **before** `PhysicsSystem` and the
|
||||
`Ogre::SceneManager`.
|
||||
|
||||
### 2.2 Per-frame update
|
||||
|
||||
Call `TerrainSystem::update(dt)` from `EditorApp::frameRenderingQueued()`
|
||||
**before** static-world generation:
|
||||
|
||||
```cpp
|
||||
if (m_terrainSystem) {
|
||||
m_terrainSystem->update(evt.timeSinceLastFrame);
|
||||
}
|
||||
```
|
||||
|
||||
Inside `update()`:
|
||||
|
||||
1. `mTerrainGroup->update(false)` (only if no derived data update is in
|
||||
progress).
|
||||
2. Process pending collider creation queue: for each loaded page with no active
|
||||
collider, build a `ZigzagHeightfieldShape` and add the static Jolt body.
|
||||
3. Process pending collider removals: only remove bodies when the corresponding
|
||||
page is unloaded and no derived data update is running.
|
||||
|
||||
### 2.3 Safety rules for background jobs
|
||||
|
||||
`Ogre::Terrain` performs `prepare()`, LOD streaming, and derived-data updates on
|
||||
Ogre's `WorkQueue`. The following rules prevent crashes, leaks, and use-after-free:
|
||||
|
||||
- Never remove or hide a `Terrain` page while
|
||||
`terrain->isDerivedDataUpdateInProgress()` is `true`.
|
||||
- Never remove the `TerrainComponent` entity while
|
||||
`mTerrainGroup->isDerivedDataUpdateInProgress()` is `true`; defer removal to
|
||||
the next frame.
|
||||
- Always pump `mTerrainGroup->update(false)` each frame so pending background
|
||||
work can finish.
|
||||
- The `TerrainGroup` destructor already waits for pending `prepare()` requests,
|
||||
but explicit draining avoids surprises.
|
||||
- Do not create a Jolt heightfield body for a page until `terrain->isLoaded()`
|
||||
is `true` and derived data is idle.
|
||||
|
||||
### 2.4 Height function
|
||||
|
||||
The height function is implemented inside `CustomTerrainDefiner::define()`:
|
||||
|
||||
```cpp
|
||||
float getHeightAtWorld(int worldX, int worldZ);
|
||||
```
|
||||
|
||||
Input:
|
||||
- Integer XZ world coordinates (not normalized, not page-local).
|
||||
|
||||
Output:
|
||||
- World-space Y height.
|
||||
|
||||
Sources (in order of priority):
|
||||
1. **Road fixup heightmap** — sparse 256x256 chunks loaded on demand. If a chunk
|
||||
covers `(worldX, worldZ)` and contains a value, return it (possibly blended
|
||||
with base height).
|
||||
2. **Base heightmap** — 256x256 binary file bilinearly sampled to the requested
|
||||
world size. If out of bounds, return a configurable fallback (e.g., `0.0f`).
|
||||
3. **Procedural detail noise** (optional) — layered Perlin/Simplex noise for
|
||||
fine detail, matching legacy `TerrainModule` behavior.
|
||||
|
||||
Page-local sampling in `define()`:
|
||||
|
||||
```cpp
|
||||
Ogre::Vector3 worldPos;
|
||||
terrainGroup->convertTerrainSlotToWorldPosition(x, y, &worldPos);
|
||||
for (int z = 0; z < terrainSize; ++z) {
|
||||
for (int x = 0; x < terrainSize; ++x) {
|
||||
long wx = (long)(worldPos.x + x - (terrainSize - 1) / 2);
|
||||
long wz = (long)(worldPos.z + z - (terrainSize - 1) / 2);
|
||||
heightMap[z * terrainSize + x] = getHeightAtWorld(wx, wz);
|
||||
}
|
||||
}
|
||||
terrainGroup->defineTerrain(x, y, heightMap);
|
||||
```
|
||||
|
||||
Note: Ogre's internal `getHeightData(x, y)` uses `index = y * size + x`, so the
|
||||
row index in the buffer corresponds to world Z.
|
||||
|
||||
## 3. ZigzagHeightfieldShape (custom Jolt collision shape)
|
||||
|
||||
`JPH::HeightFieldShape` is plain row-major and always splits quads along the
|
||||
same diagonal. `Ogre::Terrain` alternates the diagonal direction per row
|
||||
(zigzag). Bullet solved this with `btHeightfieldTerrainShape::setUseZigzagSubdivision(true)`
|
||||
(issue #625). We must implement an equivalent custom Jolt shape because the
|
||||
Jolt author does not want zigzag behavior in the core heightfield shape.
|
||||
|
||||
### 3.1 Why it matters
|
||||
|
||||
If the physics triangulation does not match the visual triangulation, small
|
||||
rigid bodies and characters can sink into or float above the terrain at the
|
||||
edges of quads, especially on slopes.
|
||||
|
||||
### 3.2 Ogre::Terrain triangulation
|
||||
|
||||
From `OgreTerrain.cpp` (row `z`):
|
||||
|
||||
- Even `z`: diagonal connects `(x, z)` → `(x+1, z+1)` (`\`).
|
||||
- Odd `z`: diagonal connects `(x+1, z)` → `(x, z+1)` (`/`).
|
||||
|
||||
Equivalent to Bullet's `m_useZigzagSubdivision && !(j & 1)`.
|
||||
|
||||
### 3.3 Implementation approach
|
||||
|
||||
Because implementing a full custom `JPH::Shape` is large, the first milestone
|
||||
uses a practical, verifiable approach:
|
||||
|
||||
1. Build a `JPH::MeshShape` per terrain page whose triangles use the exact same
|
||||
zigzag triangulation as `Ogre::Terrain`.
|
||||
2. The mesh vertices are `(x, height[y*size+x], z)` in row-major order.
|
||||
3. Each quad `(x, z)` emits two triangles according to the row parity:
|
||||
- Even row: `(x,z), (x,z+1), (x+1,z+1)` and `(x,z), (x+1,z+1), (x+1,z)`.
|
||||
- Odd row: `(x,z), (x,z+1), (x+1,z)` and `(x,z+1), (x+1,z+1), (x+1,z)`.
|
||||
4. Place the `MeshShape` at the page world position using a
|
||||
`JPH::RotatedTranslatedShape` or by offsetting vertex coordinates.
|
||||
|
||||
If runtime collision performance becomes a problem, a second milestone can
|
||||
replace the `MeshShape` with a true custom `ZigzagHeightfieldShape` derived from
|
||||
`JPH::Shape`, reusing Jolt's triangle-vs-convex helpers
|
||||
(`CollideConvexVsTriangles`, `CastConvexVsTriangles`).
|
||||
|
||||
### 3.4 Custom shape skeleton (future milestone)
|
||||
|
||||
```cpp
|
||||
class ZigzagHeightfieldShapeSettings : public JPH::ShapeSettings {
|
||||
public:
|
||||
uint32_t sampleCount = 0;
|
||||
JPH::Vec3 offset = JPH::Vec3::sZero();
|
||||
JPH::Vec3 scale = JPH::Vec3::sReplicate(1.0f);
|
||||
std::vector<float> heightSamples;
|
||||
|
||||
virtual JPH::ShapeResult Create() const override;
|
||||
};
|
||||
|
||||
class ZigzagHeightfieldShape : public JPH::Shape {
|
||||
public:
|
||||
ZigzagHeightfieldShape(const ZigzagHeightfieldShapeSettings &settings,
|
||||
JPH::ShapeResult &outResult);
|
||||
|
||||
// Required JPH::Shape overrides:
|
||||
// GetLocalBounds, GetSubShapeIDBitsRecursive, GetInnerRadius,
|
||||
// GetMassProperties, GetMaterial, GetSurfaceNormal, CastRay,
|
||||
// CollidePoint, CollideSoftBodyVertices, GetTrianglesStart/Next,
|
||||
// GetStats, GetVolume, GetSubmergedVolume.
|
||||
|
||||
// Zigzag-aware triangle iteration used by collide/cast functions.
|
||||
void VisitTriangles(const JPH::AABB &localBox,
|
||||
const std::function<void(const JPH::Triangle &t)> &fn);
|
||||
};
|
||||
```
|
||||
|
||||
Collision handlers are registered with:
|
||||
|
||||
```cpp
|
||||
CollisionDispatch::sRegisterCollideShape(typeA, typeB, MyCollideFunction);
|
||||
CollisionDispatch::sRegisterCastShape(typeA, typeB, MyCastFunction);
|
||||
```
|
||||
|
||||
Use `EShapeSubType::User1` for the shape subtype.
|
||||
|
||||
## 4. Heightmap Data
|
||||
|
||||
### 4.1 Base heightmap
|
||||
|
||||
- Format: raw binary, `heightmapSize * heightmapSize * sizeof(float)`.
|
||||
- Row-major, same layout as Ogre: `index = y * heightmapSize + x`.
|
||||
- Stored relative to the scene or project (e.g., `scenes/<scene>/heightmap.bin`).
|
||||
- Default size: 256x256.
|
||||
- Editable in the terrain editor with elevation and curve brushes.
|
||||
|
||||
### 4.2 Fixup chunks
|
||||
|
||||
- Sparse 256x256 chunks keyed by integer world XZ chunk coordinates.
|
||||
- Loaded on demand when a page needs them or when a road node is modified.
|
||||
- Stored as separate binary files (e.g., `terrain_fixup/x0_z0.bin`) or in the
|
||||
scene JSON for small edits.
|
||||
- Applied additively to the base heightmap to make terrain comply to roads and
|
||||
spawned geometry.
|
||||
|
||||
## 5. Procedural Roads
|
||||
|
||||
### 5.1 Road node graph
|
||||
|
||||
- Stored in `TerrainComponent::roadNodes` / `roadEdges`.
|
||||
- Editable in the terrain editor:
|
||||
- Add node (default Y = terrain height + offset).
|
||||
- Remove node (removes connected edges).
|
||||
- Split edge (inserts a node at midpoint).
|
||||
- Select and move nodes.
|
||||
- Join two selected nodes with an edge.
|
||||
- Set per-node vertical offset and per-edge road level for each direction.
|
||||
|
||||
### 5.2 Geometry generation
|
||||
|
||||
For each node:
|
||||
|
||||
0. Collect all nodes which are located on terrain chunks being loaded. Make sure it happens in background work.
|
||||
1. Collect all neighbors connected by edges.
|
||||
2. Sort neighbors by angle around the vertical (Y) axis.
|
||||
3. If only one neighbor, generate an end-of-road mesh from the node position to
|
||||
the midpoint between node and neighbor.
|
||||
4. If multiple neighbors, iterate sorted neighbors. For each consecutive pair
|
||||
`(neighborI, neighborI+1)` create a **wedge** bounded by:
|
||||
- Line from `neighborI` midpoint to node.
|
||||
- Line from node to `neighborI+1` midpoint.
|
||||
5. Generate wedge geometry into a `Procedural::TriangleBuffer`. The generation should use road mesh and sample its geometry among resulting line inside triangle buffer. This mesh should start at zero and go in positive direction among X and Z axis. Its geometry should be used to produce mesh with length of the resulting line and be modified by its angles. The process should be repeated for each lane for roads with multiple lanes in the same direction. If mesh was not provided, simple cube could be used for start. Because of floating point errors, adjacent wedges might end up a bit split, for that each mesh vertex with model X coordinate < 0.05 should be shifted in negative model X axis by 0.05. Same might be done for model Z axis.
|
||||
NB: Length of each edge is required to be integer number and length of road mesh should be exactly 1 unit (or a tiny bit larger for overlap)
|
||||
6. Join close wedges into larger meshes to reduce draw calls.
|
||||
7. Generate Jolt Mesh collider from joined triangle buffer.
|
||||
8. Build an Ogre mesh from the triangle buffer, then run it through the LOD
|
||||
system and set a visibility distance.
|
||||
|
||||
### 5.3 Terrain compliance
|
||||
|
||||
- For every road vertex/sample, compute the desired terrain height
|
||||
(`roadY - roadThickness`).
|
||||
- Write these heights into the sparse fixup chunk covering that world position.
|
||||
- Mark affected terrain pages `dirty` so `CustomTerrainDefiner` re-samples them.
|
||||
- Optionally add a smooth falloff around the road so the blend into the base
|
||||
heightmap is gradual.
|
||||
|
||||
## 6. Prefab Spawning on Terrain
|
||||
|
||||
- Reuse the same distance-based spawn/despawn pattern as
|
||||
`CharacterSpawnerSystem`.
|
||||
- New component `TerrainPrefabSpawn` (or keep inside `TerrainComponent`) stores:
|
||||
- Prefab name/path. A path should be selectable in the UI, taken from prefabs directory.
|
||||
- World position (optionally snapped to terrain surface).
|
||||
- `spawnDistance` / `despawnDistance`.
|
||||
- `TerrainSystem` tracks significent camera changes and if enough position change happend evaluates camera distance and creates/destroys
|
||||
prefab instances via the existing `PrefabSystem`. If terrain chunk is loaded/unloaded, prefabs should too.
|
||||
- A terrain editing tool mode can temporarily raise/lower terrain around a
|
||||
spawned prefab to make it sit flush (writes to fixup chunks).
|
||||
|
||||
## 7. Editor Integration
|
||||
|
||||
### 7.1 Component registration
|
||||
|
||||
Create `src/features/editScene/components/TerrainModule.cpp`:
|
||||
|
||||
```cpp
|
||||
#include "Terrain.hpp"
|
||||
#include "Transform.hpp"
|
||||
#include "EditorMarker.hpp"
|
||||
#include "EntityName.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/TerrainEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Terrain", "Environment", TerrainComponent, TerrainEditor)
|
||||
{
|
||||
registry.registerComponent<TerrainComponent>(
|
||||
"Terrain", "Environment",
|
||||
std::make_unique<TerrainEditor>(),
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<TerrainComponent>()) {
|
||||
e.set<TerrainComponent>({});
|
||||
}
|
||||
},
|
||||
[](flecs::entity e) {
|
||||
if (e.has<TerrainComponent>()) {
|
||||
e.remove<TerrainComponent>();
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 TerrainEditor panel
|
||||
|
||||
Minimum controls:
|
||||
- Enable/disable.
|
||||
- `terrainSize`, `heightmapSize`, `worldSize` with a **Restart Terrain** button
|
||||
(these cannot change at runtime without full rebuild).
|
||||
- `maxPixelError`, `compositeMapDistance`, `minBatchSize`, `maxBatchSize`.
|
||||
- Heightmap file path and **Load / Save** buttons.
|
||||
- Brush mode:
|
||||
- Brush type (raise, lower, smooth, flatten).
|
||||
- Brush radius and curve (stored as simple 1D curve).
|
||||
- Apply to base heightmap and affected fixup chunks.
|
||||
- Road mode:
|
||||
- Add/remove/split/join/select nodes.
|
||||
- Inspector for node position, offset, edge levels.
|
||||
- "Comply terrain to roads" button.
|
||||
- Prefab spawn mode:
|
||||
- Add/remove spawn points.
|
||||
- Pick prefab from existing prefab list.
|
||||
|
||||
## 8. Serialization
|
||||
|
||||
Add to `SceneSerializer::serializeEntity()`:
|
||||
|
||||
```cpp
|
||||
if (entity.has<TerrainComponent>()) {
|
||||
json["terrain"] = serializeTerrain(entity);
|
||||
}
|
||||
```
|
||||
|
||||
Add to both deserialize paths:
|
||||
|
||||
```cpp
|
||||
if (json.contains("terrain")) {
|
||||
deserializeTerrain(entity, json["terrain"]);
|
||||
}
|
||||
```
|
||||
|
||||
JSON schema:
|
||||
|
||||
```json
|
||||
{
|
||||
"terrain": {
|
||||
"enabled": true,
|
||||
"terrainSize": 65,
|
||||
"heightmapSize": 256,
|
||||
"worldSize": 40000000.0,
|
||||
"maxPixelError": 1.0,
|
||||
"compositeMapDistance": 300.0,
|
||||
"minBatchSize": 17,
|
||||
"maxBatchSize": 65,
|
||||
"heightmapFile": "heightmap.bin",
|
||||
"layers": [
|
||||
{
|
||||
"diffuseTexture": "Ground23_col.jpg",
|
||||
"normalTexture": "Ground23_normheight.dds",
|
||||
"worldSize": 100.0
|
||||
}
|
||||
],
|
||||
"roadNodes": [
|
||||
{ "id": 0, "position": [0, 10, 0], "verticalOffset": 0.5 }
|
||||
],
|
||||
"roadEdges": [
|
||||
{ "nodeA": 0, "nodeB": 1, "roadLevelA": 0.0, "roadLevelB": 0.0 }
|
||||
],
|
||||
"prefabSpawns": [
|
||||
{
|
||||
"prefabName": "town_01",
|
||||
"position": [100, 0, 200],
|
||||
"rotation": [0, 0, 0, 1],
|
||||
"spawnDistance": 100.0,
|
||||
"despawnDistance": 200.0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Heightmap binary files are saved next to the scene JSON or in a configured
|
||||
resource directory. Fixup chunk files are saved under a `terrain_fixup/`
|
||||
subdirectory.
|
||||
|
||||
## 9. Build Changes
|
||||
|
||||
In `src/features/editScene/CMakeLists.txt`:
|
||||
|
||||
1. Add `Paging` and `Terrain` to the OGRE component search:
|
||||
|
||||
```cmake
|
||||
find_package(OGRE REQUIRED COMPONENTS Bites Overlay MeshLodGenerator Paging Terrain CONFIG)
|
||||
```
|
||||
|
||||
2. Add new source files to the target sources.
|
||||
|
||||
3. Link `OgrePaging` and `OgreTerrain`:
|
||||
|
||||
```cmake
|
||||
target_link_libraries(editSceneEditor PUBLIC
|
||||
OgreMain
|
||||
OgreBites
|
||||
OgreOverlay
|
||||
OgreMeshLodGenerator
|
||||
OgrePaging
|
||||
OgreTerrain
|
||||
...)
|
||||
```
|
||||
|
||||
## 10. Implementation Milestones
|
||||
|
||||
### Milestone 1 — Minimal rendering terrain
|
||||
- `TerrainComponent`, `TerrainModule`, `TerrainEditor`.
|
||||
- `TerrainSystem` with `Ogre::TerrainGroup`, `PageManager`, `TerrainPaging`,
|
||||
`PagedWorld`, `TerrainPagedWorldSection`.
|
||||
- `CustomTerrainDefiner` sampling a flat or procedural 256x256 base heightmap.
|
||||
- CMake and `EditorApp` wiring.
|
||||
- Scene serialization for the component.
|
||||
|
||||
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.
|
||||
|
||||
Definition of done: rigid bodies and characters collide flush with the visible
|
||||
terrain; removing/re-adding the terrain entity is leak-free.
|
||||
|
||||
### Milestone 3 — Heightmap editing
|
||||
- Load/save binary heightmap.
|
||||
- Brush tools (raise/lower/smooth/flatten) with simple curve-based brush
|
||||
profile.
|
||||
- Sparse 256x256 fixup chunk cache and file I/O.
|
||||
- Mark affected pages dirty and rebuild them.
|
||||
|
||||
Definition of done: user can sculpt the terrain in the editor and save/load the
|
||||
result.
|
||||
|
||||
### Milestone 4 — Procedural roads
|
||||
- Node/edge editing UI.
|
||||
- Wedge geometry generation with `Procedural::TriangleBuffer`.
|
||||
- LOD + visibility distance.
|
||||
- "Comply terrain to roads" writing fixup chunks.
|
||||
|
||||
Definition of done: user can draw a road network; terrain flattens under roads
|
||||
with no gaps.
|
||||
|
||||
### Milestone 5 — Prefab spawns and compliance
|
||||
- Distance-based prefab spawn/despawn on terrain.
|
||||
- Tool mode to make terrain comply to spawned geometry.
|
||||
|
||||
Definition of done: towns/rocks spawn at configured locations and sit flush on
|
||||
terrain.
|
||||
|
||||
## 11. Risks and Mitigations
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| Background job crash on terrain removal | Always check `isDerivedDataUpdateInProgress()`; defer removal to next frame. |
|
||||
| Memory leak on repeated add/remove | `TerrainSystem` owns all runtime objects; verify destruction order in tests. |
|
||||
| Physics/visual mismatch | Use zigzag triangulation; compare debug draw of Jolt body against Ogre terrain wireframe. |
|
||||
| 40M world size precision loss | Project already uses `JPH_DOUBLE_PRECISION`; keep world coordinates as `double`/`long` where possible. |
|
||||
| Large heightmap build stalls | Use Ogre's background prepare; create Jolt colliders only after page is fully loaded. |
|
||||
| Road mesh too heavy | Join wedges, run LOD, limit visibility distance, use instancing for repeated pieces. |
|
||||
|
||||
## 12. Testing Checklist
|
||||
|
||||
- [ ] Add terrain entity → paged terrain renders.
|
||||
- [ ] Remove terrain entity → no crash, no leaks, no dangling Jolt bodies.
|
||||
- [ ] Add, remove, add again repeatedly → stable memory.
|
||||
- [ ] Camera fly-through → pages load/unload correctly.
|
||||
- [ ] Place a rigid body on a slope → it rests flush with terrain.
|
||||
- [ ] Sculpt heightmap → visual and collision update.
|
||||
- [ ] Create road network → road geometry appears and terrain complies.
|
||||
- [ ] Save scene, reload → terrain, roads, and prefab spawns restored.
|
||||
- [ ] Build with ASan/UBSan if available → no reports.
|
||||
|
||||
## 13. Reference Code
|
||||
|
||||
- Legacy terrain module: `src/gamedata/TerrainModule.cpp` and
|
||||
`src/gamedata/TerrainModule.h`.
|
||||
- Ogre EndlessWorld sample: `../../ogre/Samples/EndlessWorld`.
|
||||
- Ogre::Terrain core: `../../ogre/Components/Terrain`.
|
||||
- Bullet zigzag heightfield: `../../bullet3/src/BulletCollision/CollisionShapes/btHeightfieldTerrainShape.cpp`
|
||||
(search for `m_useZigzagSubdivision`).
|
||||
- Jolt heightfield shape: `../../jolt/Jolt/Physics/Collision/Shape/HeightFieldShape.cpp`.
|
||||
- Current Jolt wrapper: `src/features/editScene/physics/physics.h/.cpp`.
|
||||
- Existing editScene component patterns:
|
||||
- `src/features/editScene/components/WaterPlane.hpp`
|
||||
- `src/features/editScene/components/WaterPlaneModule.cpp`
|
||||
- `src/features/editScene/systems/EditorWaterPlaneSystem.hpp`
|
||||
- `src/features/editScene/systems/SceneSerializer.cpp`
|
||||
Reference in New Issue
Block a user