ノイズのタイリング

ノイズのタイリング

パーリンノイズのタイリング

 九個のQuadを並べ、シェーダでノイズ2(パーリンノイズ)で作成したパーリンノイズを生成すると

このように、境界ができてしまいます。この原因と解決法を探るため、一次元パーリンノイズを並べたグラフと作成しました。

グラフの中央が境界となっています。パーリンノイズは青色の直線と赤色の直線を補間することにより求めています。このグラフより、青色と赤色の直線は一つの直線となっていますが、境界における直線はv字となっていることが分かります。これが、境界部分で滑らかに数値が変化していない理由となります。この両直線の傾きを一致させると境界部分が滑らかにつながるはずです。傾きを一致させたグラフを以下に示します。

このグラフより、境界における直線の傾きを一致させることで、滑らかにつながることが分かりました。

shader

通常のパーリンノイズのシェーダは以下の通りです。

Shader "Noise/PerlinNoise"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
		_Seed ("SeedX", Int) = 0
        _NoiseScale ("noise scale", Range(1.0, 30.0)) = 1.0
	}
	SubShader
	{
		Tags { "RenderType"="Opaque" }

		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"

			struct appdata
			{
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
			};

			struct v2f
			{
				float2 uv : TEXCOORD0;
				float4 vertex : SV_POSITION;
			};

			sampler2D _MainTex;
			float4 _MainTex_ST;
			
			int _Seed;
            float _NoiseScale;
			
			float2 rand(float2 st, int seed)
			{
				float2 s = float2(dot(st, float2(127.1, 311.7)) + seed, dot(st, float2(269.5, 183.3)) + seed);
				return -1 + 2 * frac(sin(s) * 43758.5453123);
			}

			float noise(float2 st, int seed)
			{
				float2 p = floor(st);
				float2 f = frac(st);

				float w00 = dot(rand(p, seed), f);
				float w10 = dot(rand(p + float2(1, 0), seed), f - float2(1, 0));
				float w01 = dot(rand(p + float2(0, 1), seed), f - float2(0, 1));
				float w11 = dot(rand(p + float2(1, 1), seed), f - float2(1, 1));
				
				float2 u = f * f * (3 - 2 * f);

				return lerp(lerp(w00, w10, u.x), lerp(w01, w11, u.x), u.y);
			}

			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				float3 result = noise(i.uv * _NoiseScale, _Seed) + 0.5;
                fixed4 col = fixed4(result, 1.0);
				return col;
			}
			ENDCG
		}
	}
}

このシェーダにおいて座標は0~_NoiseScaleとなります。そのため、rand(_NoiseScale)がv字となっている青色の直線の傾きに該当します。よって、このrand(_NoiseScale)をrand(0)へ変更する処理を追加すればタイリングできるはずです。これを踏まえ、ノイズを計算する関数を以下のように変更しました。

float tiling_noise2d(float2 st, int scale, int seed)
{
    int2 p1 = floor(st);
    int2 p2 = fmod(p1 + int2(1, 1), scale);
    float2 f = frac(st);

    float w00 = dot(rand2d((float2)p1, seed), f);
    float w10 = dot(rand2d(float2(p2.x, p1.y), seed), f - float2(1.0, 0.0));
    float w01 = dot(rand2d(float2(p1.x, p2.y), seed), f - float2(0.0, 1.0));
    float w11 = dot(rand2d((float2)p2, seed), f - float2(1.0, 1.0));

    float2 u = f * f * (3.0 - 2.0 * f);

    return lerp(lerp(w00, w10, u.x), lerp(w01, w11, u.x), u.y);
}

fmodにより、rand(floor(st)+int2(1, 1))がrand(_Noisescale)となったときにrand(0)となるように処理をしています。

実行結果

 上記の通りに変更を加えたシェーダを実行した結果は以下の通りです。境界部分がきれいに繋がっていることが分かります。

オフセットの追加

 uv座標に一定の数値を加算することで、ノイズを移動できるようにします。オフセットを行わない場合はp1をfmodで処理しなくても問題ありませんでしたが、オフセットによってノイズを移動する場合はp2と同様にp1にもfmodを追加する必要があります。

float tiling_noise2d(float2 st, int scale, int seed)
{
    int2 p = floor(st);
    int2 p1 = fmod(p, scale);
    int2 p2 = fmod(p + int2(1, 1), scale);

    float2 f = frac(st);

    float w00 = dot(rand2d((float2)p1, seed), f);
    float w10 = dot(rand2d(float2(p2.x, p1.y), seed), f - float2(1.0, 0.0));
    float w01 = dot(rand2d(float2(p1.x, p2.y), seed), f - float2(0.0, 1.0));
    float w11 = dot(rand2d((float2)p2, seed), f - float2(1.0, 1.0));

    float2 u = f * f * (3.0 - 2.0 * f);

    return lerp(lerp(w00, w10, u.x), lerp(w01, w11, u.x), u.y);
}

fixed4 frag (v2f i) : SV_Target
{
    int noise_scale = _NoiseScale;
    float2 offset = _Offset;
    float2 uv = i.uv * noise_scale + offset;

    float3 result = tiling_noise2d(uv, noise_scale, _Seed) + 0.5;
    fixed4 col = fixed4(result, 1.0);
    return col;
}

x方向へ9動かした状態(offset=(9,0))で上記シェーダを実行すると以下のようになります。

ノイズをオフセットによって動かすと境界が生じてしまいました。これは、fmodによって求められるp1及びp2 が本来求められるべき値より小さい値になっていることが原因のようです(日々量産:fmod)。そこで、以下のように変更を加えました。

float tiling_noise2d(float2 st, int scale, int seed)
{
    float eps = 0.1;
    int2 p = floor(st);
    int2 p1 = fmod(p, scale) + eps;
    int2 p2 = fmod(p + int2(1, 1), scale) + eps;

    float2 f = frac(st);

    float w00 = dot(rand2d((float2)p1, seed), f);
    float w10 = dot(rand2d(float2(p2.x, p1.y), seed), f - float2(1.0, 0.0));
    float w01 = dot(rand2d(float2(p1.x, p2.y), seed), f - float2(0.0, 1.0));
    float w11 = dot(rand2d((float2)p2, seed), f - float2(1.0, 1.0));

    float2 u = f * f * (3.0 - 2.0 * f);

    return lerp(lerp(w00, w10, u.x), lerp(w01, w11, u.x), u.y);
}

p1及びp2に小さな値を加算することで、正しい値が求まるように処理をしています。これを実行すると以下のようになります。

境界をなくすことができました。さらに、オフセットをマイナスの数値としても問題なくノイズが求まるように処理を変更します。現状では、p1及びp2はそれぞれ・・・0,-4,-3,-2,-1,0・・・0,1,2,3,4,0・・・となります。これを・・・ 0,1,2,3,4,0・・・0,1,2,3,4,0・・・となるようにコードを変更します。まず、以下のコードで座標が0以上か0未満で処理を変更できるようにします。

int2 sig = step(0.0, st);

次に、・・・0,-4,-3,-2,-1,0・・・0,1,2,3,4,0・・・となるように処理を行います。

int2 p = fmod(floor(st), scale) + eps * sign(st);

fmodで求められた値に小さい数値を加算することで、正しい値が求まるようにしていましたが、マイナスの場合は小さな値を減算する必要があります。よって、epsにsign(st)を乗算することでこれに対応しています。最後に、・・・ 0,1,2,3,4,0・・・0,1,2,3,4,0・・・となるように処理を加えます。

p = p * sig + (scale + p) * sign(-p) * (1 - sig);

sigを用いてpが正と負の場合によって処理を変更できるようにしています。また、pが負の時はscaleを加算することで・・・0,-4,-3,-2,-1,0・・・は・・・5,1,2,3,4,5・・・となり、sign(-p)を乗算することで・・・0,1,2,3,4,0・・・となります。

shader

オフセットに対応したノイズは以下のコードで求めることができます。

int2 tiling_position(float2 st, float scale)
{
    float eps = 0.1;
    int2 sig = step(0.0, st);
    int2 p = fmod(floor(st), scale) + eps * sign(st);
    p = p * sig + (scale + p) * sign(-p) * (1 - sig);

    return p;
}

float tiling_noise2d(float2 st, int scale, int seed)
{
    int2 p1 = tiling_position(st, scale);
    int2 p2 = tiling_position(st + int2(1, 1), scale);

    float2 f = frac(st);

    float w00 = dot(rand2d((float2)p1, seed), f);
    float w10 = dot(rand2d(float2(p2.x, p1.y), seed), f - float2(1.0, 0.0));
    float w01 = dot(rand2d(float2(p1.x, p2.y), seed), f - float2(0.0, 1.0));
    float w11 = dot(rand2d((float2)p2, seed), f - float2(1.0, 1.0));

    float2 u = f * f * (3.0 - 2.0 * f);

    return lerp(lerp(w00, w10, u.x), lerp(w01, w11, u.x), u.y);
}

shader

 オフセットに対応したタイリング可能なパーリンノイズのシェーダを以下の通りです。

Shader "Noise/TilingNoise"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _NoiseScale ("noise scale", Range(1.0, 30.0)) = 1.0
        _Seed ("seed", INT) = 1
        _Offset ("offset", VECTOR) = (0.0, 0.0, 0.0, 0.0)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            float _Tiling;
            int _NoiseScale;
            int _Seed;

            float2 _Offset;

            int2 tiling_position(float2 st, float scale)
            {
                float eps = 0.1;
                int2 sig = step(0.0, st);
                int2 p = fmod(floor(st), scale) + eps * sign(st);
                p = p * sig + (scale + p) * sign(-p) * (1 - sig);

                return p;
			}

            float2 rand2d(float2 st, int seed)
            {
	            float2 s = float2(dot(st, float2(127.1, 311.7)) + seed, dot(st, float2(269.5, 183.3)) + seed);
	            return -1.0 + 2.0 * frac(sin(s) * 43758.5453123);
            }

            float tiling_noise2d(float2 st, int scale, int seed)
            {
                int2 p1 = tiling_position(st, scale);
                int2 p2 = tiling_position(st + int2(1, 1), scale);

	            float2 f = frac(st);

	            float w00 = dot(rand2d((float2)p1, seed), f);
	            float w10 = dot(rand2d(float2(p2.x, p1.y), seed), f - float2(1.0, 0.0));
	            float w01 = dot(rand2d(float2(p1.x, p2.y), seed), f - float2(0.0, 1.0));
	            float w11 = dot(rand2d((float2)p2, seed), f - float2(1.0, 1.0));
	
	            float2 u = f * f * (3.0 - 2.0 * f);

	            return lerp(lerp(w00, w10, u.x), lerp(w01, w11, u.x), u.y);
            }

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                int noise_scale = _NoiseScale;
                float2 offset = _Offset;
                float2 uv = i.uv * noise_scale + offset;

                float3 result = tiling_noise2d(uv, noise_scale, _Seed) + 0.5;

                fixed4 col = fixed4(result, 1.0);
                return col;
            }
            ENDCG
        }
    }
}

セルラーノイズのタイリング

 セルラーノイズ(シェーダでノイズ3(セルノイズ))もパーリンノイズと同様の方法でタイリングできます。

shader

 オフセットに対応したタイリング可能なセルラーノイズのシェーダは以下の通りです。

Shader "Noise/TilingNoise"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _NoiseScale ("noise scale", Range(1.0, 30.0)) = 1.0
        _Seed ("seed", INT) = 1
        _Offset ("offset", VECTOR) = (0.0, 0.0, 0.0, 0.0)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            float _Tiling;
            int _NoiseScale;
            int _Seed;

            float2 _Offset;

            int2 tiling_position(float2 st, float scale)
            {
                float eps = 0.1;
                int2 sig = step(0.0, st);
                int2 p = fmod(floor(st), scale) + eps * sign(st);
                p = p * sig + (scale + p) * sign(-p) * (1 - sig);

                return p;
			}

            float2 rand2d(float2 st, int seed)
            {
	            float2 s = float2(dot(st, float2(127.1, 311.7)) + seed, dot(st, float2(269.5, 183.3)) + seed);
	            return frac(sin(s) * 43758.5453123);
            }

            float Tiling_CellularNoise(float2 st, int scale, int seed)
            {
				float2 f_st = frac(st);

				float min_dist = 1.0;
				float2 min_p;
				for(int j = -1; j <= 1; j++){
					for(int k = -1; k <= 1; k++){
						float2 n = float2(j, k);
						float2 p = rand2d(tiling_position(st + n, scale), seed);
						float dist = distance(f_st, p + n);
						if(dist < min_dist){
							min_dist = dist;
							min_p = p;
						}
					}
				}

                return min_dist;
			}

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                int noise_scale = _NoiseScale;
                float2 offset = _Offset;
                float2 uv = i.uv * noise_scale + offset;

                float3 result = Tiling_CellularNoise(uv, noise_scale, _Seed);

                fixed4 col = fixed4(result, 1.0);
                return col;
            }
            ENDCG
        }
    }
}

実行結果

 Quadを9個並べた状態で上記シェーダを実行すると以下のようになります(noise scale=5、offset=(-3,-9))。セルラーノイズも問題なくタイリングできていることが分かります。

参考サイト

日々量産:fmod