2011年10月30日 星期日

8. 使用視圖與相機鏡頭轉換,及 gluLookAt() - OpenGL FAQ

返回 OpenGL FAQ 目錄

8.010 OpenGL裡的相機鏡頭到底是怎麼運作的呢? 

就目前的OpenGL來說,沒有相機鏡頭這種東西。更精確地講,相機永遠都位處於眼空間座標的原點 (0,0,0)。你的OpenGL應用程式為了假裝出移動相機的樣子,必須移動場景,對整個世界場景做相機鏡頭的逆空間轉換。

8.020 我要如何移動場景裡的眼睛或者相機鏡頭?

以相機鏡頭模型來說,OpenGL 並沒有提供介面去做這件事情。然而,GLU庫提供了gluLookAt()函數,傳入眼睛本身位置,眼睛瞄準點位置,以及朝上向量,都是世界空間坐標。這個函數依據參數計算出正確的相機逆空間轉換矩陣,並且乘上目前的矩陣。

8.030 我的相機鏡頭應該放在 ModelView 矩陣還是 Projection矩陣?

GL_PROJECTION 矩陣只應該包含投影變換,它必須將眼空間坐標(eye space coordinates)轉換到裁切坐標(clip coordinates)。

GL_MODELVIEW矩陣呢,如其名,應該包含模型(modeling)與視圖(viewing)轉換,將物體空間坐標轉換成眼空間座標。所以請記住,永遠把相機鏡頭轉換放在GL_MODELVIEW矩陣,而不是GL_PROJECTION矩陣。

你可以把投影矩陣(projection matrix)想像成相機鏡頭的各種特性,像是視角的寬窄、焦距、用了魚眼鏡頭等等。而把模型視圖矩陣(model-view matrix)想成,你目前拿著相機站立的位置,及鏡頭指向的方向。

這份 The game dev FAQ 對這兩個矩陣說明得很不錯。

去讀讀 Steve Baker 的文章「濫用投影」(備用連結) 吧。此篇文章寫得很好,值得推薦。它曾經幫助過許多OpenGL新手程式員。

8.040 我該如何實現鏡頭變焦 (Zoom) 的操作?

簡單的做法是對 ModelView矩陣做等比縮放(uniform scale)。但是當模型放得太大時,通常會導致模型被近平面及遠平面裁切掉。一個比較好的做法是限縮投影矩陣裡view volume的寬與高。舉例來說,你的程式對應使用者的輸入,儲存了一個縮放參數值。當縮放值是1.0 時不變焦。縮放值變大就縮小視角,結果模型看起來就放大了。縮放值變小時就反過來做。程式碼範例看起來可能像這樣子:
/* 如果你想就設成全域變數。依使用者輸入改變值,初始值是1.0 */
static float zoomFactor; 

/* 一個設定投影矩陣的例程,通常在 resize 事件處理呼叫它。
   繪圖區域的寬、高是整數。
   此例程會創建一個投影矩陣,有正確長寬比以及縮放參數。
*/
void setProjectionMatrix (int width, int height)
{
   glMatrixMode(GL_PROJECTION);
   glLoadIdentity();
   gluPerspective (50.0*zoomFactor, (float)width/height, zNear, zFar);
   /* 'zNear' 與 'zFar' 隨你高興 */
}
你的程式也許用glFrustum() 而不是 glPerspective()。這有點棘手,因為 left, right, bottom, top 這四個參數與近平面(zNear) 的距離,也會影響視野大小。這裡合理的假設你使用固定的近平面距離,那 glFrustum() 看起來會是:
glFrustum(left*zoomFactor, right*zoomFactor,
    bottom*zoomFactor, top*zoomFactor,
    zNear, zFar);
glOrtho() 用法也差不多。

8.050 我有了目前的ModelView矩陣,該如何反推相機鏡頭在物體空間裡頭的座標位置?

相機鏡頭或眼睛位置永遠位於眼空間的原點 (0,0,0)。將原點轉換成向量 [0 0 0 1] ,然後用 ModelView 矩陣的反矩陣去乘,結果就是相機鏡頭在物體空間中的座標位置。OpenGL不允許你查詢ModelView矩陣的反矩陣 (用glGet*等函數),你必須要自己寫代碼來計算反矩陣。

8.060 我該怎麼樣讓鏡頭不斷「環繞」著場景中的某一點運行旋轉?

你可以把鏡頭留在原位不動,而轉動整個場景/世界來模擬環繞運行效果。比方說,鏡頭要以一個Y軸為中心繞行,同時也持續地盯著原點看的話,你可以這樣做:

gluLookAt(camera[0], camera[1], camera[2], /* 鏡頭的 XYZ  位置*/
          0, 0, 0,                         /* 看著原點 */
          0, 1, 0);                        /* Y朝上向量 */
glRotatef(orbitDegrees, 0.f, 1.f, 0.f);    /* 繞行Y軸的角度 */
/* ...也許 orbitDegrees 可以從滑鼠拖曳推算出來 */

glCallList(SCENE);                         /* 畫出整個場景 */
如果你堅持一定要在物理上旋轉相機位置,你需要在視圖轉換之前,先對相機鏡頭的位置向量做空間轉換。此種狀況下,我建議你去查考一下gluLookAt()這個函式。

8.070 我要怎樣才能自動推算出剛剛好涵蓋整個物體模型的視角? (我知道 bounding sphere 與 up vector.)

以下的視角系統設定取自 Dave Shreiner 發表的文章:

首先請計算出場景中所有物體的bounding sphere(碰撞球)。它應該會提供你兩個訊息: 球體中心 (令該點為 (c.x, c.y, c.z)),以及直徑 (我們叫它"diam" )。

接著,選定一個值作為近平面zNear。依照一般準則,我們會選一個比1大,但是很接近1的值。所以我們設定如下:
zNear = 1.0;
zFar = zNear + diam;
依此順序建構矩陣 (以平行投影來說)
GLdouble left = c.x - diam;
GLdouble right = c.x + diam;
GLdouble bottom c.y - diam;
GLdouble top = c.y + diam;

glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(left, right, bottom, top, zNear, zFar);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
這個方法應該會令物體置中,同時視窗合適地包住物體。(這兒假設視窗長寬比是1.0)。如果你的視窗不是正方形,那先如上法計算出 left、right、bottom、與top的值,然後在呼叫glOrtho()之前加入以下代碼邏輯:
GLdouble aspect = (GLdouble) windowWidth / windowHeight;

if ( aspect < 1.0 ) { // 判斷視窗是狹長型還是橫寬型。
   bottom /= aspect;
   top /= aspect;
} else {
   left *= aspect;
   right *= aspect;
}
以上代碼應該可以很合宜地定位物件。如果你還想要操弄物體(例如旋轉它),那你還需要加上一個視圖轉換(viewing transform)。

典型的視圖轉換都應該做在ModelView矩陣上,看起來大概如下:
gluLookAt (0., 0., 2.*diam,
           c.x, c.y, c.z,
           0.0, 1.0, 0.0);

8.080 為什麼 gluLookAt() 故障了?

這通常肇因於錯誤的空間轉換。

假設你在投影矩陣上使用了 glPerspective() 並且給第三、四個參數傳入zNear、zFar,你必須把gluLookAt() 設在ModelView矩陣上,並且將幾何圖形放在zNear與zFar之間才行。

當你試著想弄懂視圖轉換時,最好的辦法就是用簡單的代碼來做實驗。比方說你想盯著一個位於原點的單位球體看。那你要建立如下的轉換:
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(50.0, 1.0, 3.0, 7.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
gluLookAt(0.0, 0.0, 5.0,
          0.0, 0.0, 0.0,
          0.0, 1.0, 0.0);

重點是投影轉換(Projection transform)與模型視圖轉換(ModelView transform) 之間的合作。

在這個例子中,投影轉換將視角(field of view)設定為50度,長寬比為1.0。近裁切平面位於眼睛前方3.0個單位,遠裁切平面則位於眼睛前方7.0個單位。這留下了4.0個單位的Z視體距離,有充足的空間含納單位球體。

模型視圖轉換則將眼睛位置安置於(0.0, 0.0, 5.0),並把瞄準點設為原點,也就是單位球體的中心。請注意眼睛位置點與瞄準點之間的距離有5.0單位遠。這很重要,因為眼睛前方5.0單位剛好位於Z視體的中間,就如我們剛剛在投影變換裡定義的。如果呼叫 gluLookAt() 時把眼睛置於 (0.0, 0.0, 1.0),那眼睛距離原點就只有1.0單位。這長度不夠含納整個單位球體,球體就會被近平面zNear裁切掉。

同樣地,如果你把眼睛位置放在(0.0, 0.0, 10.0),那麼距離瞄準點有10單位遠,導致單位球體距離眼睛也是10單位遠,遠遠超過了遠平面zFar的7.0個單位。

如果這令你有點混淆,讀讀OpenGL紅皮書跟OpenGL規格書裡的空間轉換。一旦你弄懂了物體座標空間(object coordinate space)、眼座標空間(eye coordinate space),與裁切座標空間(clip coordinate space) 後,上面的內容應該就會很清楚了。還有,寫小支測試程式來做實驗。如果你在主專案裡頭碰到麻煩,找不到正確的轉換,那嘗試用簡單的幾何圖形跟一支小程式來重現問題,是很好的學習方式。

8.090 我該如何令一個指定的點(XYZ) 出現在場景的中心呢?

最容易的辦法就是 gluLookAt() 了。直接傳入特定點的座標 X, Y, Z 作為gluLookAt() 的第四、五、六個參數即可。

8.100 我在投影矩陣上呼叫 gluLookAt() 後,霧、貼圖、照明就全錯了。怎麼回事?

請看問題 8.030 的解釋。

8.110 我該如何創建立體視圖?

Paul Bourke 彙整了一些關於OpenGL立體視圖的資訊

2011年10月29日 星期六

讀書心得: 朱敬一 給青年知識追求者的信

More about 給青年知識追求者的信朱敬一先生是少數幾位我聽過的中研院院士的其中之一,可惜不是因為他的研究,而是新聞報導上常常看見朱先生的大名。個人粗淺的印象中,此人敢說敢做,意見頗值得一聽,所以偶然在圖書館翻到這本書,就把它帶回來了。本書總共由十封信組成,內容大略可分為兩半,前五封信是個人治學心得,我覺得相當不錯,後五封信則是對學術圈的建議,可能要有志於學術者讀起來才比較有感覺吧。

整本書看完後,我認為朱敬一先生想要對這個高度專業分工,以致於過於狹隘與僵化的社會提供一點反思,用朱敬一先生的話來說就是: 「你如果專注於其中部分領域而一頭鑽進去,當然是好的,但是你若想遊走諸方而觸類旁通,也沒有什麼不可以」。每當我想著「資訊工程是我的專業」的時候,到底是不是替我自己立下了一道牆,阻止我往外看,而失去了一些激發思想火花的機會。朱敬一是社會學家,所以書裡用很生動的例子來說明這種法政經社本一體的狀況,雖然表面觀察的對象不同,但是底下的氣息卻隱隱互通。

書裡舉了經濟學家拉維(Levitt) 作為這類遊走諸方的學者典型。拉維教授的其中一個研究證明,美國各州到一九九零年左右突然犯罪率大幅下降,其實是肇因於十八年前美國最高法院判定禁止墮胎違憲。墮胎合法化後,許多意外懷孕的女人就不用生下「不想要的小孩」,因而抑止了將來潛在的犯罪者的出生。這樣驚奇的研究結論,實在很難相信出自一位經濟學家之手。不過我看過拉維的例子後,我開始有點相信朱敬一說的,廣博的通識教育帶來的不是顯然可見的解題能力,而是「發掘、形成新問題」的能力。這也是許多台灣學生的弱點,解問題一流,但是不懂得找問題。

而想要有這種能力,就要依靠後天的「不住相讀書」,朱敬一改自金剛經的句子說「學子不住相讀書,其功用不可限量」,讀書不該功利的只求「有用」,不帶目的廣泛的讀書,才能成其大用,在大腦裡面埋下知識火種,也許有一天各個不相干的點會突然串起來,另闢蹊徑。這讓我想起賈伯斯的演講,他就是休學跑去旁聽書法課,我們今天才有漂亮字體的Mac電腦。

書中有段話剛好點醒了我最近的一些迷惘,朱敬一說,到了一定年紀之後,就不要再以「我某某還不夠好」的彌補心態來作為學習的動力,因為知識永遠學不完。最好的方式是一腳踏到浪頭上去,讓自己成為前緣浪花的一部份。

其實不管內容的話,聽聽朱敬一閒聊也是蠻有趣的,裏頭有一段兩三頁就簡單的道出社會科學的本質,文字淺顯清楚,相當精彩。對於自然科學、社會科學與人文科學分際探討也很有趣,讓我現在開始想一個問題,究竟我念的本科是屬於哪一方面? 電腦科學固然屬自然科學,不過一腳踏入軟體工程之後,是不是就有很強的社會科學的味道了呢?