版本记录
版本号 | 时间 |
---|---|
V1.0 | 2018.08.10 |
前言
GLKit
框架的设计目标是为了简化基于OpenGL或者OpenGL ES的应用开发。 接下来几篇我们就解析一下这个框架。感兴趣的看下面几篇文章。
1. GLKit 框架详细解析(一)—— 基本概览
2. GLKit 框架详细解析(二)—— 一个详细示例和说明(一)
Setting Up the Buffers - 设置缓冲
现在,您想要开始生成和绑定缓冲区,将数据传递给它们,以便OpenGL
知道如何在屏幕上绘制方块。 首先在setupGL()
方法的底部添加以下辅助变量:
// 1
let vertexAttribColor = GLuint(GLKVertexAttrib.color.rawValue)
// 2
let vertexAttribPosition = GLuint(GLKVertexAttrib.position.rawValue)
// 3
let vertexSize = MemoryLayout<Vertex>.stride
// 4
let colorOffset = MemoryLayout<GLfloat>.stride * 3
// 5
let colorOffsetPointer = UnsafeRawPointer(bitPattern: colorOffset)
下面进行详细解析:
1)生成缓冲区时,需要指定有关如何从数据结构中读取颜色和位置的信息。 OpenGL期望
GLuint
用于颜色顶点属性。在这里,您使用GLKVertexAttrib
枚举将color
属性作为原始GLint
获取。然后将其转换为GLuint
- OpenGL方法调用的内容 - 并将其存储以用于此方法。2)与颜色顶点属性一样,您希望避免编写该长代码来存储和读取位置属性作为
GLuint
。3)在这里,您可以利用
MemoryLayout
枚举来获取步长,即在数组中Vertex
类型的item的大小(以字节为单位)。4)要获取与顶点颜色对应的变量的内存偏移量,再次使用
MemoryLayout
枚举,除非这次指定您希望GLfloat
的步幅乘以3。这对应于Vertex
结构中的x,y和z变量。5)最后,您需要将偏移量转换为所需的类型:
UnsafeRawPointer
。
准备好一些辅助常量后,您就可以创建缓冲区并通过VAO进行绘制。
1. Creating VAO Buffers - 创建VAO缓冲
在setupGL()
中添加的常量后面添加以下代码:
// 1
glGenVertexArraysOES(1, &vao)
// 2
glBindVertexArrayOES(vao)
第一行要求OpenGL生成或创建新的VAO。 该方法需要两个参数:第一个是要生成的VAO的数量 - 在这个例子中是1 - 而第二个参数指向GLuint
的指针,其中它将存储生成的对象的ID。
在第二行中,您告诉OpenGL绑定您创建并存储在vao
变量中的VAO
,并且任何即将进行的配置顶点属性指针的调用都应存储在此VAO中。 在进行绘制调用之前,OpenGL将使用您的VAO直到您解除绑定或绑定另一个VAO。
使用VAOs可以增加更多代码,但是通过不必编写代码行来绘制甚至最简单的几何图形所需的所有内容,这将节省大量时间。
创建并绑定VAO后,就可以创建和设置VBO了。
2. Creating VBO Buffers - 创建VBO对象
继续在setupGL()
的末尾添加此代码:
glGenBuffers(1, &vbo)
glBindBuffer(GLenum(GL_ARRAY_BUFFER), vbo)
glBufferData(GLenum(GL_ARRAY_BUFFER), // 1
Vertices.size(), // 2
Vertices, // 3
GLenum(GL_STATIC_DRAW)) // 4
与VAO一样,glGenBuffers
告诉OpenGL你想要生成一个VBO
并将其标识符存储在vbo
变量中。
创建VBO之后,现在将其绑定为glBindBuffer
调用中的当前VBO。绑定缓冲区的方法需要缓冲区类型和缓冲区标识符。 GL_ARRAY_BUFFER
用于指定绑定顶点缓冲区,并且因为它需要GLenum
类型的值。
对glBufferData
的调用是将所有顶点信息传递给OpenGL的地方。此方法需要四个参数:
- 1)指示传递数据的缓冲区。
- 2)指定数据的大小(以字节为单位)。在这个例子中,您在之前编写的
Array
上使用size()
辅助方法。 - 3)您要使用的实际数据。
- 4)告诉
OpenGL
您希望GPU如何管理数据。在这个例子中,您使用GL_STATIC_DRAW
,因为传递给图形卡的数据很少会发生变化(如果有的话)。这允许OpenGL针对给定场景进一步优化。
到目前为止,您可能已经注意到在Swift中使用OpenGL有一种模式,必须将某些变量或参数转换为特定于OpenGL的类型。 这些是类型别名,没有什么可以让你担心。 它使你的代码最初看起来更长或更难阅读,但是一旦你进入事物流程就不难理解。
您现在已将所有顶点的颜色和位置数据传递给GPU。 但是当你要求它在屏幕上绘制所有数据时,你仍然需要告诉OpenGL如何解释这些数据。 为此,请在setupGL()
的末尾添加此代码:
glEnableVertexAttribArray(vertexAttribPosition)
glVertexAttribPointer(vertexAttribPosition, // 1
3, // 2
GLenum(GL_FLOAT), // 3
GLboolean(UInt8(GL_FALSE)), // 4
GLsizei(vertexSize), // 5
nil) // 6
glEnableVertexAttribArray(vertexAttribColor)
glVertexAttribPointer(vertexAttribColor,
4,
GLenum(GL_FLOAT),
GLboolean(UInt8(GL_FALSE)),
GLsizei(vertexSize),
colorOffsetPointer)
您会看到另一组非常相似的方法调用。 这是每次要做的事情,以及他们采取的参数。 在您告诉OpenGL解释您的数据之前,您需要首先告诉它它甚至是什么解释。
对glEnableVertexAttribArray
的调用启用了position
的vertex
属性,以便在下一行代码中,OpenGL知道此数据是用于几何的位置。
glVertexAttribPointer
采用六个参数,以便OpenGL了解您的数据。 这是每个参数的作用:
- 1)指定要设置的属性名称。 您可以使用先前在方法中设置的常量。
- 2)指定每个顶点存在多少个值。 如果你回顾一下Vertex结构,你会看到,对于这个位置,有三个
GLfloat(x,y,z)
,对于颜色,有四个GLfloat(r,g,b,a)
。 - 3)指定每个值的类型,它对于位置和颜色都是浮点数。
- 4)指定是否要对数据进行规范化。 这几乎总是设置为false。
- 5)步幅stride的大小,这是一种奇特的方式,表示“包含每个顶点数据的数据结构体的大小,当它在数组中时。”在这里传递
vertexSize
。 - 6)位置数据的偏移量。 位置数据位于
Vertices
数组的最开头,这就是该值为nil的原因。
对glEnableVertexttribArray
和glVertexAttribPointer
的第二组调用是相同的,除了你指定有四个颜色组件(r,g,b,a),并且你传递一个指针,传递一个指针用于顶点数组书中每个顶点Vertices
的颜色内存的偏移量。
随着VBO及其数据准备就绪,是时候通过使用EBO告诉OpenGL
您的索引indices
了。 这将告诉OpenGL要绘制哪些顶点以及以什么顺序绘制。
3. Creating EBO Buffers - 创建EBO缓冲
在setupGL()
的底部添加以下代码:
glGenBuffers(1, &ebo)
glBindBuffer(GLenum(GL_ELEMENT_ARRAY_BUFFER), ebo)
glBufferData(GLenum(GL_ELEMENT_ARRAY_BUFFER),
Indices.size(),
Indices,
GLenum(GL_STATIC_DRAW))
这段代码看起来应该很熟悉。 它与您用于VBO的内容完全相同。 首先生成一个缓冲区并将其标识符存储在ebo
变量中,然后将此缓冲区绑定到GL_ELEMENT_ARRAY_BUFFER
,最后将Indices
数组数据传递给缓冲区。
要添加到此方法的最后一部分代码如下:
glBindVertexArrayOES(0)
glBindBuffer(GLenum(GL_ARRAY_BUFFER), 0)
glBindBuffer(GLenum(GL_ELEMENT_ARRAY_BUFFER), 0)
首先,解除绑定(分离)VAO,以便在此VAO上不进行任何进一步调用以设置缓冲区,属性指针或其他内容。 对顶点和元素缓冲区对象也是如此。 虽然没有必要,但解除绑定是一种很好的做法,可以通过不将设置和配置与错误的对象相关联来帮助您避免将来出现逻辑错误。
构建您的项目以确保它编译可以通过。
Tidying Up - 清理
您已经创建了几个需要清理的缓冲区。 在ViewController中添加以下方法来执行此操作:
private func tearDownGL() {
EAGLContext.setCurrent(context)
glDeleteBuffers(1, &vao)
glDeleteBuffers(1, &vbo)
glDeleteBuffers(1, &ebo)
EAGLContext.setCurrent(nil)
context = nil
}
使用上面的代码,您:
- 将
EAGLContext
设置为您的上下文 - 您一直在使用的那个。 - 删除VBO,EBO和VAO。
- 取出上下文并将上下文变量设置为nil以防止其他任何操作。
现在,添加以下方法:
deinit {
tearDownGL()
}
这是deinitializer
,它简单地称为拆解方法。
构建并运行项目 - 注意到还是相同的灰色屏幕? 关于图形编程和使用OpenGL的事情是,在看到事物之前,它通常需要大量的初始设置代码。
Introducing GLKBaseEffect - 引入GLKBaseEffect
现在是下一个话题的时候了:着色器Shaders
。
现代OpenGL使用所谓的可编程管道programmable pipeline
,使开发人员可以完全控制每个像素的渲染方式。 这为您提供了惊人的灵活性,并允许渲染一些华丽的场景和效果。 然而,权衡的是程序员的工作量比过去多。 着色器是用GLSL
(OpenGL着色语言)编写的,需要先编译才能使用。
使用GLKBaseEffect
,您不必担心编写着色器。 它可以帮助您用很少的代码实现基本的照明和着色效果。
要创建效果,请将此变量添加到ViewController:
private var effect = GLKBaseEffect()
然后,在glkView(_ view:,drawIn rect :)
的底部添加这一行:
effect.prepareToDraw()
这一行代码为您绑定和编译着色器,它在幕后完成所有操作而无需编写任何GLSL
或OpenGL
代码。 很酷,对吧? Build您的项目以确保它编译。
准备好缓冲区和效果后,您现在还需要三行代码来告诉OpenGL要绘制什么以及如何绘制它。 在刚刚添加的行的正下方添加以下行:
glBindVertexArrayOES(vao);
glDrawElements(GLenum(GL_TRIANGLES), // 1
GLsizei(Indices.count), // 2
GLenum(GL_UNSIGNED_BYTE), // 3
nil) // 4
glBindVertexArrayOES(0)
这是每个方法调用的部分。 对glBindVertexArrayOES
的调用绑定(附加)您的VAO,以便OpenGL使用它 - 以及所有设置和配置 - 用于即将进行的绘制调用。
glDrawElements()
是执行绘图的调用,它需要四个参数。 以下是他们每个的作用:
- 1)这告诉OpenGL你想要绘制什么。 在这里,您可以使用
GL_TRIANGLES
参数强制转换为GLenum
来指定三角形。 - 2)告诉OpenGL要绘制多少个顶点。 它被转换为
GLsizei
,因为这是方法所期望的。 - 3)指定每个索引中包含的值的类型。 您使用
GL_UNSIGNED_BYTE
因为Indices
是GLubyte
元素的数组。 - 4)指定缓冲区内的偏移量。 由于您使用的是
EBO
,因此该值为nil。
真相的时刻到了! 是时候构建和运行你的项目了,如下所示:
不错,但也不是你所期待的。 这不是正方形,也不是旋转。 这是怎么回事? 好吧,没有为GLKBaseEffect
设置属性,特别是投影和模型视图矩阵的transform
属性。
Setting Up the Geometry - 设置几何
首先看一下一些理论。
投影矩阵projection matrix
是告诉GPU如何在2D平面上渲染3D几何体的方式。可以把它想象成从你的眼睛通过屏幕中的每个像素画出一串线。绘制到屏幕的像素由每条线击中的最前面的3D对象确定。
GLKit有一些方便的功能来设置投影矩阵。您将要使用的那个允许您指定沿y轴的视野,纵横比以及近和远平面。
视野field of view
就像一个相机镜头。一个小视场(例如10)就像一个长焦镜头 - 它通过“拉”它们靠近你来放大图像。大视场(例如100)就像一个广角镜头 - 它使一切看起来更远。用于此的典型值约为65-75。
宽高比aspect ratio
是您要渲染的宽高比(例如,视图的宽高比)。它将其与用于y轴的视场结合使用,以确定沿x轴的视野。
近处和远处的平面是场景中“可视viewable”体积的边界框。如果某物比近平面更接近眼睛,或者远离远处平面,则不会渲染。
注意:这是一个常见的问题 - 你尝试渲染一些没有出现的东西。 要检查的一件事是物体实际上位于近平面和远平面之间。
将以下代码添加到glkViewControllerUpdate(_ :)
的底部:
// 1
let aspect = fabsf(Float(view.bounds.size.width) / Float(view.bounds.size.height))
// 2
let projectionMatrix = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(65.0), aspect, 4.0, 10.0)
// 3
effect.transform.projectionMatrix = projectionMatrix
下面详细看一下这几个步骤:
- 1)计算
GLKView
的纵横比。 - 2)使用GLKit数学库中的内置辅助函数来创建透视矩阵; 你所要做的就是传递上面讨论的参数。 您将近平面设置为距离眼睛四个单位,将远平面设置为10个单位。
- 3)在效果
effect
的transform
属性上设置投影矩阵projection matrix
。
您需要在效果上再设置一个属性 - modelViewMatrix
。 这是应用于效果渲染的任何几何体的变换。
再一次,GLKit数学库带来了一些方便的函数,即使您对矩阵数学知之甚少,也能轻松执行平移,旋转和缩放。 将以下行添加到glkViewControllerUpdate(_ controller :)
的底部:
// 1
var modelViewMatrix = GLKMatrix4MakeTranslation(0.0, 0.0, -6.0)
// 2
rotation += 90 * Float(timeSinceLastUpdate)
modelViewMatrix = GLKMatrix4Rotate(modelViewMatrix, GLKMathDegreesToRadians(rotation), 0, 0, 1)
// 3
effect.transform.modelviewMatrix = modelViewMatrix
如果回头看看为方块设置顶点的位置,请记住每个顶点的z坐标为0。如果您尝试使用此透视矩阵渲染它,它将不会显示,因为它比近平面更靠近眼睛。
以下是使用上述代码修复问题的方法:
- 1)您需要做的第一件事是向后移动对象。 在第一行中,您使用
GLKMatrix4MakeTranslation
函数创建一个向后平移六个单位的矩阵。 - 2)接下来,您要使立方体旋转。 您将增加一个实例变量,该变量将在一秒钟内增加,用于跟踪当前旋转并使用
GLKMatrix4Rotate
方法通过旋转来更改当前变换。 它需要弧度,因此您可以使用GLKMathDegreesToRadians
方法进行转换。 - 3)最后,在效果的transform属性上设置模型视图矩阵。
最后,将以下属性添加到类的顶部:
private var rotation: Float = 0.0
最后一次构建并运行应用程序并查看结果:
旋转的方块!完美!
到这里,您已经了解了重要的概念和技术,如顶点和元素缓冲区,顶点属性,顶点数组对象和转换。 GLKBaseEffect
对于反射贴图,光照,雾等有很多效果,并且使用纹理加载类将纹理应用于几何体。
后记
本篇主要讲述了一个详细示例和说明,感兴趣的给个赞或者关注~~~~