Android Lesson Five: An Introduction to Blending
这节课,我们来学习混合(blending)在OpenGL中的基本使用。我们来看看如何打开或关闭混合,怎样设置不同的混合模式,以及不同的混合模式如何模拟现实生活中的效果。在后面的课程中,我们还将介绍如何使用alpha通道,如何使用深度缓冲区在同一个场景中渲染半透明和不透明的物体,以及什么时候按深度排序对象,以及为什么。 我们还将研究如何监听触摸事件,然后基于此更改渲染状态。 |
本系列每个课程构建都是以前一个课程为基础。然而,对于这节课,如果您理解了Android OpenGL ES 2.0(一)---入门就足够了。尽管代码基本上是前一课的,但是光照和纹理部分已在本课中移除,所以我们仅关注混合。
混合是将一种颜色与另一种颜色组合以获得第三种颜色的行为。我们在现实世界任何时候都能看到混合:当光线穿过玻璃并在玻璃表面发生反射时,当光源本身叠加在背景上时,例如我们在晚上看到一盏明亮的路灯周围的耀斑。
OpenGL有不同的混合模式,我们能使用它模拟这种效果。在OpenGL中,混合发生在渲染过程的后期:一旦片元着色器计算出片元的最终输出颜色并且它即将被写入帧缓冲区,就会发生这种情况。通常情况下,这片元会覆盖之前所有内容,但如果启用了混合,那么该片元将与之前的片元混合。
默认情况下,OpenGL的默认混合方程式相当于调用函数glBlendEquation()设置值为GL_FUNC_ADD:
output = (source factor * source fragment) + (destination factor * destination fragment)
Ps:destination fragment的意思是先在屏幕规定的某个区域绘制的像素,source fragment表示你后在同样的区域绘制的像素,比如区域(50,50,100,100)。
OpenGL ES2.0 中还有另外两种模式GL_FUNC_SUBTRACT和GL_FUNC_REVERSE_SUBTRACT。这些可能在以后的教程中介绍,然而,当我尝试调用此函数时,我在Nexus S上遇到了UnsupportedOperationException,因此Android实现可能实际上不支持此功能。这不是世界末日,因为你可以用GL_FUNC_ADD做很多事情。
使用函数glBlendFunc()设置源因子和目标因子。下面将给出几个常见混合因子的概述;更多信息以及不同可能的因素的列举,请参阅Khronos在线手册。
OpenGL预期的输入被限制在[0,1]的范围内,并且输出也被限制在[0,1]。这在实践中意味着当您进行混合时,颜色可以在色调中移动。如果继续向帧缓冲区添加红色(RGB = 1,0,0),最终颜色会是红色。如果想添加一点儿绿色,您要添加(RGB = 1,0.1,0)到缓冲区,即使您开始带红色的色调,最后也会得到黄色!打开混合时,您可以在本课程的Demo中看到此效果:不同颜色的重叠后的颜色变得过渡饱和。
加法混合(Additive blending)
RGB颜色相加模型
加法混合是我们将不同颜色相加到一起所做的混合类型。这就是我们的视觉与光一起工作的方式,这就是我们如何在我们的显示器上感知数百万种不同的颜色 - 它们实际上只是将三种不同的原色混合在一起。
这种混合在3D混合中很有用,例如在粒子效果中,它们似乎发出光线和覆盖物,例如灯光周围的光晕,或光剑周围的发光效果。
相加混合能通过调用glBlendFunc(GL_ONE, GL_ONE)指定,
混合的结果等式输出=(1 * 源片段) + (1 * 目标片段),运算后:输出=源片段 + 目标片段
乘法混合(Multiplicative blending)
光照贴图的一个例子
乘法混合(也称为调制)是另一种有用的混合模式,它表示光在通过滤色器时的行为方式,或者从被点亮的物体反射并进入我们的眼睛时的行为。 红色物体对我们来说是红色的,因为当白光照射到物体上时,蓝色和绿色光被吸收。 只有红光反射回我们的眼睛。 在上面的例子中,我们可以看到表面反射一些红色和一些绿色,基本没有反射蓝色。
当多纹理不可用时,乘法混合用于在游戏中实现光照贴图。纹理与光照贴图相乘,以填充在明亮和阴影的区域。
相乘混合能通过调用glBlendFunc(GL_DST_COLOR, GL_ZERO)指定,
其混合的结果等式输出=(目标片段 * 源片段)+ (0 * 目标片段),写作:输出=目标片段 * 源片段。
相乘混合能通过调用glBlendFunc(GL_DST_COLOR, GL_ZERO)指定,
其混合的结果等式输出=(目标片段 * 源片段)+ (0 * 目标片段),写作:输出=目标片段 * 源片段。
插值混合(Interpolative blending)
插值混合结合了乘法和加法,以提供插值效果。与添加和调制本身不同,此混合模式也可是依赖绘制顺序的。因此在某些情况下,如果您先画出最远的半透明物体,然后绘制更近的物体,结果才会是正确。即使排序也不是完美,因为三角形可能重叠并相交,但产生的伪像可能是可接受的。
插值通常是将相邻的表面混合在一起,以及做有色玻璃或淡入淡出的效果。上面这个图片显示了两个纹理使用插值混合在一起。
插值混合通过调用glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)指定,
其混合结果等式输出 = (源alpha * 源片段) + ((1 - 源alpha) * 目标片段)。这是一个例子:
这是一个例子:想象一下,我们正在绘制一个透明度为25%的绿色(0,1,0),当前屏幕上的物体时红色(1,0,0)。
output = (source factor * source fragment) + (destination factor * destination fragment)
output = (source alpha * source fragment) + ((1 – source alpha) * destination fragment)
output = (0.25 * (0r, 1g, 0b)) + (0.75 * (1r, 0g, 0b))
output = (0r, 0.25g, 0b) + (0.75r, 0g, 0b)
output = (0.75r, 0.25g, 0b)
注意,我们不需要对目标alpha做任何涉及,因为这个帧缓冲区本身不需要alpha通道,这为我们提供了更多的颜色通道位。
在我们的课程中,我们的Demo将使用加法混合将立方体显示为可以自己发光的物体。因为发光的物体不需要光照,所以这个Demo中没有光照计算。我也删除了纹理,虽然它可以很好地使用。本课程的着色器程序很简单;我们只需要一个可传递颜色的着色器。
uniform mat4 u_MVPMatrix; // A constant representing the combined model/view/projection matrix.
attribute vec4 a_Position; // Per-vertex position information we will pass in.
attribute vec4 a_Color; // Per-vertex color information we will pass in.
varying vec4 v_Color; // This will be passed into the fragment shader.
// The entry point for our vertex shader.
void main()
{
// Pass through the color.
v_Color = a_Color;
// gl_Position is a special variable used to store the final position.
// Multiply the vertex by the matrix to get the final point in normalized screen coordinates.
gl_Position = u_MVPMatrix * a_Position;
}
precision mediump float; // Set the default precision to medium. We don't need as high of a
// precision in the fragment shader.
varying vec4 v_Color; // This is the color from the vertex shader interpolated across the
// triangle per fragment.
// The entry point for our fragment shader.
void main()
{
// Pass through the color
gl_FragColor = v_Color;
}
开启混合很简单,调用下面的方法:
// No culling of back faces
GLES20.glDisable(GLES20.GL_CULL_FACE);
// No depth testing
GLES20.glDisable(GLES20.GL_DEPTH_TEST);
// Enable blending
GLES20.glEnable(GLES20.GL_BLEND);
GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE);
我们关闭背面剔除,是因为如果立方体是半透明的,那么现在我们能看到立方体的背面。我们需要绘制它们,否则可能看起来会很奇怪。出于同样的原因我们关闭了深度测试。
您将注意到,当您运行Demo时,可以通过点击屏幕来打开和关闭混合。 请参考文章 “Listening to Android Touch Events, and Acting on Them” 获取更多更与触摸事件响应的信息。
这个Demo目前仅使用相加混合,尝试改变其为插值混合并重新添加灯光和纹理。如果您只在黑色背景上绘制两个半透明纹理,绘制顺序是否重要?
可以从GitHub上的项目站点下载本课程的完整源代码。