💦

【Unity】URPを拡張して水面反射を作ってみた

2021/12/30に公開

はじめに

水面反射を作ってみました。
シーンにはCameraを1つだけ置いており、カメラの変換行列を加工することで反射をレンダリングしています。
https://www.youtube.com/watch?v=BCJiAvRzMwU

環境

Unity 2022.1.0b2.2474
Universal RP 13.1.3

サンプルプロジェクト

https://github.com/rngtm/URP-WaterReflectionTest

水面反射の実装

水面反射をRenderTextureへ書き込み、ShaderGraph上で反射テクスチャを合成しています。

ShaderGraph

ShaderGraphは以下のような実装になっています。
反射テクスチャをサンプリングしています。

水面反射のShaderGraph

虚像による反射表現

水面に関して対称なオブジェクト(虚像)を置くことで、反射を表現できます。

虚像の作り方

以下の手順で、オブジェクトを水面に関して反転させることができます。(虚像になります)

  1. 平行移動を行い、水面を y = 0 に合わせる。 (y座標を-hだけズラす)
  2. Y座標を反転させる

行列を利用した水面反転

MVP行列による座標変換が前提知識として必要になるので、軽く紹介します。

MVP行列による座標変換

3Dオブジェクトがレンダリングされるとき、
Model行列 M、 View行列 V、 Projection行列 P によって座標変換が行われます。(MVP変換)

\mathbf {変換後の座標} \color{black} = P * V * M * \mathbf{元の座標}

これらの座標変換はどこで行われるのかというと、頂点シェーダーの中で実行されます。

NewUnlitShader.shader
v2f vert (appdata v)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex); // MVP行列を利用した座標変換
    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    return o;
}
UnityObjectToClipPosの内部実装

UnityObjectToClipPosの内部実装は以下のようになっています。

UnityShaderUtilities.cginc
// Tranforms position from object to homogenous space
inline float4 UnityObjectToClipPos(in float3 pos)
{
#if defined(STEREO_CUBEMAP_RENDER_ON)
    return UnityObjectToClipPosODS(pos);
#else
    // More efficient than computing M*VP matrix product
    return mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorld, float4(pos, 1.0)));
#endif
}

補足:
unity_ObjectToWorld はモデル行列 M を表しており、
UNITY_MATRIX_VP は P * Vを表しています。

MVP行列の意味

行列M, V, Pはそれぞれ以下のような変換を行います。

M : ローカル空間(Model空間)をワールド空間へ変換する

\mathbf {ワールド座標} \color{black} = M * \mathbf{モデル空間の座標}

V : ワールド空間をカメラ基準の空間(View空間)へ変換する

\mathbf {View空間の座標} \color{black} = V * \mathbf{ワールド座標}

P : View空間をスクリーン基準の空間(Clip空間)へ変換する

\mathbf {Clip空間の座標} \color{black} = P * \mathbf{View空間の座標}

行列を利用した虚像の作成

3Dモデルの各頂点は 行列Mによってモデル空間からワールド空間へ変換されます。

\mathbf {ワールド座標} = M * \mathbf{ローカル座標}

この直後に水面に関する反転を入れてあげることで、虚像を作ることができます。

\mathbf {変換後の座標} = P * V * \color{red}S_y * T_y\color{black} * M * \mathbf{ローカル座標}

\color{red} T_y は「平行移動を行い、水面を y=0に合わせる」行列で、 \color{red} S_y はY座標を反転する行列です。

反転の実装

カメラのView行列にVを代入する代わりに、V * \color{red}S_y * T_y\color{black} を代入することで、
虚像を作ることができます。

// 平行移動する行列 T
var translateMatrix = Matrix4x4.identity;
translateMatrix.m13 = -Settings.waterY;

// Y軸反転する行列 S
var reverseMatrix = Matrix4x4.identity;
reverseMatrix.m11 = -reverseMatrix.m11;
            
// 水面反転を行うように、View行列を加工する
// 変換後の頂点座標 = P * V * Reverse * Translate * M * 頂点座標
viewMatrix = viewMatrix * reverseMatrix * translateMatrix; 
RenderingUtils.SetViewAndProjectionMatrices(cmd, viewMatrix, projectionMatrix, false);

実装コード

水面反射レンダリングの実装コードを以下に示します。
不透明オブジェクトのみ反射がレンダリングされるようにしています。 (半透明シェーダーはレンダリングされません)
また、カメラスタッキングは考慮していません。

WaterReflectionPassFeature.cs
WaterReflectionPassFeature.cs
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Experimental.Rendering.Universal;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class WaterReflectionPassFeature : ScriptableRendererFeature
{
    #region Fields
    [SerializeField] private Settings settings = new Settings();
    private RenderReflectionObjectPass _renderObjectPass = null;
    private MergeReflectionPass _mergeReflectionPass = null;
    #endregion

    // 設定
    [System.Serializable]
    public class Settings
    {
        // 水面の高さ (Y座標)
        public float waterY = 0f;

        // レンダリング対象のレイヤーマスク
        public LayerMask cullingMask = -1;
        
        // レンダリングタイプ
        public RenderQueueType renderQueueType = RenderQueueType.Opaque;

        // 反射をレンダリングするタイミング
        public RenderPassEvent renderObjectPassEvent = RenderPassEvent.AfterRenderingOpaques; 
        
        // レンダリング結果をフレームバッファへ合成するタイミング (デバッグ用)
        public RenderPassEvent debugPassEvent = RenderPassEvent.AfterRenderingTransparents;
        
        // trueにすると、反射のデバッグ表示
        public bool debugReflection = false;
    }

    #region Defines
    // RenderTexture名の定義
    public static class RenderTextureNames
    {
        public static string _CameraReflectionTexture = "_CameraReflectionTexture";
    }
    
    // シェーダープロパティIDの定義
    public static class ShaderPropertyIDs
    {
        public static readonly int _CameraReflectionTexture = Shader.PropertyToID(RenderTextureNames._CameraReflectionTexture);
    }

    // RenderTargetIdentifierの定義
    public static class RenderTargetIdentifiers
    {
        public static readonly RenderTargetIdentifier _CameraReflectionTexture = ShaderPropertyIDs._CameraReflectionTexture;
    }
    
    // RTHandleの置き場所
    public static class RTHandlePool
    {
        public static RTHandle _CameraReflectionTexture;
    }
    #endregion
    
    #region RenderPass

    class MergeReflectionPass : ScriptableRenderPass
    {
        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            var src = RenderTargetIdentifiers._CameraReflectionTexture;
            var dst = renderingData.cameraData.renderer.cameraColorTargetHandle;
            var cmd = CommandBufferPool.Get(nameof(MergeReflectionPass));
            cmd.Blit(src, dst);
            context.ExecuteCommandBuffer(cmd);
            cmd.Clear();
            CommandBufferPool.Release(cmd);
        }
    }
    
    /// <summary>
    /// 反射オブジェクトを描画するパス
    /// </summary>
    class RenderReflectionObjectPass : ScriptableRenderPass
    {
        private readonly string k_ProfilerTag = nameof(RenderReflectionObjectPass); // Frame Debugger で表示される名前
        
        // レンダリング対象のShaderTag
        private List<ShaderTagId> m_ShaderTagIdList = new List<ShaderTagId> 
        {
            new ShaderTagId("SRPDefaultUnlit"),
            new ShaderTagId("UniversalForward"),
            new ShaderTagId("UniversalForwardOnly"),
        };

        private FilteringSettings _filteringSettings;
        private RenderStateBlock _renderStateBlock;
        private LayerMask CullingMask => Settings.cullingMask;
        private RenderQueueType RenderQueueType => Settings.renderQueueType;
        public Settings Settings { get; set; }

        public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
        {
            base.OnCameraSetup(cmd, ref renderingData);
            RTHandlePool._CameraReflectionTexture = RTHandles.Alloc(RenderTargetIdentifiers._CameraReflectionTexture);
        }
        
        public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
        {
            base.Configure(cmd, cameraTextureDescriptor);
            
            // RenderTexture 確保 (使い終わったらReleaseTemporaryRTで解放)
            cmd.GetTemporaryRT(ShaderPropertyIDs._CameraReflectionTexture, cameraTextureDescriptor);
            
            // レンダリング先の変更
            ConfigureTarget(RTHandlePool._CameraReflectionTexture);
            
            // 描画クリア
            ConfigureClear(ClearFlag.All, Color.black);
        }

        public override void OnCameraCleanup(CommandBuffer cmd)
        {
            base.OnCameraCleanup(cmd);
            
            // 確保したRenderTextureを解放
            cmd.ReleaseTemporaryRT(ShaderPropertyIDs._CameraReflectionTexture);
            
            RTHandles.Release(RTHandlePool._CameraReflectionTexture);
        }
        
        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            // レンダリング対象とするRenderQueue
            RenderQueueRange renderQueueRange = (RenderQueueType == RenderQueueType.Transparent)
                ? RenderQueueRange.transparent
                : RenderQueueRange.opaque;

            // フィルタリング設定
            _filteringSettings = new FilteringSettings(renderQueueRange, CullingMask);

            // オブジェクトのソート設定
            var sortingCriteria = (RenderQueueType == RenderQueueType.Transparent)
                ? SortingCriteria.CommonTransparent
                : renderingData.cameraData.defaultOpaqueSortFlags;

            // 描画 設定
            var drawingSettings = CreateDrawingSettings(
                m_ShaderTagIdList,
                ref renderingData,
                sortingCriteria);
            var cameraData = renderingData.cameraData;
            var defaultViewMatrix = cameraData.GetViewMatrix();
            var viewMatrix = cameraData.GetViewMatrix();
            
            // Y座標をwaterYだけ平行移動する行列
            var translateMat = Matrix4x4.identity;
            translateMat.m13 = -Settings.waterY; 
            
            // Y軸反転する行列
            var reverseMat = Matrix4x4.identity;
            reverseMat.m11 = -reverseMat.m11;
            
            var projectionMatrix = cameraData.GetProjectionMatrix();
            projectionMatrix =
                GL.GetGPUProjectionMatrix(projectionMatrix, cameraData.IsCameraProjectionMatrixFlipped());
            
            // コマンドバッファの確保 (使い終わったらCommandBufferPool.Releaseで解放する)
            var cmd = CommandBufferPool.Get(k_ProfilerTag);
            
            // 水面反転を行うように、View行列を加工する
            // 変換後の頂点座標 = P * V * Reverse * Translate * P * 頂点座標
            viewMatrix = viewMatrix * reverseMat * translateMat; 
            RenderingUtils.SetViewAndProjectionMatrices(cmd, viewMatrix, projectionMatrix, false);
            
            cmd.SetInvertCulling(true); // カリング反転 (ビュー行列を反転すると、メッシュの表・裏が逆転するため)
            context.ExecuteCommandBuffer(cmd);
            cmd.Clear();

            // レンダリング実行
            context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref _filteringSettings, ref _renderStateBlock);

            // 元に戻す
            cmd.SetInvertCulling(false);
            RenderingUtils.SetViewAndProjectionMatrices(cmd, defaultViewMatrix, projectionMatrix, false);
            context.ExecuteCommandBuffer(cmd);
            cmd.Clear();
            
            // コマンドバッファ解放
            CommandBufferPool.Release(cmd);
        }
    }
    #endregion

    public override void Create()
    {
        RTHandles.Initialize(Screen.width, Screen.height);
        
        // Render Pass 作成
        _renderObjectPass = new RenderReflectionObjectPass();
        _renderObjectPass.Settings = settings;
        _renderObjectPass.renderPassEvent = settings.renderObjectPassEvent;
        
        _mergeReflectionPass = new MergeReflectionPass();
        _mergeReflectionPass.renderPassEvent = settings.debugPassEvent;

    }

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

        if (settings.debugReflection)
            renderer.EnqueuePass(_mergeReflectionPass);
    }
}

使い方

Renderer Feature の登録

Universal Renderer Data へ Water Reflection Pass Feature を登録すると、
レンダーテクスチャ _CameraReflectionTextureが生成され、 反射がレンダリングされます。


Universal Renderer Data


Water Reflection Pass Featureを登録

反射テクスチャの利用 (ShaderGraph)

Shader Graphの準備

Surface Type を Transparent に設定しておきます。

ShaderGraph上に _CameraReflectionTexture というTextureプロパティを定義します。
Exposed は無効にしておきます。

反射テクスチャのサンプリング

スクリーン座標を利用して、 _CameraReflectionTexture をサンプリングすると、反射の色情報を取得できます。

確認のため、以下のようなShaderGraphを組んでみます。

反射の色が赤く表示されました。

今回作成したRendererFeatureを利用することで、水面反射を テクスチャを介してシェーダー上で利用することができます。

Discussion