LoginSignup
29

More than 5 years have passed since last update.

UnityでGANTZ風シェーダー解説

Last updated at Posted at 2018-12-22

はじめに

Unityシェーダーの基本をGANTZの転送演出風のシェーダーを作りながら学ぶ記事です。
下記のツイートにあるようなモノを作成します。

この転送シェーダーはシェーダーの基本的な要素が意外と詰まってるので勉強し始めの方には個人的におすすめです。
勉強し始めて何となく雰囲気分かってきたから具体例を知りたい人が読むとちょうどいいと思います。

前知識

今回扱うのは頂点シェーダーフラグメントシェーダーです。
この二つは最も基本的でほぼ必要不可欠なものです。
頂点シェーダー→フラグメントシェーダーの順実行されます。

頂点シェーダー

頂点シェーダーはオブジェクトの頂点の数だけ実行され、主に頂点の座標変換を担います。
オブジェクトが持つ頂点の座標やUV座標などの情報を使って様々な処理をします。

ラスタライズ処理

頂点シェーダーとフラグメントシェーダーの間にはラスタライズという処理が入ります。ラスタライズ処理は自動で行われる処理なのでコードを書くことはないです。この処理によってオブジェクトが映し出されるピクセルが定まります。
オブジェクトが何処にどれくらいの大きさで配置されていてカメラが何処にどれくらいの視野角で配置されているのかなどの情報と、映し出す画をどれくらいの解像度で出力するかという情報から、オブジェクトが映し出されるピクセル群が特定されるイメージです。

フラグメントシェーダー

フラグメントシェーダーはオブジェクトが映し出されるピクセルの数だけ実行され何色を出力するかを決定する役割を担います。
今回の転送シェーダーはUnlitシェーダーと言われるシェーダーに分類されます。シーンにあるライトの情報を元にライティング処理をしないシェーダーの事をUn + lightingでUnlitシェーダーと言います。ライティング処理をすればシーンに合わせて整合性のある見た目を作ることができますが、今回は割愛しUnlitな実装をします。

補足

シェーダーは並列的に処理されます。どういうことかというと、頂点シェーダーが実行される際はすべての頂点に対して同時に頂点シェーダーが実行され、フラグメントシェーダーでも同様に全ピクセルに対して同時に処理が走ります。

ベース実装

とりあえず完成版のコードを貼っておきます。

Shader "Transition/Plain"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Threshold("Threshold", Range(-1,1)) = 1
        _LazerTex("LazerTexture", 2D) = "white"{}
        _LazerHeight("LazerHeight", Range(0,1)) = 0.1
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent"}
        LOD 100
        Cull off
        Blend SrcAlpha OneMinusSrcAlpha 

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

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

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

            sampler2D _MainTex;
            float _Threshold;
            sampler2D _LazerTex;
            float _LazerHeight;

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

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

                float diff = _Threshold - i.oPos.y;
                clip(diff);
                if(diff < _LazerHeight){
                    fixed4 lazerCol = tex2D(_LazerTex, diff / _LazerHeight);
                    col.rgb = col.rgb * (1 - lazerCol.a) + lazerCol.rgb * col.a;
                }

                return col;
            }
            ENDCG
        }
    }
}

実行結果はこちらです。
GANTZShader.gif
頭から解説していきます。

プロパティ

Properties
{
    _MainTex ("Texture", 2D) = "white" {}
    _Threshold("Threshold", Range(-1,1)) = 1
    _LazerTex("LazerTexture", 2D) = "white"{}
    _LazerHeight("LazerHeight", Range(0,1)) = 0.1
}

_MainTexはモデルのテクスチャです。
_Thresholdはどれくらいの高さより高い位置を透明にするかを決定する値です。
LzerTexは透明部分と不透明部分の境目のレーザーのテクスチャです。
LazerHeightはレーザーの太さを決める値です。
レーザーのテクスチャはこちらです。お好きに使って下さい。
lazer7_7.png

Tags,Cull,Blend

Tags { "RenderType"="Transparent" "Queue"="Transparent"}
LOD 100
Cull off
Blend SrcAlpha OneMinusSrcAlpha 

Tagsの記述は透明・半透明なシェーダーを書く時のおまじないみたいなものです。透明なオブジェクトを描画することを宣言して内部的に良しなにやってくれます。

LODは今回の実装には特には関わらないので割愛です。あまり気にする機会も少ないと思います。

Blendは透明度のある部分の色の扱いを決めています。透明・半透明な部分は透けて見える向こう側の色と混ざって色が変わる訳ですがこの混ぜ方を定義しています。半透明なシェーダーを作る際は大抵この記述をしますが、詳細は割愛します。

Cullの記述はカリングについての処理を決定します。デフォルト(Cullの記述をしない場合)ではCull backと記述した場合と同じです。
Cull backと記述すればはポリゴンの裏側は描画しない様になります。
これに対して、ポリゴンの表面を描画しないようにしたい場合はCull frontと書けばOKです。
そして、Cull offと記述すると表も裏も描画されます。

今回Cull offしたのは透明になった部分からポリゴンの裏面を覗けてしまう為、裏面の描画もするようにしないと現象に対して整合性が保てないからです。

Cull backな状態がこちら
GANTZShaderCullBack.png

Cull offな状態がこちら
GANTZShader_CullOff.png

構造体

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

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

appdata構造体は頂点座標(これはどんな場合でも必須)とテクスチャ用のUV座標が必要なので、POSITIONセマンティクスをバインドした変数と、TEXCOORD0セマンティクスをバインドした変数を定義しています。
この構造体は頂点シェーダーの引数として入力されますが、各変数には自動的に頂点座標、UV座標が入った状態になっています。

v2f構造体は、クリップ座標とUV座標とオブジェクト座標をフラグメントシェーダーで扱いたいので、SV_POSITIONセマンティクスをバインドした変数とTEXCOORD0セマンティクスをバインドした変数とTEXCOORD1セマンティクスをバインドした変数を定義しています。TEXCOORD0TEXCOORD1の部分はなんて書いても動くんですが、習慣として"TEXCOORD"と書きます。
oPos変数は"object Space position"の略です。

ちなみに、"vertex to fragment"の略で"v2f"です。

変数定義

sampler2D _MainTex;
float _Threshold;
sampler2D _LazerTex;
float _LazerHeight;

この部分はPropertiesで定義したプロパティをシェーダー内で扱えるようにする為の記述です。
何故、Propertiesで書いたのにまた書くのかと思う方がいると思いますが、Unityシェーダーのコードは実はシェーダー言語が記述されている部分はCGPROGRAMからENDCGまで書かれている記述でPropertiesなどの部分はUnity独自?のShaderLabという言語で分かれています。なので内部的に変数を紐づける為に必要です。

頂点シェーダー

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

頂点シェーダーでは、頂点座標をオブジェクト座標→クリップ座標の変換をした座標をv2f構造体のvertex変数に代入し、頂点のオブジェクト座標をそのままoPos変数に代入し、uv座標をもそのままuv変数に代入しています。
フラグメントシェーダーの引数として入力される構造体(今回の場合はv2f構造体)のSV_POSITIONセマンティクスがバインドされた変数にはクリップ座標系上の頂点座標が入っている必要があります。座標変換は、オブジェクト座標→ワールド座標→ビュー座標→クリップ座標の順に行われますが、これをまとめてUnityObjectToClipPos()関数で処理できます。
ジオメトリシェーダーなどを扱う場合などで頂点シェーダーではワールド座標まで変換して、ジオメトリーシェーダーで何かしらの処理をした後にジオメトリーシェーダー内でクリップ座標に変換してフラグメントシェーダーに渡したりします。

フラグメントシェーダー

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

    float diff = _Threshold - i.oPos.y;
    clip(diff);
    if(diff < _LazerHeight){
        fixed4 lazerCol = tex2D(_LazerTex, diff / _LazerHeight);
        col.rgb = col.rgb * (1 - lazerCol.a) + lazerCol.rgb * col.a;
    }

    return col;
}

まずcol変数にモデルのテクスチャからサンプリングした色を代入します。

フラグメントシェーダーに入力されたオブジェクト座標系でのY座標が_Thresholdより大きい場合に透明にします。
clip()関数は引数に0未満の値が入るとそのピクセルの描画を放棄する(=透明にする)関数です。_Thresholdi.oPos.yの差の値を利用して、透明化しています。

次に、不透明な部分の色付けの処理です。
先ほど算出したdiff_LazerHeightを比較しif分岐します。_LazerHeightはレーザーの幅の値なので境目からの距離がこの値より小さければレーザーのテクスチャを重ねます。
レーザーのテクスチャから色をサンプリングしてlazerColに代入します。第2引数にfloat2ではなくfloatを渡していますが、これはfloat2(diff / _LazerHeight, diff / _LazerHeight)と同等です。レーザーのテクスチャは横に引き伸ばしたような画像なのでx座標は特に意味はないです。Y座標は、diffが0の時に0、diff_LazerHeightと等しい時に1になるようにしたいので、diff_LazerHeightで除算しています。
レーザーのテクスチャはY座標が0.5付近の時は不透明ですが、0と1に近づくにつれて透明になるようにしてあります。なので下地になる_MainTexの色とレーザーの色を適当にブレンド(混ぜる)ます。レーザーのテクスチャの透明度が低い(不透明)場合ほどレーザーのテクスチャの色が_MainTexより優先されるようにブレンドしています。

発展実装

本当はオブジェクト座標を使わずに体に沿ってエフェクトがかかる実装も解説しようと思ったんですが、ちょっと時間ないので一旦放置します。
暫定的に言葉で解説します。
モデルに2つ目のUV座標を持たせます。このUV座標はモデルを正面から平行投影で見たシルエットの状態でUV展開したものです。この時腕などのUV座標を頑張って調整してモデルが腕を真っ直ぐ下におろして待っているような形にします。ここまではBlenderなどのDCCツールでやります。
シェーダーでは、appdata構造体にTEXCOORD1をバインドしたfloat2の変数を定義します。この変数に2つ目のUV座標が入ってくるのでそのままフラグメントシェーダーに渡して、このUV座標のY座標を閾値と比較して透明/不透明の分岐処理をすれば完成です。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29