UnityのShader Graphを使用してUIのマスク、プロシージャルな縞模様(Stripe)、グリッド(Grid)、あるいはSDF(符号付き距離関数)ベースの円や図形を描画する際、避けて通れないのが「境界線のジャギー(エイリアシング)」の問題です。
境界線をハッキリさせようと Step ノードを使うと、カメラが近づいた際に境界が階段状にカクカクして見え、チープな印象を与えてしまいます。逆に Smoothstep ノードを使って境界を少しぼかすと、ズーム時は滑らかに見えますが、カメラが遠ざかった際に境界線が必要以上にボケてしまい、模様そのものが消失したり、細かいグリッドがモアレ現象によってチラついたりします。
本記事では、この現象が発生するグラフィックスの根本原因を紐解き、DDX(水平偏微分)および DDY(垂直偏微分)ノードを用いて、カメラの距離や画面解像度に関わらず、境界線を「常に画面上でちょうど1ピクセル幅だけ滑らかにボカす」というテクニックを解説します。
1. 根本原因:画面ピクセルとUV空間の解像度ズレ
なぜ単純な Step や固定値の Smoothstep では綺麗に描画できないのでしょうか?その理由は、「テクスチャ座標(UV空間)の1単位」が画面上で占める「ピクセル数」が、カメラの距離や表示解像度によって常に変化するからです。
Step(0.5, UV.x) を使用した場合、値が 0.5 未満なら 0、0.5 以上なら 1 という極端な出力になります。画面のピクセル(画素)の境界線が 0.5 の位置と完全に一致することは稀であるため、ピクセルが中間の明るさを持てず、ジャギー(カクカク)が生じます。
では、Smoothstep(0.49, 0.51, UV.x) のように遷移幅を 0.02 に固定した場合はどうでしょうか?
- カメラが非常に近い(ズーム時): UV空間の
0.02という幅が、画面上では 50ピクセル分 に相当するようになります。結果として、境界線が非常に幅広くぼやけ、締まりのないグラデーションになってしまいます。 - カメラが非常に遠い(カメラが引いた時): UV空間の
0.02という幅が、画面上では 0.1ピクセル以下 に縮小されます。1ピクセルの中に遷移幅がすっぽり収まってしまうため、実質的にStepノードと同じになり、ジャギーやモアレによるチラつきが発生します。
綺麗でシャープな境界線を作るための理想は、「どんな距離でも、画面上で常に境界線がちょうど1〜2ピクセル分の幅だけで滑らかにブレンドされる状態」を作ることです。これを実現するには、シェーダー実行時に「現在のピクセルにおいて、対象の値(UV等)が隣のピクセルと比べてどれくらい変化しているか」を知る必要があります。
図:Stepノード(左:ジャギーあり)とDDX/DDY偏微分を用いたアンチエイリアス(右:滑らかな輪郭)の視覚的比較
2. 解決策:スクリーンスペース偏微分(ddx/ddy)と fwidth
GPUは描画の際、ピクセルシェーダー(Fragment Shader)を1ピクセルずつ完全に独立して並列処理しているわけではありません。内部的には 2x2 ピクセル(合計4ピクセル)の「クアッド(Quad)」と呼ばれる単位 でまとめて実行しています。これにより、隣り合うピクセル間で値の差分(傾き)を計算することが可能になっています。
この仕組みを利用するためのHLSL命令(およびShader Graphノード)が偏微分関数です:
DDX(dFdx): 現在のピクセルの値と、右隣のピクセルの値との差分(水平方向の傾き)を計算します。DDY(dFdy): 現在のピクセルの値と、下隣 of ピクセルの値との差分(垂直方向の傾き)を計算します。
これらの絶対値を加算した fwidth(val) = abs(ddx(val)) + abs(ddy(val)) は、「その値が、画面上の1ピクセルあたりに変化する量」を意味します。
たとえば、fwidth(UV.x) の値が 0.005 であれば、隣のピクセルに移動するとUV座標のU値が 0.005 変化する、つまり「画面の1ピクセルは、UV空間の 0.005 に等しい」ということがリアルタイムに求まるのです!
3. Shader Graphでの実装フロー
この fwidth(画面1ピクセルあたりの変化量)を利用して、どのような距離でも常に「1ピクセル幅」で滑らかにブレンドするアンチエイリアス仕様の Step を構築します。
ステップ1:遷移幅(境界線の幅)の動的算出
閾値を Threshold とし、境界線のボカし幅を画面上で 1ピクセル としたい場合、値の遷移範囲(Edge1からEdge2)は以下のように定義されます。
Edge1 = Threshold - (fwidth(Value) * 0.5)
Edge2 = Threshold + (fwidth(Value) * 0.5)
これで、Threshold を中心とした「画面上でのちょうど1ピクセル」に対応するUV空間上の幅が算出されます。もし少しボカしを柔らかく(2ピクセル幅に)したい場合は、fwidth(Value) * 1.0 のように乗算する係数を増やします。
ステップ2:Smoothstep による補間
算出した Edge1 と Edge2 を Smoothstep に入力します。これにより、値が Threshold の前後1ピクセルの範囲で 0 から 1 へと滑らかにブレンドされ、ジャギーが完全に消えます。
💡 Shader Graphノードの接続手順:
- 境界判定に使いたい入力値(例:
UV.xや、計算によって生成したグラデーション値)を用意します。 - その入力値を DDX ノードと DDY ノードに接続します。
- DDXの絶対値(Absolute)とDDYの絶対値(Absolute)を Add ノードで足し合わせることで、
fwidthを自作します。 - 足し合わせた値に Multiply ノードで
0.5を掛けます。これが 0.5ピクセル分の変化量(半幅)になります。 - 目標の境界値(例:
0.5)からこの半幅を引き算(Subtract)してEdge 1とし、足し算(Add)してEdge 2とします。 - Smoothstep ノードを作成し、
Edge 1、Edge 2、および元の入力値をそれぞれ接続します。
4. Custom Functionノード(HLSL)による一括実装
Shader Graph上でノードが多くなり整理しにくくなる場合は、Custom Functionノードを用いてHLSLコードで1行で記述するのが非常に簡潔でおすすめです。
Custom Functionノードを作成し、タイプを String にして以下のコードを記述します。
// Inputs: Threshold (float), Value (float)
// Output: Out (float)
float width = fwidth(Value);
Out = smoothstep(Threshold - width * 0.5, Threshold + width * 0.5, Value);
このカスタムノードを1つ用意しておくだけで、元の Step ノードをこのアンチエイリアス版ノード(例:AntialiasedStep)に置き換えるだけで、すべてのカクカクした輪郭が美しくアンチエイリアス化されます。
5. 応用例:プロシージャルな縞模様(Stripe)とグリッド(Grid)
この手法は、単一のマスク切り抜きだけでなく、計算で生成するグリッドやストライプ(縞模様)で真価を発揮します。
通常、Fraction や Sine ノードで繰り返しの模様を作ると、遠くのピクセルでは1ピクセルの中に数十回もの縞模様が収まってしまい、描画のサンプリングが追いつかずに激しいチラつき(モアレ)を引き起こします。
アンチエイリアス版の閾値処理を入れることで、「1ピクセルあたりに模様の幅が小さくなりすぎた場合、自動的に中間のグレーに収束する(平均化される)」ため、モアレやチラつきが一切発生しなくなります。
6. 注意点と制限事項
- Fragmentステージ限定: 偏微分(ddx/ddy)は、隣接するピクセルの実行結果を比較する機能であるため、Vertexステージ(頂点シェーダー)では使用できません。 Vertexステージで
fwidthやddx系のノードを使用すると、コンパイルエラーになるか、常に 0 が返されます。必ずFragmentステージで処理を行ってください。 - モザイク(Pixel Art)表現との競合: 意図的にレトロなドット絵風のマテリアルや、カクカクしたモザイクエフェクトを作りたい場合は、このアンチエイリアス処理を施すと境界がぼやけてしまい、イメージが損なわれます。表現したいスタイルに応じて使い分ける必要があります。
7. まとめ:アンチエイリアス処理の比較
| 手法 | 解像度・距離による変化 | メリット・デメリット |
|---|---|---|
| Step ノード | 常に境界線がピクセル単位で切り落とされるため、どの距離でもジャギーが発生する。 | 🔴 ジャギーが最も目立つ。遠景でチラつき(モアレ)が発生しやすい。最軽量。 |
| 固定幅 Smoothstep | 近距離ではグラデーションが太くボケ、遠距離では遷移幅が潰れてStepと同様にジャギー化する。 | 🔺 距離が固定されたUIなどでは使えるが、3D空間上のオブジェクトでは破綻する。 |
| fwidth (ddx/ddy) 連携 | カメラが近づいても遠ざかっても、画面上で常に「境界が1〜2ピクセル」の滑らかさに自動調整される。 | 💚 常に最高のシャープさと滑らかさを維持。遠景のモアレも自動でグレーに馴染んで解消される。極めて軽量。 |
プロシージャルマテリアルやカスタムUIを作成する際は、デフォルトの Step ノードを使う代わりに、このスクリーンスペース偏微分を用いた fwidth アンチエイリアス手法を習慣化することで、ゲーム全体のルック&フィールをワンランク上のプレミアムな品質へ引き上げることができます。