2015 年 8 月

第 30 卷,第 8 期

游戏开发 - Web 游戏的 3D 简介

作者 Michael Oneppo | 2015 年 8 月

向游戏中添加第三维度,使游戏真正地“活”起来。您可以从任何视角环顾四周,并从任意角度查看对象或场景。但是,如何在幕后实际完成此事呢? 在本系列的文章中,我将向您介绍制作 3D 游戏的所有步骤,并向您展示 three.js 等库是如何帮助您实现丰富的 3D 环境的,从而在 Web 上非常受欢迎。在此第一部分中,我将从简单的地方着手,并侧重于先创建一个 3D 版的 Ping 游戏,该游戏在《在一小时内开发网络游戏》(msdn.microsoft.com/magazine/dn913185) 中有说明。

3D 视觉效果

任何 3D 图形的呈现都有惊人的技巧。实际上,人类无法看到 3D 维度,尤其是在计算机显示器上。3D 绘图的整个目标是生成或呈现 2D 图像上的场景的 3D 说明。当您添加第三维度以获取身临其境的体验时,您必须抛弃一些数据以从特定的视角中获取图像。这一概念称为投影。这是使 3D 图形工作的基本元素,如图 1 中的基本 3D 场景所示。

简单的 3D 场景
图 1 简单的 3D 场景

在此场景中,Z 轴向上和向后退去。实际上,如果我要在屏幕上查看该轴,只要从每个对象中拖动 Z 信息即可,因为这是一种简单且有效地投射 3D 场景的方法,如图 2 中所示。

受限制的 3D 场景
图 2 受限制的 3D 场景

如您所见,这并非是 Halo。对于照相现实主义者,3D 场景需要三样东西—一个合适的照相机投影、几何结构和底纹。当我将 Ping 游戏重建为 3D 决斗游戏时,我会对其中每个概念进行说明。

开始使用

首先,我将设置 three.js 库。这个配置相当快,几乎与您在 JavaScript 中对 three.js 执行操作时同时发生。下面是您将需要的 HTML 代码:

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

在 JavaScript 文件 ping3d.js 中,我将设置 three.js 以呈现一个简单的场景。首先,我需要初始化 three.js 并将它的绘图画布添加到页面:

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

这个场景正如预期一样—说明所有场景的对象以及其中的所有对象。呈现器的命名也很明显。如果给定一个场景,呈现器将在屏幕上绘制该场景。这应该和我在之前的文章中所描述的某些 2D 绘图系统一样,这些文章包括《在一小时内开发网络游戏》、《适用于 Web 游戏的 2D 绘图技术和库》(msdn.microsoft.com/magazine/dn948109) 以及《适用于 Web 的 2D 游戏引擎》(msdn.microsoft.com/magazine/dn973016)。现在,我需要向屏幕中添加一些元素。

几何结构

几乎所有的 3D 图形都是根据多边形构建而来的。甚至是包括球在内的所有曲面都近似于三角形的面,从而近似于其曲面。在组装时,这些三角形被称为网格。以下是我向场景中添加球的方法:

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

此代码将创建大量表示球体(“geometry”变量)的三角形、简单的鲜红色材料(“material”)和网格对象(“mesh”)。然后,它会将该网格添加到场景。

三角形是 3D 图形的基本构建基块。为什么会这样? 我将在本系列的下一篇文章中深入研究这方面,但是两个主要原因是:组成三角形的直线易于使用,而且您无法将一个三角形分解为更加基本的平面区域。计算机或手机上的图形处理单元 (GPU) 具有专用硬件,可以快速地将使用直线的形状转换成像素。这是实现高质量的 3D 图形的重要部分。

建模

我可以将任何几何结构传递到 three.Mesh 构造函数。这包括制作自定义形状或文件中数据的生成几何结构。对于 Ping 游戏,我希望拥有每个玩家的 3D 模型。因此,在此次练习中,我擅自在 3D 建模程序中创建几何结构。使用该模型比使用球体更加容易,因为 three.js 提供了正适合此目的的加载工具:

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

相机

相机表示场景的视角。它可存储游戏中的查看器的位置和角度。更重要的是,相机表示场景如何扁平化的过程,如本文开头处所述。

在我给出的示例中,相机被置于右下角。最终图像显示为该方向的视图。不过,无论对象的距离有多远,使用此方法后,它们在最终的图像中保持相同的大小。这称为正射投影。对于含有非现实视角(如城市模拟游戏)的游戏,这非常有帮助作用。实际上,我想要实现的目标是:当对象远去时,对象显示越小。

进入透视投影: 透视投影将相机的视野想象为从镜头中扩展的金字塔。将位置映射到屏幕时,系统会根据它们相对于金字塔各端的距离计算它们的位置。使用此方法之后,当对象远去时,它们会逐渐缩小,像现实生活一样。

幸运的是,您无需亲自进行此类映射,因为 three.js 可以为您完成这些操作,并向您提供一个表示场景中的相机的对象(而且,添加其他对象非常简单):

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

第一个参数是视野,其表示水平调整的角距离。第二个参数是屏幕的宽高比。您可以使用此参数来确保屏幕上的事物不受挤压,因为屏幕不是方形的。最后两个参数定义要显示的最近距离和最远距离。不会绘制比这更近或更远的任何事物。现在,我实际上已经可以绘制场景了。我们往回移动一点相机,以查看整个场景并开始绘制:

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

材料和光线

接下来,我将在竞技场中放置一个球,这个球可以上下弹跳:

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

我要制作一些不同的东西,而不是只制作边框几何结构。我也正在制作材料。材料是事物如何反射场景中的光线的定义。这将生成它的整体外观。在这种情况下,我要制作一个 Phong 材料,这是非常棒的默认发光物体。我还要向该边框中添加纹理,这可以通过使用 loadTexture 函数轻松地在 three.js 中完成。

此代码的另一个值得注意的方面是代码行:side: THREE.BackSide。这会指示 three.js 仅绘制边框曲面的内侧,而非外侧。这可以为球留出空间进行弹跳,而不是让一个实体球在空间中浮动。

如果我现在绘制场景,则该竞技场也不可见。它只是绘制黑色。这是因为材料定义光线反射对象的方式,而我在场景中尚未设置光线。Three.js 使得向场景中添加光线非常简单,如以下所示:

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

现在,如果我绘制场景,则可正常呈现该竞技场。若要使视图更佳,我会设置相机位置以查看竞技场的内部,然后再运行代码:

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

第一行设置向上变量,只是告诉相机哪个方向是向上。lookAt 函数正如其名所示,它在指定位置中指出相机。

制作一个 3D 游戏

现在,该游戏已经移到三维,剩下的事情就相对简单了。但是,与以前的实施情况相比,此游戏的结尾更加冗长,因为它是由 3D 对象组成,而非 2D 对象组成。因此,我将把代码分成几个单独的文件,以使其他代码更加容易处理。

此外,我还将对象定义的 JavaScript 格式转换成更加传统的构造函数模型。为了演示这一点,我将竞技场边框和光线打包到一个对象并将其放到一个文件中,如图 3 中所示。

图 3 竞技场对象

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

如果我要创建竞技场,则可以使用此构造函数来新建对象:

var arena = new Arena(scene);

接下来,我要制作一个可以在竞技场周围来回跳动的球对象。我知道如何在 three.js 中制作一个红色的球,因此我也将该代码打包到对象中:

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

现在,通过向球对象中添加函数,我将定义弹跳的基本物理过程,如图 4 中所示。

图 4 球对象的更新函数

// 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 要求您每次使用 requestAnimationFrame 呈现场景。这应该是熟悉的模式:

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

休息片刻再继续

现在,我有一个含有光线的竞技场,放在适当位置的相机以及在场景周围弹跳的球。这就是我打算在本文中介绍的全部内容。在下一步中,通过让您借助鼠标瞄准目标,我将解释 3D 投影的工作原理。我还会解释有关结构的更多信息,并使用强大的库(称为 tween.js)来制作流畅的动画。在三篇文章的最后一篇中,我将探究 three.js 背后的内容,并查看它实际上是如何绘制此类高保真图形的。


Michael Oneppo 是富有创造性思维的技术专家,并且以前是 Microsoft Direct3D 团队的项目经理。他最近的工作包括担任 Library For All(非盈利技术)的 CTO 和致力于探索获得纽约大学互动电子传播研究所的硕士学位。

衷心感谢以下技术专家对本文的审阅: Mohamed Ameen Ibrahim