【Universal RP】SRP Batcher対応の積雪シェーダーを書いてみた 


# はじめに
シェーダーで積雪を作ることに興味があったので、
**Real-time Rendering of Accumulated Snow** という論文をUniversal RPのHLSLシェーダーで実装してみました。
https://ep.liu.se/ecp/013/007/ecp01307.pdf

ちょうど良い機会なので、HLSLシェーダーを **SRP Batcher**対応させてみました。

<img width = 480 src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/533de7a1-1a33-db8e-377c-5618d94c2dac.png">

GitHub リポジトリ
https://github.com/rngtm/UniversalRP-SnowShader

## SRP Batcher って何？
SRP Batcher は CPUのレンダリング処理速度を上げるための仕組みです。
レンダリングの処理速度が **1.2 倍から4 倍**にまで向上するそうです。(すごい！)

詳細についてはUnity公式ブログをご覧ください。

SRP Batcher：レンダリングをスピードアップ
https://blogs.unity3d.com/jp/2019/02/28/srp-batcher-speed-up-your-rendering/

# 環境
Unity 2020.1.2f1
Universal RP 8.20

# 実装したもの
本記事では、面の傾きのみを利用して積雪を計算します。

# 積雪シェーディング
積雪シェーダーの実装に入る前に、論文(https://ep.liu.se/ecp/013/007/ecp01307.pdf )に載っている積雪の計算式を紹介します。

## 積雪予測関数 (Snow Accumulation Prediction Function)
次の計算式で、積雪量$f_p$ を計算します。

```math
f_p(p) = f_e(p) \cdot f_{inc}(p)
```

|記号|意味|
|:---:|:---:|
|$f_p(p)$|ある位置$p$に積もる雪の量 (0~1の値をとる)|
|$f_e(p)$|ある位置$p$の、空に対する露出の度合い(0~1の値をとる)|
|$f_{inc}(p)$|面の傾き|


## 傾きの計算 (inclination)
傾きを計算する関数 $f_{inc}$ は以下で表します。

```math
f_{inc} = \left\{ \,
    \begin{aligned}
    & cos \theta + n  & 0 \leq \theta \leq 90 \\
    & 0 & \theta \geq 90
    \end{aligned}
\right.
```

|記号|意味|
|:---:|:---:|
|$\theta$ | 雪の方向を向くベクトルUと法線ベクトルNがなす角 |
|$n$|小さなノイズ値 (0 ~ 0.4 の間の値を使うと良い感じになりやすい)|)

## 雪の陰影 (Phong Illumination Model)
雪の陰影には Phong反射モデル(Phong Illumination Model)を使用します。
Phong反射モデルについては、Wikipediaなどを参考にすると良いでしょう。
https://ja.wikipedia.org/wiki/Phong%E3%81%AE%E5%8F%8D%E5%B0%84%E3%83%A2%E3%83%87%E3%83%AB

ディレクショナルライトが1個だけシーンに存在する場合、陰影の計算式は以下のようになります。

```math
C_s = k_a + k_d \cdot (N \cdot L) + k_s\cdot (V \cdot R) ^ \alpha
```

|記号|意味|
|:---:|:---:|
|$C_s$ | 最終的な雪の色 |
|$k_a$ | 環境光の色(ambient) |
|$k_d$ | ディフューズ色(diffuse) |
|$k_s$ | スペキュラー色(specular) |
|$N$ | 光が当たる位置の法線 |
|$L$ | ライトベクトル(光が当たる位置から光源位置を向くベクトル)|
|$V$ | 視線ベクトル(光が当たる位置から視点位置を向くベクトル)|
|$R$ | 反射ベクトル(物体に当たって反射した光が進む方向)|
|$\alpha$ | 光沢度(gloss) |


## 法線をゆがませる
通常はスペキュラー項とディフューズ項には物体表面の法線Nをそのまま使用しますが、
今回の雪シェーディングでは法線にノイズを加算して、法線を歪ませるということを行います。

```math
N_{\alpha} = N + \alpha n - dE
```

|記号|意味|
|:---:|:---:|
|$N_{\alpha}$ | 歪ませた結果の法線|
|$\alpha$|歪みの強度|
|$n$|3つの小さなノイズ値を持つベクトル|
|$dE$|露出関数をスクリーンスペースで微分し、これを定数倍して得られる値|

今回は露出値 $E$ は定数値を設定するため、その微分 $dE = 0.0$ になります。(定数値の微分は0)

## ゆがみあり法線をPhongの反射モデルで利用
$\alpha = 0.4$にして得られる法線$N_\alpha$を利用して、 ディフューズ項 ($N_a \cdot L$) を計算します。
$\alpha = 0.8$にして得られる法線$N_\alpha$を利用して、スペキュラー項($V \cdot R$)を計算します。

## 最終的な色
陰影を施した雪の色 $C_s$ と、雪が乗っていない色 $C_n$ を 積雪の量 $f_p$ で線形補間したものが最終的な色になります。

```math
C = f_p \cdot C_s + (1− f_p) \cdot  C_n
```


# HLSLシェーダーで積雪を作る
今回はレンダリングへの理解を深めたいということもあり、HLSLで積雪レンダリングの実装に挑戦してみました。

HLSLでシェーダーを書く場合、
頂点座標のMVP変換(、ライト情報の管理などの取り回しを自分で書くことになります。
シェーダー実装の難易度は高くなってしまうのですが、
レンダリングパイプラインへの理解を深めるのには良い勉強になるかと思います。

HLSLでシェーダーを書く場合、Universal RPパッケージ内の ```Lit.shader``` を参考にするとよいでしょう


## シェーダーを書く
今回は2つのHLSLシェーダーと、1つの.shaderを作成します。

|ファイル名|実装内容|
|:---:|:---:|
|Noise.hlsl|雪に使用する3Dノイズの定義|
|Snow.hlsl|モデル + 積雪の描画シェーダー|
|Snow.shader|シェーダープロパティやパスの定義|

HLSLのインクルード参照関係は以下のようになっています。
オレンジ色の箱は今回作成するファイルで、白色の箱は元から存在するHLSLです。
<img width = 320 src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/f92fbcc3-ae77-7f6e-b2a5-b572008284d5.png">

<details><summary>Noise.shader</summary>
<div>

```Noise.hlsl
////////////////////////////////////////////////////////////
// Noise ( from : https://www.shadertoy.com/view/Wl2XzW )
////////////////////////////////////////////////////////////
#define HASHSCALE1 float3(0.1031, 0.1031, 0.1031)

float3 hash(float3 p3)
{
	p3 = frac(p3 * HASHSCALE1);
	p3 += dot(p3, p3.yxz+19.19);
	return frac((p3.xxy + p3.yxx)*p3.zyx);
}

float3 noise( in float3 x )
{
	float3 p = floor(x);
	float3 f = frac(x);
	f = f*f*(3.0-2.0*f);
	
	return lerp(lerp(lerp( hash(p+float3(0,0,0)), 
						hash(p+float3(1,0,0)),f.x),
					lerp( hash(p+float3(0,1,0)), 
						hash(p+float3(1,1,0)),f.x),f.y),
				lerp(lerp( hash(p+float3(0,0,1)), 
						hash(p+float3(1,0,1)),f.x),
					lerp( hash(p+float3(0,1,1)), 
						hash(p+float3(1,1,1)),f.x),f.y),f.z);
}

const float3x3 m3 = float3x3( 0.00,  0.80,  0.60,
					-0.80,  0.36, -0.48,
					-0.60, -0.48,  0.64 );
float3 fbm(in float3 q)
{
	float3 f  = 0.5000*noise( q ); q = mul(m3, q*2.01);
	f += 0.2500*noise( q ); q = mul(m3, q*2.02);
	f += 0.1250*noise( q ); q = mul(m3, q*2.03);
	f += 0.0625*noise( q ); q = mul(m3, q*2.04);
#if 0
	f += 0.03125*noise( q ); q = mul(m3, q*2.05);
	f += 0.015625*noise( q ); q = mul(m3, q*2.06);
	f += 0.0078125*noise( q ); q = mul(m3, q*2.07);
	f += 0.00390625*noise( q ); q = mul(m3, q*2.08);  
#endif
	return float3(f);
}
```

</div></details>



<details><summary>Snow.shader</summary>
<div>

```hlsl:Snow.shader
Shader "Custom/Snow" {
    Properties
    {
        [Header(Snow)]
        // 雪の量
        _SnowAmount("Snow Amount", Float) = 1.0 
        // 入ってくる雪に対するベクトル
        _SnowDirection("Snow Direction", Vector) = (0, 1, 0, 0) 
        // 雪の範囲の変換用パラメータ
        _SnowDotNormalRemap("dot(N, S) remap", Vector) = (-1, 1, -1, 1) 
        
        [Header(Color)]
        // 物体の色
        _BaseColor("Base Color", Color) = (0,0,0,0) 
        // 雪の色
        _SnowColor("Snow Color", Color) = (0,0,0,0) 
        // ディフューズ項の色
        [HDR]_DiffuseColor("Diffuse Color", Color) = (1,1,1,1) 
        // スペキュラー項の色
        [HDR]_SpecularColor("Specular Color", Color) = (1,1,1,1) 
        
        [Header(Noise)]
        // ノイズに計算に使用する座標スケール
        _NoisePositionScale("Noise Scale(Position)", Float) = 64.0 
        // 面の法線に加えるノイズの大きさ
        _NoiseInclinationScale("Noise Scale (Inclination)", Float) = 0.1 
        // ディフューズ項の法線に加えるノイズの大きさ
        _DiffuseNormalDistortion("Diffuse Normal Distortion", Float) = 0.4 
        // ディフューズ項の法線に加えるノイズの大きさ
        _SpecularNormalDistortion("Specular Normal Distortion", Float) = 0.8 
        
        [Header(Lighting)]
        // 環境光成分
        _AmbientFactor("Ambient Light", Float) = 0.5
        // 光沢度
        _SpecularGloss("Specular Gloss", Float) = 10.0
        
        // Blending state
        [HideInInspector] _Surface("__surface", Float) = 0.0
        [HideInInspector] _Blend("__blend", Float) = 0.0
        [HideInInspector] _AlphaClip("__clip", Float) = 0.0
        [HideInInspector] _SrcBlend("__src", Float) = 1.0
        [HideInInspector] _DstBlend("__dst", Float) = 0.0
        [HideInInspector] _ZWrite("__zw", Float) = 1.0
        [HideInInspector] _Cull("__cull", Float) = 2.0
    }
    
    SubShader { 
        Tags{"RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" "IgnoreProjector" = "True"}
        LOD 300
         
        Pass {
            Name "SnowPass"
            Tags { "LightMode"="UniversalForward" }
            
            Blend[_SrcBlend][_DstBlend]
            ZWrite[_ZWrite]
            Cull[_Cull]
            
            HLSLPROGRAM
            // All shaders must be compiled with HLSLcc and currently only gles is not using HLSLcc by default
            #pragma prefer_hlslcc gles
            #pragma exclude_renderers d3d11_9x
            #pragma target 2.0            
            
            #pragma vertex MyVert
            #pragma fragment MyFrag
            // hlslファイルのインクルード
            // 積雪シェーダーの実体はSnow.hlslに定義します
            #include "Assets/Shaders/HLSL/Snow.hlsl" 
            ENDHLSL
        }
    }
    FallBack "Hidden/Universal Render Pipeline/FallbackError"
}
```

</div>
</details>



<details><summary>Snow.hlsl</summary><div>

```Snow.hlsl
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#include "Noise.hlsl"

// -------------------------------------
// 定数の定義
CBUFFER_START(UnityPerMaterial)
// Texture 
float4 _MainTex_ST;
// Snow
half _SnowAmount;
half3 _SnowDirection;
half4 _SnowDotNormalRemap;
// Color
half _NoisePositionScale;
half _NoiseInclinationScale;
half4 _DiffuseColor;
half4 _SpecularColor;
// Noise
half4 _BaseColor;
half4 _SnowColor;
half _DiffuseNormalDistortion; // 色計算のディフューズ項の法線に加えるノイズ強度
half _SpecularNormalDistortion; // 色計算のスペキュラー項の法線に加えるノイズ強度
// Lighting
half _SpecularGloss;
half _AmbientFactor;
CBUFFER_END

// テクスチャサンプラー定義
TEXTURE2D(_MainTex);   
SAMPLER(sampler_MainTex);

// -------------------------------------
// 頂点シェーダーへの入力を格納するstruct
struct Attributes
{
    float4 positionOS   : POSITION; // vertex position (object space)
    float3 normalOS     : NORMAL; // vertex normal (object space)
    float2 texcoord     : TEXCOORD0; // texture coordinate
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

// -------------------------------------
// 頂点シェーダーの出力を格納するためのstruct
struct Varyings
{
    float4 positionCS : SV_POSITION;  // position (clip space)
    float2 uv : TEXCOORD0; // uv 
    float3 positionWS : TEXCOORD1; // position (world space) 
    float3 normalWS : TEXCOORD2; // normal (world space) 
};

// valueを範囲を[x, y]から[z, w]へ変換する 
float remap(float value, float4 range)
{
    return (value - range.x) * (range.w - range.z) / (range.y - range.x) + range.z;
}

// 頂点シェーダー
Varyings MyVert(Attributes IN) {
    Varyings OUT = (Varyings)0;
    
    // オブジェクト空間の座標をMVP変換
    VertexPositionInputs vertexInput = GetVertexPositionInputs(IN.positionOS.xyz); 
    OUT.positionCS = vertexInput.positionCS; // クリッピング空間の座標 (これを設定しないとメッシュが表示されない)
    OUT.positionWS = vertexInput.positionWS; // ワールド空間の座標
    
    // オブジェクト法線からワールド法線、接ベクトル空間などを計算
    VertexNormalInputs normalInputs = GetVertexNormalInputs(IN.normalOS.xyz); 
    OUT.normalWS = normalInputs.normalWS; // ワールド空間の座標
    // OUT.normalWS = TransformObjectToWorldNormal(IN.normalOS.xyz); // ワールド法線を取得するだけならこっちでも良い 
    
    // テクスチャ座標のコピー
    OUT.uv = TRANSFORM_TEX(IN.texcoord, _MainTex); // TRANSFORM_TEXはタイリング・オフセットを計算するマクロ

    return OUT;
}

// フラグメントシェーダー
half4 MyFrag(Varyings IN) : SV_Target {
    float3 snowDir = normalize(_SnowDirection); // 雪の向き
    float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - IN.positionWS.xyz); // 視線ベクトル
    float3 noise = fbm(IN.positionWS * _NoisePositionScale);

    // fe : 露出関数の値
    float fe = 1.0; // 常に露出
    
    // dE : 露出関数をスクリーンスペースで微分したもの (今回は常に同じ値をとるので0を入れておく)
    float3 dE = float3(0.0, 0.0, 0.0);
    
    // finc : 面の傾きの雪への寄与度
    float nDotS = dot(IN.normalWS, snowDir); // 面の傾きが積雪に影響する成分
    nDotS = remap(nDotS, _SnowDotNormalRemap);
    
    // 面の傾きの積雪への寄与度
    float finc = nDotS // 面の傾きによる積雪量のコントロール
     + _NoiseInclinationScale * length(noise); // ノイズを加える
        
    // fp : 積雪予測関数 (雪が積もる量)
    float fp = saturate(fe * finc * _SnowAmount);
    
    // 雪の照明計算に使う法線
    float3 diffuseNormal; 
    float3 specularNormal; 
    diffuseNormal = normalize(IN.normalWS + fp  * noise * _DiffuseNormalDistortion + dE); 
    specularNormal = normalize(IN.normalWS + fp * noise * _SpecularNormalDistortion + dE); 
    
    // 物体を雪が覆った場合の色
    float3 light = _MainLightPosition.xyz; // MainLight
    float3 refl = reflect(light, specularNormal); // 光の反射ベクトル
    
    // 歪ませた法線を使って、スペキュラー項、ディフューズ項を計算する
    float nDotL = clamp(dot(diffuseNormal, light), 0.0, 1.0); 
    float vDotR = clamp(dot(-viewDir, refl), 0.0, 1.0);
    
    // テクスチャ
    half4 texColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, IN.uv);
    half3 baseColor = _BaseColor.rgb * texColor.rgb;
    
    // 最終的な色の計算
    half3 diffuse = _DiffuseColor.rgb * _DiffuseColor.a * nDotL;
    half3 specular = _SpecularColor.rgb * _SpecularColor.a * pow(vDotR, _SpecularGloss);
    half3 snowColor = _AmbientFactor + diffuse + specular; // 雪に覆われた時の色
    
    return half4(lerp(baseColor, snowColor, fp) * _MainLightColor.rgb * nDotL, 1.0);
}
```

</div></details>

### 結果
球体モデルにシェーダーを適用すると、以下のような雪が乗っているような表示になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/e7505910-0c04-c092-db75-bd211c407c5e.png)


ただ、地面に影が落ちていません。
モデルから地面に影を落とすためには、**ShadowCasterPass** を追加する必要があります。

## ShadowCasterPassの追加
hlsl:ShadowCasterパスの中で実行するシェーダーをHLSLファイルで定義します。


```hlsl:ShadowCaster.hlsl
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

// -------------------------------------
// 頂点シェーダーへの入力を格納するstruct
struct Attributes
{
    float4 positionOS   : POSITION; // vertex position (object space)
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

// 頂点シェーダーの出力を格納するためのstruct
struct Varyings
{
    float4 positionCS : SV_POSITION;  // position (clip space)
};

// 頂点シェーダー (座標変換だけ行う)
Varyings MyVert(Attributes IN) {
    Varyings OUT = (Varyings)0;
    
    // オブジェクト空間の座標をMVP変換
    VertexPositionInputs vertexInput = GetVertexPositionInputs(IN.positionOS.xyz); 
    OUT.positionCS = vertexInput.positionCS; // クリッピング空間の座標 (影が表示されない)
    
    return OUT;
}

// フラグメントシェーダー (影を投げるだけなので、フラグメントシェーダーでは何もしない)
half4 MyFrag(Varyings IN) : SV_Target {
    return 0;
}
```


Snow.shaderには、ShadowCasterパスを追加し、中でShadowCasterPass.hlslを呼び出します。

```Snow.shader
        Pass {
            Name "SnowPass"
            Tags { "LightMode"="ShadowCaster" }
            
            Blend[_SrcBlend][_DstBlend]
            ZWrite[_ZWrite]
            Cull[_Cull]
            
            HLSLPROGRAM
            // All shaders must be compiled with HLSLcc and currently only gles is not using HLSLcc by default
            #pragma prefer_hlslcc gles
            #pragma exclude_renderers d3d11_9x
            #pragma target 2.0
                        
            #pragma vertex MyVert
            #pragma fragment MyFrag
            // hlslファイルのインクルード
            #include "Assets/Shaders/HLSL/ShadowCasterPass.hlsl" 
            ENDHLSL
        }
```

インクルード関係は以下のようになります。
![02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/db1cc2f3-6dc8-f0c8-fa5d-d38033032dd1.png)


## 結果
影が落ちるようになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/08614849-54e6-1d18-ba49-95ea99da9dc4.png)

# 発展編 : シーンのライト情報を入れる
これまでの積雪シェーダーは ```MainLight```(DirectionalLight)のみでライティング計算を行っていました。

次に、ライトマップ、ポイントライトといった情報を考慮したライティング計算を取り入れたいと思います。



## ライト情報の取得
Universal RP内部の ```Lighting.hlsl``` を見ると、BlinnPhongを計算するための関数 ```UniversalFragmentBlinnPhong``` が用意されています。

<details><summary>UniversalFragmentBlinnPhongメソッドの実装</summary><div>

```Lighting.hlsl
half4 UniversalFragmentBlinnPhong(InputData inputData, half3 diffuse, half4 specularGloss, half smoothness, half3 emission, half alpha)
{
    Light mainLight = GetMainLight(inputData.shadowCoord);
    MixRealtimeAndBakedGI(mainLight, inputData.normalWS, inputData.bakedGI, half4(0, 0, 0, 0));

    half3 attenuatedLightColor = mainLight.color * (mainLight.distanceAttenuation * mainLight.shadowAttenuation);
    half3 diffuseColor = inputData.bakedGI + LightingLambert(attenuatedLightColor, mainLight.direction, inputData.normalWS);
    half3 specularColor = LightingSpecular(attenuatedLightColor, mainLight.direction, inputData.normalWS, inputData.viewDirectionWS, specularGloss, smoothness);

#ifdef _ADDITIONAL_LIGHTS
    uint pixelLightCount = GetAdditionalLightsCount();
    for (uint lightIndex = 0u; lightIndex < pixelLightCount; ++lightIndex)
    {
        Light light = GetAdditionalLight(lightIndex, inputData.positionWS);
        half3 attenuatedLightColor = light.color * (light.distanceAttenuation * light.shadowAttenuation);
        diffuseColor += LightingLambert(attenuatedLightColor, light.direction, inputData.normalWS);
        specularColor += LightingSpecular(attenuatedLightColor, light.direction, inputData.normalWS, inputData.viewDirectionWS, specularGloss, smoothness);
    }
#endif

#ifdef _ADDITIONAL_LIGHTS_VERTEX
    diffuseColor += inputData.vertexLighting;
#endif

    half3 finalColor = diffuseColor * diffuse + emission;

#if defined(_SPECGLOSSMAP) || defined(_SPECULAR_COLOR)
    finalColor += specularColor;
#endif

    return half4(finalColor, alpha);
}
```

</div></details>


今回の積雪レンダリングは、BlinnPhongシェーディングを利用しているので
```UniversalFragmentBlinnPhong``` メソッドを少し書き換えるだけでライティング計算を簡単に実装することができます。

## 積雪用BlingPhonnシェーディング
今回は```SnowFragmentBlinnPhong```というメソッドを作成してみました。
スペキュラー用、ディフューズ用に歪ませた法線diffuseNormal, specularNormalを利用してライティング計算しています。

```Snow.hlsl
half4 SnowFragmentBlinnPhong(InputData inputData, SnowData snowData, half3 diffuse, half4 specularGloss, half smoothness, half3 emission, half alpha)
{
    Light mainLight = GetMainLight(inputData.shadowCoord);
    MixRealtimeAndBakedGI(mainLight, snowData.diffuseNormal, inputData.bakedGI, half4(0, 0, 0, 0));

    half3 attenuatedLightColor = mainLight.color * (mainLight.distanceAttenuation * mainLight.shadowAttenuation);
    
    half3 diffuseColor = inputData.bakedGI + LightingLambert(attenuatedLightColor, mainLight.direction, snowData.diffuseNormal);
    half3 specularColor = LightingSpecular(attenuatedLightColor, mainLight.direction, snowData.specularNormal, inputData.viewDirectionWS, _SpecularColor, _SnowSpecularGloss);

#ifdef _ADDITIONAL_LIGHTS
    uint pixelLightCount = GetAdditionalLightsCount();
    for (uint lightIndex = 0u; lightIndex < pixelLightCount; ++lightIndex)
    {
        Light light = GetAdditionalLight(lightIndex, inputData.positionWS);
        half3 attenuatedLightColor = light.color * (light.distanceAttenuation * light.shadowAttenuation);
        diffuseColor += LightingLambert(attenuatedLightColor, light.direction, snowData.diffuseNormal);
        specularColor += LightingSpecular(attenuatedLightColor, light.direction, snowData.specularNormal, inputData.viewDirectionWS, _SpecularColor, _SnowSpecularGloss);
    }
#endif

#ifdef _ADDITIONAL_LIGHTS_VERTEX
    diffuseColor += inputData.vertexLighting;
#endif

    half3 finalColor = diffuseColor * diffuse + emission;

#if defined(_SPECGLOSSMAP) || defined(_SPECULAR_COLOR)
    finalColor += specularColor * _SpecularColor.rgb * _SpecularColor.a;
#endif

    return half4(finalColor, alpha);
}
```

今回はライティング用にSnowDataという構造体を新たに用意しました。

```Snow.hlsl
struct SnowData {
    float fp; // 積雪量
    float3 diffuseNormal; // ディフューズ用Normal
    float3 specularNormal; // スペキュラー用Normal
};
```

## 結果 (完成)
以下のような結果になります.
<img width=720 src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/2c65ec51-74c6-0585-28bc-a07939b26f9f.jpeg">


回してみました
![output-palette.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/5512ee5d-6978-8ef8-4e68-0db4268272e5.gif)


## 最終的なシェーダー (SRP Batcher未対応版)

Snow.hlsl には、新しく LitInput.hlslをインクルードしています。
参照関係はざっくり書くと以下のような感じになります。

<img width = 320 src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/4d2530fa-84e7-27ad-6acd-3c2ee71c45af.png">

```LitInput.hlsl``` には　```InitializeStandardLitSurfaceData``` メソッドが用意されており、
マテリアルに設定されている情報をSurfaceData構造体としてまとめて取得することができます。

```LitInput.hlsl
inline void InitializeStandardLitSurfaceData(float2 uv, out SurfaceData outSurfaceData)
```

```SurfaceInput.hlsl
struct SurfaceData
{
    half3 albedo;
    half3 specular;
    half  metallic;
    half  smoothness;
    half3 normalTS;
    half3 emission;
    half  occlusion;
    half  alpha;
};
```


<details><summary>Snow.shader</summary><div>

```Snow.shader
Shader "Custom/Snow" {
    Properties
    {
        
        [Header(Color)]
        // 雪の色
        _SnowColor("Snow Color", Color) = (0,0,0,0) 
        _SnowDirection("Snow Direction", Vector) = (0,1,0,0)
        // ディフューズ項の色
        [HDR]_DiffuseColor("Diffuse Color", Color) = (1,1,1,1) 
        // スペキュラー項の色
        [HDR]_SpecularColor("Specular Color", Color) = (1,1,1,1) 
        
        [Header(Lighting)]
        // 光沢度
        _SnowSpecularGloss("Specular Gloss", Float) = 10.0
        _SnowAmount("Snow Amount", Range(0,1)) = 0.5
        
        [Header(Noise)]
        // ノイズに計算に使用する座標スケール
        _NoisePositionScale("Noise Scale(Position)", Float) = 64.0 
        // 面の法線に加えるノイズの大きさ
        _NoiseInclinationScale("Noise Scale (Inclination)", Float) = 0.1 
        // ディフューズ項の法線に加えるノイズの大きさ
        _SnowDiffuseNormalDistortion("Diffuse Normal Distortion", Float) = 0.4 
        // ディフューズ項の法線に加えるノイズの大きさ
        _SnowSpecularNormalDistortion("Specular Normal Distortion", Float) = 0.8 
        
        
        [Header(Snow)]
        // 雪の量
        // _SnowAmount("Snow Amount", Float) = 1.0 
        // 入ってくる雪に対するベクトル
        _SnowDirection("Snow Direction", Vector) = (0, 1, 0, 0) 
        // 雪の範囲の変換用パラメータ
        _SnowDotNormalRemap("dot(N, S) remap", Vector) = (-1, 1, -1, 1) 
        
        ////////////////////////////////////////////////////////////////////////////////////////////
        // Specular vs Metallic workflow
        [HideInInspector] _WorkflowMode("WorkflowMode", Float) = 1.0

        [MainTexture] _BaseMap("Albedo", 2D) = "white" {}
        [MainColor] _BaseColor("Color", Color) = (1,1,1,1)

        _Cutoff("Alpha Cutoff", Range(0.0, 1.0)) = 0.5

        _Smoothness("Smoothness", Range(0.0, 1.0)) = 0.5
        _GlossMapScale("Smoothness Scale", Range(0.0, 1.0)) = 1.0
        _SmoothnessTextureChannel("Smoothness texture channel", Float) = 0

        _Metallic("Metallic", Range(0.0, 1.0)) = 0.0
        _MetallicGlossMap("Metallic", 2D) = "white" {}

        _SpecColor("Specular", Color) = (0.2, 0.2, 0.2)
        _SpecGlossMap("Specular", 2D) = "white" {}

        [ToggleOff] _SpecularHighlights("Specular Highlights", Float) = 1.0
        [ToggleOff] _EnvironmentReflections("Environment Reflections", Float) = 1.0

        _BumpScale("Scale", Float) = 1.0
        _BumpMap("Normal Map", 2D) = "bump" {}

        _OcclusionStrength("Strength", Range(0.0, 1.0)) = 1.0
        _OcclusionMap("Occlusion", 2D) = "white" {}

        _EmissionColor("Color", Color) = (0,0,0)
        _EmissionMap("Emission", 2D) = "white" {}

        // Blending state
        [HideInInspector] _Surface("__surface", Float) = 0.0
        [HideInInspector] _Blend("__blend", Float) = 0.0
        [HideInInspector] _AlphaClip("__clip", Float) = 0.0
        [HideInInspector] _SrcBlend("__src", Float) = 1.0
        [HideInInspector] _DstBlend("__dst", Float) = 0.0
        [HideInInspector] _ZWrite("__zw", Float) = 1.0
        [HideInInspector] _Cull("__cull", Float) = 2.0

        _ReceiveShadows("Receive Shadows", Float) = 1.0
        // Editmode props
        [HideInInspector] _QueueOffset("Queue offset", Float) = 0.0

        // ObsoleteProperties
        [HideInInspector] _MainTex("BaseMap", 2D) = "white" {}
        [HideInInspector] _Color("Base Color", Color) = (1, 1, 1, 1)
        [HideInInspector] _GlossMapScale("Smoothness", Float) = 0.0
        [HideInInspector] _Glossiness("Smoothness", Float) = 0.0
        [HideInInspector] _GlossyReflections("EnvironmentReflections", Float) = 0.0
        ////////////////////////////////////////////////////////////////////////////////////////////
    }
    
    SubShader { 
        Tags{"RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" "IgnoreProjector" = "True"}
//        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline"}
        LOD 300
        
        // ------------------------------------------------------------------
        //  Forward pass. Shades all light in a single pass. GI + emission + Fog
        Pass
        {
            // Lightmode matches the ShaderPassName set in UniversalRenderPipeline.cs. SRPDefaultUnlit and passes with
            // no LightMode tag are also rendered by Universal Render Pipeline
            Name "ForwardLit"
            Tags{"LightMode" = "UniversalForward"}

            Blend[_SrcBlend][_DstBlend]
            ZWrite[_ZWrite]
            Cull[_Cull]

            HLSLPROGRAM
            

            // Required to compile gles 2.0 with standard SRP library
            // All shaders must be compiled with HLSLcc and currently only gles is not using HLSLcc by default
            #pragma prefer_hlslcc gles
            #pragma exclude_renderers d3d11_9x
            #pragma target 2.0
            
            #define _SPECULAR_COLOR

            // -------------------------------------
            // Material Keywords
            #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

            // -------------------------------------
            // Universal 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 _ _ADDITIONAL_LIGHT_SHADOWS
            #pragma multi_compile _ _SHADOWS_SOFT
            #pragma multi_compile _ _MIXED_LIGHTING_SUBTRACTIVE

            // -------------------------------------
            // Unity defined keywords
            #pragma multi_compile _ DIRLIGHTMAP_COMBINED
            #pragma multi_compile _ LIGHTMAP_ON
            #pragma multi_compile_fog

            //--------------------------------------
            // GPU Instancing
            #pragma multi_compile_instancing

            #pragma vertex SnowVert
            #pragma fragment SnowFrag

            #include "Assets/Shaders/HLSL/Snow.hlsl" 
            ENDHLSL
        }

    }
    FallBack "Hidden/Universal Render Pipeline/FallbackError"
}
```

</div></details>

<details><summary>Snow.hlsl</summary><div>

```Snow.hlsl
#include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#include "Noise.hlsl"

// -------------------------------------
// 定数の定義

// float
half _SnowAmount;
half _SnowSpecularGloss;
half _NoisePositionScale;
half _NoiseInclinationScale;
half _SnowDiffuseNormalDistortion; // 色計算のディフューズ項の法線に加えるノイズ強度
half _SnowSpecularNormalDistortion; // 色計算のスペキュラー項の法線に加えるノイズ強度
// vector
half3 _SnowDirection;
half4 _SnowDotNormalRemap;
// color
half4 _SnowColor; // 雪の色
half4 _DiffuseColor;
half4 _SpecularColor;

// -------------------------------------
// 頂点シェーダーへの入力を格納するstruct
struct Attributes
{
    float4 positionOS   : POSITION;
    float3 normalOS     : NORMAL;
    float4 tangentOS    : TANGENT;
    float2 texcoord     : TEXCOORD0;
    float2 lightmapUV   : TEXCOORD1;
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

// -------------------------------------
// フラグメントシェーダーへの入力を格納するstruct
struct Varyings
{
    float2 uv                       : TEXCOORD0;
    DECLARE_LIGHTMAP_OR_SH(lightmapUV, vertexSH, 1);

    float3 positionWS               : TEXCOORD2;
    float3 normalWS                 : TEXCOORD3;
#ifdef _NORMALMAP
    float4 tangentWS                : TEXCOORD4;    // xyz: tangent, w: sign
#endif
    float3 viewDirWS                : TEXCOORD5;

    half4 fogFactorAndVertexLight   : TEXCOORD6; // x: fogFactor, yzw: vertex light

    float4 shadowCoord              : TEXCOORD7;
    
    float3 positionOS               : TEXCOORD8; // position (object space)

    float4 positionCS               : SV_POSITION;
    UNITY_VERTEX_INPUT_INSTANCE_ID
    UNITY_VERTEX_OUTPUT_STEREO
};

// valueを範囲を[x, y]から[z, w]へ変換する 
float remap(float value, float4 range)
{
    return (value - range.x) * (range.w - range.z) / (range.y - range.x) + range.z;
}

// 頂点シェーダー
Varyings SnowVert(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);
    
    // normalWS and tangentWS already normalize.
    // this is required to avoid skewing the direction during interpolation
    // also required for per-vertex lighting and SH evaluation
    VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS, input.tangentOS);
    float3 viewDirWS = GetCameraPositionWS() - vertexInput.positionWS;
    half3 vertexLight = VertexLighting(vertexInput.positionWS, normalInput.normalWS);
    half fogFactor = ComputeFogFactor(vertexInput.positionCS.z);

    output.uv = TRANSFORM_TEX(input.texcoord, _BaseMap);

    // already normalized from normal transform to WS.
    output.normalWS = normalInput.normalWS;
    output.viewDirWS = viewDirWS;
#ifdef _NORMALMAP
    real sign = input.tangentOS.w * GetOddNegativeScale();
    output.tangentWS = half4(normalInput.tangentWS.xyz, sign);
#endif

    OUTPUT_LIGHTMAP_UV(input.lightmapUV, unity_LightmapST, output.lightmapUV);
    OUTPUT_SH(output.normalWS.xyz, output.vertexSH);

    output.fogFactorAndVertexLight = half4(fogFactor, vertexLight);

#if defined(REQUIRES_WORLD_SPACE_POS_INTERPOLATOR)
    output.positionWS = vertexInput.positionWS;
#endif

#if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR)
    output.shadowCoord = GetShadowCoord(vertexInput);
#endif

    output.positionCS = vertexInput.positionCS;
    output.positionOS = input.positionOS;
    
    return output;
}

// 積雪レンダリング用データ
struct SnowData {
    float fp;
    float3 diffuseNormal; // ディフューズ用Normal
    float3 specularNormal; // スペキュラー用Normal
};

// 積雪に使用する データの計算
void InitializeSnowData(Varyings input, out SnowData snowData) {

    float3 snowDir = normalize(_SnowDirection); // 雪の向き
    float3 noise = fbm(input.positionOS * _NoisePositionScale);

    // fe : 露出関数の値
    float fe = 1.0; // 常に露出
    
    // dE : 露出関数をスクリーンスペースで微分したもの (今回は常に同じ値をとるので0を入れておく)
    float3 dE = float3(0.0, 0.0, 0.0);
    
    // finc : 面の傾きの雪への寄与度
    float nDotS = dot(input.normalWS, snowDir); // 面の傾きが積雪に影響する成分
    nDotS = remap(nDotS, _SnowDotNormalRemap);
    
    // 面の傾きの積雪への寄与度
    float finc = nDotS // 面の傾きによる積雪量のコントロール
     + _NoiseInclinationScale * length(noise); // ノイズを加える
        
    // fp : 積雪予測関数 (雪が積もる量)
    snowData.fp = saturate(fe * finc * _SnowAmount);
    
    // 雪の照明計算に使う法線
    snowData.diffuseNormal = normalize(input.normalWS + snowData.fp  * noise * _SnowDiffuseNormalDistortion + dE); 
    snowData.specularNormal = normalize(input.normalWS + snowData.fp * noise * _SnowSpecularNormalDistortion + dE); 
}

// InputData作成
void InitializeInputData(Varyings input, half3 normalTS, out InputData inputData)
{
    inputData = (InputData)0;

#if defined(REQUIRES_WORLD_SPACE_POS_INTERPOLATOR)
    inputData.positionWS = input.positionWS;
#endif

    half3 viewDirWS = SafeNormalize(input.viewDirWS);
#ifdef _NORMALMAP 
    float sgn = input.tangentWS.w;      // should be either +1 or -1
    float3 bitangent = sgn * cross(input.normalWS.xyz, input.tangentWS.xyz);
    inputData.normalWS = TransformTangentToWorld(normalTS, half3x3(input.tangentWS.xyz, bitangent.xyz, input.normalWS.xyz));
#else
    inputData.normalWS = input.normalWS;
#endif

    inputData.normalWS = NormalizeNormalPerPixel(inputData.normalWS);
    inputData.viewDirectionWS = viewDirWS;

#if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR)
    inputData.shadowCoord = input.shadowCoord;
#elif defined(MAIN_LIGHT_CALCULATE_SHADOWS)
    inputData.shadowCoord = TransformWorldToShadowCoord(inputData.positionWS);
#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);
}

// 積雪用 BlinnPhongシェーディング
half4 SnowFragmentBlinnPhong(InputData inputData, SnowData snowData, half3 diffuse, half4 specularGloss, half smoothness, half3 emission, half alpha)
{
    Light mainLight = GetMainLight(inputData.shadowCoord);
    MixRealtimeAndBakedGI(mainLight, snowData.diffuseNormal, inputData.bakedGI, half4(0, 0, 0, 0));

    half3 attenuatedLightColor = mainLight.color * (mainLight.distanceAttenuation * mainLight.shadowAttenuation);
    
    half3 diffuseColor = inputData.bakedGI + LightingLambert(attenuatedLightColor, mainLight.direction, snowData.diffuseNormal);
    half3 specularColor = LightingSpecular(attenuatedLightColor, mainLight.direction, snowData.specularNormal, inputData.viewDirectionWS, _SpecularColor, _SnowSpecularGloss);

#ifdef _ADDITIONAL_LIGHTS
    uint pixelLightCount = GetAdditionalLightsCount();
    for (uint lightIndex = 0u; lightIndex < pixelLightCount; ++lightIndex)
    {
        Light light = GetAdditionalLight(lightIndex, inputData.positionWS);
        half3 attenuatedLightColor = light.color * (light.distanceAttenuation * light.shadowAttenuation);
        diffuseColor += LightingLambert(attenuatedLightColor, light.direction, snowData.diffuseNormal);
        specularColor += LightingSpecular(attenuatedLightColor, light.direction, snowData.specularNormal, inputData.viewDirectionWS, _SpecularColor, _SnowSpecularGloss);
    }
#endif

#ifdef _ADDITIONAL_LIGHTS_VERTEX
    diffuseColor += inputData.vertexLighting;
#endif

    half3 finalColor = diffuseColor * diffuse + emission;

#if defined(_SPECGLOSSMAP) || defined(_SPECULAR_COLOR)
    finalColor += specularColor * _SpecularColor.rgb * _SpecularColor.a;
#endif

    return half4(finalColor, alpha);
}

// Fragmentシェーダー
half4 SnowFrag(Varyings input) : SV_Target {
    UNITY_SETUP_INSTANCE_ID(input);
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

    SnowData snowData;
    InitializeSnowData(input, snowData);
    
    SurfaceData surfaceData;
    InitializeStandardLitSurfaceData(input.uv, surfaceData);

    InputData inputData;
    InitializeInputData(input, surfaceData.normalTS, inputData);
    
    half3 diffuse = lerp(surfaceData.albedo, _SnowColor.rgb, snowData.fp * _SnowColor.a);
    float specularGloss = _SnowSpecularGloss;
    half4 color = SnowFragmentBlinnPhong(inputData, snowData, diffuse, specularGloss, surfaceData.smoothness, surfaceData.emission, surfaceData.alpha);
    
    color.rgb = MixFog(color.rgb, inputData.fogCoord);
    color.a = OutputAlpha(color.a);
    
    return color;
}
```

</div></details>

# 積雪シェーダーのSRP Batcher対応
これまでに作成してきた積雪シェーダーですが、実はSRP Batcher対応ができていません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/3c6df2bf-4345-816b-10fe-46e2df8ac242.png)

SRP Batcher対応させるためにはひと工夫が必要になります。

## CBUFFER(UnityPerMaterial)
シェーダー内で使用するプロパティをCBUFFER(UnityPerMaterial) ~ CBUFFER_ENDで囲うことでSRP Batcher対応が済むのですが、
CBUFFERブロックは **1か所にまとめる** という制限があります。

CBUFFER(UnityPerMaterial)が、二つ以上存在すると以下のようなエラーが発生します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/e25fed18-b397-de1f-ea43-8d533dd9d124.png)
Assertion failed on expression: 'm_BuffersToBind[shaderType][bind].buffer == NULL' 


## LitInput.hlslは使えない
今回の積雪シェーダーでは、LitInput.hlsl をインクルードしているのですが、
その中では以下のようなCBUFFERが定義されています。

```LitInput.hlsl
CBUFFER_START(UnityPerMaterial)
float4 _BaseMap_ST;
half4 _BaseColor;
half4 _SpecColor;
half4 _EmissionColor;
half _Cutoff;
half _Smoothness;
half _Metallic;
half _BumpScale;
half _OcclusionStrength;
CBUFFER_END
```

今回の```Snow.hlsl```では```LitInput.hlsl```をインクルードしているため、
```Snow.hlsl``` にCBUFFER(UnityPerMaterial)を新しく書くとエラーが発生します。

<img width = 800 src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/c965d51e-a03b-df38-b4ab-471e0eb75774.png">

## 対策 : LitInput.hlslを改造する

Universal RP標準の```LitInput.hlsl```をインクルードすると、下流の```Snow.hlsl``` のプロパティをCBUFFERに含めることができずSRP Batcher対応ができません。
しかし```LitInput.hlsl```の機能は使いたい....

これを解決するため、今回は以下のような対策をとりました。
1. Universal RPパッケージ内に存在する ```LitInput.hlsl``` をUnityプロジェクト内へコピーする
2. ```Snow.shader``` からは、Unityプロジェクトに存在する```LitInput.hlsl```をインクルード
3. 積雪で使用するパラメータは```LitInput.hlsl```のCBUFFER内に追加する

これでSRP Batcher対応できます。

<img width = 800 src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/790472db-bdfd-0f01-a527-6e09d22b94a3.png">


# Unityプロジェクト
今回作成した積雪シェーダーはGitHub リポジトリにて公開中です
https://github.com/rngtm/UniversalRP-SnowShader

# 参考
SRP Batcherの対応方法・利用上の注意点などまとめ【Unity】
https://amagamina.jp/srp-batcher/

SRP Batcher：レンダリングをスピードアップ
https://blogs.unity3d.com/jp/2019/02/28/srp-batcher-speed-up-your-rendering/

Real-time Rendering of Accumulated Snow
https://ep.liu.se/ecp/013/007/ecp01307.pdf