本文深入解析Three.js核心源码,重点剖析Object3D类的层级管理与矩阵变换机制,以及Geometry类的顶点与面数据处理。详细解读了applyMatrix、updateMatrixWorld等关键函数的实现逻辑,探讨了位置、旋转、缩放与矩阵之间的同步策略,为理解Three.js底层渲染原理提供技术参考。
本篇为Three.js源码阅读系列的第二篇,直接切入核心内容。
Core::Object3D
Object3D类核心属性与层级管理
Object3D类在Three.js框架中占据核心地位,绝大多数其他类(如场景、几何体、相机、光照等)均继承自此类。由于它们都代表3D空间中的对象,因此统称为Object3D。其构造函数定义如下:
具体代码实现如下:
THREE.Object3D = function () {
THREE.Object3DLibrary.push( this );
this.id = THREE.Object3DIdCount ++;
this.name = '';
this.properties = {};
this.parent = undefined;
this.children = [];
this.up = new THREE.Vector3( 0, 1, 0 );
this.position = new THREE.Vector3();
this.rotation = new THREE.Vector3();
this.eulerOrder = THREE.Object3D.defaultEulerOrder;
this.scale = new THREE.Vector3( 1, 1, 1 );
this.renderDepth = null;
this.rotationAutoUpdate = true;
this.matrix = new THREE.Matrix4();
this.matrixWorld = new THREE.Matrix4();
this.matrixRotationWorld = new THREE.Matrix4();
this.matrixAutoUpdate = true;
this.matrixWorldNeedsUpdate = true;
this.quaternion = new THREE.Quaternion();
this.useQuaternion = false;
this.boundRadius = 0.0;
this.boundRadiusScale = 1.0;
this.visible = true;
this.castShadow = false;
this.receiveShadow = false;
this.frustumCulled = true;
this._vector = new THREE.Vector3();
}; 在深入函数逻辑前,需先明确该类的几个关键属性。
parent和children属性表明,大量Object3D对象通常通过树状结构进行管理。例如,一辆行驶的汽车作为一个Object3D对象,其行驶逻辑封装在内部;而雨刮器的摆动逻辑若放在汽车内部则难以实现,因此将雨刮器设为汽车的子节点(children),雨刮器仅负责相对于汽车的摆动。在这种树形结构中,Scene作为顶层Object3D,其模型矩阵即为视图矩阵的逆矩阵(由相机决定)。
matrix和matrixWorld属性含义明确:matrix表示本地模型矩阵,仅描述该对象自身的运动;而matrixWorld则需向父节点迭代计算,每次迭代左乘父节点的本地模型矩阵(实际是左乘父节点的全局模型矩阵),直至到达Scene对象。
position、rotation和scale表示模型矩阵的三种变换分量,具体细节可参考Matrix4类。rotation与eulerOrder共同描述旋转状态,quaternion也可描述旋转状态,具体使用哪种方式取决于useQuaternion布尔值的设置。
可以看出,Object3D最重要的“变换状态”信息实际上存储在两个“备份”中:一个是matrix对象,另一个是position等属性。这两部分必须保持一致,若通过某种方式修改了一个备份,另一个备份也应在适当时候同步更新。其他属性从字面和类型即可推断其含义,此处不再赘述。接下来介绍相关函数:
Object3D变换函数与矩阵同步机制
applyMatrix(matrix)函数将参数matrix左乘到this.matrix上,即对该Object3D对象执行特定的变换(该变换可能由多个基本变换组成,但已封装在参数matrix中)。注意,在左乘this.matrix后,会立即更新position等参数的值。相较于其他变换函数,该函数更“高级”,允许开发者自由指定变换矩阵,而非仅能指定“沿X轴移动5个单位”等简单指令。
相关代码如下:
applyMatrix: function ( matrix ) {
this.matrix.multiply( matrix, this.matrix );
this.scale.getScaleFromMatrix( this.matrix );
var mat = new THREE.Matrix4().extractRotation( this.matrix );
this.rotation.setEulerFromRotationMatrix( mat, this.eulerOrder );
this.position.getPositionFromMatrix( this.matrix );
}, translate(distance, axis)函数使对象沿axis轴指定的方向移动distance距离。translateX(distance)、translateY(distance)和translateZ(distance)函数则分别使其沿X、Y、Z轴移动distance距离。需注意,这些函数仅改变position对象的值,而不修改matrix的值。
代码实现如下:
translate: function ( distance, axis ) {
this.matrix.rotateAxis( axis );
this.position.addSelf( axis.multiplyScalar( distance ) );
},
translateX: function ( distance ) {
this.translate( distance, this._vector.set( 1, 0, 0 ) );
}, localToWorld(vector)函数将本地坐标转换为世界坐标,worldToLocal函数则执行相反操作。这里的“本地坐标”指未变换前的坐标,例如雨刮器的默认位置顶点坐标。
lookAt(eye, center, up)函数执行matrix属性对象的lookAt方法(此前已介绍Matrix4对象也有此方法),通常用于相机对象。该函数仅改变旋转状态,因此当matrix属性对象执行完毕后,若rotationAutoUpdate为真,则会更新rotation或quaternion的值,具体更新哪一个取决于useQuaternion属性。
add(object)和remove(object)函数分别从当前Object3D对象中添加或删除子对象。鉴于场景中的Object3D对象通过树状结构管理,这些函数的逻辑易于理解。
traverse(callback)函数遍历调用者及其所有后代,callback参数是一个函数,调用者和每个后代对象都会调用callback(this)。
具体代码如下:
traverse: function ( callback ) {
callback( this );
for ( var i = 0, l = this.children.length; i < l; i ++ ) {
this.children[ i ].traverse( callback );
}
}, getChildByName(name, recursive)函数通过字符串在调用者的子元素(recursive为false)或后代元素(recursive为true)中查询属性name匹配的对象并返回。
getDescendants(array)函数将调用者的所有后代对象全部push到数组array中。
updateMatrix()和updateMatrixWorld(force)函数根据position、rotation或quaternion、scale参数更新matrix和matrixWorld。updateMatrixWorld还会更新所有后代元素的matrixWorld,如果force值为真或调用者本身的matrixWorldNeedsUpdate值为真。在applyMatrix(matrix)函数中,改变matrix值后会立即更新position、rotation等属性;但在translate(distance, axis)函数中,改变position等变量(或直接修改这些属性)后并未立即更新matrix值,此时应手动调用updateMatrix()。这些细节值得注意,虽然可能期望加入事件监听以在值变化时立即更新所有相关值,但作者推测这可能是出于效率考虑:在适当的时候(例如使用matrix属性之前)进行更新会带来更高的效率——比如可能会频繁改变rotation值,但仅在使用matrix属性时才对其进行更新。
相关代码如下:
updateMatrix: function () {
this.matrix.setPosition( this.position );
if ( this.useQuaternion === false ) {
this.matrix.setRotationFromEuler( this.rotation, this.eulerOrder );
} else {
this.matrix.setRotationFromQuaternion( this.quaternion );
}
if ( this.scale.x !== 1 || this.scale.y !== 1 || this.scale.z !== 1 ) {
this.matrix.scale( this.scale );
this.boundRadiusScale = Math.max( this.scale.x, Math.max( this.scale.y, this.scale.z ) );
}
this.matrixWorldNeedsUpdate = true;
},
updateMatrixWorld: function ( force ) {
if ( this.matrixAutoUpdate === true ) this.updateMatrix();
if ( this.matrixWorldNeedsUpdate === true || force === true ) {
if ( this.parent === undefined ) {
this.matrixWorld.copy( this.matrix );
} else {
this.matrixWorld.multiply( this.parent.matrixWorld, this.matrix );
}
this.matrixWorldNeedsUpdate = false;
force = true;
}
for ( var i = 0, l = this.children.length; i < l; i ++ ) {
this.children[ i ].updateMatrixWorld( force );
}
}, deallocate函数手动释放调用者占用的空间,当不再需要该对象时执行此操作。
Core::Projectors
管理投影矩阵的类代码较为复杂,推测涉及render类中的操作,待适当时机再行研究。
Core::UV
辅助类:UV、射线与几何体基础
该构造函数生成一个材质坐标类——即材质上的坐标,通常与顶点对应。光栅化后每个像素都有一个材质坐标,随后从材质上“取色”以实现纹理渲染。
具体代码如下:
THREE.UV = function ( u, v ) {
this.u = u || 0;
this.v = v || 0;
}; 材质坐标类本质上是一个简化的vector2类,仅属性名称不同。
Core::Ray Core::Rectangle Core:Spline
射线类包含原点、方向和远近截断点,在点光源中可能有应用。矩形类、曲线类相对简单且非“核心”部分,留待日后研究。
Core::Geometry
Geometry类顶点与面数据处理
Geometry类同样非常重要,表示由顶点和表面构成的几何形体。
具体代码如下:
THREE.Geometry = function () {
THREE.GeometryLibrary.push( this );
this.id = THREE.GeometryIdCount ++;
this.name = '';
this.vertices = [];
this.colors = [];
this.normals = [];
this.faces = [];
this.faceUvs = [[]];
this.faceVertexUvs = [[]];
this.morphTargets = [];
this.morphColors = [];
this.morphNormals = [];
this.skinWeights = [];
this.skinIndices = [];
this.lineDistances = [];
this.boundingBox = null;
this.boundingSphere = null;
this.hasTangents = false;
this.dynamic = true;
this.verticesNeedUpdate = false;
this.elementsNeedUpdate = false;
this.uvsNeedUpdate = false;
this.normalsNeedUpdate = false;
this.tangentsNeedUpdate = false;
this.colorsNeedUpdate = false;
this.lineDistancesNeedUpdate = false;
this.buffersNeedUpdate = false;
}; 以下两组属性最重要:
vertices属性是一个数组,每个元素是vector3类型的对象,表示一个顶点坐标。colors和normals属性表示与顶点对应的颜色值和法线向量,仅在少数情况下使用,大部分情况下顶点的颜色和法线是在“表面”中定义的——例如立方体的6面颜色各不相同,则每个顶点在不同面上具有不同的颜色。
faces属性是一个数组,每个元素是face4或face3类型的对象。此前介绍face3时提到,face中存储的仅是顶点的索引值,通过索引值即可在vertices数组中获取顶点坐标。
下面说函数:
applyMatrix(matrix)函数更新geometry中所有顶点坐标和表面法线向量,其作用实际上是用变换矩阵matrix对geometry形体进行空间变换。normalMatrix是参数matrix左上角3×3矩阵的逆转置矩阵,该矩阵用于旋转矢量(法线,而非顶点坐标)。
具体代码如下:
applyMatrix: function ( matrix ) {
var normalMatrix = new THREE.Matrix3();
normalMatrix.getInverse( matrix ).transpose();
for ( var i = 0, il = this.vertices.length; i < il; i ++ ) {
var vertex = this.vertices[ i ];
matrix.multiplyVector3( vertex );
}
for ( var i = 0, il = this.faces.length; i < il; i ++ ) {
var face = this.faces[ i ];
normalMatrix.multiplyVector3( face.normal ).normalize();
for ( var j = 0, jl = face.vertexNormals.length; j < jl; j ++ ) {
normalMatrix.multiplyVector3( face.vertexNormals[ j ] ).normalize();
}
matrix.multiplyVector3( face.centroid );
}
}, computeCentroid()函数计算几何形体中每个表面的重心(而非几何形体自身的重心)。该函数似乎更适合放在face类的原型上,但由于face类内部无法获取点的坐标(除非将点坐标数组的引用作为参数传入构造函数,这样代价较大)而仅是索引值,因此只好在geometry类的原型上定义。下面几个函数也是类似情况(事实上,face类几乎没有成员函数)。
computeFaceNormals()和computeVertexNormals(areaWeight)函数计算法线向量,前者影响face数组中每个元素的normal属性,一个face仅有1个;后者影响face数组中每个元素的vertexNormal属性,一个face3型对象有3个,一个face4型对象有4个。但需注意,被多个表面共享的顶点,其法线向量只有一个,同时受到多个表面的影响。例如中心在原点、三组表面都垂直于轴的立方体,其第一象限中的顶点,法线向量是(1,1,1)的归一化。虽然看似不可思议,平面的顶点法线并非垂直于平面,但这种指定法线的方法在利用平面模拟曲面时具有良好效果。
createMorphNormal函数为每个morph创建法线。morph应用于显示固定连续动画的变形效果。
mergeVertices函数剔除坐标值相同的点,同时更新face对象中的点索引值。
Core::Quaternian
Quaternion四维数旋转类
四维数旋转类以另一种方式表达旋转变换,相比使用rotation,可避免万向节死锁问题。
具体代码如下:
THREE.Quaternion = function( x, y, z, w ) {
this.x = x || 0;
this.y = y || 0;
this.z = z || 0;
this.w = ( w !== undefined ) ? w : 1;
}; 若不讨论函数,Quaternion就是一个简单的vector4类型对象。
setFromEuler(v, order)函数通过一次欧拉旋转设置四维数旋转。
setFromAxis(axis, angle)函数通过绕任意轴旋转设定四维数旋转。
setFromRotationMatrix(matrix)函数通过旋转矩阵设置四维数旋转。
还有一些与vector4类相同的函数此处不再列出。