#if UNITY_EDITOR using UnityEditor; #endif using UnityEngine; using UnityEngine.Rendering; namespace UnityEngine { /// /// Represents camera-space light controls around a virtual pivot point. /// [AddComponentMenu("Rendering/Light Anchor")] [ExecuteInEditMode] [DisallowMultipleComponent] [CoreRPHelpURLAttribute("View-Lighting-Tool")] public class LightAnchor : MonoBehaviour { const float k_ArcRadius = 5; const float k_AxisLength = 10; internal const float k_MaxDistance = 10000f; [SerializeField, Min(0)] float m_Distance = 0f; [SerializeField] UpDirection m_FrameSpace = UpDirection.World; [SerializeField] Transform m_AnchorPositionOverride; [SerializeField] Vector3 m_AnchorPositionOffset; [SerializeField] float m_Yaw; [SerializeField] float m_Pitch; [SerializeField] float m_Roll; /// /// The camera-relative yaw. /// /// /// The range is -180 through 180 inclusive. Values between 0 and 180 are to the right of the camera, and values between 0 and -180 to the left. /// public float yaw { get { return m_Yaw; } set { m_Yaw = NormalizeAngleDegree(value); } } /// /// The pitch relative to the horizon or camera depending on value of m_Space. /// /// /// The range is -180 through 180 inclusive. Values between 0 and 180 are below the camera, and values between 0 and -180 are above the camera. /// public float pitch { get { return m_Pitch; } set { m_Pitch = NormalizeAngleDegree(value); } } /// /// The camera-relative roll. /// /// /// The range is -180 through 180 inclusive. Values between 0 and 180 are to the right of the camera, and values between 0 and -180 are to the left of the camera. /// public float roll { get { return m_Roll; } set { m_Roll = NormalizeAngleDegree(value); } } /// /// The distance from the light's anchor point. /// public float distance { get => m_Distance; set => m_Distance = Mathf.Clamp(value, 0f, k_MaxDistance); } /// /// Enum to describes to up vector for the Light Anchor /// public enum UpDirection { /// /// Up vector is world space Vector3.up /// World = Space.World, /// /// Up vector is the up of the main camera /// Local = Space.Self } /// /// Indicates whether the up vector should be in world or camera space. /// public UpDirection frameSpace { get { return m_FrameSpace; } set { m_FrameSpace = value; } } /// /// The position of the light's anchor point. /// public Vector3 anchorPosition { get { if (anchorPositionOverride != null) return anchorPositionOverride.position + anchorPositionOverride.TransformDirection(anchorPositionOffset); else return transform.position + transform.forward * distance; } } struct Axes { public Vector3 up; public Vector3 right; public Vector3 forward; } /// /// Overrides the pivot of used to compute the light position. This is useful to track an existing object in the scene. /// The transform of the light will be automatically updated by the Update() method of the LightAnchor. /// public Transform anchorPositionOverride { get => m_AnchorPositionOverride; set => m_AnchorPositionOverride = value; } /// /// Offset relative to the position of the anchor position override transform in object space. /// public Vector3 anchorPositionOffset { get => m_AnchorPositionOffset; set => m_AnchorPositionOffset = value; } /// /// Normalizes the input angle to be in the range of -180 and 180. /// /// Raw input angle or rotation. /// Returns the angle of rotation between -180 and 180. public static float NormalizeAngleDegree(float angle) { const float range = 360f; const float startValue = -180f; var offset = angle - startValue; return offset - (Mathf.Floor(offset / range) * range) + startValue; } /// /// Updates Yaw, Pitch, Roll, and Distance based on the Transform. /// /// The Camera to which light values are relative. public void SynchronizeOnTransform(Camera camera) { Axes axes = GetWorldSpaceAxes(camera, anchorPosition); Vector3 worldAnchorToLight = transform.position - anchorPosition; // In case the distance is 0 or the anchor override is at the same position than the light anchor if (worldAnchorToLight.magnitude == 0) worldAnchorToLight = -transform.forward; Vector3 projectOnGround = Vector3.ProjectOnPlane(worldAnchorToLight, axes.up); if(projectOnGround.magnitude < 0.0001f) projectOnGround = Vector3.ProjectOnPlane(worldAnchorToLight, axes.up + axes.right * 0.0001f); projectOnGround.Normalize(); float extractedYaw = Vector3.SignedAngle(axes.forward, projectOnGround, axes.up); Vector3 yawedRight = Quaternion.AngleAxis(extractedYaw, axes.up) * axes.right; float extractedPitch = Vector3.SignedAngle(projectOnGround, worldAnchorToLight, yawedRight); yaw = extractedYaw; pitch = extractedPitch; roll = transform.rotation.eulerAngles.z; } /// /// Updates the light's transform with respect to a given camera and anchor point /// /// The camera to which values are relative. /// The anchor position. public void UpdateTransform(Camera camera, Vector3 anchor) { // It is important not to read the property anchorPosition here since the distance might have been updated // (which is a part of the calculation), but the transform is yet to be set. var axes = GetWorldSpaceAxes(camera, anchor); UpdateTransform(axes.up, axes.right, axes.forward, anchor); } Axes GetWorldSpaceAxes(Camera camera, Vector3 anchor) { // Fallback when the light anchor object is child of the camera (bad setup) if (transform.IsChildOf(camera.transform)) { return new Axes { up = Vector3.up, right = Vector3.right, forward = Vector3.forward, }; } Matrix4x4 viewToWorld = camera.cameraToWorldMatrix; if (m_FrameSpace == UpDirection.Local) { Vector3 localUp = Camera.main.transform.up; viewToWorld = Matrix4x4.Scale(new Vector3(1, 1, -1)) * Matrix4x4.LookAt(camera.transform.position, anchor, localUp).inverse; viewToWorld = viewToWorld.inverse; } // Correct view to world for perspective else if (!camera.orthographic && camera.transform.position != anchor) { var d = (anchor - camera.transform.position).normalized; var f = Quaternion.LookRotation(d); viewToWorld = Matrix4x4.Scale(new Vector3(1, 1, -1)) * Matrix4x4.TRS(camera.transform.position, f, Vector3.one).inverse; viewToWorld = viewToWorld.inverse; } Vector3 up = (viewToWorld * Vector3.up).normalized; Vector3 right = (viewToWorld * Vector3.right).normalized; Vector3 forward = (viewToWorld * Vector3.forward).normalized; return new Axes { up = up, right = right, forward = forward }; } void Update() { if (anchorPositionOverride == null || Camera.main == null) return; if (anchorPositionOverride.hasChanged || Camera.main.transform.hasChanged) UpdateTransform(Camera.main, anchorPosition); } void OnDrawGizmosSelected() { var camera = Camera.main; if (camera == null) { return; } // TODO: fix light rotated when camera rotates Vector3 anchor = anchorPosition; Axes axes = GetWorldSpaceAxes(camera, anchor); Vector3 d = transform.position - anchor; Vector3 proj = Vector3.ProjectOnPlane(d, axes.up); float arcRadius = Mathf.Min(distance * 0.25f, k_ArcRadius); float axisLength = Mathf.Min(distance * 0.5f, k_AxisLength); #if UNITY_EDITOR const float alpha = 0.2f; Handles.color = Color.grey; Handles.DrawDottedLine(anchorPosition, anchorPosition + proj, 2); Handles.DrawDottedLine(anchorPosition + proj, transform.position, 2); Handles.DrawDottedLine(anchorPosition, transform.position, 2); // forward Color color = Color.green; color.a = alpha; Handles.color = color; Handles.DrawLine(anchorPosition, anchorPosition + axes.forward * axisLength); Handles.DrawSolidArc(anchor, axes.up, axes.forward, yaw, arcRadius); // up color = Color.blue; color.a = alpha; Handles.color = color; Quaternion yawRot = Quaternion.AngleAxis(yaw, axes.up * k_AxisLength); Handles.DrawSolidArc(anchor, yawRot * axes.right, yawRot * axes.forward, pitch, arcRadius); Handles.DrawLine(anchorPosition, anchorPosition + (yawRot * axes.forward) * axisLength); #endif } // arguments are passed in world space void UpdateTransform(Vector3 up, Vector3 right, Vector3 forward, Vector3 anchor) { Quaternion worldYawRot = Quaternion.AngleAxis(m_Yaw, up); Quaternion worldPitchRot = Quaternion.AngleAxis(m_Pitch, right); Vector3 worldPosition = anchor + (worldYawRot * worldPitchRot) * forward * distance; transform.position = worldPosition; Vector3 lookAt = -((worldYawRot * worldPitchRot) * forward).normalized; Vector3 angles = Quaternion.LookRotation(lookAt, up).eulerAngles; angles.z = m_Roll; transform.eulerAngles = angles; } } }