Terrain3D in Polyphase Engine
Overview
Terrain3D is a heightmap-based terrain node that generates a renderable mesh grid from per-vertex height data. It supports multi-material painting via splatmap and texture atlas, procedural splatmap generation from elevation/slope rules, collision, NavMesh integration, heightmap import, grayscale brush masks, terrain chunk snapping, and baked texture export.
Key features: - Configurable grid resolution and world-space dimensions - Runtime height manipulation via C++ and Lua - Import heightmap from grayscale textures - 4-slot material system with texture atlas support - Baked splatmap textures for smooth per-pixel material blending - Procedural splatmap generation from elevation and slope rules (Auto-Generate + Mountain presets) - Grayscale brush mask textures for custom brush shapes - Stamp tools for quickly applying height/material from textures - Automatic collision generation (Bullet physics) - NavMesh triangle contribution for pathfinding - Snap grid for aligning terrain chunks edge-to-edge - Bake & Save system with companion .oct files for PIE and runtime - Works on all platforms (Vulkan, GX, C3D) using MaterialLite
Quick Start
Creating a Terrain
- Right-click in the Scene hierarchy > Add Node > 3D > Terrain3D, OR
- Right-click in the Viewport > Terrain
A save dialog appears. Navigate to your project's Assets folder and type a base name (e.g., MyTerrain). Two companion files are created automatically:
- MyTerrain_Splatmap.oct — placeholder baked splatmap texture
- MyTerrain_Heightmap.oct — grayscale heightmap texture
The terrain spawns with collision enabled and atlas/material slots pre-configured.
Basic Sculpting Workflow
- Select the Terrain3D node
- Switch to Terrain Sculpt mode (toolbar dropdown or "Open In Terrain Sculpt" in Properties)
- Use Raise/Lower/Flatten/Smooth modes to sculpt the heightmap
- Assign an Atlas Texture in the Properties inspector (Atlas Texturing section)
- Set Tiles X/Y to match your atlas grid
- Set tile indices for each slot in Material Slot Tiles
- Switch to Paint Mat mode to paint materials
- Click Bake & Save Map to export the blended texture
- The baked map persists in PIE and runtime builds
How It Works
Heightmap Storage
- Data format:
std::vector<float>with one height value per vertex, stored row-major (Z-major) - Default resolution: 128x128 vertices
- Default height scale: 10.0 (matches Blender/DCC conventions)
- Coordinate system: Integer grid coordinates (x, z) starting from (0, 0)
- Height values: Raw floats multiplied by
mHeightScaleduring mesh generation
Mesh Generation Pipeline
- When heights or splatmap are modified, the node is marked dirty
- On the next tick,
RebuildMeshInternal()is called - Non-atlas mode: A regular grid of shared vertices with simple UV mapping
- Atlas mode: Per-triangle unique vertices (unshared) to prevent UV discontinuities at tile boundaries — same approach as Voxel3D
- Per-vertex normals are computed via central differences on the heightmap
- The mesh is uploaded to the GPU
- Collision shape is rebuilt from the same triangle data
Material System
Texture Atlas
The terrain uses a texture atlas (a single image containing a grid of material tiles) combined with a splatmap to determine which material appears where.
Setup: 1. Enable Atlas = checked 2. Set Atlas Texture to your atlas image (e.g., 512x256 with 64x64 tiles) 3. Set Tiles X/Y to match (e.g., 8x4) 4. Use Material Slots = checked 5. Assign tile indices to each slot in Material Slot Tiles
Splatmap
Each vertex stores 4 material weights packed as RGBA in vertex color: - R channel = Slot 0 weight - G channel = Slot 1 weight - B channel = Slot 2 weight - A channel = Slot 3 weight
Values are 0-255. When painting, weights are normalized so they sum to 1.0.
Baked Splatmap Texture
For smooth per-pixel material transitions (instead of per-triangle), enable Bake Splatmap. This generates a texture on the CPU where each pixel is the weighted blend of all 4 material tiles sampled from the atlas:
- Check Bake Splatmap in the Atlas Texturing properties
- Set Bake Resolution (512 default, increase for higher quality)
- Set Texture Tiling (controls how many times each material tile repeats — e.g., 8 = 8 grid cells per repeat)
- Click Bake & Save Map to export
The baked texture is saved as a .oct asset and assigned to the Baked Map property. It persists across PIE, runtime builds, and sessions.
Slot Tint Colors
Each material slot has a tint color that modulates the atlas tile during baking. Default is white (no tint). Set to gray (~0.5) if textures appear too bright with the Lit shading model.
Brush Mask System
The editor supports grayscale texture brush masks for custom brush shapes:
- Any RGBA texture works (R channel is sampled)
- White (255) = full brush effect, Black (0) = no effect
- The mask is mapped to the brush radius area
- When no mask is set, a radial cosine falloff is used
To assign a brush mask:
1. Import a grayscale brush texture as a .oct asset
2. Drag it from the Assets panel onto the "Drop Brush Mask Here" button in the Terrain Sculpt panel
Stamp tools (appear when a brush mask is assigned): - Stamp as Height — replaces the entire heightmap with the mask (white=high, black=low) - Add Height — additively blends the mask onto existing heights using current brush Strength - Stamp as Splat — stamps the mask as splatmap weight for the selected material slot, normalizes other slots, and auto-saves
Editor Tools
Entering Terrain Sculpt Mode
- Select a Terrain3D node in the scene
- Click "Open In Terrain Sculpt" in the Properties inspector, or
- Select "Terrain Sculpt" from the editor mode dropdown in the toolbar
Sculpt Modes
| Mode | Color | Description |
|---|---|---|
| Raise | Green | Increase height under brush |
| Lower | Red | Decrease height under brush |
| Flatten | Yellow | Blend heights toward target value |
| Smooth | Blue | Average heights with neighbors |
| Paint Mat | Purple | Paint material slot weights |
Brush Settings
- Radius: World-space brush radius (0.5 - 50)
- Strength: Effect intensity per frame (0.01 - 1.0)
- Falloff: Edge softness when no brush mask (0 = hard, 1 = soft cosine)
- Brush Mask: Optional grayscale texture for custom brush shape (drag from Assets panel)
- Target Height: Only for Flatten mode
- Material Slot: Only for Paint Material mode (0-3), with tile texture previews
Material Slot Tiles
The Material Slot Tiles section (open by default) lets you assign which atlas tile each slot uses: - 32px tile texture preview for each slot - Input field with +/- buttons to set the tile index - Changes take effect on next bake
Auto-Generate Splatmap
Procedurally generate the splatmap from elevation and slope rules:
Per-slot controls: - Height Min/Max — normalized elevation range (0=lowest point, 1=highest point on terrain) - Slope Min/Max — angle in degrees (0=flat ground, 90=vertical cliff) - Blend — smooth falloff at rule edges - Strength — overall weight multiplier
Default rules:
| Slot | Preset | Height Range | Slope Range |
|---|---|---|---|
| Slot 0 | Grass | 0% - 55% | 0 - 30 deg |
| Slot 1 | Rock | 0% - 100% | 30 - 90 deg |
| Slot 2 | Snow | 60% - 100% | 0 - 45 deg |
| Slot 3 | Dirt | 30% - 70% | 10 - 40 deg |
Buttons: - Generate Splatmap — generates + auto-saves baked map to disk - Generate (Preview) — generates + preview only (no save)
Mountain Generate Splatmap
A simplified preset for mountain terrain with 4 named zones:
| Zone | Default Slot | Description |
|---|---|---|
| Ground | Slot 0 | Low elevation, flat areas (grass/dirt) |
| Mountain Side | Slot 1 | Steep slopes at any height (rock/cliffs) |
| Mountain Top | Slot 2 | High flat areas (alpine grass/gravel) |
| Peak | Slot 3 | Highest points (snow/ice) |
Controls: - Each zone has a Slot selector to remap which atlas tile it uses - Ground Max Height: elevation cutoff for lowlands - Mountain Side Min Slope: angle threshold for steep slopes - Mountain Top Min Height: where mountain tops begin - Peak Min Height: where the very top begins - Blend: shared edge softness
Buttons: - Generate Mountain — generates + auto-saves - Preview Mountain — generates + preview only
Actions
- Flatten All — reset all heights to 0
- Rebuild Mesh — force immediate mesh rebuild
- Bake Splatmap — preview the baked blend texture (no save)
- Bake & Save Map — bake and save to disk (auto-overwrites if already saved)
- Save Heightmap — export heightmap as grayscale texture asset
Debug Splatmap
Check Debug Splatmap to visualize raw splatmap weights as vertex color tints: - Red = Slot 0 - Green = Slot 1 - Blue = Slot 2 - Alpha = Slot 3
Hover info shows per-vertex weight percentages.
Warning Banner
A yellow warning appears when atlas + material slots are enabled but no Baked Map is saved. Click "Bake & Save Map" to resolve.
C++ API
Height Access
void SetHeight(int32_t x, int32_t z, float height);
float GetHeight(int32_t x, int32_t z) const;
float GetHeightAtWorldPos(float worldX, float worldZ) const; // Bilinear interpolation
void SetHeightFromTexture(Texture* texture); // Import from grayscale image
void FlattenAll(float height = 0.0f);
void Resize(int32_t resX, int32_t resZ, float worldW, float worldD);
Material Slots
void SetMaterialSlot(int32_t slot, Material* mat); // 0-3, MaterialLite only
Material* GetMaterialSlot(int32_t slot) const;
void SetMaterialWeight(int32_t x, int32_t z, int32_t slot, float weight);
float GetMaterialWeight(int32_t x, int32_t z, int32_t slot) const;
Baking & Export
void BakeSplatmapTexture(); // Runtime bake (preview)
void BakeAndSaveMap(const std::string& path = ""); // Bake + save to disk
void BakeAndSaveHeightmap(const std::string& path = ""); // Export heightmap
void GenerateSplatmapFromRules(); // Procedural splatmap from rules
Mesh Control
void MarkDirty(); // Mark for rebuild on next tick
void RebuildMesh(); // Force immediate rebuild
bool IsDirty() const; // Check if rebuild pending
Accessors
int32_t GetResolutionX() const;
int32_t GetResolutionZ() const;
float GetWorldWidth() const;
float GetWorldDepth() const;
uint32_t GetNumVertices() const;
uint32_t GetNumIndices() const;
const std::vector<VertexColor>& GetVertices() const;
const std::vector<IndexType>& GetIndices() const;
Lua API
Example Usage
-- Create a terrain
local terrain = world:SpawnNode("Terrain3D")
terrain:SetPosition(Vec(0, 0, 0))
-- Set some heights
for x = 0, 127 do
for z = 0, 127 do
local h = math.sin(x * 0.1) * math.cos(z * 0.1) * 2.0
terrain:SetHeight(x, z, h)
end
end
-- Import a heightmap texture
local hmap = LoadAsset("T_Heightmap")
terrain:SetHeightFromTexture(hmap)
-- Paint material slot 1 in a region
for x = 20, 40 do
for z = 20, 40 do
terrain:SetMaterialWeight(x, z, 1, 1.0)
terrain:SetMaterialWeight(x, z, 0, 0.0)
end
end
-- Force rebuild
terrain:RebuildMesh()
Method Reference
| Method | Arguments | Returns | Description |
|---|---|---|---|
SetHeight |
x, z, height |
- | Set height at grid position |
GetHeight |
x, z |
float |
Get height at grid position |
GetHeightAtWorldPos |
worldX, worldZ |
float |
Bilinear interpolated height |
SetHeightFromTexture |
texture |
- | Import grayscale heightmap |
FlattenAll |
[height] |
- | Set all heights (default 0) |
Resize |
resX, resZ, worldW, worldD |
- | Resize grid (clears data) |
GetResolution |
- | x, z |
Get grid resolution |
GetWorldSize |
- | w, d |
Get world dimensions |
GetHeightScale |
- | float |
Get height multiplier |
SetHeightScale |
scale |
- | Set height multiplier |
MarkDirty |
- | - | Mark for rebuild |
RebuildMesh |
- | - | Force immediate rebuild |
IsDirty |
- | bool |
Check if rebuild pending |
SetMaterialSlot |
slot, material |
- | Set material (0-3) |
GetMaterialSlot |
slot |
material |
Get material at slot |
SetMaterialWeight |
x, z, slot, weight |
- | Set per-vertex weight |
GetMaterialWeight |
x, z, slot |
float |
Get per-vertex weight |
SetSnapGridSize |
size |
- | Set snap grid (0=off) |
GetSnapGridSize |
- | float |
Get snap grid size |
Platform-Specific Behavior
Vulkan (Windows / Linux / Android)
- Full support with dedicated vertex/index buffer management
- Uses
VertexColorvertex type with theForwardColorshader pipeline - Single draw call per Terrain3D node
GameCube / Wii (GX)
- Full support via display list generation with automatic batching for meshes exceeding 65,535 indices
- Same vertex attribute layout as Voxel3D
- Per-element endian-safe serialization for cross-platform compatibility
- Consider smaller resolutions (64x64 or less) for memory constraints
3DS (C3D)
- Full support via double-buffered vertex/index data
- Same shader pipeline as static meshes
- Consider smaller resolutions for performance
Material Compatibility
Terrain3D uses MaterialLite exclusively, ensuring compatibility across all platforms including Wii and 3DS.
Serialization
Terrain data is embedded in scene files with versioned fields:
ASSET_VERSION_TERRAIN3D (23):
- 2x int32: resolution (X, Z)
- 3x float: world width, world depth, height scale
- 1x float: snap grid size
- Per-float heightmap data (endian-safe)
- Per-uint32 splatmap data (endian-safe)
- 4x AssetRef: material slot references
- 1x AssetRef: heightmap texture reference
ASSET_VERSION_TERRAIN3D_MATSLOTS (24):
- bool: use material slots
ASSET_VERSION_TERRAIN3D_ATLAS (25):
- bool: enable atlas texturing
- AssetRef: atlas texture
- uint32: tiles X, tiles Y
- float: texture tiling
- Per-slot: int32 tile index + vec4 tint color
ASSET_VERSION_TERRAIN3D_BAKE (26):
- bool: bake splatmap
- uint32: bake resolution
ASSET_VERSION_TERRAIN3D_BAKEDMAP (27):
- AssetRef: baked map texture
Collision
Terrain3D generates a btBvhTriangleMeshShape from its mesh geometry:
- Built automatically when the mesh is rebuilt
- Enabled by default on spawn (collision group 1)
- Supports physics raycasting (used by the editor sculpt tools)
- Internal edge info generated for smoother physics behavior
NavMesh Integration
Terrain3D nodes automatically contribute their triangles to the NavMesh system:
- World::GatherNavTriangles() includes Terrain3D triangles
- Triangles are filtered by NavMesh3D bounds (same as StaticMesh3D)
- Call BuildNavigationData() after terrain changes to update pathfinding
Terrain Snapping
Multiple terrain chunks can be placed edge-to-edge:
- Set Snap Grid property to match World Width (e.g., 128.0)
- Position is automatically snapped to multiples of the grid size
- All terrain chunks with the same resolution and world size align their edges
Companion Files
When a Terrain3D is created via the editor, companion .oct texture assets are generated:
| File | Purpose |
|---|---|
{Name}_Splatmap.oct |
Baked splatmap texture (blended materials) |
{Name}_Heightmap.oct |
Grayscale heightmap export |
These files: - Are saved to the user's chosen directory via save dialog - Auto-overwrite on subsequent bakes (no repeated dialogs) - Can be shared between projects - Work in PIE and all runtime platforms
Configurable Properties
Terrain Section
| Property | Type | Default | Description |
|---|---|---|---|
| Resolution X | Integer | 128 | Grid vertices along X |
| Resolution Z | Integer | 128 | Grid vertices along Z |
| World Width | Float | 128.0 | World-space extent X |
| World Depth | Float | 128.0 | World-space extent Z |
| Height Scale | Float | 10.0 | Height value multiplier |
| Snap Grid | Float | 0.0 | Position snap grid (0=off) |
| Use Material Slots | Bool | false | Enable splatmap vertex colors |
| Heightmap Texture | Texture | None | Source heightmap (auto-imports on set) |
Atlas Texturing Section
| Property | Type | Default | Description |
|---|---|---|---|
| Enable Atlas | Bool | false | Enable atlas-based texturing |
| Atlas Texture | Texture | None | Texture atlas image |
| Tiles X | Integer | 4 | Atlas grid columns |
| Tiles Y | Integer | 4 | Atlas grid rows |
| Texture Tiling | Float | 32.0 | Grid cells per texture repeat |
| Slot 0-3 Tile | Integer | 0,1,2,3 | Atlas tile index per slot |
| Slot 0-3 Tint | Color | White | Per-slot color multiplier |
| Bake Splatmap | Bool | false | Enable CPU-blended baked texture |
| Bake Resolution | Integer | 512 | Baked texture size (NxN) |
| Baked Map | Texture | None | Saved baked texture asset |
Practical Guidance
When to Use Terrain3D
Good use cases: - Outdoor terrain and landscapes - Height-based procedural generation - Levels with large ground surfaces - Multi-material terrain (grass, dirt, rock, snow)
Consider alternatives when: - Cave interiors or overhangs (heightmaps can't represent overhangs) - Very small detail areas (use StaticMesh3D instead) - Voxel-based terrain (use Voxel3D)
Recommended Workflow
- Create Terrain3D via right-click menu (saves companion files)
- Import a heightmap texture OR sculpt with Raise/Lower tools
- Assign atlas texture, set tile grid dimensions
- Set slot tile indices in Material Slot Tiles
- Use Mountain Generate for quick procedural materials
- Fine-tune with Paint Mat mode
- Click Bake & Save Map to export final texture
- Test in PIE
Performance Considerations
- Resolution: 128x128 = 16K verts (shared) or ~97K verts (atlas mode). 64x64 recommended for GCN/3DS
- Bake resolution: Higher = better quality but more memory. 512 is good default, 1024 for detailed terrains
- Atlas mode: Uses 6x more vertices than non-atlas (per-triangle unshared). Disable atlas for performance-critical platforms
- Rebuild frequency: Batch height changes before the next tick
- Draw calls: Each Terrain3D node is one draw call
Best Practices
- Batch modifications: Set multiple heights before the next tick to avoid multiple rebuilds
- Use heightmap import: More efficient than sculpting for complex terrain shapes
- Bake before shipping: Always click Bake & Save Map so the texture persists in runtime
- Tint colors: Set to ~0.5 gray if textures appear too bright with Lit shading
- Stamp tools: Use brush mask stamps for quickly applying height/material patterns
- Auto-generate first, paint second: Use Mountain Generate for the base, then fine-tune with manual painting
Future Roadmap
Features planned for future phases:
- Custom terrain shader: Per-pixel splatmap blending in the fragment shader (eliminates bake step)
- LOD / Geomipmapping: Distance-based mesh simplification
- Chunked streaming: Load/unload terrain chunks based on camera distance
- Edge stitching: Automatic height averaging at shared edges between chunks
- Erosion tools: Hydraulic and thermal erosion brushes
- Foliage painting: Scatter instanced meshes based on splatmap weights
- Multi-layer atlas: Support for more than 4 material slots via multiple atlases