✠OpenGL-2-图像管线

Posted itzyjr

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了✠OpenGL-2-图像管线相关的知识,希望对你有一定的参考价值。

第一个C++/OpengGL应用程序
#include <GL\\glew.h>
#include <GLFW\\glfw3.h>
#include <iostream>
using namespace std;

void init(GLFWwindow* window) {}

void display(GLFWwindow* window, double currrentTime) {
	glClear(GL_COLOR_BUFFER_BIT);
	glClearColor(1.0, 0.0, 0.0, 1.0);
}

int main() {
	if (!glfwInit())// 初始化GLFW库
		exit(EXIT_FAILURE);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	GLFWwindow* window = glfwCreateWindow(600, 600, "Chapter2", NULL, NULL);// 创建OpenGL上下文
	glfwMakeContextCurrent(window);// GLFW窗口与OpenGL上下文关联起来
	if (glewInit() != GLEW_OK)// 初始化GLEW库
		exit(EXIT_FAILURE);
	
	glfwSwapInterval(1);// 双缓冲区交换间隔(单位为:帧)
		
	init(window);

	while (!glfwWindowShouldClose(window)) {
		display(window, glfwGetTime());
		glfwSwapBuffers(window);// 整个帧渲染完成后交换后缓冲区和前缓冲区
		glfwPollEvents();// 轮询、处理已经在窗口系统事件队列中的事件
	}

	glfwDestroyWindow(window);// 通知GLFW销毁窗口
	glfwTerminate();// 终止运行
	exit(EXIT_SUCCESS);
}


GLFW窗口默认是使用双缓冲的。这意味着你有两个渲染缓冲区:一个前缓冲和一个后缓冲。前缓冲区是显示的,后缓冲区是渲染到的。
当整个帧渲染完成后,是时候交换后缓冲区和前缓冲区了,以便显示已经渲染的内容并开始渲染新帧。这是通过glfwSwapBuffers完成的。
有时,选择何时进行缓冲区交换是很有用的。使用glfwSwapInterval函数,可以选择从glfwSwapBuffers被调用到交换缓冲区之前驱动程序应该等待的监视器刷新的最小次数:glfwSwapInterval(1); 如果间隔为0,交换将在调用glfwSwapBuffers时立即发生,无需等待刷新。否则,至少要在每个缓冲区交换之间传递间隔追溯信息。当不希望测量等待垂直回溯所花费的时间时,使用0的交换间隔对于基准测试是有用的。然而,一个交换间隔可以避免屏幕撕裂。
交换间隔表示交换缓冲区之前等待的帧数,通常称为vsync(垂直同步)。默认情况下,交换间隔为0,但因为屏幕每秒只更新60-75次,所以大部分的画面不会被显示。而且,缓冲区有可能在屏幕更新的中间交换,出现屏幕撕裂的情况。所以,可以将该间隔设为1,即每帧更新一次。它可以设置为更高的值,但这可能导致输入延迟。

“双缓冲”意味着有两个颜色缓冲区—— 一个显示,一个渲染。渲染整个帧后,将交换缓冲区。缓冲用于减少不良的视
觉伪影。

顶点着色器和片段着色器

加载顶点之前,C++/OpenGL应用必须编译链接合适的GLSL顶点着色器和片段着色器程序,之后将它们载入管线。
即:GLSL → ➀关联 → ➁编译 → ➂链接 → ➃载入管线。

顶点着色器:
所有顶点都会被传入顶点着色器。顶点们会被一个一个地处理,即着色器会对每个顶点执行一次。对拥有很多顶点的大型复杂模型而言,顶点着色器会执行成百上千甚至百万次,这些执行过程通常是并行的。

例,画一个点:

#define numVAOs 1

GLuint renderingProgram;
GLuint vao[numVAOs];

GLuint createShaderProgram() {
	const char* vshaderSource =// 顶点着色器GLSL
		"#version 430    \\n"
		"void main(void) \\n"
		"{ gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }";

	const char* fshaderSource =// 片段着色器GLSL
		"#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);// 【0:创建着色器句柄】
	GLuint fShader = glCreateShader(GL_FRAGMENT_SHADER);
	GLuint vfprogram = glCreateProgram();// 【1:创建着色器程序对象】

	glShaderSource(vShader, 1, &vshaderSource, NULL);// 【2:关联着色器句柄与GLSL代码】
	glShaderSource(fShader, 1, &fshaderSource, NULL);
	glCompileShader(vShader);// 【3:编译着色器句柄】
	glCompileShader(fShader);

	glAttachShader(vfprogram, vShader);// 【4:将着色器附加到程序对象】
	glAttachShader(vfprogram, fShader);
	glLinkProgram(vfprogram);// 【5:链接程序对象】

	return vfprogram;
}

void init(GLFWwindow* window) {
	renderingProgram = createShaderProgram();// 创建着色器程序
	glGenVertexArrays(numVAOs, vao);// 【6:生成顶点数组】
	glBindVertexArray(vao[0]);// 【7:绑定顶点数组】
}

void display(GLFWwindow* window, double currentTime) {
	glUseProgram(renderingProgram);// 【8:使用着色器程序对象】
	glPointSize(30.0f);
	glDrawArrays(GL_POINTS, 0, 1);// 【9:绘制(渲染)顶点数组】
}

int main() {
	// ...
	init(window);
	while (!glfwWindowShouldClose(window)) {
		display(window, glfwGetTime());
		// ...
	}
	// ...
}


内置变量 gl_Position 用来设置顶点在 3D 空间的坐标位置,并发送至下一个管线阶段。 在顶点着色器中并不是必须给gl_Position指定“out”标签,因为gl_Position是预定义的输出变量。
out vec4 color;中“out”标签表明color变量是输出变量。
当准备将数据集发送给管线时是以缓冲区形式发送的。这些缓冲区最后都会被存入顶点数组对象(Vertex Array Object, VAO)中。即使应用程序完全没用到任何缓冲区,OpenGL仍需要在使用着色器时至少有一个创建好的VAO。init()函数最后两行就是用来创建OpenGL要求的VAO。

glDrawArrays(GLenum mode, Glint first, GLsizei count);

从顶点着色器出来的顶点是如何变成片段着色器中的像素的?

如上图,在 [顶点着色器] 和 [像素处理] 中间存在 [光栅化] 阶段,正是在这个阶段中图元(如点或三角形)转换成了像素集合。OpenGL中默认点的大小为1像素,这就是为什么我们的单点最终渲染成了单个像素。

片段着色器:
片段着色器用于为光栅化的像素指定颜色。
在片段着色器中有个预定义的输入变量gl_FragCoord,它让程序员可以访问输入片段的坐标。
例如,先通过glPointSize(30.0f);设置渲染点的大小为30像素。然后基于位置设置每个像素的颜色:

#version 430
out vec4 color;
void main(void) {
	if (gl_FragCoord.x < 200)
		color = vec4(1.0, 0.0, 0.0, 1.0);
	else
		color = vec4(0.0, 0.0, 1.0, 1.0);
}
检测OpenGL和GLSL错误

很难检测着色器是否失败的原因:GLSL编译发生在C++运行时。GLSL代码是运行在GPU而非CPU上的。GLSL错误并不会导致C++程序崩溃。

下面程序包含3个实用程序:

  • checkOpenGLError:检查OpenGL错误标志,即是否发生OpenGL错误。
  • printShaderLog:当GLSL编译失败时,显示OpenGL日志内容。
  • printProgramLog:当GLSL链接失败时,显示OpenGL日志内容。
bool checkOpenGLError() {
	bool foundError = false;
	int glErr = glGetError();
	while (glErr != GL_NO_ERROR) {
		cout << "glError:" << glErr << endl;
		glErr = glGetError();
	}
	return foundError;
}
/*为了允许分布式实现,可能会有几个错误标志。
如果任何一个错误标志记录了一个错误,则返回该标志的值,并在调用glGetError时将该标志重置为GL_NO_ERROR。
因此,如果要重置所有的错误标志,glGetError应该始终在循环中调用,直到它返回GL_NO_ERROR。*/

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

void printProgramLog(GLuint 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错误的示例如下:

GLuint createShaderProgram() {
	GLint vertCompiled;
	GLint fragCompiled;
	GLint linked;
	// ...

	// 捕获【编译】着色器时的错误
	glCompileShader(vShader);
	checkOpenGLError();//【check】
	glGetShaderiv(vShader, GL_COMPILE_STATUS, &vertCompiled);// 【get Compile Status】
	if (vertCompiled != 1) {
		cout << "vertex compilation failed" << endl;
		printShaderLog(vShader);//【printlog】
	}
	// fShader is the same as above! ...

	// 捕获【链接】着色器时的错误
	glAttachShader(vfProgram, vShader);
	glAttachShader(vfProgram, fShader);
	glLinkProgram(vfProgram);
	checkOpenGLError();//【check】
	glGetProgramiv(vfProgram, GL_LINK_STATUS, &linked);// 【get Link Status】
	if (linked != 1) {
		cout << "linking failed" << endl;
		printProgramLog(vfProgram);//【printlog】
	}
	return vfProgram;
}	
从文件读取GLSL源代码
#include <string>
#include <iostream>
#include <fstream>
string readShaderSource(const char* filePath) {
	string content;
	ifstream fileStream(filePath, ios::in);
	string line = "";
	while (!fileStream.eof()) {
		getline(fileStream, line);
		content.append(line + "\\n");
	}
	fileStream.close();
	return content;
}
GLuint createShaderProgram() {
	// ...与之前相同
	string vertShaderStr = readShaderSource("vertShader.glsl");
	string fragShaderStr = readShaderSource("fragShader.glsl");
	const char* vertShaderSrc = vertShaderStr.c_str();
	const char* fragShaderSrc = fragShaderStr.c_str();
	glShaderSource(vShader, 1, &vertShaderSrc, NULL);
	glShaderSource(fShader, 1, &fragShaderSrc, NULL);
	// ...与之前相同
}
从顶点构建对象

在调用glDrawArray()时,指定GL_TRIANGLES,可以生成三角形。在管线中指定3个顶点,这样顶点着色器会在每个迭代运行3遍。内置变量gl_VertextID会自增(初始值为0)。通过检测gl_VertextID的值,着色器设计为可以在每次运行时输出不同的点。
对于一个三角形所需要的3个顶点,经过光栅化阶段,生成一个填充过的三角形。

顶点着色器:

#version 430
void main() {
	if (gl_VertextID == 0)
		gl_Position = vec4(0.25, 0.25, 0.0, 1.0);
	else if (gl_VertextID == 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++/OpengGL应用程序——在display()函数中:

glDrawArrays(GL_TRIANGLES, 0, 3);// 参数:GLenum mode, Glint first, GLsizei count
场景动画
float x = 0.0f;// 三角形在x轴的位置
float inc = 0.01f;// 移动三角形的偏移量
void display(GLFWwindow* window, double currentTime) {
	glClear(GL_DEPTH_BUFFER_BIT);// 清除深度缓冲区(对于动画场景,要确保深度对比不会受旧的深度数据影响。)
	glClear(GL_COLOR_BUFFER_BIT);// 清除颜色缓冲区
	glClearColor(0.0, 0.0, 0.0, 1.0);// 每次将背景清除为黑色

	glUseProgram(renderingProgram);

	x += inc;// 切换至让三角形向右移动
	if (x > 1.0f) inc = -0.01f;// 沿x轴移动三角形
	if (x < -1.0f) inc = 0.01f;// 切换至让三角形向左移动

	GLuint offsetLoc = glGetUniformLocation(renderingProgram, "offset");// 获取GLSL代码中的“offset”指针
	/* glProgramUniform1f(GLuint program, GLint location, GLfloat v0) */
	glProgramUniform1f(renderingProgram, offsetLoc, x);// 将“x”中的值传给“offset”

	glDrawArrays(GL_TRIANGLES, 0, 3);
}
#version 430
uniform float offset;
void main() {
	if (gl_VertextID == 0)
		gl_Position = vec4(0.25 + offset, -0.25, 0.0, 1.0);
	else if (gl_VertextID == 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);
}

以上是关于✠OpenGL-2-图像管线的主要内容,如果未能解决你的问题,请参考以下文章

OpenGL必知必会——问题清单

基于Qt的OpenGL可编程管线学习- obj模型绘制

[Unity Shader] 渲染管线流程

我的OpenGL学习进阶之旅OpenGL ES 3.0实现了具有可编程着色功能的图形管线

我的OpenGL学习进阶之旅OpenGL ES 3.0实现了具有可编程着色功能的图形管线

TVTK-SV02 数据管线简介