311 lines
11 KiB
Python
311 lines
11 KiB
Python
"""Terrain Generation Demo - Multi-layer Elevation
|
|
|
|
Demonstrates terrain generation with:
|
|
1. Generate base elevation with simplex FBM
|
|
2. Normalize to 0-1 range
|
|
3. Apply water level (flatten below threshold)
|
|
4. Add mountain enhancement (boost peaks)
|
|
5. Optional erosion simulation
|
|
6. Apply terrain color ranges (biomes)
|
|
"""
|
|
|
|
import mcrfpy
|
|
from typing import List
|
|
from ..core.demo_base import ProcgenDemoBase, StepDef, LayerDef
|
|
from ..core.parameter import Parameter
|
|
|
|
|
|
class TerrainDemo(ProcgenDemoBase):
|
|
"""Interactive multi-layer terrain generation demo."""
|
|
|
|
name = "Terrain"
|
|
description = "Multi-layer elevation with noise and biome coloring"
|
|
MAP_SIZE = (256, 256)
|
|
|
|
# Terrain color ranges (elevation -> color gradient)
|
|
TERRAIN_COLORS = [
|
|
(0.00, 0.15, (30, 50, 120), (50, 80, 150)), # Deep water -> Shallow water
|
|
(0.15, 0.22, (50, 80, 150), (180, 170, 130)), # Shallow water -> Beach
|
|
(0.22, 0.35, (180, 170, 130), (80, 140, 60)), # Beach -> Grass low
|
|
(0.35, 0.55, (80, 140, 60), (50, 110, 40)), # Grass low -> Grass high
|
|
(0.55, 0.70, (50, 110, 40), (100, 90, 70)), # Grass high -> Rock low
|
|
(0.70, 0.85, (100, 90, 70), (140, 130, 120)), # Rock low -> Rock high
|
|
(0.85, 1.00, (140, 130, 120), (220, 220, 225)), # Rock high -> Snow
|
|
]
|
|
|
|
def define_steps(self) -> List[StepDef]:
|
|
"""Define the generation steps."""
|
|
return [
|
|
StepDef("Generate base elevation", self.step_base_elevation,
|
|
"Create initial terrain using simplex FBM noise"),
|
|
StepDef("Normalize heights", self.step_normalize,
|
|
"Normalize elevation values to 0-1 range"),
|
|
StepDef("Apply water level", self.step_water_level,
|
|
"Flatten terrain below water threshold"),
|
|
StepDef("Enhance mountains", self.step_mountains,
|
|
"Boost high elevation areas for dramatic peaks"),
|
|
StepDef("Apply erosion", self.step_erosion,
|
|
"Smooth terrain with erosion simulation"),
|
|
StepDef("Color biomes", self.step_biomes,
|
|
"Apply biome colors based on elevation"),
|
|
]
|
|
|
|
def define_parameters(self) -> List[Parameter]:
|
|
"""Define configurable parameters."""
|
|
return [
|
|
Parameter(
|
|
name="seed",
|
|
display="Seed",
|
|
type="int",
|
|
default=42,
|
|
min_val=0,
|
|
max_val=99999,
|
|
step=1,
|
|
affects_step=0,
|
|
description="Noise seed"
|
|
),
|
|
Parameter(
|
|
name="octaves",
|
|
display="Octaves",
|
|
type="int",
|
|
default=6,
|
|
min_val=1,
|
|
max_val=8,
|
|
step=1,
|
|
affects_step=0,
|
|
description="FBM detail octaves"
|
|
),
|
|
Parameter(
|
|
name="world_size",
|
|
display="Scale",
|
|
type="float",
|
|
default=8.0,
|
|
min_val=2.0,
|
|
max_val=20.0,
|
|
step=1.0,
|
|
affects_step=0,
|
|
description="Noise scale (larger = more zoomed out)"
|
|
),
|
|
Parameter(
|
|
name="water_level",
|
|
display="Water Level",
|
|
type="float",
|
|
default=0.20,
|
|
min_val=0.0,
|
|
max_val=0.40,
|
|
step=0.02,
|
|
affects_step=2,
|
|
description="Sea level threshold"
|
|
),
|
|
Parameter(
|
|
name="mountain_boost",
|
|
display="Mt. Boost",
|
|
type="float",
|
|
default=0.25,
|
|
min_val=0.0,
|
|
max_val=0.50,
|
|
step=0.05,
|
|
affects_step=3,
|
|
description="Mountain height enhancement"
|
|
),
|
|
Parameter(
|
|
name="erosion_passes",
|
|
display="Erosion",
|
|
type="int",
|
|
default=2,
|
|
min_val=0,
|
|
max_val=5,
|
|
step=1,
|
|
affects_step=4,
|
|
description="Erosion smoothing passes"
|
|
),
|
|
]
|
|
|
|
def define_layers(self) -> List[LayerDef]:
|
|
"""Define visualization layers."""
|
|
return [
|
|
LayerDef("colored", "Colored Terrain", "color", z_index=-1, visible=True,
|
|
description="Final terrain with biome colors"),
|
|
LayerDef("elevation", "Elevation", "color", z_index=0, visible=False,
|
|
description="Grayscale height values"),
|
|
LayerDef("water_mask", "Water Mask", "color", z_index=1, visible=False,
|
|
description="Binary water regions"),
|
|
]
|
|
|
|
def __init__(self):
|
|
"""Initialize terrain demo."""
|
|
super().__init__()
|
|
|
|
# Create working heightmaps
|
|
self.hmap_elevation = self.create_heightmap("elevation", 0.0)
|
|
self.hmap_water = self.create_heightmap("water", 0.0)
|
|
|
|
# Noise source
|
|
self.noise = None
|
|
|
|
def _apply_grayscale(self, layer, hmap, alpha=255):
|
|
"""Apply grayscale visualization to layer."""
|
|
w, h = self.MAP_SIZE
|
|
for y in range(h):
|
|
for x in range(w):
|
|
val = hmap[x, y]
|
|
v = int(max(0, min(255, val * 255)))
|
|
layer.set((x, y), mcrfpy.Color(v, v, v, alpha))
|
|
|
|
def _apply_terrain_colors(self, layer, hmap, alpha=255):
|
|
"""Apply terrain biome colors based on elevation."""
|
|
w, h = self.MAP_SIZE
|
|
for y in range(h):
|
|
for x in range(w):
|
|
val = hmap[x, y]
|
|
color = self._elevation_to_color(val, alpha)
|
|
layer.set((x, y), color)
|
|
|
|
def _elevation_to_color(self, val, alpha=255):
|
|
"""Convert elevation value to terrain color."""
|
|
for low, high, c1, c2 in self.TERRAIN_COLORS:
|
|
if low <= val <= high:
|
|
# Interpolate between c1 and c2
|
|
t = (val - low) / (high - low) if high > low else 0
|
|
r = int(c1[0] + t * (c2[0] - c1[0]))
|
|
g = int(c1[1] + t * (c2[1] - c1[1]))
|
|
b = int(c1[2] + t * (c2[2] - c1[2]))
|
|
return mcrfpy.Color(r, g, b, alpha)
|
|
|
|
# Default for out of range
|
|
return mcrfpy.Color(128, 128, 128)
|
|
|
|
# === Step Implementations ===
|
|
|
|
def step_base_elevation(self):
|
|
"""Step 1: Generate base elevation with FBM noise."""
|
|
seed = self.get_param("seed")
|
|
octaves = self.get_param("octaves")
|
|
world_size = self.get_param("world_size")
|
|
|
|
# Create noise source
|
|
self.noise = mcrfpy.NoiseSource(
|
|
dimensions=2,
|
|
algorithm='simplex',
|
|
seed=seed
|
|
)
|
|
|
|
# Fill with FBM noise
|
|
self.hmap_elevation.fill(0.0)
|
|
self.hmap_elevation.add_noise(
|
|
self.noise,
|
|
world_size=(world_size, world_size),
|
|
mode='fbm',
|
|
octaves=octaves
|
|
)
|
|
|
|
# Show raw noise (elevation layer alpha=128 for overlay)
|
|
elevation_layer = self.get_layer("elevation")
|
|
self._apply_grayscale(elevation_layer, self.hmap_elevation, alpha=128)
|
|
|
|
# Also on colored layer (full opacity for final)
|
|
colored_layer = self.get_layer("colored")
|
|
self._apply_grayscale(colored_layer, self.hmap_elevation, alpha=255)
|
|
|
|
def step_normalize(self):
|
|
"""Step 2: Normalize elevation to 0-1 range."""
|
|
self.hmap_elevation.normalize(0.0, 1.0)
|
|
|
|
# Update visualization
|
|
elevation_layer = self.get_layer("elevation")
|
|
self._apply_grayscale(elevation_layer, self.hmap_elevation, alpha=128)
|
|
|
|
colored_layer = self.get_layer("colored")
|
|
self._apply_grayscale(colored_layer, self.hmap_elevation, alpha=255)
|
|
|
|
def step_water_level(self):
|
|
"""Step 3: Flatten terrain below water level."""
|
|
water_level = self.get_param("water_level")
|
|
w, h = self.MAP_SIZE
|
|
|
|
# Create water mask
|
|
self.hmap_water.fill(0.0)
|
|
|
|
for y in range(h):
|
|
for x in range(w):
|
|
val = self.hmap_elevation[x, y]
|
|
if val < water_level:
|
|
# Flatten to water level
|
|
self.hmap_elevation[x, y] = water_level
|
|
self.hmap_water[x, y] = 1.0
|
|
|
|
# Update water mask layer (alpha=128 for overlay)
|
|
water_layer = self.get_layer("water_mask")
|
|
for y in range(h):
|
|
for x in range(w):
|
|
if self.hmap_water[x, y] > 0.5:
|
|
water_layer.set((x, y), mcrfpy.Color(80, 120, 200, 128))
|
|
else:
|
|
water_layer.set((x, y), mcrfpy.Color(30, 30, 35, 128))
|
|
|
|
# Update other layers
|
|
elevation_layer = self.get_layer("elevation")
|
|
self._apply_grayscale(elevation_layer, self.hmap_elevation, alpha=128)
|
|
|
|
colored_layer = self.get_layer("colored")
|
|
self._apply_grayscale(colored_layer, self.hmap_elevation, alpha=255)
|
|
|
|
def step_mountains(self):
|
|
"""Step 4: Enhance mountain peaks."""
|
|
mountain_boost = self.get_param("mountain_boost")
|
|
w, h = self.MAP_SIZE
|
|
|
|
if mountain_boost <= 0:
|
|
return # Skip if no boost
|
|
|
|
for y in range(h):
|
|
for x in range(w):
|
|
val = self.hmap_elevation[x, y]
|
|
# Boost high elevations more than low ones
|
|
# Using a power curve
|
|
if val > 0.5:
|
|
boost = (val - 0.5) * 2 # 0 to 1 for upper half
|
|
boost = boost * boost * mountain_boost # Squared for sharper peaks
|
|
self.hmap_elevation[x, y] = min(1.0, val + boost)
|
|
|
|
# Re-normalize to ensure 0-1 range
|
|
self.hmap_elevation.normalize(0.0, 1.0)
|
|
|
|
# Update visualization
|
|
elevation_layer = self.get_layer("elevation")
|
|
self._apply_grayscale(elevation_layer, self.hmap_elevation, alpha=128)
|
|
|
|
colored_layer = self.get_layer("colored")
|
|
self._apply_grayscale(colored_layer, self.hmap_elevation, alpha=255)
|
|
|
|
def step_erosion(self):
|
|
"""Step 5: Apply erosion/smoothing."""
|
|
erosion_passes = self.get_param("erosion_passes")
|
|
|
|
if erosion_passes <= 0:
|
|
return # Skip if no erosion
|
|
|
|
for _ in range(erosion_passes):
|
|
self.hmap_elevation.smooth(iterations=1)
|
|
|
|
# Update visualization
|
|
elevation_layer = self.get_layer("elevation")
|
|
self._apply_grayscale(elevation_layer, self.hmap_elevation, alpha=128)
|
|
|
|
colored_layer = self.get_layer("colored")
|
|
self._apply_grayscale(colored_layer, self.hmap_elevation, alpha=255)
|
|
|
|
def step_biomes(self):
|
|
"""Step 6: Apply biome colors based on elevation."""
|
|
colored_layer = self.get_layer("colored")
|
|
self._apply_terrain_colors(colored_layer, self.hmap_elevation, alpha=255)
|
|
|
|
|
|
def main():
|
|
"""Run the terrain demo standalone."""
|
|
demo = TerrainDemo()
|
|
demo.activate()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|