翻译:使用 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 我们是这么容易满足的吗?并不~~ 我们的海还不够海,云还不够云,飞机也不够飞机。
而怎么让它看起来更自然,那就是我们第二部分要说的啦~~~
PREV
基于 Bootstrap file input 的文件上传 Demo(+php)
NEXT