Implementation Tutorials and Replication Guides

This section provides step-by-step guides to replicate core Pyrite systems from scratch. Each tutorial assumes knowledge of the relevant documentation section and focuses on practical implementation.

Tutorial 1: Build a Terrain Generation System

Objective: Implement a procedural terrain generator using OpenSimplex noise that produces biome-based, deterministic worlds.

Time: 2 hours | Difficulty: Medium

Prerequisites: - Understand OpenSimplex noise (read terrain.rst) - Familiarity with NumPy (for array operations) - Python 3.9+

Step 1: Set Up Noise Generator

import hashlib
from opensimplex import OpenSimplex
import numpy as np

class TerrainGenerator:
    def __init__(self, seed_string):
        # Convert string seed to integer
        hash_obj = hashlib.md5(seed_string.encode())
        self.seed = int(hash_obj.hexdigest(), 16) % (2**31 - 1)

        # Initialize noise generator
        self.noise_gen = OpenSimplex(self.seed)

        # Constants
        self.CHUNK_SIZE = 48
        self.STONE_LVL = 8
        self.WATER_LVL = 5.6

    def noise_2d(self, x, z, scale):
        """Sample 2D OpenSimplex noise"""
        return self.noise_gen.noise2(x * scale, z * scale)

    def noise_3d(self, x, y, z, scale):
        """Sample 3D OpenSimplex noise"""
        return self.noise_gen.noise3(x * scale, y * scale, z * scale)

Step 2: Implement Height Generation (FBm)

def get_height_at_column(self, world_x, world_z):
    """Calculate terrain height using fractional Brownian motion"""
    continentalness = 0
    amplitude = 1.0
    frequency = 0.005
    max_amplitude = 0  # For normalization

    # 4 octaves of noise
    for octave in [1, 2, 4, 8]:
        freq = frequency * octave
        noise = self.noise_2d(world_x, world_z, freq)
        continentalness += noise * amplitude
        max_amplitude += amplitude
        amplitude *= 0.5

    # Normalize
    continentalness /= max_amplitude
    continentalness = np.clip(continentalness, -1.0, 1.0)

    # Compute height based on continentalness
    if continentalness < -0.2:
        height = 5.6 + continentalness * 3
    elif continentalness <= 0.1:
        height = 5.6 + continentalness * 8
    elif continentalness <= 0.3:
        height = 6.0 + continentalness * 30
    else:
        height = 15.0 + continentalness * 50

    # Add jaggedness
    jaggedness = self.noise_2d(world_x, world_z, 0.04)
    height += jaggedness * 4

    return np.clip(height, 0, 256)

Step 3: Implement Biome Selection

BLOCK_IDS = {'GRASS': 1, 'DIRT': 2, 'STONE': 3, 'SAND': 4, 'SNOW': 5, 'WATER': 6}

def get_biome_at_column(self, world_x, world_z):
    """Determine biome from temperature and moisture"""
    temperature = self.noise_2d(world_x, world_z, 0.01)
    moisture = self.noise_2d(world_x, world_z, 0.015)

    # Dithering for natural transitions
    dither = self.noise_2d(world_x, world_z, 0.1) * 0.05
    temperature += dither
    moisture += dither

    if temperature > 0.3 and moisture < -0.2:
        return 'DESERT'
    elif temperature < -0.2:
        return 'SNOW'
    elif moisture > 0.2:
        return 'FOREST'
    else:
        return 'GRASSLAND'

Step 4: Generate Chunk Voxels

def generate_chunk(self, chunk_x, chunk_y, chunk_z):
    """Generate all voxels for a chunk"""
    chunk_voxels = np.zeros((self.CHUNK_SIZE ** 3,), dtype=np.uint8)

    for local_x in range(self.CHUNK_SIZE):
        for local_z in range(self.CHUNK_SIZE):
            world_x = chunk_x * self.CHUNK_SIZE + local_x
            world_z = chunk_z * self.CHUNK_SIZE + local_z

            # Get column data
            height = self.get_height_at_column(world_x, world_z)
            biome = self.get_biome_at_column(world_x, world_z)

            # Fill column
            for local_y in range(self.CHUNK_SIZE):
                world_y = chunk_y * self.CHUNK_SIZE + local_y
                voxel_index = local_x + local_z * self.CHUNK_SIZE + local_y * self.CHUNK_SIZE ** 2

                if world_y < self.STONE_LVL:
                    chunk_voxels[voxel_index] = BLOCK_IDS['STONE']
                elif world_y < height:
                    if world_y <= height - 3:
                        chunk_voxels[voxel_index] = BLOCK_IDS['STONE']
                    elif world_y <= height - 1:
                        chunk_voxels[voxel_index] = BLOCK_IDS['DIRT']
                    else:
                        # Surface block
                        if biome == 'DESERT':
                            chunk_voxels[voxel_index] = BLOCK_IDS['SAND']
                        elif biome == 'SNOW':
                            chunk_voxels[voxel_index] = BLOCK_IDS['SNOW']
                        else:
                            chunk_voxels[voxel_index] = BLOCK_IDS['GRASS']
                elif world_y < self.WATER_LVL:
                    chunk_voxels[voxel_index] = BLOCK_IDS['WATER']
                else:
                    chunk_voxels[voxel_index] = 0  # AIR

    return chunk_voxels

Verification: - Generate a test chunk at (0, 0, 0) and (1, 0, 1) - Verify same seed produces identical chunks - Visualize height map (different seeds = different patterns)

Tutorial 2: Implement Greedy Meshing

Objective: Build a greedy meshing algorithm to reduce polygon count by 90%+.

Time: 3 hours | Difficulty: Hard

Prerequisites: - Understand bit-packing (read meshes.rst) - Comfortable with 3D array indexing - Numba JIT knowledge (optional but helpful)

Step 1: Check Transparency

TRANSPARENT_BLOCKS = {0, 6, 7, 8, 9}  # AIR, WATER, GLASS, LEAVES, VINE

def is_transparent(voxel_id):
    return voxel_id in TRANSPARENT_BLOCKS

def is_solid(voxel_id):
    return not is_transparent(voxel_id)

Step 2: Implement AO Calculation

def get_ao(chunk_voxels, local_y, local_z):
    """Calculate ambient occlusion for a corner"""
    # Check 2x2 block neighbors
    corners = [
        (local_y - 1, local_z - 1),
        (local_y,     local_z - 1),
        (local_y - 1, local_z),
        (local_y,     local_z),
    ]

    ao_count = 0
    for y, z in corners:
        if 0 <= y < 48 and 0 <= z < 48:
            voxel_id = chunk_voxels[y, z]  # Simplified index
            if not is_transparent(voxel_id):
                ao_count += 1

    return min(ao_count, 3)  # Clamp to 0-3

Step 3: Build Greedy Quads

def build_greedy_quad(chunk_voxels, plane='Y'):
    """Extract greedy quads from a plane (simplified for XY plane)"""
    vertices = []

    # Build 2D mask
    mask = np.zeros((48, 48), dtype=bool)
    for y in range(48):
        for z in range(48):
            voxel_id = chunk_voxels[y, z]
            if is_solid(voxel_id):
                mask[y, z] = True

    processed = set()

    for y in range(48):
        for z in range(48):
            if (y, z) in processed or not mask[y, z]:
                continue

            # Find greedy width (extend along Z)
            width = 1
            while z + width < 48 and mask[y, z + width]:
                width += 1

            # Find greedy height (extend along Y)
            height = 1
            valid = True
            while y + height < 48 and valid:
                for z_check in range(z, z + width):
                    if not mask[y + height, z_check]:
                        valid = False
                        break
                if valid:
                    height += 1
                else:
                    break

            # Mark as processed
            for dy in range(height):
                for dz in range(width):
                    processed.add((y + dy, z + dz))
                    mask[y + dy, z + dz] = False

            # Add quad vertices
            # (simplified: just record quad dimensions)
            vertices.append({
                'y': y, 'z': z,
                'width': width, 'height': height
            })

    return vertices

Verification: - Mesh a simple grid of cubes - Count triangles (should be ~2/3 of naive implementation) - Visual check: uniform color cubes should render as expected

Tutorial 3: Implement AABB Collision Detection

Objective: Build player-terrain collision detection with axis-separated movement.

Time: 2 hours | Difficulty: Medium

Prerequisites: - Basic 3D math (vectors, AABBs) - Understand axis-separated movement (read player.rst)

Step 1: Define AABB

import glm

class AABB:
    def __init__(self, min_pos, max_pos):
        self.min = min_pos  # glm.vec3
        self.max = max_pos  # glm.vec3

    def intersects(self, other):
        """Check intersection with another AABB"""
        return (self.min.x < other.max.x and self.max.x > other.min.x and
                self.min.y < other.max.y and self.max.y > other.min.y and
                self.min.z < other.max.z and self.max.z > other.min.z)

    def get_voxel_aabbs(self, world):
        """Get all solid voxel AABBs within and around this AABB"""
        min_x, min_y, min_z = floor(self.min.x), floor(self.min.y), floor(self.min.z)
        max_x, max_y, max_z = ceil(self.max.x), ceil(self.max.y), ceil(self.max.z)

        voxel_boxes = []
        for vx in range(int(min_x), int(max_x) + 1):
            for vy in range(int(min_y), int(max_y) + 1):
                for vz in range(int(min_z), int(max_z) + 1):
                    voxel_id = world.get_voxel(vx, vy, vz)
                    if not is_transparent(voxel_id):
                        voxel_aabb = AABB(
                            glm.vec3(vx, vy, vz),
                            glm.vec3(vx + 1, vy + 1, vz + 1)
                        )
                        voxel_boxes.append(voxel_aabb)

        return voxel_boxes

Step 2: Resolve Collisions Per-Axis

def resolve_axis(axis, player_pos, player_aabb, velocity, world):
    """Resolve collision for single axis"""
    # Compute new position
    old_pos = player_pos.copy()
    player_pos[axis] += velocity[axis]

    # Get new AABB
    new_aabb = AABB(
        player_pos - player_half_extents,
        player_pos + player_half_extents
    )

    # Check collisions
    voxel_boxes = new_aabb.get_voxel_aabbs(world)

    for voxel_box in voxel_boxes:
        if new_aabb.intersects(voxel_box):
            # Collision detected: snap to voxel boundary
            if velocity[axis] > 0:  # Moving positive
                player_pos[axis] = voxel_box.min[axis] - player_half_extents[axis]
            else:  # Moving negative
                player_pos[axis] = voxel_box.max[axis] + player_half_extents[axis]

            # Handle ground detection (for Y-axis)
            if axis == 1 and velocity[axis] < 0:  # Landing downward
                on_ground = True

            velocity[axis] = 0
            break

    return player_pos, velocity

Step 3: Integrate into Player Update

def move_and_collide(player, delta_time, world):
    """Apply velocity to player with collision"""
    velocity = player.velocity.copy()

    # Apply gravity
    velocity.y += GRAVITY * delta_time

    # Resolve per-axis
    for axis in [0, 1, 2]:  # X, Y, Z
        player.feet_pos, velocity = resolve_axis(
            axis, player.feet_pos, player.get_aabb(), velocity, world
        )

    player.velocity = velocity
    player.position = player.feet_pos + glm.vec3(0, PLAYER_EYE_HEIGHT, 0)

Verification: - Place player in mid-air above ground - Verify gravity pulls down and stops at ground - Walk into walls (should stop, not clip through) - Jump and fall (should stick to ground when landing)

Tutorial 4: Implement Inventory and Crafting

Objective: Build a 41-slot inventory with drag-drop UI and crafting recipes.

Time: 1.5 hours | Difficulty: Easy

Prerequisites: - UI components (read ui.rst) - Basic event handling

Step 1: Inventory Data Structure

NUM_SLOTS = 41
HOTBAR_SIZE = 9
MAX_STACK = 64

class Inventory:
    def __init__(self):
        self.slots = [0] * NUM_SLOTS  # 0 = empty, 1-255 = voxel ID
        self.counts = [0] * NUM_SLOTS  # Stack size
        self.selected_slot = 0  # Hotbar index

    def add_item(self, voxel_id):
        """Add item to inventory, stack if possible"""
        # Try existing stacks
        for i in range(NUM_SLOTS):
            if self.slots[i] == voxel_id and self.counts[i] < MAX_STACK:
                self.counts[i] += 1
                return True

        # Add to empty slot
        for i in range(NUM_SLOTS):
            if self.slots[i] == 0:
                self.slots[i] = voxel_id
                self.counts[i] = 1
                return True

        return False  # Inventory full

    def remove_item(self, slot_index, count=1):
        """Remove items from slot"""
        if self.counts[slot_index] >= count:
            self.counts[slot_index] -= count
            if self.counts[slot_index] == 0:
                self.slots[slot_index] = 0
            return True
        return False

Step 2: Crafting Recipes

RECIPES = {
    # (slot36, slot37, slot38, slot39) → (output_id, count)
    (WOOD, 0, 0, 0): (WOOD_PLANK, 4),
    (WOOD_PLANK, WOOD_PLANK, STICK, 0): (WOODEN_PICKAXE, 1),
    # ... more recipes
}

def update_crafting(inventory):
    """Check crafting grid and update output"""
    inputs = (
        inventory.slots[36],
        inventory.slots[37],
        inventory.slots[38],
        inventory.slots[39],
    )

    if inputs in RECIPES:
        output_id, count = RECIPES[inputs]
        inventory.slots[40] = output_id
        inventory.counts[40] = count
    else:
        inventory.slots[40] = 0
        inventory.counts[40] = 0

Step 3: Drag-Drop UI Logic

class InventoryUI:
    def __init__(self, inventory):
        self.inventory = inventory
        self.dragging_from = None
        self.dragging_item_id = 0
        self.dragging_count = 0

    def on_click(self, slot_index, button):
        if button == 1:  # LClick
            if self.dragging_from is None:
                # Start drag
                self.dragging_from = slot_index
                self.dragging_item_id = self.inventory.slots[slot_index]
                self.dragging_count = self.inventory.counts[slot_index]
            else:
                # Drop on target
                self.swap_or_merge(slot_index)
                self.dragging_from = None

        elif button == 3:  # RClick
            # Split stack
            if self.inventory.counts[slot_index] > 1:
                half = self.inventory.counts[slot_index] // 2
                self.inventory.counts[slot_index] -= half
                # Create shadow item...

    def swap_or_merge(self, target_slot):
        """Merge dragged items into target slot"""
        drag_slot = self.dragging_from

        if self.inventory.slots[target_slot] == self.inventory.slots[drag_slot]:
            # Same item: merge
            space = MAX_STACK - self.inventory.counts[target_slot]
            transfer = min(space, self.inventory.counts[drag_slot])

            self.inventory.counts[target_slot] += transfer
            self.inventory.counts[drag_slot] -= transfer

            if self.inventory.counts[drag_slot] == 0:
                self.inventory.slots[drag_slot] = 0
        else:
            # Different items: swap
            self.inventory.slots[drag_slot], self.inventory.slots[target_slot] = (
                self.inventory.slots[target_slot], self.inventory.slots[drag_slot]
            )
            self.inventory.counts[drag_slot], self.inventory.counts[target_slot] = (
                self.inventory.counts[target_slot], self.inventory.counts[drag_slot]
            )

Verification: - Add items to inventory - Verify stacking works - Drag items around, test merge/swap - Check crafting recipe updates output slot