#はじめに
Unityシェーダーの基本をGANTZの転送演出風のシェーダーを作りながら学ぶ記事です。
下記のツイートにあるようなモノを作成します。
GANTZシェーダーがバージョンアップしました!!!!!!!
— 梶田悠 haruka kajita (@kajitaj63b3) 2018年7月20日
左が新しい手法のエフェクトです。
ポリゴンの境界でエフェクトが欠けなくなったのと、Y座標に依存せず体に沿ってエフェクトが働く様に改良しました!
(モデリングソフトでUVデータを一つ追加しないといけない仕様) pic.twitter.com/HiyaH0TKt7
この転送シェーダーはシェーダーの基本的な要素が意外と詰まってるので勉強し始めの方には個人的におすすめです。
勉強し始めて何となく雰囲気分かってきたから具体例を知りたい人が読むとちょうどいいと思います。
#前知識
今回扱うのは頂点シェーダーとフラグメントシェーダーです。
この二つは最も基本的でほぼ必要不可欠なものです。
頂点シェーダー→フラグメントシェーダーの順実行されます。
頂点シェーダー
頂点シェーダーはオブジェクトの頂点の数だけ実行され、主に頂点の座標変換を担います。
オブジェクトが持つ頂点の座標や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
}
}
}
プロパティ
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
はレーザーの太さを決める値です。
レーザーのテクスチャはこちらです。お好きに使って下さい。
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
したのは透明になった部分からポリゴンの裏面を覗けてしまう為、裏面の描画もするようにしないと現象に対して整合性が保てないからです。
構造体
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
セマンティクスをバインドした変数を定義しています。TEXCOORD0
とTEXCOORD1
の部分はなんて書いても動くんですが、習慣として"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未満の値が入るとそのピクセルの描画を放棄する(=透明にする)関数です。_Threshold
とi.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座標を閾値と比較して透明/不透明の分岐処理をすれば完成です。