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繪製流程圖(還需完善,如有錯誤請指正,郵箱:[email protected]):