using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Assertions; //================================================================================================= /** * @brief This class manages all lights for a particular FogVolume. * *************************************************************************************************/ public class FogVolumeLightManager : MonoBehaviour { public int CurrentLightCount { get; private set; } public int VisibleLightCount { get; private set; } public bool DrawDebugData { get; set; } public bool AlreadyUsesTransformForPoI { get { return m_pointOfInterestTf != null; } } // Very slow and garbage allocating. Only call this once! public void FindLightsInScene() { CurrentLightCount = 0; VisibleLightCount = 0; m_lights.Clear(); m_lightsInFrustum.Clear(); for (int i = 0; i < MaxLightCount; i++) { m_lights.Add(new LightData()); m_lightsInFrustum.Add(new LightData()); } FogVolumeLight[] lights = FindObjectsOfType(); for (int i = 0; i < lights.Length; i++) { Light unityLight = lights[i].GetComponent(); if (unityLight != null) { switch (unityLight.type) { case LightType.Point: { AddPointLight(unityLight); lights[i].IsAddedToNormalLight = true; break; } case LightType.Spot: { AddSpotLight(unityLight); lights[i].IsAddedToNormalLight = true; break; } } } else { if (lights[i].IsPointLight) { AddSimulatedPointLight(lights[i]); lights[i].IsAddedToNormalLight = false; } else { AddSimulatedSpotLight(lights[i]); lights[i].IsAddedToNormalLight = false; } } } } // Very slow and garbage allocating. Only call this once! public void FindLightsInFogVolume() { CurrentLightCount = 0; VisibleLightCount = 0; m_lights.Clear(); m_lightsInFrustum.Clear(); for (int i = 0; i < MaxLightCount; i++) { m_lights.Add(new LightData()); m_lightsInFrustum.Add(new LightData()); } if (m_boxCollider == null) { m_boxCollider = gameObject.GetComponent(); } Bounds boundingBox = m_boxCollider.bounds; FogVolumeLight[] lights = FindObjectsOfType(); for (int i = 0; i < lights.Length; i++) { if (boundingBox.Intersects(new Bounds(lights[i].gameObject.transform.position, Vector3.one * LightInVolumeBoundsSize))) { Light unityLight = lights[i].GetComponent(); if (unityLight != null) { switch (unityLight.type) { case LightType.Point: { AddPointLight(unityLight); lights[i].IsAddedToNormalLight = true; break; } case LightType.Spot: { AddSpotLight(unityLight); lights[i].IsAddedToNormalLight = true; break; } } } else { if (lights[i].IsPointLight) { AddSimulatedPointLight(lights[i]); lights[i].IsAddedToNormalLight = false; } else { AddSimulatedSpotLight(lights[i]); lights[i].IsAddedToNormalLight = false; } } } } } //============================================================================================= /** * @brief Add a simulated point light to the manager. * * Note that a simulated point light is any GameObject with a SimulatedPointLight component on * it. * * @param _light The SimulatedPointLight component of an existing GameObject. * * @return True if the light was added to the managers list of lights. * @return False if the manager already contains the maximum amount of lights that was * specified in the constructor. * *********************************************************************************************/ public bool AddSimulatedPointLight(FogVolumeLight _light) { Assert.IsTrue(CurrentLightCount < MaxLightCount, "The maximum number of lights is already reached!"); int index = _FindFirstFreeLight(); if (index != InvalidIndex) { LightData data = m_lights[index]; CurrentLightCount++; data.LightType = EFogVolumeLightType.FogVolumePointLight; data.Transform = _light.transform; data.Light = null; data.FogVolumeLight = _light; data.Bounds = new Bounds(data.Transform.position, Vector3.one * data.FogVolumeLight.Range * 2.5f); return true; } return false; } //============================================================================================= /** * @brief Add a simulated spot light to the manager. * * Note that a simulated spot light is any GameObject with a SimulatedSpotLight component on * it. * * @param _light The SimulatedSpotLight component of an existing GameObject. * * @return True if the light was added to the managers list of lights. * @return False if the manager already contains the maximum amount of lights that was * specified in the constructor. * *********************************************************************************************/ public bool AddSimulatedSpotLight(FogVolumeLight _light) { Assert.IsTrue(CurrentLightCount < MaxLightCount, "The maximum number of lights is already reached!"); int index = _FindFirstFreeLight(); if (index != InvalidIndex) { LightData data = m_lights[index]; CurrentLightCount++; data.LightType = EFogVolumeLightType.FogVolumeSpotLight; data.Transform = _light.transform; data.Light = null; data.FogVolumeLight = _light; Vector3 center = data.Transform.position + data.Transform.forward * data.FogVolumeLight.Range * 0.5f; data.Bounds = new Bounds(center, Vector3.one * data.FogVolumeLight.Range * (0.75f + data.FogVolumeLight.Angle * 0.03f)); return true; } return false; } //============================================================================================= /** * @brief Add an existing point light to the manager. * * @param _light The light component of an existing point light. * * @return True if the light was added to the managers list of lights. * @return False if the manager already contains the maximum amount of lights that was * specified in the constructor. * *********************************************************************************************/ public bool AddPointLight(Light _light) { Assert.IsTrue(CurrentLightCount < MaxLightCount, "The maximum number of lights is already reached!"); int index = _FindFirstFreeLight(); if (index != InvalidIndex) { LightData data = m_lights[index]; CurrentLightCount++; data.LightType = EFogVolumeLightType.PointLight; data.Transform = _light.transform; data.Light = _light; data.FogVolumeLight = null; data.Bounds = new Bounds(data.Transform.position, Vector3.one * data.Light.range * 2.5f); return true; } return false; } //============================================================================================= /** * @brief Add an existing spot light to the manager. * * @param _light The light component of an existing spot light. * * @return True if the light was added to the managers list of lights. * @return False if the manager already contains the maximum amount of lights that was * specified in the constructor. * *********************************************************************************************/ public bool AddSpotLight(Light _light) { Assert.IsTrue(CurrentLightCount < MaxLightCount, "The maximum number of lights is already reached!"); int index = _FindFirstFreeLight(); if (index != InvalidIndex) { LightData data = m_lights[index]; CurrentLightCount++; data.LightType = EFogVolumeLightType.SpotLight; data.Transform = _light.transform; data.Light = _light; data.FogVolumeLight = null; Vector3 center = data.Transform.position + data.Transform.forward * data.Light.range * 0.5f; data.Bounds = new Bounds(center, Vector3.one * data.Light.range * (0.75f + data.Light.spotAngle * 0.03f)); return true; } return false; } //============================================================================================= /** * @brief Removes the light with the specified transform. * * Note that nothing will happen if the light is not currently present in the manager. * * @param _lightToRemove The light that will be removed from this manager. * * @return True if the light was found inside the manager and removed successfully. * @return False if the light was not found an thus not removed. * *********************************************************************************************/ public bool RemoveLight(Transform _lightToRemove) { int count = m_lights.Count; for (int i = 0; i < count; i++) { if (ReferenceEquals(m_lights[i].Transform, _lightToRemove)) { m_lights[i].LightType = EFogVolumeLightType.None; CurrentLightCount--; return true; } } return false; } //============================================================================================= /** * @brief Updates the lights that will be rendered. * * Note that this method should be called once per frame, before any data is sent to the * shaders. * *********************************************************************************************/ public void ManualUpdate(ref Plane[] _frustumPlanes) { FrustumPlanes = _frustumPlanes; m_camera = m_fogVolumeData != null ? m_fogVolumeData.GameCamera : null; if (m_camera == null) { return; } if (m_boxCollider == null) { m_boxCollider = m_fogVolume.GetComponent(); } if (m_pointOfInterestTf != null) { m_pointOfInterest = m_pointOfInterestTf.position; } _UpdateBounds(); _FindLightsInFrustum(); if (m_lightsInFrustum.Count > MaxVisibleLights) { _SortLightsInFrustum(); } _PrepareShaderArrays(); } //============================================================================================= /** * @brief Draws the gizmos if the user wants to see debug data. * *********************************************************************************************/ public void OnDrawGizmos() { hideFlags = HideFlags.HideInInspector; if (m_camera == null) { return; } if (!DrawDebugData) { return; } Color tempColor = Gizmos.color; Gizmos.color = Color.green; for (int i = 0; i < VisibleLightCount; i++) { Gizmos.DrawWireCube(m_lightsInFrustum[i].Bounds.center, m_lightsInFrustum[i].Bounds.size); } Gizmos.color = Color.magenta; Matrix4x4 currentMatrix = Gizmos.matrix; Gizmos.matrix = Matrix4x4.TRS(m_camera.transform.position, m_camera.transform.rotation, Vector3.one); //Gizmos.matrix = transform.localToWorldMatrix; Gizmos.DrawFrustum(m_camera.transform.position, m_camera.fieldOfView, m_camera.nearClipPlane, m_fogVolume.PointLightingDistance2Camera, m_camera.aspect); Gizmos.color = tempColor; Gizmos.matrix = currentMatrix; } //============================================================================================= /** * @brief Sets the multiplier that is applied to the bounding box size of point lights. * * @param _cullSizeMultiplier The multiplier that is applied to the AABB of point lights. * *********************************************************************************************/ public void SetPointLightCullSizeMultiplier(float _cullSizeMultiplier) { m_pointLightCullSizeMultiplier = _cullSizeMultiplier; } //============================================================================================= /** * @brief Sets the point of interest to a fixed position. * * @param _pointOfInterest The point that will be used for prioritizing which lights need to * be rendered. * *********************************************************************************************/ public void SetPointOfInterest(Vector3 _pointOfInterest) { m_pointOfInterestTf = null; m_pointOfInterest = _pointOfInterest; } //============================================================================================= /** * @brief Sets the point of interest to the specified transform. * * The point of interest will be updated from the position of the transform. It is therefore * not necessary to call this method more than once. * *********************************************************************************************/ public void SetPointOfInterest(Transform _pointOfInterest) { Assert.IsTrue(_pointOfInterest != null, "_pointOfInterest must not be null!"); m_pointOfInterestTf = _pointOfInterest; } //============================================================================================= /** * @brief Returns the array that contains all light positions. * * Remember to call Update() before using this method, otherwise old data will be sent to the * shaders. * *********************************************************************************************/ public Vector4[] GetLightPositionArray() { return m_lightPos; } //============================================================================================= /** * @brief Returns the array that contains all light rotations. * * Remember to call Update() before using this method, otherwise old data will be sent to the * shaders. * *********************************************************************************************/ public Vector4[] GetLightRotationArray() { return m_lightRot; } //============================================================================================= /** * @brief Returns the array that contains all light colors. * * Remember to call Update() before using this method, otherwise old data will be sent to the * shaders. *********************************************************************************************/ public Color[] GetLightColorArray() { return m_lightColor; } //============================================================================================= /** * @brief Returns the array that contains all light data (intensity, range, spotlight angle, * none). * * Remember to call Update() before using this method, otherwise old data will be sent to the * shaders. * *********************************************************************************************/ public Vector4[] GetLightData() { return m_lightData; } //============================================================================================= /** * @brief Initializes the LightManager with default values. * *********************************************************************************************/ public void Initialize() { m_fogVolume = gameObject.GetComponent(); m_fogVolumeData = FindObjectOfType(); m_camera = null; m_boxCollider = null; CurrentLightCount = 0; DrawDebugData = false; if (m_lights == null) { m_lights = new List(MaxLightCount); m_lightsInFrustum = new List(MaxLightCount); for (int i = 0; i < MaxLightCount; i++) { m_lights.Add(new LightData()); m_lightsInFrustum.Add(new LightData()); } } } //============================================================================================= /** * @brief Clears the LightManager and prepares it toi be reinitialized. * *********************************************************************************************/ public void Deinitialize() { VisibleLightCount = 0; DrawDebugData = false; } public void SetFrustumPlanes(ref Plane[] _frustumPlanes) { FrustumPlanes = _frustumPlanes; } #if UNITY_EDITOR private void Update() { hideFlags = HideFlags.HideInInspector; } #endif //============================================================================================= /** * @brief Updates the axis aligned bounding boxes of all registered lights. * *********************************************************************************************/ private void _UpdateBounds() { int count = m_lights.Count; for (int i = 0; i < count; i++) { LightData data = m_lights[i]; if (data.LightType == EFogVolumeLightType.None) { continue; } switch (data.LightType) { case EFogVolumeLightType.None: { break; } case EFogVolumeLightType.PointLight: { data.Bounds = new Bounds(data.Transform.position, Vector3.one * data.Light.range * m_pointLightCullSizeMultiplier); break; } case EFogVolumeLightType.SpotLight: { Vector3 center = data.Transform.position + data.Transform.forward * data.Light.range * 0.5f; data.Bounds = new Bounds(center, Vector3.one * data.Light.range * m_pointLightCullSizeMultiplier * 1.25f); break; } case EFogVolumeLightType.FogVolumePointLight: { data.Bounds = new Bounds(data.Transform.position, Vector3.one * data.FogVolumeLight.Range * m_pointLightCullSizeMultiplier); break; } case EFogVolumeLightType.FogVolumeSpotLight: { Vector3 center = data.Transform.position + data.Transform.forward * data.FogVolumeLight.Range * 0.5f; data.Bounds = new Bounds(center, Vector3.one * data.FogVolumeLight.Range * m_pointLightCullSizeMultiplier * 1.25f); break; } } } } //============================================================================================= /** * @brief Finds the first free light in the list of all lights. * * Note that this method is necessary to ensure that adding/removing lights does not allocate * any garbage. * *********************************************************************************************/ private int _FindFirstFreeLight() { if (CurrentLightCount < MaxLightCount) { int count = m_lights.Count; for (int i = 0; i < count; i++) { if (m_lights[i].LightType == EFogVolumeLightType.None) { return i; } } } return InvalidIndex; } //============================================================================================= /** * @brief Finds all lights that are currently in the view frustum of the camera and calculates * all necessary data for them. * *********************************************************************************************/ private void _FindLightsInFrustum() { m_inFrustumCount = 0; Vector3 CameraPos = m_camera.gameObject.transform.position; int count = m_lights.Count; for (int i = 0; i < count; i++) { if (m_lights[i].Transform == null) { m_lights[i].LightType = EFogVolumeLightType.None; } if (m_lights[i].LightType == EFogVolumeLightType.None) { continue; } float distanceToCamera = (m_lights[i].Transform.position - CameraPos).magnitude; if (distanceToCamera > m_fogVolume.PointLightingDistance2Camera) { continue; } switch (m_lights[i].LightType) { case EFogVolumeLightType.None: { continue; } case EFogVolumeLightType.PointLight: case EFogVolumeLightType.SpotLight: { if (m_lights[i].Light.enabled == false) { continue; } break; } case EFogVolumeLightType.FogVolumePointLight: { if (m_lights[i].FogVolumeLight.Enabled == false) { continue; } break; } case EFogVolumeLightType.FogVolumeSpotLight: { if (m_lights[i].FogVolumeLight.Enabled == false) { continue; } break; } } if (GeometryUtility.TestPlanesAABB(FrustumPlanes, m_lights[i].Bounds)) { LightData light = m_lights[i]; Vector3 lightPos = light.Transform.position; light.SqDistance = (lightPos - m_pointOfInterest).sqrMagnitude; light.Distance2Camera = (lightPos - CameraPos).magnitude; m_lightsInFrustum[m_inFrustumCount++] = light; if (light.FogVolumeLight != null) { if (light.LightType == EFogVolumeLightType.FogVolumePointLight && !light.FogVolumeLight.IsPointLight) { light.LightType = EFogVolumeLightType.FogVolumeSpotLight; } else if (light.LightType == EFogVolumeLightType.FogVolumeSpotLight && light.FogVolumeLight.IsPointLight) { light.LightType = EFogVolumeLightType.FogVolumePointLight; } } } } } //============================================================================================= /** * @brief Sorts the lights that are currently within the view frustum of the camera by * distance to the camera. * * This method will only be called when there are more than MaxVisibleLights in the view * frustum of the camera. * *********************************************************************************************/ private void _SortLightsInFrustum() { bool finishedSorting = false; do { finishedSorting = true; for (int i = 0; i < m_inFrustumCount - 1; i++) { if (m_lightsInFrustum[i].SqDistance > m_lightsInFrustum[i + 1].SqDistance) { LightData tempData = m_lightsInFrustum[i]; m_lightsInFrustum[i] = m_lightsInFrustum[i + 1]; m_lightsInFrustum[i + 1] = tempData; finishedSorting = false; } } } while (!finishedSorting); } //============================================================================================= /** * @brief Prepares the data of the currently visible light and writes it to the arrays that * can then be sent to the shaders. * *********************************************************************************************/ private void _PrepareShaderArrays() { VisibleLightCount = 0; for (int i = 0; i < MaxVisibleLights; i++) { if (i >= m_inFrustumCount) { break; } LightData data = m_lightsInFrustum[i]; switch (data.LightType) { case EFogVolumeLightType.FogVolumePointLight: { FogVolumeLight light = data.FogVolumeLight; m_lightPos[i] = gameObject.transform.InverseTransformPoint(data.Transform.position); m_lightRot[i] = gameObject.transform.InverseTransformVector(data.Transform.forward); m_lightColor[i] = light.Color; m_lightData[i] = new Vector4(light.Intensity * m_fogVolume.PointLightsIntensity * (1.0f - Mathf.Clamp01(data.Distance2Camera / m_fogVolume .PointLightingDistance2Camera) ), light.Range / PointLightRangeDivider, InvalidSpotLightAngle, NoData); VisibleLightCount++; break; } case EFogVolumeLightType.FogVolumeSpotLight: { FogVolumeLight light = data.FogVolumeLight; m_lightPos[i] = gameObject.transform.InverseTransformPoint(data.Transform.position); m_lightRot[i] = gameObject.transform.InverseTransformVector(data.Transform.forward); m_lightColor[i] = light.Color; m_lightData[i] = new Vector4(light.Intensity * m_fogVolume.PointLightsIntensity * (1.0f - Mathf.Clamp01(data.Distance2Camera / m_fogVolume .PointLightingDistance2Camera) ), light.Range / SpotLightRangeDivider, light.Angle, NoData); VisibleLightCount++; break; } case EFogVolumeLightType.PointLight: { Light light = data.Light; m_lightPos[i] = gameObject.transform.InverseTransformPoint(data.Transform.position); m_lightRot[i] = gameObject.transform.InverseTransformVector(data.Transform.forward); m_lightColor[i] = light.color; m_lightData[i] = new Vector4(light.intensity * m_fogVolume.PointLightsIntensity * (1.0f - Mathf.Clamp01(data.Distance2Camera / m_fogVolume .PointLightingDistance2Camera) ), light.range / PointLightRangeDivider, InvalidSpotLightAngle, NoData); VisibleLightCount++; break; } case EFogVolumeLightType.SpotLight: { Light light = data.Light; m_lightPos[i] = gameObject.transform.InverseTransformPoint(data.Transform.position); m_lightRot[i] = gameObject.transform.InverseTransformVector(data.Transform.forward); m_lightColor[i] = light.color; m_lightData[i] = new Vector4(light.intensity * m_fogVolume.PointLightsIntensity * (1.0f - Mathf.Clamp01(data.Distance2Camera / m_fogVolume .PointLightingDistance2Camera) ), light.range / SpotLightRangeDivider, light.spotAngle, NoData); VisibleLightCount++; break; } case EFogVolumeLightType.None: { break; } } } } private float m_pointLightCullSizeMultiplier = 1.0f; private FogVolume m_fogVolume = null; private FogVolumeData m_fogVolumeData = null; private Camera m_camera = null; private BoxCollider m_boxCollider = null; private Transform m_pointOfInterestTf = null; private Vector3 m_pointOfInterest = Vector3.zero; private readonly Vector4[] m_lightPos = new Vector4[MaxVisibleLights]; private readonly Vector4[] m_lightRot = new Vector4[MaxVisibleLights]; private readonly Color[] m_lightColor = new Color[MaxVisibleLights]; private readonly Vector4[] m_lightData = new Vector4[MaxVisibleLights]; private List m_lights = null; private List m_lightsInFrustum = null; private int m_inFrustumCount = 0; private Plane[] FrustumPlanes = null; private const int InvalidIndex = -1; private const int MaxVisibleLights = 64; private const float InvalidSpotLightAngle = -1.0f; private const float NoData = 0.0f; private const float PointLightRangeDivider = 5.0f; private const float SpotLightRangeDivider = 5.0f; private const int MaxLightCount = 1000; /// The assumed size of a light. Used by realtime search when searching for lights inside a FV. private const float LightInVolumeBoundsSize = 5.0f; protected class LightData { public LightData() { LightType = EFogVolumeLightType.None; Light = null; FogVolumeLight = null; Transform = null; SqDistance = 0.0f; Distance2Camera = 0.0f; Bounds = new Bounds(); } public EFogVolumeLightType LightType { get; set; } public Light Light { get; set; } public FogVolumeLight FogVolumeLight { get; set; } public Transform Transform { get; set; } public float SqDistance { get; set; } public float Distance2Camera { get; set; } public Bounds Bounds { get; set; } } }