【シェーダー勉強③】ShaderLabについて

Unity シェーダー

はじめに

今回は実際にUnityで記載するシェーダー言語「ShaderLab」について、まとめていきたいと思います。
Unityでは「サーフェス」と「頂点/フラグメント」がありますが、今回は「頂点/フラグメント」しか扱いません。
各シェーダーの説明などは以前の記事などを参照ください。
【シェーダー勉強②】頂点/フラグメントシェーダーとは
また今回の記事では各項目の概要だけをまとめて、詳細に関しては項目ごとに別記事にまとめるなどしたいと思います。

ShaderLabとは

「ShaderLab」はUnityでシェーダーを記述するために設計された独自のもの。
とはいっても実際の処理部分はCgやHLSLなどの既存のシェーダー言語を踏襲されたものとなっています。
・「Cg」はNVIDIAが開発したシェーダー言語
・「HLSL」はマイクロソフトが開発したDirectXのシェーディング言語

各構成について

では実際のシェーダーの構成について見ていきましょう。
※全部まとめると、とんでもない量になるため、今回はUnlit Shaderで使われているものをベースにまとめていきます。
記載しているものの他にもまだまだ構成要素はあります。

「Projectウィンドウ」で「右クリック」>「Create」>「Shader」>「Unlit Shader」でシェーダーを作成します。

f:id:ArtAwA:20200816153528p:plain

ただし、初回の説明としてはノイズになるフォグの部分に関してはカットしたいと思います。
(処理の中でFOGとかfogとか書かれている部分)
カットして、まとめてみたものが以下となります。
こちらのシェーダーでは設定したテクスチャをそのまま表示するものとなります。

「NewUnlitShader.shader」の全文はここをクリック
Shader "Unlit/NewUnlitShader"
{
    Properties {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 100
        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;
            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 {
                fixed4 col = tex2D(_MainTex, i.uv);
                return col;
            }
            ENDCG
        }
    }
}

それでは上から順に1つづつ見ていきましょう。

Shader

Shaderの部分だけをまとめると以下のような記述となります。

Shader "Unlit/NewUnlitShader" {
    // 内容
}

Shaderはシェーダーの大本部分で1つだけ必ず記載します。
こちらではシェーダーの「名前」を設定します。
この名前はマテリアルに表示される名前で、「/」を付けることによって選択時にグルーピングされます。
この中に更に後述する「Properties」や「SubShader」などを記述していきます。

f:id:ArtAwA:20200816203444p:plain

Properties

Properties {
    // テクスチャを定義
    _MainTex ("Texture", 2D) = "white" {}
}

PropertiesではマテリアルのInspector上で設定する「カラー」「テクスチャ」「数値」などのプロパティを定義します。
今回の例ではテクスチャを定義しています。

f:id:ArtAwA:20200816203506p:plain

SubShader

SubShader {
    // 内容
}

SubShaderではシェーダーの主な処理を記述します。
複数定義することも可能ですが、基本的には1つしか記述しません。
複数のハードウェアで動作するようにする場合、ハード依存の問題などを解消する場合などに複数記述することがあります。

Tags

// "タグ名"="値"で定義
Tags { "RenderType"="Opaque" }

Tagsではいつどのようにしてレンダリングエンジンでレンダリングするかを指定します。
Tagsは“タグ名”=”値”で定義し、「RenderType」や「Queue」などの定義が出来ます。
今回の「RenderType」はシェーダーを定義済みのいくつかのグループに分類するもので、値の「Opaque」は不透明のグループに分類しています。
この分類は不透明のオブジェクトを全てワイヤーフレーム表示にしたいみたいな際に用いられます。
特に必要しない場合は定義しなくても大丈夫です。

LOD

// 100は「閾値」
LOD 100

LODはLOD(Level of Detail)に応じてSubShaderを使い分けるときに使用します。
LODの閾値以下の場合にそのSubShaderが適用されます。
LOD自体については以下を参照。

https://entry.cgworld.jp/terms/LOD.html
カメラからの距離に応じてモデルのディテールを切り替え、シーンの計算負荷を軽減する方法。主にゲームのリアルタイムレンダリングで使用される。遠景に位置するモデルのポリゴン数やテクスチャ容量を減らすことで、シーンの見た目を大きく損なうことなく、描画を高速化できる。

近くだったらノーマルマップで凹凸の表現をしたいけど、遠くだったら要らないみたいな感じで使えます。
ここも特に必要ない場合は定義しなくて大丈夫です。

Pass

Pass {
    CGPROGRAM
    // シェーダーの挙動
    ENDCG
}

Passは実際のシェーダーの挙動を記述する箇所です。
Passは複数記述することが可能で、アウトラインを描画したりする場合などは複数に分かれたりします。

ゲーム開発の際によく聞く「DrawCall」ですが、シェーダーのPass毎にレンダリングされる(Draw Call)ため、Passが多くなるほどDrawCallが上がり、描画負荷が上がります。

Passの処理について

Passの部分は記述が長いので、更に分解してまとめていきます。
自分が勉強した際は、Passの中身については前回記述しているようなレンダリングパイプラインを確認しながら、処理の流れが追ってみると理解がしやすかったです。

CGPROGRAM~END

CGPROGRAM
// シェーダーの挙動
END

CGPROGRAM ~ END の間に実際のシェーダーの挙動を記載していきます。

関数宣言

#pragma vertex vert
#pragma fragment frag

ここでは「vert」関数が「頂点シェーダー」、「frag」関数が「フラグメントシェーダー」だよという定義をしています。
定義は自分で行いますので、名前自体は自由に変更可能です。

#pragma vertex "関数名"
#pragma fragment "関数名"

これを定義することによって下の方に記載されている「vert」「frag」の処理がそれぞれのシェーダーとして適用されます。

// 頂点シェーダー
v2f vert (appdata v) {
    // 処理
}
// フラグメントシェーダー
fixed4 frag (v2f i) : SV_Target {
    // 処理
}

定義済み関数の読み込み

#include "UnityCG.cginc"

こちらではUnityが用意している便利関数を使えるように読み込みを行っています。
以下の関数などがUnityCG.cgincに定義されている関数を使用している部分です。

o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);

「UnityCG.cginc」のファイル自体は中身を見ることが出来て、以下に格納されています。

"Unityをインストールした場所"\Editor\Data\CGIncludes
UnityCG.cginc

構造体の宣言

// 入力
struct appdata {
    float4 vertex : POSITION;
    float2 uv : TEXCOORD0;
};
// 出力
struct v2f {
    float2 uv : TEXCOORD0;
    float4 vertex : SV_POSITION;
};

structの部分はC#などのプログラミング言語と同様で構造体の宣言をしています。
ここで定義している構造体は「頂点シェーダーの入力」と「頂点シェーダーの出力」に分かれており、「vert」「frag」関数で使用されています。
どのように使用しているかは後述の各関数の箇所でまとめます。

シェーダープログラムではGPUにその変数がどういったものであるかを教えてあげる必要があり、それは「セマンティクス」というもので行われています。
その定義をどこでやっているかというと「:」の以降の部分で、以下だと「POSITION」というセマンティクス名で行われています。

float4 vertex : POSITION;

定義できるセマンティクス名は決まっており、入力と出力で分かれています。
今回のシェーダーでは使われている「POSITION」は「頂点座標」、「TEXCOORD0」は「テクスチャUV座標」を設定しています。
入力セマンティクスではその他に「NORMAL(法線ベクトル)」「COLOR(頂点カラー)」なども設定可能です。

プロパティの取得

sampler2D _MainTex;
float4 _MainTex_ST;

Propertiesと同じ名前で宣言することによって、マテリアルのInspectorで設定したプロパティを取得することができます。
ここではPropertiesで宣言した「_MainTex」のテクスチャが変数に代入され、テクスチャのプロパティの場合は「”プロパティ名”_ST」で宣言することによって、「Tiling」と「Offset」の値も取得することが出来ます。

頂点シェーダー

// 頂点シェーダー
v2f vert (appdata v) {
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    return o;
}

関数定義(#pragma vertex vert)で定義した頂点シェーダーの記述が行われています。
頂点シェーダーでは前回記載していたような全ての頂点毎「モデルのローカル座標をスクリーン座標に変換」を行っています。
それでは上から順にまた見ていきましょう。

v2f vert (appdata v)

ここで構造体定義したappdataを引数として入力v2fを返り値として出力しています。

v2f o;

ここでは計算した値を返す構造体を宣言しているだけです。

o.vertex = UnityObjectToClipPos(v.vertex);

「UnityObjectToClipPos」はUnityCG.cgincに定義されている関数で「オブジェクト空間からカメラのクリップ空間へ点を変換」を行っています。
オブジェクト空間=モデルのローカル空間で、カメラのクリップ空間からスクリーン空間への変換はハードウェア側で行われるため、シェーダー側で記述することは基本ありません。
引数のv.vertexはappdataの構造体で定義していた頂点座標(vertex : POSITION)が入っています。

o.uv = TRANSFORM_TEX(v.uv, _MainTex);

「TRANSFORM_TEX」もUnityCG.cgincの関数で、テクスチャのタイリング、オフセット値を適用した状態に変換をかけてくれています。
「v.uv」でテクスチャのUV座標(uv : TEXCOORD0)を「_MainTex」でプロパティで定義したテクスチャを引数として渡してあげています。

return o;

最後に計算した値を返してあげています。
ここで返した値がラスタライズされ、次のフラグメントシェーダーに渡ります。

フラグメントシェーダー

fixed4 frag (v2f i) : SV_Target
{
    fixed4 col = tex2D(_MainTex, i.uv);
    return col;
}

関数宣言(#pragma fragment frag)で定義したフラグメントシェーダーの記述が行われています。
フラグメントシェーダーでは全てのドット毎「様々な情報を元に色を決定」しています。

fixed4 frag (v2f i) : SV_Target

こちらでは引数の「v2f i」で頂点シェーダーで変換された値が入力され、
返り値の「fied4」と最後の「: SV_Target」は構造体宣言でやったセマンティクスで「fixed4の型でカラー情報として返してあげるよ」という意味です。

fixed4 col = tex2D(_MainTex, i.uv);

ここで実際の色が情報を元に決められており、tex2D関数でi.uv(UV座標)から_MainTex(テクスチャ)のピクセルの色を計算して返してます。

おわりに

とりあえずざっくりとですがUnlitShaderを元にShaderLabについてまとめてみました。
まだまだ書けていないことが多いですが、既存のコードを解析することは結構勉強になりました。

それでは今回はこの辺で
(´・ω・`*)ノシ

参考文献

https://docs.unity3d.com/ja/2019.4/Manual/SL-Shader.html
https://docs.unity3d.com/ja/2019.4/Manual/SL-SubShaderTags.html
https://docs.unity3d.com/ja/2019.4/Manual/SL-ShaderReplacement.html
https://docs.unity3d.com/ja/2018.4/Manual/SL-Blend.html
https://nn-hokuson.hatenablog.com/entry/2018/02/15/140037
https://docs.unity3d.com/ja/2018.4/Manual/SL-ShaderLOD.html
https://docs.unity3d.com/ja/2019.4/Manual/SL-ShaderSemantics.html

コメント

タイトルとURLをコピーしました