Unity
后處理shader—以圖片作為基礎元素去渲染視野中的內容
概:本篇主要內容是如何在Unity中實作用圖片作為基礎元素去對相機最后拍到的內容做后處理渲染,(講也不是很明白,建議直接移步效果預覽那部分看看效果就明白了)
前期學習參照:這個效果的實作原理很大一部分參照于知乎羅老師的字符后處理渲染那篇文章,這里貼個鏈接Unity3D后期Shader特效-馬賽克13-文字影像(灰度轉ID|影像塊坐標偏移)
(比較草率的)最終效果預覽
- 首先關于這個最終效果有幾點需要強調下,第一,由于我只是臨時興起摸魚寫了這個東西,所以并沒有做更加精細的優化處理,導致利用差不多30張圖片就是極限了,如果將一張紋理的橫向縱向限度都利用起來,約莫可以利用近千張圖片,精細度直接冪次增長,第二,還是由于我比較懶且僅僅是摸魚寫的東西,所以我并沒有對用的圖片進行篩選,簡單抓起來自己收藏檔案夾里的“老婆”們,批量拖入就用了,導致很多圖片色調相近,色階并不是很豐富,第三,啊,這個不是我懶了,這個確實技術力有限,實作邏輯僅僅是用平均灰度來進行替換的,而并沒有涉及到具體的顏色辨識問題,比方說一個很亮的紅色,可能會用一個很亮的綠色去替代(因為灰度相近,直接按照最終效果相近就放一起了),
- 好,接下來是真正的最終效果預覽
- 首先是相機拍攝到的內容原圖

- 然后是精度較高的后處理后的效果(不知道為啥截圖放到csdn里效果有點小差別)

- 接著是精度下調的效果(為了能看明白這確實是用一張張圖片拼出來的dio)



啊,如上就是最終實作的一個效果,如果有興趣就繼續往下看,我會簡介如何實作這樣的東西,如果感覺很拉跨就跑路(如果有好的建議不妨給一手評論欸嘿嘿)
總體思路
- 首先是關于我為啥會想到做這么個玩意,早些天的時候在學習羅老師的那個后處理效果,感覺很好玩,實作起來也費了點勁,根本鬧不清這UV怎么算的(雖然鬧清后感覺也就那樣了),然后前倆天在寫(抄)作業的時候突然想到了(對,細節憑空想起來,我就是容易走神)以前看到過的別人發的那種用小圖片拼接成一個大圖片的圖,以前好奇過這種東西怎么實作的,當時根本沒學過圖形方面內容,覺得應該就是一堆圖亂拼起來,然后反手原圖蓋一層上去,就有這個效果了,然后現在想起來,草率了,人家可能還真是實打實的按照圖片本有的顏色拼起來的效果,于是腦洞大開決定在Unity中做一個這樣的后處理效果,實時處理每一幀的鏡頭(對GPU開銷還是比較大的玩玩就好),(想迫害自己做過的游戲專案了,如果用自己游戲的截圖去渲染自己的游戲,想想就覺得刺激又鬼畜

- 然后梳理下要實作這個東西需要做些什么,首先后處理需要用后處理shader來做,這個shader中我們需要拿到這些圖片如果一個一個去定義就無法實作足夠的動態性,所以我們需要在外界將這些要用到的紋理用一個腳本拼接成一張傳個這個shader,然后只需要告訴它我給你的這張紋理里包含了多少的子紋理,在shader中按照自己撰寫的邏輯去解開拼接使用就行了,
除了上面提到的這個c#腳本和shader以外,由于我是在之前專門做后處理學習的專案里做的,這個專案里我搭了個小架子,所以后面我會簡述下這個架子的內容,
綜上所述,我們要實作這個效果主要需要倆個核心東西:根據紋理去做最終后處理的shader,將要用的紋理打包成一張的c#腳本,
架子簡介
- 這個架子大概從上到下依次是,相機腳本,對應不同渲染風格的c#腳本,c#腳本所呼叫的一系列shader,
- 相機腳本:主要負責在屬性檢查器中公開當前選擇的濾鏡型別,所以我給其定義了一個列舉型別來作為濾鏡型別,并且相機腳本中得有各種渲染風格的具體物件,以便在屬性檢查器中更換了渲染模式后能及時的切換其使用的渲染腳本,(代碼如下)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
public class Pixlate : MonoBehaviour
{
public enum EDirType
{
彩鉛風格,
像素風格,
字符風格,
圖片填充
}
RenderTexture re1;
RenderTexture re2;
public EDirType 濾鏡型別 = EDirType.彩鉛風格;
public Filter 濾鏡;
static private List<Filter> col;
public Material[] effectMaterial;
private void Start()
{
col = new List<Filter>();
col.Add(new Color_Pencil_Filter());
col.Add(new PixelFilter());
col.Add(new Char_Filter());
col.Add(new Picture_Filter());
}
private void Update()
{
switch(濾鏡型別)
{
case EDirType.彩鉛風格:
if (濾鏡 == col[0]) break;
濾鏡 = col[0];
break;
case EDirType.像素風格:
if (濾鏡 == col[1]) break;
濾鏡 = col[1];
break;
case EDirType.字符風格:
if (濾鏡 == col[2]) break;
濾鏡 = col[2];
break;
case EDirType.圖片填充:
if (濾鏡 == col[3]) break;
濾鏡 = col[3];
break;
default:
break;
}
}
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if(濾鏡 == null)
{
return;
}
effectMaterial = 濾鏡.material;
if(re1 == null)
{
re1 = new RenderTexture(source);
}
if (re2 == null)
{
re2 = new RenderTexture(source);
}
Graphics.Blit(source, re1);
foreach (Material m in effectMaterial)
{
Graphics.Blit(re1, re2, m);
Graphics.Blit(re2, re1);
}
//濾鏡.Random_Parameter();
Graphics.Blit(re1, destination);
}
}
- 不同渲染風格的c#腳本:這些腳本需要負責將自身對應的渲染shader載入并實體化成材質陣列,傳給相機腳本,讓其做后處理渲染,所以我為它們定義了一個抽象父類,規定了這一類腳本都需要有的一些元素:當前濾鏡的名字,shader對應的路徑,載入好的shader物件,實體化好的材質陣列,初始化方法,材質所需引數隨機載入方法(這個方法與本篇所涉及的后處理效果沒很大關系),(父類代碼如下)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public abstract class Filter
{
public string[] shaderFile { set; get; } //shader路徑
public Shader[] shader { set; get; } //shader
public string filterName { set; get; } //濾鏡名字
public Material[] material { set; get; } //材質球
public void Init()
{
InitFile();
shader = new Shader[shaderFile.Length];
material = new Material[shaderFile.Length];
for (int i = 0;i<shaderFile.Length;i++)
{
shader[i] = Shader.Find(shaderFile[i]);
material[i] = new Material(shader[i]);
}
Random_Parameter();
}
public abstract void InitFile();
public abstract void Random_Parameter();
}
- c#腳本所呼叫的一系列shader:為什么說是呼叫的一系列shader,因為不一定一個效果只需要后處理一次,可能要經過多次不同效果的處理才能達到最終所需要的效果,所以其對應的后處理shader應該是個陣列,而不是單個shader,
紋理拼接的c#腳本
- 首先需要取得我們要用的一堆紋理,我這邊用的 Resources.Load 這個方法去加載我放在Resources檔案夾下的紋理資源,(由于人懶沒做什么檔案讀取方面的自動檢測紋理方法,我直接暴力把名字改成一樣的格式,然后一手回圈全部讀入了)
- 紋理讀取到以后還不能直接去操作這些紋理,在unity中Texture2D物件是有保護機制的,需要解開這個保護機制才能對其進行操作,我用的是早些時候在百度上嫖的一個方法代碼(啊,忘記作者是誰了,貼不得鏈接,致歉),如下方法,將一個2d紋理傳入,會回傳一個沒有保護機制的2d紋理
private Texture2D duplicateTexture(Texture2D source)
{
//2d紋理解除保護
RenderTexture renderTex = RenderTexture.GetTemporary(
source.width,
source.height,
0,
RenderTextureFormat.Default,
RenderTextureReadWrite.Linear);
Graphics.Blit(source, renderTex);
RenderTexture previous = RenderTexture.active;
RenderTexture.active = renderTex;
Texture2D readableText = new Texture2D(source.width, source.height);
readableText.ReadPixels(new Rect(0, 0, renderTex.width, renderTex.height), 0, 0);
readableText.Apply();
RenderTexture.active = previous;
RenderTexture.ReleaseTemporary(renderTex);
return readableText;
}//2d紋理解除保護
- 既然解開紋理保護極致了,就可以上手去操作了,既然是要用這些圖片去作為基礎元素顯示一張圖的,那么就需要將這些圖片的某些特定的部分和最終要顯示的部分的顏色對應起來,這樣才能保證一樣的顏色用到一樣的圖片來顯示,這邊我選擇使用灰度,所以首先要計算紋理陣列中每一個紋理的灰度,并將其存盤到一個陣列中,與紋理陣列元素一一對應起來,然后反手一個冒泡排序,以灰度陣列為依據,對這倆個陣列進行排序,就可以保證這些紋理按斬訓度從低到高的順序排起來了,(灰度的計算公式是讓顏色的rgb元素分別與0.299,0.587,0.114相乘取和)(代碼如下)
for (int i = 0; i < TexList.Count; i++)
{
//計算灰度并對應存盤
double gray = 0;
for(int j = 0;j<TexList[i].width;j++)
{
for (int k = 0; k < TexList[i].height; k++)
{
gray += TexList[i].GetPixel(j, k).r * 0.299;
gray += TexList[i].GetPixel(j, k).g * 0.587;
gray += TexList[i].GetPixel(j, k).b * 0.114;
}
}
gray /= (TexList[i].width * TexList[i].height);
GrayList.Add(gray);
}//計算灰度并對應存盤
double d = 0;
Texture2D t;
for (int i = 0;i< TexList.Count;i++)
{
for(int j = 0;j< TexList.Count-i-1;j++)
{
if(GrayList[j]> GrayList[j+1])
{
d = GrayList[j];
GrayList[j] = GrayList[j + 1];
GrayList[j + 1] = d;
t = TexList[j];
TexList[j] = TexList[j + 1];
TexList[j + 1] = t;
}
}
}//按斬訓度排序,亮的在后
- 按斬訓度排好序就可以將這些紋理拼接成最終要用的大紋理了,我們要知道,將這些紋理拼接起來最后要在shader中使用的話,簡單粗暴的拼起來最終導致各種圖片邊長不同,會及大幅度增加最終使用的難度,所以在這里我決定取遍歷這個紋理陣列,取到最短的一條邊作為基準,將所有圖片按照這個最短邊縮放為一個正方形的紋理,然后再拼接到一起,這邊我實作了一個方法,是將其中一個紋理整合到最終紋理對應的位置,由于這個專案比較摸魚所以我只是簡單實作了橫向的拼接,沒有做縱向的拓展,導致最終做出來只是一長條紋理,能存盤的紋理數量很有限,拼接原理也可以參照下圖(畫了個草圖,有點辣雞)

方法實作代碼如下:
private void Texture_Clone(Texture2D texture, Texture2D tex,int minwidth,int num)
{
//引數1:最終克隆到的texture中
//引數2:要縮放并克隆的目標
//引數3:縮放的邊長(正方)
//引數4:第幾個克隆的物件,對應位置
float xb = tex.width / minwidth;
float yb = tex.height / minwidth;
for (int i = 0;i<minwidth;i++)
{
for(int j = 0;j<minwidth;j++)
{
texture.SetPixel(i+ num * minwidth, j, tex.GetPixel((int)( i * xb), (int)(j * yb)));
}
}
texture.Apply();
}//將指定的texture2d內容按照比例縮放克隆到texture指定位置
- 紋理也拼接好了,接下里就是將紋理以及相應引數傳入對應shader中,這邊我在“材質所需引數隨機載入方法”(之前介紹架子的時候提到過)這個方法中去傳入這些引數,這個方法默認在載入時會呼叫一次,所以就可以達到引數傳遞一次的效果了,
public override void Random_Parameter()
{
//隨機傳參方法
//傳入一個紋理
material[0].SetTexture("_PictureTex", texture);
//傳入一個字符密度引數
material[0].SetFloat("_TileSize", _TileSize);
//傳入圖片數量引數
material[0].SetFloat("_PictureCount", TexList.Count);
}
- 完整的c#代碼如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Picture_Filter : Filter
{
//渲染精細度,數字越小越精細
public float _TileSize = 2;
//Texture2D _tex = (Texture2D)Resources.Load("Lighthouse");
//要用的紋理陣列
private List<Texture2D> TexList = new List<Texture2D>();
//對應紋理陣列的灰度陣列
private List<double> GrayList = new List<double>();
//最終拼接好的紋理
private Texture2D texture;
//所有紋理中的紋理最短邊
private int minTex = 1000000;
public Picture_Filter()
{
filterName = "圖片填充";
Init();
}//構造方法
private Texture2D duplicateTexture(Texture2D source)
{
//2d紋理解除保護
RenderTexture renderTex = RenderTexture.GetTemporary(
source.width,
source.height,
0,
RenderTextureFormat.Default,
RenderTextureReadWrite.Linear);
Graphics.Blit(source, renderTex);
RenderTexture previous = RenderTexture.active;
RenderTexture.active = renderTex;
Texture2D readableText = new Texture2D(source.width, source.height);
readableText.ReadPixels(new Rect(0, 0, renderTex.width, renderTex.height), 0, 0);
readableText.Apply();
RenderTexture.active = previous;
RenderTexture.ReleaseTemporary(renderTex);
return readableText;
}//2d紋理解除保護
private void InitTexList()
{
for(int i = 1;i<=30;i++)
{
TexList.Add(Resources.Load<Texture2D>("Texture/Picture_Filter/123 (" + i+")"));
}
for (int i = 0; i < TexList.Count; i++)
{
//解開紋理保護
TexList[i] = duplicateTexture(TexList[i]);
}//解開紋理保護
for (int i = 0; i < TexList.Count; i++)
{
//計算灰度并對應存盤
double gray = 0;
for(int j = 0;j<TexList[i].width;j++)
{
for (int k = 0; k < TexList[i].height; k++)
{
gray += TexList[i].GetPixel(j, k).r * 0.299;
gray += TexList[i].GetPixel(j, k).g * 0.587;
gray += TexList[i].GetPixel(j, k).b * 0.114;
}
}
gray /= (TexList[i].width * TexList[i].height);
GrayList.Add(gray);
}//計算灰度并對應存盤
double d = 0;
Texture2D t;
for (int i = 0;i< TexList.Count;i++)
{
for(int j = 0;j< TexList.Count-i-1;j++)
{
if(GrayList[j]> GrayList[j+1])
{
d = GrayList[j];
GrayList[j] = GrayList[j + 1];
GrayList[j + 1] = d;
t = TexList[j];
TexList[j] = TexList[j + 1];
TexList[j + 1] = t;
}
}
}//按斬訓度排序,亮的在后
}//初始化紋理陣列
private void Texture_Clone(Texture2D texture, Texture2D tex,int minwidth,int num)
{
//引數1:最終克隆到的texture中
//引數2:要縮放并克隆的目標
//引數3:縮放的邊長(正方)
//引數4:第幾個克隆的物件,對應位置
float xb = tex.width / minwidth;
float yb = tex.height / minwidth;
for (int i = 0;i<minwidth;i++)
{
for(int j = 0;j<minwidth;j++)
{
texture.SetPixel(i+ num * minwidth, j, tex.GetPixel((int)( i * xb), (int)(j * yb)));
}
}
texture.Apply();
}//將指定的texture2d內容按照比例縮放克隆到texture指定位置
public override void InitFile()
{
InitTexList();//初始化紋理陣列
for(int i = 0;i<TexList.Count;i++)
{
if (minTex > TexList[i].height)
{
minTex = TexList[i].height;
}
if (minTex > TexList[i].width)
{
minTex = TexList[i].width;
}
}//取最短邊
texture = new Texture2D(minTex * TexList.Count, minTex);//實體化最終要用的紋理
for (int i = 0; i < TexList.Count; i++)
{
Texture_Clone(texture, TexList[i], minTex, i);
}//為最終要用的紋理填入引數
//為渲染器初始化
shaderFile = new string[1];
shaderFile[0] = "Custom/Myshader/Picture_Shader";
}//初始化方法
public override void Random_Parameter()
{
//隨機傳參方法
//傳入一個紋理
material[0].SetTexture("_PictureTex", texture);
//傳入一個字符密度引數
material[0].SetFloat("_TileSize", _TileSize);
//傳入圖片數量引數
material[0].SetFloat("_PictureCount", TexList.Count);
}
}
利用拼接的紋理做后處理渲染的shader
- 首先是這個shader需要的引數部分,一共需要四個引數,如下分別是主紋理(這個由相機腳本傳入當前拍攝到的畫面),我們所拼接出的渲染紋理,渲染精細程度(這個涉及到像素化部分像素塊的大小,越小自然越精細),拼接出的紋理中包含多少張紋理,
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_PictureTex ("PictureTex", 2D) = "white" {}
_TileSize("TileSize", Range(0,100)) = 1
_PictureCount("PictureCount",Range(0,100)) = 1
}
- 關于頂點函式部分,這部分基本不需要做什么操作,就不多說了
- 關于片元函式部分,這部分是我們的主力,需要在這部分中對當前需要渲染點的uv進行像素區塊劃分,然后在主紋理中取到其對應的顏色,在對其灰度進行計算,得出其應該使用第幾個圖片,然后到拼接出的渲染紋理中去取對應圖片中的對應點,比較難的部分是這個要去取最終顏色的uv怎么計算,關于這個東西強烈建議看看羅老師的文章(開頭我提到的那篇),圖解很詳細,我講不太明白,但我這篇用到的最終uv計算公式也不過是羅老師那篇魔改來的,原理一樣,
- 最終shader代碼如下
Shader "Custom/Myshader/Picture_Shader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_PictureTex ("PictureTex", 2D) = "white" {}
_TileSize("TileSize", Range(0,100)) = 1
_PictureCount("PictureCount",Range(0,100)) = 1
}
SubShader
{
Cull Off ZWrite Off ZTest Always
pass
{
CGPROGRAM
#pragma vertex _Vert
#pragma fragment Pixel
#include "UnityCG.cginc"
struct VertexInput
{
float4 Pos:POSITION;
float2 uv:TEXCOORD0;
};
struct VertexOutput
{
float4 Pos:SV_POSITION;
float2 uv:TEXCOORD0;
};
//引數定義
sampler2D _MainTex;
sampler2D _PictureTex;
float _TileSize;
int _PictureCount;
VertexOutput _Vert(VertexInput v)
{
VertexOutput r;
//將頂點轉換到剪裁空間
r.Pos = UnityObjectToClipPos(v.Pos);
r.uv = v.uv;
return r;
}
fixed4 Pixel(VertexOutput v):SV_Target
{
//用目標像素尺寸和螢屏默認尺寸計算出引數TileSum
float2 TileSum = _ScreenParams / _TileSize;
//用引數TileSum對uv進行區塊劃分
float2 uv_Mosaic = ceil(v.uv *TileSum)/ TileSum;
//對紋理進行取樣顯現
fixed4 col = tex2D(_MainTex, uv_Mosaic);
//計算當前點的灰度
fixed gray = saturate(dot(col.xyz,fixed3(0.299, 0.587, 0.114)));
int num = ceil(gray *_PictureCount); //當前點應該是用第幾個圖片對應的紋理
float2 uv_picture =(v.uv *TileSum-ceil(v.uv *TileSum));
uv_picture.x /= _PictureCount;
uv_picture.x -= (float)num/_PictureCount;
fixed4 r = tex2D(_PictureTex, uv_picture);
return r;
}
ENDCG
}
}
FallBack "Diffuse"
}
結語
- 有關技術實作部分就如上所示,這邊想提一嘴這個實作中存在的一些毛病,可以優化的地方,比方說我是直接一長條拼接的紋理,所以當圖片長度和數量一上去,立馬就會報錯,顯示我要拼接的紋理長度超限,最終我平均只能利用30張圖片,如果能把縱向的空間也利用起來,其實就是30*30共900張紋理拼接,精細度能提升不少,另外用灰度作為顏色系結的依照來說,總還是不準確,比如我展示的效果圖中黃色的善矣最終替代的是黑色的部分,而黃色部分的dio卻由一些粉色的圖來替代,效果真的是一言難盡,
- 最后感謝閱讀(第一次寫這么又臭又長的博客)
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/239127.html
標籤:其他
