一.什么是屏幕后處理
1.1.概念
屏幕后處理是指渲染完整個(gè)場(chǎng)景得到屏幕圖像后墨榄,再對(duì)屏幕圖像做處理,實(shí)現(xiàn)屏幕特效巴比。使用這種技術(shù)抠璃, 可以為游戲畫面添加更多的藝術(shù)效果站楚, 例如景深( Depth of Field)、 運(yùn)動(dòng)模糊( Motion Blur) 等搏嗡。
實(shí)現(xiàn)屏幕后處理效果的關(guān)鍵在于得到渲染后的屏幕圖像,Unity提供了對(duì)應(yīng)的接口 OnRenderImage ,其函數(shù)聲明
MonoBehaviour.OnRenderImage(RenderTexture scr,RenderTexture dest)
參數(shù) src:源紋理源请,用于存儲(chǔ)當(dāng)前渲染的得到的屏幕圖像
**參數(shù) dest **:目標(biāo)紋理,經(jīng)過一系列操作后彻况,用于顯示到屏幕的圖像
在OnRenderImage函數(shù)中,通常調(diào)用 Graphics.Blit函數(shù) 完成對(duì)渲染紋理的處理
Graphics.Blit是每幀渲染時(shí)執(zhí)行t, 有3個(gè)重載:
public Static void Blit(Texture src,RenderTexture dest)
public Static void Blit(Texture src,RenderTexture dest,Material mat,int pass = -1)
public Static void Blit(Texture src,Material mat,int pass = -1)
參數(shù) mat : 使用的材質(zhì)舅踪,該材質(zhì)使用的Unity Shader將會(huì)進(jìn)行各種屏幕后處理操作,src對(duì)應(yīng)的紋理會(huì)傳遞給Shader中_MainTex的紋理屬性
參數(shù) pass:默認(rèn)值為-1纽甘,表示依次調(diào)用Shader內(nèi)所有Pass,否則調(diào)用索引指定的Pass抽碌。
第三個(gè)重載:Graphics.Blit(原圖悍赢,目標(biāo)决瞳,材質(zhì),指定PASS索引)左权,第四個(gè)參數(shù)是指定shader中的第幾個(gè)Pass進(jìn)行處理皮胡,索引是0開始。如:0 就是第一個(gè)Pass赏迟。 即Shader當(dāng)前起作用的SubShader的第一個(gè)Pass進(jìn)行渲染屡贺,其他Pass都不會(huì)執(zhí)行!所以這就是可以控制每一個(gè)Pass的執(zhí)行順序锌杀;而如果這個(gè)值為-1甩栈,則會(huì)按順序執(zhí)行所有Pass。
默認(rèn)情況下糕再,OnRenderImage 函數(shù)會(huì)在所有不透明和透明Pass執(zhí)行完后被調(diào)用量没,若想在不透明Pass執(zhí)行完后調(diào)用,即不對(duì)透明物體產(chǎn)生影響突想,可以在OnRenderImage函數(shù)前添加ImageEffectOpaque的屬性實(shí)現(xiàn)殴蹄。
Unity中實(shí)現(xiàn)屏幕后處理出效果,通常步驟:
1.在攝像機(jī)添加屏幕后處理腳本猾担,該腳本中會(huì)實(shí)現(xiàn)OnRenderImage函數(shù)獲取當(dāng)前屏幕渲染紋袭灯。
2.調(diào)用Graphics.Blit函數(shù)使用特定Shader對(duì)當(dāng)前圖像進(jìn)行處理,再將最終目標(biāo)紋理渲染到屏幕上垒探。對(duì)于復(fù)雜的后處理特效妓蛮,需要多次調(diào)用Graphics.Blit函數(shù)
1.2.具體做法
在進(jìn)行屏幕后處理前,需要檢查是否滿足后處理?xiàng)l件圾叼,創(chuàng)建一個(gè)用于屏幕后處理效果的基類蛤克,在實(shí)現(xiàn)屏幕后處理效果時(shí),只需繼承該基類夷蚊,再實(shí)現(xiàn)派生類中的具體操作构挤,完整代碼:
//希望在編輯器狀態(tài)下也可以執(zhí)行該腳本來查看效果
[ExecuteInEditMode]
//所有的屏幕后處理效果都需要綁定在某個(gè)攝像機(jī)上
[RequireComponent(typeof(Camera)]
public class PostEffectsBase : MonoBehaviour {
protected void CheckResources() {
bool isSupported = CheckSupport();
if (isSupported == false) {
NotSupport();
}
}
protected bool CheckSupport() {
if (SystemInfo.supportsImageEffects == false || SystemInfo.supportsRenderTextures == false) {
return false;
}
return true;
}
protected void NotSupport() {
enabled = false;
}
// Use this for initialization
void Start () {
//檢查資源和條件是否支持屏幕后處理
CheckResources();
}
protected Material CheckShaderAndCreateMaterial(Shader shader, Material material) {
if (shader == null) {
return null;
}
if (shader.isSupported && material && material.shader == shader) {
return material;
}
if (!shader.isSupported)
{
return null;
}
else {
material = new Material(shader);
material.hideFlags = HideFlags.DontSave;
if (material)
return material;
else
return null;
}
}
二.調(diào)整亮度、飽和度和對(duì)比度
新建一個(gè)腳本惕鼓,名為BrightnessSaturationAndContrast.cs筋现。添加到攝像機(jī)上。
using UnityEngine;
using System.Collections;
public class BrightnessSaturationAndContrast : PostEffectsBase {
public Shader briSatConShader;
private Material briSatConMaterial;
public Material material {
get {
briSatConMaterial = CheckShaderAndCreateMaterial(briSatConShader, briSatConMaterial);
return briSatConMaterial;
}
}
[Range(0.0f, 3.0f)]
public float brightness = 1.0f;
[Range(0.0f, 3.0f)]
public float saturation = 1.0f;
[Range(0.0f, 3.0f)]
public float contrast = 1.0f;
void OnRenderImage(RenderTexture src, RenderTexture dest) {
if (material != null) {
material.SetFloat("_Brightness", brightness);
material.SetFloat("_Saturation", saturation);
material.SetFloat("_Contrast", contrast);
//使用這個(gè)函數(shù)進(jìn)行將source傳入material的_MainTex(必須保證有這個(gè)字段)紋理進(jìn)行用material的著色器渲染
//注意:它會(huì)執(zhí)行所有Pass箱歧,進(jìn)行渲染矾飞,相當(dāng)于第四個(gè)參數(shù)為-1
Graphics.Blit(src, dest, material);//第三個(gè)參數(shù)是material材質(zhì),還用它身上的著色器shader進(jìn)行渲染
} else {
//否則呀邢,只是將原色source圖像輸出到屏幕,實(shí)際上是啥都沒干洒沦。因?yàn)闆]有著色器傳入(第三個(gè)參數(shù)material身上的著色器)
Graphics.Blit(src, dest);
}
}
}
繼承自屏幕處理基類PostEffectsBase,指定shader价淌,并根據(jù)該shader創(chuàng)建新的材質(zhì)申眼,通過OnRenderImage方法和Graphics.Blit方法將參數(shù)傳遞到shader中瞒津,完成著色,shader代碼:
Shader "Custom/Chapter12_BrightnessSaturateAndContrast" {
Properties{
_MainTex("Maintex",2D)="white"{}
_Brightness("Brightness",Float)=1
_Saturation("Saturation",Float)=1
_Contrast("Contrast",Float)=1
//Graphics.Blit(src,dest,material)會(huì)將第一個(gè)參數(shù)傳遞給Shader中名為_MainTex的屬性
}
SubShader{
Pass{
ZTest Always
Cull Off
ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
half _Brightness;
half _Saturation;
half _Contrast;
struct v2f{
float4 pos:SV_POSITION;
float2 uv:TEXCOORD0;
};
//appdata_img為Unity內(nèi)置的結(jié)構(gòu)體括尸,只包含圖像處理必須的頂點(diǎn)坐標(biāo)和紋理坐標(biāo)
v2f vert(appdata_img v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.uv=v.texcoord; //屏幕后處理得到的紋理和要輸出的紋理坐標(biāo)是相同的
return o;
}
//屏幕特效使用的頂點(diǎn)著色器代碼通常比較簡單巷蚪,我們只需要進(jìn)行必須的頂點(diǎn)變換
//更重要的是,我們需要把正確的紋理坐標(biāo)傳遞給片元著色器濒翻,以便對(duì)屏幕圖像進(jìn)行正確的采樣
//使用了內(nèi)置appdata_img 結(jié)構(gòu)體作為頂點(diǎn)著色器的輸入
//可以在 UnityCGxginc 中找到該結(jié)構(gòu)體的聲明屁柏, 它只包含了圖像處理時(shí)必需的頂點(diǎn)坐標(biāo)和紋理坐標(biāo)等變量
fixed4 frag(v2f i):SV_Target{
//采樣
fixed4 renderTex = tex2D(_MainTex, i.uv);
//首先原色RGB*亮度值 就拿到了亮度值影響后的顏色
fixed3 finalColor = renderTex.rgb * _Brightness;
//用一些特殊的浮點(diǎn)數(shù)進(jìn)行乘以原圖的r,g,b值再相加 得到一個(gè)特殊的值
fixed luminance = 0.2125 * renderTex.r + 0.7154 * renderTex.g + 0.0721 * renderTex.b;
//用這個(gè)特殊值構(gòu)成了一個(gè)飽和度為0的顏色
fixed3 luminanceColor = fixed3(luminance, luminance, luminance);
//_Saturation是飽和度,為0時(shí)則是飽和度為0的顏色值肴焊,否則越接近上面處理后的顏色(亮度影響后的)
finalColor = lerp(luminanceColor, finalColor, _Saturation);
//對(duì)比度為0的顏色值
fixed3 avgColor = fixed3(0.5, 0.5, 0.5);
//_Contrast為0時(shí)前联,拿到的是對(duì)比度為0的顏色值,否則越接近上面處理后的顏色(亮度影響后+飽和度影響后的)
finalColor = lerp(avgColor, finalColor, _Contrast);
//輸出,A通道采用原圖A通道值
return fixed4(finalColor, renderTex.a);
}
ENDCG
}
}
FallBack Off
}
Graphics.Blit(src,dest,material)會(huì)將第一個(gè)參數(shù)傳遞給Shader中名為_MainTex的屬性,然后用這個(gè)shader進(jìn)行渲染娶眷。
三.邊緣檢測(cè)
屏幕后處理中的邊緣檢測(cè)是利用一些邊緣檢測(cè)算子對(duì)圖像中的像素進(jìn)行卷積操作似嗤。
3.1.卷積概念
在圖像處理中,卷積操作指的就是使用一個(gè)卷積和對(duì)一張圖像中的每個(gè)像素進(jìn)行一系列操作届宠。卷積核通常是一個(gè)四方形網(wǎng)格結(jié)構(gòu)烁落,例如2x2,3x3的方形區(qū)域豌注,該區(qū)域內(nèi)每個(gè)網(wǎng)格都有一個(gè)權(quán)重值伤塌。當(dāng)對(duì)圖像中的某個(gè)像素進(jìn)行卷積時(shí),我們會(huì)把卷積核的中心放置于該像素上轧铁,如下圖所示每聪。
具體關(guān)于圖像處理中的卷積計(jì)算齿风,可以參考這篇博客(http://www.cnblogs.com/freeblues/p/5738987.html)
這樣的計(jì)算過程雖然簡單药薯,但可以實(shí)現(xiàn)很多常見的圖像處理效果,例如圖像模糊救斑、邊緣檢測(cè)等童本。
3.2.邊緣檢測(cè)算子
卷積操作的神奇指出 在于選擇的卷積核,用于邊緣檢測(cè)的卷積核(邊緣檢測(cè)算子)是什么脸候? 首先想一下 如果相鄰像素之間存在差別明顯的顏色穷娱,亮度等屬性,那么他們之間應(yīng)該有一條邊界运沦。這種相鄰像素之間的差值可以用梯度來表示泵额,邊緣處的梯度絕對(duì)值比較大,所以携添,就出現(xiàn)下面幾種邊緣檢測(cè)算子:
邊緣檢測(cè)算子包含兩個(gè)方向的卷積核梯刚,用來計(jì)算水平方向和豎直方向的梯度值,得到兩個(gè)方向的梯度值薪寓,而整體的梯度值可以是兩個(gè)方向上的梯度值平方和開根亡资,為了節(jié)約性能可以是兩個(gè)方向梯度值的絕對(duì)值求和,整體梯度的值越大向叉,說明該像素點(diǎn)則越有可能是邊緣位置锥腻。
關(guān)于相關(guān)算子的介紹,可以查看下邊的連接
https://wenku.baidu.com/view/abe192dc28ea81c758f5786b.html?from=search
3.3.實(shí)現(xiàn)
使用Sobel算子進(jìn)行邊緣檢測(cè)母谎,實(shí)現(xiàn)描邊效果瘦黑。
新建一個(gè)腳本,名為EdgeDetectiont.cs奇唤。添加到攝像機(jī)上幸斥。
using UnityEngine;
using System.Collections;
public class EdgeDetection : PostEffectsBase {
public Shader edgeDetectShader;
private Material edgeDetectMaterial = null;
public Material material {
get {
edgeDetectMaterial = CheckShaderAndCreateMaterial(edgeDetectShader, edgeDetectMaterial);
return edgeDetectMaterial;
}
}
[Range(0.0f, 1.0f)]
public float edgesOnly = 0.0f;//當(dāng)edgesOnly為0時(shí),邊緣將會(huì)疊加在原渲染圖像上咬扇,
//為1時(shí)甲葬,則會(huì)只顯示邊緣,不顯示原渲染圖像
public Color edgeColor = Color.black;//邊緣顏色
public Color backgroundColor = Color.white;//背景顏色
void OnRenderImage (RenderTexture src, RenderTexture dest) {
if (material != null) {
material.SetFloat("_EdgeOnly", edgesOnly);
material.SetColor("_EdgeColor", edgeColor);
material.SetColor("_BackgroundColor", backgroundColor);
Graphics.Blit(src, dest, material);
} else {
Graphics.Blit(src, dest);
}
}
}
新建一個(gè)Unity Shader懈贺。
Shader "Unlit/Chapter12-MyEdgeDetection"
{
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_EdgeOnly ("Edge Only", Float) = 1.0
_EdgeColor ("Edge Color", Color) = (0, 0, 0, 1)
_BackgroundColor ("Background Color", Color) = (1, 1, 1, 1)
}
SubShader {
Pass {
ZTest Always Cull Off ZWrite Off
CGPROGRAM
#include "UnityCG.cginc"
#pragma vertex vert
#pragma fragment fragSobel
sampler2D _MainTex;
//xxx_TexelSize 是Unity為我們提供訪問xxx紋理對(duì)應(yīng)的每個(gè)紋素的大小经窖。
//例如一張512×512的紋理,該值大小為0.001953(即1/512)梭灿。由于卷積需要對(duì)相鄰區(qū)域內(nèi)的紋理
//進(jìn)行采樣画侣,因此我們需要它來計(jì)算相鄰區(qū)域的紋理坐標(biāo)
uniform half4 _MainTex_TexelSize;
fixed _EdgeOnly;
fixed4 _EdgeColor;
fixed4 _BackgroundColor;
struct v2f {
float4 pos : SV_POSITION;
half2 uv[9] : TEXCOORD0;
};
v2f vert(appdata_img v) {
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
half2 uv = v.texcoord;
//我們?cè)趘2f結(jié)構(gòu)體中定義了一個(gè)維數(shù)為9的紋理數(shù)組,對(duì)應(yīng)了使用Sobel算子采樣時(shí)需要的9個(gè)
//鄰域紋理坐標(biāo)堡妒。通過把計(jì)算采樣紋理坐標(biāo)的代碼從片元著色器轉(zhuǎn)移到頂點(diǎn)著色器中配乱,可以減少
//運(yùn)算,提供性能。由于從頂點(diǎn)著色器到片元著色器的插值是線性的艰垂,因此這樣的轉(zhuǎn)移不會(huì)影響
//紋理坐標(biāo)的計(jì)算結(jié)果幌蚊。
o.uv[0] = uv + _MainTex_TexelSize.xy * half2(-1, -1);
o.uv[1] = uv + _MainTex_TexelSize.xy * half2(0, -1);
o.uv[2] = uv + _MainTex_TexelSize.xy * half2(1, -1);
o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1, 0);
o.uv[4] = uv + _MainTex_TexelSize.xy * half2(0, 0);
o.uv[5] = uv + _MainTex_TexelSize.xy * half2(1, 0);
o.uv[6] = uv + _MainTex_TexelSize.xy * half2(-1, 1);
o.uv[7] = uv + _MainTex_TexelSize.xy * half2(0, 1);
o.uv[8] = uv + _MainTex_TexelSize.xy * half2(1, 1);
return o;
}
fixed luminance(fixed4 color) {
return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b;
}
//利用Sobel算子計(jì)算梯度值
half Sobel(v2f i) {
//水平方向卷積核
const half Gx[9] = {-1, 0, 1,
-2, 0, 2,
-1, 0, 1};
//豎直方向卷積核
const half Gy[9] = {-1, -2, -1,
0, 0, 0,
1, 2, 1};
half texColor;
half edgeX = 0;
half edgeY = 0;
for (int it = 0; it < 9; it++) {
//采樣,得到亮度值
texColor = luminance(tex2D(_MainTex, i.uv[it]));
//水平方向上梯度
edgeX += texColor * Gx[it];
//豎直方向上梯度
edgeY += texColor * Gy[it];
}
//edge 越小佑钾,表面該位置越可能是一個(gè)邊緣點(diǎn)。
half edge = 1 - abs(edgeX) - abs(edgeY);
return edge;
}
fixed4 fragSobel(v2f i) : SV_Target {
half edge = Sobel(i);
//如果有看Sobel解釋的同學(xué)烦粒,可以看看這里的注釋)
//1. 為什么 1 - 總體梯度值呢休溶? 因?yàn)楫?dāng) 當(dāng)前顏色 和 周圍相差越大時(shí),梯度越大扰她, 此時(shí)edge 越小兽掰,lerp插值得到的是邊緣顏色!
// 如果徒役,你將lerp(_EdgeColor, tex2D(_MainTex, i.uv[4]), edge);改為 lerp(tex2D(_MainTex, i.uv[4], _EdgeColor), edge);
// 此時(shí)孽尽,edge 就等于 梯度值, 梯度值越大忧勿,說明當(dāng)前像素點(diǎn)顏色紙和周圍顏色相差越大杉女,說明是邊緣瞻讽!因此顯示出邊緣顏色!!
// i.uv[4]是當(dāng)前像素點(diǎn)紋理坐標(biāo)
//withEdgeColor:當(dāng)越接近邊緣時(shí),輸出的是邊緣顏色熏挎,否則輸出的是原圖顏色速勇,中間是這2個(gè)顏色的插值結(jié)果,以離邊緣的遠(yuǎn)近程度進(jìn)行插值運(yùn)算
fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[4]), edge);
//onlyEdgeColor:當(dāng)離邊緣越近時(shí)坎拐,輸出邊緣顏色烦磁,否則輸出背景純色顏色,中間是這2個(gè)顏色的插值結(jié)果哼勇,都伪,以離邊緣的遠(yuǎn)近程度進(jìn)行插值運(yùn)算
fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge);
//將上方2個(gè)顏色進(jìn)行插值_EdgeOnly,當(dāng)為1時(shí),則只顯示出邊緣顏色+背景純色顏色(注意背景是自定義的純色圖积担,而不是屏幕背景哦)
//否則為0時(shí)陨晶,則是顯示出邊緣+原圖
return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);
}
ENDCG
}
}
FallBack Off
}
四.高斯模糊
模糊實(shí)現(xiàn)的方式有多種珍逸,有均值模糊,中值模糊聋溜。
從數(shù)字信號(hào)處理的角度看谆膳,圖像模糊的本質(zhì)一個(gè)過濾高頻信號(hào),保留低頻信號(hào)的過程撮躁。過濾高頻的信號(hào)的一個(gè)常見可選方法是卷積濾波漱病。從這個(gè)角度來說,圖像的高斯模糊過程即圖像與正態(tài)分布做卷積把曼。由于正態(tài)分布又叫作“高斯分布”杨帽,所以這項(xiàng)技術(shù)就叫作高斯模糊。而由于高斯函數(shù)的傅立葉變換是另外一個(gè)高斯函數(shù)嗤军,所以高斯模糊對(duì)于圖像來說就是一個(gè)低通濾波器注盈。
用于高斯模糊的高斯核(Gaussian Kernel)是一個(gè)正方形的像素陣列,其中像素值對(duì)應(yīng)于2D高斯曲線的值叙赚。其中σ 是標(biāo)準(zhǔn)方差(一般取值為1)胧砰,x和y分別對(duì)應(yīng)了當(dāng)前位置到卷積核中心的整數(shù)距離。
要構(gòu)建一個(gè)高斯核苇瓣,只需要計(jì)算高斯核中各個(gè)位置對(duì)應(yīng)的高斯值尉间。為了保證濾波后的圖像不會(huì)變暗,需要對(duì)高斯核中的權(quán)重進(jìn)行歸一化,即讓每個(gè)權(quán)重除以所有權(quán)重的和哲嘲,這樣可以保證所有權(quán)重的和為1贪薪。因此,高斯函數(shù)中e的前面的系數(shù)實(shí)際不會(huì)對(duì)結(jié)果又任何影響眠副。
高斯核的維數(shù)越高古掏,處理后的圖像模糊程度會(huì)越高。當(dāng)使用一個(gè)NxN的高斯核對(duì)一個(gè)WxH的圖像進(jìn)行處理侦啸,那么對(duì)像素值的采樣結(jié)果為NxNxWxH次,N越大采樣的次數(shù)就越大丧枪。二維的高斯核可以拆成兩個(gè)一維的高斯核光涂,得到的結(jié)果和直接使用二維高斯核的結(jié)果是一樣的,這樣可以使采樣次數(shù)降低到2xNxWxH次拧烦。而兩個(gè)一維的高斯核中權(quán)重值有重復(fù)的權(quán)重值忘闻,例如一個(gè)5x5的一維高斯核只需要記錄三個(gè)權(quán)重值。
我們將會(huì)使用上述5×5的高斯核對(duì)原圖像進(jìn)行高斯模糊恋博。我們將先后調(diào)用兩個(gè)Pass齐佳,第一個(gè)Pass將會(huì)使用豎直方向的一維高斯核對(duì)圖像進(jìn)行濾波,第二個(gè)Pass再使用水平方向的一維高斯核對(duì)圖像進(jìn)行濾波债沮,得到最紅的目標(biāo)圖像炼吴。在實(shí)現(xiàn)中,還將利用圖像縮放來進(jìn)一步提高性能疫衩,并通過調(diào)整高斯濾波的應(yīng)用次數(shù)來控制模糊程度硅蹦。
新建一個(gè)腳本,名為GaussianBlur.cs闷煤。添加到攝像機(jī)上童芹。
public class GaussianBlur : PostEffectsBase{
public Shader gaussianBlurShader;
private Material gaussianBlurMaterial;
public Material material{
get{
gaussianBlurMaterial = CheckShaderAndCreateMaterial(gaussianBlurShader,gaussianBlurMaterial);
return gaussianBlurMaterial;
}
}
//迭代次數(shù)
[Range(0,4)]
public int iterations = 3;
//模糊范圍
[Range(0.2f,3.0f)]
public float blurSpread = 0.6f;
//縮放系數(shù)
[Range(1, 8)]
public int downSample = 2;
//第一個(gè)版本,最簡單的處理
/*void OnRenderImage(RenderTexture src,RenderTexture dest){
if(material == null){
int rtW = src.width;
int rtH = src.height;
//分配一個(gè)緩沖區(qū)
RenderTexture buffer = RenderTexture.GetTemporary(rtW,rtH,0);
Graphics.Blit(src,buffer,material,0);
Graphics.Blit(buffer,dest,material,1);
RenderTexture.ReleaseTemporary(buffer);
}
else{
Graphics.Blit(src,dest);
}
} */
//第二個(gè)版本 鲤拿,增加降采樣的處理
/*void OnRenderImage(RenderTexture src,RenderTexture dest){
if(material == null){
//使用了小于原屏幕分辨率的尺寸
int rtW = src.width/downSample;
int rtH = src.height/downSample;
//分配一個(gè)緩沖區(qū)
RenderTexture buffer = RenderTexture.GetTemporary(rtW,rtH,0);
//臨時(shí)渲染紋理的濾波模式設(shè)置為雙線性
buffer.filterMode = FilterMode.Bilinear;
Graphics.Blit(src,buffer,material,0);
Graphics.Blit(buffer,dest,material,1);
RenderTexture.ReleaseTemporary(buffer);
}
else{
Graphics.Blit(src,dest);
}
} */
//第三個(gè)版本假褪,增加降采樣處理及迭代的影響
void OnRenderImage(RenderTexture src,RenderTexture dest){
if(material == null){
//使用了小于原屏幕分辨率的尺寸
int rtW = src.width/downSample;
int rtH = src.height/downSample;
//分配一個(gè)緩沖區(qū)
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW,rtH,0);
//臨時(shí)渲染紋理的濾波模式設(shè)置為雙線性
buffer.filterMode = FilterMode.Bilinear;
Graphics.Blit(src,buffer0);
//進(jìn)行迭代模糊
for(int i=0;i<iterations;i++){
material.SetFloat("_BlurSoze",1.0f+i*blurSpread);
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW,rtH,0);
Graphics.Blit(buffer0,buffer1,material,0);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
buffer1 = RenderTexture.GetTemporary(rtW,rtH,0);
Graphics.Blit(buffer0,buffer1,material,1);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}
Graphics.Blit(buffer0,dest);
RenderTexture.ReleaseTemporary(buffer0);
}
else{
Graphics.Blit(src,dest);
}
}
}
新建一個(gè)Unity Shader。
Shader "Unlit/Chapter12-MyGaussianBlur"
{
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_BlurSize ("Blur Size", Float) = 1.0
}
SubShader {
//在SubShader 塊中利用CGINCLUDE 和 ENDCG 來定義一系列代碼
//這些代碼不需要包含在Pass語義塊中近顷,在使用時(shí)生音,我們只需要在Pass中指定需要
//使用的頂點(diǎn)著色器和片元著色器函數(shù)名即可。
//使用CGINCLUDE 來管理代碼 可以避免我們編寫兩個(gè)完全一樣的frag函數(shù)
//這里相當(dāng)于只是定義 執(zhí)行還是在下邊的Pass中
//使用時(shí)幕庐,在Pass中直接指定需要使用的著色器函數(shù)久锥,避免編寫完一樣的片元著色器函數(shù)
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize;
float _BlurSize;
struct v2f {
float4 pos : SV_POSITION;
half2 uv[5]: TEXCOORD0;
};
v2f vertBlurVertical(appdata_img v) {
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
half2 uv = v.texcoord;
o.uv[0] = uv;
o.uv[1] = uv + float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
o.uv[2] = uv - float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
o.uv[3] = uv + float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
o.uv[4] = uv - float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
return o;
}
v2f vertBlurHorizontal(appdata_img v) {
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
half2 uv = v.texcoord;
o.uv[0] = uv;
o.uv[1] = uv + float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
o.uv[2] = uv - float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
o.uv[3] = uv + float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
o.uv[4] = uv - float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
return o;
}
fixed4 fragBlur(v2f i) : SV_Target {
float weight[3] = {0.4026, 0.2442, 0.0545};
fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0];
for (int it = 1; it < 3; it++) {
sum += tex2D(_MainTex, i.uv[it*2-1]).rgb * weight[it];
sum += tex2D(_MainTex, i.uv[it*2]).rgb * weight[it];
}
return fixed4(sum, 1.0);
}
ENDCG
ZTest Always Cull Off ZWrite Off
Pass {
NAME "GAUSSIAN_BLUR_VERTICAL"
CGPROGRAM
#pragma vertex vertBlurVertical
#pragma fragment fragBlur
ENDCG
}
Pass {
NAME "GAUSSIAN_BLUR_HORIZONTAL"
CGPROGRAM
#pragma vertex vertBlurHorizontal
#pragma fragment fragBlur
ENDCG
}
}
FallBack "Diffuse"
}
實(shí)例效果:五.運(yùn)動(dòng)模糊
運(yùn)動(dòng)模糊是真實(shí)世界中的攝像機(jī)的一種效果。如果攝像機(jī)在曝光時(shí)异剥,場(chǎng)景發(fā)生變化瑟由,就會(huì)產(chǎn)生模糊的畫面。計(jì)算機(jī)產(chǎn)生的圖像由于不存在曝光,渲染出來的圖像往往是線條清晰歹苦,缺少運(yùn)動(dòng)模糊青伤。
運(yùn)動(dòng)模糊的實(shí)現(xiàn)有多種方式
1.利用積累緩存混合多張連續(xù)圖片
當(dāng)物體移動(dòng)產(chǎn)生多張圖片后,取這些圖片的平均值作為最后的運(yùn)動(dòng)模糊圖像殴瘦。這種方式對(duì)性能消耗有較大影響狠角,獲取多張幀圖像需要在同一幀內(nèi)多次進(jìn)行場(chǎng)景渲染。
2.使用速度緩存
這個(gè)緩存中存儲(chǔ)各個(gè)像素的運(yùn)動(dòng)速度蚪腋,使用該值決定模糊的方向和大小丰歌。
這里使用類似上述第一種方法的實(shí)現(xiàn)來模擬運(yùn)動(dòng)模糊的效果。我們不需要再一幀中把場(chǎng)景渲染多次屉凯,但需要保存之前的渲染結(jié)果立帖,不斷把當(dāng)前的渲染圖像疊加到之前的渲染圖像中,從而產(chǎn)生一種運(yùn)動(dòng)軌跡的視覺效果悠砚。這種方法與原始的利用累計(jì)緩存的方法相比性能更好晓勇,但模糊效果可能會(huì)略有影響。
實(shí)例代碼:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MotionBlur : PostEffectsBase{
public Shader motionBlurShader;
private Material motionBlurMaterial = null;
public Material material
{
get
{
motionBlurMaterial = CheckShaderAndCreateMaterial(motionBlurShader, motionBlurMaterial);
return motionBlurMaterial;
}
}
[Range(0f, 0.9f)]
public float blurAmount = 0.5f;
//blurAmount 的值越大灌旧, 運(yùn)動(dòng)拖尾的效果就越明顯绑咱, 為了防止拖尾效果完全替代當(dāng)前幀的渲染
//結(jié)果, 我們把它的值截取在 0.0-0.9 范圍內(nèi)枢泰。
private RenderTexture accumulationTexture;
private void OnDisable()
{
DestroyImmediate(accumulationTexture);//銷毀臨時(shí)紋理
}
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (material != null)
{
//確保臨時(shí)渲染紋理得大小是和屏幕一樣得,注意:這個(gè)臨時(shí)渲染紋理是不需要清空的描融,只有在禁用掉組件時(shí)才會(huì)銷毀
if(accumulationTexture == null || accumulationTexture.width != source.width ||
accumulationTexture.height != source.height)
{
DestroyImmediate(accumulationTexture);
accumulationTexture = new RenderTexture(source.width, source.height, 0);
//不保存和不顯示在Hierarchy中
accumulationTexture.hideFlags = HideFlags.HideAndDontSave;
//將屏幕圖像渲染到臨時(shí)紋理上
Graphics.Blit(source, accumulationTexture);
}
//進(jìn)行一個(gè)渲染紋理的恢復(fù)操作:恢復(fù)操作發(fā)生在渲染到紋理而該紋理又沒有被提前清空或銷毀的前提下!這個(gè)操作就是我在shader注釋所說的
//進(jìn)行一個(gè)恢復(fù)操作,不然上一幀的顏色會(huì)一直逗留在屏幕中 你可以試試注釋掉行代碼
accumulationTexture.MarkRestoreExpected();
//當(dāng)blurAmount越大 傳入的值越小宗苍,運(yùn)動(dòng)模糊效果越顯著稼稿,因?yàn)樯弦粠念伾煌耆A粝聛砹?
//具體看shader代碼有詳細(xì)解釋。讳窟。让歼。
material.SetFloat("_BlurAmount", 1.0f - blurAmount);
//將渲染的結(jié)果疊加到臨時(shí)紋理,注意丽啡,還沒疊加之前臨時(shí)紋理還保留著上一幀的渲染結(jié)果谋右,然后疊加是指Shader的一個(gè)透明度混合操作進(jìn)行的
//shader代碼用了2個(gè)PASS,第一個(gè)PASS是只混合RGB通道补箍,A通道不會(huì)進(jìn)行寫入到緩沖區(qū)改执,第二個(gè)PASS是將原始(最開始的)那個(gè)A通道,寫入到緩沖區(qū)
Graphics.Blit(source, accumulationTexture, material);
//臨時(shí)紋理輸出到屏幕
Graphics.Blit(accumulationTexture, destination);
}
else
{
Graphics.Blit(source, destination);
}
}
}
新建一個(gè)Unity Shader坑雅。
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "MilkShader/Twently/M_MotionBlur"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_BlurAmount ("Blur Amount", Float) = 1.0
}
SubShader
{
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
fixed _BlurAmount;
struct v2f{
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
};
v2f vert(appdata_img v){
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
fixed4 fragRGB(v2f i) : SV_Target{
//使用_BlurAmount作為這個(gè)片元的輸出顏色A通道,在第一個(gè)PASS中 我們開啟了混合,_BlurAmount就是作為混合因子的
//即 SrcAlpha 為 _BlurAmount
// OneMinusSrcAlpha = 1 - _BlurAmount
// outcolor = sourcecolor * SrcAlpha + destiColor * OneMinusSrcAlpha = sourcecolo * _BlurAmount + destiColor * (1-_BlurAmount)
//當(dāng)_BlurAmount越小時(shí)辈挂,完全保留顏色緩沖區(qū)的顏色值,也就是上一幀(A幀)的屏幕顏色被完全保留下來裹粤,當(dāng)然幀(B幀)的顏色會(huì)在下一幀進(jìn)行輸出到屏幕终蒂。
//在下一幀(C幀)時(shí)的時(shí),UNITY會(huì)清空當(dāng)前幀的上一幀(A幀)的屏幕顏色值或衰減顏色值等處理
//所以_BlurAmount越小時(shí),越能感覺到運(yùn)動(dòng)模糊效果拇泣,否則上一幀的顏色紙得不到保留噪叙,那就完全沒有運(yùn)動(dòng)模糊效果
return fixed4(tex2D(_MainTex, i.uv).rgb, _BlurAmount);
}
half4 fragA(v2f i): SV_Target {
return tex2D(_MainTex, i.uv);
}
ENDCG
ZTest Always Cull Off ZWrite Off
//這里將RGB和A通道分開,是由于在做混合時(shí)霉翔,按照_BlurAmount參數(shù)值將源 圖像和目標(biāo)圖像進(jìn)行混合
//而同時(shí)不讓其紋理受到A通道值的影響睁蕾,只是用來做混合,不改變其透明度
Pass
{
Blend SrcAlpha OneMinusSrcAlpha
//只輸出RGB到屏幕上
ColorMask RGB
CGPROGRAM
#pragma vertex vert
#pragma fragment fragRGB
ENDCG
}
Pass
{
Blend One Zero
//只輸出A到屏幕上
ColorMask A
CGPROGRAM
#pragma vertex vert
#pragma fragment fragA
ENDCG
}
}
Fallback Off
}