はじめに
前回の前半の記事では HDRP の Unlit シェーダのパスの中で、SceneSelectionPass、DepthForwardOnly、MotionVectors を読んでみました。
今回は後半ということで残りの ForwardOnly、Meta、ShadowCaster、DistortionVectors を見ていきます。
ForwardOnly パス
では早速本丸の FowardOnly を見ていきましょう。基本的なコードの構造は DepthForwardOnly などと同じです。前回の記事と重複するところもありますがおさらいも兼ねて細かく見ていきます。Unlit.shader には次のように書かれています:
Pass
{
Name "ForwardOnly"
Tags { "LightMode" = "ForwardOnly" }
Blend [_SrcBlend] [_DstBlend], [_AlphaSrcBlend] [_AlphaDstBlend]
ZWrite [_ZWrite]
ZTest [_ZTestDepthEqualForOpaque]
Stencil
{
WriteMask[_StencilWriteMask]
Ref[_StencilRef]
Comp Always
Pass Replace
}
Cull [_CullMode]
HLSLPROGRAM
#pragma only_renderers d3d11 ps4 xboxone vulkan metal switch
#pragma multi_compile_instancing
#pragma multi_compile _ DEBUG_DISPLAY
#ifdef DEBUG_DISPLAY
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Debug/DebugDisplay.hlsl"
#endif
#define SHADERPASS SHADERPASS_FORWARD_UNLIT
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Material/Material.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Material/Unlit/Unlit.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Material/Unlit/ShaderPass/UnlitSharePass.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Material/Unlit/UnlitData.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/RenderPipeline/ShaderPass/ShaderPassForwardUnlit.hlsl"
#pragma vertex Vert
#pragma fragment Frag
ENDHLSL
}
ShaderPassForwardUnlit.hlsl に頂点・フラグメントシェーダが含まれるのでそちらを見ていきます。
頂点シェーダ
頂点シェーダは次のようになっています(テッセレーション付きのものも用意されていますがここでは省略):
PackedVaryingsType Vert(AttributesMesh inputMesh)
{
VaryingsType varyingsType;
varyingsType.vmesh = VertMesh(inputMesh);
return PackVaryingsType(varyingsType);
}
前回の記事でも見た他のパスのコードと同じです。VaryingsMeshToPs は次のように指定されたキーワードでメンバの付け足し可能なフラグメントシェーダへ渡すための情報を格納できる構造体です。
#define VaryingsType VaryingsToPS struct VaryingsToPS { VaryingsMeshToPS vmesh; ... }; struct VaryingsMeshToPS { float4 positionCS; #ifdef VARYINGS_NEED_POSITION_WS float3 positionRWS; #endif #ifdef VARYINGS_NEED_TANGENT_TO_WORLD float3 normalWS; float4 tangentWS; #endif #ifdef VARYINGS_NEED_TEXCOORD0 float2 texCoord0; #endif #ifdef VARYINGS_NEED_TEXCOORD1 float2 texCoord1; #endif #ifdef VARYINGS_NEED_TEXCOORD2 float2 texCoord2; #endif #ifdef VARYINGS_NEED_TEXCOORD3 float2 texCoord3; #endif #ifdef VARYINGS_NEED_COLOR float4 color; #endif UNITY_VERTEX_INPUT_INSTANCE_ID };
これに対して、Packed がついたものたちは、実際にフラグメントシェーダへ渡す際の構造体で、セマンティクス付きの構造体になっています。
#define PackVaryingsType PackVaryingsToPS struct PackedVaryingsToPS { PackedVaryingsMeshToPS vmesh; ... }; struct PackedVaryingsMeshToPS { float4 positionCS : SV_Position; #ifdef VARYINGS_NEED_POSITION_WS float3 interpolators0 : TEXCOORD0; #endif #ifdef VARYINGS_NEED_TANGENT_TO_WORLD float3 interpolators1 : TEXCOORD1; float4 interpolators2 : TEXCOORD2; #endif #ifdef VARYINGS_NEED_TEXCOORD1 float4 interpolators3 : TEXCOORD3; #elif defined(VARYINGS_NEED_TEXCOORD0) float2 interpolators3 : TEXCOORD3; #endif #ifdef VARYINGS_NEED_TEXCOORD3 float4 interpolators4 : TEXCOORD4; #elif defined(VARYINGS_NEED_TEXCOORD2) float2 interpolators4 : TEXCOORD4; #endif #ifdef VARYINGS_NEED_COLOR float4 interpolators5 : TEXCOORD5; #endif UNITY_VERTEX_INPUT_INSTANCE_ID #if defined(VARYINGS_NEED_CULLFACE) && SHADER_STAGE_FRAGMENT FRONT_FACE_TYPE cullFace : FRONT_FACE_SEMANTIC; #endif };
複数のシェーダおよびそのパスから利用したり、シェーダグラフによって利用されるために、必要な情報のみを集めパックして、フラグメントシェーダでアンパックして使う構造になっているものと思われます。
さて、頂点シェーダのコードに戻りますと vmesh に VertMesh() の結果を渡しています。VertMesh を見てみましょう:
VaryingsMeshType VertMesh(AttributesMesh input)
{
VaryingsMeshType output;
// インスタンシングの設定
UNITY_SETUP_INSTANCE_ID(input);
UNITY_TRANSFER_INSTANCE_ID(input, output);
// メッシュの変形が適用される
// 頂点アニメーションやカスタムパスなど
#if defined(HAVE_MESH_MODIFICATION)
input = ApplyMeshModification(input, _TimeParameters.xyz);
#endif
// positionWS はカメラ座標系における位置になったのでカメラ相対ワールド座標
// という意味で positionRWS へと改名された(後述)
// unity_ObjectToWorld は直接使わない。URP 同様 TransformObjectToWorld を使う
float3 positionRWS = TransformObjectToWorld(input.positionOS);
// 法線をワールド空間へ変換
#ifdef ATTRIBUTES_NEED_NORMAL
float3 normalWS = TransformObjectToWorldNormal(input.normalOS);
#else
// 法線を必要としない ApplyVertexModification 用に 0 を詰めておく
float3 normalWS = float3(0.0, 0.0, 0.0);
#endif
// ワールド空間の Tangent へ変換
#ifdef ATTRIBUTES_NEED_TANGENT
float4 tangentWS = float4(TransformObjectToWorldDir(input.tangentOS.xyz), input.tangentOS.w);
#endif
// 必要であればディスプレースメントなどの変形が適用される
#if defined(HAVE_VERTEX_MODIFICATION)
ApplyVertexModification(input, normalWS, positionRWS, _TimeParameters.xyz);
#endif
// 出力に位置・法線・接線を詰める
// テッセレーションのためにキーワードの場合分けをしている
#ifdef TESSELLATION_ON
output.positionRWS = positionRWS;
output.normalWS = normalWS;
#if defined(VARYINGS_NEED_TANGENT_TO_WORLD) || defined(VARYINGS_DS_NEED_TANGENT)
output.tangentWS = tangentWS;
#endif
#else
#ifdef VARYINGS_NEED_POSITION_WS
output.positionRWS = positionRWS;
#endif
output.positionCS = TransformWorldToHClip(positionRWS);
#ifdef VARYINGS_NEED_TANGENT_TO_WORLD
output.normalWS = normalWS;
output.tangentWS = tangentWS;
#endif
#endif
// 残りの情報を必要に応じて詰める
#if defined(VARYINGS_NEED_TEXCOORD0) || defined(VARYINGS_DS_NEED_TEXCOORD0)
output.texCoord0 = input.uv0;
#endif
#if defined(VARYINGS_NEED_TEXCOORD1) || defined(VARYINGS_DS_NEED_TEXCOORD1)
output.texCoord1 = input.uv1;
#endif
#if defined(VARYINGS_NEED_TEXCOORD2) || defined(VARYINGS_DS_NEED_TEXCOORD2)
output.texCoord2 = input.uv2;
#endif
#if defined(VARYINGS_NEED_TEXCOORD3) || defined(VARYINGS_DS_NEED_TEXCOORD3)
output.texCoord3 = input.uv3;
#endif
#if defined(VARYINGS_NEED_COLOR) || defined(VARYINGS_DS_NEED_COLOR)
output.color = input.color;
#endif
return output;
}
コードは長く見えますが、やっていることは(当たり前ですが)従来のシェーダーと同じで、座標変換と頂点変形、その他パラメタの転送です。座標系だけカメラ相対な座標系(Camera-relative Rendering)になっているのに注意が必要です。
フラグメントシェーダ
次にフラグメントシェーダのコードを見てみましょう。
float4 Frag(PackedVaryingsToPS packedInput) : SV_Target
{
// XR 用
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(packedInput);
// パックされた情報を展開
FragInputs input = UnpackVaryingsMeshToFragInputs(packedInput.vmesh);
// 位置情報を取り出す(ワールド座標やデプスなど)
PositionInputs posInput = GetPositionInput(
input.positionSS.xy,
_ScreenSize.zw,
input.positionSS.z,
input.positionSS.w,
input.positionRWS);
// ワールドスペースのビューの方向、中では Cemare-relative Rendering かどうか
// Persepective か Orthographic かなどを場合分けして値を返してくれる
#ifdef VARYINGS_NEED_POSITION_WS
float3 V = GetWorldSpaceNormalizeViewDir(input.positionRWS);
#else
// 0 割を避けるためにセットしておく
float3 V = float3(1.0, 1.0, 1.0);
#endif
// SurfaceData / BuiltinData に情報を詰める
SurfaceData surfaceData;
BuiltinData builtinData;
GetSurfaceAndBuiltinData(input, V, posInput, surfaceData, builtinData);
// BSDF 用のデータをつめる、Unlit では color を入れるだけ
BSDFData bsdfData = ConvertSurfaceDataToBSDFData(input.positionSS.xy, surfaceData);
// ブレンドの適用
float4 outColor = ApplyBlendMode(
bsdfData.color + builtinData.emissiveColor * GetCurrentExposureMultiplier(),
builtinData.opacity);
// 大気散乱の適用
outColor = EvaluateAtmosphericScattering(posInput, V, outColor);
#ifdef DEBUG_DISPLAY
...
#endif
return outColor;
}
前回、GetPositionInput() について解説し忘れてしまったのでここで書きます。PositionInputs は次のような構造体です。
struct PositionInputs { float3 positionWS; // ワールド座標(カメラ相対座標であることもある) float2 positionNDC; // [0, 1) の正規化デバイス座標 uint2 positionSS; // [0, NumPixels) のスクリーンスペース座標 uint2 tileCoord; // [0, NumTiles) のスクリーンタイルの座標 float deviceDepth; // [0, 1) のデプスバッファの深度 float linearDepth; // [Near, Far] のビュー空間の Z 座標 };
これを次のように与えられた変数から計算します。
PositionInputs GetPositionInput(
float2 positionSS,
float2 invScreenSize,
float deviceDepth,
float linearDepth,
float3 positionWS)
{
return GetPositionInput(positionSS, invScreenSize, deviceDepth, linearDepth, positionWS, uint2(0, 0));
}
PositionInputs GetPositionInput(
float2 positionSS,
float2 invScreenSize,
float deviceDepth,
float linearDepth,
float3 positionWS,
uint2 tileCoord)
{
PositionInputs posInput = GetPositionInput(positionSS, invScreenSize, tileCoord);
posInput.positionWS = positionWS;
posInput.deviceDepth = deviceDepth;
posInput.linearDepth = linearDepth;
return posInput;
}
PositionInputs GetPositionInput(
float2 positionSS,
float2 invScreenSize,
uint2 tileCoord)
{
PositionInputs posInput;
ZERO_INITIALIZE(PositionInputs, posInput);
posInput.positionNDC = positionSS;
#if defined(SHADER_STAGE_COMPUTE) || defined(SHADER_STAGE_RAY_TRACING)
posInput.positionNDC.xy += float2(0.5, 0.5);
#endif
posInput.positionNDC *= invScreenSize;
posInput.positionSS = uint2(positionSS);
posInput.tileCoord = tileCoord;
return posInput;
}
処理に必要な一連の座標が得られていますね。
では続きを見ていきましょう。ForwardOnly パスの DepthForwardOnly との違いは ConvertSurfaceDataToBSDFData()、ApplyBlendMode()、EvaluateAtmosphericScattering() でカラーの計算を行っている点です。まずは 1 つ目の BSDF の計算を見てみましょう。従来のビルトインパイプラインでは UNITY_BRDF_PBS() を使ってBRDF(双方向反射率分布関数)の計算を行っていました。
HDRP では BRDF に加え、BTDF(双方向透過分布関数)も加えた計算である BSDF(= BRDF + BTDF)を使っています。
とはいえ今見ているのは Unlit ですので計算は特に行いません。計算結果を格納する BSDFData は各パス向けに .cs ファイルから自動生成されるようで、ForwardOnly パスに対しては HDRP/Material/Unlit/Unlit.cs.hlsl で定義される次のようなシンプルなものになっています。
struct BSDFData
{
float3 color;
};
そしてこれにデータを詰める ConvertSurfaceDataToBSDFData() は、HDRP/Material/Unlit/Unlit.hlsl で次のように定義されています。
BSDFData ConvertSurfaceDataToBSDFData(uint2 positionSS, SurfaceData data)
{
BSDFData output;
output.color = data.color;
return output;
}
単に指定された色を格納するだけですね。そしてこの色はブレンドのために ApplyBlendMode() を通されます。
float4 ApplyBlendMode(float3 color, float opacity) { return ApplyBlendMode(color, float3(0.0, 0.0, 0.0), opacity); } float4 ApplyBlendMode(float3 diffuseLighting, float3 specularLighting, float opacity) { #ifdef _BLENDMODE_PRESERVE_SPECULAR_LIGHTING #if defined(_BLENDMODE_ADD) || defined(_BLENDMODE_ALPHA) return float4(diffuseLighting * opacity + specularLighting, opacity); #else // defined(_BLENDMODE_PRE_MULTIPLY) return float4(diffuseLighting + specularLighting, opacity); #endif #else #if defined(_BLENDMODE_ADD) || defined(_BLENDMODE_ALPHA) return float4((diffuseLighting + specularLighting) * opacity, opacity); #else // defined(_BLENDMODE_PRE_MULTIPLY) return float4(diffuseLighting + specularLighting, opacity); #endif #endif }
アルファのブレンドの際はスペキュラ成分が考慮されます。以下の Naughty Dog の資料が参照されていました(実際は精度問題もあり色々試していそう)。
ただ、Unlit の場合はスペキュラ成分はなく 1 つめの関数を通じて (0.0, 0.0, 0.0) が渡されます。なので #else ブロックの方に入り、単純にブレンドモードが Premultiply かどうかで場合分けをしているだけです(事前にテクスチャにアルファ乗算済みのカラーが適用されている場合乗算処理をスキップ)。
最後にこうして決定された結果の色に対して EvaluateAtmosphericScattering() で大気散乱が適用されます。
float4 EvaluateAtmosphericScattering(PositionInputs posInput, float3 V, float4 inputColor)
{
float4 result = inputColor;
#ifdef _ENABLE_FOG_ON_TRANSPARENT
float3 volColor, volOpacity;
EvaluateAtmosphericScattering(posInput, V, volColor, volOpacity);
#if defined(_BLENDMODE_ALPHA)
result.rgb = result.rgb * (1 - volOpacity) + volColor * result.a;
#elif defined(_BLENDMODE_ADD)
result.rgb = result.rgb * (1.0 - volOpacity);
#elif defined(_BLENDMODE_PRE_MULTIPLY)
result.rgb = result.rgb * (1 - volOpacity) + volColor * result.a;
#endif
#endif
return result;
}
位置からフォグの色を計算し、ブレンドモードに応じて場合分けして出力する色に適用しています。こうして Unlit なマテリアルの色が決定されます。
ただデバッグ関連の設定がなされているときは、この値が上書きされます。Window > Render Pipeline > Render Pipeline Debug をで Debug ウィンドウを開き見たいパラメタを選択すると Scene / Game ビューで指定したパラメタを見ることができます。

設定した際は DEBUG_DIAPLY キーワードが ON になり、次のような #ifdef 文に入るようになります。
#ifdef DEBUG_DISPLAY int bufferSize = int(_DebugViewMaterialArray[0]); for (int index = 1; index <= bufferSize; index++) { int indexMaterialProperty = int(_DebugViewMaterialArray[index]); if (indexMaterialProperty != 0) { float3 result = float3(1.0, 0.0, 1.0); bool needLinearToSRGB = false; GetPropertiesDataDebug(indexMaterialProperty, result, needLinearToSRGB); GetVaryingsDataDebug(indexMaterialProperty, input, result, needLinearToSRGB); GetBuiltinDataDebug(indexMaterialProperty, builtinData, result, needLinearToSRGB); GetSurfaceDataDebug(indexMaterialProperty, surfaceData, result, needLinearToSRGB); GetBSDFDataDebug(indexMaterialProperty, bsdfData, result, needLinearToSRGB); if (!needLinearToSRGB) result = SRGBToLinear(max(0, result)); outColor = float4(result, 1.0); } } if (_DebugFullScreenMode == FULLSCREENDEBUGMODE_TRANSPARENCY_OVERDRAW) { float4 result = _DebugTransparencyOverdrawWeight * float4( TRANSPARENCY_OVERDRAW_COST, TRANSPARENCY_OVERDRAW_COST, TRANSPARENCY_OVERDRAW_COST, TRANSPARENCY_OVERDRAW_A); outColor = result; } #endif
例えば GetVaryingsDataDebug() を見てみると次のように指定されたタイプに応じて次のように場合分けされ、値が上書きされています。
void GetVaryingsDataDebug(uint paramId, FragInputs input, inout float3 result, inout bool needLinearToSRGB) { switch (paramId) { case DEBUGVIEWVARYING_TEXCOORD0: result = float3(input.texCoord0, 0.0); break; case DEBUGVIEWVARYING_TEXCOORD1: result = float3(input.texCoord1, 0.0); break; case DEBUGVIEWVARYING_TEXCOORD2: result = float3(input.texCoord2, 0.0); break; case DEBUGVIEWVARYING_TEXCOORD3: result = float3(input.texCoord3, 0.0); break; case DEBUGVIEWVARYING_VERTEX_TANGENT_WS: result = input.worldToTangent[0].xyz * 0.5 + 0.5; break; case DEBUGVIEWVARYING_VERTEX_BITANGENT_WS: result = input.worldToTangent[1].xyz * 0.5 + 0.5; break; case DEBUGVIEWVARYING_VERTEX_NORMAL_WS: result = input.worldToTangent[2].xyz * 0.5 + 0.5; break; case DEBUGVIEWVARYING_VERTEX_COLOR: result = input.color.rgb; needLinearToSRGB = true; break; case DEBUGVIEWVARYING_VERTEX_COLOR_ALPHA: result = input.color.aaa; break; } }
以上がフラグメントシェーダになります。
Meta パス
Meta パスはライトマップや GI 向けに情報を提供するパスで通常のレンダリングの間には利用されません。今回はこちらは省略します。
ShadowCaster パス
ShadowCaster パスは SceneSelectionPass や DepthForwardOnly と同じく ShaderPassDepthOnly.hlsl にパスが記述されています(共通して使っています)。ShadowCaster パスがやることは DepthForwardOnly と同じくデプスを出力する点で、違うのはカメラから見ているかライトから見ているかというものです。説明は前回の DepthForwardOnly の項をご参照ください。
DistortionVectors パス
Distortion は次のような効果を適用できます。

インスペクタ上で Distortion Vector Map (RGB) の RG に方向を、B に大きさを入力し、いくつかパラメタが調整できるようになっています。

これらのパラメタはフラグメントシェーダ内で使われます。まずフラグメントシェーダの外観は次のような感じです。
float4 Frag(PackedVaryingsToPS packedInput) : SV_Target
{
...
// アルファテストとディストーションの情報を得る
SurfaceData surfaceData;
BuiltinData builtinData;
GetSurfaceAndBuiltinData(input, V, posInput, surfaceData, builtinData);
// このピクセルをディストーションの対象にする
float4 outBuffer;
EncodeDistortion(builtinData.distortion, builtinData.distortionBlur, true, outBuffer);
return outBuffer;
}
GetSurfaceAndBuiltinData() でインスペクタで設定したパラメタを使って UV をずらす方向と大きさの計算がなされます。
void GetSurfaceAndBuiltinData(...) { ... #if (SHADERPASS == SHADERPASS_DISTORTION) || defined(DEBUG_DISPLAY) float3 distortion = SAMPLE_TEXTURE2D( _DistortionVectorMap, sampler_DistortionVectorMap, input.texCoord0.xy).rgb; distortion.rg = distortion.rg * _DistortionVectorScale.xx + _DistortionVectorBias.xx; builtinData.distortion = distortion.rg * _DistortionScale; builtinData.distortionBlur = clamp(distortion.b * _DistortionBlurScale, 0.0, 1.0) * (_DistortionBlurRemapMax - _DistortionBlurRemapMin) + _DistortionBlurRemapMin; #endif ... }
こうして計算したディストーションのパラメタを EncodeDistortion() で書き出します。このパスの RenderTarget は専用のもの(R16G16B16A16_SFloat)が用意されており、これに対して EncodeDistortion() は次のようにエンコードを行います。
void EncodeDistortion( float2 distortion, float distortionBlur, bool isValidSource, out float4 outBuffer) { outBuffer = float4(distortion, isValidSource, distortionBlur); }
いったんこうしてディストーション用のレンダーターゲットに情報を集めた後、ディストーション自体は次のステージで行われます。

この ApplyDistortion を行っているシェーダを見てみます。
float4 Frag(Varyings input) : SV_Target
{
const float _FetchBias = 0.9;
// エンコードされたディストーションの値を取得、デコード
float4 encodedDistortion = LOAD_TEXTURE2D_X(_DistortionTexture, input.positionCS.xy);
float2 distortion;
float distortionBlur;
bool distortionIsSourceValid;
DecodeDistortion(
encodedDistortion,
distortion,
distortionBlur,
distortionIsSourceValid);
// ディストーションが適用されていないピクセルは棄却
if (!distortionIsSourceValid)
{
discard;
return 0;
}
// 画面外のピクセルを参照している場合は distortion を 0.0 にする
int2 distortedEncodedDistortionId = input.positionCS.xy + int2(distortion);
if (any(distortedEncodedDistortionId < 0)
|| any(distortedEncodedDistortionId > int2(_Size.xy)))
{
distortion = 0.0f;
}
// 再度ディストーション適用後の座標のディストーションテクスチャを見に行く
// その場所がディストーション適用範囲外だったらオフセットしないようにする
float2 distordedDistortion;
float distordedDistortionBlur;
bool distordedIsSourceValid;
float4 encodedDistordedDistortion = LOAD_TEXTURE2D_X(
_DistortionTexture, distortedEncodedDistortionId);
DecodeDistortion(
encodedDistordedDistortion,
distordedDistortion,
distordedDistortionBlur,
distordedIsSourceValid);
if (!distordedIsSourceValid)
{
distortion = 0.0f;
}
// 前のステージで作成された _ColorPyramidTexture から
// 歪み量に応じてブラーの掛かった画を取ってくる
float2 distordedUV = float2(input.positionCS.xy + distortion * _FetchBias) * _Size.zw;
float mip = (_ColorPyramidScale.z - 1) * clamp(distortionBlur, 0.0, 1.0);
float4 sampled = SAMPLE_TEXTURE2D_X_LOD(
_ColorPyramidTexture,
s_trilinear_clamp_sampler,
distordedUV * _ColorPyramidScale.xy,
mip);
return sampled;
}
先のスクリーンショットにもあるように、この前のステージの ColorPyramid でスクリーンのイメージの mipmap が生成されているようで、ここから SAMPLE_TEXTURE2D_X_LOD() で distortionBlur から計算した mip のテクスチャを取ってきているようです。
おわりに
これで一通り Unlit シェーダが見終わりました。これで Unlit なら uRaymarching の移植ができるかも...。次は Lit シェーダを見ていこうと思います。
