Initial release v1.0.0 - Isometric Terrain Generator

- Procedural terrain generation using Perlin noise
- OpenGL 3D rendering with isometric view
- Biome-based coloring system (7 biomes)
- Real-time camera controls (zoom, height adjustment)
- Modular architecture with config/settings.py
- Complete Italian documentation (8 chapters)
- Interactive controls: R to regenerate, arrows for camera
This commit is contained in:
John Doe
2025-10-24 15:20:12 +02:00
commit 01799256e7
24 changed files with 5299 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""Source package"""
+122
View File
@@ -0,0 +1,122 @@
"""
Main application class that ties everything together
"""
import pygame
from pygame.locals import *
from OpenGL.GL import *
from OpenGL.GLU import *
from src.camera.camera import Camera
from src.terrain.generator import TerrainGenerator
from src.rendering.terrain_renderer import TerrainRenderer
class IsometricTerrainApp:
"""Main application class for the isometric terrain generator"""
def __init__(self, config):
"""
Initialize the application
Args:
config: Module containing all configuration settings
"""
self.config = config
self.running = False
# Initialize Pygame and OpenGL
pygame.init()
self.display = (config.WINDOW_WIDTH, config.WINDOW_HEIGHT)
pygame.display.set_mode(self.display, DOUBLEBUF | OPENGL)
pygame.display.set_caption(config.WINDOW_TITLE)
# Initialize components
self.camera = Camera(config.CAMERA)
self.terrain_generator = TerrainGenerator(config.TERRAIN)
self.renderer = TerrainRenderer(
config.TERRAIN,
config.RENDERING,
config.BIOME_COLORS,
config.BIOME_THRESHOLDS
)
# Setup OpenGL
self._setup_opengl()
# Generate terrain
heightmap = self.terrain_generator.generate()
self.renderer.set_heightmap(heightmap)
self.clock = pygame.time.Clock()
def _setup_opengl(self):
"""Setup OpenGL rendering settings"""
glEnable(GL_DEPTH_TEST)
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
# Setup lighting
glEnable(GL_LIGHTING)
glEnable(GL_LIGHT0)
glEnable(GL_COLOR_MATERIAL)
glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE)
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'])
def handle_events(self):
"""Handle pygame events"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
self.running = False
elif event.key == pygame.K_r:
# Regenerate terrain
self._regenerate_terrain()
def _regenerate_terrain(self):
"""Regenerate the terrain with new random seed"""
print("Regenerating terrain...")
heightmap = self.terrain_generator.generate()
self.renderer.set_heightmap(heightmap)
def update(self):
"""Update application state"""
keys = pygame.key.get_pressed()
self.camera.handle_input(keys)
def render(self):
"""Render the scene"""
# Clear screen
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glClearColor(*self.config.RENDERING['background_color'])
# Setup camera
aspect_ratio = self.display[0] / self.display[1]
self.camera.apply(aspect_ratio)
# Render terrain
self.renderer.render()
pygame.display.flip()
def run(self):
"""Main application loop"""
self.running = True
print("Isometric Terrain Generator")
print("Controls:")
print(" UP/DOWN: Zoom in/out")
print(" LEFT/RIGHT: Adjust camera height")
print(" R: Regenerate terrain")
print(" ESC: Exit")
while self.running:
self.handle_events()
self.update()
self.render()
self.clock.tick(self.config.FPS)
pygame.quit()
+1
View File
@@ -0,0 +1 @@
"""Camera package"""
+86
View File
@@ -0,0 +1,86 @@
"""
Camera class for controlling the isometric view
"""
from OpenGL.GL import *
from OpenGL.GLU import *
import pygame
class Camera:
"""Handles camera positioning and movement for the isometric view"""
def __init__(self, config):
"""
Initialize camera with configuration
Args:
config: Dictionary with camera settings (distance, height, angle, speeds, limits)
"""
self.distance = config['initial_distance']
self.height = config['initial_height']
self.angle = config['initial_angle']
self.zoom_speed = config['zoom_speed']
self.height_speed = config['height_speed']
self.min_distance = config['min_distance']
self.max_distance = config['max_distance']
self.fov = config['fov']
self.near_clip = config['near_clip']
self.far_clip = config['far_clip']
def handle_input(self, keys):
"""
Handle keyboard input for camera movement
Args:
keys: Pygame key state array
"""
# Zoom in/out
if keys[pygame.K_UP]:
self.distance -= self.zoom_speed
self.distance = max(self.min_distance, self.distance)
if keys[pygame.K_DOWN]:
self.distance += self.zoom_speed
self.distance = min(self.max_distance, self.distance)
# Adjust height
if keys[pygame.K_LEFT]:
self.height += self.height_speed
if keys[pygame.K_RIGHT]:
self.height -= self.height_speed
def setup_projection(self, aspect_ratio):
"""
Setup the projection matrix
Args:
aspect_ratio: Window width / height ratio
"""
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
gluPerspective(self.fov, aspect_ratio, self.near_clip, self.far_clip)
def setup_modelview(self):
"""Setup the modelview matrix for isometric view"""
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
# Position camera for isometric view
gluLookAt(
self.distance, self.height, self.distance, # Camera position
0, 0, 0, # Look at center
0, 1, 0 # Up vector
)
def apply(self, aspect_ratio):
"""
Apply camera transformations
Args:
aspect_ratio: Window width / height ratio
"""
self.setup_projection(aspect_ratio)
self.setup_modelview()
+1
View File
@@ -0,0 +1 @@
"""Rendering package"""
+177
View File
@@ -0,0 +1,177 @@
"""
Terrain rendering with isometric view
"""
from OpenGL.GL import *
class TerrainRenderer:
"""Handles rendering of the terrain mesh with isometric tiles"""
def __init__(self, terrain_config, rendering_config, biome_colors, biome_thresholds):
"""
Initialize renderer with configuration
Args:
terrain_config: Dictionary with terrain settings
rendering_config: Dictionary with rendering settings
biome_colors: Dictionary mapping biome names to RGB colors
biome_thresholds: Dictionary with height thresholds for biomes
"""
self.tile_width = terrain_config['tile_width']
self.tile_depth = terrain_config['tile_depth']
self.grid_size = terrain_config['grid_size']
self.tile_size = terrain_config['tile_size']
self.grid_line_width = rendering_config['grid_line_width']
self.grid_line_color = rendering_config['grid_line_color']
self.side_face_shading = rendering_config['side_face_shading']
self.back_face_shading = rendering_config['back_face_shading']
self.colors = biome_colors
self.thresholds = biome_thresholds
self.heightmap = None
def set_heightmap(self, heightmap):
"""
Set the heightmap to render
Args:
heightmap: 2D numpy array of height values
"""
self.heightmap = heightmap
def get_color_for_height(self, height):
"""
Get color based on height (biome mapping)
Args:
height: Height value
Returns:
tuple: RGB color values (0-1 range)
"""
if height < self.thresholds['water']:
return self.colors['water']
elif height < self.thresholds['sand']:
return self.colors['sand']
elif height < self.thresholds['grass_low']:
return self.colors['grass_low']
elif height < self.thresholds['grass_mid']:
return self.colors['grass_mid']
elif height < self.thresholds['grass_high']:
return self.colors['grass_high']
elif height < self.thresholds['rock']:
return self.colors['rock']
else:
return self.colors['snow']
def draw_tile(self, x, z, height, next_x_height, next_z_height, next_xz_height):
"""
Draw a single isometric tile with proper shading
Args:
x: X position
z: Z position
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
"""
# Define the four corners of the tile
corners = [
(x, height, z),
(x + self.tile_width, next_x_height, z),
(x + self.tile_width, next_xz_height, z + self.tile_depth),
(x, next_z_height, z + self.tile_depth)
]
# Get base color for this height
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
glBegin(GL_QUADS)
glColor3f(*base_color)
for corner in corners:
glVertex3f(*corner)
glEnd()
# 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)
glColor3f(*darker)
glVertex3f(x + self.tile_width, next_x_height, z)
glVertex3f(x + self.tile_width, 0, z)
glVertex3f(x + self.tile_width, 0, z + self.tile_depth)
glVertex3f(x + self.tile_width, next_xz_height, z + self.tile_depth)
glEnd()
# 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)
glColor3f(*darker)
glVertex3f(x, next_z_height, z + self.tile_depth)
glVertex3f(x, 0, z + self.tile_depth)
glVertex3f(x + self.tile_width, 0, z + self.tile_depth)
glVertex3f(x + self.tile_width, next_xz_height, z + self.tile_depth)
glEnd()
def render(self):
"""Render the entire terrain"""
if self.heightmap is None:
return
total_size = self.grid_size * self.tile_size
# Render tiles
for i in range(total_size - 1):
for j in range(total_size - 1):
x = (i - total_size / 2) * self.tile_width
z = (j - total_size / 2) * self.tile_depth
height = self.heightmap[i][j]
next_x = self.heightmap[i + 1][j]
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)
# Draw wireframe grid
self._draw_grid(total_size)
def _draw_grid(self, total_size):
"""
Draw wireframe grid over the terrain
Args:
total_size: Total size of the terrain grid
"""
glColor3f(*self.grid_line_color)
glLineWidth(self.grid_line_width)
glBegin(GL_LINES)
# Draw lines along Z direction
for i in range(total_size):
for j in range(total_size - 1):
x = (i - total_size / 2) * self.tile_width
z = (j - total_size / 2) * self.tile_depth
z_next = ((j + 1) - total_size / 2) * self.tile_depth
glVertex3f(x, self.heightmap[i][j], z)
glVertex3f(x, self.heightmap[i][j + 1], z_next)
# Draw lines along X direction
for j in range(total_size):
for i in range(total_size - 1):
x = (i - total_size / 2) * self.tile_width
x_next = ((i + 1) - total_size / 2) * self.tile_width
z = (j - total_size / 2) * self.tile_depth
glVertex3f(x, self.heightmap[i][j], z)
glVertex3f(x_next, self.heightmap[i + 1][j], z)
glEnd()
+1
View File
@@ -0,0 +1 @@
"""Terrain generation package"""
+101
View File
@@ -0,0 +1,101 @@
"""
Terrain generation using Perlin noise
"""
import numpy as np
import noise
class TerrainGenerator:
"""Generates terrain heightmaps using Perlin noise"""
def __init__(self, config):
"""
Initialize terrain generator with configuration
Args:
config: Dictionary with terrain settings
"""
self.grid_size = config['grid_size']
self.tile_size = config['tile_size']
self.tile_width = config['tile_width']
self.tile_depth = config['tile_depth']
# Noise parameters
self.scale = config['noise_scale']
self.octaves = config['noise_octaves']
self.persistence = config['noise_persistence']
self.lacunarity = config['noise_lacunarity']
self.repeat_x = config['noise_repeat_x']
self.repeat_y = config['noise_repeat_y']
self.base = config['noise_base']
# Height parameters
self.height_multiplier = config['height_multiplier']
self.enable_smoothing = config['enable_smoothing']
self.smoothing_kernel_size = config['smoothing_kernel_size']
def generate(self):
"""
Generate terrain heightmap using Perlin noise
Returns:
numpy.ndarray: 2D array of height values
"""
total_size = self.grid_size * self.tile_size
heightmap = np.zeros((total_size, total_size))
# Generate height values using Perlin noise
for i in range(total_size):
for j in range(total_size):
height = noise.pnoise2(
i / self.scale,
j / self.scale,
octaves=self.octaves,
persistence=self.persistence,
lacunarity=self.lacunarity,
repeatx=self.repeat_x,
repeaty=self.repeat_y,
base=self.base
)
# Normalize and scale height
heightmap[i][j] = (height + 0.5) * self.height_multiplier
# Apply smoothing if enabled
if self.enable_smoothing:
heightmap = self._smooth_terrain(heightmap)
return heightmap
def _smooth_terrain(self, heightmap):
"""
Apply smoothing to create gentler slopes
Args:
heightmap: 2D array of height values
Returns:
numpy.ndarray: Smoothed heightmap
"""
smoothed = np.copy(heightmap)
kernel_size = self.smoothing_kernel_size
for i in range(1, len(heightmap) - 1):
for j in range(1, len(heightmap[0]) - 1):
total = 0
count = 0
for di in range(-kernel_size // 2, kernel_size // 2 + 1):
for dj in range(-kernel_size // 2, kernel_size // 2 + 1):
ni, nj = i + di, j + dj
if 0 <= ni < len(heightmap) and 0 <= nj < len(heightmap[0]):
total += heightmap[ni][nj]
count += 1
smoothed[i][j] = total / count
return smoothed
def get_total_size(self):
"""Get total size of the terrain grid"""
return self.grid_size * self.tile_size