使用THREE.js制作一款3D游戏

11,768 阅读7分钟

使用THREE.js制作一款3D游戏

本文是基于某位大神使用three.js设计游戏的学习心得与知识分享 The Making of “The Aviator”: Animating a Basic 3D Scene with Three.js

前言

这个学期选修了一门计算机图形学的课程,课程选用的教材是基于WebGL。在此之前我对计算机图形学是没有任何概念的,只知道如果想要设计一款游戏具有图形学的知识是很重要的。我从来就有一个念想就是制作一款游戏,因此我对这方面是很有兴趣的。 老师推荐了一本入门教材《WebGL编程指南》,这本教材讲的很详细很适合入门使用,我花了大约一周的时间将这本书看完,对webgl的编程有了个大致的了解,当然真的只是粗粗入门。学习过程中又了解到three.js这个基于webgl的第三方3d图形库,看到了很多酷炫的3d游戏都使用了这个库,我决定下一步就是学习这个库的使用。 我大概看了半本的《THREE.js开发指南》,这本书很系统的讲了这个第三方库,但是难免很枯燥,于是就找到了现在这个使用three.js设计游戏的这么一个小项目。

代码结构

主函数由各种构件场景的函数组成,十分简洁

function init(event){
    createScene();
    
    createLights();

    createPlane();
    
    createSea();
    
    createSky();
    
    document.addEventListener('mousemove', handleMouseMove, false);
    
    loop();//循环函数,用于最后每一个帧的重绘,实现动画效果
}

使用dat.gui图形界面动态的调整数据

dat.gui是一个第三方的图形库,通过这个图形界面来调整数据真的很方便

//通过dat.gui来调整环境光
var controls = new function () {//声明一个控制对象
    this.ambientLightColor = "#dc8874";
}
//环境光的值可以是16进制的数值,如"#ffffff",每次通过gui调整了color值都会触发下面的匿名函数从而调整环境光的颜色,环境光加入到场景中后每次渲染场景时都会使用最新的环境光颜色值,从而实现了使用gui调整环境光颜色的功能
var gui = new dat.GUI;//创建gui对象
gui.addColor(controls,'ambientLightColor').onChange(function (e) {
	ambientLight.color = new THREE.Color(e);// 
});

当然可以添加更多的数据来进行动态调整,比如照相机的位置,各种颜色数值,等等。这是一个超级使用的功能。

一、搭建一个场景

使用thre.js绘制3d图形最基本的就是需要一个场景,场景像一个容器,至少需要包括灯光,照相机和渲染器

function createScene(){

    HEIGHT = window.innerHeight;
    WIDTH = window.innerWidth;

    scene = new THREE.Scene();//创建场景
    scene.fog = new THREE.Fog(0xf7d9aa, 100,950);//使用雾化的效果
	var axes = new THREE.AxisHelper(200);//场景中添加一个三维坐标系,便于观察图形的位置
    scene.add(axes);
    aspectRatio = WIDTH / HEIGHT;//宽高比设置为窗口大小,避免图案的变形
    fieldOfView = 50;
    nearPlane = 0.1;
    farPlane = 10000;
    camera = new THREE.PerspectiveCamera(fieldOfView,aspectRatio,nearPlane,farPlane);//使用一个透视相机使物体具有3d的效果
    camera.position.x = 0;//相机的位置和视点将影响观察到的物体
    camera.position.z = 200;
    camera.position.y = 100;//待优化

    renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });//声明一个webgl的渲染器,这个渲染器就如同html中的canvas
    renderer.setSize(WIDTH,HEIGHT);
    renderer.shadowMap.enabled = true;
    container = document.getElementById('world');
    container.appendChild(renderer.domElement);//将这个渲染器加到html当中

    

二、添加灯光

在three.js中添加灯光十分的简单,不同的灯光有不同的作用,比如环境光,点光源,聚光灯等等。这里用到了半球光,半球光其中设置的两个参数天空和地面的颜色可以使场景更加的真实。

function createLights(){
    hemisphereLight = new THREE.HemisphereLight(0xbbbbbb,0x000000, .9);
    ambientLight = new THREE.AmbientLight(controls.ambientLightColor);
    shadowLight = new THREE.DirectionalLight(0xffffff, .9);
    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);
    scene.add(ambientLight);
}

三、创造一片大海

这里的大海是通过一个倒置的圆柱体来实现的,通过调整照相机的位置,并且配合旋转的动画,在半球光的照射下就如同一片汪洋。

Sea = function(){
    var geom = new THREE.CylinderGeometry(600,600,800,40,10);
    geom.applyMatrix(new THREE.Matrix4().makeRotationX(-Math.PI/2));
    var mat = new THREE.MeshPhongMaterial({
        color:Colors.blue,
        transparent:true,
        opacity:.6,
        shading:THREE.FlatShading,
    });
    this.mesh = new THREE.Mesh(geom, mat);
    this.mesh.receiveShadow = true;

}

var sea;
function createSea(){
    sea = new Sea();
    sea.mesh.position.y = -600;
    scene.add(sea.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(i=0;i<nBlocs;i++){
    //实现位置随机,大小随机
        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);
    }
}

Sky = function(){
    this.mesh = new THREE.Object3D();
    this.nClouds = 20;
    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;
        c.mesh.position.y = Math.sin(a)*h;
        c.mesh.position.x = Math.cos(a)*h;
        c.mesh.rotation.z = - Math.PI/2+a;
        c.mesh.position.z = -50-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(80,50,50,1,1,1);
    var matCockpit = new THREE.MeshPhongMaterial({color:Colors.red, shading:THREE.FlatShading});
    geomCockpit.vertices[4].y-=10;
    geomCockpit.vertices[4].z+=20;
    geomCockpit.vertices[5].y-=10;
    geomCockpit.vertices[5].z-=20;
    geomCockpit.vertices[6].y+=20;
    geomCockpit.vertices[6].z+=20;
    geomCockpit.vertices[7].y+=20;
    geomCockpit.vertices[7].z-=20;

    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;

    // 螺旋桨
    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);

};

var airplane;

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

好了,伙计!现在我们的场景中有了灯光,大海,天空还有飞机,但是,似乎还少了什么。对的,我们要操控这个飞机!

六、控制我们的飞机

飞机能跟随鼠标移动的轨迹,为了做到更完美,当飞机上升和下降时,应该要有旋转的感觉!


function handleMouseMove(event) {

    // 我们要把鼠标的坐标值转换成webgl系统中规格化的数值,从-1到1
    // 这种转换很简单的伙计!tx = (x-width/2)/(width/2)

    var tx = -1 + (event.clientX / WIDTH)*2;

    // y轴在窗口坐标系和webg坐标系的方向是相反的,因此我们把他逆一下就可以
    

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

}


function updatePlane(){
    var targetY = 100+mousePos.y*75;//控制飞机在y轴25到175的位置
    var targetX = mousePos.x*195;//控制飞机在x轴-195到195的位置

    // 每一帧移动飞机移动的距离,使飞机最终到达鼠标的位置,这样制造出飞机缓缓飞向指定位置的效果,而不会显得很突兀。
    airplane.mesh.position.y += (targetY-airplane.mesh.position.y)*0.1;
    airplane.mesh.position.x += (targetX-airplane.mesh.position.x)*0.5;
    // 通过剩余距离的长度来计算旋转地幅度,这样飞机如果一次性移动的距离很多相应的旋转幅度就越大,与真实的情况也符合,使动画更加真实。
    airplane.mesh.rotation.z = (targetY-airplane.mesh.position.y)*0.0256;
    airplane.mesh.rotation.x = (airplane.mesh.position.y-targetY)*0.0256;

    airplane.propeller.rotation.x += 0.3;

}

七、让画面动起来、

实现动画的本质就是每一帧改变相应的参数,不断的渲染,使人眼感觉画面在运动

function loop(){
    airplane.propeller.rotation.x += 0.3;
    sea.mesh.rotation.z += .005;
    sky.mesh.rotation.z += .01;
    updatePlane();
    airplane.pilot.updateHairs();


    // 渲染
    renderer.render(scene, camera);

    // 再次调用
    requestAnimationFrame(loop);
}

最后

看看我们的demo吧!

enter image description here

当然还有更多的创意还可以实现,不是吗?