翻译:使用 Three.js 开发的 WebGL 3D飞机游戏 PART 1

上面这个网页 3D 飞机小游戏是不是看起 6666 的?这是 Codrops 里一个用 Three.js 写的 3D 小游戏,游戏里你可以用鼠标控制飞机的飞行,游戏规则就是躲避红色小球,吃蓝色能力块,争取飞得远。另外这款小游戏还支持 PC、平板和手机端。 具体怎么实现的就得提到 WebGL 和 Three.js 了。

开场白

WebGL 可以让我们在 canvas 上实现 3D 效果。而 Three.js 是一款 WebGL 框架,由于其易用性被广泛应用。如果你要学习 WebGL,抛弃那些复杂的原生接口从这款框架入手是一个不错的选择。

因为我对 WebGL 非常感兴趣,刚好看见这篇 Three.js 小游戏教程特别有趣,但原文是英文的,所以想着翻译一下,一来可以帮我重新了解一下 Three.js,二来可以看看他们在开发时的思路。当然在翻译过程我会加点我自己的看法、理解和表情包,争取做到更好读懂一些,如果有什么地方有错的话,欢迎指出。好啦,下面开始翻译教程~~~~

“The Aviator”教程:使用 three.js 来实现基本的 3D 动画

今天,我们要使用 Three.js 来创建一个简单的 3D 飞机,Three.js 是编写 WebGL 的第三方库,它让 WebGL 变的简单。对许多开发者来说 WebGL 是一个未知的世界,因为 GLSL 的复杂性和语法。但是,有了 Three.js,就可以让 3D 效果在浏览器中变得非常容易实现。

在本教程中,我们将创建一个简单 3D 场景。第一部分,我们解释一些 three.js 的基本知识和如何建立一个非常简单的场景。第二部分我们将详细介绍如何优化形状,如何在不同元素的场景中添加一些互动。

完整版的游戏超出了本教程的范围,但是你可以下载和查看代码;它包含许多有趣的附加部分,比如碰撞,吃硬币和加分。

在本教程中我们将中重点放在一些基本概念,可以让你使用 Three.js 打开 WebGL 世界的大门!~~~

让我们现在开始吧!(我觉得这句我翻的最棒了

The HTML & CSS

本教程主要使用 three.js。更多信息你可以看 three.js 的官网 threejs.org 和 github. 第一步,将 three.js 引入你 HTML 文件的头部。

<script type="text/javascript" src="js/three.js"></script>

然后你要在 HTML 里写一个渲染场景的容器

<div id="world"></div>

加上 css 样式,让容器充满整个窗口

#world {
  position: absolute;
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: linear-gradient(#e4e0ba, #f7d9aa);
}

这时候背景有类似天空一样的渐变效果。

The JavaScript

如果你有一些 JavaScript 基本知识的话,那么在使用 three.js 上就非常简单。

调色板

在开始码代码前,定义一个在整个项目中使用的调色板是非常有用的。对于这个项目,我们选择以下颜色

var Colors = {
  red: 0xf25346,
  white: 0xd8d0d1,
  brown: 0x59332e,
  pink: 0xf5986e,
  brownDark: 0x23190f,
  blue: 0x68c3c0
}

代码结构

虽然整个 JavaScript 代码很冗长,但它的结构却很简单。所有主要的方法我们都把它放到初始化 init 函数里

window.addEventListener('load', init, false)

function init() {
  // 设置场景scene,摄像机camera和渲染器renderer
  createScene()

  // 添加光源lights
  createLights()

  // 添加物体objects
  createPlane()
  createSea()
  createSky()

  // 循环,将更新的物体的位置,并每一帧渲染场景
  loop()
}

设置场景

写一个 three.js 项目,我们至少需要以下几点:

场景 scene:它可以看成一个将所有的对象渲染的平台

摄像机 camera:此项目中我们使用 PerspectiveCamera 投影相机,当然你也可以使用 OrthographicCamera 正交投影相机。

渲染器 renderer:渲染器会将所有场景在 WebGL 上渲染出来

物体 objects:渲染一个或多个物体,这里我们会设置一架飞机、大海和天空(云朵)

光源 lights:three.js 有很多不同类型的可用光源。在此项目中,我们主要使用半球光 HemisphereLight 来设置环境光,平行光 DirectionLight 来设置阴影。

我们将场景 scene、摄像机 camera 和渲染器 renderer 都写在 createScene 函数里

var scene, camera, fieldOfView, aspectRatio, nearPlane, farPlane, HEIGHT, WIDTH, renderer, container

function createScene() {
  // 获取场景的宽和高,用它们来设置相机的纵横比和渲染器的的尺寸
  HEIGHT = window.innerHeight
  WIDTH = window.innerWidth

  // 创建场景scene
  scene = new THREE.Scene()

  // 在场景中添加雾效; 颜色与css中背景颜色相同
  scene.fog = new THREE.Fog(0xf7d9aa, 100, 950)

  // 创建摄像机camera
  aspectRatio = WIDTH / HEIGHT
  fieldOfView = 60
  nearPlane = 1
  farPlane = 10000
  camera = new THREE.PerspectiveCamera(fieldOfView, aspectRatio, nearPlane, farPlane)

  // 设置摄像机的坐标
  camera.position.x = 0
  camera.position.z = 200
  camera.position.y = 100

  // 创建渲染器renderer
  renderer = new THREE.WebGLRenderer({
    // 允许背景透明,这样可以显示我们在css中定义的背景色
    alpha: true,

    // 开启抗锯齿效果; 性能变低,但是,因为我们的项目是基于低多边形的,应该还好
    antialias: true
  })

  // 定义渲染器的尺寸,此项目中它充满整个屏幕
  renderer.setSize(WIDTH, HEIGHT)

  // 启用阴影渲染
  renderer.shadowMap.enabled = true

  // 将渲染器元素追加到我们在HTML里创建的容器元素里
  container = document.getElementById('world')
  container.appendChild(renderer.domElement)

  // 监听屏幕:如果用户改变屏幕尺寸,必须更新摄像机和渲染器的尺寸
  window.addEventListener('resize', handleWindowResize, false)
}

随着屏幕尺寸的改变,我们需要更新渲染器的尺寸和摄像机的纵横比

function handleWindowResize() {
  // 更新渲染器和摄像机的宽高
  HEIGHT = window.innerHeight
  WIDTH = window.innerWidth
  renderer.setSize(WIDTH, HEIGHT)
  camera.aspect = WIDTH / HEIGHT
  camera.updateProjectionMatrix()
}

光源

var hemisphereLight, shadowLight

function createLights() {
  // 半球光HemisphereLight是渐变色光源;第一个参数是天空的颜色,第二个参数是地面的颜色,第三个参数是光源的强度
  hemisphereLight = new THREE.HemisphereLight(0xaaaaaa, 0x000000, 0.9)

  // 平行光DirectionLight是从指定方向照射过来的光源。在此项目里用它来实现太阳光,所以它产生的光都是平行的
  shadowLight = new THREE.DirectionalLight(0xffffff, 0.9)

  // 设置光源的位置
  shadowLight.position.set(150, 350, 350)

  // 允许投射阴影
  shadowLight.castShadow = true

  // 定义投射阴影的可见区域
  shadowLight.shadow.camera.left = -400
  shadowLight.shadow.camera.right = 400
  shadowLight.shadow.camera.top = 400
  shadowLight.shadow.camera.bottom = -400
  shadowLight.shadow.camera.near = 1
  shadowLight.shadow.camera.far = 1000

  // 定义阴影的分辨率; 越高越好,但性能也越低
  shadowLight.shadow.mapSize.width = 2048
  shadowLight.shadow.mapSize.height = 2048

  // 把光源添加到场景中激活它们
  scene.add(hemisphereLight)
  scene.add(shadowLight)
}

你可以看见,有很多设置光源的参数。你可以尝试去改变一些参数,比如,颜色,强度和灯的数量。慢慢你就会有感觉,就能调整出你需要的效果的参数。

创建物体 Object

如果你习惯使用 3D 建模软件的话,你可以建模并把它导入 three.js 项目中。但为了更好的理解 three.js 是怎样工作的,本教程中不使用上面的方案,我们用基本的形状去创建物体。

Three.js 里有很多可用的简单形状,比如立方体、球、环形、圆柱和平面。我们要创建的物体都可以用这些简单的形状组合起来。

用圆柱体创建一个大海对象

为了好理解,我们可以想象大海就是放在屏幕的底部蓝色圆柱。第二部分我们将详细介绍关于如何优化大海,让海浪看起来更真实

// 先定义一个大海对象:
Sea = function () {
  // 创建一个圆柱形几何体 Geometry;
  // 它的参数: 上表面半径,下表面半径,高度,对象的半径方向的细分线段数,对象的高度细分线段数
  var geom = new THREE.CylinderGeometry(600, 600, 800, 40, 10)

  // 让它在X轴上旋转
  geom.applyMatrix(new THREE.Matrix4().makeRotationX(-Math.PI / 2))

  // 创建材质Material
  var mat = new THREE.MeshPhongMaterial({
    color: Colors.blue,
    transparent: true,
    opacity: 0.6,
    shading: THREE.FlatShading
  })

  // 在 Three.js里创建一个物体 Object,我们必须创建一个 Mesh对象,
  // Mesh对象就是 Geometry 创建的框架贴上材质 Material 最后形成的总体。
  this.mesh = new THREE.Mesh(geom, mat)

  // 允许大海接收阴影
  this.mesh.receiveShadow = true
}

// 实例化大海对象,并把它添加到场景scene中:
var sea

function createSea() {
  sea = new Sea()

  // 把它放到屏幕下方
  sea.mesh.position.y = -600

  // 在场景中追加大海的Mesh对象
  scene.add(sea.mesh)
}

让我们来总结一下,为了要创建一个物体对象,我们需要 1.创建几何体 2.创建材质 3.把它们放到 Mesh 对象里 4.将 Mesh 对象追加到场景中

有了这些基本步骤,我们就可以创建许多不同种类的基础对象。把这些基础对象组合起来,就可以创建更多复杂的形状。在下面的步骤中,我们将学习怎样做。


(好啦 到了这时候我们全是水的大海大概就长下面这样 嘿嘿嘿 当然我这是已经渲染过的)

用简单的立方体来创建复杂形状

云要更复杂一些,它由一些立方体随机组合起来。

Cloud = function () {
  // 创建一个空的容器用来存放不同部分的云
  this.mesh = new THREE.Object3D()

  // 创建一个立方体;复制多个,来创建云
  var geom = new THREE.BoxGeometry(20, 20, 20)

  // 创建云的材质,简单的白色
  var mat = new THREE.MeshPhongMaterial({
    color: Colors.white
  })

  // 随机定义要复制的几何体数量
  var nBlocs = 3 + Math.floor(Math.random() * 3)
  for (var i = 0; i < nBlocs; i++) {
    // 给复制的几何体创建Mesh对象
    var m = new THREE.Mesh(geom, mat)

    // 给每个立方体随机的设置位置和角度
    m.position.x = i * 15
    m.position.y = Math.random() * 10
    m.position.z = Math.random() * 10
    m.rotation.z = Math.random() * Math.PI * 2
    m.rotation.y = Math.random() * Math.PI * 2

    // 随机的设置立方体的尺寸
    var s = 0.1 + Math.random() * 0.9
    m.scale.set(s, s, s)

    // 允许每朵云生成投影和接收投影
    m.castShadow = true
    m.receiveShadow = true

    // 把该立方体追加到上面我们创建的容器中
    this.mesh.add(m)
  }
}

现在我们已经创建出来一片云了,如果把它复制,然后在 Z 轴上随机排列,那就是一片天呐

// 定义天空对象
Sky = function () {
  // 创建一个空的容器
  this.mesh = new THREE.Object3D()

  // 设定散落在天空中云朵的数量
  this.nClouds = 20

  // To distribute the clouds consistently,
  // we need to place them according to a uniform angle
  var stepAngle = (Math.PI * 2) / this.nClouds

  // 创建云朵
  for (var i = 0; i < this.nClouds; i++) {
    var c = new Cloud()

    // 给每朵云设置角度和位置;
    var a = stepAngle * i // 云最终的角度
    var h = 750 + Math.random() * 200 // 轴中心到云的距离

    // 将极坐标(角度、距离)转换成笛卡尔坐标(x,y)
    c.mesh.position.y = Math.sin(a) * h
    c.mesh.position.x = Math.cos(a) * h

    // 根据云的位置做旋转
    c.mesh.rotation.z = a + Math.PI / 2

    // 为了更真实,有远有近
    c.mesh.position.z = -400 - Math.random() * 400

    // 给每朵云设置比例
    var s = 1 + Math.random() * 2
    c.mesh.scale.set(s, s, s)

    // 将每朵云追加到场景中
    this.mesh.add(c.mesh)
  }
}

// 实例化天空对象,并把它的中心点放到屏幕下方
var sky

function createSky() {
  sky = new Sky()
  sky.mesh.position.y = -600
  scene.add(sky.mesh)
}

看这就是朕给你打下的一片天

更复杂的:创建飞机

坏消息是。飞机代码更长也更复杂。好消息是,我们已经学会了怎么创建它,全部都是组合和封装形状。

var AirPlane = function () {
  this.mesh = new THREE.Object3D()

  // 创建机舱
  var geomCockpit = new THREE.BoxGeometry(60, 50, 50, 1, 1, 1)
  var matCockpit = new THREE.MeshPhongMaterial({ color: Colors.red, shading: THREE.FlatShading })
  var cockpit = new THREE.Mesh(geomCockpit, matCockpit)
  cockpit.castShadow = true
  cockpit.receiveShadow = true
  this.mesh.add(cockpit)

  // 创建发动机
  var geomEngine = new THREE.BoxGeometry(20, 50, 50, 1, 1, 1)
  var matEngine = new THREE.MeshPhongMaterial({ color: Colors.white, shading: THREE.FlatShading })
  var engine = new THREE.Mesh(geomEngine, matEngine)
  engine.position.x = 40
  engine.castShadow = true
  engine.receiveShadow = true
  this.mesh.add(engine)

  // 创建机尾
  var geomTailPlane = new THREE.BoxGeometry(15, 20, 5, 1, 1, 1)
  var matTailPlane = new THREE.MeshPhongMaterial({ color: Colors.red, shading: THREE.FlatShading })
  var tailPlane = new THREE.Mesh(geomTailPlane, matTailPlane)
  tailPlane.position.set(-35, 25, 0)
  tailPlane.castShadow = true
  tailPlane.receiveShadow = true
  this.mesh.add(tailPlane)

  // 创建机翼
  var geomSideWing = new THREE.BoxGeometry(40, 8, 150, 1, 1, 1)
  var matSideWing = new THREE.MeshPhongMaterial({ color: Colors.red, shading: THREE.FlatShading })
  var sideWing = new THREE.Mesh(geomSideWing, matSideWing)
  sideWing.castShadow = true
  sideWing.receiveShadow = true
  this.mesh.add(sideWing)

  // 创建螺旋桨
  var geomPropeller = new THREE.BoxGeometry(20, 10, 10, 1, 1, 1)
  var matPropeller = new THREE.MeshPhongMaterial({ color: Colors.brown, shading: THREE.FlatShading })
  this.propeller = new THREE.Mesh(geomPropeller, matPropeller)
  this.propeller.castShadow = true
  this.propeller.receiveShadow = true

  // blades
  var geomBlade = new THREE.BoxGeometry(1, 100, 20, 1, 1, 1)
  var matBlade = new THREE.MeshPhongMaterial({ color: Colors.brownDark, shading: THREE.FlatShading })

  var blade = new THREE.Mesh(geomBlade, matBlade)
  blade.position.set(8, 0, 0)
  blade.castShadow = true
  blade.receiveShadow = true
  this.propeller.add(blade)
  this.propeller.position.set(50, 0, 0)
  this.mesh.add(this.propeller)
}

虽然现在我们的小飞机看起来有点 low,但后面我们会优化哒。好啦,现在我们把飞机实例化并追加到场景中。

var airplane

function createPlane() {
  airplane = new AirPlane()
  airplane.mesh.scale.set(0.25, 0.25, 0.25)
  airplane.mesh.position.y = 100
  scene.add(airplane.mesh)
}

当当当当~~

渲染

我们上面已经创建了几个物体对象并把它们追加到场景里了,但是如果你现在运行的话,却什么都看不到(上面的截图是因为我已经渲染过了)。
因为我们还没有渲染场景,通过下面这一行代码就可以很简单的做到渲染

renderer.render(scene, camera)

动画

让我们给我们的场景中加点生机,比如让飞机的螺旋桨转起来,大海和云层动起来。 所以,我们要写一个无限循环:

function loop() {
  // 转动螺旋桨、大海和天空
  airplane.propeller.rotation.x += 0.3
  sea.mesh.rotation.z += 0.005
  sky.mesh.rotation.z += 0.01

  // 渲染场景
  renderer.render(scene, camera)

  // 再次调用 loop 函数
  requestAnimationFrame(loop)
}

可以看见,我们把渲染方法写到了 loop 函数里,因为在每次改变对象时都需要再重新渲染场景。

跟随鼠标:加入互动

这时候,我们的小飞机在场景的中心,下面我们要实现的是让它跟着鼠标移动。 当文档加载完时,我们需要给文档添加一个监听器来监听鼠标是否移动。

因此,我们将 init 函数做如下修改:

function init(event) {
  createScene()
  createLights()
  createPlane()
  createSea()
  createSky()

  // 添加监听器
  document.addEventListener('mousemove', handleMouseMove, false)

  loop()
}

此外,我们需要新创建一个函数来处理鼠标的移动事件:

var mousePos = { x: 0, y: 0 }

// 处理鼠标移动事件
function handleMouseMove(event) {
  // 将鼠标位置归一化到-1和1之间
  // 横轴的函数公式
  var tx = -1 + (event.clientX / WIDTH) * 2

  // 对纵轴来说,我们需求反函数,因为2D的y轴和3D的y轴方向相反

  var ty = 1 - (event.clientY / HEIGHT) * 2
  mousePos = { x: tx, y: ty }
}

现在,我们有鼠标规范化的 x 和 y 位置,就可以正常的移动飞机。

现在需要修改 loop 函数,添加一个新函数来更新飞机位置:

function loop() {
  sea.mesh.rotation.z += 0.005
  sky.mesh.rotation.z += 0.01

  // update the plane on each frame
  updatePlane()

  renderer.render(scene, camera)
  requestAnimationFrame(loop)
}

function updatePlane() {
  // 根据鼠标x轴位置在-1到1之间,我们规定飞机x轴移动位置在-100到100之间,
  // 同样规定飞机y轴移动位置在25到175之间。

  var targetX = normalize(mousePos.x, -1, 1, -100, 100)
  var targetY = normalize(mousePos.y, -1, 1, 25, 175)

  // 更新飞机的位置
  airplane.mesh.position.y = targetY
  airplane.mesh.position.x = targetX
  airplane.propeller.rotation.x += 0.3
}

function normalize(v, vmin, vmax, tmin, tmax) {
  var nv = Math.max(Math.min(v, vmax), vmin)
  var dv = vmax - vmin
  var pc = (nv - vmin) / dv
  var dt = tmax - tmin
  var tv = tmin + pc * dt
  return tv
}

以上就是这次教程的第一部分啦

到此,我们的小飞机可以跟着鼠标到处飞,满是水的大海也浪起来了,云也飘起来了,可以说一些基本的都实现了。

but 我们是这么容易满足的吗?并不~~ 我们的海还不够海,云还不够云,飞机也不够飞机。
而怎么让它看起来更自然,那就是我们第二部分要说的啦~~~

目录