
En este post continuaremos la série para desarrollar un motor de renderizado desde 0 utilizando C++. Nuestra misión a lo largo del post será la de rellenar los triángulos generados anteriormente, produciendo superficies 2D sobre las que podremos aplicar colores o texturas.
Contenidos
Primera aproximación realizando un barrido de líneas
Nuestra siguiente tarea será la de rellenar los triángulos dibujados anteriormente, generar superficies 2D a partir de los puntos. Vamos a realizar una aproximación básica que iremos optimizando con el desarrollo del post.
Implementación para dibujar un triángulo
Empezaremos con una implementación simple del triangulo aprovechando la función para dibujar una línea que tenemos del post anterior.
Que podremos utilizar de la siguiente forma para dibujar triángulos a partir de las aristas (lados):
Obteniendo un resultado como el siguiente:
Implementación para rellenar un triángulo – Superficie 2D
Ahora tenemos que implementar una función que rellene los triángulos. Vamos a realizar un barrido teniendo en cuenta lo siguiente:
- No depende del orden en el que pasemos los vértices o aristas del triángulo.
- Ordenaremos los vertices de los triángulos (con BubbleSort) en función del componente Y.
- Trazaremos una línea horizontal que separe los lados derecho e izquierdo del triángulo.
- Rasterizaremos los píxeles de los lados derecho e izquierdo del triángulo simultáneamente.
De esta forma, partimos el triángulo en dos partes:
- La parte A (roja) formada por la hipotenusa $t_0$ a $t_2$
- La parte B (verde) formada por los catetos $t_0$ a $t_1$
Y obtenemos un resultado como este
Como veis la parte verde, la parte B, está formada por dos aristas (lados). Vamos a dividir el triangulo con una línea horizontal que pase justo por el vértice común entre esos dos lados. Lo entenderéis mejor con la siguiente imagen que rastericemos.
En este caso rasterizamos medio triángulo en función de la $Y$ desde $t_0$ a $t_1$. Tendremos algunos problemas con las divisiones por 0. Estos problemas ya los solventamos en el post para dibujar las líneas correctamente. Como veremos en el siguiente paso, no tendremos problema ya que vamos a rellenar las áreas y no nos interesan tanto las lineas de las aristas (lados).
Como veis, hemos partido los triángulos de forma horizontal en dos partes.
Ahora tendremos que rasterizar también el otro lado. Lo haremos simplemente duplicando el loop para los valores de Y entre $t_1$ a $t_2$. Además en este caso dibujaremos todos los pixeles entre ambos lados para ir rellenando el triángulo.
Ahora nuestros triángulos ya pintan un poco mejor y podemos aplicarles un color. Vamos a simplificar la función anterior juntando los dos bucles en uno para que sea más fácil modificar el código posteriormente.
Sin embargo, si queremos optimizar nuestro Rasterizado de las superficies 2D de los triángulos, esta aproximación solo utiliza 1 hilo de ejecución y podría paralelizarse en 2 ejecutando los dos bucles de la implementación anterior paralelamente.
Sin embargo, tendremos que pensar en una aproximación alternativa que se pueda paralelizar fácilmente en una gran cantidad de núcleos apropiado para las tarjetas gráficas actuales.
Aproximación paralelizable a la rasterización de triángulos
Si ponemos nuestra cabeza en modo paralelizable el pseudo-código anterior parece bastante viable. Si pensamos en una tarjeta gráfica que puede tener cientos o miles de hilos de ejecución paralelos, comprobar si cada pixel de la imagen pertenece o no a una figura es una tarea muy rápida y sencilla.
Coordenadas Baricéntricas para rasterizar superficies 2D de Triángulos
Las Coordenadas Baricéntricas de un triangulo son 3 números reales $\alpha, \beta, \gamma \in [0,1] $ tales que $\alpha + \beta + \gamma = 1$ que permiten parametrizar el interior de un triángulo.
Se considera un triángulo en el plano de vértices $A = (x_A, y_A)$, $B = (x_B, y_B)$ y $C = (x_C, y_C)$. Entonces, cualquier punto del interior del triángulo puede representarse por 3 coordenadas baricéntricas $\alpha, \beta, \gamma$.
La relación entre las coordenadas cartesianas y las baricéntricas es la siguiente:
$$x = \alpha x_A + \beta x_B + \delta x_C$$
$$y = \alpha y_A + \beta y_B + \delta y_C$$
Ahora, volviendo a nuestro caso particular en el que trabajamos con vectores, dado un triangulo $ABC$ y un punto $P$ tendremos que encontrar las coordenadas baricéntricas de $P$ respecto al triángulo $ABC$.
Buscamos tres valores $(1-u-v, u, v)$ de forma que podamos encontrar el punto $P = (1-u-v)A + uB + vC$.
O si lo planteamos desde otro punto de vista, el punto $P$ tendrá las coordenadas $(u, v)$ en la base oblicua $(A, \vec{AB}, \vec{AC})$, de forma que $$P = A + u \vec{AB} + v \vec{AC}$$
Entonces, el problema se reduce a encontrar los valores $u, v$ tales que $$u \vec{AB} + v \vec{AC} + \vec{PA} = 0$$
Ahora tenemos que resolver esta ecuación vectorial, o un sistema de 2 ecuaciones con 2 incógnitas.
$$\left.
u \vec{AB_x} + v \vec{AC_x} + \vec{PA_x} = 0 \atop
u \vec{AB_y} + v \vec{AC_y} + \vec{PA_y} = 0
\right\}$$
Que podemos pasar a su forma matricial:
$$\left.
\begin{bmatrix}
u & v & 1
\end{bmatrix}
\begin{bmatrix}
\vec{AB_x} \\
\vec{AC_x} \\
\vec{PA_x}
\end{bmatrix} = 0
\atop
\begin{bmatrix}
u & v & 1
\end{bmatrix}
\begin{bmatrix}
\vec{AB_y} \\
\vec{AC_y} \\
\vec{PA_y}
\end{bmatrix} = 0
\right\}$$
Por lo tanto estamos buscando un vector $(u, v, 1)$ que sea ortogonal a $(\vec{AB_x}, \vec{AC_x}, \vec{PA_x})$ y $(\vec{AB_y}, \vec{AC_y}, \vec{PA_y})$.
Utilizando el producto vectorial para calcular las coordenadas baricéntricas de un triángulo
El producto vectorial es una operación entre dos vectores que nos permite obtener un vector en la dirección perpendicular al plano que contiene a dichos vectores. Perfecto para resolver nuestro problema para encontrar las coordenadas baricéntricas del triángulo. Estabamos buscando un vector $(u, v, 1)$ que sea ortogonal a $(\vec{AB_x}, \vec{AC_x}, \vec{PA_x})$ y $(\vec{AB_y}, \vec{AC_y}, \vec{PA_y})$.
La nueva función de rasterización de la superficie del triángulo:
- Realiza un barrido de todos los pixeles dentro de un cuadro delimitador generado a partir de un triángulo dado.
- Para cada pixel se calculan sus coordenadas baricéntricas:
- Si tienen algún componente negativo, el píxel se encuentra fuera de la figura.
- Si tienen ningún componente negativo, el píxel se encuentra dentro de la figura (triángulo) y se debe pintar.
Con la función anterior calculamos las coordenadas baricéntricas de un punto $P$ respecto al triángulo.
Utilizaremos la siguiente función auxiliar para calcular el producto vectorial:
Ahora tendremos que calcular el cuadro delimitador alrededor del triángulo. Esto lo haremos con los puntos max y mín equivalentes a la esquina inferior izquierda y superior derecha.
Iteraremos por las tres esquinas (puntos) del triángulo para calcular el min y max equivalentes a la esquina inferior izquierda y superior derecha del cuadro delimitador.
Solo nos queda barrer el cuadro delimitador que hemos generado y comprobar cada uno de sus puntos en coordenadas baricéntricas del triángulo para ver si pertenecen o no.
Y generaremos de nuevo un triángulo con está implementación:
Ahora que ya sabemos como rasterizar un triángulo de forma paralelizable vamos a aplicarlo al modelo 3D que renderizamos en el post anterior.
Renderizado de un modelo 3D con triángulos de colores – Flat Shading
En el post anterior vimos como dibujar los triángulos que forman un modelo 3D. Ahora que hemos aprendido a pintarlos por dentro de forma paralelizable vamos a intentar juntar ambas implementaciones.
- Adaptamos los puntos del modelo a la escala de la imagen que vamos a renderizar.
- Pintamos cada uno de los triángulos con un color aleatorio.
Se puede entender mejor con el siguiente código que utilizaremos para rasterizar todos los triángulos del modelo, uno a uno:
Obteniendo una imagen como la siguiente:
Como vemos, el resultado es algo extraño. Nos falta un elemento fundamental que es la iluminación y las sombras.
Vamos a quitar todos esos colores aleatorios que habíamos generado y aplicaremos una iluminación y sombreado.
Aproximación a un renderizado con iluminación frontal
En este caso nos vamos a apoyar en un par de reglas sobre la iluminación:
- Con un foco lumínico que emite la misma intensidad lumínica, el polígono se ilumina más por los lados que son ortogonales a la dirección de la luz.
- La cara del polígono tendrá 0 iluminación si es totalmente paralela a la dirección de la luz.

La intensidad lumínica que recibe una cara de un polígono es igual al producto escalar del vector iluminación y la normal de la cara de un polígono. En nuestro caso, calcularemos la normal de un triángulo, que es el producto vectorial de sus lados.
En este caso trabajaremos con una rasterización lineal de los colores. Sin embargo, el color (128, 128, 128) no tiene ni la mitad de brillo que (255, 255, 255). Ignoraremos la corrección de gamma y trabajaremos con colores con un brillo incorrecto.
Calcularemos la normal de cada triángulo y la utilizaremos para calcular la intensidad de luz que nos servirá para colorear cada triángulo. Lo entenderemos mas con la implementación:
En la clase Vec3 añadiremos la implementación del producto vectorial y la normalización:
Obteniendo un resultado final como este. Como veis, en la boca y los ojos obtenemos un rasterizado «raro». Esto lo mejoraremos en el siguiente post en el que utilizaremos un buffer-z. En este caso, para acelerar el renderizado, aquellas caras que estén «de espaldas» a la fuente de luz no las rasterizamos. Hacemos esto comprobando si el producto es positivo. Esta técnica se denomina Back-face culling.
Como siempre podeis encontrar la fuente de este código en el GitHub del autor.