using UnityEngine;
using System;
using UnityEngine.Serialization;
using System.Collections.Generic;
namespace Unity.Cinemachine
{
///
/// Interface representing something that can be used as a vcam target.
/// It has a transform, a bounding box, and a bounding sphere.
///
public interface ICinemachineTargetGroup
{
///
/// Returns true if object has not been deleted.
///
bool IsValid { get; }
///
/// Get the MonoBehaviour's Transform
///
Transform Transform { get; }
///
/// The axis-aligned bounding box of the group, computed using the targets positions and radii
///
Bounds BoundingBox { get; }
///
/// The bounding sphere of the group, computed using the targets positions and radii
///
BoundingSphere Sphere { get; }
///
/// Returns true if the group has no non-zero-weight members
///
bool IsEmpty { get; }
/// The axis-aligned bounding box of the group, in a specific reference frame
/// The frame of reference in which to compute the bounding box
/// If true, members behind the observer (negative z) will be included
/// The axis-aligned bounding box of the group, in the desired frame of reference
Bounds GetViewSpaceBoundingBox(Matrix4x4 observer, bool includeBehind);
///
/// Get the local-space angular bounds of the group, from a specific point of view.
/// Also returns the z depth range of the members.
/// Members behind the observer (negative z) will be ignored.
///
/// Point of view from which to calculate, and in whose
/// space the return values are
/// The lower bound of the screen angles of the members (degrees)
/// The upper bound of the screen angles of the members (degrees)
/// The min and max depth values of the members, relative to the observer
void GetViewSpaceAngularBounds(
Matrix4x4 observer, out Vector2 minAngles, out Vector2 maxAngles, out Vector2 zRange);
}
/// Defines a group of target objects, each with a radius and a weight.
/// The weight is used when calculating the average position of the target group.
/// Higher-weighted members of the group will count more.
/// The bounding box is calculated by taking the member positions, weight,
/// and radii into account.
///
[AddComponentMenu("Cinemachine/Helpers/Cinemachine Target Group")]
[SaveDuringPlay]
[ExecuteAlways]
[DisallowMultipleComponent]
[HelpURL(Documentation.BaseURL + "manual/CinemachineTargetGroup.html")]
public class CinemachineTargetGroup : MonoBehaviour, ICinemachineTargetGroup
{
/// Holds the information that represents a member of the group
[Serializable] public class Target
{
/// The target object. This object's position and rotation will contribute to the
/// group's average position and rotation, in accordance with its weight
[Tooltip("The target object. This object's position and rotation will contribute to the "
+ "group's average position and rotation, in accordance with its weight")]
[FormerlySerializedAs("target")]
public Transform Object;
/// How much weight to give the target when averaging. Cannot be negative
[Tooltip("How much weight to give the target when averaging. Cannot be negative")]
[FormerlySerializedAs("weight")]
public float Weight = 1;
/// The radius of the target, used for calculating the bounding box. Cannot be negative
[Tooltip("The radius of the target, used for calculating the bounding box. Cannot be negative")]
[FormerlySerializedAs("radius")]
public float Radius = 0.5f;
}
/// How the group's position is calculated
public enum PositionModes
{
///Group position will be the center of the group's axis-aligned bounding box
GroupCenter,
/// Group position will be the weighted average of the positions of the members
GroupAverage
}
/// How the group's position is calculated
[Tooltip("How the group's position is calculated. Select GroupCenter for the center of the bounding box, "
+ "and GroupAverage for a weighted average of the positions of the members.")]
[FormerlySerializedAs("m_PositionMode")]
public PositionModes PositionMode = PositionModes.GroupCenter;
/// How the group's orientation is calculated
public enum RotationModes
{
/// Manually set in the group's transform
Manual,
/// Weighted average of the orientation of its members.
GroupAverage
}
/// How the group's orientation is calculated
[Tooltip("How the group's rotation is calculated. Select Manual to use the value in the group's transform, "
+ "and GroupAverage for a weighted average of the orientations of the members.")]
[FormerlySerializedAs("m_RotationMode")]
public RotationModes RotationMode = RotationModes.Manual;
/// This enum defines the options available for the update method.
public enum UpdateMethods
{
/// Updated in normal MonoBehaviour Update.
Update,
/// Updated in sync with the Physics module, in FixedUpdate
FixedUpdate,
/// Updated in MonoBehaviour LateUpdate.
LateUpdate
};
/// When to update the group's transform based on the position of the group members
[Tooltip("When to update the group's transform based on the position of the group members")]
[FormerlySerializedAs("m_UpdateMethod")]
public UpdateMethods UpdateMethod = UpdateMethods.LateUpdate;
/// The target objects, together with their weights and radii, that will
/// contribute to the group's average position, orientation, and size
[NoSaveDuringPlay]
[Tooltip("The target objects, together with their weights and radii, that will contribute to the "
+ "group's average position, orientation, and size.")]
public List Targets = new ();
float m_MaxWeight;
Vector3 m_AveragePos;
Bounds m_BoundingBox;
BoundingSphere m_BoundingSphere;
int m_LastUpdateFrame = -1;
// Caches of valid members so we don't keep checking activeInHierarchy
List m_ValidMembers = new ();
List m_MemberValidity = new ();
void OnValidate()
{
var count = Targets.Count;
for (int i = 0; i < count; ++i)
{
Targets[i].Weight = Mathf.Max(0, Targets[i].Weight);
Targets[i].Radius = Mathf.Max(0, Targets[i].Radius);
}
}
void Reset()
{
PositionMode = PositionModes.GroupCenter;
RotationMode = RotationModes.Manual;
UpdateMethod = UpdateMethods.LateUpdate;
Targets.Clear();
}
//============================================
// Legacy support
[SerializeField, FormerlySerializedAs("m_Targets")]
Target[] m_LegacyTargets;
void Awake()
{
if (m_LegacyTargets != null && m_LegacyTargets.Length > 0)
Targets.AddRange(m_LegacyTargets);
m_LegacyTargets = null;
}
/// Obsolete Targets
[Obsolete("m_Targets is obsolete. Please use Targets instead")]
public Target[] m_Targets
{
get => Targets.ToArray();
set { Targets.Clear(); Targets.AddRange(value); }
}
//============================================
///
/// Get the MonoBehaviour's Transform
///
public Transform Transform => transform;
///
public bool IsValid => this != null;
/// The axis-aligned bounding box of the group, computed using the
/// targets positions and radii
public Bounds BoundingBox
{
get
{
if (m_LastUpdateFrame != Time.frameCount)
DoUpdate();
return m_BoundingBox;
}
private set => m_BoundingBox = value;
}
/// The bounding sphere of the group, computed using the
/// targets positions and radii
public BoundingSphere Sphere
{
get
{
if (m_LastUpdateFrame != Time.frameCount)
DoUpdate();
return m_BoundingSphere;
}
private set => m_BoundingSphere = value;
}
/// Return true if there are no members with weight > 0. This returns the
/// cached member state and is only valid after a call to DoUpdate(). If members
/// are added or removed after that call, this will not necessarily return
/// correct information before the next update.
public bool IsEmpty
{
get
{
if (m_LastUpdateFrame != Time.frameCount)
DoUpdate();
return m_ValidMembers.Count == 0;
}
}
/// Add a member to the group
/// The member to add
/// The new member's weight
/// The new member's radius
public void AddMember(Transform t, float weight, float radius)
{
Targets.Add(new Target { Object = t, Weight = weight, Radius = radius });
}
/// Remove a member from the group
/// The member to remove
public void RemoveMember(Transform t)
{
int index = FindMember(t);
if (index >= 0)
Targets.RemoveAt(index);
}
/// Locate a member's index in the group.
/// The member to find
/// Member index, or -1 if not a member
public int FindMember(Transform t)
{
var count = Targets.Count;
for (int i = 0; i < count; ++i)
if (Targets[i].Object == t)
return i;
return -1;
}
///
/// Get the bounding sphere of a group member, with the weight taken into account.
/// As the member's weight goes to 0, the position interpolates to the group average position.
/// Note that this result is only valid after DoUpdate has been called. If members
/// are added or removed after that call or change their weights or active state,
/// this will not necessarily return correct information before the next update.
///
/// Member index
/// The weighted bounding sphere
public BoundingSphere GetWeightedBoundsForMember(int index)
{
if (m_LastUpdateFrame != Time.frameCount)
DoUpdate();
if (!IndexIsValid(index) || !m_MemberValidity[index])
return Sphere;
return WeightedMemberBoundsForValidMember(Targets[index], m_AveragePos, m_MaxWeight);
}
/// The axis-aligned bounding box of the group, in a specific reference frame.
/// Note that this result is only valid after DoUpdate has been called. If members
/// are added or removed after that call or change their weights or active state,
/// this will not necessarily return correct information before the next update.
/// The frame of reference in which to compute the bounding box
/// If true, members behind the observer (negative z) will be included
/// The axis-aligned bounding box of the group, in the desired frame of reference
public Bounds GetViewSpaceBoundingBox(Matrix4x4 observer, bool includeBehind)
{
if (m_LastUpdateFrame != Time.frameCount)
DoUpdate();
var inverseView = observer;
if (!Matrix4x4.Inverse3DAffine(observer, ref inverseView))
inverseView = observer.inverse;
var b = new Bounds(inverseView.MultiplyPoint3x4(m_AveragePos), Vector3.zero);
if (CachedCountIsValid)
{
bool gotOne = false;
var unit = 2 * Vector3.one;
var count = m_ValidMembers.Count;
for (int i = 0; i < count; ++i)
{
var s = WeightedMemberBoundsForValidMember(Targets[m_ValidMembers[i]], m_AveragePos, m_MaxWeight);
s.position = inverseView.MultiplyPoint3x4(s.position);
if (s.position.z > 0 || includeBehind)
{
if (gotOne)
b.Encapsulate(new Bounds(s.position, s.radius * unit));
else
b = new Bounds(s.position, s.radius * unit);
gotOne = true;
}
}
}
return b;
}
bool CachedCountIsValid => m_MemberValidity.Count == Targets.Count;
bool IndexIsValid(int index) => index >= 0 && index < Targets.Count && CachedCountIsValid;
static BoundingSphere WeightedMemberBoundsForValidMember(Target t, Vector3 avgPos, float maxWeight)
{
var pos = TargetPositionCache.GetTargetPosition(t.Object);
var w = Mathf.Max(0, t.Weight);
if (maxWeight > UnityVectorExtensions.Epsilon && w < maxWeight)
w /= maxWeight;
else
w = 1;
return new BoundingSphere(Vector3.Lerp(avgPos, pos, w), t.Radius * w);
}
///
/// Update the group's transform right now, depending on the transforms of the members.
/// Normally this is called automatically by Update() or LateUpdate().
///
public void DoUpdate()
{
Targets ??= new (); // in case user set it to null
m_LastUpdateFrame = Time.frameCount;
UpdateMemberValidity();
m_AveragePos = CalculateAveragePosition(out m_MaxWeight);
BoundingBox = CalculateBoundingBox(m_MaxWeight);
m_BoundingSphere = CalculateBoundingSphere(m_MaxWeight);
switch (PositionMode)
{
case PositionModes.GroupCenter:
transform.position = Sphere.position;
break;
case PositionModes.GroupAverage:
transform.position = m_AveragePos;
break;
}
switch (RotationMode)
{
case RotationModes.Manual:
break;
case RotationModes.GroupAverage:
transform.rotation = CalculateAverageOrientation();
break;
}
}
void UpdateMemberValidity()
{
var count = Targets.Count;
m_ValidMembers.Clear();
m_ValidMembers.Capacity = Mathf.Max(m_ValidMembers.Capacity, count);
m_MemberValidity.Clear();
m_MemberValidity.Capacity = Mathf.Max(m_MemberValidity.Capacity, count);
for (int i = 0; i < count; ++i)
{
m_MemberValidity.Add(Targets[i].Object != null
&& Targets[i].Weight > UnityVectorExtensions.Epsilon
&& Targets[i].Object.gameObject.activeInHierarchy);
if (m_MemberValidity[i])
m_ValidMembers.Add(i);
}
}
// Assumes that UpdateMemberValidity() has been called
Vector3 CalculateAveragePosition(out float maxWeight)
{
var pos = Vector3.zero;
float weightSum = 0;
maxWeight = 0;
var count = m_ValidMembers.Count;
for (int i = 0; i < count; ++i)
{
var targetIndex = m_ValidMembers[i];
var weight = Targets[targetIndex].Weight;
weightSum += weight;
pos += TargetPositionCache.GetTargetPosition(Targets[targetIndex].Object) * weight;
maxWeight = Mathf.Max(maxWeight, weight);
}
if (weightSum > UnityVectorExtensions.Epsilon)
pos /= weightSum;
else
pos = transform.position;
return pos;
}
// Assumes that CalculateAveragePosition() has been called
Bounds CalculateBoundingBox(float maxWeight)
{
if (maxWeight < UnityVectorExtensions.Epsilon)
return m_BoundingBox;
var b = new Bounds(m_AveragePos, Vector3.zero);
var count = m_ValidMembers.Count;
for (int i = 0; i < count; ++i)
{
var s = WeightedMemberBoundsForValidMember(Targets[m_ValidMembers[i]], m_AveragePos, m_MaxWeight);
b.Encapsulate(new Bounds(s.position, s.radius * 2 * Vector3.one));
}
return b;
}
///
/// Use Ritter's algorithm for calculating an approximate bounding sphere.
/// Assumes that CalculateBoundingBox() has been called.
///
/// The maximum weight of members in the group
/// An approximate bounding sphere. Will be slightly large.
BoundingSphere CalculateBoundingSphere(float maxWeight)
{
var count = m_ValidMembers.Count;
if (count == 0 || maxWeight < UnityVectorExtensions.Epsilon)
return m_BoundingSphere;
var sphere = WeightedMemberBoundsForValidMember(Targets[m_ValidMembers[0]], m_AveragePos, maxWeight);
for (int i = 1; i < count; ++i)
{
var s = WeightedMemberBoundsForValidMember(Targets[m_ValidMembers[i]], m_AveragePos, maxWeight);
var distance = (s.position - sphere.position).magnitude + s.radius;
if (distance > sphere.radius)
{
// Point is outside current sphere: update
sphere.radius = (sphere.radius + distance) * 0.5f;
sphere.position = (sphere.radius * sphere.position + (distance - sphere.radius) * s.position) / distance;
}
}
return sphere;
}
// Assumes that CalculateBoundingSphere() has been called
Quaternion CalculateAverageOrientation()
{
if (m_MaxWeight <= UnityVectorExtensions.Epsilon)
return transform.rotation;
float weightedAverage = 0;
var r = Quaternion.identity;
var count = m_ValidMembers.Count;
for (int i = 0; i < count; ++i)
{
var targetIndex = m_ValidMembers[i];
var scaledWeight = Targets[targetIndex].Weight / m_MaxWeight;
var rot = TargetPositionCache.GetTargetRotation(Targets[targetIndex].Object);
r *= Quaternion.Slerp(Quaternion.identity, rot, scaledWeight);
weightedAverage += scaledWeight;
}
return Quaternion.Slerp(Quaternion.identity, r, 1.0f / weightedAverage);
}
void FixedUpdate()
{
if (UpdateMethod == UpdateMethods.FixedUpdate)
DoUpdate();
}
void Update()
{
if (!Application.isPlaying || UpdateMethod == UpdateMethods.Update)
DoUpdate();
}
void LateUpdate()
{
if (UpdateMethod == UpdateMethods.LateUpdate)
DoUpdate();
}
///
/// Get the local-space angular bounds of the group, from a specific point of view.
/// Also returns the z depth range of the members.
/// Note that this result is only valid after DoUpdate has been called. If members
/// are added or removed after that call or change their weights or active state,
/// this will not necessarily return correct information before the next update.
///
/// Point of view from which to calculate, and in whose
/// space the return values are
/// The lower bound of the screen angles of the members (degrees)
/// The upper bound of the screen angles of the members (degrees)
/// The min and max depth values of the members, relative to the observer
public void GetViewSpaceAngularBounds(
Matrix4x4 observer, out Vector2 minAngles, out Vector2 maxAngles, out Vector2 zRange)
{
if (m_LastUpdateFrame != Time.frameCount)
DoUpdate();
var world2local = observer;
if (!Matrix4x4.Inverse3DAffine(observer, ref world2local))
world2local = observer.inverse;
var r = m_BoundingSphere.radius;
var b = new Bounds() { center = world2local.MultiplyPoint3x4(m_AveragePos), extents = new Vector3(r, r, r) };
zRange = new Vector2(b.center.z - r, b.center.z + r);
if (CachedCountIsValid)
{
bool haveOne = false;
var count = m_ValidMembers.Count;
for (int i = 0; i < count; ++i)
{
var s = WeightedMemberBoundsForValidMember(Targets[m_ValidMembers[i]], m_AveragePos, m_MaxWeight);
var p = world2local.MultiplyPoint3x4(s.position);
if (p.z < UnityVectorExtensions.Epsilon)
continue; // behind us
var rN = s.radius / p.z;
var rN2 = new Vector3(rN, rN, 0);
var pN = p / p.z;
if (!haveOne)
{
b.center = pN;
b.extents = rN2;
zRange = new Vector2(p.z, p.z);
haveOne = true;
}
else
{
b.Encapsulate(pN + rN2);
b.Encapsulate(pN - rN2);
zRange.x = Mathf.Min(zRange.x, p.z);
zRange.y = Mathf.Max(zRange.y, p.z);
}
}
}
// Don't need the high-precision versions of UnityVectorExtensions.SignedAngle
var pMin = b.min;
var pMax = b.max;
minAngles = new Vector2(
Vector3.SignedAngle(Vector3.forward, new Vector3(0, pMax.y, 1), Vector3.right),
Vector3.SignedAngle(Vector3.forward, new Vector3(pMin.x, 0, 1), Vector3.up));
maxAngles = new Vector2(
Vector3.SignedAngle(Vector3.forward, new Vector3(0, pMin.y, 1), Vector3.right),
Vector3.SignedAngle(Vector3.forward, new Vector3(pMax.x, 0, 1), Vector3.up));
}
}
}