初めまして、クライアントエンジニアの中島です。 先に投稿された方達が予想していたよりも趣味に走ったことを書いているなあと思いながらの初投稿です。
さて、SPARKCREATIVEではSPARKGEARだけに限らない開発のお手伝いも承っています。 そのような中で最近何度かUnityのバージョンも結構上がったので、そろそろバージョンを上げておきましょうという場面に遭遇しました。
それらのタイトルではグラフィックス周りを担当していたのですが、プロジェクトで使っていたLightweight Render Pipelineはいつの間にやらUniversal Render Pipeline(以下URPと略します)へと変わっていて、移行する作業が必要になりました。 そしてついでにシェーダーもScriptable Render Pipelineに最適な形にしましょう、となったわけです。
今回はその覚書のようなものを書こうと思います。
デフォルトのUnlitをURP対応にする
URPのシェーダーのテンプレートがあれば良かったのですが、残念ながら用意されていないようです。 そこで例としてAssetメニューから作成できるお馴染みのUnlitのテンプレートをURPに対応した形にしていきたいと思います。 また詰め込みすぎると大変なので標準のForwardRendererかそれに近いものを使うことを前提とします。
作成直後の全貌はこのようになっています。
// デフォルトのUnlitテンプレート Shader "Unlit/MyUnlit" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag // make fog work #pragma multi_compile_fog #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; UNITY_FOG_COORDS(1) float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; 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; } 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; } ENDCG } } }
まずやるべきことはSubShaderとPassのTagsへの項目の追加です。
SubShaderのTagsに対応するパイプラインとして"RenderPipeline"="UniversalPipeline"を追加します。
必須ではないのですが、これがあると指定したパイプライン以外では描画されなくなります。
よそからシェーダーを持ってきてみたらなぜか動かない、などという時にこの指定が違っていたりする可能性もあるので頭の片隅に置いておきましょう。
またPassにTags { "LightMode"="UniversalForward" }を追加してレンダリングされるパスを指定します。
UniversalForwardは通常のオブジェクトが描画されるパスです。
これも指定しなくても描画はされるのですが、カスタマイズしてフィーチャーを追加した時などに意図しないところで描かれないためにもつけておくと安心です。
ついでにName "適当な名前"もつけておくとFrame Debuggerで見たときにどのPassを使って描画されたのかわかりやすくなります。
...
SubShader
{
Tags {
"RenderType"="Opaque"
"RenderPipeline"="UniversalPipeline" // <-
}
...
Pass
{
Name "ForwardLit" // <-
Tags { "LightMode"="UniversalForward" } // <-
CGPROGRAM
...
// <-を付けてある行が変更箇所です。
この段階でシェーダーのインスペクタを確認してみるとSRP Batcherという項目に何やらメッセージが出ていると思います。

SRP Batcher自体についてはUnityのマニュアルや機能紹介のブログ記事をご参照ください。
これを消すのは簡単で、Propertiesにあるテクスチャ以外のパラメーターをUnityPerMaterialというcbufferにまとめるだけです。cbufferの開始と終了のマクロがあるのでそれで挟みます。
暗黙的に定義されるテクスチャ_STも忘れずにまとめましょう。
...
sampler2D _MainTex;
CBUFFER_START(UnityPerMaterial) // <-
float4 _MainTex_ST;
CBUFFER_END // <-
SRP Batcherの項目が "compatible" になりメッセージが消えればOKです。 もしならなかったら考えられることは3つです。
はいもうお分かりいただけると思いますが、なんとこの状態ではWindowsのDirectX環境以外ではSRP Batcherに対応できていないのです。
ビルドインのシェーダーの中を探してみるとcbufferの定義が無効になってしまうプラットフォームが多数あることがわかります。
有効にするためにはCGPROGRAMの中に#pragma enable_cbufferを書かなくてはいけません。
これは2019.3から追加されたそうで、Passが1つしかないような簡単なシェーダーならこれでも良いかもしれません。
一方でURPのシェーダーを見てみると全てHLSLで書かれています。 つまりURPが定義している関数などを使いまわしたかったらHLSLにしておくべきだということです。*1 具体的に何かあるかといえば思い当たるのはシャドウ関連でしょうか。
それからこれは私も最近知ったことなのですが、UnityのマニュアルのShading language used in Unityの記述の最初のあたりを適当に省略しつつ翻訳して引用します。
Unityでは、HLSLプログラミング言語を使用してシェーダープログラムを作成します。
Unityは元々Cg言語を使用していたため、... UnityはCgを使用しなくなりました、しかし...
というわけでおとなしくHLSLにしてしまいましょう。CGPROGRAM ... ENDCGをHLSLPROGRAM ... ENDHLSLに変更します。それから#include "UnityCG.cginc"も削除してしまいます。
...
HLSLPROGRAM // <-
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
// #include "UnityCG.cginc" // <-
...
ENDHLSL // <-
...
変更するとコンパイルが通らないと思います。 UnityCG.cgincが定義していたものが無くなっているので当然です。 代わりになるものは com.unity.render-pipelines.universal と com.unity.render-pipelines.core パッケージの中にあります。
パッケージからのインクルードは#include "Packages/パッケージ名/ディレクトリ/ファイル"のように書きます。
よく使いそうなものは下のようなものでしょうか。
| 内容 | ファイル |
|---|---|
| 基本 | Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl |
| 座標変換 | Packages/com.unity.render-pipelines.core/ShaderLibrary/SpaceTransform.hlsl *2 |
| フォグ | Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl |
| シャドウ | Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl |
座標変換の関数はUnityObjectToClipPos()の代わりがTransformObjectToHClip()というように、微妙に名前が違うので面倒ですが基本的には単純に置き換えていけば大丈夫なはずです。
また頂点シェーダーからピクセルシェーダーへ渡すフォグやシャドウのパラメーターは以前のようなマクロではなく、自分で定義して受け渡す形になるようです。
それからテクスチャの定義もマクロで行うのが正統派のようです。TEXTURE2D(テクスチャ名)とSAMPLER(サンプラー名)を合わせて定義します。
サンプリングもSAMPLE_TEXTURE2D(テクスチャ, サンプラー, UV)マクロを使います。もちろん形式の違うTEXTURE2D_HALF()やTEXTURE2D_SHADOW()なども用意されています。
...
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 v2f
{
float2 uv : TEXCOORD0;
float fogFactor: TEXCOORD1; // <-
float4 vertex : SV_POSITION;
};
TEXTURE2D(_MainTex); // <-
SAMPLER(sampler_MainTex); // <-
...
v2f vert (appdata v)
{
v2f o;
o.vertex = TransformObjectToHClip(v.vertex); // <-
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
...
これで元のUnlitと同等の機能でSRP Bacherもcompatibleになりました。
シャドウ
せっかくなのでシャドウもつけてみましょう。
メインライトのシャドウを受けるだけならまずワールド空間の位置を用意してTransformWorldToShadowCoord(worlsSpaceCoord)でシャドウ空間の位置を計算します。
それを使うとGetMainLight(shadowCoord)でLightという構造体が取得でき、その中に影の強さshadowAttenuationがあります。
ただし以下のmulti_compileのpragmaを定義しておかなくてはいけません。
// Universal Pipeline shadow keywords #pragma multi_compile _ _MAIN_LIGHT_SHADOWS #pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE #pragma multi_compile _ _ADDITIONAL_LIGHT_SHADOWS #pragma multi_compile _ _SHADOWS_SOFT
パイプラインの設定や使っているライトの種類から必要なものを選びましょう。
それから以前は"LightMode"="DepthOnly"のPassを定義しないとシャドウが受けられませんでしたが、URPでは基本的にスクリーンスペースでのシャドウの解決はやめたらしいので不要です。
余談ですがこれによってTransparentでもシャドウを受けることができるようになりました。
またシャドウを落としたかったら"LightMode"="ShadowPass"のPassが必要です。Universal/Litを参考にPackages/com.unity.render-pipelines.universal/Shaders/ShadowCasterPass.hlslを少し直せば問題ないと思います。
以上を組み合わせると最終的にこのくらいになります。
Shader "Unlit/MyUnlit" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" } LOD 100 // 各Passでcbufferが変わらないようにここに定義する HLSLINCLUDE #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); CBUFFER_START(UnityPerMaterial) float4 _MainTex_ST; CBUFFER_END ENDHLSL Pass { Name "ForwardLit" Tags { "LightMode"="UniversalForward" } HLSLPROGRAM #pragma vertex vert #pragma fragment frag // make fog work #pragma multi_compile_fog // Universal Pipeline shadow keywords #pragma multi_compile _ _MAIN_LIGHT_SHADOWS #pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE #pragma multi_compile _ _ADDITIONAL_LIGHT_SHADOWS #pragma multi_compile _ _SHADOWS_SOFT #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float fogFactor: TEXCOORD1; float3 posWS : TEXCOORD2; float4 vertex : SV_POSITION; }; v2f vert (appdata v) { v2f o; o.vertex = TransformObjectToHClip(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); o.fogFactor = ComputeFogFactor(o.vertex.z); o.posWS = TransformObjectToWorld(v.vertex.xyz); return o; } float4 frag (v2f i) : SV_Target { float4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv); float4 shadowCoord = TransformWorldToShadowCoord(i.posWS); Light mainLight = GetMainLight(shadowCoord); half shadow = mainLight.shadowAttenuation; Light addLight0 = GetAdditionalLight(0, i.posWS); shadow *= addLight0.shadowAttenuation; col.rgb *= shadow; col.rgb = MixFog(col.rgb, i.fogFactor); return col; } ENDHLSL } Pass { Tags { "LightMode"="ShadowCaster" } HLSLPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_instancing #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" // ShadowsCasterPass.hlsl に定義されているグローバルな変数 float3 _LightDirection; struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2f { float4 pos : SV_POSITION; }; v2f vert(appdata v) { UNITY_SETUP_INSTANCE_ID(v); v2f o; // ShadowsCasterPass.hlsl の GetShadowPositionHClip() を参考に float3 positionWS = TransformObjectToWorld(v.vertex.xyz); float3 normalWS = TransformObjectToWorldNormal(v.normal); float4 positionCS = TransformWorldToHClip(ApplyShadowBias(positionWS, normalWS, _LightDirection)); #if UNITY_REVERSED_Z positionCS.z = min(positionCS.z, positionCS.w * UNITY_NEAR_CLIP_VALUE); #else positionCS.z = max(positionCS.z, positionCS.w * UNITY_NEAR_CLIP_VALUE); #endif o.pos = positionCS; return o; } float4 frag(v2f i) : SV_Target { return 0.0; } ENDHLSL } } }
これでシャドウに対応して、SRP Batcherもcompatibleになっているシェーダーの出来上がりです。
サンプルコードがだいぶ幅を取ってしまいましたが、今回はこのあたりで。
R.I.P. UnityCG.cginc