cvr-props/Assets/ABI.CCK/Components/CVRMirror.cs

384 lines
No EOL
14 KiB
C#
Executable file

using System;
using ABI.CCK.Components;
using UnityEngine;
// https://web.archive.org/web/20210507003436/http://wiki.unity3d.com/index.php/MirrorReflection4
[AddComponentMenu("ChilloutVR/CVR Mirror")]
[HelpURL("https://developers.abinteractive.net/cck/components/mirror/")]
[ExecuteInEditMode]
public class CVRMirror : MonoBehaviour, ICCK_Component
{
public enum MirrorClearFlags {Skybox = 1, Color = 2}
// General
public bool m_DisablePixelLights = true;
public int m_TextureSize = 4096;
public LayerMask m_ReflectLayers = -1;
// Optimization
public bool m_UseOcclusionCulling;
// Advanced
public MirrorClearFlags m_ClearFlags = MirrorClearFlags.Skybox;
public Material m_CustomSkybox;
public Color m_CustomColor = new Color(19, 30, 47);
// Advanced / Why ??
public float m_ClipPlaneOffset = 0.001f;
public int m_framesNeededToUpdate;
// Legacy behaviour forces player layers on + UI off
public bool m_ignoreLegacyBehaviour;
private Camera m_ReflectionCamera;
private RenderTexture m_ReflectionTextureLeft;
private RenderTexture m_ReflectionTextureRight;
private MaterialPropertyBlock m_PropertyBlock;
private Renderer m_MirrorRenderer;
private int m_frameCounter;
private static bool s_InsideRendering;
// mirror mesh normal in local coordinates
private Vector3 mirrorNormal = Vector3.zero;
// configurable by player in-game
private int usedTextureSize = 4096;
private int usedMsaa = 0;
private static Shader mirrorShader;
private static readonly int _propertyLeft = Shader.PropertyToID("_ReflectionTexLeft");
private static readonly int _propertyRight = Shader.PropertyToID("_ReflectionTexRight");
#if UNITY_EDITOR
private void Reset()
{
// Ensure new content is not "legacy".
m_ignoreLegacyBehaviour = true;
}
public void OnValidate()
{
// prevent against infinite reimport when viewing prefabs
if (!gameObject.scene.IsValid())
return;
CleanupMirrorObjects();
m_MirrorRenderer = GetComponent<Renderer>();
if (!m_MirrorRenderer)
{
enabled = false;
return;
}
if (mirrorShader == null)
mirrorShader = Shader.Find("FX/MirrorReflection");
m_PropertyBlock ??= new MaterialPropertyBlock();
var materials = m_MirrorRenderer.sharedMaterials;
foreach (Material mat in materials)
{
if (mat == null) continue;
if (mat.shader.name is "FX/MirrorReflection" or "Alpha Blend Interactive/MirrorReflection")
mat.shader = mirrorShader;
}
m_MirrorRenderer.sharedMaterials = materials;
}
#endif
private void Start()
{
LegacyBehaviourIfNeeded();
// Prevent mirrors from reflecting others
// This is a reserved layer, no prior content should be using it
gameObject.layer = 14;
m_ReflectLayers &= ~(1 << 14);
mirrorShader = Shader.Find("FX/MirrorReflection");
m_PropertyBlock ??= new MaterialPropertyBlock();
m_MirrorRenderer = GetComponent<Renderer>();
if (!m_MirrorRenderer)
{
enabled = false;
return;
}
var materials = m_MirrorRenderer.sharedMaterials;
foreach (Material mat in materials)
{
if (mat == null) continue;
if (mat.shader.name is "FX/MirrorReflection" or "Alpha Blend Interactive/MirrorReflection")
mat.shader = mirrorShader;
}
m_MirrorRenderer.sharedMaterials = materials;
}
private void OnDisable()
{
CleanupMirrorObjects();
}
private void OnDestroy()
{
if (m_ReflectionCamera == null)
return;
if (Application.isEditor)
DestroyImmediate(m_ReflectionCamera.gameObject);
else
Destroy(m_ReflectionCamera.gameObject);
}
private void LegacyBehaviourIfNeeded()
{
if (m_ignoreLegacyBehaviour)
return;
// Older worlds should still force player-layers on for compatability.
// CCK Mirror prefab didn't reflect local player, so user content only worked cause of this!
m_UseOcclusionCulling = false;
m_ClipPlaneOffset = 0.001f; // exposed in CCK
m_ReflectLayers &= ~(1 << 5);
m_ReflectLayers &= ~(1 << 15);
m_ReflectLayers |= 1 << 8;
m_ReflectLayers |= 1 << 9;
m_ReflectLayers |= 1 << 10;
}
// This is called when it's known that the object will be rendered by some
// camera. We render reflections and do other updates here.
// Because the script executes in edit mode, reflections for the scene view
// camera will just work!
public void OnWillRenderObject()
{
if (!enabled || !m_MirrorRenderer || !m_MirrorRenderer.sharedMaterial || !m_MirrorRenderer.enabled)
return;
// Previously was RootLogic.Instance.activeCamera;
// Camera.current produces correct reflection with *any* camera, be it photo camera, in-world camera, camera on an avatar, or anything else
// TODO: consider a marker or settings component (i.e. CVRCameraSettings) that would allow excluding mirrors from camera render (useful on both avatars and worlds)
Camera cam = Camera.current;
if (!cam)
return;
// Safeguard from recursive reflections.
if (s_InsideRendering) return;
s_InsideRendering = true;
if (m_frameCounter > 0)
{
m_frameCounter--;
return;
}
m_frameCounter = m_framesNeededToUpdate;
mirrorNormal = Vector3.up;
MeshFilter meshFilter = GetComponent<MeshFilter>();
Mesh mesh = meshFilter != null ? meshFilter.sharedMesh : null;
if (mesh != null && mesh.normals.Length > 0)
mirrorNormal = mesh.normals[0];
// Optionally disable pixel lights for reflection
int oldPixelLightCount = QualitySettings.pixelLightCount;
if (m_DisablePixelLights)
QualitySettings.pixelLightCount = 0;
try
{
RenderCamera(cam, m_MirrorRenderer, Camera.StereoscopicEye.Left, ref m_ReflectionTextureLeft);
m_PropertyBlock.SetTexture(_propertyLeft, m_ReflectionTextureLeft);
if (!cam.stereoEnabled) return;
RenderCamera(cam, m_MirrorRenderer, Camera.StereoscopicEye.Right, ref m_ReflectionTextureRight);
m_PropertyBlock.SetTexture(_propertyRight, m_ReflectionTextureRight);
}
finally
{
s_InsideRendering = false;
m_MirrorRenderer.SetPropertyBlock(m_PropertyBlock);
if (m_DisablePixelLights) // Restore pixel light count
QualitySettings.pixelLightCount = oldPixelLightCount;
}
}
private void RenderCamera(Camera cam, Renderer rend, Camera.StereoscopicEye eye,
ref RenderTexture reflectionTexture)
{
// find out the reflection plane: position and normal in world space
Vector3 pos = transform.position;
Vector3 normal = transform.TransformDirection(mirrorNormal);
CreateMirrorObjects(cam, eye, ref reflectionTexture);
CopyCameraProperties(cam, m_ReflectionCamera);
m_ReflectionCamera.useOcclusionCulling = m_UseOcclusionCulling;
m_ReflectionCamera.depthTextureMode = cam.depthTextureMode | DepthTextureMode.Depth;
m_ReflectionCamera.stereoTargetEye = StereoTargetEyeMask.None;
m_ReflectionCamera.cullingMask = m_ReflectLayers.value;
// Render reflection
// Reflect camera around reflection plane
float d = -Vector3.Dot(normal, pos) - m_ClipPlaneOffset;
Vector4 reflectionPlane = new Vector4(normal.x, normal.y, normal.z, d);
Matrix4x4 reflection = Matrix4x4.zero;
CalculateReflectionMatrix(ref reflection, reflectionPlane);
Matrix4x4 worldToCameraMatrix;
if (cam.stereoEnabled)
worldToCameraMatrix = cam.GetStereoViewMatrix(eye) * reflection;
else
worldToCameraMatrix = cam.worldToCameraMatrix * reflection;
m_ReflectionCamera.targetTexture = reflectionTexture;
Matrix4x4 cameraSideReflection = Matrix4x4.zero;
CalculateReflectionMatrix(ref cameraSideReflection, new Vector4(1, 0, 0, 0));
worldToCameraMatrix = cameraSideReflection * worldToCameraMatrix;
m_ReflectionCamera.worldToCameraMatrix = worldToCameraMatrix;
// Setup oblique projection matrix so that near plane is our reflection
// plane. This way we clip everything below/above it for free.
Vector4 clipPlane = CameraSpacePlane(worldToCameraMatrix, pos, normal, 1.0f);
m_ReflectionCamera.projectionMatrix = cameraSideReflection *
(cam.stereoEnabled
? cam.GetStereoProjectionMatrix(eye)
: cam.projectionMatrix) * cameraSideReflection.inverse;
m_ReflectionCamera.projectionMatrix = m_ReflectionCamera.CalculateObliqueMatrix(clipPlane);
m_ReflectionCamera.Render();
}
// Cleanup all the objects we possibly have created
private void CleanupMirrorObjects()
{
if (m_ReflectionTextureLeft)
{
RenderTexture.ReleaseTemporary(m_ReflectionTextureLeft);
m_ReflectionTextureLeft = null;
}
if (m_ReflectionTextureRight)
{
RenderTexture.ReleaseTemporary(m_ReflectionTextureRight);
m_ReflectionTextureRight = null;
}
}
private void CopyCameraProperties(Camera src, Camera dest)
{
if (dest == null)
return;
dest.CopyFrom(src);
if (m_ClearFlags == MirrorClearFlags.Skybox)
{
dest.clearFlags = CameraClearFlags.Skybox;
Skybox mysky = dest.GetComponent<Skybox>();
if (!mysky || !m_CustomSkybox)
{
mysky.enabled = false;
}
else
{
mysky.enabled = true;
mysky.material = m_CustomSkybox;
}
}
else if (m_ClearFlags == MirrorClearFlags.Color)
{
dest.clearFlags = CameraClearFlags.Color;
dest.backgroundColor = m_CustomColor;
}
}
private void CreateMirrorObjects(Camera currentCamera, Camera.StereoscopicEye eye,
ref RenderTexture reflectionTexture)
{
// Calculate target resolution
int currentTextureWidth = Mathf.RoundToInt(Math.Min(usedTextureSize, currentCamera.pixelWidth));
int currentTextureHeight = Mathf.RoundToInt(Math.Min(usedTextureSize, currentCamera.pixelHeight));
var targetMsaa = usedMsaa;
if (targetMsaa == 0)
{
RenderTexture targetTexture = currentCamera.targetTexture;
if (targetTexture != null)
targetMsaa = targetTexture.antiAliasing;
else
targetMsaa = QualitySettings.antiAliasing == 0 ? 1 : QualitySettings.antiAliasing;
}
// Unity is good at caching rendertextures, so releasing it here and then immediately re-allocating a texture with the same resolution is fast
// If the resolution is different, a new texture will be allocated and the old one will be freed
if (reflectionTexture)
RenderTexture.ReleaseTemporary(reflectionTexture);
// Additionally, releasing it here (instead of after mirror rendering is done) allows it to survive turning away from the mirror,
// so that turning away from and back towards a mirror does not lead to lag spikes
// Reflection render texture
reflectionTexture = RenderTexture.GetTemporary(currentTextureWidth, currentTextureHeight, 24,
RenderTextureFormat.ARGBHalf,
RenderTextureReadWrite.Default, targetMsaa, RenderTextureMemoryless.None, VRTextureUsage.None);
reflectionTexture.name = "__MirrorReflection" + eye.ToString() + GetInstanceID();
// Camera for reflection
if (m_ReflectionCamera == null)
{
GameObject go = new GameObject("Mirror Reflection Camera id" + GetInstanceID(),
typeof(Camera), typeof(Skybox), typeof(FlareLayer));
// Parent it to the mirror for easy cleanup in case of destroyed mirror
go.transform.SetParent(transform);
m_ReflectionCamera = go.GetComponent<Camera>();
// Reflection camera transform is irrelevant because it has matrices set explicitly
m_ReflectionCamera.enabled = false;
go.hideFlags = HideFlags.DontSave | HideFlags.HideInHierarchy;
}
}
// Given position/normal of the plane, calculates plane in camera space.
private Vector4 CameraSpacePlane(Matrix4x4 worldToCameraMatrix, Vector3 pos, Vector3 normal, float sideSign)
{
Vector3 offsetPos = pos + normal * m_ClipPlaneOffset;
Vector3 cpos = worldToCameraMatrix.MultiplyPoint(offsetPos);
Vector3 cnormal = worldToCameraMatrix.MultiplyVector(normal).normalized * sideSign;
return new Vector4(cnormal.x, cnormal.y, cnormal.z, -Vector3.Dot(cpos, cnormal));
}
// Calculates reflection matrix around the given plane
private static void CalculateReflectionMatrix(ref Matrix4x4 reflectionMat, Vector4 plane)
{
reflectionMat.m00 = (1F - 2F * plane[0] * plane[0]);
reflectionMat.m01 = (-2F * plane[0] * plane[1]);
reflectionMat.m02 = (-2F * plane[0] * plane[2]);
reflectionMat.m03 = (-2F * plane[3] * plane[0]);
reflectionMat.m10 = (-2F * plane[1] * plane[0]);
reflectionMat.m11 = (1F - 2F * plane[1] * plane[1]);
reflectionMat.m12 = (-2F * plane[1] * plane[2]);
reflectionMat.m13 = (-2F * plane[3] * plane[1]);
reflectionMat.m20 = (-2F * plane[2] * plane[0]);
reflectionMat.m21 = (-2F * plane[2] * plane[1]);
reflectionMat.m22 = (1F - 2F * plane[2] * plane[2]);
reflectionMat.m23 = (-2F * plane[3] * plane[2]);
reflectionMat.m30 = 0F;
reflectionMat.m31 = 0F;
reflectionMat.m32 = 0F;
reflectionMat.m33 = 1F;
}
}