目录
基于C++图形应用&管线概览
将 GLSL 程序载入这些着色器阶段也是 C++/OpenGL 应用程序的责任之一,其过程如下:
(1)使用 C++获取 GLSL 着色器代码,既可以从文件中读取,也可以硬编码在字符串中。
(2)创建 OpenGL 着色器对象,并将 GLSL 着色器代码加载到着色器对象中。
(3)用 OpenGL 命令编译并连接着色器对象,将它们装载到 GPU。
OpenGL类型
第一个C++/OpenGL应用程序
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>
using namespace std;
void init(GLFWwindow* window) { }
void display(GLFWwindow* window, double currentTime) {
glClearColor(1.0, 0.0, 0.0, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
}
int main(void) {
if (!glfwInit()) { exit(EXIT_FAILURE); }
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
GLFWwindow* window = glfwCreateWindow(600, 600, "Chapter 2 - program 1", NULL, NULL);
glfwMakeContextCurrent(window);
if (glewInit() != GLEW_OK) { exit(EXIT_FAILURE); }
glfwSwapInterval(1);
init(window);
while (!glfwWindowShouldClose(window)) {
display(window, glfwGetTime());
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwDestroyWindow(window);
glfwTerminate();
exit(EXIT_SUCCESS);
}
◍API (1)
GLSL类型
着色器——画一个点的程序
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>
using namespace std;
#define numVAOs 1
GLuint renderingProgram;
GLuint vao[numVAOs];
GLuint createShaderProgram() {
const char *vshaderSource =
"#version 430 \n"
"void main(void) \n"
"{ gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }";
const char *fshaderSource =
"#version 430 \n"
"out vec4 color; \n"
"void main(void) \n"
"{ color = vec4(0.0, 0.0, 1.0, 1.0); }";
GLuint vShader = glCreateShader(GL_VERTEX_SHADER);
GLuint fShader = glCreateShader(GL_FRAGMENT_SHADER);
GLuint vfprogram = glCreateProgram();
glShaderSource(vShader, 1, &vshaderSource, NULL);
glShaderSource(fShader, 1, &fshaderSource, NULL);
glCompileShader(vShader);
glCompileShader(fShader);
glAttachShader(vfprogram, vShader);
glAttachShader(vfprogram, fShader);
glLinkProgram(vfprogram);
return vfprogram;
}
void init(GLFWwindow* window) {
renderingProgram = createShaderProgram();
glGenVertexArrays(numVAOs, vao);
glBindVertexArray(vao[0]);
}
void display(GLFWwindow* window, double currentTime) {
glUseProgram(renderingProgram);
glPointSize(30.0f);
glDrawArrays(GL_POINTS, 0, 1);
}
int main(void) {
if (!glfwInit()) { exit(EXIT_FAILURE); }
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
GLFWwindow* window = glfwCreateWindow(600, 600, "Chapter 2 - program 2", NULL, NULL);
glfwMakeContextCurrent(window);
if (glewInit() != GLEW_OK) { exit(EXIT_FAILURE); }
glfwSwapInterval(1);
init(window);
while (!glfwWindowShouldClose(window)) {
display(window, glfwGetTime());
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwDestroyWindow(window);
glfwTerminate();
exit(EXIT_SUCCESS);
}
◍API (2)
◍API (3)
在加载顶点之前, C++/OpenGL 应用程序必须编译并链接合适的 GLSL 顶点着色器和片段着色器程序,之后将它们载入管线。
当调用 glDrawArrays()时, 管线中的 GLSL 代码开始执行。 现在可以向管线添加一些 GLSL 代码了。不管它们从何处读入,所有的顶点都会被传入顶点着色器。顶点们会被逐个处理,即着色器会对每个顶点执行一次。对拥有很多顶点的大型复杂模型而言,顶点着色器会执行成百上千甚至上百万次,这些执行过程通常是并行的。
display()函数所做的事情中包含调用glUseProgram(),用于将含有两个已编译着色器的程序载入 OpenGL 管线阶段(在 GPU 上! )。注意, glUseProgram()并没有运行着色器,它只是将着色器加载进硬件。
当准备将数据集发送给管线时,数据集是以缓冲区形式发送的。 这些缓冲区最后都会被存入顶点数组对象( Vertex Array Object, VAO)中。在本例中,我们向顶点着色器中硬编码了一个点,因此不需要任何缓冲区。但是,即使应用程序完全没有用到任何缓冲区, OpenGL 仍然需要在使用着色器的时候拥有至少一个创建好的VAO,所以这两行代码用来创建 OpenGL 要求的 VAO。
在顶点着色器,给 gl_Position 指定 out 标签不是必需的,因为 gl_Position 是预定义的输出变量。(GLSL内置类型,见:Built-in Variable (GLSL))
当准备将数据集发送给管线时,数据集是以缓冲区形式发送的。这些缓冲区最后都会被存入顶点数组对象( Vertex Array Object,VAO)中。
最后的问题就是从顶点着色器出来的顶点是如何变成片段着色器中的像素的。回忆一下,在顶点处理和像素处理中间存在着栅格化阶段。正是在这个阶段,图元(点或三角形)转换成了像素的集合。
栅格化
最终,我们 3D 世界中的点、三角形、颜色等全都需要展现在一个 2D 显示器上。这个 2D屏幕由栅格(即矩形像素阵列)组成。
当 3D 物体栅格化后, OpenGL 会将物体中的图元(通常是三角形)转化为片段。片段拥有关于像素的信息。 栅格化过程确定了为了显示由 3 个顶点确定的三角形需要绘制的所有像素的位置。
栅格化过程开始时,先对三角形的每对顶点进行插值。插值过程可以通过选项调节,就目前而言,使用下图所示的简单的线性插值就够了。原本的 3 个顶点被标记为红色。
如果我们不加入之前的那一行代码(或者配置时用 GL_FILL 而非 GL_LINE),插值过程将会继续沿着栅格线填充三角形的内部,如下图所示。
栅格化不仅可以对像素插值,任何顶点着色器输出的变量和片段着色器的输入变量都可以基于对应的像素进行插值。
像素操作——Z-buffer算法
当我们在 display()中使用 glDrawArrays()命令绘制场景中的物体时,我们通常期望前面的物体挡住后面的物体。这也可以推广到物体自身,我们通常期望看到物体的正对我们,而不是背对我们。
为了实现这个效果,我们需要执行隐藏面消除(Hidden Surface Removal,HSR)操作。基于场景需要, OpenGL 可以进行一系列不同的 HSR 操作。虽然这个阶段不可编程,但是理解它的工作原理也是非常重要的。我们不仅需要正确地配置它,之后还需要在给场景添加阴影时对它进行进一步操作。
OpenGL 可以精巧地协调两个缓冲区,即颜色缓冲区和深度缓冲区(也叫作 Z 缓冲区、Z-buffer),从而完成隐藏面消除。这两个缓冲区都和栅格的大小相同——对于屏幕上每个像素,在两个缓冲区都各有对应条目。
当绘制场景中的各种对象时, 片段着色器会生成像素颜色。像素颜色会存放在颜色缓冲区中,而最终颜色缓冲区会被写入屏幕。当多个对象占据颜色缓冲区中的相同像素时,必须根据最接近观察者的对象来确定要保留的像素颜色。
隐藏面消除按照如下步骤完成:
(1)在每个场景渲染前,将深度缓冲区全部初始化为表示最大深度的值。
(2)当片段着色器输出像素颜色时,计算它到观察者的距离。
(3)如果(对于当前像素)距离小于深度缓冲区存储的值,那么用当前像素颜色替换颜色缓冲区中的颜色,同时用当前距离替换深度缓冲区中的值;否则,抛弃当前像素。
这个过程即 Z-buffer 算法,其伪代码如下所示:
Color[][] colorBuf = new Color[pixelRows][pixelCols];// 初始化pixelRows行pixelCols列的二维数组(存像素颜色值)
double[][] depthBuf = new double[pixelRows][pixedCols];// 初始化pixelRows行pixelCols列的二维数组(存像素深度值)
for (each row and column) {// 初始化颜色和深度缓冲区
colorBuf[row][column] = backgroundColor;// 初始化所有像素的颜色值为背景颜色(深度值为“far away"处的颜色)
depthBuf[row][column] = far away;// 初始化所有像素的深度值为“far away”,即为远剪裁平面对应的值——1
}
for (each shape) {
// 当 shape 中某一个像素,比深度缓冲区中的像素,离近剪裁平面更近(也就是depth值更小)时;同时更新【两个】缓冲区
for (each pixel in the shape) {
if (depth at pixel < depthBuf value) {// shape中的像素深度值 小于 depthBuf中的深度值
depthBuf[pixel.row][pixel.col] = depth at pixel;// 替换[深度缓冲区]中位置[row][col]处的值
colorBuf[pixel.row][pixel.col] = color at pixel;// 替换[颜色缓冲区]中位置[row][col]处的值
}
}
}
// 绘制时,只用到颜色缓冲区(帧缓冲区),所以把它return使用。
return colorBuf;
检测 OpenGL 和 GLSL 错误
编译和运行 GLSL 代码的过程与普通代码的不同, GLSL 的编译发生在 C++运行时。另外一个复杂的点是 GLSL 代码并没有运行在 CPU 中(它运行在 GPU 中),因此操作系统并不总能捕获 OpenGL 运行时的错误。以上这两点使调试变得很困难,因为常常很难判断着色器的运行是否失败,以及为什么失败。
◍API (4)
最初,所有错误标志都设置为 GL_NO_ERROR 。
当前定义了以下错误:
- GL_NO_ERROR
没有记录任何错误。该符号常量的值保证为 0。 - GL_INVALID_ENUM
为枚举参数指定了不可接受的值。有问题的命令将被忽略,并且除了设置错误标志之外没有其他副作用。 - GL_INVALID_VALUE
数字参数超出范围。有问题的命令将被忽略,并且除了设置错误标志之外没有其他副作用。 - GL_INVALID_OPERATION
当前状态不允许指定的操作。有问题的命令将被忽略,并且除了设置错误标志之外没有其他副作用。 - GL_INVALID_FRAMEBUFFER_OPERATION
帧缓冲区对象不完整。有问题的命令将被忽略,并且除了设置错误标志之外没有其他副作用。 - GL_OUT_OF_MEMORY
没有足够的内存来执行该命令。记录此错误后,除了错误标志的状态外,GL 的状态未定义。 - GL_STACK_UNDERFLOW
尝试执行会导致内部堆栈下溢的操作。 - GL_STACK_OVERFLOW
尝试执行会导致内部堆栈溢出的操作。
当设置错误标志时,仅当 GL_OUT_OF_MEMORY 发生时,GL操作的结果才是未定义的。在所有其他情况下,生成错误的命令将被忽略,并且对 GL 状态或帧缓冲区内容没有影响。如果生成命令返回一个值,则返回0。如果glGetError
本身生成错误,则返回0。
下面是添加了『捕获OpenGL和GLSL异常』的完整程序代码:
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>
using namespace std;
#define numVAOs 1
GLuint renderingProgram;
GLuint vao[numVAOs];
/**
* ➊当 GLSL 代码【编译】失败时,显示 OpenGL 日志内容。
*/
void printShaderLog(GLuint shader) {
int len = 0;
int chWrittn = 0;
char *log;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &len);
if (len > 0) {
log = (char *)malloc(len);
glGetShaderInfoLog(shader, len, &chWrittn, log);
cout << "Shader Info Log: " << log << endl;
free(log);
}
}
/**
* ➋当 GLSL 【链接】失败时,显示 OpenGL 日志内容。
*/
void printProgramLog(int prog) {
int len = 0;
int chWrittn = 0;
char *log;
glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &len);
if (len > 0) {
log = (char *)malloc(len);
glGetProgramInfoLog(prog, len, &chWrittn, log);
cout << "Program Info Log: " << log << endl;
free(log);
}
}
/**
* ➌检查 OpenGL 错误标志,即是否发生 OpenGL 错误(编译 + 运行时)。
*/
bool checkOpenGLError() {
bool foundError = false;
GLenum glErr;
while ((glErr = glGetError()) != GL_NO_ERROR) {
cout << "glError: " << glErr << endl;
foundError = true;
}
return foundError;
}
GLuint createShaderProgram() {
GLint vertCompiled;
GLint fragCompiled;
GLint linked;
const char *vshaderSource =
"#version 430 \n"
"void main(void) \n"
"{ gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }";
const char *fshaderSource =
"#version 430 \n"
"out vec4 color; \n"
"void main(void) \n"
"{ color = vec4(0.0, 0.0, 1.0, 1.0); }";
GLuint vShader = glCreateShader(GL_VERTEX_SHADER);
GLuint fShader = glCreateShader(GL_FRAGMENT_SHADER);
GLuint vfprogram = glCreateProgram();
glShaderSource(vShader, 1, &vshaderSource, NULL);
glShaderSource(fShader, 1, &fshaderSource, NULL);
// catch errors while compiling shaders
glCompileShader(vShader);
checkOpenGLError();// GL compile✚✚✚✚✚✚✚✚✚✚✚✚
glGetShaderiv(vShader, GL_COMPILE_STATUS, &vertCompiled);
if (vertCompiled != GL_TRUE) {
cout << "vertex compilation failed" << endl;
printShaderLog(vShader);// GLSL compile✚✚✚✚✚✚✚✚✚✚✚✚
}
glCompileShader(fShader);
checkOpenGLError();// GL compile✚✚✚✚✚✚✚✚✚✚✚✚
glGetShaderiv(fShader, GL_COMPILE_STATUS, &fragCompiled);
if (fragCompiled != GL_TRUE) {
cout << "fragment compilation failed" << endl;
printShaderLog(fShader);// GLSL compile✚✚✚✚✚✚✚✚✚✚✚✚
}
// catch errors while linking shaders
glAttachShader(vfprogram, vShader);
glAttachShader(vfprogram, fShader);
glLinkProgram(vfprogram);
checkOpenGLError();// GL link✚✚✚✚✚✚✚✚✚✚✚✚
glGetProgramiv(vfprogram, GL_LINK_STATUS, &linked);
if (linked != GL_TRUE) {
cout << "linking failed" << endl;
printProgramLog(vfprogram);// GLSL link✚✚✚✚✚✚✚✚✚✚✚✚
}
return vfprogram;
}
void init(GLFWwindow* window) {
renderingProgram = createShaderProgram();
glGenVertexArrays(numVAOs, vao);
glBindVertexArray(vao[0]);
}
void display(GLFWwindow* window, double currentTime) {
glUseProgram(renderingProgram);
glPointSize(30.0f);
glDrawArrays(GL_POINTS, 0, 1);
}
int main(void) {
if (!glfwInit()) { exit(EXIT_FAILURE); }
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
GLFWwindow* window = glfwCreateWindow(600, 600, "Chapter 2 - program 3", NULL, NULL);
glfwMakeContextCurrent(window);
if (glewInit() != GLEW_OK) { exit(EXIT_FAILURE); }
glfwSwapInterval(1);
init(window);
while (!glfwWindowShouldClose(window)) {
display(window, glfwGetTime());
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwDestroyWindow(window);
glfwTerminate();
exit(EXIT_SUCCESS);
}
从顶点来构建一个三角形
在 C++/OpenGL 应用程序中(特别是在 glDrawArrays()调用中)我们指定 GL_TRIANGLES(而非
GL_POINTS),同时指定管线中有 3 个顶点。这样顶点着色器会在每个迭代运行 3 遍,内置变量
gl_VertexID
会自增(初始值为 0)。通过检测 gl_VertexID 的值,着色器可以在每次运行时输出不
同的点。前面说到,这 3 个顶点会经过栅格化阶段,生成一个填充过的三角形。
// 顶点着色器
#version 430
void main(void) {
if (gl_VertexID == 0) gl_Position = vec4( 0.25, -0.25, 0.0, 1.0);
else if (gl_VertexID == 1) gl_Position = vec4(-0.25, -0.25, 0.0, 1.0);
else gl_Position = vec4( 0.25, 0.25, 0.0, 1.0);
}
// C++/OpenGL 应用程序——在 display()函数中
...
glDrawArrays(GL_TRIANGLES, 0, 3);
场景动画
我们构建的main()函数只调用了init()一次,之后就重复调用display()。因此虽然前面所有的例子看起来都是静态绘制的场景,但实际上main()函数中的循环会让它们一次又一次地绘制。
因此,main()函数的结构已经可以支持动画了。我们只需要设计display()函数来随时间改变要绘制的内容。
场景的每一次绘制都叫作一帧,调用display()的频率叫作帧率。
#define numVAOs 1
GLuint renderingProgram;
GLuint vao[numVAOs];
GLuint offsetLoc;
float x = 0.0f;// location of triangle on x axis
float inc = 0.01f;// offset for moving the triangle
void init(GLFWwindow* window) {
renderingProgram = Utils::createShaderProgram("vertShader.glsl", "fragShader.glsl");
glGenVertexArrays(numVAOs, vao);
glBindVertexArray(vao[0]);
}
void display(GLFWwindow* window, double currentTime) {
glClear(GL_DEPTH_BUFFER_BIT);
glClearColor(0.0, 0.0, 0.0, 1.0);
glClear(GL_COLOR_BUFFER_BIT);// clear the background to black, each time
glUseProgram(renderingProgram);
x += inc;// move the triangle along x axis
if (x > 1.0f) inc = -0.01f;// switch to moving the triangle to the left
if (x < -1.0f) inc = 0.01f;// switch to moving the triangle to the right
offsetLoc = glGetUniformLocation(renderingProgram, "offset");// get ptr to "offset"
glProgramUniform1f(renderingProgram, offsetLoc, x);// send value in "x" to "offset"
glDrawArrays(GL_TRIANGLES, 0, 3);
}
顶点着色器代码如下:(片段着色器不变)
#version 430
uniform float offset;
void main(void) {
if (gl_VertexID == 0)
gl_Position = vec4(0.25 + offset, -0.25, 0.0, 1.0);
else if (gl_VertexID == 1)
gl_Position = vec4(-0.25 + offset, -0.25, 0.0, 1.0);
else
gl_Position = vec4(0.25 + offset, 0.25, 0.0, 1.0);
}
◍API (5)
OpenGL某些方面的数值——glGet()
你可能多次想要了解 OpenGL 某些方面的数值限制。例如,程序员可能需要知道几何着色器可以生成的最大输出数,或者可以为渲染点指定的最大尺寸。这些值中很多都依赖于实现, 即在不同的计算机上是不同的。 OpenGL 提供了使用 glGet()
命令来获取这些值的机制。基于查询的参数的不同类型, glGet()命令也有着不同的形式。例如,查询点的尺寸的最大值时,如下调用会将最小值和最大值(基于计算机上的 OpenGL 实现)放入名为 size 的数组,作为前两个元素。
int sizeA;
float sizeB[2];
glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &sizeA);
glGetFloatv(GL_POINT_SIZE_RANGE, sizeB);
cout << "MAX_TEXTURE_IMAGE_UNITS = " << sizeA << endl;
cout << "POINT_SIZE_RANGE = " << sizeB[0] << " ~ " << sizeB[1] << endl;
/****** 某机器的输出 ******/
MAX_TEXTURE_IMAGE_UNITS = 32
POINT_SIZE_RANGE = 1 ~ 63.375