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

前言

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

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
        }
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容