シェーダーをいつかちゃんと勉強しなきゃと思ってる間に、UnityのアプデでSRPが出た!とか描画に関する大きな進歩があったみたいで、これはいよいよ後回しにしてると追いつけなくなるぞと思い、話題の HLSLシェーダーの魔導書 を読み始めました。
▲この本
本当にイチから説明があり、シェーダー初心者の自分にも分かりやすいです!
ただ、この本はあくまでHLSLシェーダーの本で、サンプルコードはWindowsアプリケーション上で動かすように作られているため、そのままUnityで記述することはできません。(アルゴリズムや基本的な関数などは丸々参考にできます。誤解なきよう)
また、Unityには独自に用意されたHLSL関数群があり、例えばライトの取得などの方法も当然この本で記載されてるものと異なります。
ということで、
- 本で得たシェーダーの知識をUnityで実装をしてみる
- 次世代モバイルの標準になるURPの書き方にする
の2つの目的で諸々まとめました。
同じくシェーダー初心者のUnityエンジニアの足がかりになれればと思います。
そもそもURPとは?
Universal Render Pipeline
の略称です。
「Render Pipeline」とは、ざっくり言うと 3Dの描画の手順のこと で、ゲームエンジンごと、ツールごとに異なります。
Unityも独自のレンダパイプラインで3Dを描画していましたが、近年のアップデートで、大幅な改善が入ったHDRP
とURP
というレンダパイプラインが追加されました。
URP
はモバイル機を意識した軽量なレンダパイプラインで、これからのゲーム開発はとりあえずURPで作った方が良いらしいということだけ知っていれば十分だと思います。
詳しく知りたい方はこちらがオススメです!
ちなみにHDRP
は次世代ゲーム機を意識した超高機能なレンダパイプラインです。
HDRPの環境下で作られたUnityのデモ動画がこちら。
すげえ。
URPのプロジェクトの作り方
- 新規プロジェクト作成時に「Universal Project Template」を選択する
- Package Managerから「Universal RP」を検索してインストールする
のどちらかで作れます。
URP版のUnlitシェーダー
シェーダーを作るときは、上図のような「ただモデルにテクスチャを貼り付けるだけ」の最低限のシェーダーであるUnlit Shader
から書き始めると、シンプルなテンプレートから必要な機能を載せていけるので実装しやすいです。
Unityが提供してるUnlit Shader
のテンプレートを Create ▶︎ Shader ▶︎ Unlit Shader から新規作成することができます。
ただしこれは旧式のレンダパイプラインである、ビルトインレンダリングパイプラインでも動作する記述になっていて、URPに最適化された記述になっていないです。URPのメリットを享受できないことにもつながるため、まずはURP版のUnlitシェーダーを作ります。
Shader "Custom/Unlit"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags {
"RenderType"="Opaque"
"RenderPipeline"="UniversalPipeline"
}
LOD 100
Pass
{
Name "ForwardLit"
Tags { "LightMode"="UniversalForward" }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float fogFactor: TEXCOORD1;
float4 vertex : SV_POSITION;
};
TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);
CBUFFER_START(UnityPerMaterial)
float4 _MainTex_ST;
CBUFFER_END
v2f vert (appdata v)
{
v2f o;
o.vertex = TransformObjectToHClip(v.vertex.xyz);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.fogFactor = ComputeFogFactor(o.vertex.z);
return o;
}
float4 frag (v2f i) : SV_Target
{
// sample the texture
float4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
// apply fog
col.rgb = MixFog(col.rgb, i.fogFactor);
return col;
}
ENDHLSL
}
}
}
こちらのサイトを参考にさせてもらいました。
落ち影に関する記述も引用するとコードが長めになってしまうので、そちらは今回は取り除いています。
このシェーダーをモデルに反映するとテクスチャの色がそのまま反映され、陰影がなく立体感は表現されていないことが分かると思います。
備考:URPで用意されているUnlitシェーダー
URPのUnlitシェーダーとしてはUniversal Render Pipeline/Unlit
が用意されています。
ただ、300行にも渡る長いコードになっていてここから触り始めるには「おまじない」が多すぎるため、今回は最低限のコード量のテンプレートとして上記使います。
URP版Diffuseシェーダー
Diffuse
は、光の角度とモデルの面の向き(法線ベクトル)で影の暗さを決める、最もシンプルなシェーディングです。
Unity標準シェーダーにMobile/Diffuse
がありますが、これはURP環境下では動きません(シェーダーエラーでお馴染みの真っピンクになる)
入門にちょうどいいので、さっきのUnlitシェーダーをベースにまずはこれを実装しつつ、シェーダーの書き方をまとめてみます。
①Lighting.hlsl
をinclude
まず、URPでライトを取得するための関数が用意されているLighting.hlsl
をincludeします。
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" // <- 追加
#include
では、計算に使う関数などがまとまっているファイルをシェーダーに含めることを宣言しています。
URP以前のレンダパイプライン(ビルトインレンダパイプライン)では、UnityCG.cginc
などのファイルをincludeしていましたが、URPではcom.unity.render-pipelines.universal
以下のhlslファイルを使うことが多いようです。
②構造体に法線情報を付加
次に、頂点シェーダー(vertメソッド)、ピクセルシェーダー(fragメソッド)で扱う構造体に法線情報を付加します。
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL; // <- 追加
};
struct v2f
{
float2 uv : TEXCOORD0;
float fogFactor: TEXCOORD1;
float4 vertex : SV_POSITION;
float3 normal : NORMAL; // <- 追加
};
appdata
は頂点シェーダーの引数に使う構造体として宣言していて、モデルの情報を格納する変数を定義しています。
v2f
は頂点シェーダーの返り値兼、ピクセルシェーダーの引数に使う構造体として宣言していて、ピクセルシェーダーの計算に使う変数を定義しています。
変数名の横に:
を挟んで記述されているPOSITION
などのキーワードは、**「セマンティクス」**と呼ばれるもので、「3Dモデルのどのデータを使用するか」を指定するものです。
▼セマンティクスの例
名前 | 説明 |
---|---|
POSITION | オブジェクトベースの頂点座標 |
SV_POSITION | オブジェクトベースの頂点座標(頂点シェーダーの返り値版) |
TEXCORD | UV座標 |
NORMAL | 法線ベクトル |
▼セマンティクスのリファレンス
③頂点シェーダーからピクセルシェーダーに法線情報を渡す
頂点シェーダーからピクセルシェーダーに渡すデータに法線情報を載せます。
v2f vert (appdata v)
{
v2f o;
o.vertex = TransformObjectToHClip(v.vertex.xyz);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.fogFactor = ComputeFogFactor(o.vertex.z);
o.normal = TransformObjectToWorldNormal(v.normal); // <- 追加
return o;
}
頂点シェーダーは、引数にモデルの情報を持つ構造体を持ち、やりたい表現に合わせてモデルの情報をイジる処理を書く場所です。
例えば、一枚の板状のモデルを波や旗のように揺らめかせるような処理が書けたりします。
頂点シェーダーのメソッド名は#pragma vertex ○○
というような記述で定義してます。
本記事でのコードでは#pragma vertex vert
としてるので、vert
メソッドが頂点シェーダーの記述場所になります。
今回追記するのは陰影の計算に使う法線情報を返り値に載せるという処理です。
TransformObjectToWorldNormal
メソッドを使って法線をワールド空間へ変換した上で代入しています。
④Diffuseシェーダーのアルゴリズムを書く
ピクセルシェーダーで、ライトと法線の向きでできる影を考慮して各ピクセルの色を計算します。
float4 frag (v2f i) : SV_Target
{
// sample the texture
float4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
////////////////////////
// ▼ここから
// ライト情報を取得
Light light = GetMainLight();
// ピクセルの法線とライトの方向の内積を計算する
float t = dot(i.normal, light.direction);
// 内積の値を0以上の値にする
t = max(0, t);
// 拡散反射光を計算する
float3 diffuseLight = light.color * t;
// 拡散反射光を反映
col.rgb *= diffuseLight;
// ▲ここまで追加
////////////////////////
// apply fog
col.rgb = MixFog(col.rgb, i.fogFactor);
return col;
}
ピクセルシェーダーは、引数に頂点シェーダーの返り値の構造体を持ち、モデルの各位置をどのような色にするかを決める処理を書く場所です。
今回のように陰影をつけたり、リムライトと呼ばれる縁取りのような表現、トゥーンシェーディングと呼ばれるアニメ調の塗り方など、絵作りに関する様々な手法をここで表現することができ、HLSLシェーダーの魔導書では「シェーダー プログラミングの花形」なんて言われたりしてしました。
ピクセルシェーダーのメソッド名は#pragma fragment ○○
というような記述で定義してます。
本記事でのコードでは#pragma fragment frag
としてるので、frag
メソッドがピクセルシェーダーの記述場所になります。
陰影については、モデルの法線ベクトルi.normal
とライトの向きlight.direction
の内積を計算して、結果が0に近づくほど暗くなり、0以下では真っ黒になるような計算をしています。
ここまでの実装で、ライトの方向に合わせてモデルに陰影が付くようになります。
▲立体感が出ました
備考:数学関数について
内積を計算するdot
や2つの数値の大きい方を返すmax
など、HLSLには様々な数学関数が用意されています。
Unityのプログラムで言うとMathf.○○
でアクセスできるような関数が揃っています。
リファレンスは下記ページ。
reflect
やrefract
のような光のベクトル計算に向いた関数などもあるみたいです。
環境光を反映
これで陰影は付きましたが、ライトと法線の向きの差が90度未満の面は真っ黒になってしまいます。
現実のライトでは床や壁からの間接光によってメインの光源が届かない部分もそれなりに照らされて、真っ黒な部分というのはあまりありません。
とはいえ間接光を真面目に計算すると膨大な計算量になってしまうため、大まかに間接光を再現するシンプルな手段として、モデル全体に一律で光を当てる
という手法が使われていました。これを環境光(アンビエントライト)と言います。
Diffuseシェーダーに環境光を実装してみましょう。
まず、環境光の色の変数に定義します。
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_AmbientLight ("Ambient Light", Color) = (0.5,0.5,0.5,1) // <- 追加
}
~~中略~~~
...
float4 _MainTex_ST;
CBUFFER_END
float3 _AmbientLight; // <- 追加
v2f vert (appdata v)
{
ピクセルシェーダー(frag
メソッド)の拡散反射光を乗算している部分に、環境光を加えます。
// 拡散反射光を反映
col.rgb *= diffuseLight;
▼▼▼書き換える▼▼▼
// 拡散反射光と環境光を足し算して、最終的な光を求める
col.rgb *= diffuseLight + _AmbientLight;
これで実装は完成です。
Properties
の箇所に記述を追加したことで、マテリアルのInspectorビューにAmbient Light
の項目が追加されました。
色を調整することで、環境光(擬似的な間接光)を設定することができます。
シーンの環境に合わせて色を設定すると自然と馴染むようになります。
上図ではスカイボックスのグレーの部分に近い色を設定してみています。
〆
古典的なDiffuseシェーダーと環境光の実装を通して、Unityでのシェーダーの基礎的な書き方をまとめました。
HLSLシェーダーの魔導書を読み進めてまた学びが増えたら、同じような切り口で諸々まとめてみたいです。