Voxel functionality extension

This commit is contained in:
John McCardle 2026-02-05 12:52:18 -05:00
commit 992ea781cb
14 changed files with 3045 additions and 17 deletions

View file

@ -0,0 +1,263 @@
"""VoxelGrid Core Demo (Milestone 9)
Demonstrates the VoxelGrid data structure without rendering.
This is a "console demo" that creates VoxelGrids, defines materials,
places voxel patterns, and displays statistics.
Note: Visual rendering comes in Milestone 10 (VoxelMeshing).
"""
import mcrfpy
from mcrfpy import Color
def format_bytes(bytes_val):
"""Format bytes as human-readable string"""
if bytes_val < 1024:
return f"{bytes_val} B"
elif bytes_val < 1024 * 1024:
return f"{bytes_val / 1024:.1f} KB"
else:
return f"{bytes_val / (1024 * 1024):.1f} MB"
def print_header(title):
"""Print a formatted header"""
print("\n" + "=" * 60)
print(f" {title}")
print("=" * 60)
def print_grid_stats(vg, name="VoxelGrid"):
"""Print statistics for a VoxelGrid"""
print(f"\n {name}:")
print(f" Dimensions: {vg.width} x {vg.height} x {vg.depth}")
print(f" Total voxels: {vg.width * vg.height * vg.depth:,}")
print(f" Cell size: {vg.cell_size} units")
print(f" Materials: {vg.material_count}")
print(f" Non-air voxels: {vg.count_non_air():,}")
print(f" Memory estimate: {format_bytes(vg.width * vg.height * vg.depth)}")
print(f" Offset: {vg.offset}")
print(f" Rotation: {vg.rotation} deg")
def demo_basic_creation():
"""Demonstrate basic VoxelGrid creation"""
print_header("1. Basic VoxelGrid Creation")
# Create various sizes
small = mcrfpy.VoxelGrid(size=(8, 4, 8))
medium = mcrfpy.VoxelGrid(size=(16, 8, 16), cell_size=1.0)
large = mcrfpy.VoxelGrid(size=(32, 16, 32), cell_size=0.5)
print_grid_stats(small, "Small (8x4x8)")
print_grid_stats(medium, "Medium (16x8x16)")
print_grid_stats(large, "Large (32x16x32, 0.5 cell size)")
def demo_material_palette():
"""Demonstrate material palette system"""
print_header("2. Material Palette System")
vg = mcrfpy.VoxelGrid(size=(16, 8, 16))
# Define a palette of building materials
materials = {}
materials['stone'] = vg.add_material("stone", color=Color(128, 128, 128))
materials['brick'] = vg.add_material("brick", color=Color(165, 42, 42))
materials['wood'] = vg.add_material("wood", color=Color(139, 90, 43))
materials['glass'] = vg.add_material("glass",
color=Color(200, 220, 255, 128),
transparent=True,
path_cost=1.0)
materials['metal'] = vg.add_material("metal",
color=Color(180, 180, 190),
path_cost=0.8)
materials['grass'] = vg.add_material("grass", color=Color(60, 150, 60))
print(f"\n Defined {vg.material_count} materials:")
print(f" ID 0: air (implicit, always transparent)")
for name, mat_id in materials.items():
mat = vg.get_material(mat_id)
c = mat['color']
props = []
if mat['transparent']:
props.append("transparent")
if mat['path_cost'] != 1.0:
props.append(f"cost={mat['path_cost']}")
props_str = f" ({', '.join(props)})" if props else ""
print(f" ID {mat_id}: {name} RGB({c.r},{c.g},{c.b},{c.a}){props_str}")
return vg, materials
def demo_voxel_placement():
"""Demonstrate voxel placement patterns"""
print_header("3. Voxel Placement Patterns")
vg, materials = demo_material_palette()
stone = materials['stone']
brick = materials['brick']
wood = materials['wood']
# Pattern 1: Solid cube
print("\n Pattern: Solid 4x4x4 cube at origin")
for z in range(4):
for y in range(4):
for x in range(4):
vg.set(x, y, z, stone)
print(f" Placed {vg.count_material(stone)} stone voxels")
# Pattern 2: Checkerboard floor
print("\n Pattern: Checkerboard floor at y=0, x=6-14, z=0-8")
for z in range(8):
for x in range(6, 14):
mat = stone if (x + z) % 2 == 0 else brick
vg.set(x, 0, z, mat)
print(f" Stone: {vg.count_material(stone)}, Brick: {vg.count_material(brick)}")
# Pattern 3: Hollow cube (walls only)
print("\n Pattern: Hollow cube frame 4x4x4 at x=10, z=10")
for x in range(4):
for y in range(4):
for z in range(4):
# Only place on edges
on_edge_x = (x == 0 or x == 3)
on_edge_y = (y == 0 or y == 3)
on_edge_z = (z == 0 or z == 3)
if sum([on_edge_x, on_edge_y, on_edge_z]) >= 2:
vg.set(10 + x, y, 10 + z, wood)
print(f" Wood voxels: {vg.count_material(wood)}")
print_grid_stats(vg, "After patterns")
# Material breakdown
print("\n Material breakdown:")
print(f" Air: {vg.count_material(0):,} ({100 * vg.count_material(0) / (16*8*16):.1f}%)")
print(f" Stone: {vg.count_material(stone):,}")
print(f" Brick: {vg.count_material(brick):,}")
print(f" Wood: {vg.count_material(wood):,}")
def demo_bulk_operations():
"""Demonstrate bulk fill and clear operations"""
print_header("4. Bulk Operations")
vg = mcrfpy.VoxelGrid(size=(32, 8, 32))
total = 32 * 8 * 32
stone = vg.add_material("stone", color=Color(128, 128, 128))
print(f"\n Grid: 32x8x32 = {total:,} voxels")
# Fill
vg.fill(stone)
print(f" After fill(stone): {vg.count_non_air():,} non-air")
# Clear
vg.clear()
print(f" After clear(): {vg.count_non_air():,} non-air")
def demo_transforms():
"""Demonstrate transform properties"""
print_header("5. Transform Properties")
vg = mcrfpy.VoxelGrid(size=(8, 8, 8))
print(f"\n Default state:")
print(f" Offset: {vg.offset}")
print(f" Rotation: {vg.rotation} deg")
# Position for a building
vg.offset = (100.0, 0.0, 50.0)
vg.rotation = 45.0
print(f"\n After positioning:")
print(f" Offset: {vg.offset}")
print(f" Rotation: {vg.rotation} deg")
# Multiple buildings with different transforms
print("\n Example: Village layout with 3 buildings")
buildings = []
positions = [(0, 0, 0), (20, 0, 0), (10, 0, 15)]
rotations = [0, 90, 45]
for i, (pos, rot) in enumerate(zip(positions, rotations)):
b = mcrfpy.VoxelGrid(size=(8, 6, 8))
b.offset = pos
b.rotation = rot
buildings.append(b)
print(f" Building {i+1}: offset={pos}, rotation={rot} deg")
def demo_edge_cases():
"""Test edge cases and limits"""
print_header("6. Edge Cases and Limits")
# Maximum practical size
print("\n Testing large grid (64x64x64)...")
large = mcrfpy.VoxelGrid(size=(64, 64, 64))
mat = large.add_material("test", color=Color(128, 128, 128))
large.fill(mat)
print(f" Created and filled: {large.count_non_air():,} voxels")
large.clear()
print(f" Cleared: {large.count_non_air()} voxels")
# Bounds checking
print("\n Bounds checking (should not crash):")
small = mcrfpy.VoxelGrid(size=(4, 4, 4))
test_mat = small.add_material("test", color=Color(255, 0, 0))
small.set(-1, 0, 0, test_mat)
small.set(100, 0, 0, test_mat)
print(f" Out-of-bounds get(-1,0,0): {small.get(-1, 0, 0)} (expected 0)")
print(f" Out-of-bounds get(100,0,0): {small.get(100, 0, 0)} (expected 0)")
# Material palette capacity
print("\n Material palette capacity test:")
full_vg = mcrfpy.VoxelGrid(size=(4, 4, 4))
for i in range(255):
full_vg.add_material(f"mat_{i}", color=Color(i, i, i))
print(f" Added 255 materials: count = {full_vg.material_count}")
try:
full_vg.add_material("overflow", color=Color(255, 255, 255))
print(" ERROR: Should have raised exception!")
except RuntimeError as e:
print(f" 256th material correctly rejected: {e}")
def demo_memory_usage():
"""Show memory usage for various grid sizes"""
print_header("7. Memory Usage Estimates")
sizes = [
(8, 8, 8),
(16, 8, 16),
(32, 16, 32),
(64, 32, 64),
(80, 16, 45), # Example dungeon size
]
print("\n Size Voxels Memory")
print(" " + "-" * 40)
for w, h, d in sizes:
voxels = w * h * d
memory = voxels # 1 byte per voxel
print(f" {w:3}x{h:3}x{d:3} {voxels:>10,} {format_bytes(memory):>10}")
def main():
"""Run all demos"""
print("\n" + "=" * 60)
print(" VOXELGRID CORE DEMO (Milestone 9)")
print(" Dense 3D Voxel Array with Material Palette")
print("=" * 60)
demo_basic_creation()
demo_material_palette()
demo_voxel_placement()
demo_bulk_operations()
demo_transforms()
demo_edge_cases()
demo_memory_usage()
print_header("Demo Complete!")
print("\n Next milestone (10): Voxel Mesh Generation")
print(" The VoxelGrid data will be converted to renderable 3D meshes.")
print()
if __name__ == "__main__":
import sys
main()
sys.exit(0)

View file

@ -0,0 +1,273 @@
# voxel_dungeon_demo.py - Procedural dungeon demonstrating bulk voxel operations
# Milestone 11: Bulk Operations and Building Primitives
import mcrfpy
import sys
import math
import random
# Create demo scene
scene = mcrfpy.Scene("voxel_dungeon_demo")
# Dark background
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(20, 20, 30))
scene.children.append(bg)
# Title
title = mcrfpy.Caption(text="Voxel Dungeon Demo - Bulk Operations (Milestone 11)", 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=(620, 520),
render_resolution=(400, 320),
fov=60.0,
camera_pos=(40.0, 30.0, 40.0),
camera_target=(16.0, 4.0, 16.0),
bg_color=mcrfpy.Color(30, 30, 40) # Dark atmosphere
)
scene.children.append(viewport)
# Global voxel grid reference
voxels = None
seed = 42
def generate_dungeon(dungeon_seed=42):
"""Generate a procedural dungeon showcasing all bulk operations"""
global voxels, seed
seed = dungeon_seed
random.seed(seed)
# Create voxel grid for dungeon
print(f"Generating dungeon (seed={seed})...")
voxels = mcrfpy.VoxelGrid(size=(32, 12, 32), cell_size=1.0)
# Define materials
STONE_WALL = voxels.add_material("stone_wall", color=mcrfpy.Color(80, 80, 90))
STONE_FLOOR = voxels.add_material("stone_floor", color=mcrfpy.Color(100, 95, 90))
MOSS = voxels.add_material("moss", color=mcrfpy.Color(40, 80, 40))
WATER = voxels.add_material("water", color=mcrfpy.Color(40, 80, 160, 180), transparent=True)
PILLAR = voxels.add_material("pillar", color=mcrfpy.Color(120, 110, 100))
GOLD = voxels.add_material("gold", color=mcrfpy.Color(255, 215, 0))
print(f"Defined {voxels.material_count} materials")
# 1. Main room using fill_box_hollow
print("Building main room with fill_box_hollow...")
voxels.fill_box_hollow((2, 0, 2), (29, 10, 29), STONE_WALL, thickness=1)
# 2. Floor with slight variation using fill_box
voxels.fill_box((3, 0, 3), (28, 0, 28), STONE_FLOOR)
# 3. Spherical alcoves carved into walls using fill_sphere
print("Carving alcoves with fill_sphere...")
alcove_positions = [
(2, 5, 16), # West wall
(29, 5, 16), # East wall
(16, 5, 2), # North wall
(16, 5, 29), # South wall
]
for pos in alcove_positions:
voxels.fill_sphere(pos, 3, 0) # Carve out (air)
# 4. Small decorative spheres (gold orbs in alcoves)
print("Adding gold orbs in alcoves...")
for i, pos in enumerate(alcove_positions):
# Offset inward so orb is visible
ox, oy, oz = pos
if ox < 10:
ox += 2
elif ox > 20:
ox -= 2
if oz < 10:
oz += 2
elif oz > 20:
oz -= 2
voxels.fill_sphere((ox, oy - 1, oz), 1, GOLD)
# 5. Support pillars using fill_cylinder
print("Building pillars with fill_cylinder...")
pillar_positions = [
(8, 1, 8), (8, 1, 24),
(24, 1, 8), (24, 1, 24),
(16, 1, 8), (16, 1, 24),
(8, 1, 16), (24, 1, 16),
]
for px, py, pz in pillar_positions:
voxels.fill_cylinder((px, py, pz), 1, 9, PILLAR)
# 6. Moss patches using fill_noise
print("Adding moss patches with fill_noise...")
voxels.fill_noise((3, 1, 3), (28, 1, 28), MOSS, threshold=0.65, scale=0.15, seed=seed)
# 7. Central water pool
print("Creating water pool...")
voxels.fill_box((12, 0, 12), (20, 0, 20), 0) # Carve depression
voxels.fill_box((12, 0, 12), (20, 0, 20), WATER)
# 8. Copy a pillar as prefab and paste variations
print("Creating prefab from pillar and pasting copies...")
pillar_prefab = voxels.copy_region((8, 1, 8), (9, 9, 9))
print(f" Pillar prefab: {pillar_prefab.size}")
# Paste smaller pillars at corners (offset from main room)
corner_positions = [(4, 1, 4), (4, 1, 27), (27, 1, 4), (27, 1, 27)]
for cx, cy, cz in corner_positions:
voxels.paste_region(pillar_prefab, (cx, cy, cz), skip_air=True)
# Build mesh
voxels.rebuild_mesh()
print(f"\nDungeon generated:")
print(f" Non-air voxels: {voxels.count_non_air()}")
print(f" Vertices: {voxels.vertex_count}")
print(f" Faces: {voxels.vertex_count // 6}")
# Add to viewport
# First remove old layer if exists
if viewport.voxel_layer_count() > 0:
pass # Can't easily remove, so we regenerate the whole viewport
viewport.add_voxel_layer(voxels, z_index=0)
return voxels
# Generate initial dungeon
generate_dungeon(42)
# Create info panel
info_frame = mcrfpy.Frame(pos=(690, 60), size=(300, 280), fill_color=mcrfpy.Color(40, 40, 60, 220))
scene.children.append(info_frame)
info_title = mcrfpy.Caption(text="Dungeon Stats", pos=(700, 70))
info_title.fill_color = mcrfpy.Color(255, 255, 100)
scene.children.append(info_title)
def update_stats():
global stats_caption
stats_text = f"""Grid: {voxels.width}x{voxels.height}x{voxels.depth}
Total cells: {voxels.width * voxels.height * voxels.depth}
Non-air: {voxels.count_non_air()}
Materials: {voxels.material_count}
Mesh Stats:
Vertices: {voxels.vertex_count}
Faces: {voxels.vertex_count // 6}
Seed: {seed}
Operations Used:
- fill_box_hollow (walls)
- fill_sphere (alcoves)
- fill_cylinder (pillars)
- fill_noise (moss)
- copy/paste (prefabs)"""
stats_caption.text = stats_text
stats_caption = mcrfpy.Caption(text="", pos=(700, 100))
stats_caption.fill_color = mcrfpy.Color(200, 200, 200)
scene.children.append(stats_caption)
update_stats()
# Controls panel
controls_frame = mcrfpy.Frame(pos=(690, 360), size=(300, 180), fill_color=mcrfpy.Color(40, 40, 60, 220))
scene.children.append(controls_frame)
controls_title = mcrfpy.Caption(text="Controls", pos=(700, 370))
controls_title.fill_color = mcrfpy.Color(255, 255, 100)
scene.children.append(controls_title)
controls_text = """R - Regenerate dungeon (new seed)
1-4 - Camera presets
+/- - Zoom in/out
SPACE - Reset camera
ESC - Exit demo"""
controls = mcrfpy.Caption(text=controls_text, pos=(700, 400))
controls.fill_color = mcrfpy.Color(200, 200, 200)
scene.children.append(controls)
# Camera animation state
rotation_enabled = False
camera_distance = 50.0
camera_angle = 45.0 # degrees
camera_height = 30.0
camera_presets = [
(40.0, 30.0, 40.0, 16.0, 4.0, 16.0), # Default diagonal
(16.0, 30.0, 50.0, 16.0, 4.0, 16.0), # Front view
(50.0, 30.0, 16.0, 16.0, 4.0, 16.0), # Side view
(16.0, 50.0, 16.0, 16.0, 4.0, 16.0), # Top-down
]
def rotate_camera(timer_name, runtime):
"""Timer callback for camera rotation"""
global camera_angle, rotation_enabled
if rotation_enabled:
camera_angle += 0.5
if camera_angle >= 360.0:
camera_angle = 0.0
rad = camera_angle * math.pi / 180.0
x = 16.0 + camera_distance * math.cos(rad)
z = 16.0 + camera_distance * math.sin(rad)
viewport.camera_pos = (x, camera_height, z)
# Set up rotation timer
timer = mcrfpy.Timer("rotate_cam", rotate_camera, 33)
def handle_key(key, action):
"""Keyboard handler"""
global rotation_enabled, seed, camera_distance, camera_height
if action != mcrfpy.InputState.PRESSED:
return
if key == mcrfpy.Key.R:
seed = random.randint(1, 99999)
generate_dungeon(seed)
update_stats()
print(f"Regenerated dungeon with seed {seed}")
elif key == mcrfpy.Key.NUM_1:
viewport.camera_pos = camera_presets[0][:3]
viewport.camera_target = camera_presets[0][3:]
rotation_enabled = False
elif key == mcrfpy.Key.NUM_2:
viewport.camera_pos = camera_presets[1][:3]
viewport.camera_target = camera_presets[1][3:]
rotation_enabled = False
elif key == mcrfpy.Key.NUM_3:
viewport.camera_pos = camera_presets[2][:3]
viewport.camera_target = camera_presets[2][3:]
rotation_enabled = False
elif key == mcrfpy.Key.NUM_4:
viewport.camera_pos = camera_presets[3][:3]
viewport.camera_target = camera_presets[3][3:]
rotation_enabled = False
elif key == mcrfpy.Key.SPACE:
rotation_enabled = not rotation_enabled
print(f"Camera rotation: {'ON' if rotation_enabled else 'OFF'}")
elif key == mcrfpy.Key.EQUALS or key == mcrfpy.Key.ADD:
camera_distance = max(20.0, camera_distance - 5.0)
camera_height = max(15.0, camera_height - 2.0)
elif key == mcrfpy.Key.DASH or key == mcrfpy.Key.SUBTRACT:
camera_distance = min(80.0, camera_distance + 5.0)
camera_height = min(50.0, camera_height + 2.0)
elif key == mcrfpy.Key.ESCAPE:
print("Exiting demo...")
sys.exit(0)
scene.on_key = handle_key
# Activate the scene
mcrfpy.current_scene = scene
print("\nVoxel Dungeon Demo ready!")
print("Press SPACE to toggle camera rotation, R to regenerate")
# Main entry point for --exec mode
if __name__ == "__main__":
print("\n=== Voxel Dungeon Demo Summary ===")
print(f"Grid size: {voxels.width}x{voxels.height}x{voxels.depth}")
print(f"Non-air voxels: {voxels.count_non_air()}")
print(f"Generated vertices: {voxels.vertex_count}")
print(f"Rendered faces: {voxels.vertex_count // 6}")
print("===================================\n")

View file

@ -0,0 +1,250 @@
#!/usr/bin/env python3
"""Visual Demo: Milestone 12 - VoxelGrid Navigation Projection
Demonstrates projection of 3D voxel terrain to 2D navigation grid for pathfinding.
Shows:
1. Voxel dungeon with multiple levels
2. Navigation grid projection (walkable/unwalkable areas)
3. A* pathfinding through the projected terrain
4. FOV computation from voxel transparency
"""
import mcrfpy
import sys
import math
def create_demo_scene():
"""Create the navigation projection demo scene"""
scene = mcrfpy.Scene("voxel_nav_demo")
# =========================================================================
# Create a small dungeon-style voxel grid
# =========================================================================
vg = mcrfpy.VoxelGrid((16, 8, 16), cell_size=1.0)
# Add materials
floor_mat = vg.add_material("floor", (100, 80, 60)) # Brown floor
wall_mat = vg.add_material("wall", (80, 80, 90), transparent=False) # Gray walls
pillar_mat = vg.add_material("pillar", (60, 60, 70), transparent=False) # Dark pillars
glass_mat = vg.add_material("glass", (150, 200, 255), transparent=True) # Transparent glass
water_mat = vg.add_material("water", (50, 100, 200), transparent=True, path_cost=3.0) # Slow water
# Create floor
vg.fill_box((0, 0, 0), (15, 0, 15), floor_mat)
# Create outer walls
vg.fill_box((0, 1, 0), (15, 4, 0), wall_mat) # North wall
vg.fill_box((0, 1, 15), (15, 4, 15), wall_mat) # South wall
vg.fill_box((0, 1, 0), (0, 4, 15), wall_mat) # West wall
vg.fill_box((15, 1, 0), (15, 4, 15), wall_mat) # East wall
# Interior walls creating rooms
vg.fill_box((5, 1, 0), (5, 4, 10), wall_mat) # Vertical wall
vg.fill_box((10, 1, 5), (15, 4, 5), wall_mat) # Horizontal wall
# Doorways (carve holes)
vg.fill_box((5, 1, 3), (5, 2, 4), 0) # Door in vertical wall
vg.fill_box((12, 1, 5), (13, 2, 5), 0) # Door in horizontal wall
# Central pillars
vg.fill_box((8, 1, 8), (8, 4, 8), pillar_mat)
vg.fill_box((8, 1, 12), (8, 4, 12), pillar_mat)
# Water pool in one corner (slow movement)
vg.fill_box((1, 0, 11), (3, 0, 14), water_mat)
# Glass window
vg.fill_box((10, 2, 5), (11, 3, 5), glass_mat)
# Raised platform in one area (height variation)
vg.fill_box((12, 1, 8), (14, 1, 13), floor_mat) # Platform at y=1
# =========================================================================
# Create Viewport3D with navigation grid
# =========================================================================
viewport = mcrfpy.Viewport3D(pos=(10, 10), size=(600, 400))
viewport.set_grid_size(16, 16)
viewport.cell_size = 1.0
# Configure camera for top-down view
viewport.camera_pos = (8, 15, 20)
viewport.camera_target = (8, 0, 8)
# Add voxel layer
viewport.add_voxel_layer(vg, z_index=0)
# Project voxels to navigation grid with headroom=2 (entity needs 2 voxels height)
viewport.project_voxel_to_nav(vg, headroom=2)
# =========================================================================
# Info panel
# =========================================================================
info_frame = mcrfpy.Frame(pos=(620, 10), size=(250, 400))
info_frame.fill_color = mcrfpy.Color(30, 30, 40, 220)
info_frame.outline_color = mcrfpy.Color(100, 100, 120)
info_frame.outline = 2.0
title = mcrfpy.Caption(text="Nav Projection Demo", pos=(10, 10))
title.fill_color = mcrfpy.Color(255, 255, 100)
desc = mcrfpy.Caption(text="Voxels projected to\n2D nav grid", pos=(10, 35))
desc.fill_color = mcrfpy.Color(200, 200, 200)
info1 = mcrfpy.Caption(text="Grid: 16x16 cells", pos=(10, 75))
info1.fill_color = mcrfpy.Color(150, 200, 255)
info2 = mcrfpy.Caption(text="Headroom: 2 voxels", pos=(10, 95))
info2.fill_color = mcrfpy.Color(150, 200, 255)
# Count walkable cells
walkable_count = 0
for x in range(16):
for z in range(16):
cell = viewport.at(x, z)
if cell.walkable:
walkable_count += 1
info3 = mcrfpy.Caption(text=f"Walkable: {walkable_count}/256", pos=(10, 115))
info3.fill_color = mcrfpy.Color(100, 255, 100)
# Find path example
path = viewport.find_path((1, 1), (13, 13))
info4 = mcrfpy.Caption(text=f"Path length: {len(path)}", pos=(10, 135))
info4.fill_color = mcrfpy.Color(255, 200, 100)
# FOV example
fov = viewport.compute_fov((8, 8), 10)
info5 = mcrfpy.Caption(text=f"FOV cells: {len(fov)}", pos=(10, 155))
info5.fill_color = mcrfpy.Color(200, 150, 255)
# Legend
legend_title = mcrfpy.Caption(text="Materials:", pos=(10, 185))
legend_title.fill_color = mcrfpy.Color(255, 255, 255)
leg1 = mcrfpy.Caption(text=" Floor (walkable)", pos=(10, 205))
leg1.fill_color = mcrfpy.Color(100, 80, 60)
leg2 = mcrfpy.Caption(text=" Wall (blocking)", pos=(10, 225))
leg2.fill_color = mcrfpy.Color(80, 80, 90)
leg3 = mcrfpy.Caption(text=" Water (slow)", pos=(10, 245))
leg3.fill_color = mcrfpy.Color(50, 100, 200)
leg4 = mcrfpy.Caption(text=" Glass (see-through)", pos=(10, 265))
leg4.fill_color = mcrfpy.Color(150, 200, 255)
controls = mcrfpy.Caption(text="[Space] Recompute FOV\n[P] Show path\n[Q] Quit", pos=(10, 300))
controls.fill_color = mcrfpy.Color(150, 150, 150)
info_frame.children.extend([
title, desc, info1, info2, info3, info4, info5,
legend_title, leg1, leg2, leg3, leg4, controls
])
# =========================================================================
# Status bar
# =========================================================================
status_frame = mcrfpy.Frame(pos=(10, 420), size=(860, 50))
status_frame.fill_color = mcrfpy.Color(20, 20, 30, 220)
status_frame.outline_color = mcrfpy.Color(80, 80, 100)
status_frame.outline = 1.0
status_text = mcrfpy.Caption(
text="Milestone 12: VoxelGrid Navigation Projection - Project 3D voxels to 2D pathfinding grid",
pos=(10, 15)
)
status_text.fill_color = mcrfpy.Color(180, 180, 200)
status_frame.children.append(status_text)
# =========================================================================
# Add elements to scene
# =========================================================================
scene.children.extend([viewport, info_frame, status_frame])
# Store references for interaction (using module-level globals)
global demo_viewport, demo_voxelgrid, demo_path, demo_fov_origin
demo_viewport = viewport
demo_voxelgrid = vg
demo_path = path
demo_fov_origin = (8, 8)
# =========================================================================
# Keyboard handler
# =========================================================================
def on_key(key, state):
global demo_fov_origin
if state != mcrfpy.InputState.PRESSED:
return
if key == mcrfpy.Key.Q or key == mcrfpy.Key.ESCAPE:
# Exit
sys.exit(0)
elif key == mcrfpy.Key.SPACE:
# Recompute FOV from different origin
ox, oz = demo_fov_origin
ox = (ox + 3) % 14 + 1
oz = (oz + 5) % 14 + 1
demo_fov_origin = (ox, oz)
fov = demo_viewport.compute_fov((ox, oz), 8)
info5.text = f"FOV from ({ox},{oz}): {len(fov)}"
elif key == mcrfpy.Key.P:
# Show path info
print(f"Path from (1,1) to (13,13): {len(demo_path)} steps")
for i, (px, pz) in enumerate(demo_path[:10]):
cell = demo_viewport.at(px, pz)
print(f" Step {i}: ({px},{pz}) h={cell.height:.1f} cost={cell.cost:.1f}")
if len(demo_path) > 10:
print(f" ... and {len(demo_path) - 10} more steps")
scene.on_key = on_key
return scene
def main():
"""Main entry point"""
print("=== Milestone 12: VoxelGrid Navigation Projection Demo ===")
print()
print("This demo shows how 3D voxel terrain is projected to a 2D")
print("navigation grid for pathfinding and FOV calculations.")
print()
print("The projection scans each column from top to bottom, finding")
print("the topmost walkable floor with adequate headroom.")
print()
scene = create_demo_scene()
mcrfpy.current_scene = scene
# Print nav grid summary
grid_w, grid_d = demo_viewport.grid_size
print("Navigation grid summary:")
print(f" Grid size: {grid_w}x{grid_d}")
# Count by walkability and transparency
walkable = 0
blocking = 0
transparent = 0
for x in range(grid_w):
for z in range(grid_d):
cell = demo_viewport.at(x, z)
if cell.walkable:
walkable += 1
else:
blocking += 1
if cell.transparent:
transparent += 1
print(f" Walkable cells: {walkable}")
print(f" Blocking cells: {blocking}")
print(f" Transparent cells: {transparent}")
print()
if __name__ == "__main__":
main()
sys.exit(0)

View file

@ -0,0 +1,314 @@
"""Voxel Serialization Demo - Milestone 14
Demonstrates save/load functionality for VoxelGrid, including:
- Saving to file with .mcvg format
- Loading from file
- Serialization to bytes (for network/custom storage)
- RLE compression effectiveness
"""
import mcrfpy
import os
import tempfile
def create_demo_scene():
"""Create a scene demonstrating voxel serialization."""
scene = mcrfpy.Scene("voxel_serialization_demo")
ui = scene.children
# Dark background
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=(20, 20, 30))
ui.append(bg)
# Title
title = mcrfpy.Caption(text="Milestone 14: VoxelGrid Serialization",
pos=(30, 20))
title.font_size = 28
title.fill_color = (255, 220, 100)
ui.append(title)
# Create demo VoxelGrid with interesting structure
grid = mcrfpy.VoxelGrid((16, 16, 16), cell_size=1.0)
# Add materials
stone = grid.add_material("stone", (100, 100, 110))
wood = grid.add_material("wood", (139, 90, 43))
glass = grid.add_material("glass", (180, 200, 220, 100), transparent=True)
gold = grid.add_material("gold", (255, 215, 0))
# Build a small structure
grid.fill_box((0, 0, 0), (15, 0, 15), stone) # Floor
grid.fill_box((0, 1, 0), (0, 4, 15), stone) # Wall 1
grid.fill_box((15, 1, 0), (15, 4, 15), stone) # Wall 2
grid.fill_box((0, 1, 0), (15, 4, 0), stone) # Wall 3
grid.fill_box((0, 1, 15), (15, 4, 15), stone) # Wall 4
# Windows (clear some wall, add glass)
grid.fill_box((6, 2, 0), (10, 3, 0), 0) # Clear for window
grid.fill_box((6, 2, 0), (10, 3, 0), glass) # Add glass
# Pillars
grid.fill_box((4, 1, 4), (4, 3, 4), wood)
grid.fill_box((12, 1, 4), (12, 3, 4), wood)
grid.fill_box((4, 1, 12), (4, 3, 12), wood)
grid.fill_box((12, 1, 12), (12, 3, 12), wood)
# Gold decorations
grid.set(8, 1, 8, gold)
grid.set(7, 1, 8, gold)
grid.set(9, 1, 8, gold)
grid.set(8, 1, 7, gold)
grid.set(8, 1, 9, gold)
# Get original stats
original_voxels = grid.count_non_air()
original_materials = grid.material_count
# === Test save/load to file ===
with tempfile.NamedTemporaryFile(suffix='.mcvg', delete=False) as f:
temp_path = f.name
save_success = grid.save(temp_path)
file_size = os.path.getsize(temp_path) if save_success else 0
# Load into new grid
loaded_grid = mcrfpy.VoxelGrid((1, 1, 1))
load_success = loaded_grid.load(temp_path)
os.unlink(temp_path) # Clean up
loaded_voxels = loaded_grid.count_non_air() if load_success else 0
loaded_materials = loaded_grid.material_count if load_success else 0
# === Test to_bytes/from_bytes ===
data_bytes = grid.to_bytes()
bytes_size = len(data_bytes)
bytes_grid = mcrfpy.VoxelGrid((1, 1, 1))
bytes_success = bytes_grid.from_bytes(data_bytes)
bytes_voxels = bytes_grid.count_non_air() if bytes_success else 0
# === Calculate compression ===
raw_size = 16 * 16 * 16 # Uncompressed voxel data
compression_ratio = raw_size / bytes_size if bytes_size > 0 else 0
# Display information
y_pos = 80
# Original Grid Info
info1 = mcrfpy.Caption(text="Original VoxelGrid:",
pos=(30, y_pos))
info1.font_size = 20
info1.fill_color = (100, 200, 255)
ui.append(info1)
y_pos += 30
for line in [
f" Dimensions: 16x16x16 = 4096 voxels",
f" Non-air voxels: {original_voxels}",
f" Materials defined: {original_materials}",
f" Structure: Walled room with pillars, windows, gold decor"
]:
cap = mcrfpy.Caption(text=line, pos=(30, y_pos))
cap.font_size = 16
cap.fill_color = (200, 200, 210)
ui.append(cap)
y_pos += 22
y_pos += 20
# File Save/Load Results
info2 = mcrfpy.Caption(text="File Serialization (.mcvg):",
pos=(30, y_pos))
info2.font_size = 20
info2.fill_color = (100, 255, 150)
ui.append(info2)
y_pos += 30
save_status = "SUCCESS" if save_success else "FAILED"
load_status = "SUCCESS" if load_success else "FAILED"
match_status = "MATCH" if loaded_voxels == original_voxels else "MISMATCH"
for line in [
f" Save to file: {save_status}",
f" File size: {file_size} bytes",
f" Load from file: {load_status}",
f" Loaded voxels: {loaded_voxels} ({match_status})",
f" Loaded materials: {loaded_materials}"
]:
color = (150, 255, 150) if "SUCCESS" in line or "MATCH" in line else (200, 200, 210)
if "FAILED" in line or "MISMATCH" in line:
color = (255, 100, 100)
cap = mcrfpy.Caption(text=line, pos=(30, y_pos))
cap.font_size = 16
cap.fill_color = color
ui.append(cap)
y_pos += 22
y_pos += 20
# Bytes Serialization Results
info3 = mcrfpy.Caption(text="Memory Serialization (to_bytes/from_bytes):",
pos=(30, y_pos))
info3.font_size = 20
info3.fill_color = (255, 200, 100)
ui.append(info3)
y_pos += 30
bytes_status = "SUCCESS" if bytes_success else "FAILED"
bytes_match = "MATCH" if bytes_voxels == original_voxels else "MISMATCH"
for line in [
f" Serialized size: {bytes_size} bytes",
f" Raw voxel data: {raw_size} bytes",
f" Compression ratio: {compression_ratio:.1f}x",
f" from_bytes(): {bytes_status}",
f" Restored voxels: {bytes_voxels} ({bytes_match})"
]:
color = (200, 200, 210)
if "SUCCESS" in line or "MATCH" in line:
color = (150, 255, 150)
cap = mcrfpy.Caption(text=line, pos=(30, y_pos))
cap.font_size = 16
cap.fill_color = color
ui.append(cap)
y_pos += 22
y_pos += 20
# RLE Compression Demo
info4 = mcrfpy.Caption(text="RLE Compression Effectiveness:",
pos=(30, y_pos))
info4.font_size = 20
info4.fill_color = (200, 150, 255)
ui.append(info4)
y_pos += 30
# Create uniform grid for compression test
uniform_grid = mcrfpy.VoxelGrid((32, 32, 32))
uniform_mat = uniform_grid.add_material("solid", (128, 128, 128))
uniform_grid.fill(uniform_mat)
uniform_bytes = uniform_grid.to_bytes()
uniform_raw = 32 * 32 * 32
uniform_ratio = uniform_raw / len(uniform_bytes)
for line in [
f" Uniform 32x32x32 filled grid:",
f" Raw: {uniform_raw} bytes",
f" Compressed: {len(uniform_bytes)} bytes",
f" Compression: {uniform_ratio:.0f}x",
f" ",
f" RLE excels at runs of identical values."
]:
cap = mcrfpy.Caption(text=line, pos=(30, y_pos))
cap.font_size = 16
cap.fill_color = (200, 180, 220)
ui.append(cap)
y_pos += 22
y_pos += 30
# File Format Info
info5 = mcrfpy.Caption(text="File Format (.mcvg):",
pos=(30, y_pos))
info5.font_size = 20
info5.fill_color = (255, 150, 200)
ui.append(info5)
y_pos += 30
for line in [
" Header: Magic 'MCVG' + version + dimensions + cell_size",
" Materials: name, color (RGBA), sprite_index, transparent, path_cost",
" Voxel data: RLE-encoded material IDs",
" ",
" Note: Transform (offset, rotation) is runtime state, not serialized"
]:
cap = mcrfpy.Caption(text=line, pos=(30, y_pos))
cap.font_size = 14
cap.fill_color = (200, 180, 200)
ui.append(cap)
y_pos += 20
# API Reference on right side
y_ref = 80
x_ref = 550
api_title = mcrfpy.Caption(text="Python API:", pos=(x_ref, y_ref))
api_title.font_size = 20
api_title.fill_color = (150, 200, 255)
ui.append(api_title)
y_ref += 35
for line in [
"# Save to file",
"success = grid.save('world.mcvg')",
"",
"# Load from file",
"grid = VoxelGrid((1,1,1))",
"success = grid.load('world.mcvg')",
"",
"# Save to bytes",
"data = grid.to_bytes()",
"",
"# Load from bytes",
"success = grid.from_bytes(data)",
"",
"# Network example:",
"# send_to_server(grid.to_bytes())",
"# data = recv_from_server()",
"# grid.from_bytes(data)"
]:
cap = mcrfpy.Caption(text=line, pos=(x_ref, y_ref))
cap.font_size = 14
if line.startswith("#"):
cap.fill_color = (100, 150, 100)
elif "=" in line or "(" in line:
cap.fill_color = (255, 220, 150)
else:
cap.fill_color = (180, 180, 180)
ui.append(cap)
y_ref += 18
return scene
# Run demonstration
if __name__ == "__main__":
import sys
# Create and activate the scene
scene = create_demo_scene()
mcrfpy.current_scene = scene
# When run directly, print summary and exit for headless testing
print("\n=== Voxel Serialization Demo (Milestone 14) ===\n")
# Run a quick verification
grid = mcrfpy.VoxelGrid((8, 8, 8))
mat = grid.add_material("test", (100, 100, 100))
grid.fill_box((0, 0, 0), (7, 0, 7), mat)
print(f"Created 8x8x8 grid with {grid.count_non_air()} non-air voxels")
# Test to_bytes
data = grid.to_bytes()
print(f"Serialized to {len(data)} bytes")
# Test from_bytes
grid2 = mcrfpy.VoxelGrid((1, 1, 1))
success = grid2.from_bytes(data)
print(f"from_bytes(): {'SUCCESS' if success else 'FAILED'}")
print(f"Restored size: {grid2.size}")
print(f"Restored voxels: {grid2.count_non_air()}")
# Compression test
big_grid = mcrfpy.VoxelGrid((32, 32, 32))
big_mat = big_grid.add_material("solid", (128, 128, 128))
big_grid.fill(big_mat)
big_data = big_grid.to_bytes()
raw_size = 32 * 32 * 32
print(f"\nCompression test (32x32x32 uniform):")
print(f" Raw: {raw_size} bytes")
print(f" Compressed: {len(big_data)} bytes")
print(f" Ratio: {raw_size / len(big_data):.0f}x")
print("\n=== Demo complete ===")
sys.exit(0)