知識0からのUnityShader勉強

知識0からのUnityShader勉強

UnityのShaderをメインとして、0から学んでいくブログです。

【UnityShader】ノイズシェーダー拡張 【2】#45

前回の成果

ノイズシェーダーを拡張して、ブロックノイズとバリューノイズを作った。

soramamenatan.hatenablog.com


今回やること

パーリンノイズについて勉強します。

sasanon.hatenablog.jp


cginc

#ifndef EXTENSION_NOISE_UTIL
#define EXTENSION_NOISE_UTIL

#include "UnityCG.cginc"


fixed2 TRANSFORM_NOISE_TEX(fixed2 uv, fixed4 tilingOffset, fixed4 sizeScroll) {
    uv = uv * tilingOffset.xy * sizeScroll.xy + tilingOffset.zw;
    uv += fixed2(sizeScroll.z * tilingOffset.x, -sizeScroll.w * tilingOffset.y) * _Time.y;
    return uv;
}

fixed rand(fixed2 uv, fixed2 size) {
    uv = frac(uv / size);
    return frac(sin(dot(frac(uv / size), fixed2(12.9898, 78.233))) * 43758.5453) * 0.99999;
}

fixed2 gradientVector(fixed2 uv, fixed2 size) {
    uv = frac(uv / size);
    uv = fixed2(dot(frac(uv / size), fixed2(127.1, 311.7)), dot(frac(uv / size), fixed2(269.5, 183.3)));
    return -1.0 + 2.0 * frac(sin(uv) * 43758.5453123);
}

fixed2 bilinear(fixed f0, fixed f1, fixed f2, fixed f3, fixed fx, fixed fy) {
    return lerp(lerp(f0, f1, fx), lerp(f2, f3, fx), fy);
}

fixed fade(fixed t) {
    return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
}

fixed blockNoise(fixed2 uv, fixed2 size) {
    return rand(floor(uv), size);
}

fixed valueNoiseOne(fixed2 uv, fixed2 size) {
    fixed2 p = floor(uv);
    fixed2 f = frac(uv);
    float f00 = rand(p + fixed2(0, 0), size);
    float f10 = rand(p + fixed2(1, 0), size);
    float f01 = rand(p + fixed2(0, 1), size);
    float f11 = rand(p + fixed2(1, 1), size);
    return bilinear( f00, f10, f01, f11, fade(f.x), fade(f.y) );
}

fixed valueNoise(fixed2 uv, fixed2 size) {
    fixed f = 0;
    f += valueNoiseOne(uv *  2, size) / 2;
    f += valueNoiseOne(uv *  4, size) / 4;
    f += valueNoiseOne(uv *  8, size) / 8;
    f += valueNoiseOne(uv * 16, size) / 16;
    f += valueNoiseOne(uv * 32, size) / 32;
    return f;
}

fixed perlinNoiseOne(fixed2 uv, fixed2 size) {
    fixed2 p = floor(uv);
    fixed2 f = frac(uv);

    fixed d00 = dot(gradientVector(p + fixed2(0, 0), size), f - fixed2(0, 0));
    fixed d10 = dot(gradientVector(p + fixed2(1, 0), size), f - fixed2(1, 0));
    fixed d01 = dot(gradientVector(p + fixed2(0, 1), size), f - fixed2(0, 1));
    fixed d11 = dot(gradientVector(p + fixed2(1, 1), size), f - fixed2(1, 1));
    return bilinear(d00, d10, d01, d11, fade(f.x), fade(f.y)) + 0.5f;
}

fixed perlinNoise(fixed2 uv, fixed2 size) {
    fixed f = 0;
    f += perlinNoiseOne(uv *  2, size) / 2;
    f += perlinNoiseOne(uv *  4, size) / 4;
    f += perlinNoiseOne(uv *  8, size) / 8;
    f += perlinNoiseOne(uv * 16, size) / 16;
    f += perlinNoiseOne(uv * 32, size) / 32;
    return f;
}

fixed3 normalNoise(fixed2 uv, fixed2 size) {
    fixed3 result = fixed3(perlinNoise(uv.xy, size),
                           perlinNoise(uv.xy + fixed2(1, 1), size),
                           1.0);
    return result;
}

#endif

cgincは前回のものと同じです。


パーリンノイズとは

パーリンノイズ(英: Perlin noise)とは、コンピュータグラフィックスのリアリティを増すために使われるテクスチャ作成技法。
擬似乱数的な見た目であるが、同時に細部のスケール感が一定である。
このため制御が容易であり、各種スケールのパーリンノイズを数式に入力することで多彩なテクスチャを表現できる。

パーリンノイズ - Wikipedia:より引用

とのことです。

パーリンノイズのレンダリング結果

f:id:soramamenatan:20200321144002p:plain

パーリンノイズ(Perlin noise) - 大人になってからの再学習:より引用

パーリンノイズを使用して作られた地形

f:id:soramamenatan:20200321144021p:plain

パーリンノイズを理解する | POSTD:より引用


パーリンノイズの算出方法

パーリンノイズがなんとなくどのようなものかは理解できたと思います。
次に、どのようにソースコードに落とし込むかを説明します。

パーリンノイズのソースコード
fixed perlinNoiseOne(fixed2 uv, fixed2 size) {
    fixed2 p = floor(uv);
    fixed2 f = frac(uv);

    fixed d00 = dot(gradientVector(p + fixed2(0, 0), size), f - fixed2(0, 0));
    fixed d10 = dot(gradientVector(p + fixed2(1, 0), size), f - fixed2(1, 0));
    fixed d01 = dot(gradientVector(p + fixed2(0, 1), size), f - fixed2(0, 1));
    fixed d11 = dot(gradientVector(p + fixed2(1, 1), size), f - fixed2(1, 1));
    return bilinear(d00, d10, d01, d11, fade(f.x), fade(f.y)) + 0.5f;
}


単位座標

下記の画像はパーリンノイズで利用する単位座標と呼ばれるものです。
単位座標とは、入力されたx, y(3次元の場合はz)の値を0~1の間で繰り返す座標のことです。
単位座標を使用するため、パーリンノイズ関数は0~1の範囲で値を返します。

f:id:soramamenatan:20200321155019p:plain

Unityでパーリンノイズの実装 - e.blog:より引用


擬似乱数による勾配ベクトル

単位座標には、4つ(3次元の場合は8つ)の勾配ベクトルが存在します。
今回、勾配ベクトルは擬似乱数によって生成します。
擬似乱数ですので、値が同じであれば同じ勾配ベクトルを返します

各格子点に対する勾配ベクトルのイメージ

f:id:soramamenatan:20200321161529p:plain

Unityでパーリンノイズの実装 - e.blog:より引用


勾配ベクトルとは

ここは今回のパーリンノイズとは直接関わりがないため飛ばしていただいて構いません。

二変数関数f(x, y)に対して、その偏微分を並べた二次元ベクトル\displaystyle (\frac{∂f}{∂x},\frac{∂f}{∂y} )を勾配ベクトルといいます。

勾配ベクトルは、別名グラディエント、gradientと呼ばれます。
書き方は\displaystyle gradf = (\frac{∂f}{∂x},\frac{∂f}{∂y} )、や\displaystyle ∇f = (\frac{∂f}{∂x},\frac{∂f}{∂y} )で書かれます。
∇ ... ナブラ, ∂ ... ラウンド


勾配ベクトルが表すもの

勾配ベクトル飲む気は、今いる点からちょっと動いた時に関数の値が一番大きくなる向きのことです。

例えば、

 \displaystyle
f(x,y) = x^2+ \frac{1}{2}y


という式の勾配ベクトルは、

 \displaystyle
grad f =  (2x, \frac{1}{2})


となります。
なぜこうなるのかは微分偏微分の知識が必要です。

微分についてはこちらを、

soramamenatan.hatenablog.com

偏微分についてはこちらを参考にしてください。

soramamenatan.hatenablog.com

勾配ベクトル参考サイト様

mathtrain.jp

www.youtube.com

長くなりましたが、パーリンノイズに話を戻します。


距離ベクトル

勾配ベクトルの次に、距離ベクトルを求めます。
ここでいう距離ベクトルは、入力点-各格子点です。

距離ベクトルのイメージ

f:id:soramamenatan:20200321164704p:plain

Unityでパーリンノイズの実装 - e.blog:より引用


ノイズに対する影響値

上記で求めた勾配ベクトルと距離ベクトルの内積を取ります。 そうすることで、入力点に対する各格子点の勾配ベクトルの影響値が算出できます。


2つのベクトルの内積で影響値が算出できる理由について説明します。
内積は、2つのベクトルが同じ方向をむいていたら1を、反対をむいていたら-1を返します。
これは2つのベクトルがどれだけ似ているかを示す値と言えるので、影響値を出すことができます。

影響値のイメージ

f:id:soramamenatan:20200321165827p:plain

パーリンノイズを理解する | POSTD:より引用


終値を求める

各格子点の影響値を、加重平均によって求めます。

ソースのイメージ
// 各格子点の内積
int dot1, dot2, dot3, dot4
// uv値
int u, v;
// 横での補完
int lerp1 = lerp(dot1, dot2, u);
int lerp2 = lerp(dot3, dot4, u);
// 縦での補完
int result = lerp(lerp1, lerp2, v);


終値を滑らかにする

上で求めた最終値は線形補完なので、不自然に見えてしまいます。
そこでフェード関数を用いて滑らかにします。

フェード関数の定義
6t^5 - 15t^4 + 10^3
フェード関数のグラフ

f:id:soramamenatan:20200321171053p:plain


これでパーリンノイズの算出は終わりました。
本当はもう1ステップあるのですが今回は省略します。

パーリンノイズ参考サイト様

edom18.hateblo.jp

postd.cc


結果

パーリンノイズが完成しました。

パーリンノイズ1枚

f:id:soramamenatan:20200321173735p:plain

パーリンノイズ5枚

f:id:soramamenatan:20200321173754p:plain


ソースコードにコメントを付与

// 擬似乱数勾配ベクトル
fixed2 gradientVector(fixed2 uv, fixed2 size) {
    uv = frac(uv / size);
    uv = fixed2(dot(frac(uv / size), fixed2(127.1, 311.7)), dot(frac(uv / size), fixed2(269.5, 183.3)));
    // -1~1の範囲にする
    return -1.0 + 2.0 * frac(sin(uv) * 43758.5453123);
}

// バイリニア補間
// 4点を与えてその矩形内のxy両方向に線形補間
fixed2 bilinear(fixed f0, fixed f1, fixed f2, fixed f3, fixed fx, fixed fy) {
    return lerp(lerp(f0, f1, fx), lerp(f2, f3, fx), fy);
}

// fade関数
// 6^5 - 15^4 + 10^3
fixed fade(fixed t) {
    return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
}

// 単体パーリンノイズ
fixed perlinNoiseOne(fixed2 uv, fixed2 size) {
    // 格子点
    fixed2 p = floor(uv);
    // 入力点(0~1)
    fixed2 f = frac(uv);

    // 勾配ベクトルと距離ベクトルとの内積
    // gradientVector(p + fixed2(x, y) ... 各格子点の勾配ベクトル
    // f - fixed2(x, y) ... 入力点 - 格子点の距離ベクトル
    fixed d00 = dot(gradientVector(p + fixed2(0, 0), size), f - fixed2(0, 0));
    fixed d10 = dot(gradientVector(p + fixed2(1, 0), size), f - fixed2(1, 0));
    fixed d01 = dot(gradientVector(p + fixed2(0, 1), size), f - fixed2(0, 1));
    fixed d11 = dot(gradientVector(p + fixed2(1, 1), size), f - fixed2(1, 1));
    // フェード関数で補完
    return bilinear(d00, d10, d01, d11, fade(f.x), fade(f.y)) + 0.5f;
}

// パーリンノイズ
// オクターブの異なるパーリンノイズテクスチャを5枚合成
fixed perlinNoise(fixed2 uv, fixed2 size) {
    fixed f = 0;
    // 合成する値は、べき乗
    f += perlinNoiseOne(uv *  2, size) / 2;
    f += perlinNoiseOne(uv *  4, size) / 4;
    f += perlinNoiseOne(uv *  8, size) / 8;
    f += perlinNoiseOne(uv * 16, size) / 16;
    f += perlinNoiseOne(uv * 32, size) / 32;
    return f;
}

パーリンノイズの部分のみ抜粋しています。


今回はここまでになります。
ご視聴ありがとうございました。