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.
 

11 KiB

Capitolo 4: Perlin Noise - Generazione Procedurale del Terreno

Cos'è il Perlin Noise?

Il Perlin Noise è un algoritmo inventato da Ken Perlin nel 1983 per creare texture naturali procedurali. È usato in tantissimi videogiochi e applicazioni grafiche per generare:

  • 🏔 Terreni
  • Nuvole
  • 🌊 Onde dell'oceano
  • 🔥 Fuoco e fumo
  • 🎨 Texture organiche

Perché "Noise" (Rumore)?

Genera valori "casuali" ma con una caratteristica speciale: sono coerenti e fluidi.

Random vs Perlin Noise

Random Puro (Male!)

import random

# Genera altezze completamente casuali
for i in range(10):
    height = random.random() * 100
    print(height)

Risultato: 23, 91, 5, 88, 12, 67, 4, 95, 31, 8
           
Terreno:   ▂█▁█▁█▁█▃▁   Caotico!

Problema: Troppo casuale, non naturale!

Perlin Noise (Bene!)

import noise

# Genera altezze con Perlin Noise
for i in range(10):
    height = noise.pnoise1(i / 10.0) * 100
    print(height)

Risultato: 12, 18, 28, 42, 53, 58, 54, 45, 32, 20
           
Terreno:   ▂▃▄▆▇█▇▆▄▃   Fluido e naturale!

Vantaggio: Transizioni graduali, come in natura!

Come Funziona (Semplificato)

1. Griglia di Gradienti

Immagina una griglia dove ad ogni punto è assegnata una direzione casuale:

  ↗    →    ↘
  
  ↑    •    ↓      Griglia 3×3 con direzioni
  
  ↖    ←    ↙

2. Interpolazione

Per un punto qualsiasi nella griglia, calcola il valore interpolando i 4 punti vicini:

  v1 ●──────● v2
     │      │
     │  P • │   ← Punto P
     │      │
  v3 ●──────● v4

Valore(P) = interpola(v1, v2, v3, v4)

3. Risultato Fluido

Il risultato è una superficie continua con variazioni graduali:

Vista 2D del Perlin Noise:

▓▓▓▓▓▓▒▒▒▒░░░░░░░░▒▒▒▒▓▓▓▓
▓▓▓▓▒▒▒▒▒░░░░░░░░░░▒▒▒▒▒▓▓
▓▓▒▒▒▒▒░░░░░░░░░░░░░▒▒▒▒▒▓
▒▒▒▒░░░░░░░░░░░░░░░░░░▒▒▒▒
░░░░░░░░░░░░░░░░░░░░░░░░░░

Più scuro = più alto
Più chiaro = più basso

Perlin Noise 2D per Terreni

Nel nostro progetto usiamo pnoise2 (Perlin Noise bidimensionale):

import noise

# Per ogni punto della griglia
for i in range(grid_size):
    for j in range(grid_size):
        # Calcola altezza
        height = noise.pnoise2(i / scale, j / scale)

Parametro: Scale (Scala)

Controlla la "frequenza" delle variazioni:

Scale grande (es. 100.0):

Terreno: ~~~~~~~~
         Colline dolci, variazioni ampie

Scale piccola (es. 5.0):

Terreno: ∧∨∧∨∧∨∧∨
         Colline ripide, variazioni frequenti

Nel nostro progetto: scale = 8.0 (buon compromesso)

Ottave: Aggiungere Dettaglio

Un singolo livello di Perlin Noise è liscio. Le ottave aggiungono dettagli a scale diverse:

Ottava 1 (base):        ~~~~~~~~
                        Forma generale

Ottava 2 (dettaglio):   ∧∨∧∨∧∨∧∨
                        Piccole colline

Ottava 3 (dettaglio+):  vvvvvvvv
                        Rugosità fine

Combinate:              ∧~~∨∧~~∨∧~~∨
                        Terreno realistico!

Come Funziona

Ogni ottava ha:

  • Frequenza doppia (variazioni più rapide)
  • Ampiezza ridotta (impatto minore)
result = 0
amplitude = 1.0
frequency = 1.0

for octave in range(num_octaves):
    result += noise.pnoise2(x * frequency, y * frequency) * amplitude
    amplitude *= persistence  # Riduce ampiezza
    frequency *= lacunarity   # Aumenta frequenza

Parametri del Perlin Noise

1. Scale (Scala)

Quanto "zoomato" è il noise:

scale = 8.0  # 8 unità = 1 ciclo completo

Piccola (3.0):  ∧∨∧∨∧∨∧∨∧∨  Molto variabile
Media (8.0):    ~~~~~~~~~  Bilanciato
Grande (20.0):  ~~~~~~~~~~~  Molto liscio

2. Octaves (Ottave)

Quanti livelli di dettaglio:

octaves = 4

1 ottava:  ~~~~~~~~      Liscio
2 ottave:  ~~~~      Qualche dettaglio
4 ottave:  ∧∨~∧∨~∧∨~     Molto dettagliato
8 ottave:  v^v^v    Iper-dettagliato

Nel progetto: 4 ottave (buon compromesso tra dettaglio e performance)

3. Persistence (Persistenza)

Quanto ogni ottava influenza il risultato:

persistence = 0.6

Alta (0.8):   ∧∨∧∨∧∨∧∨∧∨  Molto "ruggine"
Media (0.6):  ~~~~~  Bilanciato
Bassa (0.3):  ~~~~~~~~  Molto liscio

Formula: ampiezza_ottava = amplitude * (persistence ^ numero_ottava)

4. Lacunarity (Lacunarità)

Quanto aumenta la frequenza per ogni ottava:

lacunarity = 2.5

Bassa (1.5):  Ottave simili
Media (2.5):  Buona differenza
Alta (4.0):   Grande differenza tra ottave

Formula: frequenza_ottava = frequency * (lacunarity ^ numero_ottava)

5. Base (Seed)

Punto di partenza per la generazione casuale:

base = 42  # Seed fisso = stesso terreno ogni volta

base = 0:   Terreno A
base = 42:  Terreno B (diverso da A)
base = 100: Terreno C (diverso da A e B)

Cambiando il base, ottieni terreni completamente diversi!

Codice nel Progetto

Generazione Heightmap

def generate(self):
    total_size = self.grid_size * self.tile_size  # 20
    heightmap = np.zeros((total_size, total_size))  # Array 20×20
    
    # Parametri Perlin Noise
    scale = 8.0
    octaves = 4
    persistence = 0.6
    lacunarity = 2.5
    repeat_x = 1024
    repeat_y = 1024
    base = 42
    
    # Per ogni punto della griglia
    for i in range(total_size):
        for j in range(total_size):
            # Calcola valore Perlin Noise
            height = noise.pnoise2(
                i / scale,           # Coordinate X normalizzata
                j / scale,           # Coordinate Y normalizzata
                octaves=octaves,
                persistence=persistence,
                lacunarity=lacunarity,
                repeatx=repeat_x,    # Ripeti dopo 1024 unità
                repeaty=repeat_y,
                base=base            # Seed casuale
            )
            
            # Normalizza (-1,+1) → (0,1) e scala
            heightmap[i][j] = (height + 0.5) * 80.0
    
    return heightmap

Spiegazione Passo per Passo

1. Crea array vuoto

heightmap = np.zeros((20, 20))
# [[0, 0, 0, ...],
#  [0, 0, 0, ...],
#  ...]

2. Per ogni punto (i, j)

Punto (0,0)  → noise.pnoise2(0/8, 0/8)    = -0.2
Punto (5,3)  → noise.pnoise2(5/8, 3/8)    = 0.4
Punto (19,19)→ noise.pnoise2(19/8, 19/8)  = -0.1

3. Normalizza e scala

# pnoise2 restituisce valori tra -1 e +1
# Aggiungi 0.5 per portare a (0, 1)
# Moltiplica per 80 per range finale (0, 80)

value = -0.2
normalized = (-0.2 + 0.5) * 80.0 = 24.0  # Collina media

value = 0.4
normalized = (0.4 + 0.5) * 80.0 = 72.0   # Montagna alta

value = -0.1
normalized = (-0.1 + 0.5) * 80.0 = 32.0  # Collina

4. Risultato finale

heightmap = [
    [24, 28, 35, 42, ...],
    [22, 26, 32, 40, ...],
    [18, 23, 30, 38, ...],
    ...
]

Smoothing (Levigatura)

Il progetto include una funzione opzionale per rendere il terreno più liscio:

def _smooth_terrain(self, heightmap):
    smoothed = np.copy(heightmap)
    kernel_size = 3  # Finestra 3×3
    
    for i in range(1, len(heightmap) - 1):
        for j in range(1, len(heightmap[0]) - 1):
            # Media dei vicini
            neighbors = [
                heightmap[i-1][j-1], heightmap[i-1][j], heightmap[i-1][j+1],
                heightmap[i][j-1],   heightmap[i][j],   heightmap[i][j+1],
                heightmap[i+1][j-1], heightmap[i+1][j], heightmap[i+1][j+1]
            ]
            smoothed[i][j] = sum(neighbors) / len(neighbors)
    
    return smoothed

Effetto dello Smoothing

Prima:                    Dopo smoothing:
▁▃▇▂▁▅▇▂▁▃                ▁▃▅▄▃▅▆▄▃▄
 Spigoloso                  Arrotondato

Nel progetto: Disabilitato di default per terreno più drammatico

Vantaggi del Perlin Noise

Naturale

I terreni sembrano organici e realistici

Veloce

Calcolo efficiente anche per griglie grandi

Deterministico

Stesso seed = stesso terreno (utile per multiplayer!)

Scalabile

Funziona per mappe piccole (20×20) e enormi (1000×1000)

Configurabile

Tantissimi parametri per personalizzare l'aspetto

Varianti e Alternative

Simplex Noise

Versione migliorata del Perlin Noise (più veloce, meno artefatti)

Worley Noise

Per pattern cellulari (tessere, pelle di giraffa)

Fractal Brownian Motion (FBM)

Perlin Noise con ottave (quello che usiamo!)

Diamond-Square

Algoritmo alternativo per terreni

Esempi di Configurazioni

Colline Dolci

scale = 15.0
octaves = 3
persistence = 0.4
lacunarity = 2.0
height_multiplier = 40.0

Montagne Drammatiche

scale = 5.0
octaves = 6
persistence = 0.7
lacunarity = 3.0
height_multiplier = 120.0

Pianura con Piccole Ondulazioni

scale = 25.0
octaves = 2
persistence = 0.3
lacunarity = 1.8
height_multiplier = 20.0

Terreno Alieno/Caotico

scale = 3.0
octaves = 8
persistence = 0.8
lacunarity = 4.0
height_multiplier = 100.0

Applicazioni Beyond Terreni

Il Perlin Noise non si usa solo per terreni:

Texture Procedurali

# Generare pattern di legno, marmo, ecc.
for x in range(texture_width):
    for y in range(texture_height):
        value = noise.pnoise2(x/50, y/50)
        color = wood_color(value)
        texture[x,y] = color

Animazioni

# Tempo come terza dimensione
time = frame_count / 60.0
for x in range(width):
    for y in range(height):
        value = noise.pnoise3(x/scale, y/scale, time)
        # Nuvole che si muovono!

Gameplay

# Spawn nemici in posizioni "naturali"
for i in range(num_enemies):
    x = i * 10
    y = noise.pnoise1(x/20) * map_height
    spawn_enemy(x, y)

Concetti Chiave

Perlin Noise genera valori pseudo-casuali coerenti Scale controlla la dimensione delle variazioni Octaves aggiungono livelli di dettaglio Persistence controlla l'influenza di ogni ottava Lacunarity controlla la differenza di frequenza tra ottave Seed/Base determina il terreno generato

Formula Completa

for ogni punto (x, y):
    height = 0
    amplitude = 1
    frequency = 1
    
    for ogni ottava:
        height += pnoise2(x * frequency, y * frequency) * amplitude
        amplitude *= persistence
        frequency *= lacunarity
    
    height = (height + 0.5) * height_multiplier

Nel Nostro Progetto

  • 400 punti (20×20 griglia)
  • 4 ottave per buon dettaglio
  • Scale 8.0 per variazioni bilanciate
  • Range 0-80 per altezze finali
  • Seed 42 per terreno consistente

Prossimo Capitolo: Architettura del Codice →

← Capitolo Precedente | Torna all'indice