前言
我的环境是macOS High Sierra + Xcode 10 + glfw + glew, 相关的配置方法请参考这篇博客.
关于OpenGL我还是零基础, 所以本文所说的仅是我个人目前的理解, 所以欢迎讨论和纠错.
关于OpenGL
OpenGL是一个用于进行图形绘制的库 (OpenCV是用于图像处理的库), 一些高级语言和框架中的几何绘制以及GUI组件的绘制实际上都是对OpenGL的调用.
OpenGL是一个底层的库, 所以它的代码非常繁琐, 比如你需要写好几十行代码才能画出一条线. 这使得它的学习门槛比较高 (我大二的时候想自学结果被劝退), 但是优点是速度快且灵活度高.
OpenGL的绘制过程类似于源代码的编译过程, 分为多个模块, 其中每一个模块都是一个Shader.
编译过程 (忽略预处理和链接过程)
OpenGL绘制过程 (简略版)
Shader
中文译名 着色器 . 是OpenGL绘制过程中不可缺少的模块. 每个Shader都是一个程序, 有自己的语言和语法. 目前来看, 我们可见的Shader分为两种: Vertex Shader 和 Fragment Shader. 前者用来计算要绘制图形的每个顶点的坐标, 后者用来计算要绘制图形的填充颜色. 试着接受OpenGL的世界观吧.
绘制的大概流程
- 以float数组的形式准备好数据 (顶点坐标)
- 编译链接Shader程序
- 将数据从内存拷贝至显存 (即后文中的 Buffer )
- 将显存中的数据喂给Shader程序
- 绘制
绘制三角形
triangle.cpp
#include <iostream>
#define GLEW_STATIC
#include <GL/glew.h>
#include <GLFW/glfw3.h>
using namespace std;
const GLchar* vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 position;\n"
"void main() {\n"
"gl_Position = vec4(position, 1.0);\n"
"}\0";
const GLchar* fragmentShaderSource = "#version 330 core\n"
"out vec4 color;\n"
"void main() {\n"
"color = vec4(0.2f, 0.4f, 1.0f, 1.0f);\n"
"}\n\0";
GLuint vertexShader, fragmentShader, shaderProgram;
GLuint VAO, VBO;
// 1.以float数组的形式准备好数据 (顶点坐标)
float vertices[] = {
-0.5, -0.5, 0,
0, 0.5, 0,
0.5, -0.5, 0
};
void shaderInit() {
vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
}
void vertexObjectInit() {
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_DYNAMIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 3, (void*)0);
glEnableVertexAttribArray(0);
}
int main() {
// 准备工作
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
GLFWwindow* window = glfwCreateWindow(800, 600, "OpenGL (One)", NULL, NULL);
glfwMakeContextCurrent(window);
glewExperimental = GL_TRUE;
glewInit();
//2.编译链接Shader程序
shaderInit();
// 3.将数据从内存拷贝至显存
vertexObjectInit();
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
//4. 喂数据
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
//5.绘制
glfwSwapBuffers(window);
}
// 善后
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glfwTerminate();
return 0;
}
结果
逐行解释
这里不讨论"准备工作"的内容.
从上往下看, 首先是 Vertex Shader的源代码:
const GLchar* vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 position;\n"
"void main() {\n"
"gl_Position = vec4(position, 1.0);\n"
"}\0";
把内容提取出来:
#version 330 core
layout (location = 0) in vec3 position;
void main() {
gl_Position = vec4(position, 1.0);
}
第一行声明了vertex shader的版本号即 3.3.0
第二行声明了一个变量 position, 类型为 vec3 即长度为3的数组, 标识符 in 表示这个变量的值是从外面传递给它的, 类似于函数的参数. layout (location = 0) 表示这个变量的位置的值是 0, 这样外界能通过这个值来定位这个变量.
接着是它的 main 函数, 逻辑就是给变量 gl_Position 赋值, 它的类型是 vec4, 第4个值 1.0 用于透视变换目前可以先不管. gl_Position 为全局变量, 也就是 Vertex Shader 的输出 (在前面我们讲过, Vertex Shader 以原始的数组数据为输入, 输出顶点信息).
接着是 Fragment Shader的代码:
const GLchar* fragmentShaderSource = "#version 330 core\n"
"out vec4 color;\n"
"void main() {\n"
"color = vec4(0.2f, 0.4f, 1.0f, 1.0f);\n"
"}\n\0";
提取出来:
#version 330 core
out vec4 color;
void main() {
color = vec4(0.2f, 0.4f, 1.0f, 1.0f);
}
第二行表示这个Shader输出一个颜色值
第四行将这个颜色值赋值为蓝色
Fragment Shader 对每一个像素点着色, 所以OpenGL对每一个像素点都会调用一次 Fragment Shader, 在这里我们只绘制一个纯色图形, 所以它的逻辑也很简单.
接着是一些变量的声明和原始数据的float数组, 略.
接着是编译, 链接Shader:
void shaderInit() {
vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL
glCompileShader(fragmentShader);
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
}
首先是分别创建 Vertex Shader 和 Fragment Shader:
GLuint fooShader = glCreate(GL_FOO_SHADER);
然后创建一个Shader对象:
glShaderSource(fooShader, 1, &fooShaderSource, NULL);
其中第二个参数表示这个shader所对应的源文件的数量, 第三个参数的类型是 char** 说明他是个二维字符数组也就是一维字符串数组, 表示的是shader对应的源文件数组, 第四个参数如果是NULL, 说明每个源文件是以 '\0’结尾的, 否则是一个 int 数组表示的是每个源文件的长度.
然后编译这个 Shader:
glCompileShader(fooShader);
然后创建一个program, 相当于这几个Shader组合成一条流水线:
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, fooShader);
glAttachShader(shaderProgram, barShader);
glLinkProgram(shaderProgram);
最后是善后工作, 略.
下一部分是将原始数据喂给Shader, 比较难理解:
void vertexObjectInit() {
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
glGenBuffers(1, &VBO); // 这是第四行
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_DYNAMIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 3, (void*)0);
glEnableVertexAttribArray(0);
}
我来解释 VAO 和 VBO 是什么:
先说 VBO, 全称 Vertex Buffer Object, 其中 Buffer 就是显存的意思, OpenGL的运算是在显存中进行的, 所以很快, 因此我们要把数据从内存拷贝至显存.
第四行表示获取缓存的句柄, 第一个参数表示想要的缓存的个数, 第二个参数类型是 GLuint*, 表示句柄数组.
第五行将VBO和GL_ARRAY_BUFFER绑定.
第六行表示向GL_ARRAY_BUFFER (也就是VBO, 也就是缓存, 也就是显存) 拷贝数据. 第二个参数表示数据的字节数, 第三个参数表示数据的起始地址, 第四个参数表示这个数据的性质, 如果它的值是 GL_STATIC_DRAW, 表示这个数据不会更改, OpenGL会把它放到一般缓存中, 如果它的值是GL_DYNAMIC_DRAW, 表示这个数据会经常更改, OpenGL会把它放到高速缓存中.
第七行告诉Vertex Shader如何解析刚才存入显存中的数据. 第一个参数表示这些数据对应的Vertex Shader中的 position 变量的位置 ( 也就是 layout (location = 0) ), 第二个参数表示缓存中数据的个数, 第三个参数表示每个数据的类型, 第四个参数表示是否标准化数据, 第五个参数表示每次读取数据的步长, 第六个参数表示偏移量.
第八行表示的是将缓存数据送入Vertex Shader的操作.
然后再说 VAO , 它是一个缓存对象, 记录了一次向 Vertex Shader 解析数据的结果, 这样的话如果之后仍需要使用相同的顶点信息, 只需要调用
glBindVertexArray(VAO);
即可实现向 Vertex Shader 传递数据的操作.
接下来进入主函数, 首先是准备工作, 这个和 OpenGL 本身无关, 所以先略去.
然后是初始化 Shader 和 VAO.
然后进入一个循环, 这个循环类似 GUI 程序的时间循环.
循环体中先处理消息, 仍然类似 GUI 程序.
接着是
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
第一行表示设置清屏的颜色, 第二行表示用这个颜色清除屏幕. 第三行按字面意思理解, 即使用在 shaderInit() 函数中创建的 program, 第四行表示将原始数据送给 Vertex Shader, 第五行表示将顶点数组的数据绘制出来, 第二个参数表示起始下标, 第三个参数表示数据个数.
glfwSwapBuffers(window);
这一行表示将绘制的内容显示在屏幕上.
后话
我用了一整天的时间来搭建OpenGL的环境, 因为操作系统版本更新以及OpenGL本身的更新, 且他们的向后兼容性都极差, 使得市面上的很多书都没法用, 踩了很多坑.
OpenGL虽然繁琐且晦涩, 不过一旦你接受了它的设定, 慢慢熟悉了它的使用方法, 就会有一种掌控一切的支配感.