Repository navigation
Terrain and Procedural Generation
SparkEngine includes heightmap terrain rendering and a procedural generation toolkit for creating game content at runtime.
Source: SparkEngine/Source/Engine/ECS/Systems/TerrainSystem.h, SparkEngine/Source/Graphics/TerrainRenderer.h, SparkEngine/Source/Engine/ECS/Components/TerrainComponents.h
Terrain and procedural generation are compiled as part of the engine; they do not have separate CMake toggles.
The terrain system renders large outdoor environments using heightmaps:
- Quadtree LOD — Automatic level-of-detail based on camera distance
- Texture Splatting — Multi-texture blending based on height, slope, or paint masks
-
Heightfield Collision — Jolt Physics
Heightfieldshape for terrain collisions - Chunk-based — Terrain is divided into chunks for efficient culling and streaming
The procedural generation system provides multiple noise algorithms:
| Function | Description |
|---|---|
| Perlin | Classic gradient noise, smooth and continuous |
| Simplex | Faster alternative to Perlin with fewer artifacts |
| Worley | Cell/Voronoi noise for organic patterns |
| FBM | Fractal Brownian Motion — layered octaves for natural detail |
| Ridged Multifractal | Ridge-like noise for mountain ranges |
| Domain Warping | Distorts input coordinates for flowing, organic shapes |
Noise-generated terrain can be refined with erosion:
Simulates material sliding downhill when slope exceeds a threshold:
- Talus angle parameter
- Iteration count
- Material transport rate
Simulates water-based erosion with sediment transport:
- Rain amount and evaporation rate
- Sediment capacity
- Erosion and deposition rates
- Droplet lifetime and inertia
Generate primitive meshes at runtime:
| Shape | Description |
|---|---|
| Plane | Flat grid with configurable subdivisions |
| Box | Cube with per-face normals |
| Sphere | UV sphere or icosphere |
| Cylinder | Cylinder with configurable segments |
| Cone | Cone with base cap |
| Torus | Donut shape with configurable radii |
| Terrain | Heightmap-based terrain mesh |
| Rock | Randomized rock shapes using noise displacement |
| Tree | Simple L-system or billboard tree geometry |
Scatter objects across terrain using configurable rules:
- Height range constraints
- Slope angle constraints
- Density and spacing parameters
- Random rotation and scale variation
- Exclusion zones
Procedural room and dungeon layout generation using the WFC algorithm:
- Define tile types with adjacency rules
- Generate valid layouts that satisfy all constraints
- Configurable grid size and tile sets
- Useful for dungeon crawlers, roguelikes, and level generation
The terrain system supports multiple heightmap formats for interoperability with external tools (World Machine, Gaea, L3DT):
| Format | Extension | Bit Depth | Notes |
|---|---|---|---|
| Raw Binary |
.raw, .r16
|
8-bit or 16-bit unsigned | No header; dimensions must be specified manually |
| PNG Grayscale | .png |
8-bit or 16-bit | Lossless; widely supported by image editors |
| OpenEXR | .exr |
32-bit float | Full floating-point precision; ideal for import from Gaea/World Machine |
| Terrain Archive | .sparkterrain |
32-bit float | SparkEngine native format with embedded metadata (dimensions, scale, splatmap references) |
TerrainImporter importer;
importer.SetDimensions(1025, 1025); // Required for headerless formats
importer.SetHeightRange(0.0f, 500.0f); // Remap to world-space height
importer.SetFlipVertical(true); // Some tools export upside-down
auto heightmap = importer.LoadFromFile("Heightmaps/terrain01.r16");TerrainExporter exporter;
exporter.SetFormat(HeightmapFormat::OpenEXR);
exporter.SetNormalize(true); // Remap values to 0..1 range
exporter.Export(terrain, "Exported/terrain01.exr");Terrain is divided into square chunks (default 64x64 vertices each). Chunks are loaded and unloaded based on camera distance to support large worlds without exhausting memory.
| Parameter | Default | Description |
|---|---|---|
ChunkSize |
64 | Vertices per chunk side (must be power-of-two + 1) |
LoadRadius |
8 chunks | Distance in chunks at which terrain data is loaded into memory |
UnloadRadius |
12 chunks | Distance at which chunk data is evicted from memory |
StreamingBudgetPerFrame |
2 | Maximum chunks loaded or unloaded per frame to avoid hitches |
PriorityMode |
DistanceBased |
Chunks closest to camera load first; alternatives: DirectionBased (favor view direction) |
-
Request -- Camera movement triggers a scan of chunks entering the
LoadRadius. - Load -- Heightmap data is read from disk (or generated procedurally) on a background thread.
- Build -- Vertex/index buffers and collision shapes are created on the main thread.
- Active -- Chunk is rendered and participates in physics queries.
-
Evict -- When the chunk leaves
UnloadRadius, GPU resources are released and data is optionally cached to a memory-mapped file.
Splatmaps control how terrain textures blend across the surface. Each splatmap is an RGBA texture where each channel stores the weight for one terrain layer.
| Channel | Default Layer |
|---|---|
| R | Grass |
| G | Rock |
| B | Dirt |
| A | Sand |
Additional splatmaps can be stacked to support more than four layers. Each splatmap adds four more layers (splatmap 2 covers layers 5--8, etc.). The engine supports up to 16 terrain layers (4 splatmaps).
Weights across all channels at every texel are normalized to sum to 1.0. The terrain shader performs this normalization at sample time:
float4 weights = splatmapTexture.Sample(splatSampler, uv);
weights /= dot(weights, float4(1, 1, 1, 1)); // Normalize- Manual painting -- The SparkEditor terrain brush tool writes directly to splatmap texels with configurable brush size, falloff, and opacity.
-
Auto-splatting -- Rules based on height and slope automatically assign layers:
- Below height threshold -> Sand
- Above slope threshold -> Rock
- High elevation -> Snow (if configured)
- Default -> Grass
The Jolt Physics heightfield shape can use a lower-resolution heightmap than the visual mesh to save CPU:
| Collision LOD | Resolution Ratio | Use Case |
|---|---|---|
| Full | 1:1 with visual | Player collision, precise projectile raycasts |
| Half | 1:2 | Distant AI navigation, vehicle physics |
| Quarter | 1:4 | Broad-phase queries, very distant chunks |
Set per-chunk collision LOD via:
terrain.SetCollisionLOD(chunkIndex, TerrainCollisionLOD::Half);The collision LOD can also be driven automatically by distance from the camera using terrain.SetAutoCollisionLOD(true).
The terrain quadtree subdivides chunks into smaller patches at closer distances. Each LOD level halves the triangle count.
| LOD Level | Triangles (64x64 chunk) | Default Distance Threshold |
|---|---|---|
| 0 (highest) | 8192 | 0 -- 100 m |
| 1 | 2048 | 100 -- 250 m |
| 2 | 512 | 250 -- 600 m |
| 3 | 128 | 600 -- 1500 m |
| 4 (lowest) | 32 | 1500 m+ |
Customize thresholds with:
terrain.SetLODDistance(0, 150.0f); // Override LOD 0 -> 1 boundary
terrain.SetLODBias(1.5f); // Global multiplier on all thresholdsCrack prevention: Adjacent patches at different LOD levels use T-junction stitching strips to prevent visible cracks along chunk boundaries.
All procedurally generated meshes share a standard vertex format:
struct ProceduralVertex
{
float3 Position; // Object-space position
float3 Normal; // Normalized surface normal
float4 Tangent; // Tangent vector (xyz) + bitangent sign (w)
float2 TexCoord0; // Primary UV coordinates
float2 TexCoord1; // Secondary UV (lightmap or detail)
float4 Color; // Per-vertex color (RGBA, default white)
};Total stride: 64 bytes per vertex. The vertex layout is compatible with the standard PBR material shader and supports normal mapping via the tangent/bitangent basis.
Selecting noise parameters has a large impact on terrain character. Here is a recommended starting point and tuning guide:
| Parameter | Recommended Range | Effect |
|---|---|---|
Octaves |
4 -- 8 | More octaves add fine detail; diminishing returns above 8 |
Frequency |
0.001 -- 0.01 | Lower = broader features; higher = more jagged terrain |
Lacunarity |
1.8 -- 2.5 | Controls frequency multiplier between octaves; 2.0 is standard |
Persistence |
0.4 -- 0.6 | Controls amplitude decay between octaves; higher = more rough detail |
Amplitude |
50 -- 500 | World-space height range of the noise output |
Seed |
any uint32 | Deterministic; same seed always produces same output |
- Apply FBM noise as the warp offset for the most natural results.
- Use a warp strength of 50--200 world units for gentle flowing hills; above 400 creates alien-looking landscapes.
- Chain two warp passes for more complex distortion (warp the warp coordinates themselves).
The engine implements the particle-based hydraulic erosion algorithm:
-
Spawn droplet at a random position on the heightmap with initial water volume
Wand zero sedimentS. - Compute gradient using bilinear interpolation of the four neighboring height samples. This determines the downhill direction.
-
Update velocity by blending the previous direction with the gradient direction, weighted by the
Inertiaparameter (0 = pure gradient, 1 = pure inertia). - Move droplet one step in the computed direction.
-
Compute height difference
deltaHbetween old and new positions. -
If moving uphill (
deltaH > 0): deposit min(deltaH,S) sediment at the old position to fill the pit. -
If moving downhill (
deltaH < 0): compute sediment capacityC = max(-deltaH, MinSlope) * Speed * W * SedimentCapacity. If carrying more than capacity, deposit(S - C) * DepositionRate; otherwise erode(C - S) * ErosionRatefrom a radius around the old position. -
Evaporate water:
W *= (1 - EvaporationRate). -
Repeat from step 2 until water is exhausted or
MaxLifetimesteps reached. -
Iterate for
NumDroplets(typically 50,000--200,000 for a 1025x1025 map).
- For each cell, compute slope to all 8 neighbors.
- If any slope exceeds
TalusAngle, transferTransportRate * (slope - TalusAngle)height to that neighbor. - Repeat for
Iterationspasses (typically 20--50).
Tiles for Wave Function Collapse are defined in JSON:
{
"tileset": "dungeon_basic",
"tileSize": [4, 4, 4],
"tiles": [
{
"id": "corridor_ns",
"mesh": "Assets/Tiles/corridor_ns.fbx",
"weight": 1.0,
"sockets": {
"+x": "wall",
"-x": "wall",
"+z": "open_door",
"-z": "open_door",
"+y": "floor",
"-y": "ceiling"
},
"tags": ["corridor"],
"rotatable": true,
"rotations": [0, 90, 180, 270]
}
],
"adjacency": {
"open_door": ["open_door"],
"wall": ["wall"],
"floor": ["floor"],
"ceiling": ["ceiling"]
}
}Socket matching: Two tiles can be placed adjacent if and only if their facing sockets appear together in the adjacency table. The rotatable flag allows the solver to try all listed rotations, multiplying the effective tile count.
The rule-based object placement system supports density maps -- grayscale textures that modulate spawn probability per texel:
- White (1.0): Full density, maximum spawn probability.
- Black (0.0): No spawning in this area.
- Intermediate values scale the configured base density linearly.
Multiple density maps can be layered (one per object type: trees, rocks, grass). The scattering system evaluates density maps in world-space UV matching the terrain splatmap coordinates.
{
"objectType": "pine_tree",
"densityMap": "Textures/Terrain/tree_density.png",
"baseDensity": 0.15,
"minSpacing": 3.0,
"maxSlope": 35.0,
"heightRange": [10.0, 450.0],
"scaleRange": [0.8, 1.4],
"rotationRange": [0, 360],
"alignToNormal": true,
"sinkAmount": 0.1
}Terrain chunks support per-vertex hole flags that make portions of the terrain invisible and non-collidable. This enables:
- Cave entrances -- Punch holes in the terrain surface and place cave meshes underneath.
- Tunnels -- Chain hole regions with tunnel geometry.
- Pits and chasms -- Bottomless pits for gameplay hazards.
terrain.SetHole(chunkX, chunkZ, localX, localZ, true); // Punch a hole
terrain.SetHole(chunkX, chunkZ, localX, localZ, false); // Fill a hole
terrain.SetHoleRect(chunkX, chunkZ, rect, true); // Rectangular hole region
bool isHole = terrain.IsHole(chunkX, chunkZ, localX, localZ);Holes are stored as a bitfield per chunk (1 bit per quad) and are serialized alongside the heightmap data. The collision shape is rebuilt when holes change.
The .sparkterrain native format is version 2 (Spark::Graphics::SparkTerrain::kVersion, magic
'SPKT'). The canonical layout is the code in namespace Spark::Graphics::SparkTerrain in
SparkEngine/Source/Graphics/TerrainRenderer.h; this table summarizes it:
| Section | Content |
|---|---|
| Header | Magic 'SPKT', an explicit version word (2), heightmap dimensions/scale, uint8 generateCollider
|
| Heightmap | 32-bit float per vertex, bounded by kMinHeightmapResolution..kMaxHeightmapResolution (2..8193) |
| Splatmaps | Bounded by kMaxSplatmapResolution (8192) |
| Texture layers | Up to kMaxTextureLayers (64): diffuse/normal/mask paths plus tiling, offset, opacity, metallic, roughness |
| Detail meshes | Up to kMaxDetailMeshes (256) with their instances (kMaxDetailInstancesPerMesh = 4,000,000) |
Both readers (engine and editor) reject version 1 files. Every read goes through a bounds-checked
Reader cursor: a truncated or hostile header can never size an allocation, strings are capped at
kMaxStringLength (4096), and a failed read leaves the reader permanently failed. There is no
per-section compression or checksum in the shipped format.
In SparkEditor, Terrain Save writes to an editable, project-relative path defaulting to
Assets/Terrains/<name>.sparkterrain (with an overwrite confirmation) and creates the directory; it
no longer writes a bare terrain.sparkterrain into the working directory. Texture layers expose
Diffuse/Normal texture fields and detail meshes expose a Mesh Path field.
The terrain can be subdivided into biomes that drive splatmap auto-generation, object scattering rules, and weather/audio ambience:
enum class BiomeType
{
Grassland,
Desert,
Forest,
Tundra,
Swamp,
Mountain,
Custom
};Biome boundaries are defined by a biome map -- a low-resolution texture where each unique color maps to a BiomeType. Biome transitions use a configurable blend radius (in world units) to smoothly cross-fade splatmap weights and scatter densities at boundaries.
{
"biome": "Forest",
"splatRules": {
"base": "grass_dark",
"slope": "mossy_rock",
"high": "pine_needle"
},
"scatterLayers": ["pine_tree", "fern", "mushroom", "fallen_log"],
"ambientAudio": "Audio/Ambience/forest_loop.wav",
"fogColor": [0.2, 0.3, 0.15],
"fogDensity": 0.008
}Terrain can be modified at runtime for gameplay purposes (explosions, digging, building):
// Raise or lower terrain in a circular area
terrain.ModifyHeight(worldPos, radius, deltaHeight, falloff);
// Flatten terrain to a target height
terrain.Flatten(worldPos, radius, targetHeight, strength);
// Smooth terrain (average neighboring heights)
terrain.Smooth(worldPos, radius, strength, iterations);
// Paint splatmap at runtime
terrain.PaintLayer(worldPos, radius, layerIndex, opacity, falloff);Runtime modifications are recorded in a circular buffer. The editor supports undo/redo for terrain edits:
terrain.Undo(); // Revert last modification
terrain.Redo(); // Re-apply last undone modification
terrain.ClearHistory(); // Free modification history memory- Height modifications trigger a partial rebuild of the affected chunk's vertex buffer and collision shape.
- Only the dirty region is updated, not the entire chunk.
- Modifications are batched per frame -- multiple overlapping edits in the same frame are merged.
- For large-scale changes, use
terrain.BeginBatch()/terrain.EndBatch()to defer GPU uploads until all edits are complete.
Heightmap terrain and the procedural-generation toolkit are part of the engine target. Do not pass invented ENABLE_TERRAIN_SYSTEM or ENABLE_PROCEDURAL cache values; CMake does not declare them.
- Physics — Heightfield collision shapes
- Rendering and Graphics — Terrain rendering and LOD
- Scene Management — Procedurally generated scenes
- Asset Pipeline — Terrain asset streaming and loading
- Gameplay Systems — Procedural content for gameplay
- Event System — Terrain generation event hooks
Published from 2b03dc797148. Edit the canonical source in wiki/.
- Documentation
- Docs route
- Wiki index
- Guides
- Tutorials
- Samples
- Examples
- API Reference
- API route
- Reference
- Build Guide
- Dependencies
- FAQ
- Changelog
- Roadmap
- Contributing
- Code of Conduct
- Home
- FAQ
- Getting Started
- Quick-Start Tutorial
- Making Your First Game
- Making Your First Multiplayer Game
- Artist Workflow Guide
- Editor Walkthrough
- Migration Guide
- How SparkEngine Works
- Architecture Overview
- Engine Architecture Flowchart
- Creating a Game Module
- Game Modules (catalog)
- Entity Component System
- Rendering and Graphics
- Physics
- Cloth Simulation
- Audio
- Input System
- Camera System
- Scripting with AngelScript
- Visual Scripting
- AI and Navigation
- Animation
- 2D Systems
- Networking
- Dedicated Server
- Multiplayer Quick Start
- Area Server Architecture
- Scene Management
- Large World Support
- Collaborative Editing
- Coroutine System
- Event System
- Event Response System
- Job System
- UI System
- UI Layout Extensions
- Localization
- Dialogue System
- Destruction System
- Replay System
- Achievement System
- Loading System
- Mod System
- Content Delivery
- Tween System
- Memory Integrity
- Gameplay Systems
- Terrain and Procedural Generation
- Save System
- Persistence System
- Day Night Cycle and Weather
- Cinematic Sequencer
- Runtime Prefabs
- SparkEditor
- Editor Tutorials
- SparkConsole
- SparkDaemon
- Shader Pipeline
- Asset Pipeline
- Asset Validation
- Asset Migration
- Game Packaging
- Online Services
- DataTable System
- Loot and Crafting System
- CSG System
- Font System
- Timer Manager
- Movie Render Pipeline
- HLOD and World Partition
- Remote Debug System
- Selection Manager
- Asset Dependency Graph
- Editor Automation
- File Watcher
- Project Templates
- System Requirements
- VR Support
- Mobile Platform
- Accessibility
- Platform Input
- Platform Certification
- Cross-Compilation: Wine Testing
- RHI Abstraction Layer
- D3D11 Backend
- D3D12 Backend
- Vulkan Backend
- OpenGL Backend
- Metal Backend
- DXR Raytracing
- Hybrid Ray Tracing
- Upscaling (DLSS/FSR)
- Render Graph
- Shader Graph
- GPU Particles
- GPU-Driven Rendering
- Volumetric Fog
- Volumetric Clouds
- Global Illumination
- Virtual Texturing
- Water Rendering
- Clustered Lighting
- Material System
- Post-Processing
- Shadow System
- Particle System
- Decal System
- Sky and Atmosphere
- Foliage System
- Mesh Shaders
- Neural Rendering
- Configuration Reference
- Performance Tips
- Benchmark Framework
- Threading Model
- Fuzz Policy and Parser Security
- Memory Safety
- Memory Management Patterns
- Build System and CMake Modules
- Profiler and Debugging
- Performance Profiling Guide
- Telemetry System
- Crash Reporting
- Golden Image Testing
- Utilities
- Testing
- Fuzz Policy and Parser Security
- Codebase Statistics
- Codebase Health
- Error Handling Patterns
- Hot Reload Overview
- Troubleshooting
- Contributing
- Workflow Patterns
- Build Optimizations
- CI Reproducible Builds
- GitHub API and PR Checks
- Git Rebase Conflicts
- Clang-Format
- Code Quality Violations
- AI Bloat Pattern
- MinGW + Wine Cross-Compilation
- Live Editor Testing
- Engine & Renderer Landscape
- DuetOS Portability Catalog
- Five-Engine Analysis
- Eleven-Engine Analysis
- ThorVG / Unity Graphics Analysis
- Advanced Techniques Catalog
- Third-Party Library Evaluation
- Engine Viability Evaluation
- Engine Feature Recommendations
- Project Recommendations
- Mac Compatibility Analysis
- Codebase Observations
- Codebase Bloat Audit
- Test Suite Audit
- Documentation Coverage Audit
- ThirdParty Dependencies Audit
- Load Test Baseline
- Gameplay Systems Status
- SparkGame Module Status
- Stub and Abandoned Features
- Memory Integrity System
- Memory Safety Evaluation
- Hardware Acceleration Systems
- Jolt Physics Integration
- GPU/CPU Separation Plan
- Daemon Services Architecture
- Reflection & Polymorphism Refactoring Plan
- SparkBuild In-Tree
- Wine No-JobSystem Breakthrough
- Wine Role and Fallback Tiers