はじめに
以前、Lightweight Render Pipeline(LWRP)と呼ばれていたパイプラインが 2019.3(SRP 7.0.0)から、Universal Render Pipeline(UniversalRP / URP)と改名されました。
私は LWRP 含む SRP をあまり調べていなかったのですが、配布しているライブラリの LWRP 対応要望もちらほら聞こえてきましたので、詳しく調べてみることにしました。が、いきなり体系的にまとめるのは自分も分かっていない点が多くかなり大変...なので、プロジェクトを作成し、以前のビルトインのパイプラインから変わったところ等を適当な順番で色々見ていくことにしました。本記事ではレンダリング回り(パイプラインやシェーダの変更点)について調べます(シェーダグラフは本記事では触れません)。URP そのものの使い方などについては余り触れません。
Scriptable Render Pipeline(SRP)についてのおさらいは(ちょっと古いですが)以下もご参照いただけると嬉しいです。
環境
- Untiy 2019.3.0b6
- URP 7.1.2
事前勉強
以下 UTJ 公式スライドを見ておくと理解が深まると思います。
セットアップ
プロジェクトの作成
Unity 2019.3 では Universal RP のテンプレートが用意されているので、Unity Hub から選択してプロジェクトを作成します。すると以下のようなデフォルトのシーンが表示されます。
URP アセット
SRP ではパイプラインの設定はアセット化されています。これを Project Settings > Graphics でセットして使う流れです。
デフォルトでは UniversalRP-HighQuality
が選択されているようなので、これを見てみましょう。
なにやら色々と設定項目がありますが...、これらは以下のマニュアルにまとまっています。
LWRP 時代と概ね同じようですが、私は以前のものをあまり見てないので順に気になったところを見ていきたいと思います。
General
Renderer List
レンダラを指定することが出来ます。現状は Forward Renderer と 2D Renderer しかないようですが、公式ブログによるとディファードも追加される見込みのようです。もちろん自分で作成して追加も可能だと思います。
レンダラでどういうことが出来るかは、応用した記事を読むとより理解が深まると思います:
DepthTexture / OpaqueTexture
以前は、Camera.depthTextureMode
を適当なコンポーネントでセットしないと使えなかった _CameraDepthTexture
はチェックボックスになっています。また、屈折などを表現するために必要だったレンダリング後の画は、従来はシェーダ内で GrabPass
を設けてキャプチャしていましたが、こちらもチェックボックスで _CameraOpaqueTexture
という形でとってこれるようになっています(半透明描画前の画になり、これを参照したシェーダによる表現を半透明のパスで利用できるという形です)。
HDR / MSAA / Render Scale
これらは従来と同じです。HDR
をオンにすると明るいピクセルが 1.0 を超えて出力され、Bloom でいい感じに光が強いことを表現できます。MSAA
を 2x、4x、8x と上げていくとアンチエイリアスのかかりがきれいになっていきます(負荷も増えていきます)。Render Scale
はレンダーターゲットの解像度を動的に変更することが出来ます。ただ、UI は影響を受けずネイティブの解像度でレンダリングされるようです。ランタイムでポチポチ切り替えれば違いが分かるので試してみてください。
Lighting
組み込みの Forward ではライティングは決め打ちの挙動をしていました。
具体的にはライトは影響度の大きい順に 4 個までピクセル単位で(かつメイン以外は追加の Pass で)ライティングが行われ、残りは頂点単位または球面調和関数による近似でライティングが行われていました。しかし、URP では後で見ていきたいと思いますが、1 Pass でライトの計算を行います。その関係でこのあたりの設定が大きく変わっています。まず、Main Light
はメインのディレクショナルライトを指します。メインのディレクショナルライトは、Lighting
ウィンドウの Sun Source
によって指定します。
設定しない場合は最も強いディレクショナルライトが採用される仕様のようです。オフにすることもできます(Per Pixel
か Disabled
が指定可能)。
追加のライトに関しては、Per Pixel
、Disabled
に加え、Per Vertex
で頂点単位のライティングも選択可能です。また、以前は決め打ちだった数も 0 ~ 8 個の間で選択することが出来ます(繰り返しですが、数に依らず 1 Pass で計算されます)。
また、メイン・追加ライトともに影の設定があり、それぞれ異なるシャドウマップの解像度を指定できます。
Shadows
影に関しての設定は以前とそれほど変わりません。距離、カスケード、各バイアス(デプスと法線)、ソフトシャドウの設定がある形です。挙動の詳細はドキュメントに書いてあります。
Post-processing
カラーグレーディング用の微調整が可能で、計算を HDR / LDR どちらで行うかと、ルックアップテーブルの解像度の指定が可能です。そのうち他のポストプロセスの調整のパラメタも追加されるかもしれません。
Advanced
SRP Batcher はデフォルトで ON になっています。同一のシェーダを利用するマテリアルが大量にある場合に高速化が望めます。詳細はこちら:
Dynamic Batching は同一マテリアルのオブジェクトをバッチ処理するものですが、GPU インスタンシングが使える場合は必要ないので、デフォルトでは OFF になっています。Mixed Lighting はデフォルトで ON です。この他に、レンダリングパイプラインが生成するログレベルをセットする Debug Level
と、シェーダバリアントのログレベルをセットする Shader Variant Log Level
もここに含まれています。
シェーダ(前半)
さて、設定は分かったので、さっそくシェーダを見てみたいと思います。取り敢えずサーフェスシェーダを変換して見てみるか~、とサーフェスシェーダを適用したマテリアルを適用すると以下のようになります。
残念ながらサーフェスシェーダは URP では使えません。。URP 専用(対応)のシェーダが必要になります。組み込みのものは以下の Packages > Universal RP から参照することが出来ます。
まずはこの中にある Unlit シェーダを元にしたコードで解説していきたいと思います。
アウトラインの変更
まずアウトラインを見てみましょう。
Shader "Universal Render Pipeline/Unlit" { Properties { ... } SubShader { Tags { "RenderType" = "Opaque" "IgnoreProjector" = "True" "RenderPipeline" = "UniversalPipeline" } LOD 100 Blend [_SrcBlend][_DstBlend] ZWrite [_ZWrite] Cull [_Cull] Pass { Name "Unlit" HLSLPROGRAM ... ENDHLSL } Pass { Tags { "LightMode" = "DepthOnly" } ... } Pass { Name "Meta" Tags {"LightMode" = "Meta" } ... } } FallBack "Hidden/InternalErrorShader" }
基本的な構造は同じで、ShaderLab の文法に則っています。CGPROGRAM
は HLSLPROGRAM
に変更されているようです(Cg は NVIDIA 開発のシェーディング言語ですが、2012 年にサポート終了していて、現在は MS 開発の HLSL ベースなのでこれを機にリネーム*1したのでしょうか)。
SubShader
ブロックで RenderPipeline
を UniversalPipeline
と指定しています。URP ではこの指定をされたブロックを利用して描画をするようです。URP が指定されておらず従来の組み込みのパイプラインを使う際にも動作させたい場合は、指定なしの SubShader
ブロックを追加するか、従来の Standard シェーダへの Fallback
の指定を行う、などの対応が必要なようです。
Pass
ブロックはこのシェーダでは 3 つあり、それぞれカラーを出力するパス(LightMode
は名無し)、デプスのみを出力する DepthOnly
パス、ライトマップに情報を渡す Meta
パス、となっています。
Unlit
では Unlit のパスを詳しく見ていきましょう。
Pass { Name "Unlit" HLSLPROGRAM #pragma prefer_hlslcc gles #pragma exclude_renderers d3d11_9x #pragma vertex vert #pragma fragment frag #pragma shader_feature _ALPHATEST_ON #pragma shader_feature _ALPHAPREMULTIPLY_ON #pragma multi_compile_fog #pragma multi_compile_instancing #include "Packages/com.unity.render-pipelines.universal/Shaders/UnlitInput.hlsl" struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct Varyings { float2 uv : TEXCOORD0; float fogCoord : TEXCOORD1; float4 vertex : SV_POSITION; UNITY_VERTEX_INPUT_INSTANCE_ID UNITY_VERTEX_OUTPUT_STEREO }; Varyings vert(Attributes input) { Varyings output = (Varyings)0; UNITY_SETUP_INSTANCE_ID(input); UNITY_TRANSFER_INSTANCE_ID(input, output); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output); VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz); output.vertex = vertexInput.positionCS; output.uv = TRANSFORM_TEX(input.uv, _BaseMap); output.fogCoord = ComputeFogFactor(vertexInput.positionCS.z); return output; } half4 frag(Varyings input) : SV_Target { UNITY_SETUP_INSTANCE_ID(input); UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input); half2 uv = input.uv; half4 texColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, uv); half3 color = texColor.rgb * _BaseColor.rgb; half alpha = texColor.a * _BaseColor.a; AlphaDiscard(alpha, _Cutoff); #ifdef _ALPHAPREMULTIPLY_ON color *= alpha; #endif color = MixFog(color, input.fogCoord); return half4(color, alpha); } ENDHLSL }
いくつか変数・関数の名前、それらの使い方が変わっているところがあるので見ていきましょう。
pragma 文
フォグやインスタンシングなど、以前と同じ形です。
ひとつだけ今回の内容(URP)とは関係ないですが、見たことのなかった prefer_hlslcc gles
というものがあるので調べてみました。Unity は HLSL を GLSL に変換するトランスパイラに数年前までは hlsl2glslfork を使っていましたが、DX9 の HLSL しか変換できないようで、現在は HLSLcc が使われているようです。ただ、GLES 2.0 に関してはどちらを使うかが選択できるようで、それがこの pragma 文のようです。
- Unity - Manual: Shading Language used in Unity
- https://forum.unity.com/threads/shader-compile-pipe-in-lightweight-rendepipeline.541809/
また、DX9 はサポートされていないので #pragma exclude_renderers
されています。
変数・関数の違い
名前も色々と変わっています。appdata
や v2f
だった構造体も、Attributes
や Varyings
となっています。また、UnityObjectToClipPos()
だったものは、GetVertexPositionInputs()
に置き換わっています。これは ShaderLibrary の Core.hlsl で次のように定義されています。
VertexPositionInputs GetVertexPositionInputs(float3 positionOS) { VertexPositionInputs input; input.positionWS = TransformObjectToWorld(positionOS); input.positionVS = TransformWorldToView(input.positionWS); input.positionCS = TransformWorldToHClip(input.positionWS); float4 ndc = input.positionCS * 0.5f; input.positionNDC.xy = float2(ndc.x, ndc.y * _ProjectionParams.x) + ndc.w; input.positionNDC.zw = input.positionCS.zw; return input; }
色々な位置の変数がありますが...、これは座標系変換を考えてもらうとわかりやすいと思います。positionOS
はオブジェクト空間(Object Space)、positionWS
はワールド空間(World Space)、positionVS
はビュー空間(View Space)、positionCS
はクリップ空間(Clip Space)、そして positionNDC
はデバイス正規化座標(Normalized Device Coordinates)の位置だと思います。
TransformObjectToWorld()
等の関数は、コア側(Core RP Library)の SpaceTransform.hlsl で次のように定義されています。
float4x4 GetObjectToWorldMatrix() { return UNITY_MATRIX_M; } float3 TransformObjectToWorld(float3 positionOS) { return mul(GetObjectToWorldMatrix(), float4(positionOS, 1.0)).xyz; } float4x4 GetWorldToViewMatrix() { return UNITY_MATRIX_V; } float3 TransformWorldToView(float3 positionWS) { return mul(GetWorldToViewMatrix(), float4(positionWS, 1.0)).xyz; } float4x4 GetWorldToHClipMatrix() { return UNITY_MATRIX_VP; } float4 TransformWorldToHClip(float3 positionWS) { return mul(GetWorldToHClipMatrix(), float4(positionWS, 1.0)); }
中身は以前と同じ UNITY_MATRIX_*
の掛け算になっています。ここでは使われていませんが、従来の UnityObjectToClipPos()
と同じ役割をする TransformObjectToHClip()
もあります。以前、VR 向けのステレオインスタンシングの説明でもしたことがあるのですが、この UNITY_MATRIX_*
にステレオインスタンシングの仕組みが仕込まれているので、UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO
などの VR 向けの対応もそのまま出来ている形になります。
tex2D
は SAMPLE_TEXTURE2D
に置き換わっています。コア側の ShaderLibrary/API/*.hlsl に定義されていて、プラットフォームごとに場合分けされるようになっています。
// DX11 #define SAMPLE_TEXTURE2D(textureName, samplerName, coord2) textureName.Sample(samplerName, coord2) // GLES 2.0 #define SAMPLE_TEXTURE2D(textureName, samplerName, coord2) tex2D(textureName, coord2)
フォグは、UNITY_TRANSFER_FOG()
と UNITY_APPLY_FOG()
の組み合わせから、ComputeFogFactor()
と MixFog()
に変更されています。UNITY_APPLY_FOG
は組み込みのフォワード用の処理(ベースパスか加算パスか、フォグタイプがリニアなのか exp なのか...など)の場合分けがあったので変更になっているようです。以前のフォグについてはこちら:
Lit シェーダ
さて、簡単なので(ちょい改変)Unlit なシェーダを見てきましたが、次に Lit シェーダを見ていきましょう。
アウトライン
Shader "Universal Render Pipeline/Lit" { Properties { ... } SubShader { Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" "IgnoreProjector" = "True" } LOD 300 Pass { Name "ForwardLit" Tags { "LightMode" = "UniversalForward" } ... } Pass { Name "ShadowCaster" Tags { "LightMode" = "ShadowCaster" } ... } Pass { Name "DepthOnly" Tags { "LightMode" = "DepthOnly" } ... } Pass { Name "Meta" Tags { "LightMode" = "Meta" } ... } Pass { Name "Universal2D" Tags { "LightMode" = "Universal2D" } ... } } FallBack "Hidden/InternalErrorShader" CustomEditor "UnityEditor.Rendering.Universal.ShaderGUI.LitShader" }
Lit シェーダでは、UniversalForward
なパスと ShadowCaster
パス、Universal2D
なパスが追加されています。順に見ていきましょう。
UniversalForward
LightMode
SRP のおさらいですが、LightMode
は SRP では自由に決めることが出来ます。UniversalForward
は、C# 側から以下のようにセットされています(簡略化したコードです)。
public override void Execute( ScriptableRenderContext context, ref RenderingData renderingData) { ... var id = new ShaderTagId("UniversalForward"); var drawSettings = new DrawingSettings(id, sortSettings); context.DrawRenderers( renderingData.cullResults, ref drawSettings, ref m_FilteringSettings, ref m_RenderStateBlock); ... }
このように SRP では独自のパスを簡単にシェーダ側で用意して使えるのでした。
シェーダ外観
さて、この UniversalForward
が URP のキモで、1 パスでライティングを行う部分になります。シェーダのコードを見てみます。
Pass { Name "ForwardLit" Tags { "LightMode" = "UniversalForward" } Blend [_SrcBlend][_DstBlend] ZWrite [_ZWrite] Cull [_Cull] HLSLPROGRAM #pragma prefer_hlslcc gles #pragma exclude_renderers d3d11_9x #pragma target 2.0 // マテリアルのキーワード #pragma shader_feature _NORMALMAP #pragma shader_feature _ALPHATEST_ON #pragma shader_feature _ALPHAPREMULTIPLY_ON #pragma shader_feature _EMISSION #pragma shader_feature _METALLICSPECGLOSSMAP #pragma shader_feature _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A #pragma shader_feature _OCCLUSIONMAP #pragma shader_feature _SPECULARHIGHLIGHTS_OFF #pragma shader_feature _ENVIRONMENTREFLECTIONS_OFF #pragma shader_feature _SPECULAR_SETUP #pragma shader_feature _RECEIVE_SHADOWS_OFF // URP のキーワード #pragma multi_compile _ _MAIN_LIGHT_SHADOWS #pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE #pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS #pragma multi_compile _ _ADDITIONAL_LIGHT_SHADOWS #pragma multi_compile _ _SHADOWS_SOFT #pragma multi_compile _ _MIXED_LIGHTING_SUBTRACTIVE // Unity のキーワード #pragma multi_compile _ DIRLIGHTMAP_COMBINED #pragma multi_compile _ LIGHTMAP_ON #pragma multi_compile_fog // インスタンシング #pragma multi_compile_instancing #pragma vertex LitPassVertex #pragma fragment LitPassFragment #include "LitInput.hlsl" #include "LitForwardPass.hlsl" ENDHLSL }
たくさんのフラグのセットがありますが、おおまかに以下のように分かれています。
- マテリアルのキーワード
- インスペクタから設定できるマテリアルに関するキーワード(下の画像参照)
- URP のキーワード
- 先程見た URP アセットでの設定に関連するキーワード
- Unity のキーワード
- Unity の設定で切り替えるキーワード(ライトマップとフォグ)
- インスタンシング
- インスタンシングの ON / OFF
実際のインスペクタを参照しながら見てみるとわかりやすいと思います。
なんとなくキーワードとの対応付は先程の URP アセットとこのマテリアルのインスペクタを見るとわかるような...気がしますが、では具体的にどこでこれらのキーワードをセットしているかというと、CustomEditor
で指定されている LitShader
を継承した BaseShaderGUI
の中で次のように行われています。
public static void SetMaterialKeywords( Material material, Action<Material> shadingModelFunc = null, Action<Material> shaderFunc = null) { ... if (material.HasProperty("_BumpMap")) { CoreUtils.SetKeyword( material, "_NORMALMAP", material.GetTexture("_BumpMap")); } ... }
入力・出力構造体
さて、キーワードは外観が掴めたので、本丸の頂点・フラグメントシェーダを見ていきます。これらは LitForwardPass.hlsl に記述されていて、それを #include
する形になっています。ちょっと長くなるのでまずは構造体から見ていきましょう:
struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; float4 tangentOS : TANGENT; float2 texcoord : TEXCOORD0; float2 lightmapUV : TEXCOORD1; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct Varyings { float2 uv : TEXCOORD0; DECLARE_LIGHTMAP_OR_SH(lightmapUV, vertexSH, 1); #ifdef _ADDITIONAL_LIGHTS float3 positionWS : TEXCOORD2; #endif #ifdef _NORMALMAP float4 normalWS : TEXCOORD3; // xyz: normal, w: viewDir.x float4 tangentWS : TEXCOORD4; // xyz: tangent, w: viewDir.y float4 bitangentWS : TEXCOORD5; // xyz: bitangent, w: viewDir.z #else float3 normalWS : TEXCOORD3; float3 viewDirWS : TEXCOORD4; #endif half4 fogFactorAndVertexLight : TEXCOORD6; // x: fogFactor, yzw: vertex light #ifdef _MAIN_LIGHT_SHADOWS float4 shadowCoord : TEXCOORD7; #endif float4 positionCS : SV_POSITION; UNITY_VERTEX_INPUT_INSTANCE_ID UNITY_VERTEX_OUTPUT_STEREO };
先程のキーワードに応じて場合分けするところがありますが、後にライティングに必要な変数が詰まっている感じで、従来のパイプラインと然程変わりありません。
頂点シェーダ
次に頂点シェーダを見ていきましょう。
Varyings LitPassVertex(Attributes input) { Varyings output = (Varyings)0; UNITY_SETUP_INSTANCE_ID(input); UNITY_TRANSFER_INSTANCE_ID(input, output); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output); VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz); VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS, input.tangentOS); half3 viewDirWS = GetCameraPositionWS() - vertexInput.positionWS; half3 vertexLight = VertexLighting(vertexInput.positionWS, normalInput.normalWS); half fogFactor = ComputeFogFactor(vertexInput.positionCS.z); output.uv = TRANSFORM_TEX(input.texcoord, _BaseMap); #ifdef _NORMALMAP output.normalWS = half4(normalInput.normalWS, viewDirWS.x); output.tangentWS = half4(normalInput.tangentWS, viewDirWS.y); output.bitangentWS = half4(normalInput.bitangentWS, viewDirWS.z); #else output.normalWS = NormalizeNormalPerVertex(normalInput.normalWS); output.viewDirWS = viewDirWS; #endif OUTPUT_LIGHTMAP_UV(input.lightmapUV, unity_LightmapST, output.lightmapUV); OUTPUT_SH(output.normalWS.xyz, output.vertexSH); output.fogFactorAndVertexLight = half4(fogFactor, vertexLight); #ifdef _ADDITIONAL_LIGHTS output.positionWS = vertexInput.positionWS; #endif #if defined(_MAIN_LIGHT_SHADOWS) && !defined(_RECEIVE_SHADOWS_OFF) output.shadowCoord = GetShadowCoord(vertexInput); #endif output.positionCS = vertexInput.positionCS; return output; }
以前と比べるとキレイに読みやすい関数に分かれています。使う関数は刷新されてますが、名前から何を処理して何を出力するかが分かりやすくなっています。以前と比べて大きな変更があるのは VertexLighting
の部分です。これは Lighting.hlsl に次のように記述されています。
half3 VertexLighting(float3 positionWS, half3 normalWS) { half3 vertexLightColor = half3(0.0, 0.0, 0.0); #ifdef _ADDITIONAL_LIGHTS_VERTEX uint lightsCount = GetAdditionalLightsCount(); for (uint lightIndex = 0u; lightIndex < lightsCount; ++lightIndex) { Light light = GetAdditionalLight(lightIndex, positionWS); half3 lightColor = light.color * light.distanceAttenuation; vertexLightColor += LightingLambert(lightColor, light.direction, normalWS); } #endif return vertexLightColor; }
最初の方で見た URP のアセットの Lighting > Additional Lights > Main Light で Per Vertex を選んだ場合に for
文が回り、ライトの個数文だけランバートによるライティングが頂点単位で行われます。GetAdditionalLightsCount()
は同アセットで指定した Per Object Limit の値が入ってきます*2。
GetAdditionalLight()
は Light
構造体に情報を詰めてライティングの結果を返してくれます。少し長いですが、以下のような処理になっています:
struct Light { half3 direction; half3 color; half distanceAttenuation; half shadowAttenuation; }; Light GetAdditionalLight(uint i, float3 positionWS) { int perObjectLightIndex = GetPerObjectLightIndex(i); return GetAdditionalPerObjectLight(perObjectLightIndex, positionWS); } int GetPerObjectLightIndex(uint index) { uint offset = unity_LightData.x; return _AdditionalLightsIndices[offset + index]; } Light GetAdditionalPerObjectLight(int perObjectLightIndex, float3 positionWS) { // Structured Buffer(_AdditionalLightsBuffer)からライト情報を取り出す float4 lightPositionWS = _AdditionalLightsBuffer[perObjectLightIndex].position; half3 color = _AdditionalLightsBuffer[perObjectLightIndex].color.rgb; half4 distanceAndSpotAttenuation = _AdditionalLightsBuffer[perObjectLightIndex].attenuation; half4 spotDirection = _AdditionalLightsBuffer[perObjectLightIndex].spotDirection; half4 lightOcclusionProbeInfo = _AdditionalLightsBuffer[perObjectLightIndex].occlusionProbeChannels; // 距離と角度による減衰を計算 float3 lightVector = lightPositionWS.xyz - positionWS * lightPositionWS.w; float distanceSqr = max(dot(lightVector, lightVector), HALF_MIN); half3 lightDirection = half3(lightVector * rsqrt(distanceSqr)); half attenuation = DistanceAttenuation(distanceSqr, distanceAndSpotAttenuation.xy) * AngleAttenuation(spotDirection.xyz, lightDirection, distanceAndSpotAttenuation.zw); // 結果を Light 構造体に格納 Light light; light.direction = lightDirection; light.distanceAttenuation = attenuation; light.shadowAttenuation = AdditionalLightRealtimeShadow(perObjectLightIndex, positionWS); light.color = color; // ライトマップを反映 #if defined(LIGHTMAP_ON) || defined(_MIXED_LIGHTING_SUBTRACTIVE) int probeChannel = lightOcclusionProbeInfo.x; half lightProbeContribution = lightOcclusionProbeInfo.y; half probeOcclusionValue = unity_ProbesOcclusion[probeChannel]; light.distanceAttenuation *= max(probeOcclusionValue, lightProbeContribution); #endif return light; }
実際は StructuredBuffer
が使えるプラットフォームとそうでないプラットフォームでの場合分けのコードがあるのでもう少し長いです。ちなみに _AdditionalLightsIndices
や _AdditionalLightsBuffer
は ForwardLight.cs でセットされています。
フラグメントシェーダ
フラグメントシェーダを見てみましょう:
half4 LitPassFragment(Varyings input) : SV_Target { UNITY_SETUP_INSTANCE_ID(input); UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input); SurfaceData surfaceData; InitializeStandardLitSurfaceData(input.uv, surfaceData); InputData inputData; InitializeInputData(input, surfaceData.normalTS, inputData); half4 color = UniversalFragmentPBR( inputData, surfaceData.albedo, surfaceData.metallic, surfaceData.specular, surfaceData.smoothness, surfaceData.occlusion, surfaceData.emission, surfaceData.alpha); color.rgb = MixFog(color.rgb, inputData.fogCoord); return color; }
こちらはとてもシンプルです。ロジックとしては SurfaceData
と InputData
を組み立てて UniversalFragmentPBR
に渡し、MixFog
でフォグを処理している形です。
InitializeStandardLitSurfaceData
は次のようにインスペクタでセットされた値を組み立てる関数です。
struct SurfaceData { half3 albedo; half3 specular; half metallic; half smoothness; half3 normalTS; half3 emission; half occlusion; half alpha; }; inline void InitializeStandardLitSurfaceData(float2 uv, out SurfaceData outSurfaceData) { half4 albedoAlpha = SampleAlbedoAlpha( uv, TEXTURE2D_ARGS(_BaseMap, sampler_BaseMap)); outSurfaceData.alpha = Alpha(albedoAlpha.a, _BaseColor, _Cutoff); half4 specGloss = SampleMetallicSpecGloss(uv, albedoAlpha.a); outSurfaceData.albedo = albedoAlpha.rgb * _BaseColor.rgb; #if _SPECULAR_SETUP outSurfaceData.metallic = 1.0h; outSurfaceData.specular = specGloss.rgb; #else outSurfaceData.metallic = specGloss.r; outSurfaceData.specular = half3(0.0h, 0.0h, 0.0h); #endif outSurfaceData.smoothness = specGloss.a; outSurfaceData.normalTS = SampleNormal( uv, TEXTURE2D_ARGS(_BumpMap, sampler_BumpMap), _BumpScale); outSurfaceData.occlusion = SampleOcclusion(uv); outSurfaceData.emission = SampleEmission( uv, _EmissionColor.rgb, TEXTURE2D_ARGS(_EmissionMap, sampler_EmissionMap)); }
InitializeInputData
の方はライティングに必要な InputData
構造体を、頂点シェーダから渡ってきた Varyings
構造体の変数から抽出して組み立てる関数です。
struct InputData { float3 positionWS; half3 normalWS; half3 viewDirectionWS; float4 shadowCoord; half fogCoord; half3 vertexLighting; half3 bakedGI; }; void InitializeInputData(Varyings input, half3 normalTS, out InputData inputData) { inputData = (InputData)0; #ifdef _ADDITIONAL_LIGHTS inputData.positionWS = input.positionWS; #endif #ifdef _NORMALMAP half3 viewDirWS = half3(input.normalWS.w, input.tangentWS.w, input.bitangentWS.w); inputData.normalWS = TransformTangentToWorld(normalTS, half3x3(input.tangentWS.xyz, input.bitangentWS.xyz, input.normalWS.xyz)); #else half3 viewDirWS = input.viewDirWS; inputData.normalWS = input.normalWS; #endif inputData.normalWS = NormalizeNormalPerPixel(inputData.normalWS); viewDirWS = SafeNormalize(viewDirWS); inputData.viewDirectionWS = viewDirWS; #if defined(_MAIN_LIGHT_SHADOWS) && !defined(_RECEIVE_SHADOWS_OFF) inputData.shadowCoord = input.shadowCoord; #else inputData.shadowCoord = float4(0, 0, 0, 0); #endif inputData.fogCoord = input.fogFactorAndVertexLight.x; inputData.vertexLighting = input.fogFactorAndVertexLight.yzw; inputData.bakedGI = SAMPLE_GI(input.lightmapUV, input.vertexSH, inputData.normalWS); }
そしてこれらの構造体を受け取ってライティングを行う本体が UniversalFragmentPBR()
です。
half4 UniversalFragmentPBR( InputData inputData, half3 albedo, half metallic, half3 specular, half smoothness, half occlusion, half3 emission, half alpha) { BRDFData brdfData; InitializeBRDFData(albedo, metallic, specular, smoothness, alpha, brdfData); Light mainLight = GetMainLight(inputData.shadowCoord); MixRealtimeAndBakedGI(mainLight, inputData.normalWS, inputData.bakedGI, half4(0, 0, 0, 0)); half3 color = GlobalIllumination(brdfData, inputData.bakedGI, occlusion, inputData.normalWS, inputData.viewDirectionWS); color += LightingPhysicallyBased(brdfData, mainLight, inputData.normalWS, inputData.viewDirectionWS); #ifdef _ADDITIONAL_LIGHTS uint pixelLightCount = GetAdditionalLightsCount(); for (uint lightIndex = 0u; lightIndex < pixelLightCount; ++lightIndex) { Light light = GetAdditionalLight(lightIndex, inputData.positionWS); color += LightingPhysicallyBased(brdfData, light, inputData.normalWS, inputData.viewDirectionWS); } #endif #ifdef _ADDITIONAL_LIGHTS_VERTEX color += inputData.vertexLighting * brdfData.diffuse; #endif color += emission; return half4(color, alpha); }
PBR 部分の詳細は難しいのでスキップしますが、URP の PBR の計算の概要はマニュアルに書いてあります:
以前との差分として眺めてみると、組み込みのフォワードではメインのライトをここで処理して、加算パスで追加のライトによるライティングを処理していました。
URP では、まず GetMainLight()
でメインのライトを取得し、これを使って LightingPhysicallyBased()
を行います。そして Lighting > Additional Lights > Main Light で Per Pixel を選んだ場合(= _ADDITIONAL_LIGHTS
が定義されている場合)は同一パス内で for
文が回り、追加のライト分だけ加えて LightingPhysicallyBased()
が行われます。Per Vertex を選んでいた場合(= _ADDITIONAL_LIGHTS_VERTEX
が定義されている場合)は、先程頂点シェーダの解説で見た計算結果を足し合わせる形になっています。
これでようやく「URP では追加のライト 8 個まで 1 パスで処理される」という全容が把握できました。
レンダリング全体像
シェーダの概要が把握できましたので、レンダリングパイプラインの方を見ていきましょう。
パイプラインの流れ
レンダリングの順序は ForwardRenderer.cs で次のように書かれています。
namespace UnityEngine.Rendering.Universal { public sealed class ForwardRenderer : ScriptableRenderer { public ForwardRenderer(ForwardRendererData data) : base(data) { ... m_MainLightShadowCasterPass = new MainLightShadowCasterPass(RenderPassEvent.BeforeRenderingShadows); m_AdditionalLightsShadowCasterPass = new AdditionalLightsShadowCasterPass(RenderPassEvent.BeforeRenderingShadows); m_DepthPrepass = new DepthOnlyPass(RenderPassEvent.BeforeRenderingPrepasses, RenderQueueRange.opaque, data.opaqueLayerMask); m_ScreenSpaceShadowResolvePass = new ScreenSpaceShadowResolvePass(RenderPassEvent.BeforeRenderingPrepasses, screenspaceShadowsMaterial); m_ColorGradingLutPass = new ColorGradingLutPass(RenderPassEvent.BeforeRenderingOpaques, data.postProcessData); m_RenderOpaqueForwardPass = new DrawObjectsPass("Render Opaques", true, RenderPassEvent.BeforeRenderingOpaques, RenderQueueRange.opaque, data.opaqueLayerMask, m_DefaultStencilState, stencilData.stencilReference); m_CopyDepthPass = new CopyDepthPass(RenderPassEvent.BeforeRenderingOpaques, copyDepthMaterial); m_DrawSkyboxPass = new DrawSkyboxPass(RenderPassEvent.BeforeRenderingSkybox); m_CopyColorPass = new CopyColorPass(RenderPassEvent.BeforeRenderingTransparents, samplingMaterial); m_RenderTransparentForwardPass = new DrawObjectsPass("Render Transparents", false, RenderPassEvent.BeforeRenderingTransparents, RenderQueueRange.transparent, data.transparentLayerMask, m_DefaultStencilState, stencilData.stencilReference); m_OnRenderObjectCallbackPass = new InvokeOnRenderObjectCallbackPass(RenderPassEvent.BeforeRenderingPostProcessing); m_PostProcessPass = new PostProcessPass(RenderPassEvent.BeforeRenderingPostProcessing, data.postProcessData); m_FinalPostProcessPass = new PostProcessPass(RenderPassEvent.AfterRenderingPostProcessing, data.postProcessData); m_CapturePass = new CapturePass(RenderPassEvent.AfterRendering); m_FinalBlitPass = new FinalBlitPass(RenderPassEvent.AfterRendering, blitMaterial); ... } ... public override void Setup(ScriptableRenderContext context, ref RenderingData renderingData) { ... if (mainLightShadows) EnqueuePass(m_MainLightShadowCasterPass); if (additionalLightShadows) EnqueuePass(m_AdditionalLightsShadowCasterPass); if (requiresDepthPrepass) { ... EnqueuePass(m_DepthPrepass); } ... } ... } ... }
小分けにクラス単位でパスが区切られていて、とてもカスタマイズしやすい感じになっています。Setup
内でこれら ScriptableRenderPass
継承で作成されたパスが EnqueuePass
されて、レンダラの基底クラスのキューに積まれていく形になっています。
この流れを Frame Debugger で見てみると、次のような形になります: