【有趣的技術(shù)】Unity中的SDF(有向距離場(chǎng))

前言

這幾天摸夠了,隨便寫點(diǎn)。這個(gè)東西是幾個(gè)月前研究的,雖然項(xiàng)目最后應(yīng)該用不上,但是挺有意思的,拿出來(lái)寫一下。

SDF全稱Signed Distance Field,中文一般翻譯為有向距離場(chǎng)。聽(tīng)起來(lái)很高端,其實(shí)原理理解了的話還好,下面我會(huì)以我的理解盡可能清晰的解釋一下這個(gè)東西是怎么回事。

如果貼圖不表示顏色而是距離?

開(kāi)局一張圖,先渲染兩個(gè)字看看:

兩個(gè)字都使用了各自下方的貼圖,貼圖分辨率都為32x32,右邊是常規(guī)的貼圖著色方式,而左邊則使用SDF的方式進(jìn)行著色。

  • 可以看到右邊常規(guī)的貼圖方式,因?yàn)樵挤直媛侍。糯箐秩竞螽a(chǎn)生了明顯的模糊失真。
  • 而左邊的SDF用了一張看起來(lái)“模糊”的貼圖,卻渲染出了銳利清晰的字樣。

原因就在于SDF采用的貼圖,其記錄的信息并不是顏色,而是距離。像素上的每個(gè)點(diǎn)都記錄了這個(gè)點(diǎn)到字樣邊緣的距離,存儲(chǔ)在貼圖的Alpha通道中。
著色器對(duì)貼圖進(jìn)行采樣并放大時(shí),會(huì)進(jìn)行插值。對(duì)于一般貼圖,即是對(duì)顏色進(jìn)行插值,信息的缺失就會(huì)造成紋理的模糊失真。而對(duì)距離插值則不一樣,即使貼圖只提供了有限的信息,但我們可以保證插值后的結(jié)果依然正確。比如在0和1之間取中間值,得出0.5,以距離而言其結(jié)果是完全正確的!

個(gè)人認(rèn)為SDF可以看作一種矢量的渲染方式。它也有局限性,只能針對(duì)圖案紋樣一類的圖片進(jìn)行處理,即邊緣明晰的單色圖樣。

那么SDF能做什么呢

字體渲染

現(xiàn)在被整合到Unity中的TextMeshPro文字渲染插件也是基于SDF實(shí)現(xiàn)的。在字體渲染上使用SDF可以很方便的實(shí)現(xiàn)描邊,外發(fā)光等效果。(UGUI中Text的Outline是使用“偏移”實(shí)現(xiàn)的,嚴(yán)格意義上根本就不算描邊,寬度一大就會(huì)穿幫)

不過(guò)這個(gè)方案用于中文項(xiàng)目時(shí)還是有一些問(wèn)題。因?yàn)檫@個(gè)方案需要事先對(duì)字符生成SDF圖,如果只是英文還好,字母加字符也就幾十的數(shù)量,但是中文字符就多了去了。一般做法是只對(duì)常用字進(jìn)行生成,大約6500字,壞處就是做文案時(shí)就沒(méi)法用到一些生僻字,而且對(duì)包體和內(nèi)存占用依然有影響。

形變動(dòng)畫(huà)

其實(shí)一開(kāi)始也是因?yàn)檫@個(gè)需求才接觸到SDF的。
如果對(duì)兩張普通貼圖進(jìn)行l(wèi)erp你能獲得一個(gè)交叉疊化的效果,而對(duì)于兩張SDF貼圖進(jìn)行l(wèi)erp,就可以獲得一個(gè)形變動(dòng)畫(huà)的效果了!

Ray-Marching

SDF也可以在3D維度中使用,配合Ray-Marching來(lái)渲染模型,還可以方便的實(shí)現(xiàn)軟陰影等特性。不過(guò)這塊我就沒(méi)繼續(xù)了解了,感興趣的可以自己搜一下,相關(guān)文章還是挺多的。

代碼

包含兩部分,首先是將普通貼圖轉(zhuǎn)化為SDF圖的代碼:

public static void GenerateSDF(Texture2D source, Texture2D destination, int serchDistance)
{
    int sourceWidth = source.width;
    int sourceHeight = source.height;
    int targetWidth = destination.width;
    int targetHeight = destination.height;

    pixels = new Pixel[sourceWidth, sourceHeight];
    targetPixels = new Pixel[targetWidth, targetHeight];
    Debug.Log("sourceWidth" + sourceWidth);
    Debug.Log("sourceHeight" + sourceHeight);
    int x, y;
    Color targetColor = Color.white;
    for (y = 0; y < sourceWidth; y++)
    {
        for (x = 0; x < sourceHeight; x++)
        {
            pixels[x, y] = new Pixel();
            if (source.GetPixel(x, y) == targetColor)
                pixels[x, y].isIn = true;
            else
                pixels[x, y].isIn = false;
        }
    }

    int gapX = sourceWidth / targetWidth;
    int gapY = sourceHeight / targetHeight;
    int MAX_SEARCH_DIST = serchDistance;
    int minx, maxx, miny, maxy;
    float max_distance = -MAX_SEARCH_DIST;
    float min_distance = MAX_SEARCH_DIST;

    for (x = 0; x < targetWidth; x++)
    {
        for (y = 0; y < targetHeight; y++)
        {
            targetPixels[x, y] = new Pixel();
            int sourceX = x * gapX;
            int sourceY = y * gapY;
            int min = MAX_SEARCH_DIST;
            minx = sourceX - MAX_SEARCH_DIST;
            if (minx < 0)
            {
                minx = 0;
            }
            miny = sourceY - MAX_SEARCH_DIST;
            if (miny < 0)
            {
                miny = 0;
            }
            maxx = sourceX + MAX_SEARCH_DIST;
            if (maxx > (int)sourceWidth)
            {
                maxx = sourceWidth;
            }
            maxy = sourceY + MAX_SEARCH_DIST;
            if (maxy > (int)sourceHeight)
            {
                maxy = sourceHeight;
            }
            int dx, dy, iy, ix, distance;
            bool sourceIsInside = pixels[sourceX, sourceY].isIn;
            if (sourceIsInside)
            {
                for (iy = miny; iy < maxy; iy++)
                {
                    dy = iy - sourceY;
                    dy *= dy;
                    for (ix = minx; ix < maxx; ix++)
                    {
                        bool targetIsInside = pixels[ix, iy].isIn;
                        if (targetIsInside)
                        {
                            continue;
                        }
                        dx = ix - sourceX;
                        distance = (int)Mathf.Sqrt(dx * dx + dy);
                        if (distance < min)
                        {
                            min = distance;
                        }
                    }
                }

                if (min > max_distance)
                {
                    max_distance = min;
                }
                targetPixels[x, y].distance = min;
            }
            else
            {
                for (iy = miny; iy < maxy; iy++)
                {
                    dy = iy - sourceY;
                    dy *= dy;
                    for (ix = minx; ix < maxx; ix++)
                    {
                        bool targetIsInside = pixels[ix, iy].isIn;
                        if (!targetIsInside)
                        {
                            continue;
                        }
                        dx = ix - sourceX;
                        distance = (int)Mathf.Sqrt(dx * dx + dy);
                        if (distance < min)
                        {
                            min = distance;
                        }
                    }
                }

                if (-min < min_distance)
                {
                    min_distance = -min;
                }
                targetPixels[x, y].distance = -min;
            }
        }
    }

    //EXPORT texture
    float clampDist = max_distance - min_distance;
    for (x = 0; x < targetWidth; x++)
    {
        for (y = 0; y < targetHeight; y++)
        {
            targetPixels[x, y].distance -= min_distance;
            float value = targetPixels[x, y].distance / clampDist;
            destination.SetPixel(x, y, new Color(1, 1, 1, value));
        }
    }
}

然后是渲染SDF貼圖的著色器代碼:

Shader "Custom/SDF_Base"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "black" {}
        _DistanceMark ("Distance Mark", Range(0,1)) = 0.5
        _SmoothDelta ("Smooth Delta", Range(0,0.02)) = 0.5
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _SmoothDelta;
            float _DistanceMark;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col;
                fixed4 sdf = tex2D(_MainTex, i.uv);
                float distance = sdf.a;
                col.a = smoothstep(_DistanceMark - _SmoothDelta, _DistanceMark + _SmoothDelta, distance); // do some anti-aliasing
                col.rgb = lerp(fixed3(0,0,0), fixed3(1,1,1), col.a);
                return col;
            }
            ENDCG
        }
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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