要件
Unity 6000.0.26f1
URPプロジェクト
はじめに
URPプロジェクトにおける反射(鏡面)床のメモです。
以下の記事を参考にさせていただき、
自プロジェクトで都合の良いように変更しています。
qiita.com
対象読者は、Unityの簡単な操作ができる方です。
概要
反射床を作る方式は複数あるようですが、
今回は「Planar Reflection」という方式で進めます。
概要としては、以下を行い反射床を表現しています。
1.床に対して反転したカメラを用意
2.反転カメラの映像をレンダーテクスチャに映す
3.レンダーテクスチャに映した映像を専用シェーダーで左右反転させる
4.左右反転したレンダーテクスチャの映像を床に映す
作成手順
1.オブジェクトの準備
1.1 反射床用のオブジェクトを用意する
平面を追加します。
ヒエラルキーの「+」ボタン
→「3Dオブジェクト」
→「平面」

このような平面が追加されます。

1.2 反射確認用オブジェクトの配置
確認用に3Dオブジェクトのキューブやカプセルを反射床の上に配置しておきます。

1.3 反射用カメラの設置
反射用カメラを追加します。
ヒエラルキーの「+」ボタン
→「カメラ」

追加したカメラの名前を「ReflectionCamera」としておきます。(好きな名前でOK)

オブジェクトの準備は以上です。
メインカメラと反射用カメラへのコンポーネント設定を行います。
メインカメラにスカイボックスコンポーネントを追加します。

スカイボックスコンポーネントを追加後、
好きなスカイボックスのマテリアルを設定してください。

今回はデフォルトのスカイボックスマテリアルを使用します。
2.2 反射用カメラの設定
反射用カメラは以下の設定を行います。
・Cameraコンポーネントをオフ
・ポストプロセスを有効化
・Audio Listenerの削除
・スカイボックスコンポーネントを追加

反射カメラのスカイボックスコンポーネントには、
メインカメラのマテリアルをスクリプトで設定します。
そのため、マテリアルの設定は不要です。
3.反転シェーダーを作成
カメラから受け取った映像を反転するシェーダーを作成します。
今回作成するシェーダーは色設定、反射強度設定、ぼかし設定の機能も持ちます。
プロジェクトタブの「+」ボタン
→「シェーダー」
→「Unlitシェーダー」

シェーダーの名前は「PlanarReflection」にしてください。

シェーダーのファイルをダブルクリックしてシェーダーファイルを開き、
中身を下記に置き換えてください。
Shader "Custom/PlanarReflection"
{
Properties
{
_Color("Base Color", Color) = (1, 1, 1, 1)
_MainTex("Main Texture", 2D) = "white" {}
_ReflectionTex("Reflection Texture", 2D) = "white" {}
_reflectionFactor("Reflection Factor", Range(0, 1)) = 1.0
_Roughness("Roughness", Range(0, 1)) = 0.0
_BlurRadius("Blur Radius", Range(0, 10)) = 5.0
}
SubShader
{
Tags { "Queue"="Geometry" "RenderType"="Opaque" }
Offset -1, -1
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 texcoord : TEXCOORD1;
};
struct v2f
{
float2 uv : TEXCOORD1;
float4 screenPos : TEXCOORD0;
float4 vertex : SV_POSITION;
};
float4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _ReflectionTex;
float4 _ReflectionTex_ST;
float _reflectionFactor;
float _Roughness;
float _BlurRadius;
float4 _BaseColor;
float gaussianWeight(float x, float sigma)
{
return exp(-(x * x) / (2.0 * sigma * sigma));
}
half4 gaussianBlur(sampler2D tex, float2 uv, float blurAmount)
{
if (blurAmount <= 0.001)
{
return tex2D(tex, uv);
}
half4 color = half4(0, 0, 0, 0);
float totalWeight = 0.0;
float2 texelSize = float2(1.0 / _ScreenParams.x, 1.0 / _ScreenParams.y);
int sampleCount = (int)lerp(3, 9, _Roughness);
float stepSize = blurAmount * _BlurRadius;
float sigma = stepSize * 0.5;
for (int x = -sampleCount; x <= sampleCount; x++)
{
for (int y = -sampleCount; y <= sampleCount; y++)
{
float2 offset = float2(x, y) * texelSize * stepSize;
float2 sampleUV = uv + offset;
if (sampleUV.x >= 0.0 && sampleUV.x <= 1.0 &&
sampleUV.y >= 0.0 && sampleUV.y <= 1.0)
{
float distance = length(float2(x, y));
float weight = gaussianWeight(distance, sigma);
color += tex2D(tex, sampleUV) * weight;
totalWeight += weight;
}
}
}
return color / totalWeight;
}
v2f vert(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.screenPos = ComputeScreenPos(o.vertex);
o.uv = v.texcoord;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
float2 screenUV = i.screenPos.xy / i.screenPos.w;
half4 tex_col = tex2D(_MainTex, i.uv);
float2 reflectionUV = float2(1 - screenUV.x, screenUV.y);
half4 reflectionColor = gaussianBlur(_ReflectionTex, reflectionUV, _Roughness);
fixed4 col = tex_col * _BaseColor * reflectionColor;
col = lerp(tex_col * _BaseColor, col, _reflectionFactor);
return col;
}
ENDCG
}
}
}
4.反転シェーダーのマテリアルの作成と割当て
4.1 マテリアル作成
反転シェーダーのマテリアルを作成します。
→作成したシェーダーファイルを右クリック
→「作成」
→「マテリアル」

作成したシェーダーを使用するマテリアルが作成されました。

4.2 マテリアル割当て
作成したマテリアルを反射床のオブジェクトにドラッグ&ドロップをして割り当てます。

反射床の見た目は真っ黒になりますが問題ありません。

5.反射用スクリプトの作成と割当て
新規スクリプト「PlanarReflectionView」を作成し下記の内容に変更します。
using UnityEngine;
using System.Collections;
public class PlanarReflectionView : MonoBehaviour
{
[Header("References")]
[SerializeField] private Camera _mainCamera;
[SerializeField] private Camera _reflectionCamera = null;
[SerializeField] private GameObject _reflectionTargetPlane = null;
[SerializeField] private Skybox _mainSkybox;
[SerializeField] private Skybox _reflectionSkybox;
[Header("Render Settings")]
[SerializeField, Range(0.3f, 1.0f)] private float _resolutionScale = 1.0f;
[Header("Material Properties")]
[SerializeField] private Color _reflectionColor = Color.white;
[SerializeField, Range(0.0f, 1.0f)] private float _reflectionFactor = 1.0f;
[SerializeField, Range(0.0f, 1.0f)] private float _roughness = 0.0f;
private const float _blurRadius = 5.0f;
[Header("Internal Runtime States")]
private RenderTexture _renderTarget;
private Material _floorMaterial;
private int _lastScreenWidth;
private int _lastScreenHeight;
private float _lastResolutionScale;
private void Start()
{
Renderer renderer = _reflectionTargetPlane.GetComponent<Renderer>();
_floorMaterial = renderer.sharedMaterial;
_reflectionCamera.enabled = false;
_lastScreenWidth = Screen.width;
_lastScreenHeight = Screen.height;
_lastResolutionScale = _resolutionScale;
CreateRenderTarget();
}
void Update()
{
if (_floorMaterial != null)
{
_floorMaterial.SetColor("_BaseColor", _reflectionColor);
_floorMaterial.SetFloat("_reflectionFactor", _reflectionFactor);
_floorMaterial.SetFloat("_Roughness", _roughness);
}
if (Screen.width != _lastScreenWidth ||
Screen.height != _lastScreenHeight ||
!Mathf.Approximately(_resolutionScale, _lastResolutionScale))
{
_lastScreenWidth = Screen.width;
_lastScreenHeight = Screen.height;
_lastResolutionScale = _resolutionScale;
RecreateRenderTarget();
}
}
private void LateUpdate()
{
StartCoroutine(RenderReflectionAtEndOfFrame());
}
private IEnumerator RenderReflectionAtEndOfFrame()
{
yield return new WaitForEndOfFrame();
RenderReflection();
}
private void CreateRenderTarget()
{
int width = Mathf.Max(256, Mathf.RoundToInt(Screen.width * _resolutionScale));
int height = Mathf.Max(256, Mathf.RoundToInt(Screen.height * _resolutionScale));
if (_renderTarget != null)
{
_reflectionCamera.targetTexture = null;
_renderTarget.Release();
DestroyImmediate(_renderTarget);
}
_renderTarget = new RenderTexture(width, height, 24)
{
name = "PlanarReflectionRT",
useMipMap = true,
autoGenerateMips = true
};
_floorMaterial.SetTexture("_ReflectionTex", _renderTarget);
_floorMaterial.SetFloat("_BlurRadius", _blurRadius);
}
private void RecreateRenderTarget()
{
if (_renderTarget != null)
{
_reflectionCamera.targetTexture = null;
_renderTarget.Release();
DestroyImmediate(_renderTarget);
}
CreateRenderTarget();
RenderReflection();
}
private void RenderReflection()
{
_reflectionCamera.CopyFrom(_mainCamera);
if (_mainSkybox != null && _mainSkybox.material != null)
{
_reflectionSkybox.material = _mainSkybox.material;
}
Vector3 cameraDirectionWorldSpace = _mainCamera.transform.forward;
Vector3 cameraUpWorldSpace = _mainCamera.transform.up;
Vector3 cameraPositionWorldSpace = _mainCamera.transform.position;
Vector3 cameraDirectionPlaneSpace = _reflectionTargetPlane.transform.InverseTransformDirection(cameraDirectionWorldSpace);
Vector3 cameraUpPlaneSpace = _reflectionTargetPlane.transform.InverseTransformDirection(cameraUpWorldSpace);
Vector3 cameraPositionPlaneSpace = _reflectionTargetPlane.transform.InverseTransformPoint(cameraPositionWorldSpace);
cameraDirectionPlaneSpace.y *= -1.0f;
cameraUpPlaneSpace.y *= -1.0f;
cameraPositionPlaneSpace.y *= -1.0f;
cameraDirectionWorldSpace = _reflectionTargetPlane.transform.TransformDirection(cameraDirectionPlaneSpace);
cameraUpWorldSpace = _reflectionTargetPlane.transform.TransformDirection(cameraUpPlaneSpace);
cameraPositionWorldSpace = _reflectionTargetPlane.transform.TransformPoint(cameraPositionPlaneSpace);
_reflectionCamera.transform.position = cameraPositionWorldSpace;
_reflectionCamera.transform.LookAt(cameraPositionWorldSpace + cameraDirectionWorldSpace, cameraUpWorldSpace);
_reflectionCamera.targetTexture = _renderTarget;
_reflectionCamera.Render();
}
}
※一部コメントアウトしているのはCinemachineを使う時用です。
空のオブジェクトを作成し、作成したスクリプト「PlanarReflectionView」を割り当てます。

空のオブジェクトをクリックして
インスペクター上に表示されているReferencesの5項目に情報を割り当てます。
MainCamera → メインカメラのオブジェクトを割当て
ReflectionCamera → 反射カメラのオブジェクトを割当て
ReflectionTargetPlane → 反射床オブジェクトを割当て
MainSkybox → メインカメラのオブジェクトを割当て
ReflectionSkybox → 反射カメラのオブジェクトを割当て

以上で準備完了です。
確認
プレイボタンを押してメインカメラの位置や回転を変更してみると、
平面上に反射しているかのような映像が映されます。

※メインカメラの映像を基に平面へ映像を映しているという手法のため
シーンビュー上では反射しているように見えません。
おわりに
アプリケーションとして出力した場合でもウインドウサイズの変更にも対応しています。
お役に立てましたら幸いです。