サイト内を検索

SDFについて

はじめに

はじめまして、ココネのクライアントエンジニアのKです。
 
仕事でUnityを使用していますが、Unity UIを使用してテキストを描画したい場合、TextとTextMeshProの2つの方法があります。
 
両者の違いは何でしょうか。また、なぜTextMeshProが必要なのでしょうか?
 
TextMeshProの説明を調べてみると、従来のビットマップの代わりに、より優れたSigned Distance Field (SDF)を使用していることがわかります。
 
一体、SDFとは何でしょうか?
 

SDF

Signed Distance Function

その前にSigned Distance Function (SDF)から紹介します。SDFは、空間内の物体を記述する方法です。

  • Signed(正or負の値):正値 (内部) 、負値 (外部)
  • Distance(距離):物体表面までの距離

SDFを使用してさまざまな形状を記述することができます。いくつかの例を見てみましょう。

float sdCircle( vec2 p, float r )
{
    return length(p) - r;
}

長方形

float sdBox( in vec2 p, in vec2 b )
{
    vec2 d = abs(p)-b;
    return length(max(d,0.0)) + min(max(d.x,d.y),0.0);
}

この分野のトップであるInigo QuilezのWebサイトには、さらに多くの例があります。

Signed Distance Field

Signed Distance Fieldは、Signed Distance Functionのグリッドサンプリングとして表されます。

 

SDFフォント

従来のビットマップ フォントでは、テキストを大きくするとぼやけて形が崩れてしまいます。

Valveは2007年にSDFを使用する新しい方法(Improved Alpha-Tested Magnification for Vector Textures and Special Effects)を発表しました。
実際のピクセルの色をテクスチャに保存する代わりに、最初にSDFの情報を保存し、レンダリング時にテキストを再構築します。これは空間ベクトルに似ていますが、GPUによる計算が簡単かつ高速になります。

SDFテクスチャの作成

入力は高解像度のテクスチャです。

それからSDFテクスチャを作成します。テクスチャからSDFを作成する方法はたくさんありますが、最もよく使用される方法は8-points Signed Sequential Euclidean Distance Transform (8ssedt)です。

8ssedt

[0][0][0]
[0][1][1]
[1][1][1]
  • 初期化
  • 二つのグリッドを用意します。(内部:0、外部:∞)と(内部:∞、外部:0)
[∞][∞][∞]
[∞][0][0]
[0][0][0]
  • 距離の計算
[#1][#2][#3]
[#4][ x][#5]
[#6][#7][#8]

xの値は、現在の近隣の距離と近隣までの距離を加えた最小値です。

x = min(#1.distance, ..., #8.distance)
#?.distance = sqrt( (?dx + offset x)^2 + (?dy + offset y)^2 )

隣接するグリッドを順番に比較します。

   - - - >       < - - -       < - - -       - - - >
| [?][?][?]   | [ ][ ][ ]   ^ [ ][ ][ ]   ^ [ ][ ][ ]
| [?][x][ ]   | [ ][x][?]   | [ ][x][?]   | [?][x][ ]
v [ ][ ][ ]   v [ ][ ][ ]   | [?][?][?]   | [ ][ ][ ]
  • グリッドの結果の結合
distance = grid1.distance - grid2.distance;

詳細な実装については、ここで参照できます。
 
最後に距離を0から1までに正規化します。ここで0.5が表面です。

レンダリング

距離が 0.5 未満の場合は外側、それ以外の場合は内側、とすることで形状を簡単に描くことができます。

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

half4 frag (v2f i) : SV_Target
{
    // SDFテクスチャから距離を読み取ります
    half4 col = tex2D(_MainTex, i.uv);
    float distance = col.a;
    // 距離 < 0.5の場合(外部)、alphaを0に設定します
    if (distance < _DistanceThreshold)
        col.a = 0.0;
    else
        col.a = 1.0;

    col.rgb = _MainColor.rgb;
    return col;
}


比較

同じサイズ(310x106)のテクスチャ。(左:SDF、右:ビットマップ)
ビットマップと比較して、SDFは拡大しても形状を維持できています。

効果

アンチエイリアス

col.a = smoothstep(_DistanceThreshold - _SmoothDelta, _DistanceThreshold + _SmoothDelta, distance);

(左:オフ、右:オン)

アウトライン

アウトラインの閾値を追加し、そして最後に2つの値をブレンドします。

half4 outlineCol;
outlineCol.a = smoothstep(_OutlineDistanceThreshold - _SmoothDelta, _OutlineDistanceThreshold + _SmoothDelta, distance);
outlineCol.rgb = _OutlineColor.rgb;

col.a = smoothstep(_DistanceThreshold - _SmoothDelta, _DistanceThreshold + _SmoothDelta, distance);
col.rgb = _MainColor.rgb;
return lerp(outlineCol, col, col.a);

他にもさまざまな効果があります。Valveの論文またはTextMeshProのソースコードで確認できます。

 

最後に

今回はTextMeshProで使用するSDFを簡単に紹介しました。実際には、特に3Dでレイマーチングを使用してSDFと組み合わせるアプリケーションは数多くあります。
例えば、

興味があれば、ShaderToyでSDFについて詳しく学ぶことができます。シェーダを書いてすぐに表示できる良いサイトです。
 
私もシンプルな形を使ってC.A.T Clubのロゴを作成してみました。

(ShaderToyに貼り付けて表示することもできますので、ぜひお試しください)

// C.A.T Club Logo
vec2 rotate(vec2 uv, float th) {
    return mat2(cos(th), sin(th), -sin(th), cos(th)) * uv;
}

// 2D functions are from https://iquilezles.org/articles/distfunctions2d
float ndot(vec2 a, vec2 b ) { return a.x*b.x - a.y*b.y; }
float sdRhombus( in vec2 p, in vec2 b ) 
{
    p = abs(p);
    float h = clamp( ndot(b-2.0*p,b)/dot(b,b), -1.0, 1.0 );
    float d = length( p-0.5*b*vec2(1.0-h,1.0+h) );
    return d * sign( p.x*b.y + p.y*b.x - b.x*b.y );
}

float sdTriangleIsosceles( in vec2 p, in vec2 q )
{
    p.x = abs(p.x);
    vec2 a = p - q*clamp( dot(p,q)/dot(q,q), 0.0, 1.0 );
    vec2 b = p - q*vec2( clamp( p.x/q.x, 0.0, 1.0 ), 1.0 );
    float s = -sign( q.y );
    vec2 d = min( vec2( dot(a,a), s*(p.x*q.y-p.y*q.x) ),
                  vec2( dot(b,b), s*(p.y-q.y)  ));
    return -sqrt(d.x)*sign(d.y);
}

float sdRing( in vec2 p, in vec2 n, in float r, in float th )
{
    p.x = abs(p.x);
    
    p = mat2(n.x,n.y,-n.y,n.x)*p;

    return max( abs(length(p)-r)-th*0.5,
                length(vec2(p.x,max(0.0,abs(r-p.y)-th*0.5)))*sign(p.x) );
}

vec3 drawScene(vec2 uv) {
    vec3 col = vec3(1);
    float rhombus = sdRhombus(uv, vec2(0.55, 0.35));
    float t = 3.14159 * 0.72;
    float ring = sdRing(rotate(uv, -1.57), vec2(cos(t),sin(t)), 0.15, 0.1);
    float triangle1 = sdTriangleIsosceles(rotate(uv + vec2(0.18, -0.28), 1.57), vec2(0.13, -0.23));
    float triangle2 = sdTriangleIsosceles(rotate(uv + vec2(-0.18, -0.28), -1.57), vec2(0.13, -0.23));
  
    col = mix(vec3(0.19, 0.15, 0.11), col, step(0., rhombus));
    col = mix(vec3(0.99, 0.86, 0.39), col, step(0., ring));
    col = mix(vec3(0.19, 0.15, 0.11), col, step(0., triangle1));
    col = mix(vec3(0.19, 0.15, 0.11), col, step(0., triangle2));
  
    return col;
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 uv = fragCoord/iResolution.xy; // (0, 1)
    uv -= 0.5; // (-0.5,0.5)
    uv.x *= iResolution.x/iResolution.y; // fix aspect ratio

    vec3 col = drawScene(uv);
    fragColor = vec4(col,1.0);
}

ココネエンジニアリングでは一緒に働く仲間を募集中です。

ご興味のある方は、ぜひこちらのエンジニア採用サイトをご覧ください。

→ココネエンジニアリング株式会社エンジニアの求人一覧

Tag

Category

Tag