Planning Terrain
This commit is contained in:
@@ -53,28 +53,47 @@ Add a robust, editor-friendly `Ogre::Terrain`-based terrain feature to the
|
||||
| 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 |
|
||||
| Editor | `src/features/editScene/ui/TerrainEditor.cpp` | ImGui property/editor panel impl |
|
||||
| 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 |
|
||||
| App wiring | `src/features/editScene/EditorApp.hpp` | Forward declaration + unique_ptr member |
|
||||
| App wiring | `src/features/editScene/EditorApp.cpp` | setupECS, setup, frameRenderingQueued, destructor |
|
||||
| Serialization | `src/features/editScene/systems/SceneSerializer.hpp` | Declare serializeTerrain/deserializeTerrain |
|
||||
| Serialization | `src/features/editScene/systems/SceneSerializer.cpp` | Save/load terrain data + include |
|
||||
| Build | `src/features/editScene/CMakeLists.txt` | Add files + `OgrePaging`/`OgreTerrain` links |
|
||||
|
||||
## 1. TerrainComponent (parameters only)
|
||||
|
||||
**Design**: This is a **singleton component** — exactly one entity in the scene
|
||||
should carry it. Ogre only supports one `TerrainGroup` per `SceneManager`, so
|
||||
multi-instance terrain makes no sense. The component holds only serializable
|
||||
parameters and a transient `dirty` flag. All runtime Ogre/Jolt objects live in
|
||||
`TerrainSystem`.
|
||||
|
||||
```cpp
|
||||
struct TerrainComponent {
|
||||
bool enabled = true;
|
||||
|
||||
// Page/tile vertex resolution. Must be 2^n+1 and <= 65 (Ogre limit).
|
||||
// Page/tile vertex resolution. Must be 2^n+1 (e.g. 33, 65, 129, 257).
|
||||
// Ogre supports up to 4097, but practical limits depend on GPU memory.
|
||||
int terrainSize = 65;
|
||||
|
||||
// Stable unique ID for this terrain — generated once at component creation,
|
||||
// serialized, and never changed. Used as the directory key for all binary
|
||||
// terrain data (heightmap, fixups, aux maps). Default 0 means "not yet
|
||||
// assigned" — the editor generates one on first save or creation.
|
||||
uint64_t terrainId = 0;
|
||||
|
||||
// Base binary heightmap resolution. Default 256x256.
|
||||
int heightmapSize = 256;
|
||||
|
||||
// World-space size of one Ogre terrain page.
|
||||
float worldSize = 40000000.0f;
|
||||
// World-space size of one Ogre terrain page in units.
|
||||
// For a single-page test scene, 2000-4000 is practical (terrainSize=65
|
||||
// gives ~31-62 units per vertex). The 40M legacy value assumes enormous
|
||||
// world coordinates; scale to your scene's coordinate system.
|
||||
float worldSize = 2000.0f;
|
||||
|
||||
// Maximum geometric error in pixels before a LOD tile splits.
|
||||
float maxPixelError = 1.0f;
|
||||
@@ -90,6 +109,8 @@ struct TerrainComponent {
|
||||
std::string heightmapFile = "heightmap.bin";
|
||||
|
||||
// Layer texture settings (diffuse+normal pairs).
|
||||
// These are loaded via Ogre resource groups and fed into
|
||||
// TerrainMaterialGeneratorA (RTShader terrain materials).
|
||||
struct Layer {
|
||||
std::string diffuseTexture;
|
||||
std::string normalTexture;
|
||||
@@ -112,15 +133,8 @@ struct TerrainComponent {
|
||||
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;
|
||||
// Prefab spawn points on the terrain managed by TerrainPrefabSpawnerComponent
|
||||
// (not embedded here — see section 6).
|
||||
|
||||
// Runtime-only: managed by TerrainSystem. Not serialized.
|
||||
bool dirty = true;
|
||||
@@ -136,6 +150,11 @@ Rules:
|
||||
restart in editor mode.
|
||||
- Runtime objects (`Ogre::Terrain*`, Jolt bodies, scene nodes) must live in
|
||||
`TerrainSystem`, not in the component.
|
||||
- The component must be registered in `EditorApp::setupECS()` with
|
||||
`m_world.component<TerrainComponent>()` so the module macro and
|
||||
serializer can use it. The `REGISTER_COMPONENT_GROUP` macro in
|
||||
`TerrainModule.cpp` only handles the editor UI registration.
|
||||
- This is a **singleton component**: exactly one entity per scene.
|
||||
|
||||
## 2. TerrainSystem (singleton owner)
|
||||
|
||||
@@ -184,8 +203,9 @@ std::unordered_map<uint64_t, TerrainCollider> mColliders;
|
||||
- Wait until `mTerrainGroup->isDerivedDataUpdateInProgress()` is `false`.
|
||||
- Remove all Jolt bodies first.
|
||||
- Unload and destroy pages.
|
||||
- Destroy `TerrainPaging`/`PageManager`/`TerrainGroup`/`TerrainGlobalOptions`
|
||||
in that order.
|
||||
- Destroy in this order: `TerrainPaging` → `PageManager` → `TerrainGroup` →
|
||||
`TerrainGlobalOptions`. (`TerrainPaging` owns a reference to `PageManager`,
|
||||
so it must go first.)
|
||||
|
||||
4. **Destruction** (`EditorApp::~EditorApp()`)
|
||||
- Destroy `TerrainSystem` **before** `PhysicsSystem` and the
|
||||
@@ -240,7 +260,7 @@ Input:
|
||||
- Integer XZ world coordinates (not normalized, not page-local).
|
||||
|
||||
Output:
|
||||
- World-space Y height.
|
||||
- World-space float Y height.
|
||||
|
||||
Sources (in order of priority):
|
||||
1. **Road fixup heightmap** — sparse 256x256 chunks loaded on demand. If a chunk
|
||||
@@ -269,6 +289,170 @@ 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.
|
||||
|
||||
### 2.5 RTShader and material configuration
|
||||
|
||||
Before creating the `TerrainGroup`, configure the terrain material generator:
|
||||
|
||||
```cpp
|
||||
mTerrainGlobals->setDefaultMaterialGenerator(
|
||||
Ogre::TerrainMaterialGeneratorPtr(
|
||||
new Ogre::TerrainMaterialGeneratorA()));
|
||||
```
|
||||
|
||||
This must happen while `Ogre::RTShader::ShaderGenerator` is active (it is, via
|
||||
`EditorApp::setup()`).
|
||||
|
||||
### 2.5a Layer declaration (texture sampler layout)
|
||||
|
||||
`TerrainMaterialGeneratorA` defines a **layer declaration** — the set of texture
|
||||
samplers each layer expects. The default `SM2Profile` declaration provides:
|
||||
|
||||
| Sampler | Semantic | Purpose |
|
||||
|---------|----------|---------|
|
||||
| `diffusespecular` | diffuse+specular | Albedo (RGB) + specular (A) |
|
||||
| `normalheight` | normal+parallax | Normal map (RGB) + displacement (A) |
|
||||
|
||||
Each `Layer` in `TerrainComponent::layers` produces an
|
||||
`Ogre::Terrain::LayerInstance` whose `textureNames` list must match the sampler
|
||||
count and order of the declaration:
|
||||
|
||||
```cpp
|
||||
Ogre::Terrain::LayerInstance layerInstance;
|
||||
layerInstance.worldSize = componentLayer.worldSize;
|
||||
// Order must match the LayerDeclaration samplers:
|
||||
layerInstance.textureNames.push_back(componentLayer.diffuseTexture);
|
||||
layerInstance.textureNames.push_back(componentLayer.normalTexture);
|
||||
importData.layerList.push_back(layerInstance);
|
||||
```
|
||||
|
||||
The `ImportData::layerDeclaration` is obtained from the material generator
|
||||
profile:
|
||||
|
||||
```cpp
|
||||
importData.layerDeclaration = generatorProfile->getLayerDeclaration();
|
||||
```
|
||||
|
||||
### 2.5b Blend maps (splatting)
|
||||
|
||||
Ogre terrain **auto-creates** a packed blend map texture for all pages.
|
||||
How blend maps work:
|
||||
|
||||
- **Layer 0** is the base layer — always 100% opaque everywhere. It needs no
|
||||
blend map.
|
||||
- **Layers 1–4** each get one RGBA channel (R, G, B, A) in the packed blend
|
||||
texture. Each texel is 0..255 (mapped to 0..1) controlling the visibility
|
||||
of that layer blended on top of the layers below.
|
||||
- With the default `SM2Profile`, **up to 5 layers** are supported per terrain
|
||||
page (layer 0 + 4 blend channels).
|
||||
- After `terrain->prepare(importData)`, blend maps exist but are initialized to
|
||||
zero everywhere (only layer 0 visible). Use
|
||||
`TerrainLayerBlendMap::setBlendValue()` to paint splat weights.
|
||||
|
||||
```cpp
|
||||
Ogre::TerrainLayerBlendMap *blendMap = terrain->getLayerBlendMap(layerIdx);
|
||||
blendMap->setBlendValue(tx, ty, 1.0f); // full opacity for this layer here
|
||||
blendMap->dirty();
|
||||
blendMap->update();
|
||||
```
|
||||
|
||||
**Composite map**: Ogre auto-generates a low-res "composite map" from the blend
|
||||
maps for distant rendering (`compositeMapDistance` controls when it switches).
|
||||
|
||||
### 2.5c Default blend map initialization
|
||||
|
||||
When a terrain page is first created, a sensible default paint is to show layer
|
||||
0 everywhere. Later milestones (M3) add brush tools to paint splat weights in
|
||||
the editor. For M1, just leave blend maps at their zero defaults or apply a
|
||||
simple slope-based heuristic:
|
||||
|
||||
```
|
||||
// Pseudocode: paint layer 1 (grass) on flat areas, layer 2 (rock) on steep
|
||||
float slope = computeSlope(x, z);
|
||||
if (slope < 30°) blendMap1->setBlendValue(x, z, 1.0f);
|
||||
else blendMap2->setBlendValue(x, z, 1.0f);
|
||||
```
|
||||
|
||||
### 2.5d Editor splat paint tools (future milestone)
|
||||
|
||||
Splat painting uses the same brush infrastructure as heightmap editing:
|
||||
- Brush type selector (which layer to paint, or "erase" to reduce weight).
|
||||
- Brush radius and opacity curve.
|
||||
- `TerrainLayerBlendMap::setBlendValue()` for each affected texel.
|
||||
- `dirtyRect()` to limit the GPU upload to the modified region.
|
||||
- The blend map image size is configurable via
|
||||
`TerrainGlobalOptions::setLayerBlendMapSize()` (default 1024x1024).
|
||||
|
||||
### 2.6 Resource locations for terrain data
|
||||
|
||||
Terrain textures and heightmaps need `Ogre::ResourceGroupManager` locations:
|
||||
|
||||
- `heightmapFile` path: `heightmaps/<terrainId>_<heightmapFile>` where
|
||||
`<terrainId>` is `TerrainComponent::terrainId`. Not scene-relative — the
|
||||
terrain ID is the stable key, independent of scene file name or save path.
|
||||
- Fixup chunks: stored in `heightmaps/<terrainId>/terrain_fixup/` subdirectory,
|
||||
registered as a resource location when the scene is loaded.
|
||||
- Layer textures: expected in the `General` resource group (same as all other
|
||||
textures), so no additional resource location is needed.
|
||||
|
||||
### 2.7 Interaction with water and skybox systems
|
||||
|
||||
- **Water reflection/refraction**: The terrain's render queue group and
|
||||
visibility mask must include the water RTT cameras. If the water system hides
|
||||
entities during RTT passes via `RenderTargetListener`, the terrain must be
|
||||
visible to those passes or water won't reflect the terrain.
|
||||
- **Skybox**: No special handling needed — sky renders behind terrain.
|
||||
- **Sun shadows**: If terrain uses shadow casting, ensure the sun light's shadow
|
||||
far distance covers the terrain pages.
|
||||
|
||||
### 2.8 EditorApp wiring
|
||||
|
||||
Follow the existing pattern from `EditorWaterPlaneSystem` and
|
||||
`EditorPhysicsSystem`:
|
||||
|
||||
**In `EditorApp.hpp`**:
|
||||
```cpp
|
||||
// Forward declaration
|
||||
class TerrainSystem;
|
||||
|
||||
// In private section:
|
||||
std::unique_ptr<TerrainSystem> m_terrainSystem;
|
||||
```
|
||||
|
||||
**In `EditorApp::setupECS()`** — register the component:
|
||||
```cpp
|
||||
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());
|
||||
```
|
||||
|
||||
**In `EditorApp::frameRenderingQueued()`** — call update **before**
|
||||
static-world generation and **after** visual mesh setup. Insert right after
|
||||
`m_proceduralMeshSystem->update()`:
|
||||
```cpp
|
||||
if (m_terrainSystem) {
|
||||
m_terrainSystem->update(evt.timeSinceLastFrame);
|
||||
}
|
||||
```
|
||||
|
||||
**In `EditorApp::~EditorApp()`** — destroy **before** `m_physicsSystem` and
|
||||
before Ogre `SceneManager` cleanup. Insert `m_terrainSystem.reset()` before
|
||||
`m_physicsSystem.reset()`. Also ensure `m_waterPlaneSystem`, `m_skyboxSystem`,
|
||||
and `m_sunSystem` are reset before terrain (the current destructor omits these
|
||||
resets — add them):
|
||||
```cpp
|
||||
m_waterPlaneSystem.reset();
|
||||
m_skyboxSystem.reset();
|
||||
m_sunSystem.reset();
|
||||
m_terrainSystem.reset();
|
||||
m_buoyancySystem.reset();
|
||||
m_physicsSystem.reset();
|
||||
```
|
||||
|
||||
## 3. ZigzagHeightfieldShape (custom Jolt collision shape)
|
||||
|
||||
`JPH::HeightFieldShape` is plain row-major and always splits quads along the
|
||||
@@ -297,6 +481,8 @@ Equivalent to Bullet's `m_useZigzagSubdivision && !(j & 1)`.
|
||||
Because implementing a full custom `JPH::Shape` is large, the first milestone
|
||||
uses a practical, verifiable approach:
|
||||
|
||||
**MeshShape approach (Milestone 2)**:
|
||||
|
||||
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.
|
||||
@@ -306,6 +492,15 @@ uses a practical, verifiable approach:
|
||||
4. Place the `MeshShape` at the page world position using a
|
||||
`JPH::RotatedTranslatedShape` or by offsetting vertex coordinates.
|
||||
|
||||
**Performance note**: `JPH::MeshShape` stores the full triangle soup and uses
|
||||
a bounding-volume tree internally, while `JPH::HeightFieldShape` exploits the
|
||||
regular grid structure for O(log n) collision queries with minimal memory.
|
||||
For a terrain page of 65x65 vertices (8192 triangles), the `MeshShape` overhead
|
||||
is acceptable (a few KB extra memory, similar collision speed for typical
|
||||
contacts). For pages above 129x129, the custom heightfield shape should be
|
||||
prioritized. **Mitigation**: Test with `terrainSize=65` first; profile with
|
||||
Jolt's stats overlay before scaling up.
|
||||
|
||||
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
|
||||
@@ -356,18 +551,102 @@ Use `EShapeSubType::User1` for the shape subtype.
|
||||
|
||||
- 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.
|
||||
- **Storage path**: Directory keyed by the stable `TerrainComponent::terrainId`:
|
||||
`heightmaps/<terrainId>_<heightmapFile>` where `<terrainId>` is the
|
||||
decimal value of `TerrainComponent::terrainId`. This ID is generated once
|
||||
at component creation (e.g. `std::random_device{}() | (uint64_t)time(0) << 32`),
|
||||
serialized with the component, and survives save/reload cycles unchanged.
|
||||
- **Default size**: 256x256.
|
||||
- Editable in the terrain editor with elevation and curve brushes.
|
||||
- A fresh empty heightmap initializes all samples to `0.0f`.
|
||||
|
||||
### 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.
|
||||
- **Purpose**: Override base heightmap values in localized regions so terrain
|
||||
complies to roads, buildings, and other placed geometry.
|
||||
- **Format**: raw binary, 256x256 floats (same as base heightmap but always
|
||||
256x256).
|
||||
- **Naming**: `x<chunkX>_z<chunkZ>.bin` where chunk coordinates are derived from
|
||||
world coordinates:
|
||||
```
|
||||
int chunkX = floor(worldX / (worldSize / 256));
|
||||
int chunkZ = floor(worldZ / (worldSize / 256));
|
||||
```
|
||||
So chunk `(0,0)` covers world `(0,0)` to `(worldSize/256, worldSize/256)` in
|
||||
X and Z.
|
||||
- **Storage directory**: `heightmaps/<terrainId>/terrain_fixup/` — registered as
|
||||
an Ogre resource location when the scene is loaded so the definer can load them
|
||||
on demand. The `<terrainId>` prefix matches the base heightmap directory scheme
|
||||
from 4.1.
|
||||
- **Semantics**: Each fixup sample is an **absolute** height value (not an
|
||||
offset). If `getHeightAtWorld()` finds a fixup chunk covering the queried
|
||||
world coordinate, it bilinearly samples the fixup chunk and **replaces** the
|
||||
base heightmap value at that point. A sentinel value (e.g., `NaN` or
|
||||
`-FLT_MAX`) indicates "no fixup at this sample" — in that case fall through
|
||||
to the base heightmap.
|
||||
- **Blending at chunk boundaries**: Use bilinear interpolation within each chunk
|
||||
(natural for floating-point heightmaps). When a queried point falls in a
|
||||
chunk that hasn't been created yet, there's no fixup — fall through to base.
|
||||
- **Sparsity**: Chunks are created lazily — only when a road brush writes to
|
||||
them or when explicitly created by editor tools. Absent chunks mean "no fixup
|
||||
data for this area."
|
||||
- **Cleanup**: Provide a "Clear all fixups" button in the terrain editor that
|
||||
deletes all files under `heightmaps/<terrainId>/terrain_fixup/` and marks all
|
||||
terrain pages dirty for re-sampling. Additionally, individual fixup chunks
|
||||
can be deleted by right-clicking a world position in the editor — the chunk
|
||||
file covering that position is removed and the affected pages rebuild.
|
||||
|
||||
### 4.3 Non-heightmap maps (foliage density, material masks, etc.)
|
||||
|
||||
Terrain systems often need auxiliary 2D maps beyond the heightfield. These are
|
||||
stored the same way as the base heightmap but with independent resolution and
|
||||
without fixup chunk support.
|
||||
|
||||
**Data structure** — add to `TerrainComponent`:
|
||||
|
||||
```cpp
|
||||
struct AuxMap {
|
||||
std::string name; // e.g. "foliage_density", "rock_mask"
|
||||
std::string fileName; // raw binary file, e.g. "foliage_density.bin"
|
||||
int resolution = 256; // independent from heightmapSize
|
||||
float defaultValue = 0.0f; // initial fill value for new maps
|
||||
};
|
||||
std::vector<AuxMap> auxMaps;
|
||||
```
|
||||
|
||||
**Storage**: `heightmaps/<terrainId>/<auxMap.fileName>` — same directory as the
|
||||
base heightmap.
|
||||
|
||||
**Editing**: Use the same brush system as heightmap editing (raise/lower/smooth)
|
||||
but interpreted as adding/subtracting density values. Values are clamped to
|
||||
[0, 1] for density maps.
|
||||
|
||||
**Visualization**: For density maps, the foliage placement itself serves as
|
||||
indirect visualization (more foliage where density is high). For debugging,
|
||||
render a heatmap overlay on the terrain surface using a simple colored quad or
|
||||
compositor pass.
|
||||
|
||||
**Foliage integration**: After terrain implementation, `Ogre::PagedGeometry`
|
||||
will sample these density maps during placement. The `AuxMap` data must be
|
||||
accessible from the foliage system (via `TerrainSystem` API or shared file
|
||||
paths). This is tracked as a follow-up task; for now, the storage and editing
|
||||
infrastructure is in scope.
|
||||
|
||||
### 4.4 Packaging
|
||||
|
||||
- **Goal**: Bundle all terrain data files into a single `.package` archive
|
||||
(using the existing Package archive format from `package/OgrePackageArchive`)
|
||||
to avoid filesystem clutter after heavy terrain editing.
|
||||
- **Trigger**: A "Package Terrain" button in the terrain editor or a CLI tool.
|
||||
- **Contents**: All files under `heightmaps/<terrainId>/` — base heightmap,
|
||||
fixup chunks, and aux maps.
|
||||
- **Package path**: `heightmaps/<terrainId>.package`
|
||||
- **Loading**: When the package file exists, register it as an Ogre resource
|
||||
location before the loose file directory so the packaged version takes
|
||||
precedence. Loose files under `heightmaps/<terrainId>/` are treated as
|
||||
overrides (editor writes to loose files, packaging snapshots them).
|
||||
- **Unpack**: An "Unpack Terrain" button extracts the package back to loose
|
||||
files for editing.
|
||||
|
||||
## 5. Procedural Roads
|
||||
|
||||
@@ -382,47 +661,260 @@ Use `EShapeSubType::User1` for the shape subtype.
|
||||
- Join two selected nodes with an edge.
|
||||
- Set per-node vertical offset and per-edge road level for each direction.
|
||||
|
||||
### 5.2 Geometry generation
|
||||
**Per-edge data** — `RoadEdge` is extended beyond the simple A/B link to carry
|
||||
local overrides for road configuration:
|
||||
|
||||
```cpp
|
||||
struct RoadEdge {
|
||||
int nodeA = -1;
|
||||
int nodeB = -1;
|
||||
float roadLevelA = 0.0f; // road surface height at nodeA end
|
||||
float roadLevelB = 0.0f; // road surface height at nodeB end
|
||||
|
||||
// Per-edge lane count override (0 = use global lanesPerDirection).
|
||||
int lanesPerDirectionOverride = 0;
|
||||
|
||||
// Per-direction lane counts for asymmetric roads.
|
||||
// Positive = A→B, negative = B→A. If 0, uses lanesPerDirectionOverride
|
||||
// or global lanesPerDirection.
|
||||
int lanesAtoB = 0;
|
||||
int lanesBtoA = 0;
|
||||
|
||||
// Prefab spawn points along this edge.
|
||||
struct RoadSidePrefab {
|
||||
std::string prefabPath;
|
||||
float edgeT = 0.5f; // 0..1 position along edge
|
||||
float sideOffset = 5.0f; // lateral offset from road center
|
||||
bool leftSide = true; // left vs right relative to A→B direction
|
||||
};
|
||||
std::vector<RoadSidePrefab> sidePrefabs;
|
||||
};
|
||||
```
|
||||
|
||||
This allows individual edges to be wider (more lanes) than the default and to
|
||||
spawn lamp posts, guardrails, or other roadside objects at configurable offsets.
|
||||
|
||||
### 5.2 Road configuration parameters
|
||||
|
||||
These live in `TerrainComponent` alongside the node graph:
|
||||
|
||||
```cpp
|
||||
// Road surface mesh template name (file in General resource group).
|
||||
// Default: a unit-length flat plane mesh at Y=0, extending +X.
|
||||
std::string roadMeshTemplate = "road_segment.mesh";
|
||||
|
||||
// Width of a single lane in world units.
|
||||
float laneWidth = 3.0f;
|
||||
|
||||
// Number of lanes in each road direction (1 = single-lane each way). default value, should be possible to set per edge per direction.
|
||||
int lanesPerDirection = 1;
|
||||
|
||||
// Vertical thickness of the road surface (for terrain compliance depth).
|
||||
float roadThickness = 0.3f;
|
||||
|
||||
// Maximum edge length beyond which roads use lower LOD.
|
||||
float roadLodDistance = 200.0f;
|
||||
|
||||
// Distance at which road meshes fade out (Ogre visibility distance).
|
||||
float roadVisibilityDistance = 1000.0f;
|
||||
|
||||
// Material name for road surfaces.
|
||||
std::string roadMaterialName = "RoadMaterial";
|
||||
```
|
||||
|
||||
### 5.3 Road mesh template
|
||||
|
||||
The `roadMeshTemplate` is an Ogre mesh file with these conventions:
|
||||
|
||||
- The mesh occupies **exactly 1 unit** along the positive X axis (from X=0 to
|
||||
X=1), Z=0 to Z=1 or Z=-1 to Z=0, centered on Y=0 (width spans 0 to
|
||||
laneWidth for each lane).
|
||||
- The mesh is repeated along the edge direction for the actual road length.
|
||||
- Vertices with `X < 0.05` (near the start cap) are shifted slightly negative
|
||||
(`-0.05`) to create overlap and prevent seams at wedge boundaries.
|
||||
- UVs span (0,0) to (1,1) along the X/Z extents and are scaled to the actual
|
||||
road length at mesh generation time.
|
||||
- If no template mesh is provided, fall back to a generated box primitive with
|
||||
dimensions `roadSegmentLength x roadThickness x laneWidth`. It should be offset right and towards front for easier calculation and have Z size of 1.0
|
||||
|
||||
### 5.4 Geometry generation
|
||||
|
||||
For each loaded terrain page, collect all road nodes whose world position falls
|
||||
within the page plus a margin equal to `roadVisibilityDistance`.
|
||||
|
||||
**Step 1 — Node sorting**
|
||||
|
||||
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.
|
||||
3. If only one neighbor, the node is an endpoint → generate an **end cap**.
|
||||
|
||||
### 5.3 Terrain compliance
|
||||
**Step 2 — Wedge generation**
|
||||
|
||||
- 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.
|
||||
For nodes with multiple neighbors, iterate sorted neighbors and for each
|
||||
consecutive pair `(neighborI, neighborI+1)` create a **wedge** bounded by:
|
||||
- Line from node to `neighborI` midpoint.
|
||||
- Line from node to `neighborI+1` midpoint.
|
||||
|
||||
For each wedge:
|
||||
1. Compute the two boundary direction vectors from the node.
|
||||
2. Generate wedge geometry into a `Procedural::TriangleBuffer`:
|
||||
- The wedge spans the angular region between the two directions.
|
||||
- The road mesh template is repeated along each radial direction and the
|
||||
triangles between adjacent samples form the wedge surface.
|
||||
- Each vertex's model-space X coordinate maps to distance from the node;
|
||||
Z maps to lateral offset.
|
||||
3. For each lane `L` from `0` to `lanesPerDirection - 1`:
|
||||
- Offset the Z coordinate by `L * laneWidth` for the outward lane and
|
||||
`-L * laneWidth` for the inward lane.
|
||||
- Generate separate wedge geometry for each lane (so lanes can be
|
||||
individually LOD'd and culled).
|
||||
|
||||
**Step 3 — End cap generation**
|
||||
|
||||
For endpoints (nodes with exactly one neighbor):
|
||||
1. Generate a half-circle or rectangular cap mesh from the node extending away
|
||||
from the neighbor direction.
|
||||
2. Same lane multiplication as wedge generation.
|
||||
|
||||
**Step 4 — Seam suppression**
|
||||
|
||||
Floating-point errors at wedge boundaries can create visible gaps. Mitigation:
|
||||
- Vertices with model-space `X < 0.05` (near zero) are shifted by `-0.05` in
|
||||
the negative X direction (toward the node center).
|
||||
- Vertices at the outer radial edge (model-space `X` near the edge midpoint
|
||||
distance) are shifted slightly outward along the edge direction.
|
||||
|
||||
**Step 5 — Mesh assembly**
|
||||
|
||||
For each wedge and end cap:
|
||||
1. Create a `TriangleBufferComponent` entity as a child of the terrain entity.
|
||||
2. The buffer is built by `ProceduralMeshSystem` during its update pass.
|
||||
3. `ProceduralMeshSystem::convertToMesh()` creates the Ogre mesh from the buffer.
|
||||
4. Apply the road material:
|
||||
- Set `materialEntity` reference on the `TriangleBufferComponent`.
|
||||
- `ProceduralMaterialSystem` resolves the material at mesh creation time.
|
||||
|
||||
**Step 6 — LOD and visibility**
|
||||
|
||||
- Each road mesh entity gets a `LodComponent` with `LodStrategy::Distance`.
|
||||
- Meshes beyond `roadLodDistance` from the camera use a lower-poly LOD level
|
||||
(generated by `MeshLodGenerator` if available, or a simple reduction).
|
||||
- Meshes beyond `roadVisibilityDistance` are culled entirely via
|
||||
`Ogre::Entity::setRenderingDistance()`.
|
||||
|
||||
**Step 7 — Physics colliders**
|
||||
|
||||
- For each road mesh, generate a `JPH::MeshShape` matching the visible geometry.
|
||||
- The collider is attached as a static body in the NON_MOVING layer.
|
||||
- Colliders are created/removed in sync with mesh visibility (same distance
|
||||
thresholds).
|
||||
- Create colliders from joined geometry.
|
||||
|
||||
**Step 8 — Wedge joining (optimization)**
|
||||
|
||||
Adjacent wedges that share an edge direction and have the same material can be
|
||||
merged into a single `TriangleBuffer` to reduce draw calls:
|
||||
1. After generating all wedges for a page, group wedges by material and seed
|
||||
node.
|
||||
2. Merge triangle buffers by appending triangles and adjusting indices.
|
||||
3. Merge only if the total triangle count stays below a threshold (e.g. 65535
|
||||
vertices for 16-bit index buffers).
|
||||
|
||||
### 5.5 Terrain compliance (road flattening)
|
||||
|
||||
For every road vertex/sample:
|
||||
1. Compute the desired terrain height at that world position:
|
||||
`targetY = roadY - roadThickness`.
|
||||
2. Write `targetY` into the fixup chunk that covers that world position.
|
||||
3. Mark affected terrain pages dirty so `CustomTerrainDefiner` re-samples them
|
||||
on the next `update()` call.
|
||||
4. Apply a smooth falloff perpendicular to the road direction:
|
||||
- Full compliance within the road width.
|
||||
- Linear fade over `laneWidth * 2` units outside the road edge.
|
||||
- Beyond the fade zone, no fixup (use base heightmap).
|
||||
|
||||
### 5.6 Edge length constraint
|
||||
|
||||
Each road edge must have an **integer** length in the road mesh template
|
||||
coordinate system. The road mesh template is exactly 1 unit long, so edges
|
||||
whose world-space length is not a whole number of road-segment units will
|
||||
have a fractional segment at the end. Round edge lengths to the nearest
|
||||
integer number of road-mesh repetitions. For very short edges (length < 1
|
||||
unit), scale the road mesh down or emit a warning/immediate LOD reduction.
|
||||
When operating on edges, like splitting and joining, it should be checked and applied. If enforcing of constraint (by moving new nodes) is impossible, the operation is cancelled with warning message. Other option would be scaling elemental mesh on one of midpoint edges together with scaling UVs so road surface will still look fine. If that would be robust enough I'd go with it, but only if fixing by moving the node is impossible. That should reduce probability of problems.
|
||||
|
||||
### 5.7 Dependencies on existing systems
|
||||
|
||||
- **ProceduralMeshSystem**: Converts `TriangleBufferComponent` → Ogre mesh.
|
||||
- **ProceduralMaterialSystem**: Resolves `ProceduralMaterialComponent` → Ogre
|
||||
material for road surfaces.
|
||||
- **LodSystem**: Manages `LodComponent` for distance-based LOD switching.
|
||||
- **EditorPhysicsSystem**: Provides `JoltPhysicsWrapper` for creating static
|
||||
mesh colliders.
|
||||
- **StaticGeometrySystem**: Optional — road meshes could be batched into
|
||||
static geometry for large road networks, but this complicates per-segment
|
||||
LOD and visibility culling.
|
||||
|
||||
## 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).
|
||||
### 6.1 Design
|
||||
|
||||
Terrain-attached prefabs use the existing `PrefabSystem` for instantiation and
|
||||
a new ECS component for distance-based spawn/despawn, similar to
|
||||
`CharacterSpawnerComponent`.
|
||||
|
||||
### 6.2 TerrainPrefabSpawnerComponent
|
||||
|
||||
A new component (not embedded in `TerrainComponent`, to keep the component
|
||||
single-responsibility):
|
||||
|
||||
```cpp
|
||||
struct TerrainPrefabSpawnerComponent {
|
||||
// Prefab JSON file path (selectable from prefabs directory in UI).
|
||||
std::string prefabPath;
|
||||
|
||||
// World position (snapped to terrain surface at placement time).
|
||||
Ogre::Vector3 position;
|
||||
Ogre::Quaternion rotation;
|
||||
|
||||
// Distance-based spawn/despawn (squared for fast comparison).
|
||||
float spawnDistanceSq = 100.0f * 100.0f;
|
||||
float despawnDistanceSq = 200.0f * 200.0f;
|
||||
|
||||
// Runtime: the spawned instance entity (null when not spawned).
|
||||
flecs::entity_t spawnedEntity = 0;
|
||||
};
|
||||
```
|
||||
|
||||
### 6.3 TerrainPrefabSpawnerSystem
|
||||
|
||||
A lightweight system that:
|
||||
1. Queries all `TerrainPrefabSpawnerComponent` entities each frame.
|
||||
2. Compares camera distance against `spawnDistanceSq` / `despawnDistanceSq`.
|
||||
3. Creates/destroys prefab instances via `PrefabSystem::createInstance()`.
|
||||
4. Tracks camera position changes; only re-evaluates when the camera moves
|
||||
significantly (e.g., more than 10 units since last check).
|
||||
|
||||
This is registered and updated alongside other systems in `EditorApp`, not
|
||||
inside `TerrainSystem`.
|
||||
|
||||
### 6.4 Terrain snap at placement
|
||||
|
||||
When the user places a prefab spawn point in the editor:
|
||||
1. Raycast against the terrain to find the surface Y at `(position.x, position.z)`.
|
||||
2. Set `position.y` to the terrain height.
|
||||
3. The spawner entity's `TransformComponent.position` matches this snapped
|
||||
position so it's visible in the editor.
|
||||
|
||||
### 6.5 Terrain compliance tool
|
||||
|
||||
A terrain editing tool mode can temporarily raise/lower terrain around a
|
||||
spawned prefab to make it sit flush:
|
||||
1. Sample the prefab's bounding box footprint on the terrain.
|
||||
2. Write fixup chunk entries that flatten the terrain under the footprint.
|
||||
3. Optionally add a slope/blend around the footprint edges.
|
||||
4. Mark affected terrain pages dirty.
|
||||
|
||||
## 7. Editor Integration
|
||||
|
||||
@@ -465,16 +957,23 @@ Minimum controls:
|
||||
- `maxPixelError`, `compositeMapDistance`, `minBatchSize`, `maxBatchSize`.
|
||||
- Heightmap file path and **Load / Save** buttons.
|
||||
- Brush mode:
|
||||
- Brush type (raise, lower, smooth, flatten).
|
||||
- Brush type: raise, lower, smooth, flatten, **layer paint** (selects which
|
||||
terrain layer to apply, with opacity).
|
||||
- Brush radius and curve (stored as simple 1D curve).
|
||||
- Apply to base heightmap and affected fixup chunks.
|
||||
- For layer paint: writes to `TerrainLayerBlendMap` via `setBlendValue()` and
|
||||
`dirtyRect()`, then `update()`.
|
||||
- Road mode:
|
||||
- Road config: `roadMeshTemplate`, `laneWidth`, `lanesPerDirection`,
|
||||
`roadThickness`, `roadLodDistance`, `roadVisibilityDistance`,
|
||||
`roadMaterialName`.
|
||||
- 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.
|
||||
- Prefab spawn mode (uses `TerrainPrefabSpawnerComponent`):
|
||||
- Add/remove spawn points (snapped to terrain surface).
|
||||
- Pick prefab from existing prefab list.
|
||||
- Configure spawn/despawn distances.
|
||||
|
||||
## 8. Serialization
|
||||
|
||||
@@ -500,9 +999,10 @@ JSON schema:
|
||||
{
|
||||
"terrain": {
|
||||
"enabled": true,
|
||||
"terrainId": 17345678901234567890,
|
||||
"terrainSize": 65,
|
||||
"heightmapSize": 256,
|
||||
"worldSize": 40000000.0,
|
||||
"worldSize": 2000.0,
|
||||
"maxPixelError": 1.0,
|
||||
"compositeMapDistance": 300.0,
|
||||
"minBatchSize": 17,
|
||||
@@ -515,28 +1015,53 @@ JSON schema:
|
||||
"worldSize": 100.0
|
||||
}
|
||||
],
|
||||
"auxMaps": [
|
||||
{
|
||||
"name": "foliage_density",
|
||||
"fileName": "foliage_density.bin",
|
||||
"resolution": 256,
|
||||
"defaultValue": 0.0
|
||||
}
|
||||
],
|
||||
"roadConfig": {
|
||||
"roadMeshTemplate": "road_segment.mesh",
|
||||
"laneWidth": 3.0,
|
||||
"lanesPerDirection": 1,
|
||||
"roadThickness": 0.3,
|
||||
"roadLodDistance": 200.0,
|
||||
"roadVisibilityDistance": 1000.0,
|
||||
"roadMaterialName": "RoadMaterial"
|
||||
},
|
||||
"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
|
||||
"nodeA": 0, "nodeB": 1,
|
||||
"roadLevelA": 0.0, "roadLevelB": 0.0,
|
||||
"lanesPerDirectionOverride": 0,
|
||||
"lanesAtoB": 0, "lanesBtoA": 0,
|
||||
"sidePrefabs": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
**Note**: Prefab spawners are serialized separately via
|
||||
`TerrainPrefabSpawnerComponent`, not embedded in the terrain JSON.
|
||||
|
||||
**Binary file storage**: All terrain binary data lives under
|
||||
`heightmaps/<terrainId>/` where `<terrainId>` is the decimal value of
|
||||
`TerrainComponent::terrainId`:
|
||||
- Base heightmap: `heightmaps/<terrainId>/<heightmapFile>`
|
||||
- Fixup chunks: `heightmaps/<terrainId>/terrain_fixup/x<chunkX>_z<chunkZ>.bin`
|
||||
- Aux maps: `heightmaps/<terrainId>/<auxMap.fileName>`
|
||||
- Package: `heightmaps/<terrainId>.package`
|
||||
|
||||
The terrainId is generated once at component creation, serialized, and survives save/reload unchanged.
|
||||
time, then serialized. This avoids the chicken-and-egg problem of time-based
|
||||
IDs and keeps all data for one terrain in one directory.
|
||||
|
||||
## 9. Build Changes
|
||||
|
||||
@@ -570,48 +1095,66 @@ target_link_libraries(editSceneEditor PUBLIC
|
||||
- `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.
|
||||
- RTShader material generator setup (TerrainMaterialGeneratorA).
|
||||
- CMake (`OgrePaging`/`OgreTerrain` links) and `EditorApp` wiring.
|
||||
- Component registration in `setupECS()` + editor module.
|
||||
|
||||
Definition of done: a paged terrain renders in the editor; terrain pages load
|
||||
and unload as the camera moves.
|
||||
|
||||
### Milestone 2 — Physics integration
|
||||
### Milestone 2 — Serialization + physics integration
|
||||
- Scene serialization for `TerrainComponent` (save/load round-trip).
|
||||
- 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.
|
||||
- Binary heightmap file save/load alongside scene JSON.
|
||||
|
||||
Definition of done: rigid bodies and characters collide flush with the visible
|
||||
terrain; removing/re-adding the terrain entity is leak-free.
|
||||
Definition of done: terrain saves/loads from JSON; 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.
|
||||
### Milestone 3 — Heightmap editing + splat painting
|
||||
- Brush tools (raise/lower/smooth/flatten) with curve-based brush profile.
|
||||
- Splat paint brush (select layer, set opacity, paint blend values via
|
||||
`TerrainLayerBlendMap`).
|
||||
- Sparse 256x256 fixup chunk cache and file I/O (lazy creation, sentinel-based
|
||||
override semantics).
|
||||
- Mark affected pages dirty and rebuild them via CustomTerrainDefiner
|
||||
re-sampling.
|
||||
- Heightmap editor UI (brush radius, strength, type selector).
|
||||
- `AuxMap` infrastructure (create, load, save, edit with brushes).
|
||||
- Fixup chunk cleanup (clear all, delete individual chunks by world position).
|
||||
|
||||
Definition of done: user can sculpt the terrain in the editor and save/load the
|
||||
result.
|
||||
result; visual and collision update after sculpting.
|
||||
|
||||
### Milestone 4 — Procedural roads
|
||||
- Node/edge editing UI.
|
||||
- Wedge geometry generation with `Procedural::TriangleBuffer`.
|
||||
- LOD + visibility distance.
|
||||
- "Comply terrain to roads" writing fixup chunks.
|
||||
- Node/edge editing UI (add, remove, split, join, select, move).
|
||||
- Road configuration parameters UI (lane width, template, material).
|
||||
- Wedge geometry generation using the road mesh template and
|
||||
`Procedural::TriangleBuffer`.
|
||||
- End cap generation for endpoints.
|
||||
- Seam suppression via vertex shifting.
|
||||
- LOD + visibility distance per road mesh segment.
|
||||
- Road physics colliders (JPH::MeshShape per segment).
|
||||
- "Comply terrain to roads" writing fixup chunks with smooth falloff.
|
||||
|
||||
Definition of done: user can draw a road network; terrain flattens under roads
|
||||
with no gaps.
|
||||
Definition of done: user can draw a road network; road geometry appears with
|
||||
correct lanes; terrain flattens under roads with no visible gaps.
|
||||
|
||||
### Milestone 5 — Prefab spawns and compliance
|
||||
- Distance-based prefab spawn/despawn on terrain.
|
||||
- Tool mode to make terrain comply to spawned geometry.
|
||||
### Milestone 5 — Prefab spawns and terrain compliance
|
||||
- `TerrainPrefabSpawnerComponent` + `TerrainPrefabSpawnerModule`.
|
||||
- `TerrainPrefabSpawnerSystem` with distance-based spawn/despawn via
|
||||
`PrefabSystem`.
|
||||
- Terrain-snap at placement (raycast against terrain).
|
||||
- Terrain compliance tool: flatten terrain under prefab footprint using fixup
|
||||
chunks.
|
||||
|
||||
Definition of done: towns/rocks spawn at configured locations and sit flush on
|
||||
terrain.
|
||||
Definition of done: towns/rocks spawn at configured locations on terrain and sit
|
||||
flush; save/load round-trips correctly.
|
||||
|
||||
## 11. Risks and Mitigations
|
||||
|
||||
@@ -620,20 +1163,36 @@ terrain.
|
||||
| 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 world size precision loss | Project already uses `JPH_DOUBLE_PRECISION`; keep world coordinates as `double`/`long` where possible. Scale `worldSize` to realistic scene dimensions. |
|
||||
| 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. |
|
||||
| Road wedge seam gaps | Shift near-seam vertices by 0.05; overlap adjacent wedge edges. |
|
||||
| Fixup chunk read/write races | `CustomTerrainDefiner::define()` runs on Ogre's WorkQueue; fixup writes must happen on the main thread before marking pages dirty. |
|
||||
| Terrain not visible in water reflections | Ensure terrain render queue/visibility mask matches water RTT camera setup. |
|
||||
| TerrainPaging/PageManager destruction order | Destroy `TerrainPaging` before `PageManager` (Paging holds reference to PageManager). |
|
||||
| EditorApp destructor leaves Ogre objects alive | Explicitly `reset()` water/skybox/sun/terrain systems before physics and SceneManager shutdown. |
|
||||
|
||||
## 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.
|
||||
- [ ] Add, remove, add again repeatedly → stable memory (valgrind/ASan).
|
||||
- [ ] 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.
|
||||
- [ ] Place a rigid body on a slope → it rests flush with terrain (no sinking/floating).
|
||||
- [ ] Sculpt heightmap → visual and collision update (brush raise/lower/smooth/flatten).
|
||||
- [ ] Paint splat layer → terrain surface changes appearance at painted locations.
|
||||
- [ ] Paint splat, save, reload → blend map changes persist.
|
||||
- [ ] Create/edit/delete AuxMap → values stored and loaded correctly.
|
||||
- [ ] Clear all fixups → terrain returns to base heightmap only.
|
||||
- [ ] Save scene, exit, reload → terrain restored with correct heightmap and layers.
|
||||
- [ ] Create road network → road geometry appears with correct lane count and terrain compliance.
|
||||
- [ ] Per-edge lane override → edge with `lanesAtoB=2` renders two lanes A→B.
|
||||
- [ ] Save/reload scene with roads → nodes, edges, and config restored.
|
||||
- [ ] Place prefab spawner → prefab appears at correct distance and sits on terrain.
|
||||
- [ ] Move camera far away and back → prefab despawns and respawns correctly.
|
||||
- [ ] Terrain reflected in water → terrain visible in reflection RTT pass.
|
||||
- [ ] Height function round-trip: write known heights → sample them back → values match.
|
||||
- [ ] Fixup chunk persistence: write fixup → save → reload → fixup still applied.
|
||||
- [ ] Build with ASan/UBSan if available → no reports.
|
||||
|
||||
## 13. Reference Code
|
||||
|
||||
Reference in New Issue
Block a user