9. 动画
在本章之前,所有画面都是静止的,本章将介绍如果使用Three.js进行动态画面的渲染。此外,将会介绍一个Three.js作者写的另外一个库,用来观测每秒帧数(
FPS
)。
9.1 实现动画效果
9.1.1 动画原理
- 在这里,我们将动态画面简称为动画(
animation
)。正如动画片的原理一样,动画的本质是利用了人眼的视觉暂留特性,快速地变换画面,从而产生物体在运动的假象。而对于Three.js程序而言,动画的实现也是通过在每秒钟多次重绘
画面实现的。 - 为了衡量画面切换速度,引入了每秒帧数
FPS(Frames Per Second)
的概念,是指每秒画面重绘的次数
。FPS越大
,则动画效果越平滑
,当FPS小于20
时,一般就能明显感受到画面的卡滞
现象。 - 那么FPS是不是越大越好呢?其实也未必。当FPS足够大(
比如达到60
),再增加帧数人眼也不会感受到明显的变化,反而相应地就要消耗更多资源(比如电影的胶片就需要更长了,或是电脑刷新画面需要消耗计算资源等等)。因此,选择一个适中的FPS即可。 - NTSC标准的电视FPS是30,PAL标准的电视FPS是25,电影的FPS标准为24。而对于Three.js动画而言,一般FPS在
30
到60
之间都是可取的。
9.1.2 setInterval方法
如果要设置特定的FPS(虽然严格来说,即使使用这种方法,JavaScript也不能保证帧数精确性),可以使用JavaScript DOM定义的方法:
setInterval(fn,mesc)
- 其中,
fn
是每过msec
毫秒执行的函数
,如果将fn
定义为重绘画面的函数,就能实现动画效果。setInterval
函数返回一个变量timer
,如果需要停止重绘,需要使用clearInterval
方法,并传入该变量timer
,具体的做法为: - 1、首先,在
init
函数中定义每20毫秒
执行draw
函数的setInterval
,返回值记录在全局变量timer
中:
timer = setInterval(draw,20);
- 2、在
draw
函数中,我们首先设定在每帧中的变化(毕竟,如果每帧都是相同的,即使重绘再多次,还是不会有动画的效果),这里我们让场景中的长方体绕y
轴转动。然后,执行渲染:
function draw() {
// 每过20ms 就会执行一次这个函数,rotation.y就会加0.01
// 转完360度就会进行取余,所以就会一直转下去
mesh.rotation.y = (mesh.rotation.y + 0.01) % (Math.PI * 2);
renderer.render(scene, camera);
}
- 这样,每
20
毫秒就会调用一次draw
函数,改变长方体的旋转值,然后进行重绘。最终得到的效果就是FPS
为50
的旋转长方体。 - 3、我们在HTML中添加两个按钮,一个是按下后停止动画,另一个是按下后继续动画:
<button id="stopBtn" onclick="stop()">Stop</button>
<button id="startBtn" onclick="start()">Start</button>
- 4、对应的
stop
和start
函数为:
function stop() {
if (timer !== null) {
clearInterval(timer);
timer = null;
}
}
function start() {
if (timer == null) {
clearInterval(timer);
timer = setInterval(draw, 20);
}
}
- 完整代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>动画效果</title>
<script type="text/javascript" src="js/three.js"></script>
<script type="text/javascript">
var scene = null;
var camera = null;
var renderer = null;
var mesh = null;
var timer = null;
function init() {
renderer = new THREE.WebGLRenderer({
canvas: document.getElementById('mainCanvas')
});
renderer.setClearColor(0x000000);
scene = new THREE.Scene();
camera = new THREE.OrthographicCamera(-5, 5, 3.75, -3.75, 0.1, 100);
camera.position.set(5, 5, 20);
camera.lookAt(new THREE.Vector3(0, 0, 0));
scene.add(camera);
mesh = new THREE.Mesh(new THREE.CubeGeometry(1, 2, 3),
new THREE.MeshLambertMaterial({
color: 0xffff00
}));
scene.add(mesh);
var light = new THREE.DirectionalLight(0xffffff);
light.position.set(20, 10, 5);
scene.add(light);
timer = setInterval(draw, 20);
}
function draw() {
mesh.rotation.y = (mesh.rotation.y + 0.01) % (Math.PI * 2);
renderer.render(scene, camera);
}
function stop() {
if (timer !== null) {
clearInterval(timer);
timer = null;
}
}
function start() {
if (timer == null) {
clearInterval(timer);
timer = setInterval(draw, 20);
}
}
</script>
</head>
<body onload="init()">
<canvas id="mainCanvas" width="800px" height="600px"></canvas>
<button id="stopBtn" onclick="stop()">Stop</button>
<button id="startBtn" onclick="start()">Start</button>
</body>
</html>
- 效果图:
9.1.3 requestAnimationFrame方法
大多数时候,我们并不在意多久重绘一次,这时候就适合用
requestAnimationFrame
方法了。它告诉浏览器在合适的时候调用指定函数,通常可能达到60FPS
。
-
requestAnimationFrame
同样有对应的cancelAnimationFrame
取消动画:
function stop() {
if (timer !== null) {
cancelAnimationFrame(timer);
timer = null;
}
}
- 和
setInterval
不同的是,由于requestAnimationFrame
只请求一帧画面,因此,除了在init
函数中需要调用,在被其调用的函数中需要再次调用requestAnimationFrame
:
function draw() {
mesh.rotation.y = (mesh.rotation.y + 0.01) % (Math.PI * 2);
renderer.render(scene, camera);
timer = requestAnimationFrame(draw);
}
- 因为
requestAnimationFrame
较为“年轻”,因而一些老的浏览器使用的是试验期的名字:mozRequestAnimationFrame
、webkitRequestAnimationFrame
、msRequestAnimationFrame
,为了支持这些浏览器,我们最好在调用之前,先判断是否定义了requestAnimationFrame
以及上述函数:
var requestAnimationFrame = window.requestAnimationFrame
|| window.mozRequestAnimationFrame
|| window.webkitRequestAnimationFrame
|| window.msRequestAnimationFrame;
window.requestAnimationFrame = requestAnimationFrame;
- 完整代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>动画效果</title>
<script type="text/javascript" src="js/three.js"></script>
<script type="text/javascript">
var requestAnimationFrame = window.requestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.msRequestAnimationFrame;
window.requestAnimationFrame = requestAnimationFrame;
var scene = null;
var camera = null;
var renderer = null;
var mesh = null;
var timer = null;
function init() {
renderer = new THREE.WebGLRenderer({
canvas: document.getElementById('mainCanvas')
});
renderer.setClearColor(0x000000);
scene = new THREE.Scene();
camera = new THREE.OrthographicCamera(-5, 5, 3.75, -3.75, 0.1, 100);
camera.position.set(5, 5, 20);
camera.lookAt(new THREE.Vector3(0, 0, 0));
scene.add(camera);
mesh = new THREE.Mesh(new THREE.CubeGeometry(1, 2, 3),
new THREE.MeshLambertMaterial({
color: 0xffff00
}));
scene.add(mesh);
var light = new THREE.DirectionalLight(0xffffff);
light.position.set(20, 10, 5);
scene.add(light);
id = requestAnimationFrame(draw);
}
function draw() {
mesh.rotation.y = (mesh.rotation.y + 0.01) % (Math.PI * 2);
renderer.render(scene, camera);
timer = requestAnimationFrame(draw);
}
function stop() {
if (timer !== null) {
cancelAnimationFrame(timer);
timer = null;
}
}
function start() {
if (timer == null) {
timer = requestAnimationFrame(draw);
}
}
</script>
</head>
<body onload="init()">
<canvas id="mainCanvas" width="800px" height="600px"></canvas>
<button id="stopBtn" onclick="stop()">Stop</button>
<button id="startBtn" onclick="start()">Start</button>
</body>
</html>
setInterval和requestAnimationFrame的区别:
-
setInterval
方法与requestAnimationFrame
方法的区别较为微妙。一方面,最明显的差别表现在setInterval
可以手动设定FPS
,而requestAnimationFrame
则会自动设定FPS
;但另一方面,即使是setInterval
也不能保证按照给定的FPS执行,在浏览器处理繁忙时,很可能低于设定值。当浏览器达不到设定的调用周期时,requestAnimationFrame
采用跳过某些帧的方式来表现动画,虽然会有卡滞的效果但是整体速度不会拖慢,而setInterval
会因此使整个程序放慢运行,但是每一帧都会绘制出来; - 总而言之,
requestAnimationFrame
适用于对于时间较为敏感
的环境(但是动画逻辑更加复杂),而setInterval
则可在保证程序的运算不至于导致延迟的情况下提供更加简洁的逻辑(无需自行处理时间)。
9.2 使用stat.js记录FPS
stat.js
是Three.js的作者Mr.Doob
的另一个有用的JavaScript库。很多情况下,我们希望知道实时的FPS
信息,从而更好地监测动画效果。这时候,stat.js就能提供一个很好的帮助,它占据屏幕中的一小块位置(如左上角),效果为:,单击后显示每帧渲染时间:。
- 首先,我们需要下载
stat.js
文件,可以在https://github.com/mrdoob/stats.js/blob/master/build/stats.min.js找到。下载后,将其放在项目文件夹下,然后在HTML中引用:
<script type="text/javascript" src="stat.js"></script>
- 在页面初始化的时候,对其初始化并将其添加至屏幕一角。这里,我们以左上角为例:
var stat = null;
function init() {
stat = new Stats();
stat.domElement.style.position = 'absolute';
stat.domElement.style.left = '0px';
stat.domElement.style.top = '0px';
document.body.appendChild(stat.domElement);
// Three.js init ...
}
- 然后,在上一节介绍的动画重绘函数
draw
中调用stat.begin();
与stat.end();
分别表示一帧的开始与结束:
function draw() {
stat.begin();
mesh.rotation.y = (mesh.rotation.y + 0.01) % (Math.PI * 2);
renderer.render(scene, camera);
stat.end();
}
- 完整代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>stats</title>
<script type="text/javascript" src="js/three.js"></script>
<script type="text/javascript" src="Stats.js"></script>
<script type="text/javascript">
var requestAnimationFrame = window.requestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.msRequestAnimationFrame;
window.requestAnimationFrame = requestAnimationFrame;
var scene = null;
var camera = null;
var renderer = null;
var mesh = null;
var id = null;
var stat = null;
function init() {
stat = new Stats();
stat.domElement.style.position = 'absolute';
stat.domElement.style.left = '0px';
stat.domElement.style.top = '0px';
document.body.appendChild(stat.domElement);
renderer = new THREE.WebGLRenderer({
canvas: document.getElementById('mainCanvas')
});
renderer.setClearColor(0x000000);
scene = new THREE.Scene();
camera = new THREE.OrthographicCamera(-5, 5, 3.75, -3.75, 0.1, 100);
camera.position.set(5, 5, 20);
camera.lookAt(new THREE.Vector3(0, 0, 0));
scene.add(camera);
mesh = new THREE.Mesh(new THREE.CubeGeometry(1, 2, 3),
new THREE.MeshLambertMaterial({
color: 0xffff00
}));
scene.add(mesh);
var light = new THREE.DirectionalLight(0xffffff);
light.position.set(20, 10, 5);
scene.add(light);
timer = requestAnimationFrame(draw);
}
function draw() {
stat.begin();
mesh.rotation.y = (mesh.rotation.y + 0.01) % (Math.PI * 2);
renderer.render(scene, camera);
timer = requestAnimationFrame(draw);
stat.end();
}
function stop() {
if (timer !== null) {
cancelAnimationFrame(timer);
timer = null;
}
}
</script>
</head>
<body onload="init()">
<canvas id="mainCanvas" width="800px" height="600px"></canvas>
<button id="stopBtn" onclick="stop()">Stop</button>
</body>
</html>
9.3 弹球案例
本节我们将使用一个弹球的例子来完整地学习使用动画效果。
- 1、首先,我们把通用的框架部分写好,按照之前的方法实现动画重绘函数,并加入stat.js库:
var requestAnimationFrame = window.requestAnimationFrame
|| window.mozRequestAnimationFrame
|| window.webkitRequestAnimationFrame
|| window.msRequestAnimationFrame;
window.requestAnimationFrame = requestAnimationFrame;
var stat;
var renderer;
var scene;
var camera;
var light;
function init() {
stat = new Stats();
stat.domElement.style.position = 'absolute';
stat.domElement.style.left= '0px';
stat.domElement.style.top = '0px';
document.body.appendChild(stat.domElement);
renderer = new THREE.WebGLRenderer({
canvas: document.getElementById('mainCanvas')
});
scene = new THREE.Scene();
timer = requestAnimationFrame(draw);
}
function draw() {
stat.begin();
renderer.render(scene, camera);
timer = requestAnimationFrame(draw);
stat.end();
}
function stop() {
if (timer !== null) {
cancelAnimationFrame(timer);
timer = null;
}
}
- 2、然后,为了实现弹球弹动的效果,我们创建一个
球体
作为弹球模型,创建一个平面
作为弹球反弹的平面。为了在draw
函数中改变弹球的位置,我们可以声明一个全局变量ballMesh
,以及弹球半径ballRadius
。
var ballMesh;
var ballRadius = 0.5;
- 3、在
init
函数中添加球体
和平面
,使弹球位于平面上,平面采用棋盘格图像作材质:
// 加载贴图
texture = THREE.ImageUtils.loadTexture('images/chess.png', {}, function() {
renderer.render(scene, camera);
});
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(4, 4);
// 平面模型
var plane = new THREE.Mesh(new THREE.PlaneGeometry(8, 8),
new THREE.MeshLambertMaterial({
map: texture
}));
// 沿x轴旋转-90度
plane.rotation.x = Math.PI / -2;
scene.add(plane);
// 球模型
ballMesh = new THREE.Mesh(new THREE.SphereGeometry(ballRadius, 40, 16),
new THREE.MeshLambertMaterial({
color: 0xffff00
}));
- 4、为了记录弹球的状态,我们至少需要
位置
、速度
、加速度
三个矢量,为了简单起见,这里弹球只做竖直方向上的自由落体运动,因此位置、速度、加速度只要各用一个变量表示。其中,位置就是ballMesh.position.y
,不需要额外的变量,因此我们在全局声明速度v
和加速度a
:
var v = 0;
var a = -0.01;
- 这里,
a = -0.01
代表每帧小球向y
方向负方向
移动0.01
个单位。 - 5、一开始,弹球从高度为
maxHeight
(自己定义的一个高度)处自由下落,掉落到平面上时会反弹
,并且速度有损耗
。当速度很小的时候,弹球会在平面上作振幅微小的抖动,所以,当速度足够小时,我们需要让弹球停止跳动。因此,定义一个全局变量表示是否在运动,初始值为false
:
var isMoving = false;
- 6、在HTML中定义一个按钮,点击按钮时,弹球从最高处下落:
<button id="dropBtn" onclick="drop()">Drop</button>
<script>
function drop() {
isMoving = true;
ballMesh.position.y = maxHeight;
v = 0;
}
</script>
- 7、下面就是最关键的函数了,在draw函数中,需要判断当前的isMoving值,并且更新小球的速度和位置:
function draw() {
stat.begin();
if (isMoving) {
ballMesh.position.y += v;
// a= -0.01
v += a;
// 当小球从定义的高度落到小球停在平面时的高度的时候
if (ballMesh.position.y <= ballRadius) {
// 让小球弹起来
v = -v * 0.9;
}
// 当小球的速度小于设定值的时候
if (Math.abs(v) < 0.001) {
// 让它停下来
isMoving = false;
ballMesh.position.y = ballRadius;
}
}
renderer.render(scene, camera);
requestAnimationFrame(draw);
stat.end();
}
- 完整代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>弹弹弹</title>
<script type="text/javascript" src="js/three.js"></script>
<script type="text/javascript" src="Stats.js"></script>
<style>
* {
margin: 0;
padding: 0;
}
body {
position: fixed;
}
button {
position: fixed;
left: 50%;
margin-left: -50px;
bottom: 20%;
width: 100px;
height: 30px;
background-color: #d3d3d3;
border: none;
border-radius: 15px;
outline: none;
font-size: 18px;
font-weight: 700;
color: #333;
box-shadow: -1px -1px 1px #fff, 1px 1px 1px #000;
}
</style>
<script>
var stat;
var renderer;
var scene;
var camera;
var light;
var texture;
var ballMesh;
var ballRadius = 0.5;
var isMoving = false;
var maxHeight = 5;
var v = 0;
var a = -0.01;
function init() {
// 处理requireAnimationFrame兼容性
var requestAnimationFrame = window.requestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.msRequestAnimationFrame;
window.requestAnimationFrame = requestAnimationFrame;
// FPS 插件
stat = new Stats();
stat.domElement.style.position = 'absolute';
stat.domElement.style.left = '0px';
stat.domElement.style.top = '0px';
document.body.appendChild(stat.domElement);
// 渲染器
renderer = new THREE.WebGLRenderer({
antialias: true
});
width = window.innerWidth;
height = window.innerHeight;
renderer.setSize(width, height);
document.body.appendChild(renderer.domElement);
renderer.setClearColor(0xd3d3d3);
// 场景
scene = new THREE.Scene();
// 相机
camera = new THREE.OrthographicCamera(width / -128, width / 128, height / 128, height / -128, 1, 1000);
camera.position.set(10, 15, 25);
camera.lookAt(new THREE.Vector3(0, 0, 0));
scene.add(camera);
// 添加光照
light = new THREE.DirectionalLight(0xffffff);
light.position.set(-10, 30, 25);
scene.add(light);
// 加载贴图
texture = THREE.ImageUtils.loadTexture('images/chess.png', {}, function() {
renderer.render(scene, camera);
});
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(4, 4);
// 平面模型
var plane = new THREE.Mesh(new THREE.PlaneGeometry(8, 8),
new THREE.MeshLambertMaterial({
map: texture
}));
// 沿x轴旋转-90度
plane.rotation.x = Math.PI / -2;
scene.add(plane);
// 球模型
ballMesh = new THREE.Mesh(new THREE.SphereGeometry(ballRadius, 40, 16),
new THREE.MeshLambertMaterial({
color: 0xffff00
}));
// 设置球的位置
ballMesh.position.y = ballRadius;
scene.add(ballMesh);
// 坐标轴
/* drawAxes(scene);
function drawAxes(scene) {
// x-axis
var xGeo = new THREE.Geometry();
xGeo.vertices.push(new THREE.Vector3(0, 0, 0));
xGeo.vertices.push(new THREE.Vector3(7, 0, 0));
var xMat = new THREE.LineBasicMaterial({
color: 0xff0000
});
var xAxis = new THREE.Line(xGeo, xMat);
scene.add(xAxis);
// y-axis
var yGeo = new THREE.Geometry();
yGeo.vertices.push(new THREE.Vector3(0, 0, 0));
yGeo.vertices.push(new THREE.Vector3(0, 7, 0));
var yMat = new THREE.LineBasicMaterial({
color: 0x00ff00
});
var yAxis = new THREE.Line(yGeo, yMat);
scene.add(yAxis);
// z-axis
var zGeo = new THREE.Geometry();
zGeo.vertices.push(new THREE.Vector3(0, 0, 0));
zGeo.vertices.push(new THREE.Vector3(0, 0, 7));
var zMat = new THREE.LineBasicMaterial({
color: 0x00ccff
});
var zAxis = new THREE.Line(zGeo, zMat);
scene.add(zAxis);
} */
requestAnimationFrame(draw);
}
// 计算球运动的速度和位置
function draw() {
stat.begin();
if (isMoving) {
ballMesh.position.y += v;
// a= -0.01
v += a;
// 当小球从定义的高度落到小球停在平面时的高度的时候
if (ballMesh.position.y <= ballRadius) {
// 让小球弹起来
v = -v * 0.9;
}
// 当小球的速度小于设定值的时候
if (Math.abs(v) < 0.001) {
// 让它停下来
isMoving = false;
ballMesh.position.y = ballRadius;
}
}
renderer.render(scene, camera);
requestAnimationFrame(draw);
stat.end();
}
// 触发函数
function drop() {
isMoving = true;
// 小球起落位置
ballMesh.position.y = maxHeight;
// 加速度为0
v = 0;
}
</script>
</head>
<body onload="init();">
<button id="dropBtn" onclick="drop();">Drop</button>
</body>
</html>
- 效果图:
10. 外部模型
前面我们了解到,使用Three.js创建常见几何体是十分方便的,但是对于人或者动物这样非常复杂的模型使用几何体组合就非常麻烦了。因此,Three.js允许用户导入由3ds Max等工具制作的三维模型,并添加到场景中。
- 本章以3ds Max为例,介绍如何导入外部模型。
10.1 支持格式
Three.js有一系列导入外部文件的辅助函数,是在
three.js
之外的,使用前需要额外下载,在https://github.com/mrdoob/three.js/tree/master/examples/js/loaders可以找到,选择对应的模型加载器,系在下来。
-
*.obj
是最常用的模型格式,导入*.obj
文件需要OBJLoader.js
;导入带*.mtl
材质的*.obj
文件需要MTLLoader.js
以及OBJMTLLoader.js
。另有PLYLoader.js
、STLLoader.js
等分别对应不同格式的加载器,可以根据模型格式自行选择。 -
目前,支持的模型格式有:
*.obj
*.obj, *.mtl
*.dae
*.ctm
*.ply
*.stl
*.wrl
*.vtk
10.2 无材质的模型
本节中,我们将将导出的没有材质的模型使用Three.js导入场景中。
- 首先,下载OBJLoader.js并在HTML的<head>中使用:
<script type="text/javascript" src="OBJLoader.js"></script>
- 然后,我们需要准备一个
*.obj
模型,在init
函数中,创建loader
变量,用于导入模型:
var loader = new THREE.OBJLoader();
-
loader
导入模型的时候,接受两个参数,第一个表示模型路径
,第二个表示完成导入后的回调函数
,一般我们需要在这个回调函数中将导入的模型添加
到场景中。
loader.load('../lib/port.obj', function(obj) {
//储存到全局变量中
mesh = obj;
scene.add(obj);
});
- 可以看到一个没有材质的茶壶
- 我们在重绘函数中让茶壶旋转:
function draw() {
renderer.render(scene, camera);
mesh.rotation.y += 0.01;
if (mesh.rotation.y > Math.PI * 2) {
mesh.rotation.y -= Math.PI * 2;
}
}
- 可以看到在某些角度时,好像有些面片没有被绘制出来,因而后方的茶嘴似乎穿越到前方了:
- 这是由于默认的情况下,只有正面的面片被绘制,而如果需要
双面绘制
,需要这样设置:
var loader = new THREE.OBJLoader();
loader.load('port.obj', function(obj) {
obj.traverse(function(child) {
if (child instanceof THREE.Mesh) {
child.material.side = THREE.DoubleSide;
}
});
mesh = obj;
scene.add(obj);
});
- 完整代码:
<!DOCTYPE html>
<html lang="en">
<head>
<script type="text/javascript" src="js/three.js"></script>
<script type="text/javascript" src="OBJLoader.js"></script>
<style>
* {
margin: 0;
padding: 0;
}
body {
position: fixed;
}
</style>
<script type="text/javascript">
var scene = null;
var camera = null;
var renderer = null;
var mesh = null;
var id = null;
function init() {
renderer = new THREE.WebGLRenderer();
renderer.setSize(800, 600);
document.body.appendChild(renderer.domElement);
renderer.setClearColor(0x000000);
scene = new THREE.Scene();
camera = new THREE.OrthographicCamera(-8, 8, 6, -6, 0.1, 100);
camera.position.set(15, 25, 25);
camera.lookAt(new THREE.Vector3(0, 2, 0));
scene.add(camera);
var loader = new THREE.OBJLoader();
loader.load('port.obj', function(obj) {
obj.traverse(function(child) {
if (child instanceof THREE.Mesh) {
child.material.side = THREE.DoubleSide;
}
});
mesh = obj;
scene.add(obj);
});
var light = new THREE.DirectionalLight(0xffffff);
light.position.set(20, 10, 5);
scene.add(light);
id = setInterval(draw, 20);
}
function draw() {
renderer.render(scene, camera);
mesh.rotation.y += 0.01;
if (mesh.rotation.y > Math.PI * 2) {
mesh.rotation.y -= Math.PI * 2;
}
}
</script>
</head>
<body onload="init()">
</body>
</html>
- 效果图:
10.3 有材质的模型
模型的材质可以有两种定义方式,一种是在
代码中
导入模型后设置材质,另一种是在建模软件中导出材质
信息。下面,我们将分别介绍这两种方法。
10.3.1 代码中设置材质
这种方法与上一节类似,不同之处在于回调函数中设置模型的材质:
var loader = new THREE.OBJLoader();
loader.load('port.obj', function(obj) {
obj.traverse(function(child) {
if (child instanceof THREE.Mesh) {
/* 修改这里以下的代码 */
child.material = new THREE.MeshLambertMaterial({
color: 0xffff00,
side: THREE.DoubleSide
});
/* 修改这里以上的代码 */
}
});
mesh = obj;
scene.add(obj);
});
- 效果图:
10.3.2 建模软件中设置材质
导出3D模型的时候,选择导出
port.obj
模型文件以及port.mtl
材质文件。
- 现在,我们不再使用
OBJLoader.js
,而是使用MTLLoader.js
与OBJMTLLoader.js
,并且要按该顺序
引用:
<script type="text/javascript" src="MTLLoader.js"></script>
<script type="text/javascript" src="OBJMTLLoader.js"></script>
- 调用的方法也略有不同:
var mtlLoader = new THREE.MTLLoader();
mtlLoader.setPath('');
mtlLoader.load('port.mtl', function(materials) {
materials.preload();
// model loader
var objLoader = new THREE.OBJLoader();
objLoader.setMaterials(materials);
objLoader.setPath('');
objLoader.load('port.obj', function(object) {
object.position.y = -95;
// if has object, add to scene
if (object.children.length > 0) {
scene.add(object.children[0]);
}
});
});
- 完整代码:
<html>
<head>
<script type="text/javascript" src="js/three.js"></script>
<script type="text/javascript" src="MTLLoader.js"></script>
<script type="text/javascript" src="OBJLoader.js"></script>
<script type="text/javascript">
var scene = null;
var camera = null;
var renderer = null;
var mesh = null;
var id = null;
function init() {
renderer = new THREE.WebGLRenderer();
renderer.setSize(800, 600);
document.body.appendChild(renderer.domElement);
renderer.setClearColor(0x000000);
scene = new THREE.Scene();
camera = new THREE.OrthographicCamera(-8, 8, 6, -6, 0.1, 100);
camera.position.set(15, 25, 25);
camera.lookAt(new THREE.Vector3(0, 2, 0));
scene.add(camera);
// material loader
var mtlLoader = new THREE.MTLLoader();
mtlLoader.setPath('');
mtlLoader.load('port.mtl', function(materials) {
materials.preload();
// model loader
var objLoader = new THREE.OBJLoader();
objLoader.setMaterials(materials);
objLoader.setPath('');
objLoader.load('port.obj', function(object) {
object.position.y = -95;
// if has object, add to scene
if (object.children.length > 0) {
scene.add(object.children[0]);
}
});
mesh = materials;
console.log(mesh);
});
var light = new THREE.DirectionalLight(0xffffff);
light.position.set(20, 10, 5);
scene.add(light);
id = setInterval(draw, 20);
}
function draw() {
renderer.render(scene, camera);
}
</script>
</head>
<body onload="init()">
</body>
</html>
- 导出时自带的效果图:
11. 光与影
图像渲染的丰富效果很大程度上也要归功于光与影的利用。真实世界中的光影效果非常复杂,但是其本质—光的传播原理却又是非常单一的,这便是自然界繁简相成的又一例证。为了使计算机模拟丰富的光照效果,人们提出了几种不同的光源模型(
环境光
、平行光
、点光源
、聚光灯
等),在不同场合下组合利用,将能达到很好的光照效果。
- 在Three.js中,光源与阴影的创建和使用是十分方便的。在学会了如何控制光影的基本方法之后,如果能将其灵活应用,将能使场景的渲染效果更加丰富逼真。在本章中,我们将探讨四种常用的光源(环境光、点光源、平行光、聚光灯)和阴影带来的效果,以及如何去创建使用光影。
11.1 环境光(AmbientLight)
环境光是指
场景整体
的光照效果,是由于场景内若干光源的多次反射形成的亮度一致的效果,通常用来为整个场景指定一个基础亮度。因此,环境光没有明确的光源位置,在各处形成的亮度也是一致的。
- 在设置环境光时,只需指定光的颜色:
var light = new THREE.AmbientLight(hex);
scene.add(light);
- 其中
hex
是十六进制的RGB颜色信息,如红色表示为0xff0000
- 但是,如果此时场景中没有物体,只添加了这个环境光,那么渲染的结果仍然是一片黑。所以,我们添加两个长方体看下效果:
// 创建一个绿色的正方体
var greenCube = new THREE.Mesh(new THREE.CubeGeometry(2, 2, 2),
new THREE.MeshLambertMaterial({color: 0x00ff00}));
greenCube.position.x = 3;
scene.add(greenCube);
// 创建一个白色的正方体
var whiteCube = new THREE.Mesh(new THREE.CubeGeometry(2, 2, 2),
new THREE.MeshLambertMaterial({color: 0xffffff}));
whiteCube.position.x = -3;
scene.add(whiteCube);
- 效果如图:
- 如果想让环境光暗些,可以将其设置为
new THREE.AmbientLight(0xcccccc)
等,效果为:
11.2 点光源(PointLight)
点光源是不计光源大小,可以看作一个点发出的光源。点光源照到不同物体表面的亮度是线性递减的,因此,离点光源距离越
远
的物体会显得越暗
。
- 点光源的构造函数是:
THREE.PointLight(hex, intensity, distance);
- 其中,
hex
是光源十六进制的颜色值;intensity
是亮度,缺省值为1
,表示100%
亮度;distance
是光源最远照射到的距离,缺省值为0
。 - 创建点光源并将其添加到场景中的完整做法是:
var light = new THREE.PointLight(0xffffff, 2, 100);
light.position.set(0, 1.5, 2);
scene.add(light);
- 效果图:
- 注意,这里光在每个面上的亮度是不同的,对于每个三角面片,将根据三个顶点的亮度进行插值。
11.3 平行光(DirectionalLight)
我们都知道,太阳光常常被看作平行光,这是因为相对地球上物体的尺度而言,太阳离我们的距离足够远。对于任意平行的平面,平行光照射的亮度都是相同的,而与平面所在位置无关。
- 平行光的构造函数是:
THREE.DirectionalLight(hex, intensity)
- 其中,
hex
是光源十六进制的颜色值;intensity
是亮度,缺省值为1
,表示100%
亮度。 - 此外,对于平行光而言,设置光源
位置
尤为重要。
var light = new THREE.DirectionalLight();
light.position.set(2, 5, 3);
scene.add(light);
- 注意,这里设置光源位置并不意味着所有光从
(2, 5, 3)
点射出(如果是的话,就成了点光源),而是意味着,平行光将以矢量(-2, -5, -3)
的方向照射到所有平面。因此,平面亮度与平面的位置无关,而只与平面的法向量
相关。只要平面是平行的,那么得到的光照也一定是相同的。 - 示例代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script type="text/javascript" src="js/three.js"></script>
<script>
var stat;
var renderer;
var scene;
var camera;
function init() {
// 渲染器
renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(800, 600);
document.body.appendChild(renderer.domElement);
renderer.setClearColor(0x000000);
// 场景
scene = new THREE.Scene();
// 相机
var camera = new THREE.OrthographicCamera(-5, 5, 3.75, -3.75, 0.1, 100);
camera.position.set(5, 15, 25);
camera.lookAt(new THREE.Vector3(0, 0, 0));
scene.add(camera);
// 平行光
var light = new THREE.DirectionalLight();
light.position.set(2, 5, 3);
scene.add(light);
// 右侧正方体
var rightCube = new THREE.Mesh(new THREE.CubeGeometry(1, 1, 1),
new THREE.MeshLambertMaterial({
color: 0x00ff00
}));
rightCube.position.x = 1;
rightCube.position.y = -1;
scene.add(rightCube);
// 左侧正方体
var leftCube = new THREE.Mesh(new THREE.CubeGeometry(1, 1, 1),
new THREE.MeshLambertMaterial({
color: 0x00ff00
}));
leftCube.position.x = -1;
scene.add(leftCube);
// 渲染
renderer.render(scene, camera);
}
</script>
</head>
<body onload="init();">
</body>
</html>
- 效果图:
11.4 聚光灯(SpotLight)
可以看出,聚光灯是一种特殊的点光源,它能够朝着一个方向投射光线。聚光灯投射出的是类似
圆锥形
的光线,这与我们现实中看到的聚光灯是一致的。
- 其构造函数为:
THREE.SpotLight(hex, intensity, distance, angle, exponent)
- 相比点光源,多了
angle
和exponent
两个参数。angle
是聚光灯的张角,缺省值是Math.PI / 3
,最大值是Math.PI / 2
;exponent
是光强在偏离target
的衰减指数(target需要在之后定义,缺省值为(0, 0, 0)
),缺省值是10
。 - 在调用构造函数之后,除了设置光源本身的位置,一般还需要设置
target
:
light.position.set(x1, y1, z1);
light.target.position.set(x2, y2, z2);
- 除了设置
light.target.position
的方法外,如果想让聚光灯跟着某一物体移动(就像真的聚光灯!),可以target
指定为该物体
:
var cube = new THREE.Mesh(new THREE.CubeGeometry(1, 1, 1),
new THREE.MeshLambertMaterial({color: 0x00ff00}));
var light = new THREE.SpotLight(0xffff00, 1, 100, Math.PI / 6, 25);
light.target = cube;
- 示例代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script type="text/javascript" src="js/three.js"></script>
<script type="text/javascript">
var scene = null;
var camera = null;
var renderer = null;
var cube = null;
var alpha = 0;
function init() {
renderer = new THREE.WebGLRenderer();
renderer.setSize(800, 600);
document.body.appendChild(renderer.domElement);
scene = new THREE.Scene();
camera = new THREE.OrthographicCamera(-5, 5, 3.75, -3.75, 0.1, 100);
camera.position.set(5, 15, 25);
camera.lookAt(new THREE.Vector3(0, 0, 0));
scene.add(camera);
// 平面
var plane = new THREE.Mesh(new THREE.PlaneGeometry(8, 8, 16, 16),
new THREE.MeshLambertMaterial({
color: 0xcccccc
}));
plane.rotation.x = -Math.PI / 2;
plane.position.y = -1;
plane.receiveShadow = true;
scene.add(plane);
// 立方体
cube = new THREE.Mesh(new THREE.CubeGeometry(1, 1, 1),
new THREE.MeshLambertMaterial({
color: 0x00ff00,
}));
cube.position.x = 2;
scene.add(cube);
// 聚光灯
var light = new THREE.SpotLight(0xffff00, 1, 100, Math.PI / 6, 25);
light.position.set(2, 5, 3);
light.target = cube;
scene.add(light);
// 环境光
var ambient = new THREE.AmbientLight(0x666666);
scene.add(ambient);
requestAnimationFrame(draw);
}
function draw() {
alpha += 0.01;
if (alpha > Math.PI * 2) {
alpha -= Math.PI * 2;
}
cube.position.set(2 * Math.cos(alpha), 0, 2 * Math.sin(alpha));
renderer.render(scene, camera);
requestAnimationFrame(draw);
}
</script>
</head>
<body onload="init();">
</body>
</html>
- 效果图:
11.5 阴影
明暗是相对的,阴影的形成也就是因为比周围获得的光照更少。因此,要形成阴影,光源必不可少。
- 在Three.js中,能形成阴影的光源只有
THREE.DirectionalLight
与THREE.SpotLight
;而相对地,能表现阴影效果的材质只有THREE.LambertMaterial
与THREE.PhongMaterial
。因而在设置光源和材质的时候,一定要注意这一点。 - 下面,我们以聚光灯为例,在之前的基础上增加阴影效果。
- 首先,我们需要在初始化时,告诉渲染器渲染阴影:
renderer.shadowMapEnabled = true;
- 然后,对于光源以及所有要产生阴影的物体调用:
// 上面的案例,产生阴影的物体是正方体
cube.castShadow = true;
- 对于接收阴影的物体调用:
// 接收阴影的物体是平面
plan.receiveShadow = true;
- 比如场景中一个
平面
上有一个正方体
,想要让聚光灯照射在正方体上,产生的阴影投射在平面上,那么就需要对聚光灯和正方体调用castShadow = true
,对于平面调用receiveShadow = true
。 - 以上就是产生阴影效果的必要步骤了,不过通常还需要设置光源的阴影相关属性,才能正确显示出阴影效果。
- 对于
聚光灯
,需要设置shadowCameraNear
、shadowCameraFar
、shadowCameraFov
三个值,类比我们在第二章学到的透视投影照相机,只有介于shadowCameraNear
与shadowCameraFar
之间的物体将产生阴影,shadowCameraFov
表示张角。 - 对于
平行光
,需要设置shadowCameraNear
、shadowCameraFar
、shadowCameraLeft
、shadowCameraRight
、shadowCameraTop
以及shadowCameraBottom
六个值,相当于正交投影照相机的六个面。同样,只有在这六个面围成的长方体内的物体才会产生阴影效果。 - 为了看到阴影照相机的位置,通常可以在调试时开启
light.shadowCameraVisible = true
。 - 如果想要修改阴影的深浅,可以通过设置
shadowDarkness
,该值的范围是0
到1
,越小越浅。 - 另外,这里实现阴影效果的方法是
Shadow Mapping
,即阴影是作为渲染前计算好的贴图贴上去的,因而会受到贴图像素大小
的限制。所以可以通过设置shadowMapWidth
与shadowMapHeight
值控制贴图的大小,来改变阴影的精确度。 - 而如果想实现
软阴影
的效果,可以通过renderer.shadowMapSoft = true;
方便地实现。 - 完整代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script type="text/javascript" src="js/three.js"></script>
<script type="text/javascript">
var scene = null;
var camera = null;
var renderer = null;
var cube = null;
var alpha = 0;
function init() {
renderer = new THREE.WebGLRenderer();
renderer.setSize(800, 600);
document.body.appendChild(renderer.domElement);
renderer.shadowMapEnabled = true;
scene = new THREE.Scene();
camera = new THREE.OrthographicCamera(-5, 5, 3.75, -3.75, 0.1, 100);
camera.position.set(5, 15, 25);
camera.lookAt(new THREE.Vector3(0, 0, 0));
scene.add(camera);
var plane = new THREE.Mesh(new THREE.PlaneGeometry(8, 8, 16, 16),
new THREE.MeshLambertMaterial({
color: 0xcccccc
}));
plane.rotation.x = -Math.PI / 2;
plane.position.y = -1;
plane.receiveShadow = true;
scene.add(plane);
cube = new THREE.Mesh(new THREE.CubeGeometry(1, 1, 1),
new THREE.MeshLambertMaterial({
color: 0x00ff00
}));
cube.position.x = 2;
cube.castShadow = true;
scene.add(cube);
var light = new THREE.SpotLight(0xffff00, 1, 100, Math.PI / 6, 25);
light.position.set(2, 5, 3);
light.target = cube;
light.castShadow = true;
light.shadowCameraNear = 2;
light.shadowCameraFar = 10;
light.shadowCameraFov = 30;
light.shadowMapWidth = 1024;
light.shadowMapHeight = 1024;
light.shadowDarkness = 0.3;
scene.add(light);
// ambient light
var ambient = new THREE.AmbientLight(0x666666);
scene.add(ambient);
requestAnimationFrame(draw);
}
function draw() {
alpha += 0.01;
if (alpha > Math.PI * 2) {
alpha -= Math.PI * 2;
}
cube.position.set(2 * Math.cos(alpha), 0, 2 * Math.sin(alpha));
renderer.render(scene, camera);
requestAnimationFrame(draw);
}
</script>
</head>
<body onload="init();">
</body>
</html>
- 效果图:
补充问题
<span id = "jump"></span>
本地服务器
- 1、下载安装node.js,因为node.js自带npm
- 2、打开电脑命令行工具,输入
npm install -g live-server
全局安装 - 3、在需要运行文件的文件夹下,按住shift键,点击鼠标右键
在此处打开命令窗口
- 4、输入
live-server
回车
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。