mixedreality/com.microsoft.mixedreality..../Core/Utilities/CameraFOVChecker.cs

109 lines
4.9 KiB
C#

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Microsoft.MixedReality.Toolkit
{
/// <summary>
/// Camera extension methods to test if colliders are within camera's FOV. Uses
/// caching to improve performance and ensure values are only computed once per frame
/// </summary>
public static class CameraFOVChecker
{
// Help to clear caches when new frame runs
private static int inFOVLastCalculatedFrame = -1;
#if !NETFX_CORE
// Map from grabbable => is the grabbable in FOV for this frame. Cleared every frame
private static Dictionary<ValueTuple<Collider, Camera>, bool> inFOVColliderCache = new Dictionary<ValueTuple<Collider, Camera>, bool>();
#else
private static Dictionary<Tuple<Collider, Camera>, bool> inFOVColliderCache = new Dictionary<Tuple<Collider, Camera>, bool>();
#endif
// List of corners shared across all sphere pointer query instances --
// used to store list of corners for a bounds. Shared and static
// to avoid allocating memory each frame
private static List<Vector3> inFOVBoundsCornerPoints = new List<Vector3>();
/// <summary>
/// Returns true if a collider's bounds is within the camera FOV.
/// Utilizes a cache to test if this collider has been seen before and returns current frame's calculated result.
/// NOTE: This is a 'loose' FOV check -- it can return true in cases when the collider is actually not in the FOV
/// because it does an axis-aligned check when testing for large colliders. So, if the axis aligned bounds are in the bounds of the camera, it will return true.
/// </summary>
/// <param name="myCollider">The collider to test</param>
public static bool IsInFOVCached(this Camera cam, Collider myCollider)
{
// if the collider's size is zero, it is not visible. Return false.
if (myCollider.bounds.size == Vector3.zero || myCollider.transform.localScale == Vector3.zero)
{
return false;
}
#if !NETFX_CORE
ValueTuple<Collider, Camera> cameraColliderPair = ValueTuple.Create(myCollider, cam);
#else
Tuple<Collider, Camera> cameraColliderPair = Tuple.Create(myCollider, cam);
#endif
bool result;
if (inFOVLastCalculatedFrame != Time.frameCount)
{
inFOVColliderCache.Clear();
inFOVLastCalculatedFrame = Time.frameCount;
}
else if (inFOVColliderCache.TryGetValue(cameraColliderPair, out result))
{
return result;
}
inFOVBoundsCornerPoints.Clear();
BoundsExtensions.GetColliderBoundsPoints(myCollider, inFOVBoundsCornerPoints, 0);
float xMin = float.MaxValue, yMin = float.MaxValue, zMin = float.MaxValue;
float xMax = float.MinValue, yMax = float.MinValue, zMax = float.MinValue;
for (int i = 0; i < inFOVBoundsCornerPoints.Count; i++)
{
var corner = inFOVBoundsCornerPoints[i];
Vector3 screenPoint = cam.WorldToViewportPoint(corner);
bool isInFOV = screenPoint.z >= 0 && screenPoint.z <= cam.farClipPlane
&& screenPoint.x >= 0 && screenPoint.x <= 1
&& screenPoint.y >= 0 && screenPoint.y <= 1;
if (isInFOV)
{
inFOVColliderCache.Add(cameraColliderPair, true);
return true;
}
// if the point is behind the camera, the x and y viewport positions are negated
var zViewport = screenPoint.z;
var xViewport = zViewport >= 0 ? screenPoint.x : -screenPoint.x;
var yViewport = zViewport >= 0 ? screenPoint.y : -screenPoint.y;
xMin = Mathf.Min(xMin, xViewport);
yMin = Mathf.Min(yMin, yViewport);
zMin = Mathf.Min(zMin, zViewport);
xMax = Mathf.Max(xMax, xViewport);
yMax = Mathf.Max(yMax, yViewport);
zMax = Mathf.Max(zMax, zViewport);
}
// Check that collider is visible even if all corners are not visible
// such as when having a large collider
result =
zMax > 0 // Front of collider is in front of the camera.
&& zMin < cam.farClipPlane // Back of collider is not too far away.
&& xMin < 1 // Left edge is not too far to the right.
&& xMax > 0 // Right edge is not too far to the left.
&& yMin < 1 // Bottom edge is not too high.
&& yMax > 0; // Top edge is not too low.
inFOVColliderCache.Add(cameraColliderPair, result);
return result;
}
}
}