OpenGL中,混合(Blending)通常是实现物体透明度(Transparency)的一种技术。透明就是说一个物体(或者其中的一部分)不是纯色(Solid Color)的,它的颜色是物体本身的颜色和它背后其它物体的颜色的不同强度结合。这在我们之前的教程中其实有过类似的功能,就是在片段着色器中对两个纹理的颜色进行mix,使得最终呈现出来的颜色是两个纹理不同程度的混合:
而这次我们可以用一种更通用的方法,那就是alpha测试
在我们读取图片的时候,有的图片数据除了RGB通道之外,还包含了Alpha通道,我们生成该纹理的时候可以设置为GL_RGBA来读取:
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
有了这个Alpha参数,我们可以怎么用呢?
同样看到之前混合纹理时用的片段着色器:
FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), visibility);
我们使用了一个mix函数,使得两个纹理根据一个0-1之间的值visibility来分别设定它们的可见度,而这个visibility其实就可以看作我们读取的Alpha值,为0时原图片完全透明(着色时不使用它的颜色),为1时原图片完全不透明(着色时完全使用它的颜色)
现在我们想要实现一种效果,我们要在游戏场景中渲染一块草地,只用一个贴图,如下图:
直接将它渲染在场景中可能会是这样:
这是因为空白部分的纯色纹理也被渲染出来了,现在我们只要这个图片中草的片段被渲染出来怎么办?
这时候就要用到之前读取的图片alpha值了,在读取的时候,空白部分的alpha值被设定为0,我们可以在片段着色器里这样设置:
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D texture1;
void main()
{
vec4 texColor = texture(texture1, TexCoords);
if(texColor.a < 0.1)
discard;
FragColor = texColor;
}
没错,GLSL提供了一个discard功能,在我们读取纹理值,发现它的alpha值小于0.1时,我们就调用discard丢弃这个片段而不着色,这样就能够只渲染草片段的纹理了:
这只是一种简单的应用,实际应用时,我们还更可能碰到一种情况,就是在渲染半透明物体时可能需要将物体本身的纹理与背景的颜色混合,而使用mix函数肯定无法完成这种较为复杂的操作,这时候就不得不提到OpenGL的另一个功能了
在OpenGL中,也提供了一种类似于mix的方法,那就是混合(Blend)
我们在开始渲染时,需要开启这个功能:
glEnable(GL_BLEND);
然后,我们需要使用一个函数来指定混合效果:
glBlendFunc(GLenum sfactor, GLenum dfactor);
它的参数有这些类型:
对于一般的mix效果(使用源颜色向量的alpha作为源因子,使用1−alpha作为目标因子),我们可以这样设置:
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
我们还可以通过glBlendEquation(GLenum mode)来设置运算符达到其他效果(默认是两个颜色相加):
首先,在初始化时我们启用混合,并设定相应的混合函数:
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
由于启用了混合,我们就不需要丢弃片段了,所以我们把片段着色器还原:
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D texture1;
void main()
{
FragColor = texture(texture1, TexCoords);
}
效果:
发现有一个问题,前面的玻璃怎么把后面的玻璃遮挡住了?
这是因为开启了深度测试的原因,如果我们从近到远渲染玻璃的话,后面玻璃被遮挡的部分就不会渲染了,所以这个是比较麻烦的事,一般情况下,我们每次渲染半透明物体的时候,都是把它们按照与相机的距离从后到前渲染,我们可以利用map存储玻璃,键值设为距离,这样它就会自动按照距离存储了:
std::map<float, glm::vec3> sorted;
for (unsigned int i = 0; i < windows.size(); i++)
{
float distance = glm::length(camera.Position - windows[i]);
sorted[distance] = windows[i];
}
这里使用了map的一个反向迭代器(Reverse Iterator),反向遍历其中的条目,实现从远到近
for(std::map<float,glm::vec3>::reverse_iterator it = sorted.rbegin(); it != sorted.rend(); ++it)
{
model = glm::mat4();
model = glm::translate(model, it->second);
shader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 6);
}
那么我们最后结合深度测试、模板测试、Alpha测试和混合测试来一遍
首先开启深度和Alpha测试
glEnable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
然后渲染箱子和灯两类不透明物体:
先渲染灯
while (!glfwWindowShouldClose(window))
{
// 输入
processInput(window);
//调用glClear函数,清除颜色缓冲之后,整个颜色缓冲都会被填充为glClearColor里所设置的颜色
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
// 渲染指令
float timeValue = glfwGetTime();
deltaTime = timeValue - lastFrame;
lastFrame = timeValue;
view = projection = glm::mat4(1.0f);
projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
view = camera.GetViewMatrix();
light.view = view;
light.projection = projection;
light.Draw(lightShader);
接着开启模板测试渲染箱子,让模板缓存先写入:
glEnable(GL_STENCIL_TEST);
glStencilOp(GL_KEEP, GL_REPLACE, GL_REPLACE);
glStencilFunc(GL_ALWAYS, 1, 0xFF);
glStencilMask(0xFF);
box.view = view;
box.projection = projection;
box.Draw(ourShader, camera, timeValue);
之后我们关闭模板测试,开始从远到近绘制半透明的玻璃:
glDisable(GL_STENCIL_TEST);
glass.view = view;
glass.projection = projection;
glass.Draw(glassShader, camera);
最后我们开启模板测试,绘制箱子边框:
glEnable(GL_STENCIL_TEST);
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
//glStencilMask(0x00);
glDisable(GL_DEPTH_TEST);
box.Draw(singleShader, camera, timeValue, 1.05f);
glEnable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
glfwSwapBuffers(window);
glfwPollEvents();
}
之所以先渲染不透明再渲染半透明,是因为半透明的物体颜色需要混合背景的不透明物体
而之所以要在第一遍渲染箱子之后关闭模板测试,是为了不让渲染玻璃时影响模板缓存
alpha的操作会在multisample fragment operations里面
也就是测试顺序是:裁剪 Alpha 模板 深度 混合