✠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(GL_POINTS, 0, 1);
glDrawArrays(GLenum mode, Glint first, GLsizei count);
mode - 指定要渲染的图元类型。接受的符号常数有:GL_POINTS, GL_LINE_STRIP, GL_LINE_LOOP, GL_LINES, GL_LINE_STRIP_ADJACENCY, GL_LINES_ADJACENCY, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_TRIANGLES, GL_TRIANGLE_STRIP_ADJACENCY, GL_TRIANGLES_ADJACENCY, GL_PATCHES.
first - 指定已启用数组中的起始索引。
count - 指定要渲染的索引数。(索引数=1就对应一个vec3顶点)
使用很少的子例程调用来指定多个几何图元。

使用例子:glShaderSource(vShader, 1, &vshaderSource, NULL);
void glShaderSource(GLuint shader, GLsizei count, const GLchar** string, const GLint* length);
shader - 指定要替换其源代码的着色器对象的句柄。
count - 指定stringlength的数组中的元素个数。
string - 指定指向包含要加载到着色器的源代码的 [字符串指针数组]。
(如:const GLchar* s = “shader src”; const GLchar** string = &s;
或:char s1[] = “shader src1”; char s2[] = “shader src2”; char* string[] = s1, s2 ; )
length - 指定字符串长度的数组。如果lengthNULL,则假定每个字符串都以NULL结尾。如果length不是NULL的值,则它指向一个数组,该数组包含string的每个对应元素的字符串长度。length数组中的每个元素可以包含相应字符串的长度(空字符不计为字符串长度的一部分)或小于0的值以表示该字符串为空终止。
:将shader中的源代码设置为string指定的字符串数组中的源代码。(不扫描或解析源代码字符串; 它们只是复制到指定的着色器对象中。)

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

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

片段着色器:
对光栅化之后2D图像中的【每个[像素]】处理一次。3D物体的表面最终显示成什么样将由它决定,例如为模型的可见表面添加纹理,处理光照、阴影的影响等等。

片段着色器用于为光栅化的像素指定颜色。

在片段着色器中有个预定义的输入变量gl_FragCoord,它让程序员可以访问输入片段的坐标。

例如,先通过glPointSize(30.0f); 设置渲染点的大小为30像素。然后基于位置设置每个像素的颜色:

#version 430
out vec4 color;
void main(void) 
	if (gl_FragCoord.x < 200)// 屏幕坐标小于20的,显示红色
		color = vec4(1.0, 0.0, 0.0, 1.0);
	else
		color = vec4(0.0, 0.0, 1.0, 1.0);

OpenGL3.3中的图形管线


曲线细分着色器&几何着色器

曲线细分着色器:
可编程曲面细分阶段是最近加入OpenGL(在 4.0 版中)的功能。它提供了一个曲面细分着色器用以生成大量三角形,通常是网格形式。同时也提供一些可以以各种方式操作这些三角形的工具。例如,程序员可能需要以下图展示的方式操作一个曲面细分过的三角形网格。

几何着色器:
几何着色器赋予了一次操作一个图元的能力(“按图元”处理)。
最通用的图元是三角形。当我们到达几何着色器阶段时,管线肯定已经完成了将顶点组合为三角形的过程(这个过程叫作图元组装)。接下来几何着色器会让程序员可以同时访问每个三角形的所有顶点。
按图元处理有很多用途,既可以让图元变形,比如拉伸或者缩小,还可以删除一些图元,从而在渲染的物体上产生“洞”——这是一种将简单模型转化为复杂模型的方法。
几何着色器也提供了生成额外图元的方法。这些方法也打开了通过转换简单模型而得到复杂模型的大门。几何着色器有一种有趣的用法,就是在物体上增加表面纹理,如凸起、鳞甚至“毛发”。

在曲面细分阶段已经给程序员同时访问模型中所有顶点的能力后,再提供一个按图元运算的着色器阶段可能看起来有点多余。它们的区别是,曲面细分只在非常少数情况下提供了这个能力——尤其在模型是由曲面细分器生成的三角形网格时。几何着色器并没有提供同时访问所有顶点,即任何从 C++用缓冲区传来的顶点的能力。

光栅化&深度剖析

光栅化:
最终,我们 3D 世界中的点、三角形、颜色等全都需要展现在一个 2D 显示器上。这个2D 屏幕由光栅——矩形像素阵列组成
当 3D 物体光栅化后, OpenGL 将物体中的图元(通常是三角形)转化为片段。片段拥有关于像素的信息。光栅化过程确定了用以显示 3 个顶点所确定的三角形的所有像素需要绘制的位置。
光栅化过程开始时先对三角形的每对顶点进行插值。插值过程可以通过选项调节。就目前而言,使用下图所示的简单的线性插值就够了。原本的 3 个顶点标记为红色。
如果光栅化过程到此为止,那么呈现出的图像将会是线框模型。呈现线框模型也是OpenGL 中的一个选项。通过在 display()函数中 glDrawArrays()调用之前添加如下命令:glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

如果我们不加入之前的那一行代码(或者我们在其中使用 GL_FILL 而非 GL_LINE),插值过程将会继续沿着光栅线填充三角形的内部:

光栅化不仅可以对像素插值。任何【顶点着色器】【输出】的变量 和 【片段着色器】的【输入】变量都可以基于对应的像素进行插值。我们将会使用该功能生成平滑的颜色渐变,实现真实光照以及许多其他效果。


光栅化——深度剖析:
光栅化(Rasterize)可以翻译成“栅格化”或者“像素化”,就是把矢量图形转化成像素点的过程。我们屏幕上显示的画面都是由像素组成,而三维物体都是点线面构成的。要让点线面变成能在屏幕上显示的像素,就需要Rasterize(/ˈræstəraɪz/)这个过程。就是从矢量的点线面的描述,变成像素的描述。

3D世界中模型最终需要展现在一个2D显示器上,这个2D屏幕由光栅——矩形像素阵列组成。

如下图,左边是告诉计算机我有一个圆形,右边(一个放大了1200%的屏幕)就是计算机把圆形转换成可以显示的像素点,这个过程就是Rasterize。

①你告诉GL:要画条线。然后告诉它线两个端点的坐标是(0,0)和(0,10),那么GL自动脑补出中间10个点的坐标,这个过程就叫光栅化,脑补的方法叫线性插值。

②你告诉GL:要画个三角形。然后给它三个顶点的坐标,它会计算出三角形三条边会涉及到的所有像素点的坐标,坐标单位为像素,计算方式也是线性插值。

③你告诉GL:点(0,0)是白色,点(0,10)是黑色。假设单位1是1像素,那么光栅化就自动计算出中间10个点各自的颜色,并自动做过渡效果的逻辑运算,这个计算方式还是线性插值。

下图演示了从[左下角红色] 到 [右上角蓝色] 的渐变效果,图中白色箭头线指示了渐变的方向(左下角->右上角)。

从渐变效果图中可以看到:颜色的混合。

所谓线性插值也没啥,自己亲手去计算如上10个点的坐标,计算过程就是线性插值,就这么简单。

构成模型的那些顶点在经过各种矩阵变换后也仅仅是顶点。而由顶点构成的三角形要在屏幕上显示出来,除了需要3个顶点的信息以外,还需要根据这3个顶点确定出构成这个三角形的所有像素的信息,光栅化就是干这个的。

➊对于纯位置信息:
光栅化会根据三角形顶点的位置,来确定需要多少个像素点才能在屏幕上完美地展现出这个三角形形状,这是通过对顶点数据进行插值来完成的。如下图:

➋对纯颜色信息:
假设点(0,M)是纯白色,点(10,M)是纯黑色,这两点连成一条直线。光栅化首先确定需要多少个像素点才能构成这条直线,然后线性插值,完成颜色的过渡效果。
对于纹理也是一样的道理,因为纹理是颜色的像素点阵。假设有两顶点(P0,Q0)、(P1,Q1),它们对应的纹理坐标分别为(s0,t0)、(s1,t1),这两顶点连成一条直线,那么光栅化会将直线转化为系列像素(对于顶点坐标是系列位置信息,对于纹理坐标是系列颜色信息),所有这些像素都包含有自己的像素坐标和像素颜色。就简单假设这条直线经过光栅化的线性插值后包含10个像素,那么这10个像素,在位置上是线性紧邻的像素点,在颜色上是线性渐变的像素点。假设(P0,Q0)=(0,M),(P1,Q1)=(10,M)、(s0,t0)=0xFFFFFFFF、(s1,t1)=0xFF000000,则最终呈现效果就和下图一样。位置上线性渐变、颜色上线性渐变,这就是插值产生的效果,这就是光栅化。


着色器分两个部分:一个顶点,一个片段;中间就是光栅化。

[最先]是顶点阶段,计算出每个顶点的坐标、颜色、纹理坐标等等;
[中间]经过光栅化,把它们都进行线性插值;
[最后]片段阶段,可以把片段(fragment)理解成像素,在片段着色器程序里面你就已经拥有了每个像素的坐标、颜色、纹理坐标等等。

[片段着色器] 中的片段(fragment)是三维空间的点、线、三角形这些基本图元映射到二维平面上的映射区域,通常一个 fragment 对应于屏幕上的一个像素,但高分辨率的屏幕可能会用多个像素点映射到一个 fragment,以减少 GPU 的工作

片段着色器GLSL常见的3个内置变量:
gl_FragColor——【out】写像素颜色;被后续的固定管线使用。
      其内部声明是:mediump vec4 gl_FragColor;
gl_FragCoord——【in】只读,用于读取像素相对于窗口的坐标位置x,y,z,1/w; 这个是固定管线图元差值后产生的;z是深度值。
      其内部声明是:mediump vec4 gl_FragCoord;
gl_FrontFacing——【in】只读,用于判断像素是否属于front-facing primitive。
      其内部声明是:bool gl_FrontFacing;


下图模拟了一个分辨率为13×8的显示器屏幕。屏幕窗口坐标是以左上角为原点(0,0)的,坐标值的单位是px(像素)。图上标出了部分像素点的像素坐标。屏幕上显示出了一个绿色三角形(如果屏分辨率高,显示的就不是像素点效果)。

现分析【在屏幕上显示出一个有颜色的三角形】的整个处理流程:(假设在最简单的情形下)

➀OpenGL空间中有3个3D坐标(vec3类型)并且设置每个坐标处的颜色都是绿色(3个颜色数据),在C++程序中调用绘制三角形的GL函数glDrawArrays(GL_TRIANGLES, 0, 3); 将3个3D坐标数据传递进顶点着色器,程序同时也把3个颜色数据传递进顶点着色器;

➁在顶点着色器程序中将要绘制的三角形模型经[模型-视图矩阵] 和 [透视投影矩阵] 变换到有透视效果的相机空间,这时就形成新三角形(由变换后的3个3D坐标构成),将新三角形的3个3D坐标数据和对应的3个颜色数据传送到光栅化阶段;

➂光栅化要完成能在屏幕上显示的三角形,即要形成像素三角形。光栅化过程确定了用以显示3个顶点所确定的三角形的所有像素需要绘制的位置及颜色(都由线性插值产生)。确定了显示三角形的所有像素位置及颜色值后,就完成了光栅化工作。对于传递进来的颜色数据,由于3个颜色相同,所以线性插值后仍是原来的颜色(绿色)。如果3个顶点分别都有着不同的颜色,则颜色数据依三角形形状进行线性插值后,形成的三角形的所有像素的颜色值都将取自[线性平滑过渡颜色数据集]中的色值。接下来,将形成三角形的所有像素的插值位置和插值颜色,一个个地传递到片段着色器。

➃片段着色器基本功能就是给像素着色,在片段着色器中,将传递来的包含颜色信息的像素,直接定义out变量输出(当然也可进行颜色的其他处理操作,但对本演示,无需其他处理),这样就完成了像素着色预处理工作。然后再将数据(此阶段未用到的像素位置数据,以及显式out输出的像素颜色数据)传递到图形管线的下一阶段。

➄测试与混合阶段,对所有像素位置进行深度测试(其实还可能有其他测试);对所有像素颜色进行混合操作,混合(不同物体的)多种颜色为一种颜色。然后将处理后的像素数据传递到图形管线的下一阶段。

➅图像生成阶段,生成能正确在屏幕上显示的所需三角形图像(由像素位置集和像素颜色集组成)。

➆创建一个帧缓冲区来承载上一步产生的图像数据。最后,在屏幕上刷新出这一帧,也即完成了所要的结果:在屏幕上显示出了一个有颜色的三角形。


上述第➂步,光栅化阶段,为什么是形成三角形而不是其他,比如形成直线呢?
因为程序调用glDrawArrays函数的参数GL_TRIANGLES就明确指示形成三角形。

如何刚好让三个顶点分别有三种不同的颜色呢?

顶点着色器GLSL:
layout(location = 0) in vec3 position;// 从C++程序传递进来的顶点坐标
out vec4 posColor;// 输出颜色,先传递到光栅化阶段处理,然后将处理后的颜色传递到片段着色器
void main() 
	gl_Position = xxxMatrix * vec4(position, 1.0);// 输出矩阵变换后的位置,传递到光栅化阶段处理
	if (position == vec3(x1,y1,z1))
		posColor = vec4(1.0, 0.0, 0.0, 1.0);// 红(color取值范围[0,1])
	else if (position == vec3(x2,y2,z2))
		posColor = vec4(0.0, 1.0, 0.0, 1.0);// 绿
	else if (position == vec3(x3,y3,z3))
		posColor = vec4(0.0, 0.0, 1.0, 1.0);// 蓝

片段着色器GLSL:
in vec4 posColor;// 接收到像素颜色
out vec4 color;// 输出像素颜色,传递到图形管线的下一阶段处理
void main() 
	color = posColor;


像素操作(隐藏面消除、深度/颜色缓冲区&深度精度&深度测试、Z-Buffer算法、glClear())

隐藏面消除:
如下图,眼睛能看立方体到绿色的面,白色顶面和侧面看不到——隐藏面。既然看不到,就不应该浪费计算资源去渲染,因为没有意义。所以应该消除这此隐藏面。

当我们在 display()方法中使用 glDrawArrays()命令绘制场景中的物体时,我们通常期望前面的物体挡住后面的物体。这也可以推广到物体自身,我们通常期望看到物体的正面对着我们,而不是背对我们。
为了实现这个效果,我们需要隐藏面消除(Hidden Surface Removal, HSR)。基于场景需要的不同效果,OpenGL 可以进行一系列不同的 HSR 操作。虽然这个阶段不可编程,由OpenGL自动处理。但是理解它的工作原理是非常重要的。我们不仅需要正确地配置它,之后还需要在给场景添加阴影时对它进行进一步操作。

OpenGL 通过精巧地协调两个缓冲区完成隐藏面消除:【颜色缓冲区】和【深度缓冲区】。这两个缓冲区都和光栅的大小相同——即对于屏幕上 [每个像素],在两个缓冲区都各有一个对应条目

当绘制场景中的各种对象时,片段着色器会生成像素颜色。像素颜色会存放在【颜色缓冲区】中——颜色缓冲区最终会被写入屏幕。当多个对象占据颜色缓冲区中的相同像素时,必须根据哪个对象最接近观察者来确定保留哪个像素颜色

当 3D 模型完全“闭合”时,意味着内部永远不可见(例如对于立方体和金字塔),那么外表面的那些与观察者背离呈一定角度的部分将始终被同一模型的其他部分遮挡。也就是说,那些背离观察者的三角形不可能被看到(无论如何它们都会在隐藏面消除的过程中被覆盖),因此没有理由光栅化或渲染它们。

隐藏面消除步骤如下:
(1) 在每个场景渲染前,深度缓冲区全部初始化为表示最大深度的值1。
(2) 当像素颜色由片段着色器【输出时】,计算它到观察者的距离。
(3) 如果距离小于深度缓冲区存储的值(对当前像素),那么用当前像素颜色替换颜色缓冲区中的颜色,同时用当前距离替换深度缓冲区中的值,否则丢弃当前像素。

启用深度测试
在代码层面,如果不启用深度测试,即不调用glEnable(GL_DEPTH_TEST); 就没法完成“隐藏面消除”的效果,导致隐藏面的像素也会被看到,如下图所示:

也就是说,OpenGL自动处理了“隐藏面消除”的一些工作,但只有手动启用深度测试,才能把OpenGL自动处理的效果呈现出来。

深度缓冲区:
深度缓冲区用于存储每个位置x、y处的深度值。OpenGL中的深度值范围为 0.0~1.0。调用glClear(GL_DEPTH_BUFFER_BIT);就可以清除深度缓冲区,默认使用1.0来填充深度缓冲区。这时,其他在视锥体中的像素点就都小于1.0,所以会把每个像素的颜色缓冲区和深度缓冲区都更新。通过glEnable(GL_DEPTH_TEST);启用深度测试,通过glDepthFunc(GL_LESS);(若不写是默认的)来进行比较测试。最终的呈现就是根据哪个对象最接近观察者来确定保留哪个像素颜色。

深度缓冲区原理就是把一个距离观察平面(近裁剪面)的深度值(或距离)与窗口中的每个像素相关联。

深度缓冲区DEPTH_BUFFER 与帧缓冲区对应,用于记录上面每个像素的深度值,通过深度缓冲区,我们可以进行深度测试,从而确定像素的遮挡关系,保证渲染正确。

深度值精度
作为比较的深度缓冲,它是位于 0.0 ~ 1.0 之间的深度值,它会与要绘制的物体的 z 值进行比较。


当 z∈[1, 25]时,Fdepth∈[0.0, 0.5];当z∈[25, 50]时,Fdepth∈[0.5, 1.0]。
对于这种转换,可以看到,当物体非常接近近平面时,深度值会接近 0.0,当物体非常接近远平面时,深度值会接近 1.0 ,这种转换是一种线性的深度缓冲转换。
而在实践中,几乎不可能是这样的线性转换。
因为当 z 值很小的时候,非常接近近平面,此时我们的观察也会更加精细,而对于较远的物体,接近远平面了,对于它的观察也会比较粗略。
这就和人眼一样,很近处的物体(比如1米)当然看得很清了,如果看不清,走近一点(比如0.5米)就好了;而对于很远的物体(比如离自己100米),走近30米和走近80米,看得的结果差别不大。

所以在实际将 z 值转换为深度缓冲值,用到的是非线性的转换方程。


对于这种转换,可以看到,当 z∈[1, 2]时,Fdepth∈[0.0, 0.5],这就占据了深度值区间范围的50%;而在2.0之后的范围也才占据了50%。这就给了近处的物体一个很大的深度精度。
对于深度值的区间 0.0 到 1.0 ,其实这个区间的前半部分还是和近平面非常近的,不要以为深度值 0.5 就是位于近平面和远平面之间了,其实还非常接近近平面呢。

要绘制物体的 z 值就是在运用透视投影或者正交投影视时,介于近平面和远平面之间的任何值。

要把这个 z 值转换为 OpenGL 中的深度值,也就是介于 0.0 和 1.0 之间的值。

颜色缓冲区COLOR_BUFFER 就是帧缓冲区(FRAME_BUFFER),你需要渲染的场景最终每一个像素都要写入该缓冲区,然后由它在渲染到屏幕上显示。

帧缓冲区用于存储每个位置x、y处颜色值的强度值。

OpenGL自动完成“隐藏面消除”所用算法——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的Z-Buffer算法,从代码中可以得知:[颜色缓冲区]的建立,是在[深度缓冲区]的算法基础上构建出来的。没有深度缓冲区的参与,是计算不出颜色缓冲区数据的。从而可推论:
没有深度缓冲区,就没有颜色缓冲区;这两个缓冲区都和光栅的大小相同,每个像素都有[深度]信息和[颜色]信息。

深度测试
什么是深度测试?DepthBuffer(深度缓冲区)和ColorBuffer(颜色缓冲区)是一一对应的,我们知道颜色缓冲区是存储像素的颜色信息,深度缓冲区是存储像素的深度信息。在决定是否要绘制某个物体表面时,首先要将表面对应的像素深度(A)与深度缓冲区中的深度值(B)进行对比,A>B则丢弃掉A对应的表面不绘制;A<B,则拿出这个像素对应的深度值和颜色值,对应的分别去更新深度缓冲区和颜色缓冲区。这个过程称为“深度测试”。

OpenGL的Z-Buffer算法自动得到了所有离近平面最近的像素,为何还要进行深度测试?
疑问可能来自:Z-Buffer算法已经比较了模型中的每个像素,生成了离近剪裁平面最近的像素点的集合(包含深度与颜色信息)。那么,不在深度缓冲区中的所有像素点肯定都是更远离观察者的,那直接绘制颜色缓冲区中颜色到屏幕上不就完事了吗?
答:虽然OpenGL的Z-Buffer算法阶段不是可编程阶段,但要启用深度缓冲区,必须先启动它,即glEnable(GL_DEPTH_TEST); 启动之后,OpenGL才能用深度缓冲区,才会自动完成Z-Buffer算法,形成颜色缓冲区和深度缓冲区。

glEnable(GL_DEPTH_TEST);
 如果启用,则进行深度比较并更新深度缓冲区。请注意,即使深度缓冲区存在且深度掩码不为零,如果禁用深度测试,深度缓冲区也不会更新。
glDepthFunc(GL_LESS);// 参数为GL_LESS的这个函数调用是默认缺省,所以可以省略。
 它是指定用于将每个传入像素深度值与深度缓冲区中的深度值进行比较的函数。
 在片段深度值小于深度缓冲区中的深度值时通过测试。如果深度测试失败了,片段将会被丢弃。
 仅当启用深度测试时,才会执行比较。


如果不启用深度测试,隐藏面也会显示出来,如下图所示:

OpenGL主要的3种缓冲区
OpenGL 中主要有 3 种 Buffer:
1.帧缓冲区(Frame Buffers) :这个是存储OpenGL 最终渲染输出结果的地方,它是一个包含多个图像的集合,例如颜色图像、深度图像、模板图像等。
2.渲染缓冲区(Render Buffers):渲染缓冲区就是一个图像,它是 Frame Buffer 的一个子集。
3.缓冲区对象(Buffer Objects):就是程序员输入到 OpenGL 的数据,分为 [结构类] 和 [索引类] 的。[结构类]被称为:数组缓冲区对象(Array Buffer Object) 或 顶点缓冲区对象(Vertex Buffer Object),即用来描述模型的数组,如顶点数组、纹理数组等;[索引类]被称为:索引缓冲区对象(Index Buffer Object),是对上述数组的[索引]

glClear()系列函数

void glClear(GLbitfield mask);
表示要清除缓冲区的掩码的位或(Bitwise OR)。
接受一个参数(如下3个),该参数是按位或多个值(bitwise OR of several values),指示要清除哪个缓冲区。
GL_COLOR_BUFFER_BIT:指示当前为颜色写入启用的缓冲区。
GL_DEPTH_BUFFER_BIT:指示深度缓冲区。
GL_STENCIL_BUFFER_BIT:指示模板缓冲区。
像素所有权测试、剪切测试、抖动和缓冲区写入磁盘会影响glClear的操作。裁剪框限定了清除的区域。glClear忽略Alpha函数、混合函数、逻辑运算、模板、纹理映射和深度缓冲。

void glClearColor(GLfloat red,GLfloat green,GLfloat blue,GLfloat alpha);
指定清除颜色缓冲区时使用的红色、绿色、蓝色和alpha值。初始值都是0。取值范围[0,1]。
glClearColor指定glClear用于清除颜色缓冲区的红色、绿色、蓝色和alpha值。
比如调用glClearColor(0.0, 0.0, 0.0, 1.0); 则是将背景清除为黑色。

void glClearDepth(GLdouble depth);
void glClearDepthf(GLfloat depth);
指定清除深度缓冲区时使用的深度值。初始值为1。取值范围[0,1]。
glClearDepth(f)指定glClear用于清除深度缓冲区的深度值。

为什么在display()函数中要先glClear()清除缓冲区?
1.OpenGL 通过精巧地协调两个缓冲区完成隐藏面消除:【颜色缓冲区】和【深度缓冲区】。
2.当前像素距离小于深度缓冲区存储的值时,会用当前像素颜色替换【颜色缓冲区】中的颜色,同时用当前距离替换【深度缓冲区】中的值。
3.片段着色器生成的像素颜色会存放在【颜色缓冲区】中,而颜色缓冲区最终会被写入屏幕。
由于display()是循环调用的,所以每次都要从新初始化,这样的话,就有必要调用glClear()清除原有的缓冲区,以建立新的缓冲区逻辑。不然的话,先前的缓冲区数据可能影响当前循环中的以上3点逻辑,可能造成颜色&深度逻辑上的问题,导致不良的伪影。

对于物体运动的情况,如果不清除颜色缓冲区,即不调用glClear(GL_COLOR_BUFFER_BIT); 就会出现残影,如下图所示:
(下图实际上是一个多彩立方体在视图空间不停地无规则平移、旋转和缩放的动画其中的一帧画面)

像素的深度值是由【视图矩阵】和【投影矩阵】决定的。在近裁平面上的像素深度值为0,在远裁平面上的像素的深度值为1。场景中的每个对象都需进行绘制,通常最靠近相机的像素会被保留,这些对象阻挡了在它们后面的对象的可见性。

检测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 (以上是关于✠OpenGL-2-图像管线的主要内容,如果未能解决你的问题,请参考以下文章

OpenGL必知必会——问题清单

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

OpenGL: 渲染管线理论

OpenGL入门之渲染管线pipeline,着色器Shader

将纹理快速加载到 OpenGL 2.0

OpenGL OpenGL管线 与 可编程管线流程