11e048df632d2bb287e211c9c4158edf
Learn OpenGL 6-1. 进入 3D 世界, 画一个正方体

预备知识

在前面文章 Learn OpenGL 2-2. “优雅地”地画一个长方形 中的结尾部分有一个关于长方体的实验。如果有做的同学会发现画出来的长方体和长方形并没有视觉上的差别,下面就来解释一下为什么。

坐标系统

将坐标变换为标准化设备坐标,接着再转化为屏幕坐标的过程通常是分步进行的,也就是类似于流水线那样子。在流水线中,物体的顶点在最终转化为屏幕坐标之前还会被变换到多个坐标系统(Coordinate System)。将物体的坐标变换到几个过渡坐标系(Intermediate Coordinate System)的优点在于,在这些特定的坐标系统中,一些操作或运算更加方便和容易,这一点很快就会变得很明显。对我们来说比较重要的总共有5个不同的坐标系统:

  • 局部空间(Local Space,或者称为物体空间(Object Space))
  • 世界空间(World Space)
  • 观察空间(View Space,或者称为视觉空间(Eye Space))
  • 裁剪空间(Clip Space)
  • 屏幕空间(Screen Space)

关于每个坐标系统的概念,建议详细阅读 坐标系统 - LearnOpenGL CN

至于为什么需要不同的坐标系统,这里用一个例子来简单说明:例如我们要建造一座大厦,从建模到施工到验收都由我们操控。

首先,我们需要设计大厦样式(建模)。而在设计的时候(我们是不知道具体的建造位置)所以这时候需要 局部空间 来解决这个问题,假定物体坐标都是 (0, 0, 0 ) 为起始位置来方便进行对大厦进行设计。

其次,设计完毕后,需要实际施工,这时候就需要把大厦放到世界的某个位置上(因为不可能所有大厦都建设在时候同一个地方),也就是将大厦的坐标从 局部空间 变为 世界坐标,也就是我们上节说到的变换,通过一系列的平移缩放旋转来达到目的。

最后,当大厦建设完毕后,我们来实地观察(毕竟不可能在家里就能观察到大厦吧)。这里需要有 观察空间 来模拟人眼(摄像机)的变化,例如我们观察大厦的外部和内部肯定是不同的。而为了更加真实地模拟人在现实世界中的体验,需要引入 裁剪空间,例如人眼的视觉范围并不是360°的,而且视力也不是可以看到无限远的地方。我们就需要把人眼(摄像机)可视范围外的物体都裁剪掉。

而开头的疑问也可以得到解答,为什么视觉上长方形和正方体视觉上没有区别,因为我们的观察空间默认是在原点(例如你正面看一堵白墙是不知道它的厚度的,要从侧面看才知道)。

局部空间、世界空间、观察空间
局部空间、世界空间、观察空间、裁剪空间

动手画画

这里就不详细介绍每个步骤,因为和前面文章中介绍的类似。因为这里我同时结合了纹理的部分,就不采用前面说的方式(定义 8 个顶点数组, 36 索引数组),而是采用定义 24 顶点数组,36 索引数组的方式(因为每个面对应的纹理坐标不能重复使用)。

定义顶点数组和索引数组

private let scale = GLfloat(UIScreen.main.bounds.width / UIScreen.main.bounds.height) // 屏幕缩放比例

// 顶点数组
private let vertices: [L6Vertex] = [

    // Front
    L6Vertex(position: (1, -scale, 1), color: (1, 0, 0, 1), texCoord: (1, 0)), // 0
    L6Vertex(position: (1, scale, 1), color: (0, 1, 0, 1), texCoord: (1, 1)), // 0
    L6Vertex(position: (-1, scale, 1), color: (0, 0, 1, 1), texCoord: (0, 1)), // 0
    L6Vertex(position: (-1, -scale, 1), color: (0, 0, 0, 1), texCoord: (0, 0)), // 0

    // Back
    L6Vertex(position: (-1, -scale, -1), color: (1, 0, 0, 1), texCoord: (1, 0)), // 0
    L6Vertex(position: (-1, scale, -1), color: (0, 1, 0, 1), texCoord: (1, 1)), // 1
    L6Vertex(position: (1, scale, -1), color: (0, 0, 1, 1), texCoord: (0, 1)), // 2
    L6Vertex(position: (1, -scale, -1), color: (0, 0, 0, 1), texCoord: (0, 0)), // 3

    // Left
    L6Vertex(position: (-1, -scale, 1), color: (1, 0, 0, 1), texCoord: (1, 0)), // 4
    L6Vertex(position: (-1, scale, 1), color: (0, 1, 0, 1), texCoord: (1, 1)), // 5
    L6Vertex(position: (-1, scale, -1), color: (0, 0, 1, 1), texCoord: (0, 1)), // 6
    L6Vertex(position: (-1, -scale, -1), color: (0, 0, 0, 1), texCoord: (0, 0)), // 7

    // Right
    L6Vertex(position: (1, -scale, -1), color: (1, 0, 0, 1), texCoord: (1, 0)), // 8
    L6Vertex(position: (1, scale, -1), color: (0, 1, 0, 1), texCoord: (1, 1)), // 9
    L6Vertex(position: (1, scale, 1), color: (0, 0, 1, 1), texCoord: (0, 1)), // 10
    L6Vertex(position: (1, -scale, 1), color: (0, 0, 0, 1), texCoord: (0, 0)), // 11

    // Top
    L6Vertex(position: (1, scale, 1), color: (1, 0, 0, 1), texCoord: (1, 0)), // 12
    L6Vertex(position: (1, scale, -1), color: (0, 1, 0, 1), texCoord: (1, 1)), // 13
    L6Vertex(position: (-1, scale, -1), color: (0, 0, 1, 1), texCoord: (0, 1)), // 14
    L6Vertex(position: (-1, scale, 1), color: (0, 0, 0, 1), texCoord: (0, 0)), // 15

    // Bottom
    L6Vertex(position: (1, -scale, -1), color: (1, 0, 0, 1), texCoord: (1, 0)), // 16
    L6Vertex(position: (1, -scale, 1), color: (0, 1, 0, 1), texCoord: (1, 1)), // 17
    L6Vertex(position: (-1, -scale, 1), color: (0, 0, 1, 1), texCoord: (0, 1)), // 18
    L6Vertex(position: (-1, -scale, -1), color: (0, 0, 0, 1), texCoord: (0, 0)), // 19
]

// 索引数组
private let indices: [GLubyte] = [
    // Front
    0, 1, 2,
    2, 3, 0,

    // Back
    4, 5, 6,
    6, 7, 4,

    // Left
    8, 9, 10,
    10, 11, 8,

    // Right
    12, 13, 14,
    14, 15, 12,

    // Top
    16, 17, 18,
    18, 19, 16,

    // Bottom
    20, 21, 22,
    22, 23, 20
]

因为我们这一节需要画一个正方体,所以需要将物理像素的长度和宽度设为一致(别忘了,OpenGL 中坐标系范围是 -1 到 1,所以长度和宽度并不是实际屏幕的物理像素比例,这里需要计算缩放比例)。

运行结果

可以看到,的确没有一点正方体的样子。为了检验我们的确画了一个正方体,可以让它旋转起来看看效果

传入变换矩阵

这里我们将正方体缩小到 0.5 倍并沿 Y 轴旋转(因为目前的可视区域是在 -1 到 1 之间,如果不缩小会出现一些因为正方体旋转而超过可视区域的情况)。

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

    ...

    effect.prepareToDraw()

        let radians = linearRadians()
    var modelMatrix = GLKMatrix4Rotate(GLKMatrix4Identity, radians, 0, 1, 0) // 沿 Y 轴旋转
    modelMatrix = GLKMatrix4Scale(modelMatrix, 0.2, 0.2, 0.2) 
    effect.modelMatrix = modelMatrix // 缩小 0.2 倍

    ...

    glDrawElements(
        GLenum(GL_TRIANGLES),
        GLsizei(indices.count),
        GLenum(GL_UNSIGNED_BYTE),
        nil
    )
}

<p align="center">
<video id="video" controls="" preload="auto" poster="https://images.xiaozhuanlan.com/photo/2019/49593ae7612e66072cb37e0a01dcbefd.png">
<source id="mp4" src="https://images.xiaozhuanlan.com/photo/2019/d84ee8b16abe23282dfcf5de8ffb4afd.mov"></video>

这的确有点像是一个立方体,但又有种说不出的奇怪。立方体的某些本应被遮挡住的面被绘制在了这个立方体其他面之上。之所以这样是因为OpenGL是一个三角形一个三角形地来绘制你的立方体的,所以即便之前那里有东西它也会覆盖之前的像素。因为这个原因,有些三角形会被绘制在其它三角形上面,虽然它们本不应该是被覆盖的。

这时候我们只需要开启深度测试以及剔除看不到的面即可。

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

    glEnable(GLenum(GL_CULL_FACE))
    glEnable(GLenum(GL_DEPTH_TEST))
    glClearColor(1, 1, 1, 1)
    glClear(GLbitfield(GL_COLOR_BUFFER_BIT) | GLbitfield
    (GL_DEPTH_BUFFER_BIT))

    ...
}

再运行看看,是不是看起来正常了很多?不过这样看起来有点单调,我们能不能换一些角度来看看这个正方体呢?在下一节中,我们会引入观察空间来处理。

上面的代码可以在 https://github.com/xurunkang/learnopengl 中找到。

小结

  • 了解到不同的坐标系统,以及温故知新,画了一个正方体。
© 著作权归作者所有
这个作品真棒,我要支持一下!
🧀 专栏介绍 - 记录日常开发和学习中遇到的知识/问题 🚀 专栏方向 目前专注于 - OpenGL - ...
0条评论
top Created with Sketch.