美文网首页Unity技术分享Unity编辑器开发分享
【有趣的技术】Unity中的SDF(有向距离场)

【有趣的技术】Unity中的SDF(有向距离场)

作者: 石膏 | 来源:发表于2020-02-01 23:36 被阅读0次

    前言

    这几天摸够了,随便写点。这个东西是几个月前研究的,虽然项目最后应该用不上,但是挺有意思的,拿出来写一下。

    SDF全称Signed Distance Field,中文一般翻译为有向距离场。听起来很高端,其实原理理解了的话还好,下面我会以我的理解尽可能清晰的解释一下这个东西是怎么回事。

    如果贴图不表示颜色而是距离?

    开局一张图,先渲染两个字看看:

    两个字都使用了各自下方的贴图,贴图分辨率都为32x32,右边是常规的贴图着色方式,而左边则使用SDF的方式进行着色。

    • 可以看到右边常规的贴图方式,因为原始分辨率太小,放大渲染后产生了明显的模糊失真。
    • 而左边的SDF用了一张看起来“模糊”的贴图,却渲染出了锐利清晰的字样。

    原因就在于SDF采用的贴图,其记录的信息并不是颜色,而是距离。像素上的每个点都记录了这个点到字样边缘的距离,存储在贴图的Alpha通道中。
    着色器对贴图进行采样并放大时,会进行插值。对于一般贴图,即是对颜色进行插值,信息的缺失就会造成纹理的模糊失真。而对距离插值则不一样,即使贴图只提供了有限的信息,但我们可以保证插值后的结果依然正确。比如在0和1之间取中间值,得出0.5,以距离而言其结果是完全正确的!

    个人认为SDF可以看作一种矢量的渲染方式。它也有局限性,只能针对图案纹样一类的图片进行处理,即边缘明晰的单色图样。

    那么SDF能做什么呢

    字体渲染

    现在被整合到Unity中的TextMeshPro文字渲染插件也是基于SDF实现的。在字体渲染上使用SDF可以很方便的实现描边,外发光等效果。(UGUI中Text的Outline是使用“偏移”实现的,严格意义上根本就不算描边,宽度一大就会穿帮)

    不过这个方案用于中文项目时还是有一些问题。因为这个方案需要事先对字符生成SDF图,如果只是英文还好,字母加字符也就几十的数量,但是中文字符就多了去了。一般做法是只对常用字进行生成,大约6500字,坏处就是做文案时就没法用到一些生僻字,而且对包体和内存占用依然有影响。

    形变动画

    其实一开始也是因为这个需求才接触到SDF的。
    如果对两张普通贴图进行lerp你能获得一个交叉叠化的效果,而对于两张SDF贴图进行lerp,就可以获得一个形变动画的效果了!

    Ray-Marching

    SDF也可以在3D维度中使用,配合Ray-Marching来渲染模型,还可以方便的实现软阴影等特性。不过这块我就没继续了解了,感兴趣的可以自己搜一下,相关文章还是挺多的。

    代码

    包含两部分,首先是将普通贴图转化为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
            }
        }
    }
    

    相关文章

      网友评论

        本文标题:【有趣的技术】Unity中的SDF(有向距离场)

        本文链接:https://www.haomeiwen.com/subject/ostfxhtx.html