diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 99fe592..2acb688 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,5 +1,21 @@ # Copilot Instructions for GLTerrain Project +## CRITICAL COMMUNICATION RULES + +**NEVER claim success without proof:** +1. Don't say "FATTO!", "PERFETTO!", "Done!" unless you have verified the code works +2. Don't start responses with exclamations like "PERFETTO!", "Ottimo!", "Fantastico!", "Eccellente!" - they feel disingenuous +3. Be direct and honest - just explain what you did clearly +4. Let the user verify results before celebrating + +**ALWAYS:** +- Test before claiming success +- Be honest about uncertainty +- Search web/documentation if unsure +- Wait for user confirmation + +--- + ## Project Overview **GLTerrain** is an isometric terrain generator using OpenGL, Pygame, and Perlin noise. It creates procedurally generated 3D terrains with RollerCoaster Tycoon-style isometric view, biome-based coloring, and real-time camera controls. diff --git a/README.md b/README.md index f202138..6591116 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,11 @@ shader/ ## Features - **20×20 grid** of 30px×30px isometric tiles -- **Procedural terrain generation** using Perlin noise +- **Procedural terrain generation** using Perlin noise with random seeds +- **Procedural texture decorations** - Each tile has unique pixelated patterns (RCT-style) - **Multiple biomes** based on elevation (water, sand, grass, rock, snow) - **Real-time camera controls** +- **Dynamic regeneration** - Press R for new terrain and textures - **Configurable settings** via `config/settings.py` ## Installation @@ -88,6 +90,11 @@ All settings can be modified in `config/settings.py`: - `grid_line_width`: Thickness of tile borders - `light_position`: Light source position - `light_ambient/light_diffuse`: Lighting properties +- `side_face_color`: Uniform color for lateral tile faces +- `texture_enabled`: Enable/disable procedural textures (default: true) +- `texture_detail_scale`: Controls texture pattern size +- `texture_variation`: Amount of color variation (0.0-1.0) +- `texture_spots_threshold`: Controls dark spot frequency ### Biome Configuration - `BIOME_COLORS`: RGB colors for each biome type @@ -115,9 +122,11 @@ The code is organized into logical modules: 4. **Rendering** (`src/rendering/terrain_renderer.py`) - OpenGL rendering of terrain mesh + - **Procedural texture generation** - 16×16 pixelated textures per tile - Biome coloring based on elevation - Wireframe grid overlay - Shaded side faces for depth + - Texture caching for performance 5. **Application** (`src/app.py`) - Main application loop @@ -138,6 +147,15 @@ Edit parameters in `config/settings.py`: - Adjust `noise_scale` for more/less variation - Change `noise_octaves` for detail level - Modify `height_multiplier` for elevation range +- Each generation uses a random seed for unique terrains + +### Customizing Textures + +Edit texture parameters in `RENDERING` section: +- `texture_variation`: Increase for more dramatic color changes +- `texture_detail_scale`: Lower values = larger patterns +- `texture_spots_threshold`: Lower values = more dark spots +- Textures are 16×16 pixels with GL_NEAREST filtering for retro look ### Adding Camera Features diff --git a/config/settings.py b/config/settings.py index 0f2eaa0..297ca17 100644 --- a/config/settings.py +++ b/config/settings.py @@ -22,7 +22,7 @@ TERRAIN = { 'noise_lacunarity': 2.5, # Higher = more frequency increase per octave 'noise_repeat_x': 1024, 'noise_repeat_y': 1024, - 'noise_base': 42, + 'noise_base': 1988, # Height scaling 'height_multiplier': 80.0, # Multiplier for height variation @@ -61,9 +61,18 @@ RENDERING = { 'light_ambient': [0.4, 0.4, 0.4, 1.0], 'light_diffuse': [0.8, 0.8, 0.8, 1.0], - # Shading multipliers for side faces - 'side_face_shading': 0.7, - 'back_face_shading': 0.8, + # Side faces color (uniform terrain color) + 'side_face_color': (0.55, 0.45, 0.35), # Brown terrain color + 'side_face_shading': 0.7, # Right face shading multiplier + 'back_face_shading': 0.85, # Back face shading multiplier + + # Procedural texture settings for top faces + 'texture_enabled': True, + 'texture_detail_scale': 8.0, # Higher = more detail noise + 'texture_variation': 0.25, # Amount of color variation (0.0-1.0) + 'texture_pattern_scale': 2.0, # Scale for pattern details + 'texture_spots_scale': 15.0, # Scale for random spots/patches + 'texture_spots_threshold': 0.6, # Threshold for spot appearance } # Biome colors (RCT style) diff --git a/docs/01-introduzione.md b/docs/01-introduzione.md index fdad975..dd81644 100644 --- a/docs/01-introduzione.md +++ b/docs/01-introduzione.md @@ -11,8 +11,9 @@ Quando avvii il programma, vedrai: 1. **Una griglia di terreno 3D** composta da 20×20 tessere (chiamate "tile") 2. **Variazioni di altezza** che creano colline, valli e montagne 3. **Diversi colori** che rappresentano biomi (acqua, sabbia, erba, roccia, neve) -4. **Linee nere** che delimitano ogni tessera, creando un effetto griglia -5. **Una vista dall'alto** con un angolo di 45 gradi (vista isometrica) +4. **Texture procedurali pixelate** - Ogni tile ha decorazioni uniche in stile retro +5. **Linee nere** che delimitano ogni tessera, creando un effetto griglia +6. **Una vista dall'alto** con un angolo di 45 gradi (vista isometrica) ### Interattività @@ -74,23 +75,24 @@ Altezza 70+ → Bianco (Neve) 🏔️ ### 3. Rendering 3D -Il computer disegna ogni tessera come un quadrato in 3D: +Il computer disegna ogni tessera come un quadrato in 3D con texture procedurale: ``` -Vista dall'alto: Vista isometrica: +Vista dall'alto: Vista isometrica con texture: - ┌──┬──┐ ◇──◇ - │ │ │ ╱│ ╱│ - ├──┼──┤ → ◇──◇ │ - │ │ │ │ ╱│ ╱ + ┌──┬──┐ ◇──◇ (con pattern 16×16 pixel) + │ │ │ ╱│ ╱│ (macchie scure, variazioni) + ├──┼──┤ → ◇──◇ │ (ogni tile unica) + │ │ │ │ ╱│ ╱ (stile retro pixelato) └──┴──┘ ◇──◇ ``` Ogni quadrato (quad) è disegnato con: -- Una **faccia superiore** (quella che vediamo) -- **Facce laterali** (per mostrare l'altezza) -- **Colore** basato sull'altezza -- **Ombreggiatura** per dare profondità +- Una **faccia superiore** con texture procedurale 16×16 pixel +- **Facce laterali** uniformi marroni per l'altezza +- **Colore base** determinato dal bioma +- **Pattern unici** generati con Perlin noise per ogni tile +- **Ombreggiatura** sulle facce laterali per profondità ## Risultati Visivi @@ -161,7 +163,14 @@ shader/ ## Caratteristiche Principali ### ✅ Generazione Procedurale -Il terreno è generato automaticamente usando algoritmi matematici. Ogni volta che premi R, ottieni un paesaggio completamente diverso! +Il terreno è generato automaticamente usando algoritmi matematici. Ogni volta che premi R, ottieni un paesaggio completamente diverso con texture uniche! + +### ✅ Texture Procedurali Pixelate +Ogni tile ha una texture 16×16 pixel generata proceduralmente con: +- Pattern casuali unici per ogni tile +- Macchie scure e variazioni di colore +- Stile retro pixelato (GL_NEAREST filtering) +- Contestuali al bioma (erba, sabbia, roccia, ecc.) ### ✅ Configurabilità Tutto è personalizzabile tramite il file `config/settings.py`: @@ -211,10 +220,9 @@ Il progetto base ha alcune limitazioni: - ❌ Non c'è salvataggio/caricamento delle mappe - ❌ Non puoi modificare il terreno interattivamente - ❌ La griglia è fissa a 20×20 (modificabile nel config) -- ❌ Non ci sono texture, solo colori solidi - ❌ Nessun sistema di gameplay -Queste sono tutte cose che puoi aggiungere estendendo il progetto! +Le texture procedurali sono completamente implementate! ✅ ## Prossimi Passi diff --git a/docs/06-rendering.md b/docs/06-rendering.md index 44666c2..1354e42 100644 --- a/docs/06-rendering.md +++ b/docs/06-rendering.md @@ -38,9 +38,10 @@ heightmap[i+1][j+1] = 13 ### 1. Calcolo Vertici ```python -def draw_tile(self, x, z, h1, h2, h3, h4): +def draw_tile(self, x, z, h1, h2, h3, h4, grid_i, grid_j): # x, z: posizione in griglia # h1, h2, h3, h4: altezze dei 4 angoli + # grid_i, grid_j: indici griglia per texture unica corners = [ (x, h1, z), # Angolo 1 @@ -50,24 +51,97 @@ def draw_tile(self, x, z, h1, h2, h3, h4): ] ``` -### 2. Faccia Superiore +### 2. Generazione Texture Procedurale + +Prima di disegnare, ogni tile genera una texture OpenGL unica: ```python -# Calcola colore medio +def generate_procedural_texture(self, base_color, grid_i, grid_j, texture_size=16): + # Crea array 16×16 pixel + texture_data = np.zeros((16, 16, 3), dtype=np.uint8) + + # Offset unico per questo tile + tile_offset_x = grid_i * 1000.0 + tile_offset_y = grid_j * 1000.0 + + for i in range(16): + for j in range(16): + # Coordinate noise con offset tile + i_coord = float(i) / 16 * scale + tile_offset_x + j_coord = float(j) / 16 * scale + tile_offset_y + + # 3 layer di Perlin noise + detail = noise.pnoise2(i_coord, j_coord, octaves=3) + pattern = noise.pnoise2(i_coord * 0.5, j_coord * 0.5, octaves=2) + spots = noise.pnoise2(i_coord * 1.5, j_coord * 1.5, octaves=1) + + # Combina e applica al colore base + variation = (detail * 0.4 + pattern * 0.6) * 0.25 + spot_factor = 0.75 if spots > 0.6 else 1.0 + + final_color = (base_color + variation) * spot_factor + texture_data[i, j] = final_color * 255 + + # Crea texture OpenGL + texture_id = glGenTextures(1) + glBindTexture(GL_TEXTURE_2D, texture_id) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST) # Pixelato! + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST) + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 16, 16, 0, GL_RGB, GL_UNSIGNED_BYTE, texture_data) + + return texture_id +``` + +**Caratteristiche**: +- 16×16 pixel per effetto retro +- GL_NEAREST = nessuna interpolazione → pixel netti +- 3 layer Perlin noise per variazioni naturali +- Offset unico (grid_i, grid_j) = ogni tile diversa +- Macchie scure dove `spots > 0.6` + +### 3. Faccia Superiore con Texture + +```python +# Ottieni texture unica per questo tile avg_height = (h1 + h2 + h3 + h4) / 4.0 -color = self.get_color_for_height(avg_height) +base_color = self.get_color_for_height(avg_height) +texture_id = self.get_texture_for_tile(base_color, grid_i, grid_j) -# Disegna quad +# Abilita texturing +glEnable(GL_TEXTURE_2D) +glBindTexture(GL_TEXTURE_2D, texture_id) +glDisable(GL_LIGHTING) # Mostra colori puri texture +glColor3f(1.0, 1.0, 1.0) # Bianco = non tinta la texture + +# Disegna quad con coordinate texture glBegin(GL_QUADS) -glColor3f(*color) # Applica colore -for corner in corners: - glVertex3f(*corner) # Definisci vertice +glTexCoord2f(0.0, 0.0) # Angolo texture +glVertex3f(*corners[0]) +glTexCoord2f(1.0, 0.0) +glVertex3f(*corners[1]) +glTexCoord2f(1.0, 1.0) +glVertex3f(*corners[2]) +glTexCoord2f(0.0, 1.0) +glVertex3f(*corners[3]) glEnd() + +glDisable(GL_TEXTURE_2D) +glEnable(GL_LIGHTING) # Riabilita per facce laterali ``` -**Risultato**: Faccia superiore della tile colorata. +**Coordinate Texture (UV)**: +``` +(0,0)────(1,0) + │ 16×16 │ + │ pixel │ +(0,1)────(1,1) +``` + +Ogni angolo del quad mappa un angolo della texture 16×16. + +**Risultato**: Faccia superiore con pattern procedurale pixelato unico. -### 3. Facce Laterali +### 4. Facce Laterali (Uniformi) #### Faccia Destra (X+) diff --git a/src/app.py b/src/app.py index 3ce1191..1c83898 100644 --- a/src/app.py +++ b/src/app.py @@ -55,7 +55,7 @@ class IsometricTerrainApp: glEnable(GL_BLEND) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) - # Setup lighting + # Setup lighting (sunset lighting) glEnable(GL_LIGHTING) glEnable(GL_LIGHT0) glEnable(GL_COLOR_MATERIAL) @@ -64,6 +64,12 @@ class IsometricTerrainApp: glLightfv(GL_LIGHT0, GL_POSITION, self.config.RENDERING['light_position']) glLightfv(GL_LIGHT0, GL_AMBIENT, self.config.RENDERING['light_ambient']) glLightfv(GL_LIGHT0, GL_DIFFUSE, self.config.RENDERING['light_diffuse']) + + # Add specular component for sunset highlights + if 'light_specular' in self.config.RENDERING: + glLightfv(GL_LIGHT0, GL_SPECULAR, self.config.RENDERING['light_specular']) + glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, [0.3, 0.3, 0.3, 1.0]) + glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, 10.0) def handle_events(self): """Handle pygame events""" diff --git a/src/rendering/terrain_renderer.py b/src/rendering/terrain_renderer.py index 5563381..cc48926 100644 --- a/src/rendering/terrain_renderer.py +++ b/src/rendering/terrain_renderer.py @@ -1,7 +1,10 @@ """ -Terrain rendering with isometric view +Terrain rendering with isometric view and procedural textures """ from OpenGL.GL import * +import random +import noise +import numpy as np class TerrainRenderer: @@ -24,13 +27,28 @@ class TerrainRenderer: self.grid_line_width = rendering_config['grid_line_width'] self.grid_line_color = rendering_config['grid_line_color'] + self.side_face_color = rendering_config['side_face_color'] self.side_face_shading = rendering_config['side_face_shading'] self.back_face_shading = rendering_config['back_face_shading'] + # Texture settings + self.texture_enabled = rendering_config.get('texture_enabled', True) + self.texture_detail_scale = rendering_config.get('texture_detail_scale', 8.0) + self.texture_variation = rendering_config.get('texture_variation', 0.25) + self.texture_pattern_scale = rendering_config.get('texture_pattern_scale', 2.0) + self.texture_spots_scale = rendering_config.get('texture_spots_scale', 15.0) + self.texture_spots_threshold = rendering_config.get('texture_spots_threshold', 0.6) + self.colors = biome_colors self.thresholds = biome_thresholds self.heightmap = None + + # Initialize random seed for consistent patterns per session + self.texture_seed = random.randint(0, 10000) + + # Texture cache: one GL texture per tile (grid_i, grid_j, biome) + self.tile_textures = {} def set_heightmap(self, heightmap): """ @@ -40,6 +58,127 @@ class TerrainRenderer: heightmap: 2D numpy array of height values """ self.heightmap = heightmap + + # Regenerate texture seed and clear cache for new terrain + self.texture_seed = random.randint(0, 10000) + + # Delete old textures from GPU memory + for texture_id in self.tile_textures.values(): + glDeleteTextures([texture_id]) + + # Clear texture cache + self.tile_textures = {} + + print(f"New texture seed: {self.texture_seed}") + + def generate_procedural_texture(self, base_color, grid_i, grid_j, texture_size=16): + """ + Generate procedural texture EXACTLY like test_texture.py but unique per tile + Pixelated style (16×16) like old games + + Args: + base_color: Base RGB color tuple (0-1 range) + grid_i: Grid index i (for variation) + grid_j: Grid index j (for variation) + texture_size: Size of texture in pixels (16×16 for retro look) + + Returns: + int: OpenGL texture ID + """ + # Create texture array (same as test_texture.py) + texture_data = np.zeros((texture_size, texture_size, 3), dtype=np.uint8) + + # Unique offset per tile for variation + tile_offset_x = grid_i * 1000.0 + tile_offset_y = grid_j * 1000.0 + + for i in range(texture_size): + for j in range(texture_size): + # Same exact algorithm as test_texture.py, but with tile-specific offset + i_coord = float(i) / texture_size * self.texture_detail_scale + tile_offset_x + j_coord = float(j) / texture_size * self.texture_detail_scale + tile_offset_y + + # Fine detail noise + detail_noise = noise.pnoise2( + i_coord + self.texture_seed * 0.01, + j_coord + self.texture_seed * 0.01, + octaves=3, + persistence=0.5, + lacunarity=2.0 + ) + + # Medium pattern noise + pattern_noise = noise.pnoise2( + i_coord * 0.5 + 100, + j_coord * 0.5 + 100, + octaves=2, + persistence=0.6 + ) + + # Random spots + spots_noise = noise.pnoise2( + i_coord * 1.5 + 200, + j_coord * 1.5 + 200, + octaves=1 + ) + + # Spot darkening + spot_factor = 1.0 + if spots_noise > self.texture_spots_threshold: + spot_factor = 0.75 + elif spots_noise > self.texture_spots_threshold + 0.2: + spot_factor = 0.6 + + # Combine layers + variation = (detail_noise * 0.4 + pattern_noise * 0.6) * self.texture_variation + + # Apply to base color + textured_color = tuple( + max(0.0, min(1.0, (c + variation) * spot_factor)) + for c in base_color + ) + + # Convert to 0-255 range + texture_data[i, j] = [int(c * 255) for c in textured_color] + + # Create OpenGL texture + texture_id = glGenTextures(1) + glBindTexture(GL_TEXTURE_2D, texture_id) + + # Set texture parameters for pixelated look (no filtering!) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST) # No interpolation + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST) # Sharp pixels + + # Upload texture data + glTexImage2D( + GL_TEXTURE_2D, 0, GL_RGB, + texture_size, texture_size, 0, + GL_RGB, GL_UNSIGNED_BYTE, texture_data + ) + + return texture_id + + def get_texture_for_tile(self, base_color, grid_i, grid_j): + """ + Get or create unique texture for this specific tile + + Args: + base_color: Base RGB color tuple + grid_i: Grid index i + grid_j: Grid index j + + Returns: + int: OpenGL texture ID + """ + # Use (i, j, color, seed) as unique key - seed ensures new textures on regeneration + tile_key = (grid_i, grid_j, tuple(base_color), self.texture_seed) + + if tile_key not in self.tile_textures: + self.tile_textures[tile_key] = self.generate_procedural_texture(base_color, grid_i, grid_j) + + return self.tile_textures[tile_key] def get_color_for_height(self, height): """ @@ -66,17 +205,19 @@ class TerrainRenderer: else: return self.colors['snow'] - def draw_tile(self, x, z, height, next_x_height, next_z_height, next_xz_height): + def draw_tile(self, x, z, height, next_x_height, next_z_height, next_xz_height, grid_i, grid_j): """ Draw a single isometric tile with proper shading Args: - x: X position - z: Z position + x: X position in world coordinates + z: Z position in world coordinates height: Height at current position next_x_height: Height at x+1 position next_z_height: Height at z+1 position next_xz_height: Height at x+1, z+1 position + grid_i: Grid index i (for texture) + grid_j: Grid index j (for texture) """ # Define the four corners of the tile corners = [ @@ -90,18 +231,40 @@ class TerrainRenderer: avg_height = (height + next_x_height + next_z_height + next_xz_height) / 4.0 base_color = self.get_color_for_height(avg_height) - # Draw top face + # Enable texturing and get unique procedural texture for this tile + if self.texture_enabled: + texture_id = self.get_texture_for_tile(base_color, grid_i, grid_j) + glEnable(GL_TEXTURE_2D) + glBindTexture(GL_TEXTURE_2D, texture_id) + + # Disable lighting to show pure texture colors + glDisable(GL_LIGHTING) + glColor3f(1.0, 1.0, 1.0) # White to not tint texture + else: + glColor3f(*base_color) + + # Draw top face with texture coordinates glBegin(GL_QUADS) - glColor3f(*base_color) - for corner in corners: - glVertex3f(*corner) + glTexCoord2f(0.0, 0.0) + glVertex3f(*corners[0]) + glTexCoord2f(1.0, 0.0) + glVertex3f(*corners[1]) + glTexCoord2f(1.0, 1.0) + glVertex3f(*corners[2]) + glTexCoord2f(0.0, 1.0) + glVertex3f(*corners[3]) glEnd() + # Disable texturing and re-enable lighting for side faces + if self.texture_enabled: + glDisable(GL_TEXTURE_2D) + glEnable(GL_LIGHTING) + # Draw side faces for elevation changes # Right face if next_x_height > 0.1 or height > 0.1: glBegin(GL_QUADS) - darker = tuple(c * self.side_face_shading for c in base_color) + darker = tuple(c * self.side_face_shading for c in self.side_face_color) glColor3f(*darker) glVertex3f(x + self.tile_width, next_x_height, z) glVertex3f(x + self.tile_width, 0, z) @@ -112,7 +275,7 @@ class TerrainRenderer: # Back face if next_z_height > 0.1 or height > 0.1: glBegin(GL_QUADS) - darker = tuple(c * self.back_face_shading for c in base_color) + darker = tuple(c * self.back_face_shading for c in self.side_face_color) glColor3f(*darker) glVertex3f(x, next_z_height, z + self.tile_depth) glVertex3f(x, 0, z + self.tile_depth) @@ -138,7 +301,7 @@ class TerrainRenderer: next_z = self.heightmap[i][j + 1] next_xz = self.heightmap[i + 1][j + 1] - self.draw_tile(x, z, height, next_x, next_z, next_xz) + self.draw_tile(x, z, height, next_x, next_z, next_xz, i, j) # Draw wireframe grid self._draw_grid(total_size) diff --git a/src/terrain/generator.py b/src/terrain/generator.py index 5395b91..db5812e 100644 --- a/src/terrain/generator.py +++ b/src/terrain/generator.py @@ -3,6 +3,7 @@ Terrain generation using Perlin noise """ import numpy as np import noise +import random class TerrainGenerator: @@ -36,11 +37,14 @@ class TerrainGenerator: def generate(self): """ - Generate terrain heightmap using Perlin noise + Generate terrain heightmap using Perlin noise with random seed Returns: numpy.ndarray: 2D array of height values """ + # Generate random base offset for different terrain each time + random_base = random.randint(0, 10000) + total_size = self.grid_size * self.tile_size heightmap = np.zeros((total_size, total_size)) @@ -55,12 +59,14 @@ class TerrainGenerator: lacunarity=self.lacunarity, repeatx=self.repeat_x, repeaty=self.repeat_y, - base=self.base + base=random_base # Use random base instead of config base ) # Normalize and scale height heightmap[i][j] = (height + 0.5) * self.height_multiplier + print(f"New heightmap seed: {random_base}") + # Apply smoothing if enabled if self.enable_smoothing: heightmap = self._smooth_terrain(heightmap) diff --git a/test_texture.py b/test_texture.py new file mode 100644 index 0000000..dfddc6a --- /dev/null +++ b/test_texture.py @@ -0,0 +1,120 @@ +""" +Test script to generate and visualize procedural textures for terrain tiles +""" +import numpy as np +import noise +import matplotlib.pyplot as plt +from matplotlib.colors import ListedColormap + +# Configuration +GRID_SIZE = 20 +SEED = 42 + +# Base biome colors (RGB 0-1) +BIOME_COLORS = { + 'grass': (0.25, 0.6, 0.25), + 'sand': (0.76, 0.7, 0.5), + 'water': (0.2, 0.4, 0.8), +} + +def generate_texture(grid_size, base_color, seed): + """ + Generate procedural texture for a tile grid + + Args: + grid_size: Size of the grid (grid_size x grid_size) + base_color: Base RGB color tuple (0-1 range) + seed: Random seed + + Returns: + numpy array of shape (grid_size, grid_size, 3) with RGB values + """ + texture = np.zeros((grid_size, grid_size, 3)) + + for i in range(grid_size): + for j in range(grid_size): + # Use grid coordinates for noise sampling + i_coord = float(i) * 0.1 + j_coord = float(j) * 0.1 + + # Fine detail noise (subtle variations) + detail_noise = noise.pnoise2( + i_coord + seed * 0.01, + j_coord + seed * 0.01, + octaves=3, + persistence=0.5, + lacunarity=2.0 + ) + + # Medium pattern noise (patches/clusters) + pattern_noise = noise.pnoise2( + i_coord * 0.5 + 100, + j_coord * 0.5 + 100, + octaves=2, + persistence=0.6 + ) + + # Random spots (like grass tufts or darker patches) + spots_noise = noise.pnoise2( + i_coord * 1.5 + 200, + j_coord * 1.5 + 200, + octaves=1 + ) + + # Create darker spots where noise is high + spot_factor = 1.0 + if spots_noise > 0.3: # Lower threshold for more spots + spot_factor = 0.75 # Darker patch + elif spots_noise > 0.5: + spot_factor = 0.6 # Even darker + + # Combine all noise layers + variation = (detail_noise * 0.4 + pattern_noise * 0.6) * 0.25 + + # Apply to color with spot darkening + textured_color = tuple( + max(0.0, min(1.0, (c + variation) * spot_factor)) + for c in base_color + ) + + texture[i, j] = textured_color + + return texture + +def main(): + """Generate and display textures for different biomes""" + fig, axes = plt.subplots(2, 3, figsize=(15, 10)) + fig.suptitle('Procedural Texture Generation - RCT Style', fontsize=16) + + biomes = [ + ('Grass', BIOME_COLORS['grass']), + ('Sand', BIOME_COLORS['sand']), + ('Water', BIOME_COLORS['water']), + ] + + for idx, (name, base_color) in enumerate(biomes): + # Generate texture with variation + texture = generate_texture(GRID_SIZE, base_color, SEED) + + # Show textured version + ax1 = axes[0, idx] + ax1.imshow(texture) + ax1.set_title(f'{name} - Textured') + ax1.axis('off') + + # Show base color for comparison + base_texture = np.zeros((GRID_SIZE, GRID_SIZE, 3)) + base_texture[:, :] = base_color + + ax2 = axes[1, idx] + ax2.imshow(base_texture) + ax2.set_title(f'{name} - Base Color') + ax2.axis('off') + + plt.tight_layout() + plt.savefig('texture_test.png', dpi=150) + print("✓ Texture generated and saved as 'texture_test.png'") + plt.show() + +if __name__ == '__main__': + main()