Compartilhar via



Agosto de 2015

Número 8 do Volume 30

Desenvolvimento de jogos - Introdução ao 3D para jogos da Web

Por Michael Oneppo | Agosto de 2015

A adição de uma terceira dimensão a um jogo, realmente dá vida a ele. Você pode olhar ao redor de qualquer ponto de vista e ver todos os ângulos de um objeto ou uma cena. Mas, como você pode realmente fazer que isso aconteça nos bastidores das cenas? Nesta série de artigos, vou percorrer as etapas para criar jogos 3D e mostrar a você como bibliotecas como a Three.js pode ajudá-lo a obter o ambiente em 3D sofisticado que estão se tornando tão populares na Web. Nesta primeira edição, vou manter a simplicidade e me concentrar na criação de uma versão 3D do jogo Ping, descrito pela primeira vez em “Um jogo da Web em uma hora” (msdn.microsoft.com/magazine/dn913185).

A ilusão do 3D

Qualquer processamento de gráfico em 3D tem um truque surpreendente em sua manga. As pessoas realmente não veem em 3 dimensões, principalmente em um monitor de computador. O principal objetivo do desenho em 3D é gerar, ou renderizar, uma descrição em 3D de uma cena em uma imagem em 2D. Quando você adiciona uma terceira dimensão para obter cenas mais envolventes e realistas, você precisa descartar alguns dados para obter uma imagem de um ponto de vista específico. Esse conceito é chamado de projeção. Esse é um elemento fundamental em como fazer os gráficos em 3D funcionarem, conforme mostrado na cena básica em 3D na Figura 1.

Uma cena em 3D simples
Figura 1 Uma cena em 3D simples

Nessa cena, o eixo Z é recuado para cima e para trás. Se eu quisesse realmente exibi-lo na tela, eu poderia passar apenas as informações de Z de todos os objetos como uma maneira simples e válida para projetar a cena em 3D, conforme mostrado na Figura 2.

Uma cena achatada em 3D
Figura 2 Uma cena achatada em 3D

Como você pode ver, isso não é exatamente o Halo. Para o realismo fotográfico, uma cena em 3D precisa de três coisas: uma projeção de câmera adequada, geometria e sombreamento. Falarei sobre cada um deses três conceitos, na medida que reconstruo o jogo Ping como um jogo de duelo em 3D.

Introdução

Primeiro, vou configurar a biblioteca do Three.js. Essa é uma configuração bem rápida, como quase tudo o que se faz com o three.js ocorre no JavaScript. Aqui está o código HTML que você precisará:

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

No arquivo em JavaScript ping3d.js, vou configurar o three.js para renderizar uma cena simples. Primeiro, preciso inicializar o three.js e adicionar sua tela de desenho à página:

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

A cena é exatamente o que parece: um objeto que descreve nossa cena e todos os objetos nela. O processador também é obviamente nomeado. Ao receber um cena, o processador a desenhará na tela. Isso deve ser semelhante a alguns dos sistemas de desenho em 2D descritos nos artigos anteriores, “Um jogo da Web em uma hora”, “Técnicas de desenho 2D e bibliotecas para jogos da Web” (msdn.microsoft.com/magazine/dn948109) e “Mecanismos de jogos 2D para a Web” (msdn.microsoft.com/magazine/dn973016). Agora, preciso adicionar alguns elementos à tela.

Geometria

Quase todos os gráficos 3D são criados de polígonos. Mesmo as superfícies curvas como uma bola são aproximadas em faces triangulares para aproximar de sua superfície. Quando reunidos, esses triângulos são chamados de uma malha. Aqui está como adiciono a bola à cena:

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

Esse código cria um grande número de triângulos representando uma esfera (a “geometria” variável), um material vermelho brilhante simples (o “material”) e um objeto de malha (a “malha”). Isso adicionará a malha à cena.

O triângulo é o bloco de construção fundamental de gráficos 3D. Por que isso acontece? Investigarei isso mais profundamente no próximo artigo desta série, mas os dois motivos principais são que as linhas retas que compõem um triângulo são fáceis de trabalhar e não é possível dividir um triângulo em uma superfície plana mais simples. A unidade de processamento gráfico (GPU) em seu computador ou telefone dedicou hardware que pode converter rapidamente formas com linhas retas em pixels. Isso é uma boa parte do que possibilita gráficos 3D de alta qualidade.

Modelagem

Posso passar qualquer geometria para o construtor do three.Mesh. Isso inclui a geometria gerada para criar formas personalizadas ou até mesmo dados de arquivos. Para o jogo Ping, gostaria de ter modelos em 3D de cada um dos jogadores. Por isso, tomei a liberdade de criação de geometria em um programa de modelagem 3D para esse exercício. É surpreendentemente fácil de usar o modelo em vez de uma esfera, na medida que o three.js fornece uma ferramenta de carregamento para esse fim:

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âmera

A câmera representa o ponto de vista da cena. Ela armazena a posição e o ângulo do visualizador no jogo. Mais importante, a câmera representa como a cena se torna bidimensional, conforme descrito no início deste artigo.

No meu exemplo, a câmera foi posicionada para baixo e à direita. A imagem final aparece como se fosse visualizada dessa direção. No entanto, usando esse método de projeção, não importa a distância dos objetos, eles permanecerão do mesmo tamanho na imagem final. Isso é chamado de uma projeção ortográfica. Isso geralmente é útil para jogos com ângulos de exibição não realistas como jogos de simulação de cidades. O que realmente quero descobrir é como fazer os objetos parecerem menores na medida que eles recuam na distância.

Insira a projeção da perspectiva: A projeção da perspectiva imagina o campo de visualização de uma câmera como uma extensão da pirâmide das lentes. Quando as posições são mapeadas para a tela, elas são calculadas com base em suas distâncias relativas ao lado da pirâmide. Usando esse modelo, conforme os objetos recuam na distância, eles parecem diminuir como na vida real.

Felizmente, você não precisa fazer esse mapeamento porque o three.js faz isso para você e fornece um objeto que representa a câmera na cena (e adicionar outra é simples):

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

O primeiro argumento é o campo de visualização, que indica uma distância angular tirada horizontalmente. O segundo argumento e a proporção entre a largura e a altura da tela, que você precisa assegurar para que os objetos não fiquem espremidos porque a tela não é quadrada. Os dois parâmetros finais definem a menor e a maior distância para mostrar. Qualquer coisa mais próxima ou mais distante desses valores não será desenhada. Agora estou no ponto onde eu realmente posso desenhar a cena. Vamos voltar a câmera um pouco para ver a cena inteira e começar a desenhar:

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

Materiais e luzes

Em seguida, vou colocar a bola na arena na qual ela será ricocheteada:

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

Estou fazendo algo diferente do que fazer apenas a geometria da caixa. Além disso, estou fazendo um material. Um material é uma definição de como algo deve refletir a luz em uma cena. Isso gera sua aparência geral. Nesse caso, estou fazendo um material Phong, que é um bom padrão para objetos brilhantes. Também estou adicionando uma textura à caixa, que é simplesmente usar a função loadTexture no three.js.

Um outro aspecto importante desse código é a linha que lê: lado: THREE.BackSide. Isso instrui o Three.js para desenhar somente os lados internos da superfícies da caixa, e não os lados externos. Isso proporciona um lugar para a bola ricochetear, em vez de ter uma caixa sólida flutuando no espaço.

Se eu fosse desenhar a cena agora, a arena do jogo não seria visível. Ela seria desenhada apenas em preto. Isso ocorre porque os materiais definem como a luz reflete os objetos e ainda não tenho luz na cena. O Three.js torna simples a adição de luz a uma cena, como mostrado aqui:

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

Agora, se desenhar a cena, a arena será renderizada corretamente. Para tornar a visualização melhor, vou definir a posição da câmera para olhar no lado da arena antes de executar o 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));

A primeira linha define a variável para cima, que simplesmente informa à câmera qual é o sentido de para cima. A função lookAt faz exatamente o que parece: ela aponta a câmera para a posição especificada.

Fazendo um jogo em 3D

Agora que o jogo foi movido em três dimensões, fazer o restante deve ser bem fácil. No entanto, este jogo está indo para um final um pouco mais detalhado do que as implementações anteriores, porque ele é composto de objetos em 3D em vez de 2D. Portanto, vou dividir o código em arquivos separados para facilitar como lidar com o código adicional.

Também substituirei os estilos em JavaScript para a definição do objeto para um modelo de construtor mais tradicional. Para demonstrar isso, encapsulei a caixa da arena e as luzes em um objeto e o coloquei em um único arquivo, como mostrado na Figura 3.

Figura 3 O objeto Arena

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

Se eu quiser criar uma Arena, posso criar um novo objeto usando essa função do construtor:

var arena = new Arena(scene);

Em seguida, vou fazer um objeto da bola que pode ricochetear ao redor da arena. Sei como fazer uma bola vermelha em Three.js, portanto, vou encapsular o código em um objeto:

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

Agora, vou definir a física básica do ricocheteio da bola adicionando uma função ao objeto bola, conforme mostrado na Figura 4.

Figura 4 A função Atualizar do objeto 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;
  }
}

O Three.js requer a renderização da cena toda vez que se usa o requestAnimationFrame. Isso deveria ser um padrão 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);

Fique ligado

Agora tenho uma arena com luzes, uma câmera bem posicionada e uma bola em movimento ao redor do cenário. Isso é tudo o que vou abordar neste artigo. No próximo artigo, explicarei como a projeção em 3D funciona, permitindo que você direcione com o mouse. Também explicarei mais sobre texturas e como tornar as animações suaves usando uma poderosa biblioteca chamada tween.js. No último desses três artigos, vou examinar os bastidores do three.js e ver como ele está realmente desenhando esses gráficos de alta fidelidade.


Michael Oneppo é um tecnólogo criativo e antigo gerente de programas da Microsoft na equipe do Direct3D. Seus esforços recentes incluem trabalhar como CTO em tecnologia sem fins lucrativos da Library For All e explorar um mestrado no Programa de Telecomunicações Interativas da NYU.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Mohamed Ameen Ibrahim