OpenGL本身沒有攝像機的概念共虑,但我們可以通過把場景中的所有物體往相反方向移動的方式來模擬出攝像機,這樣感覺就像我們在移動,而不是場景在移動癞蚕。
本節(jié)我們將會討論如何在OpenGL中模擬一個攝像機,將會討論FPS風格的可自由在3D場景中移動的攝像機辉哥。我們也會討論鍵盤和鼠標輸入桦山,最終完成一個自定義的攝像機類。
攝像機/觀察空間(Camera/View Space)
當我們討論攝像機/觀察空間的時候醋旦,是我們在討論以攝像機的透視圖作為場景原點時場景中所有可見頂點坐標恒水。觀察矩陣把所有的世界坐標變換到觀察坐標,這些新坐標是相對于攝像機的位置和方向的饲齐。定義一個攝像機钉凌,我們需要一個攝像機在世界空間中的位置、觀察的方向捂人、一個指向它的右測的向量以及一個指向它上方的向量御雕。細心的讀者可能已經(jīng)注意到我們實際上創(chuàng)建了一個三個單位軸相互垂直的矢沿、以攝像機的位置為原點的坐標系。
1. 攝像機位置
攝像機位置簡單來說就是世界空間中指向攝像機位置的向量酸纲。我們把攝像機位置設置為前面教程中的那個相同的位置:
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
不要忘記正z軸是從屏幕指向你的捣鲸,如果我們希望攝像機向后移動,我們就往z軸正方向移動闽坡。
2. 攝像機方向
下一個需要的向量是攝像機的方向栽惶,比如它指向哪個方向。現(xiàn)在我們讓攝像機指向場景原點:(0, 0, 0)疾嗅。用場景原點向量減去攝像機位置向量的結果就是攝像機指向向量(此時指向z軸負方向)外厂。由于我們知道攝像機指向z軸負方向,我們希望方向向量指向攝像機的z軸正方向宪迟。如果我們改變相減的順序酣衷,我們就會獲得一個指向攝像機正z軸方向的向量(譯注:注意看前面的那個圖,所說的「方向向量/Direction Vector」是指向z的正方向的次泽,而不是攝像機所注視的那個方向):
glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, 0.0f);
glm::vec3 cameraDirection = glm::normalize(cameraPos - cameraTarget);
方向向量(Direction Vector)并不是最好的名字穿仪,因為它正好指向從它到目標向量的相反方向。
3. 右軸(Right axis)
我們需要的另一個向量是一個右向量(Right Vector)意荤,它代表攝像機空間的x軸的正方向啊片。
為獲取右向量我們需要先使用一個小技巧:定義一個上向量(Up Vector)。我們把上向量和第二步得到的攝像機方向向量進行叉乘玖像。兩個向量叉乘的結果就是同時垂直于兩向量的向量紫谷,因此我們會得到指向x軸正方向的那個向量(如果我們交換兩個向量的順序就會得到相反的指向x軸負方向的向量):
glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f);
glm::vec3 cameraRight = glm::normalize(glm::cross(up, cameraDirection));
4. 上軸(Up axis)
現(xiàn)在我們已經(jīng)有了x軸向量和z軸向量,獲取攝像機的正y軸相對簡單捐寥;我們把方向向量(Direction Vector)和右向量進行叉乘:
glm::vec3 cameraUp = glm::cross(cameraDirection, cameraRight);
在叉乘和一些小技巧的幫助下笤昨,我們創(chuàng)建了所有觀察/攝像機空間的向量。對于想學到更多數(shù)學原理的讀者握恳,提示一下瞒窒,在線性代數(shù)中這個處理叫做Gram-Schmidt(葛蘭—施密特)正交。
使用這些攝像機向量我們就可以創(chuàng)建一個LookAt矩陣了乡洼,它在創(chuàng)建攝像機的時候非常有用崇裁。
Look At
使用矩陣的好處之一是如果你定義了一個坐標空間,里面有3個相互垂直的軸束昵,你可以用這三個軸外加一個平移向量來創(chuàng)建一個矩陣拔稳,你可以用這個矩陣乘以任何向量來變換到那個坐標空間。這正是LookAt矩陣所做的锹雏,現(xiàn)在我們有了3個相互垂直的軸和一個定義攝像機空間的位置坐標巴比,我們可以創(chuàng)建我們自己的LookAt矩陣了:
R是右向量,U是上向量,D是方向向量轻绞,P是攝像機位置向量腰耙。注意,位置向量是相反的铲球,因為我們最終希望把世界平移到與我們自身移動的相反方向。
使用這個LookAt矩陣坐標觀察矩陣可以很高效地把所有世界坐標變換為觀察坐標晰赞。LookAt矩陣就像它的名字表達的那樣:它會創(chuàng)建一個觀察矩陣looks at(看著)一個給定目標稼病。
幸運的是,GLM已經(jīng)提供了這些支持掖鱼。我們要做的只是定義一個攝像機位置然走,一個目標位置和一個表示上向量的世界空間中的向量(我們使用上向量計算右向量)。接著GLM就會創(chuàng)建一個LookAt矩陣戏挡,我們可以把它當作我們的觀察矩陣:
glm::mat4 view;
view = glm::lookAt(glm::vec3(0.0f, 0.0f, 3.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
glm::LookAt函數(shù)需要一個位置芍瑞、目標和上向量。它可以創(chuàng)建一個和上一節(jié)(見下面的代碼)所定義的同樣的觀察矩陣褐墅。
view = glm::translate (view, glm::vec3 (0.0f, 0.0f, -3.0f));
在開始做用戶輸入之前拆檬,我們來做些有意思的事,讓我們的攝像機在場景中旋轉妥凳。我們的注視點保持在(0, 0, 0)竟贯。
我們在每一幀都創(chuàng)建x和z坐標,這要使用一點三角學知識逝钥。x和z表示一個在一個圓圈上的一點屑那,我們會使用它作為攝像機的位置。通過重復計算x和z坐標艘款,遍歷所有圓圈上的點持际,這樣攝像機就會繞著場景旋轉了。我們預先定義這個圓圈的半徑哗咆,使用glfwGetTime
函數(shù)不斷改變x和z的值蜘欲,在每次渲染迭代創(chuàng)建一個新的觀察矩陣。
GLfloat radius = 10.0f;
GLfloat camX = sin(glfwGetTime()) * radius;
GLfloat camZ = cos(glfwGetTime()) * radius;
glm::mat4 view;view = glm::lookAt(glm::vec3(camX, 0.0, camZ), glm::vec3(0.0, 0.0, 0.0), glm::vec3(0.0, 1.0, 0.0));
如果你運行代碼你會得到下面的東西:
視頻演示
項目代碼在這
自由移動
讓攝像機繞著場景轉很有趣岳枷,但是讓我們自己移動攝像機更有趣芒填!首先我們必須設置一個攝像機系統(tǒng),在我們的程序前面定義一些攝像機變量很有用:
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f);
glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);
LookAt函數(shù)現(xiàn)在成了:
view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
我們首先設置之前定義的cameraPos為攝像機位置空繁。目標是當前的位置加上我們剛剛定義的方向向量(cameraFront)殿衰。這樣能保證無論我們怎么移動,攝像機都會注視目標方向盛泡。我們在按下某個按鈕時更新cameraPos向量闷祥。
我們已經(jīng)為GLFW的鍵盤輸入定義了一個key_callback函數(shù),我們來添加幾個新按鍵命令:
void key_callback (GLFWwindow * window, int key, int scancode, int action, int mode)
{
...
GLfloat cameraSpeed = 0.05f;
if (key == GLFW_KEY_W)
cameraPos += cameraSpeed * cameraFront;
if (key == GLFW_KEY_S)
cameraPos -= cameraSpeed * cameraFront;
if (key == GLFW_KEY_A)
cameraPos -= glm::normalize (glm::cross (cameraFront, cameraUp)) * cameraSpeed;
if (key == GLFW_KEY_D)
cameraPos += glm::normalize (glm::cross (cameraFront, cameraUp)) * cameraSpeed;
}
當我們按下WASD鍵,攝像機的位置都會相應更新凯砍。如果我們希望向前或向后移動箱硕,我們就把位置向量加上或減去方向向量。如果我們希望向旁邊移動悟衩,我們做一個叉乘來創(chuàng)建一個右向量剧罩,沿著它移動就可以了。這樣就創(chuàng)建了類似使用攝像機橫向座泳、前后移動的效果惠昔。
注意,我們對右向量進行了標準化挑势。如果我們沒對這個向量進行標準化镇防,最后的叉乘結果會根據(jù)cameraFront變量的大小返回不同的大小。如果我們不對向量進行標準化潮饱,我們就得根據(jù)攝像機的方位加速或減速移動了来氧,但假如進行了標準化移動就是勻速的。
如果你用這段代碼更新key_callback函數(shù)香拉,你就可以在場景中自由的前后左右移動了啦扬。
你可能會注意到這個攝像機系統(tǒng)不能同時朝兩個方向移動,當你按下一個按鍵時凫碌,它會先頓一下才開始移動考传。這是因為大多數(shù)事件輸入系統(tǒng)一次只能處理一個鍵盤輸入,它們的函數(shù)只有當我們激活了一個按鍵時才被調(diào)用证鸥。大多數(shù)GUI系統(tǒng)都是這樣的僚楞,它對攝像機來說用并不合理。我們可以用一些小技巧解決這個問題枉层。
這個技巧是在回調(diào)函數(shù)中只跟蹤哪個鍵被按下/釋放泉褐。在游戲循環(huán)中我們讀取這些值,檢查那個按鍵被激活了鸟蜡,然后做出相應反應膜赃。我們來創(chuàng)建一個布爾數(shù)組代表按下/釋放的鍵:
bool keys[1024];
然后我們必須在key_callback函數(shù)中設置按下/釋放鍵為true或false:
if(action == GLFW_PRESS)
keys[key] = true;
else if(action == GLFW_RELEASE)
keys[key] = false;
我們創(chuàng)建一個新的叫做do_movement的函數(shù),用它根據(jù)按下的按鍵來更新攝像機的值:
void do_movement ()
{
// 攝像機控制
GLfloat cameraSpeed = 0.01f;
if (keys[GLFW_KEY_W])
cameraPos += cameraSpeed * cameraFront;
if (keys[GLFW_KEY_S])
cameraPos -= cameraSpeed * cameraFront;
if (keys[GLFW_KEY_A])
cameraPos -= glm::normalize (glm::cross (cameraFront, cameraUp)) * cameraSpeed;
if (keys[GLFW_KEY_D])
cameraPos += glm::normalize (glm::cross (cameraFront, cameraUp)) * cameraSpeed;
}
之前的代碼移動到了do_movement函數(shù)中揉忘。由于所有GLFW的按鍵枚舉都是整數(shù)跳座,我們可以把它們當數(shù)組索引使用。
最后泣矛,我們需要在游戲循環(huán)中添加新函數(shù)的調(diào)用:
while (!glfwWindowShouldClose (window))
{
// 檢測并調(diào)用事件
glfwPollEvents ();
do_movement ();
// 渲染
...
}
至此疲眷,你可以同時向多個方向移動了,并且當你按下按鈕也會立刻運動了您朽。如遇困難查看源碼以及項目狂丝。
移動速度
目前我們的移動速度是個常量。看起來不錯几颜,但是實際情況下根據(jù)處理器的能力不同倍试,有的人在同一段時間內(nèi)會比其他人繪制更多幀。也就是調(diào)用了更多次do_movement函數(shù)蛋哭。每個人的運動速度就都不同了县习。當你要發(fā)布的你應用的時候,你必須確保在所有硬件上移動速度都一樣谆趾。
圖形和游戲應用通常有回跟蹤一個deltaTime變量准颓,它儲存渲染上一幀所用的時間。我們把所有速度都去乘以deltaTime值棺妓。當我們的deltaTime變大時意味著上一幀渲染花了更多時間,所以這一幀使用這個更大的deltaTime的值乘以速度炮赦,會獲得更高的速度怜跑,這樣就與上一幀平衡了。(當deltaTime變小時同理吠勘。)使用這種方法時性芬,無論你的機器快還是慢,攝像機的速度都會保持一致剧防,這樣每個用戶的體驗就都一樣了植锉。
我們要用兩個全局變量來計算出deltaTime值:
GLfloat deltaTime = 0.0f; // 當前幀遇上一幀的時間差
GLfloat lastFrame = 0.0f; // 上一幀的時間
在每一幀中我們計算出新的deltaTime以備后用
GLfloat currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
現(xiàn)在我們有了deltaTime在計算速度的時候可以使用了:
void Do_Movement()
{
GLfloat cameraSpeed = 5.0f * deltaTime;
...
}
與前面的部分結合在一起,我們有了一個更流暢點的攝像機系統(tǒng):
現(xiàn)在我們有了一個在任何系統(tǒng)上移動速度都一樣的攝像機峭拘。這里是源碼俊庇。我們可以看到任何移動都會影響返回的deltaTime
值。
自由觀看
只用鍵盤移動沒什么意思鸡挠。特別是我們還不能轉向辉饱。是時候使用鼠標了!
為了能夠改變方向拣展,我們必須根據(jù)鼠標的輸入改變cameraFront向量彭沼。
歐拉角
歐拉角是表示3D空間中可以表示任何旋轉的三個值,由萊昂哈德·歐拉在18世紀提出备埃。有三種歐拉角:俯仰角(Pitch)姓惑、偏航角(Yaw)和滾轉角(Roll),下面的圖片展示了它們的含義:
俯仰角是描述我們?nèi)绾瓮虾屯驴吹慕前唇牛诘谝粡垐D中表示于毙。第二張圖顯示了偏航角,偏航角表示我們往左和往右看的大小辅搬。滾轉角代表我們?nèi)绾畏瓭L攝像機望众。每個歐拉角都有一個值來表示,把三個角結合起來我們就能夠計算3D空間中任何的旋轉了。
對于我們的攝像機系統(tǒng)來說烂翰,我們只關心俯仰角和偏航角夯缺,所以我們不會討論滾轉角。用一個給定的俯仰角和偏航角甘耿,我們可以把它們轉換為一個代表新的方向向量的3D向量踊兜。
俯仰角和偏航角轉換為方向向量的處理需要一些三角學知識,我們以最基本的情況開始:
如果我們把斜邊邊長定義為1佳恬,我們就能知道鄰邊的長度是cos x/h = cos x/1 = cos x捏境,它的對邊是sin y/h = sin y/1 = sin y。這樣我們獲得了能夠得到x和y方向的長度的公式毁葱,它們?nèi)Q于所給的角度垫言。我們使用它來計算方向向量的元素:
這個三角形看起來和前面的三角形很像,所以如果我們想象自己在xz平面上倾剿,正望向y軸聋袋,我們可以基于第一個三角形計算 長度/y方向的強度 (我們往上或往下看多少)凝垛。從圖中我們可以看到一個給定俯仰角的y值等于sinθ:
direction.y = sin(glm::radians(pitch)); // 注意我們先把角度轉為弧度
這里我們只更新了y值截珍,仔細觀察x和z元素也被影響了坎拐。從三角形中我們可以看到它們的值等于:
direction.x = cos(glm::radians(pitch));
direction.z = cos(glm::radians(pitch));
看看我們是否能夠為偏航角找到需要的元素:
就像俯仰角一樣我們可以看到x元素取決于cos(偏航角)的值,z值同樣取決于偏航角的正弦值芹缔。把這個加到前面的值中坯癣,會得到基于俯仰角和偏航角的方向向量:
譯注:這里的球坐標與笛卡爾坐標的轉換把x和z弄反了,如果你去看最后的源碼最欠,會發(fā)現(xiàn)作者在攝像機源碼那里寫了yaw = yaw – 90
示罗,實際上在這里x就應該是sin(glm::radians(yaw))
,z也是同樣處理芝硬,當然也可以認為是這個詭異的坐標系鹉勒,但是在這里使用球坐標轉笛卡爾坐標有個大問題,就是在初始渲染時吵取,無法指定攝像機的初始朝向禽额,還要花一些功夫自己實現(xiàn)這個;此外這只能實現(xiàn)像第一人稱游戲一樣的簡易攝像機皮官,類似Maya脯倒、Unity3D編輯器窗口的那種攝像機還是最好自己設置攝像機的位置、上捺氢、右藻丢、前軸,在旋轉時用四元數(shù)對這四個變量進行調(diào)整摄乒,才能獲得更好的效果悠反,而不是僅僅調(diào)整攝像機前軸残黑。
direction.x = cos (glm::radians (pitch)) * cos (glm::radians (yaw));//譯注:direction代表攝像機的“前”軸,但此前軸是和本文第一幅圖片的第二個攝像機的direction是相反的斋否,但與之后的cameraFront方向相同
direction.y = sin (glm::radians (pitch));
direction.z = cos (glm::radians (pitch)) * sin (glm::radians (yaw));
這樣我們就有了一個可以把俯仰角和偏航角轉化為用來自由旋轉的攝像機的3個維度的方向向量了梨水。你可能會奇怪:我們怎么得到俯仰角和偏航角?
鼠標輸入
偏航角和俯仰角是從鼠標移動獲得的茵臭,鼠標水平移動影響偏航角疫诽,鼠標垂直移動影響俯仰角。它的思想是儲存上一幀鼠標的位置旦委,在當前幀中我們當前計算鼠標位置和上一幀的位置相差多少奇徒。如果差別越大那么俯仰角或偏航角就改變越大。
首先我們要告訴GLFW缨硝,應該隱藏光標摩钙,并捕捉(Capture)它。捕捉鼠標意味著當應用集中焦點到鼠標上的時候光標就應該留在窗口中(除非應用拾取焦點或退出)查辩。我們可以進行簡單的配置:
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
這個函數(shù)調(diào)用后胖笛,無論我們怎么去移動鼠標,它都不會顯示了宜肉,也不會離開窗口。對于FPS攝像機系統(tǒng)來說很好翎碑。
為計算俯仰角和偏航角我們需要告訴GLFW監(jiān)聽鼠標移動事件谬返。我們用下面的原型創(chuàng)建一個回調(diào)函數(shù)來做這件事(和鍵盤輸入差不多):
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
這里的xpos和ypos代表當前鼠標的位置。我們注冊GLFW的回調(diào)函數(shù)日杈,鼠標一移動mouse_callback函數(shù)就被調(diào)用:
glfwSetCursorPosCallback(window, mouse_callback);
在處理FPS風格的攝像機鼠標輸入的時候遣铝,我們必須在獲取最終的方向向量之前做下面這幾步:
- 計算鼠標和上一幀的偏移量。
- 把偏移量添加到攝像機和俯仰角和偏航角中莉擒。
- 對偏航角和俯仰角進行最大和最小值的限制酿炸。
- 計算方向向量。
第一步計算鼠標自上一幀的偏移量涨冀。我們必須先儲存上一幀的鼠標位置填硕,我們把它的初始值設置為屏幕的中心(屏幕的尺寸是800乘600):
GLfloat lastX = 400, lastY = 300;
然后在回調(diào)函數(shù)中我們計算當前幀和上一幀鼠標位置的偏移量:
GLfloat xoffset = xpos - lastX;
GLfloat yoffset = lastY - ypos; // 注意這里是相反的,因為y坐標的范圍是從下往上的???
lastX = xpos;
lastY = ypos;
GLfloat sensitivity = 0.05f;
xoffset *= sensitivity;
yoffset *= sensitivity;
注意我們把偏移量乘以了sensitivity值鹿鳖。如果我們移除它扁眯,鼠標移動就會太大了;你可以自己調(diào)整sensitivity的值翅帜。
第二步我們把偏移量加到全局變量pitch和yaw上:
yaw += xoffset;
pitch += yoffset;
第三步我們給攝像機添加一些限制姻檀,這樣攝像機就不會發(fā)生奇怪的移動了。對于俯仰角涝滴,要讓用戶不能看向高于89度(90度時視角會逆轉绣版,所以我們把89度作為極限)的地方胶台,同樣也不允許小于-89度。這樣能夠保證用戶只能看到天空或腳下但是不能更進一步超越過去杂抽。限制可以這樣做:
if (pitch > 89.0f)
pitch = 89.0f;
if (pitch < -89.0f)
pitch = -89.0f;
注意我們沒有給偏航角設置限制是因為我們不希望限制用戶的水平旋轉诈唬。
第四也是最后一步,就是通過俯仰角和偏航角來計算以得到前面提到的實際方向向量:
glm::vec3 front;
front.x = cos (glm::radians (pitch)) * cos (glm::radians (yaw));
front.y = sin (glm::radians (pitch));
front.z = cos (glm::radians (pitch)) * sin (glm::radians (yaw));
cameraFront = glm::normalize (front);
這回計算出方向向量默怨,根據(jù)鼠標點的移動它包含所有的旋轉讯榕。由于cameraFront向量已經(jīng)包含在glm::lookAt函數(shù)中,我們直接去設置匙睹。
如果你現(xiàn)在運行代碼愚屁,你會發(fā)現(xiàn)當程序運行第一次捕捉到鼠標的時候攝像機會突然跳一下。原因是當你的鼠標進入窗口鼠標回調(diào)函數(shù)會使用這時的xpos和ypos痕檬。這通常是一個距離屏幕中心很遠的地方霎槐,因而產(chǎn)生一個很大的偏移量,所以就會跳了梦谜。我們可以簡單的使用一個布爾變量檢驗我們是否是第一次獲取鼠標輸入丘跌,如果是,那么我們先把鼠標的位置更新為xpos和ypos唁桩,這樣就能解決這個問題闭树;最后的鼠標移動會使用進入以后鼠標的位置坐標來計算它的偏移量:
最后的代碼應該是這樣的:
bool firstMouse = true;
void mouse_callback (GLFWwindow* window, double xpos, double ypos)
{
if (firstMouse)
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
GLfloat xoffset = xpos - lastX;
GLfloat yoffset = lastY - ypos; // Reversed since y-coordinates range from bottom to top
lastX = xpos;
lastY = ypos;
GLfloat sensitivity = 0.05f; // Change this value to your liking
xoffset *= sensitivity;
yoffset *= sensitivity;
yaw += xoffset;
pitch += yoffset;
// Make sure that when pitch is out of bounds, screen doesn't get flipped
if (pitch > 89.0f)
pitch = 89.0f;
if (pitch < -89.0f)
pitch = -89.0f;
glm::vec3 front;
front.x = cos (glm::radians (yaw)) * cos (glm::radians (pitch));
front.y = sin (glm::radians (pitch));
front.z = sin (glm::radians (yaw)) * cos (glm::radians (pitch));
cameraFront = glm::normalize (front);
}
現(xiàn)在我們可以自由的在3D場景中移動了!如果你遇到困難荒澡,這是源碼报辱。
縮放
我們還要往攝像機系統(tǒng)里加點東西,實現(xiàn)一個縮放接口单山。碍现。前面教程中我們說視野(Field of View或fov)定義了我們可以看到場景中多大的范圍。當視野變小時可視區(qū)域就會減小米奸,產(chǎn)生放大了的感覺昼接。我們用鼠標滾輪來放大。和鼠標移動悴晰、鍵盤輸入一樣我們需要一個鼠標滾輪的回調(diào)函數(shù):
void scroll_callback (GLFWwindow* window, double xoffset, double yoffset)
{
if (aspect >= 1.0f && aspect <= 45.0f)
aspect -= yoffset;
if (aspect <= 1.0f)
aspect = 1.0f;
if (aspect >= 45.0f)
aspect = 45.0f;
}
yoffset值代表我們滾動的大小慢睡。當scroll_callback函數(shù)調(diào)用后,我們改變?nèi)謅spect變量的內(nèi)容铡溪。因為45.0f是默認的fov一睁,我們將會把縮放級別限制在1.0f到45.0f。
我們現(xiàn)在在每一幀都必須把透視投影矩陣上傳到GPU佃却,但這一次使aspect變量作為它的fov:
projection = glm::perspective(aspect, (GLfloat)WIDTH/(GLfloat)HEIGHT, 0.1f, 100.0f);
最后不要忘記注冊滾動回調(diào)函數(shù):
glfwSetScrollCallback(window, scroll_callback);
現(xiàn)在我們實現(xiàn)了一個簡單的攝像機系統(tǒng)者吁,它能夠讓我們在3D環(huán)境中自由移動。
視頻演示
自由的去實驗饲帅,如果遇到困難對比項目代碼复凳。
注意瘤泪,使用歐拉角作為攝像機系統(tǒng)并不完美。你仍然可能遇到萬向節(jié)死鎖育八。最好的攝像機系統(tǒng)是使用四元數(shù)的对途,后面會有討論。
攝像機類
像著色器對象一樣髓棋,我們把攝像機類寫在一個單獨的頭文件中实檀。你可以在這里找到它。
我們介紹的歐拉角FPS風格攝像機系統(tǒng)能夠滿足大多數(shù)情況需要按声,但是在創(chuàng)建不同的攝像機系統(tǒng)膳犹,比如飛行模擬就要當心。每個攝像機系統(tǒng)都有自己的有點和不足签则,所以確保對它們進行了詳細研究须床。比如,這個FPS攝像機不允許俯仰角大于90度渐裂,由于使用了固定的上向量(0, 1, 0)豺旬,我們就不能用滾轉角。
使用新的攝像機對象的更新后的版本源碼可以在這里找到柒凉。(譯注:總而言之這個攝像機實現(xiàn)并不十分完美族阅,你可以看看最終的源碼。建議先看這篇文章膝捞,對旋轉有更深的理解后坦刀,你就能做出更好的攝像機類,不過本文有些內(nèi)容比如如何防止按鍵停頓和glfw鼠標事件實現(xiàn)攝像機的注意事項比較重要绑警,其它的就要做一定的取舍了)
練習
- 看看你是否能夠變換攝像機類從而使得其能夠變- 成一個真正的FPS攝像機(也就是說不能夠隨意飛行)求泰;你只能夠呆在xz平面上
void ProcessKeyboard (Camera_Movement direction, GLfloat deltaTime)
{
GLfloat velocity = this->MovementSpeed * deltaTime;
if (direction == FORWARD)
this->Position += this->Front * velocity;
if (direction == BACKWARD)
this->Position -= this->Front * velocity;
if (direction == LEFT)
this->Position -= this->Right * velocity;
if (direction == RIGHT)
this->Position += this->Right * velocity;
this->Position.y = 0.0f; // this one - liner keeps the user at the ground level (xz plane)
}
- 試著創(chuàng)建你自己的LookAt函數(shù)央渣,使你能夠手動創(chuàng)建一個我們在一開始討論的觀察矩陣计盒。用你的函數(shù)實現(xiàn)來替換glm的LookAt函數(shù),看看它是否還能一樣的工作:
// Custom implementation of the LookAt function
glm::mat4 calculate_lookAt_matrix (glm::vec3 position, glm::vec3 target, glm::vec3 worldUp)
{
// 1. Position = known
// 2. Calculate cameraDirection
glm::vec3 zaxis = glm::normalize (position - target);
// 3. Get positive right axis vector
glm::vec3 xaxis = glm::normalize (glm::cross (glm::normalize (worldUp), zaxis));
// 4. Calculate camera up vector
glm::vec3 yaxis = glm::cross (zaxis, xaxis);
// Create translation and rotation matrix
// In glm we access elements as mat[col][row] due to column-major layout
glm::mat4 translation; // Identity matrix by default
translation[3][0] = -position.x; // Third column, first row
translation[3][1] = -position.y;
translation[3][2] = -position.z;
glm::mat4 rotation;
rotation[0][0] = xaxis.x; // First column, first row
rotation[1][0] = xaxis.y;
rotation[2][0] = xaxis.z;
rotation[0][1] = yaxis.x; // First column, second row
rotation[1][1] = yaxis.y;
rotation[2][1] = yaxis.z;
rotation[0][2] = zaxis.x; // First column, third row
rotation[1][2] = zaxis.y;
rotation[2][2] = zaxis.z;
// Return lookAt matrix as combination of translation and rotation matrix
return rotation * translation; // Remember to read from right to left (first translation then rotation)
}