using System; namespace UnityEngine.Rendering { /// /// Light Unit Utils contains functions and definitions to facilitate conversion between different light intensity units. /// public static class LightUnitUtils { static float k_LuminanceToEvFactor => Mathf.Log(100f / ColorUtils.s_LightMeterCalibrationConstant, 2); static float k_EvToLuminanceFactor => -k_LuminanceToEvFactor; /// /// The solid angle of a full sphere in steradians. /// public const float SphereSolidAngle = 4.0f * Mathf.PI; /// /// Get the unit that light intensity is measured in, for a specific light type. /// /// The type of light to get the native light unit for. /// The native unit of that light types intensity. public static LightUnit GetNativeLightUnit(LightType lightType) { switch (lightType) { // Punctual lights case LightType.Spot: case LightType.Point: case LightType.Pyramid: return LightUnit.Candela; // Directional lights case LightType.Directional: case LightType.Box: return LightUnit.Lux; // Area lights case LightType.Rectangle: case LightType.Disc: case LightType.Tube: return LightUnit.Nits; default: throw new ArgumentOutOfRangeException(); } } /// /// Check if a light types intensity can be converted to/from a light unit. /// /// Light type to check. /// Unit to check. /// True if light unit is supported. public static bool IsLightUnitSupported(LightType lightType, LightUnit lightUnit) { const int punctualUnits = 1 << (int)LightUnit.Lumen | 1 << (int)LightUnit.Candela | 1 << (int)LightUnit.Lux | 1 << (int)LightUnit.Ev100; const int directionalUnits = 1 << (int)LightUnit.Lux; const int areaUnits = 1 << (int)LightUnit.Lumen | 1 << (int)LightUnit.Nits | 1 << (int)LightUnit.Ev100; int lightUnitFlag = 1 << (int)lightUnit; switch (lightType) { // Punctual lights case LightType.Point: case LightType.Spot: case LightType.Pyramid: return (lightUnitFlag & punctualUnits) > 0; // Directional lights case LightType.Directional: case LightType.Box: return (lightUnitFlag & directionalUnits) > 0; // Area lights case LightType.Rectangle: case LightType.Disc: case LightType.Tube: return (lightUnitFlag & areaUnits) > 0; default: return false; } } /// /// Get the solid angle of a Point light. /// /// 4 * Pi steradians. public static float GetSolidAngleFromPointLight() { return SphereSolidAngle; } /// /// Get the solid angle of a Spot light. /// /// The spot angle in degrees. /// Solid angle in steradians. public static float GetSolidAngleFromSpotLight(float spotAngle) { double angle = Math.PI * spotAngle / 180.0; double solidAngle = 2.0 * Math.PI * (1.0 - Math.Cos(angle * 0.5)); return (float)solidAngle; } /// /// Get the solid angle of a Pyramid light. /// /// The spot angle in degrees. /// The aspect ratio of the pyramid. /// Solid angle in steradians. public static float GetSolidAngleFromPyramidLight(float spotAngle, float aspectRatio) { if (aspectRatio < 1.0f) { aspectRatio = (float)(1.0 / aspectRatio); } double angleA = Math.PI * spotAngle / 180.0; double length = Math.Tan(0.5 * angleA) * aspectRatio; double angleB = Math.Atan(length) * 2.0; double solidAngle = 4.0 * Math.Asin(Math.Sin(angleA * 0.5) * Math.Sin(angleB * 0.5)); return (float)solidAngle; } internal static float GetSolidAngle(LightType lightType, bool spotReflector, float spotAngle, float aspectRatio) { return lightType switch { LightType.Spot => spotReflector ? GetSolidAngleFromSpotLight(spotAngle) : SphereSolidAngle, LightType.Pyramid => spotReflector ? GetSolidAngleFromPyramidLight(spotAngle, aspectRatio) : SphereSolidAngle, LightType.Point => GetSolidAngleFromPointLight(), _ => throw new ArgumentException("Solid angle is undefined for lights of type " + lightType) }; } /// /// Get the projected surface area of a Rectangle light. /// /// The width of the rectangle. /// The height of the rectangle. /// Surface area. public static float GetAreaFromRectangleLight(float rectSizeX, float rectSizeY) { return Mathf.Abs(rectSizeX * rectSizeY) * Mathf.PI; } /// /// Get the projected surface area of a Rectangle light. /// /// The size of the rectangle. /// Projected surface area. public static float GetAreaFromRectangleLight(Vector2 rectSize) { return GetAreaFromRectangleLight(rectSize.x, rectSize.y); } /// /// Get the projected surface area of a Disc light. /// /// The radius of the disc. /// Projected surface area. public static float GetAreaFromDiscLight(float discRadius) { return discRadius * discRadius * Mathf.PI * Mathf.PI; } /// /// Get the projected surface area of a Tube light. /// /// Note that Tube lights have no physical surface area. /// Instead this method returns a value suitable for Nits<=>Lumen unit conversion. /// The length of the tube. /// 4 * Pi * (tube length). public static float GetAreaFromTubeLight(float tubeLength) { // Line lights expect radiance (W / (sr * m^2)) in the shader. // In the UI, we specify luminous flux (power) in lumens. // First, it needs to be converted to radiometric units (radian flux, W). // // Then we must recall how to compute power from radiance: // // radiance = differential_power / (differential_projected_area * differential_solid_angle), // radiance = differential_power / (differential_area * differential_solid_angle * ), // power = Integral{area, Integral{hemisphere, radiance * }}. // // Unlike line lights, our line lights have no surface area, so the integral becomes: // // power = Integral{length, Integral{sphere, radiance}}. // // For an isotropic line light, radiance is constant, therefore: // // power = length * (4 * Pi) * radiance, // radiance = power / (length * (4 * Pi)). return Mathf.Abs(tubeLength) * 4.0f * Mathf.PI; } /// /// Convert intensity in Lumen to Candela. /// /// Intensity in Lumen. /// Light solid angle in steradians. /// Intensity in Candela. public static float LumenToCandela(float lumen, float solidAngle) { return lumen / solidAngle; } /// /// Convert intensity in Candela to Lumen. /// /// Intensity in Candela. /// Light solid angle in steradians. /// Intensity in Lumen. public static float CandelaToLumen(float candela, float solidAngle) { return candela * solidAngle; } /// /// Convert intensity in Lumen to Nits. /// /// Intensity in Lumen. /// Projected surface area of the light source. /// Intensity in Nits. public static float LumenToNits(float lumen, float area) { return lumen / area; } /// /// Convert intensity in Nits to Lumen. /// /// Intensity in Nits. /// Projected surface area of the light source. /// Intensity in Lumen. public static float NitsToLumen(float nits, float area) { return nits * area; } /// /// Convert intensity in Lux to Candela. /// /// Intensity in Lux. /// Distance between light and surface. /// Intensity in Candela. public static float LuxToCandela(float lux, float distance) { return lux / (distance * distance); } /// /// Convert intensity in Candela to Lux. /// /// Intensity in Lux. /// Distance between light and surface. /// Intensity in Lux. public static float CandelaToLux(float candela, float distance) { return candela * distance * distance; } /// /// Convert intensity in Ev100 to Nits. /// /// Intensity in Ev100. /// Intensity in Nits. public static float Ev100ToNits(float ev100) { return Mathf.Pow(2.0f, ev100 + k_EvToLuminanceFactor); } /// /// Convert intensity in Nits to Ev100. /// /// Intensity in Nits. /// Intensity in Ev100. public static float NitsToEv100(float nits) { return Mathf.Log(nits, 2) + k_LuminanceToEvFactor; } /// /// Convert intensity in Ev100 to Candela. /// /// Intensity in Ev100. /// Intensity in Candela. public static float Ev100ToCandela(float ev100) { return Ev100ToNits(ev100); } /// /// Convert intensity in Candela to Ev100. /// /// Intensity in Candela. /// Intensity in Ev100. public static float CandelaToEv100(float candela) { return NitsToEv100(candela); } internal static float ConvertIntensityInternal(float intensity, LightUnit fromUnit, LightUnit toUnit, LightType lightType, float area, float luxAtDistance, float solidAngle) { if (!IsLightUnitSupported(lightType, fromUnit) || !IsLightUnitSupported(lightType, toUnit)) { throw new ArgumentException("Converting " + fromUnit + " to " + toUnit + " is undefined for lights of type " + lightType); } if (fromUnit == toUnit) { return intensity; } switch (fromUnit) { case LightUnit.Lumen: { switch (toUnit) { case LightUnit.Candela: { // Lumen => Candela: return LumenToCandela(intensity, solidAngle); } case LightUnit.Lux: { // Lumen => Candela => Lux float candela = LumenToCandela(intensity, solidAngle); return CandelaToLux(candela, luxAtDistance); } case LightUnit.Nits: { // Lumen => Nits return LumenToNits(intensity, area); } case LightUnit.Ev100: { // Lumen => Candela/Nits => Ev100 float candelaNits = lightType switch { LightType.Point or LightType.Spot or LightType.Pyramid => LumenToCandela(intensity, solidAngle), LightType.Rectangle or LightType.Disc or LightType.Tube => LumenToNits(intensity, area), _ => throw new ArgumentException("Converting from Lumen to Ev100 is undefined for light type " + lightType) }; return NitsToEv100(candelaNits); } default: throw new ArgumentOutOfRangeException(nameof(toUnit), toUnit, null); } } case LightUnit.Candela: { switch (toUnit) { case LightUnit.Lumen: { // Candela => Lumen return CandelaToLumen(intensity, solidAngle); } case LightUnit.Lux: { // Candela => Lux return CandelaToLux(intensity, luxAtDistance); } case LightUnit.Ev100: { // Candela => Ev100 return NitsToEv100(intensity); } default: throw new ArgumentOutOfRangeException(nameof(toUnit), toUnit, null); } } case LightUnit.Lux: { switch (toUnit) { case LightUnit.Lumen: { // Lux => Candela => Lumen float candela = LuxToCandela(intensity, luxAtDistance); return CandelaToLumen(candela, solidAngle); } case LightUnit.Candela: { // Lux => Candela return LuxToCandela(intensity, luxAtDistance); } case LightUnit.Ev100: { // Lux => Candela => Ev100 float candela = LuxToCandela(intensity, luxAtDistance); return NitsToEv100(candela); } default: throw new ArgumentOutOfRangeException(nameof(toUnit), toUnit, null); } } case LightUnit.Nits: { switch (toUnit) { case LightUnit.Lumen: { return NitsToLumen(intensity, area); } case LightUnit.Ev100: { return NitsToEv100(intensity); } default: throw new ArgumentOutOfRangeException(nameof(toUnit), toUnit, null); } } case LightUnit.Ev100: { switch (toUnit) { case LightUnit.Lumen: { // Ev100 => Candela/Nits => Lumen float candelaOrNits = Ev100ToNits(intensity); return lightType switch { LightType.Point or LightType.Spot or LightType.Pyramid => CandelaToLumen(candelaOrNits, solidAngle), LightType.Rectangle or LightType.Disc or LightType.Tube => NitsToLumen(candelaOrNits, area), _ => throw new ArgumentException("Converting from Lumen to Ev100 is undefined for light type " + lightType) }; } case LightUnit.Nits: case LightUnit.Candela: { // Ev100 => Candela/Nits return Ev100ToNits(intensity); } case LightUnit.Lux: { // Ev100 => Candela => Lux float candela = Ev100ToNits(intensity); return CandelaToLux(candela, luxAtDistance); } default: throw new ArgumentOutOfRangeException(nameof(toUnit), toUnit, null); } } default: throw new ArgumentOutOfRangeException(nameof(fromUnit), fromUnit, null); } } /// /// Convert intensity from one unit to another using the parameters of a given Light. /// /// Light to use parameters from. /// Intensity to be converted. /// Unit to convert from. /// Unit to convert to. /// Converted intensity. public static float ConvertIntensity(Light light, float intensity, LightUnit fromUnit, LightUnit toUnit) { LightType lightType = light.type; float area = lightType switch { LightType.Rectangle => GetAreaFromRectangleLight(light.areaSize), LightType.Disc => GetAreaFromDiscLight(light.areaSize.x), // Disc radius is stored in areaSize.x LightType.Tube => GetAreaFromTubeLight(light.areaSize.x), // Tube length is stored in areaSize.x _ => 0.0f }; float luxAtDistance = light.luxAtDistance; float solidAngle = lightType switch { LightType.Spot or LightType.Pyramid or LightType.Point => GetSolidAngle(lightType, light.enableSpotReflector, light.spotAngle, light.areaSize.x), // Pyramid aspect ratio is store in areaSize.x _ => 0.0f }; return ConvertIntensityInternal(intensity, fromUnit, toUnit, lightType, area, luxAtDistance, solidAngle); } } }