发现问题
刚刚接触一个需要截屏分享的项目,但Unity那边一直反馈透明物体截图会错误,有透明物体的地方不管该物体后面有没有其他物体,截屏出来的图片都会变透明掉了。
寻找原因
我使用的截屏方法如下:
private IEnumerator ScreenShot()
{
yield return new WaitForEndOfFrame();
Rect rect = new Rect(0, 0, (int)Screen.width, (int)Screen.height);
Texture2D screenShot = new Texture2D((int)Screen.width, (int)Screen.height, TextureFormat.ARGB32, false);
screenShot.ReadPixels(rect, 0, 0);
screenShot.Apply();
byte[] bytes = screenShot.EncodeToTGA();
string filename = Application.dataPath + "/Screenshot.tga";
System.IO.File.WriteAllBytes(filename, bytes);
Debug.Log("截图成功了");
}
刚开始我以为是我的截屏方法出了问题,我百度了很多的截屏方法也是出现这个问题,也尝试了使用Unity的PackageManager自带的Recorder截屏也是出现这样的问题,那说明问题不是出在于截屏的方法,极有可能是shader的问题。
最后尝试使用Unity默认的StandardShader,通过多次测试发现并不是所有的透明物体都会出现这个问题,Transparent显示是正常的,Fade显示是有问题的,自定义的shader一般都会出现问题。
下面的这张图就是在Unity测试的,左边是在Unity的Game视图,右边是截屏保存下来的图
通过对比发现,这三种RenderingMode的区别在于:
Opaque是不透明混合(Blend One Zero)
Transparent是预乘透明度混合(Blend One OneMinusSrcAlpha)
Fade是传统透明度混合(Blend SrcAlpha OneMinusDstAlpha)
这三种RenderingMode的意思见下表(可以参考我另一篇文章"【Unity Shader入门】6、Blend-混合",里面有详细的介绍)
假设该材质的颜色为RmGmBmAm 屏幕颜色为RsGsBsAs
RenderingMode | Blend | 最终输出 |
---|---|---|
Opaque | Blend One Zero | outColor = RmGmBmAm |
Transparent | Blend One OneMinusSrcAlpha | outColor = RmGmBmAm + RsGsBsAs * (1 - Am) |
Fade | Blend SrcAlpha OneMinusDstAlpha | outColor = RmGmBmAm * Am + RsGsBsAs * (1 - Am) |
那为什么Transparent显示是正常的,Fade显示是有问题的呢?
原来,通过上面的公式可以算出:
Transparent的模式下,输出的A通道值 outColor_A = Am + As * (1 - Am),假如原来屏幕的颜色A值是1的话,那 outColor_A = 1;其实不难发现,这个就是PS的不透明度叠加的A通道的算法,所以就会显示正常了
Fade的模式下,输出的A通道值 outColor_A = Am * Am + As * (1 - Am),假如原来屏幕的颜色A值是1的话,那 outColor_A = Am * Am + (1 - Am) ;很明显,如果材质的A通道小于1,则outColor_A也会小于1,所以就会导致出现A通道错误的效果。
其实Transparent模式就是PS的不透明度叠加的A通道的算法;Fade模式就是PS的不透明度叠加的RGB通道的算法
如果知道是这个问题就容易解决了
解决问题
在写shader的时候就得注意一下,如果是直接使用Transparent模式的话截图是没问题的;如果使用Fade模式的话A通道的不透明度混合需要单独区分开来
Shader "Unlit/BlendTest"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
}
SubShader
{
Pass
{
//这是Fade模式的,Blend SrcAlpha OneMinusDstAlpha
Blend SrcAlpha OneMinusDstAlpha,One OneMinusSrcAlpha
//这是Transparent模式的,是没问题的,Blend One OneMinusSrcAlpha
//Blend One OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 vertex : SV_POSITION;
};
uniform float4 _Color;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = _Color;
return col;
}
ENDCG
}
}
}
那问题来了,如果是不透明材质(关闭混合),材质的A通道又被调了不是1呢?
这样的话屏幕的A通道值就会被该材质的A通道值直接替换,所以截出来的图片还是会变透明的,但是Unity默认的shader没有这个问题,看一下Unity内置的shader的源码就不难发现:
如果不开启_ALPHABLEND_ON和_ALPHAPREMULTIPLY_ON,则输出的A通道强制为1,这样截屏就不会出现问题了。
所以我们在写shader的时候也可以这样实现,如果是不透明shader,则强制A通道输出为1。