1.1.1 建立一個基本的螢幕後處理腳本系統
螢幕後處理,通常指的是渲染完整個場景得到螢幕圖像後,再對這個圖像進行一系列操作,實作各種螢幕特效。使用這種技術可以為遊戲畫面添加更多的藝術效果,例如景深(Depth of Field)、運動模糊(Motion Blur)等。
Unity提供了抓取螢幕的接口——OnRenderImage函數,其聲明如下:
MonoBehaviour.OnRenderImage(RenderTexture src, RenderTexture dest)
當腳本中聲明此函數後,Unity會把目前渲染得到的圖像存儲到第一個參數對應的原渲染紋理中,通過函數一系列操作再把目标渲染紋理,即第二個參數對應的渲染紋理顯示在螢幕上。在OnRenderImage函數中通常使用Graphics.Blit函數來完成對渲染紋理的處理,其有三種函數聲明:
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);
src對應了源紋理,在螢幕後處理技術中,該參數通常是目前螢幕的渲染紋理或是上一步處理後得到的渲染紋理。參數dest是目标渲染紋理,如果它的值為null就會直接将螢幕後處理操作,而src紋理将會被傳遞給shader中命名為_MainTex的紋理屬性,mat則是使用的材質,該材質使用的Unity Shader會進行各種螢幕後處理操作,參數pass預設為-1則是表示會依次調用shader内的所有pass,否則隻會調用給定索引的pass。
OnRenderImage函數會在所有的不透明和透明的pass執行完畢後被帶哦用,進而對場景中所有的遊戲對象都産生影響。而有時若希望在不透明Pass執行完畢後立馬調用OnRenderImage函數,進而不對透明物體産生任何影響,是以若在OnRenderImage函數前添加ImageEffectOpaque屬性來實作這樣的目的。
要在Unity中實作螢幕後處理效果,其過程通常如下:
[1]在攝像中添加一個用于螢幕後處理的腳本,在該腳本中将會調用OnRenderImage函數來擷取目前螢幕的渲染紋理。
[2]再調用Graphics.Blit函數使用特定的Unity Shader來對目前圖像進行處理,再把傳回的渲染紋理顯示到螢幕上。對于一些複雜的螢幕特效,則需要多次調用Graphics.Blit函數來對上一步的輸出結果進行下一步處理。
[3]再進行螢幕處理前需要檢查一系列條件是否滿足,例如目前*台是否支援渲染紋理和螢幕特效,是否支援目前使用的Unity Shader等。進而可以建立一個用于螢幕後處理效果的基類,在實作各種螢幕特效時隻需要繼承自該基類,再實作派生類中不同的操作即可。
其腳本代碼如下:
using UnityEngine;
using System.Collections;
[ExecuteInEditMode]
[RequireComponent (typeof(Camera))] //螢幕效果需要綁定在某個錄影機中
public class PostEffectsBase : MonoBehaviour {
// Called when start
protected void CheckResources() { //設定檢查資源的函數
bool isSupported = CheckSupport(); //bool函數判斷true 或 false
if (isSupported == false) { //如果其為false,則不支援
NotSupported();
}
}
// Called in CheckResources to check support on this platform
protected bool CheckSupport() { //設定檢查支援的函數
if (SystemInfo.supportsImageEffects == false || SystemInfo.supportsRenderTextures == false) { //判斷是否滿足不支援的條件,如果不支援,則聲明不支援然後傳回false的結果
Debug.LogWarning("This platform does not support image effects or render textures.");
return false;
}
return true; //否則傳回正确
}
// Called when the platform doesn't support this effect
protected void NotSupported() { //聲明不支援函數
enabled = false;
}
protected void Start() { //開始函數,調用檢查資源的函數,進而實作開始檢查
CheckResources();
}
// Called when need to create the material used by this effect
//因為每一個螢幕處理效果通常都需要制定一個shader來建立一個用于處理渲染紋理的材質,進而基類中也提供了該方法
protected Material CheckShaderAndCreateMaterial(Shader shader, Material material) { //建立函數,輸入值為shader檔案與material材質
if (shader == null) { //如果shader為空,則傳回空
return null;
}
if (shader.isSupported && material && material.shader == shader) //如果sahder是可支援的,同時材質的shader為該shader,則傳回材質
return material;
if (!shader.isSupported) { //shader不支援則傳回空
return null;
}
else {
material = new Material(shader); //建立材質,并把該shader賦給該材質
material.hideFlags = HideFlags.DontSave;
if (material)
return material;
else
return null;
}
}
}
1.1.2 調整螢幕的亮度、飽和度和對比度
實踐步驟:
[1]建立場景并去掉天空盒子
[2]将照片拖拽到場景中,并調節其位置進而使它充滿整個場景(該照片的紋理類型已被設定為Sprite,進而可以直接拖到場景中)
[3]建立腳本,并把該腳本拖到錄影機中
[4]建立一個Unity Shader,并編寫腳本與shader的代碼
腳本代碼:
using UnityEngine;
using System.Collections;
public class BrightnessSaturationAndContrast : PostEffectsBase { //繼承PostEffectsBase基類
public Shader briSatConShader; //聲明該效果需要的shader
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) { //調用OnRenderImage函數
if (material != null) { //如果材質不為空,則把參數傳遞給材質,若不可用,則直接把願圖像顯示到螢幕上,不做任何處理
material.SetFloat("_Brightness", brightness);
material.SetFloat("_Saturation", saturation);
material.SetFloat("_Contrast", contrast);
Graphics.Blit(src, dest, material); //調用Graphics.Blit函數
} else {
Graphics.Blit(src, dest);
}
}
}
shader代碼:
Shader "Unity Shaders Book/Chapter 12/Brightness Saturation And Contrast" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_Brightness ("Brightness", Float) = 1
_Saturation("Saturation", Float) = 1
_Contrast("Contrast", Float) = 1
}
SubShader {
Pass { //定義螢幕後處理的pass
螢幕後處理實際上是在場景中繪制了一個與螢幕同寬同高的四邊形面片,為了防止它對物體産生影響,需要設定相關的渲染狀态
ZTest Always Cull Off ZWrite Off //關閉深度寫入為了防止擋住其他後面被渲染的物體
//如如果目前OnRenderImage函數在所有不透明的Pass執行完畢後立刻被調用,不關閉深度寫入就會影響後面透明的Pass渲染,該設定可以看作是用于螢幕後處理的shader的标配
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
half _Brightness; //聲明對應的變量,接受該shader的腳本中也有
half _Saturation;
half _Contrast;
struct v2f {
float4 pos : SV_POSITION;
half2 uv: TEXCOORD0;
};
//使用Unity内置的appdata_img結構體作為頂點着色器的輸入,其隻包含圖像處理時必須的頂點坐标和紋理坐标等變量
v2f vert(appdata_img v) { //定義頂點着色器,螢幕特效使用的頂點着色器通常比較簡單,隻需要進行必要的頂點變換,把正确的紋理坐标傳遞給片元着色器,以便對螢幕圖像進行正确的采樣
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
fixed4 frag(v2f i) : SV_Target { //實作用于調整亮度、飽和度和對比度的片元着色器
fixed4 renderTex = tex2D(_MainTex, i.uv); //得到
// Apply brightness
fixed3 finalColor = renderTex.rgb * _Brightness; //利用 _Brightness來調整亮度,亮度調整隻需要把原顔色乘以亮度洗漱即可
// Apply saturation
fixed luminance = 0.2125 * renderTex.r + 0.7154 * renderTex.g + 0.0721 * renderTex.b; //luminance:亮度值;通過對每個顔色分量乘以一個特定的系數再相加即可得到
fixed3 luminanceColor = fixed3(luminance, luminance, luminance); //使用該亮度值建立一個飽和度為0的顔色值
finalColor = lerp(luminanceColor, finalColor, _Saturation); //使用_Saturation和luminanceColor進行插值進而得到希望的飽和度顔色
// Apply contrast
fixed3 avgColor = fixed3(0.5, 0.5, 0.5); //建立一個對比度為0的顔色值
finalColor = lerp(avgColor, finalColor, _Contrast); //使用_Contrast和avgColor進行插值進而得到最終的額結果處理
return fixed4(finalColor, renderTex.a);
}
ENDCG
}
}
Fallback Off
}
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsISPrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdsATOfd3bkFGazxCMx8VesATMfhHLlN3XnxCMwEzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5iMmBDMjJmN1MzN5YmY5MTOhR2Y3gzM4UmZ1EWY0UDNl9CX5EzLchDMxIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjL1M3Lc9CX6MHc0RHaiojIsJye.png)
1.1.3 邊緣檢測
卷積:卷積指的是使用一個卷積核(kernel)對一張圖像中的每個像素進行一系列操作。通過卷積,可以實作許多效果,如圖像模糊、邊緣檢測等。
常見的邊緣檢測算子:
邊緣檢測則是:将卷積核所在的區域與邊緣檢測算子進行卷積,然後
根據不同Gx、Gy得到不同的數字後,按照下面公式進行計算進而得到最終的卷積核。
或者:
using UnityEngine;
using System.Collections;
public class EdgeDetection : PostEffectsBase {
public Shader edgeDetectShader;
private Material edgeDetectMaterial = null; //初始化
public Material material {
get {
edgeDetectMaterial = CheckShaderAndCreateMaterial(edgeDetectShader, edgeDetectMaterial); //edgeDetectShader是指定的shader,edgeDetectMaterial是對應的材質
return edgeDetectMaterial;
}
}
[Range(0.0f, 1.0f)]
public float edgesOnly = 0.0f; //邊緣線強度并初始化,當其為0時,邊緣将會疊加在原渲染圖像上;當為1時,則智慧顯示邊緣,不顯示原渲染圖像
public Color edgeColor = Color.black; //描邊顔色并初始化
public Color backgroundColor = Color.white; //背景顔色的參數并初始化
void OnRenderImage (RenderTexture src, RenderTexture dest) { //當OnRenderImage被調用時,将會檢測材質是否可用,如果可用則将參數傳遞給材質
if (material != null) {
material.SetFloat("_EdgeOnly", edgesOnly); //引号部分對應shader中的參數命名,兩者需一緻
material.SetColor("_EdgeColor", edgeColor);
material.SetColor("_BackgroundColor", backgroundColor);
Graphics.Blit(src, dest, material); //調用Graphics.Blit進行處理,若不可用則直接顯示原來的圖像到螢幕上,不做任何處理
} else {
Graphics.Blit(src, dest);
}
}
}
Shader "Unity Shaders Book/Chapter 12/Edge Detection" {
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;
uniform half4 _MainTex_TexelSize;//xxx_TexelSize是Unity為開發者提供的通路xxx紋理對應的每個紋素的大小;因為卷積需要對相鄰區域内的紋理進行采樣,是以需要利用_Main_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 = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
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;
}
half Sobel(v2f i) { //定義一個維數為9的紋理數組,對應使用的sobel算子采樣時需要的9個領域紋理坐标
const half Gx[9] = {-1, 0, 1, //定義了水*方向的卷積核Gx
-2, 0, 2,
-1, 0, 1};
const half Gy[9] = {-1, -2, -1, //定義了豎直方向的卷積核Gy
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])); //依次對9個像素進行采樣,計算他們的亮度值
edgeX += texColor * Gx[it]; //再與Gx和Gy對應的權重進行相乘,得到各自的梯度值
edgeY += texColor * Gy[it];
}
half edge = 1 - abs(edgeX) - abs(edgeY); //從1中減去水*方向和豎直方向的梯度值的絕對值,得到edge,edge值越小則表明該位置越可能是一個邊緣點
return edge;
}
fixed4 fragSobel(v2f i) : SV_Target {
half edge = Sobel(i); //調用Soble函數計算目前像素的梯度值edge
fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[4]), edge); //利用該值分别計算了背景為原圖的顔色值
fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge); //利用該值分别計算了純色下的顔色值
return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly); //利用_EdgeOnly在兩者之間插值得到最終的像素值
}
ENDCG
}
}
FallBack Off
}
1.1.4 高斯模糊
模糊的實作有多種方法,例如均值模糊和中值模糊。模糊使用了卷積操作,它使用的卷積核中的每個元素值相等,且相加等于1。卷積得到的像素值是其鄰域内對各個像素值的*均值,而中值模糊則是選擇鄰域内對所有像素排序後的中值替換掉原顔色。一個更進階的模糊方法是高斯模糊。
均值濾波計算方法:
中值濾波計算方法:
高斯濾波:
為了保證濾波後的圖像不會變暗,進而需要對高斯核中的權重進行歸一化。讓每個權重除以所有權重的和。進而保證所有權重的和為1。
高斯房子模拟了鄰域每個像素對目前處理像素的影響程度——距離越*,影響越大。
二維高斯函數可以拆分成兩個一維函數,而兩個一維函數中包含了許多重複的權重。進而對于一個大小為5的一維高斯核隻需要記錄3個權重值。
而對于5x5高斯核對原圖像進行高斯模糊,将會先後調用兩個Pass,第一個Pass将會使用豎直方向的一維高斯核對圖像進行濾波,第二個Pass再使用水*方向的一維高斯核對圖像進行濾波,得到最終的目标圖像。在現實中還會利用圖像縮放來進一步提高性能,通過調整高斯濾波的應用次數來控制模糊程度(次數越多,圖像越模糊)。
實作步驟:
using UnityEngine;
using System.Collections;
public class GaussianBlur : PostEffectsBase { //繼承基類
public Shader gaussianBlurShader; //聲明該效果所需要的shader
private Material gaussianBlurMaterial = null; //建立相應的材質
public Material material {
get {
gaussianBlurMaterial = CheckShaderAndCreateMaterial(gaussianBlurShader, gaussianBlurMaterial);
return gaussianBlurMaterial;
}
}
// Blur iterations - larger number means more blur.
[Range(0, 4)]
public int iterations = 3; //高斯模糊疊代次數
// Blur spread for each iteration - larger value means more blur
[Range(0.2f, 3.0f)]
public float blurSpread = 0.6f; //模糊範圍,_BlueSize越大,模糊程度越大,但采樣數不會受到影響,過大的_BlueSize值會造成虛影
[Range(1, 8)]
public int downSample = 2; //縮放系數的參數,downSample越大,需要處理的像素數越少,同時也可以進一步提高模糊程度,但過大的downSample會造成圖像像素化
/// 1st edition: just apply blur
// void OnRenderImage(RenderTexture src, RenderTexture dest) {
// if (material != null) {
// int rtW = src.width;
// int rtH = src.height;
// 利用RenderTexture.GetTemporary函數配置設定了一塊與螢幕圖像大小相同的緩沖區,因為告訴模糊需要調用兩個Pass,進而需要使用一塊中間緩存來存儲第一個Pass執行完畢後得到的模糊結果
// RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0);
//
// // Render the vertical pass
// Graphics.Blit(src, buffer, material, 0); //第一個Pass執行完畢後得到的結果,首先利用Graphics.Blit(src, buffer, material, 0)(即豎直方向的一維高斯核進行濾波),對src進行處理,并存儲到buffer中
// // Render the horizontal pass
// Graphics.Blit(buffer, dest, material, 1); //然後利用第二個pass對buffer進行處理,傳回最終的螢幕圖像
//
// RenderTexture.ReleaseTemporary(buffer); //調用RenderTexture.ReleaseTemporary(buffer)釋放之前配置設定的記憶體
// } else {
// Graphics.Blit(src, dest);
// }
// }
/// 2nd edition: scale the render texture //在該版本中利用縮放對圖像進行降采樣,進而減少需要處理的像素個數,提高性能
// void OnRenderImage (RenderTexture src, RenderTexture dest) {
// if (material != null) {
// int rtW = src.width/downSample;
// int rtH = src.height/downSample;
// RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0);
// buffer.filterMode = FilterMode.Bilinear; //多了聲明緩沖區的大小時,使用了小于原螢幕分辨率的尺寸,并将該臨時渲染紋理的濾波模式設定為雙線性,進而不僅減少需要處理的像素個數,同時更提高了性能,适當的降采樣還可以得到更好的模糊效果
//
// // Render the vertical pass
// Graphics.Blit(src, buffer, material, 0);
// // Render the horizontal pass
// Graphics.Blit(buffer, dest, material, 1);
//
// RenderTexture.ReleaseTemporary(buffer);
// } else {
// Graphics.Blit(src, dest);
// }
// }
/// 3rd edition: use iterations for larger blur
void OnRenderImage (RenderTexture src, RenderTexture dest) { //考慮了高斯模糊的疊代次數
if (material != null) {
int rtW = src.width/downSample;
int rtH = src.height/downSample;
//執行第一個Pass時,輸入的是buffer0,輸出的時buffer1,完畢後首先把buffer0釋放,把結果隻buffer1存儲到buffer0中,再重新配置設定buffer1
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0); //定義第一個緩存buffer0
buffer0.filterMode = FilterMode.Bilinear;
Graphics.Blit(src, buffer0); //把src中的圖像縮放後存儲到buffer0中
for (int i = 0; i < iterations; i++) {
material.SetFloat("_BlurSize", 1.0f + i * blurSpread);
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// Render the vertical pass
Graphics.Blit(buffer0, buffer1, material, 0);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);//定義第二個緩存buffer1
// Render the horizontal pass
Graphics.Blit(buffer0, buffer1, material, 1);
//第二個Pass時,輸入的是buffer0,輸出的是buffer1,重複以上步驟,疊代完畢後,最終存儲在buffer0為最終的圖像
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}
Graphics.Blit(buffer0, dest); //利用Graphics.Blit(buffer0, dest)把結果顯示在螢幕上
RenderTexture.ReleaseTemporary(buffer0);
} else {
Graphics.Blit(src, dest);
}
}
}
Shader "Unity Shaders Book/Chapter 12/Gaussian Blur" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {} //聲明屬性
_BlurSize ("Blur Size", Float) = 1.0
}
SubShader {
CGINCLUDE //該代碼不需要包含任何Pass語義塊,在使用時隻需要在Pass中直接制定需要使用的頂點着色器和片元着色器即可,類似于C++中的頭檔案
//因為高斯模糊中需要定義兩個pass,但他們的片元着色器是一樣的,進而可以避免開發者編寫兩個完全一樣的frag函數
#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 = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
//因為可以拆分成2個大小為5的一維高斯核,進而隻需要計算5個紋理坐标即可
//通過把計算采樣紋理坐标的代碼從片元着色器中轉移到頂點着色器中,可以減少運算,提高性能
o.uv[0] = uv; //存儲目前的采樣紋理
o.uv[1] = uv + float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize; //剩餘的四個坐标則是高斯模糊中對鄰域采樣時使用的紋理坐标,通過和_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 = UnityObjectToClipPos(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}; //weight變量:高斯權重
fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0]; //sum初始化為目前的像素值乘以它的權重值
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); //傳回濾波結果sum
}
ENDCG
ZTest Always Cull Off ZWrite Off
Pass { //定義高斯模糊使用的兩個Pass
NAME "GAUSSIAN_BLUR_VERTICAL" //使用NAME語義定義了它們的名字,為Pass定義名字可以在shader中直接通過它們的名字來使用該Pass,而不需要重新寫代碼
CGPROGRAM
#pragma vertex vertBlurVertical
#pragma fragment fragBlur
ENDCG
}
Pass {
NAME "GAUSSIAN_BLUR_HORIZONTAL"
CGPROGRAM
#pragma vertex vertBlurHorizontal
#pragma fragment fragBlur
ENDCG
}
}
FallBack "Diffuse"
}
1.1.5 Bloom效果
(Bloom有點像輝光效果
Bloom特效是遊戲中常見的一種螢幕效果,該效果可以模拟真實錄影機的一種圖像效果,它讓畫面中較亮的區域“擴散”到周圍的區域,制造一種朦胧的效果。
Bloom的實作通過一個門檻值提取出圖像中的較亮區域,把它們存儲到一張渲染紋理中,再利用高斯模糊對這張渲染紋理進行模糊處理,模拟光線擴散的效果,最後再将其和原圖像進行混合,得到最終的效果。(有點像ps處理圖層的方法)
using UnityEngine;
using System.Collections;
public class Bloom : PostEffectsBase { //繼承基類
public Shader bloomShader; //聲明該效果所需要的shader
private Material bloomMaterial = null; //建立對應的材質并初始化
public Material material {
get {
bloomMaterial = CheckShaderAndCreateMaterial(bloomShader, bloomMaterial);
return bloomMaterial;
}
}
// Blur iterations - larger number means more blur.
[Range(0, 4)]
public int iterations = 3; //定義屬性并初始化
// Blur spread for each iteration - larger value means more blur
[Range(0.2f, 3.0f)]
public float blurSpread = 0.6f;
[Range(1, 8)]
public int downSample = 2;
[Range(0.0f, 4.0f)] //盡管在絕大多數的情況下,圖像的亮度值不會超過1,但如果開啟了HDR,硬體會允許把顔色值存儲在一個更高精度範圍的緩沖中,此時像素的亮度值可能會超過1,進而可以把luminanceThreshold的值規定在[0,4]中
public float luminanceThreshold = 0.6f; //增加luminanceThreshold來控制提取較亮的區域時使用的門檻值大小
//Bloom建立在高斯模糊之上實作,進而與高斯模糊的代碼差不多
void OnRenderImage (RenderTexture src, RenderTexture dest) {
if (material != null) {
material.SetFloat("_LuminanceThreshold", luminanceThreshold); //提取圖像中較亮的區域(沒有像前面的高斯模糊一樣一上來就直接對src進行采樣)
int rtW = src.width/downSample; //采樣
int rtH = src.height/downSample;
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0); //定義buffer0
buffer0.filterMode = FilterMode.Bilinear;
Graphics.Blit(src, buffer0, material, 0); //存儲摘取得到的較亮區域到buffer0中
for (int i = 0; i < iterations; i++) {
material.SetFloat("_BlurSize", 1.0f + i * blurSpread);
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// Render the vertical pass
Graphics.Blit(buffer0, buffer1, material, 1); //豎直,模糊後的較亮區域區域将會存儲在buffer1中
RenderTexture.ReleaseTemporary(buffer0); //釋放buffer0
buffer0 = buffer1; //buffer1的資料存儲會buffer0中
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0); //重新定義buffer1
// Render the horizontal pass
Graphics.Blit(buffer0, buffer1, material, 2); //橫向,模糊後的較亮區域區域将會存儲在buffer1中
RenderTexture.ReleaseTemporary(buffer0); //釋放buffer0
buffer0 = buffer1; //buffer0 等于 buffer1
}
material.SetTexture ("_Bloom", buffer0); //将buffer0傳遞給次啊之中的_Bloom紋理屬性
Graphics.Blit (src, dest, material, 3); //将第四個pass進行最後的混合,并存儲到目标渲染紋理dest中
RenderTexture.ReleaseTemporary(buffer0); //釋放臨時緩存
} else {
Graphics.Blit(src, dest);
}
}
}
Shader "Unity Shaders Book/Chapter 12/Bloom" {
Properties { //定義屬性
_MainTex ("Base (RGB)", 2D) = "white" {} //對應輸入的渲染紋理
_Bloom ("Bloom (RGB)", 2D) = "black" {} //高斯模糊後較亮的區域
_LuminanceThreshold ("Luminance Threshold", Float) = 0.5 //提取較亮區域的門檻值
_BlurSize ("Blur Size", Float) = 1.0 //控制不同疊代之間高斯模糊的模糊區域範圍
}
SubShader {
CGINCLUDE
#include "UnityCG.cginc"
//聲明代碼中需要使用的各個變量
sampler2D _MainTex;
half4 _MainTex_TexelSize;
sampler2D _Bloom;
float _LuminanceThreshold;
float _BlurSize;
struct v2f {
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
};
//定義提取較亮區域需要使用的頂點着色器和片元着色器
v2f vertExtractBright(appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
fixed luminance(fixed4 color) {
return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b;
}
fixed4 fragExtractBright(v2f i) : SV_Target {
fixed4 c = tex2D(_MainTex, i.uv);
fixed val = clamp(luminance(c) - _LuminanceThreshold, 0.0, 1.0); //采樣得到的亮度值減去門檻值_LuminanceThreshold,并把結果截取在0~1之間
return c * val; //把該值和願像素相乘,得到提取後的亮度區域
}
struct v2fBloom {
float4 pos : SV_POSITION;
half4 uv : TEXCOORD0;
};
//定義混合亮部圖像和原圖像時使用的頂點着色器和片元着色器
v2fBloom vertBloom(appdata_img v) {
v2fBloom o;
o.pos = UnityObjectToClipPos (v.vertex);
o.uv.xy = v.texcoord;
o.uv.zw = v.texcoord;
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0.0)
o.uv.w = 1.0 - o.uv.w;
#endif
return o;
}
fixed4 fragBloom(v2fBloom i) : SV_Target {
return tex2D(_MainTex, i.uv.xy) + tex2D(_Bloom, i.uv.zw); //定義兩個紋理坐标,并存儲在同一個類型為half4的變量uv中,xy對應_MainTex,即原圖像的紋理坐标;zw對應_Bloom,即對應模糊後的較亮區域的紋理坐标
}
ENDCG
ZTest Always Cull Off ZWrite Off
//定義Bloom效果需要的4個pass
Pass {
CGPROGRAM
#pragma vertex vertExtractBright
#pragma fragment fragExtractBright
ENDCG
}
//第二第三個pass使用上一節的高斯模糊定義的兩個pass,該是通過UsePass來指明所使用的pass,而Unity内部會把shader名字全部轉換為大寫字母,進而使用時需要大寫字母
UsePass "Unity Shaders Book/Chapter 12/Gaussian Blur/GAUSSIAN_BLUR_VERTICAL"
UsePass "Unity Shaders Book/Chapter 12/Gaussian Blur/GAUSSIAN_BLUR_HORIZONTAL"
Pass {
CGPROGRAM
#pragma vertex vertBloom
#pragma fragment fragBloom
ENDCG
}
}
FallBack Off
}
1.1.6 運動模糊
運動模糊時真實世界中的錄影機的一種效果,錄影機曝光時拍攝場景發生了變化就會産生模糊的畫面。運動模糊效果可以讓物體運動看起來更加真實光滑,但在計算機産生的圖像中,由于不存在曝光這一實體現象,渲染出來的圖像往往都是棱角分明,缺少運動模糊。
運動模糊的實作有許多種方法,[1]一種實作方法是利用一塊累積緩存(accumulation buffer)來混合多張連續的圖像。當物體快速移動産生多張圖像後,開發者取它們之間的*均值作為最後的運動模糊圖像。而該暴力的方法對性能的消耗很大,因為想要擷取多張幀圖意味着開發者需要在同一幀裡渲染多次場景。[2]另一種應用廣泛的方法是建立和使用速度緩存(velocity buffer),在該緩存中存儲了各個像素目前的運動速度,然後利用
[1]把天空盒去掉後,添加物體在場景中,并建立一個腳本和shader檔案
[2]把腳本挂在錄影機中,shader檔案挂在腳本中
[3]可以編寫一個移動腳本挂在物體上,進而可以運作檢視效果
運動模糊腳本代碼:
using UnityEngine;
using System.Collections;
public class MotionBlur : PostEffectsBase {
public Shader motionBlurShader;
private Material motionBlurMaterial = null;
public Material material {
get {
motionBlurMaterial = CheckShaderAndCreateMaterial(motionBlurShader, motionBlurMaterial);
return motionBlurMaterial;
}
}
[Range(0.0f, 0.9f)] //定義運動模糊在混合圖像時使用的模糊參數,并聲明範圍值
public float blurAmount = 0.5f;
private RenderTexture accumulationTexture; //定義一個RenderTexture類型的變量,并且儲存之前圖像疊加的結果
void OnDisable() {
DestroyImmediate(accumulationTexture);
}
void OnRenderImage (RenderTexture src, RenderTexture dest) { //定義運動模糊所使用的OnRenderImage函數
if (material != null) { //若材質不為空,可用,則運作下一個判斷
// Create the accumulation texture
if (accumulationTexture == null || accumulationTexture.width != src.width || accumulationTexture.height != src.height) { //判斷用于混合圖像的accumulationTexture是否滿足條件:不為空且與目前的螢幕分辨率相等
DestroyImmediate(accumulationTexture); //若不滿足則需要把該變量銷毀掉,并重新建立一個
accumulationTexture = new RenderTexture(src.width, src.height, 0);
accumulationTexture.hideFlags = HideFlags.HideAndDontSave; //HideAndDontSave表示該變量不會顯示在Hierarchy,也不會儲存到場景中
Graphics.Blit(src, accumulationTexture); //初始化該變量
}
// We are accumulating motion over frames without clear/discard
// by design, so silence any performance warnings from Unity
//得到有效accumulationTexture後,帶調用accumulationTexture.MarkRestoreExpected()來表示需要進行一個渲染紋理的恢複操作
//恢複操作指發生在渲染紋理而該紋理又沒有被提前清空或銷毀的情況下
//在本例中每次調用OnRenderImage需要把目前幀圖像和accumulationTexture中的圖像混合,accumulationTexture不需要提前清空,因為儲存了之前的混合結果
accumulationTexture.MarkRestoreExpected();
material.SetFloat("_BlurAmount", 1.0f - blurAmount);
Graphics.Blit (src, accumulationTexture, material);
Graphics.Blit (accumulationTexture, dest);
} else {
Graphics.Blit(src, dest);
}
}
}
Shader "Unity Shaders Book/Chapter 12/Motion Blur" {
Properties {
_MainTex ("Base (RGB)", 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;
}
//定義兩個片元着色器,一個用于更新渲染紋理的RGB通道部分,一個用于更新渲染紋理的A通道部分
fixed4 fragRGB (v2f i) : SV_Target { //RGB通道的shader對目前圖像進行采樣,并将其A通道設定為_BlurAmount,以便與可以使它的透明通道進行混合
return fixed4(tex2D(_MainTex, i.uv).rgb, _BlurAmount);
}
half4 fragA (v2f i) : SV_Target { //A通道直接傳回采樣結果,該隻是為了維護渲染紋理的透明通道,不讓其收到混合時使用的透明度值的影響
return tex2D(_MainTex, i.uv);
}
ENDCG
ZTest Always Cull Off ZWrite Off
Pass {
Blend SrcAlpha OneMinusSrcAlpha
ColorMask RGB
CGPROGRAM
#pragma vertex vert
#pragma fragment fragRGB
ENDCG
}
Pass {
Blend One Zero
ColorMask A
CGPROGRAM
#pragma vertex vert
#pragma fragment fragA
ENDCG
}
}
FallBack Off
(加了個可以用鍵盤控制物體移動的腳本在物體身上,可以看的比較仔細清楚)
移動代碼:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class NewBehaviourScript : MonoBehaviour
{
protected Transform m_transform;
[Range(0, 20)]
public float m_speed = 1;
// Use this for initialization
void Start()
{
m_transform = this.transform;
}
// Update is called once per frame
void Update()
{
float movev = 0;
float moveh = 0;
if (Input.GetKey(KeyCode.UpArrow))
{
movev += m_speed * Time.deltaTime;
}
if (Input.GetKey(KeyCode.DownArrow))
{
movev -= m_speed * Time.deltaTime;
}
if (Input.GetKey(KeyCode.LeftArrow))
{
moveh -= m_speed * Time.deltaTime;
}
if (Input.GetKey(KeyCode.RightArrow))
{
moveh += m_speed * Time.deltaTime;
}
m_transform.Translate(new Vector3(moveh, 0, movev));
}
}
1.2 Unity shader入門精要筆記(十三)
1.2.1 擷取深度和法線紋理
螢幕後處理效果都隻是在螢幕顔色圖像上進行各種操作來實作的,而很多時候不僅需要目前螢幕的顔色資訊,還希望得到深度和法線資訊。如在進行邊緣檢測時,直接利用顔色資訊會使檢測到的邊緣資訊受物體紋理和光照等外部因素的影響,得到許多不需要的邊緣點。
而更好的方法是,開發者可以在深度紋理和法線上進行邊緣檢測,這些圖像不會受紋理和光照的影響,而僅僅儲存了目前渲染的模型資訊,通過這樣的方法檢測出來的邊緣更加可靠。
背後的原理:
深度紋理實際就是一張渲染紋理,隻不過裡面存儲的像素值不是顔色值,而是一個高精度的深度值。由于被存儲到一張紋理中,深度紋理裡的深度值範圍是[0,1],而且通常是非線性分布的。而裡面的深度值來自于頂點變換後得到的歸一化的裝置坐标(Normalized Device Cooridiance, NDC)。一個模型想要最終被繪制到螢幕上,需要把它的頂點從模型空間變換到其次裁剪坐标系下,這是通過在頂點着色器中乘以MVP變換矩陣的來的。在變換的最後一步,開發者需要使用一個投影矩陣來變換頂點,而當開發者所使用的是透視投影類型的錄影機時,該投影矩陣是非線性的。
圖 1.73 透視投影矩陣轉換過程及結果
圖 1.74 非透視投影矩陣轉換過程及結果
在得到NDC後,深度紋理中的像素值可以很友善地計算得到,這些深度值對應了NDC隻能夠頂點坐标的z分量的值。由于NDC中z分量的範圍在[-1,1],為了讓這些值可以存儲在一張圖像中,可以使用下公式對其進行映射:
其中d對應深度紋理中的像素值,對應NDC中的z分量的值。
在Unity中,深度紋理可以直接來自真正的深度緩沖,其可以是由一個單獨的Pass渲染而得,這取決于使用的渲染路徑和硬體。當使用延遲渲染路徑(包括遺留的延遲渲染路徑)時,深度紋理理所當然可以通路到,因為延遲渲染會把這些資訊渲染到G=buffer中。當無法直接擷取深度緩存時,深度和法線紋理是通過一個單獨的Pass渲染而得。Unity會使用着色器替換(Shader Replacement)技術選擇那些渲染路徑(即SubShader的RenderType标簽)為Opaque的物體,判斷他們使用的渲染隊列是否小玉等于2500(内置的Background、Geometry和AlphaTest渲染隊列均在此範圍内),如果滿足條件,就把它渲染到深度和法線紋理中。是以若想要讓物體能夠出現在深度和法線紋理中,需要在Shader中設定正确的RenderType标簽。
在Unity中,可以選擇讓一個錄影機生成一個深度紋理或是一張深度+法線紋理。當選擇前者,隻需要一張單獨的深度紋理,Unity會直接擷取深度緩存或是着色器替換技術,選取需要的不透明物體,并使用它投射陰影時使用的Pass(即LightMode被設定為ShadowCaster的Pass)來得到深度紋理。而如果shader中不包含這一個Pass,那麼該物體将不會出現在深度紋理中,也不能向其他物體投射影。
如果選擇生成一張深度+法線紋理,Unity會建立一張和螢幕分辨率相同、精度為32位(每個通道位8位)的紋理,其中觀察空間下的法線資訊會被編碼進紋理的R和G通道,深度資訊被編碼進B和A通道。法線資訊的擷取在延遲渲染中可以非常容易得到,Unity隻需要合并深度和法線緩存即可。而在前向渲染中,預設情況下不會建立法線緩存,進而unity使用一個單獨的pass把整個場景再次渲染一遍來完成。
如何獲得:
通過腳本中設定錄影機的depthTextureMode來完成,然後再通過shader中直接通路特點的紋理屬性即可:
camera.depthTextureMode = DepthTextureMode.Depth;
設定好錄影機模式後,在shader中通過_CameraDepthTexture變量來通路。
而深度+法線紋理,隻需要:
camera.depthTextureMode = DepthTextureMode.DepthNormals
在shader中聲明_CameraDepthNormalsTexture變量來通路。
而通過以下組合可以讓錄影機同時産生一張深度和深度+法線紋理:
camera.depthTextureMode |= DepthTextureMode.Depth;
camera.depthTextureMode |= DepthTextureMode.DepthNormals;
在通路到深度紋理_CameraDepthTexture後,可以使用目前像素的紋理坐标對它進行采樣,而大多數情況下直接利用tex2D函數采樣即可,但某些*台如PS3和PSP2則需要特殊處理。進而為處理這些*台差異,可以在shader中利用宏函數:SAMPLE_DEPTH_TEXTURE來對深度紋理進行采樣:
float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture , i.uv);
i.uv時float2類型,對應目前像素的紋理坐标。
而SAMPLE_DEPTH_TEXTURE_PROJ同樣接受兩個參數——深度紋理和一個float3和float4類型的紋理坐标,其第二個參數是由頂點着色器輸出插值而得的螢幕坐标,如:
float d = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture , UNITY_PROJ_COORD(i.scrPos));
i.srcPos是在頂點着色器中通過調用ComputeScreenPos(o.pos)得到的螢幕坐标。
而Unity提供了計算過程,LinearEyeDepth負責把深度紋理的采樣結果轉換到視角空間下的深度值,Linear01Depth負責傳回一個[0,1]的線性深度值,即。而兩個函數内部使用了内置的_ZBufferParams變量來得到遠*裁剪螢幕的距離。
檢視深度和法線紋理:
通過幀調節器可以檢視生成的深度和法線紋理,以便對shader進行調試。
1.2.2 再談運動模糊
速度映射圖存儲了每個像素的速度,然後使用這個速度來決定模糊的方向和大小。速度緩沖的生成有多種,一種方法是把場景中所有物體的速度渲染到一張紋理中,但其缺點是需要修改場景中所有物體的shader代碼,使其添加計算速度的代碼并輸出到一個渲染紋理中。
生成速度映射圖的方法:利用深度紋理在片元着色器中為每個像素計算其在世界空間下的位置,這是通過使用目前的視角*投影矩陣的逆矩陣隊NDC下的頂點坐标進行變換得到的。當得到世界空間中的頂點坐标後,就使用前一幀的視角*投影矩陣隊其進行變換,得到該位置在前一幀中的NDC坐标。然後計算前一幀和目前幀的位置差,生成該像素的速度。該方法的優點是可以在一個螢幕後處理步驟中完成整個效果的模拟,缺點為需要在片元着色器中進行兩次矩陣乘法的操作,對性能有所影響。
[注]:需要錄影機移動才可以看到運動模糊,若物體移動是看不到效果的
[1]建立物體場景後,把腳本挂在錄影機,然後給腳本添加對應的shader
[2]給shader和腳本編碼
using UnityEngine;
using System.Collections;
public class MotionBlurWithDepthTexture : PostEffectsBase { //繼承父類
public Shader motionBlurShader; //聲明所需shader
private Material motionBlurMaterial = null; //建立所需材質,并初始化
public Material material {
get {
motionBlurMaterial = CheckShaderAndCreateMaterial(motionBlurShader, motionBlurMaterial); //将shader賦給建立的材質
return motionBlurMaterial;
}
}
private Camera myCamera; //建立對應的錄影機
public Camera camera {
get {
if (myCamera == null) {
myCamera = GetComponent<Camera>();//擷取該腳本所在的錄影機組建
}
return myCamera;
}
}
[Range(0.0f, 1.0f)]
public float blurSize = 0.5f; //建立模糊時模糊圖像使用的大小,并初始化
private Matrix4x4 previousViewProjectionMatrix; //定義一個變量來儲存上一幀錄影機的視角*投影矩陣
void OnEnable() {
camera.depthTextureMode |= DepthTextureMode.Depth; //擷取錄影機的深度紋理
previousViewProjectionMatrix = camera.projectionMatrix * camera.worldToCameraMatrix; //設定錄影機的狀态
}
void OnRenderImage (RenderTexture src, RenderTexture dest) { //實作OnRenderImage函數(數字圖像處理,處理圖檔最終效果的函數)
if (material != null) {
material.SetFloat("_BlurSize", blurSize); //将blurSize值賦給shader中的_BlurSize,讓其接收
material.SetMatrix("_PreviousViewProjectionMatrix", previousViewProjectionMatrix); //設定矩陣參數并賦給shader中對應的參數
Matrix4x4 currentViewProjectionMatrix = camera.projectionMatrix * camera.worldToCameraMatrix; //調用camera.projectionMatrix和camera.worldToCameraMatrix分别獲得目前錄影機的視角矩陣和投影矩陣
Matrix4x4 currentViewProjectionInverseMatrix = currentViewProjectionMatrix.inverse; //對其相乘後取逆得到目前幀的視角*投影矩陣的逆矩陣
material.SetMatrix("_CurrentViewProjectionInverseMatrix", currentViewProjectionInverseMatrix); //将該矩陣傳遞給shader中對應的變量
previousViewProjectionMatrix = currentViewProjectionMatrix; //将未取逆的結果存儲到previousViewProjectionMatrix以便與接下來調用到shader中
Graphics.Blit (src, dest, material); //若有material,則混合;若無則輸出原圖
} else {
Graphics.Blit(src, dest);
}
}
}
Shader "Unity Shaders Book/Chapter 13/Motion Blur With Depth Texture" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {} //聲明輸入的渲染紋理
_BlurSize ("Blur Size", Float) = 1.0 //聲明腳本中模糊圖像的控制量
//而因為Unity沒有提供矩陣的屬性,進而在腳本中定義了,然後把腳本中的參數傳遞進shader中
}
SubShader {
CGINCLUDE //進而避免重複編寫一樣的片元着色器
#include "UnityCG.cginc"
sampler2D _MainTex; //定義紋理
half4 _MainTex_TexelSize; //定義紋理的大小變換參數,使用該變量來對深度紋理的采樣坐标進行*台差異化處理
sampler2D _CameraDepthTexture; //定義錄影機的深度紋理
float4x4 _CurrentViewProjectionInverseMatrix; //定義錄影機投影*視角矩陣的逆矩陣
float4x4 _PreviousViewProjectionMatrix; //定義錄影機投影*視角矩陣,這兩個矩陣由腳本傳遞過來
half _BlurSize; //定義模糊圖像的控制量
struct v2f {
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
half2 uv_depth : TEXCOORD1; //對深度紋理采樣的紋理坐标變量
};
v2f vert(appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
o.uv_depth = v.texcoord;
//因為要同時處理多張渲染紋理
#if UNITY_UV_STARTS_AT_TOP //在Directx*台上需要處理圖像反轉問題(因為在OpenGL中遠點在左下角,而directx是在左上角)
if (_MainTex_TexelSize.y < 0) //如果紋理的y<0
o.uv_depth.y = 1 - o.uv_depth.y; //翻轉
#endif
return o;
}
fixed4 frag(v2f i) : SV_Target { //定義片元着色器
// Get the depth buffer value at this pixel.
float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth); //對深度紋理進行采樣
// H is the viewport position at this pixel in the range -1 to 1.
float4 H = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, d * 2 - 1, 1); //d是NDC坐标映射而來,若要建構像素的NDC坐标H,需要把這個深度值重新映射回NDC,使用原映射的反函數
// Transform by the view-projection inverse.
float4 D = mul(_CurrentViewProjectionInverseMatrix, H); //使用目前幀的視角*投影矩陣的逆矩陣對其進行變換
// Divide by w to get the world position.
float4 worldPos = D / D.w; //将結果除以w分量得到世界空間下的坐标表示worldPos
//得到世界坐标下的坐标
// Current viewport position
float4 currentPos = H;
// Use the world position, and transform by the previous view-projection matrix.
float4 previousPos = mul(_PreviousViewProjectionMatrix, worldPos); //使用前一幀的視角*投影矩陣對它進行變換,得到前一幀在NDC下的坐标previousPos
// Convert to nonhomogeneous points [-1,1] by dividing by w.
previousPos /= previousPos.w; //除以w得到前一幀世界空間下的坐标,表示previousPos
// Use this frame's position and last frame's to compute the pixel velocity.
float2 velocity = (currentPos.xy - previousPos.xy)/2.0f; //得到該像素的速度velocity
float2 uv = i.uv;
float4 c = tex2D(_MainTex, uv);
uv += velocity * _BlurSize; //使用該速度值對它的鄰域像素進行采樣,相加後取*均值
for (int it = 1; it < 3; it++, uv += velocity * _BlurSize) {
float4 currentColor = tex2D(_MainTex, uv);
c += currentColor;
}
c /= 3;
return fixed4(c.rgb, 1.0);
}
ENDCG
//定義模糊所需要的Pass
Pass {
ZTest Always Cull Off ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
ENDCG
}
}
FallBack Off
}
1.2.3 全局霧效
霧效(Fog)是遊戲裡經常使用的一種效果。Unity内置的霧效可以産生基于距離的線性或指數霧效。而想在編寫的頂點/片元着色器中實作霧效,開發者需要在shader中添加#pragma multi_compole_fog指令,同時還需要使用相關的内置宏,例如UNITY_FOG_COORDS、UNITY_TRANSFER_FOG和UNITY_APPLY_FOG等。該方法的缺點在于,開發者不僅需要為場景中所有物體添加相關的渲染代碼,而且能夠實作的效果也非常有限。當開發者需要對霧效進行一些個性化操作時,例如使用基于高度的霧效等,即您使用Unity内置的霧效變得不可行。
而基于螢幕後處理的全局霧效可以友善地模拟各種霧效,例如均勻的霧效、基于距離的線性/指數霧效、基于高度的霧效等。而其關鍵是,根據深度紋理來重建各個像素在世界空間下的位置。而在模拟運動模糊時已經實作了這個要求,即建構出目前像素的NDC坐标,再通過目前錄影機的視角*投影矩陣的逆矩陣來得到世界空間下的像素坐标,但是這樣實作需要在片元着色器中進行矩陣乘法的操作,這通常會影響遊戲的性能。
而有一個方法可以快速從深度紋理中重建世界坐标的方法,該方法首先對圖像空間下的視錐體射線(從錄影機出發,指向圖像上的某點的射線)進行插值,這條射線存儲了該像素在世界空間下到錄影機的方向資訊。然後把該射線和線性化後的視角空間下的深度值相乘,再加上錄影機的世界位置,就可以得到該像素在世界空間下的位置。在得到世界坐标後,可以通過各個公式來模拟全局霧效。
重建世界坐标:
該像素的世界坐标計算:
float4 worldPos = _WorldSpaceCameraPos + linearDepth * interpolatedRay;
(worldPos是該像素的世界坐标,_WorldSpaceCameraPos是錄影機的空間位置坐标,linearDepth是深度紋理得到的線性深度值,interpolatedRay是由頂點着色器輸出并插值後得到的射線,它不僅包含了該像素到錄影機的方向,也包含了距離資訊。)
TL = camera.forward * Near + toTop – toRight
TR = camera.forward * Near + toTop + toRight
BL = camera.forward * Near - toTop – toRight
BR = camera.forward * Near + toTop + toRight
而現所得的線性深度值并非是點到錄影機的歐式距離,而是在z方向上的距離,進而不能直接使用深度值和4個角的機關方向的乘積來計算它們到錄影機的偏移量。(即我們現在所得到的隻是z方向的距離,而不是眼睛實際上到該物體的距離(即所謂的深度值))。
而根據相似三角形可得到:
而因為TL與其他三個角互相對稱的,而3個向量的模和TL相等,進而可以設數值:
該幕後處理的原理是使用特定的材質去渲染一個剛好填充整個螢幕的四邊形面片,這個四邊形面片的4個頂點就對應了*裁剪*面的4個角。是以可以把上面的計算結果傳遞給頂點着色器,頂點着色器根據目前的位置選擇它所對應的向量,然後再将它輸出,經插值後傳遞給片元着色器得到interpolatedRay。
霧的計算:
在簡單的霧效實作中,需要計算一個霧效系數f,作為混合原始顔色和霧的顔色的混合系數:
float3 afterFog = f * fogColor + (1 - f ) * origColor;
這個霧效系數f有許多計算方法,在Unity内置的霧效實作中,支援三種霧的計算方式——線性(Linear)、指數(Exponential)、指數的*方(Exponential Squared)。當給定距離z後,f的計算公式分别如下:
Linear:
dmax為受霧影響的最大距離,dmin為受霧影響的最小距離
Exponential:
d是控制霧的濃度的參數
Exponential Squared:
[1]建立基本場景,給錄影機添加運動腳本和制造霧的腳本和shader
[2]添加對應的代碼,可以看到随着錄影機向前移動,本來看不清楚的物體的部分随着前進可以看得清
霧腳本代碼:
using UnityEngine;
using System.Collections;
public class FogWithDepthTexture : PostEffectsBase {
public Shader fogShader;
private Material fogMaterial = null;
public Material material {
get {
fogMaterial = CheckShaderAndCreateMaterial(fogShader, fogMaterial);
return fogMaterial;
}
}
private Camera myCamera; //存儲camera元件
public Camera camera {
get {
if (myCamera == null) {
myCamera = GetComponent<Camera>();
}
return myCamera;
}
}
private Transform myCameraTransform; //建立transform元件
public Transform cameraTransform {
get {
if (myCameraTransform == null) {
myCameraTransform = camera.transform;
}
return myCameraTransform;
}
}
[Range(0.0f, 3.0f)]
public float fogDensity = 1.0f; //控制霧的濃度
public Color fogColor = Color.white;
//fogstart 和 fogend相差越遠則越稀疏,若相差越小,則越厚,線條越硬朗
public float fogStart = 0.0f; //霧的起始濃度
public float fogEnd = 2.0f; //霧的終止高度
void OnEnable() {
camera.depthTextureMode |= DepthTextureMode.Depth; //深度紋理
}
void OnRenderImage (RenderTexture src, RenderTexture dest) {
if (material != null) {
Matrix4x4 frustumCorners = Matrix4x4.identity; //計算*裁剪*面的四個角對應的向量,并存儲到一個矩陣類型的變量frustumCorners
float fov = camera.fieldOfView; //camera的fov
float near = camera.nearClipPlane; //camera的最*的距離
float aspect = camera.aspect; //camera的縱橫比
//公式的計算過程
float halfHeight = near * Mathf.Tan(fov * 0.5f * Mathf.Deg2Rad);
Vector3 toRight = cameraTransform.right * halfHeight * aspect;
Vector3 toTop = cameraTransform.up * halfHeight;
Vector3 topLeft = cameraTransform.forward * near + toTop - toRight;
float scale = topLeft.magnitude / near;
topLeft.Normalize();
topLeft *= scale;
//計算各點的歐式距離
Vector3 topRight = cameraTransform.forward * near + toRight + toTop;
topRight.Normalize();
topRight *= scale;
Vector3 bottomLeft = cameraTransform.forward * near - toTop - toRight;
bottomLeft.Normalize();
bottomLeft *= scale;
Vector3 bottomRight = cameraTransform.forward * near + toRight - toTop;
bottomRight.Normalize();
bottomRight *= scale;
frustumCorners.SetRow(0, bottomLeft);
frustumCorners.SetRow(1, bottomRight);
frustumCorners.SetRow(2, topRight);
frustumCorners.SetRow(3, topLeft);
material.SetMatrix("_FrustumCornersRay", frustumCorners);
material.SetFloat("_FogDensity", fogDensity);
material.SetColor("_FogColor", fogColor);
material.SetFloat("_FogStart", fogStart);
material.SetFloat("_FogEnd", fogEnd);
Graphics.Blit (src, dest, material);
} else {
Graphics.Blit(src, dest);
}
}
}
Shader "Unity Shaders Book/Chapter 13/Fog With Depth Texture" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_FogDensity ("Fog Density", Float) = 1.0
_FogColor ("Fog Color", Color) = (1, 1, 1, 1)
_FogStart ("Fog Start", Float) = 0.0
_FogEnd ("Fog End", Float) = 1.0
}
SubShader {
CGINCLUDE
#include "UnityCG.cginc"
float4x4 _FrustumCornersRay;
sampler2D _MainTex;
half4 _MainTex_TexelSize;
sampler2D _CameraDepthTexture;
half _FogDensity;
fixed4 _FogColor;
float _FogStart;
float _FogEnd;
struct v2f {
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
half2 uv_depth : TEXCOORD1;
float4 interpolatedRay : TEXCOORD2;
};
v2f vert(appdata_img v) { //定義頂點着色器
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
o.uv_depth = v.texcoord;
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
o.uv_depth.y = 1 - o.uv_depth.y;
#endif
int index = 0;
if (v.texcoord.x < 0.5 && v.texcoord.y < 0.5) {
index = 0;
} else if (v.texcoord.x > 0.5 && v.texcoord.y < 0.5) {
index = 1;
} else if (v.texcoord.x > 0.5 && v.texcoord.y > 0.5) {
index = 2;
} else {
index = 3;
}
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
index = 3 - index;
#endif
o.interpolatedRay = _FrustumCornersRay[index]; //利用索引值來擷取_FrustumCornersRay
return o;
}
fixed4 frag(v2f i) : SV_Target { //片元着色器産生霧效
float linearDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth));
float3 worldPos = _WorldSpaceCameraPos + linearDepth * i.interpolatedRay.xyz;
float fogDensity = (_FogEnd - worldPos.y) / (_FogEnd - _FogStart);
fogDensity = saturate(fogDensity * _FogDensity);
fixed4 finalColor = tex2D(_MainTex, i.uv);
finalColor.rgb = lerp(finalColor.rgb, _FogColor.rgb, fogDensity);
return finalColor;
}
ENDCG
Pass {
ZTest Always Cull Off ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
ENDCG
}
}
FallBack Off
}
1.2.4 再談邊緣檢測
在上一節中所介紹的通過sobel算子對螢幕圖像進行邊緣檢測,進而實作描邊效果,但是很多時候直接利用顔色資訊進行邊緣檢測的方法會産生很多開發者所不希望得到的邊緣線(因為sobel算子的檢測原理是通過如果顔色相差太大則勾選出該線條,進而很多時候如果一個物體的貼圖如果有過多凹凸不*的效果也會被勾選出來)。
而本節所介紹的是如何在深度和法線紋理上進行邊緣檢測,這些圖像不會受紋理和光照的影響,而僅僅儲存了目前渲染物體的模型資訊(即其外輪廓)。
而本節所使用Roberts算子來進行邊緣檢測,其卷積核如下圖:
Roberts算子本質是通過計算左上角和右下角的內插補點,乘以右上角和左下角的內插補點作為評估邊緣的依據。而在下面實作中,通過該方式取對角方向的深度或法線值,比較它們之間的內插補點,如果超過某個門檻值(可由參數控制),則認為它們之間存在一條邊。
[1]建立場景,給錄影機挂靠腳本與對應的shader到腳本中
[2]檢視效果,不過鋸齒有點嚴重,可以考慮進行抗鋸齒操作
using UnityEngine;
using System.Collections;
public class EdgeDetectNormalsAndDepth : PostEffectsBase { //繼承父集
public Shader edgeDetectShader; //建立shader選項和其對應的material
private Material edgeDetectMaterial = null;
public Material material {
get {
edgeDetectMaterial = CheckShaderAndCreateMaterial(edgeDetectShader, edgeDetectMaterial);
return edgeDetectMaterial;
}
}
[Range(0.0f, 1.0f)]
public float edgesOnly = 0.0f; //背景顔色的參數
public Color edgeColor = Color.black; //描邊顔色
public Color backgroundColor = Color.white; //背景顔色
public float sampleDistance = 1.0f; //用于控制對深度+法線紋理采樣時,使用的采樣距離,即視覺看為描邊粗細程度
public float sensitivityDepth = 1.0f; //depth和normal則影響當鄰域的深度值或法線值相差多少時被認為存在一條邊街,而如果靈敏度跳的很大,那麼可能是深度或法線上很小的變化也會成為一條邊
public float sensitivityNormals = 1.0f;
void OnEnable() {
GetComponent<Camera>().depthTextureMode |= DepthTextureMode.DepthNormals; //因為需要錄影機的深度+法線紋理,是以需要調整對應的錄影機狀态
}
[ImageEffectOpaque]
void OnRenderImage (RenderTexture src, RenderTexture dest) {
if (material != null) {
material.SetFloat("_EdgeOnly", edgesOnly);
material.SetColor("_EdgeColor", edgeColor);
material.SetColor("_BackgroundColor", backgroundColor);
material.SetFloat("_SampleDistance", sampleDistance);
material.SetVector("_Sensitivity", new Vector4(sensitivityNormals, sensitivityDepth, 0.0f, 0.0f));
Graphics.Blit(src, dest, material);
} else {
Graphics.Blit(src, dest);
}
}
}
Shader "Unity Shaders Book/Chapter 13/Edge Detection Normals And Depth" {
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)
_SampleDistance ("Sample Distance", Float) = 1.0
_Sensitivity ("Sensitivity", Vector) = (1, 1, 1, 1)
}
SubShader {
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize; //聲明存儲紋理大小的變量
fixed _EdgeOnly;
fixed4 _EdgeColor;
fixed4 _BackgroundColor;
float _SampleDistance; //控制采樣距離
half4 _Sensitivity;
sampler2D _CameraDepthNormalsTexture;
struct v2f {
float4 pos : SV_POSITION;
half2 uv[5]: TEXCOORD0; //定義維數為5的紋理坐标數組,第一個坐标存儲了螢幕顔色圖像的采樣紋理(有時候因為*台的差異進而需要對它的豎直方向進行翻轉),剩下四個存儲使用Roberts算子時需要采樣的紋理坐标
};
v2f vert(appdata_img v) { //頂點着色器
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
o.uv[0] = uv;
#if UNITY_UV_STARTS_AT_TOP //解決*台差異性
if (_MainTex_TexelSize.y < 0)
uv.y = 1 - uv.y;
#endif
//計算羅伯特算子
o.uv[1] = uv + _MainTex_TexelSize.xy * half2(1,1) * _SampleDistance;
o.uv[2] = uv + _MainTex_TexelSize.xy * half2(-1,-1) * _SampleDistance;
o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1,1) * _SampleDistance;
o.uv[4] = uv + _MainTex_TexelSize.xy * half2(1,-1) * _SampleDistance;
return o;
}
half CheckSame(half4 center, half4 sample) { //定義checksame函數,其傳回值要不為0,要不為1;若為0則表明兩點之間存在一條邊界,若為1則不在一條邊界
//得到兩個點的法線和深度值
half2 centerNormal = center.xy;
float centerDepth = DecodeFloatRG(center.zw);
half2 sampleNormal = sample.xy;
float sampleDepth = DecodeFloatRG(sample.zw);
// difference in normals
// do not bother decoding normals - there's no need here
half2 diffNormal = abs(centerNormal - sampleNormal) * _Sensitivity.x; //兩個采樣點的對應值相減并取絕對值在乘以靈敏度參數
int isSameNormal = (diffNormal.x + diffNormal.y) < 0.1; //把差異值的每個分量相加再和門檻值相比,如果它們的和小于門檻值則傳回1,否則則傳回0
// difference in depth
float diffDepth = abs(centerDepth - sampleDepth) * _Sensitivity.y;
// scale the required threshold by the distance
int isSameDepth = diffDepth < 0.1 * centerDepth;
// return:
// 1 - if normals and depth are similar enough
// 0 - otherwise
return isSameNormal * isSameDepth ? 1.0 : 0.0; //檢查結果相乘,作為返還值
}
fixed4 fragRobertsCrossDepthAndNormal(v2f i) : SV_Target { //片元着色器
//使用4個紋理坐标對深度+法線紋理進行法線采樣
half4 sample1 = tex2D(_CameraDepthNormalsTexture, i.uv[1]);
half4 sample2 = tex2D(_CameraDepthNormalsTexture, i.uv[2]);
half4 sample3 = tex2D(_CameraDepthNormalsTexture, i.uv[3]);
half4 sample4 = tex2D(_CameraDepthNormalsTexture, i.uv[4]);
half edge = 1.0;
edge *= CheckSame(sample1, sample2); //調用checksame計算對角線上兩個紋理值的內插補點,若傳回0則兩點間存在一條邊界,反之則為1
edge *= CheckSame(sample3, sample4);
fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[0]), edge);
fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge);
return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);
}
ENDCG
//定義邊緣檢測所需要的pass
Pass {
ZTest Always Cull Off ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment fragRobertsCrossDepthAndNormal //該片元着色器需要與上述所需定義的片元着色器名字對應上
ENDCG
}
}
FallBack Off
}