OpenGL (一): 基础知识 & 三角形

前言

我的环境是macOS High Sierra + Xcode 10 + glfw + glew, 相关的配置方法请参考这篇博客.
关于OpenGL我还是零基础, 所以本文所说的仅是我个人目前的理解, 所以欢迎讨论和纠错.

关于OpenGL

OpenGL是一个用于进行图形绘制的库 (OpenCV是用于图像处理的库), 一些高级语言和框架中的几何绘制以及GUI组件的绘制实际上都是对OpenGL的调用.
OpenGL是一个底层的库, 所以它的代码非常繁琐, 比如你需要写好几十行代码才能画出一条线. 这使得它的学习门槛比较高 (我大二的时候想自学结果被劝退), 但是优点是速度快且灵活度高.
OpenGL的绘制过程类似于源代码的编译过程, 分为多个模块, 其中每一个模块都是一个Shader.


编译过程 (忽略预处理和链接过程)

编译器
汇编器
执行
源代码
汇编代码
机器代码
结果

OpenGL绘制过程 (简略版)

Vertex Shader
Fragment Shader
绘制
原始数据
顶点 / 几何形状
填充颜色
结果

Shader

中文译名 着色器 . 是OpenGL绘制过程中不可缺少的模块. 每个Shader都是一个程序, 有自己的语言和语法. 目前来看, 我们可见的Shader分为两种: Vertex ShaderFragment Shader. 前者用来计算要绘制图形的每个顶点的坐标, 后者用来计算要绘制图形的填充颜色. 试着接受OpenGL的世界观吧.

绘制的大概流程

  1. 以float数组的形式准备好数据 (顶点坐标)
  2. 编译链接Shader程序
  3. 将数据从内存拷贝至显存 (即后文中的 Buffer )
  4. 将显存中的数据喂给Shader程序
  5. 绘制

绘制三角形

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);
}

我来解释 VAOVBO 是什么:
先说 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虽然繁琐且晦涩, 不过一旦你接受了它的设定, 慢慢熟悉了它的使用方法, 就会有一种掌控一切的支配感.

猜你喜欢

转载自blog.csdn.net/vinceee__/article/details/85151386