【Unity】Unityシェーダーの基礎とLambert拡散反射の実装【シェーダー】

Unity
マイケル
マイケル
みなさんこんにちは!
マイケルです!
エレキベア
エレキベア
こんにちクマ〜〜〜
マイケル
マイケル
今回はUnityのシェーダーについて触れていきます!
シェーダーについては何度か記事にしていますが、Unity特有の事項もいくつかあるためその辺りを重点的に見ていこうと思います。
エレキベア
エレキベア
ついにシェーダークマ〜〜〜
これまでの記事ではC++で開発する時に使っていたクマね
マイケル
マイケル
Unityのシェーダー基礎について触れた後、他の記事と同様にLambert拡散反射を実装するところまでやっていこうと思います!
シェーダーについての基礎知識については上記の記事もご参照ください!
エレキベア
エレキベア
シェーダーは毎度ランバートから始まるクマね
マイケル
マイケル
なお、今回使用したUnityのバージョンは以下になります!
バージョン違いやURPで記述する際には少し異なる場合もあるため、ご了承ください!
Unity2021.3.1f1
エレキベア
エレキベア
こればっかりは仕方ないクマ〜〜・・・
スポンサーリンク

シェーダーとは

マイケル
マイケル
まずシェーダーとはそもそも何か?という点についてなのですが、
基本的には 描画する際の陰影処理を行うプログラムのこと になります!
しかし、シェーダーで実装出来る範囲はかなり広くなっていて、陰影だけでなく頂点の増減やポリゴン分割をしたり、見た目のトゥーン調にするといったことも出来るようになってきました、
例としてDirectX11の描画パイプラインは下記のようになります。
UntitledImage
↑DirectX11の描画パイプライン
エレキベア
エレキベア
シェーダーの種類もかなり多いクマね
描画全般で使用するプログラムと言ってもよさそうクマ
マイケル
マイケル
今はシェーダー芸と呼ばれるほどの表現もできるくらいだからね・・・
GLSLだけど、ShaderToyというサイトで表現の広さは合間見えるのでこちらを見てみるのもおすすめです!

shadertoy

エレキベア
エレキベア
これ全部シェーダーで実装されているのクマか・・・
マイケル
マイケル
しかし、これだけシェーダーの種類があっても、主流は「頂点シェーダー」「フラグメントシェーダー(ピクセルシェーダー)」になります。
受け取った座標情報を頂点シェーダーで編集し、その情報をもとにフラグメントシェーダーで色合いを決めるといった流れで、シェーダーを書くというのは基本的にこの辺りの処理を書くことを示すのが多いです。
ScreenShot 2021 10 09 21 59 57
↑頂点シェーダー、フラグメントシェーダーの役割
エレキベア
エレキベア
Unityでもこの部分の処理を実装できるクマね
マイケル
マイケル
この辺りの詳しい話については、Unityの下記動画が分かりやすいので是非ご参照ください!

シェーダを理解しよう | Unity Learning Materials

エレキベア
エレキベア
安原さんの説明は本当分かりやすいクマ〜〜

Unityシェーダーの基礎知識

マイケル
マイケル
それではここからUnity特有の話に入っていきます!

シェーダーの作成方法

マイケル
マイケル
まずはシェーダーの作成方法の種類について!
こちらはプログラムを書く方法と、ShaderGraphといったノードベースエディタを使用する方法があります。
シェーダープログラムを書くCg/HLSLといった言語を使用して実装する
ノードベースエディタを使用するShaderGraphといったツールを使用して実装する
エレキベア
エレキベア
ShaderGraphは流行りクマね〜〜〜
マイケル
マイケル
ShaderGraphは、視覚的に確認しながら実装できるのが強みです!
ただ、複雑になるほどノードも煩雑になってしまうのでそこは注意が必要です。。
また、シェーダー言語はGLSLも使用できるようですが、Cg/HLSLの使用が推奨されているようです。

マイケル
マイケル
ただ基本的な考え方はどちらも同じなので、個人的にはどちらから触ってもよいと思っています!
今回は内部が分かりやすいよう、コードベースで解説していこうと思います。
エレキベア
エレキベア
ShaderGraphで遊んでから入るのも有りかもしれないクマね

シェーダープログラムの実装方法

マイケル
マイケル
次は具体的なシェーダープログラムの実装方法について!
こちらは頂点、フラグメントシェーダーを書く方法の他、サーフェイスシェーダー、固定関数シェーダーというものも用意されています。

シェーダーを書く | Unityマニュアル

頂点シェーダーとフラグメントシェーダー基本的に全て自分で書く一般的な方法
サーフェイスシェーダーライティングをある程度Unityに任せることができる(※URPでは非推奨)
固定関数シェーダー用意された機能を組み合わせて描画する・・・ようだが多分使わない
マイケル
マイケル
ただ、サーフェイスシェーダーはURPでは非対応になっていてShaderGraphへの移行が推奨されているようなので、
コードで書く場合には頂点シェーダーとフラグメントシェーダーを使用する方法を取るのがよさそうです。
エレキベア
エレキベア
サーフェイスシェーダーでの実装例も多いクマからここは注意クマね

その他のシェーダー

マイケル
マイケル
また、実際にシェーダーを作成しようとすると上記以外にも選択できるShaderがあると思います。
20221229 01
マイケル
マイケル
UnlitShader、StandardSurfaceShaderは先ほど解説した標準のシェーダーとサーフェイスシェーダになります。
その他3つのシェーダーは下記のようになっています。
Image Effect Shaderポストプロセス用に用意されたテンプレート
Compute ShaderGPUを計算目的で使用するためのもの(GPGPU)
Ray Tracing Shaderレイトレーシングを行うためのもの
マイケル
マイケル
今回は特に触れませんが、この辺りも後々触っていこうと思います!
エレキベア
エレキベア
ComputeShaderはよく聞くし触ってみたいクマね

簡単なシェーダープログラムの実装

マイケル
マイケル
前提知識を学んだところで、早速コードを見ていきましょう!

Unlit Shaderの作成

マイケル
マイケル
まずは Shader > Unlit Shader からシェーダーコードを作成します。
Unlitは陰影処理を行わずにそのまま出力するシンプルなシェーダーになります。
20221229 02↑Unlit Shaderを作成
マイケル
マイケル
今回は SimpleUnlitShaderという名前で作成しました。
作成後は下記のようにマテリアルでも選択できるようになっているはずです。
20221229 03↑作成したシェーダーが選択できるようになっている
マイケル
マイケル
このマテリアルをオブジェクトに設定すると、デフォルトでは下記のように真っ白に表示される状態になっています。
ひとまずこれで準備は完了です。
20221229 04
エレキベア
エレキベア
ここからコードを見ていくクマ〜〜〜

ShaderLabとTags

マイケル
マイケル
作成されたコードを見ると、下記のように見慣れない文法が出てきて初めは戸惑うと思います。
これはUnity特有のShaderLabという構文で、ここでプロパティやシェーダーの基本的な設定を行う形となります。
「CGPROGRAM」「ENDCG」で囲まれた部分が実際に実行されるプログラムで、こちらにCg/HLSLで実装していきます。
// シェーダー宣言
Shader "Unlit/SimpleUnlitShader"
{
    // プロパティ宣言
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    // シェーダーの実行単位
    // 実行条件に合うものを選んで実行する
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100
				
        // 実行されるシェーダープログラム
        Pass
        {
            CGPROGRAM
            // --- Cg/HLSLの実装(省略) ---
            ENDCG
        }
    }
}
↑UnityのシェーダーはShaderLab構文でラップされている
エレキベア
エレキベア
Unityで使用するために囲む必要があるクマね
マイケル
マイケル
それぞれの構文の意味は下記のようになっています。
一つ一つ見てみるとそこまで難しくはないですね。
Shaderシェーダー宣言シェーダーの種類と名前を定義する。
Propertiesプロパティ宣言シェーダーで設定できるプロパティを定義する。
SubShaderシェーダーの実行単位複数のSubShaderを定義でき、LOD等の実行条件から実行するSubShaderが選択、実行される。
Pass実行されるシェーダープログラム※左記そのままの意味

ShaderLab構文 | Unityマニュアル

マイケル
マイケル
少し複雑なのはSubShader内のTags指定です。
こちらはQueue(レンダリング順)ForceNoShaderCasting(シャドウの投影可否)といった設定を記述することができます。
公式マニュアルの他、下記の記事が分かりやすかったので引用させていただきます。

ShaderLab: SubShader 内のタグ | Unityマニュアル

SubShaderとPassの中で使用できるTagリスト | 渋谷ほととぎす通信

エレキベア
エレキベア
ここでもいろいろな設定ができるクマね
マイケル
マイケル
そしてTagsはSubShader内だけでなく、Pass内にも記述することができます。
こちらはLightMode(ライト設定)、PassFlags(パスに渡すデータのフラグ管理)といった設定があります。

ShaderLab: Pass 内のタグ | Unityマニュアル

エレキベア
エレキベア
こればっかりは触りながら覚えていくしかなさそうクマね

Cg/HLSLの実装

マイケル
マイケル
次は「CGPROGRAM」「ENDCG」で囲まれたシェーダープログラム部分を見てみましょう。
作成直後は下記のようになっていると思います。
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

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

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
↑作成直後のシェーダープログラム(Cg/HLSL部分)
マイケル
マイケル
デフォルトだとフォグの設定など不要な処理があって見にくいので、少し修正してコメントを付けてみました。
            // 各シェーダーと関数名との紐付け
            #pragma vertex vert
            #pragma fragment frag

            // Unityで用意された関数を使用するためinclude
            #include "UnityCG.cginc"

            // vertが受け取るデータ
            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            // vert->fragへ渡すデータ
            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            // テクスチャ情報
            sampler2D _MainTex;
            float4 _MainTex_ST;

            // 頂点シェーダー処理
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex); // オブジェクト座標->クリップ座標への変換
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);      // Scale、Tilingを考慮した変換(XX_STも使用)
                return o;
            }

            // フラグメントシェーダー処理
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv); // テクスチャのカラーを設定
                return col;
            }
↑不要な処理を削除してコメントを付与したもの
マイケル
マイケル
こうしてみるとだいぶ見やすくなったのではないでしょうか?
vert、flag関数がそれぞれ頂点シェーダー、フラグメントシェーダーの処理部分になっています。
エレキベア
エレキベア
Unity以外のシェーダープログラムも大体こんな感じクマね
セマンティクスの定義
マイケル
マイケル
structで定義された部分は、それぞれのシェーダーに渡すデータの定義になります。
「POSITION」「TEXCOORD0」といった部分はセマンティクスといって、データの意味を表すものになります。
            // vertが受け取るデータ
            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            // vert->fragへ渡すデータ
            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };
↑データにはそれぞれセマンティクスが定義されている

Semantics – Win32 apps | Microsoft Learn

エレキベア
エレキベア
頂点位置やテクスチャ座標、法線座標とかいろいろあるのクマね
頂点シェーダーの実装
マイケル
マイケル
頂点シェーダー部分では、受け取った座標をフラグメントシェーダーに渡すために変換しています。
頂点座標をクリップ座標に変換して、uv座標はScale、Tilingを考慮した変換を行なっています。
ここでUnity固有の関数がそれぞれ使用しているため、UnityCG.cgincのincludeが必要になってきます。
            // 頂点シェーダー処理
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex); // オブジェクト座標->クリップ座標への変換
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);      // Scale、Tilingを考慮した変換(XX_STも使用)
                return o;
            }
↑頂点シェーダーの実装
マイケル
マイケル
座標変換についてよく分からない方は下記記事をご参照ください!
最終的に描画するための2D座標に変換するためのプロセスで、UnityObjectToClipPosでも似たような変換を行なっていると思います。(おそらく)
エレキベア
エレキベア
(自信無さげクマ・・・)
フラグメントシェーダーの実装
マイケル
マイケル
フラグメントシェーダーでは下記のように最終的な色を返しています。
今回は受け取ったUV座標からテクスチャを参照して返しているだけですね
            // フラグメントシェーダー処理
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv); // テクスチャのカラーを設定
                return col;
            }
↑フラグメントシェーダー
エレキベア
エレキベア
陰影を付ける時はここでわちゃわちゃやるクマね

設定したカラーをそのまま出力する

マイケル
マイケル
イメージが分かったところで、「設定したカラーをそのまま出力する」シェーダーに改造してみましょう!
今回はSimpleColorShaderとして新たに作成し、下記のように実装してみました!
Shader "Unlit/SimpleColorShader"
{
    Properties
    {
        _Color ("Main Color", Color) = (0, 0, 1, 1)
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            fixed4 _Color;

            // 頂点座標のみ受け取り、クリップ位置のみ返す
            float4 vert (float4 vertex : POSITION) : SV_POSITION
            {
                return UnityObjectToClipPos(vertex);
            }

            // カラーのみ返す
            fixed4 frag () : SV_Target
            {
                return _Color;
            }

            ENDCG
        }
    }
}
マイケル
マイケル
各シェーダーに渡すデータはstructで定義されていましたが、このように引数や戻り値に直接記述することもできます。
極端に少ない場合はこちらの方が見やすいかもしれません。
エレキベア
エレキベア
だいぶシンプルで見やすくなったクマね
マイケル
マイケル
後はこのシェーダーをマテリアルに設定して表示を見てみると・・
20221229 08
20221229 05
マイケル
マイケル
このように単色で描画されることが確認できました!
エレキベア
エレキベア
第一ステップクリアクマ〜〜〜

Lambert拡散反射シェーダーの実装

マイケル
マイケル
それでは最後に応用として、Lambert拡散反射シェーダーを実装していきます!
エレキベア
エレキベア
いよいよちゃんとしたシェーダーの実装クマね

Lambert拡散反射とは

マイケル
マイケル
Lambert拡散反射とは、光のN次的な反射は考慮せずに、頂点に光源からの光が当たる量のみを考慮したモデルになります。
頂点からの法線をN、頂点から光源へ向かうベクトルをLとした場合、下記の計算で表すことができます。
ScreenShot 2021 10 16 0 28 45 01
ScreenShot 2021 10 16 0 28 54 01
エレキベア
エレキベア
いわゆる擬似的なライティングというやつクマね
マイケル
マイケル
簡単な計算で表せるから比較的計算負荷が軽いのも特徴だね
詳しくは下記の記事でも紹介していますので、こちらもよければご参照ください!
エレキベア
エレキベア
懐かしいクマ〜〜〜〜

シェーダープログラムの実装

マイケル
マイケル
そんなこんなで実装したシェーダープログラムは下記のようになります!
Shader "Unlit/SimpleLambertShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        // フォワードレンダリングパイプラインのベースパスであることを示す
        // ディレクショナルライト 方向: _WorldSpaceLightPos0
        // ディレクショナルライト カラー: _LightColor0
        Tags { "LightMode"="ForwardBase" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "UnityLightingCommon.cginc" // ライティング関数を使用するため

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

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

            sampler2D _MainTex;
            float4 _MainTex_ST;

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

            fixed4 frag (v2f i) : SV_Target
            {
                // 拡散反射光の計算
                const float3 l = normalize(_WorldSpaceLightPos0.xyz);
                const float3 n = normalize(i.normal);
                const float nl = max(0, dot(n, l));
                float3 diffuse = _LightColor0.xyz * nl;

                // 環境光を加算
                const half3 ambient = ShadeSH9(half4(i.normal, 1));
                diffuse.rgb += ambient;

                // 最終的なカラーに加算
                fixed4 col = tex2D(_MainTex, i.uv);
                col.xyz *= diffuse;
                return col;
            }
            ENDCG
        }
    }
}
エレキベア
エレキベア
これまでの話を踏まえるとだいぶ読めるようになってきたクマ〜〜
マイケル
マイケル
今回はライトの情報が必要になるため、TagsでLightModeをForwardBaseに指定して、UnityLightingCommon.cgincをincludeしています。
こうすることで最も優先度の高いライト情報を_WorldSpaceLightPos0、_LightColor0で受け取れるようになります。
Shader "Unlit/SimpleLambertShader"
{
・・・省略・・・
    SubShader
    {
        // フォワードレンダリングパイプラインのベースパスであることを示す
        // ディレクショナルライト 方向: _WorldSpaceLightPos0
        // ディレクショナルライト カラー: _LightColor0
        Tags { "LightMode"="ForwardBase" }
        LOD 100
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "UnityLightingCommon.cginc" // ライティング関数を使用するため
・・・省略・・・
            ENDCG
        }
    }
}
↑ライト情報を受け取るための設定
エレキベア
エレキベア
Unityのライト情報はこんな感じで受け取るクマね
マイケル
マイケル
なお、複数ライトを考慮する場合はForwardAddとしてパスを追加する必要があるようです。
こちらは下記の記事が分かりやすかったのでご参照ください!

【Unity】【シェーダ】Forward Renderingで複数のライトを取り扱う | LIGHT11

エレキベア
エレキベア
ライティングはまたUnity固有で覚えないといけないことがいろいろありそうクマね・・・
マイケル
マイケル
あとはこのライト情報と法線情報を使ってLambertの計算を行うのみ!
ディレクショナルライトの他、環境光もShadeSH9関数を使用することで考慮しています。
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.normal = mul(unity_ObjectToWorld, v.normal);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // 拡散反射光の計算
                const float3 l = normalize(_WorldSpaceLightPos0.xyz);
                const float3 n = normalize(i.normal);
                const float nl = max(0, dot(n, l));
                float3 diffuse = _LightColor0.xyz * nl;

                // 環境光を加算
                const half3 ambient = ShadeSH9(half4(i.normal, 1));
                diffuse.rgb += ambient;

                // 最終的なカラーに加算
                fixed4 col = tex2D(_MainTex, i.uv);
                col.xyz *= diffuse;
                return col;
            }
↑Lambert拡散反射の計算
エレキベア
エレキベア
綺麗な実装クマ〜〜〜
マイケル
マイケル
この状態で実行すると下記のように陰影も描画されていることが分かります!
ディレクショナルライトの向きを変えたり、環境光の色を変えることで陰影結果も変わることも合わせて確認できると思います。
20221229 06↑陰影が表示された!
01 lambert↑ライトの向きを変えると陰影も変わる
エレキベア
エレキベア
やったクマ〜〜〜

参考:サーフェイスシェーダーでの実装

マイケル
マイケル
最後におまけですが、サーフェイスシェーダーで同様の処理を実装した場合は下記のようになります。
Lambertのライティングはデフォルトで用意されており、#pragma surface surf Lambertの指定だけで完結してしまいます。。
Shader "Custom/SurfaceLambertShader"
{
    Properties
    {
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        CGPROGRAM

        #pragma surface surf Lambert
        sampler2D _MainTex;

        struct Input
        {
            float2 uv_MainTex;
        };

        fixed4 _Color;

        void surf (Input IN, inout SurfaceOutput o)
        {
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex);
            o.Albedo = c.rgb;
            o.Alpha = c.a;
        }

        ENDCG
    }
    FallBack "Diffuse"
}
↑サーフェイスシェーダーでのLambertライティング
20221229 07↑陰影も問題なさそう
エレキベア
エレキベア
Unity側にライティングを任せるとはこういうことクマか・・・
マイケル
マイケル
Lambertの他、Phong反射やPBRライティングも用意されているので、こちらも触ってみると色々面白いと思います!
書籍等でもサーフェイスシェーダを使って解説されていることもありますし・・・

参考:
【Unity】Surface Shaderの基本を総まとめ!難しい計算はUnity任せでサクッとシェーダ作成 | LIGHT11

エレキベア
エレキベア
今後使用する機会は減っても一度は触っておいた方がよさそうクマね

おわりに

マイケル
マイケル
というわけで今回はUnityシェーダーの基礎についてでした!
どうだったかな??
エレキベア
エレキベア
Unity固有で覚えないといけないこともいろいろあったクマが、
まあ何となく使える気がしたクマ〜〜
マイケル
マイケル
いきなりUnityシェーダーを実装しようとすると、Unity固有の知識と一般的なシェーダープログラムの知識がごっちゃになってしまいそうだね・・・
シェーダーを覚えるといろいろ面白いと思うから、その辺りも整理しながら触っていこう!!
マイケル
マイケル
それでは今日はこの辺で!
アデューー!!
エレキベア
エレキベア
クマ〜〜〜〜

【Unity】Unityシェーダーの基礎とLambert拡散反射の実装【シェーダー】〜完〜

コメント