You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

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:

  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

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!

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 →

← Capitolo Precedente | Torna all'indice