Unity

Unityシェーダプログラム入門 UnlitShaderの要素を全て解説

Unityシェーダプログラム入門【UnlitShaderの要素を全て解説】
記事内に商品プロモーションを含む場合があります

この記事ではUnityにおけるシェーダプログラムの読み方・書き方を解説します!

その中でもUnity特有の機能であるShaderLabと、頂点シェーダ・フラグメントシェーダというものを中心に扱っていきます。

なお現在はShaderGraphなどのビジュアルエディタを利用することでシェーダプログラムを書かなくてもシェーダを作ることが可能です。

そんな時代ですがシェーダプログラムを読み書きできるようになっておくメリットも大きいので、今回はあえてビジュアルエディタについては一切取り扱いません。

こんな人にオススメ
  • Unityでシェーダの使い方は分かるけど、書いたことはない
  • シェーダを編集しようと思ってファイルを開いたものの、ナンモワカラン
  • C#など、他のプログラムはある程度分かる(C言語が分かるとGood)
  • ShaderGraphに対応していない環境(VRChat等)のシェーダを書きたい

動画バージョン

この記事の内容をアップデートして、更に詳しく解説しています!

Unityシェーダプログラム入門【Youtube】「Unityでシェーダを書いてみたいけど何から始めたらいいか分からない…」そんな方のファーストステップとして、レンダリングの基礎知識から学ぶことができる入門講座です。...

今回やること

Unityで右クリックメニューから作成できるシェーダのテンプレート「UnlitShader」内の全ての要素を解説します。「これはおまじないです」は一切使いません。

UnlitShaderとは

ライトの影響を考慮せず、落ち影などを付けないシンプルなシェーダです。

本当に全ての要素を解説するので、この記事だけで普通の記事10個分くらいはボリュームがあります。

残念ながらシェーダー芸人が作るような華やかな画面は出てきません。

しかしながら基本形であるUnlitShaderを正しく理解することは間違いなく良いシェーダが書けるようになることに繋がるので、ぜひ最後まで読んでいただけると嬉しいです。

シェーダは見えないところでUnityが色々と自動で処理してくれています。時にそれが混乱する原因になるので、つまづきやすいところは詳しく解説します。

該当箇所を理解するのに必要なレンダリングの基礎知識も一緒に説明します。

最後にUnlitShaderを改造して半透明描画ができるようにします。

シェーダの解説に入る前に、必要となる前提知識を説明していきます!

レンダリングパイプライン

シェーダを理解する上でレンダリングパイプラインの話は避けられないので、最初に解説しておきます。

レンダリングパイプラインとは、3D空間上に配置されたオブジェクトがデバイスの画面に表示されるまでの処理の流れのことです。

環境によって内容が異なるので細かくは解説しませんが、シェーダはこのレンダリングパイプラインから適切なタイミングで呼び出されるように作られています。

シェーダはCPUではなくGPUで実行されるというのも重要なポイントです。

  • どんなパラメータを受け取りたいか
  • いつ呼び出して欲しいか
  • どんな描画内容にするか

これらはシェーダ側に記述することで指定することができます。これがシェーダを書くという事です。

シェーダ側に「こういうパラメータを受け取りたいな」「こんな時に呼んで欲しいな」と記述されていても、レンダリングパイプラインがそれに対応していなかったら正しく動きません。

SRPについて

従来のUnityにおけるレンダリングパイプラインはUnityによって用意されたものを使うしかありませんでしたが、現在はScriptableRenderPipeline(SRP)を使用することでカスタマイズできるようになりました。

Unityでプロジェクトを作成する際は、テンプレートからUnityによって用意されたSRPであるHDRPLWRP(現在はURP)を選択できるようになっています。

これから解説するシェーダの内容は、あくまで標準のレンダリングパイプラインを使用した場合の話です。SRPを使用する場合は事情が異なりますので、そこはご了承下さい。

テンプレートから「2D」「3D」「3D With Extras」のいずれかを選択してプロジェクトを作成した際は、標準のレンダリングパイプラインです。

テンプレートの選択テンプレートの選択

なお、こちらの記事を作成した際のUnityバージョンは2019.1.8f1、テンプレートは「3D」です。

標準でも2種類ある

実は標準のレンダリングパイプラインの中にも「Forward」と「Deferred」の2種類があります。

今回は、デフォルトで使用されるForwardを扱っていきます。

Unityシェーダ

大きく3つに分類されます。

固定機能シェーダ

今回テーマとなる頂点フラグメントシェーダのように自由に描画処理の内容を書けるシェーダをプログラマブルシェーダと呼びます。

固定機能シェーダは、予め用意された機能を組み合わせて描画を行います。

プログラマブルシェーダが動かないハードウェア(ほとんどありませんが…)では、固定機能シェーダを使用します。

できることが非常に限られてしまうため、テクスチャをそのまま表示するだけといったシンプルな内容でしか使用しません。

サーフェスシェーダ

3Dモデルの表示に特化したシェーダです。

最小限の内容を用意するだけで、ライトの計算や影付けの計算はUnity側で用意されている処理に任せることができます。

マテリアルを作成した時にデフォルトで設定されているStandardシェーダはサーフェスシェーダが使われています。

たしかに便利なのですが、用途が限られてしまうこと、SRPでは廃止されていることなどを踏まえて今回は解説しません。

頂点フラグメントシェーダ

頂点シェーダ・フラグメントシェーダの2種類を使用します。

頂点シェーダは頂点単位、フラグメントシェーダはピクセル単位の処理を行います。

今回解説するUnlitShaderを含め、頂点フラグメントシェーダは幅広く利用されています。

シェーダを調べているとUnity以外の環境(DirectX、OpenGLなど)も含め、頂点フラグメントシェーダに出会うことが多々あります。

これらを読むことができればShaderGraphなどのビジュアルエディタに応用することもできるので、覚えるメリットは大きいです。

そのため今回のテーマを頂点フラグメントシェーダに設定しました。頂点フラグメントシェーダについてはShaderLabの解説の後に詳しく説明します。

ShaderLab

それではシェーダの書き方に入っていきます!

UnityシェーダはShaderLabというものを使用して記述します。拡張子は「.shader」です。

C#スクリプトと異なり、シェーダはテストプレイ中でも書き換えて保存したら起動し直さなくても反映されます。

シェーダを書くエディタについて

シェーダプログラムで補完が効くようにするにはプラグインが必要になる場合が多いです。

ひとまずVisualStudio2019を使っておけばシンタックスハイライトはされます。

シェーダに記述エラーがある場合はシェーダを適用しているオブジェクトがピンク色で表示されるのですぐに分かります。

C#を書くのに使っているエディタと同じで基本的には問題ありません。

UnlitShaderの作成

アセット作成メニューからUnlitShaderを作成します。

UnlitShaderの作成UnlitShaderの作成

Projectビューで右クリック、もしくはAssetsメニューから「Create > Shader > Unlit Shader」を選択します。

作成できたら、ダブルクリック等で開きます。

UnlitShaderコード全文

作成されたUnlitShaderのコード全文はこちらです。

シェーダの要素を理解していくには、ブロック単位( { から } )で見ることが大切です。これは他のプログラムでも同じですね。

最後に説明するPassブロックの中に書くのが頂点フラグメントシェーダです。それ以外はUnity側にシェーダの設定内容を伝えるための記述なので、厳密にはシェーダではありません。

まずは各ブロックについて解説していきます。

Shaderブロック

Shader "Unlit/NewUnlitShader"
{
    // シェーダの中身
}

一番外側のブロックには、シェーダ名を書きます。なおファイル名と一致させないといけないなどのルールはありません。

他のシェーダと同じ名前を付けることはできません。

せっかくなので、名前を「MyUnlitShader」に変更しておきます。

Shader "Unlit/MyUnlitShader"

マテリアル設定

これでマテリアルからMyUnlitShaderを選択できるようになっているはずです。実際にマテリアルに設定してみます。

CreateメニューからMaterialを選択してマテリアルを作成したら、Inspector上でシェーダを変更します。

マテリアルにシェーダを設定マテリアルにシェーダを設定

このマテリアルは後ほど利用します。

Propertiesブロック

Properties
{
    _MainTex ("Texture", 2D) = "white" {}
}

マテリアルのInspectorで設定したいプロパティを記述します。

プロパティの形式は以下の通りです。

プロパティ名 ("Inspectorに表示する名前", 型) = "デフォルト値" { オプション }

プロパティ名の先頭に _ が付いているのは慣習です。行末にセミコロン(;)は付けません。

テクスチャプロパティ

UnlitShaderで _MainTex に指定されている型「2D」はTexture2Dのことです。要するに普通のテクスチャですね。

Texture2DプロパティTexture2Dプロパティ

デフォルト値には、テクスチャがNoneの時に使用される単色の仮テクスチャを名前で設定できます。

いくつか試してみたところ、利用できるのはwhite、black、redの3種類だけでした。これ以外を指定するとグレーになります。

デフォルト値をblackに変えてみるとこんな感じにマテリアルのInspectorでの表示も変わります。

オプションについて

テクスチャのようにオプションが存在するプロパティで行末に {} を書かなくてもエラーにはなりませんが、そもそもオプションが存在しないプロパティに対して {} を書いてしまうとエラーになります。

オプションは基本的に使わないので、特に解説はしません。

よく使うプロパティ

UnlitShaderではテクスチャのみ指定されていますが、よく使うものとしては以下のものがあります。

Float
_Float ("Float", float) = 0.1

小数点の値を自由に設定できます。

Int

floatと同じように整数もintで利用できます。

_Int ("Int", int) = 5
Range

指定範囲内の値をスライダーで設定できます。

_Range ("Range", Range(0.5, 1.0)) = 0.75
RangeプロパティRangeプロパティ
Color

色をカラーピッカーで設定できます。

_Color ("Color", Color) = (1, 0, 0, 1)
ColorプロパティColorプロパティ
色について

シェーダ上では、は0〜1の値で表現されます。(r: 1, g: 1, b: 1, a: 1)なら白です。

0〜255に慣れている場合はちょっとややこしいかもしれませんが、覚えておきましょう。

シェーダへの受け渡し機能

Propertiesブロックで設定されたプロパティは、頂点フラグメントシェーダ側で用意した同じ名前のプロパティに自動的に受け渡されます。

UnlitShaderの _MainTex に対応するのは、Passブロック中の真ん中あたりにあるこれです。

sampler2D _MainTex;
float4 _MainTex_ST;

シェーダ側でのsampler2D型はTexture2Dのことです。ここは問題ないですね。

では _MainTex の下にある _MainTex_ST とは何なのか。

floatについて

まずfloat4についてですが、これはC#スクリプト側でのVector4と同じと考えておけば大丈夫です。

(x, y, z, w)の4つのfloat値を格納できる型ですね。

float2 は Vector2

float3 は Vector3 と同じです。

単にfloatと書いた場合は、C#のfloatと同じです。(float1と書くこともできます)

_STが付いているのは?

_MainTex_ST の正体は、テクスチャをInspectorで設定する時に表示されているこの部分です。

_MainTex_STの正体_MainTex_STの正体

ここで設定されたTilingとOffsetの値を「テクスチャ名_ST」のfloat4プロパティに自動で格納してくれる仕組みです。ややこしいですね。

以下のように格納されます。

_MainTex_ST.xTiling X
_MainTex_ST.yTiling Y
_MainTex_ST.zOffset X
_MainTex_ST.wOffset Y

TilingとOffsetとは?

TilingとOffsetを利用したことがない方もいると思うので、こんな感じのテクスチャを板ポリ(Quad)に貼り付けてテストしてみます。

Tilingを増やすと、ポリゴン内でテクスチャUVが繰り返します。縮小して詰め込むイメージですね。

Tiling X:2 Y:1Tiling X:2 Y:1
Tiling X:2 Y:2Tiling X:2 Y:2

OffsetはUVをそのままずらします。1を入れると1タイル分ずれてしまって見た目が変わらないので0.5を入れてみます。

Tiling X:2 Y:2 Offset X:0.5 Y:0Tiling X:2 Y:2
Offset X:0.5 Y:0

横方向に対して元のテクスチャの半分だけ、UVがずれました。

TilingとOffsetを使わない場合

TilingとOffsetが不要なテクスチャであれば、以下のようにNoScaleOffset属性を付けることでInspectorにも表示されなくなり、_STが付いた変数を宣言する必要もなくなります。

[NoScaleOffset] _MainTex ("Texture", 2D) = "black" {}

SubShaderブロック

SubShader 
{
    // シェーダの中身
}

シェーダの中身はSubShaderブロック内に記述します。SubShaderは複数書くことができます。

  • 対象ハードウェアで対応しておらず、実行できない場合
  • 用途に合わせたシェーダが定義されていない場合

このような場合はSubShaderをスキップし、実行できるSubShaderが見つかるまで上から順番に探してくれます。

SubShader 
{
    // シェーダ1
} 
SubShader 
{
    // シェーダ2
} 
FallBack off // 何もしない 

どのSubShaderも実行できない場合は最後に書いたFallBackが実行されます。

FallBack "Standard"

例えばこのように書いた場合は、FallBackはStandardシェーダになります。

FallBackさせてみる

ハードウェアで未対応の場合にスキップされるのは古いハードウェアで最新機能を使おうとした場合で、そこまで発生することはありません。

用途に合わせたシェーダが定義されていない場合の方は簡単に再現できるので、試してみましょう。

UnlitShaderに書かれているのは、オブジェクトの描画だけを行うシェーダです。

これは、影の描画には対応していません。

Standardシェーダを適用した真っ白な床に対して、MyUnlitShaderを適用したBoxを置いてみました。

オブジェクトは描画されましたが影は描画されていません。

MyUnlitShaderのFallBackにStandardシェーダを指定してみます。

FallBack "Standard"

すると、影が描画されるようになりました。

影の描画は、影を与える側と影を受ける側の両方がシェーダで対応されていることで成立します。

影を受けることに対応しているStandardシェーダを床に設定しても、与える側であるBoxが対応していなければ影は落ちないわけですね。

FallBackにStandardシェーダを指定したことで、

  • オブジェクトの描画はMyUnlitShader
  • 影を与える処理はStandardシェーダ

このように自動的に使い分けてくれるようになったわけです!

もちろん、MyUnlitShaderの中に影を与える処理を自前で実装することもできます。

Tags

Unity側にシェーダの設定を伝えるためにタグを付けます。

UnlitShaderでは1つしかないですが、複数ある場合は次のように書きます。

Tags
{
    "Queue"="Geometry"
    "RenderType"="Opaque"
}

タグはこの後出てくるPassブロックの中にも書くことができます。

SubShaderと同じようにPassも同一SubShader内に複数書くことができるのですが、SubShaderに書いたTagは全てのPassに対して適用されます。

Passの中に書いたTagは、そのPassにしか適用されません。

SubShaderでしか使えないタグ、Passでしか使えないタグがあります。一覧がまとめられている記事があったので載せておきます。

僕もよく使うものしか覚えていません。まずは、UnlitShaderに書かれているRenderTypeを押さえておきましょう。

もう1つよく使うQueueは、この記事の最後で半透明描画を試す時に一緒に解説します。

RenderType

色々な種類があるのですが、シェーダがどのようなグループに属しているのかを指定します。主にUnity側でシェーダの種類を判定するのに利用します。

普通にシェーダを利用していて、RenderTypeが影響することは滅多にありません。

影響があるものとして、オブジェクトを半透明にする場合はOpaqueではなくTransparentを指定するとパフォーマンスが向上することを確認しています。

半透明ならTransparent、それ以外ならOpaqueぐらいで考えておいても、とりあえず問題ありません。

数少ない用途の1つにレンダリング時にシェーダを差し替える機能があります。興味のある方はこちらを読んでみて下さい。

LOD

LOD 100

次にLODです。LODは Level of Detail の略で、しきい値の役割を果たしています。

こちらも、SubShaderを使い分けるための仕組みです。

ただLODをシェーダに書くだけでは何も変化しませんが、C#スクリプトから指定することで有効になります。

わかりやすく検証してくれている記事があったので、紹介しておきます。

例えばゲームのグラフィックス品質設定に合わせてシェーダを切り替えるといったことができます。

LODを特に使わないのであれば、省略しても問題ありません。

他のオプション

TagsやLOD以外にもオプションがいくつか存在します。詳しくは後ほど半透明描画を試す時に解説します。

Passブロック

Pass
{
    Tags { // 必要に応じてタグ付け }
    
    // シェーダ本体
}

いよいよ最後のPassブロックです。

先述した通りPassの冒頭に必要に応じてタグを書きます。Passでしか使えないタグはライティングに関わる部分が主で、今回は特にタグは登場しません。

このPassの中に

  • 固定機能シェーダ
  • サーフェスシェーダ
  • 頂点フラグメントシェーダ

上記いずれかをシェーダ本体として記述することになります。

Passを複数書いた場合は、上から順番に全て実行されます。Passが複数あるシェーダをマルチパスシェーダと言います。

ここまで解説してきた内容がUnity特有の機能であるShaderLabでした。

ここから先は、頂点フラグメントシェーダについて解説します。

頂点フラグメントシェーダ

前提知識の解説

最初に頂点フラグメントシェーダがそれぞれどういった処理を行うものなのか、解説していきます。

ここを把握せずにシェーダを読もうとすると混乱してしまうので、ぜひ押さえておきましょう。

頂点シェーダについて

3DモデルでもパーティクルでもUIでも、それらは全てメッシュの集まりです。

メッシュは、ポリゴンの集まりです。

Unityではポリゴンは全て三角形で、3つの頂点によって三角形を表現します。

三角形ポリゴン三角形ポリゴン

頂点シェーダにはこの頂点の情報が渡されるので、頂点単位で処理を行いフラグメントシェーダに渡すデータを作成します。

必ず行わなければいけない処理として、座標変換があります。

座標変換

頂点情報の中に入っている座標は、3D空間における座標です。

このままではスクリーン上のどの位置に表示すればいいか分からないので、カメラから見た時にスクリーン上のどの位置に表示されるかを計算し、フラグメントシェーダに渡します。

詳しい話は割愛しますが、Unityではこれを1つの関数を実行するだけで計算してくれます。

正確には頂点シェーダで最後まで計算するわけではなく、-1〜1に収まるような値にして渡します。

最後にGPUが端末の解像度に合わせて自動的にマッピングしてくれます。例えば解像度が横1280x縦720なら、横0〜1280、縦0〜720に変換します。

ラスタライズ

フラグメントシェーダを実行する前に、ラスタライズという処理が自動で実行されます。

先ほどの三角形ポリゴンと同じものが頂点シェーダから渡された場合、以下のように処理されます。

ラスタライズのイメージラスタライズのイメージ

頂点座標は先述した座標変換によって-1〜1に変換されています。(ざっくり目分量なので正確ではないです)

たとえば赤い点がこれからフラグメントシェーダで処理したいピクセルだとすると、3つの頂点座標から補間した値を計算してフラグメントシェーダに渡します。

この時点で、描画される位置は決定されます。

図では座標を例にしていますが、テクスチャUV色情報も同様に補間されます。

同じ処理を図でいうところの青色で塗りつぶされている範囲全てに対して行い、同じくフラグメントシェーダも塗りつぶされている範囲全てのピクセルに対して実行します。

フラグメントシェーダ

ピクセル単位の処理を行うので、ピクセルシェーダと呼ばれることもあります。

フラグメントシェーダでは、画面に出力するを決定します。

逆に言うと、フラグメントシェーダでは色以外の情報を出力することはできません

位置を調整したい場合などは、頂点シェーダの時点で決めておく必要があります。

Cg言語

それでは頂点フラグメントシェーダの書き方に入っていきます!

UnlitShaderの頂点フラグメントシェーダの部分だけ抜き出したものを再度載せます。

要素を順番に見ていきます。最初は冒頭の「CGPROGRAM」について。

CGPROGRAM // ここからCg言語のプログラムを書くという合図 

// シェーダを記述 

ENDCG // プログラム終了

UnlitShaderには、Cg言語というシェーダ言語が使われています。

Cgは C for graphics の略です。名前の通りC言語と非常によく似ています。

これが冒頭でちらっとC言語が分かるとGoodと書いた理由でした。

標準レンダリングパイプラインでUnityが用意してくれているシェーダは基本的にCgで書かれています。

他の言語

DirectXで使用されるHLSLや、OpenGLで使用されるGLSLがあります。どちらもUnityで使用可能です。

HLSL、GLSLを使用する場合はシェーダを囲むコマンドも変わります。

// HLSLの場合
HLSLPROGRAM

// シェーダを記述 

ENDHLSL
// GLSLの場合
GLSLPROGRAM

// シェーダを記述 

ENDGLSL

最初に解説したScriptableRenderPipelineで用意されているシェーダは、HLSLで書かれています。今後はHLSLが主流になっていくのかもしれません。

CgとHLSLは細かい違いこそあれ、基本的なところはあまり変わらないので今回はそのままCgで解説していきます。

プラグマ(pragma)

#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog

プラグマ(pragma)とは、コンパイラに対して情報を渡すための命令のことです。

ここで使われているもの以外にも色々な種類がありますが、UnlitShaderで使われているものについて見ていきましょう。

頂点シェーダ・フラグメントシェーダの指定

#pragma vertex 頂点シェーダ関数名
#pragma fragment フラグメントシェーダ関数名

冒頭の2つは、頂点シェーダ・フラグメントシェーダそれぞれがどの関数なのかを指定します。

シェーダ内には関数をいくつも定義できるので、どれが頂点シェーダでどれがフラグメントシェーダなのかをコンパイラに伝えてあげる必要があるわけですね。

シェーダバリアント

#pragma multi_compile_fog

multi_compile から始まるのはシェーダバリアントという機能を使うための命令です。

UnlitShaderは所々に「FOG」とついた命令があるのが分かると思います。

FOGとはそのままフォグという機能のことで、

メニューから「Window > Rendering > Lighting Settings」 のOtherSettingsの中で設定できます。

フォグの設定フォグの設定

本来フォグは霧を表現するためのものなので白色にすることが多いですが、ここではわかりやすくするために赤色で設定しています。

このように、奥に行けば行くほど色が乗るような表現ができます。

ところでこのフォグ、チェックボックスでオンにしなければ動作することはありません。

しかしシェーダ側にはフォグの処理を書いておかないと、オンにした時にフォグが動きません。

普通にフォグの処理を書いてしまうと、もし使わなかった時に処理がムダになってしまうのです。

そこで登場するのがシェーダバリアントです。

multi_compile_fogを書くことで

  • フォグ処理が入ったシェーダ
  • フォグ処理が入っていないシェーダ

2種類のシェーダを自動で生成して、フォグがオンになっているかどうかを見て自動で切り替えてくれます。

フォグのようにUnityが用意している機能だけでなく、自分で作った機能もシェーダバリアントで切り替えることができます。

慣れないうちはシェーダバリアントの挙動を理解するのが難しいかもしれません。今回は詳しく解説しませんが、使いこなせるようになると非常に強力な機能です。

インクルード(include)

#include "UnityCG.cginc"

インクルード(include)は、指定したファイルの中身をそのままこの位置に貼り付ける命令です。

複数のシェーダを書いていて、同じ内容をどのシェーダにも書いていると思ったらファイルを分けてインクルードで読み込むようにすることで共通処理を再利用しやすくなるのでとても便利です。

インクルード用ファイルの拡張子は以下の通りです。

Cg.cginc
HLSL.hlsl
GLSL.glslinc

Unityに標準で入っているシェーダを見ていると、Passブロックの中に書かれているのはプラグマとインクルードだけ、というものもたくさん存在します。処理の実体は全てインクルードファイルに持たせることも可能というわけです。

指定はシェーダファイル自身からの相対パスもしくは「Assets/Shaders/Example.cginc」のように指定しますが、ここで使われているUnityCG.cgincのようにUnityに組み込まれているファイルについてはファイル名だけで読み込めます。

UnityCG.cginc

UnlitShaderでインクルードされている「UnityCG.cginc」は、Unityで用意されている便利な関数などがいろいろ詰め込まれています。

中身は膨大なので僕も全部把握しているわけではないですが、UnlitShaderの中でUnityCG.cgincの中身を利用している箇所が出てきたら都度解説します。

UnityCG.cgincを含め、Unityに入っているシェーダはUnityのアーカイブダウンロードページからダウンロードできます。

お使いのUnityバージョンのダウンロードタブから「ビルトインシェーダ」をクリックして下さい。WinでもMacでも内容は同じです。

UnityCG.cgincは「CGIncludes」フォルダの中に入っています。

新しくシェーダを作る時は、内容が近いシェーダをベースに書き換えて作るのが楽です。

ネット上から探してきてもいいですが、ビルトインシェーダを参考にするのも手っ取り早い方法なのでぜひダウンロードしておきましょう。

頂点シェーダへの入力定義

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

こちらは頂点シェーダへ入力する頂点データの定義です。

struct(構造体)で定義するルールですが、名前は自由です。

セマンティクス

コロン(:)の後に書かれているPOSITION、TEXCOORD0はセマンティクスといい、種類は以下の通りです。

POSITION頂点座標(float3〜4)
TEXCOORDnn番目のテクスチャUV。nには0〜3※を指定可能(float2〜4) ※プラットフォームによっては4以降も使用可
NORMAL頂点の法線(float3)
TANGENT接線(float4)
COLOR頂点カラー(float4)

これら全てを毎回使うわけではないので、対象のシェーダで必要なものだけを受け取るために定義する仕組みです。

UnlitShaderでは、頂点座標とテクスチャUVを1つ受け取るのでこのような定義になっています。

変数名は自由ですが、セマンティクスとデータ型は決められたものを使わなければいけません。

floatのアクセス方法

floatについてはC#スクリプト側のVectorのようなものと説明しましたが、頂点カラーのようなもfloatで扱います。

そのため、以下のようにxyzwでのアクセスの他に、rgbaでのアクセスもできるようになっています。値そのものはどちらでアクセスしても同じです。

float4 f4 = float4(1, 2, 3, 4);
float3 f3 = 0;   // = (0, 0, 0)
f3 = f4.xyz;     // = (1, 2, 3)
f3.xyz = f4.rgb; // = (1, 2, 3)
f3.yz = f4.ba;   // = (3, 4)
f3 = f4.b;       // = (3, 3, 3)
f3 = f4.rg;      // = 代入元の方が要素が少ないのでエラー
f3 = f4;         // = (1, 2, 3)

一部だけ取り出す書き方が出来るのもシェーダの特徴です。

代入元の方が要素が少ないとエラーになります。代入元が1つだけなら代入先でアクセスしている要素全てにコピーされます。

フラグメントシェーダへ渡すデータ

struct v2f
{
    float2 uv : TEXCOORD0;
    UNITY_FOG_COORDS(1)
    float4 vertex : SV_POSITION;
};

こちらは頂点シェーダからフラグメントシェーダへ受け渡すデータの定義です。こちらもstructで定義します。

ポリゴン単位でラスタライザに渡されてラスタライズ処理され補間された値がフラグメントシェーダに渡されます。

名前も自由ですが、v2f(vertex to fragment)が分かりやすいのでよく使われている印象です。以降はv2fと呼びます。

v2fのセマンティクス

v2fにおけるセマンティクスの一覧は以下の通りです。(他にも一応ありますが、使わないので省略しています)

SV_POSITION座標変換された後の頂点座標(float4)
TEXCOORDnテクスチャUVなど
COLORn色など

用途が決まっている頂点シェーダへの入力と異なり、v2fは自由な部分が多いです。

まず、SV_POSITIONは必須です。画面のどこに描画するかどうかはラスタライズの時点で決定されるためですね。

残りのTEXCOORDとCOLORは、どんな値を入れてもいいし、型も自由です。

古いGPUではTEXCOORDを使うかCOLORを使うかで扱いに違いがあったようですが、現在特に違いはありません。

そのため、分かりやすく色を扱うならCOLOR、そのほかの値は全てTEXCOORDにすることが多いです。

用途自体は自由ですが、に注意です。種類ごとの数に制限はありませんが、全て合計して9個以上になると、プラットフォームやGPUの種類によっては補間処理されずそのまま値が渡されてきます。

基本的には8個までに抑えるようにしましょう。

UNITY_FOG_COORDS

UNITY_FOG_COORDSは、UnityCG.cgincで定義されています。

#define UNITY_FOG_COORDS(idx) float1 fogCoord : TEXCOORD##idx;

厳密には更に定義が分かれていて複雑なのですが、その部分を展開すると上記のようになります。

defineは、特定の文字列を定義しておいて、コンパイル時にそれがプログラムの中に出てきたら別の文字列に置き換える命令です。

#define 置き換え前(文字列名,文字列名...) 置き換え後

置き換え前の後ろに付けた()の中に文字列を入れることで、それを置き換え後の特定の文字列と##で結合することが出来る仕組みです。

UNITY_FOG_COORDS(1)は、コンパイル時には以下のように変換されます。

float1 fogCoord : TEXCOORD1;

フォグ処理をするために必要なfogCoord変数を定義する必要があるけれど、何番目のTEXCOORDを使ったらいいかはシェーダによって変わるので指定できる仕組みになっているわけですね。

そのため以下のように書くとエラーになります。

struct v2f
{
    float2 uv : TEXCOORD0;
    float3 worldPos : TEXCOORD1;
    UNITY_FOG_COORDS(1) // TEXCOORD1が重複してエラー
};

この場合はUNITY_FOG_COORDS(2)と書く必要があるわけですね。

シェーダバリアントの仕組み

また、フォグのチェックがオンになっていない時はシェーダバリアントによって何も記述されないような仕組みになっています。

#if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2)
    #define UNITY_FOG_COORDS(idx) float1 fogCoord : TEXCOORD##idx;
#else
    #define UNITY_FOG_COORDS(idx) // 何も出力しない
#endif

シェーダバリアントの切り替えには #if 〜 #else 〜 #endif を使用します。

definedは、()の中に入れた文字列がdefineで定義されているかどうかを調べる命令です。

例えばフォグのModeがLinearなら「FOG_LINEAR」がdefineで事前に定義されています。

そのため、defined(FOG_LINEAR)がtrueになります。

反対にフォグのチェックがオフの時はいずれも定義されないので、elseに入って何も出力されません。

このようにdefineを使って記述を切り分けるのがシェーダバリアントの仕組みです。

if文に似ていますが、全く性質の異なるものだということを覚えておきましょう。

グローバル変数の宣言

sampler2D _MainTex;
float4 _MainTex_ST;

ここについてはPropertiesブロックからプロパティが受け渡されるものと説明しました。

ここで宣言したプロパティはグローバル変数であり、どの関数からもアクセスできます。

  • Propertiesブロックからの受け渡し
  • C#スクリプトからのアクセス
  • 頂点シェーダからのアクセス
  • フラグメントシェーダからのアクセス

これら全てに対応しています。

反対に頂点シェーダの中で宣言した変数は頂点シェーダの中でしか使うことは出来ませんし、フラグメントシェーダの中で宣言した変数はフラグメントシェーダの中でしか使うことは出来ません。

頂点シェーダの処理

v2f vert (appdata v)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    UNITY_TRANSFER_FOG(o,o.vertex);
    return o;
}

形式としては以下の通りです。

v2f 関数名 (頂点データ型 頂点データ変数名)
{
    v2f o;
    // 頂点シェーダの処理
    return o;
} 

関数名がvertで頂点データの型がappdataなのは先述した通りです。

頂点データの変数名は「v」とすることが一般的です。フラグメントシェーダへの出力も変数名は自由ですが「o」とすることが多いです。

それでは頂点シェーダの処理について1行ずつ見ていきます。

UnityObjectToClipPos

o.vertex = UnityObjectToClipPos(v.vertex);

UnityObjectToClipPosはUnityCG.cgincで定義されている、3D空間からスクリーン上の位置への座標変換を行ってくれる関数です。

実行している内容としては以下のようなものなので古いシェーダだとそのまま書かれていることもありますが、UnityObjectToClipPosを使った方がパフォーマンスが良いようです。

mul(UNITY_MATRIX_MVP, v.vertex)

また、これと全く同じものを書くとUnityによって勝手にUnityObjectToClipPosに置き換えられます。

mul()は積を計算する関数です。ここではUNITY_MATRIX_MVPという行列と頂点座標であるv.vertex(float4のベクトル)の積を計算しています。

組み込み関数

Unityによって定義されている関数ではなく、mul()のようにCgやHLSLで定義されている関数を組み込み関数と言います。GPUによって最大限の最適化が行われているため、高速に動作するものが多いです。

どんな組み込み関数があるのか知っておき、いつでも使えるようにしておくことで複雑な処理を書いてしまうことを避けられるのでぜひ活用していきましょう。

参考
Cgの組み込み関数(英語)
HLSLの組み込み関数

行列について

4×4の行列であればfloat4x4のように宣言します。

行列とは何なのか?という説明を始めてしまうとそれだけで1つの大きなテーマになってしまうので、ここではmul()についての詳細とUNITY_MATRIX_MVPの簡単な説明に留めておきます。

行列がよく分からない方のために参考リンクを載せておきます。

mul()について

mul()は実はCg(HLSL)にはあってGLSLには存在しない組み込み関数です。

GLSLでは a * b のように「*」演算子を使って行列同士も、行列とベクトルの積も計算することができます。

Cg(HLSL)においては行列の積、行列とベクトルの積を計算する際にmul()を使用します。

ちなみにCg(HLSL)でも同じ次元数の行列同士なら「*」演算子を使うことができますが、mul()とは挙動が異なります。

「*」演算子を使った場合は、数学的な行列の積計算(それぞれ行と列の内積を求める)ではなくて、単に各成分の値をかけ合わせるだけになります。

行列を使ってこのような計算を行うことは滅多にありませんから、GLSLでは「*」演算子で行列の積を計算できるようになっているのでしょうね(ややこしいですが…)。

UNITY_MATRIX_MVP

UNITY_MATRIX_MVPは、名前そのままで「Unityによって定義されたMVP行列」です。

MVPは Model x View x Projection の略です。

座標変換はモデル(Model)変換、ビュー(View)変換、プロジェクション(Projection)変換の3段階で行われます(厳密には頂点シェーダの後に透視変換が行われて完了します)。

それぞれの変換がどういったものなのか詳しく知りたい方は、こちらを読んでみて下さい。

これら3つの変換を1回の計算で終わらせられるように、Unityが事前に計算しておいたものをUNITY_MATRIX_MVPの中に入れてくれているわけです。

具体的にはそれぞれ行列化したものをかけ合わせています。

ただしMVPという名前だから「Model x View x Projection」の順番に掛け合わせているのかというと、そういうわけでもないのです。

ややこしい話なのでここで覚える必要はないですが一応解説しておくと、このMVPという名前はDirectXに由来しています。

DirectXとOpenGLでは行列のメモリ上の配置が異なります。

4×4の行列が要素数16個の配列だとイメージした時に、0〜3の要素に1行目が入るものを行オーダー1列目が入るものを列オーダーと言います。(以降は行オーダーなら4〜7に2行目、8〜11に3行目、列オーダーなら4〜7に2列目、8〜11に3列目と続きます)

OpenGL、そしてUnityは列オーダー、DirectXは行オーダーです。

列オーダーと行オーダーでは行列をかける順序が反転します。これはメモリ配置の仕方が違っても、行列計算の数学上のルールはそのまま使うためです。

行オーダーのDirectXにおいて「MVP」と命名されたので、Unityにおいては逆の順番、つまり「Projection x View x Model」の順番でかけます。

そして最後に頂点座標のベクトル(v.vertex)との積を求めて座標変換は終了でした。

先程のmul()計算においてv.vetexが右側に来ていたのは、これが理由です。

mul(UNITY_MATRIX_MVP, v.vertex)

詳しくはこちらを参照して下さい。

座標変換されていることを確認してみる

先述した通り、座標変換後はスクリーン上のどの位置なのかを-1〜1で表現します。

頂点シェーダの処理を少し書き換えて、画面の中央に表示されている板ポリが画面の右上にくるように調整してみます。

画面の中央に表示される板ポリ画面の中央に表示される板ポリ

o.vertex = UnityObjectToClipPos(v.vertex);
o.vertex.x += 1;
o.vertex.y += 1 * _ProjectionParams.x;

右上なのでそれぞれ+1します。

_ProjectionParams.xを掛けている理由ですが、これはY軸がプラットフォームによって反転しているためです。

プラットフォームによる違いを吸収するための値が_ProjectionParams.xの中に入っています。

UNITY_MATRIX_MVPや_ProjectionParamsのようにUnity側が事前に値を入れてくれている変数の一覧はこちらに載っています。

Y軸のプラットフォームによる違いを考慮せずに座標変換後の値を操作してしまうと思った通りの位置に表示されなくなってしまうため注意して下さい。

右上に表示されるようになった右上に表示されるようになった

緑色の3の部分だけ見えているということは、きれいに板ポリの中心部分がスクリーンの右上にずれたことが分かりますね。

TRANSFORM_TEX

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

テクスチャのUV値を計算する処理です。

全部大文字なのでピンときた方もいるでしょうか。TRANSFORM_TEXはUnityCG.cgincでdefineされたものです。

#define TRANSFORM_TEX(tex,name) (tex.xy * name##_ST.xy + name##_ST.zw)

_STのように文字列に対して結合する場合は##が必要ですが、変数名としてそのまま使う場合はtex.xyのように利用できます。

展開すると以下のようになります。

o.uv = v.uv.xy * _MainTex_ST.xy + _MainTex_ST.zw;

頂点データから受け取ったUV値にTilingとOffsetの値を反映させるための処理です。

これはつまり、TilingとOffsetを使わないテクスチャならそのままUV値を渡してあげれば問題ないということになります。これも覚えておきましょう。

o.uv = v.uv;

UNITY_TRANSFER_FOG

UNITY_TRANSFER_FOG(o,o.vertex);

こちらもUnityCG.cgincでdefineされているフォグの処理です。

プラットフォームによって使い分けられていますが、基本形は以下です。

#define UNITY_TRANSFER_FOG(o,outpos) o.fogCoord.x = (outpos).z

展開すると以下のようになります。

o.fogCoord.x = o.vertex.z;

フォグは奥に行けば行くほど色が乗る処理でしたね。頂点シェーダの段階では、座標変換後のz値(奥行き情報)をfogCoordに移しています

なぜここで移しておく必要があるかということですが、

o.vertex、つまりSV_POSITIONはスクリーン上のどのピクセルを対象にフラグメントシェーダを実行するか決めるための特殊なセマンティクスです。

他の値と異なり、そのまま補間されて渡されるわけではなく全く異なる値に変換されてしまいます

そのため、TEXCOORDで定義したfogCoordに移しています。

フラグメントシェーダの処理

fixed4 frag (v2f i) : SV_Target
{
    // sample the texture
    fixed4 col = tex2D(_MainTex, i.uv);
    // apply fog
    UNITY_APPLY_FOG(i.fogCoord, col);
    return col;
}

形式としては以下の通りです。

fixed4 関数名 (v2f v2fの変数名) : SV_Target
{
    // フラグメントシェーダの処理

    return 画面に出力する色;
} 

フラグメントシェーダにはセマンティクスが必要です。古いシェーダでは「COLOR」が使われていることもありますが現在は「SV_Target」で問題ありません。

v2fの変数名は頂点シェーダからの入力(input)ということで「i」とすることが多いです。

fixed型

戻り値がfloatではなくfixedになっていますね。

実はシェーダ上で小数点を扱う時はfloat以外にhalf、fixedがあります。

なぜ分かれているかというと、パフォーマンスを限界まで上げる必要があるモバイル端末に対応するためです。

パフォーマンスは fixed > half > float の順です。

それぞれ使うビット数と扱える値の範囲、精度が異なります。Unityのドキュメントに載っているので引用します。

fixed11bit、-2〜2の範囲、1/256の精度
half16bit、-60000〜60000の範囲、小数点以下3桁までの精度
float32bit、範囲は他の言語と同じで非常に広い

fixedは1/256より小さい値が扱えないので使える場面が限られますが、色は0〜1で表現することは先述した通りです。

色の階調は256段階なので、1/256より小さい値はそもそも必要ありません。fixedはまさに色を格納するための入れ物と言えます。

ただし、計算途中の段階でfixedを使ってしまうと1/256より小さい値がカットされてしまう分、誤差が出る可能性があります。

最終的な出力を格納するためであればfixedで全く問題ないですが、計算段階で誤差が出ては困る場合はhalfを使いましょう。

fixed、half、floatが使い分けられるのはあくまでモバイル端末のGPUに限った話です。PCではfixedやhalfと書いてもfloatとして扱われます。

floatとhalfの使い分け

ではなぜ今までfloatしか出てこなかったかというと、扱ってきたのが座標UV値だったからです。

頂点シェーダの段階では3D空間の座標を計算に利用するわけですが、Unity上で座標は-100000〜100000の値を扱うことができます(これ以上は警告が出ます)。

halfでは-60000〜60000までしか表現できないので、範囲が足りません。

また、座標変換が行われた後の値は-1〜1に凝縮されているので、小数点以下3桁まででは精度が足りません。例えばxの値はスクリーンの幅が1000より大きいと表示される位置がずれてしまう可能性があります。

UVも同様に、小数点以下の小さな値でズレが生じてしまうことを防ぐためにfloatを使うことが多いです。

座標やUV値のように細かい精度が必要ない値であれば、halfを使っても大丈夫です。

また、パフォーマンスを限界まで高めるためにfloatを避けてhalfやfixedを積極的に使うこともあります。

テクスチャサンプリング

それでは処理の中身を見ていきます。

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

まずこちらですが、tex2D()は組み込み関数です。

指定したテクスチャから、指定UV値の色を取り出す処理です。

この色を取り出すことを、テクスチャサンプリングと言います。

注意することがあるとすればUV値の起点はテクスチャの中央ではなく左下であることです。左下が(0, 0)で右上が(1, 1)です。モデリングをされている方ならご存知の通りですね。

UnlitShaderではこれ以降の計算はフォグの1回だけなので、fixedで色を受け取っているようです。

色を反転させてみる

ではここでフラグメントシェーダを少し編集して、色が反転することを確認してみましょう。

色の反転はとても簡単です。このように1から本体の色を引けばOKです。

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

なお、1はfixed4(1, 1, 1, 1)に変換されます。このままだとα値まで反転してしまいますが、このシェーダではα値を考慮しないので影響はありません。

色が反転した色が反転した

先ほどから使っているテクスチャの色が反転したことが確認できました。

UNITY_APPLY_FOG

いよいよ最後の1行です。フォグの適用処理です。

UNITY_APPLY_FOG(i.fogCoord, col);

例のごとくUnityCG.cgincでdefineされています。

実は頂点シェーダで出てきたUNITY_TRANSFER_FOGはモバイル版が複雑だったので無視してPC版を載せたのですが、今度はPC版が複雑なのでモバイル版を載せます。

それでもdefineが3段階に分かれていて超ややこしいです…。ある意味最後に相応しいかもしれません。

#define UNITY_APPLY_FOG(coord,col) UNITY_APPLY_FOG_COLOR(coord,col,unity_FogColor)
↓
#define UNITY_APPLY_FOG_COLOR(coord,col,fogCol) UNITY_FOG_LERP_COLOR(col,fogCol,(coord).x)
↓
#define UNITY_FOG_LERP_COLOR(col,fogCol,fogFac) col.rgb = lerp((fogCol).rgb, (col).rgb, saturate(fogFac))

ちなみに省略したのは、フォグのMode(Linear、Exponential、Exponential Squared)による違いが出る部分です。モバイルでは高速化するため頂点シェーダで事前に計算行い、PCでは精度を高めるためにフラグメントシェーダで計算を行うようです。

このままでは意味が分からないので、こちらもパズルを解いて展開します。僕もフォグの処理内容を見るのは初めてです。

col.rgb = lerp(unity_FogColor.rgb, col.rgb, saturate(i.fogcoord.x));

かなりシンプルになりました!

unity_FogColorは、フォグの設定ウィンドウで設定した色が渡されます。

lerp

まずlerpですが、線形補間を行う組み込み関数です。厳密には違いますが計算イメージはこんな感じです。

// start * (1 - t) + end * t; 
lerp(start, end, t)

計算例です。

lerp(0, 2, 0.5)  // 1
lerp(5, 10, 0.5) // 7.5
lerp(2, 4, 0)    // 2
lerp(2, 4, 1)    // 4

フォグのように色を計算する場面ではよく登場するので、覚えておきましょう。

saturate

受け取った値を0〜1の間におさめてくれる組み込み関数です。

マイナスの値は0に、1を超えた値は1に、それ以外の値はそのまま返します。

saturateに渡しているi.fogcoord.xは頂点シェーダの段階で座標変換後のz値を格納したものでしたから、奥行き値を0〜1に収めている処理です。

テクスチャ色に寄せるかフォグ色に寄せるか

以上を踏まえると、奥行き値に応じて

  • サンプリングしたテクスチャ色に寄せるか
  • 設定したフォグ色に寄せるか

これを判断して最終的な色を決定する処理ということが分かりました!

順番、逆じゃない?

ところでもう一度lerpを見てみましょう。

lerp(unity_FogColor.rgb, col.rgb, saturate(i.fogcoord.x));

0が手前で1が奥なら unity_FogColor.rgb と col.rgb の順番は逆じゃないの?と思った方もいるでしょうか。

はい。これは僕が省略してしまった部分です。ごめんなさい…。

超絶ややこしいのでプログラムは省略することに変わりはないのですが、説明はちゃんとします。

Nearクリップ、Farクリップ

頂点シェーダで座標変換した後のxとyの値は-1〜1でスクリーンの端から端までに対応していました。

ではz値はどのような値で-1〜1に収まっているのでしょうか?

これは、カメラで設定するClipping PanelsのNearクリップ、Farクリップが関係しています。

Nearクリップ・FarクリップNearクリップ・Farクリップ

この設定であれば、カメラからの距離が0.3〜1000に収まっているものだけ描画します。

座標変換後のz値は、これと対応しています。

ちなみにマイナス方向は使わず、0〜1になります。(後述しますがプラットフォームにより変わる)

Unity5.5から挙動が変わった

実はこのz値、Unity5.5から0がFar、1がNearに変更されました。

Unity5.4までは0がNear、1がFarでした。

当時はこの変更でバグが頻発しました…。

なぜこんなややこしい変更が入ってしまったのかはUnityドキュメントのUnity5.5へのアップグレードガイドで解説されています。

Zバッファ(デプスバッファ)の方向が反転されました。つまり、Z バッファの値がニアのクリッピング平面で 1.0、ファーのクリッピング平面で 0.0 になります。これが浮動小数点デプスバッファと組み合わさることでデプスバッファの精度が著しく高まり、その結果 Z ファイティングが削減されシャドウの質が向上します。これは特に、小さなニアのクリッピング平面と大きなファーのクリッピング平面を使用する場合に顕著です。 – Unityドキュメントから引用

ここで登場しているZバッファについては、後ほど半透明描画と一緒に解説します。

Zファイティングというのは2つのポリゴンが同じ位置に重なった時にちらついて見えてしまう現象のことです。

要は反転させることでハードウェア処理的にZファイティングの軽減と影の質が向上する効果を得られるから反転したよ。ということです。

ちなみに反転後かどうかはdefinedで取得できます。

#if defined(UNITY_REVERSED_Z)

OpenGL環境では更に挙動が異なる

Unityを使っていても、最終的にハードウェアに合わせてDirectX、OpenGL、Metalなどプラットフォームに合わせたグラフィックスAPIに変換されます。

Unity本体(C#側で実行する処理)も含めDirectXやMetalは座標が左手座標系で扱われてZ値のプラス方向が奥になります。

OpenGL系統は右手座標系でZ値のマイナス方向が奥です。

Unityにおいても、シェーダがOpenGL系統で実行される場合はシェーダ内のみ右手座標系になります。

最近のiOSやMacはMetalが動きますが、まだOpenGL系も現役なので考慮しないといけません。

OpenGL系ではUnity5.5以降、Farが0でNearが-1です。

フォグの処理ではUnity5.5以降かどうか、OpenGL系かどうかも考慮して0がFar、1がNearとして扱えるように変換されていたわけです。そのため非常にややこしい分岐が発生していてコードは載せませんでした。

こんなことを気にしないといけないのはあくまでフォグのように座標変換された後のz値を使わないといけない場合です。

通常シェーダを書く際はUNITY_MATRIX_MVPの行列がこのあたりのプラットフォームによる差異を全て吸収してくれているので気にする必要はありません。とはいえここが原因で悩んでしまうこともあり得るので覚えておいて損は無いでしょう。

(2020/01/21追記) C#側は左手座標系の前提で問題ありませんが、C#側で作成した行列をシェーダに渡す時は注意が必要です。詳しくはこちらを参照して下さい。

以上でUnlitShaderの解説は終了です。お疲れ様でした!

これで少なくともUnlitShaderの中に出てくる要素についてわからないことは1つも無くなったはずです。

シェーダに手を付け始めた頃の僕も含め、シェーダをある程度書いている人でも個々の要素についてはなんとなーくの理解で済ませていることが多いはずです。

これからは「シェーダ完全に理解した」と言ってしまいましょう(笑)

ここから先は応用の内容に入っていきますが、とても重要なのでよろしければ最後までお付き合い下さい。

半透明描画用にカスタマイズ

新しくシェーダを作成

それでは新しくUnlitShaderを作成し、半透明用に内容を書き換えていきます。

名前は「AlphaBlendUnlit」としました。

Shader "Unlit/AlphaBlendUnlit"

透明度のプロパティを追加

Propertiesブロックに透明度を制御するパラメータを追加します。

Properties
{
    _MainTex ("Texture", 2D) = "white" {}
    _Alpha ("Alpha", Range(0, 1)) = 1
}

正確には不透明度です。0〜1で設定したいので、Rangeにしました。

このプロパティをシェーダで受け取るためにグローバル変数も用意します。

half _Alpha;

floatほどの精度は必要ないので、half型にしました。

フラグメントシェーダで計算

テクスチャサンプリングした後の色のα値に対して、_Alphaを乗算します。

half4 col = tex2D(_MainTex, i.uv);
col.a *= _Alpha;

乗算を行うため、精度によるズレが発生しないようfixed4をhalf4に変更しました。とはいえ誤差レベルなのでfixed4のままでも問題ないと思います。

ブレンドモードの変更

透明度の計算はこれで終了です。しかしここまでの内容だけではフラグメントシェーダから出力したα値は特に利用されません。

半透明にするには、ブレンドモードを変更しアルファブレンドが行われるようにする必要があります。

以降はSubShaderの冒頭に記述を追加していきます。

LODは特に使わないので削除しました。RenderTypeもTransparentにしておくとパフォーマンスが上がるので変更しておきます(Opaqueのままでも、半透明描画自体は可能です)。

Tags { "RenderType"="Transparent" }
Blend SrcAlpha OneMinusSrcAlpha

アルファブレンドとは

SrcAlpha? OneMinusSrcAlpha? これでは全く意味が分からないので、まずはこのBlend指定の形式について見ていきます。

Blend [SrcFactor] [DstFactor]

SrcFactor、DstFactorと言われてもまだ意味が分からないですね。

ここを理解するには 最終的に出力される色がどう計算されるのか から逆算すると分かりやすいです。

最終的に出力される色

フラグメントシェーダの出力 * SrcFactor + 既に画面に描画されている色 * DstFactor

SrcFactorは フラグメントシェーダの出力をどう使うか

DstFactorは 既に画面に描画されている色をどう使うか を決めるための係数です。

最終的に出力される色は、これらの係数が掛けた値を足し合わせたものです。

それでは改めてアルファブレンドの指定を見ていきましょう。

Blend SrcAlpha OneMinusSrcAlpha

SrcAlphaは フラグメントシェーダから出力されたα値

OneMinusSrcAlphaは 1 – フラグメントシェーダから出力されたα値 です。

これを計算式に当てはめてみます。

最終的に出力される色

フラグメントシェーダの出力 * α値 + 既に画面に描画されている色 * (1 – α値)

この計算式、なんとなく見覚えありませんか?

// start * (1 - t) + end * t
lerp(start, end, t)

// +の左側と右側を反転したら…

// end * t + start * (1 - t)
フラグメントシェーダの出力 * α値 + 既に画面に描画されている色 * (1 - α値)

そう、lerpです!!

つまり

アルファブレンドとは

既に描画されている色とフラグメントシェーダの出力色を、α値を係数にして線形補間するブレンド方法

アルファブレンドを指定したことで無事に半透明で描画されるようになりました。

他のブレンド方法

SrcFactorとDstFactorには以下のものが利用できます。

One1
Zero0
[OneMinus]SrcColorフラグメントシェーダ出力色
[OneMinus]SrcAlphaフラグメントシェーダ出力α値
[OneMinus]DstColor既に描画されている色
[OneMinus]DstAlpha既に描画されているα値

組み合わせについては、よく使うものだけ覚えておけば基本的に問題ありません。むしろ覚えなくてもどこかにメモしておけば大丈夫です。

参考 Unityマニュアル – Blending

アルファブレンド(Alpha Blended)
Blend SrcAlpha OneMinusSrcAlpha
加算合成(Addtive)
Blend One One

それぞれ1を掛けるということは、そのまま足すということです。

明るい表現ができることから、エフェクトでよく使われます。

ソフト加算合成(Soft Addtive)
Blend OneMinusDstColor One

加算合成だと眩しくなってしまうことが多いため、既に描画されている色も考慮しつつ加算する合成方法です。

乗算合成(Multiplicative)
Blend DstColor Zero

単純に色をかけ合わせます。もともと描画されていた色に馴染ませる効果があります。

乗算合成したもの同士を足す(2x Multiplicative)
Blend DstColor SrcColor

1以下の数で掛け算をするということは必ず数が小さくなる(色が暗くなる)ので、足し合わせることで明るさを維持する効果があります。

通常の乗算合成通常の乗算合成
乗算合成 + 乗算合成乗算合成 + 乗算合成

Zバッファ

半透明描画はできるようになりましたが、これで終了ではありません。

このように手前にあるものを半透明にすると、奥にあるものがくり抜かれてしまいます。

奥にあるものがくり抜かれてしまう奥にあるものがくり抜かれてしまう

この現象はZバッファによって引き起こされています。

Zバッファ(デプスバッファ)とは

フラグメントシェーダから出力され、ブレンド計算した後の色はそのままスクリーンに反映されるわけではありません。

まずカラーバッファというピクセル単位で色を格納するための領域にコピーされ、全ピクセルの色情報を格納し終わった後にまとめてスクリーンに反映されます。

実はカラーバッファの他にZバッファというものも用意されています。

Zバッファには色情報ではなく、座標変換した後のz値が格納されます。

座標変換後のz値は、0がFarで1がNearでしたね。この値がそのまま入っています。

カメラにClearFlagsという設定項目がありますが、これは対象カメラの描画を実行する前に初期化するバッファを指定するものです。

Clear FlagsClear Flags
SkyBoxカラーバッファはスカイボックスの色で、Zバッファは0で埋める
Solid Colorカラーバッファは指定色で、Zバッファは0で埋める
Depth onlyカラーバッファには手を付けず、Zバッファは0で埋める
Don’t ClearカラーバッファもZバッファもそのまま

Zバッファを使って何をしているのかというと、奥にあるオブジェクトが描画されてしまうことを防いでいます。この処理をZテストと言います。

Zテスト(デプステスト)

Zテストの内容はSubShader内に記述することで変更することができます。省略した場合は「LEqual」になります。

ZTest LEqual

LEqualは Less or Equal の略なので、値が小さいか等しかったら。という意味です。

既にZバッファに格納されている値を dstZ これから描画しようとしているピクセルのz値を srcZ とすると、LEqualは次の条件を満たした時に成功になります。

LEqual

srcZ >= dstZ

Unity5.5以降はZバッファの値が反転していますが、Zテストのタイプ名は変更されていないので意味が逆になっている点に注意して下さい。

1がNearなので、描画しようとしているピクセルのほうが等しいか近くにある場合は成功。ということになります。

Zテストが成功したときのみ、カラーバッファとZバッファに新しい値が格納されます。

その他のZテスト

Less

例えば近いものを描画したいなら Less or Equal ではなくて Less だけでもいいのでは?ということでちゃんと用意されています。

ZTest Less

しかし、これはZファイティングが起きやすく、使いづらいです。

ZファイティングZファイティング

そのため、デフォルトはLEqualです。Zバッファが反転した効果もあり、Zファイティングはかなり起きにくくなっています。

Greater(GEqual)

Lessと条件が反転します。手前にオブジェクトがある時だけ奥のものが描画されることになるので、ちょっと特殊な表現が可能です。

Always

Zテストを必ず成功で返します。隠れていても必ず描画されるといった表現が可能です。

描画順の話

勘の良い方は気づいているかもしれませんが、奥にあるものを描画したくないなら奥から手前に向かってオブジェクトを描画すれば奥のものは自然に隠れてくれます。

あれ、Zテストいらない…?

いえいえ、ちゃんとZテストには意味があります。

Zテストが失敗した時は、フラグメントシェーダがそもそも実行されません

奥から手前に向かって描画する場合、一度塗ったのに後から上塗りされて隠れてしまうようなピクセルに対してもフラグメントシェーダを実行することになります。

手前から奥に向かってZテストを実行しながら描画すれば、不要な処理を省略することができます。これによる負荷軽減効果はとても大きいです。

そのため、Unityのレンダリングパイプラインではオブジェクトは手前から奥に向かって順番に描画されます。

Zテストと半透明は相性が悪い

最初の奥のものがくり抜かれてしまう現象に戻りますが、Zテストと半透明は相性が悪いです。

Zテストは「奥のオブジェクトは手前のオブジェクトで隠れてるはず」という前提に基づいて実行しています。

そのため半透明にした場合でも奥が見えて欲しいのにZテストに失敗してくり抜かれてしまうわけです。

レンダーキューの設定

そこで、半透明オブジェクトを描画する時は奥から手前に向かって描画するようにします。

これは、レンダーキューの設定によって実現できます。

SubShaderのTagsに追加します。

Tags
{
    "Queue"="Transparent"
    "RenderType"="Transparent"
}

レンダーキューは、マテリアルの設定で上書きすることもできます。Inspectorから設定できるこの項目ですね。

レンダーキューレンダーキュー

シェーダで設定していない場合はデフォルトのGeometryで2000ですが、Transparentに変更したことで3000になりました。

レンダーキューは、オブジェクトの描画順を制御するためのものです。

シンプルに、数字が小さいものから順番に描画される仕組みです。

2500までと2501以降では挙動が違う

2999までと、3000以降(Transparent)では描画順のソート方法が異なります。

2500までは、全く同じレンダーキューの値であれば手前から奥に向かってソートします。

2501以降は、全く同じレンダーキューの値であれば奥から手前に向かってソートします。

※(2020/6/7修正)2500までと、2501以降でした。3000番にTransparentの名前が付いているので騙されましたが、「Transparent-100」といった表記でも半透明として扱われるように余裕をもたせているのだと思います。

奥のものも見えるように

これで半透明描画は無事にできるようになりました。奥のものがちゃんと見えています。

奥のものも見えるようになった奥のものも見えるようになった

しかし、使い方によってはこれで不都合が起きてしまうことがあります。

ソートはオブジェクト単位である

こちらはAssetStoreから拝借した家のモデルを半透明にして、更にその中に半透明のボックスを入れたものです。

ボックスを前後に動かすと、ある一定の位置からボックスが消えてしまうのが分かると思います。

なぜこんなことが起きてしまうのかというと、同一レンダーキューにおける奥から手前のソートはオブジェクト単位で実行されるためです。

ソートの基準になる座標はモデルの中心なので、家のように大きいモデルだとはっきりと不都合が出てきます。

レンダーキューの調整

簡単な対応法としてはレンダーキューを調整することです。

家の方に適用しているマテリアルのレンダーキューを3001に変更し、ボックスよりも後に描画されるようにします。

レンダーキューを3001に変更レンダーキューを3001に変更

ちなみにシェーダ側のTagsで3001にする場合は以下のように書きます。

"Queue"="Transparent+1"

こうすることでかなり手前に持ってきてもちゃんと描画されます。

手前に持ってきても描画されるようになった手前に持ってきても描画されるようになった

しかし更に問題は残ります。

まずボックス自体も半透明なので、ボックスが先にZバッファ書き込みを行ってしまうことでボックスより後ろの部分については描画されなくなります。

ボックスより奥の部分は描画されないボックスより奥の部分は描画されない

このように家の中に入っている状態であれば見た目的にそんなに違和感を感じないので問題ないかもしれませんが、はっきりと違和感が出てしまうのはボックスを家よりも手前に持ってきた時です。

ボックスが家の外に出ている時ボックスが家の外に出ている時

これでは半透明というよりも色が薄くなっただけのように見えてしまいますね。

家よりも手前にきている時は、ボックスのレンダーキューを家のレンダーキューよりも上げた方が自然に見えます。

残念ながら、レンダーキューを良い感じに自動調整してくれる機能はありません。

レンダーキューの調整は手動か、C#スクリプト側で条件を付けて調整してあげる必要があります。

Zバッファ書き込みの設定

いよいよ最後の項目です。

レンダーキューを調整するのは大変ですし、これまで解説してきたような知識を持っていなければレンダーキューの調整という発想すら出てきません。

そこでUnityのビルトインシェーダに含まれる半透明系のシェーダでは、Zバッファの書き込みをしないという選択をとっています。

これはSubShaderのオプションに一行追加するだけで簡単に設定できます。

ZWrite Off

しかしこれでも問題は残ります。

ソート順の影響は残る

まず、Zバッファの書き込みをやめても結局ソート順の影響は受けてしまいます。

ソート順の影響は受けるソート順の影響は受ける

アルファブレンドはα値を利用して既に描画されている色との線形補間を行うものでしたね。

Zバッファを書き込まないということは、後から描画されるオブジェクトが自分のα値を使って色を決めてしまうということになります。

後から描画されるオブジェクトのα値が1なら既に描画されている色は一切考慮されません。

それでもオブジェクトが消失してしまうよりはまだ良いかもしれません。

見えて欲しくないところまで見える

Zバッファは同一オブジェクト内におけるポリゴン同士の前後関係を正しく表示することにも役立っています。

Zバッファの書き込みをやめてしまうことで同じメッシュ内の全てのポリゴンがZテストに成功します。

α値を1まで上げると本来は不透明なはずですが、見えないはずの奥の柱なども見えています。

ZWrite OnZWrite On
ZWrite OffZWrite off

複雑な形状をしたモデルであればあるほど、エグい見た目になってしまいます。

結局どうしたらいいの?

残念ですが、これはケースバイケースとしか言えません。

半透明を自然に見せることについては、シェーダ単体で出せる完全な解決法がないのです。

レンダーキューを調整したり、シェーダを使い分けていく、その他ゲームで見せたい画に合わせて工夫していくことが必要です。

僕はZバッファの書き込みをやめることは極力避けて、レンダーキューの調整で頑張るようにしています。見た目的にもよくないしモデラーの立場から考えても見えて欲しくないところが見えるのはイヤですからね。

これはあくまで3Dオブジェクトを半透明にしようとした場合の話で、2Dの場合はZバッファの書き込みはオフで問題ありません。

こういった諸問題や、色を何度も塗り直す関係でパフォーマンスへの影響も大きいことから、半透明は避けた方が良いというのはよく言われることですね。

Inspectorからも設定可能にする

それでは最後にZバッファの書き込みをマテリアルのInspectorからOn/Offできるようにしておきましょう。

先述した通り、ケースバイケースで対応できるようにするためです!

Unityのビルトインシェーダに合わせて、デフォルトはOffにしておきます。

Propertiesブロックに以下を追加します。

[Toggle] _ZWrite ("ZWrite", Float) = 0

SubShaderのZバッファ書き込み設定を以下のように変更します。

ZWrite [_ZWrite]

Toggle属性はマテリアルのInspector上にチェックボックスを表示して、オフなら0、オンなら1を対象プロパティに設定してくれます。

プロパティはオプションに対しても [ ] で囲むことで使用できます。

ZWriteの設定は0がOffで0以外ならOnに対応するので、これでデフォルトはOffに、チェックを付ければOnになります。

Zバッファ書き込み設定の追加Zバッファ書き込み設定の追加

以上で半透明なUnlitシェーダは完成です!

AlphaBlendUnlit全文

最後に今回作成したシェーダの全文を載せておきます。

シェーダ参考サイト

今回の内容からのステップアップ、理解を深めるために良い記事などをいくつか紹介しておきます。

シェーダの情報を探す時は、最初に解説したレンダリングパイプラインは何を使っているのか、サーフェスシェーダの内容なのか、頂点フラグメントシェーダの内容なのかといったことを見落としてしまうと混乱します。

今回はUnity標準のレンダリングパイプラインで、Forwardレンダリングでした。

Forwardレンダリングを使っているのにDeferredレンダリングの内容を参考にしようとすると落とし穴にはまります。例えばラスタライズの概念は、Forwardレンダリングにしかありません。

またシェーダはややこしい概念が多いため、誤情報もよく見かけます。十分注意して下さい。

エフェクト統一シェーダがあると捗るよね

SEGAさんの技術ブログ記事です。シェーダバリアントを活用して複数の機能を1つのシェーダにまとめる知見が得られます。

LIGHT11

記事の中でもいくつかリンクを貼らせて頂いたブログです。分かりやすいUnityシェーダの記事が多いのでオススメです。

Renderingタグからレンダリング関連に絞って記事一覧が見れます。

今回はライトの影響を考慮しないUnlitShaderを解説しました。ライティング関連の記事も多いのでライティングについて学びたい場合は特にオススメです。

7日間でマスターするUnityシェーダ入門

サーフェスシェーダを利用して色々なシェーダを作る内容です。サーフェスシェーダについても知っておきたい方はもちろん、作ったシェーダは頂点フラグメントシェーダに応用することも可能です。

グラフィックスパフォーマンスの最適化

Unityドキュメントです。モバイルやVRなど、パフォーマンスが要求される環境に向けてシェーダを書く場合は必読です。

CGエンジニア検定/コンピュータグラフィックス

僕が書いたCGエンジニア検定の紹介記事です。検定そのものは学生さんならオススメですが、この中で紹介している本『コンピュータグラフィックス』は今回解説したようなレンダリングの基礎知識が詰まっているので、体系的にCG技術について学んでみたいと思った方はぜひ。

また『数学ガールの秘密ノート』シリーズも紹介していますが、今回は省略した数学(ベクトルや行列など)を理解するのにとても役立ちます。数学をある程度理解しておくことでシェーダを書く効率が上がるのは間違いないので、苦手という方にはオススメです。

ゲーム制作者になるための3Dグラフィックス技術改訂3版

グラフィックス関係の本では2019年12月に発売されたこちらの本も非常におすすめです。ゲーム制作でよく使われる実践的なテクニックが詰まっています。

あとがき

この記事には、これからシェーダを学びたいと思っている人がいたら「これ読んで!」と言うつもりで僕の持っている知識を詰め込みました。

もしこの記事が皆さんのシェーダ理解に役立てたのなら何よりです。内容が良かったらぜひ他の人にもオススメしてもらえると嬉しいです。

気分次第ですが続き(ライティング関連・ポストエフェクトetc)をいずれ書くかもしれません。

こんな長い記事を最後まで読んで頂きありがとうございました!