Menu

  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay
  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay

Three.js学习笔记

1
Jan
2017

Three.js学习笔记

By Alex
/ in JavaScript
/ tags WebGL, 学习笔记
0 Comments
简介
关于WebGL

Web图形库(Web Graphics Library)简称WebGL,是在浏览器环境下进行3D/2D图像渲染的技术。你不需要额外的插件,就可以在HTML5的Canvas上绘制复杂的、可交互的图形。

大部分现代浏览器支持WebGL技术,IE从11开始支持,老版本的IE可以通过第三方插件支持,例如IEWebGL。

WebGL基于OpenGL ES 2.0提供3D图形接口。后者是OpenGL的一个子集,主要针对手机、PDA之类的嵌入式设备。

关于Three.js

直接使用WebGL编程难度较高,需要了解WebGL的细节、学习复杂的着色器(Shader)语言。Three.js对WebGL的底层细节进行了封装,让你更加容易的、仅利用JavaScript语言创建3D图形,你可以:

  1. 创建简单/复杂的3D几何图形
  2. 在3D场景中动画、移动对象
  3. 给对象应用纹理、材质
  4. 从3D模型软件中加载对象
Three.js基本概念
术语 说明
场景
Scene
存储并跟踪所有待渲染对象的容器。场景被渲染器渲染到一个HTML5画布中
镜头
Camera

定义查看场景的视角,有多种实现:

  1. PerspectiveCamera,基于透视投影(perspective projection)的镜头。透视投影模拟人的视觉效果(近大远小),从某个投射中心(人眼)将物体投射到单一投影面(画面)之上。是最常使用的投影模式
视截锥
View Frustum

在3D计算机图形学中,视截锥是指被建模世界空间的一个区域,该区域可以出现在屏幕中,视截锥定义了概念相机的视界(Field of view,FOV)

使用两个平行的平面,对视野金字塔(pyramid of vision)进行截断操作,即得到视截锥。视截锥的精确形状取决于期望模拟的相机棱镜的形状,典型情况下为六面体,其中远近平面为同长宽比的矩形,如下图:

viewfrustum-02 

所谓远、近平面,是指六面体中与视觉方向正交的那两个平面。近平面即上图中标为黄色的那一面。比近平面更近、远平面更远的区域中的对象,不会被绘制。某些情况下远平面被放置到无限远处

视截锥选择(View frustum culling)是指从渲染过程中移除完全位于视截锥之外的对象的处理步骤

视界
FOV

在第一人称游戏中,所谓视界( field of view, field of vision)是指某一时刻游戏世界中显示在屏幕中的(矩形)范围(extent)。视界通常用角度(angle)来描述,但是此角度可能指FOV在垂直、水平、对角线(diagonal)方向的分量

在一定的分辨率下,FOV会依据屏幕纵横比(aspect ratio)而变化,通常FOV在宽屏上更大

我们常以FOV在水平/垂直方向的角度、结合纵横比来描述FOV。它们之间的换算公式如下:

r  = w / h = tan(H/2) / tan(V/2)

其中r为屏幕纵横比,w/h为屏幕宽高度,H/V为水平、垂直方向的FOV分量

在Three.js中,PerspectiveCamera的fov参数为FOV的垂直分量,这意味着取值从0 ~ 180之间时变化时,如果r保持不变,则视界越大,场景中的目标显得越小

渲染器
Renderer
负责计算在指定的Camera之下,Scene长得什么样子
多边形网格
Polygon mesh
多边形网格是一系列顶点(vertex)、边线(edge)、面(face)的几何。它在3D计算机图形学中定义了一个多面体的轮廓。网格中的面通常由三角形、四边形或者其它简单的凸面多边形( convex polygons)构成,以简化渲染的计算量。下面是一个由三角形网格构成的海豚模型示例:dolphin_triangle_mesh 
混合模式
Blend mode / 
Mixing mode

图像处理中的概念。用于确定两层图像如何叠加到一起。大部分应用中默认的叠加模式就是让顶层(top layer )直接覆盖较低的层(lower layers )。由于每个像素的色彩都是基于数字来表示的,因此基于数学运算的大量混合模式可用

大部分图像处理软件,例如Photoshop、GIMP,都支持用户修改混合模式。

参考:https://en.wikipedia.org/wiki/Blend_modes

粒子,精灵
Particles, Sprite

指存在于3D场景中的二维图形或者动画

右手系
right-handed system

Three.js默认使用右手坐标系,因为这是OpenGL默认的坐标系

所谓右手系,是指:

  1. 伸出右手,伸直拇指,让它与另外四指垂直
  2. 弯曲中、无名、小指,让它们与食指垂直
  3. 以拇指指向为X轴正向、食指指向为Y轴正向、其它手指指向为Z轴正向的坐标系,即右手系

图示如下:

right-handed-system

Blender等3D建模软件,使用Z轴向上(上图右手系沿X轴正向逆时针旋转90度)的右手系。主要原因是大部分CAD软件均使用这样的坐标系

第一个3D场景
渲染并查看3D对象

在本节,我们创建以下几个对象:

对象 说明
Plane 平面,二维的矩形,作为“地面”在场景的中央展示
Cube 三维盒子,展示为红色
Sphere 三维球体,展示为蓝色
Camera 镜头,决定你看到的场景是什么样子
Axes X/Y/Z轴,辅助的调试工具,方便查看对象在哪里渲染

代码及注释:

XHTML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Three.js Study</title>
    <script src="https://code.jquery.com/jquery-1.12.4.js"></script>
    <script src="three.js"></script>
    <style>
        body {
            margin: 0;
            overflow: hidden;
        }
    </style>
</head>
<body>
<div id="WebGL"></div>
<script type="text/javascript">
    $( function () {
        // 场景
        var scene = new THREE.Scene();
 
        var aspect = window.innerWidth / window.innerHeight;
        /**
         * 定义一个透视镜头,参数:
         * fov FOV垂直分量,镜头到视截锥近平面上下边之间的夹角
         * aspect 视截锥的纵横比
         * near 近平面离镜头多远
         * far 远平面离镜头多远
         */
        var camera = new THREE.PerspectiveCamera( 45, aspect, 0.1, 1000 );
 
        // WebGL渲染器,使用显卡来渲染场景。尽管还存在其它渲染器实现,但是处于性能、特性的考虑,不推荐使用
        var renderer = new THREE.WebGLRenderer();
        // 设置背景色,第二参数为透明度
        renderer.setClearColor( 0xEEEEEE, 1 );
        // 设置场景的大小
        renderer.setSize( window.innerWidth, window.innerHeight );
 
        // 创建一个调试用途的坐标轴,X轴红色、Y轴绿色、Z轴蓝色。20表示轴线长度
        var axes = new THREE.AxisHelper( 20 );
        // 把对象添加到场景中
        scene.add( axes );
 
        // 创建一个平面几何图形,宽60高20,在宽、高方向上分段数为1(不切分)
        var planeGeometry = new THREE.PlaneGeometry( 60, 20, 1, 1 );
        // 材质,定义颜色、透明度、反光效果之类的属性
        // MeshBasicMaterial是一种简单材质,它不受光线影响,使用纯色或者网格(wireframe)渲染几何图形
        var planeMaterial = new THREE.MeshBasicMaterial( { color: 0xcccccc } );
        // Mesh表示一类基于三角形网格(triangular polygon mesh)的对象
        var plane = new THREE.Mesh( planeGeometry, planeMaterial );
        // 默认的,平面的对称中心位于原点,width与X轴平行,height与Y轴平行
        // 在X轴方向逆时针(从原点往X轴正向看)旋转90度
        plane.rotation.x = -0.5 * Math.PI; // 圆周长2PI,PI代表180度,
        // 在X轴方向偏移15
        plane.position.x = 15;
        plane.position.y = 0;
        plane.position.z = 0;
        scene.add( plane );
 
        // 类似的,创建一个立方体,类似的,其对称中心也是默认位于原点
        var cubeGeometry = new THREE.CubeGeometry( 4, 4, 4 );
        // wireframe表示绘制网格线
        var cubeMaterial = new THREE.MeshBasicMaterial( { color: 0xff0000, wireframe: true } );
        var cube = new THREE.Mesh( cubeGeometry, cubeMaterial );
        cube.position.x = -4;
        cube.position.y = 3;
        cube.position.z = 0;
        scene.add( cube );
 
        // 绘制一个球体
        var sphereGeometry = new THREE.SphereGeometry( 4, 20, 20 );
        var sphereMaterial = new THREE.MeshBasicMaterial( { color: 0x7777ff, wireframe: true } );
        var sphere = new THREE.Mesh( sphereGeometry, sphereMaterial );
        sphere.position.x = 20;
        sphere.position.y = 4;
        sphere.position.z = 2;
        scene.add( sphere );
 
        // 移动镜头
        camera.position.x = -30;
        camera.position.y = 40;
        camera.position.z = 30;
        // 将镜头指向场景的中心
        camera.lookAt( scene.position );
        
        // 渲染
        $( "#WebGL" ).append( renderer.domElement );
        renderer.render( scene, camera );
    } );
</script>
</body>
</html>

渲染效果:first-threejs-scene-01

添加材质/光影效果

本节我们改进一下上面的例子,修改材质,并添加光线、阴影效果。

首先,为场景添加一个光源:

JavaScript
1
2
3
4
5
// 聚光灯效果的白色光源
var spotLight = new THREE.SpotLight( 0xffffff );
// 设置聚光灯的位置
spotLight.position.set( -40, 60, -10 );
scene.add( spotLight );

添加这段代码后,渲染效果不会有任何改变。原因我们已经在前面的代码注释中提到过, MeshBasicMaterial这种材质不会对光线作出反应。我们替换一下材质:

JavaScript
1
2
3
4
5
var planeMaterial = new THREE.MeshLambertMaterial( { color: 0xcccccc } );
// ...
var cubeMaterial = new THREE.MeshLambertMaterial( { color: 0xff0000 } );
// ...
var sphereMaterial = new THREE.MeshLambertMaterial( { color: 0x7777ff} );

除了MeshLambertMaterial之外,MeshPhongMaterial也会对光源作出反应。

现在刷新一下页面,可以看到如下渲染效果:first-threejs-scene-02

比上一幅截图好看多了,但是还有点不自然,因为没有阴影效果。

由于渲染阴影比较消耗资源,因此默认情况下Three.js关闭了阴影。要启用阴影其实很简单:

JavaScript
1
2
// 启用阴影效果
renderer.shadowMapEnabled = true;

此外,你还需要定义什么对象产生(cast)阴影,什么对象接收阴影:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
// 阴影由平面接收
plane.receiveShadow = true;
// ...
cube.castShadow = true;
// ...
sphere.castShadow = true;
 
 
// 设置产生阴影的光源
spotLight.castShadow = true;
// 提高阴影质量
spotLight.shadowMapWidth = spotLight.shadowMapHeight = 1024 * 4;
添加动画效果

要想为场景添加动画效果,我们需要找到定期重渲染场景的方法。setInterval()这种定时器是不适合的,因为它与渲染行为不是同步的,会导致严重性能问题。

requestAnimationFrame() 是现代浏览器支持的、避免两setInterval()缺点的函数。你可以为它提供一个回调,此回调会定期(间隔由浏览器定义)的被调用。在回调中你可以指定任何渲染逻辑,浏览器负责尽可能平滑、高效的绘制。示例代码:

JavaScript
1
2
3
4
function renderScene() {
    requestAnimationFrame( renderScene );
    renderer.render( scene, camera );
}

上面的函数把自己传递给requestAnimationFrame,从而导致函数的逻辑被反复调用,从而可以产生动画效果。

FPS统计

为了显示动画帧率信息,我们引入一个助手库stats.js:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script src="stat.js"></script>
<script type="text/javascript">
    var stats = new Stats();
    stats.showPanel( 0 ); // 0: fps, 1: ms, 2: mb, 3+: custom
    document.body.appendChild( stats.dom );
    function renderScene() {
        // 开始统计
        stats.begin();
        // 这里编写被监控的代码
        stats.end();
        requestAnimationFrame( animate );
    }
    requestAnimationFrame( renderScene );
</script>
添加动画

下面我们为立方体添加一个翻滚效果,为球体添加一个弹跳效果:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var step = 0;
 
function animate() {
    stats.begin();
    cube.rotation.x += 0.02;
    cube.rotation.y += 0.02;
    cube.rotation.z += 0.02;
    step += 0.04; // 定义弹跳速度
    sphere.position.x = 20 + ( 10 * (Math.cos( step )));
    sphere.position.y = 2 + ( 10 * Math.abs( Math.sin( step ) ));
    renderer.render( scene, camera );  // 反复渲染
    stats.end();
    requestAnimationFrame( animate );
}
 
requestAnimationFrame( animate );

刷新浏览器,可以查看动画效果,注意左上角的帧率窗口。

使用基本组件

上一章的学习中,我们创建了由几个对象构成的简单场景,并制作了简单的动画效果。现在我们来更深入的了解一下构成Three.js场景的组件。 

场景的内容物

之前我们调用 new THREE.Scene() 创建了一个场景。场景是一个容器,它内部可以包含三类东西:

  1. 镜头(Camera):决定了查看场景的角度和方式。在渲染场景的时候镜头可以自动创建,但是你也可以手工指定其参数
  2. 光源(Lights):影响材质的渲染效果、阴影
  3. 物体(Objects):场景中渲染的主要东西。包括各种几何形状、导入的模型
场景的基本API
属性/方法 说明
children 所有对象组成的数组
getChildByName(name) 根据名称来查找对象
remove(obj) 从场景中移除一个对象
traverse(callback) 指定一个回调,针对场景中所有对象调用之
fog 添加烟雾效果,这样越远的物体显示越模糊:
JavaScript
1
2
3
4
// 白色雾,从near=0.015开始出现,far=100表示雾变浓厚的速率
scene.fog = new THREE.Fog( 0xffffff, 0.015, 100 );
// 指定颜色、浓度
scene.fog = new THREE.FogExp2( 0xffffff, 0.01 );
overrideMaterial 覆盖场景中所有物体的材质设置:
JavaScript
1
scene.overrideMaterial = new THREE.MeshLambertMaterial({color: 0xffffff});
Geometry/Mesh的基本API

Three.js提供了大量开箱即用的Geometry, Geometry用于定义物体的形状,材质则定义其外观。

在大部分3D图形库中,Geometry基本上都是三维空间中一系列点、以及连接这些点的面的集合。以立方体为例:

  1. 每个立方体包含8个角,这些角可以由三维空间中的一个点来确定。这些点称为顶点(vertices)
  2. 每个立方体包含6个面,这些面的每个角都对应一个顶点。这些面称为face

当使用Three.js自带的Geometry时你不需要逐个定义所有点、面。例如对于Cube,你只需要定义长宽高即可,Three.js会利用你提供的这些信息创建所有必须的点、面。

Three.js允许自定义点、面,然后构成一个几何图形。下面是手工构建Cube的例子:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 所有顶点
var vertices = [
    new THREE.Vector3( 1, 3, 1 ),
    new THREE.Vector3( 1, 3, -1 ),
    new THREE.Vector3( 1, -1, 1 ),
    new THREE.Vector3( 1, -1, -1 ),
    new THREE.Vector3( -1, 3, -1 ),
    new THREE.Vector3( -1, 3, 1 ),
    new THREE.Vector3( -1, -1, -1 ),
    new THREE.Vector3( -1, -1, 1 )
];
// 所有三角形的面,数字为从0开始的顶点序号
var faces = [
    new THREE.Face3( 0, 2, 1 ),
    new THREE.Face3( 2, 3, 1 ),
    new THREE.Face3( 4, 6, 5 ),
    new THREE.Face3( 6, 7, 5 ),
    new THREE.Face3( 4, 5, 1 ),
    new THREE.Face3( 5, 0, 1 ),
    new THREE.Face3( 7, 6, 2 ),
    new THREE.Face3( 6, 3, 2 ),
    new THREE.Face3( 5, 7, 0 ),
    new THREE.Face3( 7, 2, 0 ),
    new THREE.Face3( 1, 3, 4 ),
    new THREE.Face3( 3, 6, 4 ),
];
var geom = new THREE.Geometry();
geom.vertices = vertices;
geom.faces = faces;
// 在重新设置顶点数组后,提示需要更新。这是因为Three.js默认假设Mesh的Geometry的形状在生命周期内保持不变
geom.verticesNeedUpdate = true;
// 根据顶点重新计算面法线
geom.computeFaceNormals();

在以前版本的Three.js中,允许使用四边形来定义面。四边形在建模时比较受欢迎,原因是很容易被增强、平滑。三角形在渲染、游戏引擎中比较受欢迎,原因是比较简单。 

有了Geometry后,加上材质就可以构成简单的3D物体——Mesh了:

JavaScript
1
2
3
4
5
6
7
8
// 材质数组
var materials = [
    new THREE.MeshLambertMaterial( { opacity: 0.6, color: 0x44ff44, transparent: true } ),
    new THREE.MeshBasicMaterial( { color: 0x666666, wireframe: true } )
];
// Mesh组
var mesh = THREE.SceneUtils.createMultiMaterialObject( geom, materials );
scene.add( mesh );

Three.js允许给Geometry应用多个材质,上例中的Cube既有颜色填充,也显示了线条,这是两种材质的混合效果。从实现角度来说,Three.js创建了两个THREE.Mesh实例,每个材质对应一个实例,这两个实例被放置到一个组里面。添加组到场景的方式,与添加Mesh一致。

我们可以调用组的forEach,对其中所有Mesh进行操作:

JavaScript
1
2
3
mesh.children.forEach( function ( e ) {
    e.castShadow = true
} );
Geometry的基本API
属性/方法 说明
vertices 构成此Geometry的顶点坐标数组
faces 构成Geometry的三角形面数组
verticesNeedUpdate 修改顶点数组后,提示Three.js需要更新顶点
computeFaceNormals() 重新根据顶点来计算面
clone() 克隆一个Geometry
Mesh的基本API
属性/方法 说明
position.x|y|z
position.set(x,y,z)
此物体相对于父对象的位置,大部分物体的父对象是THREE.Scene对象,对于组中的Mesh,其父对象是组。示例代码:
JavaScript
1
2
3
4
5
6
7
8
// 方法一:
cube.position.x=10;
cube.position.y=3;
cube.position.z=1;
// 方法二:
cube.position.set(10,3,1);
// 方法三:
cube.postion=new THREE.Vector3(10,3,1)
rotation.x|y|z 让物体围绕自己的轴(而不是场景的)旋转一定角度。与position类似,具有三种设置方法
scale.x|y|z 让物体在其轴方向缩放。与position类似,具有三种设置方法
translateX(amount)

将物体沿着X/Y/Z轴方向移动

这些方法指定的是相对位移,而position指定的是绝对值

translateY(amount)
translateZ(amount)
使用镜头

Three.js支持两种类型的镜头:正交(orthographic)镜头、透视(perspective)镜头。到目前为止我们还没有使用过正交镜头。

正交镜头的特点是,物品的渲染尺寸与它距离镜头的远近无关。也就是说在场景中移动一个物体,其大小不会变化。正交镜头适合2D游戏。

透视镜头则是模拟人眼的视觉特点,距离远的物体显得更小。透视镜头通常更适合3D渲染。

THREE.PerspectiveCamera的API

构造函数参数:

参数 说明
fov

视界,从镜头可以看到的场景的部分。其值为镜头到近平面上下边的夹角

人眼的FOV接近180度,某些鸟类的FOV打到360度。但是计算机屏幕做不到覆盖视野,通常3D游戏的FOV取值在60-90度之间

较好的默认值为45

aspect 渲染区域的纵横比。较好的默认值为window.innerWidth/window.innerHeight
near 近平面离镜头的距离。较好的默认值为0.1
far 远平面离镜头的距离。较好的默认值为1000

关于这些参数的形象化描述,请参考术语视截锥中的截图。

THREE.OrthographicCamera的API

正交镜头不关心FOV、纵横比这些概念。其构造函数实际上是指定了一个Cube,落在其中的物体会被渲染:

参数 说明
left 相机截锥左平面位置,如果你将其设置为-100,则位置在其更左边的物体将不可见
right 相机截锥右平面位置
top 相机截锥上平面位置
bottom 相机截锥下平面位置
near 近平面的位置
far 远平面的位置

关于这些参数的形象化描述,参考下图:

orthographic-camera镜头聚焦

创建镜头后,还需要将其移动、然后对准物体积聚的场景中心位置,才能确保物体的渲染。移动镜头,通过设置其position属性来实现:

JavaScript
1
2
3
camera.position.x = 120;
camera.position.y = 60;
camera.position.z = 180;

聚焦,则是调用下面的方法实现:

JavaScript
1
camera.lookAt( new THREE.Vector3( x, 10, 0 ) );
HUD

所谓HUD(head-up display,平视显示),是指在屏幕(挡风玻璃)上显示一些辅助信息(例如飞机、汽车的仪表信息),避免驾驶员低头分散注意力。HUD的特点是其显示内容的大小、位置与镜头无关。

要实现HUD效果,可以同时渲染两套场景,其中HUD场景使用OrthographicCamera镜头:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
var scene = new THREE.Scene();
var sceneOrtho = new THREE.Scene();
 
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 250);
// 正交镜头的近平面的大小,和浏览器窗口大小一致
var cameraOrtho = new THREE.OrthographicCamera(0, window.innerWidth, window.innerHeight, 0, -10, 10);
 
var webGLRenderer = new THREE.WebGLRenderer();
 
webGLRenderer.render(scene, camera);
// 防止在下一次render时,自动清屏
webGLRenderer.autoClear = false;
webGLRenderer.render(sceneOrtho, cameraOrtho);
使用光源

在Three.js中光源很重要。不设置光源你就看不到被渲染的物体。Three.js内置了多种光源以满足特定场景的需要。

光源分类
光源 说明
AmbientLight 环境光,其颜色均匀的应用到场景及其所有对象上
PointLight 3D空间中的一个点光源,向所有方向发出光线
SpotLight 产生圆锥形光柱的聚光灯,台灯、天花板射灯通常都属于这类光源
DirectionalLight 也就无限光,光线是平行的。典型的例子是日光
HemisphereLight 特殊光源,用于创建户外自然的光线效果,此光源模拟物体表面反光效果、微弱发光的天空
AreaLight 面光源,指定一个发光的区域
LensFlare 不是光源,用于给光源添加镜头光晕效果
基本光源
AmbientLight

这种光源为场景添加全局的环境光。这种光没有特定的方向,不会产生阴影。通常不会把AmbientLight作为唯一的光源,而是和SpotLight、DirectionalLight等光源结合使用,从而达到柔化阴影、添加全局色调的效果。

指定颜色时要相对保守,例如#0c0c0c。设置太亮的颜色会导致整个画面过度饱和,什么都看不清:

JavaScript
1
2
3
var ambiColor = "#0c0c0c";
var ambientLight = new THREE.AmbientLight(ambiColor);
scene.add(ambientLight);
PointLight

该类模拟一个点光源,具有以下属性:

属性 说明
color 光线的颜色
intensity 光线的强度,默认1,浮点数
distance 光线能照耀的距离
position 光源的位置
visible 设置为true则打开光源

示例代码:

JavaScript
1
2
3
4
5
6
var pointColor = "#ccffcc";
var pointLight = new THREE.PointLight( pointColor );
pointLight.distance = 100;
scene.add( pointLight );
// 设置强度
pointLight.intensity = 2.4;
SpotLight

这种光源的使用场景最多,特别是在你需要阴影效果的时候。PointLight的所有属性对于SpotLight可用,前者还包括以下属性:

属性 说明
castShadow

此光源是否可以导致物体产生阴影

注意:目标物体需要设置receiveShadow

shadowCameraNear 从距离光源多远的地方开始创建阴影
shadowCameraFar 到距离光源多远的地方不再创建阴影
shadowCameraFov 阴影的FOV
target 此光源指向的目标。光线从光源照向该目标:
JavaScript
1
2
3
4
var targetObject = new THREE.Object3D();
scene.add(targetObject);
// 聚光灯将跟踪三维空间中的一个点
light.target = targetObject;
shadowBias 设置阴影的位置偏移
angle 光锥的夹角,默认Math.PI/3
exponent 衰减指数,即随着与光源距离的增加,光线衰减的速度
onlyShadow 如果设置为true,仅仅产生阴影,而不照亮物体
shadowCameraVisible 如果设置为true,你将看到光源如何、从何处产生阴影(显示截锥)。用于调试目的
shadowDarkness 阴影的暗度,默认0.5。一旦场景被创建此参数即不可修改
shadowMapWidth
shadowMapHeight

有多少像素用于创建阴影,如果阴影出现锯齿效果,可以增加此参数。一旦场景被创建此参数即不可修改

另一种减轻阴影锯齿的方法是,让阴影相机截锥尽可能小

示例代码:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
var spotLight = new THREE.SpotLight(pointColor);
spotLight.position.set(-40, 60, -10);
spotLight.castShadow = true;
spotLight.shadowCameraNear = 2;
spotLight.shadowCameraFar = 200;
spotLight.shadowCameraFov = 30;
spotLight.target = plane;   // 跟踪目标
spotLight.distance = 0;
spotLight.angle = 0.4;
 
scene.add(spotLight);

光锥的宽、高可以基于以下代码求出:

JavaScript
1
2
var coneLength = light.distance ? light.distance : 10000;
var coneWidth = coneLength * Math.tan( light.angle * 0.5 ) * 2;
DirectionalLight

用于模拟遥远的,类似太阳那样的光源。该光源与SpotLight的主要区别是,它不会随着距离而变暗,所有被照耀的地方获得相同的光照强度。

DirectionalLight具有大部分SpotLight的属性。示例代码:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var directionalLight = new THREE.DirectionalLight( pointColor );
directionalLight.position.set( -40, 60, -10 );
directionalLight.castShadow = true;
directionalLight.shadowCameraNear = 2;
directionalLight.shadowCameraFar = 200;
directionalLight.shadowCameraLeft = -50;
directionalLight.shadowCameraRight = 50;
directionalLight.shadowCameraTop = 50;
directionalLight.shadowCameraBottom = -50;
 
directionalLight.distance = 0;
directionalLight.intensity = 0.5;
directionalLight.shadowMapHeight = 1024;
directionalLight.shadowMapWidth = 1024;
 
scene.add( directionalLight );
高级光源
HemisphereLight

模拟穹顶(半球)的微弱发光效果,让户外场景更加逼真。使用DirectionalLight + AmbientLight可以在某种程度上来模拟户外光线,但是不够真实,因为无法体现大气层的散射效果、地面或物体的反射效果。常用属性:

属性 说明
color 天空散射的光线颜色
groundColor 地面散射的光线颜色
intensity 光线强度

示例代码:

JavaScript
1
2
3
4
// 三个参数分别对应天空颜色、地面颜色、强度
var hemiLight = new THREE.HemisphereLight(0x0000ff, 0x00ff00, 0.6);
hemiLight.position.set(0, 500, 0);
scene.add(hemiLight);
AreaLight

用于定义一个发光的矩形区域,该光源属于Three.js扩展。

THREE.WebGLRenderer这个渲染器不能和AreaLight一起使用,原因是THREE.AreaLight是一种复杂的光源,与WebGLRenderer一起使用会导致严重的性能问题。

渲染器THREE.WebGLDeferredRenderer使用不同的途径来渲染场景,它将渲染拆分为几个步骤。它能够处理复杂的光源或者数量众多的光源。

使用材质

通过前面章节的学习,我们已经知道材质 + Geometry可以构成Mesh——可以添加到3D场景中的物体。

Geometry就好像是骨架,材质则类似于皮肤,它定义了Geometry的外观——是否有金属质感、是否透明、是否显示为线框(wireframe)。

材质分类
材质 说明
MeshBasicMaterial 基本的材质,显示为简单的颜色或者显示为线框。不考虑光线的影响
MeshDepthMaterial 使用简单的颜色,但是颜色深度和距离相机的远近有关
MeshNormalMaterial 基于面Geometry的法线(normals)数组来给面着色
MeshFacematerial 容器,允许为Geometry的每一个面指定一个材质
MeshLambertMaterial 考虑光线的影响,哑光材质
MeshPhongMaterial 考虑光线的影响,光泽材质
ShaderMaterial 允许使用自己的着色器来控制顶点如何被放置、像素如何被着色
LineBasicMaterial 用于THREE.Line对象,创建彩色线条
LineDashMaterial 用于THREE.Line对象,创建虚线条
RawShaderMaterial 仅和THREE.BufferedGeometry联用,优化静态Geometry(顶点、面不变)的渲染
SpriteCanvasMaterial 在针对单独的点进行渲染时用到
SpriteMaterial
PointCloudMaterial
公共属性

作为所有材质的基类,THREE.Material提供了以下属性:

属性 说明
基本属性
最常用的属性,用于控制对象的透明度、是否可见、如何被引用(基于ID还是名称)
id 当你创建一个材质的时候自动分配,作为材质实例的标识,从0开始自动计数
uuid 全局唯一标识,内部使用
name 给材质分配一个名称,调试用
opacity

设定材质的透明度,和transparent联用,范围0~1

transparent

如果设置为true,则Three.js考虑opacity的设置。对于具有Alpha通道的纹理,该属性也需要设置为true

overdraw 使用THREE.CanvasRenderer渲染器时多边形会比预期的绘制的大一些。如果使用该渲染器时你法线Gaps可设置为true 
visible 材质是否可见,如果设置为false则物体看不见
side

材质应用到目标Geomotry的哪一面。默认THREE.Frontside表示应用在外面,可选值THREE.BackSide应用在里面、THREE.DoubleSide应用到两面

对于不封闭空间的Geomotry,例如平面,此属性重要

needsUpdate 改变材质的某些属性后,你可以设置该属性为true,这样Three.js就会丢弃缓存,重新渲染材质
混合(Blending)属性
定义对象如何与其背景混合,或者说我们渲染的颜色如何与其背后的颜色交互
blending  决定材质如何与背景混合,默认值THREE.NormalBlending,表示仅仅显示顶层颜色
blendsrc 定义物体(源)如何混合到背景(目标)中,默认THREE.SrcAlphaFactor表示基于物体的Alpha通道进行混合
blenddst 定义在混合时,背景(目标)如何渲染,默认THREE.OneMinusSrcAlphaFactor表示基于物体的Alpha通道进行混合
 blendequation 定义blendsrc、blenddst如何使用,默认将它们相加(AddEquation)
高级属性
控制低级别的WebGL上下文如何渲染对象,大部分情况下不需要使用
depthTest

如果关闭depthTest,意味着同时关闭reading/testing/writing

到底什么是深度测试(depthTest)呢?假设由两个完全一样的形状,位于你的正前方。真实世界中,你仅能看到里你近的那一个。但是在3D渲染过程中:

  1. 如果远的物体先被绘制,那么没有问题,效果和真实世界一致
  2. 如果近的物体先被绘制,远物体后被绘制,就会有问题,远物体可以被看见

所谓深度测试,是现代GPU中内置的一个工具,能够让渲染输出总是符合预期,而不管对象的输出先后顺序。具体实现机制是:当绘制一个像素时,会查看此像素位置原先的depth(即离相机的远近)值,如果新的像素depth值较小,则执行绘制,否则保留原来的值

由于深度测试的实现机制,和透明度(混合)在一起工作时可能出现问题,有时候需要禁用

本章跳过了所有和纹理(textures)、映射(maps)、动画 有关的属性。

简单Mesh材质

你可以把属性组成一个对象,作为构造函数的入参:

JavaScript
1
2
3
4
5
6
var material = new THREE.MeshBasicMaterial( {
    color: 0xff0000,
    name: 'material-1',
    opacity: 0.5,
    transparency: true
} );

或者逐个的设置属性:

JavaScript
1
2
3
4
5
var material = new THREE.MeshBasicMaterial();
material.color = new THREE.Color( 0xff0000 );  // 这种方式必须提供Color对象
material.name = 'material-1';
material.opacity = 0.5;
material.transparency = true;
THREE.MeshBasicMaterial

该材质不考虑场景中的光源,目标物体被渲染成简单的、扁平(Flat)的形状。可选的,你可以显示物体的线框(Wireframe),线框由所有面的边构成。

该材质具有以下额外属性:

属性 说明
color 材质的颜色
wireframe 是否显示线框。显示线框对于调试有帮助
Wireframelinewidth 线框的线条宽度
shading

定义如何着色,可选值THREE.SmoothShading、THREE.NoShading、THREE.FlatShadin

默认值THREE.SmoothShading,导致渲染平滑的渲染——例如平滑过渡颜色

vertexColors 可以定义各个顶点的颜色,默认值THREE.NoColors。你可以设置为THREE.VertexColors,这样渲染器会考虑Geometry.colors属性
fog 该材质是否被全局迷雾效果影响
THREE.MeshDepthMaterial

使用这种材质,物体的外观会受到物体离镜头的距离的影响——随着距离增加而淡出。你可以联合使用其它材质,产生淡出效果。该材质具有以下额外属性:

属性 说明
wireframe 是否显示线框
wireframeLineWidth 线框的宽度

设置相机的near、far属性,可以决定使用此材质的物体的淡出速度 。如果far - near很大,则物体的淡出速度非常慢。

THREE.MeshNormalMaterial

每个面显示特定的颜色,其颜色取决于该面的法线(垂直于面的向量)。当物体旋转时,其固定角度的颜色保持不变。

法线在Three.js中被大量使用,它被用来确定光线反射效果、帮助映射纹理到3D模型,并且为如何照亮、shade、染色(color)一个表面上的像素点。

为了查看法线的方向,我们可以使用THREE.ArrowHelper:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//遍历球体的所有面
for ( var f = 0, fl = sphere.geometry.faces.length; f < fl; f++ ) {
    var face = sphere.geometry.faces[ f ];
    // 计算面的中心点:把面的3个顶点依次加到三维向量中,然后除以3
    var centroid = new THREE.Vector3( 0, 0, 0 );
    centroid.add( sphere.geometry.vertices[ face.a ] );
    centroid.add( sphere.geometry.vertices[ face.b ] );
    centroid.add( sphere.geometry.vertices[ face.c ] );
    centroid.divideScalar( 3 );
    // 创建一个箭头助手
    var arrow = new THREE.ArrowHelper(
        face.normal,  // 法线矢量(箭头方向)
        centroid,   // 中心点 (箭头起点)
        2, // 长度
        0x3333FF, // 颜色
        0.5,  //箭头长度
        0.5 //箭头宽度
    );
    sphere.add( arrow );
}

该材质的额外属性包括:wireframe、wireframeLineWidth、shading。 使用FlatShading、SmoothShading的效果分别如下图:

shading-diff

THREE.MeshFaceMaterial

这不是一个单独的材质,而是一个容器。使用它,你可以为每个面指定材质。例如,对于具有12个面(Three.js仅支持三角形面)的Cube,你可以指定具有12个元素的MeshFaceMaterial:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var mats = [];
mats.push(new THREE.MeshBasicMaterial({color: 0x009e60}));
mats.push(new THREE.MeshBasicMaterial({color: 0x009e60}));
mats.push(new THREE.MeshBasicMaterial({color: 0x0051ba}));
mats.push(new THREE.MeshBasicMaterial({color: 0x0051ba}));
mats.push(new THREE.MeshBasicMaterial({color: 0xffd500}));
mats.push(new THREE.MeshBasicMaterial({color: 0xffd500}));
mats.push(new THREE.MeshBasicMaterial({color: 0xff5800}));
mats.push(new THREE.MeshBasicMaterial({color: 0xff5800}));
mats.push(new THREE.MeshBasicMaterial({color: 0xC41E3A}));
mats.push(new THREE.MeshBasicMaterial({color: 0xC41E3A}));
mats.push(new THREE.MeshBasicMaterial({color: 0xffffff}));
mats.push(new THREE.MeshBasicMaterial({color: 0xffffff}));
 
var faceMaterial = new THREE.MeshFaceMaterial(mats);
 
var cubeGeom = new THREE.BoxGeometry( 2.9, 2.9, 2.9 );
var cube = new THREE.Mesh( cubeGeom, faceMaterial );

你可以设置 geometry.faces[*].materialIndex  来指名某个面使用MeshFaceMaterial中的哪个元素来渲染。

联合多个材质

像MeshDepthMaterial这样的材质,不能设置颜色或者纹理,基本不能单独使用。

Three.js允许联合使用多个材质,以产生新的特效。材质联合也使混合(blending)有意义。示例:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var cubeMaterial = new THREE.MeshDepthMaterial();
var colorMaterial = new THREE.MeshBasicMaterial( {
    // 绿色材质
    color: 0x00ff00,
    // 允许透明度
    transparent: true,
    // 决定如何与背景(即使用了MeshDepthMaterial的那个内部盒子)进行交互
    // MultiplyBlending将前景、背景色进行乘积运算(正片叠底)
    blending: THREE.MultiplyBlending
} );
// 创建两个物体构成的组
var cube = new THREE.SceneUtils.createMultiMaterialObject( cubeGeometry, [ colorMaterial, cubeMaterial ] );
// 避免两个相同大小的重叠物体产生闪烁
cube.children[ 1 ].scale.set( 0.99, 0.99, 0.99 );
高级材质
THREE.MeshLambertMaterial 

此材质用于创建哑光效果。提供额外属性:

属性 说明
ambient 材质的阴影色,与AmbientLight配合。AmbientLight的颜色与该颜色进行乘积混合(正片叠底),默认白色
emissive 材质发出的光线的颜色,注意这不会让材质成为光源,只是一个不会被其它光源影响的颜色而已,默认黑色 
wrapAround

设置为true则启用半环境光(half-lambert lighting)技术——光线的减弱行为更加微妙。如果你的Mesh具有尖锐、黑暗的区域,设置为true可以柔化阴影、更均匀的分散(distribute)光线

wrapRGB 当wrapAround设置为true时,使用一个THREE.Vector3来控制光线减弱(drop off)的速度,可以用来微调物体的色泽
THREE.MeshPhongMaterial

此材质用于创建高反光效果。提供额外属性: 

属性 说明
ambient 材质的阴影色,与AmbientLight配合。AmbientLight的颜色与该颜色进行乘积混合(正片叠底),默认白色
emissive 材质发出的光线的颜色,注意这不会让材质成为光源,只是一个不会被其它光源影响的颜色而已,默认黑色
specular

材质的高光色,即反光的颜色。如果将其设置:

  1. 和color属性相同,可以得到金属质感(metallic-looking)的材质
  2. 为灰色,可以得到塑料质感(plastic-looking)的材质
shininess 高光色的亮度,默认30
metal 设置为true,则Three.js更改算法,让材质更加像金属
wrapAround

设置为true则启用半环境光(half-lambert lighting)技术——光线的减弱行为更加微妙。如果你的Mesh具有尖锐、黑暗的区域,设置为true可以柔化阴影、更均匀的分散(distribute)光线

wrapRGB 当wrapAround设置为true时,使用一个THREE.Vector3来控制光线减弱(drop off)的速度,可以用来微调物体的色泽
THREE.ShaderMaterial

基于这种材质,可以应用自己开发的着色器。通过定制着色器,你可以精确的定义物体如何被渲染,或者修改Threee.js的默认渲染行为。

ShaderMaterial支持wireframe、Wireframelinewidth、linewidth、shading、vertexColors、fog以及以下额外属性:

属性 说明
fragmentShader

使用的片断着色器程序的名称

片断着色器,也叫像素着色器(pixel shader)。用于定义顶点之间每个点如何渲染

vertexShader

使用的顶点着色器程序的名称

顶点着色器,可以操控顶点的属性(例如改变顶点位置)

如果你像让多边形为全红色,可以基于此着色器,指定所有顶点为红色(此颜色信息会传递给片断着色器)。反之,如果你想在顶点之间产生渐变效果,则需要基于片断着色器

顶点着色器位于图形管线(graphic pipeline)的早期,在模型坐标转换、多边形修剪(clipping)之前,此时实际渲染工作并为开始

uniforms 用于向着色器程序发送信息,相同的信息被传递给每个vertex、fragment
defines  转换为#define代码片断,设置一些全局变量供着色器程序使用
attributes 用于传递位置性的、法线相关的信息。如果使用该属性,必须为每个顶点提供
lights 是否把光照数据传入着色器,默认false

对于前面已经讨论过的其它材质,Three.js已经提供了它们的片断着色器、顶点着色器。

GSGL

着色器不是基于JavaScript语言编写的,它的专用语言是GSGL,即OpenGL ES着色器语言的WebGL支持。这种语言的语法风格类似于C语言。

示例一:动画材质

在本节,我们编写:

  1. 一个简单顶点着色器。该着色器能够修改Cube顶点的坐标值
  2. 多个借用自glslsandbox代码的片断着色器,创建具有动画效果的材质

顶点着色器代码:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script id="vertex-shader" type="x-shader/x-vertex">
    // 外部传入的时间
    uniform float time;
    varying vec2 vUv;
 
 
    void main()
    {
        // 计算变换后的位置
        vec3 posChanged = position;
        posChanged.x = posChanged.x*(abs(sin(time*1.0)));
        posChanged.y = posChanged.y*(abs(cos(time*1.0)));
        posChanged.z = posChanged.z*(abs(sin(time*1.0)));
        gl_Position = projectionMatrix * modelViewMatrix * vec4(posChanged,1.0);
    }
 
</script>

为了JavaScript与着色器之间的通信,我们使用所谓uniforms。上面的例子中定义了一个uniform,传递外部的时间,根据此时间来变换顶点的位置。

gl_Position 是一个特殊变量,用于将顶点位置信息传回JavaScript。

其中一个片断着色器代码:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<script id="fragment-shader-6" type="x-shader/x-fragment">
 
 
    uniform float time;
    uniform vec2 resolution;
 
 
    void main( void )
    {
        vec2 uPos = ( gl_FragCoord.xy / resolution.xy );
 
        uPos.x -= 1.0;
        uPos.y -= 0.5;
 
        vec3 color = vec3(0.0);
        float vertColor = 2.0;
        for( float i = 0.0; i < 15.0; ++i )
        {
        float t = time * (0.9);
 
        uPos.y += sin( uPos.x*i + t+i/2.0 ) * 0.1;
        float fTemp = abs(1.0 / uPos.y / 100.0);
        vertColor += fTemp;
        color += vec3( fTemp*(10.0-i)/10.0, fTemp*i/10.0, pow(fTemp,1.5)*1.5 );
        }
 
        vec4 color_final = vec4(color, 1.0);
        // 把颜色传递回JavaScript
        gl_FragColor = color_final;
    }
 
</script> 

材质的创建,可以基于以下助手函数:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function createMaterial( vertexShader, fragmentShader ) {
    // 从HTML标签中读取着色器源码vertexShader、fragmentShader为脚本标签的ID
    var vertShader = document.getElementById( vertexShader ).innerHTML;
    var fragShader = document.getElementById( fragmentShader ).innerHTML;
 
    var attributes = {};
    // 向着色器传递变量
    var uniforms = {
        time: { type: 'f', value: 0.2 },
        scale: { type: 'f', value: 0.2 },
        alpha: { type: 'f', value: 0.6 },
        resolution: { type: "v2", value: new THREE.Vector2() }
    };
 
    uniforms.resolution.value.x = window.innerWidth;
    uniforms.resolution.value.y = window.innerHeight;
 
    // 创建一个ShaderMaterial材质
    var meshMaterial = new THREE.ShaderMaterial( {
        uniforms: uniforms,
        attributes: attributes,
        vertexShader: vertShader,
        fragmentShader: fragShader,
        transparent: true
 
    } );
 
    return meshMaterial;
}

渲染循环中,我们需要改变uniform,从而导致着色器绘制结果发生变化,进而产生动画效果:

JavaScript
1
2
3
4
5
6
7
8
9
function render() {
    // 递增time
    cube.material.materials.forEach( function ( e ) {
        e.uniforms.time.value += 0.01;
    } );
 
    requestAnimationFrame( render );
    renderer.render( scene, camera );
}
线形几何图形的材质

有两类仅仅支持用在线条(THREE.Line)的材质。线条这种特殊的Geometry仅仅具有顶点,而没有面。

THREE.LineBasicMaterial

这种线条非常简单,可用属性:

属性 说明
color 线条的颜色
linewidth 线条的宽度
vertexColors 设置各个顶点的颜色为THREE.VertexColors类型。覆盖color属性
fog 是否受到全局迷雾的影响
THREE.LineDashedMaterial

除了上面的四个属性以外,还具有以下额外属性:

属性 说明
scale 虚线条、线条间隔的缩放比例
dashSize 虚线条的大小
gapSize 线条间隔的大小
使用几何图形

Three.js内置了大量的Geometry,可以开箱即用。 本章介绍其中的二维、三维Geometry,线条类不再介绍。

二维几何图形

二维图形的初始摆放位置是X-Y平面,但是很多情况下需要需要将它们(特别是PlaneGeometry)放置到“地面”上,也就是X-Z平面上。此时可以让它绕着X轴逆时针旋转90度:

JavaScript
1
mesh.rotation.x =- Math.PI/2; 
THREE.PlaneGeometry

外观上是一个矩形。示例:

JavaScript
1
new THREE.PlaneGeometry(width, height, widthSegments, heightSegments);

可用属性:

属性 必 说明
 width Y 矩形的宽度
 height Y  矩形的高度 
 widthSegments N  宽方向上分段的数量,默认1 
 heightSegments N  高方向上分段的数量,默认1 
THREE.CircleGeometry

外观上是一个圆形或者扇形。示例:

JavaScript
1
2
3
4
// 半径为3的圆
new THREE.CircleGeometry(3, 12);
// 半径为3的半圆
new THREE.CircleGeometry(3, 12, 0, Math.PI);

可用属性:

属性 必 说明
radius N 圆的半径,默认50
segments N 分段数,定义了构成圆的面的数量,最小值3,默认值8。更大的面数意味着更平滑的边缘
thetaStart N 从什么角度绘制起始扇边,默认0,支持范围0 ~ 2 * PI
thetaLength N 从什么角度绘制终止扇边,默认 2 * PI,支持范围0 ~ 2 * PI
THREE.RingGeometry

外观上是一个圆环或者扇环。示例:

JavaScript
1
Var ring = new THREE.RingGeometry();

可用属性: 

属性 必 说明
innerRadius N 内半径,默认0
outerRadius N 外半径,默认50
thetaSegments N 分段数,定义了构成环的面的数量。影响圆弧的平滑度
phiSegments N 不影响圆环的平滑度,但是可以增加其构成面的数量
thetaStart N 从什么角度绘制起始扇边,默认0,支持范围0 ~ 2 * PI
thetaLength N 从什么角度绘制终止扇边,默认 2 * PI,支持范围0 ~ 2 * PI
THREE.ShapeGeometry

该形状允许你创建自定义的二维图形,其操作方式类似于SVG/Canvas中的画布。 示例:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
var shape = new THREE.Shape();
 
// 移动画笔到指定的点
shape.moveTo( 10, 10 );
 
// 向Y方向画30像素的线段
shape.lineTo( 10, 40 );
 
// 绘制贝塞尔曲线
shape.bezierCurveTo( 15, 25, 25, 25, 30, 40 );
 
// 绘制拟合曲线
shape.splineThru( [
    new THREE.Vector2( 32, 30 ),
    new THREE.Vector2( 28, 20 ),
    new THREE.Vector2( 30, 10 ),
] );
 
// 绘制二次方曲线
shape.quadraticCurveTo( 20, 15, 10, 10 );
 
// 添加一个路径到形状中,挖洞
var hole1 = new THREE.Path();
hole1.absellipse( 16, 24, 2, 3, 0, Math.PI * 2, true );
shape.holes.push( hole1 );
// 再挖一个洞
var hole2 = new THREE.Path();
hole2.absellipse( 23, 24, 2, 3, 0, Math.PI * 2, true );
shape.holes.push( hole2 );
// 再一个挖洞
var hole3 = new THREE.Path();
hole3.absarc( 20, 16, 2, 0, Math.PI, true );
shape.holes.push( hole3 );
 
// 基于上述形状创建Geometry对象
new THREE.ShapeGeometry( shape );

运行结果示意图:

shapegeometryShapeGeometry支持以下属性:

属性 必 说明
shapes Y 构成此Geometry的一个或者多个THREE.Shape对象,可以传入数组
options N

应用到所有THREE.Shape的选项:

  1. curveSegments,决定了曲线的平滑程度,默认12
  2. material,使用MeshFaceMaterial时,指定该形状使用的materialIndex
  3. UVGenerator,当为材质指定纹理时,指定UV Mapping——决定纹理的哪个部分给哪个面使用。默认THREE.ExtrudeGeometry.WorldUVGenerator
THREE.Shape

该类型是THREE.ShapeGeometry的最重要的部分,允许你创建自定义的形状。它提供以下方法和属性:

moveTo(x,y)  移动画笔到指定的位置
lineTo(x,y)  从当前位置向(x,y)绘制直线

quadraticCurveTo(aCPx, aCPy, x, y)
bezierCurveTo(aCPx1, aCPy1, aCPx2, aCPy2, x, y)

你可以使用两种方式绘制曲线:二次曲线、贝塞尔曲线。两种方式的不同之处在于如何指定曲线的曲率(curvature)。下图显示这两种曲线的差别:

curve

除了曲线的两个端点以外,对于:

  1. 二次曲线,你需要提供额外的一个点(aCPx, aCPy),这个点决定了曲线的曲率
  2. 三次曲线(贝塞尔曲线),你需要提供额外的两个点(aCPx1, aCPy1, aCPx2, aCPy2)

注意:起点都是画笔当前位置,不需要在参数中指定

splineThru(pts)
在一系列点之间绘制流线型(拟合)的曲线,参数必须是THREE.Vector2对象的数组
arc(aX, aY, aRadius, aStartAngle, aEndAngle, aClockwise)
绘制一个圆圈或者圆弧。(aX, aY)指定离开当前画笔位置的偏移量,aRadius表示半径,(aStartAngle, aEndAngle)表示起始、终止角度,aClockwise为布尔值,表示是否顺时针绘制
absArc(aX, aY, aRadius, aStartAngle, aEndAngle,AClockwise)
在绝对位置上绘制圆弧
ellipse(aX, aY, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise)
绘制椭圆或者部分椭圆
absellipse(aX, aY, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise)
在绝对位置上绘制椭圆或者部分椭圆
fromPoints(vectors)
根据THREE.Vector2或者THREE.Vector3数组绘制路径
holes
THREE.Shape对象的数组,表示在当前形状上挖去的洞
makeGeometry(options)
基于此形状生成一个THREE.ShapeGeometry对象
createPointsGeometry(divisions)
把形状转换为一系列采样点的数组,divisions指定点的数量。你可以基于这些点生成一个线条对象:
JavaScript
1
2
3
4
new THREE.Line(
    shape.createPointsGeometry(10), new
    THREE.LineBasicMaterial( { color: 0xff3333, linewidth: 2 } )
);
createSpacedPointsGeometry(divisions)
与上面类似,但是生成一个Path对象
三维几何图形
THREE.BoxGeometry

这是一个非常简单的三维图形,具有长宽高的盒子:

JavaScript
1
new THREE.BoxGeometry(10,10,10);

可用属性:

属性 必 说明
width Y 宽度,沿着X轴
height Y 高度,沿着Y轴
depth Y 长度,沿着Z轴
widthSegments N 在三个方向上的分段数
heightSegments N
depthSegments N
THREE.SphereGeometry

基于此类型,你可以绘制三维球体、不完整球体:

sphere可以看到,你可以截取球体经度、纬度方向的任意片断。

代码示例:

JavaScript
1
new THREE.SphereGeometry(radius,widthSegments,heightSegments,phiStart,phiLength,thetaStart,thetaLength) 

该类型提供以下属性:

属性 必 说明
radius N 球体的半径,默认50
widthSegments N 垂直方向的分段数,默认8
heightSegments N 水平方向的分段数,默认8
phiStart N 在经度方向上,绘制球体的起点,向东绘制。范围0 ~ 2*PI
phiLength N 在经度方向上,绘制的长度。范围0 ~ 2*PI
thetaStart N 在纬度方向上,绘制球体的起点,向南绘制。范围0 ~ 2*PI
thetaLength N 在纬度方向上,绘制的长度。范围0 ~ 2*PI
THREE.CylinderGeometry

可以绘制圆柱、圆筒、圆锥或者截锥。代码示例:

Java
1
new THREE.CylinderGeometry(radiusTop,radiusBottom,height,radialSegments,heightSegments,openEnded)

可用属性:

属性 必 说明
radiusTop N 上半径
radiusBottom N 下半截
height N 高度
radialSegments N 在上下底方向上的分段数,决定光滑度
heightSegments N 在高度方向上的分段数
openEnded N 是否上下底开放,默认false
THREE.TorusGeometry

类似于甜甜圈的圆环面。代码示例:

JavaScript
1
new THREE.TorusGeometry(radius, tube, radialSegments,tubularSegments,arc)

 可用属性:

属性 必 说明
radius N 外半径
tube N 甜甜圈管道的半径
radialSegments N 分段数
tubularSegments N
arc N 弧度,决定是不是绘制完整的甜甜圈,最大值2 * PI
高级图形及二进制操作
THREE.ConvexGeometry

创建基于若干点的最小化凸面体。该形状不是Three.js核心库的组成部分。

THREE.LatheGeometry

允许你基于一个光滑曲线来创建形状。此曲线由一系列的点(Knots)指定,通常是拟合曲线。曲线围绕对象的中心Z轴转动,可以产生类似于花瓶、钟之类的形状。示例:

lathe

可用属性:

属性 必 说明
points Y 绘制曲线的基准点
segments N 分段数,数字越大则形状越光滑
phiStart N 开始弧度
phiLength N 绘制弧长
THREE.ExtrudeGeometry

可以把2D图形凸起、抬高为3D图形。比如我们可以把上面章节中的ShapeGeometry抬高:

JavaScript
1
2
3
4
5
6
7
8
9
10
var options = {
    amount: 10,
    bevelThickness: 2,
    bevelSize: 1,
    bevelSegments: 3,
    bevelEnabled: true,
    curveSegments: 12,
    steps: 1
};
new THREE.ExtrudeGeometry( drawShape(), options );

 运行效果图如下:

extrude-geometry

可用属性:

属性 必 说明
shapes Y 基于其进行凸起、抬高的THREE.Shape或者THREE.Shape数组
amount N 抬高的高度,默认100
bevelThickness N

在形状前面、后面,以及抬起的哪个侧面之间,创建一个斜坡

此厚度,即为圆滑斜坡给侧面“增加”的厚度的1/2

bevelSize N 斜坡的高度,此高度导致从前/后面看形状,其面积变大
bevelSegments N 斜坡分段数,让斜坡光滑
bevelEnabled N 是否启用斜坡,默认启用
curveSegments N 让曲线光滑
steps N 凸起生成的面的分段数,默认1
extrudePath N 沿着什么路径执行凸起,默认沿着Z轴,可以指定任意的路径
material N 用作前后面的材质的索引,如果希望前后面使用不同材质可以调用THREE.SceneUtils.createMultiMaterialObject()
extrudeMaterial N 凸起面和斜坡使用的材质的索引
uvGenerator N UVGenerator,当为材质指定纹理时,指定UV Mapping——决定纹理的哪个部分给哪个面使用。默认THREE.ExtrudeGeometry.WorldUVGenerator
THREE.TubeGeometry

与ExtrudeGeometry类似,这个类也是用于“凸起”的,只是它凸起的目标是3D的拟合曲线,而非2D图形。可用属性:

属性 必 说明
path Y 凸起的目标,一个THREE.SplineCurve3对象
segments N 分段数,路径越长,该值应该越大,默认64
radius N 管道的半径,默认1
radiusSegments N 管道截面分段数,默认8
closed N 是否闭合管道,默认false
THREE.ParametricGeometry

基于一个函数来生成几何图形。此函数有两个入参u、v,其返回值是一个三维向量,此向量作为几何图形的顶点,本质上是二维平面到三维空间的映射。

可用属性:

属性 必 说明
function Y 生成器函数,其返回值作为结果Geometry的顶点
slices Y u值被划分为多少子值,u的取值范围是0~1
stacks Y v值被划分为多少子值,v的取值范围是0~1

示例:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
var radialWave = function ( u, v ) {
    var r = 50;
 
    var x = Math.sin( u ) * r;
    var z = Math.sin( v / 2 ) * 2 * r;
    var y = (Math.sin( u * 4 * Math.PI ) + Math.cos( v * 2 * Math.PI )) * 2.8;
 
    return new THREE.Vector3( x, y, z );
};
 
var mesh = createMesh( new THREE.ParametricGeometry( radialWave, 120, 120, false ) );

运行效果图:

radialwave 

THREE.TextGeometry

该类型用于创建凸起、抬升的3D文本。可用属性:

属性 必 说明
size N 文本的尺寸,默认100
height N 凸起的高度
weight N 粗体设置,可选值bold、normal
font N 字体名称,默认helvetiker
style N 字体样式,可选值normal、italic
bevelThickness N 斜坡设置,默认不启用斜坡
bevelSize N
bevelSegments N
bevelEnabled N
curveSegments N 让曲线光滑
steps N 参考ExtrudeGeometry
extrudePath N
material N
extrudeMaterial N
uvGenerator N
二进制操作

你可以把Three.js的标准几何图形联合起来,形成复杂的新图形,这种技术叫做CSG(Constructive Solid Geometry,构造实体几何)。

为了支持CSG,我们需要使用到Three.js扩展ThreeBSP。该库提供了以下函数:

函数 说明
intersect 基于两个既有Geometry的空间交叉部分(intersection)来生成新的Geometry
union 联合两个既有Geometry的空间,生成新的Geometry 
subtract 从一个Geometry中挖去与另外一个Geometry重叠的部分,形成新的Geometry

注意:这三个函数都基于Mesh的绝对位置执行计算。因此,如果你对组(或者应用多重材质)进行操作,可能得到意外的结果。

下面的代码示例了如何对两个球体进行二进制操作:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 创建BSP对象
var sphere1BSP = new ThreeBSP( sphere1 );
var sphere2BSP = new ThreeBSP( sphere2 );
 
var resultBSP;
 
switch ( controls.actionSphere ) {
    case "subtract":
        resultBSP = sphere1BSP.subtract( sphere2BSP );
        break;
    case "intersect":
        resultBSP = sphere1BSP.intersect( sphere2BSP );
        break;
    case "union":
        resultBSP = sphere1BSP.union( sphere2BSP );
        break;
    case "none": // noop;
}
// 转换为Mesh并添加到场景
result = resultBSP.toMesh();
result.geometry.computeFaceNormals();
result.geometry.computeVertexNormals();
scene.add(result);
粒子和点云

在前面的章节中,我们以及了解了Three.js的大部分重要组件:场景、镜头、灯光、图形、材质。本章主要研究一个重要的,但是迄今为止尚未提及的重要概念——粒子。

粒子(particles)某些时候也称为精灵(sprites),是场景中的小物体。这些物体很容易被大量的创建,以模拟雨、雪、烟以及其它多种有趣的特效。

需要注意,在较近版本的Three.js中,与粒子相关的物体的类型名称从THREE.ParticleSystem变为THREE.PointCloud。粒子本身的类型名称从THREE.Particle变为THREE.Sprite。

理解粒子

粒子是2D的平面,该平面总是正向面对镜头。下面的代码创建了100个粒子:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
// 粒子的材质
var material = new THREE.SpriteMaterial();
for ( var x = -5; x < 5; x++ ) {
    for ( var y = -5; y < 5; y++ ) {
        // 创建粒子
        var sprite = new THREE.Sprite( material );
        // 设置其位置
        sprite.position.set( x * 10, y * 10, 0 );
        // 添加到场景
        scene.add( sprite );
    }
}

当你不指定任何属性的时候,粒子被渲染为白色二维小方块。 所以,上面的代码会在场景中展示10 x 10的小方块阵列。

粒子接受的材质类型只有:THREE.SpriteCanvasMaterial、THREE.SpriteMaterial。

与Three.Mesh类似,THREE.Sprite也继承自THREE.Object3D。这意味着THREE.Mesh的很多属性/方法对于粒子也是可用的,你可以使用scale属性对其缩放、使用position让其移动。

理解点云

虽然创建并移动粒子很简单,但是如果操控的粒子数量很大,你很快就会遇到性能问题。为此,Three.js提供了THREE.PointCloud用来统一处理大量的粒子。基于PointCloud的、与上面等效的代码如下:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var geom = new THREE.Geometry();
// 点云材质
var material = new THREE.PointCloudMaterial( {
    size: 4,
    vertexColors: true, color: 0xffffff
} );
for ( var x = -5; x < 5; x++ ) {
    for ( var y = -5; y < 5; y++ ) {
        // 每个粒子是三维空间中的一个点
        var particle = new THREE.Vector3( x * 10, y * 10, 0 );
        geom.vertices.push( particle );
        geom.colors.push( new THREE.Color( Math.random() * 0x00ffff ) );
    }
}
// 点的集合,点云
var cloud = new THREE.PointCloud( geom, material );
scene.add( cloud );

要创建点云,需要两个参数:

  1. 材质,使用颜色或者纹理来装饰粒子
  2. Geometry, 指定所有粒子的位置

下面再举一个例子:创建15000个随机亮度的绿色粒子构成的点云:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var geom = new THREE.Geometry();
var material = new THREE.PointCloudMaterial( {
    size: size,
    transparent: transparent,
    opacity: opacity,
    vertexColors: vertexColors,
    sizeAttenuation: sizeAttenuation,
    color: color
} );
 
 
var range = 500;
for ( var i = 0; i < 15000; i++ ) {
    // 位置随机的粒子
    var particle = new THREE.Vector3(
        Math.random() * range - range / 2,
        Math.random() * range - range / 2,
        Math.random() * range - range / 2
    );
    geom.vertices.push( particle );
    var color = new THREE.Color( 0x00ff00 );
    // 以色相、饱和度、亮度的方式设置颜色。随机亮度的绿色
    color.setHSL( color.getHSL().h, color.getHSL().s, Math.random() * color.getHSL().l );
    // 顶点颜色数组
    geom.colors.push( color );
 
}

当自动旋转点云时,你可以看到粒子满天飞舞的效果。

THREE.PointCloudMaterial

该材质的属性说明如下:

属性 说明
color 点云(粒子系统)中所有粒子的颜色,如果vertexColors设置为true,并且设置了Geometry的colors属性,则该属性被覆盖
map 指定该属性,你可以为粒子设置纹理。使用该属性,你可以让粒子看起来更像真实世界中的粒子,例如雪花 
size 粒子的尺寸,默认1
sizeAnnutation 如果false,则所有粒子的大小一样。否则,其尺寸取决于粒子距离镜头的远近
vertexColors 默认情况下,点云中所有粒子的颜色一致,设置该属性为THREE.VertexColors则Geometry的colors属性被用来指定粒子的颜色。默认值THREE.NoColors 
opacity 与transparent联用,设置粒子的透明度
transparent 默认false,如果设置为true,允许粒子具有透明度 
blending 渲染粒子时使用的混合模式 
fog  默认true,粒子是否被全局迷雾影响
基于HTML5画布来装饰粒子

你可以使用三种方式来基于HTML画布装饰(Style)粒子:

  1. 如果使用THREE.CanvasRenderer,你可以直接通过THREE.SpriteCanvasMaterial引用HTML5画布对象
  2. 如果使用THREE.WebGLRenderer,你需要一些额外的步骤来使用HTML5画布
使用THREE.CanvasRenderer

在使用该渲染器时,你可以使用THREE.SpriteCanvasMaterial,直接把画布的输出作为粒子的纹理使用。SpriteCanvasMaterial这个材质是专门为CanvasRenderer准备的,支持以下属性:

属性 说明
color 粒子的颜色,依据混合模式的设置,该颜色会和画布中图片进行混合
program 一个函数,以画布上下文作为入参。在粒子渲染时该函数被调用,函数的输出被绘制为粒子
opacity 粒子的透明度
transparent 是否允许粒子透明
blending 使用的混合模式
rotation 用于旋转画布的内容

示例:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var canvasRenderer = new THREE.CanvasRenderer();
// ...
 
// 抽取纹理的程序
var getTexture = function ( ctx ) {
 
    // the body
    ctx.translate( -81, -84 );
 
    ctx.fillStyle = "orange";
    ctx.beginPath();
    // ...
    ctx.fill();
 
};
 
// 粒子材质
var material = new THREE.SpriteCanvasMaterial( {
        program: getTexture,
        color: 0xffffff
    }
);
// 旋转
material.rotation = Math.PI;
// 创建粒子
var range = 500;
for ( var i = 0; i < 1500; i++ ) {
    var sprite = new THREE.Sprite( material );
    sprite.position.set( /* random */ );
    sprite.scale.set( 0.1, 0.1, 0.1 );
    scene.add( sprite );
}
使用WebGLRenderer

使用该渲染器时, 你需要手工在内存中创建画布对象,完成2D图形绘制,并返回一个纹理对象:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
var getTexture = function () {
    var canvas = document.createElement( 'canvas' );
    canvas.width = 32;
    canvas.height = 32;
 
    var ctx = canvas.getContext( '2d' );
    // ...
 
    // 返回一个纹理对象
    var texture = new THREE.Texture( canvas );
    texture.needsUpdate = true;
    return texture;
};
 
var geom = new THREE.Geometry();
 
 
var material = new THREE.PointCloudMaterial( {
    size: size,
    transparent: transparent,
    opacity: opacity,
    // 指定使用的THREE.Texture对象
    map: getTexture(),
    sizeAttenuation: sizeAttenuation,
    color: color
} );
 
 
var range = 500;
for ( var i = 0; i < 5000; i++ ) {
    var particle = new THREE.Vector3( /* random */ );
    geom.vertices.push( particle );
}
 
cloud = new THREE.PointCloud( geom, material );
径向渐变的例子

下面的代码演示了如何使用Canvas绘制一个径向渐变的光球:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var canvas = document.createElement('canvas');
canvas.width = 16;
canvas.height = 16;
 
var context = canvas.getContext('2d');
var gradient = context.createRadialGradient(
    canvas.width / 2, canvas.height / 2, 0, canvas.width / 2, canvas.height / 2, canvas.width / 2
);
gradient.addColorStop(0, 'rgba(255,255,255,1)');
gradient.addColorStop(0.2, 'rgba(0,255,255,1)');
gradient.addColorStop(0.4, 'rgba(0,0,64,1)');
gradient.addColorStop(1, 'rgba(0,0,0,1)');
 
context.fillStyle = gradient;
context.fillRect(0, 0, canvas.width, canvas.height);
 
var texture = new THREE.Texture(canvas);
texture.needsUpdate = true;
return texture;

可以使用此光球来装饰粒子,产生荧光那样的效果。 

使用纹理装饰粒子

上一个粒子中,我们已经使用了纹理,纹理的图像从画布中抓取。

实际上,我们可以把任何图片作为纹理使用:

JavaScript
1
var texture = THREE.ImageUtils.loadTexture( "../assets/textures/particles/raindrop-3.png" );

注意:作为纹理的图片,大小必须是2的N次方,必须是正方形。

下雨的例子

下面使用该纹理模拟下雨效果:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
var geom = new THREE.Geometry();
 
var material = new THREE.ParticleBasicMaterial( {
    size: size,
    transparent: transparent,
    opacity: opacity,
    map: texture,
    // 设置混合模式为相加,意味着雨滴图片中黑色背景部分(000000)不会被绘制——背景色 + 0 仍然是背景色
    // 使用透明背景的纹理是不支持的
    blending: THREE.AdditiveBlending,
    sizeAttenuation: sizeAttenuation,
    color: color
} );
 
 
var range = 40;
for ( var i = 0; i < 1500; i++ ) {
    var particle = new THREE.Vector3(
        Math.random() * range - range / 2,
        Math.random() * range * 1.5,
        Math.random() * range - range / 2
    );
    // 设置随机的速度属性,备用
    particle.velocityY = 0.1 + Math.random() / 5;
    particle.velocityX = (Math.random() - 0.5) / 3;
    geom.vertices.push( particle );
}
 
cloud = new THREE.ParticleSystem( geom, material );
cloud.sortParticles = true;
 
scene.add( cloud );
 
 
function render() {
    scene.children.forEach( function ( child ) {
        if ( child instanceof THREE.PointCloud ) {
            var vertices = child.geometry.vertices;
            // 在渲染循环中遍历处理所有粒子,根据速度设置其位置
            vertices.forEach( function ( v ) {
                v.y = v.y - (v.velocityY);
                v.x = v.x - (v.velocityX);
                // 如果粒子超出显示范围,则重置其位置
                if ( v.y <= 0 ) v.y = 60;
                if ( v.x <= -20 || v.x >= 20 ) v.velocityX = v.velocityX * -1;
            } );
        }
    } );
    requestAnimationFrame( render );
    webGLRenderer.render( scene, camera );
}
下雪的例子

我们改进一下上面的例子,模拟更真实的下雪效果:

  1. 建立多个点云,来模拟不同大小的雪花
  2. 在Z轴方向改变雪花的位置,模拟三维空间中雪花的飘舞

代码如下:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
function createPointCloud( name, texture, size, transparent, opacity, sizeAttenuation, color ) {
    var geom = new THREE.Geometry();
 
    var color = new THREE.Color( color );
    // 随机的改变亮度
    color.setHSL( color.getHSL().h, color.getHSL().s, (Math.random()) * color.getHSL().l );
 
    var material = new THREE.PointCloudMaterial( {
        size: size,
        transparent: transparent,
        opacity: opacity,
        map: texture,
        blending: THREE.AdditiveBlending,
        // 设置为false,表示对象不影响WebGL的depth buffer,这样不同的粒子系统就不会相互干扰
        depthWrite: false,
        sizeAttenuation: sizeAttenuation,
        color: color
    } );
 
    var range = 40;
    for ( var i = 0; i < 50; i++ ) {
        var particle = new THREE.Vector3(
            Math.random() * range - range / 2,
            Math.random() * range * 1.5,
            Math.random() * range - range / 2 );
        // 雪花在三个轴的方向上都具有速度
        particle.velocityY = 0.1 + Math.random() / 5;
        particle.velocityX = (Math.random() - 0.5) / 3;
        particle.velocityZ = (Math.random() - 0.5) / 3;
        geom.vertices.push( particle );
    }
 
    var system = new THREE.PointCloud( geom, material );
    system.name = name;
    system.sortParticles = true;
    return system;
}
// 创建多个粒子系统
function createPointClouds( size, transparent, opacity, sizeAttenuation, color ) {
 
    var texture1 = THREE.ImageUtils.loadTexture( "../assets/textures/particles/snowflake1.png" );
    var texture2 = THREE.ImageUtils.loadTexture( "../assets/textures/particles/snowflake2.png" );
    var texture3 = THREE.ImageUtils.loadTexture( "../assets/textures/particles/snowflake3.png" );
    var texture4 = THREE.ImageUtils.loadTexture( "../assets/textures/particles/snowflake5.png" );
 
    scene.add( createPointCloud( "system1", texture1, size, transparent, opacity, sizeAttenuation, color ) );
    scene.add( createPointCloud( "system2", texture2, size, transparent, opacity, sizeAttenuation, color ) );
    scene.add( createPointCloud( "system3", texture3, size, transparent, opacity, sizeAttenuation, color ) );
    scene.add( createPointCloud( "system4", texture4, size, transparent, opacity, sizeAttenuation, color ) );
}
 
createPointClouds( controls.size, controls.transparent, controls.opacity, controls.sizeAttenuation, controls.color );
 
function render() {
 
    scene.children.forEach( function ( child ) {
        if ( child instanceof THREE.PointCloud ) {
            var vertices = child.geometry.vertices;
            vertices.forEach( function ( v ) {
                // 模拟三维飘落效果
                v.y = v.y - (v.velocityY);
                v.x = v.x - (v.velocityX);
                v.z = v.z - (v.velocityZ);
 
                if ( v.y <= 0 ) v.y = 60;
                if ( v.x <= -20 || v.x >= 20 ) v.velocityX = v.velocityX * -1;
                if ( v.z <= -20 || v.z >= 20 ) v.velocityZ = v.velocityZ * -1;
            } );
        }
    } );
 
    requestAnimationFrame( render );
    webGLRenderer.render( scene, camera );
}
Sprite Map

我们可以把多个Sprite放在一个图片中,然后通过偏移量来加载、使用。就像CSS Sprite那样:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var spriteMaterial = new THREE.SpriteMaterial({
        opacity: opacity,
        color: color,
        transparent: transparent,
        map: getTexture() // 这个纹理中包含五个横向排列的小图片,我们只需要其中一个
    }
);
// 纹理图片中,X轴(u)\Y轴(v)方向的偏移量
// 如果spriteNumber取2,表示需要第三个小图片,u-offset = 0.2 * 2 = 0.4,即u偏移为0.4
// 注意u、v的最大值都是1,表示整个图片的大小,因此每个图片的大小是0.2
spriteMaterial.map.offset = new THREE.Vector2(0.2 * spriteNumber, 0);
// 如果不设置repeat,那么3、4、5几个小图都称为Sprite的一部分。而我们只需要第三个图片
// 1/5表示在u方向仅需要1/5长度,恰好是一个小图的大小
spriteMaterial.map.repeat = new THREE.Vector2(1 / 5, 1);
spriteMaterial.depthTest = false;
 
spriteMaterial.blending = THREE.AdditiveBlending;
 
var sprite = new THREE.Sprite(spriteMaterial);
从高级形状创建点云

点云基于你所提供的Geometry的顶点来渲染粒子。这意味着我们可以向它传递前面所学过的任何几何图形。

创建、加载高级Mesh和Geometry

前面的章节我们了解到,可以通过ThreeBSP这个插件来创建复合的Mesh。本章我们将学习另外两种创建高级Geometry/Mesh的机制:

  1. 分组、合并:Three.js支持内置的分组/合并机制,允许基于现存的对象来创建Mesh/Geometry
  2. 加载模型:Three.js支持从多种外部格式来加载Mesh/Geometry
分组多个Mesh

这个机制我们已经使用过,当为Geometry应用多个材质时,实际上Three.js就创建了组。

创建组非常容易,任何Mesh都可以包含子元素,你可以通过 add() 方法随时添加子元素:

JavaScript
1
2
3
4
5
6
7
8
9
10
sphere = createMesh(new THREE.SphereGeometry(5, 10, 10));
cube = createMesh(new THREE.BoxGeometry(6, 6, 6));
 
// 任何3D对象可以作为组的容器,Object3D是Mesh、Scene的超类
group = new THREE.Object3D();   // 最近版本的Three.js引入THREE.Group,专门用作组容器
// 向容器中添加其它Mesh
group.add(sphere);
group.add(cube);
 
scene.add(group); 

当你针对组中的父对象进行移动、缩放、旋转等操作时,所有子对象将会被应用相同的操作。需要强调的是旋转操作,执行旋转的时候,是整个组围绕组的中心进行旋转,而不是每个元素绕着各自的中心旋转。

使用组时,你依然可以对单个元素进行移动、缩放、旋转等操作。但是需要注意,这些操作都是相对于父对象进行的。

合并多个Geometry

大部分情况下,使用分组可以让你方便的操控大量的Mesh。但是性能问题也可能出现,因为使用分组时,每个对象依然需要被单独的处理、渲染。

使用 THREE.Geometry.merge() 你可以合并多个几何图形,然后创建单个Mesh:

JavaScript
1
2
3
4
5
6
7
8
var geometry = new THREE.Geometry();
for ( var i = 0; i < controls.numberOfObjects; i++ ) {
    var cubeMesh = createCube();
    cubeMesh.updateMatrix();
    // 提供被合并geometry的转换矩阵,确保geometry被正确的置位、旋转
    geometry.merge( cubeMesh.geometry, cubeMesh.matrix );
}
scene.add( new THREE.Mesh( geometry, cubeMaterial ) );
加载外部模型

使用编程方式来模拟真实世界中复杂的形状是困难的,Three.js允许加载3D建模软件设计的Geometry/Mesh。

加载器

加载外部模型,是通过Three.js加载器(Loader)实现的。加载器把文本/二进制的模型文件转化为Three.js对象结构。

每个加载器理解某种特定的文件格式。

支持的格式
格式 说明
JSON

Three.js自定义的、基于JSON的格式。可以声明式的定义一个Geometry或者Scene

利用该格式,你可以方便的重用复杂的Geometry或Scene

OBJ / MTL

OBJ是Wavefront开发的一种简单3D格式,此格式被广泛的支持,用于定义Geometry

MTL用于配合OBJ,它指定OBJ使用的材质

Three.js提供了OBJExporter.js,使用它可以把Three.js模型导出为OBJ格式

Collada 基于XML的格式,被大量3D应用程序、渲染引擎支持
STL

STereoLithography的简写,在快速原型领域被广泛使用。3D打印模型通常使用该格式定义

Three.js提供了STLExporter.js,使用它可以把Three.js模型导出为STL格式

CTM openCTM定义的格式,以紧凑的格式存储基于三角形的Mesh
VTK Visualization Toolkit定义的格式,用于声明顶点和面。此格式有二进制/ASCII两种变体,Three.js仅支持ASCII变体
AWD 3D场景的二进制格式,主要被away3d引擎使用,Three.js不支持AWD压缩格式
Assimp 开放资产导入库(Open asset import library)是导入多种3D模型的标准方式。使用该Loader你可以导入多种多样的3D模型格式
VRML

虚拟现实建模语言(Virtual Reality Modeling Language)是一种基于文本的格式,现已经被X3D格式取代

尽管Three.js不直接支持X3D,但是后者很容易被转换为其它格式

Babylon

游戏引擎Babylon的私有格式

PLY

常用于存储来自3D扫描仪的信息

保存/加载JSON格式

使用Three.js的JSON格式,你可以保存/加载一个Mesh或者整个场景。

保存/加载Mesh
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
knot = createMesh(new THREE.TorusKnotGeometry());
scene.add(knot);
 
// 保存
var result = knot.toJSON();
localStorage.setItem("json", JSON.stringify(result));
 
// 加载
var json = localStorage.getItem("json");
if (json) {
    var loadedGeometry = JSON.parse(json);
    var loader = new THREE.ObjectLoader();
    // 将JSON解析为Mesh
    loadedMesh = loader.parse(loadedGeometry);
    loadedMesh.position.x -= 50;
    scene.add(loadedMesh);
}
保存/加载场景

首先引入必要的脚本:

XHTML
1
2
<script type="text/javascript" src="../libs/SceneLoader.js"></script>
<script type="text/javascript" src="../libs/SceneExporter.js"></script>

代码示例:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
var exporter = new THREE.SceneExporter();
var sceneJson = JSON.stringify(exporter.parse(scene));
localStorage.setItem('scene', sceneJson);
 
 
var json = (localStorage.getItem('scene'));
var sceneLoader = new THREE.SceneLoader();
// 最后一个参数 . 定义了相对URL,加载纹理时需要
sceneLoader.parse(JSON.parse(json), function(e) {
    scene = e.scene;
}, '.');
与Blender一起使用

市场上有大量3D建模软件,用来设计复杂的Mesh。开源领域比较流行的是Blender。

Three.js提供了针对Blender、Maya、3D Studio Max等流行软件的Exporter,可以把基于这些软件设计的模型直接导出为Three.js的JSON格式。 

Exporter并非Three.js支持Blender的唯一途径,因为Three.js本身理解多种3D格式,而Blender也支持保存为这些格式。

安装Blender加载项
  1. 复制/home/alex/JavaScript/three.js/utils/exporters/blender/addons目录到/home/alex/Applications/blender/2.78/scripts/addons
  2. 打开Blender,点击菜单栏File ⇨ User Preferences,选择Addons选项卡,搜索Three.js,勾选以启用:blender-user-preferences_001
  3. 点击File ⇨ Export,在弹出的菜单中应该可以看到Three.js项
从Blender导出

点击File ⇨ Open,你可以打开既有模型文件并编辑。点击File ⇨ Export ⇨ Three.js(json),选择目标路径即可导出。

在导出对话框中,可以修改设置:

blenderexportsettings

这样,导出的JSON会包含材质的声明,并且模型使用的纹理自动导出为图片。

导入到Three.js场景
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var loader = new THREE.JSONLoader();
// 设置纹理的加载路径,JSON中仅仅包含纹理图片文件的名称,不包括目录前缀
loader.setTexturePath( './assets/' ); // 注意结尾的 /
// 异步的加载操作,回调函数提供两个入参:加载的Geometry、加载到的材质的数组
loader.load( './assets/chair.json', function ( geometry, materials ) {
    // 回调参数是THREE.Geometry,THREE.Material[]
    var material = new THREE.MultiMaterial( materials );
    var mesh = new THREE.Mesh( geometry, material );
    // 放大以便看清
    mesh.scale.x = 15;
    mesh.scale.y = 15;
    mesh.scale.z = 15;
    scene.add( mesh );
 
} ); 
加载OBJ/MTL格式

此格式被Blender原生支持、Three.js也提供了相应的加载器。

首先引入必要的脚本:

XHTML
1
2
3
4
<script type="text/javascript" src="../libs/OBJLoader.js"></script>
<!-- 下面两个用于加载MTL -->
<script type="text/javascript" src="../libs/MTLLoader.js"></script>
<script type="text/javascript" src="../libs/OBJMTLLoader.js"></script>
仅加载OBJ
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
var loader = new THREE.OBJLoader();
loader.load( '../assets/models/pinecone.obj', function ( loadedMesh ) {
    // 回调参数是一个THREE.Object3D对象
    var material = new THREE.MeshLambertMaterial( { color: 0x5C3A21 } );
    loadedMesh.children.forEach( function ( child ) {
        child.material = material;
        child.geometry.computeFaceNormals();
        child.geometry.computeVertexNormals();
    } );
 
    scene.add( loadedMesh );
} );

一个好的实践是,在回调中打印加载对象的结构。通常,加载的Geometry/Mesh表现为层次化的Group。理解此Group的结构,以便正确的应用材质,并执行额外的处理步骤。

此外,注意查看顶点的位置信息,然后估算是否需要进行缩放、如何放置镜头。

对Geometry调用computeFaceNormals、computeVertexNormals,以确保材质被正确的渲染。

同时加载MTL

如果你需要通过OBJ/MTL来加载模型,首先检查MTL的内容,确保它以相对路径来引用纹理图片。

下面的例子加载一个蝴蝶模型,需要注意,某些时候需要对材质进行微调:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var loader = new THREE.OBJMTLLoader();
 
loader.load( '../assets/models/butterfly.obj', '../assets/models/butterfly.mtl', function ( object ) {
    // 回调参数是一个THREE.Group对象
 
    var wing2 = object.children[ 5 ].children[ 0 ];
    var wing1 = object.children[ 4 ].children[ 0 ];
 
    // 模型源文件中,蝴蝶翅膀的透明度设置有误导致看不见,这里手工调整一下材质
    wing1.material.opacity = 0.6;
    wing1.material.transparent = true;
    // 禁用深度测试,避免渲染错误(不指定下面的代码来运行示例,可以看到翅膀中部分像素不停抖动)
    wing1.material.depthTest = false;
    // 默认情况下,Three.js仅仅会渲染一个面
    wing1.material.side = THREE.DoubleSide;
 
    wing2.material.opacity = 0.6;
    wing2.material.depthTest = false;
    wing2.material.transparent = true;
    wing2.material.side = THREE.DoubleSide;
 
    object.scale.set( 140, 140, 140 );
    mesh = object;
    scene.add( mesh );
 
    object.rotation.x = 0.2;
    object.rotation.y = -1.3;
} );
加载Collada格式

此格式的默认扩展名为.dae,也被广泛的使用。此格式用来定义场景、模型,甚至是动画。一个Collada模型同时包含了Geometry、材质的定义。

不意外的,要加载Collada格式同样需要引入Loader脚本:

JavaScript
1
<script type="text/javascript" src="../libs/ColladaLoader.js"></script>

下面的代码,从Collada模型中导入一个卡车模型:

JavaScript
1
2
3
4
5
6
7
8
9
10
var loader = new THREE.ColladaLoader();
 
var mesh;
loader.load( "../assets/models/dae/Truck_dae.dae", function ( result ) {
    // 从模型场景中克隆出一个对象
    mesh = result.scene.children[ 0 ].children[ 0 ].clone();
    mesh.scale.set( 4, 4, 4 );
    // 添加到当前场景中
    scene.add( mesh );
} );

需要注意的是,Collada加载器回调参数是如下结构:

JavaScript
1
2
3
4
5
6
7
var result = {
    scene: scene,  // 场景对象,THREE.Scene,包括所有模型对象,都在其中
    morphs: morphs,
    skins: skins,
    animations: animData,
    dae: {}
};

本章仅关心scene属性中的对象。需要注意,纹理可能基于WebGL不支持的格式(例如.tga),你可能需要将其转换为.png格式,并编辑Collada文件。

动画和镜头控制
基本动画

我们之前的动画,都是基于渲染循环来实现——通知Three.js尽快的重新渲染。实现代码都是如下的模式:

JavaScript
1
2
3
4
5
6
7
8
9
render();
function render() {
    // 在此,可以修改模型属性
    /* ... */
    // 执行渲染
    renderer.render( scene, camera );
    // 调度下依次渲染
    requestAnimationFrame( render );
}

我们只需要手工触发一次render()调用,之后它就会被定期(通常是每秒60次)递归调用了。 

基于这种方式,我们可以修改模型的各种属性——otation,scale, position, material, vertices, faces从而产生简单的动画效果。

选择对象

尽管用鼠标选择对象和动画没有直接关系,但是为了深入理解镜头和动画,我们需要用到这一功能。

Three.js没有直接提供“点击”功能,但是我们可以基于THREE.Projector、THREE.Raycaster来判断鼠标当前对应到哪个物体:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
document.addEventListener( 'mousedown', onDocumentMouseDown, false );
 
var projector = new THREE.Projector();
 
function onDocumentMouseDown( event ) {
    // 基于鼠标当前位置,创建一个3D向量
    var x = ( event.clientX / window.innerWidth ) * 2 - 1;
    var y = -( event.clientY / window.innerHeight ) * 2 + 1;
    var z = 0.5;
    var vector = new THREE.Vector3( x, y, z );
    // 把鼠标当前位置转换为Three.js场景中的坐标 —— 把2D屏幕坐标unproject为3D世界坐标
    vector = vector.unproject( camera );
    // 从相机所在位置发出一条射线,射到鼠标位置
    var raycaster = new THREE.Raycaster( camera.position, vector.sub( camera.position ).normalize() );
    // 检查此射线穿过哪些物体
    var intersects = raycaster.intersectObjects( [ sphere, cylinder, cube ] );
 
    if ( intersects.length > 0 ) {
 
        console.log( intersects[ 0 ] );
 
        intersects[ 0 ].object.material.transparent = true;
        intersects[ 0 ].object.material.opacity = 0.1;
    }
}
基于Tween.js的动画

Tween.js是一个简单的JS库,可以基于给定的初值、终值自动计算所有中间值。这个中间值计算过程一般叫做tweening。示例代码:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var coords = { x: 0, y: 0 };
var tween = new TWEEN.Tween( coords )
    .to( { x: 100, y: 100 }, 1000 ) // 在1秒内完成变换
    .onUpdate( function () {  // 每当值变化时,执行的回调
        console.log( this.x, this.y );
    } )
    .start();
 
requestAnimationFrame( animate );
// requestAnimationFrame会自动把一个高精度的、从DOM加载到当前流逝的时间传递给回调
function animate( time ) {
    requestAnimationFrame( animate );
    TWEEN.update( time );
}

我们可以创建一个改变物体位置的循环动画:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var posSrc = { pos: 1 };
// 创建两个Tween并链接,形成循环动画
var tween = new TWEEN.Tween( posSrc ).to( { pos: 0 }, 5000 );
tween.easing( TWEEN.Easing.Sinusoidal.InOut );
 
var tweenBack = new TWEEN.Tween( posSrc ).to( { pos: 1 }, 5000 );
tweenBack.easing( TWEEN.Easing.Sinusoidal.InOut );
 
tween.chain( tweenBack );
tweenBack.chain( tween );
// 当值变化时,改变物体的顶点位置
var onUpdate = function () {
    var count = 0;
    var pos = this.pos;
 
    loadedGeometry.vertices.forEach( function ( e ) {
        var newY = ((e.y + 3.22544) * pos) - 3.22544;
        pointCloud.geometry.vertices[ count++ ].set( e.x, newY, e.z );
    } );
 
    pointCloud.sortParticles = true;
};
 
tween.onUpdate( onUpdate );
tweenBack.onUpdate( onUpdate );
 
function render() {
    TWEEN.update();
    // 以16.7次/秒的频率,更新Tween,然后重渲染场景
    requestAnimationFrame( render );
    webGLRenderer.render( scene, camera );
}
镜头控制
TrackballControls

跟踪球控制,允许你使用鼠标进行镜头的平移(鼠标左键)、缩放(鼠标中键)、旋转操作(鼠标右键)。

使用该控制方式,需要引入:

XHTML
1
<script type="text/javascript" src="../libs/TrackballControls.js"></script>

然后,创建控制器对象:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 关联到镜头
var trackballControls = new THREE.TrackballControls( camera );
// 设置速度
trackballControls.rotateSpeed = 1.0;
trackballControls.zoomSpeed = 1.0;
trackballControls.panSpeed = 1.0;
 
 
function render() {
    // 获取上一次调用getDelta到现在流逝的时间
    var delta = clock.getDelta();
    // 传递此时间增量,控制器会根据移动速度来计算距离
    trackballControls.update( delta );
    requestAnimationFrame( render );
    webGLRenderer.render( scene, camera )
}
FlyControls

飞行控制,好像你在驾驶一架飞机,在场景中穿梭。

使用该控制方式,需要引入:

XHTML
1
<script type="text/javascript" src="../libs/FlyControls.js"></script>

示例代码:

JavaScript
1
2
3
4
5
6
7
var flyControls = new THREE.FlyControls(camera);
flyControls.movementSpeed = 25;
// 需要指向渲染场景的DOM元素
flyControls.domElement = document.querySelector('#WebGL');
flyControls.rollSpeed = Math.PI / 24;
flyControls.autoForward = true;
flyControls.dragToLook = false;
FirstPersonControls

第一人称视角。示例代码:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
var camControls = new THREE.FirstPersonControls(camera);
camControls.lookSpeed = 0.1;
camControls.movementSpeed = 20;
camControls.noFly = true;
camControls.lookVertical = true;
camControls.constrainVertical = true;
camControls.verticalMin = 1.0;
camControls.verticalMax = 2.0;
// 场景最初渲染时,镜头的位置
camControls.lon = -150;
camControls.lat = 120; 
PointerLockControls

与上一个类似,但是提供鼠标锁定功能。避免镜头一直移动导致晃眼。具体查看这个示例。

OrbitControl

这种方式可以很方便的旋转、平移、缩放位于场景中心位置的物体。例如太空场景中的星球。示例代码:

JavaScript
1
2
3
4
5
6
var orbitControls = new THREE.OrbitControls(camera);
orbitControls.autoRotate = true;
var clock = new THREE.Clock();
...
var delta = clock.getDelta();
orbitControls.update(delta);
变形与骨骼动画

当利用3D建模软件创建动画时,通常有两种机制—— 变形目标、骨骼动画。

Morph targets

使用变形目标(Morph targets),你可以定义模型的变形(deformed)版本——Mesh的一个关键位置(key position)。对于此变形版本,所有顶点的位置被记录下来。根据原始版本、变形版本的顶点位置的变化,可以方便的创建变化。其本质就是移动顶点的位置。

变形目标是定义动画最直接的方式,其缺点是对于大的Mesh和大的动画,模型文件会边的庞大。

Three.js支持手工的从一个关键位置移动到另一个,但是手工控制比较麻烦,你需要跟踪当前位置、需要变形到的目标位置。THREE.MorphAnimMesh把这些细节封装起来,我们通常直接使用该类。下面的代码示例了如何加载内置了变形目标的模型:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
var loader = new THREE.JSONLoader();
loader.load( '../assets/models/horse.js', function ( geometry, mat ) {
 
    var mat = new THREE.MeshLambertMaterial({
        morphTargets: true, // 一定要设置材质的morphTargets为true,否则不支持动画
        vertexColors: THREE.FaceColors
    } );
 
    geometry.computeVertexNormals();
    geometry.computeFaceNormals();
    // 在创建MorphAnimMesh之前,要调用computeMorphNormals确保变形目标的所有法向量被正确的计算
    // 此操作对于正确的灯光、阴影效果是必须的
    geometry.computeMorphNormals();
    if ( geometry.morphColors && geometry.morphColors.length ) {
        // 你可以为某个特定的变形目标的面定制颜色
        var colorMap = geometry.morphColors[ 0 ];
        for ( var i = 0; i < colorMap.colors.length; i++ ) {
            geometry.faces[ i ].color = colorMap.colors[ i ];
            geometry.faces[ i ].color.offsetHSL( 0, 0.3, 0 );
        }
    }
    meshAnim = new THREE.MorphAnimMesh( geometry, mat );
    meshAnim.duration = 1000;
    meshAnim.position.x = 200;
    meshAnim.position.z = 0;
    
    scene.add( meshAnim );
}, '../assets/models' );
 
function render() {
    var delta = clock.getDelta();
    webGLRenderer.clear();
    // 推进动画
    meshAnim.updateAnimation( delta * 1000 );
    requestAnimationFrame( render );
    webGLRenderer.render( scene, camera );
}

Three.js的默认行为是一下子运行所有动画,如果为单个Geometry定义了多个动画,则可以通过 parseAnimations() 和 playAnimation(name,fps) 来运行其中一个动画。

Skeletal animation

这种方式允许你为模型定义骨骼,并且把顶点附着在骨骼上。当你移动骨骼的时候,所有相连的骨骼也跟随移动,并导致顶点移动,产生变形。

变形动画比较简单,Three.js只需要转换顶点位置就可以了。骨骼动画则要复杂一些,当你移动骨骼时,Three.js需要知道如何计算附着其上的皮肤(Mesh顶点)的位置。

下面这个例子是手工执行骨骼动画的代码:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
var clock = new THREE.Clock();
 
var loader = new THREE.JSONLoader();
loader.load( '../assets/models/hand-1.js', function ( geometry, mat ) {
    // 注意设置skinning为true,否则不会看到任何骨骼移动的效果
    var mat = new THREE.MeshLambertMaterial( { color: 0xF0C8C9, skinning: true } );
    // 专门针对骨骼/皮肤几何图形的Mesh
    mesh = new THREE.SkinnedMesh( geometry, mat );
    scene.add( mesh );
    // 开始动画
    tween.start();
 
}, '../assets/models' );
 
 
var onUpdate = function () {
    var pos = this.pos;
    // 转动手指
    mesh.skeleton.bones[ 5 ].rotation.set( 0, 0, pos );
    mesh.skeleton.bones[ 6 ].rotation.set( 0, 0, pos );
    mesh.skeleton.bones[ 10 ].rotation.set( 0, 0, pos );
    mesh.skeleton.bones[ 11 ].rotation.set( 0, 0, pos );
    mesh.skeleton.bones[ 15 ].rotation.set( 0, 0, pos );
    mesh.skeleton.bones[ 16 ].rotation.set( 0, 0, pos );
    mesh.skeleton.bones[ 20 ].rotation.set( 0, 0, pos );
    mesh.skeleton.bones[ 21 ].rotation.set( 0, 0, pos );
    // 转动手腕
    mesh.skeleton.bones[ 1 ].rotation.set( pos, 0, 0 );
};
var tween = new TWEEN.Tween( { pos: -1 } )
    .to( { pos: 0 }, 3000 )
    .easing( TWEEN.Easing.Cubic.InOut )
    .yoyo( true ) // 下一次反向执行
    .repeat( Infinity ) // 无限执行
    .onUpdate( onUpdate );
 
render();
 
function render() {
    TWEEN.update();
    requestAnimationFrame( render );
    webGLRenderer.render( scene, camera );
} 
从外部模型创建动画

前面我们讨论过,Three.js支持多种外部模型格式。这些格式中的一部分,支持动画:

  1. 对于JSON格式,可以使用Blender with the JSON exporter导出
  2. Collada,该格式支持动画
导入Blender骨骼动画

基于Blender创建骨骼动画时,要注意以下几点:

  1. 模型的所有顶点,至少分配到一个顶点组(vertex group)中
  2. 顶点组的名称必须和控制它的骨骼的名称一致,这样Three.js才知道,移动骨骼时,需要修改哪些顶点
  3. 注意仅仅第一个Action被导出,因此要确保你需要导出的动画时第一个
  4. 创建关键帧时,最好选取所有骨头,即使某些不变化
  5. 导出模型时,需要保证模型处于rest pose,否则动画可能变形严重
  6. 导出时,注意勾选:Vertices、Faces、Normals、Skinning、UVs、Colors、Materials、Flip YZ、Skeletal animation

这样,骨骼的移动路径会被一同导出,在Three.js中可以简单的进行回放:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var loader = new THREE.JSONLoader();
loader.load( '../assets/models/hand-2.js', function ( model, mat ) {
 
    var mat = new THREE.MeshLambertMaterial( { color: 0xF0C8C9, skinning: true } );
    mesh = new THREE.SkinnedMesh( model, mat );
 
    var animation = new THREE.Animation( mesh, model.animation );
 
    mesh.rotation.x = 0.5 * Math.PI;
    mesh.rotation.z = 0.7 * Math.PI;
    scene.add( mesh );
    // 此助手用于查看骨骼如何变化
    helper = new THREE.SkeletonHelper( mesh );
    helper.material.linewidth = 2;
    helper.visible = false;
    scene.add( helper );
 
    // 开始播放动画
    animation.play();
 
}, '../assets/models' );
 
render();
 
function render() {
    var delta = clock.getDelta();
    if ( mesh ) {
        helper.update();
        THREE.AnimationHandler.update( delta );
    }
    requestAnimationFrame( render );
    webGLRenderer.render( scene, camera );
}
导入Collada动画

和从JSON格式导入动画的方式差不多,但是要注意Collada可以存储整个场景,包括镜头、灯光、动画,因此你需要找到需要使用的那个附带动画的Mesh:

JavaScript
1
2
3
4
var child = collada.skins[0];  // THREE.SkinnedMesh
scene.add(child);
var animation = new THREE.Animation(child, child.geometry.animation);
animation.play(); 
使用纹理

纹理在Three.js中有多种不同的使用方式,你可以用纹理来定义Mesh的颜色,或者定义发光效果、凹凸(bump)、反射效果。

在材质中使用纹理

最基本的用例是加载纹理,并将其作为材质上的一个map。当你基于此材质创建Mesh时,Mesh被纹理着色:

JavaScript
1
2
3
4
5
var texture = THREE.ImageUtils.loadTexture("../assets/textures/general/" + imageFile)
var mat = new THREE.MeshPhongMaterial();
mat.map = texture;
var mesh = new THREE.Mesh(geom, mat);
return mesh;

作为纹理的图片,可以是PNG、GIF或者JPG格式,且大小必须是2的N次方。 需要注意,纹理图片的加载是异步的,如果你希望纹理加载完毕之后再进行渲染,可以:

JavaScript
1
texture = THREE.ImageUtils.loadTexture('texture.png', {},function() { renderer.render(scene); });

由于纹理图片的像素一般不能和面的像素一一对应,纹理需要放大或者缩小后使用。WebGL/Three.js提供了几个不同的选项。你可以设置纹理的magFilter、minFilter属性,来声明它将如何被缩放 。两个基本的取值为:

THREE.NearestFilter 使用最临近的像素。当放大时,出现色块;当缩小时,丢失细节
THREE.LinearFilter 基于周围四个像素的值,来决定一个正确的颜色。缩小时仍然会丢失细节,但是放大时会更加平滑,不会出现色块

除了这两个基本的取值之外,我们还可以使用mipmap——一系列纹理图片的集合,后者是前者的一半大小。mipmap可以在加载纹理时自动创建,结合以下filter取值使用:

THREE.NearestMipMapNearestFilter 选取最匹配分辨率的mipmap,并应用NearestFilter规则。放大时仍然出现色块,但是看起来好很多
THREE.NearestMipMapLinearFilter 选取最接近的两个mipmap级别,在两个级别上分别应用NearestFilter规则,得到中间结果。这两个中间结果随之传递给LinearFilter获得最终结果
THREE.LinearMipMapNearestFilter  
THREE.LinearMipMapLinearFilter   

如果不明确指定,magFilter默认取值THREE.LinearFilter,minFilter默认取值THREE.LinearMipMapLinearFilter。

纹理本身是方形的,但是Three.js可以确保不管对于什么形状,材质都能正确的覆盖(wrap around),此保证由UV mapping实现。

创建凹凸效果
基于bump map

所谓bump map,是一幅额外的纹理,用于在材质上添加更多的深度效果:

JavaScript
1
2
3
4
5
6
7
8
var texture = THREE.ImageUtils.loadTexture( "../assets/textures/general / " + imageFile)
var mat = new THREE.MeshPhongMaterial();
mat.map = texture;
var bump = THREE.ImageUtils.loadTexture("../assets/textures/general/" + bump )
mat.bumpMap = bump;
mat.bumpScale = 0.2;  // 设置凸起的高度(负值则表示凹下的深度)
var mesh = new THREE.Mesh( geom, mat );
return mesh;

bump map通常都是灰度图,像素的密度代表了凸起的(相对)高度 。

基于normal map

对于normal map来说,高度信息没有被保存,但是法线的方向被保存了。使用normal map你可以在仅使用很少点、面的情况下创建具有复杂细节的模型:

JavaScript
1
2
3
4
5
6
7
8
var t = THREE.ImageUtils.loadTexture("../assets/textures/general/" + imageFile);
var m = THREE.ImageUtils.loadTexture("../assets/textures/general/" + normal);
var mat2 = new THREE.MeshPhongMaterial();
mat2.map = t;
mat2.normalMap = m;
mat.normalScale.set(1,1); // 设置凸起的高度(负值则表示凹下的深度)
var mesh = new THREE.Mesh(geom, mat2);
return mesh;

normal map的缺点是不容易创建,需要使用Blender/Photoshop之类的特殊工具。 

创建假反射

计算环境反射效果是非常消耗资源的操作。在Three.js中你可以模拟这种效果。步骤如下:

  1. 创建一个CubeMap对象,CubeMap是六个纹理的集合,可以被应用到Cube的六个面
  2. 使用CubeMap创建一个Box,此Box作为场景的环境,当你转动镜头时,看到的是此Box的内侧
  3. 把上述模拟环境的CubMap应用到需要反射效果的Mesh上面,Three.js可以确保其看起来就像是环境的反射
创建CubeMap

准备好纹理图片后,创建CubeMap非常容易。你需要的是能够组成完整环境的六幅图片:向前看时的图片(posz)、向后看时的图片(negz)、向上看的图片(posy)、向下看的图片(negy)、向右看的图片(posx)、向左看的图片(negx)。示例代码:

JavaScript
1
2
3
4
5
6
7
8
var path = "../assets/textures/cubemap/parliament/";
var format = '.jpg';
var urls = [
    path + 'posx' + format, path + 'negx' + format,
    path + 'posy' + format, path + 'negy' + format,
    path + 'posz' + format, path + 'negz' + format
];
var textureCube = THREE.ImageUtils.loadTextureCube( urls );

如果你已经获得360度全景图片,可以利用工具将其切割为上面的六幅图。 或者直接让Three.js处理切割过程:

JavaScript
1
var textureCube = THREE.ImageUtils.loadTexture("360-degrees.png", new THREE.UVMapping());
创建Skybox

Three.js提供了一个特殊的着色器,用来基于CubeMap来创建Skybox(环境):

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var shader = THREE.ShaderLib[ "cube" ];
shader.uniforms[ "tCube" ].value = textureCube;
 
var material = new THREE.ShaderMaterial( {
 
    fragmentShader: shader.fragmentShader,
    vertexShader: shader.vertexShader,
    uniforms: shader.uniforms,
    depthWrite: false,
    side: THREE.DoubleSide
 
} );
 
var skybox = new THREE.Mesh( new THREE.BoxGeometry( 10000, 10000, 10000 ), material );
scene.add( skybox );
创建反射物体
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 此镜头看到的景象,将用于球体的动态反射效果
cubeCamera = new THREE.CubeCamera( 0.1, 20000, 256 );
scene.add( cubeCamera );
 
// 动态反射,不仅仅CubeMap出现在反射图像中,Mesh也是
// 两个动态反射的物体不支持相互反射
var dynamicEnvMaterial = new THREE.MeshBasicMaterial( { envMap: cubeCamera.renderTarget, side: THREE.DoubleSide } );
sphere = new THREE.Mesh( sphereGeometry, dynamicEnvMaterial );
scene.add( sphere );
 
// 静态反射
// 注意材质可以设置反射率
var envMaterial = new THREE.MeshBasicMaterial( { envMap: textureCube, side: THREE.DoubleSide, reflection: 1 } );
var cylinder = new THREE.Mesh( cylinderGeometry, envMaterial );
scene.add( cylinder );
 
 
function render() {
    // 此镜头看到的内容需要更新,否则动态反射物体漆黑一片
    cubeCamera.updateCubeMap( renderer, scene );
    requestAnimationFrame( render );
}

材质的envMap属性可以设置为一个CubeMap对象,这样Mesh就可以反射CubeMap代表的环境。

← PostCSS学习笔记
Ubuntu开发知识集锦 →

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">

Related Posts

  • Cesium学习笔记
  • ExtJS 4常用组件之树面板
  • Webpack学习笔记
  • Yarn学习笔记
  • HTTP知识集锦

Recent Posts

  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
  • A Comprehensive Study of Kotlin for Java Developers
  • 背诵营笔记
  • 利用LangChain和语言模型交互
  • 享学营笔记
ABOUT ME

汪震 | Alex Wong

江苏淮安人,现居北京。目前供职于腾讯云,专注容器方向。

GitHub:gmemcc

Git:git.gmem.cc

Email:gmemjunk@gmem.cc@me.com

ABOUT GMEM

绿色记忆是我的个人网站,域名gmem.cc中G是Green的简写,MEM是Memory的简写,CC则是我的小天使彩彩名字的简写。

我在这里记录自己的工作与生活,同时和大家分享一些编程方面的知识。

GMEM HISTORY
v2.00:微风
v1.03:单车旅行
v1.02:夏日版
v1.01:未完成
v0.10:彩虹天堂
v0.01:阳光海岸
MIRROR INFO
Meta
  • Log in
  • Entries RSS
  • Comments RSS
  • WordPress.org
Recent Posts
  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
    In this blog post, I will walk ...
  • A Comprehensive Study of Kotlin for Java Developers
    Introduction Purpose of the Study Understanding the Mo ...
  • 背诵营笔记
    Day 1 Find Your Greatness 原文 Greatness. It’s just ...
  • 利用LangChain和语言模型交互
    LangChain是什么 从名字上可以看出来,LangChain可以用来构建自然语言处理能力的链条。它是一个库 ...
  • 享学营笔记
    Unit 1 At home Lesson 1 In the ...
  • K8S集群跨云迁移
    要将K8S集群从一个云服务商迁移到另外一个,需要解决以下问题: 各种K8S资源的迁移 工作负载所挂载的数 ...
  • Terraform快速参考
    简介 Terraform用于实现基础设施即代码(infrastructure as code)—— 通过代码( ...
  • 草缸2021
    经过四个多月的努力,我的小小荷兰景到达极致了状态。

  • 编写Kubernetes风格的APIServer
    背景 前段时间接到一个需求做一个工具,工具将在K8S中运行。需求很适合用控制器模式实现,很自然的就基于kube ...
  • 记录一次KeyDB缓慢的定位过程
    环境说明 运行环境 这个问题出现在一套搭建在虚拟机上的Kubernetes 1.18集群上。集群有三个节点: ...
  • eBPF学习笔记
    简介 BPF,即Berkeley Packet Filter,是一个古老的网络封包过滤机制。它允许从用户空间注 ...
  • IPVS模式下ClusterIP泄露宿主机端口的问题
    问题 在一个启用了IPVS模式kube-proxy的K8S集群中,运行着一个Docker Registry服务 ...
  • 念爷爷
      今天是爷爷的头七,十二月七日、阴历十月廿三中午,老人家与世长辞。   九月初,回家看望刚动完手术的爸爸,发

  • 6 杨梅坑

  • liuhuashan
    深圳人才公园的网红景点 —— 流花山

  • 1 2020年10月拈花湾

  • 内核缺陷触发的NodePort服务63秒延迟问题
    现象 我们有一个新创建的TKE 1.3.0集群,使用基于Galaxy + Flannel(VXLAN模式)的容 ...
  • Galaxy学习笔记
    简介 Galaxy是TKEStack的一个网络组件,支持为TKE集群提供Overlay/Underlay容器网 ...
TOPLINKS
  • Zitahli's blue 91 people like this
  • 梦中的婚礼 64 people like this
  • 汪静好 61 people like this
  • 那年我一岁 36 people like this
  • 为了爱 28 people like this
  • 小绿彩 26 people like this
  • 彩虹姐姐的笑脸 24 people like this
  • 杨梅坑 6 people like this
  • 亚龙湾之旅 1 people like this
  • 汪昌博 people like this
  • 2013年11月香山 10 people like this
  • 2013年7月秦皇岛 6 people like this
  • 2013年6月蓟县盘山 5 people like this
  • 2013年2月梅花山 2 people like this
  • 2013年淮阴自贡迎春灯会 3 people like this
  • 2012年镇江金山游 1 people like this
  • 2012年徽杭古道 9 people like this
  • 2011年清明节后扬州行 1 people like this
  • 2008年十一云龙公园 5 people like this
  • 2008年之秋忆 7 people like this
  • 老照片 13 people like this
  • 火一样的六月 16 people like this
  • 发黄的相片 3 people like this
  • Cesium学习笔记 90 people like this
  • IntelliJ IDEA知识集锦 59 people like this
  • 基于Kurento搭建WebRTC服务器 38 people like this
  • Bazel学习笔记 37 people like this
  • PhoneGap学习笔记 32 people like this
  • NaCl学习笔记 32 people like this
  • 使用Oracle Java Mission Control监控JVM运行状态 29 people like this
  • Ceph学习笔记 27 people like this
  • 基于Calico的CNI 27 people like this
Tag Cloud
ActiveMQ AspectJ CDT Ceph Chrome CNI Command Cordova Coroutine CXF Cygwin DNS Docker eBPF Eclipse ExtJS F7 FAQ Groovy Hibernate HTTP IntelliJ IO编程 IPVS JacksonJSON JMS JSON JVM K8S kernel LB libvirt Linux知识 Linux编程 LOG Maven MinGW Mock Monitoring Multimedia MVC MySQL netfs Netty Nginx NIO Node.js NoSQL Oracle PDT PHP Redis RPC Scheduler ServiceMesh SNMP Spring SSL svn Tomcat TSDB Ubuntu WebGL WebRTC WebService WebSocket wxWidgets XDebug XML XPath XRM ZooKeeper 亚龙湾 单元测试 学习笔记 实时处理 并发编程 彩姐 性能剖析 性能调优 文本处理 新特性 架构模式 系统编程 网络编程 视频监控 设计模式 远程调试 配置文件 齐塔莉
Recent Comments
  • qg on Istio中的透明代理问题
  • heao on 基于本地gRPC的Go插件系统
  • 黄豆豆 on Ginkgo学习笔记
  • cloud on OpenStack学习笔记
  • 5dragoncon on Cilium学习笔记
  • Archeb on 重温iptables
  • C/C++编程:WebSocketpp(Linux + Clion + boostAsio) – 源码巴士 on 基于C/C++的WebSocket库
  • jerbin on eBPF学习笔记
  • point on Istio中的透明代理问题
  • G on Istio中的透明代理问题
  • 绿色记忆:Go语言单元测试和仿冒 on Ginkgo学习笔记
  • point on Istio中的透明代理问题
  • 【Maven】maven插件开发实战 – IT汇 on Maven插件开发
  • chenlx on eBPF学习笔记
  • Alex on eBPF学习笔记
  • CFC4N on eBPF学习笔记
  • 李运田 on 念爷爷
  • yongman on 记录一次KeyDB缓慢的定位过程
  • Alex on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • haolipeng on 基于本地gRPC的Go插件系统
  • 吴杰 on 基于C/C++的WebSocket库
©2005-2025 Gmem.cc | Powered by WordPress | 京ICP备18007345号-2