【Unity】URPを拡張して水面反射を作ってみた
はじめに
水面反射を作ってみました。
シーンにはCameraを1つだけ置いており、カメラの変換行列を加工することで反射をレンダリングしています。
環境
Unity 2022.1.0b2.2474
Universal RP 13.1.3
サンプルプロジェクト
水面反射の実装
水面反射をRenderTextureへ書き込み、ShaderGraph上で反射テクスチャを合成しています。
ShaderGraph
ShaderGraphは以下のような実装になっています。
反射テクスチャをサンプリングしています。
水面反射のShaderGraph
虚像による反射表現
水面に関して対称なオブジェクト(虚像)を置くことで、反射を表現できます。
虚像の作り方
以下の手順で、オブジェクトを水面に関して反転させることができます。(虚像になります)
- 平行移動を行い、水面を
に合わせる。 (y座標をy = 0 だけズラす)-h - Y座標を反転させる
行列を利用した水面反転
MVP行列による座標変換が前提知識として必要になるので、軽く紹介します。
MVP行列による座標変換
3Dオブジェクトがレンダリングされるとき、
Model行列
これらの座標変換はどこで行われるのかというと、頂点シェーダーの中で実行されます。
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex); // MVP行列を利用した座標変換
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
UnityObjectToClipPosの内部実装
UnityObjectToClipPos
の内部実装は以下のようになっています。
// 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
はモデル行列
UNITY_MATRIX_VP
は P * Vを表しています。
MVP行列の意味
行列
行列を利用した虚像の作成
3Dモデルの各頂点は 行列
この直後に水面に関する反転を入れてあげることで、虚像を作ることができます。
反転の実装
カメラのView行列に
虚像を作ることができます。
// 平行移動する行列 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
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