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)
Riepilogo
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 →