从 OpenGL 入门,打开计算机图形学的大门。
从零搭建 OpenGL 开发环境以及一些简单的概念和操作
在学习 OpenGL 之前,先来了解一下计算机图形学
什么是计算机图形学
计算机图形学是一门很广泛的学科。通俗来讲,就是研究如何在计算机中表示图形,而这其中又有着很多高深的学问。计算机图形学可以帮助我们将二维或者三维的图形转换成可以显示在显示器的数据形式。在当今的很多电影、游戏中都使用到了计算机图形学的知识,将人为设计的一些二维图形或者三维模型加以渲染之后展示在大荧幕或者显示器上。
说到计算机图形学(Computer Graphics, CG),就不得不提一下计算机视觉(Computer Vision, CV)和数字图像处理(Digital Image Processing, DIP)。从一个角度来看,计算机图形学可以看作是计算机视觉和数字图像处理的逆过程。数字图像处理将现实的图像进行处理,然后计算机视觉根据处理后的图像,生成另一些一些图像或者生成现实世界中的一些模型,而计算机图形学就是反过来将模型转换成图像。如果说 CV 和 DIP 是立足于现实世界,将现实搬向虚拟,那么 CG 就是将虚拟的模型转换成类似于现实中的图像,使得我们可以身临其境地体会到设计者所表达的虚拟世界。
虽然说 CG 与 CV、DIP 是两个相反的过程,但是他们在另一个角度上看也是互相交叉的。当我们使用 CG 处理图形的时候也会用到 DIP 对其进行特效增强,而使用 CV 的时候也需要使用 DIP 对于图像进行预处理。就比如前几天微软在巴塞罗那的 MWC2019 大会上发布的混合现实设备 HoloLens 2,他的 Azure Kinect 传感器采集四周环境的深度信息,加上摄像头的图像信息,利用 DIP 进行处理,形成计算机可以理解的图形。然后使用 CV 技术进行眼球和手部跟踪,收集使用者的操作,再加上 6DoF 跟踪、空间映射以及混合现实捕捉,最后通过其中的透视全息透镜、基于眼睛的渲染的显示效果,使用 CG 技术,将三维的立体计算机界面投影到眼睛中。HoloLens 使用就是 AR 技术,CG、CV、DIP 这三者缺一不可。
根据分类,CG 主要可以分为四个部分:建模(Modeling)、渲染(Rendering)、动画(Animation)和人机交互(Human-computer Interaction, HCI)。建模是使用几何模型在计算机中表示三维物体,而渲染就是使得这个三维模型更加真实或者炫酷的技术,其中光照的渲染是比较重要的一个研究方向。渲染完之后,还需要使其动起来,这就需要根据现实的物理原理对其进行合理的动画设计。最后使用人机交互让我们可以操控这些东西,就像 HoloLens 中的眼球和手部跟踪,可以以更加直观的方式对计算机图形进行操作。
什么是 OpenGL? OpenGL ES? Web GL? Vulkan? DirectX?
OpenGL
本质上只是一个由 Khronos 组织设计维护的规范,规范了每个函数应该如何执行以及输出值,这样一来,OpenGL 就有了一套统一的 API,上层的应用并不需要了解这个函数是如何实现的,只需要调用就可以了,然而实际上不同的 OpenGL 库也可能有着不同的实现以最大限度地发挥对应硬件的潜力。一般显卡驱动就包含了这个显卡的 OpenGL 的最佳实现库,装好相应的驱动之后,上层的应用不需要关注使用的是怎么样的显卡,只需要根据 OpenGL 的规范调用 API,就可以以最优化的方案调用这个硬件的资源。使用 OpenGL 的接口,可以调用支持 OpenGL 的硬件,在屏幕上显示模型或者绘制图形。
OpenGL ES
是 OpenGL 的一个子集,去除了一些绘制复杂图形等非必要的冗余 API,保留最核心的部分,同时兼顾了一些功耗的管理,针对移动端做了一定的优化,目前主要用于移动端等嵌入式设备。OpenGL ES 使得编写代码更舒服,不需要判断硬件然后再调用显卡商为了提高性能专门加入的各种 extension,重新赋予了 OpenGL 完美的跨平台能力。
WebGL
同样是 Khronos 组织来维护, 基于 OpenGL ES 2.0 设计的一套 JavaScript API 规范,但其实质上依旧是运行在 OpenGL 上。使用 JavaScript 调用 WebGL 可以在任何兼容的浏览器上绘制 2D 和 3D 图形,调用 GPU 等硬件加速,使得 Web 界面的图形绘制有着更好的性能。
Vulkan
同样是一个跨平台的图形接口规范。和其他规范相比,Vulkan 提供的 API 更加底层,并且可以更好地使用多个 CPU 核心,有着更加高的性能和均衡的 CPU-GPU 负载。作为一套 2015 年提出的比较年轻的 API,相对于 Direct3D 11 与 OpenGL 4 最初为单核心 CPU 设计的 API,Vulkan 在当今手机和 PC 端 CPU 核数飞速增长的情况下,无疑有着更加优秀的功耗和性能,只是这样一来复杂度和维护难度就明显会比 OpenGL 高。目前在 Unity3D 中已经进入了稳定版本
DirectX
是微软的一套图像接口规范,和其他规范不同的是,这套规范一般只能用于 Windows 平台,并且 DirectX 还包含图形, 声音, 输入, 网络的一整套游戏开发解决方案。和 OpenGL 相比,DirectX 更加注重于游戏领域,而 OpenGL 部分特性对于一些工业软件,比如 CAD 更加友好。在 Windows 上,DirectX 甚至可以封装成 OpenGL 库供应用调用。现在最新的 DX12 还加入了多核心多线程的支持,在性能方面也是很强的。
OpenGL 环境配置
现在,就使用 C++来开始第一个 OpenGL 的项目
首先,我们需要配置好环境。
这里使用的环境为Visual Studio 2019 Priview 4
+ OpenGL 4.5
+ GLAD
+ GLFW
首先我们新建一个include
目录和一个lib
目录,用于存放用到的库
GLAD
由于 OpenGL 的驱动是由各个显卡厂商编写的,大多数的函数的位置都无法在编译时确定下来,因此开发者需要在运行时获取函数地址并将其保存在一个函数指针中供以后使用,而GLAD 可以帮助我们管理 OpenGL 的函数指针。
GLAD
的使用比较简单,可以使用在线服务生成我们需要的代码
选择C/C++
为目标语言,使用最新版本4.6
的 API,然后选择Core
核心模式,直接生成代码。将得到的glad.h
放入include
目录,glad.c
直接加入到我们的项目当中就可以。
GLFW
GLFW
是一个专门针对 OpenGL 的 C 语言库,它提供了一些渲染物体所需的最低限度的接口。它允许用户创建 OpenGL 上下文,定义窗口参数以及处理用户输入。我们不需要单独为不同的平台使用不同的 API 来创建窗口,只需要调用这些库就可以很简单处理创建窗口,定义 OpenGL 上下文以及处理用户输入。
GLFW
使用起来也不复杂,对于 Windows 系统,官网上就有编译好的二进制文件下载。当然,我们也可以选择下载源码使用 CMake 编译。
将glfw.h
放入include
目录,然后将glfw3.lib
放入libs
目录
VS
VS 的配置也很简单,首先创建一个空白的 C++项目,然后在解决方案资源管理器右键项目属性。
- VC++目录
- 包含目录加入刚才创建的
include
目录 - 库目录中加入刚才创建的
libs
目录
- 包含目录加入刚才创建的
- 链接器-输入
- 附加依赖项中加入
opengl32.lib
和glfw3.lib
- 附加依赖项中加入
初始化
首先,我们需要初始化OpenGL
, GLAD
, GLFW
这三个东西
1 | GLFWwindow* initWindow(const char* title) { |
定义顶点
首先,定义三角形的三个顶点,分别在窗口的左下角、右下角以及顶部中央。
在 OpenGL 中,其坐标系为标准化设备坐标。x, y 和 z 值都是在-1.0
到1.0
之间,并且坐标系的中心在屏幕的中央。
1 | float vertices[] = { |
使用顶点缓冲对象(VBO)在内存中管理这些顶点,并且告诉程序如何解析这些数据。
使用顶点数组对象(VAO)存储这些顶点属性调用。
1 | // 顶点数组对象 |
着色器
着色器语言 GLSL(OpenGL Shading Language)用于编写着色器,其语法类似于 C 语言。
有了着色器,我们就可以描述我们图形的颜色
顶点着色器
1 | // tri.vs.glsl |
#version 450 core
表示 4.5 版本的核心模式layout (location = 0)
表示输入的数据位于第一层in vec3
表示aPos
是一个vec3
类型的输入数据
然后在main()
函数里面表示这个点的数据如何映射到屏幕上面,这里是这里将 x, y, z 轴的坐标映射到屏幕,而最后一个 w 则用于透视除法当中。
片段着色器
1 | // tri.fs.glsl |
out vec4 FragColor
表示FragColor
是一个vec4
类型的输出数据在
main()
中给这个输出数据赋予对应的颜色,其类型为RGBA
使用
然后,写一个 Shader 类来编译并链接这两个着色器
1 | // shader.h |
1 | // shader.cpp |
这样,就定义好一个着色器的类,我们可以通过这个类读取指定的顶点着色器和片段着色器的文件并且编译为一个着色器程序。具体的用法可以参照下面。
画三角形
设定好着色器和 VAO 之后,就可以在循环里面直接画三角形了。
1 | // 着色器 |
结果:
彩色三角形
尝试一下把上面的三角形编程彩色的三角形
首先,我们修改一下顶点的定义,将颜色信息一同放在顶点中
1 | // 顶点 |
然后修改一下解析方式,将一组数据分成两层,每层 3 个数据。
1 | // 解析顶点数据 |
然后修改一下顶点着色器,从第二层读取出颜色信息并且输出到vertexColor
当中
1 | // tri.vs.glsl |
修改片段着色器,捕获vertexColor
信息并将其输出
1 | // tri.fs.glsl |
然后就可以看到彩色的三角形了
为什么只需要设置三个顶点的颜色就可以获得彩色的三角形呢?
因为片段着色器对其进行了片段插值
对于三角形的三个顶点,我们明确指出了需要渲染的颜色,因此他们也被正确渲染成纯正的绿/红/蓝色。而对于三角形内的任意一个片段,程序并不知道我们所需要的颜色。
在一个线段上,如果两个顶点分别是红色和绿色。那么,线段中间的片段的颜色就会被渲染成 50%红色+50%绿色这种线性的插值结果。
因此,在三角形中,根据这个内部片段距离三个顶点的距离,这个片段的颜色也就被渲染成 x%蓝色+x%绿色+x%红色,最终得出这个呈现出所有颜色的调色盘。
EBO 的使用
为了重复利用顶点资源,我们可以使用 EBO,利用顶点索引来绘制多个图案
1 | unsigned int drawColorTriangles() { |
使用和之前有一点不同,使用glDrawElements
替代glDrawArrays
进行渲染
1 | if (show_color_triangle) { |
得到以下结果
画线
画线也是和画三角形差不多,也是通过 VAO 保存下来然后在渲染循环内使用
1 | unsigned int drawLine() { |
使用
1 | glBindVertexArray(VAO_line); |
使用 ImGui
现在,我们为这个小程序加入一个控制台,这里使用的是 ImGui。
虽然官方并没有给出如何使用的教程,但是也有详细的例子。按照example
里面的做法,将主目录下的源代码引入到我们的项目中,然后再根据我们的环境引入相应的impl
代码
ImGui 默认使用的是gl3w
,因此我们选哟在imconfig.h
中声明使用的库
1 |
最后在main.cpp
中include
头文件就可以使用了
1 |
初始化
1 | // 初始化ImGui |
然后在渲染循环中调用 ImGui 绘制 UI
1 | while (!glfwWindowShouldClose(window)) { |
可以得到以下界面,通过 GUI 可以控制图形的显示以及颜色
使用 Uniform 控制颜色
我们可以使用 uniform 来控制着色器的颜色,uniform 相当于一个在着色器里面的全局变量,通过改变这个变量可以改变输出的颜色。
1 | // color_tri.fs.glsl |
然后再渲染循环内动态设置triColor
的值
1 | changeColorShader.Use(); |