什么是OpenGL
OpenGL 是一种图形接口,比如 API 代表应用程序接口
由于 OpenGL 是一种图形 API,它允许我们做一些与图形相关的事情,特别的是它允许我们访问 GPU 也就是显卡,图形处理单元(Graphics Processing Unit)。
为了利用电脑或其他设备(比如手机)中强大的图形处理器,需要调用一些 API 访问固件。OpenGL 正好是允许访问和操作 GPU 的许多接口中的一种,当然我们也有一些其他的接口,比如 Direct3D、Vulcan 和 Metal 等等,所以某种角度来说 OpenGL 允许我们控制显卡。
OpenGL 核心本身只是一种规范,和 CPP 规范差不多。
设置OpenGL 和 创建窗口
我们会使用一个向我提供窗口创建和窗口管理的实际代码类库,不管 Windows、Mac 还是 Linux。GLFW是一个轻量级类库,它虽然不如 SDL 那么全面(实际上它就是个渲染器),但依然可以创建窗口、OpenGL context 以及给我们访问一些类似输入之类的基础东西。
glBegin(GL_TRIANGLES);
glVertex2f(-0.5f, -0.5f);
glVertex2f(0.0f, 0.5f);
glVertex2f(0.5f, -0.5f);
glEnd();
使用现代 OpenGL
顶点缓冲区和绘制三角形
现代 OpenGL 比那种传统 OpenGL 更具可编程性,它的扩展性更好也更加强大,你可以用它做许多事情,但代价是在绘制一个简单三角形前,我们实际上需要做很多设置。而前面我们已经用传统方法简单绘制了三角形,非常简单并且不需要什么设置。
对于现代 OpenGL 而言,首先我们需要能够创建一个顶点缓冲区,然后还要创建一个着色器
顶点缓冲区与着色器
顶点缓冲区就是一个内存缓冲区,就是一块用来存字节的内存。但是顶点缓冲区又和 C++ 中像字符数组的内存缓冲区不太一样,它是 OpenG 中的内存缓冲区,这意味着它实际上在显卡显存(Video RAM)上。
基本思路就是定义一些数据来表示三角形,把它放入显卡的 VRAM 中,然后发出 DrawCall 绘制指令。然后告诉显卡如何读取和解释这些数据,以及如何把它放到我们屏幕上,告诉显卡怎么做就是对显卡编程,这就是着色器,着色器只是一个运行在显卡上的程序,它是一堆我们可以编写的在显卡上以一种非常特殊的方式运行的代码。
简而言之我们有一个可以指定的内存,还有一些我可以指定的数据,告诉显卡:嘿,这是数据。然后从显卡那边说:好了,现在读一下这些数据并解释一下(例如屏幕上的位置),可能的话把它们连成一个三角形。这就是整件事的原理,也是 OpenGL 渲染的流程。
要注意 OpenGL 是作为一个状态机来运行的,这意味着你不必把它当作一个对象或任何类似的东西来对待,你所做的是设置一系列的状态
Alt Shift 截图! 很方便
unsigned int buffer;
glGenBuffers(1, &buffer); //获得一个缓冲区
glBindBuffer(GL_ARRAY_BUFFER, buffer); //绑定缓冲区
glBufferData(GL_ARRAY_BUFFER, 6 * sizeof(float),positions,GL_STATIC_DRAW); //缓冲区的大小和内容
/* Loop until the user closes the window */
while (!glfwWindowShouldClose(window))
{
/* Render here */
glClear(GL_COLOR_BUFFER_BIT);
glDrawArrays(GL_TRIANGLES, 0, 3); //绘制三角形
到这里只写了30%渲染需要的代码,所以执行只看只能看到黑屏,欲知后事如何,请看下集
顶点属性和内存布局
顶点属性
什么是顶点属性?OpenGL 渲染管线的工作原理是我们为我们的显卡提供数据,我们在显卡上存储一些内存,它包含了我们想要绘制的所有数据;然后我们使用一个着色器在显卡上读取数据,并且完全显示在屏幕上。
通常我们绘制几何图元的方式就是使用一个叫顶点缓冲区的东西,也就是一个存储在显卡上的内存缓冲区,所以当对着色器编程时实际上是从读取顶点缓冲区开始的,它需要知道缓冲区的布局,这个缓冲区包含的浮点数指定了每个顶点的位置、纹理坐标、法线之类的。
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0);
这两段代码告诉 OpenGL 缓冲区的布局是什么,理论上如果有一个着色器就可以看到在屏幕上看到三角形了。记得要启动顶点,enablevertexAttribute
着色器原理
着色器就是一个运行在显卡上的程序代码,它是我们可以在计算机上以文本或者字符串形式编写的代码,然后我们可以把它给 OpenGL 发到显卡上像其他程序一样编译链接,最后运行它,不同的是它是在我们的显卡上运行
我们希望能够告诉显卡该做些什么,显卡处理图形的速度要快得多,我们想要利用显卡的能力在屏幕上绘制图形。这并不意味着所有的工作必须在显卡上完成,CPU 有自己擅长的部分,也许之后我们可以将结果数据发送给显卡同时仍然在 CPU 上进行处理。
着色器类型
对于大多数图形编程,我们会把重点放在两种着色器:顶点着色器和片段着色器(又称像素着色器)。
渲染管线大致工作流程:我们在 CPU 上写了一堆数据,向显卡发送这些数据并且发出一个叫做 DrawCall 指令的东西,也绑定了某些状态,最后我们进入了着色器阶段,GPU 实际处理 DrawCall 指令并在屏幕上绘制一些东西。这个特定的过程基本上就是渲染管道,我们如何在屏幕上从数据到结果的。
现在当显卡开始绘制三角形时,着色器就派上用场了。顶点着色器和片段着色器是渲染管线两种不同的着色器类型,所以当我们真正发出 DrawCall 指令时,顶点着色器会被调用,然后片段着色器会被调用,最后我们会在屏幕上看到结果。
顶点着色器
它会被我们渲染的每个顶点调用,在这个例子中我们有一个三角形三个顶点,这意味着顶点着色器会被调用三次,每个顶点调用一次。顶点着色器的主要目的是告诉 OpenGL 你希望这个顶点在屏幕空间的什么位置。再强调一次,顶点着色器的主要目的是提供那些顶点的位置,如果有必要我们需要能够提供一些变换以便 OpenGL 能把这些数字转化成屏幕坐标,这样我们就能在窗口中看到我们的图形在对的位置。
片段着色器
一旦顶点着色器运行结束,我们就进入了管道的下一个阶段:片段着色器或者像素着色器。
虽然片段和像素在术语上有点小差别,但现在你可以把像素当成片段或者把片段想象成像素,因为片段着色器会为每个需要光栅化的像素运行一次。我们的窗口基本上是由像素组成的,我们指定的那三个顶点组成我们的三角形现在需要用实际的像素填充,这就是光栅化阶段所做的。
片段着色器或像素着色器就是对三角形中需要填充的每个像素调用一次,主要决定这个像素是什么颜色,这就是它的作用,它决定了像素的输出颜色,这样像素就可以用正确的颜色着色。
有些东西显然需要按像素计算例如光源。如果你在计算光源,每个像素都有一个颜色值,这个值是由很多东西决定:光源、环境、纹理、提供给表面的材质…所有这些一起来确定一个特定像素的正确颜色。显然这取决于一些输入,例如相机的位置在哪里,而这些所有的东西结束后你在片段着色器中的决定仅仅是单个像素的颜色,这就是片段着色器的作用。
写一个着色器
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>
static unsigned int CompileShader(unsigned int type, const std::string& source) {
unsigned int id = glCreateShader(type);
const char* src = source.c_str();
glShaderSource(id, 1, &src, nullptr);
glCompileShader(id);
//Syntax error handling
int result;
glGetShaderiv(id, GL_COMPILE_STATUS, &result);
if (result == GL_FALSE) {
//编译失败 获取错误信息
int length;
glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length);
char* message = (char*)alloca(length * sizeof(char));
glGetShaderInfoLog(id, length, &length, message);
std::cout << "Failed to compile" << (type == GL_VERTEX_SHADER ? "vertexShader"
: "fragmentShader ") << " shader!" << std::endl;
std::cout << message << std::endl;
glDeleteShader(id);
return 0;
}
return id;
}
//我们向OpenGL提供我们实际的着色器源代码,我的着色器文本。我们想让OpenGL编译那个程序,将这两个链接到一个
//独立的着色器程序,然后给我们一些那个着色器返回的唯一标识符,我们就可以绑定着色器并使用
static unsigned int CreateShader(const std::string& vertexShader, const std::string& fragmentShader) {
unsigned int program = glCreateProgram();
unsigned int vs = CompileShader(GL_VERTEX_SHADER, vertexShader);
unsigned int fs = CompileShader(GL_FRAGMENT_SHADER, fragmentShader);
//将这两个着色器附加到我们的程序上
glAttachShader(program, vs);
glAttachShader(program, fs);
//链接程序
glLinkProgram(program);
glValidateProgram(program);
//删除一些无用的中间文件
glDeleteShader(vs);
glDeleteShader(fs);
return program;
}
int main(void)
{
GLFWwindow* window;
/* Initialize the library */
if (!glfwInit())
return -1;
/* Create a windowed mode window and its OpenGL context */
window = glfwCreateWindow(640, 480, "Hello World", NULL, NULL);
if (!window)
{
glfwTerminate();
return -1;
}
/* Make the window's contex