一、從問題切入
先看代碼(省略無用部分):
//模型視圖棧
GLMatrixStack modelViewMatrix;
//投影棧
GLMatrixStack projectionMatrix;
//觀察者矩陣
GLFrame cameraFrame;
//模型矩陣
GLFrame objectFrame;
//平截頭體
GLFrustum viewFrustum;
//幾何變換的管道
GLGeometryTransform transformPipeline;
void SetupRC(){
//變換管道綁定模型視圖棧與投影棧
transformPipeline.SetMatrixStacks(modelViewMatrix, projectionMatrix);
}
void ChangeSize(int w, int h)
{
...
//創(chuàng)建投影矩陣,并將它載入投影矩陣堆棧中
viewFrustum.SetPerspective(35.0f, float(w) / float(h), 1.0f, 500.0f);
//獲取投影矩陣,放入投影棧
projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix());
//調(diào)用頂部載入單元矩陣
modelViewMatrix.LoadIdentity();
...
}
// 召喚場景
void RenderScene(void)
{
...
//壓棧
modelViewMatrix.PushMatrix();
M3DMatrix44f mCamera;
cameraFrame.GetCameraMatrix(mCamera);
//矩陣乘以矩陣堆棧的頂部矩陣,相乘的結(jié)果隨后簡存儲在堆棧的頂部
modelViewMatrix.MultMatrix(mCamera);
M3DMatrix44f mObjectFrame;
//只要使用 GetMatrix 函數(shù)就可以獲取矩陣堆棧頂部的值,這個函數(shù)可以進(jìn)行2次重載。用來使用GLShaderManager 的使用。或者是獲取頂部矩陣的頂點(diǎn)副本數(shù)據(jù)
objectFrame.GetMatrix(mObjectFrame);
//矩陣乘以矩陣堆棧的頂部矩陣,相乘的結(jié)果隨后簡存儲在堆棧的頂部
modelViewMatrix.MultMatrix(mObjectFrame);
/* GLShaderManager 中的Uniform 值——平面著色器
參數(shù)1:平面著色器
參數(shù)2:運(yùn)行為幾何圖形變換指定一個 4 * 4變換矩陣
--transformPipeline.GetModelViewProjectionMatrix() 獲取的
GetMatrix函數(shù)就可以獲得矩陣堆棧頂部的值
參數(shù)3:顏色值(黑色)
*/
shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vBlack);
...
//還原到以前的模型視圖矩陣(單位矩陣)
modelViewMatrix.PopMatrix();
...
}
...
從上面的代碼中,我們看到,在RenderScene函數(shù)中,模型視圖棧首先乘以mCamera(觀察者矩陣),然后,我們再是乘以mObjectFrame(模型矩陣),最后,使用transformPipeline.GetModelViewProjectionMatrix()方法,我們用投影矩陣乘以模型視圖矩陣,將最后結(jié)果返回。
也就是如下順序:
projection * view * model
那么,我們?yōu)槭裁匆凑者@個順序來添加矩陣呢?
這要從兩個方面講起,一個是看這幾個方法的實(shí)現(xiàn),還有一個就是坐標(biāo)變換。
二、實(shí)現(xiàn)
首先是矩陣相乘的實(shí)現(xiàn)
inline void MultMatrix(const M3DMatrix44f mMatrix) {
M3DMatrix44f mTemp;
m3dCopyMatrix44(mTemp, pStack[stackPointer]);
m3dMatrixMultiply44(pStack[stackPointer], mTemp, mMatrix);
}
從實(shí)現(xiàn)可以看出,調(diào)用這個方法就是將棧頂?shù)木仃嚾〕?,乘以參?shù)的矩陣,再將相乘結(jié)果存放在原來的棧頂。
我們在兩個地方依次調(diào)用了這個方法
//1.矩陣乘以模型視圖棧的頂部矩陣,相乘的結(jié)果隨后簡存儲在堆棧的頂部
modelViewMatrix.MultMatrix(mCamera);
//2.矩陣乘以模型視圖棧的頂部矩陣,相乘的結(jié)果隨后簡存儲在堆棧的頂部
modelViewMatrix.MultMatrix(mObjectFrame);
按照實(shí)現(xiàn),這兩部合并就是將mCamera * mObjectFrame的結(jié)果存放在模型視圖矩陣的棧頂。
再看transformPipeline.GetModelViewProjectionMatrix()的實(shí)現(xiàn):
const M3DMatrix44f& GetModelViewProjectionMatrix(void)
{
m3dMatrixMultiply44(_mModelViewProjection, _mProjection->GetMatrix(), _mModelView->GetMatrix());
return _mModelViewProjection;
}
這個的實(shí)現(xiàn)也很簡單,就是返回投影矩陣 * 模型視圖矩陣的結(jié)果,綜合上面的分析,也就是:
projection * view * model
順序是如此,下面我們從坐標(biāo)變換來講解為什么是這個順序
三、坐標(biāo)變換

如圖所示,為了將坐標(biāo)從一個坐標(biāo)系變換到另一個坐標(biāo)系,我們需要用到幾個變換矩陣,最重要的幾個分別是模型(Model)、觀察(View)、投影(Projection)三個矩陣。我們的頂點(diǎn)坐標(biāo)起始于局部空間(Local Space),在這里它稱為局部坐標(biāo)(Local Coordinate),它在之后會變?yōu)槭澜缱鴺?biāo)(World Coordinate),觀察坐標(biāo)(View Coordinate),裁剪坐標(biāo)(Clip Coordinate),并最后以屏幕坐標(biāo)(Screen Coordinate)的形式結(jié)束。它的流程如下:
- 局部坐標(biāo)是對象相對于局部原點(diǎn)的坐標(biāo),也是物體起始的坐標(biāo)。
- 下一步是將局部坐標(biāo)變換為世界空間坐標(biāo),世界空間坐標(biāo)是處于一個更大的空間范圍的。這些坐標(biāo)相對于世界的全局原點(diǎn),它們會和其它物體一起相對于世界的原點(diǎn)進(jìn)行擺放。
- 接下來我們將世界坐標(biāo)變換為觀察空間坐標(biāo),使得每個坐標(biāo)都是從攝像機(jī)或者說觀察者的角度進(jìn)行觀察的。
- 坐標(biāo)到達(dá)觀察空間之后,我們需要將其投影到裁剪坐標(biāo)。裁剪坐標(biāo)會被處理至-1.0到1.0的范圍內(nèi),并判斷哪些頂點(diǎn)將會出現(xiàn)在屏幕上。
- 最后,我們將裁剪坐標(biāo)變換為屏幕坐標(biāo),我們將使用一個叫做視口變換(Viewport Transform)的過程。視口變換將位于-1.0到1.0范圍的坐標(biāo)變換到由glViewport函數(shù)所定義的坐標(biāo)范圍內(nèi)。最后變換出來的坐標(biāo)將會送到光柵器,將其轉(zhuǎn)化為片段。
我們?yōu)樯厦娴牟襟E創(chuàng)建了三個矩陣:模型矩陣、觀察矩陣和投影矩陣。
一個頂點(diǎn)坐標(biāo)將會根據(jù)以下過程被變換到剪裁坐標(biāo):
Vclip=Mprojection?Mview?Mmodel?Vlocal
注意矩陣運(yùn)算的順序是相反的(我們需要從右往左閱讀矩陣的乘法)。最后的頂點(diǎn)應(yīng)該被復(fù)制到頂點(diǎn)著色器的內(nèi)建變量gl_position,OpenGL將會自動進(jìn)行透視除法和裁剪。
根據(jù)這個過程,我們就可以回答一開始的問題,為什么OpenGL中需要先加入camera(也就是view),再加入model,最后是projection