
基本原理與實現
主要使用噪聲和透明度測試,從噪聲圖中讀取某個通道的值,然后使用該值進行透明度測試。 主要代碼如下:
fixed cutout = tex2D(_NoiseTex, i.uvNoiseTex).r;
clip(cutout - _Threshold);

邊緣顏色
如果純粹這樣鏤空,則效果太朴素了,因此通常要在鏤空邊緣上弄點顏色來模擬火化、融化等效果。
1. 純顏色
第一種實現很簡單,首先定義_EdgeLength和_EdgeColor兩個屬性來決定邊緣多長范圍要顯示邊緣顏色;然后在代碼中找到合適的范圍來顯示邊緣顏色。 主要代碼如下:
//Properties
_EdgeLength("Edge Length", Range(0.0, 0.2)) = 0.1
_EdgeColor("Border Color", Color) = (1,1,1,1)
...
//Fragment
if(cutout - _Threshold < _EdgeLength)
return _EdgeColor;

2. 兩種顏色混合
第一種純顏色的效果並不太好,更好的效果是混合兩種顏色,來實現一種更加自然的過渡效果。 主要代碼如下:
if(cutout - _Threshold < _EdgeLength)
{
float degree = (cutout - _Threshold) / _EdgeLength;
return lerp(_EdgeFirstColor, _EdgeSecondColor, degree);
}

3. 邊緣顏色混合物體顏色
為了讓過渡更加自然,我們可以進一步混合邊緣顏色和物體原本的顏色。 主要代碼如下:
float degree = saturate((cutout - _Threshold) / _EdgeLength); //需要保證在[0,1]以免后面插值時顏色過亮
fixed4 edgeColor = lerp(_EdgeFirstColor, _EdgeSecondColor, degree);
fixed4 col = tex2D(_MainTex, i.uvMainTex);
fixed4 finalColor = lerp(edgeColor, col, degree);
return fixed4(finalColor.rgb, 1);

4. 使用漸變紋理
為了讓邊緣顏色更加豐富,我們可以進而使用漸變紋理:
然后我們就可以利用degree來對這條漸變紋理采樣作為我們的邊緣顏色:
float degree = saturate((cutout - _Threshold) / _EdgeLength);
fixed4 edgeColor = tex2D(_RampTex, float2(degree, degree));
fixed4 col = tex2D(_MainTex, i.uvMainTex);
fixed4 finalColor = lerp(edgeColor, col, degree);
return fixed4(finalColor.rgb, 1);

從特定點開始消融
為了從特定點開始消融,我們需要把片元到特定點的距離考慮進clip中。 第一步需要先定義消融開始點,然后求出各個片元到該點的距離(本例子是在模型空間中進行):
//Properties
_StartPoint("Start Point", Vector) = (0, 0, 0, 0) //消融開始點
...
//Vert
//把點都轉到模型空間
o.objPos = v.vertex;
o.objStartPos = mul(unity_WorldToObject, _StartPoint);
...
//Fragment
float dist = length(i.objPos.xyz - i.objStartPos.xyz); //求出片元到開始點距離
第二步是求出網格內兩點的最大距離,用來對第一步求出的距離進行歸一化。這一步需要在C#腳本中進行,思路就是遍歷任意兩點,然后找出最大距離:
public class Dissolve : MonoBehaviour {
void Start () {
Material mat = GetComponent<MeshRenderer>().material;
mat.SetFloat("_MaxDistance", CalculateMaxDistance());
}
float CalculateMaxDistance()
{
float maxDistance = 0;
Vector3[] vertices = GetComponent<MeshFilter>().mesh.vertices;
for(int i = 0; i < vertices.Length; i++)
{
Vector3 v1 = vertices[i];
for(int k = 0; k < vertices.Length; k++)
{
if (i == k) continue;
Vector3 v2 = vertices[k];
float mag = (v1 - v2).magnitude;
if (maxDistance < mag) maxDistance = mag;
}
}
return maxDistance;
}
}
同時Shader里面也要同時定義_MaxDistance來存放最大距離的值:
//Properties
_MaxDistance("Max Distance", Float) = 0
//Pass
float _MaxDistance;
第三步就是歸一化距離值
//Fragment
float normalizedDist = saturate(dist / _MaxDistance);
第四步要加入一個_DistanceEffect屬性來控制距離值對整個消融的影響程度:
//Properties
_DistanceEffect("Distance Effect", Range(0.0, 1.0)) = 0.5
...
//Pass
float _DistanceEffect;
...
//Fragment
fixed cutout = tex2D(_NoiseTex, i.uvNoiseTex).r * (1 - _DistanceEffect) + normalizedDist * _DistanceEffect;
clip(cutout - _Threshold);
上面已經看到一個合適_DistanceEffect的效果了,下面貼出_DistanceEffect為1的效果圖:
這就完成了從特定點開始消融的效果了,不過有一點要注意,消融開始點最好是在網格上面,這樣效果會好點。
應用:場景切換
利用這個從特定點消融的原理,我們可以實現場景切換。 假設我們要實現如下效果:
因為我們原來的Shader是從中間開始鏤空的,和圖中從四周開始鏤空有點不同,因此我們需要稍微修改一下計算距離的方式:
//Fragment
float normalizedDist = 1 - saturate(dist / _MaxDistance);
這時候我們的Shader就能從四周開始消融了。 第二步就是需要修改計算距離的坐標空間,原來我們是在模型空間下計算的,而現在很明顯多個不同的物體會同時受消融值的影響,因此我們改為世界空間下計算距離:
//Vert
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
//Fragment
float dist = length(i.worldPos.xyz - _StartPoint.xyz);
完整代碼點這里 為了讓Shader應用到場景物體上好看點,我加了點漫反射代碼。
第三步為了計算所有場景的物體的頂點到消融開始點的最大距離,我定義了下面這個腳本:
public class DissolveEnvironment : MonoBehaviour {
public Vector3 dissolveStartPoint;
[Range(0, 1)]
public float dissolveThreshold = 0;
[Range(0, 1)]
public float distanceEffect = 0.6f;
void Start () {
//計算所有子物體到消融開始點的最大距離
MeshFilter[] meshFilters = GetComponentsInChildren<MeshFilter>();
float maxDistance = 0;
for(int i = 0; i < meshFilters.Length; i++)
{
float distance = CalculateMaxDistance(meshFilters[i].mesh.vertices);
if (distance > maxDistance)
maxDistance = distance;
}
//傳值到Shader
MeshRenderer[] meshRenderers = GetComponentsInChildren<MeshRenderer>();
for(int i = 0; i < meshRenderers.Length; i++)
{
meshRenderers[i].material.SetVector("_StartPoint", dissolveStartPoint);
meshRenderers[i].material.SetFloat("_MaxDistance", maxDistance);
}
}
void Update () {
//傳值到Shader,為了方便控制所有子物體Material的值
MeshRenderer[] meshRenderers = GetComponentsInChildren<MeshRenderer>();
for (int i = 0; i < meshRenderers.Length; i++)
{
meshRenderers[i].material.SetFloat("_Threshold", dissolveThreshold);
meshRenderers[i].material.SetFloat("_DistanceEffect", distanceEffect);
}
}
//計算給定頂點集到消融開始點的最大距離
float CalculateMaxDistance(Vector3[] vertices)
{
float maxDistance = 0;
for(int i = 0; i < vertices.Length; i++)
{
Vector3 vert = vertices[i];
float distance = (vert - dissolveStartPoint).magnitude;
if (distance > maxDistance)
maxDistance = distance;
}
return maxDistance;
}
}
這個腳本同時還提供了一些值來方便控制所有場景的物體。
像這樣把場景的物體放到Environment物體下面,然后把腳本掛到Environment,就能實現如下結果了: 
從特定方向開始消融
理解了上面的從特定點開始消融,那么理解從特定方向開始消融就很簡單了。 下面實現X方向消融的效果。 第一步求出X方向的邊界,然后傳給Shader:
using UnityEngine;
using System.Collections;
public class DissolveDirection : MonoBehaviour {
void Start () {
Material mat = GetComponent<Renderer>().material;
float minX, maxX;
CalculateMinMaxX(out minX, out maxX);
mat.SetFloat("_MinBorderX", minX);
mat.SetFloat("_MaxBorderX", maxX);
}
void CalculateMinMaxX(out float minX, out float maxX)
{
Vector3[] vertices = GetComponent<MeshFilter>().mesh.vertices;
minX = maxX = vertices[0].x;
for(int i = 1; i < vertices.Length; i++)
{
float x = vertices[i].x;
if (x < minX)
minX = x;
if (x > maxX)
maxX = x;
}
}
}
第二步定義是從X正方向還是負方向開始消融,然后求出各個片元在X分量上與邊界的距離:
//Properties
_Direction("Direction", Int) = 1 //1表示從X正方向開始,其他值則從負方向
_MinBorderX("Min Border X", Float) = -0.5 //從程序傳入
_MaxBorderX("Max Border X", Float) = 0.5 //從程序傳入
...
//Vert
o.objPosX = v.vertex.x;
...
//Fragment
float range = _MaxBorderX - _MinBorderX;
float border = _MinBorderX;
if(_Direction == 1) //1表示從X正方向開始,其他值則從負方向
border = _MaxBorderX;
灰燼飛散效果
主要效果就是上面的從特定方向消融加上灰燼向特定方向飛散。 首先我們需要生成灰燼,我們可以延遲clip的時機:
float edgeCutout = cutout - _Threshold;
clip(edgeCutout + _AshWidth); //延至灰燼寬度處才剔除掉
這樣可以在消融邊緣上面留下一大片的顏色,而我們需要的是細碎的灰燼,因此我們還需要用白噪聲圖對這片顏色再進行一次Dissolve:
float degree = saturate(edgeCutout / _EdgeWidth);
fixed4 edgeColor = tex2D(_RampTex, float2(degree, degree));
fixed4 finalColor = fixed4(lerp(edgeColor, albedo, degree).rgb, 1);
if(degree < 0.001) //粗略表明這是灰燼部分
{
clip(whiteNoise * _AshDensity + normalizedDist * _DistanceEffect - _Threshold); //灰燼處用白噪聲來進行碎片化
finalColor = _AshColor;
}
下一步就是讓灰燼能夠向特定方向飛散,實際上就是操作頂點,讓頂點進行偏移,因此這一步在頂點着色器中進行:
float cutout = GetNormalizedDist(o.worldPos.y);
float3 localFlyDirection = normalize(mul(unity_WorldToObject, _FlyDirection.xyz));
float flyDegree = (_Threshold - cutout)/_EdgeWidth;
float val = max(0, flyDegree * _FlyIntensity);
v.vertex.xyz += localFlyDirection * val;
Trifox的鏡頭遮擋消融

具體原理參考 Unity案例介紹:Trifox里的遮擋處理和溶解着色器(一)
完整代碼點這里 我這里的實現是簡化版。
項目代碼
項目代碼在Github上,點這里查看
參考
《Unity Shader 入門精要》 Tutorial - Burning Edges Dissolve Shader in Unity A Burning Paper Shader Unity案例介紹:Trifox里的遮擋處理和溶解着色器(一) 《Trifox》中的遮擋處理和溶解着色器技術(下)
