domingo, 17 de abril de 2011

OpenGL-ES2.0 y Monotouch II (textures)

Hoy publico un nuevo ejemplo de cómo cargar y renderizar texturas con Monotouch y OpenGL ES2.0. En el anterior artículo publiqué como crear un poligono en pantalla el ejemplo de hoy es un poco más complejo aunque sólo amplía el código del artículo anterior, por lo que si no lo has leido te recomiendo que lo hagas "OpenGLES2.0 y Monotouch I".

Lo siguiente a cuando ya sabemos crear polígonos es poder aplicarles texturas, para ello es muy importante tener algunos conceptos claros.

Lo primero vamos a crear un square en pantalla esto sería:


public float[] squareVertices = { -0.5f, -0.5f,
   0.5f, -0.5f,
   -0.5f, 0.5f,
   0.5f, 0.5f };


Con esto definimos los cuatro vertices que tendrá nuestro cuadrado.
A la hora de texturizarlo es importante conocer lo que son las coordenadas de texturas. Estas permiten a los polígonos posicionar la textura sobre ellos. Estas suelen llamarse (u,v) o (s,t) y es un vector de 2 float en el que sus valores estan entre [0-1] por cada vertice de nuestro poligono.

Para que la textura cubra totalmente nuestro square debemos definir un array de 4 vectores (u,v) que son las coordenadas de textura de cada vertice.



Las definimos como:

float[] texCoords = new float[] { 0.0f,1.0f,
  1.0f,1.0f,
  0.0f,0.0f,
  1.0f,0.0f
};


Ahora Vamos a definir el Vertex shader y el Fragment shader:

Vertex Shader



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


Definimos el atributo aTexCoord que es dónde cargaremos las coordenadas de textura del vertice que en ese momento estemos procesando. Pero como la única salida de un Vertex Shader es a través de variables de tipo varying definimos también vTexCoord al que se le copiaran los valores.

Y ahora el Fragment Shader


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


En el fragment shader tenemos como variable de entrada vTexCoord que fue pasada por el vertex shader y se define un variable de tipo Uniform que es la que albergará los pixeles de la textura. Finalmente para aplicar la textura llamamos a la función texture2D y el pasamos la textura y las coordenadas de textura que le corresponden y esta pintará correctamente la textura sobre el polígono.

Ahora necesitamos cargar la textura. Para ello desde código OpenGLES2 puro escribimos:


//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,0);

Creamos un objeto textura con la función GenTextures

Y luego muy importante activamos el canal de textura0 que tiene OpenGL-ES, básicamente le estamos diciendo que vamos a trabajar sobre el primer canal le decimos también que es una textura 2D y no una textura cúbica con la función BindTexture y le decimos que la textura cargada será enlazada contra la primera variable de tipo uniform con Uniform1.

Para cargar la textura usaremos las clases que wrapea monotouch sobre IPhone, que es la clase UIImage.


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


(Nota. La imagen debe ser multiplo de 2 para que la cargue opengl-es, es decir, 2-4-8-16-32-64... En otro tutorial explicaré como adaptar nuestra imagen dada la resolución que sea).

Tener en cuenta que la imagen que estoy cargando es un retrato de Mario Bros en formato PNG que como sabeis tiene formato RGBA. de hay que el espacio reservado sea 4(bytes) por image.width para cada fila de nuestra variable data y tantas columnas como image.height.

Una vez la imagen cargada para enlazarla a nuestro objeto textura escribiremos:


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


Con esto le decimos que es una imagen de tipo RGBA (con canal alpha) y que el contenido son bytes sin signos.

Una vez cargada le aplicamos los filtros y el wrap a la textura:


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


En otro tutorial explicaré los filtros posibles, así como los 3 modos de wrap que soporta OpenGL-ES2.0.

Y por último y no menos importante liberar el espacio reservado:

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


En el método draw aplicaremos al vertex shader los vértices y las coordenadas de textura de la siguiente forma:

//Active params
GL.EnableVertexAttribArray (0);
GL.EnableVertexAttribArray (1);


//Set params
GL.VertexAttribPointer(0,2,All.Float,false,2*sizeof(float),squareVertices);
GL.VertexAttribPointer(1,2,All.Float,false,2*sizeof(float),texCoords);


Y listo, si quereis probarlo sólo necesitais tener monodevelop con Monotouch instalado en vuestro MacOSX. Crear un proyecto de tipo Monotouch OpenGL y modificar el código de la clase llamada EAGLView.cs por:




#define OPENGLES2


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

#if OPENGLES2
using OpenTK.Graphics.ES20;
#else
using OpenTK.Graphics.ES11;
#endif
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 Texturing
{
public partial class EAGLView : iPhoneOSGameView
{
int viewportWidth, viewportHeight;
int program;

float[] squareVertices = { -0.5f,-0.5f,
   0.5f,-0.5f,
   -0.5f,0.5f,
   0.5f,0.5f
};
// byte[] pixels = {255,0,0, //Red
// 0,255,0, //Green
//          0,0,255, //Blue
// 255,255,0 //Yellow
// };

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

IntPtr data;


[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 ()
{
#if OPENGLES2
ContextRenderingApi = EAGLRenderingAPI.OpenGLES2;
base.CreateFrameBuffer();
Initialize();
#else
ContextRenderingApi = EAGLRenderingAPI.OpenGLES1;
base.CreateFrameBuffer();
#endif
}

                                

#if OPENGLES2

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

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

string fragmentShaderSrc = @"precision mediump float;
varying vec2 vTexCoord;
uniform sampler2D sTexture;
            void main()                                
            {                                        
              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, 0, "aPosition");
GL.BindAttribLocation (program, 1, "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");
}

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


//Use tightly packed data
//GL.PixelStore(All.UnpackAlignment, 1);

//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,0);

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

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


#endif

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


#if OPENGLES2
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.Viewport (0, 0, viewportWidth, viewportHeight);
GL.UseProgram (program);

//Active params
GL.EnableVertexAttribArray (0);
GL.EnableVertexAttribArray (1);

//Set params
GL.VertexAttribPointer(0,2,All.Float,false,2*sizeof(float),squareVertices);
GL.VertexAttribPointer(1,2,All.Float,false,2*sizeof(float),texCoords);

GL.DrawArrays (All.TriangleStrip, 0, 4);

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

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

float[] squareVertices = { -0.5f, -0.5f, 0.5f, -0.5f, -0.5f, 0.5f, 0.5f, 0.5f };
byte[] squareColors = { 255, 255, 0, 255, 0, 255, 255, 255, 0, 0,
0, 0, 255, 0, 255, 255 };

MakeCurrent ();
GL.Viewport (0, 0, Size.Width, Size.Height);

GL.MatrixMode (All.Projection);
GL.LoadIdentity ();
GL.Ortho (-1.0f, 1.0f, -1.5f, 1.5f, -1.0f, 1.0f);
GL.MatrixMode (All.Modelview);
GL.Rotate (3.0f, 0.0f, 0.0f, 1.0f);

GL.ClearColor (0.5f, 0.5f, 0.5f, 1.0f);
GL.Clear ((uint)All.ColorBufferBit);

GL.VertexPointer (2, All.Float, 0, squareVertices);
GL.EnableClientState (All.VertexArray);
GL.ColorPointer (4, All.UnsignedByte, 0, squareColors);
GL.EnableClientState (All.ColorArray);

GL.DrawArrays (All.TriangleStrip, 0, 4);

SwapBuffers ();
}
#endif
}
}