上面这个网页 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, .9)
// 平行光DirectionLight是从指定方向照射过来的光源。在此项目里用它来实现太阳光,所以它产生的光都是平行的
shadowLight = new THREE.DirectionalLight(0xffffff, .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:.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 = .1 + Math.random()*.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(.25,.25,.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 += .005;
sky.mesh.rotation.z += .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 += .005;
sky.mesh.rotation.z += .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 我们是这么容易满足的吗?并不~~ 我们的海还不够海,云还不够云,飞机也不够飞机。
而怎么让它看起来更自然,那就是我们第二部分要说的啦~~~