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

DirectionAsh場景

基本原理與實現

主要使用噪聲透明度測試,從噪聲圖中讀取某個通道的值,而後使用該值進行透明度測試。
主要代碼以下:html

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

完整代碼點這裏git

Basic場景


邊緣顏色

若是純粹這樣鏤空,則效果太樸素了,所以一般要在鏤空邊緣上弄點顏色來模擬火化、融化等效果。github

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;

完整代碼點這裏3d

EdgeColor場景

2. 兩種顏色混合

第一種純顏色的效果並不太好,更好的效果是混合兩種顏色,來實現一種更加天然的過渡效果。
主要代碼以下:code

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

完整代碼點這裏orm

TwoEdgeColor場景

3. 邊緣顏色混合物體顏色

爲了讓過渡更加天然,咱們能夠進一步混合邊緣顏色和物體本來的顏色。
主要代碼以下:htm

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

完整代碼點這裏blog

BlendOriginColor場景

4. 使用漸變紋理

爲了讓邊緣顏色更加豐富,咱們能夠進而使用漸變紋理:

而後咱們就能夠利用degree來對這條漸變紋理採樣做爲咱們的邊緣顏色:ip

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

完整代碼點這裏

Ramp場景


從特定點開始消融

DissolveFromPoint場景
爲了從特定點開始消融,咱們須要把片元到特定點的距離考慮進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的效果圖:
_DistanceEffect = 1
這就完成了從特定點開始消融的效果了,不過有一點要注意,消融開始點最好是在網格上面,這樣效果會好點。

完整代碼點這裏

應用:場景切換

利用這個從特定點消融的原理,咱們能夠實現場景切換。
假設咱們要實現以下效果:
來自Trifox的圖
由於咱們原來的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,就能實現以下結果了:
DissolveEnvironment場景

具體的場景文件點這裏


從特定方向開始消融

DissolveFromDirectionX場景
理解了上面的從特定點開始消融,那麼理解從特定方向開始消融就很簡單了。
下面實現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;

完整代碼點這裏


灰燼飛散效果

DirectionAsh場景
主要效果就是上面的從特定方向消融加上灰燼向特定方向飛散。
首先咱們須要生成灰燼,咱們能夠延遲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的鏡頭遮擋消融

Trifox場景

具體原理參考 Unity案例介紹:Trifox裏的遮擋處理和溶解着色器(一)

完整代碼點這裏 我這裏的實現是簡化版。


項目代碼

項目代碼在Github上,點這裏查看


參考

《Unity Shader 入門精要》
Tutorial - Burning Edges Dissolve Shader in Unity
A Burning Paper Shader
Unity案例介紹:Trifox裏的遮擋處理和溶解着色器(一)
《Trifox》中的遮擋處理和溶解着色器技術(下)

相關文章
相關標籤/搜索