domingo, 24 de abril de 2011

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

En esta segunda parte veremos dos técnicas que mejorarán el ejemplo anterior. La primera será crear una estructura para todas las propiedades de nuestros vértices. Si nos damos cuenta de cada vértice cada vez vamos a necesitar más información, empezamos por la posición, el color, luego añadimos las coordenadas de textura y para técnicas de iluminación o normal map necesitaremos la normal, la binomial y la tangente por lo que tendríamos que tener un motón de arrays que pasar al VertexShader.

La forma más elegante de resolver esto es usar una estructura básica para nuestros vértices. Para nuestro ejemplo en principio sólo necesitamos conocer la posición y las coordenadas de textura por lo que nuestra estructura será:

struct VertexType{
public Vector3 position;
public Vector2 textureCoordinates;
}


Una vez tenemos nuestra estructura vamos a definir nuestro cubo. En la primera parte de este tutorial usamos para dibujar la función GL.DrawArrays de OpenGL lo cual nos provocaba tener que especificarle los 3 vértices de cada triángulo y teníamos muchos vértices repetidos en el array de vértices que le pasábamos a nuestro VertexShader.

Esto se soluciona usando el método de dibujado GL.DrawElements ya que nos permite especificarle los triangulos a través de un array de índices. De esta forma nosotros ahora sólo tenemos que especificar los 8 vértices que tiene nuestro cubo y luego pasar el array de índices que cuenta con combinaciones de estos vértices.



por lo que especificaremos los 8 vértices:

vertexArray = new VertexType[8];
vertexArray[0].position = new Vector3(-0.5f,-0.5f,0.0f);
vertexArray[0].textureCoordinates = new Vector2(0,1);
vertexArray[1].position = new Vector3(0.5f,-0.5f,0.0f);
vertexArray[1].textureCoordinates = new Vector2(1,1);
vertexArray[2].position = new Vector3(0.5f,0.5f,0.0f);
vertexArray[2].textureCoordinates = new Vector2(1,0);
vertexArray[3].position = new Vector3(-0.5f,0.5f,0.0f);
vertexArray[3].textureCoordinates = new Vector2(0,0);
vertexArray[4].position = new Vector3(-0.5f,-0.5f,1.0f);
vertexArray[4].textureCoordinates = new Vector2(0,1);
vertexArray[5].position = new Vector3(0.5f,-0.5f,1.0f);
vertexArray[5].textureCoordinates = new Vector2(1,1);
vertexArray[6].position = new Vector3(0.5f,0.5f,1.0f);
vertexArray[6].textureCoordinates = new Vector2(1,0);
vertexArray[7].position = new Vector3(-0.5f,0.5f,1.0f);
vertexArray[7].textureCoordinates = new Vector2(0,0);


y ahora vamos a especificar los índices o lo que es lo mismo como combinar dichos 8 vértices para construir los triángulos de nuestro cubo.

ushort[] indexes = {0,1,2,0,2,3,
4,5,1,4,1,0,
4,0,3,4,3,7,
1,5,6,1,6,2,
6,7,3,6,3,2,
5,4,7,5,7,6};


(Nota. Como podeís observar en el array de índices, cada fila sería una cara de nuestro cubo. Ej. la cara frontal estaría compuesta por 2 triángulos formados por los vértices 0-1-2 y 0-2-3)

Bien pues ahora a nuestra función de dibujado GL.DrawElements le pasamos:

GL.DrawElements(All mode, int count, All type, IntPtr indices)
- mode puede valer (All.Points/All.Lines/All.LineStrip,/All.LineLoop/ All.Triangles/ All.TriangleStrip / All.TriangleFan)
- count es el número total de índices que vamos a pasarle.
- type especifica el tipo de dato que alberga el array de índices, (All.UnsignedShort para nuestro ejemplo)
- indices que será el array de índices.

Una cosa importante en este punto es detectar que le tenemos que enviar un puntero a nuestra estructura y no nuestra propia estructura para ellos haremos uso de la clase GCHandle.

GCHandle indexesHandle = GCHandle.Alloc(indexes, GCHandleType.Pinned);


De esta forma tenemos un puntero a nuestro array de índices y para llamar al GL.DrawElements haremos:

GL.DrawElements(All.Triangles, indexes.Length, All.UnsignedShort, indexesHandle.AddrOfPinnedObject());


Bien pues una cosa similar vamos a hacer con nuestra estructura de vértices para poder pasarsela a nuestro VertexShader, lo primero creamos un puntero a nuestra estructura.

GCHandle vertexArrayHandle = GCHandle.Alloc(vertexArray, GCHandleType.Pinned);


Y para poder inyectar los vértices en el VertexShader:

int size = sizeof(float)*3+sizeof(float)*2;
GL.VertexAttribPointer(attributePosition,3,All.Float,false,size,vertexArrayHandle.AddrOfPinnedObject());
GL.VertexAttribPointer(attributeTexCoord,2,All.Float,false,size,(IntPtr)((uint)vertexArrayHandle.AddrOfPinnedObject()+(uint)(sizeof(float)*3)));


Donde size es la variable que calcula el tamaño de nuestra estructura compuesta por un Vector3 y un Vector2 de ahí el sizeof(float)*3 + sizeof(float)*2 que nos será útil pasar al VertexAttribPointer para que conozca la unidad básica o stride que es como se conoce el parámetro. También añadir que para las coordenadas de texturas hemos tenido que sumar un offset a nuestro puntero que salte el Vector3 de la posición de los vértices y empiece siempre con las coordenadas de textura. Si tuvieramos más atributos para nuestros vértices tendríamos que ir realizando diferentes offset.

Bueno pues con la creación de estructuras y la utilización del método de pintado GL.DrawElements el código final de nuestra clase EAGLView.cs nos queda:

#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 Cube3D2
{
public partial class EAGLView : iPhoneOSGameView
{
int viewportWidth, viewportHeight;
int program;

struct VertexType{
public Vector3 position;
public Vector2 textureCoordinates;
}

VertexType[] vertexArray;
GCHandle vertexArrayHandle;

ushort[] indexes = {0,1,2,0,2,3,
4,5,1,4,1,0,
4,0,3,4,3,7,
1,5,6,1,6,2,
6,7,3,6,3,2,
5,4,7,5,7,6};
GCHandle indexesHandle;

//Texture
int textureId;

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;

//Structure
vertexArray = new VertexType[8];
vertexArray[0].position = new Vector3(-0.5f,-0.5f,0.0f);
vertexArray[0].textureCoordinates = new Vector2(0,1);
vertexArray[1].position = new Vector3(0.5f,-0.5f,0.0f);
vertexArray[1].textureCoordinates = new Vector2(1,1);
vertexArray[2].position = new Vector3(0.5f,0.5f,0.0f);
vertexArray[2].textureCoordinates = new Vector2(1,0);
vertexArray[3].position = new Vector3(-0.5f,0.5f,0.0f);
vertexArray[3].textureCoordinates = new Vector2(0,0);
vertexArray[4].position = new Vector3(-0.5f,-0.5f,1.0f);
vertexArray[4].textureCoordinates = new Vector2(0,1);
vertexArray[5].position = new Vector3(0.5f,-0.5f,1.0f);
vertexArray[5].textureCoordinates = new Vector2(1,1);
vertexArray[6].position = new Vector3(0.5f,0.5f,1.0f);
vertexArray[6].textureCoordinates = new Vector2(1,0);
vertexArray[7].position = new Vector3(-0.5f,0.5f,1.0f);
vertexArray[7].textureCoordinates = new Vector2(0,0);
vertexArrayHandle = GCHandle.Alloc(vertexArray, GCHandleType.Pinned);

indexesHandle = GCHandle.Alloc(indexes, GCHandleType.Pinned);

// 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
int size = sizeof(float)*3+sizeof(float)*2;
GL.VertexAttribPointer(attributePosition,3,All.Float,false,size,vertexArrayHandle.AddrOfPinnedObject());
GL.VertexAttribPointer(attributeTexCoord,2,All.Float,false,size,(IntPtr)((uint)vertexArrayHandle.AddrOfPinnedObject()+(uint)(sizeof(float)*3)));

//GL.DrawArrays (All.Triangles, 0, vertexArray);
GL.DrawElements(All.Triangles, indexes.Length, All.UnsignedShort, indexesHandle.AddrOfPinnedObject());

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);
}

}
}