282 lines
11 KiB
Python
282 lines
11 KiB
Python
import mcrfpy
|
||
|
||
class ProcgenDemo:
|
||
"""Multi-step procedural generation: terrain with embedded caves."""
|
||
|
||
MAP_SIZE = (64, 48)
|
||
CELL_SIZE = 14
|
||
|
||
# Terrain colors (outside caves)
|
||
TERRAIN_RANGES = [
|
||
((0.0, 0.15), ((30, 50, 120), (50, 80, 150))), # Water
|
||
((0.51, 0.25), ((50, 80, 150), (180, 170, 130))), # Beach
|
||
((0.25, 0.55), ((80, 140, 60), (50, 110, 40))), # Grass
|
||
((0.55, 0.75), ((50, 110, 40), (120, 100, 80))), # Rock
|
||
((0.75, 1.0), ((120, 100, 80), (200, 195, 190))), # Mountain
|
||
]
|
||
|
||
# Cave interior colors
|
||
CAVE_RANGES = [
|
||
((0.0, 0.15), (35, 30, 28)), # Wall (dark)
|
||
((0.15, 0.5), ((50, 45, 42), (100, 90, 80))), # Floor gradient
|
||
((0.5, 1.0), ((100, 90, 80), (140, 125, 105))), # Lighter floor
|
||
]
|
||
|
||
# Mask visualization
|
||
MASK_RANGES = [
|
||
((0.0, 0.01), (20, 20, 25)),
|
||
((0.01, 1.0), (220, 215, 200)),
|
||
]
|
||
|
||
def __init__(self):
|
||
# HeightMaps
|
||
self.terrain = mcrfpy.HeightMap(self.MAP_SIZE, fill=0.0)
|
||
self.cave_selection = mcrfpy.HeightMap(self.MAP_SIZE, fill=0.0)
|
||
self.cave_interior = mcrfpy.HeightMap(self.MAP_SIZE, fill=0.0)
|
||
self.scratchpad = mcrfpy.HeightMap(self.MAP_SIZE, fill=0.0)
|
||
|
||
self.bsp = None
|
||
self.terrain_noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=44)
|
||
self.cave_noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=900)
|
||
|
||
# Scene setup
|
||
scene = mcrfpy.Scene("procgen_demo")
|
||
|
||
self.grid = mcrfpy.Grid(
|
||
grid_size=self.MAP_SIZE,
|
||
pos=(0,0),
|
||
size=(1024, 768),
|
||
layers={"viz": "color"}
|
||
)
|
||
scene.children.append(self.grid)
|
||
|
||
self.title = mcrfpy.Caption(text="Terrain + Cave Procgen",
|
||
pos=(20, 15), font_size=24)
|
||
self.label = mcrfpy.Caption(text="", pos=(20, 45), font_size=16)
|
||
scene.children.append(self.title)
|
||
scene.children.append(self.label)
|
||
|
||
mcrfpy.current_scene = scene
|
||
|
||
# Steps with longer pauses for complex operations
|
||
self.steps = [
|
||
(500, self.step_01_terrain, "1: Generate terrain elevation"),
|
||
(4000, self.step_02_bsp_all, "2: BSP partition (all leaves)"),
|
||
(5000, self.step_03_bsp_subset, "3: Select cave-worthy BSP nodes"),
|
||
(7000, self.step_04_terrain_mask, "4: Exclude low terrain (water/canyon)"),
|
||
(9000, self.step_05_valid_caves, "5: Valid cave regions (BSP && high terrain)"),
|
||
(11000, self.step_06_cave_noise, "6: Organic cave walls (noise threshold)"),
|
||
(13000, self.step_07_apply_to_selection,"7: Walls within selection only"),
|
||
(15000, self.step_08_invert_floors, "8: Invert -> cave floors"),
|
||
(17000, self.step_09_floor_heights, "9: Add floor height variation"),
|
||
(19000, self.step_10_smooth, "10: Smooth floor gradients"),
|
||
(21000, self.step_11_composite, "11: Composite: terrain + caves"),
|
||
(23000, self.step_done, "Complete!"),
|
||
]
|
||
self.current_step = 0
|
||
self.start_time = None
|
||
|
||
self.timer = mcrfpy.Timer("procgen", self.tick, 50)
|
||
|
||
def tick(self, timer, runtime):
|
||
if self.start_time is None:
|
||
self.start_time = runtime
|
||
|
||
elapsed = runtime - self.start_time
|
||
|
||
while (self.current_step < len(self.steps) and
|
||
elapsed >= self.steps[self.current_step][0]):
|
||
_, step_fn, step_label = self.steps[self.current_step]
|
||
self.label.text = step_label
|
||
step_fn()
|
||
self.current_step += 1
|
||
|
||
if self.current_step >= len(self.steps):
|
||
timer.stop()
|
||
|
||
def apply_colors(self, hmap, ranges):
|
||
w, h = self.MAP_SIZE
|
||
for y in range(h):
|
||
for x in range(w):
|
||
val = hmap[x, y]
|
||
color = self._value_to_color(val, ranges)
|
||
self.grid[x, y].viz = color
|
||
|
||
def _value_to_color(self, val, ranges):
|
||
for (lo, hi), color_spec in ranges:
|
||
if lo <= val <= hi:
|
||
if isinstance(color_spec[0], tuple):
|
||
c1, c2 = color_spec
|
||
t = (val - lo) / (hi - lo) if hi > lo else 0
|
||
return tuple(int(c1[i] + t * (c2[i] - c1[i])) for i in range(3))
|
||
else:
|
||
return color_spec
|
||
return (128, 128, 128)
|
||
|
||
# =========================================================
|
||
# STEP 1: BASE TERRAIN
|
||
# =========================================================
|
||
|
||
def step_01_terrain(self):
|
||
"""Generate the base terrain with elevation."""
|
||
self.terrain.fill(0.0)
|
||
self.terrain.add_noise(self.terrain_noise,
|
||
world_size=(10, 10),
|
||
mode='fbm', octaves=5)
|
||
self.terrain.normalize(0.0, 1.0)
|
||
self.apply_colors(self.terrain, self.TERRAIN_RANGES)
|
||
|
||
# =========================================================
|
||
# STEPS 2-5: CAVE SELECTION (where caves can exist)
|
||
# =========================================================
|
||
|
||
def step_02_bsp_all(self):
|
||
"""Show all BSP leaves (potential cave locations)."""
|
||
self.bsp = mcrfpy.BSP(pos=(2, 2), size=(60, 44))
|
||
self.bsp.split_recursive(depth=4, min_size=(8, 6), seed=66)
|
||
|
||
all_rooms = self.bsp.to_heightmap(self.MAP_SIZE, 'leaves', shrink=1)
|
||
self.apply_colors(all_rooms, self.MASK_RANGES)
|
||
|
||
def step_03_bsp_subset(self):
|
||
"""Select only SOME BSP leaves for caves."""
|
||
self.cave_selection.fill(0.0)
|
||
|
||
# Selection criteria: only leaves whose center is in
|
||
# higher terrain AND not too close to edges
|
||
w, h = self.MAP_SIZE
|
||
for leaf in self.bsp.leaves():
|
||
cx, cy = leaf.center()
|
||
|
||
# Skip if center is out of bounds
|
||
if not (0 <= cx < w and 0 <= cy < h):
|
||
continue
|
||
|
||
terrain_height = self.terrain[cx, cy]
|
||
|
||
# Criteria:
|
||
# - Terrain height > 0.4 (above water/beach)
|
||
# - Not too close to map edges
|
||
# - Some randomness based on position
|
||
edge_margin = 8
|
||
in_center = (edge_margin < cx < w - edge_margin and
|
||
edge_margin < cy < h - edge_margin)
|
||
|
||
# Pseudo-random selection based on leaf position
|
||
pseudo_rand = ((cx * 7 + cy * 13) % 10) / 10.0
|
||
|
||
if terrain_height > 0.45 and in_center and pseudo_rand > 0.3:
|
||
# Fill this leaf into selection
|
||
lx, ly = leaf.pos
|
||
lw, lh = leaf.size
|
||
for y in range(ly, ly + lh):
|
||
for x in range(lx, lx + lw):
|
||
if 0 <= x < w and 0 <= y < h:
|
||
self.cave_selection[x, y] = 1.0
|
||
|
||
self.apply_colors(self.cave_selection, self.MASK_RANGES)
|
||
|
||
def step_04_terrain_mask(self):
|
||
"""Create mask of terrain high enough for caves."""
|
||
# Threshold: only where terrain > 0.35 (above water/beach)
|
||
high_terrain = self.terrain.threshold_binary((0.35, 1.0), value=1.0)
|
||
self.scratchpad.copy_from(high_terrain)
|
||
self.apply_colors(self.scratchpad, self.MASK_RANGES)
|
||
|
||
def step_05_valid_caves(self):
|
||
"""AND: selected BSP nodes × high terrain = valid cave regions."""
|
||
# cave_selection has our chosen BSP leaves
|
||
# scratchpad has the "high enough" terrain mask
|
||
self.cave_selection.multiply(self.scratchpad)
|
||
self.apply_colors(self.cave_selection, self.MASK_RANGES)
|
||
|
||
# =========================================================
|
||
# STEPS 6-10: CAVE INTERIOR (detail within selection)
|
||
# =========================================================
|
||
|
||
def step_06_cave_noise(self):
|
||
"""Generate organic noise for cave wall shapes."""
|
||
self.cave_interior.fill(0.0)
|
||
self.cave_interior.add_noise(self.cave_noise,
|
||
world_size=(15, 15),
|
||
mode='fbm', octaves=4)
|
||
self.cave_interior.normalize(0.0, 1.0)
|
||
|
||
# Threshold to binary: 1 = solid (wall), 0 = open
|
||
walls = self.cave_interior.threshold_binary((0.42, 1.0), value=1.0)
|
||
self.cave_interior.copy_from(walls)
|
||
self.apply_colors(self.cave_interior, self.MASK_RANGES)
|
||
|
||
def step_07_apply_to_selection(self):
|
||
"""Walls only within the valid cave selection."""
|
||
# cave_interior has organic wall pattern
|
||
# cave_selection has valid cave regions
|
||
# AND them: walls only where both are 1
|
||
self.cave_interior.multiply(self.cave_selection)
|
||
self.apply_colors(self.cave_interior, self.MASK_RANGES)
|
||
|
||
def step_08_invert_floors(self):
|
||
"""Invert to get floor regions within caves."""
|
||
# cave_interior: 1 = wall, 0 = not-wall
|
||
# We want floors where selection=1 AND wall=0
|
||
# floors = selection AND (NOT walls)
|
||
|
||
walls_inverted = self.cave_interior.inverse()
|
||
walls_inverted.clamp(0.0, 1.0)
|
||
|
||
# AND with selection to get floors only in cave areas
|
||
floors = mcrfpy.HeightMap(self.MAP_SIZE)
|
||
floors.copy_from(self.cave_selection)
|
||
floors.multiply(walls_inverted)
|
||
|
||
self.cave_interior.copy_from(floors)
|
||
self.apply_colors(self.cave_interior, self.MASK_RANGES)
|
||
|
||
def step_09_floor_heights(self):
|
||
"""Add height variation to cave floors."""
|
||
floor_noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=456)
|
||
|
||
heights = mcrfpy.HeightMap(self.MAP_SIZE, fill=0.0)
|
||
heights.add_noise(floor_noise, world_size=(25, 25),
|
||
mode='fbm', octaves=3, scale=0.5)
|
||
heights.add_constant(0.5)
|
||
heights.clamp(0.2, 1.0) # Keep floors visible (not too dark)
|
||
|
||
# Mask to floor regions
|
||
heights.multiply(self.cave_interior)
|
||
self.cave_interior.copy_from(heights)
|
||
self.apply_colors(self.cave_interior, self.CAVE_RANGES)
|
||
|
||
def step_10_smooth(self):
|
||
"""Smooth the floor heights for gradients."""
|
||
self.cave_interior.smooth(iterations=1)
|
||
self.apply_colors(self.cave_interior, self.CAVE_RANGES)
|
||
|
||
# =========================================================
|
||
# STEP 11: COMPOSITE
|
||
# =========================================================
|
||
|
||
def step_11_composite(self):
|
||
"""Composite: terrain outside caves + cave interior inside."""
|
||
w, h = self.MAP_SIZE
|
||
|
||
for y in range(h):
|
||
for x in range(w):
|
||
cave_val = self.cave_interior[x, y]
|
||
terrain_val = self.terrain[x, y]
|
||
|
||
if cave_val > 0.01:
|
||
# Inside cave: use cave colors
|
||
color = self._value_to_color(cave_val, self.CAVE_RANGES)
|
||
else:
|
||
# Outside cave: use terrain colors
|
||
color = self._value_to_color(terrain_val, self.TERRAIN_RANGES)
|
||
|
||
self.grid[x, y].viz = color
|
||
|
||
def step_done(self):
|
||
self.label.text = "Mixed procgen terrain"
|
||
|
||
# Launch
|
||
demo = ProcgenDemo()
|
||
|