Búsqueda

19/2/13

Dibujando un cubo sin texturas

Vamos a dibujar el siguiente cubo con WebGL explicando todos los detalles necesarios para entender como funciona OpenGL.

Podeis descargaros el proyecto completo desde aquí. El proyecto tiene la siguiente estructura:

Los archivos J3DI.js, J3DIMath.js y webgl-utils.js también los podéis encontrar en la wiki de WebGL. De estos archivos no es necesario cambiar nada, solo son librerias que usamos para configurar WebGL.

index.html


El archivo index.html sirve para crear el layout de la página.

En la cabecera importamos las librerías que vamos a usar:
<head>
  <script type="text/javascript" src="js/lib/jquery-1.8.3.js"></script>
  <script type="text/javascript" src="js/lib/J3DI.js"></script>
  <script type="text/javascript" src="js/lib/J3DIMath.js"></script>
  <script type="text/javascript" src="js/lib/webgl-utils.js"></script> 
A continuación incluimos los shaders. Los shaders son trozos de código que se ejecutan en la GPU de nuestra tarjeta gráfica, permitiendo una rápida transformación de los vertices de los objetos así como modificaciones en el color de los pixeles que forman el objeto. Este lenguaje de programación se llama GLSL.
  <script type="text/javascript" src="js/engine/shaders/default/vShader.js"></script>
  <script type="text/javascript" src="js/engine/shaders/default/fShader.js"></script>
Por último, incluimos los archivos render.js y scene.js.
  <script type="text/javascript" src="js/engine/render.js"></script>
  <script type="text/javascript" src="js/scene.js"></script>
</head>

En la etiqueta <body> definimos el canvas que va a usar WebGL para mostrarnos el cubo. Podemos personalizarlo como cualquier elemento HTML, en mi caso le he puesto un ancho de 800px y un alto de 500px.
<body onload="start()">
  <canvas id="example" style="width:800px;height:500px">
    Por favor, usa un navegador compatible (Firefox, Chrome, ...)
  </canvas>
</body>

 

scene.js

En este archivo lo único que definimos son los objetos que se van a mostrar en el canvas.
function initScene(){
  scene.elements = { "box": makeBox(gl)};
}

 

render.js

Este archivo será el verdadero motor gráfico. Es el que se encarga de inicializar OpenGL, posicionar la cámara, dibujar los objetos, ....

Función init

Inicializamos el contexto de OpenGL.
function init()
{
    var gl = initWebGL("example");
    if (!gl) {
      return;
A continuación definimos los shaders. El contenido del vertex shader, que es el que se encarga de realizar transformaciones en los vértices del modelo, lo leemos de la variable defaultVShader. Esta variable la hemos declarado al importar el archivo "js/engine/shaders/default/vShader.js" en el tag head de index.html.
  vshader = $("<script id='vshader' type='x-shader/x-vertex'></script>");
  vshader.append(defaultVShader);
  $("body").append(vshader);
El contenido del fragment shader, que es el que se encarga de dar color a los pixeles que ocupa el modelo en pantalla y de aplicar las texturas, lo leemos de la variable defaultFShader. Esta variable la hemos declarado al importar el archivo "js/engine/shaders/default/fShader.js" en el tag head de index.html.
  fshader = $("<script id='fshader' type='x-shader/x-fragment'></script>");
  fshader.append(defaultFShader);
  $("body").append(fshader);
Después declaramos un programa o configuración. Para declarar un programa necesitamos:
  1. Un contexto de OpenGL, 
  2. Vertex shader. Le pasamos el id del vertex shader definido previamente.
  3. Fragment shader. Le pasamos el id del fragmente shader definido previamente.
  4. Una lista de atributos que necesitan los shaders para ejecutarse, en nuestro caso "vNormal" y "vPosition"
  5. Un color de fondo
  6. Valor inicial de depth, también llamado z-buffer que se usa cuando dos objetos ocupan el mismo pixel en la pantalla, determinar cual de ellos se muestra.
Podemos definir varios programas en OpenGL. Como hemos visto en cada programa se usan dos shaders esto nos permite realizar diferentes transformaciones en los objetos de la escena. Por ejemplo, podemos definir un programa que dibuje los objetos en blanco y negro y otro que los dibuje en color. 
  program = simpleSetup(
    gl,
    "vshader", "fshader",
    [ "vNormal", "vPosition"],
    [ 0, 0, 0.5, 1 ], 10000);
Inicializamos las matrices necesarias para realizar las transformaciones en los vértices del modelo.
  render.mvMatrix = new J3DIMatrix4();
  render.normalMatrix = new J3DIMatrix4();
  render.mvpMatrix = new J3DIMatrix4();
Por último, devolvemos el contexto de OpenGL inicializado.
  return gl;
}

Función reshape

Esta función redimensiona toda la escena siempre que cambie el tamaño del canvas.
function reshape(gl){
  var canvas = document.getElementById('example');
  if (canvas.clientWidth == canvas.width && canvas.clientHeight == canvas.height)
    return;
  canvas.width = canvas.clientWidth;
  canvas.height = canvas.clientHeight;
Indica el tamaño del rectangulo donde se visualizará la escena. En este caso es el tamaño del canvas:
  gl.viewport(0, 0, canvas.clientWidth, canvas.clientHeight); 
Establecemos como queremos visualizar la escena, en este caso cada objeto debe ser proyectado en perspectiva a la pantalla. Como en el mundo real, los objetos lejanos parecen más pequeños que los objetos que están cerca de la camara. Existe otro tipo de proyección que es ortográfica en la que todos los objetos, se proyectan en la pantalla con su tamaño real, independientemente de la posición en la que se encuentren.
  render.perspectiveMatrix = new J3DIMatrix4();
  render.perspectiveMatrix.perspective(30, canvas.clientWidth /  canvas.clientHeight, 
                                                                              1, 10000);
Por último, posicionamos la cámara en el punto (0,0,7) con:
  render.perspectiveMatrix.lookat(0, 0, 7, 0, 0, 0, 0, 1, 0);
}

Función drawPicture

Esta función es la que se encarga de mostrar todos los objetos de la escena en pantalla. Comienza llamando a la función explicada en el apartado anterior y posteriormente limpia el canvas con el color de fondo.
function drawPicture(gl)
{
  reshape(gl);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
Crea la matriz de transformación para pasar las coordenadas del modelo a coordenadas del mundo. En este caso rotamos el modelo 20º sobre el eje X y otros 20º sobre el eje Y
  render.mvMatrix.makeIdentity();
  render.mvMatrix.rotate(20, 1,0,0);
  render.mvMatrix.rotate(20, 0,1,0);
Copia la matriz anterior para realizar la misma operación con las normales de los vertices del objeto.
  render.normalMatrix.load(render.mvMatrix);
Creamos la matriz que transforma las coordenadas de los vertices del modelo a sus correspondientes en la pantalla, multiplicando (render.mvMatrix) x (render.perspectiveMatrix).
  render.mvpMatrix.load(render.perspectiveMatrix);
  render.mvpMatrix.multiply(render.mvMatrix);
Pulsa aquí para entender mejor todas estas transformaciones.

Indicamos con que programa queremos mostrar los objetos en la escena. Usaremos el programa definido en el metodo init().
  gl.useProgram(program);
Situamos la luz en el punto (0,0,1) 
  gl.uniform3f(gl.getUniformLocation(program, "lightDir"), 0, 0, 1);
Pasamos la matriz para transformar las normales al programa. Para calcular las normales no necesitamos saber donde esta la cámara ni tampoco el tipo de proyección por eso se usa una matriz que solo transforma las coordenadas desde el modelo al mundo. Una vez realizada esta transformación podremos saber como se debe iluminar el objeto.
  render.normalMatrix.setUniform(gl, 
                        gl.getUniformLocation(program, "u_normalMatrix"), false);
Pasamos la matriz de transformación desde el modelo a la pantalla.
  render.mvpMatrix.setUniform(gl, 
                        gl.getUniformLocation(program, "u_modelViewProjMatrix"), false);
Indica a OpenGL que queremos utilizar los atributos 0 y 1, en nuestro caso estos atributos son vNormal y vVertex.
  gl.enableVertexAttribArray(0);
  gl.enableVertexAttribArray(1);
Mostramos todos los elementos de la escena.
for(index in scene.elements){
  element = scene.elements[index];
Asignamos los vertices del objeto al buffer y lo usamos para establecer el attributo vVertex del shader, en nuestro caso es el número 1, además le decimos que coja los float de 3 en 3.
  gl.bindBuffer(gl.ARRAY_BUFFER, element.vertexObject);
  gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 0, 0);
Hacemos lo mismo con las normales, como veis en esta caso el primer argumento es un 0 ya que ese es el indice de vNormal en nuestro shader.
  gl.bindBuffer(gl.ARRAY_BUFFER, element.normalObject);
  gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
Por último, le indicamos los indices necesarios para formar los triangulos del objeto         
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, element.indexObject);     
  gl.drawElements(gl.TRIANGLES, element.numIndices, gl.UNSIGNED_BYTE, 0);
}

No hay comentarios:

Publicar un comentario