(一)基础知识
网格(Mesh)
Geometry
是Three.js对3D物体的一个整合,记录了渲染一个3D物体所需要的基本数据。本文选取最重要的三个属性,包括顶点(vertices
),面(faces
),法向量(normal
)。
3D物体由网格(Mesh
)组成,网格由三角形组成,三角形由点组成。这里,组成网格的三角形叫做面(face
),组成三角形的点叫做顶点(vertex
),法向量(normal
)决定了每个顶点在光照下所呈现出的颜色。
上面中绿色的框就是球体的网格,可以看到这个是网格通过把顶点连接组合成多个三角形而组成的。
Face & Vertex
网格由面(Face)组成,在计算机图形学中,每个基本的面都是三角形。
我们思考一下如何画出上面的三角形,假设这面的三个点为a,b,c。
一种思路是直接指定这三个点的位置,如下:
face.a = (-50,50,0)
face.b = (50,50,0)
face.c = (-50,-50,0)
第二种思路,我们将这三个点单独放在统一的缓存中,a,b,c则用来指定这三个点在缓存中的位置:
//将顶点统一存储
const vertices = [
(-50,50,0),
(50,50,0),
(-50,-50,0)
];
//为face指定顶点在缓存中的位置
face.a = 2
face.b = 1
face.c = 0
第二种思路表面看起来似乎更加复杂了一些,但它却是实际中采用的方式,我们看看为什么。
假设我们需要画一个矩形,在3D世界中,我们需要通过两个三角形来实现
如果用第一种思路,我们需要6个点来存储位置信息,然而其中有两个点的位置是完全一样的,造成极大的内存浪费。因此,我们通过顶点缓存加位置引用的方式来指定每个三角形的顶点。
//将顶点统一存储
const vertices = [
(-50,50,0),
(50,50,0),
(-50,-50,0),
(50,-50,0)
]
//为face指定顶点在缓存中的位置
face1.a = 2
face1.b = 1
face1.c = 0
//为face指定顶点在缓存中的位置
face2.a = 1
face2.b = 2
face2.c = 3
法向量(normal)
简单说来,法向量就是垂直于平面的向量。光照和法向量的夹角决定了平面反射出的光照强度。
以上图片来自网络
假设法向量为norm, 入射光线向量为in,只需要将norm和in做简单的点积,就能得到物体的反射光照强度。
//如果点击小于0,说明表面处背光,显示为黑色
//注意,这里的光照是针对漫反射的光照。通常情况下场景中还会有环境光,因此即使背光面也不会纯黑
lightness = max(dot(in, norm),0)
那么如何得到法向量呢?由3D图形学的知识可以知道,对两个向量a,b做叉积,能得到一个向量c,并且这个向量同时垂直于a和b,也即垂直于a和b形成的平面。然后将向量c标准化(normalize
),就得到了平面ab的法向量。
//将向量a,b做叉乘,得到垂直于a,b平面的向量c
c = a x b
//将向量c标准化(c / ||c||, ||c||!==0)
norm = normalize(c)
以上图片来自网络
(二)代码分析
通过上面的分析,我们知道了顶点(Vertex),面(Face),法向量(Normal)的重要性,现在,探索一下Three.js中是如何维护这3个属性的。
Vertices(顶点)
Geometry.js
中的属性this.vertices
通过数组的形式保存了一个3D物体所有的顶点位置信息。
this.vertices: Array = [
v_1: THREE.Vector3,
v_2: THREE.Vector3,
...
v_n: THREE.Vector3
]
Face(面)
Geometry.js
中的属性this.faces
通过数组的形式保存了一个3D物体所有的三角面信息。
this.faces: Array = [
face_1: THREE.Face3,
face_2: THREE.Face3,
...
face_n: THREE.Face3,
]
Three.js提供了类Face3来更好的封装一个三角面。一个Face3类,除了用上面提到的a,b,c属性来指定3个顶点在顶点缓存vertices中的位置外,还有以下重要属性:
normal: THREE.Vector3 : 三角面的法向量
vertexNormals: Array: 每个顶点的法向量
color: THREE.Color: 指定面的颜色
-
vertexColors:Array 指定每个顶点的颜色
一个三角面只会有一个法向量。一个顶点会属于不同的三角面,因此一个顶点会有多个法向量:
上图中,红色短线表示顶点法向量,黄色短线表示面法向量。可以看到,一个顶点有多个红色法向量,一个三角面只有一个黄色法向量。
color的作用很好理解,用它来指定三角面的颜色。那么vertextColors的作用是不是仅仅指定顶点的颜色呢?要理解vertexColors,我们需要了解GPU的着色流程。
一个三角面可能包含上百个像素。vertexColors指定了三角面顶点的颜色,GPU通过插值的方式算出其他像素的颜色,最终实现整个三角面着色。下图中的渐变效果就是GPU通过插值的方式实现的。
Geometry形变
3D物体的形变(位移,旋转,缩放)涉及到两个方面,一是顶点位置的变化,二是法向量的变化。顶点位置变化才能实现形变的各种效果,法向量变化是更新物体的光照反射效果。在Geometry.js中,物体形变通过applyMatrix实现。我们来分析applyMatrix代码:
- 更新顶点位置
// 遍历Geometry中的全部顶点,为每个顶点加上形变
// new_vertex = dot(matrix,old_vertex)
for ( var i = 0, il = this.vertices.length; i < il; i ++ ) {
var vertex = this.vertices[ i ];
vertex.applyMatrix4( matrix );
}
- 更新法向量
随着物体形变,计算物体形变后的法向量是很复杂的。幸运的是,有一个规则可以直接使用:
用法向量乘以形变矩阵的逆转置举证,就可以得到形变后的法向量
//逆转置矩阵 = 逆矩阵的转置
// inverse: 获取逆矩阵
//transpose: 获取转置矩阵
逆转置矩阵 = transpose(inverse(matrix))
新法向量 = dot(逆转置矩阵,旧法向量)
//获取形变矩阵的逆转置矩阵,并且标准化
var normalMatrix = new Matrix3().getNormalMatrix( matrix );
for ( var i = 0, il = this.faces.length; i < il; i ++ ) {
var face = this.faces[ i ];
//遍历物体每个面的法向量,得到形变后的法向量
face.normal.applyMatrix3( normalMatrix ).normalize();
//遍历物体每个面的顶点法向量,得到形变后的顶点法向量
for ( var j = 0, jl = face.vertexNormals.length; j < jl; j ++ ) {
face.vertexNormals[ j ].applyMatrix3( normalMatrix ).normalize();
}
}
基于此,就不难理解rotateX
,rotateY
,rotateZ
,rotateZ
,scale
的实现原理。