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);
}
}
}