UnityのUniversal Render Pipeline(URP)でPBRライティングをする基本的なシェーダを書く方法をまとめました。
Unity2020.3.15f2
Universal RP 10.5.1
はじめに
本記事では、UnityのURPでPBRライティングをする基本的なシェーダの書き方についてまとめます。
PBRやライトマップ、シャドウなどの予めURPで用意されている各計算処理を扱えることを目的とし、
それらを使ってPBRライティングされた簡単なマテリアルを作成することをゴールとします。
またForwardレンダリングを前提とします。
URPのシェーダの基礎知識については以下の記事にまとめていますので、必要に応じて参照してください。
方針の整理
さて実際にシェーダを書く前に、方針を整理しておきます。
まずURPでPBRのライティング計算を行うには、Lighting.hlsl
のUniversalFragmentPBR
を使うことになります。
これは以下のようなインターフェースを持ちます。
half4 UniversalFragmentPBR(InputData inputData, SurfaceData surfaceData)
この引数のうち、InputData
はInput.hlsl
に定義されている以下の構造体になります。
struct InputData
{
float3 positionWS;
half3 normalWS;
half3 viewDirectionWS;
float4 shadowCoord;
half fogCoord;
half3 vertexLighting;
half3 bakedGI;
float2 normalizedScreenSpaceUV;
half4 shadowMask;
};
またSurfaceData
はSurface.hlsl
に定義されている以下の構造体になります。
struct SurfaceData
{
half3 albedo;
half3 specular;
half metallic;
half smoothness;
half3 normalTS;
half3 emission;
half occlusion;
half alpha;
half clearCoatMask;
half clearCoatSmoothness;
};
したがって、これらに入力する値を適切に用意すれば、ライティング結果が得られるということになります。
シェーダ
さてそれでは実際にシェーダを記述します。
まずはシェーダの全貌を掲載します。
Shader "PBRExample" { Properties { _BaseMap("Base Map", 2D) = "white" {} _BaseColor("Base Color", Color) = (1, 1, 1, 1) [Normal] _NormalMap("Normal Map", 2D) = "bump" {} _Metallic("Metallic", Range(0.0, 1.0)) = 0.0 _Smoothness("Smoothness", Range(0.0, 1.0)) = 0.0 } SubShader { Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" "UniversalMaterialType" = "Lit" "IgnoreProjector" = "True" "Queue" = "Geometry" } Pass { Tags { "LightMode" = "UniversalForward" } HLSLPROGRAM #pragma vertex vert #pragma fragment frag // Universal Render Pipeline keywords #pragma multi_compile _ _MAIN_LIGHT_SHADOWS #pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE #pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS #pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS #pragma multi_compile_fragment _ _SHADOWS_SOFT #pragma multi_compile_fragment _ _SCREEN_SPACE_OCCLUSION #pragma multi_compile _ LIGHTMAP_SHADOW_MIXING #pragma multi_compile _ SHADOWS_SHADOWMASK // Unity keywords #pragma multi_compile _ DIRLIGHTMAP_COMBINED #pragma multi_compile _ LIGHTMAP_ON #pragma multi_compile_fog #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; float2 lightmapUV : TEXCOORD1; float3 normal : NORMAL; float4 tangent : TANGENT; }; struct Varyings { float4 positionHCS : SV_POSITION; float2 uv : TEXCOORD0; float3 positionWS : TEXCOORD1; float3 normalWS : TEXCOORD2; float3 tangentWS : TEXCOORD3; float3 bitangentWS : TEXCOORD4; float3 viewDirWS : TEXCOORD5; half fogFactor : TEXCOORD6; half3 vertexLight : TEXCOORD7; #if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR) float4 shadowCoord : TEXCOORD8; #endif DECLARE_LIGHTMAP_OR_SH(lightmapUV, vertexSH, 9); }; TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); TEXTURE2D(_NormalMap); SAMPLER(sampler_NormalMap); CBUFFER_START(UnityPerMaterial) float4 _BaseMap_ST; half4 _BaseColor; float4 _NormalMap_ST; float _NormalScale; float _Metallic; float _Smoothness; CBUFFER_END Varyings vert(Attributes input) { Varyings output; output.uv = TRANSFORM_TEX(input.uv, _BaseMap); output.positionWS = TransformObjectToWorld(input.positionOS); output.positionHCS = TransformWorldToHClip(output.positionWS); output.viewDirWS = GetWorldSpaceViewDir(output.positionWS); output.normalWS = TransformObjectToWorldNormal(input.normal); output.tangentWS = TransformObjectToWorldDir(input.tangent.xyz); output.bitangentWS = cross(output.normalWS, output.tangentWS) * input.tangent.w; OUTPUT_LIGHTMAP_UV(input.lightmapUV, unity_LightmapST, output.lightmapUV); OUTPUT_SH(output.normalWS.xyz, output.vertexSH); output.fogFactor = ComputeFogFactor(output.positionHCS.z); output.vertexLight = VertexLighting(output.positionWS, output.normalWS); // Shadow #if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR) output.shadowCoord = TransformWorldToShadowCoord(output.positionWS); #endif return output; } half4 frag(Varyings input) : SV_Target { // SurfaceDataを作成 SurfaceData surfaceData; surfaceData.normalTS = UnpackNormal(SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, input.uv)); half4 col = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv) * _BaseColor; surfaceData.albedo = col.rgb; surfaceData.alpha = col.a; surfaceData.emission = 0.0; surfaceData.metallic = _Metallic; surfaceData.occlusion = 1.0; surfaceData.smoothness = _Smoothness; surfaceData.specular = 0.0; surfaceData.clearCoatMask = 0.0h; surfaceData.clearCoatSmoothness = 0.0h; // InputDataを作成 InputData inputData = (InputData)0; inputData.positionWS = input.positionWS; inputData.normalWS = TransformTangentToWorld(surfaceData.normalTS, half3x3(input.tangentWS.xyz, input.bitangentWS.xyz, input.normalWS.xyz)); inputData.normalWS = NormalizeNormalPerPixel(inputData.normalWS); inputData.viewDirectionWS = SafeNormalize(input.viewDirWS); inputData.fogCoord = input.fogFactor; inputData.vertexLighting = input.vertexLight; inputData.bakedGI = SAMPLE_GI(input.lightmapUV, input.vertexSH, inputData.normalWS); inputData.normalizedScreenSpaceUV = GetNormalizedScreenSpaceUV(input.positionHCS); inputData.shadowMask = SAMPLE_SHADOWMASK(input.lightmapUV); #if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR) inputData.shadowCoord = input.shadowCoord; #elif defined(MAIN_LIGHT_CALCULATE_SHADOWS) inputData.shadowCoord = TransformWorldToShadowCoord(input.positionWS); #else inputData.shadowCoord = float4(0, 0, 0, 0); #endif // PBRのライティング計算 half4 color = UniversalFragmentPBR(inputData, surfaceData); // フォグを適用 color.rgb = MixFog(color.rgb, inputData.fogCoord); return color; } ENDHLSL } } }
長いですが、フラグメントシェーダのコメントを見ると以下の処理をしていることがわかります。
- SurfaceDataを作成
- InputDataを作成
- 1.と2.を使ってPRBのライティング計算
- フォグを適用
頂点シェーダから渡しているパラメータはこれらに必要なものを計算しているだけです。
この辺りは本記事では説明しませんが、興味があればURPに定義されている処理を追ってみてください。
結果
このシェーダをマテリアルに適用してレンダリングすると以下のような結果が得られます。
PBRでレンダリングされていることが確認できました。
ちなみに設定しているパラメータはこんな感じです。