本文主要内容翻译自:https://tympanus.net/codrops/2016/04/26/the-aviator-animating-basic-3d-scene-threejs/
今天我们准备创建一个简单的飞机飞行的3D场景,使用的工具是Three.js。这是一个3D javascript,通过这个库我们可以更简单的通过WebGL编写3D程序。因为WebGL的复杂性和GLSL语言(OpenGL着色语言)的语法问题,导致很多人望而却步。但是通过Three.js我们可以很容易的在浏览器中实现3D效果。
在这一个教程中我们将会创建一个简单的3D场景,在两个主要场景中有一些简单的交互。在第一个部分我们会介绍Three.js的基本内容并且介绍如何搭建一个简单的场景。在第二个部分我们会介绍如何优化某些形状,如何为场景的不同元素添加合适的氛围和更好的移动效果。
现在开始吧!
Three.js的官网地址:https://threejs.org/
下载好three.js或者Three.min.js脚本文件之后,就可以准备编写程序了。
第一件事是把这个脚本引入到你的HTML文件的header中去:
接下来你需要在HTML文件的body中创建一个容器元素来放置渲染场景:
你可以简单的设置一下这个容器的格式,让它填充满整个页面:
* { margin: 0; }
#world {
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
background: linear-gradient(#e4e0ba, #f7d9aa);
}
OK,这样写完的话,现在的页面应该是这样的:
如果你之前有写过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() {
// 创建场景,相机和渲染器
createScene();
// 添加光源
createLights();
// 添加对象
createPlane();
createSea();
createSky();
// 调用循环函数,在每帧更新对象的位置和渲染场景
loop();
}
想要创建一个three.js场景,我们至少需要以下内容:
我们在createScene()中创建场景,相机和渲染器。
var scene, camera, fieldOfView, aspectRatio, nearPlane, farPlane, HEIGHT, WIDTH, renderer, container;
function createScene() {
// Get the width and the height of the screen,
// use them to set up the aspect ratio of the camera
// and the size of the renderer.
HEIGHT = window.innerHeight;
WIDTH = window.innerWidth;
// Create the scene
scene = new THREE.Scene();
// Add a fog effect to the scene; same color as the
// background color used in the style sheet
scene.fog = new THREE.Fog(0xf7d9aa, 100, 950);
// Create the camera
aspectRatio = WIDTH / HEIGHT;
fieldOfView = 60;
nearPlane = 1;
farPlane = 10000;
/**
* PerspectiveCamera 透视相机
* @param fieldOfView 视角
* @param aspectRatio 纵横比
* @param nearPlane 近平面
* @param farPlane 远平面
*/
camera = new THREE.PerspectiveCamera(
fieldOfView,
aspectRatio,
nearPlane,
farPlane
);
// Set the position of the camera
camera.position.x = 0;
camera.position.z = 200;
camera.position.y = 100;
// Create the renderer
renderer = new THREE.WebGLRenderer({
// Allow transparency to show the gradient background
// we defined in the CSS
alpha: true,
// Activate the anti-aliasing; this is less performant,
// but, as our project is low-poly based, it should be fine :)
antialias: true
});
// Define the size of the renderer; in this case,
// it will fill the entire screen
renderer.setSize(WIDTH, HEIGHT);
// Enable shadow rendering
renderer.shadowMap.enabled = true;
// Add the DOM element of the renderer to the
// container we created in the HTML
container = document.getElementById('world');
container.appendChild(renderer.domElement);
// Listen to the screen: if the user resizes it
// we have to update the camera and the renderer size
window.addEventListener('resize', handleWindowResize, false);
}
当屏幕的大小改变之后,我们需要更新渲染器的大小并且更新相机的纵横比:
function handleWindowResize() {
// update height and width of the renderer and the camera
HEIGHT = window.innerHeight;
WIDTH = window.innerWidth;
renderer.setSize(WIDTH, HEIGHT);
camera.aspect = WIDTH / HEIGHT;
camera.updateProjectionMatrix();
}
在创建一个场景时,灯光应该是最棘手的部分。灯光可以奠定整个场景的基调,因此要仔细设计这一部分。在这一部分,我们将只设置灯光使得场景中的所有物体可见即可。
var hemisphereLight, shadowLight;
function createLights() {
// A hemisphere light is a gradient colored light;
// the first parameter is the sky color, the second parameter is the ground color,
// the third parameter is the intensity of the light
hemisphereLight = new THREE.HemisphereLight(0xaaaaaa,0x000000, .9)
// A directional light shines from a specific direction.
// It acts like the sun, that means that all the rays produced are parallel.
shadowLight = new THREE.DirectionalLight(0xffffff, .9);
// Set the direction of the light
shadowLight.position.set(150, 350, 350);
// Allow shadow casting
shadowLight.castShadow = true;
// define the visible area of the projected shadow
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;
// define the resolution of the shadow; the higher the better,
// but also the more expensive and less performant
shadowLight.shadow.mapSize.width = 2048;
shadowLight.shadow.mapSize.height = 2048;
// to activate the lights, just add them to the scene
scene.add(hemisphereLight);
scene.add(shadowLight);
}
如你所见,创建灯光需要好多的参数。不要犹豫,大胆尝试用不同的颜色,强度的光源。你发现不同的光源在场景中能够营造有趣的氛围和环境。而且你会找到感觉:如何按照你的需求优化它们。
如果你会使用3D建模软件的话,你可以自己3D模型然后导入到Three.js中,不过在这个教程中我不会涉及到这一部分。不过为了让大家了解如何创建对象,我们会使用Three.js中提供的几何体来创建物体。
Three.js内置了许多内置的对象原型,比如立方体、球、圆形面、圆柱体和飞机。
在我们的项目中,所有对象都由这些基本的原型组合而成。对于一个低多边形风格的场景来说这再合适不过了,我们也不需要使用3D建模软件来构建模型了,开心。
首先来创建大海,因为这应该是我们的场景里最简单的对象了。简单起见,我们用一个放置在屏幕下方的圆柱体来简单的表示大海。之后再深入研究如何改变大海的外观。
接下来我们让大海和浪花看起来更逼真一点。
// First let's define a Sea object :
Sea = function(){
// create the geometry (shape) of the cylinder;
// the parameters are:
// radius top, radius bottom, height, number of segments on the radius, number of segments vertically
var geom = new THREE.CylinderGeometry(600,600,800,40,10);
// rotate the geometry on the x axis
geom.applyMatrix(new THREE.Matrix4().makeRotationX(-Math.PI/2));
// create the material
var mat = new THREE.MeshPhongMaterial({
color:Colors.blue,
transparent:true,
opacity:.6,
shading:THREE.FlatShading,
});
// To create an object in Three.js, we have to create a mesh
// which is a combination of a geometry and some material
this.mesh = new THREE.Mesh(geom, mat);
// Allow the sea to receive shadows
this.mesh.receiveShadow = true;
}
// Instantiate the sea and add it to the scene:
var sea;
function createSea(){
sea = new Sea();
// push it a little bit at the bottom of the scene
sea.mesh.position.y = -600;
// add the mesh of the sea to the scene
scene.add(sea.mesh);
}
总结一下创建一个物体需要些什么:
通过这些步骤,我们可以创建许多不同种类的几何体。现在,如果我们把它们组合起来,就可以创建更多复杂的形状。
接下来我们将会学习如何创建更加精致的几何形状。
云朵的形状比较复杂,因为云朵是有许多个立方体随机的组合形成的几何形状。
Cloud = function(){
// Create an empty container that will hold the different parts of the cloud
this.mesh = new THREE.Object3D();
// create a cube geometry;
// this shape will be duplicated to create the cloud
var geom = new THREE.BoxGeometry(20,20,20);
// create a material; a simple white material will do the trick
var mat = new THREE.MeshPhongMaterial({
color:Colors.white,
});
// duplicate the geometry a random number of times
var nBlocs = 3+Math.floor(Math.random()*3);
for (var i=0; i
通过以上代码我们定义好了云朵这个对象,下面通过复制该对象并将其放置在z轴的随机位置来实现天空的效果 。
// Define a Sky Object
Sky = function(){
// Create an empty container
this.mesh = new THREE.Object3D();
// choose a number of clouds to be scattered in the sky
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;
// create the clouds
for(var i=0; i
有一个好消息一个坏消息,坏消息是创建飞机对象的代码将会有点长而且有点复杂。好消息是不管代码多么长多么复杂,使用到的方法我们都已经学过了。这里所有的都是关于组合和封装的代码。
var AirPlane = function() {
this.mesh = new THREE.Object3D();
// Create the cabin
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);
// Create the engine
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);
// Create the tail
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);
// Create the wing
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);
// propeller
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);
};
现在我们可以实例化我们的飞机,将它加入到场景中去:
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(){
// Rotate the propeller, the sea and the sky
airplane.propeller.rotation.x += 0.3;
sea.mesh.rotation.z += .005;
sky.mesh.rotation.z += .01;
// render the scene
renderer.render(scene, camera);
// call the loop function again
requestAnimationFrame(loop);
}
如你所见,现在我们将渲染器的 render() 函数移动到 loop() 函数中。因为每次修改物体的位置或颜色之类的属性就需要重新调用一次 render() 函数。
现在如果运行起程序来看,你会发现飞机一直在屏幕的中央。我们接下来想实现的,是让飞机跟随鼠标移动。
一旦这个html文件加载完成,我们需要添加一个监听器监听鼠标是否移动了。
为了实现这个功能,我们在init()函数中做如下修改:
function init(event){
createScene();
createLights();
createPlane();
createSea();
createSky();
//add the listener
document.addEventListener('mousemove', handleMouseMove, false);
loop();
}
另外,我们需要新创建一个函数handleMouseMove()来响应鼠标移动事件:
var mousePos={x:0, y:0};
// now handle the mousemove event
function handleMouseMove(event) {
// here we are converting the mouse position value received
// to a normalized value varying between -1 and 1;
// this is the formula for the horizontal axis:
var tx = -1 + (event.clientX / WIDTH)*2;
// for the vertical axis, we need to inverse the formula
// because the 2D y-axis goes the opposite direction of the 3D y-axis
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(){
// let's move the airplane between -100 and 100 on the horizontal axis,
// and between 25 and 175 on the vertical axis,
// depending on the mouse position which ranges between -1 and 1 on both axes;
// to achieve that we use a normalize function (see below)
var targetX = normalize(mousePos.x, -1, 1, -100, 100);
var targetY = normalize(mousePos.y, -1, 1, 25, 175);
// update the airplane's position
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;
}
恭喜!到目前为止,你已经实现了一个能够跟随你鼠标移动的飞机了!看看目前实现的效果如何:
https://tympanus.net/Tutorials/TheAviator/part1.html