前言
这是一篇OpenGlES 系统学习教程,记录自己的学习过程。
环境: Xcode10.2 + OpenGL ES 3.0
目标: 3D 立方体
这里是demo,你的star和fork是对我最好的支持和动力。
效果展示
坐标系统
主要有5个不同的坐标系统:
- 局部空间(Local Space,或者称为物体空间(Object Space))
- 世界空间(World Space)
- 观察空间(View Space,或者称为视觉空间(Eye Space))
- 裁剪空间(Clip Space)
- 屏幕空间(Screen Space)
变换
为了将坐标从一个坐标系变换到另一个坐标系,我们需要用到几个变换矩阵,最重要的几个分别是模型
(Model)、观察
(View)、投影
(Projection)三个矩阵。我们的顶点坐标起始于局部空间
(Local Space),在这里它称为局部坐标
(Local Coordinate),它在之后会变为世界坐标
(World Coordinate),观察坐标
(View Coordinate),裁剪坐标
(Clip Coordinate),并最后以屏幕坐标
[图片上传中...(坐标系变换.png-d41a13-1555986546530-0)]
(Screen Coordinate)的形式结束。下面的这张图展示了整个流程以及各个变换过程做了什么:
- 局部坐标是对象相对于局部原点的坐标,也是物体起始的坐标。
- 下一步是将局部坐标变换为世界空间坐标,世界空间坐标是处于一个更大的空间范围的。这些坐标相对于世界的全局原点,它们会和其它物体一起相对于世界的原点进行摆放。
- 接下来我们将世界坐标变换为观察空间坐标,使得每个坐标都是从摄像机或者说观察者的角度进行观察的。
- 坐标到达观察空间之后,我们需要将其投影到裁剪坐标。裁剪坐标会被处理至-1.0到1.0的范围内,并判断哪些顶点将会出现在屏幕上
- 最后,我们将裁剪坐标变换为屏幕坐标,我们将使用一个叫做视口变换(Viewport Transform)的过程。视口变换将位于-1.0到1.0范围的坐标变换到由glViewport函数所定义的坐标范围内。最后变换出来的坐标将会送到光栅器,将其转化为片段。
以上摘自这里,感兴趣的朋友可以细读一番。
投影变换
透视投影
在现实生活中近大远小的效果称之为透视
。如铁轨的两条轨道,由于透视,在很远的地方看起来会相交一样,这就是透视投影想要模仿的效果,它通过透视投影矩阵
来完成,推导过程可以看这里。
上图是视椎体,透视投影图形化的过程。
如果要对视椎体进行完全控制,可以使用frustum方法,或者也可以使用更为直观的lookA方法。
// 根据给定的视椎体设置返回一个透视投影矩阵。近平面的矩形通过left、right、bottom和top定义。近平面和远平面的距离通过near和far定义
static func frustum(resultM4 result:UnsafeMutablePointer<MatrixArray<Float>>, _ left:Float, _ right:Float, _ bottom:Float, _ top:Float, _ nearZ:Float, _ farZ:Float)
// 根据eye朝向target的视线,以及up定义的上方向,返回一个透视投影矩阵。
static func lookAt(resultM4 result:UnsafeMutablePointer<MatrixArray<Float>>, eye:UnsafePointer<Vec3>, target:UnsafePointer<Vec3>, up:UnsafePointer<Vec3>)
正交投影
当使用正射投影时,每一个顶点坐标都会直接映射到裁剪空间中而不经过任何精细的透视除法(它仍然会进行透视除法,只是w分量没有被改变(它保持为1),因此没有起作用)。因为正射投影没有使用透视,远处的物体不会显得更小,所以产生奇怪的视觉效果。由于这个原因,正射投影主要用于二维渲染以及一些建筑或工程的程序,在这些场景中我们更希望顶点不会被透视所干扰
组合
把以上每个步骤创建的变换矩阵:模型矩阵、观察矩阵和投影矩阵组合起来。一个顶点坐标将会根据以下过程被变换到剪裁坐标系:
注意矩阵运算的顺序是相反的(记住我们需要从右往左阅读矩阵的乘法)。最后的顶点应该被赋值到顶点着色器中的gl_Position,OpenGL将会自动进行透视除法和裁剪。
3D立方体
顶点数据
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
// 我们设置视锥体的近裁剪面到观察者的距离为 0.1, 远裁剪面到观察者的距离为 100,视角为 35度,然后装载投影矩阵。默认的观察者位置在原点,视线朝向 -Z 方向,因此近裁剪面其实就在 z = -0.01 这地方,远裁剪面在 z = -100 这地方,z 值不在(-0.01, -100) 之间的物体是看不到的
Matrix.perspective(resultM4: &projectionMatrix, 35, Float(aspect), 0.1, 100) //透视变换,视角30°
// 设置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)
// 平移
// 设置 z 值 (-0.01,-100)之间
Matrix.matrixTranslate(resultM4: &modelViewMatrix, tx: 0, ty: 0, tz: -3)
var rotationMatrix = Matrix.matrix4(0)
Matrix.matrixLoadIdentity(resultM4: &rotationMatrix)
// 旋转
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);
最后注意点
- 需要手动开启深度缓存,否则立方体将丢失深度信息
glEnable(GLenum(GL_DEPTH_TEST))
注:OpenGL应该只需要开启就ok了,OpenGlES 还需要手动创建深度缓存(这点还未深究,有知道朋友可以留言)。如下
// 创建深度缓冲区
var depthRenderBuffer:GLuint = 0
glGenRenderbuffers(1, &depthRenderBuffer)
glBindRenderbuffer(GLenum(GL_RENDERBUFFER), depthRenderBuffer)
glRenderbufferStorage(GLenum(GL_RENDERBUFFER), GLenum(GL_DEPTH_COMPONENT16), width, height)
myDepthRenderBuffer = depthRenderBuffer