攝像機(jī)(Camera)
前面的教程中我們討論了觀察矩陣以及如何使用觀察矩陣移動(dòng)場景,。OpenGL本身沒有攝像機(jī)的概念,,但我們可以通過把場景中的所有物體往相反方向移動(dòng)的方式來模擬出攝像機(jī),這樣感覺就像我們?cè)谝苿?dòng),,而不是場景在移動(dòng)。 本節(jié)我們將會(huì)討論如何在OpenGL中模擬一個(gè)攝像機(jī),將會(huì)討論FPS風(fēng)格的可自由在3D場景中移動(dòng)的攝像機(jī)。我們也會(huì)討論鍵盤和鼠標(biāo)輸入,,最終完成一個(gè)自定義的攝像機(jī)類。 攝像機(jī)/觀察空間(Camera/View Space)當(dāng)我們討論攝像機(jī)/觀察空間的時(shí)候,,是我們?cè)谟懻撘詳z像機(jī)的透視圖作為場景原點(diǎn)時(shí)場景中所有可見頂點(diǎn)坐標(biāo),。觀察矩陣把所有的世界坐標(biāo)變換到觀察坐標(biāo),這些新坐標(biāo)是相對(duì)于攝像機(jī)的位置和方向的,。定義一個(gè)攝像機(jī),,我們需要一個(gè)攝像機(jī)在世界空間中的位置、觀察的方向,、一個(gè)指向它的右測的向量以及一個(gè)指向它上方的向量,。細(xì)心的讀者可能已經(jīng)注意到我們實(shí)際上創(chuàng)建了一個(gè)三個(gè)單位軸相互垂直的、以攝像機(jī)的位置為原點(diǎn)的坐標(biāo)系,。 1. 攝像機(jī)位置獲取攝像機(jī)位置很簡單,。攝像機(jī)位置簡單來說就是世界空間中代表攝像機(jī)位置的向量,。我們把攝像機(jī)位置設(shè)置為前面教程中的那個(gè)相同的位置:
Important 不要忘記正z軸是從屏幕指向你的,如果我們希望攝像機(jī)向后移動(dòng),,我們就往z軸正方向移動(dòng),。 2. 攝像機(jī)方向下一個(gè)需要的向量是攝像機(jī)的方向,比如它指向哪個(gè)方向?,F(xiàn)在我們讓攝像機(jī)指向場景原點(diǎn):(0, 0, 0),。用攝像機(jī)位置向量減去場景原點(diǎn)向量的結(jié)果就是攝像機(jī)指向向量,。由于我們知道攝像機(jī)指向z軸負(fù)方向,,我們希望方向向量指向攝像機(jī)的z軸正方向。如果我們改變相減的順序,,我們就會(huì)獲得一個(gè)指向攝像機(jī)正z軸方向的向量(譯注:注意看前面的那個(gè)圖,,所說的「方向向量/Direction Vector」是指向z的正方向的,而不是攝像機(jī)所注視的那個(gè)方向):
Attention 方向向量(Direction Vector)并不是最好的名字,,因?yàn)樗弥赶驈乃侥繕?biāo)向量的相反方向,。 3. 右軸(Right axis)我們需要的另一個(gè)向量是一個(gè)右向量(Right Vector),它代表攝像機(jī)空間的x軸的正方向,。為獲取右向量我們需要先使用一個(gè)小技巧:定義一個(gè)上向量(Up Vector),。我們把上向量和第二步得到的攝像機(jī)方向向量進(jìn)行叉乘。兩個(gè)向量叉乘的結(jié)果就是同時(shí)垂直于兩向量的向量,,因此我們會(huì)得到指向x軸正方向的那個(gè)向量(如果我們交換兩個(gè)向量的順序就會(huì)得到相反的指向x軸負(fù)方向的向量):
4. 上軸(Up axis)現(xiàn)在我們已經(jīng)有了x軸向量和z軸向量,,獲取攝像機(jī)的正y軸相對(duì)簡單;我們把右向量和方向向量(Direction Vector)進(jìn)行叉乘:
在叉乘和一些小技巧的幫助下,,我們創(chuàng)建了所有觀察/攝像機(jī)空間的向量,。對(duì)于想學(xué)到更多數(shù)學(xué)原理的讀者,提示一下,,在線性代數(shù)中這個(gè)處理叫做Gram-Schmidt(葛蘭—施密特)正交,。使用這些攝像機(jī)向量我們就可以創(chuàng)建一個(gè)LookAt矩陣了,它在創(chuàng)建攝像機(jī)的時(shí)候非常有用,。 Look At使用矩陣的好處之一是如果你定義了一個(gè)坐標(biāo)空間,,里面有3個(gè)相互垂直的軸,你可以用這三個(gè)軸外加一個(gè)平移向量來創(chuàng)建一個(gè)矩陣,,你可以用這個(gè)矩陣乘以任何向量來變換到那個(gè)坐標(biāo)空間,。這正是LookAt矩陣所做的,現(xiàn)在我們有了3個(gè)相互垂直的軸和一個(gè)定義攝像機(jī)空間的位置坐標(biāo),,我們可以創(chuàng)建我們自己的LookAt矩陣了: 是右向量,,是上向量,是方向向量是攝像機(jī)位置向量,。注意,,位置向量是相反的,,因?yàn)槲覀冏罱K希望把世界平移到與我們自身移動(dòng)的相反方向。使用這個(gè)LookAt矩陣坐標(biāo)觀察矩陣可以很高效地把所有世界坐標(biāo)變換為觀察坐標(biāo)LookAt矩陣就像它的名字表達(dá)的那樣:它會(huì)創(chuàng)建一個(gè)觀察矩陣looks at(看著)一個(gè)給定目標(biāo),。 幸運(yùn)的是,,GLM已經(jīng)提供了這些支持。我們要做的只是定義一個(gè)攝像機(jī)位置,,一個(gè)目標(biāo)位置和一個(gè)表示上向量的世界空間中的向量(我們使用上向量計(jì)算右向量),。接著GLM就會(huì)創(chuàng)建一個(gè)LookAt矩陣,我們可以把它當(dāng)作我們的觀察矩陣:
在開始做用戶輸入之前,,我們來做些有意思的事,,把我們的攝像機(jī)在場景中旋轉(zhuǎn)。我們的注視點(diǎn)保持在(0, 0, 0),。 我們?cè)诿恳粠紕?chuàng)建x和z坐標(biāo),,這要使用一點(diǎn)三角學(xué)知識(shí)。x和z表示一個(gè)在一個(gè)圓圈上的一點(diǎn),,我們會(huì)使用它作為攝像機(jī)的位置,。通過重復(fù)計(jì)算x和y坐標(biāo),遍歷所有圓圈上的點(diǎn),,這樣攝像機(jī)就會(huì)繞著場景旋轉(zhuǎn)了,。我們預(yù)先定義這個(gè)圓圈的半徑,使用
如果你運(yùn)行代碼你會(huì)得到下面的東西: 這一小段代碼中,攝像機(jī)圍繞場景轉(zhuǎn)動(dòng),。自己試試改變半徑和位置/方向參數(shù),,看看LookAt矩陣是如何工作的。同時(shí),,這里有源碼,、頂點(diǎn)和片段著色器。 自由移動(dòng)讓攝像機(jī)繞著場景轉(zhuǎn)很有趣,,但是讓我們自己移動(dòng)攝像機(jī)更有趣,!首先我們必須設(shè)置一個(gè)攝像機(jī)系統(tǒng),在我們的程序前面定義一些攝像機(jī)變量很有用:
LookAt函數(shù)現(xiàn)在成了:
我們首先設(shè)置之前定義的 我們已經(jīng)為GLFW的鍵盤輸入定義了一個(gè)
當(dāng)我們按下WASD鍵,攝像機(jī)的位置都會(huì)相應(yīng)更新,。如果我們希望向前或向后移動(dòng),,我們就把位置向量加上或減去方向向量。如果我們希望向旁邊移動(dòng),,我們做一個(gè)叉乘來創(chuàng)建一個(gè)右向量,,沿著它移動(dòng)就可以了。這樣就創(chuàng)建了類似使用攝像機(jī)橫向,、前后移動(dòng)的效果,。 Important 注意,我們對(duì)右向量進(jìn)行了標(biāo)準(zhǔn)化,。如果我們沒對(duì)這個(gè)向量進(jìn)行標(biāo)準(zhǔn)化,,最后的叉乘結(jié)果會(huì)根據(jù) 如果你用這段代碼更新 你可能會(huì)注意到這個(gè)攝像機(jī)系統(tǒng)不能同時(shí)朝兩個(gè)方向移動(dòng),,當(dāng)你按下一個(gè)按鍵時(shí),,它會(huì)先頓一下才開始移動(dòng)。這是因?yàn)榇蠖鄶?shù)事件輸入系統(tǒng)一次只能處理一個(gè)鍵盤輸入,,它們的函數(shù)只有當(dāng)我們激活了一個(gè)按鍵時(shí)才被調(diào)用,。大多數(shù)GUI系統(tǒng)都是這樣的,它對(duì)攝像機(jī)來說用并不合理,。我們可以用一些小技巧解決這個(gè)問題,。 這個(gè)技巧是只在回調(diào)函數(shù)中跟蹤哪個(gè)鍵被按下/釋放。在游戲循環(huán)中我們讀取這些值,,檢查那個(gè)按鍵被激活了,,然后做出相應(yīng)反應(yīng)。我們只儲(chǔ)存哪個(gè)鍵被按下/釋放的狀態(tài)信息,,在游戲循環(huán)中對(duì)狀態(tài)做出反應(yīng),,我們來創(chuàng)建一個(gè)布爾數(shù)組代表按下/釋放的鍵:
然后我們必須在
我們創(chuàng)建一個(gè)新的叫做
之前的代碼移動(dòng)到了 最后,,我們需要在游戲循環(huán)中添加新函數(shù)的調(diào)用:
至此,,你可以同時(shí)向多個(gè)方向移動(dòng)了,,并且當(dāng)你按下按鈕也會(huì)立刻運(yùn)動(dòng)了。如遇困難查看源碼,。 移動(dòng)速度目前我們的移動(dòng)速度是個(gè)常量,。看起來不錯(cuò),,但是實(shí)際情況下根據(jù)處理器的能力不同,,有的人在同一段時(shí)間內(nèi)會(huì)比其他人繪制更多幀。也就是調(diào)用了更多次 圖形和游戲應(yīng)用通常有回跟蹤一個(gè) 我們要用兩個(gè)全局變量來計(jì)算出
在每一幀中我們計(jì)算出新的
現(xiàn)在我們有了
與前面的部分結(jié)合在一起,,我們有了一個(gè)更流暢點(diǎn)的攝像機(jī)系統(tǒng): 現(xiàn)在我們有了一個(gè)在任何系統(tǒng)上移動(dòng)速度都一樣的攝像機(jī),。這里是源碼。我們可以看到任何移動(dòng)都會(huì)影響返回的 自由觀看只用鍵盤移動(dòng)沒什么意思,。特別是我們還不能轉(zhuǎn)向。是時(shí)候使用鼠標(biāo)了,! 為了能夠改變方向,,我們必須根據(jù)鼠標(biāo)的輸入改變 歐拉角歐拉角是表示3D空間中可以表示任何旋轉(zhuǎn)的三個(gè)值,,由萊昂哈德·歐拉在18世紀(jì)提出。有三種歐拉角:俯仰角(Pitch),、偏航角(Yaw)和滾轉(zhuǎn)角(Roll),,下面的圖片展示了它們的含義: 俯仰角是描述我們?nèi)绾瓮虾屯驴吹慕牵诘谝粡垐D中表示,。第二張圖顯示了偏航角,,偏航角表示我們往左和往右看的大小。滾轉(zhuǎn)角代表我們?nèi)绾畏瓭L攝像機(jī),。每個(gè)歐拉角都有一個(gè)值來表示,,把三個(gè)角結(jié)合起來我們就能夠計(jì)算3D空間中任何的旋轉(zhuǎn)了。 對(duì)于我們的攝像機(jī)系統(tǒng)來說,,我們只關(guān)心俯仰角和偏航角,,所以我們不會(huì)討論滾轉(zhuǎn)角。用一個(gè)給定的俯仰角和偏航角,,我們可以把它們轉(zhuǎn)換為一個(gè)代表新的方向向量的3D向量,。俯仰角和偏航角轉(zhuǎn)換為方向向量的處理需要一些三角學(xué)知識(shí),我們以最基本的情況開始: 如果我們把斜邊邊長定義為1,,我們就能知道鄰邊的長度是,,它的對(duì)邊是,。這樣我們獲得了能夠得到x和y方向的長度的公式,,它們?nèi)Q于所給的角度。我們使用它來計(jì)算方向向量的元素: 這個(gè)三角形看起來和前面的三角形很像,,所以如果我們想象自己在xz平面上,,正望向y軸,我們可以基于第一個(gè)三角形計(jì)算長度/y方向的強(qiáng)度(我們往上或往下看多少),。從圖中我們可以看到一個(gè)給定俯仰角的y值等于sinθ:
這里我們只更新了y值,,仔細(xì)觀察x和z元素也被影響了。從三角形中我們可以看到它們的值等于:
看看我們是否能夠?yàn)槠浇钦业叫枰脑兀?/p> 就像俯仰角一樣我們可以看到x元素取決于cos(偏航角)的值,,z值同樣取決于偏航角的正弦值,。把這個(gè)加到前面的值中,會(huì)得到基于俯仰角和偏航角的方向向量: Important 譯注:這里的球坐標(biāo)與笛卡爾坐標(biāo)的轉(zhuǎn)換把x和z弄反了,,如果你去看最后的源碼,,會(huì)發(fā)現(xiàn)作者在攝像機(jī)源碼那里寫了
這樣我們就有了一個(gè)可以把俯仰角和偏航角轉(zhuǎn)化為用來自由旋轉(zhuǎn)的攝像機(jī)的3個(gè)維度的方向向量了,。你可能會(huì)奇怪:我們?cè)趺吹玫礁┭鼋呛推浇牵?/p> 鼠標(biāo)輸入偏航角和俯仰角是從鼠標(biāo)移動(dòng)獲得的,鼠標(biāo)水平移動(dòng)影響偏航角,,鼠標(biāo)垂直移動(dòng)影響俯仰角,。它的思想是儲(chǔ)存上一幀鼠標(biāo)的位置,在當(dāng)前幀中我們當(dāng)前計(jì)算鼠標(biāo)位置和上一幀的位置相差多少,。如果差別越大那么俯仰角或偏航角就改變?cè)酱蟆?/p> 首先我們要告訴GLFW,,應(yīng)該隱藏光標(biāo),并捕捉(Capture)它,。捕捉鼠標(biāo)意味著當(dāng)應(yīng)用集中焦點(diǎn)到鼠標(biāo)上的時(shí)候光標(biāo)就應(yīng)該留在窗口中(除非應(yīng)用拾取焦點(diǎn)或退出),。我們可以進(jìn)行簡單的配置:
這個(gè)函數(shù)調(diào)用后,無論我們?cè)趺慈ヒ苿?dòng)鼠標(biāo),,它都不會(huì)顯示了,,也不會(huì)離開窗口。對(duì)于FPS攝像機(jī)系統(tǒng)來說很好: 為計(jì)算俯仰角和偏航角我們需要告訴GLFW監(jiān)聽鼠標(biāo)移動(dòng)事件,。我們用下面的原型創(chuàng)建一個(gè)回調(diào)函數(shù)來做這件事(和鍵盤輸入差不多):
這里的
在處理FPS風(fēng)格的攝像機(jī)鼠標(biāo)輸入的時(shí)候,,我們必須在獲取最終的方向向量之前做下面這幾步:
第一步計(jì)算鼠標(biāo)自上一幀的偏移量。我們必須先儲(chǔ)存上一幀的鼠標(biāo)位置,,我們把它的初始值設(shè)置為屏幕的中心(屏幕的尺寸是800乘600):
然后在回調(diào)函數(shù)中我們計(jì)算當(dāng)前幀和上一幀鼠標(biāo)位置的偏移量:
注意我們把偏移量乘以了 下面我們把偏移量加到全局變量
第三步我們給攝像機(jī)添加一些限制,這樣攝像機(jī)就不會(huì)發(fā)生奇怪的移動(dòng)了,。對(duì)于俯仰角,,要讓用戶不能看向高于89度(90度時(shí)視角會(huì)逆轉(zhuǎn),所以我們把89度作為極限)的地方,,同樣也不允許小于-89度,。這樣能夠保證用戶只能看到天空或腳下但是不能更進(jìn)一步超越過去。限制可以這樣做:
注意我們沒有給偏航角設(shè)置限制是因?yàn)槲覀儾幌M拗朴脩舻乃叫D(zhuǎn),。然而,,給偏航角設(shè)置限制也很容易,只要你愿意,。 第四也是最后一步,,就是通過俯仰角和偏航角來計(jì)算以得到前面提到的實(shí)際方向向量:
這回計(jì)算出方向向量,根據(jù)鼠標(biāo)點(diǎn)的移動(dòng)它包含所有的旋轉(zhuǎn),。由于 如果你現(xiàn)在運(yùn)行代碼,,你會(huì)發(fā)現(xiàn)當(dāng)程序運(yùn)行第一次捕捉到鼠標(biāo)的時(shí)候攝像機(jī)會(huì)突然跳一下,。原因是當(dāng)你的鼠標(biāo)進(jìn)入窗口鼠標(biāo)回調(diào)函數(shù)會(huì)使用這時(shí)的
最后的代碼應(yīng)該是這樣的:
現(xiàn)在我們可以自由的在3D場景中移動(dòng)了!如果你遇到困難,,這是源碼,。 縮放我們還要往攝像機(jī)系統(tǒng)里加點(diǎn)東西,實(shí)現(xiàn)一個(gè)縮放接口,。前面教程中我們說視野(Field of View或fov)定義了我們可以看到場景中多大的范圍,。當(dāng)視野變小時(shí)可視區(qū)域就會(huì)減小,產(chǎn)生放大了的感覺,。我們用鼠標(biāo)滾輪來放大,。和鼠標(biāo)移動(dòng)、鍵盤輸入一樣我們需要一個(gè)鼠標(biāo)滾輪的回調(diào)函數(shù):
我們現(xiàn)在在每一幀都必須把透視投影矩陣上傳到GPU,,但這一次使
最后不要忘記注冊(cè)滾動(dòng)回調(diào)函數(shù):
現(xiàn)在我們實(shí)現(xiàn)了一個(gè)簡單的攝像機(jī)系統(tǒng),它能夠讓我們?cè)?D環(huán)境中自由移動(dòng),。 自由的去實(shí)驗(yàn),,如果遇到困難對(duì)比源代碼。 Important 注意,,使用歐拉角作為攝像機(jī)系統(tǒng)并不完美,。你仍然可能遇到萬向節(jié)死鎖。最好的攝像機(jī)系統(tǒng)是使用四元數(shù)的,,后面會(huì)有討論,。 攝像機(jī)類接下來的教程我們會(huì)使用一個(gè)攝像機(jī)來瀏覽場景,從各個(gè)角度觀察結(jié)果,。然而由于一個(gè)攝像機(jī)會(huì)占教程的很大的篇幅,,我們會(huì)從細(xì)節(jié)抽象出創(chuàng)建一個(gè)自己的攝像機(jī)對(duì)象。與著色器教程不同我們不會(huì)帶你一步一步創(chuàng)建攝像機(jī)類,,如果你想知道怎么工作的的話,,只會(huì)給你提供一個(gè)(有完整注釋的)源碼。 像著色器對(duì)象一樣,,我們把攝像機(jī)類寫在一個(gè)單獨(dú)的頭文件中,。你可以在這里找到它。你應(yīng)該能夠理解所有的代碼,。我們建議您至少看一看這個(gè)類,,看看如何創(chuàng)建一個(gè)自己的攝像機(jī)類。 Attention 我們介紹的歐拉角FPS風(fēng)格攝像機(jī)系統(tǒng)能夠滿足大多數(shù)情況需要,,但是在創(chuàng)建不同的攝像機(jī)系統(tǒng),,比如飛行模擬就要當(dāng)心。每個(gè)攝像機(jī)系統(tǒng)都有自己的有點(diǎn)和不足,,所以確保對(duì)它們進(jìn)行了詳細(xì)研究,。比如,這個(gè)FPS攝像機(jī)不允許俯仰角大于90度,,由于使用了固定的上向量(0, 1, 0),,我們就不能用滾轉(zhuǎn)角。 使用新的攝像機(jī)對(duì)象的更新后的版本源碼可以在這里找到,。(譯注:總而言之這個(gè)攝像機(jī)實(shí)現(xiàn)并不十分完美,,你可以看看最終的源碼,。建議先看這篇文章,對(duì)旋轉(zhuǎn)有更深的理解后,,你就能做出更好的攝像機(jī)類,,不過本文有些內(nèi)容比如如何防止按鍵停頓和glfw鼠標(biāo)事件實(shí)現(xiàn)攝像機(jī)的注意事項(xiàng)比較重要,其它的就要做一定的取舍了) 練習(xí) |
|