10 KiB
Capitolo 6: Il Sistema di Rendering
Panoramica
Il rendering trasforma i dati della heightmap in un'immagine 3D visibile. Questo capitolo spiega come ogni tile viene disegnata e come si crea l'effetto isometrico.
Da Heightmap a Mesh 3D
Input: Heightmap
heightmap = [
[10, 12, 15, 18], # Riga 0
[8, 10, 13, 16], # Riga 1
[5, 7, 10, 13], # Riga 2
[3, 5, 8, 11], # Riga 3
]
Output: Geometria 3D
Ogni celle diventa un quad (quadrilatero) in 3D:
heightmap[i][j] = 10
heightmap[i+1][j] = 8
heightmap[i][j+1] = 12
heightmap[i+1][j+1] = 13
v4(j+1)────v3(i+1,j+1)
12/│ /13
/ │ /│
/ │ / │
v1(i,j)────v2(i+1,j)
10 8
Disegno di una Singola Tile
1. Calcolo Vertici
def draw_tile(self, x, z, h1, h2, h3, h4):
# x, z: posizione in griglia
# h1, h2, h3, h4: altezze dei 4 angoli
corners = [
(x, h1, z), # Angolo 1
(x + tile_width, h2, z), # Angolo 2
(x + tile_width, h4, z + tile_depth), # Angolo 3
(x, h3, z + tile_depth) # Angolo 4
]
2. Faccia Superiore
# Calcola colore medio
avg_height = (h1 + h2 + h3 + h4) / 4.0
color = self.get_color_for_height(avg_height)
# Disegna quad
glBegin(GL_QUADS)
glColor3f(*color) # Applica colore
for corner in corners:
glVertex3f(*corner) # Definisci vertice
glEnd()
Risultato: Faccia superiore della tile colorata.
3. Facce Laterali
Faccia Destra (X+)
if h2 > 0.1 or h1 > 0.1: # Solo se c'è altezza
darker_color = tuple(c * 0.7 for c in color)
glBegin(GL_QUADS)
glColor3f(*darker_color)
# Dall'alto al basso
glVertex3f(x + tile_width, h2, z)
glVertex3f(x + tile_width, 0, z)
glVertex3f(x + tile_width, 0, z + tile_depth)
glVertex3f(x + tile_width, h4, z + tile_depth)
glEnd()
Shading: Moltiplicato per 0.7 → più scuro del 30%
Faccia Posteriore (Z+)
if h3 > 0.1 or h1 > 0.1:
darker_color = tuple(c * 0.8 for c in color)
glBegin(GL_QUADS)
glColor3f(*darker_color)
glVertex3f(x, h3, z + tile_depth)
glVertex3f(x, 0, z + tile_depth)
glVertex3f(x + tile_width, 0, z + tile_depth)
glVertex3f(x + tile_width, h4, z + tile_depth)
glEnd()
Shading: Moltiplicato per 0.8 → più scuro del 20%
Visualizzazione Completa
Faccia superiore (100%)
┌────────┐
│░░░░░░░░│
└────────┘
│
Faccia │ Faccia
retro │ destra
(80%) │ (70%)
Rendering dell'Intero Terreno
Loop Principale
def render(self):
total_size = self.grid_size # 20
# Disegna tutte le tile
for i in range(total_size - 1):
for j in range(total_size - 1):
# Calcola posizione mondo
x = (i - total_size/2) * self.tile_width
z = (j - total_size/2) * self.tile_depth
# Ottieni altezze degli angoli
h1 = self.heightmap[i][j]
h2 = self.heightmap[i+1][j]
h3 = self.heightmap[i][j+1]
h4 = self.heightmap[i+1][j+1]
# Disegna tile
self.draw_tile(x, z, h1, h2, h3, h4)
# Disegna griglia wireframe
self._draw_grid(total_size)
Centratura della Griglia
# Senza centratura:
x = i * tile_width # Tile (0,0) a (0,0), (19,19) a (570,570)
# Con centratura:
x = (i - total_size/2) * tile_width
# Tile (0,0) a (-300, -300)
# Tile (10,10) a (0, 0) ← Centro
# Tile (19,19) a (270, 270)
Mappatura Biomi → Colori
Funzione di Mappatura
def get_color_for_height(self, height):
if height < 10.0:
return (0.2, 0.4, 0.8) # Acqua blu
elif height < 20.0:
return (0.76, 0.7, 0.5) # Sabbia beige
elif height < 30.0:
return (0.2, 0.5, 0.2) # Erba chiara
elif height < 45.0:
return (0.25, 0.6, 0.25) # Erba media
elif height < 60.0:
return (0.3, 0.65, 0.3) # Erba scura
elif height < 70.0:
return (0.5, 0.5, 0.5) # Roccia grigia
else:
return (0.9, 0.9, 0.95) # Neve bianca
Gradienti vs Soglie
Il progetto usa soglie nette:
Height: 0 10 20 30 45 60 70 80
Color: 🌊 🏖️ 🌿 🌲 🌳 ⛰️ 🏔️
Blu Beige Verde1 Verde2 Verde3 Grigio Bianco
Alternative possibili:
- Interpolazione tra colori
- Rumore aggiuntivo per variazioni
- Texture invece di colori solidi
Wireframe Grid
Perché il Wireframe?
- ✅ Mostra chiaramente i confini delle tile
- ✅ Stile classico dei giochi isometrici
- ✅ Aiuta a percepire la struttura
Implementazione
def _draw_grid(self, total_size):
glColor3f(0.0, 0.0, 0.0) # Nero
glLineWidth(5.0) # 5 pixel di spessore
glBegin(GL_LINES)
# Linee lungo l'asse Z
for i in range(total_size):
for j in range(total_size - 1):
x = (i - total_size/2) * self.tile_width
z1 = (j - total_size/2) * self.tile_depth
z2 = ((j+1) - total_size/2) * self.tile_depth
# Linea da (i,j) a (i,j+1)
glVertex3f(x, self.heightmap[i][j], z1)
glVertex3f(x, self.heightmap[i][j+1], z2)
# Linee lungo l'asse X
for j in range(total_size):
for i in range(total_size - 1):
x1 = (i - total_size/2) * self.tile_width
x2 = ((i+1) - total_size/2) * self.tile_width
z = (j - total_size/2) * self.tile_depth
# Linea da (i,j) a (i+1,j)
glVertex3f(x1, self.heightmap[i][j], z)
glVertex3f(x2, self.heightmap[i+1][j], z)
glEnd()
Risultato Visivo
Senza wireframe: Con wireframe:
███████████████ ┌─┬─┬─┬─┬─┐
███████████████ ├─┼─┼─┼─┼─┤
███████████████ ├─┼─┼─┼─┼─┤
███████████████ ├─┼─┼─┼─┼─┤
███████████████ └─┴─┴─┴─┴─┘
Ordine di Rendering
Problema: Depth Fighting
Se disegni nell facce nello stesso ordine, possono sovrapporsi male.
Soluzione: Depth Buffer
# All'inizializzazione
glEnable(GL_DEPTH_TEST)
# Ogni frame
glClear(GL_DEPTH_BUFFER_BIT)
OpenGL automaticamente:
- Calcola profondità di ogni pixel
- Confronta con depth buffer
- Disegna solo se più vicino alla camera
Back-to-Front vs Depth Buffer
Vecchio metodo (painter's algorithm):
1. Ordina oggetti per distanza
2. Disegna da lontano a vicino
Metodo moderno (depth buffer):
1. Disegna in qualsiasi ordine
2. GPU gestisce automaticamente
Noi usiamo depth buffer → nessun ordinamento necessario!
Illuminazione durante il Rendering
Setup Globale
glEnable(GL_LIGHTING)
glEnable(GL_LIGHT0)
glLightfv(GL_LIGHT0, GL_POSITION, [1.0, 1.0, 1.0, 0.0])
glLightfv(GL_LIGHT0, GL_AMBIENT, [0.4, 0.4, 0.4, 1.0])
glLightfv(GL_LIGHT0, GL_DIFFUSE, [0.8, 0.8, 0.8, 1.0])
Color Material
glEnable(GL_COLOR_MATERIAL)
glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE)
Questo dice a OpenGL: "Usa i colori dei vertici come materiale"
Effetto dell'Illuminazione
Senza luce: Con luce:
████████████ ▓▓▓▓▓▓▓▓▓▓▓▓ ← Lati luminosi
████████████ → ▒▒▒▒▒▒▒▒▒▒▒▒ ← Lati in ombra
████████████ ░░░░░░░░░░░░ ← Base scura
Performance e Ottimizzazione
Conteggio Draw Calls
Tile: 400 (20×20)
Facce per tile: ~3 (top + 2 lati)
Grid lines: ~800 linee
Total draw calls: ~2000/frame
A 60 FPS: 120,000 draw calls/secondo
Questo è gestibile per GPU moderne!
Possibili Ottimizzazioni
1. Vertex Buffer Objects (VBO)
# Invece di glBegin/glEnd
vbo = create_vbo_from_heightmap(heightmap)
render_vbo(vbo) # Una sola chiamata!
2. Instancing
# Disegna molte tile identiche in una chiamata
glDrawArraysInstanced(...)
3. Frustum Culling
# Non disegnare tile fuori dal campo visivo
if is_visible(tile, camera):
draw_tile(tile)
4. Level of Detail (LOD)
# Tile lontane = meno dettaglio
if distance_to_camera(tile) > 100:
draw_simple_tile(tile)
else:
draw_detailed_tile(tile)
Per questo progetto: non necessarie (griglia piccola)!
Clear e Swap
Clear Buffer
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
Pulisce:
- Color Buffer: Pixel sullo schermo
- Depth Buffer: Informazioni profondità
Background Color
glClearColor(0.53, 0.81, 0.92, 1.0) # Sky blue
Quando clearColor viene applicato, lo schermo diventa di questo colore.
Swap Buffers
pygame.display.flip()
Scambia front e back buffer → mostra il frame renderizzato
Pipeline Completa di un Frame
1. CLEAR
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
↓
2. SETUP CAMERA
camera.apply(aspect_ratio)
├─ Projection matrix
└─ ModelView matrix
↓
3. RENDER TILES
for each tile:
├─ Calcola posizione
├─ Ottieni altezze
├─ Calcola colore
├─ Disegna faccia top
└─ Disegna facce laterali
↓
4. RENDER GRID
for each edge:
└─ Disegna linea
↓
5. SWAP BUFFERS
pygame.display.flip()
↓
FRAME VISIBILE!
Riepilogo
Concetti Chiave
- Heightmap → Mesh: Array 2D diventa geometria 3D
- Quad: Ogni tile = 4 vertici
- Shading: Facce laterali più scure
- Biomi: Altezza determina colore
- Wireframe: Linee nere per visibilità
- Depth Buffer: Gestisce sovrapposizioni
- Double Buffering: Animazione fluida
Flusso Rendering
- Clear buffer
- Setup camera
- Disegna tile (facce + lati)
- Disegna grid
- Swap buffer
Prossimo Capitolo: Il Sistema Camera →