174-fly.gif

上面这个网页 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 世界的大门!~~~

174-1.jpg

让我们现在开始吧!(我觉得这句我翻的最棒了[译]使用 Three.js 开发的 WebGL 3D飞机游戏 PART 1 - 图3

The HTML & CSS

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

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

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

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

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

  1. #world {
  2. position: absolute;
  3. width: 100%;
  4. height: 100%;
  5. overflow: hidden;
  6. background: linear-gradient(#e4e0ba, #f7d9aa);
  7. }

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

The JavaScript

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

调色板

174-2.png

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

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 来设置阴影。

174-3.png

我们将场景 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对象追加到场景中

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

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

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

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

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轴上随机排列,那就是一片天呐 [译]使用 Three.js 开发的 WebGL 3D飞机游戏 PART 1 - 图8

// 定义天空对象
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);
}

看这就是朕给你打下的一片天
174-6.png

更复杂的:创建飞机

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

174-7.png

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

当当当当~~
174-8.png

渲染

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

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;

}

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

174-demo.gif

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

but 我们是这么容易满足的吗?并不~~ 我们的海还不够海,云还不够云,飞机也不够飞机。

而怎么让它看起来更自然,那就是我们第二部分要说的啦~~~ [译]使用 Three.js 开发的 WebGL 3D飞机游戏 PART 1 - 图13