1. 程式人生 > >OpenGL邊用邊學------2 經典照相機模型

OpenGL邊用邊學------2 經典照相機模型

好的 實現 details 照相功能 top 做的 移動位置 相同 計算

https://blog.csdn.net/smstong/article/details/50290327

  • 實際照相步驟
    • 1 布置場景和調整照相機位置
    • 3 選擇鏡頭對焦Focus
    • 4 按下快門
    • 5 在電腦窗口中欣賞圖片
  • OpenGL的相機模型
    • 0 確定膠片位置
    • 1 確立場景世界坐標系
    • 2 在世界坐標系中確定相機位置與方向
    • 3 在世界坐標系中建立物理世界模型
    • 4 視圖變換與模型變換的抉擇
    • 5 在照相機坐標系中確定可視範圍對焦投影變換
    • 6 調用glBegin glEnd拍照
  • OpenGL相機模型與實際相機的不同
    • 1 物體和相機都可以任意移動
    • 2 最終的照片可以是多次拍攝合成的
    • 3 特殊的投影方式正投影
  • 相機模型使用建議
    • 1 盡量讓編程符合這個模型
    • 2 註意區分兩大坐標系世界坐標系和相機坐標系
    • 3 實例

學習OpenGL必須要有一個感性的認識,最經典的類比就是照相機照相模型。現在幾乎人人都有一部自帶照相功能的手機了,所以這個模型對於絕大多數人來說都是非常容易理解的。

1 實際照相步驟

1.1 布置場景和調整照相機位置

技術分享圖片
之所以把布置場景和調整相機位置放到了一起,是因為這兩個步驟的順序不是固定的,攝影師既可以自己移動相機,也可以指揮被拍的人移動位置。

1.3 選擇鏡頭,對焦(Focus)

鏡頭的選擇主要是為了達到合適的視角範圍,鏡頭確定以後,焦距就確定了。

一般來說,調焦(Focus)並不是改變鏡頭的焦距,而是改變鏡頭與膠片的距離(像距)。

凸透鏡能成像,一般用凸透鏡做照相機的鏡頭時,它成的最清晰的像一般不會正好落在焦點上,或者說,最清晰的像到光心的距離(像距)一般不等於焦距,而是略大於焦距。具體的距離與被照的物體與鏡頭的距離(物距)有關,物距越大,像距越小,(但實際上總是大於焦距)。

技術分享圖片

1.4 按下快門

一切都調整好了,就可以拍照了。按下快門的那一刻,相機位置、場景布置都定格了,不再改變。感光器件(膠片)會記錄下這一時刻的場景,繪制成一個矩形的二位像素圖。

1.5 在電腦窗口中欣賞圖片

然後就可以把照片轉移到電腦中,使用看圖軟件把圖片放到合適的位置進行欣賞了。

2 OpenGL的相機模型

2.0 確定膠片位置

OpenGL中沒有真正的膠片,必須把拍攝到的圖像放到一個指定的地方顯示,這個地方就是視口。在上篇博文中已經專門介紹了視口的設置方法了,這裏不再累述。

2.1 確立場景(世界)坐標系

與實際照相不同的是,OpenGL默認的物理世界裏是沒有任何物體的。需要程序員通過代碼來建立場景中的物體。建模的最基本要素是確定物體的位置,而要描述物體位置首先要做的就是確定一個世界(場景)坐標系。這個坐標系必須是標準的笛卡爾右手坐標系,而其原點和長度單位卻沒有任何限制,由程序員根據實際需要自由確定。例如,對於整個城市的場景,原點可以設置在市政府,x,y方向可以分別設置為東西方向和南北方向,長度單位設置為米。而對於分子結構的場景,長度單位則可能是納米了。

同其他狀態變量一樣,OpenGL的世界坐標系也有默認值,那就是一個標準右手三維直角坐標系,y軸向上,原點位置和長度單位沒有定義。

創建坐標系是程序員的思維中進行的,無法直接體現到代碼上,通過後面相機、物體的定位坐標數值來體現。

2.2 在世界坐標系中確定相機位置與方向

與場景中的其他物體一樣,相機也是在世界坐標系中進行定位的。在OpenGL中,改變相機位置和方向的行為叫做視圖變換,完成這個功能的函數是gluLookAt()

void WINAPI gluLookAt(
   GLdouble eyex,
   GLdouble eyey,
   GLdouble eyez,
   GLdouble centerx,
   GLdouble centery,
   GLdouble centerz,
   GLdouble upx,
   GLdouble upy,
   GLdouble upz
);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

首先,確定的是照相機的坐標,通過(eyex, eyey, eyez)確定。

其次,是確定鏡頭的朝向,通過瞄準線上一點(centerx,centery,centerz)指定,需要註意的是,這個點並不是拍攝時對焦的點,只是用來確定鏡頭的方向而已。最終方向就是從點(eyex,eyey,eyez)到(centerx,centery,centerz)的連線方向。

最後,是旋轉鏡頭,通過指定向上方向上經過的一點(upx,upy,upz)確定。需要註意的是,向上方向是由(0,0,0)到(upx,upy,upz)連線確定的,與相機坐標(eyex,eyey,eyez)沒有關系。

OpenGL同樣會在初始化時,為相機的位置和方向在世界坐標系中指定一個默認值。那就是,照相機位於原點,鏡頭朝向z軸負方向,向上方向為y軸正方向。相當於調用了gluLookAt(0,0,0,0,0,0,0,1,0)

2.3 在世界坐標系中建立物理世界模型

與實際照相不同,OpenGL裏的物體都是虛擬的,想象出來的。具體來說,是通過在世界坐標系裏確定其位置來描述的。這種虛擬的特性給了OpenGL比實際照相更多的控制性。例如,虛擬的物體可以任意擺放,任意建立或刪除,任意著色,任意形狀。只要你敢想象,沒有人能阻擋你建立一座倒立的房子。由於這些物體可以任意移動,所以完全可以把照相機始終固定,只是通過移動物體來達到和移動相機相同的效果。相機成像的結果就是相機和物體相對位置來確定的,所以理論上移動物體和移動相機都能達到相同的效果。實際上,OpenGL就是使用了一個矩陣來描述這種相對關系,這就是大名鼎鼎的模型視圖矩陣。

移動物體在OpenGL裏被叫做模型變換。視圖變換和模型變換改變的都是同一個模型視圖矩陣。模型變換就是修改模型視圖矩陣,OpenGL為最常用的三種變換提供了函數:

/*平移*/
void WINAPI glTranslatef(
   GLfloat x,
   GLfloat y,
   GLfloat z
);
/*旋轉*/
void WINAPI glRotatef(
   GLfloat angle,
   GLfloat x,
   GLfloat y,
   GLfloat z
);
/*縮放*/
void WINAPI glScalef(
   GLfloat x,
   GLfloat y,
   GLfloat z
);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

一般情況下,這三種變換完全能夠滿足程序的需要。如果需要特殊的變化,OpenGL也提供了直接操作矩陣的方式。


void WINAPI glMultMatrixf(
   const GLfloat *m
);
  • 1
  • 2
  • 3
  • 4

2.4 視圖變換與模型變換的抉擇

具體是采用視圖變換還是模型變換,取決於具體的場景渲染需求,往往一種變換比另一種變換更直接、更容易理解、更簡單。例如駕駛飛機遊戲中,從飛機內部向外看時,則視圖變換明顯要更簡單、更符合正常思維。

2.5 在照相機坐標系中確定可視範圍,對焦(投影變換)

鏡頭有個橫向有個視角問題,縱深也有範圍,不可能看到無限遠的物體。實際照相時鏡頭的不同決定了水平視角大小,光線的強弱決定了能拍攝的最大縱深,對焦決定了膠片位置。在OpenGL中,則通過一個函數完成了這些功能。

OpenGL提供了如下函數來完成這個任務。

void WINAPI glFrustum(
   GLdouble left,
   GLdouble right,
   GLdouble bottom,
   GLdouble top,
   GLdouble zNear,
   GLdouble zFar
);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

技術分享圖片

這個可視範圍也是在照相機坐標系中指定的。照相機坐標系的定義如下:

  • 原點位於照相機本身
  • 照相機的向上方向(由gluLookAt指定)為y軸正方向,照相機朝向為z軸負方向,x軸根據右手定則確定;
  • 長度單位與世界坐標系相同

這樣近平面左下角坐標就是(left, bottom, -near),右上角坐標是(right, top, -near)。
註意:參數中的zNear, zFar都是距離值,必須為正;其他參數則是坐標值,可正可負。

由於近平面也是最終的投影平面,所以zNear也就成了對焦距離了。

實際使用時,glFrustum中各個參數的確定不是很方便,所以OpenGL提供了另外一個更方便調用的函數來確定可視範圍。

gluPerspective()

void WINAPI gluPerspective(
   GLdouble fovy,
   GLdouble aspect,
   GLdouble zNear,
   GLdouble zFar
);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

當然,這個函數中的參數也是在相機坐標系中確定的。其中zNear, zFar的含義與glFrustum()中的相同。
fovy是在垂直方向的可視角度,單位為度,範圍為(0.0,180.0),aspect是水平方向可視角度範圍與垂直方向可視角度範圍的比例,也就是水平方向可視範圍為 fovy*aspect。

這個函數定義了一個對稱的可視空間,用起來要比glFrustum方便很多。

2.6 調用glBegin(); …; glEnd()拍照

前面的所有的設置確定了最終拍攝的範圍、方向、存放地。最後一步就是進行實際拍攝了。在OpenGL中,拍攝動作是伴隨建模一起的。

3 OpenGL相機模型與實際相機的不同

雖然用相機拍照過程來比喻OpenGL渲染過程非常適合,但是畢竟OpenGL是計算機軟件行為,所以和真正的還是相機拍攝過程還是有很多的不同之處。

3.1 物體和相機都可以任意移動

這個在前面已經說過了。

3.2 最終的“照片”可以是多次拍攝合成的

OpenGL的每一次glBegin(); …; glEnd(); 相當於一次拍攝,而最終的“照片”是多次拍攝的合成結果,而且每次拍攝都可以使用不同的場景、不同的相機位置、不同的視口….。

程序中可以無限次調用glBegin(); …; glEnd()進行拍攝。

3.3 特殊的投影方式—正投影

前面介紹的glFrustum()以非常接近實際相機的方式設置了可是範圍與投影方式(透視投影)。然而OpenGL畢竟是軟件,靈活性要比實際相機強得多,它還提供了一種正投影的方式。這相當於照相機的鏡頭不是凸透鏡,而是平面玻璃,現實中不可能存在這樣的照相機。

void WINAPI glOrtho(
   GLdouble left,
   GLdouble right,
   GLdouble bottom,
   GLdouble top,
   GLdouble zNear,
   GLdouble zFar
);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

這個函數確定的可視範圍是一個平行於視線的長方體。同樣也是在照相機坐標系中點定義坐標。

技術分享圖片
可視範圍內的物體,無論近遠,投影到近平面上後都不改變其大小。

4 相機模型使用建議

4.1 盡量讓編程符合這個模型

照相機模型很好的反映了OpenGL內部的運算過程,非常適合構造場景時使用它來幫助思考。程序中也盡量使用這種模型去實現。除非特殊需要,避免不符合該模型的操作方法,例如一幀圖像的多次渲染使用不同相機位置,但是使用同一個視口,這相當於拍攝了多張照片,然後疊加到了一起,容易讓人摸不著頭腦。

4.2 註意區分兩大坐標系:世界坐標系和相機坐標系。

首先在世界坐標系中確定相機位置、視線方向、向上方向,然後在相機坐標系中確定可視範圍。

世界坐標系的軸方向由程序員隨意定義,只要滿足笛卡爾右手定則就可以:例如數學上常見的三維坐標系,往往是z軸向上,而不是OpenGL中常見的y軸向上。

相機坐標系的原點,各軸方向則必須由相機,視線方向和向上方向來確定:原點必定位於相機本身,視線方向必定是z軸負方向,向上方向必定是y軸正向。

4.3 實例

下面是一個漫遊觀察四面體的小實例。

技術分享圖片

初始化時,設置可視空間和相機初始位置。

// 安排相機
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    gluLookAt(
        3, 0, 1,
        0, 0, 1,
        0, 0, 1);

    // 確定可視範圍
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(100, 1, 0.5, 100);
    ::InvalidateRect(hWnd, NULL, TRUE);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

鼠標單擊一下客戶區,照相機就會繞z軸你是神旋轉5度,可視空間不再改變。代碼如下:

void OnLeftButtonDown(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    static float a = 0;

    float x, y, z;
    x = 3 * cos(a*3.14/180);
    y = 3 * sin(a*3.14/180);
    z = 1;
    a += 5;
    while(a > 360) {
        a -= 360;
    }

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    gluLookAt(x, y, z, 0, 0, 1, 0, 0, 1);
    ::InvalidateRect(hWnd, NULL, TRUE);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

實際的渲染代碼為:

void OnPaint(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    glClear(GL_COLOR_BUFFER_BIT);
    // 繪制世界坐標系
    glColor3f(1, 1, 1);
    glBegin(GL_LINES);
    glVertex3d(0, 0, 0);
    glVertex3d(100, 0, 0);
    glVertex3d(0, 0, 0);
    glVertex3d(0, 100, 0);
    glVertex3d(0, 0, 0);
    glVertex3d(0, 0, 100);
    glEnd();
    // 在原點處,建立一個四面體,四個頂點分別位於三個軸上,和原點處。
    glEnable(GL_CULL_FACE);
    glCullFace(GL_BACK);
    glBegin(GL_TRIANGLES);
    glColor3f(1, 0, 0);
    glVertex3d(0, 1, 0);
    glVertex3d(1, 0, 0);
    glVertex3d(0, 0, 0);

    glColor3f(0, 1, 0);
    glVertex3d(1, 0, 0);
    glVertex3d(0, 0, 2);
    glVertex3d(0, 0, 0);

    glColor3f(0, 0, 1);
    glVertex3d(0, 0, 2);
    glVertex3d(0, 1, 0);
    glVertex3d(0, 0, 0);

    glColor3f(1, 1, 0);
    glVertex3d(1, 0, 0);
    glVertex3d(0, 1, 0);
    glVertex3d(0, 0, 2);

    glEnd();
    HDC hdc = ::GetDC(hWnd);
    ::SwapBuffers(hdc);
    ::ReleaseDC(hWnd, hdc);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

運行截圖:

技術分享圖片

源碼下載

OpenGL邊用邊學------2 經典照相機模型