透明度与混合
“透过窗户,你看到的是乌云还是彩虹?”
上一节的最后,我们利用变换在屏幕上画了很多个正方形,你会注意到,他们有的互相重叠了甚至有的被完全覆盖了。
我们将它与先前绘制彩色三角形时使用的着色器与顶点格式结合起来,试试改变一下正方形的顶点颜色,调整一下它们的透明度(Alpha值),看看会发生什么:
顶点着色器
attribute vec4 a_position;
attribute vec4 a_color;
attribute vec2 a_texCoord0;
varying vec2 v_texCoord;
varying vec4 v_color;
uniform mat4 u_proj;
uniform mat4 u_trns;
void main() {
v_texCoord = a_texCoord0;
v_color = a_color;
gl_Position = u_proj * u_trns * a_position;
}片段着色器
uniform sampler2D u_texture;
varying vec2 v_texCoord;
varying vec4 v_color;
void main() {
gl_FragColor = texture2D(u_texture, v_texCoord)*v_color;
}class Example{
Mesh mesh = new Mesh(true, 4, 6,
VertexAttribute.position,
VertexAttribute.color,
VertexAttribute.texCoords
);
//...
{
float c = Color.white.cpy().a(0.5f).toFloatBits(); // 透明度设置为0.5f
mesh.setVertices(new float[]{
//顶点坐标 颜色 纹理坐标
-0.5f, -0.5f, c, 0f, 1f,
0.5f, -0.5f, c, 1f, 1f,
0.5f, 0.5f, c, 1f, 0f,
-0.5f, 0.5f, c, 0f, 0f,
});
mesh.setIndices(new short[]{
0, 1, 2, //第一个三角形
0, 2, 3 //第二个三角形
});
}
//...
}class Example{
val mesh = Mesh(true, 4, 6,
VertexAttribute.position,
VertexAttribute.color,
VertexAttribute.texCoords
)
//...
init {
val c = Color.white.cpy().a(0.5f).toFloatBits() // 透明度设置为0.5f
mesh.setVertices(floatArrayOf(
//顶点坐标 颜色 纹理坐标
-0.5f, -0.5f, c, 0f, 1f,
0.5f, -0.5f, c, 1f, 1f,
0.5f, 0.5f, c, 1f, 0f,
-0.5f, 0.5f, c, 0f, 0f,
))
mesh.setIndices(shortArrayOf(
0, 1, 2, //第一个三角形
0, 2, 3 //第二个三角形
))
}
//...
}运行一下,看看结果:

和预期的一样,图像的alpha值降低后,我们可以透过物体看到它背后的图像,或者换个更严谨的说法“物体的颜色与它背后的图像颜色混合(Blending) 了”。
混合(Blending)
实际上,我们之所以直接使用透明度混合就能得到正确的结果,是在Mindustry的主渲染流程前已经设置了OpenGL的颜色混合模式。
我们又要拿出第二节时出现的那张GL渲染管线图:

能看到,在片段着色器为像素上色完成之后,还有一步是测试(Testing) 与混合(Blending),而我们讨论的正是其中的混合。
在片段着色器完成绘图以后,它需要将输出的像素显示到屏幕上,而此时屏幕上仍存储着之前绘制的图像,片段着色输出的像素颜色会需要与旧有的像素颜色进行混合才能正确显示透明纹理。
OpenGL默认是关闭了混合的,尽管Mindustry已经在渲染流程中开启了它,但我们还是应当知道如何设置它:
void example(){
// 开启混合
Gl.enable(Gl.blend);
// 禁用混合
Gl.disable(Gl.blend);
}fun example(){
// 开启混合
Gl.enable(Gl.blend)
// 禁用混合
Gl.disable(Gl.blend)
}如果不开启混合,那么片段着色器输出的颜色会直接覆盖旧有的颜色,而不是表现为透明图像。
启用混合后,还需要告诉OpenGL如何去混合新旧像素的颜色值,GL为这个混合方式建立了一个混合函数(Blending Function),这个方程决定了新像素与旧像素的混合计算方式。
我们将颜色定义为记录rgba的四维向量:
那么混合函数的形式是这样的:
其中:
:计算结果,像素最终的混合颜色 :来自片段着色器渲染的像素颜色 :来自屏幕上缓存的现有的像素颜色 :源颜色的计算因子 原有颜色的计算因子

我们需要为OpenGL提供的参数就是两个计算因子,计算因子由设置的生成函数来生成,可以通过OpenGL的操作函数Gl.blendFunc(sourceFactor, destFactor)来设置生成函数,OpenGL中提供了如下可选的因子生成函数,用
| 选项 | 描述 | 表达式 |
|---|---|---|
| Gl.one | 因子值恒定为1.0 | |
| Gl.zero | 因子值恒定为0.0 | |
| Gl.src_color | 因子值等于源颜色的RGB值 | |
| Gl.src_alpha | 因子值等于源颜色的alpha值 | |
| Gl.dst_color | 因子值等于目标颜色的RGB值 | |
| Gl.dst_alpha | 因子值等于目标颜色的alpha值 | |
| Gl.one_minus_src_color | 因子值等于1.0减去源颜色的RGB值 | |
| Gl.one_minus_src_alpha | 因子值等于1.0减去源颜色的alpha值 | |
| Gl.one_minus_dst_color | 因子值等于1.0减去目标颜色的RGB值 | |
| Gl.one_minus_dst_alpha | 因子值等于1.0减去目标颜色的alpha值 | |
| Gl.constant_color | 因子值等于常量颜色RGB值 | |
| Gl.one_minus_constant_color | 因子值等于1.0减去常量颜色RGB值 | |
| Gl.constant_alpha | 因子值等于常量颜色alpha值 | |
| Gl.one_minus_constant_alpha | 因子值等于1.0减去常量颜色alpha值 |
看起来是不是很复杂?实际上我们最常用的就是Gl.src_alpha与Gl.one_minus_dst_alpha。
比如,Mindustry渲染流程中设置的默认混合方式其实就是这两个函数:
void normalBlend() {
Gl.blendFunc(
Gl.srcAlpha, // 源颜色混合因子生成函数
Gl.oneMinusSrcAlpha // 现有颜色混合因子生成函数
);
}fun normalBlend() {
Gl.blendFunc(
sfactor = Gl.srcAlpha, // 源颜色混合因子生成函数
dfactor = Gl.oneMinusSrcAlpha // 现有颜色混合因子生成函数
)
}这个组合产生的混合方程形式是这样的:
这个形式很明显,就是用绘制出来的颜色alpha值来在源颜色与缓存的旧颜色之间进行线性插值,而这就是最常见的透明度混合方式。

另外,我们还可以将aplha通道的计算和RGB值的混合函数分开设置,这样做的话计算混合的颜色通道和透明度通道会使用不同的混合函数:
void example(){
Gl.blendFuncSeparate(
Gl.srcAlpha, // 源颜色混合因子生成函数
Gl.oneMinusSrcAlpha, // 现有颜色混合因子生成函数
Gl.one, // 源透明度混合因子生成函数
Gl.zero // 现有透明度混合因子生成函数
);
}fun example(){
Gl.blendFuncSeparate(
srcRGB = Gl.srcAlpha, // 源颜色混合因子生成函数
dstRGB = Gl.oneMinusSrcAlpha, // 现有颜色混合因子生成函数
srcAlpha = Gl.one, // 源透明度混合因子生成函数
dstAlpha = Gl.zero // 现有透明度混合因子生成函数
)
}在Arc中对混合做了简单的封装(真的很简单...),这个封装类型为arc.graphics.Blending,这个类型包装了四个int值用于分别存储四个混合因子生成函数,构造器有两个,区分我们能前文提到过的颜色通道与透明度通道是否分开设置:
Blending blend = new Blending(srcColor, dstColor, srcAlpha, dstAlpha);
Blending blendComb = new Blending(srcFactor, dstFactor);val blend = Blending(srcColor, dstColor, srcAlpha, dstAlpha)
val blendComb = Blending(srcFactor, dstFactor)在Blending中也定义了三个默认的混合模式,分别是:
normal:默认的混合模式,可正确显示透明度纹理,源颜色与目标颜色使用Gl.srcAlpha与Gl.oneMinusSrcAlpha,透明度使用Gl.one与Gl.zeroadditive:加法混合模式,使用透明度乘算颜色值后直接与屏幕颜色相加,源颜色与目标颜色使用Gl.srcAlpha与Gl.one,透明度使用Gl.one与Gl.oneMinusSrcAlphadisabled:禁用混合,以非混合模式直接覆盖绘制
而Blending的使用也很简单,只需要调用其apply()方法即可,这里我们修改一下前文范例中的draw方法,测试一下使用additive会得到怎么样的效果:
void draw(){
shader.bind();
tex.bind(); // 绑定纹理
Blending.additive.apply(); // 设置混合模式为加法混合模式
camera.position.set(10f, 20f);
camera.width = Core.graphics.getWidth();
camera.height = Core.graphics.getHeight();
camera.update(); // 每次使用前需要更新一次数据
shader.setUniformMatrix4("u_proj", camera.mat);
Mathf.rand.setSeed(seed);
for (int i = 0; i < 10; i++) {
float size = Mathf.random(100, 300);
transform.idt()
.translate(Mathf.range(1000), Mathf.range(1000))
.scale(size, size)
.rotate(Mathf.random(360));
shader.setUniformMatrix4("u_trns", transform);
mesh.render(shader, Gl.triangles);
}
Blending.normal.apply(); // 重设为通常混合模式
}fun draw(){
shader.bind()
tex.bind() // 绑定纹理
Blending.additive.apply() // 设置混合模式为加法混合模式
camera.position.set(10f, 20f)
camera.width = Core.graphics.getWidth()
camera.height = Core.graphics.getHeight()
camera.update() // 每次使用前需要更新一次数据
shader.setUniformMatrix4("u_proj", camera.mat)
Mathf.rand.setSeed(seed)
for (i in 0 until 10) {
val size = Mathf.random(100, 300)
transform.also{
it.idt()
it.translate(Mathf.range(1000), Mathf.range(1000))
it.scale(size, size)
it.rotate(Mathf.random(360))
}
shader.setUniformMatrix4("u_trns", transform)
mesh.render(shader, Gl.triangles)
}
Blending.normal.apply() // 重设为通常混合模式
}嗯,因为颜色值相加会提升整体亮度,结果如预期的变亮了。

小练习
在不使用着色器去处理颜色的情况下,要怎么样进行反色渲染呢?再试试通过混合制作一个颜色滤镜。