自从Unity发布了Command Buffer这个功能,我就一直想用一下这个功能,正好最近遇上个需求,需要让想要的物体高亮而其余物体正常,这正是用Command Buffer的好机会。
先来介绍一下bloom效果,中文一般译作“全屏泛光”,是一种常见的后处理效果。简而言之,bloom就是在高亮物体周围产生羽毛状或条纹状的光芒,造成一种光很强的错觉,如下图1所示。
最常见的bloom做法是我们先拿到摄像机渲染完成的图像,将这个图像模糊化,然后把模糊过的图像在和原图像做叠加,就得到了bloom效果。这样的做法虽然得到了bloom效果,却是对整个屏幕上的物体做的,一般来说我们会用某个亮度作为阈值,超过这个阈值的所有在屏幕上的物体都会做bloom,剩下的不做。不过我遇到的需求是只需要将屏幕中的某个物体做bloom,用亮度做阈值筛选会“伤及无辜”,所以干脆就把常见的bloom做法变通一下,改成要某个物体有bloom效果就有。
步骤不麻烦:
- 获取屏幕图像t0并将它涂黑,记为t1
- 将需要做bloom的物体画到第一步得到的图像t1上,记为t2
- 将t2模糊,得到一张糊掉的图,记为t3
- t0 + t3,再乘以想要的bloom颜色,搞定!
然后,我们照着上面的思想来开始写代码。
preTex = RenderTexture.GetTemporary(Screen.width >> blurDownSample, Screen.height >> blurDownSample,24,RenderTextureFormat.Default);
这是第一步获取一张全屏大小的图像,其中第三个参数是要求传入一个depth,根据官方文档
Depth buffer bits (0, 16 or 24). Note that only 24 bit depth has stencil buffer.
我们只能传入0、16或者24,简单来说,传入0代表获取到的全屏图像RT中的物体是不带排序的,只适用于全部物体都指定了渲染顺序的情况;16则代表是RT中的物体是排序好的;24代表RT中的物体不但排序好,RT中还有stencil buffer参与的痕迹。
还有选24的时候Z buffer的精度是“32 bit floating”,对于Z buffer精度有要求的时候就选24好了。
然后我们创造一个command buffer的实例,command buffer的作用是预定义一些渲染指令,然后在我们想要执行的时候去执行这些指令,所以我们在这个实例中塞入第一个指令涂黑图像,代码是这样的
cmd.SetRenderTarget(preTex);
cmd.ClearRenderTarget(true,true,Color.black);
再来塞入步骤2,也就是第二个指令,画个图像到全黑的RT上,我们通过command buffer中的方法DrawRenderer
实现,代码是这样的cmd.DrawRenderer(r,preMat);
其中preMat
就是需要做bloom物体的material。现在我们得到了这样一张图
接着我们要模糊这张RT
blurTex = RenderTexture.GetTemporary(Screen.width >> blurDownSample, Screen.height >>blurDownSample,0);
temp = RenderTexture.GetTemporary(blurTex.width,blurTex.height);
cmd.Blit(preTex,blurTex);
for (int i=0;i<blurNums;i++){
cmd.SetGlobalVector("offsets",new Vector4(Mathf.Pow(2.0f, i+1) / (Screen.width >> blurDownSample),0,0,0));
cmd.Blit(blurTex,temp,blurMat);
cmd.SetGlobalVector("offsets",new Vector4(0,Mathf.Pow(2.0f, i+1) / (Screen.width >> blurDownSample),0,0));
cmd.Blit(temp,blurTex,blurMat);
}
compositeMat.SetTexture("_blurTex", blurTex);
compositeMat.SetFloat("_Intensity",intensity);
compositeMat.SetColor("_GlowColor",glowColor);
我们创造了一张屏幕大小的RT叫blurTex
和一张一样大小的RT叫temp
,先把preText
复制到blurTex
(Blit的大概意思就是把一个图像复制给另一个图像),然后blurTex
在x方向模糊一下(模糊的材质指定为blurMat)给到temp
,这时temp
就是模糊过的图像,所以我们再让temp
在y方向模糊一下给回blurTex
,重复这一过程直到到达设定的模糊上限。最后blurTex
就是模糊过的图像了。
其中blurMat
是我从官方的command buffer示例中找到的高斯模糊的shader,具体请看官方示例。
PS. 这里我勉强用语言说了一下这段代码在干什么,说的不好见谅啊>_<
最后让这个模糊过的图像和屏幕图像相加。这一步我们要依靠Unity生命周期里的一个方法OnRenderImage
,具体代码是这样的。
private void OnRenderImage(RenderTexture src, RenderTexture dest) {
Graphics.ExecuteCommandBuffer(cmd);
Graphics.Blit(src,dest,compositeMat);
}
我们先执行command buffer里面一系列的渲染指令,再通过一个材质compositeMat
把原图像src
变成目标图像dest
。这个材质compositeMat
的shader是这样的。
Shader "Unlit/GlowComposite"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Pass
{
Cull Off
ZWrite Off
ZTest Always
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _blurTex;
fixed4 _GlowColor;
float _Intensity;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
fixed glow = tex2D(_blurTex,i.uv);
return col + _GlowColor * _Intensity * glow;
}
ENDCG
}
}
}
其中Cull Off
ZWrite Off
ZTest Always
为官方文档要求添加
Turn off depth buffer writes and tests in your post-processing effect shaders. This ensures that Graphics.Blit does not write unintended values into destination Z buffer. Almost all post-processing shader passes should contain
Cull Off ZWrite Off ZTest Always
states.
这样可以保证Graphics.Blit
在执行时不会把不想要的值写入Z buffer。
这里的_MainTex
就是Graphics.Blit
传入的全屏图像src,_blurTex
就是上面代码里执行的compositeMat.SetTexture("_blurTex", blurTex);
,这东西乘上强度和颜色,再和src图像叠加,最终得出的结果就是我们想要的bloom效果了。
不过现在有个问题,这个bloom效果有遮挡问题,如下图
我的解决方案是在渲染那个圆柱体的shader里面获取一下摄像头的depth buffer,把输出的颜色乘以摄像头中depth buffer的值来解决的。具体代码如下
Shader "Unlit/Test1"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque"}
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float4 screenPos : TEXCOORD2;
};
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _CameraDepthTexture;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.screenPos = ComputeScreenPos(o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
float2 texcoord = i.screenPos.xy / i.screenPos.w;
float camDepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, texcoord);
camDepth = Linear01Depth (camDepth);
col *= camDepth;
return col;
}
ENDCG
}
}
}
这样就不会有遮挡问题了。
这里要注意一下要用自带的_CameraDepthTexture
需要在Camera上挂脚本,脚本里写上GetComponent<Camera>().depthTextureMode = DepthTextureMode.Depth;
才能用。还有float2 texcoord = i.screenPos.xy / i.screenPos.w;
这里做除法是为了抵消透视所带来的影响。(原文:This division is to counteract the perspective correction the GPU automatically performs on interpolators. 可以看下这篇博客做更多了解)
参考
Unity中镜头使用的若干研究
Using Command Buffers in Unity: Selective Bloom
Unity辉光效果/噪声生成笔记