using System; using System.Collections.Generic; using EscapeRoomEngine.Engine.Runtime.Modules.State; using EscapeRoomEngine.Engine.Runtime.Utilities; using EscapeRoomEngine.VR.Runtime; using NaughtyAttributes; using UnityEngine; namespace EscapeRoomEngine.Portal.Runtime { /// /// The portal is a specific type of for seamless transitions between spaces. /// public class Portal : DoorState { private static readonly int PortalNumberProperty = Shader.PropertyToID("_PortalNumber"); public static readonly Matrix4x4 HalfRotation = Matrix4x4.Rotate(Quaternion.Euler(0, 180, 0)); private static int _portalCounter = 1; /// /// The portal that is connected with this one. /// public Portal linkedPortal; /// /// The minimum near clip plane distance to be used when calculating the oblique clip plane. /// public float minNearClipPlane = 0.0001f; /// /// The meshes where the portal will draw its stencil and depth. /// [BoxGroup("Internal")] [SerializeField] private MeshRenderer screen; /// /// The transform marking the edge of the portal plane. /// [BoxGroup("Internal")] public Transform portalTransform; /// /// Whether this portal is connected is determined by whether the reference to its linked portal is set. /// internal bool Connected => linkedPortal != null; [ShowNativeProperty] internal int PortalNumber { get; private set; } internal readonly List closePortalDrivers = new(); protected virtual void Awake() { PortalNumber = _portalCounter++; screen.material.SetInt(PortalNumberProperty, PortalNumber); DoorEvent += (_, type) => { // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault switch (type) { case DoorEventType.Connected: linkedPortal = FromDoorState(Module.ConnectedDoorState); screen.gameObject.SetActive(true); break; case DoorEventType.Locked: screen.gameObject.SetActive(false); linkedPortal = null; break; } }; } private void FixedUpdate() { if (Connected) { for (var i = 0; i < closePortalDrivers.Count; i++) { var portalDriver = closePortalDrivers[i]; if (portalDriver.entrySide < 0 && CalculateSide(portalDriver.transform) >= 0) // must have entered from the front and exited the back { StopTrackingDriver(portalDriver); linkedPortal.StartTrackingDriver(portalDriver, -1); portalDriver.Teleport(this, linkedPortal); if (portalDriver.player) { linkedPortal.ExitFrom(); } i--; // decrease the loop counter because the list is one element smaller now } } } } public Camera SetUpCamera(Camera playerCamera, Camera.MonoOrStereoscopicEye monoOrEye) { // place camera Matrix4x4 cameraTransform; if (monoOrEye == Camera.MonoOrStereoscopicEye.Mono || playerCamera != Player.Instance.camera) { cameraTransform = portalTransform.localToWorldMatrix * HalfRotation * linkedPortal.portalTransform.worldToLocalMatrix * playerCamera.transform.localToWorldMatrix; } else { var eye = monoOrEye == Camera.MonoOrStereoscopicEye.Left ? Camera.StereoscopicEye.Left : Camera.StereoscopicEye.Right; cameraTransform = portalTransform.localToWorldMatrix * HalfRotation * linkedPortal.portalTransform.worldToLocalMatrix * Player.Instance.GetEye(eye).localToWorldMatrix; playerCamera.projectionMatrix = playerCamera.GetStereoProjectionMatrix(eye); } playerCamera.transform.SetPositionAndRotation(cameraTransform.GetPosition(), cameraTransform.rotation); // 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 // clip plane normal var n = -portalTransform.forward; // clip plane in world space var portalPlane = new Plane(n, portalTransform.position); if (-portalPlane.GetDistanceToPoint(playerCamera.transform.position) >= minNearClipPlane) { // vector format clip plane in camera space var clipPlane = playerCamera.worldToCameraMatrix.inverse.transpose * new Vector4(n.x, n.y, n.z, portalPlane.distance); // only adjust the near clip plane if it doesn't intersect with the camera playerCamera.projectionMatrix = playerCamera.CalculateObliqueMatrix(clipPlane); } return playerCamera; } /// /// Begin tracking a portal driver that came close to this portal and might need to be teleported. /// internal void StartTrackingDriver(PortalDriver portalDriver, int entrySide) { closePortalDrivers.Add(portalDriver); portalDriver.EnableClone(linkedPortal); portalDriver.entrySide = entrySide; } /// /// End tracking a portal driver that distanced itself from this portal or was teleported to the linked portal. /// internal void StopTrackingDriver(PortalDriver portalDriver) { closePortalDrivers.Remove(portalDriver); portalDriver.DisableClone(linkedPortal); } /// /// Calculate which side of the portal plane a transform is. /// internal int CalculateSide(Transform portalDriverTransform) { return Math.Sign(Vector3.Dot(portalTransform.forward, portalDriverTransform.position - portalTransform.position)); } private static Portal FromDoorState(DoorState state) { if (state is Portal portal) { return portal; } throw new WrongTypeException(typeof(Portal), state.GetType(), typeof(DoorState)); } } }