Beziers, Kurven und Pfade

Bezierkurven sind eine mathematische Annäherung an natürliche geometrische Formen. Wir verwenden sie um eine Kurve mit möglichst wenig Informationen und einem hohen Maß an Flexibilität darzustellen.

Im Gegensatz zu abstrakteren mathematischen Konzepten wurden Bezier-Kurven für das Industriedesign erstellt. Sie sind ein beliebtes Werkzeug in der Grafiksoftwareindustrie.

Sie basieren auf interpolation die wir im vorherigen Artikel gesehen haben und kombinieren mehrere Schritte um glatte Kurven zu erstellen. Um besser zu verstehen wie Bezier-Kurven funktionieren, beginnen wir mit der einfachsten Form: Quadratisches Bezier.

Quadratisches Bezier

Nehmen Sie drei Punkte, das Minimum, das erforderlich ist, damit quadratisches Bezier funktioniert:

../../_images/bezier_quadratic_points.png

Um eine Kurve zwischen ihnen zu zeichnen, interpolieren wir zunächst allmählich über die beiden Scheitelpunkte jedes der beiden Segmente, die durch die drei Punkte gebildet werden, und verwenden dabei Werte zwischen 0 und 1. Dadurch erhalten wir zwei Punkte, die sich entlang der Segmente bewegen, wenn wir den Wert von t ändern.

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

Wir interpolieren dann q0 und q1 um einen einzelnen Punkt r zu erhalten, der sich entlang einer Kurve bewegt.

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

Diese Art wird als Quadratische Bezier-Kurve bezeichnet.

../../_images/bezier_quadratic_points2.gif

(Image credit: Wikipedia)

Kubischer Bezier

Aufbauend auf dem vorherigen Beispiel können wir mehr Kontrolle erhalten, indem wir zwischen vier Punkten interpolieren.

../../_images/bezier_cubic_points.png

Wir verwenden zuerst eine Funktion mit vier Parametern, um vier Punkte als Eingabe zu verwenden: p0, p1, p2 und p3:

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

Wir wenden eine lineare Interpolation auf jedes Paar von Punkten an, um sie auf drei zu reduzieren:

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

Wir nehmen dann unsere drei Punkte und reduzieren sie auf zwei:

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

und auf einen:

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

Hier ist die komplette Funktion:

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

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

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

Das Ergebnis ist eine glatte Kurve, die zwischen allen vier Punkten interpoliert:

../../_images/bezier_cubic_points.gif

(Image credit: Wikipedia)

Bemerkung

Die kubische Bezier-Interpolation funktioniert in 3D genauso. Verwenden Sie einfach Vector3 anstelle von Vector2.

Kontrollpunkte hinzufügen

Aufbauend auf Cubische Bezier können wir die Funktionsweise von zwei Punkten ändern, um die Form unserer Kurve frei zu kontrollieren. Anstatt p0, p1, p2 und p3 zu haben, speichern wir sie wie folgt:

  • point0 = p0: Ist der erste Punkt, die Quelle

  • control0 = p1 - p0: Ist ein Vektor relativ zum ersten Kontrollpunkt

  • control1 = p3 - p2: Ist ein Vektor relativ zum zweiten Kontrollpunkt

  • point1 = p3: Ist der zweite Punkt, das Ziel

Auf diese Weise haben wir zwei Punkte und zwei Kontrollpunkte, die relative Vektoren zu den jeweiligen Punkten sind. Wenn Sie zuvor Grafik- oder Animationssoftware verwendet haben, kommt Ihnen dies möglicherweise bekannt vor:

../../_images/bezier_cubic_handles.png

Auf diese Weise präsentiert Grafiksoftware den Benutzern Bezier-Kurven und wie diese in Godot arbeiten und aussehen.

Curve2D, Curve3D, Path und Path2D

Es gibt zwei Objekte, die Kurven enthalten: Curve3D und Curve2D (für 3D bzw. 2D).

Sie können mehrere Punkte enthalten, wodurch längere Pfade möglich sind. Es ist auch möglich, sie auf Nodes zu setzen: Pfad und Pfad2D (auch für 3D bzw. 2D):

../../_images/bezier_path_2d.png

Ihre Verwendung ist jedoch möglicherweise nicht ganz offensichtlich. Im Folgenden werden die häufigsten Anwendungsfälle für Bezier-Kurven beschrieben.

Auswerten

Nur eine Beurteilung kann eine Option sein, ist aber in den meisten Fällen nicht sehr nützlich. Der große Nachteil bei Bezier-Kurven besteht darin, dass sich die tatsächliche Interpolation nicht mit konstanter Geschwindigkeit bewegt, wenn Sie sie mit konstanter Geschwindigkeit von t = 0 bis t = 1 durchlaufen. Die Geschwindigkeit ist auch eine Interpolation zwischen den Abständen zwischen den Punkten p0, p1, p2 und p3 und es gibt keine mathematisch einfache Möglichkeit die Kurve mit konstanter Geschwindigkeit zu durchlaufen.

Schauen wir uns ein einfaches Beispiel mit dem folgenden Pseudocode an:

var t = 0.0

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

Wie Sie sehen können variiert die Geschwindigkeit (in Pixel pro Sekunde) des Kreises, obwohl t mit konstanter Geschwindigkeit erhöht wird. Dies macht es schwierig, Bezier standardmäßig für Praktisches zu verwenden.

Zeichnen

Das Zeichnen von Bezier (oder Objekten basierend auf der Kurve) ist ein sehr häufiger Anwendungsfall, aber auch nicht einfach. In fast jedem Fall müssen Bezier-Kurven in eine Art Segment konvertiert werden. Dies ist jedoch normalerweise schwierig, ohne eine sehr große Menge davon zu erzeugen.

Der Grund dafür ist, dass einige Abschnitte einer Kurve (insbesondere Ecken) möglicherweise eine beträchtliche Anzahl von Punkten erfordern, andere Abschnitte möglicherweise nicht:

../../_images/bezier_point_amount.png

Wenn beide Kontrollpunkte 0, 0 wären (denken Sie daran, dass es sich um relative Vektoren handelt), wäre die Bezier-Kurve nur eine gerade Linie (daher wäre das Zeichnen einer großen Anzahl von Punkten verschwenderisch).

Vor dem Zeichnen von Bezier-Kurven ist Tessellation erforderlich. Dies geschieht häufig mit einer rekursiven oder "Teile und Herrsche"-Funktion, die die Kurve teilt, bis der Krümmungsbetrag einen bestimmten Schwellenwert unterschreitet.

Die Curve-Klassen bieten dies über die Funktion Curve2D.tessellate() (die optionale Stufen der Rekursion und Winkel Toleranz-Argumente empfängt). Auf diese Weise ist es einfacher etwas basierend auf einer Kurve zu zeichnen.

Durchlaufen

Der letzte häufige Anwendungsfall für die Kurven besteht darin, sie zu durchlaufen. Aufgrund der zuvor erwähnten konstanten Geschwindigkeit ist dies ebenfalls schwierig.

Um dies zu vereinfachen, müssen die Kurven in Punkte gleichmäßiger Abstände gebacken werden. Auf diese Weise können sie durch regelmäßige Interpolation angenähert werden (was mit einer kubischen Option weiter verbessert werden kann). Verwenden Sie dazu einfach die Methode Curve.interpolate_baked() zusammen mit Curve2D.get_baked_length(). Der erste Aufruf von einer der beiden backt die Kurve intern.

Das Durchlaufen mit konstanter Geschwindigkeit kann dann mit dem folgenden Pseudocode erfolgen:

var t = 0.0

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

Und das Ergebnis ist dann, eine Bewegung mit konstanter Geschwindigkeit:

../../_images/bezier_interpolation_baked.gif