02 octubre 2009

Mi primer videojuego independiente (3)


Después de poner en marcha la librería OpenGL vamos a ver todo lo que necesitamos antes de implementar la clase TSprite. Necesitamos preparar nuestra librería UJuego.pas con la capacidad de dibujar polígonos.

LOS VERTICES

Dibujar sprites en con aceleración en 3D no suele ser tan fácil como copiar una imagen de un sitio a otro. En OpenGL debemos dibujar polígonos proporcionando tanto las coordenadas de sus vértices como las de sus texturas.

Los polígonos pueden ser cuadrados o triángulares, aunque cada GPU divide los cuadrados en triángulos ya que se procesan más rápidos internamente. Como a mí siempre me ha gustado hacer las cosas al nivel más bajo posible, entonces lo que hago es utilizar dos triángulos por cada sprite:

A cada triángulo hay que darle sus coordenadas en el sentido de las agujas del reloj, tal como elegimos anteriormente en las opciones de inicialización de OpenGL. Por lo tanto, por cada triángulo tenemos que proporcionar los tres vértices:

En el ejemplo de la imagen superior, suponiendo que nuestro sprite este situado en la esquina superior izquierda de la pantalla y que tenga un algo y alto de 100 pixels entonces las coordenadas del primer triángulo serían:

V1(x= 0, y=100, z=0)
V2(x= 0, y= 0, z=0)
V3(x=100, y= 0, z=0)

Supuestamente nunca llegaremos a utilizar la coordenada de profundidad Z porque estamos haciendo un juego 2D. Luego veremos más adelante como la utilizo mediante el Z-Buffer para ordenar la impresión de sprites. Y estas serían las coordenadas para el segundo triángulo:

V1(x= 0, y=100, z=0)
V2(x=100, y= 0, z=0)
V3(x=100, y=100, z=0)

Pero es que aquí no acaba la cosa. Sólo le hemos dicho donde queremos dibujar el polígono en pantalla pero es que además hay que mapear una textura encima del triángulo. Es como si a cada triángulo le supiésemos una pegatina encima (la textura):

Por ello, cada vértice tiene dos sistemas de coordenadas, una para dibujarlo (x,y,z) y otras para la textura (u, v). La coordenada u es la horizontal y v la vertical. Equivalen a unas coordenadas x, y dentro de la textura. Estas coordenadas vienen en números reales y van desde 0 a 1. Para nuestro sprite estas serían las coordendas u,v de la textura:

V1(u=0, v=1)
V2(u=0, v=0)
V3(u=1, v=0)

Y para el segundo polígono:

V1(u=0,v=1)
V2(u=1,v=0)
V3(u=1,v=1)

Es prácticamente lo mismo que las coordenadas x, y pero a escala 0 a 1. Si hiciésemos un juego en 3D ya se encargarían los programas como 3D Studio de darnos las coordenadas (x,y,z,u,v) por cada vértice.

Entonces, en el momento justo donde llevamos los polígonos a pantalla debemos primero darle a OpenGL las coordenadas de la textura y después las coordenadas del triángulo. Ya lo veremos más adelante cuando lancemos los polígonos a pantalla.

Pues bien, todo este rollo que os he soltado es para definir la clase TVertice que contiene las coordenadas en pantalla y en la textura:

TVertice = class
x, y, z: GLfloat; // Coordenadas
u, v: GLfloat; // Coordenadas de la textura

constructor Create; overload; // constructor por defecto
constructor Create(x, y, z, u, v: GLfloat); overload; // constructor con parámetros
end;

He definido un par de constructores. El básico:

constructor TVertice.Create;
begin
x := 0;
y := 0;
z := 0;
u := 0;
v := 0;
end;

Y otro por si queremos crear el vértice y pasarle directamente las coordenadas:

constructor TVertice.Create(x, y, z, u, v: GLfloat);
begin
Self.x := x;
Self.y := y;
Self.z := z;
Self.u := u;
Self.v := v;
end;

El tipo GLfloat viene definido por defecto por OpenGL aunque también podemos utilizar los tipos Real o Double. Lo mejor es utilizar todo lo posible los tipos de datos de OpenGL por si hay que pasar nuestro motor a otros lenguajes como C/C++, C#, Python o Ruby, ya que tanto la librería SDL como la OpenGL son multiplataforma y multilenguaje.

LOS TRIÁNGULOS

Lo siguiente es definir la clase TTriangulo que va a contener los 3 vértices del polígono y un identificador para la textura:

TTriangulo = class
Vertice: array[1..3] of TVertice; // Los 3 vertices del triángulo
IdTextura: GLuint; // Nº de textura asignada a este triángulo

constructor Create; overload; // constructor por defecto
constructor Create(x1, y1, z1, u1, v1,
x2, y2, z2, u2, v2,
x3, y3, z3, u3, v3: GLFloat ); overload; // constructor con parámetros
destructor Destroy; override;
end;

Antes de comenzar a renderizar polígonos en pantalla debemos subir todas las texturas a la memoria de vídeo. Cada vez que subimos una textura, la librería OpenGL nos proporciona un identificador de la textura (1, 2, 3, …). Necesitamos guardarlo en IdTextura para más adelante.

Para esta clase he creado dos constructores y un destructor:

constructor TTriangulo.Create;
var i: Integer;
begin
// Creamos los tres vértices
for i := 1 to 3 do
Vertice[i] := TVertice.Create;
end;

constructor TTriangulo.Create(x1, y1, z1, u1, v1,
x2, y2, z2, u2, v2, x3,
y3, z3, u3, v3: GLFloat);
begin
// Creamos los tres vértices según sus parámetros
Vertice[1] := TVertice.Create(x1, y1, z1, u1, v1);
Vertice[2] := TVertice.Create(x2, y2, z2, u2, v2);
Vertice[3] := TVertice.Create(x3, y3, z3, u3, v3);
end;

destructor TTriangulo.Destroy;
begin
Vertice[1].Free;
Vertice[2].Free;
Vertice[3].Free;
end;

LAS TEXTURAS

La clase encargada de almacenar los datos de la textura para cada par de triángulos es la siguiente:

TTextura = class
sNombre: string; // Nombre del sprite
iAncho, iAlto: Integer; // Ancho y alto de la textura
iIncX, iIncY: Integer; // Incremento de X, Y respecto al sprite
iAnchoTex, iAltoTex: Integer; // Alto y ancho de la textura que vamos a recortar

Triangulo: array[1..2] of TTriangulo; // Los dos triángulos del sprites
ID: GLuint; // Identificador de la textura

constructor Create(Origen: PSDL_Surface; xInicial, yInicial, iAncho, iAlto,
iIncrementoX, iIncrementoY, iAnchoTex, iAltoTex, iAnchoSub,
iAltoSub: Integer; bSubsprites: Boolean); overload;
destructor Destroy; override;
end;

Definimos las siguientes variables:

sNombre: nombre del sprite asociado a esta textura.

iAncho, iAlto: Alto y ancho de la textura.

iIncX, iIncY: Son las coordenadas desde donde capturamos la textura (por efecto en la esquina superior izquierda (0,0)). Cuando un sprite es más grande de 256x256 necesito crear varias texturas que simulen un supersprite. Por ello necesito estas variables para saber cual es la esquina superior izquierda donde comienza la textura dentro del sprite.

iAnchoTex, iAltoTex: Ancho y largo real del sprite dentro de la textura. Como en OpenGL sólo podemos crear texturas con un ancho y alto que sea potencia de dos entonces guardo también en estas variables el ancho y largo real del sprite. Puede ser que el sprite sea de 96x78 pero necesite crear para el mismo una textura de 128x128.

Después tenemos dos triángulos definidos dentro de la textura y su identificador:

Triangulo: array[1..2] of TTriangulo; // Los dos triángulos del sprites
ID: GLuint; // Identificador de la textura

El constructor que he creado para la textura es algo complejo, porque no sólo crea los triángulos que vamos a utilizar sino que también carga la textura desde la superficie que le pasamos como parámetro. En vez de cargar la textura desde un archivo PNG la cargo desde un archivo ZIP comprimido. La razón no es otra que la de proteger nuestros gráficos PNG para que nadie pueda manipularlos.

Lo que hago es comprimir todos los gráficos que necesito en archivos ZIP con contraseña de manera que sólo el programador sabe la clave de los mismos. De este modo mantenemos la propiedad intelectual del trabajo realizado por el diseñador gráfico, evitando a sí que cualquier pardillo coja nuestros gráficos y haga un juego web con flash en cuatro días llevándose el mérito.

Es algo más complejo de lo normal pero merece la pena. Veamos primero todo el procedimiento y luego analicemos sus partes:

constructor TTextura.Create(Origen: PSDL_Surface; xInicial, yInicial, iAncho,
iAlto, iIncrementoX, iIncrementoY, iAnchoTex, iAltoTex,
iAnchoSub, iAltoSub: Integer; bSubsprites: Boolean);
var
Temp: PSDL_Surface;
Org, Des: TSDL_Rect;
iLongitudX, iLongitudY: Integer;
begin
// Guardamos el ancho y alto de la textura que vamos a crear
Self.iAncho := iAncho;
Self.iAlto := iAlto;
iIncX := iIncrementoX;
iIncY := iIncrementoY;
Self.iAnchoTex := iAnchoTex;
Self.iAltoTex := iAltoTex;

// Definimos las coordenadas de los dos triángulos de la textura
if bSubsprites then
begin
iLongitudX := iAnchoSub;
iLongitudY := iAltoSub;
end
else
begin
iLongitudX := iAncho;
iLongitudY := iAlto;
end;

Triangulo[1] := TTriangulo.Create( 0.0, iLongitudY, 0.0, 0.0, 1.0,
0.0, 0.0, 0.0, 0.0, 0.0,
iLongitudX, 0.0, 0.0, 1.0, 0.0);

Triangulo[2] := TTriangulo.Create( 0.0, iLongitudY, 0.0, 0.0, 1.0,
iLongitudX, 0.0, 0.0, 1.0, 0.0,
iLongitudX, iLongitudY, 0.0, 1.0, 1.0);

// Creamos una nueva superficie con canal alpha
if SDL_BYTEORDER = SDL_BIG_ENDIAN then
Temp := SDL_CreateRGBSurface(SDL_SRCALPHA, iAncho, iAlto, 32, $FF000000,
$00FF0000, $0000FF00, $000000FF)
else
Temp := SDL_CreateRGBSurface(SDL_SRCALPHA, iAncho, iAlto, 32, $000000FF,
$0000FF00, $00FF0000, $FF000000);

// Activamos el canal alpha en la superficie origen antes de copiarla
SDL_SetAlpha(Origen, 0, 0);

// Copiamos la superficie origen a la superficie temporal
Org.x := xInicial;
Org.y := yInicial;

// Evitamos que copie un trozo más ancho que el de la textura
if Origen.w > iAncho then
Org.w := iAncho
else
Org.w := Origen.w;

// Evitamos que copie un trozo más ancho que el de la textura
if Origen.h > iAlto then
Org.h := iAlto
else
Org.h := Origen.h;

Des.x := 0;
Des.y := 0;
Des.w := Org.w;
Des.h := Org.h;
SDL_BlitSurface(Origen, @Org, Temp, @Des);

// Le pedimos a OpenGL que nos asigne un identificador de la textura
glGenTextures(1, ID);

// Enviamos la textura a la superficie de vídeo con OpenGL
glBindTexture(GL_TEXTURE_2D, ID);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // Filtro de
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); // suavizado
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); // Para que no se
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); // repita la textura
glTexImage2D(GL_TEXTURE_2D, 0, 4, iAncho, iAlto, 0, GL_RGBA,
GL_UNSIGNED_BYTE, Temp.pixels);

// Liberamos la superficie temporal
SDL_FreeSurface(Temp);
end;

En primer lugar defino una supercifie, dos rectángulos y las longitudes horizontales y verticales de la textura:

var
Temp: PSDL_Surface;
Org, Des: TSDL_Rect;
iLongitudX, iLongitudY: Integer;

Luego guardamos todo lo que nos pasan como parámetro en las variables de la clase:

Self.iAncho := iAncho;
Self.iAlto := iAlto;
iIncX := iIncrementoX;
iIncY := iIncrementoY;
Self.iAnchoTex := iAnchoTex;
Self.iAltoTex := iAltoTex;

Determinamos el ancho y alto de la textura según si vamos a dibujar todo el sprite o una parte del mismo (subsprites):

if bSubsprites then
begin
iLongitudX := iAnchoSub; // ancho subsprite
iLongitudY := iAltoSub; // alto subsprite
end
else
begin
iLongitudX := iAncho; // ancho sprite
iLongitudY := iAlto; // alto sprite
end;

Ahora creamos los dos tríangulos dentro de nuestra textura:

Triangulo[1] := TTriangulo.Create( 0.0, iLongitudY, 0.0, 0.0, 1.0,
0.0, 0.0, 0.0, 0.0, 0.0,
iLongitudX, 0.0, 0.0, 1.0, 0.0);

Triangulo[2] := TTriangulo.Create( 0.0, iLongitudY, 0.0, 0.0, 1.0,
iLongitudX, 0.0, 0.0, 1.0, 0.0,
iLongitudX, iLongitudY, 0.0, 1.0, 1.0);

Mientras las coordenadas de los vértices vayan en el sentido de las agujas del reloj, da igual su orden. Creamos una nueva superficie activando el canal alfa:

if SDL_BYTEORDER = SDL_BIG_ENDIAN then
Temp := SDL_CreateRGBSurface(SDL_SRCALPHA, iAncho, iAlto, 32, $FF000000,
$00FF0000, $0000FF00, $000000FF)
else
Temp := SDL_CreateRGBSurface(SDL_SRCALPHA, iAncho, iAlto, 32, $000000FF,
$0000FF00, $00FF0000, $FF000000);

SDL_SetAlpha(Origen, 0, 0);

Después copiamos a la superficie creada el trozo de sprite que vamos a aplicar como textura:

Org.x := xInicial;
Org.y := yInicial;

// Evitamos que copie un trozo más ancho que el de la textura
if Origen.w > iAncho then
Org.w := iAncho
else
Org.w := Origen.w;

// Evitamos que copie un trozo más ancho que el de la textura
if Origen.h > iAlto then
Org.h := iAlto
else
Org.h := Origen.h;

Des.x := 0;
Des.y := 0;
Des.w := Org.w;
Des.h := Org.h;
SDL_BlitSurface(Origen, @Org, Temp, @Des);

Por último, subimos la textura a la memoria de vídeo pidiéndole a OpenGL que nos proporcione un identificador para la misma y liberamos la superficie temporal:

glGenTextures(1, ID);

// Enviamos la textura a la superficie de vídeo con OpenGL
glBindTexture(GL_TEXTURE_2D, ID);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // Filtro de
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); // suavizado
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); // Para que no se
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); // repita la textura
glTexImage2D(GL_TEXTURE_2D, 0, 4, iAncho, iAlto, 0, GL_RGBA,
GL_UNSIGNED_BYTE, Temp.pixels);

// Liberamos la superficie temporal
SDL_FreeSurface(Temp);

Sólo queda eliminar la textura en su destructor para no dejar nada en la memoria de vídeo:

destructor TTextura.Destroy;
var Textura: GLuint;
begin
Textura := ID;
glDeleteTextures(ID,Textura);
Triangulo[2].Free;
Triangulo[1].Free;
inherited;
end;

En el próximo artículo entraremos con la clase TSprite y veremos como se carga desde un archivo ZIP nuestras texturas en archivos PNG utilizando el componente ZipForge.

Pruebas realizadas en Delphi 7.

2 comentarios:

Nelson dijo...

He estado siguiento los post que has hecho acerca de SDL, y me parece una buena guia para los que comienzan con el tema.

Ahora despues de pasar por los pasos basicos de SDL, he querido comenzar como nos recomiendas el manejo de SDL con OpenGL.

Ahora, el caso es que probando las sentensias que pones en este articulo en delphi 7, me da un error de compilacion, diciendome que no encuentra el procedimiento glGenTextures, ya he incluido la Unit OpenGL, pero no he resuelto el problema, tal vez tenga que incluir otra unit, pero cual???

Saludos.

Administrador dijo...

Tienes razón, me faltó añadir esta declaración en la unidad UJuego.pas que llama a estas funciones de OpenGL

// Fundiones externas especiales de OpenGL

procedure glGenTextures(n: GLsizei; var textures: GLuint); stdcall; external opengl32;

procedure glBindTexture(target: GLenum; texture: GLuint); stdcall; external opengl32;

procedure glDeleteTextures(target: GLenum; var texture: GLuint); stdcall; external opengl32;

Hay que declararlas en la interfaz, justo antes de la palabra implementation.

Saludos.

Publicidad