0c0ab0ae5fa78885b758bd1b204e510d
Learn OpenGL 2-1. 画几个“点线面”

预备知识

在开始本章之前,我们先学习一些预备的概念知识(刚开始学习 OpenGL 会遇到一堆概念,可能会让人有点困惑,但是越学习到后面你会有豁然开朗的感觉)。

坐标系

由于 OpenGL 实际上是在 3D 空间下工作的,所以实际上 OpenGL 的坐标是由 (x, y, z) 三点组成。而坐标原点则是在屏幕中间。注意坐标长度是以单位 1 来定义的,并不是实际长度。从下面的图片,我们也可以看出:Y 轴比 X 轴长,但是单位是一样的的。
<img src="https://images.xiaozhuanlan.com/photo/2019/1b4e7a91bb39c69522c1df04e7bf7755.png" width = "300" height = "400" alt="坐标系" align=center />

Vertex Buffer Object

Vertex Buffer Object:顶点缓冲对象, VBO

VBO 的作用是可以一次性传输一大批顶点数据,因为 CPU 传输数据到 GPU 的速度较慢,所以我们最好一次性传输尽可能多的数据。

渲染管线

OpenGL 采用 C/S 模型,CPU 是 client 端,GPU 是 Server 端。CPU 传入顶点信息和纹理信息,GPU 输出图像到屏幕。
渲染流程大致如下图所示。

着色器

从上面图中可以发现三种着色器的存在:顶点着色器、几何着色器和片段着色器
着色器实际上是一段 OpenGL Shader Language(GLSL) 的代码,在这一节我们暂时不关注具体的着色器如何编写,直接利用 GLKit 提供的 GLKBaseEffect。

图元装配

图元装配实际上是将顶点着色器输出的所有顶点作为输入,并且将顶点装配成指定的图元形状

常见的图元形状有:
* GL_POINTS(点)
* GL_LINES (线)
* GL_TRIANGLES(三角形)

动手画画

设置上下文

这里和上一节实际上是差不多的,多了 EAGLContext.setCurrent(context) 的调用。要执行 OpenGL ES 的命令,我们需要设置当前渲染上下文。

override func viewDidLoad() {
    super.viewDidLoad()

    guard let context = EAGLContext(api: .openGLES2) else {
        fatalError()
    }

    (self.view as! GLKView).context = context
    EAGLContext.setCurrent(context)
}

定义顶点数据

预备知识中,我们讲到了坐标系的坐标是如何定义的,所以我们定义一个顶点最少需要 3 个参数(x, y, z)。
可以看到,我们定义了一个 L2Vertex 的结构体来声明当前顶点的坐标位置,以及我们声明了一个 vertices 变量来存储了 6 个坐标点。

import GLKit

typealias Position = (x: GLfloat, y: GLfloat, z: GLfloat)

struct L2Vertex {

    // 顶点坐标
    let position: Position
}

// 顶点数组
private let vertices: [L2Vertex] = [
    L2Vertex(position: (0, 0.7, 0)), // 点

    L2Vertex(position: (-1, 0.5, 0)), // 线
    L2Vertex(position: (1, 0.5, 0)), // 线

    // 三角形
    L2Vertex(position: (0, 0.5, 0)),
    L2Vertex(position: (-0.5, -0.5, 0)),
    L2Vertex(position: (0.5, -0.5, 0))
]

将顶点数据输入缓冲区

预备知识中,我们讲到了 OpenGL 是采用 C/S 模型的,所以我们现在需要准备好顶点数据,然后通过某个 API 来将数据从 CPU 传输到 GPU 中。

  1. 生成缓冲区对象
  2. 绑定缓冲区对象
  3. 初始化缓冲区数据

对应的代码则是

private func setupBuffer() {
    // 创建缓冲区对象
    glGenBuffers(1, &VBO)

    // 将上面生成的缓存区对象设置为当前缓冲区对象
    // 提示: OpenGL 自身就是一个状态机
    // GL_ARRAY_BUFFER 指顶点数组缓冲区对象
    // GL_ELEMENT_ARRAY_BUFFER 指索引缓冲区对象
    // 目前我们先使用 GL_ARRAY_BUFFER。
    // 后面会当顶点出现重复后,我们会使用 GL_ELEMENT_ARRAY_BUFFER 来减少顶点数据传输量。
    glBindBuffer(GLenum(GL_ARRAY_BUFFER), VBO)

    // 为缓冲区申请内存空间,并进行初始化
    //
    // 提示: 之前我很疑惑 glBufferData 是怎么知道把 vertices 数据赋值到 VBO 里面的。
    //      因为 API 并没有传入 VBO 参数,直到我理解上面的提示: OpenGL 自身就是一个状态机。
    //      glBindBuffer 其实已经把 VBO 设置为当前缓冲区对象了。所以下面的所有操作都是基于 VBO。
    //
    // vertices.elementsSize 表示初始化的大小
    // vertices 则是初始化元素
    glBufferData(
        GLenum(GL_ARRAY_BUFFER),
        vertices.elementsSize,
        vertices,
        GLenum(GL_STATIC_DRAW)
    )
}

注意:不要把 API 写错了,不然 Debug 的时候很痛苦。

缓冲区初始化三部曲:

1.生成缓冲区对象
<br/>glGenBuffers (GLsizei n, GLuint* buffers)

n: 生成缓冲区对象的个数
buffers: 接受生成缓冲区对象的 ID 指针
2. 绑定缓冲区对象
<br/>glBindBuffer (GLenum target, GLuint buffer)

target: 缓冲区需要存储的的东西,GL_ARRAY_BUFFER 或者 GL_ELEMENT_ARRAY_BUFFER
buffer: 当前使用的缓冲区对象的 ID
3. 初始化缓冲区数据
<br/>glBufferData (GLenum target, GLsizeiptr size, const GLvoid* data, GLenum usage)

target: 含义和上文一致
size: 写入数据的大小
data: 写入数据的指针
usage: 优化参数

传输顶点数据

由于我们需要传输的是顶点的坐标数据,而且 OpenGL 中的顶点属性默认都是关闭的,所以需要通过 glEnableVertexAttribArray 开启指定的顶点属性。然后通过 glVertexAttribPointer 来实现将数据从 CPU 传输到 GPU。

private func setupVertexAttributes() {
    // 着色器属性
    let position = GLuint(GLKVertexAttrib.position.rawValue)

    // 开启顶点着色器属性
    // 出于性能考虑,所有顶点着色器的属性都是关闭的。
    glEnableVertexAttribArray(position)

    // 传输顶点着色器属性
    // 这一步是将数据从 CPU 传送到 GPU
    glVertexAttribPointer(
        position, // 传输的顶点属性
        3, // 顶点属性的参数个数,这里是 (x, y, z),所以是 3 个。
        GLenum(GL_FLOAT), // 参数类型 -> GLfloat
        GLboolean(GL_FALSE), // 是否标准化 01 坐标系
        GLsizei(MemoryLayout<L2Vertex>.stride), // 步长, 相邻顶点属性间的间距
        nil
    )
}

传输顶点数据两部曲:

1. 开启指定顶点属性
<br/>glEnableVertexAttribArray (GLuint index)

index: 需要启用的顶点属性索引
2. 传输顶点属性
<br/>glVertexAttribPointer (GLuint indx, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid* ptr)

indx: 同上
size: 顶点属性参数的个数。例如这里传输的 position(x, y, z),所以是 3
type: 参数类型
normalized: 是否需要标准化
stride: 连续顶点属性间的间距,也就是步长
ptr: 指顶点属性在在传输数组中的偏移量

渲染

这里我们没有使用自定义的 Shader,而是直接使用 GLKit 提供的 GLKBaseEffect。在下一节,我们才会使用到自定义的 Shader。

private let effect: GLKBaseEffect = GLKBaseEffect()

override func viewDidLoad() {
    super.viewDidLoad()

    setupBuffer()
    setupVertexAttributes()
}

override func glkView(_ view: GLKView, drawIn rect: CGRect) {
    glClearColor(0, 0, 0, 1)
    glClear(GLenum(GL_COLOR_BUFFER_BIT))

    // 调用执行的着色器
    effect.prepareToDraw()

    // 绘制所需要的物体
    // 第一个参数是: 绘制的图元类型
    // 第二个参数是: 顶点数组的起始索引
    // 第三个参数是:打算绘制的顶点个数
    glDrawArrays(GLenum(GL_POINTS), 0, 1) // 绘制一个点
    glDrawArrays(GLenum(GL_LINES), 1, 2) // 绘制一条线
    glDrawArrays(GLenum(GL_TRIANGLES), 3, 3) // 绘制一个三角形
}

glDrawArrays (GLenum mode, GLint first, GLsizei count)
mode: 渲染的图元
first: 需要渲染数据的起始索引
count: 需要渲染数据的个数

例如 glDrawArrays(GLenum(GL_TRIANGLES), 3, 3) 代表的是想要渲染的数据是从 vertices 的第 4 个数据(也就是 vertices[3])起的连续 3 个数据(最后一个数据是 vertices[3+3] )

效果

如果我们的代码一切都正常,那我们就会看到下面这样的效果。如果看不到点,可以把模拟器屏幕进行截图,然后放大就可以看到。上面的代码可以在 https://github.com/xurunkang/learnopengl 中找到。
<img src="https://images.xiaozhuanlan.com/photo/2019/4094a69f933fde83fccb1d4faeaccb5f.png" width = "469" height = "800" alt="坐标系" align=center />

实验

可以尝试自己画一个长方形。(提示: 实际上可以分解为画两个三角形,答案在小结部分)

小结

  • 我们了解到如果通过 GLKBaseEffect 来渲染出基本的图元形状。
  • 了解到 VBO 的使用,以及如何将数据从 CPU 传输数据到 GPU 来渲染。

实验答案

在 vertices 后面增加 6 个坐标点

private let vertices: [L2Vertex] = [

    ...

    // 长方形
    L2Vertex(position: (0.5, -0.6, 0)),
    L2Vertex(position: (0.5, -0.8, 0)),
    L2Vertex(position: (-0.5, -0.6, 0)),

    L2Vertex(position: (0.5, -0.8, 0)),
    L2Vertex(position: (-0.5, -0.6, 0)),
    L2Vertex(position: (-0.5, -0.8, 0)),
]

在 override func glkView(_ view: GLKView, drawIn rect: CGRect) 后面增加

override func glkView(_ view: GLKView, drawIn rect: CGRect) {

    ...
        // 绘制长方形
    glDrawArrays(GLenum(GL_TRIANGLES), 6, 6)
}
© 著作权归作者所有
这个作品真棒,我要支持一下!
🧀 专栏介绍 - 记录日常开发和学习中遇到的知识/问题 🚀 专栏方向 目前专注于 - OpenGL - ...
0条评论
top Created with Sketch.