最近听到了一个想法,是将Unity内的一些特效进行截图,导出带透明度的png用于视频制作,于是回顾了一下以前研究过的混融相关的问题,研究了一下如何实现,也学到了一些新的技巧和知识。
(图1:OpenGL渲染管线)
在OpenGL渲染管线中,对于透明相关的处理是在管线后期进行的。画面中的物体,三角形是按照一定顺序渲染的,当物体或三角形在位置发生重叠时可能会出现一个颜色覆盖另一个颜色的情况 ,对透明的处理过程,混融blending,实际上就是shader输出颜色与此颜色所在像素位置已有颜色的合并过程。
假如将当前Shader片段着色器要输出的颜色称为源颜色Source Color,简称Cs,将Cs(r,g,b,a)的alpha值称为Source alpha,简称srcA,此像素已有颜色设为目标颜色Color Destination简称为Cd,混融后得出的颜色Color(r,g,b,a)),简称C,那么求C的公式为:
C=Cs * srcA + Cd * (1-srcA) (公式1)
以上既是Unity中默认的混融模式“Blend SrcAlpha OneMinusSrcAlpha”背后的计算公式。此公式实际为OpenGL多个混融方程中的其中一个版本,在OpenGL中所有方程都是从公式2演变出:
C=Cs*srcfactor 操作符 Cd*dstfactor (公式2)
OpenGL对srcfactor,dstfactor提供了19个可选参数,例如公式1中的源颜色透明度srcA,也可以换为Color Destination的alpha值,或者是常数0或1,甚至还可以是一个固定rgb颜色值(管线预设好的)。
Unity对srcfactor,distractor提供了10种选择:
One | The value of one - use this to let either the source or the destination color come through fully. |
Zero | The value zero - use this to remove either the source or the destination values. |
SrcColor | The value of this stage is multiplied by the source color value. |
SrcAlpha | The value of this stage is multiplied by the source alpha value. |
DstColor | The value of this stage is multiplied by frame buffer source color value. |
DstAlpha | The value of this stage is multiplied by frame buffer source alpha value. |
OneMinusSrcColor | The value of this stage is multiplied by (1 - source color). |
OneMinusSrcAlpha | The value of this stage is multiplied by (1 - source alpha). |
OneMinusDstColor | The value of this stage is multiplied by (1 - destination color). |
OneMinusDstAlpha | The value of this stage is multiplied by (1 - destination alpha) . |
对于操作符,OpenGL可以进行5种操作,对于乘以参数后的Cs与Cd进行Cs+Cd,Cs-Cd,Cd-Cs,取最小,取最大。
在Unity中可以用 Blendop命令对操作符进行设置:
例如如果在Shader中写明Blendop Sub,那么公式1会变为:C=Cs * srcA - Cd * (1-srcA)
Unity提供的5个可选操作符:
Add | Add source and destination together. |
Sub | Subtract destination from source. |
RevSub | Subtract source from destination. |
Min | Use the smaller of source and destination. |
Max | Use the larger of source and destination. |
另外还有16种DX专用操作符。
虽然混融阶段不是可编程的,但是通过选择不同的参数与操作符,混融也有很大的可操作空间,例如在Unity中,不考虑DX专用操作符,所有可能的混融模式有10*10*5=500种。
(图2:同样的两张png图片在Unity中不同混融模式下的一些渲染差异对比)
Unity Camera的Clear Flags控制着相机在渲染当前帧之前如何处理上一帧的帧缓存,或者也可以理解为对当前帧缓存的初始化。例如Skybox意味着清除所有内容并渲染skybox,然后才进行接下来的渲染。Clear flags的设置可以看做是场景背景的设置。
剔除背景图最简单的思路是将clear flags选为solid color,然后选项下方background中的alpha改为0,再用代码将render texture写入到png中。或者利用Unity提供的custom render texture将画面export出来。但是这种方法有些问题,一是在Editor中Clear flags的background alpha为0不能正常显示为透明背景,第二是如果渲染物体Shader有需要DstAlpha(目标颜色的alpha值)时,由于它是0,而正常背景alpha都是1,会出现错误的渲染效果。
PS抠图也算是一种思路,但是它无法处理透明物体后面的背景颜色。
这里介绍的方法和上文中的混融公式有关。我们先设一个半透明物体的源颜色Cs,与背景混融后的颜色C,既然已知了公式1混融的参数与操作数:
C=Cs * srcA + Cd * (1-srcA)
进行一些变换后我们可以反过来求Cs:
Cs=(C-Cd*(1-srcA))/srcA (公式3)
观察等式发现首先要求出srcA。设两个相机,一个纯黑色背景0001,一个纯白色背景1111,对同一个画面同时进行渲染,设渲染后的颜色分别为Cblack,Cwhite,由于srcA是一个常数,我们只使用Cblack和Cwhite的rgb中的一个值进行计算,将两个r值导入公式1中设一个方程组:
Cblack.r=Cs.r*srcA+0*(1-srcA)= Cs.r*srcA
Cwhite.r=Cs.r*srcA+1*(1-srcA)
将Cblack.r导入到Cwhite.r等式中:
Cwhite.r=Cblack.r+1*(1-srcA)=Cblack.r + 1 - srcA
可变为:
srcA=Cblack.r-Cwhite.r+1
这样利用两个黑白渲染帧的r值,成功的求得了srcA,再利用srcA与Cblack还原Cs.rgb。已知:
Cblack=Cs*srcA+0001*(1-srcA)= Cs*srcA+0001-000srcA = Cs*srcA + 000B
Cblack除以srcA就得出了:
Cblack/srcA=(Sc*srcA+000B)/srcA=Cs+000C
000C是一个未知数导致Cs.a是不正确的,但是这里不用管它,因为已经求得了正确的SrcA,我们只要再将srcA赋值给Cs就最后得出了正确的剔除背景后的颜色值:
Cs.a=srcA;
以上,对于一般场景(没有多个透明物体重叠)的背景剔除还原后的rgba值是精准的,但是对于有两个以上半透明物体重叠的场景,重叠处的像素的颜色计算会有误差,还需要进一步改进。以后有时间会在回来Update一下。
老外利用以上思路写的一个工具: http://wiki.unity3d.com/index.php/AnimationToPNG
在Unity内对不同的Shader可以设置渲染顺序优先级,例如不透明的物体优先级大于透明物体。相同优先级物体的会根据Z值进行先后渲染。不透明物体根据相机视角从近到远渲染,透明物体从远到近渲染。这个设计在大部分情况下是没有问题的,并且也比较优化,但是有一个问题是如果前方的半透明物体的一部分被后方的半透明物体遮挡会出现什么效果?用以下Shader测试:
Shader "Unlit/trans1"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "Queue" = "Transparent" }
LOD 100
Blend SrcAlpha OneMinusSrcAlpha
ZWrite On
ZTest LEqual
Cull Back
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
col.a=0.5f;
return col;
}
ENDCG
}
}
}
(图3:测试结果)
可以看出后方的cube渲染后,所有像素的z值写入了深度缓存,渲染前方的cube时,每个像素的z值是精确的,但是渲染管线丢弃了Cs的颜色,对于ZTest来讲是正确的操作,但是对于混融来讲是错误的,但由于ZTest是在混融之前所以会发生这种冲突。将ZTest LEqual改为ZTest Always可以解决这一问题,但是会繁衍出另一问题,它们无法被渲染顺序在transparent之前的非透明物体遮挡。
(图4:ZTest LEqual改为ZTest Always后的测试结果)
另一个问题是同一个半透明3D物体的各个面能不能正确处理互相遮挡的关系呢?还是用上面的Shader测试:
(图5:ZTest LEqual,从一个怪物型模型头顶处观察)
(图6:ZTest Always)
测试结果是同一个物体的各个面之间无法依赖于z值去正确的互相遮挡。
再把Cull Back改为Cull Off,观察cube和sphere:
(图7:Cull Off,ZTest Lqual。cube后方反面部分三角形没有正确渲染)
(图8:Cull Off,ZTest Lqual。sphere后方反面部分三角形没有正确渲染)
(图9:Cull Off,ZTest Always。全部三角形正确渲染)
可以看出同一个物体的各个面的渲染顺序遮挡关系是以三角形为基础单位的,不管是正反面都无法期望依赖z值按正确的顺序渲染,但可以用ZTest Always强行混融渲染出正确的效果。这个问题的引发原因还要深入研究一下,以后有机会再Update此文。
————————————————————————————————————————————————————
参考:
AnimationToPNG-- Brad Nelson
OpenGL 编程指南 Chapter4--Khronos group
Unity Manual: ShaderLab Blending -- Unity Technology
维护日志:
2018-5-2:增,改
2018-5-12:改标题。增加Custom Render Texture部分。
2018-9-26:增加“当前混融机制的局限性”
2020-2-4:增,改