using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
namespace kTools.Mirrors
{
///
/// Mirror Object component.
///
[AddComponentMenu("kTools/Mirror"), ExecuteInEditMode]
[RequireComponent(typeof(Camera), typeof(UniversalAdditionalCameraData))]
public class Mirror : MonoBehaviour
{
#region Enumerations
///
/// Camera override enumeration for Mirror properties
///
public enum MirrorCameraOverride
{
UseSourceCameraSettings,
Off,
}
///
/// Scope enumeration for Mirror output destination
///
public enum OutputScope
{
Global,
Local,
}
#endregion
#region Serialized Fields
[SerializeField]
float m_Offset;
[SerializeField]
int m_LayerMask;
[SerializeField]
OutputScope m_Scope;
[SerializeField]
List m_Renderers;
[SerializeField]
float m_TextureScale;
[SerializeField]
MirrorCameraOverride m_AllowHDR;
[SerializeField]
MirrorCameraOverride m_AllowMSAA;
#endregion
#region Fields
const string kGizmoPath = "Packages/com.kink3d.mirrors/Gizmos/Mirror.png";
Camera m_ReflectionCamera;
UniversalAdditionalCameraData m_CameraData;
RenderTexture m_RenderTexture;
RenderTextureDescriptor m_PreviousDescriptor;
#endregion
#region Constructors
public Mirror()
{
// Set data
m_Offset = 0.01f;
m_LayerMask = -1;
m_Scope = OutputScope.Global;
m_Renderers = new List();
m_TextureScale = 1.0f;
m_AllowHDR = MirrorCameraOverride.UseSourceCameraSettings;
m_AllowMSAA = MirrorCameraOverride.UseSourceCameraSettings;
}
#endregion
#region Properties
/// Offset value for oplique near clip plane.
public float offest
{
get => m_Offset;
set => m_Offset = value;
}
/// Which layers should the Mirror render.
public LayerMask layerMask
{
get => m_LayerMask;
set => m_LayerMask = value;
}
///
/// Global output renders to the global texture. Only one Mirror can be global.
/// Local output renders to one texture per Mirror, this is set on all elements of the Renderers list.
///
public OutputScope scope
{
get => m_Scope;
set => m_Scope = value;
}
/// Renderers to set the reflection texture on.
public List renderers
{
get => m_Renderers;
set => m_Renderers = value;
}
/// Scale value applied to the size of the source camera texture.
public float textureScale
{
get => m_TextureScale;
set => m_TextureScale = value;
}
/// Should reflections be rendered in HDR.
public MirrorCameraOverride allowHDR
{
get => m_AllowHDR;
set => m_AllowHDR = value;
}
/// Should reflections be resolved with MSAA.
public MirrorCameraOverride allowMSAA
{
get => m_AllowMSAA;
set => m_AllowMSAA = value;
}
Camera reflectionCamera
{
get
{
if(m_ReflectionCamera == null)
m_ReflectionCamera = GetComponent();
return m_ReflectionCamera;
}
}
UniversalAdditionalCameraData cameraData
{
get
{
if(m_CameraData == null)
m_CameraData = GetComponent();
return m_CameraData;
}
}
#endregion
#region State
void OnEnable()
{
// Callbacks
RenderPipelineManager.beginCameraRendering += BeginCameraRendering;
// Initialize Components
InitializeCamera();
}
void OnDisable()
{
// Callbacks
RenderPipelineManager.beginCameraRendering -= BeginCameraRendering;
// Dispose RenderTexture
SafeDestroyObject(m_RenderTexture);
}
#endregion
#region Initialization
void InitializeCamera()
{
// Setup Camera
reflectionCamera.cameraType = CameraType.Reflection;
reflectionCamera.targetTexture = m_RenderTexture;
// Setup AdditionalCameraData
cameraData.renderShadows = false;
cameraData.requiresColorOption = CameraOverrideOption.Off;
cameraData.requiresDepthOption = CameraOverrideOption.Off;
}
#endregion
#region RenderTexture
RenderTextureDescriptor GetDescriptor(Camera camera)
{
// Get scaled Texture size
var width = (int)Mathf.Max(camera.pixelWidth * textureScale, 4);
var height = (int)Mathf.Max(camera.pixelHeight * textureScale, 4);
// Get Texture format
var hdr = allowHDR == MirrorCameraOverride.UseSourceCameraSettings ? camera.allowHDR : false;
var renderTextureFormat = hdr ? RenderTextureFormat.DefaultHDR : RenderTextureFormat.Default;
return new RenderTextureDescriptor(width, height, renderTextureFormat, 16) { autoGenerateMips = true, useMipMap = true };
}
#endregion
#region Rendering
void BeginCameraRendering(ScriptableRenderContext context, Camera camera)
{
// Never render Mirrors for Preview or Reflection cameras
if(camera.cameraType == CameraType.Preview || camera.cameraType == CameraType.Reflection)
return;
// Profiling command
CommandBuffer cmd = CommandBufferPool.Get($"Mirror {gameObject.GetInstanceID()}");
using (new ProfilingSample(cmd, $"Mirror {gameObject.GetInstanceID()}"))
{
ExecuteCommand(context, cmd);
// Test for Descriptor changes
var descriptor = GetDescriptor(camera);
if(!descriptor.Equals(m_PreviousDescriptor))
{
// Dispose RenderTexture
if(m_RenderTexture != null)
{
SafeDestroyObject(m_RenderTexture);
}
// Create new RenderTexture
m_RenderTexture = new RenderTexture(descriptor);
m_PreviousDescriptor = descriptor;
reflectionCamera.targetTexture = m_RenderTexture;
}
// Execute
RenderMirror(context, camera);
SetShaderUniforms(context, m_RenderTexture, cmd);
}
ExecuteCommand(context, cmd);
}
void RenderMirror(ScriptableRenderContext context, Camera camera)
{
// Mirror the view matrix
var mirrorMatrix = GetMirrorMatrix();
reflectionCamera.worldToCameraMatrix = camera.worldToCameraMatrix * mirrorMatrix;
// Make oplique projection matrix where near plane is mirror plane
var mirrorPlane = GetMirrorPlane(reflectionCamera);
var projectionMatrix = camera.CalculateObliqueMatrix(mirrorPlane);
reflectionCamera.projectionMatrix = projectionMatrix;
// Miscellanious camera settings
reflectionCamera.cullingMask = layerMask;
reflectionCamera.allowHDR = allowHDR == MirrorCameraOverride.UseSourceCameraSettings ? camera.allowHDR : false;
reflectionCamera.allowMSAA = allowMSAA == MirrorCameraOverride.UseSourceCameraSettings ? camera.allowMSAA : false;
reflectionCamera.enabled = false;
// Render reflection camera with inverse culling
GL.invertCulling = true;
UniversalRenderPipeline.RenderSingleCamera(context, reflectionCamera);
GL.invertCulling = false;
}
#endregion
#region Projection
Matrix4x4 GetMirrorMatrix()
{
// Setup
var position = transform.position;
var normal = transform.forward;
var depth = -Vector3.Dot(normal, position) - offest;
// Create matrix
var mirrorMatrix = new Matrix4x4()
{
m00 = (1f - 2f * normal.x * normal.x),
m01 = (-2f * normal.x * normal.y),
m02 = (-2f * normal.x * normal.z),
m03 = (-2f * depth * normal.x),
m10 = (-2f * normal.y * normal.x),
m11 = (1f - 2f * normal.y * normal.y),
m12 = (-2f * normal.y * normal.z),
m13 = (-2f * depth * normal.y),
m20 = (-2f * normal.z * normal.x),
m21 = (-2f * normal.z * normal.y),
m22 = (1f - 2f * normal.z * normal.z),
m23 = (-2f * depth * normal.z),
m30 = 0f,
m31 = 0f,
m32 = 0f,
m33 = 1f,
};
return mirrorMatrix;
}
Vector4 GetMirrorPlane(Camera camera)
{
// Calculate mirror plane in camera space.
var pos = transform.position - Vector3.forward * 0.1f;
var normal = transform.forward;
var offsetPos = pos + normal * offest;
var cpos = camera.worldToCameraMatrix.MultiplyPoint(offsetPos);
var cnormal = camera.worldToCameraMatrix.MultiplyVector(normal).normalized;
return new Vector4(cnormal.x, cnormal.y, cnormal.z, -Vector3.Dot(cpos, cnormal));
}
#endregion
#region Output
void SetShaderUniforms(ScriptableRenderContext context, RenderTexture renderTexture, CommandBuffer cmd)
{
var block = new MaterialPropertyBlock();
switch(scope)
{
case OutputScope.Global:
// Globals
cmd.SetGlobalTexture("_ReflectionMap", renderTexture);
ExecuteCommand(context, cmd);
// Property Blocm
block.SetFloat("_LocalMirror", 0.0f);
foreach(var renderer in renderers)
{
renderer.SetPropertyBlock(block);
}
break;
case OutputScope.Local:
// Keywords
Shader.EnableKeyword("_BLEND_MIRRORS");
// Property Block
block.SetTexture("_LocalReflectionMap", renderTexture);
block.SetFloat("_LocalMirror", 1.0f);
foreach(var renderer in renderers)
{
renderer.SetPropertyBlock(block);
}
break;
}
}
#endregion
#region CommandBufer
void ExecuteCommand(ScriptableRenderContext context, CommandBuffer cmd)
{
context.ExecuteCommandBuffer(cmd);
cmd.Clear();
}
#endregion
#region Object
void SafeDestroyObject(Object obj)
{
if(obj == null)
return;
#if UNITY_EDITOR
DestroyImmediate(obj);
#else
Destroy(obj);
#endif
}
#endregion
#region AssetMenu
#if UNITY_EDITOR
// Add a menu item to Mirrors
[UnityEditor.MenuItem("GameObject/kTools/Mirror", false, 10)]
static void CreateMirrorObject(UnityEditor.MenuCommand menuCommand)
{
// Create Mirror
GameObject go = new GameObject("New Mirror", typeof(Mirror));
// Transform
UnityEditor.GameObjectUtility.SetParentAndAlign(go, menuCommand.context as GameObject);
// Undo and Selection
UnityEditor.Undo.RegisterCreatedObjectUndo(go, "Create " + go.name);
UnityEditor.Selection.activeObject = go;
}
#endif
#endregion
#region Gizmos
#if UNITY_EDITOR
void OnDrawGizmos()
{
// Setup
var bounds = new Vector3(1.0f, 1.0f, 0.0f);
var color = new Color32(0, 120, 255, 255);
var selectedColor = new Color32(255, 255, 255, 255);
var isSelected = UnityEditor.Selection.activeObject == gameObject;
// Draw Gizmos
Gizmos.matrix = transform.localToWorldMatrix;
Gizmos.color = isSelected ? selectedColor : color;
Gizmos.DrawIcon(transform.position, kGizmoPath, true);
Gizmos.DrawWireCube(Vector3.zero, bounds);
}
#endif
#endregion
}
}