Unity3D-实现屏幕特效十字耀斑
一、前言
本文旨在与大家一起探讨学习新知识,如有疏漏或者谬误,请大家不吝指出。
二、概述
先来看下我们最终的实现效果:
上面的三对截图,左边是未加cross flare的效果,右边则是加了cross flare的效果。为了对比明显,我特意加强了flare的亮度。可以看出来flare对于氛围的营造是十分有益的,它使得整体画面更活泼和生动。
耀斑的形成,通常是由于光的折射和散射。在雨雾或者早晚湿气比较大的天气,光在空气中的折射和散射愈发明显,这使得进入人眼的光线比光源本身的范围要广,耀斑由此产生。当然,像玻璃反射太阳(视角需要特殊角度)、光线在钻石内部全反射最后汇聚射出,也同样会产生耀斑。
三、实现原理
1、使用相机渲染一张源图像。
2、将第一步产生的图像作为输入,对其进行“抠图”处理,筛选出具有亮斑的部位。保存这张图作为模板。这里需要注意模板图的分辨率,降低模板的分辨率,可以得到更大范围的耀斑。
3、使用第二步产生的图像,对其进行模糊处理,得到一张耀斑图。在这一步,不同的模糊处理将会有不同形状的耀斑。我们在这一步骤的处理方式是,沿着一个方向对其进行一次模糊,再计算出垂直方向进行二次模糊,这样就可以得到一个十字星芒图。
4、混合源图像和耀斑图,输出到屏幕。
用图来说明过程的话,大概是这个样子的:
四、具体实现
新建一个脚本,在OnRenderImage(RenderTexture src, RenderTexture dest);函数中对输入的图像进行处理。
这里简单说一说OnRenderImage这个函数,OnRenderImage是专门用做后期特效处理,它在所有渲染完成之后被调用,参数src就是屏幕完成渲染的图像,而dest则是我们在OnRenderImage函数中处理完src图像后需要返回的最终结果。
第一步,我们需要剔除源图中暗的部分,保留高亮部分用作模糊:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public class CrossFlare_Cutoff : MonoBehaviour { public float m_lumCutoff=0.75f; public float m_lum=1; public Material m_cutoffMat; void OnRenderImage(RenderTexture source, RenderTexture destination) { if ( null == m_cutoffMat) { Graphics.Blit(source, destination); return ; } m_cutoffMat.SetFloat( "_LumCutoff" , m_lumCutoff); m_cutoffMat.SetFloat ( "_Lum" , m_lum); Graphics.Blit(source, destination, m_cutoffMat); } } |
这里需要对另一个关键函数进行说明”Graphics.Blit(RenderTexture src, RenderTexture dest, Material mat)”:
Blit函数的功能很简单,将src贴图送入材质mat中作为_MainTex,然后在mat中对其进行处理,处理的结果返回到dest中。而材质mat其核心其实就是shader了。我们来看下第一步剔除低亮度的shader如何来写:
1 2 3 4 5 6 7 8 9 | float4 frag(v2f i):COLOR { float4 result; result = tex2D(_MainTex, i.uv.xy); float lum = dot(result.rgb, float3(0.33,0.33,0.33)); result.rgb *= max(0, lum-_LumCutoff)*_Lum; return result; } |
这里只列出关键的片段着色器fragment部分的代码。
其关键在于求亮度lum的操作,本例中,我们对原颜色的rgb三个部分各区0.33,即平均求值然后相加。另外,你也可以试试其他的求亮度的公式,比如:
float lum = dot(result.rgb, float3(0.2125,0.7154,0.0721));
上述公式采用不同的权重是由于眼睛对红绿蓝三种颜色有不同的敏感度,简单来说对绿色最敏感,红色次之,蓝色最后。
max(0, lum-_LumCutoff)这一步是将亮度值小于_LumCutoff阈值的颜色剔除掉(赋值为0),_Lum用于美术调整整体亮度。
图1,图为_LumCutoff=0.75, _Lum=1的渲染结果
当然,这一步还没有完成,我们还需要修改它的分辨率:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | void OnRenderImage(RenderTexture source, RenderTexture destination) { if ( null == m_cutoffMat) { Graphics.Blit(source, destination); return ; } RenderTexture lightTex = RenderTexture.GetTemporary(Mathf.FloorToInt(source.width*0.0625f), Mathf.FloorToInt(source.height*0.0625f)); m_cutoffMat.SetFloat( "_LumCutoff" , m_lumCutoff); m_cutoffMat.SetFloat ( "_Lum" , m_lum); Graphics.Blit(source, lightTex, m_cutoffMat); Graphics.Blit(lightTex, destination); RenderTexture.ReleaseTemporary(lightTex); } |
红色部分的代码是我们修改的部分。创建一张RenderTexture的贴图,其分辨率我们可以自由设置,然后将其作为渲染结果的载体。用完之后记得用Release释放贴图。
图2,图为缩小分辨率的渲染结果
接着,我们进行模糊的处理,我们先来看在shader中怎么进行模糊:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | sampler2D _MainTex; half4 _MainTex_TexelSize; //(1/width,1/height, width, height) half4 _Off1; half4 _Off2; half4 _Off3; half4 _Off4; half4 _Weight1; half4 _Weight2; half _BlurDist; half _BlurStrength; v2f vert (appdata_img v) { v2f o; float tsx = _MainTex_TexelSize.x; float tsy = _MainTex_TexelSize.y; o.pos = mul (UNITY_MATRIX_MVP, v.vertex); half2 uv = MultiplyUV (UNITY_MATRIX_TEXTURE0, v.texcoord.xy); o.uv = uv; o.offuv[0].xy = half2( _Off1.x*tsx, _Off1.y*tsy)*_BlurDist; o.offuv[1].xy = half2( _Off1.z*tsx, _Off1.w*tsy)*_BlurDist; o.offuv[2].xy = half2( _Off2.x*tsx, _Off2.y*tsy)*_BlurDist; o.offuv[3].xy = half2( _Off2.z*tsx, _Off2.w*tsy)*_BlurDist; o.offuv[4].xy = half2( _Off3.x*tsx, _Off3.y*tsy)*_BlurDist; o.offuv[5].xy = half2( _Off3.z*tsx, _Off3.w*tsy)*_BlurDist; o.offuv[6].xy = half2( _Off4.x*tsx, _Off4.y*tsy)*_BlurDist; o.offuv[7].xy = half2( _Off4.z*tsx, _Off4.w*tsy)*_BlurDist; return o; } half4 frag( v2f i ) : COLOR { half4 c=half4(0,0,0,0); c += tex2D( _MainTex, i.uv+i.offuv[0]+half2(-0.01,0.01) )*_Weight1.x; c += tex2D( _MainTex, i.uv+i.offuv[1]+half2(-0.01,0.01) )*_Weight1.y; c += tex2D( _MainTex, i.uv+i.offuv[2]+half2(-0.01,0.01) )*_Weight1.z; c += tex2D( _MainTex, i.uv+i.offuv[3]+half2(-0.01,0.01) )*_Weight1.w; c += tex2D( _MainTex, i.uv+i.offuv[4]+half2(-0.01,0.01) )*_Weight2.x; c += tex2D( _MainTex, i.uv+i.offuv[5]+half2(-0.01,0.01) )*_Weight2.y; c += tex2D( _MainTex, i.uv+i.offuv[6]+half2(-0.01,0.01) )*_Weight2.z; c += tex2D( _MainTex, i.uv+i.offuv[7]+half2(-0.01,0.01) )*_Weight2.w; c += tex2D( _MainTex, i.uv-i.offuv[0]+half2(-0.01,0.01) )*_Weight1.x; c += tex2D( _MainTex, i.uv-i.offuv[1]+half2(-0.01,0.01) )*_Weight1.y; c += tex2D( _MainTex, i.uv-i.offuv[2]+half2(-0.01,0.01) )*_Weight1.z; c += tex2D( _MainTex, i.uv-i.offuv[3]+half2(-0.01,0.01) )*_Weight1.w; c += tex2D( _MainTex, i.uv-i.offuv[4]+half2(-0.01,0.01) )*_Weight2.x; c += tex2D( _MainTex, i.uv-i.offuv[5]+half2(-0.01,0.01) )*_Weight2.y; c += tex2D( _MainTex, i.uv-i.offuv[6]+half2(-0.01,0.01) )*_Weight2.z; c += tex2D( _MainTex, i.uv-i.offuv[7]+half2(-0.01,0.01) )*_Weight2.w; c *= _BlurStrength; return c; } |
咋一看感觉特别复杂,其实原理十分简单。对一张图象进行模糊处理,就是取当前点以及周围点的颜色按照权重进行叠加。这里我们要做的是十字的模糊,那么按理来说应该是在点的x轴正负方向和y轴正负方向进行模糊。但是在实际中我们把x轴向和y轴向这两个操作合并成一个,通过二次迭代来达到同样的效果。
从上述的shader代码可以看出来,如何进行模糊是由参数_Offset1-_Offset4以及Weight1、Weight2进行控制的。我们来看看脚本中是怎么处理的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | void OnRenderImage(RenderTexture source, RenderTexture destination) { if ( null == m_cutoffMat || null == m_blurMat) { Graphics.Blit(source, destination); return ; } RenderTexture lightTex = RenderTexture.GetTemporary(Mathf.FloorToInt(source.width*0.0625f), Mathf.FloorToInt(source.height*0.0625f)); RenderTexture blurTexH = RenderTexture.GetTemporary(Mathf.FloorToInt(source.width*0.5f), Mathf.FloorToInt(source.height*0.5f)); RenderTexture blurTexV = RenderTexture.GetTemporary(Mathf.FloorToInt(source.width*0.5f), Mathf.FloorToInt(source.height*0.5f)); m_cutoffMat.SetFloat( "_LumCutoff" , m_lumCutoff); m_cutoffMat.SetFloat ( "_Lum" , m_lum); Graphics.Blit (source, lightTex, m_cutoffMat); Vector2 dir = new Vector2(1, 0.5f).normalized; for ( int i=1, j=1; i<=8; i+=2, j++) { m_blurMat.SetVector( "_Off" +j, new Vector4(i*dir.x,i*dir.y,(i+1)*dir.x,(i+1)*dir.y)); } m_blurMat.SetVector( "_Weight1" , new Vector4(0.4f, 0.35f, 0.3f, 0.25f)); m_blurMat.SetVector( "_Weight2" , new Vector4(0.2f, 0.15f, 0.1f, 0.05f)); Graphics.Blit (lightTex, blurTexH, m_blurMat); for ( int i=1, j=1; i<=8; i+=2, j++) { m_blurMat.SetVector( "_Off" +j, new Vector4(-i*dir.y,i*dir.x,-(i+1)*dir.y,(i+1)*dir.x)); } Graphics.Blit (lightTex, blurTexV, m_blurMat); Graphics.Blit (blurTexV, destination); RenderTexture.ReleaseTemporary(lightTex); RenderTexture.ReleaseTemporary(blurTexH); RenderTexture.ReleaseTemporary(blurTexV); } |
关键看两个for循环。这两个for循环就是设置_Off参数。当然,这里的写法看起来比较麻烦,是因为这个blur的代码是我比较久之前写的,unity的shader还不支持以数组的方式设置参数,现在最新的unity shader 中已经可以支持设置数组参数了。读者可以参考Unity的API:Material.SetVectorArray。
图3,上图输出的是blurTexV的结果
图4,上图输出的是blurTexH的结果
在历尽千辛万苦之后,终于只剩下最后一个步骤,混合。将blurTexV、blurTexH以及source进行混合,我们就大功告成了。
我们先来看shader部分的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | float4 frag(v2f i):COLOR { float4 result; float4 blurH, blurV; result = tex2D(_MainTex, i.uv.xy); float2 blurUV = i.uv; //flip #if UNITY_UV_STARTS_AT_TOP if (_MainTex_TexelSize.y < 0) blurUV.y = 1 - blurUV.y; #endif blurH = tex2D(_BlurTexH, blurUV); blurV = tex2D(_BlurTexV, blurUV); result.rgb = (blurV.rgb+blurH.rgb)+result.rgb; return result; } |
这是一眼就能看到底的代码,取blurTexV、blurTexH以及_MainTex三张贴图的颜色值进行相加,然后输出就行了。唯一需要注意的地方是,当unity中开启了抗锯齿(Anti Aliasing)后,需要对_MainTex进行翻转操作。
最后是脚本部分的代码:
1 2 3 | m_blendMat.SetTexture( "_BlurTexH" , blurTexH); m_blendMat.SetTexture( "_BlurTexV" , blurTexV); Graphics.Blit (source, destination, m_blendMat); |
图5,上图是最终混合后的结果
当然,眼尖的读者可能会注意到,上图中skybox部分也被加了十字耀斑的特效。如果想要只对球体进行特效叠加,那么我们需要额外创建一个Camera,然后设置此Camera的ClearFlags以及CullingMask,让它只渲染我们希望叠加十字耀斑特效的部分,将它的渲染结果作为输入即可(这将会增加额外的渲染批次drawcall)。至于这一部分的具体实现,就交给读者来解决吧(当然也可以参考后面给出的源码)。
最后,上述的解决方案是比较传统的,而且肯定不是最优的。如果您有任何想法,欢迎留言讨论。