這次效果制作主要學(xué)習(xí)了Value Noise的活用以及使用腳本處理屏幕后期效果。
大致步驟是:首先用一個(gè)新的相機(jī)(注:已使用Command Buffer,現(xiàn)在只需要主相機(jī))使用SetReplacementShader
把屏幕中材質(zhì)標(biāo)記有Glowable
的物體替換為只渲染全白的顏色的shader,得到的RT記為貼圖1;然后在OnRenderImage
中使用Graphics.Blit
將生成的RT提供給模糊后處理材質(zhì)進(jìn)行全屏模糊,得到的RT記為貼圖2。將貼圖1貼圖2提供給一個(gè)混合材質(zhì),其shader的作用是先得到貼圖1貼圖2的差值的絕對值(純白的輝光),并計(jì)算Value Noise的采樣得到最終混合的彩色加性輝光效果。
模糊
這里采用了高斯模糊。一種比較有效的方法是在shader中寫兩個(gè)Pass,一個(gè)縱向模糊一個(gè)橫向模糊,然后在OnRenderImage
中開一個(gè)臨時(shí)RT(記得最后要RenderTexture.ReleaseTemporary(temp)
),我們需要做的就是將src
RT做一次橫向模糊再做一次縱向模糊,即達(dá)到二維高斯模糊的效果。如果再增加模糊的迭代次數(shù)可以得到較好的效果,但是相對的也消耗性能。
for (int i = 0; i < 5; i++)
{
var temp = RenderTexture.GetTemporary(Blurred.width, Blurred.height);
Graphics.Blit(Blurred, temp, _blurMat, 0);
Graphics.Blit(temp, Blurred, _blurMat, 1);
RenderTexture.ReleaseTemporary(temp);
}
另外,如果一直保持高斯模糊采樣的距離不變的話,會(huì)導(dǎo)致離得太遠(yuǎn)的物體輝光過于強(qiáng)烈。
因此我們可以在腳本中計(jì)算好物體和相機(jī)的距離,然后在模糊的過程中將高斯模糊的距離除以一個(gè)和距離正相關(guān)的數(shù)即可(類似透視除法)。
_BlurSize.x /= _Distance / 10;
fixed4 s = tex2D(_MainTex, i.uv) * 0.38774;
s += tex2D(_MainTex, i.uv + float2(_BlurSize.x * 2, 0)) * 0.06136;
s += tex2D(_MainTex, i.uv + float2(_BlurSize.x, 0)) * 0.24477;
s += tex2D(_MainTex, i.uv + float2(_BlurSize.x * -1, 0)) * 0.24477;
s += tex2D(_MainTex, i.uv + float2(_BlurSize.x * -2, 0)) * 0.06136;
如果在最后混合時(shí)將采用的貼圖1貼圖2的差絕對值改為差最小為0的話,也能做出無對內(nèi)發(fā)光的效果。
//fixed4 glow = abs(tex2D(_GlowBlurredTex, uvTemp) - tex2D(_GlowPrePassTex,uvTemp));
fixed4 glow = max(0, tex2D(_GlowBlurredTex, uvTemp) - tex2D(_GlowPrePassTex,uvTemp));
Value Noise
開始時(shí)的想法是制作類似寒氣向外消散的效果,于是在網(wǎng)上搜索了一番發(fā)現(xiàn)了Shadertoy的Ball of Fire這個(gè)效果。雖然只有短短三四十行代碼,但是效果確實(shí)驚艷。
分析效果和代碼后發(fā)現(xiàn),這個(gè)效果其實(shí)是由一個(gè)非常簡單的3D Value 噪聲得到的,這里就不具體講其定義了。
第一維是屏幕坐標(biāo)相對中心的角度,第二維是屏幕坐標(biāo)相對中心的距離加上時(shí)間(以達(dá)到向外擴(kuò)散的效果),第三個(gè)維度是時(shí)間(以達(dá)到得到圖像整體隨時(shí)間變化的效果)。不過此處我們還有一個(gè)問題要解決,因?yàn)榻嵌染S度是從0到360度,如何保證在0度附近圖像具有連續(xù)性?
這里shadertoy的作者使用了一種非常精妙的處理方法,讓0度和360度的Value Noise網(wǎng)格頂點(diǎn)生成的是相同的數(shù)據(jù)。首先隨機(jī)數(shù)的產(chǎn)生是如下所示:
float4 v = float4(xyz0.x + xyz0.y + xyz0.z , xyz1.x + xyz0.y + xyz0.z,
xyz0.x + xyz1.y + xyz0.z, xyz1.x + xyz1.y + xyz0.z);
rand = frac(sin(v/res*6.2832)*1000.0);
其中v
是根據(jù)網(wǎng)格頂點(diǎn)的整數(shù)坐標(biāo)三維相加得到的,隨機(jī)數(shù)是采用sinv
值取其小數(shù)點(diǎn)第4位之后的數(shù)得到的偽隨機(jī)數(shù)。那么如何保證0度和360度的Value Noise網(wǎng)格頂點(diǎn)生成相同隨機(jī)數(shù)呢?只需要讓網(wǎng)格x坐標(biāo)0(角度0度)和晶格坐標(biāo)x為最大值(res
,角度360度)生成的v
具有相同數(shù)值即可!因此我們使用v/res*6.2832
就可以保證0度和360度處具有連續(xù)性。當(dāng)然,此處采用的三坐標(biāo)相加的隨機(jī)數(shù)產(chǎn)生,為了讓三坐標(biāo)之間互無相關(guān),我們要讓他們在數(shù)量級上各不相同。
在我們的腳本中,要記得把物體的屏幕坐標(biāo)傳送給shader,當(dāng)做計(jì)算中屏幕的中心。
float3 xyz0 = floor(fmod(xyz,res)) * float3(1,200,1000);
float3 xyz1 = floor(fmod(xyz + float3(1,1,1),res)) * float3(1,200,1000);
最后,經(jīng)過一定的顏色處理我們就能得到這種十分酷炫的向外噴射的效果。
混合
得到上述的噪聲后,其實(shí)這里就很簡單了,自定義自己想要的Color Ramp即可。封面圖采用的是簡單的
n = smoothstep(0.1,0.9,n);
return col + glow * _Intensity * float4(n,1-n,1,0);
其中n是疊加了三個(gè)不同分辨率的octave得到的采樣值,會(huì)生成類似云彩的效果(Photoshop的云彩效果就是這么做出來的)。
float n = noiseSampler(float3(x,y*0.75 - _Time.x * 2, _Time.x * 0.15),16) * 0.5;
n += noiseSampler(float3(x,y*0.75 - _Time.x * 2, _Time.x * 0.15),32) * 0.25;
n += noiseSampler(float3(x,y*0.75 - _Time.x * 2, _Time.x * 0.15),64) * 0.125;
n += noiseSampler(float3(x,y*0.75 - _Time.x * 2, _Time.x * 0.15),128) * 0.0625;
類似的改變Color Ramp、噪聲疊加方式以及模糊距離我們還能得到許多有趣的效果。
return col + glow * _Intensity * float4(frac(sin( 4 * n)),frac(sin(4 * n + 3)),frac(sin(4 * n + 5)),0);
float n = noiseSampler(float3(x,y*0.75 - _Time.x * 2, _Time.x * 0.15),16) * 1;
return col + glow * _Intensity * n;
優(yōu)化
以上噪聲效果都是基于實(shí)時(shí)計(jì)算的,如果我們暫且不用效果不是最明顯的噪聲第三個(gè)維度(整體隨時(shí)間慢慢變化),那么實(shí)際上我們的噪聲可以通過直接采樣一張tileable的噪聲圖得到。將角度以及離中心長度度映射到UV的0-1,采樣tileable的圖既能保證角度上采樣的連續(xù)性也能保證隨時(shí)間向外擴(kuò)散的采樣連續(xù)性。
生成tileable噪聲的具體方法是在四維空間生成兩個(gè)正交圓,然后將第一第二個(gè)維度代表的圓的角度映射為0-1,將第三第四個(gè)維度代表的圓的角度也映射為0-1,然后將這兩個(gè)0-1作為UV,用對應(yīng)的圓查找四維空間的采樣值即可生成tileable的噪聲。這個(gè)貼圖的生成我們可以在Unity中寫一個(gè)小插件做到。
使用Command Buffer優(yōu)化腳本結(jié)構(gòu)
最近看了相關(guān)command buffer的內(nèi)容,回想起這個(gè)效果能夠只用一個(gè)相機(jī)就完成,腳本的數(shù)量也減少了?,F(xiàn)在我們只需要為command buffer提供一串指令讓主相機(jī)在渲染過程中完成即可,因此我們不再需要一個(gè)專門的相機(jī)去渲染標(biāo)記發(fā)光的物體,而只需要使用CommandBuffer.DrawRenderer
指令去單獨(dú)渲染某一個(gè)想要的物體即可。完整腳本內(nèi)容如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
[RequireComponent(typeof(Camera))]
public class GlowComposite : MonoBehaviour
{
[Range (0, 10)]
public float Intensity = 2;
public GameObject glowTargets = null;
public static Material compositeMat;
public static Material pureColorMaterial;
public static Material blurMat;
private CommandBuffer commandBuffer = null;
private static RenderTexture prePass;
private static RenderTexture blurred;
private static RenderTexture temp;
void OnEnable()
{
prePass = new RenderTexture(Screen.width, Screen.height, 24,RenderTextureFormat.Default);
blurred = new RenderTexture(Screen.width >> 2, Screen.height >> 2, 0);
pureColorMaterial = new Material(Shader.Find("Custom/WhiteReplace"));
blurMat = new Material(Shader.Find("Hidden/Blur"));
blurMat.SetVector("_BlurSize", new Vector2(blurred.texelSize.x * 1.5f, blurred.texelSize.y * 1.5f));
Renderer[] renderers = glowTargets.GetComponentsInChildren<Renderer>();
commandBuffer = new CommandBuffer();
commandBuffer.SetRenderTarget(prePass);
commandBuffer.ClearRenderTarget(true, true, Color.black);
foreach (Renderer r in renderers)
{
commandBuffer.DrawRenderer(r, pureColorMaterial);
}
temp = RenderTexture.GetTemporary(blurred.width, blurred.height);
commandBuffer.Blit(prePass,blurred);
for (int i = 0; i < 5; i++)
{
commandBuffer.Blit(blurred, temp,blurMat, 0);
commandBuffer.Blit(temp, blurred,blurMat, 1);
}
compositeMat = new Material(Shader.Find("Custom/GlowComposite"));
compositeMat.SetTexture("_GlowPrePassTex", prePass);
compositeMat.SetTexture("_GlowBlurredTex", blurred);
}
void OnRenderImage(RenderTexture src, RenderTexture dst)
{
Graphics.ExecuteCommandBuffer(commandBuffer);
compositeMat.SetFloat("_Intensity", Intensity);
Graphics.Blit(src, dst, compositeMat, 0);
}
void OnDisable()
{
RenderTexture.ReleaseTemporary(temp);
}
}