using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; using UnityEngine.XR; namespace Escape_Room_Engine.Portal { [RequireComponent(typeof(Camera))] public class PortalCamera : MonoBehaviour { private static readonly Matrix4x4 HalfRotation = Matrix4x4.Rotate(Quaternion.Euler(0, 180, 0)); private static readonly Camera.StereoscopicEye[] Eyes = { Camera.StereoscopicEye.Left, Camera.StereoscopicEye.Right }; private static readonly Dictionary EyeTextureNames = new() { { Camera.StereoscopicEye.Left, Shader.PropertyToID("_LeftTex") }, { Camera.StereoscopicEye.Right, Shader.PropertyToID("_RightTex") } }; /// /// The minimum near clip plane distance to be used when calculating the oblique clip plane. /// public float minNearClipPlane = 0.02f; /// /// The portal this camera renders through. /// [SerializeField] private Portal portal; /// /// The quad where the rendered texture will be drawn on. /// [SerializeField] private MeshRenderer portalQuad; private PlayerCamera _playerCamera; private Camera _camera; private readonly Dictionary _textures = new(); private void Awake() { // get player camera if (Camera.main != null) _playerCamera = Camera.main.GetComponent(); else throw new Exception("Main camera has no player camera script set up."); // get portal camera _camera = GetComponent(); } private void OnEnable() { RenderPipelineManager.beginCameraRendering += Render; } private void OnDisable() { RenderPipelineManager.beginCameraRendering -= Render; } private void Render(ScriptableRenderContext scriptableRenderContext, Camera camera) { // check whether the portal plane is visible from the player camera var frustumPlanes = GeometryUtility.CalculateFrustumPlanes(_playerCamera.camera); if (!GeometryUtility.TestPlanesAABB(frustumPlanes, portal.other.portalCamera.portalQuad.bounds)) // don't render this portal if it is not visible return; var t = portal.transform; foreach (var eye in Eyes) { // create portal texture if it doesn't exist yet if (!_textures.ContainsKey(eye)) { if (XRSettings.eyeTextureWidth > 0 && XRSettings.eyeTextureHeight > 0) { _textures.Add(eye, new RenderTexture(XRSettings.eyeTextureWidth, XRSettings.eyeTextureHeight, 24)); portal.other.portalCamera.portalQuad.material.SetTexture(EyeTextureNames[eye], _textures[eye]); } else // no texture was created so nothing should be rendered continue; } // position portal camera var m = t.localToWorldMatrix * HalfRotation * portal.other.transform.worldToLocalMatrix * _playerCamera.getEyeTransform(eye).localToWorldMatrix; transform.SetPositionAndRotation(m.GetPosition(), m.rotation); _camera.projectionMatrix = _playerCamera.camera.GetStereoProjectionMatrix(eye); // set camera clip plane to portal (otherwise the wall behind the portal would be rendered) // calculating the clip plane: https://computergraphics.stackexchange.com/a/1506 var n = -t.forward; // clip plane normal var portalPlane = new Plane(n, t.position); // clip plane in world space var clipPlane = _camera.worldToCameraMatrix.inverse.transpose * new Vector4(n.x, n.y, n.z, portalPlane.distance); // vector format clip plane in camera space if (Math.Abs(portalPlane.GetDistanceToPoint(transform.position)) >= minNearClipPlane) // only adjust the near clip plane if it doesn't intersect with the camera _camera.projectionMatrix = _camera.CalculateObliqueMatrix(clipPlane); // render portal view _camera.targetTexture = _textures[eye]; UniversalRenderPipeline.RenderSingleCamera(scriptableRenderContext, _camera); } } } }