RenderFeature でフェードアウトする床反射

# はじめに
ScriptableRenderFeature の勉強に、フェードアウトする床反射を作ってみました。
![Preview](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308883/57ae7f80-6cff-e067-1bb9-7bb6efe164ec.png "Preview")

# 謝辞
この記事の内容は [【Unity】URPを拡張して水面反射を作ってみた](
https://zenn.dev/r_ngtm/articles/urp-water-reflection) を参考・ベースにさせて頂いています。
「反射する3Dオブジェクトの描画」部分はほぼそのままなので、詳しくは参考記事をご覧ください。

# Git Repository

https://github.com/fo-ta/HorizontalPlaneReflection

# 環境
- Unity 2021.3.16f1 (LTS)
- Universal RP 12.1.8

## Render 設定方法

`HorizontalPlaneReflectionTextureRenderFeature` を UnivarsalRenderData に追加して使用します。

![Settings](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308883/90b10c15-b76e-ecff-82df-b7761aa53810.png "RenderSettings")


<details><summary>Property 詳細</summary>

| Property | Description |
| :-- | :-- |
| Culling Mask | レンダリング対象とするレイヤー |
| Plane Height | 鏡面の高さ
| Plane Thickness | 鏡面の厚み<br />鏡像のレンダリング位置が厚みの分だけ下がります。 |
| Fade Base Height | フェードアウトが始まる高さ | 
| Fade Range | フェードアウトしきるまでの幅 |
| Draw Opaque | 不透明オブジェクトを描画するか |
| Draw Transparent | 透明オブジェクトを描画するか |
| Render Pass Event | Render Pass のタイミング<br />鏡面オブジェクトを Opaque で描画することを想定して、デフォルト値は Before Rendering Opaques. |
| Render Texture Scale | Render Texture の大きさ倍率 |
| Filter Mode | Render Texture の Filter Mode |
| Render Texture Format | Render Texture のフォーマット<br /> HDR を使用する際は RGBAHalf などのアルファチャンネルが使えるフォーマットを指定することを推奨。
| Render Texture Debug View | Render Texture を表示する（デバッグ用） |

</details>

# フロー概要
- RenderTexture に特定の高さで反転した3Dオブジェクトをフェードアウト状態でレンダリングする
  - フェードアウト描画用に専用の Pass を用意
  - 不透明パスは Depth 描画 → Color 描画の2ステップを踏むことで、前後関係の破綻を回避する
- 上記 RenderTexture をサンプリングした床オブジェクトを描画する
  - AlphaBlend だけではなく Additive にも対応したいため、 P-MAP ブレンド手法を採用
  - ※ P-MAP ブレンド手法について詳しくは [CEDEC2015「加算合成コストが０になる！？すぐに使えるP-MAPブレンドテクニック」](https://www.slideshare.net/toshiyasumiyabe/cedec2015pmap) をご覧ください。

# 実装

## 鏡像を描画する設定

鏡像の描画部分は [参考記事（【Unity】URPを拡張して水面反射を作ってみた）](
https://zenn.dev/r_ngtm/articles/urp-water-reflection) そのまま利用させて頂いています。
非常に丁寧に解説してくれているので、詳細はそちらをご覧ください。

## フェードアウト描画

Plane Height などのパラメータを ShaderGlobalParameter で渡して、専用パスで描画します。
ScriptableRenderPass では LightMode を指定して描画できるため、 Opaque と Transparent にそれぞれ独自の LightMode の Tag を付けます。

- Opaeque : HorizontalPlaneReflectionOpaque
- Transpaernt : - Opaeque : HorizontalPlaneReflectionTransparent

### Render Pass実装

```HorizontalPlaneReflectionTextureRenderFeature.cs
cmd.SetGlobalFloat(FadeRangeId, Settings.fadeRange);
cmd.SetGlobalFloat(FadeBaseHeightId, Settings.fadeBaseHeight);
// 高さぴったりにすると床にピッタリ着く面も消してしまうため、少し余裕を持たせる
cmd.SetGlobalFloat(PlaneHeightId, Settings.planeHeight - 0.01f);

// Opaque Pass は Depth だけ先に描画する
context.DrawRenderers(cullingResults, ref depthDrawingSettings, ref FilteringSettings);
// Color を描画
context.DrawRenderers(cullingResults, ref drawingSettings, ref FilteringSettings);
```

### Shader 実装

#### Opaque Shader

```SampleLit.shader
// URP Lit Shader に Pass を追加
Pass
{
    Name "HorizontalPlaneReflectionOpaque"
    Tags{"LightMode" = "HorizontalPlaneReflectionOpaque"}

    Blend SrcAlpha OneMinusSrcAlpha
    Cull [_Cull]
    ZTest Equal  // 事前に Depth を描いているので Equal で描画
    ZWrite Off

    ...

    float GetReflectionAlpha(float heightWS)
    {
        half reflectionFadeAlpha = smoothstep(
            _ReflectionFadeBaseHeight + _ReflectionFadeRange,
            _ReflectionFadeBaseHeight,
            heightWS);
        reflectionFadeAlpha *= step(_ReflectionPlaneHeight, heightWS);

        return reflectionFadeAlpha;
    }

    half4 ReflectionLitPassFragment(Varyings input) : SV_Target
    {
        half4 outColor = LitPassFragment(input);

        half reflectionFadeAlpha = GetReflectionAlpha(input.positionWS.y);
        outColor.a *= reflectionFadeAlpha;

        return outColor;
    }
}
```

#### Alpha Blend Shader

```SampleAlphaBlend.shader
Pass
{
    Name "HorizontalPlaneReflectionTransparent"
    Tags { "LightMode"="HorizontalPlaneReflectionTransparent" }
    Blend SrcAlpha OneMinusSrcAlpha
    Cull [_CullMode]
    ZTest LEqual
    ZWrite Off

    HLSLPROGRAM
    #pragma vertex vert
    #pragma fragment fragReflection

    ...

    half4 fragReflection (Varyings input) : SV_Target
    {
        // sample the texture
        half4 outColor = frag(input);
        half reflectionFadeAlpha = GetReflectionAlpha(input.positionWS.y);

        outColor.a *= reflectionFadeAlpha;

        return outColor;
    }
    ENDHLSL
}
```

#### Additive Shader

```SampleAdditive.shader
Pass
{
    Name "HorizontalPlaneReflectionTransparent"
    Tags { "LightMode"="HorizontalPlaneReflectionTransparent" }
    Blend One One, Zero One  // アルファチャンネルは書き込まない
    Cull [_CullMode]
    ZTest LEqual
    ZWrite Off

    HLSLPROGRAM
    #pragma vertex vert
    #pragma fragment fragReflection

    ...

    half4 fragReflection (Varyings input) : SV_Target
    {
        half4 outColor = frag(input);
        half reflectionFadeAlpha = GetReflectionAlpha(input.positionWS.y);

        outColor.rgb *= reflectionFadeAlpha;

        return outColor;
    }
    ENDHLSL
}
```

## RenderTexture を鏡面に合成

RenderTexture を P-MAP ブレンド手法で合成します。
![Textures](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308883/0fa27646-d2a3-769f-289d-b63b5e017b30.png "Textures")

```SamplePlane.shader
Pass
{
    Name "ForwardUnlit"
    Tags { }
    ZWrite On

    HLSLPROGRAM
    #pragma vertex vert
    #pragma fragment frag

    // Includes
    #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/UnityInput.hlsl"
    #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"

    struct Attributes
    {
        float4 positionOS : POSITION;
        float2 texcoord : TEXCOORD0;
    };

    struct Varyings
    {
        float4 positionCS : SV_POSITION;
        float2 uv : TEXCOORD0;
        float4 positionSS : TEXCOORD1;
    };

    TEXTURE2D(_MainTex);  SAMPLER(sampler_MainTex);
    TEXTURE2D(_HorizontalPlaneReflectionTexture);  SAMPLER(sampler_HorizontalPlaneReflectionTexture);

    CBUFFER_START(UnityPerMaterial)
    half4 _BaseColor;
    float4 _MainTex_ST;
    float _ReflectionTextureIntensity;
    CBUFFER_END

    Varyings vert (Attributes input)
    {
        Varyings output = (Varyings)0;
        output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
        output.uv = TRANSFORM_TEX(input.texcoord, _MainTex);
        // Screen Space Position
        output.positionSS = ComputeScreenPos(output.positionCS);
        return output;
    }

    half4 frag (Varyings input) : SV_Target
    {
        half4 outColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv);
        outColor *= _BaseColor;
        half4 reflectionColor = SAMPLE_TEXTURE2D(
            _HorizontalPlaneReflectionTexture,
            sampler_HorizontalPlaneReflectionTexture,
            input.positionSS.xy / input.positionSS.w);

        // SrcColor + DstColor * (1 - SrcAlpha) で合成
        outColor.rgb = reflectionColor.rgb * _ReflectionTextureIntensity
            + outColor.rgb * (1.0 - reflectionColor.a* _ReflectionTextureIntensity);
        
        return outColor;
    }
    ENDHLSL
}
```

サンプルでは Unlit ですが、もちろん PBR などの Lit Shader にも合成できます。

# 課題:ビルボードと相性が悪い

![ビルボードと相性が悪い](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308883/04db06eb-d2b3-eca5-7612-6671630da43f.png "ビルボードと相性が悪い")
反転後のビルボードメッシュの向きがカメラ方向を向かない課題があります。
ScriptableRenderPass からメッシュの向きを調整できれば良いのですが、良い方法が思いついてない……:innocent:


# ユニティちゃんライセンス

![ユニティちゃんライセンス](http://unity-chan.com/images/imageLicenseLogo.png)
この記事は[ユニティちゃんライセンス条項](http://unity-chan.com/contents/license_jp/)の元に提供されています。