using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; using UnityEngine.XR; namespace Escape_Room_Engine.Portal { public class Portal : 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 portal that is connected with this one. /// public Portal other; /// /// The minimum near clip plane distance to be used when calculating the oblique clip plane. /// public float minNearClipPlane = 0.02f; /// /// The camera that will draw the view for this portal. /// [SerializeField] private Camera portalCamera; /// /// The quad where the rendered texture will be drawn on. /// [SerializeField] private MeshRenderer portalQuad; private PlayerCamera _playerCamera; private Dictionary _textures = new(); private void Awake() { // check whether the other portal is set up if (!other || other.other != this) throw new Exception("Other portal not set up correctly."); // get player camera if (Camera.main != null) _playerCamera = Camera.main.GetComponent(); else throw new Exception("Main camera has no player camera script set up."); } 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, other.portalQuad.bounds)) // don't render this portal if it is not visible return; var t = 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)); other.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 * other.transform.worldToLocalMatrix * _playerCamera.getEyeTransform(eye).localToWorldMatrix; portalCamera.transform.SetPositionAndRotation(m.GetPosition(), m.rotation); portalCamera.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 = portalCamera.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(portalCamera.transform.position)) >= minNearClipPlane) // only adjust the near clip plane if it doesn't intersect with the camera portalCamera.projectionMatrix = portalCamera.CalculateObliqueMatrix(clipPlane); // render portal view portalCamera.targetTexture = _textures[eye]; UniversalRenderPipeline.RenderSingleCamera(scriptableRenderContext, portalCamera); } } } }