Three.js源码解读二:Geometry

(一)基础知识

网格(Mesh)

Geometry是Three.js对3D物体的一个整合,记录了渲染一个3D物体所需要的基本数据。本文选取最重要的三个属性,包括顶点(vertices),面(faces),法向量(normal)。
3D物体由网格(Mesh)组成,网格由三角形组成,三角形由点组成。这里,组成网格的三角形叫做面(face),组成三角形的点叫做顶点(vertex),法向量(normal)决定了每个顶点在光照下所呈现出的颜色。

Three.js源码解读二:Geometry_第1张图片
图1.多个三角形组成球体网格

上面中绿色的框就是球体的网格,可以看到这个是网格通过把顶点连接组合成多个三角形而组成的。

Face & Vertex

网格由面(Face)组成,在计算机图形学中,每个基本的面都是三角形。

Three.js源码解读二:Geometry_第2张图片
图2.一个面

我们思考一下如何画出上面的三角形,假设这面的三个点为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世界中,我们需要通过两个三角形来实现

Three.js源码解读二:Geometry_第3张图片
图3.两个三角形组成矩形

如果用第一种思路,我们需要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
Three.js源码解读二:Geometry_第4张图片
图4.画出矩形

法向量(normal)

简单说来,法向量就是垂直于平面的向量。光照和法向量的夹角决定了平面反射出的光照强度。

Three.js源码解读二:Geometry_第5张图片
图5. 光照和法向量(图片来自网络)

以上图片来自网络
假设法向量为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)

Three.js源码解读二:Geometry_第6张图片
图5.叉积(图片来自网络)

以上图片来自网络

(二)代码分析

通过上面的分析,我们知道了顶点(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 指定每个顶点的颜色
    一个三角面只会有一个法向量。一个顶点会属于不同的三角面,因此一个顶点会有多个法向量:
Three.js源码解读二:Geometry_第7张图片
法向量

上图中,红色短线表示顶点法向量,黄色短线表示面法向量。可以看到,一个顶点有多个红色法向量,一个三角面只有一个黄色法向量。
color的作用很好理解,用它来指定三角面的颜色。那么vertextColors的作用是不是仅仅指定顶点的颜色呢?要理解vertexColors,我们需要了解GPU的着色流程。

Three.js源码解读二:Geometry_第8张图片
片元着色流程

一个三角面可能包含上百个像素。vertexColors指定了三角面顶点的颜色,GPU通过插值的方式算出其他像素的颜色,最终实现整个三角面着色。下图中的渐变效果就是GPU通过插值的方式实现的。

Three.js源码解读二:Geometry_第9张图片
插值着色

Geometry形变

3D物体的形变(位移,旋转,缩放)涉及到两个方面,一是顶点位置的变化,二是法向量的变化。顶点位置变化才能实现形变的各种效果,法向量变化是更新物体的光照反射效果。在Geometry.js中,物体形变通过applyMatrix实现。我们来分析applyMatrix代码:

  1. 更新顶点位置
// 遍历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 );
}
  1. 更新法向量
    随着物体形变,计算物体形变后的法向量是很复杂的。幸运的是,有一个规则可以直接使用:
    用法向量乘以形变矩阵的逆转置举证,就可以得到形变后的法向量
//逆转置矩阵 = 逆矩阵的转置
// 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();
  }
}

基于此,就不难理解rotateXrotateYrotateZrotateZscale的实现原理。

你可能感兴趣的:(Three.js源码解读二:Geometry)