320 lines
11 KiB
Python
320 lines
11 KiB
Python
|
|
# terrain_demo.py - Visual demo of terrain system
|
||
|
|
# Shows procedurally generated 3D terrain using HeightMap + Viewport3D
|
||
|
|
|
||
|
|
import mcrfpy
|
||
|
|
import sys
|
||
|
|
import math
|
||
|
|
|
||
|
|
# Create demo scene
|
||
|
|
scene = mcrfpy.Scene("terrain_demo")
|
||
|
|
|
||
|
|
# Dark background frame
|
||
|
|
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(15, 15, 25))
|
||
|
|
scene.children.append(bg)
|
||
|
|
|
||
|
|
# Title
|
||
|
|
title = mcrfpy.Caption(text="Terrain System Demo - HeightMap to 3D Mesh", pos=(20, 10))
|
||
|
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||
|
|
scene.children.append(title)
|
||
|
|
|
||
|
|
# Create the 3D viewport
|
||
|
|
viewport = mcrfpy.Viewport3D(
|
||
|
|
pos=(50, 60),
|
||
|
|
size=(600, 450),
|
||
|
|
render_resolution=(320, 240), # PS1 resolution
|
||
|
|
fov=60.0,
|
||
|
|
camera_pos=(30.0, 20.0, 30.0),
|
||
|
|
camera_target=(20.0, 0.0, 20.0),
|
||
|
|
bg_color=mcrfpy.Color(100, 150, 200) # Sky blue background
|
||
|
|
)
|
||
|
|
scene.children.append(viewport)
|
||
|
|
|
||
|
|
# Generate terrain using HeightMap
|
||
|
|
print("Generating terrain heightmap...")
|
||
|
|
hm = mcrfpy.HeightMap((40, 40))
|
||
|
|
|
||
|
|
# Use midpoint displacement for natural-looking terrain
|
||
|
|
hm.mid_point_displacement(0.5, seed=42)
|
||
|
|
hm.normalize(0.0, 1.0)
|
||
|
|
|
||
|
|
# Optional: Add some erosion for more realistic terrain
|
||
|
|
hm.rain_erosion(drops=1000, erosion=0.08, sedimentation=0.04, seed=42)
|
||
|
|
hm.normalize(0.0, 1.0)
|
||
|
|
|
||
|
|
# Build terrain mesh from heightmap
|
||
|
|
print("Building terrain mesh...")
|
||
|
|
vertex_count = viewport.build_terrain(
|
||
|
|
layer_name="terrain",
|
||
|
|
heightmap=hm,
|
||
|
|
y_scale=8.0, # Vertical exaggeration
|
||
|
|
cell_size=1.0 # World-space grid cell size
|
||
|
|
)
|
||
|
|
print(f"Terrain built with {vertex_count} vertices")
|
||
|
|
|
||
|
|
# Create color maps for terrain (decoupled from height)
|
||
|
|
# This demonstrates using separate HeightMaps for R, G, B channels
|
||
|
|
print("Creating terrain color maps...")
|
||
|
|
r_map = mcrfpy.HeightMap((40, 40))
|
||
|
|
g_map = mcrfpy.HeightMap((40, 40))
|
||
|
|
b_map = mcrfpy.HeightMap((40, 40))
|
||
|
|
|
||
|
|
# Generate a "moisture" map using different noise
|
||
|
|
moisture = mcrfpy.HeightMap((40, 40))
|
||
|
|
moisture.mid_point_displacement(0.6, seed=999)
|
||
|
|
moisture.normalize(0.0, 1.0)
|
||
|
|
|
||
|
|
# Color based on height + moisture combination:
|
||
|
|
# Low + wet = water blue, Low + dry = sand yellow
|
||
|
|
# High + wet = grass green, High + dry = rock brown/snow white
|
||
|
|
for y in range(40):
|
||
|
|
for x in range(40):
|
||
|
|
h = hm[x, y] # Height (0-1)
|
||
|
|
m = moisture[x, y] # Moisture (0-1)
|
||
|
|
|
||
|
|
if h < 0.3: # Low elevation
|
||
|
|
if m > 0.5: # Wet = water
|
||
|
|
r_map[x, y] = 0.2
|
||
|
|
g_map[x, y] = 0.4
|
||
|
|
b_map[x, y] = 0.8
|
||
|
|
else: # Dry = sand
|
||
|
|
r_map[x, y] = 0.9
|
||
|
|
g_map[x, y] = 0.8
|
||
|
|
b_map[x, y] = 0.5
|
||
|
|
elif h < 0.6: # Mid elevation
|
||
|
|
if m > 0.4: # Wet = grass
|
||
|
|
r_map[x, y] = 0.2 + h * 0.3
|
||
|
|
g_map[x, y] = 0.5 + m * 0.3
|
||
|
|
b_map[x, y] = 0.1
|
||
|
|
else: # Dry = dirt
|
||
|
|
r_map[x, y] = 0.5
|
||
|
|
g_map[x, y] = 0.35
|
||
|
|
b_map[x, y] = 0.2
|
||
|
|
else: # High elevation
|
||
|
|
if h > 0.85: # Snow caps
|
||
|
|
r_map[x, y] = 0.95
|
||
|
|
g_map[x, y] = 0.95
|
||
|
|
b_map[x, y] = 1.0
|
||
|
|
else: # Rock
|
||
|
|
r_map[x, y] = 0.5
|
||
|
|
g_map[x, y] = 0.45
|
||
|
|
b_map[x, y] = 0.4
|
||
|
|
|
||
|
|
# Apply colors to terrain
|
||
|
|
viewport.apply_terrain_colors("terrain", r_map, g_map, b_map)
|
||
|
|
print("Terrain colors applied")
|
||
|
|
|
||
|
|
# Info panel on the right
|
||
|
|
info_panel = mcrfpy.Frame(pos=(670, 60), size=(330, 450),
|
||
|
|
fill_color=mcrfpy.Color(30, 30, 40),
|
||
|
|
outline_color=mcrfpy.Color(80, 80, 100),
|
||
|
|
outline=2.0)
|
||
|
|
scene.children.append(info_panel)
|
||
|
|
|
||
|
|
# Panel title
|
||
|
|
panel_title = mcrfpy.Caption(text="Terrain Properties", pos=(690, 70))
|
||
|
|
panel_title.fill_color = mcrfpy.Color(200, 200, 255)
|
||
|
|
scene.children.append(panel_title)
|
||
|
|
|
||
|
|
# Property labels
|
||
|
|
props = [
|
||
|
|
("HeightMap Size:", f"{hm.size[0]}x{hm.size[1]}"),
|
||
|
|
("Vertex Count:", f"{vertex_count}"),
|
||
|
|
("Y Scale:", "8.0"),
|
||
|
|
("Cell Size:", "1.0"),
|
||
|
|
("", ""),
|
||
|
|
("Generation:", ""),
|
||
|
|
(" Algorithm:", "Midpoint Displacement"),
|
||
|
|
(" Roughness:", "0.5"),
|
||
|
|
(" Erosion:", "1000 drops"),
|
||
|
|
("", ""),
|
||
|
|
("Layer Count:", f"{viewport.layer_count()}"),
|
||
|
|
]
|
||
|
|
|
||
|
|
y_offset = 100
|
||
|
|
for label, value in props:
|
||
|
|
if label:
|
||
|
|
cap = mcrfpy.Caption(text=f"{label} {value}", pos=(690, y_offset))
|
||
|
|
cap.fill_color = mcrfpy.Color(180, 180, 200)
|
||
|
|
scene.children.append(cap)
|
||
|
|
y_offset += 22
|
||
|
|
|
||
|
|
# Instructions at bottom
|
||
|
|
instructions = mcrfpy.Caption(
|
||
|
|
text="[Space] Orbit | [C] Colors | [1-4] PS1 effects | [+/-] Height | [ESC] Quit",
|
||
|
|
pos=(20, 530)
|
||
|
|
)
|
||
|
|
instructions.fill_color = mcrfpy.Color(150, 150, 150)
|
||
|
|
scene.children.append(instructions)
|
||
|
|
|
||
|
|
# Status line
|
||
|
|
status = mcrfpy.Caption(text="Status: Terrain rendering with PS1 effects", pos=(20, 555))
|
||
|
|
status.fill_color = mcrfpy.Color(100, 200, 100)
|
||
|
|
scene.children.append(status)
|
||
|
|
|
||
|
|
# Animation state
|
||
|
|
animation_time = [0.0]
|
||
|
|
camera_orbit = [True]
|
||
|
|
terrain_height = [8.0]
|
||
|
|
colors_enabled = [True]
|
||
|
|
|
||
|
|
# White color map for "no colors" mode
|
||
|
|
white_r = mcrfpy.HeightMap((40, 40))
|
||
|
|
white_g = mcrfpy.HeightMap((40, 40))
|
||
|
|
white_b = mcrfpy.HeightMap((40, 40))
|
||
|
|
white_r.fill(1.0)
|
||
|
|
white_g.fill(1.0)
|
||
|
|
white_b.fill(1.0)
|
||
|
|
|
||
|
|
# Camera orbit animation
|
||
|
|
def update_camera(timer, runtime):
|
||
|
|
animation_time[0] += runtime / 1000.0
|
||
|
|
|
||
|
|
if camera_orbit[0]:
|
||
|
|
# Orbit camera around terrain center
|
||
|
|
angle = animation_time[0] * 0.3 # Slow rotation
|
||
|
|
radius = 35.0
|
||
|
|
center_x = 20.0
|
||
|
|
center_z = 20.0
|
||
|
|
height = 15.0 + math.sin(animation_time[0] * 0.2) * 5.0
|
||
|
|
|
||
|
|
x = center_x + math.cos(angle) * radius
|
||
|
|
z = center_z + math.sin(angle) * radius
|
||
|
|
|
||
|
|
viewport.camera_pos = (x, height, z)
|
||
|
|
viewport.camera_target = (center_x, 2.0, center_z)
|
||
|
|
|
||
|
|
# Key handler
|
||
|
|
def on_key(key, state):
|
||
|
|
if state != mcrfpy.InputState.PRESSED:
|
||
|
|
return
|
||
|
|
|
||
|
|
# Toggle PS1 effects with number keys
|
||
|
|
if key == mcrfpy.Key.NUM_1:
|
||
|
|
viewport.enable_vertex_snap = not viewport.enable_vertex_snap
|
||
|
|
status.text = f"Vertex Snap: {'ON' if viewport.enable_vertex_snap else 'OFF'}"
|
||
|
|
elif key == mcrfpy.Key.NUM_2:
|
||
|
|
viewport.enable_affine = not viewport.enable_affine
|
||
|
|
status.text = f"Affine Mapping: {'ON' if viewport.enable_affine else 'OFF'}"
|
||
|
|
elif key == mcrfpy.Key.NUM_3:
|
||
|
|
viewport.enable_dither = not viewport.enable_dither
|
||
|
|
status.text = f"Dithering: {'ON' if viewport.enable_dither else 'OFF'}"
|
||
|
|
elif key == mcrfpy.Key.NUM_4:
|
||
|
|
viewport.enable_fog = not viewport.enable_fog
|
||
|
|
status.text = f"Fog: {'ON' if viewport.enable_fog else 'OFF'}"
|
||
|
|
|
||
|
|
# Toggle terrain colors
|
||
|
|
elif key == mcrfpy.Key.C:
|
||
|
|
colors_enabled[0] = not colors_enabled[0]
|
||
|
|
if colors_enabled[0]:
|
||
|
|
viewport.apply_terrain_colors("terrain", r_map, g_map, b_map)
|
||
|
|
status.text = "Terrain colors: ON (height + moisture)"
|
||
|
|
else:
|
||
|
|
viewport.apply_terrain_colors("terrain", white_r, white_g, white_b)
|
||
|
|
status.text = "Terrain colors: OFF (white)"
|
||
|
|
|
||
|
|
# Camera controls
|
||
|
|
elif key == mcrfpy.Key.SPACE:
|
||
|
|
camera_orbit[0] = not camera_orbit[0]
|
||
|
|
status.text = f"Camera orbit: {'ON' if camera_orbit[0] else 'OFF (WASD/QE to move)'}"
|
||
|
|
|
||
|
|
# Height adjustment
|
||
|
|
elif key == mcrfpy.Key.EQUAL: # + key
|
||
|
|
terrain_height[0] += 1.0
|
||
|
|
rebuild_terrain()
|
||
|
|
elif key == mcrfpy.Key.HYPHEN: # - key
|
||
|
|
terrain_height[0] = max(1.0, terrain_height[0] - 1.0)
|
||
|
|
rebuild_terrain()
|
||
|
|
|
||
|
|
elif key == mcrfpy.Key.ESCAPE:
|
||
|
|
mcrfpy.exit()
|
||
|
|
|
||
|
|
# Manual camera movement (when orbit is off)
|
||
|
|
if not camera_orbit[0]:
|
||
|
|
pos = list(viewport.camera_pos)
|
||
|
|
target = list(viewport.camera_target)
|
||
|
|
speed = 2.0
|
||
|
|
|
||
|
|
if key == mcrfpy.Key.W:
|
||
|
|
# Move forward (toward target)
|
||
|
|
dx = target[0] - pos[0]
|
||
|
|
dz = target[2] - pos[2]
|
||
|
|
length = math.sqrt(dx*dx + dz*dz)
|
||
|
|
if length > 0.01:
|
||
|
|
pos[0] += (dx / length) * speed
|
||
|
|
pos[2] += (dz / length) * speed
|
||
|
|
target[0] += (dx / length) * speed
|
||
|
|
target[2] += (dz / length) * speed
|
||
|
|
elif key == mcrfpy.Key.S:
|
||
|
|
# Move backward
|
||
|
|
dx = target[0] - pos[0]
|
||
|
|
dz = target[2] - pos[2]
|
||
|
|
length = math.sqrt(dx*dx + dz*dz)
|
||
|
|
if length > 0.01:
|
||
|
|
pos[0] -= (dx / length) * speed
|
||
|
|
pos[2] -= (dz / length) * speed
|
||
|
|
target[0] -= (dx / length) * speed
|
||
|
|
target[2] -= (dz / length) * speed
|
||
|
|
elif key == mcrfpy.Key.A:
|
||
|
|
# Strafe left
|
||
|
|
dx = target[0] - pos[0]
|
||
|
|
dz = target[2] - pos[2]
|
||
|
|
# Perpendicular direction
|
||
|
|
pos[0] -= dz / math.sqrt(dx*dx + dz*dz) * speed
|
||
|
|
pos[2] += dx / math.sqrt(dx*dx + dz*dz) * speed
|
||
|
|
target[0] -= dz / math.sqrt(dx*dx + dz*dz) * speed
|
||
|
|
target[2] += dx / math.sqrt(dx*dx + dz*dz) * speed
|
||
|
|
elif key == mcrfpy.Key.D:
|
||
|
|
# Strafe right
|
||
|
|
dx = target[0] - pos[0]
|
||
|
|
dz = target[2] - pos[2]
|
||
|
|
pos[0] += dz / math.sqrt(dx*dx + dz*dz) * speed
|
||
|
|
pos[2] -= dx / math.sqrt(dx*dx + dz*dz) * speed
|
||
|
|
target[0] += dz / math.sqrt(dx*dx + dz*dz) * speed
|
||
|
|
target[2] -= dx / math.sqrt(dx*dx + dz*dz) * speed
|
||
|
|
elif key == mcrfpy.Key.Q:
|
||
|
|
# Move down
|
||
|
|
pos[1] -= speed
|
||
|
|
elif key == mcrfpy.Key.E:
|
||
|
|
# Move up
|
||
|
|
pos[1] += speed
|
||
|
|
|
||
|
|
viewport.camera_pos = tuple(pos)
|
||
|
|
viewport.camera_target = tuple(target)
|
||
|
|
status.text = f"Camera: ({pos[0]:.1f}, {pos[1]:.1f}, {pos[2]:.1f})"
|
||
|
|
|
||
|
|
def rebuild_terrain():
|
||
|
|
"""Rebuild terrain with new height scale"""
|
||
|
|
global vertex_count
|
||
|
|
vertex_count = viewport.build_terrain(
|
||
|
|
layer_name="terrain",
|
||
|
|
heightmap=hm,
|
||
|
|
y_scale=terrain_height[0],
|
||
|
|
cell_size=1.0
|
||
|
|
)
|
||
|
|
# Reapply colors after rebuild
|
||
|
|
if colors_enabled[0]:
|
||
|
|
viewport.apply_terrain_colors("terrain", r_map, g_map, b_map)
|
||
|
|
else:
|
||
|
|
viewport.apply_terrain_colors("terrain", white_r, white_g, white_b)
|
||
|
|
status.text = f"Terrain rebuilt: height scale = {terrain_height[0]}"
|
||
|
|
|
||
|
|
# Set up scene
|
||
|
|
scene.on_key = on_key
|
||
|
|
|
||
|
|
# Create timer for camera animation
|
||
|
|
timer = mcrfpy.Timer("camera_update", update_camera, 16) # ~60fps
|
||
|
|
|
||
|
|
# Activate scene
|
||
|
|
mcrfpy.current_scene = scene
|
||
|
|
|
||
|
|
print()
|
||
|
|
print("Terrain Demo loaded!")
|
||
|
|
print("A 40x40 heightmap has been converted to 3D terrain mesh.")
|
||
|
|
print("Terrain colored using separate R/G/B HeightMaps based on height + moisture.")
|
||
|
|
print()
|
||
|
|
print("Controls:")
|
||
|
|
print(" [C] Toggle terrain colors")
|
||
|
|
print(" [1-4] Toggle PS1 effects")
|
||
|
|
print(" [Space] Toggle camera orbit")
|
||
|
|
print(" [+/-] Adjust terrain height scale")
|
||
|
|
print(" [ESC] Quit")
|