Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

Bézier, curve e percorsi

Le curve di Bézier sono un'approssimazione matematica delle forme geometriche naturali. Le utilizziamo per rappresentare una curva con il minor numero di informazioni possibile e con un elevato livello di flessibilità.

A differenza dei concetti matematici più astratti, le curve di Bézier sono state create per il design industriale. Sono uno strumento popolare nell'industria dei software di grafica.

Si basano sull'interpolazione, che abbiamo visto nell'articolo precedente, combinando più passaggi per creare curve lisce. Per capire meglio come funzionano le curve di Bézier, partiamo dalla loro forma più semplice: la Bézier quadratica.

Bézier quadratica

Prendi tre punti, il minimo richiesto affinché la Bezier quadratica funzioni:

../../_images/bezier_quadratic_points.png

Per tracciare una curva tra di essi, interpoliamo prima gradualmente trai due vertici di ciascuno dei due segmenti formati dai tre punti, utilizzando valori compresi tra 0 e 1. Ciò ci fornisce due punti che si muovono lungo i segmenti man mano che modifichiamo il valore di t da 0 a 1.

func _quadratic_bezier(p0: Vector2, p1: Vector2, p2: Vector2, t: float):
    var q0 = p0.lerp(p1, t)
    var q1 = p1.lerp(p2, t)

Interpoliamo poi q0 e q1 per ottenere un singolo punto r che si muove lungo una curva.

var r = q0.lerp(q1, t)
return r

Questo tipo di curva è chiamata curva di Bézier quadratica.

../../_images/bezier_quadratic_points2.gif

(Credito dell'immagine: Wikipedia)

Bezier cubica

Partendo dall'esempio precedente, possiamo ottenere un più controllo interpolando tra quattro punti.

../../_images/bezier_cubic_points.png

Per prima cosa utilizziamo una funzione con quattro parametri per prendere quattro punti come input, p0, p1, p2 e p3:

func _cubic_bezier(p0: Vector2, p1: Vector2, p2: Vector2, p3: Vector2, t: float):

Applichiamo un'interpolazione lineare a ciascuna coppia di punti per ridurli a tre:

var q0 = p0.lerp(p1, t)
var q1 = p1.lerp(p2, t)
var q2 = p2.lerp(p3, t)

Poi prendiamo i nostri tre punti e li riduciamo a due:

var r0 = q0.lerp(q1, t)
var r1 = q1.lerp(q2, t)

E a uno:

var s = r0.lerp(r1, t)
return s

Ecco l'intera funzione:

func _cubic_bezier(p0: Vector2, p1: Vector2, p2: Vector2, p3: Vector2, t: float):
    var q0 = p0.lerp(p1, t)
    var q1 = p1.lerp(p2, t)
    var q2 = p2.lerp(p3, t)

    var r0 = q0.lerp(q1, t)
    var r1 = q1.lerp(q2, t)

    var s = r0.lerp(r1, t)
    return s

Il risultato sarà una curva liscia che interpola tra tutti e quattro i punti:

../../_images/bezier_cubic_points.gif

(Credito dell'immagine: Wikipedia)

Nota

L'interpolazione di Bezier cubica funziona allo stesso modo in 3D, basta utilizzare Vector3 invece di Vector2.

Aggiunta di punti di controllo

Basandoci sulla Bézier cubica, possiamo modificare il modo in cui due dei punti funzionano per controllare liberamente la forma della nostra curva. Invece di avere p0, p1, p2 e p3, li memorizzeremo come:

  • point0 = p0: è il primo punto, la sorgente

  • control0 = p1 - p0: è un vettore relativo al primo punto di controllo

  • control1 = p3 - p2: è un vettore relativo al secondo punto di controllo

  • point1 = p3: è il secondo punto, la destinazione

In questo modo, abbiamo due punti e due punti di controllo che sono vettori relativi ai rispettivi punti. Se hai già utilizzato software di grafica o animazione, questo potrebbe sembrarti familiare:

../../_images/bezier_cubic_handles.png

Ecco come i software di grafica presentano le curve di Bézier agli utenti, e come funzionano e appaiono in Godot.

Curve2D, Curve3D, Path e Path2D

Ci sono due oggetti che contengono curve: Curve3D e Curve2D (rispettivamente per 3D e 2D).

Possono contenere più punti, consentendo di avere percorsi più lunghi. È anche possibile impostarli sui nodi: Path3D e Path2D (altrettanto rispettivamente per 3D e 2D):

../../_images/bezier_path_2d.png

Tuttavia, il loro utilizzo potrebbe non essere del tutto evidente. Pertanto di seguito si descrivono i casi d'uso più comuni per le curve di Bézier.

Valutarle

Valutarle e basta può essere un'opzione, ma nella maggior parte dei casi non è molto utile. Il grande difetto delle curve di Bézier è che se le si attraversa a velocità costante, da t = 0 a t = 1, l'interpolazione effettiva non si muoverà a velocità costante. Anche la velocità è un'interpolazione tra le distanze tra i punti p0, p1, p2 e p3 e non esiste un modo matematicamente semplice per attraversare la curva a velocità costante.

Facciamo un esempio con il seguente pseudocodice:

var t = 0.0

func _process(delta):
    t += delta
    position = _cubic_bezier(p0, p1, p2, p3, t)
../../_images/bezier_interpolation_speed.gif

Come puoi vedere, la velocità (in pixel al secondo) del cerchio varia, anche se t è aumentata a velocità costante. Questo rende le curve di Bézier difficili da utilizzare direttamente per scopi pratici.

Disegnare

Disegnare curve di Bézier (o oggetti basati su di esse) è un caso d'uso molto comune, sebbene non sia facile. Praticamente in ogni caso, le curve di Bézier devono essere convertite in una sorta di segmenti. Questo, tuttavia, è solitamente difficile, senza creare moltissimi segmenti.

La ragione è che alcune sezioni di una curva (in particolare gli angoli) potrebbero richiedere una quantità considerevole di punti, mentre altre sezioni potrebbero non richiederne:

../../_images/bezier_point_amount.png

Inoltre, se entrambi i punti di controllo fossero 0, 0 (ricorda che sono vettori relativi), la curva di Bézier sarebbe semplicemente una linea retta (quindi disegnare un numero elevato di punti sarebbe uno spreco).

Prima di disegnare le curve di Bézier, una tassellazione è necessaria. Questa operazione è spesso eseguita attraverso una funzione ricorsiva (o "dividi et impera") che suddivide la curva finché il valore di curvatura non scende al di sotto di una certa soglia.

Le classi Curve forniscono ciò tramite la funzione Curve2D.tessellate(), la quale riceve argomenti facoltativi per le fasi di ricorsione (stages) e la tolleranza angolare (tolerance). Disegnare qualcosa basato su una curva è più facile così.

Percorrerle

L'ultimo caso d'uso comune per le curve è percorrerle. A causa di quanto menzionato prima riguardo alla velocità costante, anche questo è difficile.

Per facilitare il processo, le curve devono essere pre-calcolate in punti equidistanti. Facendo così, è possibile approssimarle con un'interpolazione regolare (che può essere ulteriormente migliorata con un'opzione cubica). Per farlo, basta usare il metodo Curve3D.sample_baked() insieme a Curve2D.get_baked_length(). La prima chiamata a uno dei due eseguirà il pre-calcolo interno della curva.

Una traversata a velocità costante si può quindi effettuare con il seguente pseudocodice:

var t = 0.0

func _process(delta):
    t += delta
    position = curve.sample_baked(t * curve.get_baked_length(), true)

E il risultato si muoverà quindi a velocità costante:

../../_images/bezier_interpolation_baked.gif