Artículo creado en 2008.
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
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:
- Vertex Processor, (o Procesador de Vértices) que permite realizar transformaciones de la geometría. A los programas para este sub-procesador se les denomina indistintamente Vertex Programs o Vertex Shaders.
- Fragment Processor, (o Procesador de Fragmentos) que "grosso modo" permite realizar transformaciones en el color aplicado. A los programas para este sub-procesador se les denomina indistintamente como Fragment Programs, Píxel Programs o Píxel Shaders.
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:
- return-type es el tipo de dato a devolver (un real, un vector, un quaternion, etc...).
- program-name es el nombre del programa y puede ser cualquiera, no tiene porque ser main.
- parameters son los parámetros del programa y aunque los podemos definir nosotros, corresponden con estructuras existentes (como texturas, vértices, etc...).
- semantic-name indica el sentido que se le da a return-type indicando si es un color, un vértice, etc...
- instructions son las instrucciones propias del programa y deben terminar con un return del tipo a devolver.
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:
- Varying inputs algo así como "información cambiante" y no es más que los datos específicos de cada iteración en la que se ejecuta nuestro programa, en el ejemplo anterior sería cada una de las cajas, pues una caja no es la misma que la siguiente...
- Uniform inputs algo así como "información uniforme" y no es más que los datos que permanecen invariantes (no varían, no cambian, son constantes) durante todas las ejecuciones de nuestro programa, en el ejemplo, podría ser el tipo de sello de nuestra máquina.
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:
- float es un número en coma flotante de 32 bits IEEE s23e8. Está disponible siempre aunque algunos perfiles degradan su precisión con algunas operaciones. Su sufijo para constantes es f (p.e. 2.0f).
- half similar a float pero 16 bits IEEE s10e5. Su sufijo para constantes es h (p.e. 2.0h).
- int entero de 32 bits. En algunos perfiles no está o bien es tratado a través de float (lo andan redondeando y tal).
- fixed coma fija de 12 bits s1.10 y es soportado por todos los perfiles. Su sufijo para constantes es x (p.e. 2.0x).
- bool pues eso, es soportado en todos los perfiles.
- sampler* es un manejador de texturas y se presenta en seis variantes: sampler,
sampler1D, sampler2D, sampler3D, samplerCUBE y samplerRECT aunque este último no está disponible en perfiles DirectX.
- type# son vectores de # escalares del tipo type donde # puede ser 1, 2, 3 y 4. Por ejemplo float3 indica un vector de 3 números en coma flotante.
- typeNxM son matrices de N por M elementos del tipo type.
Tipos compuestos
- type# son vectores de # escalares del tipo type donde # puede ser 1, 2, 3 y 4. Por ejemplo float3 indica un vector de 3 números en coma flotante. Se puede especificar un vector constante en cualquier sitio con sólo hacer por ejemplo float3( 1.0f, 2.0f, 3.0f ).
- typeNxM son matrices de N por M elementos del tipo type.
- ARRAY se pueden definir arrays, pero estos deben estar dimensionados a un tamaño fijo. P.e. "float4x4 v[4]" declararía un array de cuatro matrices 4 por 4. CUIDADO que se pasan por valor.
- STRUCT pues eso, estructuras, aunque no admite uniones. CUIDADO que se pasan por valor.
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:
- Estándar no proyectadas:
- tex2D (sampler2D tex, float2 s);
- texRECT (samplerRECT tex, float2 s);
- texCUBE (samplerCUBE tex, float3 s);
- Estándar proyectadas:
- tex2Dproj (sampler2D tex, float3 s);
- texRECTproj (samplerRECT tex, float3 s);
- texCUBEproj (samplerCUBE tex, float4 s);
- No proyectadas con filtro en el que se especifica el tamaño y las coordenadas de las derivadas de la coordenada respecto de cada componente (x e y):
- tex2D (sampler2D tex, float2 s, float2 dsdx, float2 dsdy);
- texRECT (samplerRECT tex, float2 s, float2 dsdx, float2 dsdy);
- texCUBE (samplerCUBE tex, float3 s, float3 dsdx, float3 dsdy);
- Mapa de sombra Shadowmap (esto se usa normalmente para calcular sombras mediante texturas, que renderiza una escena sobre una textura cuyo origen es el foco de luz y luego se compara el z-buffer para saber si un objeto está oscurecido o no) para que se compare el z-buffer debe estar habilitado desde la aplicación (sino, no se comparará z-buffer por hardware):
- tex2Dproj (sampler2D tex, float4 szq);
- tex2DRECT (samplerRECT tex, float4 szq);
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:
- Matemáticas:
- abs(x), acos(x), asin(x), atan(x), atan2(x,y), ceil(x), cos(x), cosh(x), determinant(M), dot(a,b), exp(x), exp2(x), floor(x), fmod(x,y), frac(x), isfinite(x), isinf(x), isnan(x), log(x), log2(x), log10(x), max(a,b), min(a,b), mul(a,b), pow(x,y), round(x), rsqrt(x), sign(x), sin(x), sinh(x), sqrt(x), tan(x), tanh(x), transpose(M) que vamos a contar.
- all(x) cierto si no hay ceros en el vector.
- any(x) cierto si hay algún distinto de cero en el vector.
- clamp(x,a,b) ajusta x al intervalo [a,b].
- cross(a,b) producto vectorial de los vectores a y b ¡deben ser de 3 componentes!.
- degress(x), radians(x) de radianes a grados y de grados a radianes.
- frexp(x, out exp) que cosa más rara, devuelve una fracción normalizada en el intervalo [0.5, 1) y una potencia de dos metida en el parámetro exp, si x es cero, las dos cosas son cero también.
- ldexp(x,n) x * 2^n.
- lerp(a,b,f) interpolación lineal haciendo (1-f) * a + b * f (a, b y f pueden ser escalares o vectores).
- lit( ndot1, ndoth, m) es una macro para calcular los coeficientes ambiente, difusa y especular de la luz, devolviendo 4 componentes (en un vector) con:
- ambiente siempre es 1.0f.
- difusa que es ndotl si éste es positivo, sino cero.
- especular que es cero si ndotl o ndoth es cero, sino devuelve ndoth^m.
- siempre 1.0f
- modf(x, out ip) la parte entera en ip al parte decimal es lo devuelto.
- noise(x) pues algo así como un rand(x) pero 1, 2, o 3 dimensional según el tipo de la semilla que se aporte.
- saturate(x) viene a ser clamp(x,0,1).
- sincos(float x, out s, out c ) curiosa función que calcula a la vez el seno y coseno (más rapido que separado).
- smoothstep( min, max, x ) calcula -2 * ((x-min)/(max-min))^3 + 3 * ((x-min)/(max-min))^2.
- step(a,x) curioso, 0 si x<a sino 1.
- Geométricas:
- distance(pt1, pt2) distancia euclídea.
- faceforward(N, I, Ng) N si dot(Ng,I)<0 sino -N.
- length(v) distancia euclídea al origen.
- normalize(v) normaliza el vector.
- reflect(i, n) calcula el rayo reflejado cuya dirección es i y la normal de la superfície es n, deben ser 3 componentes.
- refract(i, n, eta) calcula la refracción entre el rayo (dirección) i, la superfície (normal) n y el índice de refracción eta. Si el ángulo entre i y n es mayor que eta devuelve (0,0,0). Deben ser 3 componentes.
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.
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):
- Compilación específica al compilarse en el momento, el compilador puede tomar decisiones y optimizaciones específicas para la plataforma concreta en la que está corriendo la aplicación.
- Compatibilidad futura aunque algunas optimizaciones no estén disponibles actualmente, cuando lo estén en el futuro una compilación al vuelo permitirá aprovechar dichas mejoras.
- Un sólo programa, varios perfiles el programa puede ser el mismo para varios perfiles, si lo tenemos ya compilado, debemos tener una versión para cada perfil en el que deba correr.
- No limitación de dependencias al enlazarlo al vuelo, se pueden resolver posibles problemas de dependencias con otras librerías o parámetros de entrada, registros del procesador, etc...
- Manejo de parámetros existen algunas ventajas a la hora de manejar los parámetros de entrada si se compila en el momento.
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:
- CGbool con su CG_TRUE y su CG_FALSE.
- CGenum que tiene (¡hala! todo a rebullón) varios elementos enumerados.
- Zero = Error convienen en que si una función devuelva el tipo que devuelva, si devuelve cero, entonces es que ha habido error.
Se paran en detallar algunas cosas obvias, tú que no eres tonto, te bastará con un rápido (aunque ámplio) listado:
- CGcontext
- CGcontext cgCreateContext(); no dicen nada pero me da que lo suyo es crear los contextos después de tener inicializado OpenGL (o DirectX).
- void cgDestroyContext(CGcontext context); aquí sí dicen que esta llamada eliminará todos los programas y parámetros creados y que debe llamarse antes de liberar el control de OpenGL (o DirectX).
- CGbool cgIsContext(CGcontext context); permite saber si el contexto es válido para ser usado.
- CGprogram
- CGprogram cgCreateProgram(CGcontext context, CGenum programType, const char* program, CGprofile profile, const char* entry, const char** args); crea y compila un programa escrito en una cadena de entrada.
- CGprogram cgCreateProgramFromFile(CGcontext context, CGenum programType, const char* program, CGprofile profile, const char* entry, const char** args); crea y compila un programa existente en un archivo de texto.
- En ambos casos, los parámetros son:
- context el contexto.
- programType es una enumeración que indica que lo que entregamos es un código fuente que debe ser compilado si es CG_SOURCE o bien que es código objeto (ya compilado) si es CG_OBJECT.
- program en el primer caso es el programa en sí, en el segundo es la ruta del archivo que lo contiene.
- profile es de la enumeración el perfil elegido.
- entry es el nombre de la función que inicia el programa (y de la cual se asignan los parámetros).
- args es un null terminated array de null terminated strings con los parámetros a pasar al compilador.
- (return) el valor devuelto es el manejador del programa y si falla pues será NULL.
- void cgDestroyProgram(CGprogram program); destruye todos los recursos asociados al programa.
- CGbool cgIsProgramCompiled(CGprogram program); indica si el programa necesita ser recompilado.
- cgCompileProgram(CGprogram program); recompila un programa.
- CGprogram cgCopyProgram(CGprogram program); permite realizar una copia completa del programa (cuya copia pertenecerá al mismo contexto) para modificarla si es preciso (o por ejemplo para establecer diferentes parámetros).
- CGbool cgIsProgram(CGprogram program); indica si es un manejador de un programa válido.
- const char* cgGetLastListing(CGcontext context); devuelve la última salida de una compilación de un programa (como si se hiciera desde la línea de comandos). Si no se realizó ninguna compilación previa será cero.
- CGcontext cgGetProgramContext(CGprogram program); dado un programa devuelve el contexto al que está asociado.
- CGprofile cgGetProgramProfile(CGprogram program); el perfil sobre el que se ha compilado.
- const char* cgGetProgramString(CGprogram program,
CGenum stringType); devuelve información del programa según se indique: CG_PROGRAM_SOURCE, CG_PROGRAM_ENTRY, CG_PROGRAM_PROFILE o CG_COMPILED_PROGRAM.
- Iterar entre los programas:
- CGprogram cgGetFirstProgram(CGcontext context); devuelve el primer programa de un contexto (o cero si no hay o el contexto no es válido).
- CGprogram cgGetNextProgram(CGprogram program); devuelve el siguiente programa de la lista de un contexto (o cero si no hay más). El orden en el que se devuelven los programas en una iteración no es determinista.
- Perfiles
- CGprofile cgGetProfile(const char* profileString); pues eso.
- const char* cgGetProfileString(CGprofile profile); pues eso.
- CGparameter
- CGparameter cgGetFirstParameter(CGprogram program, CGenum namespace); inicia la lista de iteración para recuperar los parámetros de un determinado namespace de momento el único namespace admitido es CG_PROGRAM.
- CGparameter cgGetNextParameter(CGparameter parameter); siguiente parámetro.
- CGparameter cgGetFirstStructParameter(CGparameter parameter); si el parámetro de entrada es una estructura CG_STRUCT permite iniciar la iteración de sus parámetros internos (con cgGetNextParameter seguimos con el resto).
- int cgGetArrayDimension(CGparameter parameter); si el parámetro es un CG_ARRAY devuelve el número de dimensiones.
- int cgGetArraySize(CGparameter parameter, int dimension); el número de elementos en esa dimensión.
- CGparameter cgGetArrayParameter(CGparameter parameter, int index); el parámetro index-ésimo de la dimensión 0 (que si tiene subdimensiones devolverá otro CG_ARRAY).
- CGparameter cgGetFirstLeafParameter(CGprogram program, CGenum namespace); permite iterar sobre TODO el árbol de parámetros sin importar su estructura.
- CGparameter cgGetNextLeafParameter(CGparameter parameter); pues el siguiente.
- CGparameter cgGetNamedParameter(CGprogram program, const char* name); si sabemos el nombre de un parámetro, podemos acceder directamente a él. Se pueden usar cosas como "Datos[1].Direccion.x" para acceder al parámetro.
- CGbool cgIsParameter(CGparameter parameter); indica si es un parámetro válido o no.
- CGbool cgIsParameterReferenced(CGparameter parameter); indica si ese parámetro es referenciado (usado) finalmente por el programa al haber sido compilado.
- CGprogram cgGetParameterProgram(CGparameter parameter); el programa de un parámetro.
- CGenum cgGetParameterVariability(CGparameter parameter); indica el tipo de parámetro del que se trata en el sentido de si es CG_VARYING, CG_UNIFORM o CG_CONSTANT.
- CGenum cgGetParameterDirection(CGparameter parameter); para saber si es de CG_IN, CG_OUT o CG_INOUT.
- CGtype cgGetParameterType(CGparameter parameter); dice el tipo del parámetro: CG_STRUCT, CG_ARRAY, CG_HALF*, CG_FLOAT* o CG_SAMPLER*, se puede saber la correspondencia entre el tipo y su enumerado mediante:
- CGtype cgGetType(const char* typeString);
- const char* cgGetTypeString(CGtype type);
- const char* cgGetParameterName(CGparameter parameter); nombre del parámetro.
- const char* cgGetParameterSemantic(CGparameter parameter); la semántica del parámetro (o NULL) como POSITION, COLOR, etc...
- CGresource cgGetParameterResource(CGparameter parameter); todos los parámetros están asociados con un recurso hardware, esta función devuelve información del mismo.
- CGresource cgGetResource(const char* resourceString); está claro.
- const char* cgGetResourceString(CGresource resource); igual.
- CGresource cgGetParameterBaseResource(CGparameter parameter); similar a cgGetParameterResource sólo que cuando el recurso está indizado entonces siempre devuelve el 0, por ejemplo existen N texturas y el recurso que contiene sus coordenadas son respectivamente CG_TEXCOORD0, CG_TEXCOORD1, ... entonces esta función siempre devolverá CG_TEXCOORD0 sea la textura que sea a que se refiera el parámetro.
- unsigned long cgGetParameterResourceIndex(CGparameter parameter); complementa a la anterior en el sentido de que devuelve precisamente el índice al que se refiere.
- const double* cgGetParameterValues(CGparameter parameter, CGenum valueType, int* numberOfValuesReturned); devuelve el valor una variable "uniforme" bien su valor por defecto o constante según se indique en valueType CG_DEFAULT o CG_CONSTANT.
- Errores
- CGerror error = cgGetError(); pemite recuperar el último error producido uno de ellos es CG_NO_ERROR (mira la doc si quieres la lista del resto de códigos de error).
- const char* errorString = cgGetErrorString(error); para acceder a un mensaje detallado del mismo.
- void cgSetErrorCallback( void *MyErrorCallback( void ) ); permite especificar una función que será llamada en caso de producirse un error.
- API-Specific Cg Runtimes (OpenGL Cg Runtime), aquí nos explican algunas funciones concretas para la API concreta que estemos usando (DirectX u OpenGL) yo como te digo, me limito a OpenGL, eso sí, el contexto de OpenGL debe estar inicializado (mediante wglCreateContext o glXCreateContext) antes de acceder a Cg:
- Parameter Shadowing en OpenGL varios de los parámetros específicos están "sombreados" en la API de forma que nosotros no nos tenemos que preocupar por ellos y estarán disponibles sin más en nuestro VS o PS (ojo, en DirectX esto cambia un poco).
- Establecer los valores en los parámetros son todo funciones muy similares a las típicas de OpenGL, estas son void cgGLSetParameter1f(CGparameter parameter, float x);, void cgGLSetParameter2fv(CGparameter parameter, const float* array); y otras similares.
- Recoger los valores de los parámetros pues igual: void cgGLGetParameter1f(CGparameter parameter, float* array);, void cgGLGetParameter3f(CGparameter parameter, float* array); y otras similares.
- Matrices pues otro tanto: void cgGLSetMatrixParameterfr(CGparameter parameter, const float* matrix);, void cgGLGetMatrixParameterfr(CGparameter parameter, float* matrix); y otras.
- Matrices OpenGL para establecer matrices de OpenGL se usa void cgGLSetStateMatrixParameter(CGparameter parameter, GLenum stateMatrixType, GLenum transform); donde:
- stateMatrixType puede ser: CG_GL_MODELVIEW_MATRIX, CG_GL_PROJECTION_MATRIX, CG_GL_TEXTURE_MATRIX o CG_GL_MODELVIEW_PROJECTION_MATRIX.
- transform CG_GL_MATRIX_IDENTITY, CG_GL_MATRIX_TRANSPOSE, CG_GL_MATRIX_INVERSE o CG_GL_MATRIX_INVERSE_TRANSPOSE.
- Arrays de vectores otras como void cgGLSetParameterArray3d(CGparameter parameter, long startIndex, long numberOfElements, const double* array); establecen los componentes desde startIndex numberOfElements elementos, es decir, desde el startIndex al startIndex+numberOfElements-1. Si numberOfElements es 0 entonces son todos los elementos desde startIndex, es decir, hasta cgGetArraySize(parameter,0)-1. El array debe tener tantos elementos como el índice de función por numberOfElements (es decir, son arrays de vectores de 1, 2, 3 o 4 componentes).
- Matrices (II) si son arrays de matrices pues hay otras como void cgGLSetMatrixParameterArrayfr(CGparameter parameter, long startIndex, long numberOfElements, const float* array);.
- Parámetros para cada vértice los elementos anteriores son parámetros que se establecen para todo un programa, se pueden establecer parámetros para cada una de las iteraciones (parámetros varying) aunque sólo en el VS pues el PS sólo sabe los píxeles a procesar cuando se rasteriza cada tríangulo. Pues bien, para pasar estos parámetros de cada vértice se siguen dos pasos:
- Cargar datos se establece un puntero a la zona de memoria que tiene los datos mediante void cgGLSetParameterPointer(CGparameter parameter, GLint size, GLenum type, GLsizei stride, GLvoid* array); donde:
- size número de valores por vértice que se meten en array. que puede ser 1, 2, 3 o 4. Si resulta que el parámetro requiere 4 y se pasan menos aquí, entonces los valores por defecto para cada componente son: x=0, y=0, z=0 y w=1.
- type tipo de los valores en el array: GL_SHORT, GL_INT, GL_FLOAT o GL_DOUBLE.
- stride es el número de bytes que ocupa un bloque de datos de vértice hasta el siguiente. Dicho de otra forma si estamos al comienzo de datos de un vértice, lo que hay que saltar para pasar al siguiente. Se puede poner 0 para indicar que es size * sizeof( type ).
- array contiene los datos y debe almacenar (al menos) tanta información como vértices se vayan a procesar.
- Activar varying se debe activar esa "lectura de cliente" mediante una llamada a void cgGLEnableClientState(CGparameter parameter);. Posteriormente otra llamada a void cgGLDisableClientState(CGparameter parameter); lo deshabilitaría. Aunque un intento de llamar a cgGLSetParameter con un parámetro varying también lo habilitaría.
- Samplers para establecer estos parámetros hacen falta otros dos pasos:
- Asignar textura OpenGL mediante la llamada a void cgGLSetTextureParameter(CGparameter parameter, GLuint textureName);. Se puede saber qué textura OpenGL está asociada a un parámetro llamando a cgGLGetTextureParameter. Si queremos el valor de la enumeración de textura sería con GLenum cgGLGetTextureEnum(CGparameter parameter); que devuelve su GL_TEXTURE#_ARB correspondiente.
- Activar textura mediante una llamada a void cgGLEnableTextureParameter(CGparameter parameter); que debe hacerse entre la llamada a cgGLSetTextureParameter() y el proceso de dibujado. Se desactiva con cgGLDisableTextureParameter.
- CGbool cgGLIsProfileSupported(CGprofile profile); indica si un determinado perfil está disponible.
- OpenGL Profile Support este está muy bien, nos da algunas funciones que nos dicen la mejor configuración para la implementación en la que se ejecuta nuestra aplicación:
- CGprofile cgGLGetLatestProfile(CGGLenum profileType); nos dice el perfil más adecuado donde profileType es CG_GL_VERTEX o CG_GL_FRAGMENT.
- void cgGLSetOptimalOptions(CGprofile profile); para no tener que andar con el parámetro args podemos establecer el perfil por defecto al crear programas.
- void cgGLEnableProfile(CGprofile profile); habilita un perfil.
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 exelente
Opinado el 11/11/10 13:52, valoración Muy bueno!
Opinado el 02/12/10 09:38, valoración
Opinado el 04/05/12 16:05, valoración
Opinado el 11/06/12 13:58, valoración
Opinado el 29/07/19 10:38, valoración
¿Te ha gustado? ¡aporta tu opinión!