• Skip to primary navigation
  • Skip to main content
  • Skip to footer

Codemotion Magazine

We code the future. Together

  • Discover
    • Events
    • Community
    • Partners
    • Become a partner
    • Hackathons
  • Magazine
    • Backend
    • Dev community
    • Carriere tech
    • Intelligenza artificiale
    • Interviste
    • Frontend
    • DevOps/Cloud
    • Linguaggi di programmazione
    • Soft Skill
  • Talent
    • Discover Talent
    • Jobs
    • Manifesto
  • Companies
  • For Business
    • EN
    • IT
    • ES
  • Sign in
ads

Gabriele ScaggianteAgosto 13, 2025 8 min di lettura

Le basi della generazione procedurale di terreno in Unity

sviluppo videogiochi
risultato della generazione procedurale
facebooktwitterlinkedinreddit

Nel contesto del game developement, la generazione procedurale del terreno è una tecnica che utilizza algoritmi specifici per creare automaticamente ambienti di gioco (montagne, colline, isole…) in modo dinamico, ovvero senza che gli sviluppatori debbano realizzarli a mano.

La generazione procedurale è particolarmente comuni in giochi in cui si vuole fare in modo che il giocatore viva un’esperienza diversa ad ogni partita. È infatti molto comune in quei videogiochi in cui il gameplay è incentrato sull’esplorazione, la rigiocabilità, la scoperta e la varietà del mondo di gioco. Essa non è limitata solo al terreno o al mondo di gioco, ma può anche essere estesa a grafiche, effetti sonori, animazioni, questlines e molto altro.

Recommended article
Aprile 17, 2025

Advergames: cosa sono, a cosa servono e come usarli al meglio

Gabriele Scaggiante

Gabriele Scaggiante

sviluppo videogiochi

In questo articolo cerchiamo di capire meglio come funziona la generazione procedurale del terreno, osservando un esempio funzionante realizzato in Unity.

Il funzionamento di un generatore procedurale, ad alto livello

Un generatore procedurale per un mondo di gioco è essenzialmente un algoritmo che prende in input un numero intero, detto seed, e eventualmente altri parametri utili, e genera il mondo di gioco in maniera automatica sulla base dei parametri passati e del seed.

Il seed è utilizzato per garantire che il mondo sia generato in maniera deterministica: a parità di seed e parametri, il mondo generato dall’algoritmo sarà sempre identico.

Il seed è infatti utilizzato per inizializzare uno o più generatori di numeri pseudo-casuali, che, in quanto tali, generano numeri che sembrano casuali, ma che sono in realtà completamente deterministici. Attraverso lunghe serie di trasformazioni, questi dati determinano la struttura del mondo di gioco.

Questo può non sembrare molto interessante, ma offre in realtà un grande vantaggio: un mondo generato proceduralmente non ha bisogno di essere memorizzato localmente. Questo significa che anche mondi di dimensioni “infinite” sono tranquillamente realizzabili e riproducibili.

Vediamo allora più nel dettaglio una semplice implementazione per un generatore procedurale per il terreno di un generico mondo di gioco.

Perlin noise – che cos’è e come funziona

Il Perlin noise è un tipo di rumore sviluppato da Ken Perlin nel 1983. Si tratta di una funzione pseudo-casuale che restituisce valori numerici compresi tra -1 e 1 che variano in maniera graduale e “naturale” tra loro.

Immagine del Perlin noise in due dimensioni.
Rappresentazione grafica del Perlin noise bidimensionale (normalizzato): le zone scure corrispondono a zone con valori prossimi allo zero, quelle chiare a zone con valori prossimi a 1.

Il Perlin noise è implementato creando una griglia sui cui vertici vengono definiti dei vettori di lunghezza unitaria con direzione scelta in maniera pseudo-casuale. Questi vettori sono detti “gradienti“.

Per ogni singolo punto all’interno della griglia si calcola poi il vettore di offset rispetto ai vertici della cella della griglia in cui si trova il punto (che in 2D sono 4).

Si calcola poi il prodotto scalare tra i vettori di offset e i gradienti dei rispettivi vertici. Si ottengono così degli scalari che sono poi “fusi” insieme tramite interpolazione, ottenendo il risultato sopra.

Perlin noise – utilizzi

La maggior parte dei game engine mettono a disposizione funzioni come il Perlin noise pronte per l’uso. Tipicamente l’output di queste funzioni è normalizzato nel range 0-1.

Unity, ad esempio, mette a disposizione la funzione PerlinNoise nella classe Mathf, definita come:

public static float PerlinNoise(float x, float y);Code language: PHP (php)

Questa funzione prende in input i punti di sample x e y, da cui sarà calcolato il valore in output.

La chiave di moltissimi algoritmi di generazione procedurale del terreno è utilizzare il valore in output dal Perlin noise come offset verticale per i vertici di un piano.

Possiamo infatti costruire un piano suddiviso tante volte in modo da essere costituito da tante celle della stessa dimensione allineate a formare una griglia. Calcolando il Perlin noise in corrispondenza delle coordinate dei vertici della griglia, otteniamo un valore per ciascun vertice. Questo valore è utilizzato per determinare di quanto alzare il vertice da cui è stato calcolato.

In questo modo il Perlin noise viene “proiettato” sul terreno, alzandolo e facendogli assumere una forma ondulatoria, simile a un paesaggio collinare.


Si noti che il metodo Mathf.PerlinNoise offerto da Unity è una funzione deterministica: dato lo stesso input, restituisce sempre lo stesso valore, ma non consente di specificare un seed in maniera diretta, perché non si basa su un generatore di numeri pseudo-casuali interno.

È comunque possibile simulare un comportamento simile al seed introducendo un offset costante nelle coordinate di input. Ad esempio, usando x + seedOffsetX e y + seedOffsetY, è possibile ottenere varianti del terreno generate in modo consistente a partire da un seed arbitrario.

Generazione della mesh

Per questo esempio rimaniamo su qualcosa di molto semplice. La prima cosa da fare è costruire la mesh di partenza che poi deformeremo utilizzando il Perlin noise.

La mesh in questione non è altro che un piano suddiviso un certo numero di volte.

Definiamo allora due variabili:

  • Scale: specifica la dimensione del piano (per questo esempio supponiamo di forma quadrata)
  • Resolution: il numero di suddivisioni del piano

In Unity una mesh è definita da 3 ingredienti:

  • Un array di Vector3 (vertices), che rappresentano i vertici della mesh
  • Un array di int (triangles), che rappresentano i triangoli di cui è costituita la mesh
  • Un array di Vector2 (uvs), che rappresenta la mappa uv della mesh

La generazione procedurale e l’interazione con ambienti di gioco generati proceduralmente funziona quasi sempre operando su questi dati. Il codice per generare il piano è il seguente:

using System;
using UnityEngine;

[ExecuteInEditMode]
public class worldGenerator : MonoBehaviour
{
    public int scale = 20;
    public int resolution = 10;

    void Awake()
    {
        Generate();
    }

    void OnValidate()
    {
        Generate();
    }

    void Generate()
    {
        Mesh mesh = new Mesh();
        mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;

        int vertexCount = (resolution + 1) * (resolution + 1);
        Vector3[] vertices = new Vector3[vertexCount];
        Vector2[] uvs = new Vector2[vertexCount];
        for (int y = 0; y <= resolution; y++)
        {
            for (int x = 0; x <= resolution; x++)
            {
                int index = x + y * (resolution + 1);
                Vector2 normalizedPosition = new Vector2((float)x / resolution, (float)y / resolution);
                Vector2 worldPosition = normalizedPosition * scale;
                vertices[index] = new Vector3(worldPosition.x, 0f, worldPosition.y);
                uvs[index] = new Vector2(normalizedPosition.x, normalizedPosition.y);
            }
        }

        int[] triangles = new int[resolution * resolution * 6];
        int i = 0;
        for (int y = 0; y < resolution; y++)
        {
            for (int x = 0; x < resolution; x++)
            {
                int vertexIndex = x + y * (resolution + 1);

                triangles[i] = vertexIndex;
                triangles[i + 1] = vertexIndex + resolution + 1;
                triangles[i + 2] = vertexIndex + 1;

                triangles[i + 3] = vertexIndex + 1;
                triangles[i + 4] = vertexIndex + resolution + 1;
                triangles[i + 5] = vertexIndex + resolution + 2;
                i += 6;
            }
        }

        mesh.vertices = vertices;
        mesh.triangles = triangles;
        mesh.uv = uvs;
        mesh.RecalculateNormals();

        GetComponent<MeshFilter>().mesh = mesh;
    }
}

Code language: C# (cs)

Con questo codice:

  • Specifichiamo la posizione dei vertici disponendoli su una griglia quadrata che copre l’intero piano.
  • Specifichiamo gli uv normalizzando le coordinate dei vertici.
  • Definiamo i triangoli che costituiscono la mesh indicando come connettere i vari vertici tra loro per formare ogni singolo triangolo.
  • Assegnamo i dati così prodotti alla mesh creata e la associamo al MeshFilter dell’oggetto su cui stiamo lavorando.

Applichiamo il Perlin noise

A questo punto possiamo applicare il Perlin noise. Per prima cosa, deformiamo la mesh usando Mathf.PerlinNoise: ci basta specificare come coordinata y nella definizione dei vertici il valore in output dal Perlin noise calcolato in corrispondenza di ciascun vertice:

vertices[index] = new Vector3(worldPosition.x, Mathf.PerlinNoise(normalizedPosition.x, normalizedPosition.y), worldPosition.y);
Code language: JavaScript (javascript)

Con questa modifica il piano viene deformato a tutti gli effetti, ma di poco: Mathf.PerlinNoise restituisce infatti valori compresi tra 0 e 1. Possiamo definire una variabile height da usare per scalarne il risultato e avere così una deformazione maggiore. Per comodità, definiamo anche una funzione getElevation(float x, float y) che ritorni la y da assegnare a ciascun vertice. La logica di generazione sarà specificata al suo interno.

float getElevation(float x , float y) {
    return Mathf.PerlinNoise(x , y) * height;
}

...

vertices[index] = new Vector3(worldPosition.x, getElevation(normalizedPosition.x, normalizedPosition.y), worldPosition.y);
Code language: JavaScript (javascript)

Si noti che, pur passando a getElevation le coordinate normalizzate, sarebbe stato corretto anche passare le coordinate effettive dei vertici (e, anzi, solitamente si fa così). Ho preferito fare in questo modo per mantenere la logica che garantisce l’indipendenza tra la deformazione del terreno e il valore della variabile scale visibile all’esterno della funzione.

Poiché le coordinate sono sempre normalizzate in un range 0-1, il Perlin noise è calcolato su coordinate con la stessa magnitudine, indipendentemente dalle dimensioni della mappa. Questo garantisce generazioni identiche anche con dimensioni della mappa molto diverse. La variabile height, volendo, può essere scalata dinamicamente in modo da mantenere anche l’altezza della deformazione costante su scale diverse.

Perlin noise e ottave

Il terreno che abbiamo ottenuto nel paragrafo precedente è abbastanza noioso e piuttosto artificiale. Non assomiglia molto a una catena montuosa e manca di dettagli.

Per risolvere il problema dobbiamo trovare un modo per aggiungere imperfezioni ai valori ottenuti dal Perlin noise. Una tecnica usatissima per fare proprio questo è la tecnica delle ottave.

Quello che si fa è usare come rumore non uno, ma più Perlin noise sommati tra di loro.

Il concetto riprende le ottave musicali. Questa tecnica prevede di partire con un Perlin noise semplice, con frequenza bassa e ampiezza alta.

  • Per frequenza si intende quanto velocemente variano i valori del Perlin noise
  • Per ampiezza si intende quanto grandi sono questi valori

A partire da una frequenza e un’ampiezza iniziale, si generano via via Perlin noise con frequenza doppia ma ampiezza dimezzata. Ciascun Perlin noise è poi sommato al precedente, e il processo viene ripetuto un certo numero, finito, di volte. Ciascun passaggio aumenta il dettaglio del rumore di output, permettendo quindi al nostro paesaggio di assumere un aspetto ben più naturale.

Implementiamo allora una funzione che calcoli il Perlin noise con un certo numero di ottave:

float octaveNoise(float x, float y, int octaveCount, float baseFrequency , float baseAmplitude, float lacunarityValue, float persistenceValue)
{
    float frequency = baseFrequency;
    float amplitude = baseAmplitude;
    float totalAmplitude = 0;
    float value = 0;
    for (int i = 0; i < octaveCount; i++)
    {
        value += Mathf.PerlinNoise(x * frequency, y * frequency) * amplitude;
        totalAmplitude += amplitude;
        frequency *= lacunarityValue;
        amplitude *= persistenceValue;
    }

    return value / totalAmplitude;
}
Code language: HTML, XML (xml)

Si noti che qui specifichiamo anche di quanto via via scalare frequenza e ampiezza, rispettivamente tramite lacunarityValue e persistenceValue. Il valore calcolato come somma delle ottave, prima di essere ritornato dalla funzione, viene normalizzato tramite divisione per la somma delle ampiezze di ogni ottava.

Ora possiamo usare questa versione del Perlin noise nel nostro generatore procedurale:

float getElevation(float x , float y)
{
    return octaveNoise(x, y, terrainOctaves, terrainBaseFrequency, terrainBaseAmplitude, terrainLacunarity, terrainPersistence) * height;
}Code language: JavaScript (javascript)

Un po’ di colore

Ora che abbiamo un terreno che ha un aspetto accettabile, possiamo divertirci a colorarlo come preferiamo. Per fare ciò ho creato un piccolo shader tramite shader graph usato dal materiale associato al terreno.

Il compito di questo shader è colorare di verde le zone più pianeggianti, di marrone le zone di montagna e le zone pendenti, e di bianco le zone più alte, come se le cime delle montagne siano coperte di neve.

Per colorare in maniera diversa zone pianeggianti e zone montuose, calcoliamo il prodotto scalare tra la normale del terreno e il vettore unitario che rappresenta l’asse verticale del mondo di gioco.

Fare ciò ci restituisce un valore che è pari a 1 se i due vettori puntano entrambi nello stesso verso, e minore di uno se puntano in direzioni diverse. Man mano che i vettori diventano ortogonali, il prodotto scalare approccia zero.

Più la normale si discosta dal vettore verticale al mondo di gioco, più il terreno si colora di marrone.
Schema riassuntivo del funzionamento dello shader per la colorazione del terreno. L’inclinazione del terreno, data dalla normale, è usata per fare il sample del colore da utilizzare a partire da un gradiente realizzato appositamente.

Questo fa sì che i punti del terreno più in pendenza assumano un colore marrone, mentre i punti meno in pendenza si colorino di verde.

Per fare in modo che le cime delle montagne siano innevate, possiamo semplicemente valutare la posizione di ciascun vertice in relazione al bounding box del terreno, e aggiungere al colore derivante dal procedimento spiegato sopra un colore bianco che è tanto più chiaro quanto più in alto si trova il vertice rispetto al bounding box. Questo sistema è ottimo quando siamo sicuri che il terreno assuma un certo valore massimo in prossimità della cima di una montagna, ma va adattato meglio nel caso in cui si prevedano chunk di terreno pianeggianti, eventualmente inserendo un threshold assoluto o in altro modo.

Lo shader graph finale è il seguente:

La posizione y nello spazio oggetto, divisa per il bounds size, viene usata per prelevare il colore da un gradiente e applicarlo alla cima delle montagne

E il risultato complessivo è questo:

Terreno generato proceduralmente, con una piccola zona pianeggiante verde e aree montagnose che la circondano
Una piccola pianura è stata aggiunta per mostrare meglio le differenze tra montagne e sezioni pianeggianti. La pianura è ottenuta sommando al perlin noise l’inverso di un perlin noise a 5 ottave elevato al quadrato.

Considerazioni aggiuntive

Questo articolo funge da esempio base per quello che l’universo della generazione procedurale del terreno. Ci sarebbe moltissimo da aggiungere a un sistema del genere, ma si tratta pur sempre di un punto di partenza utile per comprendere i concetti fondamentali di queste metodologie.

A questo punto sarebbe interessante riscrivere la generazione del terreno utilizzando un vertex shader, gestire la generazione di chunk e biomi, approfondire tecniche di ottimizzazione e di LOD e molto altro. Le possibilità sono infinite!

Related Posts

shockwave, onda d'urto

Creiamo un effetto “onda d’urto” in GLSL

Gabriele Scaggiante
Agosto 27, 2024
gaming industry facts

10 sorprese e paradossi nell’industria dei giochi

Luca Fregoso
Aprile 23, 2024
console emulators, retrogaming, build your emulator

Come creare un Console Emulator in Python

Codemotion
Novembre 21, 2023
Share on:facebooktwitterlinkedinreddit

Tagged as:Game developement Unity

Gabriele Scaggiante
Sviluppatore di videogiochi per professione e divulgatore, realizzo advergame per aiutare brand e aziende a promuoversi attraverso il gaming.
E se l’anello debole della programmazione fosse il programmatore stesso?
Previous Post

Footer

Discover

  • Events
  • Community
  • Partners
  • Become a partner
  • Hackathons

Magazine

  • Tech articles

Talent

  • Discover talent
  • Jobs

Companies

  • Discover companies

For Business

  • Codemotion for companies

About

  • About us
  • Become a contributor
  • Work with us
  • Contact us

Follow Us

© Copyright Codemotion srl Via Marsala, 29/H, 00185 Roma P.IVA 12392791005 | Privacy policy | Terms and conditions