Antes de ir a las entradas de Dreamcast y PlayStation 2 en lo que a Gráficos Retro se refiere me gustaria hablar del World Space Pipeline o más bien de la parte encargada de calcular la geometria de la escena antes de rasterizarla, algo que tanto en PlayStation 2 como en Dreamcast se realiza en unidades programables, ya sea el propio SH4 en el caso de Dreamcast como una o ambas VU en el caso de PlayStation 2 pero todas parten de los mismos conceptos matemáticos.

#1 Aplicación

La primera etapa del pipeline es llevada a cabo siempre por la CPU y una lista de dibujado, es decir, realizar una lista para que el elemento que calcule la geometria sepa donde se encuentra cada objeto, en el pipeline gráfico no se calcula el movimiento de nada, ni la detección de colisiones, solo creamos la lista de dibujado de un fotograma de la escena que es la representación visual de lo antes calculado por la CPU respecto a la posición de los objetos. Podemos hoy en dia desviar ese trabajo a la GPU utilizando Compute Shader, especialmente las operaciones relacionadas con la física de los objetos pero son funciones que no entran aquí. Lo que simplemente tiene que quedar claro es que cuanto más compleja es una escena en cuanto a elementos más tiempo va a tomar la fase de aplicación en crear sus lista de pantalla, la cual repito siempre es único excepto cuando se dibuja la imagen en estéreo y hemos de tener en cuenta que hay sistemas que generan el estereo de una imagen de forma automática.

#2 Geometry Engine

Se conoce como Geometry Engine a los mecanismos de hardware encargados de calcular la geometria de una escena de manera especializada, ya sean unidades programables o de función fija o simplemente sistemas mixtos. Las unidades programables ejecutan un programa que ha de ser previamente codificado, las unidades de función fija tiene el programa codificado por hardware y lo único que entramos son los valores, en el caso que nos ocupa hablamos de la operaciones matemáticas correspondientes.

Los Geometry Engine trabajan por etapas y son altamente paralelizables porque podemos hacer que cada procesador se encargue de un vertice en concreto, originalmente no era así y se utilizaba un pipeline secuencial para las diferentes etapas para un solo vertice pero con el paso del tiempo se ha ido paralelizando con el tiempo hasta tener cantidades ingentes de unidades trabajando en paralelo y un pipeline cada vez más complejo añadiendo nuevas etapas y característica. En esta entrada ire a lo más básico y no hablare de arquitecturas sino que hablare de los fundamentos matemáticos que son generales pero tampoco voy a realizar una clase de matemáticas, especialmente porque esto lo hice en carrera hace mucho tiempo y de manera teórica y si tuviese que repasarlo para hacer algo en 3D (se que debería) sería una tarea titánica en estos momentos y para vosotros sería sumamente aburrido.

Hemos de tener en cuenta que lo vertices son representados como vectores en la tres dimensiones del espacio debido a que tenemos 3 coordenadas en el el espacio (anchura, altura y profundidad) que se representan como  (x,y,z) pero resulta que en los gráficos en 3D utilizamos una cuarta coordenada llamada w (x,y,z,w) y dicha coordenada es esencial para poder representar una escena en 3D en una pantalla en 2D. Dado que w es lo que nos permite crear la perspectiva, el motivo de ello es que en el sistema tridimensional cartesiano con solo las 3 coordenadas  dos lineas en paralelo no pueden interseccionar la una con la otra. Por lo tanto no la podemos utilizar para crear una visión en perspectiva. A esto se le llama coordenadas homogeneas y gracias a su existencia podemos proyectar una escena 3D en una pantalla en 2D.

Es por ello que todos los Geometry Engine sean del tipo que sean trabajan con 4 componentes al mismo tiempo sobre el que se le aplica la misma instrucción, por eso se utilizan unidades vectoriales, más conocidas como SIMD (una sola instrucción ejecutandose en varios operandos al mismo tiempo).

La forma más conocida es la unidad SIMD de 4 componentes pero podemos tener formas multiples de 8 y 16 ALUs por SIMD. El otro elemento esencial en todo Geometry Engine es la instrucción MADD o FMADD (en este ultimo casos si se hace en coma flotante) aunque a veces es conocida como MAC oFMAC también, basicamente se trata de la siguiente operación básica:

(a+b)*c

Decimos que un sistema soporta FMADD/MADD/MAC/FMADD cuando es capaz de hacer dicha instrucción en un solo ciclo y por tanto puede realizar dos operaciones por ciclo.

#3 Vectores y Matrices

La primera etapa o mejor dicho las primeras etapas en el pipeline de la geometria son las transformaciones de coordenadas donde tenemos que transformar las coordenadas de los objetos a renderizar en la escena de un espacio a otro y para ello se hacen multiplicando un vector por una matriz de 4×4 en cada una de las etapas donde el resultado es otro vector.

Fijaos en la cantidad de sumas y multiplicaciones combinadas para generar el vector transformado que nos va a permitir trasladar el objeto primero a las coordenadas cartesianas del mundo renderizado y luego después según la posición de la cámara (ojos) para terminar con la matriz de proyección, en total tenemos unas 16 multiplicaciones y unas 12 sumas, unas 28 operaciones en total.

Las transformaciones básicas en el pipeline en toda escena son las siguientes y se hacen siempre en el orden especificado en el diagrama:

¿Y como sabemos el contenido de las matrices? Por suerte siempre son las mismas, esto permite codificarlas por hardware en sistemas de función fija donde solo tenemos que entrar los datos de lo operandos del vertice a transformar, en otros sistemas es necesario introducir los valores de la matriz manualmente aunque no es problema desde el momento en que se pueden aplicar como una constante a la que ir invocando en el código.

Los objetos almacenados en memoria tienen unas coordenadas respecto a si mismos por lo que dichos valores se han de transformar a uno valores comunes para todos lo objetos del mundo. Como vemos la escena en 3D desde la posición de cámara tenemos que transformar dichas coordenadas respecto a los ojos/cámara pero no es suficiente sino que la tenemos que proyectar sobre una superficie 2D.

¿Es la Projection Matrix la etapa de rasterizado? No, es la que coloca todos los vertices en un espacio tridimensional ordenado para que luego la unidad de rasterizado que ya se encuentra más alla de la etapa geometrica del pipeline haga su trabajo. No obstante la matriz de proyección tiene un problema… Después de realizar la multiplicación por la Projection Matrix, la coordenadas resultantes son divididas cada una por el propio componente W de la matriz. Con esta operación los objetos que están más alejados del origen tienen sus sus coordenadas x e y escaladas según la distancia y se hacen más pequeñas según la lejania respecto a la cámara, si no se hiciese esta operación entonces lo que veriamos sería una serie de objetos en orden sin tener en cuenta su tamaño respecto a la lejania. 

#4 Clipping/Culling

El paso anterior al rasterizado y posterior a la aplicación de la matrix de proyeccion es lo que conocemos como Clipping, el Clipping no es más que el descarte de los vertices no visibles por el jugador por el hecho de estar ocluidos por otros o simplemente están más allá de la frontera. El algoritmo utilizado en el 99.9% de las veces es el llamado algoritmo Sutherland-Hodgman que basicamente consiste en que recortamos los vertices no visibles en los 6 planos del espacio que tenemos en este momento.

¿6 planos?

Pues si, 6 planos en el espacio.

En realidad no es más que una simple operación por cada uno de los 6 planos por lo que son unas 6 operaciones por vertice. Dado que es una operación general en todos los pipelines 3D como la división por w que he comentado antes muchos procesadores gráficos actuale realizan esta parte con una unidad fija especializada en el que el programador no ha de hacer nada porque el vector a operar es previamente conocido y por tanto se puede automatizar el proceso, aparte que si no se realizará el Clipping estaríamos enviamos de manera absurda toda la geometria no visible en la escena. 

En algunos sistemas contemporaneos se realiza un proceso de Clipping llamado Pre-Culling antes del renderizado convencional con tal de eliminar de la lista de vertices los objetos demasiado pequeños, fuera de la frontera o simplemente que están ocluidos por otros. ¿Que sentido tiene hacer esto? Para entenderlo tenemos que ir al siguiente punto (el #5)

#4 Clipping en el Tile Rendering (Addendum)

En el caso del Tile Rendering antes de realizar el Clipping lo que se hace es ordenar la geometria en memoria según su posición en pantalla y se crean «listas de geometria» por cada tile de esta…

El Clipping a nivel del Tile Rendering no tiene en cuenta la pantalla al completo sino todo el tile, en realidad las etapas posteriores las realizará como si el area del Tile fuese la pantalla y rasterizara y texturizara a nivel de Tile donde el bufer de imagen es lo suficientemente pequeño como para ser procesado en una memoria interna. Los Tile Renderers siempre requieren memoria adicional para almacenar la geometria utilizando la memoria del rasterizador para ello.

Habitualmente en los Tile Renderers lo que se hace es aplicar el Clipping por hardware para eliminar superficies ocluidas por el hecho que al ser regiones pequeñas es fácil aplicarlo a mucha velocidad. La eliminación de superficies ocluidas elimina el overdraw pero es solo recomendable cuando el coste de la eliminación es menor que el coste del rasterizado+texturizado del fragmento resultante.

#5 Etapas Adicionales

Hasta ahora he explicado la geometria de la escena de la forma más simple posible pero hay una serie de etapas adicionales que tienen que ver con la aplicación de ciertas funciones gráficas más complejas.

Normal Matrix: Se trata de una matriz de 3×3 componentes que es indispensable para ciertas operaciones con lo Vertex Shaders o la iluminación del vertice. Esto se traduce en 9 multiplicaciones y 6 sumas (15 operaciones).

Normalización de un vector: Es la segunda etapa necesaria para poder realizar la iluminación del vertice o la aplicación de los Vertex Shaders, consiste en unas 8 operaciones (sumas y multiplicaciones) por vertice.

Vertex Lighting: Conocida también como Gouraud Shading, se basa en aplicar un color en cada vertice, actualmente no se utiliza como antaño porque la iluminación se aplica a nivel de vertice y dicha etapa ha sido reemplazada por los Vertex Shaders. Pero en los sistemas primitivos era una forma de calcular la iluminación de la escena teniendo en cuenta una sola luz (sin perdida de intensidad) y generando la luz respecto a un solo punto de vista (el de la cámara) lo cual sirve para simular parcialmente lo que es el impacto de la fuente de luz sobre el vertice.

El calculo general son: 12  multiplicaciones, 10 sumas, 5 comparaciones (restas que no son más que sumas inversas) y la comprobación en una tabla y esto hacerse por cada componente de color (RGB) por lo que tenemos que para calcular el Vertex Lighting de una sola fuente de luz por objeto unas 28 operaciones por componente y en total unas 84 operaciones por vertice, siendo esta la sección que más procesamiento requiere en total. 

El color de cada vertice del poligono es interpolado (puede ser aditivamente o multiplicativamente pero tiene más precisión el metodo multiplicativo) durante el texturizado o durante la fase de rasterizado (en el caso de no haber texturas) con el color de cada pixel dentro del poligono, pero dicha etapa esta fuera de la etapa de la geometria y no forma parte de la etapa de rasterizado, esto tiene dos efecto, el primero de ellos es que elimina la visión de las aristas (Gouraud Shading)…

… y la segunda de ellas es que es el tipo de iluminación que se ha utilizado en juegos hasta la llegada de la primera Xbox donde se empezó a utilizar la iluminación a nivel de pixel y esta fue trasladada al Screen Space Pipeline.

Hay que tener en cuenta que en el caso del Vertex Ligthing es considerado el color de la luz del vertice como un componente de textura más a efectos practicos, por lo que entra dentro del limite de las 8 texturas por fragmento en el OpenGL, es decir, solo podemos aplicar en las escenas fabricadas en APIs «basadas» en el OpenGL original (Glide, Direct3D hasta la versión 7) una combinación de 8 luces y 8 texturas por triangulo.

Vertex Shader: Un Vertex Shader no es má que un programa que nos permite manipular los 4 componentes de un vector, dado que la longitud de los Vertex Shader y la cantidad de ellos depende de cada juego y de cada escena desde que aparecieron es imposible calcular la carga computacional necesaria en un hardware en la etapa de la geometria por lo que los desarrolladores no pueden saber el rendimiento de un juego en lo sistemas gráficos contemporaneos hasta que no lo prueban y hacen diversas pruebas con diferentes algoritmos de mayor o menor precisión y mayor o menor velocidad para obtener el resultado esperado. 

Todas la etapas previamente descritas se realizan antes del Clipping, es por eso que el Pre-Culling no son calculadas y dado su peso computacional el hecho de descartarlas de antemano es esencial con tal de no sobrecargar al sistema.

Teselación: La teselación no es más que la subdivisión de un vector por 2 o varios vectores pero cuya longitud total no aumenta, esto permite «redondear» los gráficos. El algoritmo más utilizado es el Catmull Clark y se ha de tener en cuenta que el resultado es aumentar enormemente la cantidad de triangulos/poligonos en la escena por lo que luego se van a tener que rasterizar y texturizar más. 

La teselación fue introducida en PC por primera vez por ATI con una unidad de función fija en sus primeras Radeon, a la que llamaron Truform, dicha unidad no obstante no teselaba de la forma que se tesela hoy en dia (después del Vertex Shader) sino que lo hacía antes del Vertex Shader, dicha unidad fue añadida en la GPU de Xbox 360 (Xenos) y se suele confundir con un Geometry Shader.

En el caso de las consolas anteriores la teselación es llevada a nivel de CPU, por ejemplo Wind Waker de Gamecube utiliza teselación via CPU para representar las olas del agua del mar aunque hay más ejemplos de ello:

Geometry Shader (DX10), Domain Shader y Hull Shader (DX11 en adelante): En DX10 Microsoft implemente el Geometry Shader, cuyo trabajo principal es la teselación de la escena de manera controlada por el desarrollador y también para otras funciones basadas en crear geometria adicional en la etapa final del calculo de la geometria de la escena. ¿Que cosas podemos hacer con el Geometry Shader?

  • Obviamente teselación.
  • Se puede utilizar para añadir o quitar vertices de un objeto o incluso eliminar objetos por completo de la escena.
  • Nos permite crear vertices unitarios de manera directa sin tener que calculalos previamente (particulas)
  • Se utiliza para aplicar el Displacemente Mapping para obtener un modelado geometrico más complejo a partir de uno más simple, esto significa que los Geometry Shaders al igual que los Pixel/Fragment Shaders tienen acceso a la memoria. Es sobretodo utilizado para generar la orografia del terreno pero en algunos casos se utiliza para generar modelados más complejos

  • Podemos generar una visión estereo de la geometria de la escena a través del Geometry Shader para no tener que calcular la geometria de la segunda cámara.
  • Podemos utilizar instancias, basadas en replicar un mismo objeto muchas veces en pantalla, esto es ideal para escenas con muchos elemento iguales en pantalla, solo tenemos que animar uno e irlo repitiendo por el escenario. Obviamente cada uno de lo elementos instanciados va a ser rasterizado y texturizado. Por ejemplo como es muy dificil animar la hierba una por una lo que se hace es animar una o un conjunto pequeño y se instancia durante esta etapa.

#6 Triangulos vs Cuadrangulos

Pese a que hablamos de triangulos el caso que nos ocupa también es aplicado a los cuadrangulos, en realidad los primeros sistemas 3D se basaron en cuadrangulos porque con estos es matematicamente más simple aplicar el rasterizado pero a medida que los sistemas se hicieron más complejos se acabo aplicando el rasterizado de triangulos. Pero la etapa de rasterizado no forma parte de la etapa geométrica sino que es es posterior y no la voy a relatar aquí.

#7 ¿Pero cuantos poligonos dices que calcula x maquina?

Aqui es donde entramos en romper un mito para mucha gente, cuando hablamos que un sistema puede realizar una enorme cantidad de transformaciones estamos hablando de una simple operación de un vector por una matriz y para colmo con los siguientes parametros:

  • Es el mismo vector todo el rato de manera recursiva.
  • No es la aplicación de todo el pipeline geométrico.
  • La tasa es una tasa de velocidad con el tiempo utilizando el 100% del tiempo.
  • Los operandos en dichos benchmark se suelen almacenar en la memoria más cercana al procesador y la que menos latencia tiene. En los actuales shaders como operan a nivel de registro siempre operan en la memoria más rápida posible para ellos, pero cuando se hacía a nivel de CPU se tenia que tener en cuenta el uso de caches para el rendimiento.

#8 El gran cuello de botella

Antes los motores encargados de la geometria y encargados de la rasterización estaban separados por buses extremadamente estrechos, esto significa que la cantidad de geometria que podían pasar para rasterizar era limitada realmente. Esto era sumamente sonado por ejemplo en el caso de las primeras aceleradoras 3D que utilizaban el puerto PCI con un ancho de banda de solo 66MB/s… Lo cual no era mucho más rapido que el bus que tenía el R3000A de PSX con el GTE calculando la geometria y la GPU (rasterizador de la consola).

La primera consola en solventar este problema fue la Dreamcast con un bus de 800MB/s entre la CPU y el rasterizador (lo ampliare en la entrada de Dreamcast) y luego en PlayStation 2 con el bus GIF entre el EE y el GS de 1.2 GB/s… ¿Pero que ocurría cuando el bus no era lo suficientemente ancho? Pues que se almacenaba la geometria calculada en una memoria asignada para luego irla transmitiendo. ¿Un ejemplo de esto? Por ejemplo en el Model 3 de Sega la comunicación entre la geometria y el rasterizado se hacia con un bus SCSI-II de 20MB/s lo que hacia imposible la transmisión de datos de manera directa y se tenian que almacenar en una RAM temporal, bueno, en todos los sistemas 3D de Sega y Namco de mediados de los 90 ocurría esto, por eso eran tan caros porque tenian complejas placas con memoria para almacenar la geometria.

El caso es que a nivel de PC con la aparición de la NV10/Geforce 256 y la integración del Geometry Engine y el Rasterizador en un solo chip el problema se solvento por completo y desparecio el cuello de botella.

Y con esto termino esta entrada, espero que hayáis aprendido algo y os haya sido amena, ya sabéis que teneís el canal de Discord del blog y los comentarios de esta misma entrada.