using System.Collections.Generic; using EscapeRoomEngine.VR.Runtime; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; using UnityEngine.XR; namespace EscapeRoomEngine.Portal.Runtime { [RequireComponent(typeof(Camera))] public class PortalCamera : MonoBehaviour { 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.0001f; /// /// The portal this camera renders through. /// [SerializeField] private Portal portal; /// /// The mesh where the rendered texture will be drawn on. /// [SerializeField] public MeshRenderer screen; private Camera _camera; private readonly Dictionary _textures = new(); private void Awake() { // get portal camera _camera = GetComponent(); } private void OnEnable() { _camera.enabled = true; RenderPipelineManager.beginCameraRendering += Render; } private void OnDisable() { RenderPipelineManager.beginCameraRendering -= Render; _camera.enabled = false; } private void Render(ScriptableRenderContext scriptableRenderContext, Camera _) { // check whether the portal plane is visible from the player camera var frustumPlanes = GeometryUtility.CalculateFrustumPlanes(Player.Instance.camera); if (!GeometryUtility.TestPlanesAABB(frustumPlanes, portal.linkedPortal.portalCamera.screen.bounds)) // don't render this portal if it is not visible return; screen.enabled = false; 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.linkedPortal.portalCamera.screen.material.SetTexture(EyeTextureNames[eye], _textures[eye]); } else // no texture was created so nothing should be rendered continue; } // position portal camera var m = portal.portalTransform.localToWorldMatrix * Portal.HalfRotation * portal.linkedPortal.portalTransform.worldToLocalMatrix * Player.Instance.GetEye(eye).localToWorldMatrix; transform.SetPositionAndRotation(m.GetPosition(), m.rotation); _camera.projectionMatrix = Player.Instance.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 = -portal.portalTransform.forward; // clip plane normal var portalPlane = new Plane(n, portal.portalTransform.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 (-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); } screen.enabled = true; } } }