Unity Shader - 消融效果原理與變體

基本原理與實(shí)現(xiàn)

主要使用噪聲透明度測試,從噪聲圖中讀取某個(gè)通道的值,然后使用該值進(jìn)行透明度測試。
主要代碼如下:

fixed cutout = tex2D(_NoiseTex, i.uvNoiseTex).r;
clip(cutout - _Threshold);

完整代碼點(diǎn)這里

Basic場景

邊緣顏色

如果純粹這樣鏤空,則效果太樸素了,因此通常要在鏤空邊緣上弄點(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;

完整代碼點(diǎn)這里

EdgeColor場景

2. 兩種顏色混合

第一種純顏色的效果并不太好,更好的效果是混合兩種顏色,來實(shí)現(xiàn)一種更加自然的過渡效果。
主要代碼如下:

if(cutout - _Threshold < _EdgeLength)
{
    float degree = (cutout - _Threshold) / _EdgeLength;
    return lerp(_EdgeFirstColor, _EdgeSecondColor, degree);
}

完整代碼點(diǎn)這里

TwoEdgeColor場景

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);

完整代碼點(diǎn)這里

BlendOriginColor場景

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)這里

Ramp場景

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

DissolveFromPoint場景

為了從特定點(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的效果圖:


_DistanceEffect = 1

這就完成了從特定點(diǎn)開始消融的效果了,不過有一點(diǎn)要注意,消融開始點(diǎn)最好是在網(wǎng)格上面,這樣效果會好點(diǎn)。

完整代碼點(diǎn)這里

應(yīng)用:場景切換

利用這個(gè)從特定點(diǎn)消融的原理,我們可以實(shí)現(xiàn)場景切換。
假設(shè)我們要實(shí)現(xiàn)如下效果:


來自Trifox的圖

因?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é)果了:


DissolveEnvironment場景

具體的場景文件點(diǎn)這里


從特定方向開始消融

DissolveFromDirectionX場景

理解了上面的從特定點(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;

完整代碼點(diǎn)這里


灰燼飛散效果

DirectionAsh場景

主要效果就是上面的從特定方向消融加上灰燼向特定方向飛散。
首先我們需要生成灰燼,我們可以延遲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;

完整代碼點(diǎn)這里


Trifox的鏡頭遮擋消融

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ù)(下)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容