Beziers, curvas y caminos

Las curvas de Bezier son una aproximación matemática de las formas geométricas naturales. Las utilizamos para representar una curva con la menor información posible y con un alto nivel de flexibilidad.

A diferencia de los conceptos matemáticos más abstractos, las curvas de Bezier fueron creadas para el diseño industrial. Son una herramienta popular en la industria del software gráfico.

Se basan en interpolation, que vimos en el artículo anterior, combinando múltiples pasos para crear curvas suaves. Para entender mejor cómo funcionan las curvas de Bezier, empecemos por su forma más simple: Bezier Cuadrática.

Bezier Cuadrática

Toma tres puntos, el mínimo requerido para que funcione la Bezier Cuadrática:

../../_images/bezier_quadratic_points.png

Para dibujar una curva entre ellos, primero interpolamos gradualmente sobre los dos vértices de cada uno de los dos segmentos formados por tres puntos, usando valores que van del 0 al 1. Esto nos da dos puntos que se mueven a lo largo de los segmentos a medida que cambiamos el valor de t del 0 al 1.

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)

Luego interpolamos q0 y q1 para obtener un único punto r que se mueve a lo largo de una curva.

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

Este tipo de curva se llama curva de Bezier Cuadrática.

../../_images/bezier_quadratic_points2.gif

(Crédito de la imagen: Wikipedia)

Bezier Cúbico

Basándonos en el ejemplo anterior, podemos obtener más control interpolando entre cuatro puntos.

../../_images/bezier_cubic_points.png

Primero usamos una función con cuatro parámetros para tomar cuatro puntos como entrada, p0, p1, p2 y p3:

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

Aplicamos una interpolación lineal a cada par de puntos para reducirlos a tres:

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

Luego tomamos nuestros tres puntos y los reducimos a dos:

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

Y a uno:

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

Aquí está la función completa:

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

El resultado será una curva suave que interpola entre los cuatro puntos:

../../_images/bezier_cubic_points.gif

(Crédito de la imagen: Wikipedia)

Nota

La interpolación de Bezier Cúbica funciona igual en 3D, sólo que usa Vector3 en lugar de Vector2.

Agregando puntos de control

Basándonos en la Bezier Cúbica, podemos cambiar la forma en que dos de los puntos funcionan para controlar la forma de nuestra curva libremente. En lugar de tener p0, p1, p2 y p3, los guardaremos como:

  • point0 = p0: Es el primer punto, la fuente

  • control0 = p1 - p0: Es un vector relativo al primer punto de control

  • control1 = p3 - p2: Es un vector relativo al segundo punto de control

  • point1 = p3: Es el segundo punto, el destino

De esta manera, tenemos dos puntos y dos puntos de control que son vectores relativos a los respectivos puntos. Si has usado software de gráficos o de animación antes, esto puede resultar familiar:

../../_images/bezier_cubic_handles.png

Así es como el software de gráficos presenta las curvas de Bezier a los usuarios, y cómo funcionan y se ven en Godot.

Curve2D, Curve3D, Path y Path2D

Existen dos objetos que contienen curvas: Curve3D y Curve2D (para 3D y 2D respectivamente).

Pueden contener varios puntos, permitiendo caminos más largos. También es posible establecerlos en nodos: Path y Path2D (también para 3D y 2D respectivamente):

../../_images/bezier_path_2d.png

Sin embargo, su uso puede no ser completamente obvio, por lo que a continuación se describen los casos de uso más comunes de las curvas de Bezier.

Evaluando

Evaluarlos puede ser una opción, pero en la mayoría de los casos no es muy útil. El gran inconveniente de las curvas de Bezier es que si las atraviesas a velocidad constante, de t = 0 a t = 1, la interpolación real no se moverá a velocidad constante. La velocidad también es una interpolación entre las distancias entre los puntos p0, p1, p2 y p3 y no hay una forma matemáticamente simple de atravesar la curva a velocidad constante.

Hagamos un ejemplo simple con el siguiente pseudocódigo:

var t = 0.0

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

Como puedes ver, la velocidad (en píxeles por segundo) del círculo varía, aunque t se incrementa a velocidad constante. Esto hace que las beziers sean difíciles de usar para cualquier cosa práctica fuera de la caja.

Dibujando

Dibujar beziers (u objetos basados en la curva) es un caso de uso muy común, pero tampoco es fácil. En casi todos los casos, las curvas Bezier deben ser convertidas en algún tipo de segmento. Sin embargo, esto normalmente es difícil sin crear una cantidad muy alta de ellos.

La razón es que algunas secciones de una curva (específicamente, las esquinas) pueden requerir cantidades considerables de puntos, mientras que otras secciones pueden no necesitarlos:

../../_images/bezier_point_amount.png

Además, si ambos puntos de control fueran 0, 0 (recuerda que son vectores relativos), la curva de Bezier sería sólo una línea recta (por lo que dibujar una gran cantidad de puntos sería un desperdicio).

Antes de dibujar las curvas de Bezier, se requiere un teselado. Esto se hace a menudo con una función recursiva o de dividir y conquistar que divide la curva hasta que la cantidad de curvatura se vuelve menor que un cierto umbral.

Las clases Curve proporcionan esto a través de la función Curve2D.tessellate() (que recibe los argumentos opcionales stages de recursividad y ángulo de tolerance). De esta manera, dibujar algo basado en una curva es más fácil.

Traversal

El último caso de uso común de las curvas es atravesarlas. Debido a lo que se mencionó antes sobre la velocidad constante, esto también es difícil.

Para hacerlo más fácil, las curvas deben ser *horneadas*(creadas) en puntos equidistantes. De esta manera, pueden ser aproximadas con una interpolación regular (que puede ser mejorada aún más con una opción cúbica). Para ello, sólo hay que utilizar el método Curve.interpolate_baked() junto con Curve2D.get_baked_length(). La primera llamada a cualquiera de ellos creará la curva internamente.

Travesía a velocidad constante, entonces, puede hacerse con el siguiente pseudo-código:

var t = 0.0

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

Y la salida, entonces, se moverá a velocidad constante:

../../_images/bezier_interpolation_baked.gif