Publicado el 10/10/2012 por

OpenTK: Múltiples vistas (GLControl + Windows.Forms)

Dificultad
Dominio de C#
Dominio de OpenGL
Dominio de Visual Studio

En este tutorial aprenderemos hacer una aplicación con múltiples vistas utilizando OpenTK y Windows.Forms. Una vista o viewport puede definirse como una ventana pequeña donde se despliega la escena vista desde cierta perspectiva. En nuestro caso crearemos tres vistas para porder observar un cubo desde ciertos angulos: una vista libre, superior y frontalmente.

Las vistas o viewports son muy utilizados en aplicaciones de diseño tridimensional o aplicaciones tipo CAD (Computer-Aided Design – Diseño Asistido por Computadora) como por ejemplo: Autodesk 3ds Max, Cinema 4D, Blender, etc.

Este será el resultado final
penTK: Múltiples vistas

Configuración del ambiente de trabajo

Lo primero que debemos hacer es crear nuestro proyecto de Visual Studio (para este tutorial estaré usando Visual Studio 2010), para ello nos vamos al menú Archivo > Nuevo > Proyecto. Se desplegará una ventana donde podremos elegir el tipo de proyecto que queremos crear y el directorio donde deseamos que se cree. Como nuestra aplicación será de Windows.Forms seleccionaremos Visual C# > Windows > Aplicación de Windows Forms, colocamos el nombre de proyecto y pulsamos el botón Aceptar.

penTK: Múltiples vistas

Esculpiendo nuestro formulario

Lo siguiente será agregar tres componentes GLControl de OpenTK a nuestro formulario (te recomiendo que leas la sección Construyendo la aplicación del tutorial Primera aplicación gráfica en OpenTK (GLControl + Windows.Forms) para mayores detalles sobre este punto). Luego de ser agregados configuraremos cada uno de ellos del siguiente modo:

El glControl1 deberá tener un tamaño de 200×200 píxeles y su color de fondo 224, 224, 224.

penTK: Múltiples vistas

La propiedad Dock del glControl2 deberá ser Fill.

penTK: Múltiples vistas

El glControl3 deberá tener un tamaño de 200×200 píxeles y su color de fondo DimGray.

penTK: Múltiples vistas

Por último debemos asegurar que glControl2 se encuentre por debajo de los otro componentes. Esto se logra haciendo pulsando clic derecho sobre el glControl2 y en el menú contextual pulsar Enviar al fondo.

penTK: Múltiples vistas

Finalmente ubicamos los GLControls a nuestro gusto para terminar el diseño del formulario.

Rutina, rutina y rutina…

En este punto crearemos los eventos necesarios para que nuestra aplicación funcione. El primer evento que crearemos será el Load de nuestro glControl1. Para agregar el evento simplemente seleccionamos el componente y nos vamos a la pestaña de Eventos (botón con un icono de trueno) de nuestro panel de propiedades y buscamos en la lista el evento Load, escribimos GlControl1Load y pulsamos la tecla Enter. Nos aparecerá nuestra vista de código fuente, especificamente el evento GlControl1Load. El mismo procedimiento debe ser seguido para agregar el evento Paint (debemos repetir ambos procedimientos para glControl2 y glControl3).

penTK: Múltiples vistas

El resultado será el siguiente código (o algo similar):

public partial class Form1 : Form
{
	public Form1()
	{
		InitializeComponent();
	}

	private void GlControl1Paint(object sender, PaintEventArgs e)
	{

	}

	private void GlControl2Paint(object sender, PaintEventArgs e)
	{

	}

	private void GlControl3Paint(object sender, PaintEventArgs e)
	{

	}

	private void GlControl1Load(object sender, EventArgs e)
	{

	}

	private void GlControl2Load(object sender, EventArgs e)
	{

	}

	private void GlControl3Load(object sender, EventArgs e)
	{

	}

	private void GlControl2Resize(object sender, EventArgs e)
	{

	}
}

Configuración del contexto OpenGL

Si todo marcha bien hasta este punto podemos continuar configurando nuestro contexto de OpenGL. Para ello agregaremos las siguientes líneas en los eventos Load de caga GLControl:

private void GlControl1Load(object sender, EventArgs e)
{
	GL.ClearColor(Color.GreenYellow);
	GL.Enable(EnableCap.DepthTest);
}

private void GlControl2Load(object sender, EventArgs e)
{
	GL.ClearColor(Color.SkyBlue);
	GL.Enable(EnableCap.DepthTest);
}

private void GlControl3Load(object sender, EventArgs e)
{
	GL.ClearColor(Color.DarkCyan);
	GL.Enable(EnableCap.DepthTest);
}

Lo que hemos hecho es indicarle a OpenGL los colores de fondo a utilizar para cada GLControl con la instrucción GL.ClearColor y seguidamente estamos habilitando una bandera para que OpenGL use “profundidad” ya que estaremos dibujando un cubo tridimensional.

Por último agregaremos las siguientes líneas a los eventos Paint para poder dibujar:

private void GlControl1Paint(object sender, PaintEventArgs e)
{
	GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
	glControl1.SwapBuffers();
}

private void GlControl2Paint(object sender, PaintEventArgs e)
{
	GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
	glControl2.SwapBuffers();
}

private void GlControl3Paint(object sender, PaintEventArgs e)
{
	GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
	glControl3.SwapBuffers();
}

Si probamos lo que hemos hecho hasta ahora obtendremos lo siguiente:

penTK: Múltiples vistas

Si eres detallista habrás notado que un sólo GLControl se está pintando con el color que le indicamos, entonces ¿Qué ha pasado con los demás?.

¡Escógeme!, ¡Escógeme!, ¡Escógeme!

En este punto empezamos a lidiar con la “problemática” de tener varios viewports en una aplicación. Si bien debemos trabajar un poco más para resolver los problemas que se presenten, les aseguro que el trabajo será menor si trabajamos ordenadamente.

Lo que está sucediendo es que los componentes GLControl “son ciegos”, es decir, no reconocen que junto a ellos existen otros componentes que están tratando de acceder a la tarjeta gráfica para realizar una operación. Por ese motivo cuando uno de los GLControl hace uso de la tarjeta gráfica simplemente “imposibilita” a los demás para que la usen. En resumen sólo podremos realizar operaciones sobre un GLControl a la vez, y cuando ese GLControl tome el control, los otros ya no podrán hacerlo.

Para resolver este problema debemos encontrar una forma de indicarle a cada GLControl que tome el control de la tarjeta gráfica. Afortunadamente existe una instrucción que nos facilita ese proceso: MakeCurrent. El método MakeCurrent le indicará al GLControl seleccionado que debe tomar el control para empezar a realizar operaciones.

Probemos añadiendo esas líneas en los eventos Load y los eventos Paint:

private void GlControl1Load(object sender, EventArgs e)
{
	glControl1.MakeCurrent();
	GL.ClearColor(Color.GreenYellow);
	GL.Enable(EnableCap.DepthTest);
}

private void GlControl2Load(object sender, EventArgs e)
{
	glControl2.MakeCurrent();
	GL.ClearColor(Color.SkyBlue);
	GL.Enable(EnableCap.DepthTest);
}

private void GlControl3Load(object sender, EventArgs e)
{
	glControl3.MakeCurrent();
	GL.ClearColor(Color.DarkCyan);
	GL.Enable(EnableCap.DepthTest);
}

 

private void GlControl1Paint(object sender, PaintEventArgs e)
{
	glControl1.MakeCurrent();
	GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
	glControl1.SwapBuffers();
}

private void GlControl2Paint(object sender, PaintEventArgs e)
{
	glControl2.MakeCurrent();
	GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
	glControl2.SwapBuffers();
}

private void GlControl3Paint(object sender, PaintEventArgs e)
{
	glControl3.MakeCurrent();
	GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
	glControl3.SwapBuffers();
}

Y probemos.

penTK: Múltiples vistas

Ahora todo funciona, ¡Perfecto!.

Rutina++

Si queremos empezar a desplegar elementos en pantalla primero debemos configurar nuestras matrices de Projection y Modelview. Este paso podremos lograrlo creando un método como el siguiente:

private void SetupViewport1()
{
	float aspectRatio = (float)glControl1.Width / (float)glControl1.Height;

	GL.Viewport(0, 0, glControl1.Width, glControl1.Height);
	GL.MatrixMode(MatrixMode.Projection);
	GL.LoadIdentity();

	Matrix4 perspective = Matrix4.CreatePerspectiveFieldOfView(MathHelper.PiOver4, aspectRatio, 0.01f, 2000f);
	GL.MultMatrix(ref perspective);

	GL.MatrixMode(MatrixMode.Modelview);
	GL.LoadIdentity();
}

Y asi para cada GLControl que tengamos… Mi sugerencia es que siempre intenten crear métodos genéricos para todo segmento de código que vean que están repitiendo constantemente (como en este caso).

Lo que haremos es crear un método genérico que reciba como parámetro un GLControl:

private void SetupViewport(GLControl glControl)
{
	float aspectRatio = (float)glControl.Width / (float)glControl.Height;

	GL.Viewport(0, 0, glControl.Width, glControl.Height);
	GL.MatrixMode(MatrixMode.Projection);
	GL.LoadIdentity();

	Matrix4 perspective = Matrix4.CreatePerspectiveFieldOfView(MathHelper.PiOver4, aspectRatio, 0.01f, 2000f);
	GL.MultMatrix(ref perspective);

	GL.MatrixMode(MatrixMode.Modelview);
	GL.LoadIdentity();
}

E invocamos el método en los eventos Load:

private void GlControl1Load(object sender, EventArgs e)
{
	glControl1.MakeCurrent();
	GL.ClearColor(Color.GreenYellow);
	GL.Enable(EnableCap.DepthTest);

	SetupViewport(glControl1);
}

private void GlControl2Load(object sender, EventArgs e)
{
	glControl2.MakeCurrent();
	GL.ClearColor(Color.SkyBlue);
	GL.Enable(EnableCap.DepthTest);

	SetupViewport(glControl2);
}

private void GlControl3Load(object sender, EventArgs e)
{
	glControl3.MakeCurrent();
	GL.ClearColor(Color.DarkCyan);
	GL.Enable(EnableCap.DepthTest);

	SetupViewport(glControl3);
}

Con esa configuración ya podemos empezar el despliegue de elementos.

Primer despliegue

Lo primero que haremos será desplegar un cuadrado en el glControl2. En este punto no me detendré a dar explicaciones desplegar primitivas, simplemente les enseñaré el código (sí quieres aprender paso a paso como desplegar elementos te recomiendo que leas el tutorial Despliegue básico de primitivas 3D en OpenTK).

Debemos agregar el siguiente código al evento Paint del glControl2:

private void GlControl2Paint(object sender, PaintEventArgs e)
{
	glControl2.MakeCurrent();
	GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

	Matrix4 lookat = Matrix4.LookAt(Vector3.One * 2, Vector3.Zero, Vector3.UnitY);
	GL.MatrixMode(MatrixMode.Modelview);
	GL.LoadMatrix(ref lookat);

	GL.Begin(BeginMode.Quads);
	{
		GL.Color3(Color.Orange);
		GL.Vertex3(-0.5f, 0.5f, 0.0f);
		GL.Vertex3(-0.5f, -0.5f, 0.0f);
		GL.Vertex3(0.5f, -0.5f, 0.0f);
		GL.Vertex3(0.5f, 0.5f, 0.0f);
	}
	GL.End();

	glControl2.SwapBuffers();
}

El resultado será el siguiente:

penTK: Múltiples vistas

Vistas independientes

Uno de los usos más frecuentes de los múltiples viewports es permitir a los usuarios ver diferentes elementos simultaneamente. El primer ejemplo que se me viene a la mente es la cabina de un arreglo de cámaras de seguridad, en donde podremos ver varios lugares desde una misma pantalla.

Para ejemplificar con código vamos a desplegar en cada viewport un cuadrado (todos de colores distintos). El código sería el siguiente:

private void GlControl1Paint(object sender, PaintEventArgs e)
{
	glControl1.MakeCurrent();
	GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

	Matrix4 lookat = Matrix4.LookAt(Vector3.UnitZ * 3, Vector3.Zero, Vector3.UnitY);
	GL.MatrixMode(MatrixMode.Modelview);
	GL.LoadMatrix(ref lookat);

	GL.Begin(BeginMode.Quads);
	{
		GL.Color3(Color.Red);
		GL.Vertex3(-0.5f, 0.5f, 0.0f);
		GL.Vertex3(-0.5f, -0.5f, 0.0f);
		GL.Vertex3(0.5f, -0.5f, 0.0f);
		GL.Vertex3(0.5f, 0.5f, 0.0f);
	}
	GL.End();

	glControl1.SwapBuffers();
}

private void GlControl2Paint(object sender, PaintEventArgs e)
{
	glControl2.MakeCurrent();
	GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

	Matrix4 lookat = Matrix4.LookAt(Vector3.One * 2, Vector3.Zero, Vector3.UnitY);
	GL.MatrixMode(MatrixMode.Modelview);
	GL.LoadMatrix(ref lookat);

	GL.Begin(BeginMode.Quads);
	{
		GL.Color3(Color.Orange);
		GL.Vertex3(-0.5f, 0.5f, 0.0f);
		GL.Vertex3(-0.5f, -0.5f, 0.0f);
		GL.Vertex3(0.5f, -0.5f, 0.0f);
		GL.Vertex3(0.5f, 0.5f, 0.0f);
	}
	GL.End();

	glControl2.SwapBuffers();
}

private void GlControl3Paint(object sender, PaintEventArgs e)
{
	glControl3.MakeCurrent();
	GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

	Matrix4 lookat = Matrix4.LookAt(Vector3.UnitZ * 3, Vector3.Zero, Vector3.UnitY);
	GL.MatrixMode(MatrixMode.Modelview);
	GL.LoadMatrix(ref lookat);

	GL.Begin(BeginMode.Quads);
	{
		GL.Color3(Color.Blue);
		GL.Vertex3(-0.5f, 0.5f, 0.0f);
		GL.Vertex3(-0.5f, -0.5f, 0.0f);
		GL.Vertex3(0.5f, -0.5f, 0.0f);
		GL.Vertex3(0.5f, 0.5f, 0.0f);
	}
	GL.End();

	glControl3.SwapBuffers();
}

El resultado es el siguiente:

penTK: Múltiples vistas

Vistas dependientes

A diferencia del ejemplo anterior esta configuración es utilizada cuando deseamos ver un mismo elemento desde puntos de vistas diferentes. Esta opción es muy usada en aplicaciones tipo CAD como mencione al principio del tutorial.

Para ejemplificar esta sección desplegaremos un cubo (ejemplo de otros tutoriales) visto desde un ángulo libre, desde arriba y frontalmente.

Lo primero será agregar el método para el despliegue del cubo:

private void DrawCube()
{
	GL.Begin(BeginMode.Quads);
	{
		// Frente
		GL.Color3(Color.Orange);
		GL.Vertex3(-0.5f, 0.5f, 0.5f);
		GL.Vertex3(-0.5f, -0.5f, 0.5f);
		GL.Vertex3(0.5f, -0.5f, 0.5f);
		GL.Vertex3(0.5f, 0.5f, 0.5f);

		// Atras
		GL.Color3(Color.Blue);
		GL.Vertex3(-0.5f, 0.5f, -0.5f);
		GL.Vertex3(-0.5f, -0.5f, -0.5f);
		GL.Vertex3(0.5f, -0.5f, -0.5f);
		GL.Vertex3(0.5f, 0.5f, -0.5f);

		// Derecha
		GL.Color3(Color.Lime);
		GL.Vertex3(0.5f, 0.5f, -0.5f);
		GL.Vertex3(0.5f, -0.5f, -0.5f);
		GL.Vertex3(0.5f, -0.5f, 0.5f);
		GL.Vertex3(0.5f, 0.5f, 0.5f);

		// Izquierda
		GL.Color3(Color.Magenta);
		GL.Vertex3(-0.5f, 0.5f, -0.5f);
		GL.Vertex3(-0.5f, -0.5f, -0.5f);
		GL.Vertex3(-0.5f, -0.5f, 0.5f);
		GL.Vertex3(-0.5f, 0.5f, 0.5f);

		// Arriba
		GL.Color3(Color.Cyan);
		GL.Vertex3(-0.5f, 0.5f, -0.5f);
		GL.Vertex3(-0.5f, 0.5f, 0.5f);
		GL.Vertex3(0.5f, 0.5f, 0.5f);
		GL.Vertex3(0.5f, 0.5f, -0.5f);

		// Abajo
		GL.Color3(Color.Yellow);
		GL.Vertex3(-0.5f, -0.5f, -0.5f);
		GL.Vertex3(-0.5f, -0.5f, 0.5f);
		GL.Vertex3(0.5f, -0.5f, 0.5f);
		GL.Vertex3(0.5f, -0.5f, -0.5f);
	}
	GL.End();
}

Seguidamente, remplazaremos el código de despliegue de cuadrados por el llamado al método DrawCube y cambiaremos la posición de la cámara en el evento GlControl3Paint para poder apreciar el cubo desde arriba:

private void GlControl1Paint(object sender, PaintEventArgs e)
{
	glControl1.MakeCurrent();
	GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

	Matrix4 lookat = Matrix4.LookAt(Vector3.UnitZ * 3, Vector3.Zero, Vector3.UnitY);
	GL.MatrixMode(MatrixMode.Modelview);
	GL.LoadMatrix(ref lookat);

	DrawCube();

	glControl1.SwapBuffers();
}

private void GlControl2Paint(object sender, PaintEventArgs e)
{
	glControl2.MakeCurrent();
	GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

	Matrix4 lookat = Matrix4.LookAt(Vector3.One * 2, Vector3.Zero, Vector3.UnitY);
	GL.MatrixMode(MatrixMode.Modelview);
	GL.LoadMatrix(ref lookat);

	DrawCube();

	glControl2.SwapBuffers();
}

private void GlControl3Paint(object sender, PaintEventArgs e)
{
	glControl3.MakeCurrent();
	GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

	Matrix4 lookat = Matrix4.LookAt(Vector3.UnitY * 3, Vector3.Zero, Vector3.UnitZ);
	GL.MatrixMode(MatrixMode.Modelview);
	GL.LoadMatrix(ref lookat);

	DrawCube();

	glControl3.SwapBuffers();
}

El resultado es el siguiente:

penTK: Múltiples vistas

¿Como hacer animaciones con múltiples vistas?

En realidad es muy parécido al procedimiento del tutorial Animaciones 3D en OpenTK (si no has hecho ninguna animación aún te recomiendo leerlo).

Lo primero que debemos hacer es crear nuestro hilo de programa para lograr la animación. Para ello agregamos el siguiente código al constructor de la clase:

public Form1()
{
	InitializeComponent();

	Application.Idle += ApplicationIdle;
}

Y seguidamente agregamos el evento ApplicationIdle y colocaremos el siguiente código:

void ApplicationIdle(object sender, EventArgs e)
{
	glControl1.Invalidate();
	glControl2.Invalidate();
	glControl3.Invalidate();
}

Con esa tres líneas estaremos repintando constantemente los tres GLControls. Lo siguiente será rotar el cubo en el eje Y.
Agregamos el siguiente atributo privado a nuestra clase:

private float _angle;

Y la incrementamos en el hilo de la aplicación del siguiente modo:

void ApplicationIdle(object sender, EventArgs e)
{
	_angle += MathHelper.DegreesToRadians(3);

	glControl1.Invalidate();
	glControl2.Invalidate();
	glControl3.Invalidate();
}

Con eso haremos que el ángulo de rotación incremente 3 grados cada vez.

Por último agregaremos las siguientes líneas en nuestros eventos Paint:

private void GlControl1Paint(object sender, PaintEventArgs e)
{
	glControl1.MakeCurrent();
	GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

	Matrix4 lookat = Matrix4.LookAt(Vector3.UnitZ * 3, Vector3.Zero, Vector3.UnitY);
	GL.MatrixMode(MatrixMode.Modelview);
	GL.LoadMatrix(ref lookat);

	GL.Rotate(_angle, Vector3.UnitY);
	DrawCube();

	glControl1.SwapBuffers();
}

private void GlControl2Paint(object sender, PaintEventArgs e)
{
	glControl2.MakeCurrent();
	GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

	Matrix4 lookat = Matrix4.LookAt(Vector3.One * 2, Vector3.Zero, Vector3.UnitY);
	GL.MatrixMode(MatrixMode.Modelview);
	GL.LoadMatrix(ref lookat);

	GL.Rotate(_angle, Vector3.UnitY);
	DrawCube();

	glControl2.SwapBuffers();
}

private void GlControl3Paint(object sender, PaintEventArgs e)
{
	glControl3.MakeCurrent();
	GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

	Matrix4 lookat = Matrix4.LookAt(Vector3.UnitY * 3, Vector3.Zero, Vector3.UnitZ);
	GL.MatrixMode(MatrixMode.Modelview);
	GL.LoadMatrix(ref lookat);

	GL.Rotate(_angle, Vector3.UnitY);
	DrawCube();

	glControl3.SwapBuffers();
}

Si probamos nuestra aplicación tendremos ¡tres viewports animados! (fácil).

penTK: Múltiples vistas

Casi terminamos

Con todo lo que hemos hecho hasta ahora podríamos decir que el contenido de este tutorial fue abarcado totalmente. Si en este punto maximizamos la ventana nos encontraremos con el siguiente error:

penTK: Múltiples vistas

Para solucionarlo basta con agregar en el evento Risize para el glControl2 y agregar el siguiente código:

private void GlControl2Resize(object sender, EventArgs e)
{
	glControl2.MakeCurrent();
	SetupViewport(glControl2);
}

Con esas instrucciones hemos concluido nuestra “mini-aplicación”.

Conclusión

En este tutorial aprendimos como trabajar con múltiples vistas y como configurar nuestro ambiente de trabajo para lograrlo. En este punto ya podemos empezar a desarrollar aplicaciones más complejas usando Windows.Forms y OpenTK.

Espero hayas aprendido mucho con este tutorial y de parte de todo el equipo de Widget 101 te agradezco haberlo seguido.

No olvides suscribirte para recibir boletines de actualizaciones directamente en tu bandeja de entrada, seguirnos en Twitter, hacerte fan en Facebook y compartir con tus amigos.

No olvides que compartir lo que conoces te hará crecer como profesional y como persona.

Etiquetas: , , , , ,

Sobre el formador:
Christiam Mena

Licenciado en Ciencias de la Computación de la Universidad Central de Venezuela. Actualmente está dedicado al área de diseño y desarrollo web, animaciones en Flash, diseño y desarrollo de video juegos y computación gráfica. Apasionado por la Computación Gráfica, la buena música y el cine independiente. 100% UCVista. PS3 Gamer. Creador de la frase "Salud" dicha después que alguien estornuda. Amante de la tecnología y el deporte, en especial del béisbol. Súper fanático de los Medias Rojas de Boston y Navegantes del Magallanes.