ボロノイ図(Voronoi)をUE4マテリアルでレンダリングしてみた。
UE4の勉強兼、ボロノイ図を実装して見ました。基本的なものですが、実装するために様々なページを探してみたりしましたのでいい経験でした。(かもしれない)
ボロノイ図とは
ボロノイ図はある平面で(または平面と呼ばれる地域など)いくつかの点が配置されるとしたら、位置のよってどの点に一番近いかを測って区切りしたダイアグラムを指します。各点を中心として一定の地域を「Voronoi Cell」と言います。そしてそのボロノイ図を生成する過程自体を「ボロノイ分割」と言います。
ボロノイ図を生成するために必要となる、ある点()と中心となる点()の距離を求める数式は大体に2つに別れます。一つは「ユークリッド距離」、もう一つは「マンハッタン距離」です。
- ユークリッド距離:
- マンハッタン距離:
実装
float resMin = min(iResolution.x, iResolution.y); vec2 uv = texCoord / resMin; vec2 pos = uv * 12.0; vec2 iN = floor(pos); vec2 iF = frac(pos);
- 最初にスクリーン上の位置を標準化して(])、ボロノイ図の各セルの個数を掛け算したものを定数と少数で分けます。
iN
とiF
変数はボロノイ図を実装するに必要となります。
- ボロノイ図を実装します。上の青色いノードは各セルの壁部分(枠線)の色となります。下の黒い色のノードは各セルの中身の色を表します。(テクスチャーなどで塗り替えっても構いません)
Custom
ノードはfloat
リターン値を返します。(MASK
ノードでRチャンネルだけ値を取ります)そしてsmoothstep
ノードで0か1かを定めます。smoothstep
の基準値の範囲が広くなればなるほど枠線が太ります。
Customオード(ボロノイ図)
Functions sF; return sF.Out(iN, iF);
- UE4のCustomノードは素のままじゃカスタム関数が作れないという謎仕様です。ですが、構造体を作って、その中に関数を作ることでカスタム関数が作れます。というわけで、
Functions
という構造体インスタンスを作って、そしてOut
という関数を呼び出します。 iN
、iF
が使われます。全部float2
です。(vec2
と同じ)
以下の関数は全部Functions
のメンバー関数となります。
float3 Out(in float2 n, in float2 f) { float md = 8.0; float2 mg, mr; FirstPhase(n, f, md, mg, mr); md = 8.0; SecondPhase(n, f, md, mg, mr); return float3(md, 0, 0); }
Out
関数は2つのフェイズを通してボロノイ図のWeight値を作り出します。- 最初の
md
は、FirstPhaseで各セルの中点から測定する距離の範囲を示します。こっちでは8.0
で多めに測るようにしました。0.25
みたいに小さめになる時には測定結果がおかしくなりますし、最終的には枠線がちゃんと引きません。 mg
は中点があるグリッドの位置オフセットを、mr
はその中点までの方向ベクトルを示します。
float2 PseudoRandom(in float2 c) { const float2 l = float2(127.1, 311.7); const float2 r = float2(266, 183); return frac(sin(float2(dot(c, l), dot(c, r))) * 43760); }
PseudoRandom
関数は、float2
型の類似乱数を返します。ボロノイ図のランダムな中点を取り出すのに必須です。(ノイズマップを使用しても構いませんね)以下の図はこの関数にiF
を入れてのリターン値をR、Gチャンネルに入力し、描画した結果です。- リターン地は小数点のみとなります。(重要?)
pseudoRandom
の小数点は、グリッド内で座標が]の場合の中点への方向・距離ベクトルを表しているようです。
void FirstPhase(in float2 n, in float2 f, inout float md, inout float2 mg, inout float2 mr) { for (int y = -1; y <= 1; ++y) for (int x = -1; x <= 1; ++x) { float2 g = float2(float(x), float(y)); float2 o = PseudoRandom(n + g); float2 r = o + g - f; float d = dot(r, r); if (d < md) { md = d; mr = r; mg = g; } } }
md
はボロノイ図の各セルの中点への距離を表します。二重for
ループを使用して、現在のグリッドから連接したセルの中点への距離、方向を探します。それで、一番距離d
が短い時のr
、g
、d
がリターンされるようになります。- 上の図で、
o
、g
、f
と距離関係でのベクトル相応がどうなっているかがわかります。ちょっと注意するところは、各グリッドで一番右下が中点になるわけではないということですね。 - 中点に近いピクセル座標は
md
が0に近接します。ですのでこれをsmoothstep
でフィルタリングして描画させてみると、以下の図みたいになります。
void SecondPhase(in float2 n, in float2 f, inout float md, in float2 mg, in float2 mr) { for (int y = -2; y <= 2; ++y) // 2じゃなくて1でもあり for (int x = -2; x <= 2; ++x) // 2じゃんくて1でもあり { float2 g = mg + float2(float(x), float(y)); float2 o = PseudoRandom(n + g); float2 r = g + o - f; float d = dot(r, r); if (dot(mr-r,mr-r) > 0.0) { md = min(md, dot(0.5*(mr+r), normalize(r-mr))); } } }
SecondPhase
では、枠線のWeight値となるmd
を0または0より小さい値に記録します。これで枠線が生成できます。- 数式
dot(0.5*(mr+r), normalize(r-mr))
はちょっと最適化することで0.5 / len * (length2(r) - length2(mr))
に置き換えることが出来ますね。len
はベクトルmr
、r
の距離となります。この数式によって、ボロノイ図の各セルの境界線部分は大体マイナス値を持ち、そしてそこから離れた部分は離れるほど大きな値を均一に持つことが出来ます。 - 最終の
md
値はピクセルのボロノイ図の枠線の負のWeight値となります。
Customノードの全体コード
struct Functions { float2 PseudoRandom(in float2 c) { const float2 l = float2(127.1, 311.7); const float2 r = float2(266, 183); return frac(sin(float2(dot(c, l), dot(c, r))) * 43760); } void FirstPhase(in float2 n, in float2 f, inout float md, inout float2 mg, inout float2 mr) { for (int y = -1; y <= 1; ++y) { for (int x = -1; x <= 1; ++x) { float2 g = float2(float(x), float(y)); float2 o = PseudoRandom(n + g); float2 r = g + o - f; float d = dot(r, r); if (d < md) { md = d; mr = r; mg = g; } } } } void SecondPhase(in float2 n, in float2 f, inout float md, in float2 mg, in float2 mr) { for (int y = -2; y <= 2; ++y) { for (int x = -2; x <= 2; ++x) { float2 g = mg + float2(float(x), float(y)); float2 o = PseudoRandom(n + g); float2 r = g + o - f; float d = dot(r, r); float len = distance(mr, r); if (dot(mr-r,mr-r) > 0.0) { md = min(md, 0.5 / len * (length2(r) - length2(mr))); } } } } float3 Out(in float2 n, in float2 f) { float md = 8.0; float2 mg, mr; FirstPhase(n, f, md, mg, mr); md = 8.0; SecondPhase(n, f, md, mg, mr); return float3(md, 0, 0); } }; Functions sF; return sF.Out(iN, iF);