Win32 OpenGL編程(7) 3D視圖變換——真3D的關鍵
write by 九天雁翎(JTianLing) -- blog.csdn.net/vagrxie
提要
本文從照相機比喻開始,引入視圖變換的概念,然后講解了視圖變換的關鍵函數并用例子演示了參數變化時圖形顯示效果響應的動態變化,然后再補充了3D圖形繪制時的多邊形正面背面識別及處理。可算作是真正的3D入門。
照相機比喻
在《 OpenGL Programming Guide 》中將所有的3D變換統一到一個有意思的現實世界模型,照相機比喻。
1.確定照相機的位置的過程對應于“視圖變換”(Viewing Transformations)
2.確定物體位置的過程對應于“模型變換”(Modeling Transformations)
3.確定照相機放大倍數的過程對應于“投影變換”(Projection Transformations)
4.確定照片大小的過程對應于“視口變換”(Viewport Transformations)
實際的照相過程遵循這個過程,在我們處理3D圖形的時候也遵循這個過程,其中,上述的4個變換就是我今天準備介紹的。
視圖變換——確定視角
現實生活中的物體,從不同的角度觀察,我們看到的東西是不一樣的,這還引發了歷史上著名的“金銀盾事件”(實際上是寓言-_-!),從一面看,是金盾,一面看是銀盾,從中間看是一面金,一面銀。。。。。。
以《 Win32 OpenGL編程(6) 踏入3D世界 》(以后簡稱XO6,該系列文章類似)一例中最后最復雜的三角錐為例,此例中是三角錐本身在旋轉,我們的觀察角度并沒有變。現在我們反過來,三角錐不動,我們自己移動自己的位置,看看三角錐不同方向的樣子。現實生活中你看雕塑可不是總能讓別人扛著雕塑旋轉吧-_-!這時候,總得自己走動走動。這個時候,我們變化的是觀察的角度,但是看到的確是雕塑不同的側面。見下例,我們可以從不同的觀察角度來觀察這個簡單的三角錐。見下例:
// 觀察者位置
GLfloat gViewPosX;
GLfloat gViewPosY;
GLfloat gViewPosZ = 1.0;
// 觀察者視角方向
GLfloat gViewDirX = 0.0;
GLfloat gViewDirY = 0.0;
GLfloat gViewDirZ = 0.0;
GLfloat gViewUpDirX = 0.0;
GLfloat gViewUpDirY = 1.0;
GLfloat gViewUpDirZ = 0.0;
// 是改變位置還是視角
bool gbChangePos = true;
//這里進行所有的繪圖工作
void SceneShow(GLvoid)
{
glClear(GL_COLOR_BUFFER_BIT);
glColor3f(1.0, 0.0, 0.0);
glPushMatrix();
DrawSmoothColorPyramid(0.5);
glPopMatrix();
glLoadIdentity();
gluLookAt(gViewPosX, gViewPosY, gViewPosZ, gViewDirX, gViewDirY, gViewDirZ, gViewUpDirX, gViewUpDirY, gViewUpDirZ);
glFlush();
}
///////////////////////////////////////////////////////////
int Game_Main( void *parms = NULL, int num_parms = 0)
{
DWORD dwStartTime;
dwStartTime = GetTickCount();
// this is the main loop of the game, do all your processing
// here
// for now test if user is hitting ESC and send WM_CLOSE
if (KEYDOWN(VK_ESCAPE))
SendMessage(ghWnd,WM_CLOSE,0,0);
if (gbChangePos)
{
if (KEYDOWN(VK_UP))
{
gViewPosY += 0.01;
gViewPosZ = sqrt( 1.0 - gViewPosY * gViewPosY);
}
if (KEYDOWN(VK_DOWN))
{
gViewPosY -= 0.01;
gViewPosZ = sqrt( 1.0 - gViewPosY * gViewPosY);
}
if (KEYDOWN(VK_LEFT))
{
gViewPosX += 0.01;
gViewPosZ = sqrt( 1.0 - gViewPosX * gViewPosX);
}
if (KEYDOWN(VK_RIGHT))
{
gViewPosX -= 0.01;
gViewPosZ = sqrt( 1.0 - gViewPosX * gViewPosX);
}
}
else
{
if (KEYDOWN(VK_UP))
{
gViewDirY += 0.01;
}
if (KEYDOWN(VK_DOWN))
{
gViewDirY -= 0.01;
}
if (KEYDOWN(VK_LEFT))
{
gViewDirX += 0.01;
}
if (KEYDOWN(VK_RIGHT))
{
gViewDirX -= 0.01;
}
}
if (KEYDOWN(VK_NUMPAD8))
{
gViewUpDirY += 0.01;
}
if (KEYDOWN(VK_NUMPAD2))
{
gViewUpDirY -= 0.01;
}
if (KEYDOWN(VK_NUMPAD6))
{
gViewUpDirX += 0.01;
}
if (KEYDOWN(VK_NUMPAD4))
{
gViewUpDirX -= 0.01;
}
SceneShow();
// 控制幀率
while (GetTickCount() - dwStartTime < TIME_IN_FRAME)
{
Sleep(1);
}
// return success or failure or your own return code here
return (1);
} // end Game_Main
gluLookAt — define a viewing transformation
C Specification
void gluLookAt( GLdouble eyeX,
GLdouble eyeY,
GLdouble eyeZ,
GLdouble centerX,
GLdouble centerY,
GLdouble centerZ,
GLdouble upX,
GLdouble upY,
GLdouble upZ);
ParameterseyeX, eyeY, eyeZ
Specifies the position of the eye point.
centerX, centerY, centerZSpecifies the position of the reference point.
upX, upY, upZSpecifies the direction of the up vector.
此例中,通過全局變量保存觀察者的位置,并通過上下左右鍵改變方向,以從不同的角度觀察三角錐,作為演示,我開始時鎖定了觀察的方向,一直是朝向觀察的三角錐的,并且通過計算,讓觀察者總是保持與觀察物品的距離相等,這樣最能看到觀察位置改變帶來的效果,但是目前只實現了在物體正面時的方向改變,當越過正面后,由于象限的改變,實際上增加的數值需要編程減小,然后將另外一個輔助變量的正負號更改。比如,當一直向上按時,Y坐標不停增長,但是到Y=1.0的時候就需要變成減小了,Z的坐標也變成了負值,因為此時已經到了物體的后方。
當按下INSERT鍵后,再按上下左右改變的就是看哪里的方向了,嘗試一下會發現當觀察方向向右時,物體向左移動,直到移出屏幕以外,這點很像你開始面對著屏幕,然后不停的將頭向右偏,那么屏幕也就向左移動,你頭偏的角度足夠大的時候,已經就看不見屏幕了。
而當通過小鍵盤的8426方向控制時,改變的是觀察的向上的角度,就像你看屏幕時偏著腦袋看一樣,具體的演示效果就大家自己去看了,這里提供一個截圖
還是那句老話,為節省篇幅僅貼出關鍵片段,完整源代碼見我博客源代碼的2009-10-25/glViewingTrans 目錄,獲取方式見文章最后關于獲取博客完整源代碼的說明。
此例強烈建議通過運行程序去感受一下,也算是體會在游戲中3D的轉換是怎么回事兒,其實也就是這么回事兒。既然都已經是3D圖形了,那為什么有的游戲要限制你的觀察角度呢?還冒出了2.5D,2.8D等新鮮詞匯(其實我也不太懂怎么命名的),但是學了視圖變換后就會明白,限制角度對程序處理的簡化作用了,就像我此處一樣,雖然已經是個真正的3D三角錐了,我還是沒有提供720度的隨意旋轉,因為當移動到物體的另一面時,處理的方式有些改變,老是變來變去會比較麻煩,要知道,2D平面有4個象限,而3D呢?8個象限,可不少了,每個象限在視圖變換時的計算方式可是不太一樣的。我的程序因此而簡化了很多,而那些2.XD的游戲程序自然簡化的更多羅。
多邊形表面顯示方式
在確定了視角后,會發現原來的三角錐有問題了,背面都顯示出來了-_-!見上面的截圖,這個可是不行的,問題出在我們一沒有告訴OpenGL那個面是背面,那個面是正面,而且OpenGL默認的現實方式是不管正面背面一樣處理,自然我們得告訴OpenGL按照我們想要處理的方式處理才行,比如,我們想要正面顯示,背面不顯示(這是多么正常的需求啊),那么我們可以通過glPolygonMode函數指明。
glPolygonMode — select a polygon rasterization mode
C Specification
void glPolygonMode( GLenum face,
GLenum mode);
Parametersface
Specifies the polygons that mode applies to.
Must be
GL_FRONT for front-facing polygons,
GL_BACK for back-facing polygons,
or GL_FRONT_AND_BACK for front- and back-facing polygons.
modeSpecifies how polygons will be rasterized.
Accepted values are
GL_POINT,
GL_LINE, and
GL_FILL.
The initial value is GL_FILL for both front- and back-facing polygons.
要達到我們的要求只需要這樣調用此函數:
glPolygonMode(GL_FRONT, GL_FILL);
glPolygonMode(GL_BACK, GL_LINE);
這樣指示以后,背面就不會填充了,但是,OpenGL怎么知道哪個是正面,哪個是背面呢?通過glFrontFace可以指定,
glFrontFace — define front- and back-facing polygons
C Specification
void glFrontFace( GLenum mode);
Parametersmode
Specifies the orientation of front-facing polygons.
GL_CW and GL_CCW are accepted.
The initial value is GL_CCW.
OpenGL自然不是傻到要你具體一個一個面去指定哪個面是正面,哪個面是方面,它是通過頂點繪制的方向來決定的,OpenGL通過此函數指定了什么方向時多邊形表示正反面,默認是是逆時針為正面,順時針為方面,想想這樣的好處,指定起來還是方便點,繪制頂點的時候考慮好就行了,不用額外再次指定,然后,的確還有效,正面看的是逆時針的東西,到了反面還真是順時針-_-!(廢話)
如下例所示:
void DrawSmoothColorPyramid(GLfloat adSize)
{
static GLfloat fPyramidDatas[] = { 0.0, 1.0, 0.0, // 三角錐上頂點
-1.0, 0.0, 1.0, // 底面左前頂點
1.0, 0.0, 1.0, // 底面右前下頂點
0.0, 0.0, -1.0}; // 底面后下頂點
GLfloat fPyramidSizeDatas[ sizeof (fPyramidDatas)/ sizeof (GLfloat)] = {0};
// 計算大小
for ( int i = 0; i < 12; ++i)
{
fPyramidSizeDatas[i] = fPyramidDatas[i] * adSize;
}
static GLfloat fPyramidColors[] = { 0.0, 0.0, 0.0,
1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
0.0, 0.0, 1.0};
//static GLubyte ubyIndices[] = { 0, 1, 2, // 正面
// 0, 1, 3, // 左側面
// 0, 2, 3, // 右側面
// 1, 2, 3}; // 底面
static GLubyte ubyIndices[] = { 0, 1, 2, // 正面
0, 3, 1, // 左側面
0, 2, 3, // 右側面
1, 3, 2}; // 底面
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);
glVertexPointer(3, GL_FLOAT, 0, fPyramidSizeDatas);
glColorPointer(3, GL_FLOAT, 0, fPyramidColors);
glPolygonMode(GL_FRONT, GL_FILL);
glPolygonMode(GL_BACK, GL_LINE);
for ( int i = 0; i < 4; ++i)
{
glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_BYTE, ubyIndices+i*3);
}
}
為節省篇幅僅貼出關鍵片段,完整源代碼見我博客源代碼的2009-10-25/glPolygonFace 目錄,獲取方式見文章最后關于獲取博客完整源代碼的說明。
多邊形表面剔除
另外,事實上,我們這里是指定了背面繪制輪廓(即GL_LINE),但是既然是背面,為啥我們一定要輪廓呢?完全可以剔除之,glCullFace就是干這個的。
glCullFace — specify whether front- or back-facing facets can be culled
C Specification
void glCullFace( GLenum mode);
Parametersmode
Specifies whether front- or back-facing facets are candidates for culling.
Symbolic constants
GL_FRONT, GL_BACK, and GL_FRONT_AND_BACK are accepted.
The initial value is GL_BACK.
既然背面都指定好了,此函數使用起來就非常簡單了,無非就是調用glCullFace(GL_BACK)表示剔除背面的多邊形(雖然你可以剔除正面或者全部都剔除了),并且,需要注意的是,剔除時需要用glEnable(GL_CULL_FACE)開啟剔除功能。為了速度,像這種功能OpenGL一般都是默認關閉的,我們也習慣了。需要添加的代碼實在也就簡單了:
//OpenGL初始化開始
void SceneInit( int w, int h)
{
GLenum err = glewInit();
if (err != GLEW_OK)
{
MessageBox(NULL, _T( "Error" ), _T( "Glew init failed." ), MB_OK);
exit(-1);
}
glClearColor(0.0, 0.0, 0.0, 0.0);
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
}
但是,需要說明的是,背面剔除后,在轉換視角,到背面變成正面的那一瞬間,會需要重新渲染背面,此時效率較低,事實上,這么一個簡單的程序,再沒有開啟背面剔除時,從正面到背面的轉換非常流暢,開啟后,轉換會有明顯的小小遲緩,但是好處就是背面沒有需要顯示的時候是不占用資源的了,孰優孰劣,開啟與否,那就只能看情況把握了。
為節省篇幅僅貼出關鍵片段,完整源代碼見我博客源代碼的2009-10-25/glCullFace 目錄,獲取方式見文章最后關于獲取博客完整源代碼的說明。
參考資料
1. 《 OpenGL Reference Manual 》,OpenGL參考手冊
2. 《OpenGL 編程指南》(《 OpenGL Programming Guide 》),Dave Shreiner,Mason Woo,Jackie Neider,Tom Davis 著,徐波譯,機械工業出版社
3. 《Nehe OpenGL Tutorials》,Nehe著,在 http://nehe.gamedev.net/ 上可以找到教程及相關的代碼下載,(有PDF版本教程下載)Nehe自己還做了一個面向對象的框架,作為演示程序來說,這樣的框架非常合適。也有 中文版 ,各取所需吧。
4. 《OpenGL入門學習》 ,eastcowboy著,這是我在網上找到的一個比較好的教程,較為完善,而且非常通俗。這是第一篇的地址: http://bbs.pfan.cn/post-184355.html
本系列下一篇《 Win32 OpenGL編程(8) 3D模型變換及其組合應用 》
本OpenGL系列其他文章
1. 《 Win32 OpenGL 編程(1)Win32下的OpenGL編程必須步驟 》
2. 《 Win32 OpenGL編程(2) 尋找缺失的OpenGL函數 》
3. 《 Win32 OpenGL編程(3) 基本圖元(點,直線,多邊形)的繪制 》
4. 《 Win32 OpenGL編程(4) 2D圖形基礎(顏色及坐標體系進階知識) 》
5. 《 Win32 OpenGL編程(5)頂點數組詳細介紹 》
6.《 Win32 OpenGL編程(6) 踏入3D世界 》
應用舉例:《 Win32 OpenGL編程系列 2D例子 -- 七巧板圖形繪制 》
完整源代碼獲取說明
由于篇幅限制,本文一般僅貼出代碼的主要關心的部分,代碼帶工程(或者makefile)完整版(如果有的話)都能用Mercurial在Google Code中下載。文章以博文發表的日期分目錄存放,請直接使用Mercurial克隆下庫:
https://blog-sample-code.jtianling.googlecode.com/hg/
Mercurial使用方法見《 分布式的,新一代版本控制系統Mercurial的介紹及簡要入門 》
要是僅僅想瀏覽全部代碼也可以直接到google code上去看,在下面的地址:
http://code.google.com/p/jtianling/source/browse?repo=blog-sample-code
原創文章作者保留版權 轉載請注明原作者 并給出鏈接
write by 九天雁翎(JTianLing) -- blog.csdn.net/vagrxie
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
