El trastero de José Juan Valid XHTML 1.1 Valid CSS! Estilo de página alternativo
Artículo creado en 2008.

Cg "C for graphics" (III)

Introducción

Después de realizar alguna práctica sobre Cg en Cg "C for graphics" (II) nos percatamos que todos los procesos que deben realizarse para establecer un shader nos hacen perder un tiempo precioso (todo tiempo es precioso, incluso aquel en el que perezosos, dejamos vagar la mente). Por supuesto existen funciones como las existentes en el NVIDIA OpenGL SDK 10, pero dicho sea de paso, son feísimas. En todo momento se establecen variables globales y dichas funciones sólo actúan localmente sobre dichas variables globales, en fin, un desastre.

Estoy seguro (o al menos eso espero) que en librerías más o menos profesionales este aspecto estará resuelto de una forma adecuada. Yo por mi parte, voy a sugerir una clase base abstracta que facilite la construcción de wrappers que envuelvan todo lo relacionado con un shader, de tal forma que podamos centrarnos en la programación del código concreto del shader (vertex program y fragment progam) y en su posterior utilización.

Enfoque del problema

En realidad, lo ideal no es crear una clase abstracta para implementar wrappers de shaders. Lo que realmente nos gustaría, es disponer de una clase cuya instancia supiera adaptarse a cada shader programado. Esto podría hacerse desde C# utilizando reflexión y desde C++ podría llegarse sin demasiada complicación a una solución similar a las funciones del NVIDIA OpenGL SDK 10 pero con programación orientada a objetos. En ambos casos tendríamos un importante problema (en C++ no veo forma de resolverlo y en C# difícilmente) que curiosamente no tiene nada que ver con la compilación ni código generado, sino con la programación en sí misma, pues me refiero a disponer de los miembros desplegados por cada shader en tiempo de programación, es decir, IntelliSense.

Veamos un ejemplo de nuestra clase ideal (fictícia, claro):


// La clase controla cualquier shader:
class myCgShader {
	...
};


// Para instanciar un shader, haríamos algo "parecido" a
// las plantillas (templates) o bien...
myCgShader<"C:\\shaders\\myShader"> myshader;

// ...que sepa reconocer en tiempo de programación los argumentos:
myCgShader myshader = new myCgShader("C:\\shaders\\myShader");

// de tal modo que en seguida, podemos hacer uso de las
// particularidades de nuestro shader:
myshader.SetModelViewMatrix( mymatrix );
myshader.SetAmbientColor( ambient_color );
...
myshader.Enable();
...
draw_scene();
...
myshader.Disable();

...

No pienses que es tan raro que un IDE sepa reconocer los argumentos de una determinada función o creación de nuevo objeto (como el que expongo anteriormente), ya en Interdev (que formaba parte de Microsoft Visual Studio 6) reconocía los objetos creados con la función CreateObject desplegando los miembros concretos del componente:


' Esto es Visual Basic Script en un archivo ASP
Dim fso
Set fso = Server.CreateObject("Scripting.FileSystemObject")

' Aquí, el IDE de Interdev reconoce en tiempo de programación
' los miembros del objeto fso mostrándolos automáticamente
' mediante IntelliSense:
fso.DeleteFile "..."

...

Desgraciadamente y hasta donde yo se, ninguno de los compiladores admiten hacer directamente lo que expongo de tal forma que el IDE nos trivialice el uso de los shader (que es lo que queremos).

Si estás pensando en otros lenguajes como Perl o PHP sí permiten hacer de forma trivial la generación automática de miembros, pero tienen dos infranqueables problemas: estamos hablando de aplicaciones críticas en tiempo real (deben generarse un mínimo de 30 o 40 frames por segundo) y no, que yo sepa ningún IDE podría hacer esto.

Mejor solución (no implementada)

Probablemente la mejor alternativa en un entorno productivo (en el que no me encuentro) es generar un wrapper de forma automática a partir de los códigos de los vertex program y fragment program. Aunque no es tan bonito como la solución ideal, sí tiene todas las demás ventajas. Se trata ni más ni menos, de implementar un programa que parsee los v.program y f.program reconociendo sus propiedades (y particularmente los argumentos uniformes) y genere una clase específica de cada shader lista para ser compilada y reconocida por el IDE.

Si estás pensando otra vez que es complicado o no puede hacerse, la tan fructífera mecanización de los Web Services se basa precisamente en este mecanismo. Dado el archivo que describe al Web Service (el WSDL) nuestro entorno de programación genera un wrapper (una clase) que "sabe" conectar con el servidor web.

En nuestro caso, el proceso sería el siguiente:

El archivo de definición de shader podría tener esta pinta:

<shaders>
	<shader name="myShader">
		<vertex_program entry_point="main">
			<versions>
				<version>vp20</version>
				<version>vp30</version>
			</versions>
			<source_code path="C:\shaders\myShader.vp20.cg" />
		</vertex_program>
		<vertex_program entry_point="main">
			<versions>
				<version>vp40</version>
			</versions>
			<source_code path="C:\shaders\myShader.vp40.cg" />
		</vertex_program>
		<fragment_program entry_point="ColorGenerator">
			<versions>
				<version>fp20</version>
				<version>fp30</version>
			</versions>
			<source_code path="C:\shaders\myShader.fp20.cg" />
		</fragment_program>
		<fragment_program entry_point="ColorGenerator">
			<versions>
				<version>fp40</version>
			</versions>
			<source_code path="C:\shaders\myShader.fp40.cg" />
		</fragment_program>
	</shader>
</shaders>

En el sentido de que podría contener múltiples shaders (de cada cual se generaría un wrapper), contendría información para múltiples versiones, hardware, API gráfica, etc...

Como puedes imaginar, hacer el programa cggw.exe no es trivial y por supuesto no tengo ninguna intención de hacerlo.

Peor solución (no implementada)

Podemos implementar nuestra clase, que lea los argumentos de los programas y exponga métodos que permitan leer los agumentos de nuestro shader, eso sí, sin IntelliSense. Tiene la ventaja de que es sencillo, pero tres enormes desventajas: la primera y fatal que acceder a los argumentos deberá ser por nombre (como hace el SDK de NVidia) o por índice, en el primer caso consume recursos preciosos de CPU (deben compararse cadenas), la segunda que no es nada práctico para el programador acceder mediante índices (1º argument, 2º argumento, ...) y la tercera que en ambos casos la comprobación de tipos debe hacerse bien acordándose el programador (como en las funciones OpenGL o el SDK de NVidia) o bien fallarán en tiempo de ejecución (y no en tiempo de compilación como debe y mejor aún, en tiempo de programación).

Un ejemplo de los problemas es:


// Creamos una instancia de nuestro cargados de shaders:
myCgShaderLoader myshader = new myCgShaderLoader(
		"C:\\shaders\\myshader.vertex.cg",
		"C:\\shaders\\myshader.fragment.cg"
	);

// Accedemos mediante cadenas a los argumentos:
myshader.SetParam3fv( "colorAmbiente", // ¡¡¡ lentísimo !!!
		ambient_color );

// Accedemos mediante índices:
myshader.SetParam3fv( 3, // ¿qué índice es el del color ambiente?, ¿y si cambia?
		ambient_color );

// ¿Es SetParam3fv u otra llamada?

Solución intermedia (sí implementada)

Si queremos la mejor solución pero no programar un generador de wrapper, tendremos que encontrar la forma de hacer el trabajo de dicho generador pero sin repetir esfuerzos.

Crearemos una clase base abstracta que hará las tareas típicas de un cargador de shaders, cgCreateContext, cgGLGetLatestProfile, cgGLSetOptimalOptions, cgCreateProgramFromFile, etc... que son comunes a todos los shader.

Luego, para cada shader crearemos una nueva clase (nuestro wrapper) a la que normalmente sólo ampliaremos los miembros concretos de los argumentos específicos de nuestro shader. Aquí, un ejemplo de una clase wrapper:


class myCgShaderDeph : public myCgShader {
private:
	CGparameter vp_ModelViewMatrix; // argumento del vertex program
	CGparameter fp_DephTexture; // argumento del fragment program

public:
	bool Inicializar(
		myCgContext & context,
		const char *vp_file, const char *vp_main,
		const char *fp_file, const char *fp_main
	);

	// Está claro como actúa este parámetro y qué hay que pasarle
	// como parámetros, actualizará la matriz usando la de OpenGL
	void Set_vp_ModelViewMatrix( void );

	// Está claro como actúa este parámetro y qué hay que pasarle,
	// espera un entero sin signo con el ID de textura
	void Set_fp_DephTexture( GLuint texture );

};

Así sí, con esta clase lo tenemos chupado para usar el shader y no tenemos problema en saber qué parámetros tiene y qué tipos hay que pasarle.

La implementación también es muy corta:

#define MYCMP_V(s) ( vp_##s = _vp.GetParam( #s ) )
#define MYCMP_F(s) ( fp_##s = _fp.GetParam( #s ) )

bool myCgShaderDeph::Inicializar(
		myCgContext & context,
		const char *vp_file, const char *vp_main,
		const char *fp_file, const char *fp_main
		) {
	if( ! myCgShader::Inicializar( context, vp_file, vp_main, fp_file, fp_main ) )
		return false;

	return MYCMP_V(ModelViewMatrix) && MYCMP_F(DephTexture);
}

bool myCgShaderDeph::Inicializado( void ) {
	return _inicializado;
}

void myCgShaderDeph::Set_vp_ModelViewMatrix( void ) {
	cgGLSetStateMatrixParameter( vp_ModelViewMatrix,
		CG_GL_MODELVIEW_PROJECTION_MATRIX,
		CG_GL_MATRIX_IDENTITY
	);
}

void myCgShaderDeph::Set_fp_DephTexture( GLuint texture ) {
	cgGLSetTextureParameter( fp_DephTexture, texture );
	cgSetSamplerState( fp_DephTexture );
	cgGLEnableTextureParameter( fp_DephTexture );
}

Como ves, la clase base abstracta myCgShader hace todo el trabajo duro mientras que la clase específica (el wrapper) myCgShaderDeph sólo se centra en la particularidad de los argumentos.

Usando el wrapper

Usar el wrapper es bien fácil, se tiene:

// Donde sea definimos un contexto y nuestro wrapper:
myCgContext *cgc;
myCgShaderDeph *cgsd;

...

// En la inicialización del sistema Cg haríamos:
cgc = new myCgContext();
if( !cgc->Inicializar() )
	return ¡ERROR!;

// Creamos el shader para ver la textura de profundidad
cgsd = new myCgShaderDeph();
if( ! cgsd->Inicializar(
	*cgc,
	"vp001.cg", "VertexProgram",
	"fp001.cg", "FragmentProgram"
) )
	return ¡ERROR!;

...

// Para usar el shader al renderizar haríamos:
cgsd->Bind();

cgsd->Set_vp_ModelViewMatrix();
cgsd->Set_fp_DephTexture( depth_rb ); // depth_rb | color_tex

cgc->Enable();

...draw scene...

cgc->Disable();

¡Así sí que es sencillo! y no con el entramado de llamadas a las funciones Cg.




¿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