はじめに
シェーダーの知識0の人が読んで分かるような資料がネット上に無さそうだったので書きました。
自分の知識もゼロなので手探りで学びながら書いています。
なお、この記事ではタイトルで書いたように2Dのことにのみ触れていきます。
今回はシェーダーの基本知識と、スプライトの表示まで触れていきます。
基本事項
Unityにおけるシェーダー
UnityではShaderLabという言語で記述します。
そしてShaderLab内にはCg/HLSLという、C言語のように書くことが出来る部分があり、これを記述することで様々な表現が出来ます。
頂点シェーダーとフラグメントシェーダー
HLSLの箇所に頂点シェーダーとフラグメントシェーダーを用いて描画していきます。
頂点シェーダー (Vertex Sharder)
頂点情報の計算を行います。
Unityの座標系は3次元で、GPUに渡すのも3次元空間です。
が、実際のディスプレイは2次元なので、変換して上げる必要があります。
3次元空間における頂点が2次元空間でどこに当たるのかを計算してくれるのがこの頂点シェーダーです。
フラグメントシェーダー (Fragment Shader)
色情報の計算を行います。
頂点シェーダーによって計算された図形を元に、図形のピクセルをどのように塗るかを計算してくれます。
ピクセルの数だけ実行されます。
テンプレート
Shader "Custom/Shader1" { Propaties { } SubShader { Pass{ Tags { } [RenderSetup] CGPROGRAM //HLSL Program ENDCG } } }
プロパティを用いてInspectorからShaderで使う情報を渡すことが出来ます。
それぞれのShaderはSubShader内で書き、SubShaderは一つ以上のPassから構成されます。
Passは1回分の塗り(レンダリング)に相当し、複数書けばそれだけ塗られます。
Pass内には、TagやRenderSetUpで「いつどのように」レンダリングするのかの設定が出来るほか、
CGOPROGRAM
からENDCG
までにCg(HLSL)を書くことが出来ます。
最も簡単な形
Shader "Custom/Minimum" { SubShader { Pass {} } }
最もシンプルな形のShaderLabの形です。
これを適用すると真っ白になります。
Propaties
プロパティを設定することで、Unityのマテリアルインスペクターからシェーダーにデータを投げることが出来ます。
プロパティには以下のような種類があります。
Shader "Custom/SampleShader" { Propaties { //int _name ("display name", Int) = number //float _name ("display name", Float) = num //範囲付きfloat _name ("display name", Range(min, max)) = num //2Dテクスチャ _name ("display name", 2D) = "name" {} //長方形テクスチャ _name ("display name", Rect) = "name" {} //キューブマップテクスチャ _name ("display name", Cube) = "name" {} //Vector4 _name ("display name", Vector) = (num, num, num, num) //Color (R,G,B,A) _name ("display name", Color) = (num, num, num, num) } SubShader {} }
Unityで作成したシェーダーを実行する方法
- Projectウィンドウにて
Create > Shader
からシェーダーファイルを作成 - Projectウィンドウにて
Create > Material
からマテリアルを作成 - シェーダーの名前(ファイル名ではない)を変更した上でマテリアルから紐付ける
- Sprite等をScene上に配置し、作成したMaterialを参照する
シェーダーサンプル
実際にサンプル書きながら説明していきます。
コピペすれば動きます。
単色
コード
Shader "Custom/SolidColor" { SubShader { Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag struct VertexInput { float4 pos: POSITION; // 3D座標 }; // struct VertexOutput { float4 sv_pos: SV_POSITION; // 2D座標 }; //頂点シェーダー VertexOutput vert(VertexInput input) { VertexOutput output; //フラグメントシェーダーに渡す構造体の宣言 output.sv_pos = UnityObjectToClipPos (input.pos); //3D座標を2D座標に変換する return output; //フラグメントシェーダーに渡す } //フラグメントシェーダー float4 frag() : SV_Target { return float4(1.0, 0.0, 0.0, 1.0); //赤色を返す } ENDCG } } }
解説
赤色に塗りつぶすシェーダーです。
以下、順に解説していきます。
#pragma vertex vert #pragma fragment frag
で頂点シェーダーはvert
関数に、
フラグメントシェーダーはfrag
関数に記述することを宣言しています。
続いてこれ。
struct VertexInput { float4 pos: POSITION; // 3D座標 }; struct VertexOutput { float4 sv_pos: SV_POSITION; // 2D座標 };
ここでVertexInput
は頂点シェーダへの入力、VertexOutput
は頂点シェーダの出力かつピクセルシェーダの入力です。
このように:
でつなげて書くものを「セマンティクス」と呼び、シェーダーの入力と出力に用います。
つまり、VertexInput構造体の変数pos
で3D座標を受け取り、
VertexOutput構造体の変数sv_pos
に変換後の2D座標を代入し、これをフラグメントシェーダーに投げています。
フラグメントシェーダーでは、すべてのピクセルで色(1,0,0,1)を返しているので、すべてのピクセルで赤色を描画します。
なお、上記コードで変数が1つなのに構造体にしているのは、
今後返すものが増えても基本形が変わらないようにして読みやすくするためです。
グラデーション
コード
Shader "Custom/Gradation" { SubShader { Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag struct VertexInput { float4 pos: POSITION; // 3D座標 float2 uv: TEXCOORD0; // テクスチャ座標 }; struct VertexOutput { float4 sv_pos: SV_POSITION; // 2D座標 float2 uv: TEXCOORD0; // テクスチャ座標 }; // 頂点シェーダー VertexOutput vert (VertexInput input) { VertexOutput output; output.sv_pos = UnityObjectToClipPos(input.pos); output.uv = input.uv; return output; } // フラグメントシェーダー float4 frag (VertexOutput output) : SV_Target { float2 tex = output.uv; return float4( tex.x, tex.y, 1.0, 1.0); //座標に応じて色を変更 } ENDCG } } }
結果
解説
先ほどと比べ構造体の内容が1つ増えました。
テクスチャー座標、いわゆる「UV」です。
シェーダー内ではテクスチャは読み込みませんが、書き込む際のスプライトにおける座標によって色を変えるため、描画先のテクスチャ座標を見ています。
頂点シェーダーでやっていることはさっきと同じですね。
フラグメントシェーダーで返す色を座標基準にすることでグラデーションにしています。
結果から、座標は左下が(0,0)で右上が(1,1)となっていることがわかります。
スプライトシェーダー(簡易版)
コード
Shader "Custom/Sprite-Minimum" { Properties { _MainTex ("Sprite Texture", 2D) = "white" {} _Color ("Tint", Color) = (1,1,1,1) } SubShader{ Tags { "Queue"="Transparent" } ZWrite Off Blend One OneMinusSrcAlpha //乗算済みアルファ Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag struct VertexInput { float4 pos : POSITION; // 3D座標 float4 color: COLOR; float2 uv : TEXCOORD0; // テクスチャ座標 }; struct VertexOutput { float4 v : SV_POSITION; // 2D座標 float4 color: COLOR; float2 uv : TEXCOORD0; // テクスチャ座標 }; //プロパティの内容を受け取る float4 _Color; sampler2D _MainTex; VertexOutput vert (VertexInput input) { VertexOutput output; output.v = UnityObjectToClipPos(input.pos); output.uv = input.uv; //もとの色(SpriteRendererのColor)と設定した色(TintColor)を掛け合わせる output.color = input.color * _Color; return output; } float4 frag (VertexOutput output) : SV_Target { float4 c = tex2D(_MainTex, output.uv) * output.color; c.rgb *= c.a; return c; } ENDCG } } }
解説
概要
指定したテクスチャを表示するシェーダーです。
ビルトインシェーダーであるSprites/Default
とおおよそ同じ挙動をしますが、削れるところを削っています。
プロパティ
Properties { _MainTex ("Sprite Texture", 2D) = "white" {} _Color ("Tint", Color) = (1,1,1,1) }
テクスチャの読み込み用の_MainTex
、彩色用の_Color
プロパティを用意します。
TintColorがピンと来ない人は、Materialを作ると出て来るこいつです。(画像はSprites/Defaultを選んだ場合)
以上のプロパティをHLSL内で受け取る変数として、
float4 _Color; sampler2D _MainTex;
を宣言しています。プロパティと同名にすることで初期化は自動で行われます。
宣言する際、HLSLにおける変数の型には注意しましょう。
タグ
Tags { "Queue"="Transparent" }
ここではレンダリングの順序の設定をしています。
これを外すと透明度の情報が無視されてしまいます。
その他設定
ZWrite Off Blend One OneMinusSrcAlpha //乗算済みアルファ
まずZWrite
について。
詳しい話は公式のドキュメントに記載されていますので割愛。
要は、不透明であればOn(デフォルト)に、半透明部分があればOffにします。
これを書くことで既存のSprites/Default
シェーダーとの共存が可能になります。
これをコメントアウトした状態で、SpriteRendererのOrder in Layer
の値を弄ってみるのが分かりやすいです。
Order in Layer
が負の数字のとき、描画順序がおかしくなり、半透明部分が正しく描画されなくなります。
上がZWrite Off
で下がZWrite On
の場合です。
OffのときはOrder in Layerがきちんと適用されていることがわかります。
続いてBlend
について。
ここでは半透明部分のブレンド方法の設定をしています。
乗算済みアルファについての説明は以下の記事が分かりやすいです。
とりあえず、一般的なブレンド方法がこれである、ということです。
これ以外のブレンドタイプはUnityのドキュメントにリストアップされています。
頂点シェーダー
グラデーションから増えた箇所はこれだけですね。
//もとの色(SpriteRendererのColor)と設定した色(TintColor)を掛け合わせる output.color = input.color * _Color;
SpriteRendererを使っている場合、input.color
はSpriteRendererのColor値を指しています。
これとプロパティの値をかけ合わせて、最終的にテクスチャの色と掛ける色を計算します。
フラグメントシェーダー
グラデーションのときはUV座標に応じて適当な色を返しましたが、
今回はUV座標に応じてテクスチャの色を返すことでテクスチャの表示を行います。
テクスチャにおける指定座標のピクセル色取得はtex2D
を使います。
float4 tex2D (sampler2D texture, float2 uv);
これでピクセルごとの色の取得が出来ました。
続いて、アルファ値の適用を行います。
現段階では、RGB値はアルファ値を考慮しない値になっているため、これを補正します。
c.rgb *= c.a;
これの計算式にピンと来る人なら良いんですが、僕は分からなかったので少し解説します。
Wikipediaのアルファチャンネルについての説明が分かりやすいですね。
RGBAと一口に言っても色データはRGBのみで、A、つまりアルファ値は色データに対する補助データです。
ここで、単純なアルファブレンドを考えてみましょう。
背景となる既に描画されたRGBデータに対し、いま描画しようとしているRGBAデータがあるとします。
このとき、計算式は以下のようになります。
計算色 = 背景色 * (1.0 - アルファ値) + ピクセル色 * アルファ値
このことから、A値におけるアルファブレンドをHLSL内で行っていると推測できます。
試しに、以下のような画像を用意します。
(クリックで拡大してみると透明・半透明が分かります)
上記シェーダーからアルファブレンドの式を削除したものを適用して例が以下です。
スプライトの変化に注目してみてください。
計算して確かめてみましょう。
- 背景黒 + 半透明白 のとき
背景黒に半透明な白ですから、アルファブレンドなら当然灰色になります。
が、アルファ値を考慮しなければ
(0, 0, 0) + (1, 1, 1) = (1, 1, 1)
となり不透明な白になります。上のGIFでは実際にそうなっていますね。 背景透明 + 半透明白 のとき
これも同様に(1,1,1)となり不透明な白になっていることが分かります。
以上から、事前にアルファブレンドの準備をしておく必要があり、その計算をしていたということが分かりました。
どうやらパフォーマンス上の都合でこのようにしているようですが、詳細は不明です。
さいごに
HLSLについての入門記事のようなものはいくつもありますが、
隅から隅まで解説されているものが見当たらなかったので書いてみました。
まさかUnityで画像表示するだけでこんなにつらいとは。
その2の記事が出せるよう頑張ります。
参考文献
- OpenGL 基礎シリーズ
- Unity のシェーダの基礎を勉強してみたのでやる気出してまとめてみた – 凹みTips
- [Unity]ShaderLab入門とか | KentaKomai Blog
- Effectiveさお
- セマンティクス (DirectX HLSL)
- Explaining Unity Sprite-Default Shader – Game Development Stack Exchange
- Unity の Shader (ShaderLab) 知識ざっくりメモ – 自習室
- 【Unity】 Unityのシェーダで2Dのリング(円)を描く
- Cg_Users_Manual_JP.pdf
- HLSL のサンプル
- シェーダー利用でオーバーレイ合成: 花蝶風