おもちゃラボ

Unityで遊びを作ってます

【Unityシェーダ入門】シェーダで作るノイズ5種盛り

ノイズといえば、砂嵐のようなランダムノイズから連続性を保ったパーリンノイズまで様々なノイズが考えられています。残念なことにUnityのシェーダにはノイズを生成するメソッドが用意されていないため、プログラムを作る必要があります。
 この記事では、Unityでランダムノイズ、ブロックノイズ、バリューノイズ、パーリンノイズ、fBmを作る方法を見ていきましょう。

f:id:nn_hokuson:20170613223024j:plain:w500

ランダムノイズ

ランダムノイズとは場所や時間に依存せずに全くのランダムに値を決めるノイズのことです。テレビの砂嵐(古い!)と同じものですね。

www.youtube.com

まずはノイズ生成の基本になるランダムノイズを表示するシェーダを作ってみましょう!シーン中にノイズを表示するための平面モデルをUnityのシーンビューに配置して、そこにシェーダプログラムをアタッチします。

Unityのヒエラルキービューから「Create」→「3D Object」→「Plane」を選択します。続いて、プロジェクトビューで右クリックし、「Create」→「Shader」→「Standard Surface Shader」を選択します。作成したシェーダのファイル名は「RandomNoise」にしておきましょう。

f:id:nn_hokuson:20170127192620p:plain

先に、マテリアルを作っておきましょう。プロジェクトビューから「Create」→「Material」を選択しファイル名を「NoiseTest」に変更します。作成したマテリアルをシーンビューのPlaneにドラッグ&ドロップしてアタッチし、インスペクタからシェーダファイルを「Custom/RandomNoise」に変更します。まだRandomNoiseのシェーダプログラムを書いていないので、マテリアルは真っ白のままです。

f:id:nn_hokuson:20170127192909p:plain

ではランダムノイズのシェーダプログラムを作っていきましょう。RandomNoise.shaderを開いて次のプログラムを入力してください。

Shader "Custom/RandomNoise" {
    Properties {
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200
        
        CGPROGRAM
        #pragma surface surf Standard fullforwardshadows
        #pragma target 3.0

        sampler2D _MainTex;

        struct Input {
            float2 uv_MainTex;
        };

        float random (fixed2 p) { 
            return frac(sin(dot(p, fixed2(12.9898,78.233))) * 43758.5453);
        }

        void surf (Input IN, inout SurfaceOutputStandard o) {
            float c = random(IN.uv_MainTex);
            o.Albedo = fixed4(c,c,c,1);
        }
        ENDCG
    }
    FallBack "Diffuse"
}


ここでは、ランダムノイズを表示するためにrandomメソッドを使っています。randomメソッドは引数にuv座標をとり、その座標からランダムな値を生成するメソッドです。このメソッドの値がどのように決められたかは下の記事に書かれているようです。

qiita.com

randomメソッドからの戻り値を明度として出力しています。Unityで実行すると、実行結果は次のようになります。まさにランダムな砂嵐ですね。

f:id:nn_hokuson:20170127194012p:plain:w450

ブロックノイズ

次にブロックノイズを見ていきます。ブロックノイズはランダムノイズに比べると粒度が荒いノイズになります。ディジタルテレビの調子が悪いときや、動画を圧縮しすぎたときに出てくるやつです。


www.youtube.com


先程と同様に、Unityでブロックノイズを表示するためのシェーダプログラムを作り、シェーダのファイル名は「BlockNoise」にします。また、シーンビューで平面モデルを選択し、インスペクタからシェーダを「Custom/BlockNoise」 に変更します。では、ブロックノイズを生成するシェーダプログラムを作りましょう。次のプログラムを入力してください。

Shader "Custom/BlockNoise" {
    Properties {
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200
        
        CGPROGRAM
        #pragma surface surf Standard fullforwardshadows
        #pragma target 3.0

        sampler2D _MainTex;

        struct Input {
            float2 uv_MainTex;
        };

        float random (fixed2 p) 
        { 
            return frac(sin(dot(p, fixed2(12.9898,78.233))) * 43758.5453);
        }

        float noise(fixed2 st)
        {
            fixed2 p = floor(st);
            return random(p);
        }

        void surf (Input IN, inout SurfaceOutputStandard o) 
        {
            float c = noise( IN.uv_MainTex*8 );            
            o.Albedo = fixed4(c,c,c,1);
        }
        ENDCG
    }
    FallBack "Diffuse"
}


実行すると次のように、正方形のブロックがランダム色で配置されます。

f:id:nn_hokuson:20170127194116p:plain:w450

ブロックノイズのシェーダプログラムは、先ほど作ったランダムノイズを拡張して作っています。やっていることは非常に簡単で、ランダムノイズからブロックごとに代表的な1点を取り出してベタ塗りしているだけです。

f:id:nn_hokuson:20170127194125p:plain:w450

代表的な一点はfloorメソッドを使って決めています。floorメソッドは小数値の整数部分を返すメソッドです。例えば、4.3なら4、5.1なら5を返すので、これをuv座標に使うと(0 <= x < 1 かつ 0 <= y < 1) のエリアの代表点は(0, 0)に、(3 <= x < 4 かつ 1 <= y < 2)のエリアの代表点は(3, 1)になります。

f:id:nn_hokuson:20170127194624p:plain:w250

ここではテクスチャ座標を8倍してからnoiseメソッドに渡しているので、8x8のブロックノイズを生成できます。

バリューノイズ

ブロックノイズではブロック単位できっちりと色が分かれてしまったため、ノイズっぽくありませんでした。そこでブロックノイズを平滑化したバリューノイズというものが考案されます。

Unityでバリューノイズ用のシェーダファイルを作りましょう。作成したシェーダのファイル名は「ValueNoise」にします。また、シーンビューで平面モデルを選択し、インスペクタからシェーダを「Custom/ValueNoise」 に変更します。次のシェーダプログラムを入力して下さい。

Shader "Custom/ValueNoise" {
    Properties {
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200
        
        CGPROGRAM
        #pragma surface surf Standard fullforwardshadows
        #pragma target 3.0

        sampler2D _MainTex;

        struct Input {
            float2 uv_MainTex;
        };

        float random (fixed2 p) { 
            return frac(sin(dot(p, fixed2(12.9898,78.233))) * 43758.5453);
        }

        float noise(fixed2 st)
        {
            fixed2 p = floor(st);
            return random(p);
        }

        float valueNoise(fixed2 st)
        {
            fixed2 p = floor(st);
            fixed2 f = frac(st);

            float v00 = random(p+fixed2(0,0));
            float v10 = random(p+fixed2(1,0));
            float v01 = random(p+fixed2(0,1));
            float v11 = random(p+fixed2(1,1));
            
            fixed2 u = f * f * (3.0 - 2.0 * f);            

            float v0010 = lerp(v00, v10, u.x);
            float v0111 = lerp(v01, v11, u.x);
            return lerp(v0010, v0111, u.y);
        }

        void surf (Input IN, inout SurfaceOutputStandard o) {
            float c = valueNoise( IN.uv_MainTex*8);            
            o.Albedo = fixed4(c,c,c,1);
        }
        ENDCG
    }
    FallBack "Diffuse"
}


バリューノイズでは、ブロックノイズで使った四隅の色からブロック内の色を補間して決めます。四隅の色はv00、v10、v01、v11で求めています。

f:id:nn_hokuson:20170127194347p:plain:w400

値を4隅の値から補間するためには、補間したい座標がブロック内の左下からどれくらい離れているかを知る必要があります。ここではそのオフセット値をfracメソッドで求めています。fracメソッドは少数値の少数部分を返すメソッドです。たとえば、(x, y)=(2.7, 1.2)であれば、fracの値は(0.7, 0.2)になります。

f:id:nn_hokuson:20170127194804p:plain:w250

このオフセット値を -2f^3 + 3f^2という数式を使って補間します。この数式をグラフ化すると次のようになり、0〜1の間の値を緩やかに補間できることがわかります(この行をu=f;にすると線形補間になります)

f:id:nn_hokuson:20170127194405p:plain:w450

ここではまずブロックの下側の色(v0010)を決めて、次にブロックの上側の色(v0111)を決めています。最後にその間の2点を補間して目的の座標の色を決めています。

f:id:nn_hokuson:20170127194411p:plain

実行結果は次のようになります。先ほどのブロックノイズと比べると滑らかなノイズになっているのが分かりますね。

f:id:nn_hokuson:20170127194418p:plain:w500

パーリンノイズ

バリューノイズは境界線をぼかしたことでかなり滑らかになりましたが、まだブロック感が残っていますね。パーリンノイズはバリューノイズをさらに滑らかにしたものになります。このパーリンノイズは、炎や雲などにみられる自然な変化のノイズを表現するのによく使われます。

こちらもUnityでパーリンノイズ用のシェーダファイルを作りましょう。作成したシェーダのファイル名は「PerlinNoise」にします。また、シーンビューで平面モデルのシェーダを「Custom/PerlinNoise」 に変更します。PerlinNoise.shaderには次のシェーダプログラムを入力して下さい。

Shader "Custom/PerlinNoise" {
    Properties {
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200
        
        CGPROGRAM
        #pragma surface surf Standard fullforwardshadows
        #pragma target 3.0

        sampler2D _MainTex;

        struct Input {
            float2 uv_MainTex;
        };

        fixed2 random2(fixed2 st){
            st = fixed2( dot(st,fixed2(127.1,311.7)),
                           dot(st,fixed2(269.5,183.3)) );
            return -1.0 + 2.0*frac(sin(st)*43758.5453123);
        }

        float perlinNoise(fixed2 st) 
        {
            fixed2 p = floor(st);
            fixed2 f = frac(st);
            fixed2 u = f*f*(3.0-2.0*f);

            float v00 = random2(p+fixed2(0,0));
            float v10 = random2(p+fixed2(1,0));
            float v01 = random2(p+fixed2(0,1));
            float v11 = random2(p+fixed2(1,1));

            return lerp( lerp( dot( v00, f - fixed2(0,0) ), dot( v10, f - fixed2(1,0) ), u.x ),
                         lerp( dot( v01, f - fixed2(0,1) ), dot( v11, f - fixed2(1,1) ), u.x ), 
                         u.y)+0.5f;
        }

        void surf (Input IN, inout SurfaceOutputStandard o) {
            float c = perlinNoise(IN.uv_MainTex*8);//(perlinNoise( IN.uv_MainTex*6))*0.5+0.5;        
            o.Albedo = fixed4(c,c,c,1);
            o.Metallic = 0;
            o.Smoothness = 0;
            o.Alpha = 1;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

パーリンノイズの実行結果は次のようになります。バリューノイズに比べると滑らかに変化しているのがわかります。

f:id:nn_hokuson:20170613211116j:plain:w500

バリューノイズが格子状に区切った四隅の色を補完してブロック内部の色を決めていたのにに対して、パーリンノイズではブロック内部の点の座標により、四隅の色もなだらかに変化します。

この「四隅の色をなだらかに変化」させるため、格子点に設定されたランダムなベクトルと、ブロック内部の点から格子点に向かうベクトルの内積を取って、それを色情報として扱います。これにより、ブロック内部の座標を変化させることで、四隅の色もなだらかにグラデーション状に変化します。

f:id:nn_hokuson:20170613205332p:plain:w500

パーリンノイズでは「内部の点から格子点へのベクトル」の内積を取っているため、格子点上では必ず黒色になっていることに注意して下さい。(ここでは分かりやすいように、-1.0〜1.0の範囲でスケーリングして、マイナスの値を赤、プラスの値を青色で表示しています)

f:id:nn_hokuson:20170613211457j:plain:w250

fBmノイズ

パーリンノイズをさらに綺麗に見せるためには、さまざまな解像度のノイズをずらしながら重畳させます。これをfBm (Fractional Brownian Motion)とよび、地形や雲など幅広い表現に使われています。

UnityでfBm用のシェーダファイルを作りましょう。作成したシェーダのファイル名は「fBm.shader」にします。また、シーンビューで平面モデルのシェーダを「Custom/fBm」 に変更します。fBm.shaderには次のシェーダプログラムを入力して下さい。

Shader "Custom/fBm" {
    Properties {
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200
        
        CGPROGRAM
        #pragma surface surf Standard fullforwardshadows
        #pragma target 3.0

        sampler2D _MainTex;

        struct Input {
            float2 uv_MainTex;
        };

        fixed2 random2(fixed2 st){
            st = fixed2( dot(st,fixed2(127.1,311.7)),
                           dot(st,fixed2(269.5,183.3)) );
            return -1.0 + 2.0*frac(sin(st)*43758.5453123);
        }

        float perlinNoise(fixed2 st) 
        {
            fixed2 p = floor(st);
            fixed2 f = frac(st);
            fixed2 u = f*f*(3.0-2.0*f);

            float v00 = random2(p+fixed2(0,0));
            float v10 = random2(p+fixed2(1,0));
            float v01 = random2(p+fixed2(0,1));
            float v11 = random2(p+fixed2(1,1));

            return lerp( lerp( dot( v00, f - fixed2(0,0) ), dot( v10, f - fixed2(1,0) ), u.x ),
                         lerp( dot( v01, f - fixed2(0,1) ), dot( v11, f - fixed2(1,1) ), u.x ), 
                         u.y)+0.5f;
        }

        float fBm (fixed2 st) 
        {
            float f = 0;
            fixed2 q = st;

            f += 0.5000*perlinNoise( q ); q = q*2.01;
             f += 0.2500*perlinNoise( q ); q = q*2.02;
            f += 0.1250*perlinNoise( q ); q = q*2.03;
            f += 0.0625*perlinNoise( q ); q = q*2.01;

            return f;
        }

        void surf (Input IN, inout SurfaceOutputStandard o) {
            float c = fBm(IN.uv_MainTex*6);    
            o.Albedo = fixed4(c,c,c,1);
            o.Metallic = 0;
            o.Smoothness = 0;
            o.Alpha = 1;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

fBmには新しい概念は登場しません。パーリンノイズの解像度を変えたテクスチャを、一定の割合で足し合わせていきます。このとき、いくつの階層のノイズを合成するかを表す値を「オクターブ」、それぞれのノイズをどのような割合で合成するかを「パーシステンス」と呼びます。

 上のプログラムでは、4つの解像度のノイズのテクスチャを次のような割合で合成しています。

f:id:nn_hokuson:20170613213952j:plain

 ノイズの解像度が高まるごとに、パーシステンスをべき乗した値を合成する割合として使用しています。最も解像度の低いテクスチャを0.5、次に解像度が低いテクスチャを0.25、次のテクスチャを0.125、最も解像度の高いテクスチャを0.0625の割合で合成しています。

fBmを使用したノイズ画像がこちらになります。これまでの中で最も綺麗なノイズになっていますね。

f:id:nn_hokuson:20170613222453j:plain:w500

まとめ&参考書

ここでは、Unityのシェーダで作れるノイズを5種類(ランダムノイズ・ブロックノイズ・バリューノイズ・パーリンノイズ・fBm)紹介しました。