using System; using System.Collections; using System.Collections.Generic; using System.Runtime.InteropServices; using Unity.XR.CoreUtils; using UnityEngine; using UnityEngine.XR; public class Portal : MonoBehaviour { public bool allowRender; private bool _canRender; private bool _shouldRender = true; public Portal targetPortal; public Transform normalVisible; public Transform normalInvisible; public Camera portalLCamera; public Camera portalRCamera; public Renderer viewThroughRenderer; private RenderTexture _viewThroughRenderTextureL; private RenderTexture _viewThroughRenderTextureR; private Material _viewThroughMaterial; private Camera _mainCamera; private Vector4 _vectorPlane; public bool _shouldTeleport; private bool ShouldRender(Plane[] cameraPlanes) => viewThroughRenderer.isVisible && GeometryUtility.TestPlanesAABB(cameraPlanes, viewThroughRenderer.bounds); private void Start() { // Generate bounding plane var plane = new Plane(normalVisible.forward, transform.position + normalVisible.forward * -0.01f); _vectorPlane = new Vector4(plane.normal.x, plane.normal.y, plane.normal.z, plane.distance); if (allowRender) { float scaleX = transform.localScale.x; float scaleY = transform.localScale.y; // Create render texture _viewThroughRenderTextureL = new RenderTexture((1440), (1600), 24); _viewThroughRenderTextureL.Create(); _viewThroughRenderTextureR = new RenderTexture((1440), (1600), 24); _viewThroughRenderTextureR.Create(); // Assign render texture to portal camera portalLCamera.targetTexture = _viewThroughRenderTextureL; portalRCamera.targetTexture = _viewThroughRenderTextureR; // Assign render texture to portal material (cloned) _viewThroughMaterial = viewThroughRenderer.material; _viewThroughMaterial.SetTexture("_TexL", _viewThroughRenderTextureL); _viewThroughMaterial.SetTexture("_TexR", _viewThroughRenderTextureR); // Cache the main camera _mainCamera = Camera.main; Application.onBeforeRender += OnBeforeRender; _shouldTeleport = true; _canRender = true; } else { portalLCamera.gameObject.SetActive(false); portalRCamera.gameObject.SetActive(false); viewThroughRenderer.enabled = false; } } private void OnBeforeRender() { if (!_canRender) return; var cameraPlanes = GeometryUtility.CalculateFrustumPlanes(_mainCamera); if (!ShouldRender(cameraPlanes)) { if (!_shouldRender) return; Debug.Log("Disabling render for " + transform.name); viewThroughRenderer.material = new Material(Shader.Find("Unlit/Color")); portalLCamera.enabled = false; portalRCamera.enabled = false; _shouldRender = false; return; } if (!_shouldRender) { Debug.Log("Enabling render for " + transform.name); viewThroughRenderer.material = _viewThroughMaterial; portalLCamera.enabled = true; portalRCamera.enabled = true; _shouldRender = true; } UpdateCamera(portalLCamera, XRNode.LeftEye); UpdateCamera(portalRCamera, XRNode.RightEye); } private void UpdateCamera(Camera portalCamera, XRNode eye) { // Calculate portal camera position and rotation var virtualPosition = TransformPositionBetweenPortals(this, targetPortal, GetEyeWorldPosition(eye)); var virtualRotation = TransformRotationBetweenPortals(this, targetPortal, GetEyeRotation(eye)); // Position camera portalCamera.transform.SetPositionAndRotation(virtualPosition, virtualRotation); // Calculate projection matrix var clipThroughSpace = Matrix4x4.Transpose(Matrix4x4.Inverse(portalCamera.worldToCameraMatrix)) * targetPortal._vectorPlane; // Set portal camera projection matrix to clip walls between target portal and portal camera // Inherits main camera near/far clip plane and FOV settings portalCamera.projectionMatrix = CalculateObliqueMatrix( _mainCamera.GetStereoProjectionMatrix(eye == XRNode.LeftEye ? Camera.StereoscopicEye.Left : Camera.StereoscopicEye.Right), clipThroughSpace); } private void OnDestroy() { if (!_canRender) return; Application.onBeforeRender -= OnBeforeRender; // Release render texture from GPU _viewThroughRenderTextureL.Release(); _viewThroughRenderTextureR.Release(); // Destroy cloned material and render texture Destroy(_viewThroughMaterial); Destroy(_viewThroughRenderTextureL); Destroy(_viewThroughRenderTextureR); } private static Vector3 TransformPositionBetweenPortals(Portal sender, Portal target, Vector3 position) { return target.normalInvisible.TransformPoint( sender.normalVisible.InverseTransformPoint(position)); } private static Quaternion TransformRotationBetweenPortals(Portal sender, Portal target, Quaternion rotation) { return target.normalInvisible.rotation * Quaternion.Inverse(sender.normalVisible.rotation) * rotation; } private Vector3 GetEyeWorldPosition(XRNode eye) { if (!XRSettings.enabled) return _mainCamera.transform.position; InputDevice device = InputDevices.GetDeviceAtXRNode(eye); if (!device.isValid) return default; float cameraSeparation = _mainCamera.stereoSeparation; return _mainCamera.transform.position + _mainCamera.transform.right * (eye == XRNode.LeftEye ? -cameraSeparation : cameraSeparation) / 2; } private Quaternion GetEyeRotation(XRNode _) { return _mainCamera.transform.rotation; } static Matrix4x4 CalculateObliqueMatrix(Matrix4x4 projection, Vector4 clipPlane) { Matrix4x4 obliqueMatrix = projection; Vector4 q = projection.inverse * new Vector4( Math.Sign(clipPlane.x), Math.Sign(clipPlane.y), 1.0f, 1.0f ); Vector4 c = clipPlane * (2.0F / (Vector4.Dot(clipPlane, q))); obliqueMatrix[2] = c.x - projection[3]; obliqueMatrix[6] = c.y - projection[7]; obliqueMatrix[10] = c.z - projection[11]; obliqueMatrix[14] = c.w - projection[15]; return obliqueMatrix; } private void OnTriggerEnter(Collider other) { if (!_canRender || !_shouldTeleport) return; if (!other.CompareTag("Player")) return; Debug.Log(transform.name + " player entered and should teleport"); targetPortal._shouldTeleport = false; // Move player to target portal collider relative position other.transform.position = TransformPositionBetweenPortals(this, targetPortal, other.transform.position); other.transform.rotation = TransformRotationBetweenPortals(this, targetPortal, other.transform.rotation); } private void OnTriggerExit(Collider other) { if (!_canRender || !_shouldTeleport || IsInvoking(nameof(AllowTeleport))) return; if (!other.CompareTag("Player")) return; Debug.Log(transform.name + " player exited"); Invoke(nameof(AllowTeleport), 1f); } private void AllowTeleport() { _shouldTeleport = true; } }