Agosto de 2015

Volumen 30, número 8

Desarrollo de juegos: introducción a 3D para juegos web

Por Michael Oneppo | Agosto de 2015

La adición de una tercera dimensión a un juego realmente le da vida. Puede buscar a su alrededor desde cualquier punto de vista y ver todos los ángulos de un objeto o una escena. Pero, ¿cómo puede sacar esto realmente del segundo plano? En esta serie de artículos, le guiaré por los pasos para crear juegos en 3D y le mostraré de qué manera las bibliotecas como three.js pueden ayudarle a lograr el entorno 3D enriquecido que se está haciendo tan popular en la web. En esta primera entrega, haré el tema sencillo y me centraré en la creación de una versión 3D del juego de Ping que se describió por primera vez en "Un juego web en una hora" (msdn.microsoft.com/magazine/dn913185).

La ilusión del 3D

Cualquier representación de gráficos 3D tiene un truco sorprendente bajo la manga. Los seres humanos no pueden ver realmente en dimensiones 3D, especialmente en un monitor de equipo. El objetivo principal del dibujo 3D es generar, o representar, una descripción en 3D de una escena en una imagen en 2D. Cuando se agrega una tercera dimensión para obtener escenas más envolventes y realistas, debe deshacerse de algunos datos para obtener una imagen desde un punto de vista específico. Este concepto se llama proyección. Es un elemento esencial de lo que hace funcionar los gráficos 3D, como se muestra en la escena 3D básica en la Figura 1.

Un escena 3D sencilla
Figura 1 Un escena 3D sencilla

En esta pantalla, el eje Z retrocede hacia arriba y hacia atrás. Si quisiera verlo realmente en pantalla, bastaría con colocar la información de Z de todos los objetos como una manera sencilla y válida para proyectar la escena 3D, como se muestra en la Figura 2.

Escena 3D aplastada
Figura 2 Escena 3D aplastada

Como puede ver, esto no es exactamente Halo. Para fotorealismo, una escena 3D requiere tres cosas: una proyección de cámara adecuada, geometría y sombreado. Abordaré cada uno de estos conceptos conforme vuelvo a crear el juego Ping como juego de duelos en 3D.

Introducción

En primer lugar, configuraré una biblioteca de three.js. Se trata de una configuración bastante rápida, puesto que casi todo lo que haga con three.js sucede en JavaScript. Este es el código HTML que necesitará:

<html>
  <head>
    <title>Ping!</title>
    <script src=
      "//cdnjs.cloudflare.com/ajax/libs/three.js/r69/three.min.js"></script>
    <script src="ping3d.js"></script>
  </head>
  <body>
  </body>
</html>

En el archivo ping3d.js de JavaScript, voy a configurar three.js para representar una escena simple. En primer lugar, necesito inicializar three.js y agregar su lienzo de dibujo a la página:

var scene = new THREE.Scene();
var renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

La escena es exactamente lo que parece: un objeto que describe nuestra escena y todos los objetos que se encuentran en ella. El representador también recibe un nombre obvio. Cuando se da una escena, el representador la dibujará en la pantalla. Esto debería ser similar a algunos de los sistemas de dibujo 2D que he descrito en los artículos anteriores, "Un juego web en una hora", "Técnicas de dibujo 2D y bibliotecas para juegos web" (msdn.microsoft.com/magazine/dn948109) y "Motores de juegos 2D para la web" (msdn.microsoft.com/magazine/dn973016). Ahora necesito agregar algunos elementos a la pantalla.

Geometría

Casi todos los gráficos 3D se crean fuera de los polígonos. Incluso las superficies curvas como una bola se aproximan en facetas triangulares para parecerse a su superficie. Cuando se ensamblan, estos triángulos se denominan malla. Esta es la manera de agregar la pelota a la escena:

var geometry = new THREE.SphereGeometry(10);
var material = new THREE.BasicMaterial({color: 0xFF0000});
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

Este código creará un gran número de triángulos que representan una esfera (la variable "geometría"), un material de color rojo brillante simple (el "material") y un objeto de malla (la "malla"). A continuación, agregará la malla a la escena.

El triángulo es el bloque de creación fundamental de los gráficos 3D. ¿A qué se debe? Analizaré esto con mayor detalle en el siguiente artículo de esta serie, pero las dos razones principales son que las líneas rectas que componen un triángulo facilitan el trabajo y que no puede dividir un triángulo en una superficie plana más básica. La unidad de procesamiento de gráficos (GPU) del equipo o teléfono ha dedicado hardware que puede convertir rápidamente formas con líneas rectas en píxeles. Esta es una buena parte de lo que hace posibles los gráficos 3D.

Modelado

Puedo pasar cualquier geometría al constructor three.Mesh. Esto incluye la geometría generada para crear formas personalizadas o incluso datos a partir de archivos. Para el juego de Ping, deseo tener modelos 3D de cada uno de los jugadores. Por tanto, me he tomado la libertad de crear geometría en un programa de modelado 3D para este ejercicio. Es sorprendentemente sencillo usar el modelo en lugar de una esfera, ya que three.js ofrece una herramienta de carga justamente para este fin:

var jsonLoader = new THREE.JSONLoader();
jsonLoader.load('tank1.json', function (geometry) {
  var material = new THREE.BasicMaterial({color: 0xFF0000});
  var mesh = new THREE.Mesh(geometry, material);
  scene.add(mesh);
});

Cámara

La cámara representa el punto de vista de la escena. Almacena tanto la posición como el ángulo del visor dentro del juego. Lo que es más importante, la cámara representa cómo se aplana la escena, como se describe al principio de este artículo.

En mi ejemplo, la cámara se colocó hacia abajo y a la derecha. La imagen final aparecía como la vista desde esa dirección. Sin embargo, con este método de proyección, no importa cómo de lejos estén los objetos, seguirán teniendo el mismo tamaño en la imagen final. Esto se denomina proyección ortográfica. A menudo esto resulta útil para juegos con ángulos de visión no realistas como juegos de simulación de ciudades. Lo que realmente quiero lograr es que algunos objetos parezcan más pequeños a medida que se desvanecen en la distancia.

Introduzca la proyección en perspectiva: La proyección en perspectiva imagina el campo de visión de una cámara como una pirámide que se amplía desde la lente. Cuando las posiciones están asignadas a la pantalla, se calculan en función de sus distancias relativas hacia los lados de la pirámide. Con este modelo, conforme los objetos se desvanecen en la distancia, parecen reducirse como en la vida real.

Afortunadamente, no necesita realizar esta asignación usted mismo porque three.js lo hace por usted y ofrece un objeto que representa la cámara en la escena (y la adición de otro es sencillo):

var camera = new THREE.PerspectiveCamera(
  75, window.innerWidth/window.innerHeight, 0.1, 1000 );

El primer argumento es el campo de visión, que indica cuánto tomar de una distancia angular en horizontal. El segundo argumento es la proporción del ancho y el alto de pantalla, que necesita para asegurarse de que las cosas no están apretadas porque la pantalla no es cuadrada. Los dos parámetros finales definen las distancias más cercanas y más lejanas que se van a mostrar. No se dibujará cualquier cosa que se encuentre más cerca o más lejos de esos valores. Ahora estoy en el punto donde realmente puedo dibujar la escena. Vamos a mover la cámara hacia atrás un poco para ver toda la escena y empezar a dibujar:

camera.position.z = 50;
renderer.render(scene, camera);

Materiales y luces

Luego voy a colocar la pelota en el campo en el que se rebotará:

var room = new THREE.BoxGeometry( 50, 30, 100 );
var material = new THREE.MeshPhongMaterial({
    side:  THREE.BackSide,
    map: THREE.ImageUtils.loadTexture('arena.png')
});
var model = new THREE.Mesh(room, material);
model.position.y = 15;
scene.add(model);

Estoy haciendo algo diferente que crear únicamente geometría de cuadros. También estoy creando un material. Un material es una definición de cómo algo debe reflejar la luz en una escena. Esto genera su aspecto general. En este caso, voy a crear un material de Phong, que es una buena opción predeterminada para objetos brillantes. También voy a agregar una textura al cuadro, lo cual es sencillo en three.js usando la función loadTexture.

Otro aspecto importante de este código es la línea que indica: lado: THREE.BackSide. Esto indica a three.js que dibuje solo los lados interiores de las superficies del cuadro, en lugar de los lados exteriores. Esto ofrece espacio para que rebote la pelota, en lugar de tener un cuadro sólido flotando en el espacio.

Si tuviese que dibujar la escena ahora, el campo no sería visible. Solo dibujaría negro. Esto se debe a que los materiales definen cómo la luz refleja los objetos y no hay luz todavía en la escena. Three.js hace sencilla la adición de luz a una escena, como se muestra aquí:

this.lights = [];
this.lights[0] = new THREE.PointLight( 0x888888, 1, 300 );
this.lights[0].position.set( 0, 10, 40 );
scene.add( this.lights[0] );
this.lights[1] = new THREE.PointLight( 0x888888, 1, 300 );
this.lights[1].position.set( 0, 20, -40 );
scene.add( this.lights[1] );

Ahora si dibujo la escena, el ámbito se representará correctamente. Para crear una mejor perspectiva, estableceré la posición de la cámara para que mire en el lado del campo antes de ejecutar el código:

camera.up.copy(new THREE.Vector3(0,1,0));
camera.position.copy(new THREE.Vector3(0,17, -80));
camera.lookAt(new THREE.Vector3(0,0,40));

La primera línea establece la variable hacia arriba, que simplemente indica a la cámara que sentido está hacia arriba. La función lookAt señala la cámara en la posición especificada.

Creación de un juego 3D

Ahora que se ha movido el juego en tres dimensiones, hacer el resto debería ser bastante sencillo. Sin embargo, este juego va a terminar un poco más detallado que las implementaciones anteriores porque se compone de objetos 3D en lugar de objetos 2D. Así podrá dividir el código en archivos independientes para que el código adicional sea más sencillo de administrar.

También cambiaré los estilos de JavaScript para la definición de objetos al modelo de constructor más tradicional. Para demostrarlo, he encapsulado el cuadro del campo y las luces en un objeto y lo he colocado en un solo archivo, como se muestra en la Figura 3.

Figura 3 El objeto Campo

function Arena(scene) {
  var room = new THREE.BoxGeometry( 50, 30, 100 );
  var material = new THREE.MeshPhongMaterial({
    side:  THREE.BackSide,
    map: THREE.ImageUtils.loadTexture('arena.png')
  });
  var model = new THREE.Mesh(room, material);
  model.position.y = 15;
  scene.add(model);
  this.lights = [];
  this.lights[0]= new THREE.PointLight( 0x888888, 1, 300 );
  this.lights[0].position.set( 0, 10, 40 );
  scene.add( this.lights[0] );
  this.lights[1]= new THREE.PointLight( 0x888888, 1, 300 );
  this.lights[1].position.set( 0, 20, -40 );
  scene.add( this.lights[1] );
}

Si desea crear un campo, puedo crear un nuevo objeto mediante esta función de constructor:

var arena = new Arena(scene);

A continuación, crearé un objeto de bola que puede rebotar por el campo. Sé cómo crear una bola roja en three.js, por lo que ajustaré ese código en un objeto también:

function Ball(scene) {
  var mesh = new THREE.SphereGeometry(1.5, 10, 10);
  var material = new THREE.MeshPhongMaterial({
    color: 0xff0000,
    specular: 0x333333
  });
  var _model = new THREE.Mesh(mesh, material);
  _model.position.y = 10;
  scene.add(_model);
}

Ahora definiré la física básica de rebotar la bola agregando una función al objeto de bola, como se muestra en la Figura 4.

Figura 4 Función de actualización del objeto de bola

// Create a private class variable and set it to some initial value.
var _velocity = new THREE.Vector3(40,0,40);
this.update = function(t) {
  // Apply a little gravity to the ball.
  _velocity.y -= 25 * t;
  // Move the ball according to its velocity
  var offset = _velocity.clone()
    .multiplyScalar(t);
  _model.position.add(offset);
  // Now bounce it off the walls and the floor.
  // Ignore the ends of the arena.
  if (_model.position.y - 1.5 <= 0) {
    _model.position.y = 1.5;
    _velocity.y *= -1;
  }
  if (_model.position.x - 1.5 <= -25) {
    _model.position.x = -23.5;
    _velocity.x *= -1;
  }
  if (_model.position.x + 1.5 >= 25) {
    _model.position.x = 23.5;
    _velocity.x *= -1;
  }
}

Three.js requiere representar la escena cada vez que se use requestAnimationFrame. Este debería ser un patrón familiar:

var ball = new Ball(scene);
var Arena = new Arena(scene);
var render = function (time) {
  var t = (time - lastTime) / 1000.0;
  lastTime = time;
  ball.update(t);
  renderer.render(scene, camera);
  requestAnimationFrame( render );
}
requestAnimationFrame(render);

No deje de leernos

Ahora tengo un campo con luces, una cámara bien colocada y una pelota que rebota alrededor del campo. Esto es todo lo que trataré en este artículo. En la siguiente entrega, explicaré cómo funciona la proyección 3D permitiéndole apuntar con el mouse. También explicaré más sobre las texturas y realizaré animaciones uniformes mediante una biblioteca eficaz denominada tween.js. En el último de estos tres artículos, estudiaré three.js y veré cómo dibujar realmente esos gráficos de alta fidelidad.


Michael Oneppo es un especialista en tecnología creativa que fue jefe de programas en el equipo de Direct3D de Microsoft. Entre sus esfuerzos recientes se incluye su trabajo como director de tecnología en la tecnología sin ánimo de lucro Library For All y la investigación en un máster en el NYU Interactive Telecommunications Program.

Gracias al siguiente experto técnico por su ayuda en la revisión de este artículo: Mohamed Ameen Ibrahim