はじめに
この記事は連載記事です。過去に説明した扱いになっている部分については深く触れません。
記事を読むにあたって**レンダリングパイプラインでどのような処理を行っているのか?**を知っている事は前提になります。
非常に分かりやすい資料がありますので、レンダリングパイプラインを知らない方は読む前にご覧ください。
Unity道場 2019.2 シェーダを書けるプログラマになろう #1 シェーダを理解しよう
Unity道場 2019.2 シェーダを書けるプログラマになろう #2 GPUの神秘
アーカイブ
Unity、Shaderことはじめ【Shader : 0】
Unity、UVScroll(UVの処理)をやってみる【Shader : 2】
概要
UnityのUnlitShaderのコードをしっかりと追いかけます。
UnlitShaderとは、陰影を付ける処理を行わず、テクスチャの色をそのまま表示する非常にシンプルなShaderです。
まずは基礎として最もシンプルなShaderをちゃんと理解する所からはじめます。
ShaderLabとHLSLの関係
UnityのShaderで使われてる言語はShaderLabと呼ばれています。
そしてShader本体となる部分である CGPROGRAM
からはじまり ENDCG
で終わるまでの間はHLSLで書かれています。
HLSL
HLSLとはDirectXで使われているシェーダー言語です。
そもそも、DirectXとは…Microsoftが作ったゲーム・マルチメディアの処理に必要なAPIの集合です。
APIとは何だ…?という話ですが、APIはアプリケーションプログラミングインターフェースです。
端的に言って**「アプリケーションインターフェース?は?」**という感じなので、相関図を書いてみます。
※DirectX8の時にプログマブルシェーダー登場ただし言語がプリミティブだった。DirectX9になって言語がバージョンアップしてHLSLとして登場した。
まず、そもそもプログラムに色を指示する命令があったとしても、ハードがそれに対応してなければ意味がありません。
白黒のドットしか映せないハードに対して、色を表示する命令を送っても、色は表示されないわけです。
つまり、ハードを作るにも、プログラムを作るにも、両者が対応している仕様で作る必要があるわけですね。
そこで出てくるのがAPIというやつで…インターフェースというとわけわかんないですが**「情報をやり取りするルール」**です。
例えば、ジャンケンで考えてみます。
ジャンケンで使う**「手の形と形の名称」を決めます。これがハードの仕様にあたります。
そして、ハードの仕様が分かってるので「グーはチョキに勝つ、パーはグーに勝つ、チョキはパーに勝つ、同じ場合は引き分け」**という風にソフトを実行すると、ジャンケンができるわけです。
この取り決めがAPIなわけです。
そして、色々な人がジャンケンAPIに合わせてジャンケンをすると、知らない人ともお互いにジャンケンができて、勝敗を決める事ができるというわけです。
ジャンケンAPIのライブラリを持ってない人とはジャンケンができません。この場合は相手にジャンケンのルールを教える事で、APIをインストールするという事です。
ちょっと長くなりましたが…
DirectX(API)とはハードと、ハードを動かす為のプログラムを繋ぐ取り決めという事です。
例えば、色に関する命令の定義とか、データ構造とかを決めて、受けた命令をハードでどう解釈すればいいのか理解できるので、ちゃんとレンダリングできるという事。
そして、DirectX7までは固定シェーダーだったのが、DirectX8でプログラマブルシェーダーになりました。
その際に シェーダーを命令する為の言語として作られたのがHLSL というわけです。(長かった)
参考リンク
アプリケーションプログラミングインタフェース
Microsoft DirectX
High Level Shading Language
ShaderLab
それに対して**「ShaderLabとは何だ?」という話があります。
しれっと、UnityのShaderを選択するとインスペクターにShaderに必要なパラメータや、テクスチャが表示されるわけですが、それらはUnityに依存している固有の機能**です。
Properties
{
// Unlit/texture の場合だとテクスチャ1枚だけのPropertiesが決められている。
_MainTex ("Texture", 2D) = "white" {}
//下記のように記述すれば、色(color)やfloatを入力にする事ができる。
_Value ("Value", Range(0.0, 1.0)) = 0
_Color ("Color", Color) = (0, 1, 0, 1)
}
このProperties
の部分は、記述するとテクスチャやパラメータをshaderに必要な入力として受け取れるようになります。
ここがShaderLab シンタックスと呼ばれる領域で…要するにHLSLだけではUnityのエディタからパラメータや、テクスチャなどの入力を渡せないのをHLSLを拡張したShaderLabにすることで、より取り回しをよくしているわけです。
Propertyの他にも…下記のように色々あります。※まだまだあります。
//ポリゴンのカリングの設定
Cull Off
//デプスバッファテストモードを設定
ZTest Greater
//デプスバッファ書き込みモードを設定。
ZWrite On
HLSLを含んでShaderLabですが、HLSLとShaderLab固有の機能の決定的な違いは、HLSLはプログラマブルな制御に対して、ShaderLabの機能はあくまで設定を記述しているだけという点です。
例えば、HLSLの結果を使って動的にPropertyを変更する事はできません。
一応、C#からはPropertyを変更する事はできますが、これはあくまで外から設定を変えてるだけであって動的な処理ではありません。
本来、バーテクスシェーダーとフラグメントシェーダーを指す意味でのシェーダーが制御できる範囲を超えて、shaderLabでは色々な設定をできるようにしてくれているわけです。
これらの設定についてはshaderの責任範囲外でありますが、現実的に運用するにあたってほぼほぼ必要になりますし、shaderを決める側としても設定できた方が便利なわけです。
参考リンク
ShaderLab シンタックス
HLSL スニペット
HLSLとShaderLabの違いが分かった所で…本体となるUnlitShaderのHLSLを追っかけていきます。
まず最初にあるのが #pragma
ではじまる部分です。
HLSLスニペットと呼ばれる部分にコンパイラーディレクティブを書いてるらしいですが…重要なのは、ここの役割です。
#pragma vertex hogevertex
#pragma fragment hogefragment
とした場合…
hogevertexという名前の関数はバーテクスシェーダーとして解釈され、hogefragmentという名前の関数はフラグメントシェーダーと解釈されます。
この部分では**「どの関数をどう解釈するか」という事を宣言しています。
デフォルトのUnlit/Textureではfragがフラグメントシェーダーとしてvertがバーテクスシェーダーとして**宣言されています。
それによって、レンダリングパイプラインの実行時に、レンダリングを行う側がそれぞれのタイミングで関数を呼び出します。
HLSL スニペット
その下にある #include "UnityCG.cginc"
は関数の読み込みです。
float4
とか UnityObjectToClipPos
というオブジェクト空間からカメラのクリップ空間への変換を行ってくれる関数などがあります。
この読込をすると、そのプログラム上ではUnityCG.cgincに書かれている様々な関数や変数が使えるようになります。
ビルトインシェーダーヘルパー機能
ビルトインのシェーダー include ファイル
UnlitShaderの処理
HLSLの部分
**めちゃくちゃ読みづらい!**の申し訳ない。
sampler2D _MainTex;
処理を追いかける前に、この部分。
先ほど、ShaderLabとHLSLの違いについて触れましたが、当然レンダリングする時にHLSLの中にテクスチャの情報が含まれてないといけないわけです。
なので、Propertyで割り当てた値はちゃんとHLSLの中で再度宣言する必要があります。(バーテクスシェーダーとフラグメントシェーダーの処理より上に書いた方がいいです。)
ShaderLabはあくまでUnityが操作できるにようにしてる設定値であって、**レンダリングの処理に必要な事はHLSLの中に全部書かないといけない。**というのをお忘れなく…。
そして、レンダリングの処理は主に4つの段階に分かれていると思います。
1.struct appdata では、下記の情報を保存する構造を定義します。
float4 vertex : POSITION
で「頂点の空間座標」
float2 uv : TEXCOORD0
で「頂点のUV座標」
2.struct v2f では、下記の情報を保存する構造を定義します。
float4 vertex : SV_POSITION
で「頂点のクリップ座標」
float2 uv : TEXCOORD0
で「頂点のUV座標」
※appdataの時は、純粋にモデルの形だけが保存されてますが v2f ではモデルをカメラで写した時のスクリーン上での座標に変換されるわけです。
v2fはバーテクスシェーダーでワールド座標からスクリーン上の座標に変換された後の構造という事です。
3.そしてバーテクスシェーダーで appdata を v2f に変換しフラグメントシェーダーに渡します。
4.最後に、フラグメントシェーダーはバーテクスシェーダーから受け取ったv2fの情報を使ってピクセルの色を決めてます。
疑問点になりうる所を解説してゆきます。
「struct appdata の中でfloat4は配列じゃないのにオブジェクトの情報が保存できるの?」という所ですが float4 vertex : POSITION
と表記される事で、ハードウェア側でただのfloat4ではなくオブジェクトの頂点座標と解釈されるます。
そして、実行時にはfloat4はオブジェクトの全ての頂点配列として保持されます。
UVについても同様です。
このように…変数に意味・役割をつける事で、ハードウェアに変数に意味・役割を伝える構文をシェーダーセマンティクスといいます。
シェーダーセマンティクス
次に**「フラグメントシェーダーがどうしてピクセルの色を塗れるの?」**という所ですが…
まず、バーテクスシェーダーから渡されるv2fには、スクリーンにおけるオブジェクト(頂点)の位置と、各頂点が参照しているUV座標が保持されています。
なので、スクリーンで色を塗るべき位置は、頂点の情報を参照すれば分かりますし、テクスチャの色は頂点が参照しているUV空間を見てくればいいわけです。
(フラグメントシェーダー実行→v2fの頂点位置→頂点が持ってるUV情報→UV空間にアクセス、テクスチャの色が参照できる)
※FOG、LOD、Tag について触れてませんが、あまりUnlitShaderの処理そのものとは関係ない部分なので省略します。
おわり
これで、1番基本と思われる UnlitShader/texture を読み終えました。
DirectXの本とか見てるともっと大変そうなので、3Dのレンダリングは相当大変な事なんだなと…。
次回以降は、UnlitShaderをベースにカスタムなんかできたらいいなと思います。