El trastero de José Juan Valid XHTML 1.1 Valid CSS! Estilo de página alternativo
Artículo creado en 2008.
Valoración ValoraciónValoraciónValoraciónValoraciónValoraciónValoraciónValoraciónValoraciónValoraciónValoración sobre 5 comentarios.

Cg "C for graphics" (I)

Introducción

Hace bastante tiempo que no hago ningún experimento relacionado con los gráficos en tiempo real. Por supuesto suelo leer con fruición todo lo relacionado con este tema sin embargo, no soy de los que equipan a la última su ordenador. No es que no me guste, simplemente que raramente utilizo mi ordenador para jugar o para renderizar animaciones o en definitiva, cualquier tipo de tarea que requiera una máquina especialmente equipada (buenos procesadores, mucha memoria y/o una tarjeta gráfica a la última). Es por esto que, aunque ya hacía tiempo me había picado la curiosidad de programar directamente las GPU de la tarjeta gráfica, sencillamente, yo no disponía de una con este hardware. Por supuesto lo hubiera podido hacer por software, pero no tiene la misma gracia...

Afortunadamente ahora, la más sencilla de las tarjetas gráficas vienen con al menos una GPU y me he animado a realizar algún tipo de práctica sobre esto.

Breve introducción a las GPU

Esquema de la pipeline de una GPU

El término GPU proviene de Graphics Processing Unit que viene a ser Unidad para el Procesado de Gráficos y aunque comparte muchas de las características de un procesador electrónico (de propósito general) también posee muchas otras características que lo diferencian enormemente y por tanto, no es un procesador "al uso".

Un procesador gráfico implementa por hardware (y por tanto se supone que mucho más rápido) ciertas tareas genéricas que suelen realizar las aplicaciones de gráficos. Por ejemplo, si una aplicación debe trazar una línea en la pantalla, el programador puede implementar algún algoritmo para el trazado de líneas (como el de Bresenham) sin embargo, el tiempo que le tome al procesador trazar la línea (o millones de líneas) puede ser muy alto; si existe otro procesador en el ordenador, específicamente diseñado y optimizado para trazar líneas, cuando el programador necesite trazar una línea puede llamar a ese otro procesador, que hará la tarea (de trazar millones de líneas) muchísimo más rápido; pero es que además, durante ese tiempo, el procesador principal podrá realizar otras tareas.

Hace bastante tiempo (antes que saliera Cg) ya se podían programar determinados elementos en las GPU. Los dos elementos principales que se pueden programar (en cuanto a lo que hablamos en este artículo) son:

Pues bien, estos dos sub-procesadores de la GPU es lo que nos proponemos programar y ver hasta que punto se le puede sacar partido (no sólo para la generación de gráficos).

Procesadores gráficos para propósitos generales

Dada la impresionante potencia que han desarrollado los procesadores gráficos últimamente (debido a lo exigentes que son los adolescentes cuando juegan delante de su televisor con las consolas... bueno no exactamente, la razón es que el mercado de los videojuegos supera al del cine y eso es mucho dinero...) a mucha gente se le ha ocurrido si no se podría utilizar esa capacidad para realizar cálculos que poco o nada tengan que ver con los gráficos y así utilizar ese recurso tan potente.

Existen diversos usos realizados por mucha gente, desde la optimizaciones de inversiones de fondos (es decir, obtener el conjunto de fondos óptimo [bueno, más probablemente óptimo] de entre todos los del mercado), análisis de señales de radio, ordenación, etc... Aquí describo un sencillo ejemplo que creo será muy visual y fácil de entender:

Supongamos que una empresa está interesada en realizar fotografías aéreas de una serie de ciudades por todo el mundo con el fin de realizar callejeros de dichas ciudades. Como alquilar un avión en cada ciudad les costaría mucho dinero y tiempo, se proponen hacer lo siguiente: primero indagarán que aviones sobrevuelan dichas ciudades y luego intentarán convencer a alguna de las azafatas para que haga una foto y se las mande por Internet. De esta forma, en lugar de alquilar un avión, un piloto y un fotógrafo profesional, sólo deben comprar un bonito regalo.

Ellos saben las ciudades que les interesa, sin embargo de los aviones sólo conocen la ciudad de la que salen y la ciudad a la que llegan, pues está en los tablones de anuncios de todas las compañías (las compañías no dicen qué ciudades sobrevuelan). En la hipótesis de que los aviones viajen en línea recta (bueno sí, una geodésica), ¿qué ciudades sobrevuela cada avión?, o mejor, ¿qué aviones sobrevuelan cada ciudad?.

Deciden hacer lo siguiente, pintarán primero un puntito por cada cuidad que les interesa y luego trazarán las líneas de todas las líneas aéreas, aquellas que sobre escriban un puntito, es candidata a hacer foto. Puesto que ellos quieren información de miles de ciudades y existen cientos de miles de vuelos, el trabajo a realizar no es como para hacerlo a mano alzada... y es un trabajo perfecto para una aceleradora gráfica (las aceleradoras gráficas tienen un mecanismo que nos dice sobre que objetos [las ciudades] se ha pintado cuando trazamos una línea [u otra primitiva]).

Perfiles de programación Cg

Ya metidos en harina, lo primero que notamos es que no todos los Cg son iguales y programar de una u otra forma depende de dónde y para qué estemos programando, a eso lo han llamado Cg Language Profiles de los que algunos ejemplos son:

  DirectX 9 OpenGL ARB
Vertex Shader Píxel Shader Vertex Shader Píxel Shader
Runtime Profile CG_PROFILE_VS_2_X
CG_PROFILE_VS_2_0
CG_PROFILE_PS_2_X
CG_PROFILE_PS_2_0
CG_PROFILE_ARBVP1 CG_PROFILE_ARBFP1
Compiler option -profile vs_2_x
-profile vs_2_0
-profile ps_2_x
-profile ps_2_0
-profile arbvp1 -profile arbfp1

Para no aburrir demasiado simplemente decir que cada perfil es válido para según que API gráfica, fabricante, versión, etc... (DirectX, OpenGL, NVidia, etc...) y que tienen algunas diferencias en cuanto a limitación de tamaño de programa, repertorio de instrucciones (Instructions Set) y otros. Por ejemplo, el DirectX 9 vertex shader tiene una limitación de un máximo de 256 instrucciones por programa (no son muchas la verdad) y se tiene acceso a diferentes funciones estándar (como cálculo de un producto de vectores, transpuesta de una matriz, etc...).

Estructura general de un programa Cg

La declaración principal de un programa Cg sería algo así:

<return-type> <program-name>( <parameters> ) [: <semantic-name>]
{
	<instructions>
}

Donde:

Datos de entrada y de salida

Los procesadores programables de la GPU operan con cadenas (streams) de información, es decir la ejecución de nuestro programa se repetirá tantas veces como datos haya en la cadena de procesamiento. Esto sería similar a una cadena de producción de una fábrica en la que una cinta transportadora lleva paquetes, en un punto de la cinta transportadora hay una máquina que baja un sello y marca los paquetes con el sello (arriba, abajo, arriba, ...), pues nuestro programa es el sello y la GPU nos va pasando por debajo los paquetes (la información).

Como ya habrás adivinado, los vertex shader operan con vértices y los píxel shader con colores.

No obstante, se distinguen dos tipologías de datos de entrada a los que se refieren como:

Datos de entrada para los vertex programs

Más concretamente, la información "cambiante" para los vertex programs son lógicamente la información que tenemos para cada vértice y ésta suele ser la posición del vértice (POSITION), la normal de la superficie en ese vértice (NORMAL), la tangente (TANGENT) o la coordenada de la textura (TEXCOORD3).

Nosotros podemos crear nuestra propia forma de representar estos datos que tenemos de entrada (otra forma podría haber sido que fueran constantes para el programa) de forma similar a como se declaran las estructuras en C, un ejemplo bastante clarificador (creo) sería:

struct MiEntrada {
	float3 Posicion : POSITION;
	float3 Normal : NORMAL;
	float3 Tangente : TANGENT;

	float MiFactor : TEXCOORD3;
		// Aquí utilizamos la coordenada de textura
		// para almacenar nuestro propio valor. Desde
		// el programa principal deberemos rellenar en
		// lugar de la coordenada de la textura nuestro
		// factor para el cálculo. (El factor es un valor
		// específico para nuestros cálculos).
};
dato_salida MiProceso( MiEntrada q ) {

	// usamos los datos como queramos:
	... q.Posicion.x ...
	.... q.MiFactor * dot( q.Normal, q.Tangente ) ...
	...

}

POSITION, NORMAL, TANGENT y TEXCOORD3 son sólo algunos de los datos de entrada que tenemos disponibles, a cada uno de estos alias los llaman binding semantics y además de los estándar, cada perfil puede tener otros disponibles.

Otra forma de declarar nuestros propios nombres para acceder a la información en el programa puede ser:

dato_salida MiProceso(
	float3 Posicion : POSITION,
	float3 Normal : NORMAL,
	float3 Tangente : TANGENT,
	float MiFactor : TEXCOORD3
) {

	// usamos los datos como queramos:
	... Posicion.x ...
	.... MiFactor * dot( Normal, Tangente ) ...
	...

}
Datos de salida para los vertex programs

Para indicar la salida es exactamente igual, sólo que en lugar de los parámetros debemos definir ahora el tipo de retorno (allí donde poníamos "dato_salida").

struct DatoSalida {
	float4 Posicion : POSITION;
	float4 Color : COLOR0;
	float4 Coord0 : TEXCOORD0;
	float4 Coord1 : TEXCOORD1;
};
DatoSalida MiVertexShader( ... ) {
	DatoSalida q;

	...
	q.Posicion = ... ;
	q.Color = ...;
	q.Coord0 = ...;
	q.Coord1 = ...;
	...

	return q;
}
Datos de entrada y salida para los fragment programs

Pues igual, sólo tener en cuenta que primero se ejecutan los vertex shaders (en adelante VS) y luego los píxel shaders (en adelante PS) con lo que los datos que modifiquemos en los VS quedarán modificados para ser usados en los PS y sí, es la forma en la que podemos pasar información del VS al PS.

Debe tener en cuenta que algunos binding semantics del VS no estarán disponibles en el PS (como POSITION) sin embargo sí podemos declararlos en la estructura (para que pueda ser la misma).

Para los datos de salida y aunque hay varias alternativas, recomiendan hacerlo indicando los bindings como parámetros de salida de la función, así que sólo pondré esta:


void MiPixelShader( ...,
	out float Color : COLOR,
	out float Zbuf : DEPTH
) {
	...
	Color = ...;
	Zbuf = ...;
	...
}

// La forma corta es:
float4 MiPixelShader( ... ) : COLOR {
	...
	return ...;
}

Aspectos concretos de un programa Cg y su implementación

Bien, la verdad es que ya está mucho más claro en que consiste la programación en Cg, sus capacidades y limitaciones así como los pasos y esquemas generales a tener en cuenta. Ahora queda concretar temas...

Tipos de datos básicos

Sin más los resumimos:

Tipos compuestos
Control de flujo

return como cabe esperar sale de la función o programa en curso devolviendo un valor.En algunos perfiles sólo puede estar al final.

if / else es similar solo que la expresión debe devolver un bool.

for / while también están disponibles pero en algunos perfiles sólo se pueden usar si se pueden desenrollar es decir, si en tiempo de compilación se puede determinar el número de iteraciones.

switch / case / default aunque son palabras reservas, no se admiten por ahora.

Funciones

Se pueden declarar funciones sin embargo la recursión no esta permitida.

Los parámetros de las funciones son siempre por valor (no existen los punteros) y admiten tres modificadores obviamente reveladores in, out e inout. Luego hay otro modificador que es uniform que se verá más adelante.

Sí se admite sobrecarga de funciones.

Conclusión que tan sólo se trata de alias o macros y no de verdaderas funciones, está claro.

Operaciones aritméticas

Las operaciones +, -, * y / están disponibles para los escalares (que menos).

Con vectores también está permitido, pero el significado es simplemente de agrupación, es decir float3( A, B, C ) * float3( D, E, F ) == float3( A * D, B * E, C * F ) y su homólogo con escalares es trivial (factor común).

No se admite sin embargo estos operandos para matrices, para ello debe usarse la función mul( a, b ) donde uno de los dos parámetros como mínimo debe ser una matriz, el otro puede ser un vector o una matriz y el significado es evidente.

Operadores lógicos

Tenemos &&, || y ! cuyos parámetros deben ser siempre bool.

Aunque no es lógico lo pongo aquí. También está el operador "if abreviado", ( predicado ) ? cierto : falso. Decir que los tipos devueltos por las expresiones deben ser equivalentes.

Otros operadores

Cg tiene un curioso "operador" denominado "swizzle" que permite reordenar un vector con sólo renumerar sus componentes. Por ejemplo, si tenemos q = float4( 1.0f, 2.0f, 3.0f, 4.0f ) y hacemos r = q.wzyx pues resulta que se cumple que r == float4( 4.0f, 3.0f, 2.0f, 1.0f ). Con los colores para similar así que los alias para cada componente son en orden: x/r, y/g, z/b y w/a. Se pueden hacer mezclas "raras" como por ejemplo que float2(1.0f,2.0f).xyxy devuelve un float4.

Acceso a texturas

Las texturas son datos de entrada que no se pueden modificar y pertenecen a esos tipos de información "uniformes", vamos, constantes globales a los VS y PS.

En lo que llaman Advanced Fragment Profiles se detalla lo siguiente:

Por un lado tenemos las texturas en sí, que son los sampler* de antes y de los diversos tipos de antes. Por otro, están las coordenadas que no son mas que vectores (y que pueden ser 1, 2, 3 o 4 dimensional). Recuperar el color de una textura es tan "difícil" como color = tex2D( textura, coordenada ) donde puede ser our float4 color : COLOR0, float2 coordenada : TEXCOORD0 y uniform sampler2D textura. Existe una gran variedad de funciones para recuperar datos de texturas que se verán después. Pero básicamente:

Cg Standard Library Functions

Bien, el título lo dice todo, aquí iremos algo rapiditos, ya habrá tiempo de consultar la referencia cuando queramos usar alguna función, con saber que están es suficiente:

Usando Cg Runtime Library

Siempre me he sentido atraído por OpenGL y mirado con indiferencia a DirectX la razón es sencilla, OpenGL es totalmente intuitivo, simple, potente y además un estandar en gráficos, mientras que DirectX comenzó con muy mal pie al ser muy engorroso de programar. Por esto, me saltaré todas las especificaciones para DirectX y sus ejemplos.

Esquema del acceso al Cg Runtime

Cg Runtime Library permite "compilar al vuelo" los shaders que programemos, la documentación indica que existen varias ventajas de usar este procedimiento (y no tener ya compilados los shaders):

La API Cg Runtime consiste en tres partes (ver dibujo de arriba): funciones para acceder directamente a él desde la aplicación (prefijo cg), funciones para interactuar mediante DirectX (prefijo cgD3D) y funciones para interactuar mediante OpenGL (prefijo cgGL).

El acceso a las correspondientes definiciones de las API se hace mediante las cabeceras:

// Funciones directas a Cg Runtime API:
#include <Cg/cg.h>

// Funciones integradas en OpenGL:
#include <Cg/cgGL.h>

Lo primero que debemos hacer es crear un "contexto cg" que básicamente nos permitirá acceder a todas las funciones, es el punto de partida.

CGcontext context = cgCreateContext();

Compilar un programa es sencillo y aunque en la documentación original no ponen la declaración de la función, yo sí la pongo porque me parece mucho más clarificadora que sólo un ejemplo de su llamada (está bien saber cuales son los tipos de los parámetros).

// Prototipo:
CGDLL_API CGprogram cgCreateProgram(
	CGcontext ctx,			// contexto que gestiona el tema.
	CGenum program_type,	// tipo de programa
	const char *program,	// el código del programa
	CGprofile profile,		// perfil para el cual se compila
	const char *entry,		// función de entrada al programa
	const char **args		// argumentos del programa
);

// Una llamada típica podría ser:
CGprogram program = cgCreateProgram( context,
	CG_SOURCE,
	myVertexProgramString,
	CG_PROFILE_ARBVP1,
	"main",
	args // esto son los parámatros ¡AL COMPILADOR!
);

Para cargar un programa en el procesador (y poder ejecutarlo) se utiliza (en OpenGL) la trivial llamada:

cgGLLoadProgram(program);

Para establecer o modificar los parámetros del programa, debemos primero tomar un manejador de parámetro y luego acceder a él:

// Conseguimos un manejador del parámetro...
CGparameter myParameter = cgGetNamedParameter(
	program,
	"myParameter" // tiene que ser tal cual aparece en el programa.
);

// ...y luego lo usamos:
cgGLSetParameter4fv( myParameter, value );

// notar que el tipo de llamada a realizar debe ser compatible con el tipo del parámetro.
// en éste caso, value sería un float[4].

Pues ya estamos listos para ejecutar el programa, en OpenGL es preciso antes habilitar la ejecución del perfil en cuestión (en DirectX no es preciso porque no hay ambigüedad):

// Habilitamos el perfil en el que deseemos ejecutar el programa:
cgGLEnableProfile( CG_PROFILE_ARBVP1 );

// Deshabilitamos el perfil, esto provocaría la detención de un programa en ejecución:
cgGLDisableProfile( CG_PROFILE_ARBVP1 );

Pues nada, ahora activamos el programa para su ejecución y ésto sustituirá al anterior que pudiera estar activo:

// Fijamos el programa que debe estar en el procesador de la GPU:
cgGLBindProgram( program );

Sólo es posible fijar un programa VS y un programa PS a la vez, y dicha pareja de programas (el VS y el PS) serán los que se ejecuten cada vez que sea preciso dibujar la escena, es decir, procesar las entradas de vértices y texturas.

Como mandan los cánones debemos liberar aquellos recursos que no usemos, haremos:


// Liberamos cada programa compilado:
cgDestroyProgram( program );

// Liberamos cada contexto creado:
cgDestroyContext( context );

Core Cg Runtime

Explican que todo lo basan en la jerarquía formada por que la aplicación puede crear varios CGcontext, este varios CGprogram y este a su vez varios CGparameter aunque ya advierten que en un futuro plantean que los parámetros puedan definirse a nivel de contexto con el fin de que puedan ser usados por varios programas.

Definen una serie de elementos comunes:

Se paran en detallar algunas cosas obvias, tú que no eres tonto, te bastará con un rápido (aunque ámplio) listado:

Finalmente, para ejecutar un programa en OpenGL debemos cargar primeramente (una cosa es crearlo, ahora es cargarlo) usando void cgGLLoadProgram(CGprogram program);, esto hará que el programa esté "calentito" para su uso.

Para hacer que sea el programa activo (y sólo puede haber un VS a la vez y un PS a la vez) debemos llamar a void cgGLBindProgram(CGprogram program); que reemplazará a cualquier programa anterior. Si explícitamente queremos deshabilitarlo entonces llamamos a void cgGLDisableProfile(CGprofile profile);.

Y bueno, ya le hemos dado un repaso a toda la documentación del Cg Toolkit. Ahora sólo queda el aspecto práctico, que será ver qué perfiles tenemos disponibles e implementar algún ejemplo, pero esto mejor en otro artículo que este se hace muy largo ya.



Opinado el 17/08/10 04:43, valoración ValoraciónValoraciónValoraciónValoraciónValoraciónValoraciónValoraciónValoraciónValoraciónValoración
    exelente
Opinado el 11/11/10 13:52, valoración ValoraciónValoraciónValoraciónValoraciónValoraciónValoraciónValoraciónValoraciónValoraciónValoración
    Muy bueno!
Opinado el 02/12/10 09:38, valoración ValoraciónValoraciónValoraciónValoraciónValoraciónValoraciónValoraciónValoraciónValoraciónValoración
    
Opinado el 04/05/12 16:05, valoración ValoraciónValoraciónValoraciónValoraciónValoraciónValoraciónValoraciónValoraciónValoraciónValoración
    
Opinado el 11/06/12 13:58, valoración ValoraciónValoraciónValoraciónValoraciónValoraciónValoraciónValoraciónValoraciónValoraciónValoración
    
¿Te ha gustado? ¡aporta tu opinión!
Valoración:
 0    1    2    3    4    5    6    7    8    9    10

Comentario:
NOTA: si es una petición... ¡pon el e-mail al que responderte o no sabré a dónde escribir!

Código de verificación captcha