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

邊緣顏色
如果純粹這樣鏤空,則效果太樸素了,因此通常要在鏤空邊緣上弄點(diǎn)顏色來模擬火化、融化等效果。
1. 純顏色
第一種實(shí)現(xiàn)很簡單,首先定義_EdgeLength和_EdgeColor兩個(gè)屬性來決定邊緣多長范圍要顯示邊緣顏色;然后在代碼中找到合適的范圍來顯示邊緣顏色。
主要代碼如下:
//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. 兩種顏色混合
第一種純顏色的效果并不太好,更好的效果是混合兩種顏色,來實(shí)現(xiàn)一種更加自然的過渡效果。
主要代碼如下:
if(cutout - _Threshold < _EdgeLength)
{
float degree = (cutout - _Threshold) / _EdgeLength;
return lerp(_EdgeFirstColor, _EdgeSecondColor, degree);
}

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

4. 使用漸變紋理
為了讓邊緣顏色更加豐富,我們可以進(jìn)而使用漸變紋理:

然后我們就可以利用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);

從特定點(diǎn)開始消融

為了從特定點(diǎn)開始消融,我們需要把片元到特定點(diǎn)的距離考慮進(jìn)clip中。
第一步需要先定義消融開始點(diǎn),然后求出各個(gè)片元到該點(diǎn)的距離(本例子是在模型空間中進(jìn)行):
//Properties
_StartPoint("Start Point", Vector) = (0, 0, 0, 0) //消融開始點(diǎn)
...
//Vert
//把點(diǎn)都轉(zhuǎn)到模型空間
o.objPos = v.vertex;
o.objStartPos = mul(unity_WorldToObject, _StartPoint);
...
//Fragment
float dist = length(i.objPos.xyz - i.objStartPos.xyz); //求出片元到開始點(diǎn)距離
第二步是求出網(wǎng)格內(nèi)兩點(diǎn)的最大距離,用來對第一步求出的距離進(jìn)行歸一化。這一步需要在C#腳本中進(jìn)行,思路就是遍歷任意兩點(diǎn),然后找出最大距離:
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;
}
}
同時(shí)Shader里面也要同時(shí)定義_MaxDistance來存放最大距離的值:
//Properties
_MaxDistance("Max Distance", Float) = 0
//Pass
float _MaxDistance;
第三步就是歸一化距離值
//Fragment
float normalizedDist = saturate(dist / _MaxDistance);
第四步要加入一個(gè)_DistanceEffect屬性來控制距離值對整個(gè)消融的影響程度:
//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);
上面已經(jīng)看到一個(gè)合適_DistanceEffect的效果了,下面貼出_DistanceEffect為1的效果圖:

這就完成了從特定點(diǎn)開始消融的效果了,不過有一點(diǎn)要注意,消融開始點(diǎn)最好是在網(wǎng)格上面,這樣效果會好點(diǎn)。
應(yīng)用:場景切換
利用這個(gè)從特定點(diǎn)消融的原理,我們可以實(shí)現(xiàn)場景切換。
假設(shè)我們要實(shí)現(xiàn)如下效果:

因?yàn)槲覀冊瓉淼腟hader是從中間開始鏤空的,和圖中從四周開始鏤空有點(diǎn)不同,因此我們需要稍微修改一下計(jì)算距離的方式:
//Fragment
float normalizedDist = 1 - saturate(dist / _MaxDistance);
這時(shí)候我們的Shader就能從四周開始消融了。
第二步就是需要修改計(jì)算距離的坐標(biāo)空間,原來我們是在模型空間下計(jì)算的,而現(xiàn)在很明顯多個(gè)不同的物體會同時(shí)受消融值的影響,因此我們改為世界空間下計(jì)算距離:
//Vert
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
//Fragment
float dist = length(i.worldPos.xyz - _StartPoint.xyz);
完整代碼點(diǎn)這里
為了讓Shader應(yīng)用到場景物體上好看點(diǎn),我加了點(diǎn)漫反射代碼。
第三步為了計(jì)算所有場景的物體的頂點(diǎn)到消融開始點(diǎn)的最大距離,我定義了下面這個(gè)腳本:
public class DissolveEnvironment : MonoBehaviour {
public Vector3 dissolveStartPoint;
[Range(0, 1)]
public float dissolveThreshold = 0;
[Range(0, 1)]
public float distanceEffect = 0.6f;
void Start () {
//計(jì)算所有子物體到消融開始點(diǎn)的最大距離
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);
}
}
//計(jì)算給定頂點(diǎn)集到消融開始點(diǎn)的最大距離
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;
}
}
這個(gè)腳本同時(shí)還提供了一些值來方便控制所有場景的物體。

像這樣把場景的物體放到Environment物體下面,然后把腳本掛到Environment,就能實(shí)現(xiàn)如下結(jié)果了:

從特定方向開始消融

理解了上面的從特定點(diǎn)開始消融,那么理解從特定方向開始消融就很簡單了。
下面實(shí)現(xiàn)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正方向還是負(fù)方向開始消融,然后求出各個(gè)片元在X分量上與邊界的距離:
//Properties
_Direction("Direction", Int) = 1 //1表示從X正方向開始,其他值則從負(fù)方向
_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正方向開始,其他值則從負(fù)方向
border = _MaxBorderX;
灰燼飛散效果

主要效果就是上面的從特定方向消融加上灰燼向特定方向飛散。
首先我們需要生成灰燼,我們可以延遲clip的時(shí)機(jī):
float edgeCutout = cutout - _Threshold;
clip(edgeCutout + _AshWidth); //延至灰燼寬度處才剔除掉
這樣可以在消融邊緣上面留下一大片的顏色,而我們需要的是細(xì)碎的灰燼,因此我們還需要用白噪聲圖對這片顏色再進(jìn)行一次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); //灰燼處用白噪聲來進(jìn)行碎片化
finalColor = _AshColor;
}
下一步就是讓灰燼能夠向特定方向飛散,實(shí)際上就是操作頂點(diǎn),讓頂點(diǎn)進(jìn)行偏移,因此這一步在頂點(diǎn)著色器中進(jìn)行:
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里的遮擋處理和溶解著色器(一)
完整代碼點(diǎn)這里 我這里的實(shí)現(xiàn)是簡化版。
項(xiàng)目代碼
項(xiàng)目代碼在Github上,點(diǎn)這里查看
參考
《Unity Shader 入門精要》
Tutorial - Burning Edges Dissolve Shader in Unity
A Burning Paper Shader
Unity案例介紹:Trifox里的遮擋處理和溶解著色器(一)
《Trifox》中的遮擋處理和溶解著色器技術(shù)(下)