UnityのURPのVolumeにHeightFogを追加する

Unityで高さフォグを使おうと思ったがURPパッケージに入っていなかったので自分で作る。
使い方は、RenderFeatureとVolumeで設定すると使えるようになります。

こんな見た目になります。

Screenshot

Github

コードとサンプルはここに置いてあります。

https://github.com/jnhtt/height-fog-urp-volume

開発環境

以下のような環境で実装しました。

  • MacBook Pro (16インチ,2021) M1 Pro/32GB
  • Unity2022.3.13f1
  • URP 14.0.9

技術的要素

  • 高さフォグをVolumeに追加
  • RenderFeature/RenderPassの追加

高さフォグをVolumeに追加

高さフォグの制御のためのパラメーターはVolume経由で行うようにしたいと思うのでVolumeComponentを継承したクラスを作成します。
fogHeightMin/Maxの値は用途に合わせて調整してください。

パラメーター 意味 値の範囲
fogColor フォグの色 rgb
fogHeightMin フォググラデの範囲の最小値、これより小さいとフォグ色で塗られる floatで[-200,300]
fogHeightMax フォググラデの範囲の最大値、これより大きいとフォグはない floatで[-200,300]
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

namespace FingerTip.Volume
{
    public class HeightFog : VolumeComponent
    {
        public ColorParameter fogColor = new ColorParameter(Color.white);
        public ClampedFloatParameter fogHeightMin = new ClampedFloatParameter(0, -200, 300);
        public ClampedFloatParameter fogHeightMax = new ClampedFloatParameter(0, -200, 300);

        public bool IsActive => fogHeightMin.value < fogHeightMax.value;
    }
}

Volumeに追加すると高さフォグが使えるようになります。ただし、サポートしているのは、Global Volumeだけです。
もちろん、次に説明するRenderFeatureを追加する必要があります。

RenderFeature/RenderPassの追加

まずは、RenderFeatureを作成します。
メニューからRenderFeatureを作成するとRenderPassクラスを内包するコードを自動出力してくれます。
この自動出力されたコードを改造して実装します。

Projectビュー > 右クリック > Create > Rendering > URP RendererFeature  

CustomRenderPassFeatureのファイル名をHeightFogなど高さフォグが分かる名前に変更します。
こんな感じのコードを作成します。
VolumeにHeightFogの機能を追加して、そこから_FogColorや_FogHeightMin/MaxのパラメーターをCustom/HeightFogシェーダーに渡しています。

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

namespace FingerTip
{
    using FingerTip.Volume;
    public sealed class HeightFogRendererFeature : ScriptableRendererFeature
    {
        private sealed class HeightFogRenderPass : ScriptableRenderPass
        {
            private const string RenderPassName = nameof(HeightFogRenderPass);
            private Material material;

            private static readonly int FogColorId = Shader.PropertyToID("_FogColor");
            private static readonly int FogHeightMinId = Shader.PropertyToID("_FogHeightMin");
            private static readonly int FogHeightMaxId = Shader.PropertyToID("_FogHeightMax");

            public HeightFogRenderPass()
            {
                renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;

                var shaderName = "Custom/HeightFog";
                var shader = Shader.Find(shaderName);
                if (shader == null)
                {
                    Debug.LogError($"Not found shader!{shaderName}");
                    return;
                }

                material = CoreUtils.CreateEngineMaterial(shader);
            }

            public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
            {
                if (renderingData.cameraData.camera.cameraType == CameraType.Preview)
                {
                    return;
                }
                if (material == null || !renderingData.cameraData.postProcessEnabled)
                {
                    return;
                }

                var volumeStack = VolumeManager.instance.stack;
                if (volumeStack == null)
                {
                    return;
                }

                var heightFog = volumeStack.GetComponent<HeightFog>();
                if (heightFog == null || !heightFog.active || !heightFog.IsActive)
                {
                    return;
                }

                material.SetColor(FogColorId, heightFog.fogColor.value);
                material.SetFloat(FogHeightMinId, heightFog.fogHeightMin.value);
                material.SetFloat(FogHeightMaxId, heightFog.fogHeightMax.value);

                var cmd = CommandBufferPool.Get(RenderPassName);
                Blit(cmd, ref renderingData, material);
                context.ExecuteCommandBuffer(cmd);
                CommandBufferPool.Release(cmd);
            }

            public void Cleanup()
            {
                CoreUtils.Destroy(material);
            }
        }

        private HeightFogRenderPass rendererPass;

        public override void Create()
        {
            rendererPass = new HeightFogRenderPass();
        }

        protected override void Dispose(bool disposing)
        {
            rendererPass.Cleanup();
        }

        public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
        {
            renderer.EnqueuePass(rendererPass);
        }
    }
}

次のコードは、高さフォグを実現するHeightFogシェーダーです。
ビルドの際には、Preloadに追加してビルドに含まれるようにする必要があります。

重要な箇所としては、_BlitTextureは組み込みのテクスチャでColorバッファを参照できます。
また、SampleSceneDepth関数でピクセルの深度値を取得できます。深度値を取得できるとComputeWorldSpacePosition関数でワールド座標を計算できます。
各ピクセルのワールド座標のy座標を高さとすると、どの高さからフォグの効果を描画するかを決めることができます。
フォグの計算式はあとで説明しますが、至って簡単な処理になっています。

Shader "Custom/HeightFog"
{
    HLSLINCLUDE
        #pragma exclude_renderers gles

        #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
        #include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"
        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl"

        float4 _FogColor;
        float _FogHeightMin;
        float _FogHeightMax;

        float4 Frag(Varyings input) : SV_Target
        {
            float4 c = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_PointClamp, input.texcoord);
#if UNITY_REVERSED_Z
            float depth = SampleSceneDepth(input.texcoord).x;
#else
            float depth = lerp(UNITY_NEAR_CLIP_VALUE, 1, SampleSceneDepth(input.texcoord).x);
#endif
            float3 worldPos = ComputeWorldSpacePosition(input.texcoord, depth, UNITY_MATRIX_I_VP);

            float rate = smoothstep(_FogHeightMin, _FogHeightMax, worldPos.y);
            return lerp(_FogColor, c, rate);

        }
    ENDHLSL

    SubShader
    {
        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline"}
        LOD 100
        ZTest Always
        ZWrite Off
        Cull Off

        Pass
        {
            Name "HeightFog"
        
            HLSLPROGRAM
                #pragma vertex Vert
                #pragma fragment Frag
            ENDHLSL
        }
    }
}

作成したRendererFeatureを UniversalRenderer Data に追加します。
さらにVolumeにHeightFogを追加して機能を有効にすることで使用可能です。

高さフォグだけでなくいろんな効果を追加できそうです。
ScreenSpaceで各ピクセルのワールド座標を取得できると様々な効果を実装できそうです。