# 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 ```python 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 ```python 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 (x + tile_width, h2, z), # Angolo 2 (x + tile_width, h4, z + tile_depth), # Angolo 3 (x, h3, z + tile_depth) # Angolo 4 ] ``` ### 2. Generazione Texture Procedurale Prima di disegnare, ogni tile genera una texture OpenGL unica: ```python 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 base_color = self.get_color_for_height(avg_height) texture_id = self.get_texture_for_tile(base_color, grid_i, grid_j) # 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) 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 ``` **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. ### 4. Facce Laterali (Uniformi) #### Faccia Destra (X+) ```python 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+) ```python 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 ```python 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 ```python # 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 ```python 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 ```python 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 ```python # All'inizializzazione glEnable(GL_DEPTH_TEST) # Ogni frame glClear(GL_DEPTH_BUFFER_BIT) ``` OpenGL automaticamente: 1. Calcola profondità di ogni pixel 2. Confronta con depth buffer 3. 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 ```python 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 ```python 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)** ```python # Invece di glBegin/glEnd vbo = create_vbo_from_heightmap(heightmap) render_vbo(vbo) # Una sola chiamata! ``` **2. Instancing** ```python # Disegna molte tile identiche in una chiamata glDrawArraysInstanced(...) ``` **3. Frustum Culling** ```python # Non disegnare tile fuori dal campo visivo if is_visible(tile, camera): draw_tile(tile) ``` **4. Level of Detail (LOD)** ```python # 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 ```python glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) ``` Pulisce: - **Color Buffer**: Pixel sullo schermo - **Depth Buffer**: Informazioni profondità ### Background Color ```python glClearColor(0.53, 0.81, 0.92, 1.0) # Sky blue ``` Quando clearColor viene applicato, lo schermo diventa di questo colore. ### Swap Buffers ```python 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 1. Clear buffer 2. Setup camera 3. Disegna tile (facce + lati) 4. Disegna grid 5. Swap buffer --- **Prossimo Capitolo**: [Il Sistema Camera →](07-camera.md) [← Capitolo Precedente](05-implementazione.md) | [Torna all'indice](README.md)