コガネブログ

平日更新を目標に Unity や C#、Visual Studio、ReSharper などのゲーム開発アレコレを書いていきます

【Unity】シェーダ入門

はじめに

この記事は、上記のページで公開されている
Unity のシェーダのチュートリアルを翻訳したものになります
主に、シェーダファイルの作成方法の紹介や構文の解説を行っていきます

目次

シーンの設定

まず、シェーダを書く前に、シーンを設定していきます

f:id:baba_s:20171227110544p:plain

Unity で新しいプロジェクトを作成して Cube をシーンに追加します

f:id:baba_s:20171227110640p:plain

次に Project ビューの「Create」ボタンを押して、
「Shader>Unlit Shader」を選択することで、新しいシェーダを作成します

f:id:baba_s:20171227110653p:plain

作成したシェーダのファイル名は「Tutorial_Shader」にします

f:id:baba_s:20171227110712p:plain

作成したシェーダを右クリックして「Create>Material」を選択することで、
新しいマテリアルを作成します

f:id:baba_s:20171227110807p:plain

最後に、作成したマテリアルを Cube にドラッグして適用します

f:id:baba_s:20171227110827p:plain

すると、Cube から影や陰影が無くなり、真っ白になりました
これで、シェーダを作成する準備が完了です

シェーダの構造

シェーダファイルの準備

f:id:baba_s:20171227111056p:plain

「Tutorial_Shader」を Visual Studio や MonoDevelop などのエディタで開きます

f:id:baba_s:20171227111356p:plain

「Tutorial_Shader」には最初からシェーダのコードが書かれていることがわかります
このコードをすべて削除して、シェーダファイルをまっさらにします

そして、まず下記のコードを追加します

Shader "Unlit/Tutorial_Shader" {
}

これは、シェーダの場所を指定するだけのコードです。例えば、

Shader "A/B/C/D/E_Shader" {
}

このように記載した場合

f:id:baba_s:20171227111917p:plain

マテリアルのシェーダをこのように指定することになります

f:id:baba_s:20171227112031p:plain

シェーダを保存して Unity に戻ると Cube がピンクになっていることがわかります
作成したシェーダに問題がある場合、このように表示されます
今はまだ、シェーダの場所を指定しただけで、
シェーダのプログラムを記述していないのでこのように表示されます

シェーダの構造

次に、シェーダの構造を簡単に解説していきます

Shader "Unlit/Tutorial_Shader" {
    Properties {
        // ...
    }
}

Properties ブロックは Unity からパラメータを受け取る場所です

Shader "Unlit/Tutorial_Shader" {
    Properties {
    }

    SubShader {
        // ...
    }
}

Properties ブロックの下には SubShader ブロックが存在します
すべてのシェーダには SubShader が1つ以上存在します
複数のプラットフォーム向けにシェーダを作成する場合、
複数の SubShader を追加すると便利です
例えば、PC では高い品質の、モバイルでは低い品質の SubShader を作成したりします

Shader "Unlit/Tutorial_Shader" {
    Properties {
    }

    SubShader {
        Pass {
            // ...
        }
    }
}

SubShader ブロック内には Pass ブロックが存在します
各 SubShader には少なくとも1回の Pass が存在します
これは、実際にオブジェクトがレンダリングされる場所です
エフェクトによっては複数の Pass が必要になることがあります

Shader "Unlit/Tutorial_Shader" {
    Properties {
    }

    SubShader {
        Pass {
            CGPROGRAM
                // ...
            ENDCG
        }
    }
}

Pass ブロック内にはレンダリングコードブロックがあります
CGPROGRAM と ENDCGの中に、実際のシェーダコードを記述していきます

CGPROGRAM
    #pragma vertex vertexFunction
    #pragma fragment fragmentFunction
ENDCG

CGPROGRAM と ENDCGの中で、
頂点シェーダとフラグメントシェーダで使用する関数の名前を指定します
また、これらの関数も同様に定義します

CGPROGRAM
    #pragma vertex vertexFunction
    #pragma fragment fragmentFunction

    void vertexFunction () {
            // ...
    }

    void fragmentFunction () {
            // ...
    }
ENDCG

さらに、シェーダを作成する上で必要になるヘルパー関数が定義された
「UnityCG.inc」を読み込むために include 構文を追加します

CGPROGRAM
    #pragma vertex vertexFunction
    #pragma fragment fragmentFunction

    #include "UnityCG.cginc"
    
    void vertexFunction () {
    }

    void fragmentFunction () {
    }
ENDCG

また、Unity からシェーダを適用するモデルのデータを受け取るために
appdata というデータ構造を用意し、
頂点シェーダ用の関数に appdata を受け取る引数を追加します

CGPROGRAM
    #pragma vertex vertexFunction
    #pragma fragment fragmentFunction

    #include "UnityCG.cginc"

    struct appdata {
    };

    void vertexFunction (appdata IN) {
    }

    void fragmentFunction () {
    }
ENDCG

例えば、シェーダを作成する時に、
モデルの頂点座標と UV 座標が必要になる場合は
次のように appdata を定義します

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

また、頂点シェーダの関数からフラグメントシェーダの関数にデータを渡す必要があるので
フラグメントの頂点を表す v2f というデータ構造を作成します
さらに、頂点シェーダ用の関数で v2f を返すようにします

CGPROGRAM
    #pragma vertex vertexFunction
    #pragma fragment fragmentFunction

    #include "UnityCG.cginc"

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

    struct v2f {
    };

    v2f vertexFunction (appdata IN) {
        v2f OUT;

        return OUT;
    }

    void fragmentFunction () {
    }
ENDCG

appdata と同じように、v2f でも頂点シェーダの関数から
フラグメントシェーダの関数に渡すデータを定義できます

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

SV_POSITION と POSITION の違いについて簡単に説明すると、
SVは「システム値」を表しており、レンダリングのために変換された頂点座標になります

これで基本的なシェーダ構文の準備が整ったので、
最後にフラグメントシェーダの関数を編集します
まず、v2f 構造体を受け取って、fixed4 値を返すように修正します

fixed4 fragmentFunction (v2f IN) {
}

フラグメントシェーダの関数が返すのは(A, R, G, B)値で表される色になります
次に、SV_TARGET をフラグメントシェーダの関数に追加します

fixed4 fragmentFunction (v2f IN) : SV_TARGET {
}

これは、Unity に fixed4 の色を渡すことを表しています
以上で、頂点シェーダとフラグメントシェーダをコーディングする準備ができました
これまでに作成したシェーダのスケルトンは下記のようになります

Shader "Unlit/Tutorial_Shader" {
    Properties {
        
    }

    SubShader {
        Pass {
            CGPROGRAM
                #pragma vertex vertexFunction
                #pragma fragment fragmentFunction

                #include "UnityCG.cginc"

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

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

                v2f vertexFunction (appdata IN) {
                    v2f OUT;

                    return OUT;
                }

                fixed4 fragmentFunction (v2f IN) : SV_TARGET {

                }
            ENDCG
        }
    }
}

基本的なシェーダの作成

緑色で表示する

最初に、頂点シェーダの関数からフラグメントシェーダの関数に
頂点座標の情報を渡します

v2f vertexFunction (appdata IN) {
    v2f OUT;
    OUT.position = UnityObjectToClipPos(IN.vertex);
    return OUT;
}

UnityObjectToClipPos は、オブジェクト空間で表される頂点座標を、
カメラのクリップ空間に変換するものです
OUT.position に値を設定することで、
フラグメントシェーダの関数に頂点座標の情報を渡します

次に、フラグメントシェーダの関数で、緑色を返すようにします

fixed4 fragmentFunction (v2f IN) : SV_TARGET {
    return fixed4(0, 1, 0, 1); //(R, G, B, A)
}

これで、シェーダを保存して Unity に戻ると
Cube が緑色で表示されていることがわかります

f:id:baba_s:20171227120001p:plain

好きな色で表示する

これで、シェーダで色を付ける方法がわかりましたが
現在はシェーダのコードに色の情報が書き込まれてしまっているので
Unity で色を設定できるように変更していきます

まず、シェーダの Properties を下記のように編集します

Properties {
    _Colour ("Totally Rad Colour!", Color) = (1, 1, 1, 1)
}

ここでは _Colour という名前で色情報を定義しています
これは、Unity 上で「Totally Rad Color」という項目で表示されます
デフォルトは白色です

f:id:baba_s:20171227121721p:plain

シェーダを保存して Unity に戻り、マテリアルを選択するとこのように表示されます
この色情報をフラグメントシェーダの関数で使用するために
CGPROGRAM 内で定義する必要があります

CGPROGRAM
    #pragma vertex vertexFunction
    #pragma fragment fragmentFunction

    #include "UnityCG.cginc"

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

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

    // ★追加
    float4 _Colour;

    v2f vertexFunction (appdata IN) {
        v2f OUT;
        OUT.position = UnityObjectToClipPos(IN.vertex);
        return OUT;
    }

    fixed4 fragmentFunction (v2f IN) : SV_TARGET {
        return fixed4(0, 1, 0, 1);
    }
ENDCG

定義は CGPROGRAM のトップスコープ内のどこに記載しても問題ありません
これで、Colour 値をフラグメントシェーダの関数で使用できるようになりました
緑色を返すのではなく
Colour 値を返すように変更します

fixed4 fragmentFunction (v2f IN) : SV_TARGET {
    return _Colour;
}

シェーダを保存して Unity に戻り、マテリアルの色情報を変更すると
Cube の色も変化することがわかります

f:id:baba_s:20171227122209p:plain

好きなテクスチャを表示する

これまでで、Unity 上で色を変更できるようになったので
次は、テクスチャを設定できるようにしていきます
まず、Properties にテクスチャ情報を追加します

Properties {
    _Colour ("Colour", Color) = (1, 1, 1, 1)
    _MainTexture ("Main Texture", 2D) = "white" {}
}

そして、これをフラグメントシェーダの関数で使用するために
CGPROGRAM 内で定義します

float4 _Colour;
sampler2D _MainTexture;

次に、頂点シェーダの関数内でモデルから UV 座標を取得して
フラグメントシェーダの関数に渡すようにします

v2f vertexFunction (appdata IN) {
    v2f OUT;
    OUT.position = UnityObjectToClipPos(IN.vertex);
    OUT.uv = IN.uv;
    return OUT;
}

そして、フラグメントシェーダの関数でテクスチャの色を使用するために
tex2D という機能を使用します

fixed4 fragmentFunction (v2f IN) : SV_TARGET {
    return tex2D(_MainTexture, IN.uv);
}

これでシェーダを保存して Unity に戻ると
マテリアルでテクスチャが設定できるようになっていることがわかります

f:id:baba_s:20171227122738p:plain

そして、適当なテクスチャを設定すると、Cube に反映されます

f:id:baba_s:20180107143837p:plain

ここまでで作成したシェーダは下記のとおりになります

Shader "Unlit/Tutorial_Shader" {
    Properties {
        _Colour ("Colour", Color) = (1, 1, 1, 1)
        _MainTexture ("Main Texture", 2D) = "white" {}
    }
    SubShader {
        Pass {
            CGPROGRAM
                #pragma vertex vertexFunction
                #pragma fragment fragmentFunction

                #include "UnityCG.cginc"

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

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

                float4 _Colour;
                sampler2D _MainTexture;

                v2f vertexFunction (appdata IN) {
                    v2f OUT;
                    OUT.position = UnityObjectToClipPos(IN.vertex);
                    OUT.uv = IN.uv;
                    return OUT;
                }

                fixed4 fragmentFunction (v2f IN) : SV_TARGET {
                    return tex2D(_MainTexture, IN.uv);
                }
            ENDCG
        }
    }
}

シェーダで遊ぶ

まず、Properties を下記のように編集します

Properties {
    _Colour ("Colour", Color) = (1, 1, 1, 1)
    _MainTexture ("Main Texture", 2D) = "white" {}
    _DissolveTexture ("Dissolve Texture", 2D) = "white" {}
    _DissolveCutoff ("Dissolve Cutoff", Range(0, 1)) = 1
}

_DissolveCutoff の値は Range という機能で ( 0, 1 ) の範囲で設定されています
このように記述すると、Unity 上でスライダーを使用して値を設定できるようになります
次は、これらの定義を CGPROGRAM に追加します

float4 _Colour;
sampler2D _MainTexture;
sampler2D _DissolveTexture;
float _DissolveCutoff;

これで、フラグメントシェーダの関数で
ディゾルブテクスチャをサンプリングできるようになります

fixed4 fragmentFunction (v2f IN) : SV_TARGET {
    float4 textureColour = tex2D(_MainTexture, IN.uv);
    float4 dissolveColour = tex2D(_DissolveTexture, IN.uv);
    clip(dissolveColour.rgb - _DissolveCutoff);
    return textureColour;
}

clip 関数は、指定された値が 0 より小さい場合、ピクセルを破棄します
これでシェーダを保存して Unity に戻り、

f:id:baba_s:20180107144222p:plain

マテリアルの Inspector で「Dissolve Texture」にノイズテクスチャに設定し、
「Dissolve Cutoff」スライダを動かすと、次のような見た目になります

f:id:baba_s:20180107143731g:plain

ここまでで作成したシェーダは下記のとおりになります

Shader "Unlit/Tutorial_Shader" {
    Properties {
        _Colour ("Colour", Color) = (1, 1, 1, 1)
        _MainTexture ("Main Texture", 2D) = "white" {}
        _DissolveTexture ("Dissolve Texture", 2D) = "white" {}
        _DissolveCutoff ("Dissolve Cutoff", Range(0, 1)) = 1
    }
    SubShader {
        Pass {
            CGPROGRAM
                #pragma vertex vertexFunction
                #pragma fragment fragmentFunction

                #include "UnityCG.cginc"

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

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

                float4 _Colour;
                sampler2D _MainTexture;
                sampler2D _DissolveTexture;
                float _DissolveCutoff;

                v2f vertexFunction (appdata IN) {
                    v2f OUT;
                    OUT.position = UnityObjectToClipPos(IN.vertex);
                    OUT.uv = IN.uv;
                    return OUT;
                }

                fixed4 fragmentFunction (v2f IN) : SV_TARGET {
                    float4 textureColour = tex2D(_MainTexture, IN.uv);
                    float4 dissolveColour = tex2D(_DissolveTexture, IN.uv);
                    clip(dissolveColour.rgb - _DissolveCutoff);
                    return textureColour;
                }
            ENDCG
        }
    }
}

最後に

Unity におけるシェーダをより深く知りたい場合は
下記の公式ドキュメントが参考になります

関連記事