Este artículo detalla el proceso de construcción del algoritmo encargado de realizar el barrido con rayos del plano de proyección en un sistema básico de Ray Tracing. Indicando, uno a uno, qué parámetros de entrada son necesarios, y qué valores se calculan a partir de ellos. Siendo el objetivo final generar un rayo por cada pixel. Rayos que partan de la posición del observador, y pasen por el centro de cada uno de los pixeles del plano de proyección.

El plano de proyección es la superficie sobre la que representa la imagen bidimensional generada a partir de la escena definida tridimensionalmente. En la práctica el plano tiene una extensión continua ilimitada, pero de él sólo se toma una pequeña región, normalmente rectángular, a modo de ventana. Esta ventana se divide a su vez en una cuadrícula rectángular de filas y columnas en la que cada celda representa un pixel al que puede asignársele un color individualmente.

Este artículo trata de cómo se define dicho plano de proyección, de cómo se calcula la posición de cada pixel dentro del mismo, y de cómo se pueden construir rayos que los recorran (“barran”).

Sistema de Referencia

En lo sucesivo consideraremos un sistema de referencia en el que el eje horizontal X crece de izquierda a derecha, el vertical Y de abajo hacia arriba, y los valores sobre el eje de profundidad Z aumentan a medida que se penetra en el plano XY (adentrándose en la pantalla del monitor, si se toma este como referencia).

Dirección en la que mira el Observador

Los primeros parámetros de entrada al sistema a considerar son la posición que ocupa el observador y el punto concreto al que mira. Normalmente el observador de la escena recibe el nombre de “cámara”, pero por el momento seguiré llamándolo observador. Creo que el término cámara tiene unas connotaciones técnicas implícitas, como puede ser el uso de lentes, que no resultan adecuadas introducir aún. Me parece más natural considerar un observador humano.

Denotaremos con EYE a la posición del observador, y LOOKAT al punto concreto que mira.

Sistema de Referencia

El primer valor calculado será la dirección VIEW en la que mira el observador, que es la dirección del vector que une EYE con LOOKAT:

VIEW = (LOOKATEYE) / |LOOKATEYE|

La mayoría de los vectores que se calculen se normalizarán para simplificar cálculos posteriores. Es decir, se dividirán por su módulo (longitud) para convertirlos en vectores unitarios. Los vectores unitarios tienen módulo unidad, lo que interesa de ellos es su dirección y sentido.

Los parámetros EYE y LOOKAT se pueden inicializar con valores por defecto evitando tener que pedir al usuario que los introduzca. Por ejemplo, EYE = (0, 0, 0) y LOOKAT = (1, 1, 1) situan al observador en el origen de coordenadas mirando a un punto situado arriba a su derecha. Aunque lo normal es que se obligue a introducir al menos uno de estos dos valores.

En ningún caso EYE y LOOKAT deben ser iguales, ya que ello equivaldría a decir que el observador observa la misma posición que ocupa, lo que es físicamente imposible. Y matemáticamente equivaldría a que la longitud de VIEW fuese cero, o sea, un punto en vez de un vector, no se podría realizar la división para normalizarlo.

Arriba y Abajo

El siguiente parámetro de entrada requiere una explicación un poco más detallada. Se trata de indicar cual es el plano vertical que considera el observador.

Intentaré explicarlo con un ejemplo. Supongamos que quiere hacer una fotografía. Se ubica, apunta al objetivo, y retrocede o avanza para captar mejor el motivo a fotografiar. Sin embargo, a veces no es posible incluir todo lo observable en la fotografía, no cabe. Como cuando se retrata a una persona de pie. Si se apunta con la cámara en su postura natural las piernas salen “cortadas”. Por lo que se utiliza el truco de girar la cámara y ponerla en posición vertical para que la persona salga completa, a lo ancho de la fotografía en vez de a lo alto. Lo que estaba “arriba” ahora está en un lateral.

El observador de una escena, al igual que un fotógrafo, debe especificar que parte de la escena debe quedar “arriba”, para que no exista ningún tipo de ambigüedad con respecto al resultado buscado. Para ello se define otro parámetro de entrada al sistema, el vector unitario UP que apunta hacia “arriba”.

El vector UP suele tomar el valor (0, 1, 0) por defecto, es decir, apuntando hacia el “cielo”. Y como se verá en el siguiente apartado, su dirección no tiene porque ser perpendicular (formar un ángulo recto) a la dirección en la que mira el observador.

Ejes sobre el Plano de Proyección

El plano de proyección contiene la ventana sobre la que se representa la escena. Y para definir un plano matemáticamente se necesita al menos un punto sobre el mismo y un vector normal a la superficie. En la práctica existen más formas de definir un plano, pero esta forma es la más conveniente para este caso, ya que se dispone del vector VIEW, normal (perpendicular) al mismo, y el punto sobre el plano se calculará muy fácilmente introduciendo la distancia dis que separa al observador del plano.

La distancia dis es un parámetro de entrada al sistema. Suele asignársele el valor 1 por defecto. Y en todo caso debe ser un valor positivo mayor que cero, ya que un valor de cero querría decir que el observador se encuentra contenido en el propio plano de proyección, y un valor negativo que el plano de proyección se encuentra por detrás de la dirección en la que se mira.

Plano de Proyección

El punto sobre el plano, CENTER, se obtendrá partiendo del observador y siguiendo la dirección de visión una distancia dis:

CENTER = EYE + (dis * VIEW)

Este punto CENTER se utilizará como origen de un eje de coordenadas locales sobre la ventana en la que se proyecta la escena, es decir, un eje centrado en ella que será, a la postre, el que permita moverse por ella recorriendo todos los pixels que la forman.

Los ejes de coordenadas se definen matemáticamente con un punto origen, que ya se tiene (CENTER), y tres vectores, del que también se tiene uno (VIEW). El vector VIEW permite moverse en profundidad con respecto al plano de proyección, y el eje de coordenadas se completará añadiendo dos nuevos vectores, HORZ y VERT, que permitan moverse horizontal y verticalmente.

El vector HORZ es normal al plano que contiene a los vectores unitarios UP y VIEW, por lo que su cálculo es sencillo. Se obtiene del producto cruzado (vectorial) de UP por VIEW:

HORZ = (UP x VIEW) / |UP x VIEW|

Se tiene que tener en cuenta que UP y VIEW no pueden tener la misma dirección, ya que HORZ no podría calcularse. Este caso ocurre cuando se indica que la dirección en la que mira el observador es la misma que la dirección indicada para “arriba”. Un sistema así definido no es coherente.

Por su parte, el vector VERT es normal al plano que contiene a los vectores unitarios VIEW y HORZ. Por lo que puede obtenerse del producto cruzado de ambos vectores:

VERT = (VIEW x HORZ) / |VIEW x HORZ|

La explicación de qué consiste el producto vectorial queda fuera del alcance de este artículo, puede encontrarse en cualquier tutorial básico de cálculo de vectores. Baste decir que dados dos vectores A = (ax, ay, az) y B = (bx, by, bz), el producto vectorial es el vector resultante de evaluar la siguiente expresión:

A x B = (ay * bzaz * by, az * bxax * bz, ax * byay * bx)

Tamaño de la Ventana de Proyección

El tamaño de la ventana de proyección es el tamaño del rectángulo en el que se impresiona la escena observada. Para su cálculo necesitamos introducir dos nuevos parámetros de entrada, dos ángulos. El ángulo de visión del observador en el plano horizontal hfov, y el ángulo de visión del observador en el plano vertical vfov. La nomenclatura de los nombres procede del inglés “Field Of View” (Campo de Visión).

Estos ángulos determinan el ancho y alto de la ventana de proyección en la medida que las partes visibles de la escena serán sólo aquellas que se encuentren dentro de estos ángulos de visión.

Para calcular el ancho de la ventana de proyección se debe notar que el observador y el centro de la ventana se encuentran ambos contenidos dentro del plano que definen VIEW y HORZ, por lo que el cálculo se puede reducir a un problema de trigonometría 2D sobre este plano:

Tamaño de la Ventana de Proyección

Resolviendo el triángulo superior de la imagen se obtiene el ancho del campo de visión:

TAN(hfov / 2) = (hsize / 2) / dis
hsize = 2 * dis * TAN(hfov / 2)

Y siguiendo el mismo razonamiento se obtiene la altura del campo de visión:

vsize = 2 * dis * TAN(vfov / 2)

Los dos ángulos pueden tomar un mismo valor de entrada por defecto, unos 45 grados, lo que haría que se genere una ventana de proyección cuadrada. Algunos sistemas prefieren que se indique el aspect ratio (cociente entre el ancho y alto) de la imagen deseada, y calcular los ángulos a partir de esta cantidad.

Tamaño y Posición de un Pixel

El último parámetro de entrada al sistema es la resolución (tamaño) en pixels que se quiere que tenga la imagen resultante, width x height. Con la resolución se determina el número de pixels totales y el tamaño de cada pixel individual en la ventana de proyección.

El tamaño horizontal de un pixel en la ventana de proyección se calcula diviendo el tamaño horizontal de la ventana de proyección por el ancho en pixels de la imagen deseada:

hpixel = hsize / width

Y de forma análoga se obtiene el tamaño vertical de un pixel:

vpixel = vsize / height

Como parámetro de entrada, la resolución puede tomar un valor por defecto, por ejemplo 100×100, o se puede obligar a introducirla obligatoriamente. En cualquier caso, tanto la resolución vertical como la horizontal deben ser mayores que cero. Sin fueran cero no se podría realizar la división para obtener el tamaño de un pixel, sería como decir que los pixels no tienen dimensiones.

Origen de la Ventana de Proyección

De entre todos los pixels interesa destacar la posición del que se encuentra en la esquina superior izquierda de la ventana de proyección, porque será el primero en visitarse. Su ubicación se obtiene partiendo del centro de la ventana y desplazándose hacia la izquierda y hacia arriba una distancia equivalente a la mitad de la resolución deseada:

ORIGIN = CENTER – ( ( (width – 1) / 2) * hpixel * HORZ) + ( ( (height – 1) / 2) * vpixel * VERT)

Y este mismo criterio se puede aplicar al resto de pixels con el objetivo de obtener una expresión más genérica que obtenga el centro de cualquier pixel (i, j) dentro de la ventana de proyección:

PIXEL (i, j) = ORIGIN + (i * hpixel * HORZ) – (j * vpixel * VERT)

Notar que en esta última expresión la dirección de avance vertical está invertida, debido a que se parte de ORIGIN, y que la numeración vertical habitual de los pixels crece de arriba hacia abajo. El pixel (0, 0) está localizado en la esquina superior izquierda y el pixel (width – 1, height – 1) en la esquina inferior derecha.

Generación de Rayos

Una vez definido completamente el sistema, se puede proceder al barrido de la ventana de proyección con rayos, mediante dos bucles que recorran los pixels de izquierda a derecha y de arriba hacia abajo, generando para cada pixel (i, j) un rayo que parta de la posición del observador y atraviese el centro del pixel:

FOR j = 0 TO height – 1

FOR i = 0 TO width – 1

RAY (i, j, t) = EYE + ( (PIXEL(i, j) – EYE) / |PIXEL (i, j) – EYE| ) * t

NEXT i

NEXT j

Definidos de esta forma, los rayos no son más que vectores con un origen, el punto EYE, y una dirección, la que une el origen con el centro del pixel.

El parámetro t de la ecuación del rayo nos permite movernos sobre la línea recta imaginaria que define el mismo. Un valor cero nos sitúa en el origen, un valor negativo por detrás de este y uno positivo por delante. Este parámetro t será además el que se utilice para el cálculo de intersecciones con los objetos de la escena. Tema del siguiente artículo de esta serie.

Resumen

Parámetros de Entrada:

EYE: Posición del Observador
LOOKAT: Punto concreto al que mira el Observador
UP: Dirección para determinar el Plano Vertical del Observador
dis: Distancia del Observador al Plano de Proyección
hfov: Ángulo del Campo de Visión Horizontal
vfov: Ángulo del Campo de Visión Vertical
width: Resolución Horizontal en pixels de la Imagen deseada
height: Resolución Vertical en pixels de la Imagen deseada

Valores Calculados:

VIEW: Dirección en la que mira el Observador
CENTER: Posición del centro de la Ventana de Proyección
HORZ: Vector de desplazamiento Horizontal sobre la Ventana de Proyección
VERT: Vector de desplazamiento Vertical sobre la Ventana de Proyección
hsize: Tamaño Horizontal de la Ventana de Proyección
vsize: Tamaño Vertical de la Ventana de Proyección
hpixel: Tamaño Horizontal de un pixel en la Ventana de Proyección
vpixel: Tamaño Vertical de un pixel en la Ventana de Proyección
ORIGIN: Posición del centro del pixel de la esquina superior izquierda de la Ventana de Proyección
PIXEL(i, j): Posición del centro del pixel (i, j) en la Ventana de Proyección
RAY(i, j, t): Ecuación del rayo que parte del Observador y atraviesa el centro del pixel (i, j)

Proceso:

1. Cálculo de la Dirección en la que mira el Observador:

VIEW = (LOOKATEYE) / |LOOKATEYE|

2. Obtención del Sistema de Referencia sobre el Plano de Proyección:

CENTER = EYE + (dis * VIEW)
HORZ = (UP x VIEW) / |UP x VIEW|
VERT = (VIEW x HORZ) / |VIEW x HORZ|

3. Cálculo del Tamaño de la Ventana de Proyección:

hsize = 2 * dis * TAN(hfov / 2)
vsize = 2 * dis * TAN(vfov / 2)

4. Cálculo del Tamaño de los pixels en la Ventana de Proyección:

hpixel = hsize / width
vpixel = vsize / height

5. Obtención del centro del pixel de la esquina superior izquierda de la Ventana de Proyección:

ORIGIN = CENTER – ( ( (width – 1) / 2) * hpixel * HORZ) + ( ( (height – 1) / 2) * vpixel * VERT)

6. FOR j = 0 TO height – 1

7. FOR i = 0 TO width – 1

8. Generación del rayo que atraviesa el centro del pixel (i, j):

PIXEL(i, j) = ORIGIN + (i * hpixel * HORZ) – (j * vpixel * VERT)
RAY(i, j, t) = EYE + ( (PIXEL(i, j) – EYE) / |PIXEL(i, j) – EYE| ) * t

NEXT i

NEXT j