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.
 

13 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, 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:

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

# 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+)

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