Created
April 17, 2020 00:50
-
-
Save limdingwen/5d42b3f09a1fbf4fa422b9b60a76df2d to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections; | |
using System.Collections.Generic; | |
using UnityEngine; | |
using UnityEngine.AI; | |
using UnityEngine.Profiling; | |
using UnityEngine.Serialization; | |
using LayerMask = UnityEngine.LayerMask; | |
public class Portal : MonoBehaviour | |
{ | |
[FormerlySerializedAs("target")] | |
[SerializeField] private Portal targetPortal; | |
/// <summary> | |
/// A transform whose forward represents the normal of the visible plane of the portal. | |
/// </summary> | |
[FormerlySerializedAs("normal")] | |
[SerializeField] private Transform normalVisible; | |
/// <summary> | |
/// A transform whose forward represents the normal of the invisible plane of the portal. | |
/// </summary> | |
[SerializeField] private Transform normalInvisible; | |
[SerializeField] private Shader viewthroughShader; | |
/// <summary> | |
/// When rendering recursively, if the recurse limit is reached, use this default texture instead. | |
/// </summary> | |
[SerializeField] private Texture viewthroughDefaultTexture; | |
private MeshRenderer viewthroughRenderer; | |
private Material viewthroughMaterial; | |
[SerializeField] private Portal[] visiblePortals; | |
/// <summary> | |
/// Overrides the recursion rendering amount, for views going OUT of this portal. | |
/// For example, if the value is 6, any portal that sees through this portal will see a recursion level of 6. | |
/// Lower (or set to 0) for performance reasons, or increase for better infinite portal effects with limited performance hit. | |
/// Set to any number less than 0 (default -1) to disable the override. | |
/// | |
/// This override only applies if the portal is being rendered by a portal that the player is directly with, | |
/// in the same occlusion volume. | |
/// </summary> | |
[SerializeField] private int maxRecursionsDirectOverride = -1; | |
/// <summary> | |
/// Same as above, but only if the portal is being rendered by a portal that was already recursively rendered. | |
/// Use this to optimize for cases where an infinite portal might need to be rendered many times when seen directly, | |
/// but not that many times when seen from another occlusion volume. | |
/// </summary> | |
[SerializeField] private int maxRecursionsIndirectOverride = -1; | |
[SerializeField] private bool teleportEnable = true; | |
private readonly HashSet<PortalableObject> teleportObjectsTouchingPortal = new HashSet<PortalableObject>(); | |
private readonly HashSet<PortalableObject> teleportObjectsToRemove = new HashSet<PortalableObject>(); | |
/// <summary> | |
/// How many meters between each OffMeshLink? | |
/// </summary> | |
[SerializeField] private float offMeshLinkResolution = 0.2f; | |
[SerializeField] private Transform offMeshLinkRef1; | |
[SerializeField] private Transform offMeshLinkRef2; | |
[SerializeField] private int offMeshLinkArea; | |
private readonly List<PortalOffMeshLink> offMeshLinks = new List<PortalOffMeshLink>(); | |
private new Collider collider; | |
private WaitForFixedUpdate waitForFixedUpdate; | |
/// <summary> | |
/// A Vector4 math representation of the plane of the portal. Used for clipping purposes by other viewthrough portals. | |
/// </summary> | |
private Vector4 vectorPlane; | |
public Portal TargetPortal => targetPortal; | |
public Portal[] VisiblePortals => visiblePortals; | |
public bool ViewthroughVisible => viewthroughRenderer.isVisible; | |
public bool VisibleInCameraPlanes(Plane[] cameraPlanes) => | |
GeometryUtility.TestPlanesAABB(cameraPlanes, collider.bounds); | |
public bool ShouldRender(Plane[] cameraPlanes) => ViewthroughVisible && VisibleInCameraPlanes(cameraPlanes); | |
public float OffMeshLinkResolution => offMeshLinkResolution; | |
private void Awake() | |
{ | |
collider = GetComponent<Collider>(); | |
viewthroughRenderer = GetComponent<MeshRenderer>(); | |
// Generate material | |
viewthroughMaterial = new Material(viewthroughShader); | |
viewthroughRenderer.material = viewthroughMaterial; | |
// Generate bounding plane | |
var plane = new Plane(normalVisible.forward, transform.position); | |
vectorPlane = new Vector4(plane.normal.x, plane.normal.y, plane.normal.z, plane.distance); | |
// Generate OffMeshLinks | |
// TODO: Offload to editor-time | |
var directionToRef2 = offMeshLinkRef2.position - offMeshLinkRef1.position; | |
var distanceToGenerate = directionToRef2.magnitude; | |
directionToRef2.Normalize(); | |
for (var currentDistance = 0f; currentDistance <= distanceToGenerate; currentDistance += offMeshLinkResolution) | |
{ | |
var newPosition = offMeshLinkRef1.position + directionToRef2 * currentDistance; | |
var newTransform = new GameObject("[AUTO] OffMeshLink Transform").transform; | |
newTransform.parent = transform; | |
newTransform.position = newPosition; | |
newTransform.tag = "Portal OffMeshLink"; | |
offMeshLinks.Add(new PortalOffMeshLink() | |
{ | |
RefTransform = newTransform | |
}); | |
} | |
// Cache for coroutine | |
waitForFixedUpdate = new WaitForFixedUpdate(); | |
} | |
private IEnumerator Start() | |
{ | |
// Finish OffMeshLink generation | |
for (var i = 0; i < offMeshLinks.Count; i++) | |
{ | |
var offMeshLink = offMeshLinks[i]; | |
var newLink = offMeshLink.RefTransform.gameObject.AddComponent<OffMeshLink>(); | |
newLink.startTransform = offMeshLink.RefTransform; | |
newLink.endTransform = targetPortal.offMeshLinks[offMeshLinks.Count - 1 - i].RefTransform; | |
newLink.biDirectional = false; | |
newLink.costOverride = -1; // Use the default cost of the area assigned below (hopefully 0) | |
newLink.autoUpdatePositions = false; | |
newLink.activated = true; | |
newLink.area = offMeshLinkArea; | |
//offMeshLink.Link = newLink; | |
} | |
// Main loop | |
while (true) | |
{ | |
// Wait for end of fixed update frame, that means, after OnTriggerEnter so we can check for any fresh objects | |
yield return waitForFixedUpdate; | |
CheckPortalCrossing(); | |
} | |
// ReSharper disable once IteratorNeverReturns | |
} | |
private void OnTriggerEnter(Collider other) | |
{ | |
if (!teleportEnable) | |
return; | |
var portalableObject = other.GetComponent<PortalableObject>(); | |
if (!portalableObject) | |
return; | |
if (teleportObjectsTouchingPortal.Contains(portalableObject)) | |
return; | |
teleportObjectsTouchingPortal.Add(portalableObject); | |
portalableObject.OnEnterPortal(this, targetPortal); | |
// Check portal crossing immediately | |
// in case the player crosses the portal in the same frame | |
// OMITTING THIS IS WHY one-frame behind-portal flashes happen!! | |
// CheckPortalCrossing(); | |
// Using WaitForFixedUpdate instead | |
} | |
private void OnTriggerExit(Collider other) | |
{ | |
if (!teleportEnable) | |
return; | |
var portalableObject = other.GetComponent<PortalableObject>(); | |
if (!portalableObject) | |
return; | |
if (!teleportObjectsTouchingPortal.Contains(portalableObject)) | |
return; | |
teleportObjectsTouchingPortal.Remove(portalableObject); | |
portalableObject.OnExitPortal(this, targetPortal); | |
} | |
private void OnDrawGizmos() | |
{ | |
if (targetPortal) | |
{ | |
Gizmos.color = Color.red; | |
Gizmos.DrawLine(transform.position, targetPortal.transform.position); | |
} | |
Gizmos.color = Color.blue; | |
foreach (var visiblePortal in visiblePortals) | |
{ | |
Gizmos.DrawLine(transform.position, visiblePortal.transform.position); | |
} | |
} | |
private void OnDestroy() | |
{ | |
Destroy(viewthroughMaterial); | |
} | |
private void CheckPortalCrossing() | |
{ | |
if (!teleportEnable) | |
return; | |
// Clear removal queue | |
teleportObjectsToRemove.Clear(); | |
// Check every touching object | |
foreach (var portalableObject in teleportObjectsTouchingPortal) | |
{ | |
// Check if portalable object is behind the portal | |
// If so, we can assume they have crossed through the portal. | |
// Implying from this, you should not be able to touch a portal from behind. | |
// This can be changed later to allow portals to be touched from behind, but no support for now. | |
var pivot = portalableObject.Pivot ? portalableObject.Pivot : portalableObject.transform; | |
var directionToPivotFromTransform = pivot.position - transform.position; | |
directionToPivotFromTransform.Normalize(); | |
var pivotToNormalDotProduct = Vector3.Dot(directionToPivotFromTransform, normalVisible.forward); | |
if (pivotToNormalDotProduct > 0) continue; | |
// Warp object | |
portalableObject.OnWillTeleport(this, targetPortal); | |
var newPosition = TransformPositionBetweenPortals(this, targetPortal, portalableObject.transform.position); | |
var newRotation = TransformRotationBetweenPortals(this, targetPortal, portalableObject.transform.rotation); | |
if (portalableObject.AutoTeleport) | |
portalableObject.transform.SetPositionAndRotation(newPosition, newRotation); | |
else | |
portalableObject.OnManualTeleport(this, targetPortal, newPosition, newRotation); | |
portalableObject.OnHasTeleported(this, targetPortal, newPosition, newRotation); | |
// Update physics transforms after warp to force update | |
// CharacterController requires this to teleport. (Now done using PortalableObject callbacks) | |
// Physics.SyncTransforms(); | |
// Object is no longer touching this side of the portal | |
teleportObjectsToRemove.Add(portalableObject); | |
// Add to target portal so target portal will not trigger OnEnterPortal event next frame | |
// This is so that we can control the change better with OnChangePortal with per-frame accuracy | |
targetPortal.teleportObjectsTouchingPortal.Add(portalableObject); | |
portalableObject.OnChangePortal(targetPortal, this); // Teleported, they're on the other side now | |
// TODO: Check for OnExitPortal in case the player is teleported to a position that isn't touching the exit portal, | |
// TODO: thus not triggering OnExitTrigger (eg going too fast) | |
} | |
// Remove all objects queued up for removal | |
// Also causes this Portal to not trigger OnExitPortal event next frame | |
foreach (var portalableObject in teleportObjectsToRemove) | |
{ | |
teleportObjectsTouchingPortal.Remove(portalableObject); | |
} | |
} | |
/// <summary> | |
/// For use in recursive rendering, set default viewthrough texture temporarily for rendering by caller. | |
/// </summary> | |
private void ShowViewthroughDefaultTexture(out Texture originalTexture) | |
{ | |
originalTexture = viewthroughMaterial.mainTexture; | |
viewthroughMaterial.mainTexture = viewthroughDefaultTexture; | |
} | |
// TODO: Separate public function to start the recursion safely | |
/// <summary> | |
/// Renders the portal, with recursions. | |
/// The portal will follow the manually-defined portal visibility graph up to a certain number of recursions, depth-first. | |
/// It will then render the innermost portals first, followed by outer ones, all the way until the original one is rendered. | |
/// REMEMBER TO RELEASE temporaryRenderTexturePoolItem!!! Responsibility is on the CALLER so the caller can render the portal texture | |
/// before releasing it. If you no longer need any render textures (eg render done), call ReleaseAll in the pool. | |
/// </summary> | |
public void RenderViewthroughRecursive( | |
Vector3 refPosition, | |
Quaternion refRotation, | |
out PortalRenderTexturePoolManager.Item temporaryRenderTexturePoolItem, | |
out Texture originalTexture, | |
out int renderCount, | |
Camera portalCamera, | |
int currentRecursion, | |
int maxRecursions, | |
PortalOcclusionVolume currentOcclusionVolume, | |
LayerMask noCloneMask, | |
LayerMask renderCloneMask, | |
Portal portalNotRenderingClone) | |
{ | |
renderCount = 1; | |
#region Calculate For Recursion | |
Profiler.BeginSample("Calculate Portals"); | |
// Calculate virtual camera position and rotation | |
var virtualPosition = TransformPositionBetweenPortals(this, targetPortal, refPosition); | |
var virtualRotation = TransformRotationBetweenPortals(this, targetPortal, refRotation); | |
Profiler.EndSample(); | |
// Setup portal camera for calculations | |
portalCamera.transform.SetPositionAndRotation(virtualPosition, virtualRotation); | |
// Convert target portal's plane to camera space (relative to target camera) | |
// Explanation: https://danielilett.com/2019-12-18-tut4-3-matrix-matching/ | |
Profiler.BeginSample("Calculate Camera Space Portals"); | |
var targetViewThroughPlaneCameraSpace = | |
Matrix4x4.Transpose(Matrix4x4.Inverse(portalCamera.worldToCameraMatrix)) | |
* targetPortal.vectorPlane; | |
Profiler.EndSample(); | |
// Set portal camera projection matrix to clip walls between target portal and target camera | |
// Inherits main camera near/far clip plane and FOV settings | |
Profiler.BeginSample("Calculate Oblique Matrix Portals"); | |
var obliqueProjectionMatrix = GameManager.Instance.MainCamera.CalculateObliqueMatrix(targetViewThroughPlaneCameraSpace); | |
portalCamera.projectionMatrix = obliqueProjectionMatrix; | |
Profiler.EndSample(); | |
// Generate camera planes for visibility testing | |
var cameraPlanes = GeometryUtility.CalculateFrustumPlanes(portalCamera); | |
// Store visible portal resources to release and reset (see function description for details) | |
var visiblePortalResourcesList = new List<VisiblePortalResources>(); | |
#endregion | |
#region Recurse Portals | |
// Recurse if not at limit | |
var portalInCurrentVolume = false; | |
foreach (var portalInVolume in currentOcclusionVolume.Portals) | |
{ | |
if (portalInVolume == this) | |
{ | |
portalInCurrentVolume = true; | |
} | |
} | |
var localMaxRecursionsOverride = | |
portalInCurrentVolume ? targetPortal.maxRecursionsDirectOverride : targetPortal.maxRecursionsIndirectOverride; | |
if (currentRecursion < | |
(localMaxRecursionsOverride < 0 | |
? maxRecursions | |
: localMaxRecursionsOverride)) | |
{ | |
foreach (var visiblePortal in targetPortal.visiblePortals) | |
{ | |
if (!visiblePortal.ShouldRender(cameraPlanes)) | |
continue; | |
visiblePortal.RenderViewthroughRecursive( | |
virtualPosition, | |
virtualRotation, | |
out var visiblePortalTemporaryRenderTexturePoolItem, | |
out var visiblePortalOriginalTexture, | |
out var visiblePortalRenderCount, | |
portalCamera, | |
currentRecursion + 1, | |
maxRecursions, | |
currentOcclusionVolume, | |
noCloneMask, | |
renderCloneMask, | |
portalNotRenderingClone); | |
visiblePortalResourcesList.Add(new VisiblePortalResources() | |
{ | |
OriginalTexture = visiblePortalOriginalTexture, | |
PortalRenderTexturePoolItem = visiblePortalTemporaryRenderTexturePoolItem, | |
VisiblePortal = visiblePortal | |
}); | |
renderCount += visiblePortalRenderCount; | |
} | |
} | |
else | |
{ | |
foreach (var visiblePortal in targetPortal.visiblePortals) | |
{ | |
if (!visiblePortal.ShouldRender(cameraPlanes)) | |
continue; | |
visiblePortal.ShowViewthroughDefaultTexture(out var visiblePortalOriginalTexture); | |
visiblePortalResourcesList.Add(new VisiblePortalResources() | |
{ | |
OriginalTexture = visiblePortalOriginalTexture, | |
VisiblePortal = visiblePortal | |
}); | |
} | |
} | |
#endregion | |
#region Render Portal | |
Profiler.BeginSample("Render Portals"); | |
// Get new temporary render texture and set to portal's material | |
// Will be released by CALLER, not by this function. This is so that the caller can use the render texture | |
// for their own purposes, such as a Render() or a main camera render, before releasing it. | |
Profiler.BeginSample("Allocate Texture Portals"); | |
temporaryRenderTexturePoolItem = PortalRenderTexturePoolManager.Instance.Allocate(); | |
Profiler.EndSample(); | |
// Use portal camera | |
Profiler.BeginSample("Setup Camera Portals"); | |
portalCamera.targetTexture = temporaryRenderTexturePoolItem.Resource; | |
portalCamera.transform.SetPositionAndRotation(virtualPosition, virtualRotation); | |
portalCamera.projectionMatrix = obliqueProjectionMatrix; | |
portalCamera.cullingMask = (currentRecursion == 0 && this == portalNotRenderingClone | |
? noCloneMask | |
: renderCloneMask).value; | |
Profiler.EndSample(); | |
// Render portal camera to target texture | |
Profiler.BeginSample("Actually Render Portals"); | |
portalCamera.Render(); | |
Profiler.EndSample(); | |
Profiler.EndSample(); | |
#endregion | |
#region Release and Restore | |
Profiler.BeginSample("Release and Restore Portals"); | |
foreach (var resources in visiblePortalResourcesList) | |
{ | |
// Reset to original texture | |
// So that it will remain correct if the visible portal is still expecting to be rendered | |
// on another camera but has already rendered its texture. Originally the texture may be overriden by other renders. | |
resources.VisiblePortal.viewthroughMaterial.mainTexture = resources.OriginalTexture; | |
// Release temp render texture | |
if (resources.PortalRenderTexturePoolItem != null) | |
{ | |
PortalRenderTexturePoolManager.Instance.Release(resources.PortalRenderTexturePoolItem); | |
} | |
} | |
Profiler.EndSample(); | |
#endregion | |
#region Present Portal | |
Profiler.BeginSample("Present Portals"); | |
// Must be after camera render, in case it renders itself (in which the texture must not be replaced before rendering itself) | |
// Must be after restore, in case it restores its own old texture (in which the new texture must take precedence) | |
originalTexture = viewthroughMaterial.mainTexture; | |
viewthroughMaterial.mainTexture = temporaryRenderTexturePoolItem.Resource; | |
Profiler.EndSample(); | |
#endregion | |
} | |
public static Vector3 TransformPositionBetweenPortals(Portal sender, Portal target, Vector3 position) | |
{ | |
return | |
target.normalInvisible.TransformPoint( | |
sender.normalVisible.InverseTransformPoint(position)); | |
} | |
public static Vector3 TransformVectorBetweenPortals(Portal sender, Portal target, Vector3 direction) | |
{ | |
return | |
target.normalInvisible.TransformVector( | |
sender.normalVisible.InverseTransformVector(direction)); | |
} | |
public static Vector3 TransformDirectionBetweenPortals(Portal sender, Portal target, Vector3 direction) | |
{ | |
return | |
target.normalInvisible.TransformDirection( | |
sender.normalVisible.InverseTransformDirection(direction)); | |
} | |
public static Quaternion TransformRotationBetweenPortals(Portal sender, Portal target, Quaternion rotation) | |
{ | |
return | |
target.normalInvisible.rotation * | |
Quaternion.Inverse(sender.normalVisible.rotation) * | |
rotation; | |
} | |
/// <summary> | |
/// Raycasts recursively through portals. | |
/// </summary> | |
/// <param name="position">Origin position</param> | |
/// <param name="direction">Direction of ray</param> | |
/// <param name="customPreRaycast">(Current recursion) => Any preparation before the current raycast</param> | |
/// <param name="layerMaskSelector">(Last source portal traversed, Current recursion) => LayerMask to use</param> | |
/// <param name="maxRecursions">Maximum recursions (if max recursions reached, treated as if no hit)</param> | |
/// <param name="hitInfo">Returned final raycast hit info</param> | |
/// <param name="finalDirection">Returned direction of the final raycast</param> | |
/// <returns>Was anything hit?</returns> | |
public static bool RaycastRecursive(Vector3 position, | |
Vector3 direction, | |
Action<int> customPreRaycast, | |
Func<Portal, int, LayerMask> layerMaskSelector, | |
int maxRecursions, | |
out RaycastHit hitInfo, | |
out Vector3 finalDirection) | |
{ | |
return RaycastRecursiveInternal(position, | |
direction, | |
customPreRaycast, | |
layerMaskSelector, | |
maxRecursions, | |
out hitInfo, | |
out finalDirection, | |
0, | |
null, | |
null); | |
} | |
private static bool RaycastRecursiveInternal(Vector3 position, | |
Vector3 direction, | |
Action<int> customPreRaycast, | |
Func<Portal, int, LayerMask> layerMaskSelector, | |
int maxRecursions, | |
out RaycastHit hitInfo, | |
out Vector3 finalDirection, | |
int currentRecursion, | |
GameObject ignoreObject, | |
Portal lastTraversedPortal) | |
{ | |
finalDirection = direction; | |
// Ignore a specific object when raycasting. | |
// Useful for preventing a raycast through a portal from hitting the target portal from the back, | |
// which makes a raycast unable to go through a portal since it'll just be absorbed by the target portal's trigger. | |
var ignoreObjectOriginalLayer = 0; | |
if (ignoreObject) | |
{ | |
ignoreObjectOriginalLayer = ignoreObject.layer; | |
ignoreObject.layer = 2; // Ignore raycast | |
} | |
// Custom pre-raycast | |
customPreRaycast?.Invoke(currentRecursion); | |
// Shoot raycast | |
var raycastHitSomething = Physics.Raycast( | |
position, | |
direction, | |
out var hit, | |
Mathf.Infinity, | |
layerMaskSelector(lastTraversedPortal, currentRecursion)); // Clamp to max array length | |
// Reset ignore | |
if (ignoreObject) | |
ignoreObject.layer = ignoreObjectOriginalLayer; | |
// If no objects are hit, the recursion ends here, with no effect | |
if (!raycastHitSomething) | |
{ | |
hitInfo = new RaycastHit(); // Dummy | |
return false; | |
} | |
var portal = PortalManager.Instance.GetPortalFromGameObjectCached(hit.collider.gameObject); | |
if (portal) | |
{ | |
// If the object hit is a portal, recurse, unless we are already at max recursions | |
if (currentRecursion >= maxRecursions) | |
{ | |
hitInfo = new RaycastHit(); // Dummy | |
return false; | |
} | |
// Continue going down the rabbit hole... | |
//Debug.Log($"Shooting recursive, #{currentRecursion+1}"); | |
return RaycastRecursiveInternal( | |
TransformPositionBetweenPortals(portal, portal.TargetPortal, hit.point), | |
TransformDirectionBetweenPortals(portal, portal.TargetPortal, direction), | |
customPreRaycast, | |
layerMaskSelector, | |
maxRecursions, | |
out hitInfo, | |
out finalDirection, | |
currentRecursion + 1, | |
portal.TargetPortal.gameObject, | |
portal); | |
} | |
else | |
{ | |
hitInfo = hit; | |
return true; | |
} | |
} | |
private struct VisiblePortalResources | |
{ | |
public Portal VisiblePortal; | |
public PortalRenderTexturePoolManager.Item PortalRenderTexturePoolItem; | |
public Texture OriginalTexture; | |
} | |
private struct PortalOffMeshLink | |
{ | |
public Transform RefTransform; | |
//public OffMeshLink Link; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment