前言
這是一篇OpenGlES 系統(tǒng)學(xué)習(xí)教程,記錄自己的學(xué)習(xí)過程。
環(huán)境: Xcode10.2 + OpenGL ES 3.0
目標(biāo): 3D 立方體
這里是demo,你的star和fork是對我最好的支持和動(dòng)力。
效果展示

坐標(biāo)系統(tǒng)
主要有5個(gè)不同的坐標(biāo)系統(tǒng):
- 局部空間(Local Space,或者稱為物體空間(Object Space))
- 世界空間(World Space)
- 觀察空間(View Space,或者稱為視覺空間(Eye Space))
- 裁剪空間(Clip Space)
- 屏幕空間(Screen Space)
變換
為了將坐標(biāo)從一個(gè)坐標(biāo)系變換到另一個(gè)坐標(biāo)系,我們需要用到幾個(gè)變換矩陣,最重要的幾個(gè)分別是模型(Model)、觀察(View)、投影(Projection)三個(gè)矩陣。我們的頂點(diǎn)坐標(biāo)起始于局部空間(Local Space),在這里它稱為局部坐標(biāo)(Local Coordinate),它在之后會(huì)變?yōu)?code>世界坐標(biāo)(World Coordinate),觀察坐標(biāo)(View Coordinate),裁剪坐標(biāo)(Clip Coordinate),并最后以屏幕坐標(biāo)[圖片上傳中...(坐標(biāo)系變換.png-d41a13-1555986546530-0)]
(Screen Coordinate)的形式結(jié)束。下面的這張圖展示了整個(gè)流程以及各個(gè)變換過程做了什么:

- 局部坐標(biāo)是對象相對于局部原點(diǎn)的坐標(biāo),也是物體起始的坐標(biāo)。
- 下一步是將局部坐標(biāo)變換為世界空間坐標(biāo),世界空間坐標(biāo)是處于一個(gè)更大的空間范圍的。這些坐標(biāo)相對于世界的全局原點(diǎn),它們會(huì)和其它物體一起相對于世界的原點(diǎn)進(jìn)行擺放。
- 接下來我們將世界坐標(biāo)變換為觀察空間坐標(biāo),使得每個(gè)坐標(biāo)都是從攝像機(jī)或者說觀察者的角度進(jìn)行觀察的。
- 坐標(biāo)到達(dá)觀察空間之后,我們需要將其投影到裁剪坐標(biāo)。裁剪坐標(biāo)會(huì)被處理至-1.0到1.0的范圍內(nèi),并判斷哪些頂點(diǎn)將會(huì)出現(xiàn)在屏幕上
- 最后,我們將裁剪坐標(biāo)變換為屏幕坐標(biāo),我們將使用一個(gè)叫做視口變換(Viewport Transform)的過程。視口變換將位于-1.0到1.0范圍的坐標(biāo)變換到由glViewport函數(shù)所定義的坐標(biāo)范圍內(nèi)。最后變換出來的坐標(biāo)將會(huì)送到光柵器,將其轉(zhuǎn)化為片段。
以上摘自這里,感興趣的朋友可以細(xì)讀一番。
投影變換
透視投影
在現(xiàn)實(shí)生活中近大遠(yuǎn)小的效果稱之為透視。如鐵軌的兩條軌道,由于透視,在很遠(yuǎn)的地方看起來會(huì)相交一樣,這就是透視投影想要模仿的效果,它通過透視投影矩陣來完成,推導(dǎo)過程可以看這里。

上圖是視椎體,透視投影圖形化的過程。
如果要對視椎體進(jìn)行完全控制,可以使用frustum方法,或者也可以使用更為直觀的lookA方法。
// 根據(jù)給定的視椎體設(shè)置返回一個(gè)透視投影矩陣。近平面的矩形通過left、right、bottom和top定義。近平面和遠(yuǎn)平面的距離通過near和far定義
static func frustum(resultM4 result:UnsafeMutablePointer<MatrixArray<Float>>, _ left:Float, _ right:Float, _ bottom:Float, _ top:Float, _ nearZ:Float, _ farZ:Float)
// 根據(jù)eye朝向target的視線,以及up定義的上方向,返回一個(gè)透視投影矩陣。
static func lookAt(resultM4 result:UnsafeMutablePointer<MatrixArray<Float>>, eye:UnsafePointer<Vec3>, target:UnsafePointer<Vec3>, up:UnsafePointer<Vec3>)
正交投影
當(dāng)使用正射投影時(shí),每一個(gè)頂點(diǎn)坐標(biāo)都會(huì)直接映射到裁剪空間中而不經(jīng)過任何精細(xì)的透視除法(它仍然會(huì)進(jìn)行透視除法,只是w分量沒有被改變(它保持為1),因此沒有起作用)。因?yàn)檎渫队皼]有使用透視,遠(yuǎn)處的物體不會(huì)顯得更小,所以產(chǎn)生奇怪的視覺效果。由于這個(gè)原因,正射投影主要用于二維渲染以及一些建筑或工程的程序,在這些場景中我們更希望頂點(diǎn)不會(huì)被透視所干擾

組合
把以上每個(gè)步驟創(chuàng)建的變換矩陣:模型矩陣、觀察矩陣和投影矩陣組合起來。一個(gè)頂點(diǎn)坐標(biāo)將會(huì)根據(jù)以下過程被變換到剪裁坐標(biāo)系:

注意矩陣運(yùn)算的順序是相反的(記住我們需要從右往左閱讀矩陣的乘法)。最后的頂點(diǎn)應(yīng)該被賦值到頂點(diǎn)著色器中的gl_Position,OpenGL將會(huì)自動(dòng)進(jìn)行透視除法和裁剪。
3D立方體
頂點(diǎn)數(shù)據(jù)
let vertices: [GLfloat] = [
// 前面
-0.5, 0.5, 0.5, 0.0, 1.0, // 前左上 0
-0.5, -0.5, 0.5, 0.0, 0.0, // 前左下 1
0.5, -0.5, 0.5, 1.0, 0.0, // 前右下 2
0.5, 0.5, 0.5, 1.0, 1.0, // 前右上 3
// 后面
-0.5, 0.5, -0.5, 1.0, 1.0, // 后左上 4
-0.5, -0.5, -0.5, 1.0, 0.0, // 后左下 5
0.5, -0.5, -0.5, 0.0, 0.0, // 后右下 6
0.5, 0.5, -0.5, 0.0, 1.0, // 后右上 7
// 左面
-0.5, 0.5, -0.5, 0.0, 1.0, // 后左上 8
-0.5, -0.5, -0.5, 0.0, 0.0, // 后左下 9
-0.5, 0.5, 0.5, 1.0, 1.0, // 前左上 10
-0.5, -0.5, 0.5, 1.0, 0.0, // 前左下 11
// 右面
0.5, 0.5, 0.5, 0.0, 1.0, // 前右上 12
0.5, -0.5, 0.5, 0.0, 0.0, // 前右下 13
0.5, -0.5, -0.5, 1.0, 0.0, // 后右下 14
0.5, 0.5, -0.5, 1.0, 1.0, // 后右上 15
// 上面
-0.5, 0.5, 0.5, 0.0, 0.0, // 前左上 16
0.5, 0.5, 0.5, 1.0, 0.0, // 前右上 17
-0.5, 0.5, -0.5, 0.0, 1.0, // 后左上 18
0.5, 0.5, -0.5, 1.0, 1.0, // 后右上 19
// 下面
-0.5, -0.5, 0.5, 0.0, 1.0, // 前左下 20
0.5, -0.5, 0.5, 1.0, 1.0, // 前右下 21
-0.5, -0.5, -0.5, 0.0, 0.0, // 后左下 22
0.5, -0.5, -0.5, 1.0, 0.0, // 后右下 23
]
// 索引
let indices:[GLubyte] = [
// 前面
0, 1, 2,
0, 2, 3,
// 后面
4, 5, 6,
4, 6, 7,
// 左面
8, 9, 11,
8, 11, 10,
// 右面
12, 13, 14,
12, 14, 15,
// 上面
18, 16, 17,
18, 17, 19,
// 下面
20, 22, 23,
20, 23, 21
]
矩陣變換過程
let width = frame.size.width
let height = frame.size.height
var projectionMatrix = Matrix.matrix4(0)
Matrix.matrixLoadIdentity(resultM4: &projectionMatrix)
let aspect = width / height
// 我們設(shè)置視錐體的近裁剪面到觀察者的距離為 0.1, 遠(yuǎn)裁剪面到觀察者的距離為 100,視角為 35度,然后裝載投影矩陣。默認(rèn)的觀察者位置在原點(diǎn),視線朝向 -Z 方向,因此近裁剪面其實(shí)就在 z = -0.01 這地方,遠(yuǎn)裁剪面在 z = -100 這地方,z 值不在(-0.01, -100) 之間的物體是看不到的
Matrix.perspective(resultM4: &projectionMatrix, 35, Float(aspect), 0.1, 100) //透視變換,視角30°
// 設(shè)置glsl投影矩陣
glUniformMatrix4fv(GLint(projectionMatrixSlot), 1, GLboolean(GL_FALSE), projectionMatrix.array)
var modelViewMatrix = Matrix.matrix4(0)
Matrix.matrixLoadIdentity(resultM4: &modelViewMatrix)
var viewMatrix = Matrix.matrix4(0)
Matrix.matrixLoadIdentity(resultM4: &viewMatrix)
var eyeVec3 = Vec3(x:0,y:0,z:3)
var targetVec3 = Vec3(x:0,y:0,z:0)
var upVec3 = Vec3(x:0,y:1,z:0)
Matrix.lookAt(resultM4: &viewMatrix, eye: &eyeVec3, target: &targetVec3, up: &upVec3)
glUniformMatrix4fv(GLint(viewSlot), 1, GLboolean(GL_FALSE), viewMatrix.array)
// 平移
// 設(shè)置 z 值 (-0.01,-100)之間
Matrix.matrixTranslate(resultM4: &modelViewMatrix, tx: 0, ty: 0, tz: -3)
var rotationMatrix = Matrix.matrix4(0)
Matrix.matrixLoadIdentity(resultM4: &rotationMatrix)
// 旋轉(zhuǎn)
Matrix.matrixRotate(resultM4: &rotationMatrix, angle: degreeY, x: 1, y: 0, z: 0)
Matrix.matrixRotate(resultM4: &rotationMatrix, angle: degreeX, x: 0, y: 1, z: 0)
var modelViewMatrixCopy = modelViewMatrix
Matrix.matrixMultiply(resultM4: &modelViewMatrix, aM4: &rotationMatrix, bM4: &modelViewMatrixCopy)
glUniformMatrix4fv(GLint(modelViewMatrixSlot), 1, GLboolean(GL_FALSE), modelViewMatrix.array);
最后注意點(diǎn)
- 需要手動(dòng)開啟深度緩存,否則立方體將丟失深度信息
glEnable(GLenum(GL_DEPTH_TEST))
注:OpenGL應(yīng)該只需要開啟就ok了,OpenGlES 還需要手動(dòng)創(chuàng)建深度緩存(這點(diǎn)還未深究,有知道朋友可以留言)。如下
// 創(chuàng)建深度緩沖區(qū)
var depthRenderBuffer:GLuint = 0
glGenRenderbuffers(1, &depthRenderBuffer)
glBindRenderbuffer(GLenum(GL_RENDERBUFFER), depthRenderBuffer)
glRenderbufferStorage(GLenum(GL_RENDERBUFFER), GLenum(GL_DEPTH_COMPONENT16), width, height)
myDepthRenderBuffer = depthRenderBuffer