NeumorphismなUIをUnity uGUI上につくる

DrribleやPinterestでよく見るデザイントレンドになっているNeumorphism。 とてもおしゃれに見えますが、みんなMaterial Designに飽きてきて、目新しさ目当てで流行ってるだけちゃうのという意見もちらほら目にします。私も懐疑派でした。

一方でこんな意見も。

note.com

なるほど。今後xRなアプリケーションも同時に開発するようになったときには、ライティングや立体感が重要も再び重要になってくるのかもと。 まだ答えは出ていませんが、、。

静的にfigmaPhotoshopのエフェクトで作った立体感ではなく、動的にユーザーの動作に合わせて自然に変化すると面白いんじゃないかと…。個人的な実験の意味も込めて以下のようなデモを作って見ました。

gif

UnityのシェーダーでNeumorphismを再現して。リアルタイムに影や凹凸を変化させられるようにしています。更に画像をかなり拡大してもエッジはきれいなまま。

Neumorphismを解析する

Unityで実装を説明する前にまずNeumorphismがどうやって作られているかFigmaで公開してくれているサンプルを分析してみました。こちらを参考にしました。

dribbble.com

一番シンプルそうなパネルをFigmaで見てみます。(CSSで描くのは簡単だ…。)

f:id:asus4:20200811220919p:plain
figma-ui

  • 光の方向は左上から右下に。
  • 全体にうっすらとグラデーション
  • 右下に暗い色のドロップシャドウ
  • 左上に明るい色のドロップシャドウ
  • 縁を細いベベルでグラデーション

ざっくりこんな感じでしょうか。これを参考にしながらUnity上でできる範囲で作っていきます。

SDFで拡大に強くきれいな線を描く

Unityで作るスマホゲームでUIをピクセルパーフェクトで作ってるゲームってあるんでしょか?@1x @2x @3xなテクスチャを用意してるゲーム。実際 iPhone SE(640x1136)からiPad Pro 12.9(2048x2732)までサポートするようなゲームで無理…ですよね。FullHDでデザインして拡縮している現場が多いように思います。Vector Graphicsモジュールもありますが、UI全部に使ってしまうと重いですね。

今回はTextMesh Proでも使われているSDF(signed distance field)を使ってみました。SDFはパスからの距離をテクスチャに書き込むことでテクスチャ解像度が小さくてもきれいなエッジを再現できます。私が仕事で開発に参加してるLyric SpeakerでもSDFを使ってきれいなフォントを描画しています。

さらに今回はSDFの改良版アルゴリズムのMSDFを使ってみました。詳しくはGitHubリポジトリと、こちらの論文PDFが詳しいです。

github.com

https://user-images.githubusercontent.com/18639794/86908136-5fdd4780-c116-11ea-96c5-4f58a42043a4.pnghttps://user-images.githubusercontent.com/18639794/86908146-6370ce80-c116-11ea-87ee-95bfb699665c.pnghttps://user-images.githubusercontent.com/18639794/86908155-65d32880-c116-11ea-9583-1b45f806bbd9.png msdfgenより引用

簡単に言うとSDFではグレースケールでエッジとの距離を書き込んでいましたが、MSDFではRGBチャンネルそれぞれに角度ごとに分けた距離を書き込むことで、小さい解像度でも角がきれいに出るように改良したようです。

反面、テクスチャ圧縮をかけてしまうとmsdfはきれいに表示されないので、無圧縮に設定する必要があります。容量削減の意味では、効果は少ないです。

実際にshaderのコードを簡略化するとこんな感じになります。

inline float msdf(sampler2D tex, float2 uv)
{
    // MSDFのテクスチャ取得
    half3 c = tex2D(tex, uv);
    // RGB平均値の取得
    return max(min(c.r, c.g), min(max(c.r, c.g), c.b)) - 0.5;
}

fixed4 frag(v2f IN) : SV_Target
{
   // パスからの距離をfloatで
   float sdf = msdf(_MainTex, IN.texcoord);

   // UIのTint colorを取得
   half4 color = IN.color;

   // パスの境界線より外側のアルファを0に
   float2 sdfUnit = _PixelRange / _MainTex_TexelSize.zw;
   float clipSdf = sdf * max(dot(sdfUnit, 0.5 / fwidth(IN.texcoord)), 1);
   color.a *= saturate(clipSdf + 0.5);
   
   return color;
}

肝の部分はRGB平均値を取得する部分と、クリッピングする部分だと思いますが、とてもシンプルで良いですね。

SDFでドロップシャドウを描く

Unity UI付属の公式Dropshadow エフェクトはただ同じ画像をコピーしてオフセットかけているだけですが、
SDFはきれいな線を描くだけではなく、パス境界との距離と、勾配がわかるので、ドロップシャドウなどのエフェクトにも使えます。TextMeshProのエフェクト設定動画がわかりやすいです。

www.youtube.com

Normalを取得するコードは以下のようになります。

// さっきと同じ平均値を返すだけ。
inline float msdf_median(float3 c)
{
    return max(min(c.r, c.g), min(max(c.r, c.g), c.b));
}
 
// ノーマル画像を返す
inline float3 msdf_normal(sampler2D tex, float4 texel, float2 uv)
{
    texel *= 2;
    float left = msdf_median(tex2D(tex, float2(uv.x - texel.x, uv.y)));
    float right = msdf_median(tex2D(tex, float2(uv.x + texel.x, uv.y)));
    float bottom = msdf_median(tex2D(tex, float2(uv.x, uv.y - texel.y)));
    float top = msdf_median(tex2D(tex, float2(uv.x, uv.y + texel.y)));
    return float3(left - right, bottom - top, 0);
}

以上のように隣接ピクセルからの勾配情報をNormalテクスチャのような感じで取得します。これをもとにブラーやベベルを書き込んでいます。

Unity uGUI上に組み込む

実際にUnity uGUI上にNeumorphismデザインを実装してみます。

ドロップシャドウをuGUIのエフェクトとして使う。

github.com

UIEffectで行われているUIMeshの拡張を参考にしています。

同じ画像をコピーしても作れるのですが、一個の画像を作るのにレイヤー3枚…。などは避けたかったため。

すべてのNeumorphismコンポーネントの中ですべての頂点を一つにまとめ、uv2の中に、オフセット上をを埋め込み。Vertex shaderでDirectional Lightの方向にオフセットをかけています。

f:id:asus4:20200811222851p:plain
fig uv2

こんな感じです。

改善点

SDFでは実際にはテクスチャサイズより小さな領域が描画されます。配置のときに手動で直してましたが、ライブラリ側でサイズの計算を吸収できるのがベストです。

まとめ

このNeumorphism UIはGitHub上で公開しています。

github.com

またNeumorphismにしなくてもMSDFを使ったUIは使い所がありそうです。

ほっといても実践で使われることはなさそうなので、先日公開した自分のアプリでこのシェーダーを使っています。

asus4.hatenablog.com

おまけで:実験的に加速度センサでライティングの向きが変わる機能を搭載してたりします。 mobile.twitter.com