一、深度测试
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,即不修改缓冲。
- 从这三个选项中可以看出,模板操作是在深度测试之前的,这样做的原因是:虽然二者都是片段级别的测试,但是深度测试是用于渲染的,而模板操作是用于实现效果的,所以后者的抽象级别要更高,因此如果模板测试不通过,即没有相关的效果绘制的需求,但是仍然实现了深度测试并更新了缓冲,那么么此时的缓冲就被破坏了。
- 该函数用于定义测试后的操作,指对缓冲的写操作。第一个参数用于设置模板测试失败后的操作,第二个是模板通过但是深度失败的操作,第三个是都通过的操作。默认三个参数都是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);
}