// // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. // using System; using System.Collections.Generic; using UnityEngine; namespace HoloToolkit.Unity.UX { /// <summary> /// Base class for bounding box objects /// </summary> public class BoundingBox : MonoBehaviour { public enum BoundsCalculationMethodEnum { MeshFilterBounds, // Better for flattened objects - this mode also treats RectTransforms as quad meshes RendererBounds, // Better for objects with non-mesh renderers Colliders, // Better if you want precise control Default, // Use the default method (RendererBounds) } public enum FlattenModeEnum { DoNotFlatten, // Always use XYZ axis FlattenX, // Flatten the X axis FlattenY, // Flatten the Y axis FlattenZ, // Flatten the Z axis FlattenAuto, // Flatten the smallest relative axis if it falls below threshold } /// <summary> /// The target object. GameObject that the BoundingBox surrounds. /// </summary> [Header("Objects")] [Tooltip("The target object")] [SerializeField] protected GameObject target; /// <summary> /// "The transform used to scale the bounding box (will be auto-generated) /// </summary> [Tooltip("The transform used to scale the bounding box (will be auto-generated)")] [SerializeField] protected Transform scaleTransform = null; /// <summary> /// Flattening behavior setting. Which axis will be considered flat? /// </summary> [Header("Flattening & Padding")] [Tooltip("Flattening behavior setting.")] [SerializeField] protected FlattenModeEnum flattenPreference = FlattenModeEnum.FlattenAuto; /// <summary> /// Public property describing axis intended to be regarded as flat. /// </summary> public virtual FlattenModeEnum FlattenPreference { get { return flattenPreference; } set { flattenPreference = value; } } /// <summary> /// The relative % size of an axis must meet before being auto-flattened. /// </summary> [Tooltip("The relative % size of an axis must meet before being auto-flattened")] [SerializeField] protected float flattenAxisThreshold = 0.025f; /// <summary> /// The relative % size of a flattened axis /// </summary> [Tooltip("The relative % size of a flattened axis")] [SerializeField] protected float flattenedAxisThickness = 0.01f; /// <summary> /// How much to pad the scale of the box to fit around objects (as % of largest dimension) /// </summary> [Tooltip("How much to pad the scale of the box to fit around objects (as % of largest dimension)")] [SerializeField] protected float scalePadding = 0.05f; /// <summary> /// How much to pad the scale of the box on an axis that's flattened /// </summary> [Tooltip("How much to pad the scale of the box on an axis that's flattened")] [SerializeField] protected float flattenedScalePadding = 0f; /// <summary> /// Method used to calculate the bounds of the object. /// </summary> [Header("Bounds Calculation")] [Tooltip("Method used to calculate the bounds of the object.")] [SerializeField] protected BoundsCalculationMethodEnum boundsCalculationMethod = BoundsCalculationMethodEnum.MeshFilterBounds; /// <summary> /// Any renderers on this layer will be ignored when calculating object bounds /// </summary> [Tooltip("Any renderers on this layer will be ignored when calculating object bounds")] [SerializeField] protected LayerMask ignoreLayers = (1 << 2); // Ignore Raycast Layer protected Vector3 targetBoundsWorldCenter = Vector3.zero; protected Vector3 targetBoundsLocalScale = Vector3.zero; protected Bounds localTargetBounds = new Bounds(); protected List<Vector3> boundsPoints = new List<Vector3>(); protected FlattenModeEnum flattenedAxis = FlattenModeEnum.DoNotFlatten; protected bool isVisible = true; protected Renderer rendererForVisibility; /// <summary> /// Event Handler- called when the FlattenedAxis property is changed. /// </summary> public Action OnFlattenedAxisChange; /// <summary> /// instruction to boundingBox which determines which of several methods /// to use to calculate the bounds of the gameObject it surrounds. /// </summary> public virtual BoundsCalculationMethodEnum BoundsCalculationMethod { get { return boundsCalculationMethod; } set { boundsCalculationMethod = value; } } /// <summary> /// The target object being manipulated /// </summary> public virtual GameObject Target { get { return target; } set { if (target != value) { // Send a message to the new / old targets // TODO send OnTargetSelected / Deselected events target = value; } if (!isActiveAndEnabled) { return; } if (target != null) { CreateTransforms(); // Set our transforms to the target immediately RefreshTargetBounds(); } } } /// <summary> /// The world-space center of the target object's bounds /// </summary> public Vector3 TargetBoundsCenter { get { return targetBoundsWorldCenter; } } /// <summary> /// The world scale of the target object's bounds /// Use this if you want to get the size of the target object /// </summary> public Vector3 TargetBoundsScale { get { return scaleTransform.localScale; } } /// <summary> /// The local scale of the target object's bounds /// </summary> public Vector3 TargetBoundsLocalScale { get { return targetBoundsLocalScale; } } /// <summary> /// Sets the bounding box invisible while not interrupting /// the computation of bounds points. /// </summary> public bool IsVisible { get { return isVisible; } set { if (rendererForVisibility == null) { Transform scale = transform.GetChild(0); Transform rig = scale.GetChild(0); GameObject rigobject = rig.gameObject; rendererForVisibility = rigobject.gameObject.GetComponent<Renderer>(); } rendererForVisibility.enabled = value; isVisible = value; } } /// <summary> /// The current flattened axis, if any /// </summary> public virtual FlattenModeEnum FlattenedAxis { get { return flattenedAxis; } protected set { if (flattenedAxis != value) { flattenedAxis = value; if (OnFlattenedAxisChange != null) { OnFlattenedAxisChange(); } } } } #region /// <summary> /// Override so we're not overwhelmed by button gizmos /// </summary> #if UNITY_EDITOR protected virtual void OnDrawGizmos() { // nothing if (!Application.isPlaying) { // Do this here to ensure continuous updates in editor CreateTransforms(); RefreshTargetBounds(); UpdateScaleTransform(); } if (target != null) { foreach (Vector3 point in boundsPoints) { Gizmos.DrawSphere(target.transform.TransformPoint(point), 0.01f); } } } #endif /// <summary> /// private Update function provided by MonoBehaviour /// </summary> protected virtual void Update() { CreateTransforms(); RefreshTargetBounds(); UpdateScaleTransform(); } /// <summary> /// Method which instantiates new Transforms /// if they have not been declared earlier. /// Method assigns the variable scaleTransform /// which represents the transform /// to which the boundingbox aligns itself. It can be assigned /// directly if 'this' already has a Transform. If not /// it gets set by instantiating a new Transform. /// Once it is set- the new transform becomes the parent of scaleTransform. /// </summary> protected virtual void CreateTransforms() { if (scaleTransform == null) { scaleTransform = transform; } if (scaleTransform == null) { scaleTransform = new GameObject("Scale").transform; } scaleTransform.parent = transform; } /// <summary> /// re-calculates the Bounding box extrema (corners) /// of the axis aligned cube that bounds the Target gameObject. /// This method takes into account flattening. /// </summary> protected virtual void RefreshTargetBounds() { if (target == null) { targetBoundsWorldCenter = Vector3.zero; targetBoundsLocalScale = Vector3.one; return; } // Get the new target bounds boundsPoints.Clear(); switch (boundsCalculationMethod) { case BoundsCalculationMethodEnum.RendererBounds: default: GetRenderBoundsPoints(target, boundsPoints, ignoreLayers); break; case BoundsCalculationMethodEnum.Colliders: GetColliderBoundsPoints(target, boundsPoints, ignoreLayers); break; case BoundsCalculationMethodEnum.MeshFilterBounds: GetMeshFilterBoundsPoints(target, boundsPoints, ignoreLayers); break; } if (boundsPoints.Count > 0) { // We now have a list of all points in world space // Translate them all to local space for (int i = 0; i < boundsPoints.Count; i++) { boundsPoints[i] = target.transform.InverseTransformPoint(boundsPoints[i]); } // Encapsulate the points with a local bounds localTargetBounds.center = boundsPoints[0]; localTargetBounds.size = Vector3.zero; foreach (Vector3 point in boundsPoints) { localTargetBounds.Encapsulate(point); } } // Store the world center of the target bb targetBoundsWorldCenter = target.transform.TransformPoint(localTargetBounds.center); // Store the local scale of the target bb targetBoundsLocalScale = localTargetBounds.size; targetBoundsLocalScale.Scale(target.transform.localScale); // Now check our flatten behavior UpdateFlattenedAxis(); } /// <summary> /// recomputes flattening if axis to be flattened has changed. /// </summary> protected virtual void UpdateFlattenedAxis() { // Find the maximum size of the new bounds float maxAxisThickness = Mathf.Max(Mathf.Max(targetBoundsLocalScale.x, targetBoundsLocalScale.y), targetBoundsLocalScale.z); FlattenModeEnum newFlattenedAxis = FlattenModeEnum.DoNotFlatten; switch (flattenPreference) { case FlattenModeEnum.DoNotFlatten: // Do nothing break; case FlattenModeEnum.FlattenAuto: // Flattening order of preference - z, y, x if (Mathf.Abs(targetBoundsLocalScale.z / maxAxisThickness) < flattenAxisThreshold) { newFlattenedAxis = FlattenModeEnum.FlattenZ; targetBoundsLocalScale.z = flattenedAxisThickness * maxAxisThickness; } else if (Mathf.Abs(targetBoundsLocalScale.y / maxAxisThickness) < flattenAxisThreshold) { newFlattenedAxis = FlattenModeEnum.FlattenY; targetBoundsLocalScale.y = flattenedAxisThickness * maxAxisThickness; } else if (Mathf.Abs(targetBoundsLocalScale.x / maxAxisThickness) < flattenAxisThreshold) { newFlattenedAxis = FlattenModeEnum.FlattenX; targetBoundsLocalScale.x = flattenedAxisThickness * maxAxisThickness; } break; case FlattenModeEnum.FlattenX: newFlattenedAxis = FlattenModeEnum.FlattenX; targetBoundsLocalScale.x = flattenedAxisThickness * maxAxisThickness; break; case FlattenModeEnum.FlattenY: newFlattenedAxis = FlattenModeEnum.FlattenY; targetBoundsLocalScale.y = flattenedAxisThickness * maxAxisThickness; break; case FlattenModeEnum.FlattenZ: newFlattenedAxis = FlattenModeEnum.FlattenZ; targetBoundsLocalScale.z = flattenedAxisThickness * maxAxisThickness; break; } FlattenedAxis = newFlattenedAxis; } /// <summary> /// Updates bounding box to match target position etc /// </summary> protected virtual void UpdateScaleTransform() { // If we don't have a target, nothing to do here if (target == null) { return; } // Get position of object based on renderers transform.position = targetBoundsWorldCenter; Vector3 scale = targetBoundsLocalScale; // Use absolute value when determining smallest axis // so we don't get fooled by inverted scales float largestDimension = Mathf.Max(Mathf.Max( Mathf.Abs(scale.x), Mathf.Abs(scale.y)), Mathf.Abs(scale.z)); switch (flattenedAxis) { case BoundingBox.FlattenModeEnum.DoNotFlatten: default: scale.x += (largestDimension * scalePadding); scale.y += (largestDimension * scalePadding); scale.z += (largestDimension * scalePadding); break; case BoundingBox.FlattenModeEnum.FlattenX: scale.x += (largestDimension * flattenedScalePadding); scale.y += (largestDimension * scalePadding); scale.z += (largestDimension * scalePadding); break; case BoundingBox.FlattenModeEnum.FlattenY: scale.x += (largestDimension * scalePadding); scale.y += (largestDimension * flattenedScalePadding); scale.z += (largestDimension * scalePadding); break; case BoundingBox.FlattenModeEnum.FlattenZ: scale.x += (largestDimension * scalePadding); scale.y += (largestDimension * scalePadding); scale.z += (largestDimension * flattenedScalePadding); break; } scaleTransform.localScale = scale; Vector3 rotation = target.transform.eulerAngles; transform.eulerAngles = rotation; } #endregion #region static utility functions /// <summary> /// Method to get bounding box points using Collider method. /// </summary> /// <param name="target">gameObject that boundingBox bounds.</param> /// <param name="boundsPoints">array reference that gets filled with points</param> /// <param name="ignoreLayers">layerMask to simplify search</param> public static void GetColliderBoundsPoints(GameObject target, List<Vector3> boundsPoints, LayerMask ignoreLayers) { Collider[] colliders = target.GetComponentsInChildren<Collider>(); for (int i = 0; i < colliders.Length; i++) { if (ignoreLayers == (1 << colliders[i].gameObject.layer | ignoreLayers)) { continue; } switch (colliders[i].GetType().Name) { case "SphereCollider": SphereCollider sc = colliders[i] as SphereCollider; Bounds sphereBounds = new Bounds(sc.center, Vector3.one * sc.radius * 2); sphereBounds.GetFacePositions(sc.transform, ref corners); boundsPoints.AddRange(corners); break; case "BoxCollider": BoxCollider bc = colliders[i] as BoxCollider; Bounds boxBounds = new Bounds(bc.center, bc.size); boxBounds.GetCornerPositions(bc.transform, ref corners); boundsPoints.AddRange(corners); break; case "MeshCollider": MeshCollider mc = colliders[i] as MeshCollider; Bounds meshBounds = mc.sharedMesh.bounds; meshBounds.GetCornerPositions(mc.transform, ref corners); boundsPoints.AddRange(corners); break; case "CapsuleCollider": CapsuleCollider cc = colliders[i] as CapsuleCollider; Bounds capsuleBounds = new Bounds(cc.center, Vector3.zero); switch (cc.direction) { case 0: capsuleBounds.size = new Vector3(cc.height, cc.radius * 2, cc.radius * 2); break; case 1: capsuleBounds.size = new Vector3(cc.radius * 2, cc.height, cc.radius * 2); break; case 2: capsuleBounds.size = new Vector3(cc.radius * 2, cc.radius * 2, cc.height); break; } capsuleBounds.GetFacePositions(cc.transform, ref corners); boundsPoints.AddRange(corners); break; default: break; } } } /// <summary> /// GetRenderBoundsPoints gets bounding box points using RenderBounds /// </summary> /// <param name="target">gameObject that boundingbox bounds</param> /// <param name="boundsPoints">array reference that gets filled with points</param> /// <param name="ignoreLayers">layerMask to simplify search</param> public static void GetRenderBoundsPoints(GameObject target, List<Vector3> boundsPoints, LayerMask ignoreLayers) { Renderer[] renderers = target.GetComponentsInChildren<Renderer>(); for (int i = 0; i < renderers.Length; ++i) { var rendererObj = renderers[i]; if (ignoreLayers == (1 << rendererObj.gameObject.layer | ignoreLayers)) { continue; } rendererObj.bounds.GetCornerPositionsFromRendererBounds(ref corners); boundsPoints.AddRange(corners); } } /// <summary> /// GetMeshFilterBoundsPoints - gets boundingbox points using MeshFilter method. /// </summary> /// <param name="target">gameObject that boundingbox bounds</param> /// <param name="boundsPoints">array reference that gets filled with points</param> /// <param name="ignoreLayers">layerMask to simplify search</param> public static void GetMeshFilterBoundsPoints(GameObject target, List<Vector3> boundsPoints, LayerMask ignoreLayers) { MeshFilter[] meshFilters = target.GetComponentsInChildren<MeshFilter>(); for (int i = 0; i < meshFilters.Length; i++) { var meshFilterObj = meshFilters[i]; if (ignoreLayers == (1 << meshFilterObj.gameObject.layer | ignoreLayers)) { continue; } Bounds meshBounds = meshFilterObj.sharedMesh.bounds; meshBounds.GetCornerPositions(meshFilterObj.transform, ref corners); boundsPoints.AddRange(corners); } RectTransform[] rectTransforms = target.GetComponentsInChildren<RectTransform>(); for (int i = 0; i < rectTransforms.Length; i++) { rectTransforms[i].GetWorldCorners(rectTransformCorners); boundsPoints.AddRange(rectTransformCorners); } } private static Vector3[] corners = null; private static Vector3[] rectTransformCorners = new Vector3[4]; #endregion } }