追記:本記事の改良版
[Unity] Shader Graph でノイズ関数を改造してタイリングに対応するカスタムノードを作る(改)
を投稿しました。コードの修正があるので、冒頭のカスタムノードの作成方法について以外はそちらを参照してください。
この記事で使用したUnityバージョンは2019.2.1f1です。
#はじめに
Shader Graph はUnityでコードを書かずにシェーダを記述できる素敵な機能です。これがたいへんよくできていて、テキストエディタが大好きな私でも「もうシェーダをテキストで書いてる場合じゃないな」と思わされます。
とはいえ既存のノードでやりたいことが何もかもカンタンにできるわけでもないようで、例えば標準のノイズ生成は繰り返したときの境界がリピートされていなかったりします。今回は、これを対応するためのカスタムノードを作成してみましょう。
#現状確認
普通に Simple Noise を使用したノードの接続がこうなります。
これをQuad4つ並べて貼り付けるとこう。
UVの 0.0 と 1.0 が連続でないため、境界が見えてしまっています。円筒に貼り付ける場合などはこれがたいへん不便なので、カスタムノードで対応してみましょう。
#カスタムノード作成
Shader Graph で右クリックして Create Node から
Inputsに Vector2 で UV, Vector1 で Scale を追加、Outputs に Vector1 で Out を追加します。
この時点ではシェーダを記述していないので、エラーが出ています。
SimpleNoise を改造するので、名前は TiledSimpleNoise としましょう。Name に TiledSimpleNoiseを入れ、
TiledSimpleNoise.hlsl
というテキストファイルをプロジェクトのどこかに作成します。
てっとり早く結果を得たい方は、下にある改良後の TiledSimleNoise.hlsl の内容をテキストファイルとして TiledSimpleNoise.hlsl に保存し、ノードでこれを指定して完了です。
#オリジナルの確認
SimpleNoise を参考にするため、既存ノードの SimpleNoise から Show Generated Code を選択すると、テキストエディタにソースコードが表示されます。
その記述の中で、自作の hlsl ファイルにおくべき部分は以下です。
inline float Unity_SimpleNoise_RandomValue_float (float2 uv)
{
return frac(sin(dot(uv, float2(12.9898, 78.233)))*43758.5453);
}
inline float Unity_SimpleNnoise_Interpolate_float (float a, float b, float t)
{
return (1.0-t)*a + (t*b);
}
inline float Unity_SimpleNoise_ValueNoise_float (float2 uv)
{
float2 i = floor(uv);
float2 f = frac(uv);
f = f * f * (3.0 - 2.0 * f);
uv = abs(frac(uv) - 0.5);
float2 c0 = i + float2(0.0, 0.0);
float2 c1 = i + float2(1.0, 0.0);
float2 c2 = i + float2(0.0, 1.0);
float2 c3 = i + float2(1.0, 1.0);
float r0 = Unity_SimpleNoise_RandomValue_float(c0);
float r1 = Unity_SimpleNoise_RandomValue_float(c1);
float r2 = Unity_SimpleNoise_RandomValue_float(c2);
float r3 = Unity_SimpleNoise_RandomValue_float(c3);
float bottomOfGrid = Unity_SimpleNnoise_Interpolate_float(r0, r1, f.x);
float topOfGrid = Unity_SimpleNnoise_Interpolate_float(r2, r3, f.x);
float t = Unity_SimpleNnoise_Interpolate_float(bottomOfGrid, topOfGrid, f.y);
return t;
}
void TiledSimpleNoise_float(float2 UV, float Scale, out float Out)
{
float t = 0.0;
float freq = pow(2.0, float(0));
float amp = pow(0.5, float(3-0));
t += Unity_SimpleNoise_ValueNoise_float(float2(UV.x*Scale/freq, UV.y*Scale/freq))*amp;
freq = pow(2.0, float(1));
amp = pow(0.5, float(3-1));
t += Unity_SimpleNoise_ValueNoise_float(float2(UV.x*Scale/freq, UV.y*Scale/freq))*amp;
freq = pow(2.0, float(2));
amp = pow(0.5, float(3-2));
t += Unity_SimpleNoise_ValueNoise_float(float2(UV.x*Scale/freq, UV.y*Scale/freq))*amp;
Out = t;
}
これを先ほどの TiledSimpleNoise.hlsl にコピペして、エラーがなくなってノイズが使えるようになったら改造準備完了です。
ファイルの最後に書いてある関数名は
TiledimpleNoise_float
と変更してあります。
#改造
コードを見つめてみます。
Unity_SimpleNoise_ValueNoise_float
この関数がキモで
float2 c0 = i + float2(0.0, 0.0);
float2 c1 = i + float2(1.0, 0.0);
float2 c2 = i + float2(0.0, 1.0);
float2 c3 = i + float2(1.0, 1.0);
この部分で隣接の値を生成しています。剰余を返す modulo という関数を
inline float2 modulo(float2 value, float2 scale)
{
return frac(value/scale)*scale;
}
と作っておいて、
lsl
float2 c0 = i + float2(0.0, 0.0);
float2 c1 = i + float2(1.0, 0.0);
float2 c2 = i + float2(0.0, 1.0);
float2 c3 = i + float2(1.0, 1.0);
c0 = modulo(c0, Period);
c1 = modulo(c1, Period);
c2 = modulo(c2, Period);
c3 = modulo(c3, Period);
のように剰余にすることでタイルを作ります。ここのPeriodは、TiledimpleNoise_float という関数で freq がScaleの最大4倍になるので、Scale/4 を与えることにします。Period をノードの引数で与えて調整する方法もあると思います。
結果、以下のようなコードになります。
inline float2 modulo(float2 value, float2 scale)
{
return frac(value/scale)*scale;
}
inline float Unity_SimpleNoise_RandomValue_float (float2 uv)
{
return frac(sin(dot(uv, float2(12.9898, 78.233)))*43758.5453);
}
inline float Unity_SimpleNnoise_Interpolate_float (float a, float b, float t)
{
return (1.0-t)*a + (t*b);
}
inline float Unity_SimpleNoise_ValueNoise_float (float2 uv, float Period)
{
float2 i = floor(uv);
float2 f = frac(uv);
f = f * f * (3.0 - 2.0 * f);
// uv = abs(frac(uv) - 0.5); この行はオリジナルに存在するが無意味なので削除
float2 c0 = i + float2(0.0, 0.0);
float2 c1 = i + float2(1.0, 0.0);
float2 c2 = i + float2(0.0, 1.0);
float2 c3 = i + float2(1.0, 1.0);
c0 = modulo(c0, Period);
c1 = modulo(c1, Period);
c2 = modulo(c2, Period);
c3 = modulo(c3, Period);
float r0 = Unity_SimpleNoise_RandomValue_float(c0);
float r1 = Unity_SimpleNoise_RandomValue_float(c1);
float r2 = Unity_SimpleNoise_RandomValue_float(c2);
float r3 = Unity_SimpleNoise_RandomValue_float(c3);
float bottomOfGrid = Unity_SimpleNnoise_Interpolate_float(r0, r1, f.x);
float topOfGrid = Unity_SimpleNnoise_Interpolate_float(r2, r3, f.x);
float t = Unity_SimpleNnoise_Interpolate_float(bottomOfGrid, topOfGrid, f.y);
return t;
}
void TiledSimpleNoise_float(float2 UV, float Scale, out float Out)
{
float t = 0.0;
float2 uv = UV*Scale;
float Period = Scale/4; // since the freq below can be 4 maximum.
float freq = pow(2.0, float(0));
float amp = pow(0.5, float(3-0));
t += Unity_SimpleNoise_ValueNoise_float(float2(uv.x/freq, uv.y/freq), Period)*amp;
freq = pow(2.0, float(1));
amp = pow(0.5, float(3-1));
t += Unity_SimpleNoise_ValueNoise_float(float2(uv.x/freq, uv.y/freq), Period)*amp;
freq = pow(2.0, float(2));
amp = pow(0.5, float(3-2));
t += Unity_SimpleNoise_ValueNoise_float(float2(uv.x/freq, uv.y/freq), Period)*amp;
Out = t;
}
#GradientNoise
同様にして GradientNoise も対応します。オリジナルではこうなってしまうので
改造します。同じ手順でカスタムノードを作り、オリジナルを改造した結果のソースコードはこうなります:
inline float2 modulo(float2 value, float2 scale)
{
return frac(value/scale)*scale;
}
float2 Unity_GradientNoise_Dir_float(float2 p, float Period)
{
p = modulo(p, Period);
// Permutation and hashing used in webgl-nosie goo.gl/pX7HtC
p = p % 289;
float x = (34 * p.x + 1) * p.x % 289 + p.y;
x = (34 * x + 1) * x % 289;
x = frac(x / 41) * 2 - 1;
return normalize(float2(x - floor(x + 0.5), abs(x) - 0.5));
}
void TiledGradientNoise_float(float2 UV, float Scale, out float Out)
{
float2 p = UV * Scale;
float Period = Scale;
float2 ip = floor(p);
float2 fp = frac(p);
float d00 = dot(Unity_GradientNoise_Dir_float(ip, Period), fp);
float d01 = dot(Unity_GradientNoise_Dir_float(ip + float2(0, 1), Period), fp - float2(0, 1));
float d10 = dot(Unity_GradientNoise_Dir_float(ip + float2(1, 0), Period), fp - float2(1, 0));
float d11 = dot(Unity_GradientNoise_Dir_float(ip + float2(1, 1), Period), fp - float2(1, 1));
fp = fp * fp * fp * (fp * (fp * 6 - 15) + 10);
Out = lerp(lerp(d00, d01, fp.y), lerp(d10, d11, fp.y), fp.x) + 0.5;
}
#まとめ
ShaderGraph はとても便利で、カスタムノードも作れるともっと便利ですね。また、繰り返しに対応したノイズは使い勝手が良いと思います。自由に使っていただければ。