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