ThreeJS的核心基类Object3D被很多场景对象所继承,例如Scene、Camera、Mesh、Light等;凡是继承于该基类的对象都会拥有“旋转平移缩放”(物理世界三大基本变换)的能力,该类还有两个“绝顶”重要的矩阵:matrix(本地矩阵)和matrixWorld(模型矩阵也叫作世界矩阵)。
1.WebGL中的MVP矩阵
在计算机图形学中,“矩阵”是无法逃避的话题。渲染管线最终是要将3D物体经过变换和投影转换成2D纹理数据,2D纹理数据更新进帧缓冲区就会绘制到屏幕上,其中变换和投影所使用的就是MVP矩阵左乘3D物体每个顶点。接下来介绍MVP矩阵是如何构建而成的:
1.M矩阵
模型顶点左乘M矩阵的效果:将该模型转换到世界坐标系下。
M矩阵如何构建:通过该模型的旋转平移缩放构建而成。矩阵是如何存储旋转平移缩放变换的可参见《3D数学基础:图形与游戏开发》一书第7、8章节。如果想深入理解向量和矩阵,可以观看老外做的非常棒的视频《线性代数的本质》系列(中文字幕),地址:https://www.bilibili.com/video/av6731067/?p=1
2.V矩阵
模型顶点左乘M矩阵再乘V矩阵的效果:将该模型从世界坐标系下转换到视图坐标系(相机坐标系)。
V矩阵如何构建:通过相机在世界坐标系下的旋转平移缩放构建而成。但是需要注意:渲染管线中的真实用的V矩阵是相机世界矩阵的逆矩阵,也就是说,我们使用相机的旋转平移缩放构建的矩阵再计算出的逆矩阵才是渲染管线中的真实使用的那个V矩阵,为什么呢?在3D世界中,我们变换相机观察世界,和相机不动,将世界所有物体向相机变换的反方向变换,效果是等价的。
3.P矩阵
模型顶点左乘M矩阵再乘V矩阵再乘P矩阵的效果:将该模型从视图坐标系转换到投影坐标系。
P矩阵如何构建:P矩阵的构建需要相机的视椎体信息(left, left + width, top, top - height, near, far),具体怎么把矩阵算出来,就去找个Matrix的数学库看一眼吧,这里只说理论。
2.Object3D中的本地矩阵和世界矩阵
ThreeJS-Object3D中matrix和matrixWorld矩阵有时相同有时不同,这要看该对象是否拥有父物体。当该对象没有父物体的时候,matrix和matrixWorld是相同的;当该对象有父物体的时候,matrixWorld= 父物体的世界矩阵matrixWorld * matrix,注意:矩阵的左乘和右乘是不一样。
在ThreeJS渲染器类WebGLRenderer中的render方法中(相信用过ThreeJS的人都会知道),我们会传入scene和camera两个参数,该函数首先就会递归计算scene下所有对象和camera的matrix和matrixWorld两个矩阵,代码如下:
// 递归计算场景中所有对象的本地矩阵和世界矩阵
if (scene.autoUpdate === true) scene.updateMatrixWorld();
// 计算相机本地矩阵、相机世界矩阵、相机世界矩阵的逆矩阵(MVP的V)
if (camera.parent === null) camera.updateMatrixWorld();
updateMatrixWorld方法就在Object3D中,scene会递归所有的子物体都计算matrix和matrixWorld矩阵,camera在自己类中重写此方法,具体实现如下:
// 根据PRS计算对象Matrix
updateMatrix: function () {
this.matrix.compose( this.position, this.quaternion, this.scale );
this.matrixWorldNeedsUpdate = true;
},
// 在Render中,此处用于计算scene以及子物体和camera的matrix和matrixWorld
updateMatrixWorld: function ( force ) {
if ( this.matrixAutoUpdate ) this.updateMatrix();
if ( this.matrixWorldNeedsUpdate || force ) {
if ( this.parent === null ) {
this.matrixWorld.copy( this.matrix );
} else {
this.matrixWorld.multiplyMatrices( this.parent.matrixWorld, this.matrix );
}
this.matrixWorldNeedsUpdate = false;
force = true;
}
// update children
var children = this.children;
for ( var i = 0, l = children.length; i < l; i ++ ) {
children[ i ].updateMatrixWorld( force );
}
},
3.Object3D中lookAt方法的计算原理
lookAt是一个经常使用的接口,相机可以设置lookAt(瞅着目标点),物体也可以调用lookAt设置朝向(如上图),那么lookAt是如何计算出物体的姿态信息的呢?源码代码如下:
lookAt: function () {
// This method does not support objects having non-uniformly-scaled parent(s)
var q1 = new Quaternion();
var m1 = new Matrix4();
var target = new Vector3();
var position = new Vector3();
return function lookAt( x, y, z ) {
if ( x.isVector3 ) {
target.copy( x );
} else {
target.set( x, y, z );
}
var parent = this.parent;
this.updateWorldMatrix( true, false );
position.setFromMatrixPosition( this.matrixWorld );
if ( this.isCamera || this.isLight ) {
m1.lookAt( position, target, this.up );
} else {
m1.lookAt( target, position, this.up );
}
this.quaternion.setFromRotationMatrix( m1 );
if ( parent ) {
m1.extractRotation( parent.matrixWorld );
q1.setFromRotationMatrix( m1 );
this.quaternion.premultiply( q1.inverse() );
}
};
}()
Matrix4中的lookAt矩阵计算源码如下:
lookAt: function () {
var x = new Vector3();
var y = new Vector3();
var z = new Vector3();
return function lookAt( eye, target, up ) {
var te = this.elements;
z.subVectors( eye, target );
if ( z.lengthSq() === 0 ) {
// eye and target are in the same position
z.z = 1;
}
z.normalize();
x.crossVectors( up, z );
if ( x.lengthSq() === 0 ) {
// up and z are parallel
if ( Math.abs( up.z ) === 1 ) {
z.x += 0.0001;
} else {
z.z += 0.0001;
}
z.normalize();
x.crossVectors( up, z );
}
x.normalize();
y.crossVectors( z, x );
te[ 0 ] = x.x; te[ 4 ] = y.x; te[ 8 ] = z.x;
te[ 1 ] = x.y; te[ 5 ] = y.y; te[ 9 ] = z.y;
te[ 2 ] = x.z; te[ 6 ] = y.z; te[ 10 ] = z.z;
return this;
};
}(),
文末奉上自己整理的ThreeJS Render绘制流程图(还需完善,如有错误请指正,邮箱:1780721345@qq.com):