1. Code
  2. JavaScript
  3. Web APIs

Conceptos básicos de WebGL: Parte I

Scroll to top
This post is part of a series called WebGL Essentials.
WebGL Essentials: Part II

Spanish (Español) translation by Andrea Jiménez (you can also view the original English article)

WebGL es un renderizador 3D en el navegador basado en OpenGL, que te permite mostrar tu contenido 3D directamente en una página HTML5. En este tutorial cubriré todos los elementos esenciales que necesitas para comenzar a usar este framework.


Introducción

Hay un par de cosas que debes saber antes de comenzar. WebGL es una API de JavaScript que representa contenido 3D en un lienzo HTML5. Para ello, utiliza dos scripts que se conocen en el "mundo 3D" como Shaders. Los dos shaders son:

  • El shader de vértices
  • El shader de fragmentos

Ahora no te pongas demasiado nervioso cuando escuches estos nombres; es solo una forma elegante de decir "calculadora de posición" y "selector de color" respectivamente. El shader de fragmentos es el más fácil de entender; simplemente le dice a WebGL de qué color debe ser un punto dado en tu modelo. El shader de vértices es un poco más técnico, pero básicamente convierte los puntos de tus modelos 3D en coordenadas 2D. Debido a que todos los monitores de computadora son superficies planas en 2D, y cuando ves objetos 3D en tu pantalla, son simplemente una ilusión de perspectiva.

Si quieres saber exactamente cómo funciona este cálculo, deberás preguntarle a un matemático, ya que utiliza multiplicaciones avanzadas de matriz 4 x 4, que están un poco más allá del tutorial 'Conceptos básicos'. Afortunadamente, no tienes que saber cómo funciona porque WebGL se encargará de la mayor parte. Así que comencemos.


Paso 1: Configuración de WebGL

WebGL tiene una gran cantidad de configuraciones pequeñas que debes configurar casi cada vez que dibujas algo en la pantalla. Para ahorrar tiempo y hacer que tu código sea ordenado, voy a hacer un objeto JavaScript que contendrá todas las cosas 'detrás de escena' en un archivo separado. Para comenzar, crea un nuevo archivo llamado 'WebGL.js' y coloca el siguiente código dentro de él:

1
function WebGL(CID, FSID, VSID){
2
	var canvas = document.getElementById(CID);
3
	if(!canvas.getContext("webgl") && !canvas.getContext("experimental-webgl"))
4
		alert("Your Browser Doesn't Support WebGL");
5
	else
6
	{
7
		this.GL = (canvas.getContext("webgl")) ? canvas.getContext("webgl") : canvas.getContext("experimental-webgl");	
8
		
9
		this.GL.clearColor(1.0, 1.0, 1.0, 1.0); // this is the color 
10
		this.GL.enable(this.GL.DEPTH_TEST); //Enable Depth Testing
11
		this.GL.depthFunc(this.GL.LEQUAL); //Set Perspective View
12
		this.AspectRatio = canvas.width / canvas.height;
13
		
14
		//Load Shaders Here
15
	}
16
}

Esta función constructora toma los IDs del lienzo y los dos objetos shader. Primero, obtenemos el elemento de lienzo y nos aseguramos de que sea compatible con WebGL. Si lo hace, entonces le asignamos el contexto WebGL a una variable local llamada "GL". El color claro es simplemente el color de fondo, y vale la pena señalar que en WebGL la mayoría de los parámetros van de 0.0 a 1.0, por lo que tendrías que dividir tus valores rgb por 255. Entonces, en nuestro ejemplo, 1.0, 1.0, 1.0, 1.0 significa un fondo blanco con 100% de visibilidad (sin transparencia). Las siguientes dos líneas le dicen a WebGL que calcule la profundidad y la perspectiva para que un objeto más cercano a ti bloquee los objetos detrás de él. Finalmente, configuramos la relación de aspecto que se calcula dividiendo el ancho del lienzo por su altura.

Antes de continuar y cargar los dos shaders, vamos a escribirlos. Escribiré estos en el archivo HTML donde pondremos el elemento de lienzo real. Crea un archivo HTML y coloca los dos elementos de script siguientes justo antes de la etiqueta del cuerpo de cierre:

1
<script id="VertexShader" type="x-shader/x-vertex">
2
  
3
	attribute highp vec3 VertexPosition;
4
	attribute highp vec2 TextureCoord;
5
	
6
	
7
	uniform highp mat4 TransformationMatrix;
8
	uniform highp mat4 PerspectiveMatrix;
9
	
10
	varying highp vec2 vTextureCoord;
11
	
12
	void main(void) {
13
		gl_Position = PerspectiveMatrix * TransformationMatrix * vec4(VertexPosition, 1.0);
14
		vTextureCoord = TextureCoord;
15
	}
16
</script>
17
18
<script id="FragmentShader" type="x-shader/x-fragment"> 
19
	varying highp vec2 vTextureCoord;
20
	
21
	uniform sampler2D uSampler;
22
	
23
	void main(void) {
24
		highp vec4 texelColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));
25
		gl_FragColor = texelColor;
26
	}     
27
</script>

El shader de vértices se crea primero, y definimos dos atributos:

  • la posición del vértice, que es la ubicación en las coordenadas x, y y z del vértice actual (Punto en el modelo)
  • la coordenada de textura; la ubicación en la imagen de textura que se le debe asignar a este punto

Después, creamos variables para las matrices de transformación y perspectiva. Estos se utilizan para convertir el modelo 3D en una imagen 2D. La siguiente línea crea una variable compartida con el shader de fragmentos, y en la función principal calculamos el gl_Position (la posición final 2D). Luego, le asignamos la 'coordenada de textura actual' a la variable compartida.

En el shader de fragmentos simplemente tomamos las coordenadas que definimos en el shader de vértices y 'muestreamos' la textura en esa coordenada. Básicamente, solo obtenemos el color en la textura que corresponde al punto actual de nuestra geometría.

Ya que escribimos los shaders, podemos volver a cargarlos en nuestro archivo JS. Así que reemplaza el "//Load Shaders Here" con el siguiente código:

1
var FShader = document.getElementById(FSID);
2
var VShader = document.getElementById(VSID);
3
4
if(!FShader || !VShader)
5
	alert("Error, Could Not Find Shaders");
6
else
7
{
8
	//Load and Compile Fragment Shader
9
	var Code = LoadShader(FShader);
10
	FShader = this.GL.createShader(this.GL.FRAGMENT_SHADER);
11
	this.GL.shaderSource(FShader, Code);
12
	this.GL.compileShader(FShader);
13
	
14
	//Load and Compile Vertex Shader
15
	Code = LoadShader(VShader);
16
	VShader = this.GL.createShader(this.GL.VERTEX_SHADER);
17
	this.GL.shaderSource(VShader, Code);
18
	this.GL.compileShader(VShader);
19
	
20
	//Create The Shader Program
21
	this.ShaderProgram = this.GL.createProgram();
22
	this.GL.attachShader(this.ShaderProgram, FShader);
23
	this.GL.attachShader(this.ShaderProgram, VShader);
24
	this.GL.linkProgram(this.ShaderProgram);
25
	this.GL.useProgram(this.ShaderProgram);
26
	
27
	//Link Vertex Position Attribute from Shader
28
	this.VertexPosition = this.GL.getAttribLocation(this.ShaderProgram, "VertexPosition");
29
	this.GL.enableVertexAttribArray(this.VertexPosition);
30
	
31
	//Link Texture Coordinate Attribute from Shader
32
	this.VertexTexture = this.GL.getAttribLocation(this.ShaderProgram, "TextureCoord");
33
	this.GL.enableVertexAttribArray(this.VertexTexture);
34
}

Tus texturas tienen que estar en tamaños de bytes uniformes u obtendrás un error... como 2x2, 4x4, 16x16, 32x32...

Primero nos aseguramos de que los shaders existan, y luego pasamos a cargarlos uno a la vez. El proceso básicamente obtiene el código fuente del shader, lo compila y lo adjunta al programa del shader central. Hay una función, llamada LoadShader, que obtiene el código del shader del archivo HTML; llegaremos a eso en un segundo. Usamos el 'programa shader' para vincular los dos shaders, y nos da acceso a sus variables. Almacenamos los dos atributos que definimos en los shaders; para que podamos ingresar nuestra geometría en ellos más tarde.

Ahora veamos la función LoadShader, debes poner esto fuera de la función WebGL:

1
function LoadShader(Script){
2
	var Code = "";
3
	var CurrentChild = Script.firstChild;
4
	while(CurrentChild)
5
	{
6
		if(CurrentChild.nodeType == CurrentChild.TEXT_NODE)
7
			Code += CurrentChild.textContent;
8
		CurrentChild = CurrentChild.nextSibling;
9
	}
10
	return Code;
11
}

Básicamente, simplemente recorre el shader y recopila el código fuente.


Paso 2: El cubo "simple"

Para dibujar objetos en WebGL vas a necesitar las siguientes tres matrices:

  • vértices; los puntos que componen los objetos
  • triángulos; le indica a WebGL cómo conectar los vértices a las superficies
  • coordenadas de textura; define cómo se asignan los vértices en la imagen de textura

Esto se conoce como mapeo UV. Para nuestro ejemplo, crearemos un cubo básico. Dividiré el cubo en 4 vértices por lado que se conectan en dos triángulos. Hagamos una variable que contenga las matrices de un cubo.

1
var Cube = {
2
	Vertices : [ // X, Y, Z Coordinates
3
	
4
		//Front
5
		
6
		 1.0,  1.0,  -1.0,
7
		 1.0, -1.0,  -1.0,
8
		-1.0,  1.0,  -1.0,
9
		-1.0, -1.0,  -1.0,
10
		
11
		//Back
12
		
13
		 1.0,  1.0,  1.0,
14
		 1.0, -1.0,  1.0,
15
		-1.0,  1.0,  1.0,
16
		-1.0, -1.0,  1.0,
17
		
18
		//Right
19
		
20
		 1.0,  1.0,  1.0,
21
		 1.0, -1.0,  1.0,
22
		 1.0,  1.0, -1.0,
23
		 1.0, -1.0, -1.0,
24
		 
25
		 //Left
26
		 
27
		-1.0,  1.0,  1.0,
28
		-1.0, -1.0,  1.0,
29
		-1.0,  1.0, -1.0,
30
		-1.0, -1.0, -1.0,
31
		
32
		//Top
33
		
34
		 1.0,  1.0,  1.0,
35
		-1.0, -1.0,  1.0,
36
		 1.0, -1.0, -1.0,
37
		-1.0, -1.0, -1.0,
38
		
39
		//Bottom
40
		
41
		 1.0, -1.0,  1.0,
42
		-1.0, -1.0,  1.0,
43
		 1.0, -1.0, -1.0,
44
		-1.0, -1.0, -1.0
45
	
46
	],
47
	Triangles : [ // Also in groups of threes to define the three points of each triangle
48
		//The numbers here are the index numbers in the vertex array
49
		
50
		//Front
51
		
52
		0, 1, 2,
53
		1, 2, 3,
54
		
55
		//Back
56
		
57
		4, 5, 6,
58
		5, 6, 7,
59
		
60
		//Right
61
		
62
		8, 9, 10,
63
		9, 10, 11,
64
		
65
		//Left
66
		
67
		12, 13, 14,
68
		13, 14, 15,
69
		
70
		//Top
71
		
72
		16, 17, 18,
73
		17, 18, 19,
74
		
75
		//Bottom
76
		
77
		20, 21, 22,
78
		21, 22, 23
79
		
80
	],
81
	Texture : [ //This array is in groups of two, the x and y coordinates (a.k.a U,V) in the texture
82
		//The numbers go from 0.0 to 1.0, One pair for each vertex
83
		
84
		 //Front
85
		 
86
		 1.0, 1.0,
87
		 1.0, 0.0,
88
		 0.0, 1.0,
89
		 0.0, 0.0,
90
		 
91
		
92
		 //Back
93
		
94
		 0.0, 1.0,
95
		 0.0, 0.0,
96
		 1.0, 1.0,
97
		 1.0, 0.0,
98
		
99
		 //Right
100
		
101
		 1.0, 1.0,
102
		 1.0, 0.0,
103
		 0.0, 1.0,
104
		 0.0, 0.0,
105
		 
106
		 //Left
107
		 
108
		 0.0, 1.0,
109
		 0.0, 0.0,
110
		 1.0, 1.0,
111
		 1.0, 0.0,
112
		
113
		 //Top
114
		
115
		 1.0, 0.0,
116
		 1.0, 1.0,
117
		 0.0, 0.0,
118
		 0.0, 1.0,
119
		
120
		 //Bottom
121
		
122
		 0.0, 0.0,
123
		 0.0, 1.0,
124
		 1.0, 0.0,
125
		 1.0, 1.0
126
	]
127
};

Puede parecer una gran cantidad de datos para un cubo simple, sin embargo, en la segunda parte de este tutorial, haré un script que importará tus modelos 3D para que no tengas que preocuparte por calcularlos.

También te estarás preguntando por qué hice 24 puntos (4 para cada lado), cuando en realidad solo hay ocho puntos únicos en total en un cubo. Hice esto porque solo se puede asignar una coordenada de textura por vértice; así que si solo pusimos los 8 puntos, entonces todo el cubo tendría que verse igual porque envolvería la textura alrededor de todos los lados que toca el vértice. Pero de esta manera, cada lado tiene sus propios puntos para que podamos poner una parte diferente de la textura en cada lado.

Ahora tenemos esta variable de cubo y estamos listos para comenzar a dibujarlo. Volvamos al método WebGL y agreguemos una función Draw.


Paso 3: La función Dibujar

El procedimiento para dibujar objetos en WebGL tiene muchos pasos; por lo tanto, es una buena idea hacer una función para simplificar el proceso. La idea básica es cargar las tres matrices en búferes WebGL. Luego, conectamos estos búferes a los atributos que definimos en los shaders junto con las matrices de transformación y perspectiva. Luego, tenemos que cargar la textura en la memoria, y, por último, podemos llamar al comando draw. Así que comencemos.

El siguiente código va dentro de la función WebGL:

1
this.Draw = function(Object, Texture)
2
{
3
    var VertexBuffer = this.GL.createBuffer(); //Create a New Buffer
4
5
    //Bind it as The Current Buffer
6
    this.GL.bindBuffer(this.GL.ARRAY_BUFFER, VertexBuffer);
7
8
    // Fill it With the Data
9
    this.GL.bufferData(this.GL.ARRAY_BUFFER, new Float32Array(Object.Vertices), this.GL.STATIC_DRAW);
10
11
    //Connect Buffer To Shader's attribute
12
    this.GL.vertexAttribPointer(this.VertexPosition, 3, this.GL.FLOAT, false, 0, 0);
13
14
    //Repeat For The next Two
15
    var TextureBuffer = this.GL.createBuffer();
16
    this.GL.bindBuffer(this.GL.ARRAY_BUFFER, TextureBuffer);
17
    this.GL.bufferData(this.GL.ARRAY_BUFFER, new Float32Array(Object.Texture), this.GL.STATIC_DRAW);
18
    this.GL.vertexAttribPointer(this.VertexTexture, 2, this.GL.FLOAT, false, 0, 0);
1
    var TriangleBuffer = this.GL.createBuffer();
2
    this.GL.bindBuffer(this.GL.ELEMENT_ARRAY_BUFFER, TriangleBuffer);
3
    //Generate The Perspective Matrix
4
    var PerspectiveMatrix = MakePerspective(45, this.AspectRatio, 1, 10000.0);
5
6
    var TransformMatrix = MakeTransform(Object);
7
8
    //Set slot 0 as the active Texture
9
    this.GL.activeTexture(this.GL.TEXTURE0);
10
11
    //Load in the Texture To Memory
12
    this.GL.bindTexture(this.GL.TEXTURE_2D, Texture);
13
14
    //Update The Texture Sampler in the fragment shader to use slot 0
15
    this.GL.uniform1i(this.GL.getUniformLocation(this.ShaderProgram, "uSampler"), 0);
16
17
    //Set The Perspective and Transformation Matrices
18
    var pmatrix = this.GL.getUniformLocation(this.ShaderProgram, "PerspectiveMatrix");
19
    this.GL.uniformMatrix4fv(pmatrix, false, new Float32Array(PerspectiveMatrix));
20
21
    var tmatrix = this.GL.getUniformLocation(this.ShaderProgram, "TransformationMatrix");
22
    this.GL.uniformMatrix4fv(tmatrix, false, new Float32Array(TransformMatrix));
23
24
    //Draw The Triangles
25
    this.GL.drawElements(this.GL.TRIANGLES, Object.Trinagles.length, this.GL.UNSIGNED_SHORT, 0);
26
};

El shader de vértices posiciona, gira y escala el objeto en función de las matrices de transformación y perspectiva. Profundizaremos más en las transformaciones en la segunda parte de esta serie.

Añadí dos funciones: MakePerspective() y MakeTransform(). Estos solo generan las matrices 4x4 necesarias para WebGL. La función MakePerspective() acepta el campo de visión vertical, la relación de aspecto y los puntos más cercanos y lejanos como argumentos. Todo lo que esté más cerca de 1 unidad y más lejos de 10000 unidades no se mostrará, pero puedes editar estos valores para obtener el efecto que estás buscando. Ahora revisemos estas dos funciones:

1
function MakePerspective(FOV, AspectRatio, Closest, Farest){
2
	var YLimit = Closest * Math.tan(FOV * Math.PI / 360);
3
	var A = -( Farest + Closest ) / ( Farest - Closest );
4
	var B = -2 * Farest * Closest / ( Farest - Closest );
5
	var C = (2 * Closest) / ( (YLimit * AspectRatio) * 2 );
6
	var D =	(2 * Closest) / ( YLimit * 2 );
7
	return [
8
		C, 0, 0, 0,
9
		0, D, 0, 0,
10
		0, 0, A, -1,
11
		0, 0, B, 0
12
	];
13
}
14
function MakeTransform(Object){
15
	return [
16
		1, 0, 0, 0,
17
		0, 1, 0, 0,
18
		0, 0, 1, 0,
19
		0, 0, -6, 1
20
	];
21
}

Ambas matrices afectan el aspecto final de tus objetos, pero la matriz de perspectiva edita tu "mundo 3D" como el campo de visión y los objetos visibles, mientras que la matriz de transformación edita los objetos individuales como su escala de rotación y posición. Con esto hecho estamos casi listos para dibujar, todo lo que queda es una función para convertir una imagen en una textura WebGL.


Paso 4: Carga de texturas

La carga de una textura es un proceso de dos pasos. Primero tenemos que cargar una imagen como lo harías en una aplicación JavaScript estándar, y luego tenemos que convertirla a una textura WebGL. Así que comencemos con la segunda parte ya que ya estamos en el archivo JS. Agrega lo siguiente en la parte inferior de la función WebGL justo después del comando Draw:

1
this.LoadTexture = function(Img){
2
	//Create a new Texture and Assign it as the active one
3
	var TempTex = this.GL.createTexture();
4
	this.GL.bindTexture(this.GL.TEXTURE_2D, TempTex);  
5
	
6
	//Flip Positive Y (Optional)
7
	this.GL.pixelStorei(this.GL.UNPACK_FLIP_Y_WEBGL, true);
8
	
9
	//Load in The Image
10
	this.GL.texImage2D(this.GL.TEXTURE_2D, 0, this.GL.RGBA, this.GL.RGBA, this.GL.UNSIGNED_BYTE, Img);  
11
	
12
	//Setup Scaling properties
13
	this.GL.texParameteri(this.GL.TEXTURE_2D, this.GL.TEXTURE_MAG_FILTER, this.GL.LINEAR);  
14
	this.GL.texParameteri(this.GL.TEXTURE_2D, this.GL.TEXTURE_MIN_FILTER, this.GL.LINEAR_MIPMAP_NEAREST);  
15
	this.GL.generateMipmap(this.GL.TEXTURE_2D); 
16
	
17
	//Unbind the texture and return it.
18
	this.GL.bindTexture(this.GL.TEXTURE_2D, null);
19
	return TempTex;
20
};

Vale la pena señalar que tus texturas tienen que estar en tamaños de bytes uniformes, o recibirás un error; por lo que tienen que ser dimensiones, como 2x2, 4x4, 16x16, 32x32, así sucesivamente. Agregué la línea para voltear las coordenadas Y simplemente porque las coordenadas Y de mi aplicación 3D estaban al revés, pero dependerá de lo que estés usando. Esto se debe a que algunos programas hacen que 0 en el eje Y sea la esquina superior izquierda y algunas aplicaciones la convierten en la esquina inferior izquierda. Las propiedades de escala que configuré solo le dicen a WebGL cómo debe escalar la imagen y reducir la escala. Puedes jugar con diferentes opciones para obtener diferentes efectos, pero pensé que funcionaban mejor.

Ya que terminamos con el archivo JS, volvamos al archivo HTML e implementemos todo esto.


Paso 5: Conclusión

Como mencioné anteriormente, WebGL se representa en un elemento de lienzo. Eso es todo lo que necesitamos en la sección del cuerpo. Después de agregar el elemento de lienzo, tu página html debería verse así:

1
<html>
2
	<head>
3
		<!-- Include Our WebGL JS file -->
4
		<script src="WebGL.js" type="text/javascript"></script>
5
		<script>
6
			
7
		</script>
8
	</head>
9
	<body onload="Ready()">  
10
	  <canvas id="GLCanvas" width="720" height="480">
11
	    	Your Browser Doesn't Support HTML5's Canvas.  
12
	  </canvas>
13
	  
14
	<!-- Your Vertex Shader -->
15
	
16
	<!-- Your Fragment Shader -->
17
	
18
	</body>
19
</html>

Es una página bastante simple. En el área del encabezado vinculé a nuestro archivo JS. Ahora implementaremos nuestra función Ready, que se llama cuando se carga la página:

1
//This will hold our WebGL variable
2
var GL; 
3
	
4
//Our finished texture
5
var Texture;
6
	
7
//This will hold the textures image 
8
var TextureImage;
9
	
10
function Ready(){
11
	GL = new WebGL("GLCanvas", "FragmentShader", "VertexShader");
12
	TextureImage = new Image();
13
	TextureImage.onload = function(){
14
		Texture = GL.LoadTexture(TextureImage);
15
		GL.Draw(Cube, Texture);
16
	};
17
	TextureImage.src = "Texture.png";
18
}

Así que creamos un nuevo objeto WebGL y pasamos los ID para el lienzo y los shaders. Luego, cargamos la imagen de textura. Una vez cargado, llamamos al método Draw() con el Cubo y la Textura. Si lo seguiste, tu pantalla debe tener un cubo estático con una textura.

Ahora, a pesar de que dije que cubriremos las transformaciones la próxima vez, no puedo dejarlos con un cuadrado estático; no es lo suficientemente 3D. Regresemos y agreguemos una pequeña rotación. En el archivo HTML, cambia la función onload para que se vea así:

1
TextureImage.onload = function(){
2
		Texture = GL.LoadTexture(TextureImage);
3
		setInterval(Update, 33);
4
};

Esto llamará a una función llamada Update() cada 33 milisegundos lo que nos dará una velocidad de fotogramas de unos 30 fps. Esta es la función de actualización:

1
function Update(){
2
	GL.GL.clear(16384 | 256);
3
	GL.Draw(GL.Cube, Texture);
4
}

Esta es una función bastante simple; limpia la pantalla y luego dibuja el Cubo actualizado. Ahora, vayamos al archivo JS para agregar el código de rotación.


Paso 6: Agregar un poco de giro

No implementaré completamente las transformaciones, porque estoy guardando eso para la próxima, pero agreguemos una rotación alrededor del eje Y. Lo primero que debes hacer es agregar una variable de Rotación a nuestro objeto Cubo. Esto hará un seguimiento del ángulo actual y nos permitirá seguir incrementando la rotación. Por lo tanto, la parte superior de la variable Cubo debería verse así:

1
var Cube = {
2
	Rotation : 0,
3
	//The Other Three Arrays
4
};

Ahora actualicemos la función MakeTransform() para incorporar la rotación:

1
function MakeTransform(Object){
2
	var y = Object.Rotation * (Math.PI / 180.0);
3
	var A = Math.cos(y);
4
	var B = -1 * Math.sin(y);
5
	var C = Math.sin(y);
6
	var D = Math.cos(y);
7
	Object.Rotation += .3;	
8
	return [
9
		A, 0, B, 0,
10
		0, 1, 0, 0,
11
		C, 0, D, 0,
12
		0, 0, -6, 1
13
	];
14
}

Conclusión

¡Y eso es todo! En el próximo tutorial, hablaremos sobre la carga de modelos y la realización de transformaciones. Espero que te haya gustado este tutorial; siéntete libre de dejar cualquier pregunta o comentario que puedas tener, a continuación.