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


DirectionAsh場景

基本原理與實現

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

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

完整代碼點這里

Basic場景


邊緣顏色

如果純粹這樣鏤空,則效果太朴素了,因此通常要在鏤空邊緣上弄點顏色來模擬火化、融化等效果。

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;

完整代碼點這里

EdgeColor場景

2. 兩種顏色混合

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

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

完整代碼點這里

TwoEdgeColor場景

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

完整代碼點這里

BlendOriginColor場景

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

完整代碼點這里

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》中的遮擋處理和溶解着色器技術(下)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM