sábado, 23 de abril de 2011

OpenGL-ES2.0 y Monotouch IV (Cube3D parte1)

Siguiendo con el aprendizaje de OpenGLES2.0 hoy voy a escribir un tutorial de como crear un cubo en 3D y texturizarlo. Esta tutorial es muy facil de entender si habéis leido los anteriores.

Lo primero que necesitamos es definir nuestro cubo 3D, para ellos tenemos que entender como OpenGLES2.0 pinta objetos o mejor dicho triangulos en pantalla. OpenGLES2.0 tiene dos métodos para pintar:

GL.DrawArrays
GL.DrawElements

En los tutoriales anteriores hemos usado siempre DrawArrays pero a la hora de pintar un objeto más complejo que un quad es mucho más interesante usar DrawElements. De todas formas en esta primera parte vamos a usar el método DrawArrays y ya en la segunda parte veremos las ventajas que nos plantea usar DrawElements.

Bueno como ya sabéis el método DrawArrays tiene la siguiente declaración:

GL.DrawArrays(All mode, int first, int count);

- mode -> (All.Points/All.Lines/All.LineStrip,/All.LineLoop/ All.Triangles/ All.TriangleStrip / All.TriangleFan)
- first -> Indica el índice del primer vertices a pintar
- count -> Indica el número total de vertices a pintar


Por lo tanto para pintar nuestro cubo necesitaremos descomponerlo en triangulos. Un cubo tiene 6 caras y cada cara se puede descomponer en 2 triangulos que son 3 vértices, por lo que tendremos un total de 36 vertices.




float[] squareVertices = { //Front
   -0.5f,-0.5f,0.0f, //0
   0.5f,-0.5f,0.0f, //1
   0.5f,0.5f,0.0f, //2
   -0.5f,-0.5f,0.0f, //0
   0.5f,0.5f,0.0f, //2
   -0.5f,0.5f,0.0f, //3
  //Botton
-0.5f,-0.5f,1.0f, //4  
           0.5f,-0.5f,1.0f, //5
       0.5f,-0.5f,0.0f, //1
   -0.5f,-0.5f,1.0f, //4
   0.5f,-0.5f,0.0f, //1
-0.5f,-0.5f,0.0f, //0
//Left
   -0.5f,-0.5f,1.0f, //4
       -0.5f,-0.5f,0.0f, //0
   -0.5f,0.5f,0.0f, //3
   -0.5f,-0.5f,1.0f, //4
   -0.5f,0.5f,0.0f, //3
   -0.5f,0.5f,1.0f, //7

//Right
   0.5f,-0.5f,0.0f, //1
   0.5f,-0.5f,1.0f, //5
   0.5f,0.5f,1.0f, //6
   0.5f,-0.5f,0.0f, //1
   0.5f,0.5f,1.0f, //6
   0.5f,0.5f,0.0f, //2
   //Top
0.5f,0.5f,1.0f, //6
-0.5f,0.5f,1.0f, //7
-0.5f,0.5f,0.0f, //3
0.5f,0.5f,1.0f, //6
-0.5f,0.5f,0.0f, //3
   0.5f,0.5f,0.0f, //2

//Back
0.5f,-0.5f,1.0f, //5
-0.5f,-0.5f,1.0f, //4
-0.5f,0.5f,1.0f, //7
0.5f,-0.5f,1.0f, //5
-0.5f,0.5f,1.0f, //7
0.5f,0.5f,1.0f //6   
};


Es importante el orden en el que especificáis los vértices ya que indican hacia donde apunta la normal del triangulo o lo que es lo mismo desde donde se va a ver el triangulo. Yo uso el sentido antihorario que es el estandar también llamado en física "la regla de la mano derecha" donde siguiendo el sentido en el que los dedos de la mano derecha rodearía en triangulo el pulgar sería el vector normal del triangulo.

De la misma forma necesitamos también especificar las coordenadas de textura para cada vértice por lo tanto tendremos que construir el array de coordenadas de texturas:


flfloat[] texCoords = new float[] { 0.0f,1.0f,
  1.0f,1.0f,
  1.0f,0.0f,
  0.0f,1.0f,
  1.0f,0.0f,
  0.0f,0.0f,

  0.0f,1.0f,
  1.0f,1.0f,
  1.0f,0.0f,
  0.0f,1.0f,
  1.0f,0.0f,
  0.0f,0.0f,

  0.0f,1.0f,
  1.0f,1.0f,
  1.0f,0.0f,
  0.0f,1.0f,
  1.0f,0.0f,
  0.0f,0.0f,

  0.0f,1.0f,
  1.0f,1.0f,
  1.0f,0.0f,
  0.0f,1.0f,
  1.0f,0.0f,
  0.0f,0.0f,

  0.0f,1.0f,
  1.0f,1.0f,
  1.0f,0.0f,
  0.0f,1.0f,
  1.0f,0.0f,
  0.0f,0.0f,

  0.0f,1.0f,
  1.0f,1.0f,
  1.0f,0.0f,
  0.0f,1.0f,
  1.0f,0.0f,
  0.0f,0.0f
};


(Nota. Seguro que ya empezáis a notar que debe haber una forma más cómoda y menos repetitiva de definir un cubo, bien lo veremos en la segunda parte)

Bien ahora una de las cosas que vamos a añadirle a este tutorial es que el cubo este girando, el motivo es que si no, no podríamos verlo desde todas sus caras y no podríamos comprobar si está correctamente construido y todas sus caras están correctamente texturizadas.

Para ello vamos a definir las típicas 3 matrices (World / View / Projection):


matWorld = Matrix4.Identity;
float aspectRatio = (float) (Size.Width) / (float)(Size.Height);
matProjection = Matrix4.CreatePerspectiveFieldOfView(((float)(Math.PI) / 180.0f)*45.0f, aspectRatio, 1.0f,20.0f);
matView = Matrix4.CreateRotationX(0.5f)*Matrix4.CreateTranslation(0,0,-8);
matWorldViewProjection = matWorld * matView * matProjection;


Bien pues modificando la matrix del mundo podremos hacer que nuestro cubo 3d esté rotando.


float rotateY = 0;

protected override void OnRenderFrame (FrameEventArgs e)
{
                        ...

matWorld = Matrix4.CreateRotationY((float)e.Time + rotateY) * Matrix4.CreateTranslation(Vector3.Zero);
matWorldViewProjection = matWorld * matView * matProjection;


(Nota. Por motivos de eficiencia vamos a pasarle al VertexShader la multiplicación de las 3 matrices ya hecha de manera que
no sea necesario que el la haga por cada vertice. Por lo que no se puede olvidar modificar la matriz matWorldViewProjection cada vez que modifiquemos la matriz del mundo.)


Nuestro VertexShader tendrá pues una variable de tipo uniform que albergará la matriz resultado de la multiplicación de las 3 matrices. Por lo que nuestro VertexShader será:


string vertexShaderSrc =  @"uniform mat4 uMVPMatrix;
attribute vec4 aPosition;
attribute vec2 aTexCoord;
varying vec2 vTexCoord;
    void main()                  
    {                        
   vTexCoord = aTexCoord;
       gl_Position = uMVPMatrix * aPosition;
    }";


Bien ahora el problema siguiente es setear el valor de la variable uniform del VertexShader. Para ello existen unas funciones dependiendo del tipo de estructura que quieras pasar llamadas GL.UniformX y concretamente para setear una variable de tipo matrix4x4 tenemos GL.UniformMatrix4. El problema es que dicha función no admite un objeto de tipo Matrix4 que es lo que tenemos sino que se le tenemos que pasar un puntero a nuestra matriz.

Para ello yo me he creado el siguiente método:


private void SetUniformMatrix4(int location, bool transpose, ref Matrix4 matrix)
{
unsafe
{
fixed (float* matrix_ptr = &matrix.Row0.X)
{
GL.UniformMatrix4(location,1,transpose,matrix_ptr);
}
}
}


(Nota. Como podéis observar el método usa código no seguro "unsafe" por lo que para que esto es funcione deberéis activar la posibilidad de ejecutar código no seguro en vuestro IDE).

Para terminar en nuestro método de dibujado deberemos especificar el sentido en el que vamos a dibujar los triángulos, a favor de las agujas del reloj o de forma antihoraria esto es conocido como el cullmode. En OpenGLES2.0 existen 3 funciones necesarias para especificar el cullmode:

GL.FrontFace(All type) -> (Donde type puede ser All.Cw (horario) o All.Ccw (antihorario)



Nosotros como explicamos anteriormente hemos especificado los triángulos de forma antihoraria por lo que usaremos All.Ccw.

GL.CullFace(All mode) -> (Donde mode puede ser All.Front / All.Back / All.FrontAndBack)

Donde indicamos cual de las caras del triangulo vamos a dibujar, la delantera, la trasera o las dos. El valor por defecto es las caras traseras por lo que nosotros setearemos esta a All.Front.

GL.Enable(All.CullFace)

Y por último y para que todo esto tenga efecto activaremos el cullmode de la misma forma que la mayoría de parámetros en OpenGL, llamando al método GL.Enable.

Como siempre os pongo el código completo de la clase EAGLView.cs de la plantilla de monotouch.

Paste your text here.#define OPENGLES2


using System;
using OpenTK.Platform.iPhoneOS;
using MonoTouch.CoreAnimation;
using OpenTK;

using OpenTK.Graphics.ES20;

using MonoTouch.Foundation;
using MonoTouch.ObjCRuntime;
using MonoTouch.OpenGLES;
using System.Text;
using System.Drawing;
using OpenTK.Platform;
using MonoTouch.CoreGraphics;
using MonoTouch.UIKit;
using System.Runtime.InteropServices;

namespace Cube3D
{
public partial class EAGLView : iPhoneOSGameView
{
int viewportWidth, viewportHeight;
int program;

float[] squareVertices = { //Front
-0.5f,-0.5f,0.0f, //0
0.5f,-0.5f,0.0f, //1
0.5f,0.5f,0.0f, //2
-0.5f,-0.5f,0.0f, //0
0.5f,0.5f,0.0f, //2
-0.5f,0.5f,0.0f, //3
//Botton
-0.5f,-0.5f,1.0f, //4
0.5f,-0.5f,1.0f, //5
0.5f,-0.5f,0.0f, //1
-0.5f,-0.5f,1.0f, //4
0.5f,-0.5f,0.0f, //1
-0.5f,-0.5f,0.0f, //0
//Left
-0.5f,-0.5f,1.0f, //4
-0.5f,-0.5f,0.0f, //0
-0.5f,0.5f,0.0f, //3
-0.5f,-0.5f,1.0f, //4
-0.5f,0.5f,0.0f, //3
-0.5f,0.5f,1.0f, //7

//Right
0.5f,-0.5f,0.0f, //1
0.5f,-0.5f,1.0f, //5
0.5f,0.5f,1.0f, //6
0.5f,-0.5f,0.0f, //1
0.5f,0.5f,1.0f, //6
0.5f,0.5f,0.0f, //2
//Top
0.5f,0.5f,1.0f, //6
-0.5f,0.5f,1.0f, //7
-0.5f,0.5f,0.0f, //3
0.5f,0.5f,1.0f, //6
-0.5f,0.5f,0.0f, //3
0.5f,0.5f,0.0f, //2

//Back
0.5f,-0.5f,1.0f, //5
-0.5f,-0.5f,1.0f, //4
-0.5f,0.5f,1.0f, //7
0.5f,-0.5f,1.0f, //5
-0.5f,0.5f,1.0f, //7
0.5f,0.5f,1.0f //6
};
// int[] indices = { 0,1,2,0,2,3,
// 0,3,4,0,4,5,
// 0,5,6,0,6,1,
// 7,6,1,7,1,2,
// 7,4,5,7,5,6,
// 7,2,3,7,3,4
// };

//Texture
int textureId;
float[] texCoords = new float[] { 0.0f,1.0f,
1.0f,1.0f,
1.0f,0.0f,
0.0f,1.0f,
1.0f,0.0f,
0.0f,0.0f,

0.0f,1.0f,
1.0f,1.0f,
1.0f,0.0f,
0.0f,1.0f,
1.0f,0.0f,
0.0f,0.0f,

0.0f,1.0f,
1.0f,1.0f,
1.0f,0.0f,
0.0f,1.0f,
1.0f,0.0f,
0.0f,0.0f,

0.0f,1.0f,
1.0f,1.0f,
1.0f,0.0f,
0.0f,1.0f,
1.0f,0.0f,
0.0f,0.0f,

0.0f,1.0f,
1.0f,1.0f,
1.0f,0.0f,
0.0f,1.0f,
1.0f,0.0f,
0.0f,0.0f,

0.0f,1.0f,
1.0f,1.0f,
1.0f,0.0f,
0.0f,1.0f,
1.0f,0.0f,
0.0f,0.0f
};

IntPtr data;

Matrix4 matWorldViewProjection, matProjection, matView, matWorld;
int uniformMat, uniformSampler;
int attributePosition = 0;
int attributeTexCoord = 1;

[Export("layerClass")]
static Class LayerClass ()
{
return iPhoneOSGameView.GetLayerClass ();
}

[Export("initWithCoder:")]
public EAGLView (NSCoder coder) : base(coder)
{
LayerRetainsBacking = false;
LayerColorFormat = EAGLColorFormat.RGBA8;
}

protected override void CreateFrameBuffer ()
{
ContextRenderingApi = EAGLRenderingAPI.OpenGLES2;
base.CreateFrameBuffer();
Initialize();
}

private bool Initialize()
{
viewportHeight = Size.Height;
viewportWidth = Size.Width;

// Vertex and fragment shaders
string vertexShaderSrc = @"uniform mat4 uMVPMatrix;
attribute vec4 aPosition;
attribute vec2 aTexCoord;
varying vec2 vTexCoord;
void main()
{
vTexCoord = aTexCoord;
gl_Position = uMVPMatrix * aPosition;
}";

string fragmentShaderSrc = @"precision mediump float;
varying vec2 vTexCoord;
uniform sampler2D sTexture;
void main()
{
//gl_FragColor = vec4(1.0,0.0,0.0,1.0);
gl_FragColor = texture2D(sTexture, vTexCoord);
}";

int vertexShader = LoadShader (All.VertexShader, vertexShaderSrc );
int fragmentShader = LoadShader (All.FragmentShader, fragmentShaderSrc );
program = GL.CreateProgram();
if (program == 0)
throw new InvalidOperationException ("Unable to create program");

GL.AttachShader (program, vertexShader);
GL.AttachShader (program, fragmentShader);

//Set position
GL.BindAttribLocation (program, attributePosition, "aPosition");
GL.BindAttribLocation (program, attributeTexCoord, "aTexCoord");


GL.LinkProgram (program);

int linked = 0;
GL.GetProgram (program, All.LinkStatus, ref linked);
if (linked == 0) {
// link failed
int length = 0;
GL.GetProgram (program, All.InfoLogLength, ref length);
if (length > 0) {
var log = new StringBuilder (length);
GL.GetProgramInfoLog (program, length, ref length, log);
Console.WriteLine ("GL2", "Couldn't link program: " + log.ToString ());
return false;
}

GL.DeleteProgram (program);
throw new InvalidOperationException ("Unable to link program");
}

//View
matWorld = Matrix4.Identity;
float aspectRatio = (float) (Size.Width) / (float)(Size.Height);
matProjection = Matrix4.CreatePerspectiveFieldOfView(((float)(Math.PI) / 180.0f)*45.0f, aspectRatio, 1.0f,20.0f);
matView = Matrix4.CreateRotationX(0.5f)*Matrix4.CreateTranslation(0,0,-8);
matWorldViewProjection = matWorld * matView * matProjection;

GetUniformVariables();

LoadTexture();

return true;
}

private int LoadShader ( All type, string source )
{
int shader = GL.CreateShader(type);

if ( shader == 0 )
throw new InvalidOperationException("Unable to create shader");

// Load the shader source
int length = 0;
GL.ShaderSource(shader, 1, new string[] {source}, (int[])null);

// Compile the shader
GL.CompileShader( shader );

int compiled = 0;
GL.GetShader (shader, All.CompileStatus, ref compiled);
if (compiled == 0) {
length = 0;
GL.GetShader (shader, All.InfoLogLength, ref length);
if (length > 0) {
var log = new StringBuilder (length);
GL.GetShaderInfoLog (shader, length, ref length, log);
Console.WriteLine("GL2", "Couldn't compile shader: " + log.ToString ());
}

GL.DeleteShader (shader);
throw new InvalidOperationException ("Unable to compile shader of type : " + type.ToString ());
}

return shader;

}

private void LoadTexture()
{

UIImage ui = UIImage.FromFile("mario3.jpg");
CGImage image = ui.CGImage;

CGColorSpace colorspace = CGColorSpace.CreateDeviceRGB();
data = Marshal.AllocHGlobal(image.Height*image.Width*4);
CGContext context = new CGBitmapContext(data,image.Width,image.Height,8,4*image.Width, colorspace,CGImageAlphaInfo.NoneSkipLast);
colorspace.Dispose();
context.ClearRect(new RectangleF(0,0,image.Width,image.Height));
context.DrawImage(new RectangleF(0,0,image.Width,image.Height),image);

//Generate a texture object
GL.GenTextures(1,ref textureId);

//Bind the texture object
GL.ActiveTexture(All.Texture0);
GL.BindTexture(All.Texture2D, textureId);
GL.Uniform1(textureId,uniformSampler);

//Load the texture
GL.TexImage2D(All.Texture2D, 0, (int)All.Rgba, image.Width, image.Height, 0 ,All.Rgba, All.UnsignedByte, data);

//Set the filtering mode
GL.TexParameter(All.Texture2D, All.TextureMinFilter, (float)All.Nearest);
GL.TexParameter(All.Texture2D, All.TextureMagFilter, (float)All.Nearest);

//Wrap setting
GL.TexParameter(All.Texture2D, All.TextureWrapS,(int)All.Repeat);
GL.TexParameter(All.Texture2D, All.TextureWrapT,(int)All.Repeat);

context.Dispose();
Marshal.FreeHGlobal(data);
}

protected override void ConfigureLayer (CAEAGLLayer eaglLayer)
{
eaglLayer.Opaque = true;
}

private void GetUniformVariables()
{
uniformMat = GL.GetUniformLocation(program, "uMVPMatrix");
uniformSampler = GL.GetUniformLocation(program, "sTexture");
}

private void SetUniformMatrix4(int location, bool transpose, ref Matrix4 matrix)
{
unsafe
{
fixed (float* matrix_ptr = &matrix.Row0.X)
{
GL.UniformMatrix4(location,1,transpose,matrix_ptr);
}
}
}

float rotateY = 0;

protected override void OnRenderFrame (FrameEventArgs e)
{
base.OnRenderFrame (e);

MakeCurrent();

GL.ClearColor (0.7f, 0.7f, 0.7f, 1);
GL.Clear ((int)All.ColorBufferBit);

GL.FrontFace(All.Ccw);
GL.CullFace(All.Front);
GL.Enable(All.CullFace);


GL.Viewport (0, 0, viewportWidth, viewportHeight);
GL.UseProgram (program);

matWorld = Matrix4.CreateRotationY((float)e.Time + rotateY) * Matrix4.CreateTranslation(Vector3.Zero);
matWorldViewProjection = matWorld * matView * matProjection;

SetUniformMatrix4(uniformMat,false,ref matWorldViewProjection);

//Active params
GL.EnableVertexAttribArray (attributePosition);
GL.EnableVertexAttribArray (attributeTexCoord);

//Set params
GL.VertexAttribPointer(attributePosition,3,All.Float,false,3*sizeof(float),squareVertices);
GL.VertexAttribPointer(attributeTexCoord,2,All.Float,false,2*sizeof(float),texCoords);

GL.DrawArrays (All.Triangles, 0, 36);
//GL.DrawElements(All.Triangles, indices.Length, All.Int, indices);

rotateY += (float)Math.PI / 270;

//Swapped buffer (Front and Back buffer)
SwapBuffers ();
}

protected override void OnDisposed (EventArgs e)
{
base.OnDisposed (e);
Marshal.FreeHGlobal (data);
if (textureId != 0)
GL.DeleteTextures(1, ref textureId);
}

}
}



Y el resultado en pantalla sería algo así: