OpenGL-04 高级OpenGL

一、深度测试

1. 深度缓冲

  • 深度缓冲记录了屏幕空间内的像素深度(width*height),它会以16、24或32位float的形式储存它的深度值。在大部分的系统中,深度缓冲的精度都是24位的。

2. gl_FragCoord

  • 对于每一个片段的深度,可以从GL_FragCoord中获取,其xy坐标是屏幕空间的坐标,其z坐标是0到1的深度值。
  • GL_FragCoord是这样得到的:
    • 首先经过MV变换,得到观察空间的坐标
    • 然后经过投影变换和正交变换,得到裁剪空间,其中在进行投影变换时深度值由线性深度变成了非线性深度,给近处的深度了更明显的区分度
    • 再进行除以w的操作,得到[-1,1]^3的NDC标准化设备空间
    • 然后将[-1,1]3的NDC标准化设备空间变换到[0,1]3,注意此时z向已经是取反的了,即左手系(该操作一般在得到NDC时完成,即NDC是左手系),此时,z值已经变成0-1的非线性深度了。
    • 然后,按照glViewPort的宽度和高度,将xy坐标调整到[0,width]x[0,height]。具体着色片段的坐标可能是整数值,也可能是将像素边界取整数值而像素坐标取中值,一般是后者,那么width就会对应width-1个像素,高度方向亦然。
    • 然后进行光栅化,可以将顶点坐标的深度插值得到片段深度
    • 这便是gl_FragCoord的坐标。

3. 提前深度测试

  • 深度测试一般是在片段着色器完成颜色计算后,再进行alpha测试、模板测试、深度测试,来决定是否将其加载到颜色缓冲中。可见,所有片段都需要计算颜色,无论是否会被显示,这是一个很大的开销,因为片段着色器是shader的一个主要计算部分。
  • 因此,可以采取提前深度测试,即在片段着色器之前进行深度比较,因为经过坐标变换之后就得到了顶点的屏幕空间坐标和深度,再光栅化插值之后就可以得到片段深度了,因此片段深度是在片段着色器之前得到了,所以这是可行的,如果不通过深度测试就不进行着色了。
  • 但是,很多情况下提前深度测试就失效了,因为提前深度测试只考虑了遮蔽问题,而缺少与其他问题的方案的配合,尤其是当深度测试结果不能完全决定着色时,直接通过提前深度测试就丢弃片段就会造成坏的效果。
  • 另外,提前深度测试的效果取决于片段的绘制顺序,当由浅及深绘制时,效果最好,不存在重复绘制的情况,反之就没有效果。

4. OpenGL的深度测试

  • glEnable(GL_DEPTH_TEST);
    开启深度测试
  • glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    每帧开始需要清理深度缓冲,就如同颜色缓冲的清理。
  • glDepthMask(GL_FALSE);
    开启深度缓冲只读不写,即只测试,不重写。
  • glDepthFunc(GL_LESS);
    深度测试的通过方案:

5. 线性深度

  • 保存在gl_FragCoord中的深度是非线性的深度,如果有必要转换成线性深度只需要进行反变换就可以了:首先将0-1反变换为-1到1,得到裁剪空间的深度,然后反变换坐标变换就可以得到观察空间的深度,即线性深度。
  • 注意,下面的变换没有包含左手系向右手系的转换,所以越远深度值越大。
float LinearizeDepth(float depth) 
{
    float z = depth * 2.0 - 1.0; // back to NDC 
    return (2.0 * near * far) / (far + near - z * (far - near));    
}

二、模板测试

1.模板缓冲

  • 模板缓冲是8位的,有256个值,可以由GLFW实现。

2.OpenGL的模板缓冲操作

  • glEnable(GL_STENCIL_TEST)
    开启模板测试操作
  • glClear(GL_STENCIL_BUFFER_BIT);
    清理缓冲,置为0.
  • glStencilMask(0xFF) glStencilMask(0x00)
    写入缓冲时对被写入的值按位与操作,因此可见是八位的值,0xFF为原样写入,0x00为禁止写入(原文说这里是禁止写入,但是我猜测可能是写入0x00,由于缓冲默认是被清理为全零值,所以写入零就是禁止写入)。
  • glStencilFunc(GLenum func, GLint ref, GLuint mask)
    该函数定义了测试方式,包括比较对象、比较内容和通过条件
    • 第一个参数用于设置通过条件,指将缓冲中的值与参考值对比:GL_NEVER、GL_LESS、GL_LEQUAL、GL_GREATER、GL_GEQUAL、GL_EQUAL、GL_NOTEQUAL和GL_ALWAYS
    • 第二个参数用于设置参考值
    • 第三个参数用于将参考值和缓冲值在比较前都进行按位与运算,即用于实现按位提取的比较操作。如果没有特殊要求就设置0x00。
glStencilFunc(GL_EQUAL, 1, 0xFF);
  • glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass)
    • 该函数用于定义测试后的操作,指对缓冲的写操作。第一个参数用于设置模板测试失败后的操作,第二个是模板通过但是深度失败的操作,第三个是都通过的操作。默认三个参数都是keep,即不修改缓冲。
    • 从这三个选项中可以看出,模板操作是在深度测试之前的,这样做的原因是:虽然二者都是片段级别的测试,但是深度测试是用于渲染的,而模板操作是用于实现效果的,所以后者的抽象级别要更高,因此如果模板测试不通过,即没有相关的效果绘制的需求,但是仍然实现了深度测试并更新了缓冲,那么么此时的缓冲就被破坏了。

3. 应用

//VAO
	glBindVertexArray(stencilVAO);
	glBindBuffer(GL_ARRAY_BUFFER, VBO);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 11 * sizeof(float), (void*)0);
	glEnableVertexAttribArray(0);
	glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 11 * sizeof(float), (void*)(6 * sizeof(float)));
	glEnableVertexAttribArray(1);
//shader
	Shader twocubeShader("res/shader/stencilVertex.shader", "res/shader/stencilFragment.shader");
	Shader singlecolorShader("res/shader/stencilVertex.shader", "res/shader/singlecolorFragment.shader");
		//stencil
		glEnable(GL_STENCIL_TEST);
		glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);

		twocubeShader.use();

		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, texture1);
		twocubeShader.setInt("texture_diffuse", 0);

		glBindVertexArray(stencilVAO);

		glStencilMask(0x00);

		twocubeShader.setMat4("view", glm::value_ptr(view));
		twocubeShader.setMat4("projection", glm::value_ptr(projection));
		glm::mat4 cubemodel3;
		cubemodel3 = glm::translate(cubemodel3, glm::vec3(11.0, -16.0, 11.0));
		cubemodel3 = glm::scale(cubemodel3, glm::vec3(10.0));
		twocubeShader.setMat4("model", glm::value_ptr(cubemodel3));
		glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);

		glStencilFunc(GL_ALWAYS, 1, 0xFF);
		glStencilMask(0xFF);

		glm::mat4 cubemodel1;
		cubemodel1 = glm::translate(cubemodel1, glm::vec3(10.0, -10.0, 10.0));
		twocubeShader.setMat4("model", glm::value_ptr(cubemodel1));
		glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);
		glm::mat4 cubemodel2;
		cubemodel2 = glm::translate(cubemodel2, glm::vec3(11.0, -10.0, 11.0));
		twocubeShader.setMat4("model", glm::value_ptr(cubemodel2));
		glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);


		singlecolorShader.use();

		glBindVertexArray(stencilVAO);

		glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
		glStencilMask(0x00);
		glDisable(GL_DEPTH_TEST);

		singlecolorShader.setMat4("view", glm::value_ptr(view));
		singlecolorShader.setMat4("projection", glm::value_ptr(projection));
		glm::mat4 colormodel1;
		colormodel1 = glm::scale(cubemodel1, glm::vec3(1.1));
		singlecolorShader.setMat4("model", glm::value_ptr(colormodel1));
		glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);
		glm::mat4 colormodel2;
		colormodel2 = glm::scale(cubemodel2, glm::vec3(1.1));
		singlecolorShader.setMat4("model", glm::value_ptr(colormodel2));
		glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);

		glStencilMask(0xFF);
		glEnable(GL_DEPTH_TEST);

三、混合

1. 丢弃片段

  • 丢弃片段只需要在片段着色器中检查纹理颜色的alpha值,然后通过设置阈值,进行discard就可以了。
  • 需要注意的是,当进行discard时,如果采取纹理环绕GL_REPEAT,那么超过纹理坐标范围内的片段会显示出一条边框,因此采用GL_CLAMP_TO_EDGE不做环绕。会出现这种情况是因为,光栅化时会进行反走样,那么边缘的片段就会超出三角形范围。
#version 330 core

layout(location = 0) in vec3 aPos;
layout(location = 1) in vec2 aTexCoord;

out vec2 texCoord;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
	gl_Position = projection * view * model * vec4(aPos, 1.0);
	texCoord = aTexCoord;
}
//VAO
	unsigned int grassVAO;
	glGenVertexArrays(1, &grassVAO);
	glBindVertexArray(grassVAO);
	glBindBuffer(GL_ARRAY_BUFFER, VBO);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 11 * sizeof(float), (void*)0);
	glEnableVertexAttribArray(0);
	glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 11 * sizeof(float), (void*)(6 * sizeof(float)));
	glEnableVertexAttribArray(1);
//program
Shader grassShader("res/shader/discardVertex.shader", "res/shader/discardFragment.shader");
//texture
	unsigned int texture3;
	glGenTextures(1, &texture3);
	glBindTexture(GL_TEXTURE_2D, texture3);

	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);

	stbi_set_flip_vertically_on_load(true);
	data = stbi_load("res/texture/grass.png", &width, &height, &nrChannels, 0);
	if (data)
	{
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
		glGenerateMipmap(GL_TEXTURE_2D);
	}
	else
	{
		std::cout << "Failed to load texture3." << std::endl;
	}
	stbi_image_free(data);
//grass
	vector<glm::vec3> grassPositions;
	grassPositions.push_back(glm::vec3(10.0, -10.0, 6.0));
	grassPositions.push_back(glm::vec3(9.0, -10.0, 8.0));
	grassPositions.push_back(glm::vec3(8.0, -10.0, 7.0));

//discard
		glDisable(GL_STENCIL_TEST);
		grassShader.use();

		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D,texture3);
		grassShader.setInt("texture_grass", 0);

		grassShader.setMat4("view", glm::value_ptr(view));
		grassShader.setMat4("projection", glm::value_ptr(projection));
		for (unsigned int i = 0; i != grassPositions.size(); i++)
		{
			glm::mat4 model;
			model = glm::translate(model, grassPositions[i]);
			grassShader.setMat4("model", glm::value_ptr(model));
			glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
		}

2. 混合

2.1 OpenGL的混合操作

  • gllEnable(GL_BLEND);
    启动混合。
  • glBlendFunc(GLenum sfactor, GLenum dfactor)
    设置源片段与目标片段颜色的因子:
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
  • glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO);
    分别设置RGB颜色通道(前)与alpha通道(后)的因子。
  • glBlendEquation(GLenum mode)
    设置运算方式。默认加法。

2.2 应用

  • 只需要设置glEnable和因子就可以使用了。
  • 需要处理好blend与深度测试的关系,blend是在深度测试后面进行的,因此如果半透明物体在被其遮挡的物体之前绘制,那么在绘制被遮挡物体时不会着色,因此就不会显示透明效果。为此,应当按照这样的顺序渲染:首先是所有不透明物体,然后将透明物体的远近进行排序,然后由远及近的渲染。这样一来,前面的透明物体就可以从颜色缓冲中拿到目标颜色,并进行blend。
//VAO
	unsigned int winVAO;
	glGenVertexArrays(1, &winVAO);
	glBindVertexArray(winVAO);
	glBindBuffer(GL_ARRAY_BUFFER, VBO);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 11 * sizeof(float), (void*)0);
	glEnableVertexAttribArray(0);
	glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 11 * sizeof(float), (void*)(6 * sizeof(float)));
	glEnableVertexAttribArray(1);

//shader
Shader winShader("res/shader/blendVertex.shader", "res/shader/blendFragment.shader");
//texture
	unsigned int texture4;
	glGenTextures(1, &texture4);
	glBindTexture(GL_TEXTURE_2D, texture4);

	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);

	stbi_set_flip_vertically_on_load(true);
	data = stbi_load("res/texture/blending_transparent_window.png", &width, &height, &nrChannels, 0);
	if (data)
	{
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
		glGenerateMipmap(GL_TEXTURE_2D);
	}
	else
	{
		std::cout << "Failed to load texture4." << std::endl;
	}
	stbi_image_free(data);
//windows
	vector<glm::vec3> winPositions;
	winPositions.push_back(glm::vec3(7.0, -10.0, 6.0));
	winPositions.push_back(glm::vec3(8.0, -10.0, 8.0));
	winPositions.push_back(glm::vec3(9.0, -10.0, 7.0));
//blend
		glEnable(GL_BLEND);
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

		map<float, glm::vec3> sorted;
		for (unsigned int i = 0; i != winPositions.size(); i++)
		{
			float len = glm::length(camera.Position-winPositions[i]);
			sorted[len] = winPositions[i];
		}

		winShader.use();

		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, texture4);
		winShader.setInt("texture_win",0);

		winShader.setMat4("view", glm::value_ptr(view));
		winShader.setMat4("projection", glm::value_ptr(projection));
		for (auto i = sorted.rbegin(); i != sorted.rend(); i++)
		{
			glm::mat4 model;
			model = glm::translate(model, i->second);
			winShader.setMat4("model", glm::value_ptr(model));
			glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
		}

四、面剔除

  • 当顶点数据是按照顺时针或者逆时针排序时,如果该三角形在背面,那么按照这个顺序绘制就会是反向的,否则是同乡的,按此可以判断是正向面还是反向面。
  • glEnable(GL_CULL_FACE);
    启动面剔除
  • glCullFace(GL_BACK)
    选择剔除的面:
  • glFrontFace(GL_CCW)
    选择顺时针绘制还是逆时针绘制的。默认值是GL_CCW,它代表的是逆时针的环绕顺序,另一个选项是GL_CW,它(显然)代表的是顺时针顺序。
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
glFrontFace(GL_CCW);

这是顶点顺序没有正确排序的结果。

  • 一个需要注意的地方是,只有一个面的物体显然也只能从一个方向看见,这明显不符合要求。因此绘制单面时需要关闭cull。

五、帧缓冲

1. 帧缓冲

  • glGenFrameBuffers:创建帧缓冲
unsigned int FBO;
glGenFrameBuffers(1,&FBO);
  • glBindFrameBuffer:绑定帧缓冲
glBindFrameBuffer(GL_FRAMEBUFFER, FBO);
  • GL_FRAMEBUFFER用于读写
  • GL_READ_FRAMEBUFFER只读
  • GL_DRAW_FRAMEBUFFER只写
  • glCheckFrameBufferStatus:检查完整性
    以上只是申请了一个帧缓冲,但是没有具体的内容,帧缓冲必须满足以下条件:
    • 至少附加一个缓冲
    • 至少有一个颜色缓冲
    • 所有缓冲必须已经申请内存
    • 样本数一样
if(glCheckFrameBufferStatus(GL_FRAMEBUFFER)==GL_FRAMEBUFFER_COMPLETE)//完整性检查通过
  • glBindFrameBuffer:解绑
    GLFW会自行生成一套帧缓冲,包括颜色、深度、模板等,不过没有我们自己的帧缓冲操作,那么对帧缓冲的读写都是在默认帧缓冲上进行的,它的id是0,而只要我们绑定了其他帧缓冲,那么就会在绑定的缓冲上操作,即离屏渲染,此时能否执行某种缓冲操作还取决于我们是否附加了对应类型的缓冲,从这个意义上,任何一个帧缓冲都必须具备颜色缓冲,否则就没有意义了。因此,解绑就是绑定默认帧缓冲。
glBindFrameBuffer(GL_FRAMEBUFFER,0);
  • glDeleteFrameBuffers:删除帧缓冲
glDeleteFrameBuffers(1,&FBO)

2. 附件

2.1 纹理附件

  • 创建纹理
    • 纹理申请与一般纹理一样
    • glTexture2D不再给他传输数据,而是用来分配内存
    • 此处,纹理被设置为屏幕大小
    • 因为纹理附件是用来做颜色缓冲的,所以不必考虑纹理环绕以及mipmap
unsigned int texture;
glGenTextures(1,&texture);
glBindTexture(GL_TEXTURE_2D,texture);

glTexture2D(GL_TEXTURE_2D,0,GL_RGB,800,600,0,GL_RGB,GL_UNSIGNED_BYTE,NULL);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  • 附加纹理 glFramebufferTexture2D
    第一个参数是缓冲对象的状态标记,第二个参数是缓冲类型,此处是颜色缓冲,另外可以附加多个颜色缓冲,只需要在后面附加序号,第三个参数是纹理类型,第四个参数是纹理id,可见这个函数对于纹理来说不是一个状态函数,只是帧缓冲的状态函数,第五个参数是mipmap层级,设置为0.
glFramebufferTexture2D(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,texture,0);

相对于不同的缓冲类型来说,只有第二个参数有区别,此外在申请内存时也有区别:

  • 第二个参数:GL_COLOR_ATTACHMENT, GL_DEPTH_ATTACHMENT, GL_STENCIL_ATTACHMENT, GL_DEPTH_STENCIL_ATTACHMENT
  • 对于glTexture2D,颜色缓冲的纹理格式和内部格式显然是颜色通道如GL_RGBA,而对于深度缓冲来说是GL_DEPTH_COMPONENT,对于模板缓冲来说是GL_STENCIL_INDEX。而对于深度和模板缓冲来说,内部格式是GL_DEPTH24_STENCIL8,格式是GL_DEPTH_STENCIL,特别的数据类型是GL_UNSIGNED_INT_24_8。
glTexImage2D(
  GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, 800, 600, 0, 
  GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, NULL
);

glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, texture, 0);

2.2 缓冲附件

  • 缓冲附件不是像纹理一样的通用数据格式,不能像纹理一样的方式读取,但是其是缓冲的标准格式,所以速度比较好。一般用于深度缓冲和模板缓冲。
  • 创建缓冲与附加缓冲
    • glGenRenderbuffers/glBindRenderbuffer:基本流程与纹理附件是相似的,只是函数不同罢了
    • glRenderbufferStorage:用于给缓冲分配空间,第一个参数是渲染缓冲的标志,第二个参数是内部格式,第三四个参数是尺寸,可见与纹理的关键参数是一样的。
    • glFramebufferRenderbuffer:在附加渲染缓冲时,同纹理缓冲相比,将第三个参数换做渲染缓冲,第四个换做渲染缓冲的id,此外不再需要第五个参数。
unsigned int RBO;
glGenRenderbuffers(1,&RBO);
glBindRenderbuffer(GL_RENDERBUFFER,RB0);

glRenderbufferStorage(GL_RENDERBUFFER,GL_DEPTH24_STENICL8,800,600);

glFramebufferRenderbuffer(GL_FRAMEBUFFER,GL_DEPTH_STENCIL_ATTACHMENT,GL_RENDERBUFFER,RBO);

3. 渲染到缓冲

  • 创建帧缓冲,并为其附加颜色纹理和深度模板缓冲。需要注意的是,在每一个纹理缓冲、渲染缓冲、帧缓冲的准备工作完成后最好解绑到默认缓冲,正如前面所说,附件附件只是帧缓冲的状态函数,因此只要保持帧缓冲的绑定就可以了,不需要保持纹理和渲染缓冲的绑定,因此可以在解绑之后进行附加。
	//帧缓冲
	unsigned int FBO;
	glGenFramebuffers(1, &FBO);
	glBindFramebuffer(GL_FRAMEBUFFER, FBO);

	unsigned int texColorBuffer;
	glGenTextures(1, &texColorBuffer);
	glBindTexture(GL_TEXTURE_2D, texColorBuffer);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 800, 600, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glBindTexture(GL_TEXTURE_2D, 0);

	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texColorBuffer, 0);

	unsigned int RBO;
	glGenRenderbuffers(1, &RBO);
	glBindRenderbuffer(GL_RENDERBUFFER, RBO);
	glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);
	glBindRenderbuffer(GL_RENDERBUFFER, 0);

	glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, RBO);

	if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
	{
		std::cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!" << std::endl;
	}
	glBindFramebuffer(GL_FRAMEBUFFER, 0);
  • 准备节点数据:因为我们是要将颜色缓冲直接渲染到屏幕上,所以采取的方案是渲染一个以颜色缓冲为纹理的矩形,我们设置矩形的顶点是标准设备空间NDC中的边界顶点坐标,即+/-1,此外将纹理坐标也设置为边界即0/1。
	float frameVertices[] =
	{
		-1.0, -1.0,  0.0, 0.0,
		 1.0, -1.0,  1.0, 0.0,
		 1.0,  1.0,  1.0, 1.0,
		 1.0,  1.0,  1.0, 1.0,
		-1.0,  1.0,  0.0, 1.0,
		- 1.0, -1.0,  0.0, 0.0
	};

	unsigned int frameVBO;
	glGenBuffers(1, &frameVBO);
	unsigned int frameVAO;
	glGenVertexArrays(1, &frameVAO);

	glBindVertexArray(frameVAO);
	glBindBuffer(GL_ARRAY_BUFFER, frameVBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(frameVertices), frameVertices, GL_STATIC_DRAW);
	glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
	glEnableVertexAttribArray(0);
	glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));
	glEnableVertexAttribArray(1);
  • shader:我们的shader也很简单,只需要对传入的坐标进行纹理提取就看可以了。一个有趣的地方是,我们只需要传入NDC的xy坐标就可以了,对于z坐标设置为0.0,即NDC中间位置,事实上z的取值不影响效果。
#version 330 core

layout(location = 0) in vec2 aPos;
layout(location = 1) in vec2 aTexCoord;

out vec2 texCoord;

void main()
{
	gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0);
	texCoord = aTexCoord;
}
#version 330 core

in vec2 texCoord;

out vec4 fragColor;

uniform sampler2D texColorBuffer;

void main()
{
	fragColor = texture(texColorBuffer, texCoord);
}
  • 渲染过程
    首先绑定帧缓冲,并进行缓冲清理,然后开始正常的渲染操作。在渲染完成后,解绑帧缓冲到默认缓冲并进行缓冲清理,此时应当关闭除颜色缓冲之外的缓冲以及一系列的效果,这是因为我们只需要渲染一个四边形。
		//framebuffer
		glBindFramebuffer(GL_FRAMEBUFFER, FBO);
		glEnable(GL_DEPTH_TEST);
		glClearColor(0.3f, 0.3f, 0.3f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

//framebuffer->defaultbuffer
		glBindFramebuffer(GL_FRAMEBUFFER, 0);
		glDisable(GL_DEPTH_TEST);
		glDisable(GL_STENCIL_TEST);
		glClearColor(0.3f, 0.3f, 0.3f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT);

		frameShader.use();

		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, texColorBuffer);
		frameShader.setInt("texColorBuffer", 0);

		glBindVertexArray(frameVAO);
		glDrawArrays(GL_TRIANGLES, 0, 6);

4. 后期处理

  • 原图

4.1 反相

  • 反向只需要对颜色取反。
#version 330 core

in vec2 texCoord;

out vec4 fragColor;

uniform sampler2D texColorBuffer;

void main()
{
	fragColor = vec4(1.0-vec3(texture(texColorBuffer, texCoord)),1.0);
}

4.2 灰度

  • 灰度只需要对三色取均值。
#version 330 core

in vec2 texCoord;

out vec4 fragColor;

uniform sampler2D texColorBuffer;

void main()
{
	fragColor = texture(texColorBuffer, texCoord);
	float average = (fragColor.r + fragColor.g + fragColor.b) / 3.0;
	fragColor = vec4(average, average, average, 1.0);
}

4.3 核

锐利

#version 330 core

in vec2 texCoord;

out vec4 fragColor;

uniform sampler2D texColorBuffer;

const float offset = 1.0 / 300.0;

void main()
{
	vec2 offsets[9] =
	{
		vec2(-offset,offset),//左上
		vec2(0.0,offset),//上
		vec2(offset,offset),//右上
		vec2(-offset,0.0),//左
		vec2(0.0,0.0),//中
		vec2(offset,0.0),//右
		vec2(-offset,-offset),//左下
		vec2(0.0,-offset),//下
		vec2(offset,-offset),//左上
	};

	float kernal[9] =
	{
		-1, -1, -1,
		-1, 9, -1,
		-1, -1, -1
	};

	vec3 samples[9];
	for (int i = 0; i != 9; i++)
	{
		samples[i] = vec3(texture(texColorBuffer, texCoord+offsets[i]));
	}
	vec3 col = vec3(0.0);

	for (int i = 0; i != 9; i++)
	{
		col += samples[i] * kernal[i];
	}

	fragColor = vec4(col, 1.0);
}
  • 可以看出,在屏幕边缘的地方有些异样

模糊

float kernel[9] = float[](
    1.0 / 16, 2.0 / 16, 1.0 / 16,
    2.0 / 16, 4.0 / 16, 2.0 / 16,
    1.0 / 16, 2.0 / 16, 1.0 / 16  
);

边缘检测

	float kernal[9] =
	{
		1, 1, 1,
		1, -8, 1,
		1, 1, 1
	};

六、立方体贴图

  • 立方体贴图可以使用方向向量来采样纹理,方向向量的原点在立方体中心,方向向量的大小并不重要。

1.创建

  • 申请贴图
unsigned int textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);
  • 分配贴图:对每一面使用glTexImage2D,其中第一个参数表明是那一面,因为这些枚举是递增的,所以可以使用循环。
int width, height, nrChannels;
unsigned char *data;  
for(unsigned int i = 0; i < textures_faces.size(); i++)
{
    data = stbi_load(textures_faces[i].c_str(), &width, &height, &nrChannels, 0);
    glTexImage2D(
        GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 
        0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data
    );
}
  • 环绕过滤:多了一个R维度,其他区别不大
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
  • 纹理查询:使用方向向量查询
in vec3 textureDir; // 代表3D纹理坐标的方向向量
uniform samplerCube cubemap; // 立方体贴图的纹理采样器

void main()
{             
    FragColor = texture(cubemap, textureDir);
}

2.天空盒

  • 将天空盒画在一个以原点为中心的立方体上,不对其进行model变换,只进行view与projection,此外对于view中的位移部分也不做处理,即转换成mat3再转换成mat4然后计算,这样天空盒与camera只存在旋转的相对运动,并且天空盒包围camera。
  • 一个点子在于,将顶点着色器的坐标输出gl_Position的z坐标使用w来表示,呢么在NDC中的z永远等于1,即最远处,因此使用深度测试可以最后一步画天空盒,通过提前深度测试,减少不必要的计算。当然,测试通过要改成GL_LEQUAL。
	glBindVertexArray(skyVAO);
	glBindBuffer(GL_ARRAY_BUFFER, VBO);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 11 * sizeof(float), (void*)0);
	glEnableVertexAttribArray(0);	

Shader skyboxShader("res/shader/skyboxVertex.shader", "res/shader/skyboxFragment.shader");
	//cubemap
	vector<string> faces
	{
		"right.jpg",
		"left.jpg",
		"top.jpg",
		"bottom.jpg",
		"front.jpg",
		"back.jpg"
	};
	unsigned int cubemapTexture = loadCubemap(faces, "res/texture/skybox/");
		//skybox
		glDisable(GL_CULL_FACE);
		glDepthMask(GL_FALSE);
		skyboxShader.use();

		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture);
		skyboxShader.setInt("skybox", 0);

		glm::mat4 viewSky = glm::mat4(glm::mat3(view));
		skyboxShader.setMat4("view", glm::value_ptr(viewSky));
		skyboxShader.setMat4("projection", glm::value_ptr(projection));

		glDepthFunc(GL_LEQUAL);

		glBindVertexArray(skyVAO);
		glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);
		glDepthMask(GL_TRUE);
//函数
unsigned int loadCubemap(const vector<string>& faces, string path)
{

	unsigned int texID;
	glGenTextures(1, &texID);
	glBindTexture(GL_TEXTURE_CUBE_MAP, texID);

	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

	stbi_set_flip_vertically_on_load(false);
	int width, height, nrChannels;
	for (unsigned int i = 0; i != faces.size(); i++)
	{
		unsigned char* data = stbi_load((path+faces[i]).c_str(), &width, &height, &nrChannels, 0);
		if (data)
		{
			glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X+i, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
		}
		else
		{
			std::cout << "Cubemap texture failed to load at path: " << faces[i] << std::endl;
		}
		stbi_image_free(data);
	}

	return texID;
}
#version 330 core

layout(location = 0) in vec3 aPos;

uniform mat4 view;
uniform mat4 projection;

out vec3 texCoord;

void main()
{
	texCoord = aPos;
	vec4 pos = projection * view * vec4(aPos, 1.0);
	gl_Position = pos.xyww;

}
#version 330 core

in vec3 texCoord;

out vec4 fragColor;

uniform samplerCube skybox;

void main()
{
	fragColor = texture(skybox, texCoord);
}

3.环境映射


  • 使用reflect或者refract计算出反射或者折射光线的方向,将其作为采样的方向矢量对天空盒采样,就可以获得反射或者折射的效果。

七、高级数据

八、高级GLSL

九、几何着色器

  • 顶点着色器的处理对象是顶点,其数据来自于VBO的规范的顶点属性,其内置的处理结果是gl_Position,即顶点位置;几何着色器的处理对象是与图元相关的一系列顶点,具体的顶点集合及其数量取决于几何着色器的输入类型,其处理的数据是来自顶点着色器的数据,通过gl_in数组来获取,一个特殊的属性是gl_in[i].gl_Position,对应着顶点着色器的输出gl_Positioin,其内置的处理结果是对输入的顶点集合进行处理,来获得新的顶点集合,一个特殊的属性是gl_Position,一般来说这是最终的裁剪空间坐标,交给后面的光栅化处理,通过新的顶点集合可以定义一个新的图元并作为光栅化的对象,此外,可以定义多个图元;片段着色器的处理对象是光栅化后的片段,片段着色器处理的数据来自几何着色器,当然片段不是顶点,其获得的并非几何着色器的直接输出,但是也具备一样的顶点属性,然而这些顶点属性都是通过几何着色器输出的图元的顶点属性做插值得到的。此外,所有着色器都可以拿到uniform数据。另外,有时候需要将信息跨着色器处理,那么就需要输入输出变量。
  • 总的看来,流程是这样的:
    • 顶点着色器拿到规范的顶点属性组为输入,以及可能存在的全局变量,以顶点为对象处理其属性,将顶点坐标交付给几何着色器,必要时会将新定义的其他顶点属性作为输出交付给几何着色器。
    • 几何着色器按照某种绘制图元的规范拿到一个对应的顶点属性数组,或者可能有来自顶点着色器的顶点属性输出数组,然后利用这些顶点去逐一创建新的顶点(改变属性,当然也可能不变),对于每一个顶点都做一次顶点坐标的输出,必要时会将其他修改的顶点属性作为输出,重复创建绘制某种图元所需要的顶点,然后得到一个图元的顶点集,重复这个步骤可以得到一个或者多个图元的顶点集。
    • 光栅化基于几何着色器的所有图元的顶点属性集合,进行光栅化,得到了一系列片段。
    • 片段着色器在光栅化了的片段位置上,以几何着色器输出的对应图元的顶点属性通过插值得到该片段对应位置的顶点属性,作为输入信息,来进行着色。
  • 因此,几何着色器的作用便是,在一个一般来说已经有基本图形结构的图元集合上,重定义每一个图元为一个新的图元或者是图元集合,来获得总体上的一群新的图元集合。

1. 几何着色器的GLSL规范

#version 330 core
layout(points) in;
layout(line_strip, max_vertixes=2) out;

void main()
{
  gl_Position = gl_in[0].gl_Position+vec4(-0.1,0.0,0.0,0.0);
   EmitVertex();

  gl_Position = gl_in[0].gl_Position+vec4(0.1,0.0,0.0,0.0);
   EmitVertex(); 
  
  EndPrimitive();
}
  • 首先,需要设定输入格式layout() in;。输入格式用于确定几何着色器能够拿到的顶点数量。此处的格式应当与绘制时的格式对应。
  • 然后,需要确定输出格式以及最大顶点数量layout( , max-vertices=2) out;。输出格式用于确定绘制图元的形状。输出格式很简单,只有三种图元。几何着色器同时希望我们设置一个它最大能够输出的顶点数量(如果你超过了这个值,OpenGL将不会绘制多出的顶点)。
  • 此外,可以看到gl_in便是几何着色器的输入,这是一个数组用于容纳该图元的定点信息。当然这是内置的输入,一般用于获取Position。他的格式大致可以理解成这样的内建变量。
in gl_Vertex
{
    vec4  gl_Position;
    float gl_PointSize;
    float gl_ClipDistance[];
} gl_in[];
  • 然后,可以看到,他对每一个顶点都做了一次顶点位置输出,这个与顶点着色器类似,也是内置的流程。进一步的,还可以给其他的输出赋值。
  • 一个新的信息是,在每一个顶点信息的更新之后,跟着EmitVertex()函数,这是一个定点信息更新的结束
  • 另外一个新的信息是,在更新按绘制line_strip所需的两个顶点之后,使用了EndPrimitive()函数,代表一个图元定义的结束。重复使用该函数并为其设置顶点,就可以得到多个图元。

2.house

3.爆破

  • 注意:原文的代码存在一些问题:首先,在求法向量的时候,应当bxa,这样符合逆时针正面点顺序,否则爆炸就是向内爆炸的;其次,顶点着色器交给几何着色器的坐标应当是观察空间坐标,不经过projection变换,即爆炸效果应当在观察空间进行,这是在裁剪空间进行图元坐标变换不容易把握坐标的尺度,会引起深度测试的失效;因此,能观察到两种现象,一个是裁剪空间的爆炸效果比较小,这是因为经过除以w之后,xy坐标的变化减小了,第二个是深度测试效果不好,这是因为裁剪空间计算法向量没有对w分量进行变换,即z值没有变换,因此经过除以w操作之后,z分量会变得很小,当然裁剪空间的z分量变化是不会在屏幕上显示出来效果的,因此一时之间也挑不出毛病。
#version 330 core

layout(triangles) in;
layout(triangle_strip, max_vertices = 3) out;

in VS_OUT{
    vec2 TexCoord;
} gs_in[];

uniform float time;
uniform mat4 projection;
out vec2 texCoord;

vec3 GetNormal()
{
    vec3 a = vec3(gl_in[0].gl_Position) - vec3(gl_in[1].gl_Position);
    vec3 b = vec3(gl_in[2].gl_Position) - vec3(gl_in[1].gl_Position);

    return normalize(cross(b, a));
}

vec4 explode(vec4 position, vec3 normal)
{
    float magnitude = 6.0;
    vec3 direction = (sin(time) + 1.0) / 2.0 * magnitude * normal;
    return projection * (position + vec4(direction, 0.0));
}


void main()
{
    vec3 normal = GetNormal();

    texCoord = gs_in[0].TexCoord;
    gl_Position = explode(gl_in[0].gl_Position, normal);    // 1:左下
    EmitVertex();
    texCoord = gs_in[1].TexCoord;
    gl_Position = explode(gl_in[1].gl_Position, normal);    // 2:右下
    EmitVertex();
    texCoord = gs_in[2].TexCoord;
    gl_Position = explode(gl_in[2].gl_Position, normal);   // 3:左上
    EmitVertex();

    EndPrimitive();
}

4.法向量可视化

  • 法向量可视化的思路很简单,只需要用定点着色器拿到模型的顶点信息,然后在几何着色器中输入triangles(模型原本的图元组成),并对每一个顶点输出一个line_strip就可以了,当然因为一个三角形有三个顶点,所以最大顶点数要设置为6个。最后,在片段着色器中绘制固定的颜色就可以了。
  • 一个需要注意的地方是关于法线的空间变换。在以前的片段着色器中,在计算光照时,只是将法线转换到世界坐标系,去除了位移部分且进行了二次变换保证缩放不变形,这是因为世界坐标系足以计算光照了,然而,如果我们要将法线做出显示,那么进一步转换到观察坐标系又会经过view矩阵的作用,那么有会存在变形,这样就可能会导致无法发挥检查法线的作用,因此,将法线经过model和view变换(调整了的)之后的坐标再传入几何着色器中进行计算形成线元的顶点。那么为什仫不一步到位变换到裁剪空间呢,这是因为裁剪空间的坐标是未经标准化的,需要经过转换到NDC中才可以说是尺度一致,但是这一步不是可控的;当然也可以手动归一,但是NDC的z向坐标不是线性的;额(⊙﹏⊙),好像问题也不大,那么原因是什么呢,可能是因为,projection变换是针对坐标的变换而不是针对方向的变换吧;额,按照原文的代码,其实直接变换到裁剪空间也是可以的,毕竟矩阵乘法的分配律。

十、实例化

1.实例化

  • 如果对于一个VAO,即一种GPU数据空间分布,我们希望绘制多个物体,而这些绘制之间不需要重新修改VAO,那么如果在CPU中多次绘制就会造成对绘制函数的多次调用,而每一次绘制函数的调用都是一次CPU到GPU的计算指令和数据传输,这二者之间的数据传输是低效的,鉴于这些绘制的VAO并没有变化,则需要告知GPU的绘制事宜是相同的,事实上是没有必要采取CPU端多次设置的。
  • 因此,实例化可以在绑定VAO之后,通过glDrawArraysInstanced或者glDrawElementsInstanced来实现多个物体的绘制,只需要一次CPU-GPU交互。glDrawArraysInstanced或者glDrawElementsInstanced与glDrawArrays或者glDrawElements的参数是一致的,但是在最后有一个实例数量的参数。如,glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100);
  • 使用实例化有两个高效的工具,一个是gl_InstanceID,这个可以在顶点着色器中获取到当前着色的实例化编号。另一个是实例化数组,这个可以将不同实例之间的差异以类似顶点属性的方式保存在GPU中(与一般的顶点属性保存在不同的缓冲VBO中,即这说明了一次绘制可以使用多个VBO,一个VAO可以记录多个VBO),并且可以设置为逐实例更新数据或者多个实例更新数据(对比一般的VBO,是逐顶点更新数据)。
    • gl_InstanceID
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;

out vec3 fColor;

uniform vec2 offsets[100];

void main()
{
    vec2 offset = offsets[gl_InstanceID];
    gl_Position = vec4(aPos + offset, 0.0, 1.0);
    fColor = aColor;
}
  • 实例化数组:可以看到,实例化数组的创建和数据传输与一般的VBO相同,但是在指针函数部分需要注意,一个是虽然是不同的VBO,但是它们对应的shader是一个,因此输入变量的编号是需要排序的,不能另起炉灶,另一个是glVertexAttribDivisor函数,这个函数用于说明shader中的第i个变量(参数一)是什么频率更新的(参数二),如果是0则意味着逐顶点更新即一般的顶点属性,如果是1则意味着逐实例更新即每个实例各自所有顶点共同的属性,以此推之如果是k则是k个实例更新一次。一个需要注意的是这些顶点数组函数之间的关系,glEnableVertexAttribArray是VAO的状态函数,事关该变量是否使能,当然要绑定VAO,glVertexAttribPointer是VAO,VBO和EBO的状态函数,需要在此之前绑定VBO和EBO,才能指定变量,当然在此之前要绑定VAO,glVertexAttribDivisor也是VAO的状态函数。
//创建
unsigned int instanceVBO;
glGenBuffers(1, &instanceVBO);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(glm::vec2) * 100, &translations[0], GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);

//顶点属性
glEnableVertexAttribArray(2);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
glBindBuffer(GL_ARRAY_BUFFER, 0);   
glVertexAttribDivisor(2, 1);

2.小行星带

  • 一个有趣的地方是,一个VAO是可以绑定多个VBO的。这样一来,我们使用模型加载获得的VAO,可以提取出来绑定之后在附加其他的缓冲,即可以附加实例化数组了,而附加过程与顶点属性数组是一样的,只是需要表明更新频率是1个实例。
//rock
	unsigned int amount = 1000;
	glm::mat4* modelMatrices;
	modelMatrices = new glm::mat4[amount];
	srand((unsigned int)glfwGetTime()); // 初始化随机种子    
	float radius = 50.0;
	float offset = 5.0f;
	for (unsigned int i = 0; i < amount; i++)
	{
		glm::mat4 model;
		// 1. 位移:分布在半径为 'radius' 的圆形上,偏移的范围是 [-offset, offset]
		float angle = (float)i / (float)amount * 360.0f;
		float displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;
		float x = sin(angle) * radius + displacement;
		displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;
		float y = displacement * 0.4f; // 让行星带的高度比x和z的宽度要小
		displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;
		float z = cos(angle) * radius + displacement;
		model = glm::translate(model, glm::vec3(x, y, z));

		// 2. 缩放:在 0.05 和 0.25f 之间缩放
		float scale = (float)(rand() % 20) / 100.0f + 0.05f;
		model = glm::scale(model, glm::vec3(scale));

		// 3. 旋转:绕着一个(半)随机选择的旋转轴向量进行随机的旋转
		float rotAngle = (float)(rand() % 360);
		model = glm::rotate(model, rotAngle, glm::vec3(0.4f, 0.6f, 0.8f));

		// 4. 添加到矩阵的数组中
		modelMatrices[i] = model;
	}

	unsigned int rockVBO;
	glGenBuffers(1, &rockVBO);
	glBindBuffer(GL_ARRAY_BUFFER, rockVBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(glm::mat4) * amount, &modelMatrices[0], GL_STATIC_DRAW);
	for (unsigned int i = 0; i != rockModel.meshes.size(); i++)
	{
		glBindVertexArray(rockModel.meshes[i].VAO);
		//glBindBuffer(GL_ARRAY_BUFFER,rockVBO);

		GLsizei vec4Size = sizeof(glm::vec4);

		glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)0);
		glEnableVertexAttribArray(3);
		glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(vec4Size));
		glEnableVertexAttribArray(4);
		glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(2 * vec4Size));
		glEnableVertexAttribArray(5);
		glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(3 * vec4Size));
		glEnableVertexAttribArray(6);

		glVertexAttribDivisor(3, 1);
		glVertexAttribDivisor(4, 1);
		glVertexAttribDivisor(5, 1);
		glVertexAttribDivisor(6, 1);

		//glBindBuffer(GL_ARRAY_BUFFER, 0);
		glBindVertexArray(0);
	}
  • 一个需要注意的地方是,shader中的顶点属性输入变量最大是一个vec4,因此对于mat4变量是占据着连续的变量位置的,因此定义layout(location=3) mat4 matrix;,其占据了3456四个位置,相应的在cpu中需要给这四个位置分别传输其对应的vec4值。一个有趣的地方是,glm中的矩阵分布是列主序的,而GPU中的矩阵分布也是列主序的,所以不需要担心矩阵被拆散。
#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 2) in vec2 aTexCoord;
layout(location = 3) in mat4 ainstanceMatrix;

uniform mat4 view;
uniform mat4 projection;
uniform mat4 rotate;

out vec2 texCoord;

void main()
{
	gl_Position = projection * view * rotate * ainstanceMatrix * vec4(aPos, 1.0);
	texCoord = aTexCoord;
}

#version 330 core

in vec2 texCoord;

out vec4 fragColor;

struct Material {
	sampler2D texture_diffuse1;
};
uniform Material material;

void main()
{
	fragColor = texture(material.texture_diffuse1, texCoord);
}
  • 当然,我们定义的模型加载的Draw函数不是实例化绘制函数,因此为了绘制,我们只能从加载的模型的每一个mesh,提取出他们的纹理和VAO进行绘制。显然的,我们应该对每一个Mesh分别绘制。
//rock
		rockShader.use();

		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, rockModel.textures_loaded[0].id);
		rockShader.setInt("material.texture_diffuse1", 0);

		rockShader.setMat4("projection", glm::value_ptr(projection));
		rockShader.setMat4("view", glm::value_ptr(view));
		glm::mat4 rotateOrbital;
		rotateOrbital = glm::translate(rotateOrbital, glm::vec3(5.0f, 33.0f, 0.0f));
		rotateOrbital = glm::rotate(rotateOrbital, - (float)glfwGetTime(), glm::vec3(0.0f, 1.0f, 0.0f));
		rockShader.setMat4("rotate", glm::value_ptr(rotateOrbital));
		for (unsigned int i = 0; i != rockModel.meshes.size(); i++)
		{
			glBindVertexArray(rockModel.meshes[i].VAO);
			glDrawElementsInstanced(GL_TRIANGLES, (GLsizei)rockModel.meshes[i].indices.size(), GL_UNSIGNED_INT, 0,amount);
		}

十一、抗锯齿

1.超采样抗锯齿(Super Sample Anti-aliasing, SSAA)

  • 比正常分辨率更高的分辨率(即超采样)来渲染场景,当图像输出在帧缓冲中更新时,分辨率会被下采样(Downsample)至正常的分辨率。
  • 显然的,这样会需要更多次的绘制,而片段着色器是性能大头,所以不提倡。

2.多重采样抗锯齿(Multisample Anti-aliasing, MSAA)

  • 不同于SSAA,MSAA对一个像素只进行一次片段着色器计算,其使用的顶点属性也不是每一个采样点的属性,而是像素中间位置由顶点插值得到的属性,即与没有反走样时一样。但是,颜色缓冲却是对每一个采样点都有的,即被遮盖的采样点的颜色缓冲将会记录像素中间位置计算出来的片段颜色。最终的,一个像素的所有采样点都会记录一个颜色,然后将它们取均值就是这个像素的颜色。
  • 另外一个有趣的地方是,颜色缓冲、深度缓冲、模板缓冲都是会被细粒化到采样点,即除了片段着色器的计算结果由多个采样点共享之外,其他过程跟把采样点看作一个像素也差不多。

3. OpenGL的MSAA操作

  • 多重采样缓冲:首先通过glfw创建多重采样的缓冲
glfwWindowHint(GLFW_SAMPLES,4);
  • 启动MSAA
glEnable(GL_MULTISAMPLE);
  • 下面的图看上去没有区别对吧,😄,因为我采取的是离屏渲染,却没有给他设置多重采样缓冲,我们渲染一个四边形自然是没有太大的走样区别的。

3.离屏MSAA

  • 使用离屏MSAA与普通的离屏渲染有一些差异:
    • 首先,应当选定多重采样纹理GL_TEXTURE_2D_MULTISAMPLE,而非普通的2D纹理GL_TEXTURE_2D。并且申请空间的函数变为glTexImage2DMultisample,这是很合理的,因而二者很大的区别就在于空间,这个函数与原本的函数参数变化还挺大的,当然有用的参数还是只有那些,但是有两个特殊的,第二个参数是采样点数,最后一个参数是是否所有像素采取一样的采样方式。进一步的,其他所有的纹理相关的操作都要使用GL_TEXTURE_2D_MULTISAMPLE状态标记,自然也包括framebuffer附加纹理的函数glFramebufferTexture2D。
	glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, texColorBuffer);
	glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, GL_RGB, 800, 600,GL_TRUE);
	glTexParameteri(GL_TEXTURE_2D_MULTISAMPLE, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D_MULTISAMPLE, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0);

	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, texColorBuffer, 0);
  • 然后,对于渲染缓冲只需要修改内存分配函数就可以了,使用glRenderbufferStorageMultisample,没什么变化只是第二个参数是采样点数。
	unsigned int RBO;
	glGenRenderbuffers(1, &RBO);
	glBindRenderbuffer(GL_RENDERBUFFER, RBO);
	glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples,GL_DEPTH24_STENCIL8, 800, 600);
	glBindRenderbuffer(GL_RENDERBUFFER, 0);
  • 最后,如果我们要将帧缓冲渲染到屏幕上,不能再使用四边形渲染的方法了,这是因为多重采样帧缓冲的纹理缓冲不能用于其他的计算,尤其是不能用在着色器中,这就限制了它不能用于绘制,而是使用glBlitFramebuffer来讲一个多重采样帧缓冲还原到一般的帧缓冲中。在这里,我们采取了GL_READ_FRAMEBUFFERGL_DRAW_FRAMEBUFFER这两个状态标记。对于glBlitFramebuffer,前四个参数是源帧缓冲的原点坐标和宽高,后面四个是目标帧缓冲的参数,第七个参数是缓冲类型,这里采取的是传递颜色缓冲,最后一个参数是滤波方式。
  • 需要注意的是,如果采取离屏MSAA,就不要让GLFW开启多重采样缓冲了。
		glBindFramebuffer(GL_READ_FRAMEBUFFER, FBO);
		glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
		glBlitFramebuffer(0, 0, SCR_WIDTH, SCR_HEIGHT, 0, 0, SCR_WIDTH, SCR_HEIGHT, GL_COLOR_BUFFER_BIT, GL_NEAREST);

4.离屏MSAA的后期处理

  • 因为多重采样帧缓冲不能用于绘制,所以我们需要将其glBlitFramebuffer到一个普通的帧缓冲中去做后期处理,然后将这个普通的帧缓冲作为一个四边形绘制到默认帧缓冲的颜色缓冲中。看下图,很帅!
//后期处理
	unsigned int intermediateFBO;
	glGenFramebuffers(1, &intermediateFBO);
	glBindFramebuffer(GL_FRAMEBUFFER, intermediateFBO);

	unsigned int intermediateTex;
	glGenTextures(1, &intermediateTex);
	glBindTexture(GL_TEXTURE_2D, intermediateTex);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 800, 600, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glBindTexture(GL_TEXTURE_2D, 0);
	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, intermediateTex, 0);

	if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
	{
		std::cout << "ERROR::FRAMEBUFFER:: Indermediate Framebuffer is not complete!" << std::endl;
	}
	glBindFramebuffer(GL_FRAMEBUFFER, 0);

	float frameVertices[] =
	{
		-1.0, -1.0,  0.0, 0.0,
		 1.0, -1.0,  1.0, 0.0,
		 1.0,  1.0,  1.0, 1.0,
		 1.0,  1.0,  1.0, 1.0,
		-1.0,  1.0,  0.0, 1.0,
		-1.0, -1.0,  0.0, 0.0
	};

	unsigned int frameVBO;
	glGenBuffers(1, &frameVBO);
	unsigned int frameVAO;
	glGenVertexArrays(1, &frameVAO);

	glBindVertexArray(frameVAO);
	glBindBuffer(GL_ARRAY_BUFFER, frameVBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(frameVertices), frameVertices, GL_STATIC_DRAW);
	glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
	glEnableVertexAttribArray(0);
	glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));
	glEnableVertexAttribArray(1);
		//多重帧缓冲后期处理
		glBindFramebuffer(GL_READ_FRAMEBUFFER, FBO);
		glBindFramebuffer(GL_DRAW_FRAMEBUFFER, intermediateFBO);
		glBlitFramebuffer(0, 0, SCR_WIDTH, SCR_HEIGHT, 0, 0, SCR_WIDTH, SCR_HEIGHT, GL_COLOR_BUFFER_BIT, GL_NEAREST);

		glBindFramebuffer(GL_FRAMEBUFFER, 0);
		glDisable(GL_DEPTH_TEST);
		glDisable(GL_STENCIL_TEST);
		glClearColor(0.3f, 0.3f, 0.3f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT);

		frameShader.use();

		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, intermediateTex);
		frameShader.setInt("texColorBuffer", 0);

		glBindVertexArray(frameVAO);
		glDrawArrays(GL_TRIANGLES, 0, 6);
posted @ 2023-05-14 22:04  ETHERovo  阅读(136)  评论(0编辑  收藏  举报