目錄
一、多坐標系
1. 世界坐標系
2. 物體(模型)坐標系
3. 攝像機坐標系
4. 慣性坐標系
二、坐標空間
1. 世界空間
2. 模型空間
3. 攝像機空間
4. 裁剪空間
5. 屏幕空間
三、OpenGL ES 2 3D 空間
1. 變換發生的過程
2. 各個變換流程分解簡述
3. 四次變換與編程應用
四、工程例子
五、參考書籍
一、多坐標系
1. 世界坐標系
即物體存在的空間,以此空間某點為原點,建立的坐標系
世界坐標系是最大的坐標系,世界坐標系不一定是指“世界”,準確來說是一個空間或者區域,就是足以描述區域內所有物體的最大空間坐標,是我們關心的最大坐標空間;
-
例子
-
ep1:
比如我現在身處廣州,要描述我現在所在的空間,對我而言最有意義就是,我身處廣州的那里,而此時的廣州就是我關心的“世界坐標系”,而不用描述我現在的經緯坐標是多少,不需要知道我身處地球的那個經緯位置。
這個例子是以物體的方向思考的最合適世界坐標系;(當然是排除我要與廣州以外的區域進行行為交互的情況咯!)
-
ep1:
ep2:
如果現在要描述廣州城的全貌,那么對于我們而言,最大的坐標系是不是就是廣州這個世界坐標系,也就是所謂的我們最關心的坐標系;
這個例子是以全局的方向思考的最合適世界坐標系;世界坐標系主要研究的問題:
- 每個物體的位置和方向
- 攝像機的位置和方向
- 世界的環境(如:地形)
- 物體的運動(從哪到哪)
2. 物體(模型)坐標系
模型自身的坐標系,坐標原點在模型的某一點上,一般是幾何中心位置為原點
模型坐標系是會跟隨模型的運動而運動,因為它是模型本身的 “一部份” ;
模型內部的構件都是以模型坐標系為參考進而描述的;
ep:
比如有一架飛機,機翼位于飛機的兩側,那么描述機翼最合適的坐標系,當然是相對于飛機本身,機翼位于那里;飛機在飛行的時候,飛機本身的坐標系是不是在跟隨運動,機翼是不是在飛機的坐標中同時運動著。
3. 攝像機坐標系
攝像機坐標系就是以攝像機本身為原點建立的坐標系,攝像機本身并不可見,它表示的是有多少區域可以被顯示(渲染)
白色線所圍成的空間,就是攝像機所能捕捉到的最大空間,而物體則位于空間內部;
位于攝像機捕捉空間外的圖形會直接被剔除掉;
4. 慣性坐標系
它的 X 軸與世界坐標系的 X 軸平行且方向相同,Y 軸亦然,它的原點與模型坐標系相同
它的存在的核心價值是,簡化坐標系的轉換,即簡化模型坐標系到世界坐標系的轉換;
二、坐標空間
坐標空間就是坐標系形成的空間
1. 世界空間
世界坐標系形成的空間,光線計算一般是在此空間統一進行;
2. 模型空間
模型坐標系形成的空間,這里主要包含模型頂點坐標和表面法向量的信息;
第一次變換
模型變換(Model Transforms):就是指從模型空間轉換到世界空間的過程
3. 攝像機空間
攝像機空間,就是黃色區域所包圍的空間;
攝像機空間在這里就是透視投影,透視投影用于 3D 圖形顯示,反映真實世界的物體狀態;
透視知識擴展 《透視》
第二次變換
視變換(View Transforms):就是指從世界空間轉換到攝像機空間的過程
- 攝像機空間,也被稱為眼睛空間,即可視區域;
- 其中,LookAt(攝像機的位置) 和 Perspective(攝像機的空間) 都是在調整攝像空間;
4. 裁剪空間
圖形屬于裁剪空間則保留,圖形在裁剪空間外,則剔除(Culled)
標號(3)[視景體] ,所指的空間即為裁剪空間,這個空間就由 Left、Right、Top、Bottom、Near、Far 六個面組成的四棱臺,即視景體。
圖中紫色區域為視場角
從而引出,視場縮放為:
- 其次,頂點是用齊次坐標表示{x, y, z, w}, 3D 坐標則為{x/w, y/w, z/w}而 w 就是判斷圖形是否屬于裁剪空間的關鍵:
錐面 | 關系 |
---|---|
Near | z < -w |
Far | z > w |
Bottom | y < -w |
Top | y > w |
Left | x < -w |
Right | x > w |
即坐標值,不符合這個范圍的,都會被裁剪掉
坐標 | 值范圍 |
---|---|
x | [-w , w] |
y | [-w, w] |
z | [-w, w] |
第三次變換
投影變換(Projection Transforms): 當然包括正交、透視投影了,就是指從攝影機空間到視景體空間的變換過程
5. 屏幕空間
它就是顯示設備的物理屏幕所在的坐標系形成的空間,它是 2D 的且以像素為單位,原點在屏幕的幾何中心點
第四次變換(最后一次)
視口變換(ViewPort Transforms): 指從裁剪空間到屏幕空間的過程,即從 3D 到 2D
這里主要是關注像素的分布,即像素縱橫比;因為圖形要從裁剪空間投影映射到屏幕空間中,需要知道真實的環境的像素分布情況,不然圖形就會出現變形;
《OpenGL ES 2.0 (iOS)[02]:修復三角形的顯示》這篇文章就是為了修復屏幕像素比例不是 1 : 1 引起的拉伸問題,而它也就是視中變換中的一個組成部分。
- 像素縱橫比計算公式
三、OpenGL ES 2 3D 空間
1. 變換發生的過程
這個過程表明的是 GPU 處理過程(渲染管線);
變換過程發生在,頂點著色與光柵化之間,即圖元裝配階段;
編寫程序的時候,變換的操作是放在頂點著色器中進行處理;
右下角寫明了,總共就是四個變換過程:模型變換、視變換、投影變換、視口變換,經過這四個變換后,圖形的點就可以正確并如愿地顯示在用戶屏幕上了;
側面反應,要正確地渲染圖形,就要掌握這四種變換;
2. 各個變換流程分解簡述
-
階段一:追加 w 分量為 1.0 (第一個藍框)
這個階段不需要程序員操作
這里的原因是,OpenGL 需要利用齊次坐標去進行矩陣的運算,核心原因當然就是方便矩陣做乘法咯(R(4x4) 點乘 R(4x1) 嘛)!
-
階段二:用戶變換 (第二個藍框)
這個階段需要程序員操作,在 Vertex Shader Code 中進行操作
這個階段主要是把模型正確地通過 3D 變換(旋轉、縮放、平移)放置于攝像機的可視區域(視景體)中,包括處理攝像機的位置、攝像機的可視區域占整個攝像機空間的大小。
這個階段過后,w 就不在是 1.0 了
-
階段三:重新把齊次坐標轉換成 3D 坐標 (第三個藍框)
這個階段不需要程序員操作
要重新轉換回來的原因,也很簡單 ---- 齊次坐標只是為了方便做矩陣運算而引入的,而 3D 坐標點才是模型真正需要的點位置信息。
這個階段過后,所有的點坐標都會標準化(所謂標準化,就是單位為1),x 和 y 值范圍均在 [-1.0, 1.0 ]之間,z 就在 [ 0.0, 1.0 ] 之間;
x 和 y 值范圍均在 [-1.0, 1.0 ]之間,才能正確顯示,原因是 OpenGL 的正方體值范圍就是 [ -1.0, 1.0 ] 不存在其它范圍的值;而 z 的值范圍是由攝像機決定的,攝像機所處的位置就是 z = 0,的位置,所以 0 是指無限近,攝像機可視區的最遠處就是 z = 1, 所以 1 是指無限遠;
-
階段四:重新把齊次坐標轉換成 3D 坐標 (第四個藍框)
*這個階段需要程序員操作,在圖形渲染前要進行操作,即在 gldraw 前 **
這個階段核心的就是 ViewPort 和 DepthRange 兩個,前者是指視口,后者是深度,分別對應的 OpenGL ES 2 的 API 是:
函數 | 描述 |
---|---|
glViewport | 調整視窗位置和尺寸 |
glDepthRange | 調整視景體的 near 和 far 兩個面的位置 (z) |
glViewport | |
---|---|
void glViewport(GLint x, GLint y, GLsizei w, GLsizei h) | |
x, y 以渲染的屏幕坐標系為參考的視口原點坐標值(如:蘋果的移動設備都是是以左上角為坐標原點) | |
w, h 要渲染的視口尺寸,單位是像素 |
glDepthRange | |
---|---|
void glDepthRange(GLclampf n, GLclampf f) | |
n, f n, f 分別指視景體的 near 和 far ,前者的默認值為 0 ,后者的默認值為 1.0, 它們的值范圍均為 [ 0.0, 1.0 ], 其實就是 z 值 |
3. 四次變換與編程應用
- 下面這兩張圖片就是 Vertex Shader Code 中的最終代碼
#version 100
attribute vec4 v_Position;
uniform mat4 v_Projection, v_ModelView;
attribute vec4 v_Color;
varying mediump vec4 f_color;
void main(void) {
f_color = v_Color;
gl_Position = v_Projection * v_ModelView * v_Position;
}
v_Projection 表示投影變換;v_ModelView 表示模型變換和視變換;
- 第一次變換:模型變換,模型空間到世界空間 ( 1 -> 2 )
請看《OpenGL ES 2.0 (iOS)[02]:修復三角形的顯示》 這篇文章,專門講模型變換的。
- 余下的幾次變換,都是和攝像機模型在打交道
攝像機里面的模型
要完成攝像機正確地顯示模型,要設置攝像機位置、攝像機的焦距:
- 設置攝像機的位置、方向 --> (視變換) gluLookAt (ES 沒有這個函數),使要渲染的模型位于攝像機可視區域中;【完成圖中 1 和 2】
- 選擇攝像機的焦距去適應整個可視區域 --> (投影變換) glFrustum(視景體的六個面)、gluPerspective(透視) 、glOrtho(正交)( ES 沒有這三個函數) 【完成圖中 3】
- 設置圖形的視圖區域,對于 3D 圖形還可以設置 depth- range --> glViewport 、glDepthRange
- 第二次變換:視變換,世界空間到攝像機空間 ( 2 -> 3 )
上面提到, ES 版本沒有 gluLookAt 這個函數,但是我們知道,這里做的都是矩陣運算,所以可以自己寫一個功能一樣的矩陣函數即可;
// 我不想寫,所以可以用 GLKit 提供給我們的函數
/*
Equivalent to gluLookAt.
*/
GLK_INLINE GLKMatrix4 GLKMatrix4MakeLookAt(float eyeX, float eyeY, float eyeZ,
float centerX, float centerY, float centerZ,
float upX, float upY, float upZ);
函數的 eye x、y、z 就是對應圖片中的 Eye at ,即攝像機的位置;
函數的 center x、y、z 就是對應圖片中的 z-axis 可視區域的中心點;
函數的 up x、y、z 就是對應圖片中的 up 指攝像機上下的位置(就是角度);
- 第三次變換:投影變換,攝像機空間到裁剪空間 ( 3 -> 4 )
當模型處于視景體外時會被剔除掉,如果模型有一部分在視景體內時,模型的點信息只會剩下在視景體內的,其它的點信息不渲染;
/*
Equivalent to glFrustum.
*/
GLK_INLINE GLKMatrix4 GLKMatrix4MakeFrustum(float left, float right,
float bottom, float top,
float nearZ, float farZ);
這個是設置視景體六個面的大小的;
- 透視投影
對應的投影公式 :
使用 GLKit 提供的函數:
/*
Equivalent to gluPerspective.
*/
GLK_INLINE GLKMatrix4 GLKMatrix4MakePerspective(float fovyRadians, // 視場角
float aspect, // 屏幕像素縱橫比
float nearZ, // 近平面距攝像機位置的距離
float farZ); // 遠平面攝像機位的距離
- 正交投影
對應的投影公式 :
/*
Equivalent to glOrtho.
*/
GLK_INLINE GLKMatrix4 GLKMatrix4MakeOrtho(float left, float right,
float bottom, float top,
float nearZ, float farZ);
- 第四次變換:視口變換,裁剪空間到屏幕空間 ( 4 -> 5 )
這里就是設置 glViewPort 和 glDepthRange 當然 2D 圖形不用設置 glDepthRange ;
實際編程過程中的使用過程
-
第一步,如果是 3D 圖形的渲染,那么要綁定深度渲染緩存(DepthRenderBuffer),若是 2D 可以跳過,因為它的頂點信息中沒有 z 信息 ( z 就是頂點坐標的深度信息 );
- Generate ,請求 depth buffer ,生成相應的內存標識符
- Bind,綁定申請的內存標識符
- Configure Storage,配置儲存 depth buffer 的尺寸
- Attach,裝載 depth buffer 到 Frame Buffer 中
具體的程序代碼:
- 第二步,縮寫 Vertex Shader Code
#version 100
attribute vec4 v_Position;
uniform mat4 v_Projection, v_ModelView; // 投影變換、模型視圖變換
attribute vec4 v_Color;
varying mediump vec4 f_color;
void main(void) {
f_color = v_Color;
gl_Position = v_Projection * v_ModelView * v_Position;
}
一般是把四次變換寫成這兩個,當然也可以寫成一個;因為它們是一矩陣,等同于一個常量,所以使用的是 uniform 變量,變量類型就是 mat4 四乘四方陣(齊次矩陣);
- 第三步,就是外部程序賦值這兩個變量
注意,要在 glUseProgram 函數后,再使用 glUniform 函數來賦值變量,不然是無效的;*
依次完成 模型變換、視變換、投影變換,即可;它們兩兩用矩陣乘法進行連接即可;
如:modelMatrix 點乘 viewMatrix , 它們的結果再與 projectionMatrix 點乘,即為 ModelViewMatrix ;
GLKit 點乘函數,
GLK_INLINE GLKMatrix4 GLKMatrix4Multiply(GLKMatrix4 matrixLeft, GLKMatrix4 matrixRight);
- 第四步,如果是 3D 圖形,有 depth buffer ,那么要清除深度渲染緩存
使用 glClear(GL_DEPTH_BUFFER_BIT); 進行清除,當然之后就是要使能深度測試 glEnable(GL_DEPTH_TEST); 不然圖形會變形;
最好,也使能 glEnable(GL_CULL_FACE); 這里的意思就是,把在屏幕后面的點剔除掉,就是不渲染;判斷是前還是后,是利用提供的模型頂點信息中點與點依次連接形成的基本圖元的時鐘方向進行判斷的,這個 OpenGL 會自行判斷;
左為順時針,右為逆時針;
- 第五步,設置 glViewPort 和 glDepthRange
使用 OpenGL ES 提供的 glViewPort 和 glDepthRange 函數即可;
四、工程例子
Github: 《DrawSquare_3DFix》
五、參考書籍
《OpenGL ES 2.0 Programming Guide》
《OpenGL Programming Guide 8th》
《3D 數學基礎:圖形與游戲開發》
《OpenGL 超級寶典 第五版》
《Learning OpenGL ES For iOS》