A game about forced loneliness, made by TACStudios
1using System; 2 3namespace UnityEngine.Rendering 4{ 5 /// <summary> 6 /// Light Unit Utils contains functions and definitions to facilitate conversion between different light intensity units. 7 /// </summary> 8 public static class LightUnitUtils 9 { 10 static float k_LuminanceToEvFactor => Mathf.Log(100f / ColorUtils.s_LightMeterCalibrationConstant, 2); 11 12 static float k_EvToLuminanceFactor => -k_LuminanceToEvFactor; 13 14 /// <summary> 15 /// The solid angle of a full sphere in steradians. 16 /// </summary> 17 public const float SphereSolidAngle = 4.0f * Mathf.PI; 18 19 /// <summary> 20 /// Get the unit that light intensity is measured in, for a specific light type. 21 /// </summary> 22 /// <param name="lightType">The type of light to get the native light unit for.</param> 23 /// <returns>The native unit of that light types intensity.</returns> 24 public static LightUnit GetNativeLightUnit(LightType lightType) 25 { 26 switch (lightType) 27 { 28 // Punctual lights 29 case LightType.Spot: 30 case LightType.Point: 31 case LightType.Pyramid: 32 return LightUnit.Candela; 33 34 // Directional lights 35 case LightType.Directional: 36 case LightType.Box: 37 return LightUnit.Lux; 38 39 // Area lights 40 case LightType.Rectangle: 41 case LightType.Disc: 42 case LightType.Tube: 43 return LightUnit.Nits; 44 45 default: 46 throw new ArgumentOutOfRangeException(); 47 } 48 } 49 50 /// <summary> 51 /// Check if a light types intensity can be converted to/from a light unit. 52 /// </summary> 53 /// <param name="lightType">Light type to check.</param> 54 /// <param name="lightUnit">Unit to check.</param> 55 /// <returns>True if light unit is supported.</returns> 56 public static bool IsLightUnitSupported(LightType lightType, LightUnit lightUnit) 57 { 58 const int punctualUnits = 1 << (int)LightUnit.Lumen | 59 1 << (int)LightUnit.Candela | 60 1 << (int)LightUnit.Lux | 61 1 << (int)LightUnit.Ev100; 62 63 const int directionalUnits = 1 << (int)LightUnit.Lux; 64 65 const int areaUnits = 1 << (int)LightUnit.Lumen | 66 1 << (int)LightUnit.Nits | 67 1 << (int)LightUnit.Ev100; 68 69 int lightUnitFlag = 1 << (int)lightUnit; 70 71 switch (lightType) 72 { 73 // Punctual lights 74 case LightType.Point: 75 case LightType.Spot: 76 case LightType.Pyramid: 77 return (lightUnitFlag & punctualUnits) > 0; 78 79 // Directional lights 80 case LightType.Directional: 81 case LightType.Box: 82 return (lightUnitFlag & directionalUnits) > 0; 83 84 // Area lights 85 case LightType.Rectangle: 86 case LightType.Disc: 87 case LightType.Tube: 88 return (lightUnitFlag & areaUnits) > 0; 89 90 default: 91 return false; 92 } 93 } 94 95 /// <summary> 96 /// Get the solid angle of a Point light. 97 /// </summary> 98 /// <returns>4 * Pi steradians.</returns> 99 public static float GetSolidAngleFromPointLight() 100 { 101 return SphereSolidAngle; 102 } 103 104 /// <summary> 105 /// Get the solid angle of a Spot light. 106 /// </summary> 107 /// <param name="spotAngle">The spot angle in degrees.</param> 108 /// <returns>Solid angle in steradians.</returns> 109 public static float GetSolidAngleFromSpotLight(float spotAngle) 110 { 111 double angle = Math.PI * spotAngle / 180.0; 112 double solidAngle = 2.0 * Math.PI * (1.0 - Math.Cos(angle * 0.5)); 113 return (float)solidAngle; 114 } 115 116 /// <summary> 117 /// Get the solid angle of a Pyramid light. 118 /// </summary> 119 /// <param name="spotAngle">The spot angle in degrees.</param> 120 /// <param name="aspectRatio">The aspect ratio of the pyramid.</param> 121 /// <returns>Solid angle in steradians.</returns> 122 public static float GetSolidAngleFromPyramidLight(float spotAngle, float aspectRatio) 123 { 124 if (aspectRatio < 1.0f) 125 { 126 aspectRatio = (float)(1.0 / aspectRatio); 127 } 128 129 double angleA = Math.PI * spotAngle / 180.0; 130 double length = Math.Tan(0.5 * angleA) * aspectRatio; 131 double angleB = Math.Atan(length) * 2.0; 132 double solidAngle = 4.0 * Math.Asin(Math.Sin(angleA * 0.5) * Math.Sin(angleB * 0.5)); 133 return (float)solidAngle; 134 } 135 136 internal static float GetSolidAngle(LightType lightType, bool spotReflector, float spotAngle, float aspectRatio) 137 { 138 return lightType switch 139 { 140 LightType.Spot => spotReflector ? GetSolidAngleFromSpotLight(spotAngle) : SphereSolidAngle, 141 LightType.Pyramid => spotReflector ? GetSolidAngleFromPyramidLight(spotAngle, aspectRatio) : SphereSolidAngle, 142 LightType.Point => GetSolidAngleFromPointLight(), 143 _ => throw new ArgumentException("Solid angle is undefined for lights of type " + lightType) 144 }; 145 } 146 147 /// <summary> 148 /// Get the projected surface area of a Rectangle light. 149 /// </summary> 150 /// <param name="rectSizeX">The width of the rectangle.</param> 151 /// <param name="rectSizeY">The height of the rectangle.</param> 152 /// <returns>Surface area.</returns> 153 public static float GetAreaFromRectangleLight(float rectSizeX, float rectSizeY) 154 { 155 return Mathf.Abs(rectSizeX * rectSizeY) * Mathf.PI; 156 } 157 158 /// <summary> 159 /// Get the projected surface area of a Rectangle light. 160 /// </summary> 161 /// <param name="rectSize">The size of the rectangle.</param> 162 /// <returns>Projected surface area.</returns> 163 public static float GetAreaFromRectangleLight(Vector2 rectSize) 164 { 165 return GetAreaFromRectangleLight(rectSize.x, rectSize.y); 166 } 167 168 /// <summary> 169 /// Get the projected surface area of a Disc light. 170 /// </summary> 171 /// <param name="discRadius">The radius of the disc.</param> 172 /// <returns>Projected surface area.</returns> 173 public static float GetAreaFromDiscLight(float discRadius) 174 { 175 return discRadius * discRadius * Mathf.PI * Mathf.PI; 176 } 177 178 /// <summary> 179 /// Get the projected surface area of a Tube light. 180 /// </summary> 181 /// <remarks>Note that Tube lights have no physical surface area. 182 /// Instead this method returns a value suitable for Nits&lt;=&gt;Lumen unit conversion.</remarks> 183 /// <param name="tubeLength">The length of the tube.</param> 184 /// <returns>4 * Pi * (tube length).</returns> 185 public static float GetAreaFromTubeLight(float tubeLength) 186 { 187 // Line lights expect radiance (W / (sr * m^2)) in the shader. 188 // In the UI, we specify luminous flux (power) in lumens. 189 // First, it needs to be converted to radiometric units (radian flux, W). 190 // 191 // Then we must recall how to compute power from radiance: 192 // 193 // radiance = differential_power / (differential_projected_area * differential_solid_angle), 194 // radiance = differential_power / (differential_area * differential_solid_angle * <N, L>), 195 // power = Integral{area, Integral{hemisphere, radiance * <N, L>}}. 196 // 197 // Unlike line lights, our line lights have no surface area, so the integral becomes: 198 // 199 // power = Integral{length, Integral{sphere, radiance}}. 200 // 201 // For an isotropic line light, radiance is constant, therefore: 202 // 203 // power = length * (4 * Pi) * radiance, 204 // radiance = power / (length * (4 * Pi)). 205 206 return Mathf.Abs(tubeLength) * 4.0f * Mathf.PI; 207 } 208 209 /// <summary> 210 /// Convert intensity in Lumen to Candela. 211 /// </summary> 212 /// <param name="lumen">Intensity in Lumen.</param> 213 /// <param name="solidAngle">Light solid angle in steradians.</param> 214 /// <returns>Intensity in Candela.</returns> 215 public static float LumenToCandela(float lumen, float solidAngle) 216 { 217 return lumen / solidAngle; 218 } 219 220 /// <summary> 221 /// Convert intensity in Candela to Lumen. 222 /// </summary> 223 /// <param name="candela">Intensity in Candela.</param> 224 /// <param name="solidAngle">Light solid angle in steradians.</param> 225 /// <returns>Intensity in Lumen.</returns> 226 public static float CandelaToLumen(float candela, float solidAngle) 227 { 228 return candela * solidAngle; 229 } 230 231 /// <summary> 232 /// Convert intensity in Lumen to Nits. 233 /// </summary> 234 /// <param name="lumen">Intensity in Lumen.</param> 235 /// <param name="area">Projected surface area of the light source.</param> 236 /// <returns>Intensity in Nits.</returns> 237 public static float LumenToNits(float lumen, float area) 238 { 239 return lumen / area; 240 } 241 242 /// <summary> 243 /// Convert intensity in Nits to Lumen. 244 /// </summary> 245 /// <param name="nits">Intensity in Nits.</param> 246 /// <param name="area">Projected surface area of the light source.</param> 247 /// <returns>Intensity in Lumen.</returns> 248 public static float NitsToLumen(float nits, float area) 249 { 250 return nits * area; 251 } 252 253 /// <summary> 254 /// Convert intensity in Lux to Candela. 255 /// </summary> 256 /// <param name="lux">Intensity in Lux.</param> 257 /// <param name="distance">Distance between light and surface.</param> 258 /// <returns>Intensity in Candela.</returns> 259 public static float LuxToCandela(float lux, float distance) 260 { 261 return lux * (distance * distance); 262 } 263 264 /// <summary> 265 /// Convert intensity in Candela to Lux. 266 /// </summary> 267 /// <param name="candela">Intensity in Lux.</param> 268 /// <param name="distance">Distance between light and surface.</param> 269 /// <returns>Intensity in Lux.</returns> 270 public static float CandelaToLux(float candela, float distance) 271 { 272 return candela / (distance * distance); 273 } 274 275 /// <summary> 276 /// Convert intensity in Ev100 to Nits. 277 /// </summary> 278 /// <param name="ev100">Intensity in Ev100.</param> 279 /// <returns>Intensity in Nits.</returns> 280 public static float Ev100ToNits(float ev100) 281 { 282 return Mathf.Pow(2.0f, ev100 + k_EvToLuminanceFactor); 283 } 284 285 /// <summary> 286 /// Convert intensity in Nits to Ev100. 287 /// </summary> 288 /// <param name="nits">Intensity in Nits.</param> 289 /// <returns>Intensity in Ev100.</returns> 290 public static float NitsToEv100(float nits) 291 { 292 return Mathf.Log(nits, 2) + k_LuminanceToEvFactor; 293 } 294 295 /// <summary> 296 /// Convert intensity in Ev100 to Candela. 297 /// </summary> 298 /// <param name="ev100">Intensity in Ev100.</param> 299 /// <returns>Intensity in Candela.</returns> 300 public static float Ev100ToCandela(float ev100) 301 { 302 return Ev100ToNits(ev100); 303 } 304 305 /// <summary> 306 /// Convert intensity in Candela to Ev100. 307 /// </summary> 308 /// <param name="candela">Intensity in Candela.</param> 309 /// <returns>Intensity in Ev100.</returns> 310 public static float CandelaToEv100(float candela) 311 { 312 return NitsToEv100(candela); 313 } 314 315 internal static float ConvertIntensityInternal(float intensity, LightUnit fromUnit, LightUnit toUnit, 316 LightType lightType, float area, float luxAtDistance, float solidAngle) 317 { 318 if (!IsLightUnitSupported(lightType, fromUnit) || !IsLightUnitSupported(lightType, toUnit)) 319 { 320 throw new ArgumentException("Converting " + fromUnit + " to " + toUnit 321 + " is undefined for lights of type " + lightType); 322 } 323 324 if (fromUnit == toUnit) 325 { 326 return intensity; 327 } 328 329 switch (fromUnit) 330 { 331 case LightUnit.Lumen: 332 { 333 switch (toUnit) 334 { 335 case LightUnit.Candela: 336 { 337 // Lumen => Candela: 338 return LumenToCandela(intensity, solidAngle); 339 } 340 341 case LightUnit.Lux: 342 { 343 // Lumen => Candela => Lux 344 float candela = LumenToCandela(intensity, solidAngle); 345 return CandelaToLux(candela, luxAtDistance); 346 } 347 348 case LightUnit.Nits: 349 { 350 // Lumen => Nits 351 return LumenToNits(intensity, area); 352 } 353 354 case LightUnit.Ev100: 355 { 356 // Lumen => Candela/Nits => Ev100 357 float candelaNits = lightType switch 358 { 359 LightType.Point or LightType.Spot or LightType.Pyramid => 360 LumenToCandela(intensity, solidAngle), 361 362 LightType.Rectangle or LightType.Disc or LightType.Tube => 363 LumenToNits(intensity, area), 364 365 _ => 366 throw new ArgumentException("Converting from Lumen to Ev100 is undefined for light type " 367 + lightType) 368 }; 369 return NitsToEv100(candelaNits); 370 } 371 372 default: 373 throw new ArgumentOutOfRangeException(nameof(toUnit), toUnit, null); 374 } 375 } 376 377 case LightUnit.Candela: 378 { 379 switch (toUnit) 380 { 381 case LightUnit.Lumen: 382 { 383 // Candela => Lumen 384 return CandelaToLumen(intensity, solidAngle); 385 } 386 387 case LightUnit.Lux: 388 { 389 // Candela => Lux 390 return CandelaToLux(intensity, luxAtDistance); 391 } 392 393 case LightUnit.Ev100: 394 { 395 // Candela => Ev100 396 return NitsToEv100(intensity); 397 } 398 399 default: 400 throw new ArgumentOutOfRangeException(nameof(toUnit), toUnit, null); 401 } 402 } 403 404 case LightUnit.Lux: 405 { 406 switch (toUnit) 407 { 408 case LightUnit.Lumen: 409 { 410 // Lux => Candela => Lumen 411 float candela = LuxToCandela(intensity, luxAtDistance); 412 return CandelaToLumen(candela, solidAngle); 413 } 414 415 case LightUnit.Candela: 416 { 417 // Lux => Candela 418 return LuxToCandela(intensity, luxAtDistance); 419 } 420 421 case LightUnit.Ev100: 422 { 423 // Lux => Candela => Ev100 424 float candela = LuxToCandela(intensity, luxAtDistance); 425 return NitsToEv100(candela); 426 } 427 428 default: 429 throw new ArgumentOutOfRangeException(nameof(toUnit), toUnit, null); 430 } 431 } 432 433 case LightUnit.Nits: 434 { 435 switch (toUnit) 436 { 437 case LightUnit.Lumen: 438 { 439 return NitsToLumen(intensity, area); 440 } 441 442 case LightUnit.Ev100: 443 { 444 return NitsToEv100(intensity); 445 } 446 447 default: 448 throw new ArgumentOutOfRangeException(nameof(toUnit), toUnit, null); 449 } 450 } 451 452 case LightUnit.Ev100: 453 { 454 switch (toUnit) 455 { 456 case LightUnit.Lumen: 457 { 458 // Ev100 => Candela/Nits => Lumen 459 float candelaOrNits = Ev100ToNits(intensity); 460 return lightType switch 461 { 462 LightType.Point or LightType.Spot or LightType.Pyramid => 463 CandelaToLumen(candelaOrNits, solidAngle), 464 465 LightType.Rectangle or LightType.Disc or LightType.Tube => 466 NitsToLumen(candelaOrNits, area), 467 468 _ => 469 throw new ArgumentException("Converting from Lumen to Ev100 is undefined for light type " 470 + lightType) 471 }; 472 } 473 474 case LightUnit.Nits: 475 case LightUnit.Candela: 476 { 477 // Ev100 => Candela/Nits 478 return Ev100ToNits(intensity); 479 } 480 481 case LightUnit.Lux: 482 { 483 // Ev100 => Candela => Lux 484 float candela = Ev100ToNits(intensity); 485 return CandelaToLux(candela, luxAtDistance); 486 } 487 488 default: 489 throw new ArgumentOutOfRangeException(nameof(toUnit), toUnit, null); 490 } 491 } 492 default: 493 throw new ArgumentOutOfRangeException(nameof(fromUnit), fromUnit, null); 494 } 495 } 496 497 /// <summary> 498 /// Convert intensity from one unit to another using the parameters of a given Light. 499 /// </summary> 500 /// <param name="light">Light to use parameters from.</param> 501 /// <param name="intensity">Intensity to be converted.</param> 502 /// <param name="fromUnit">Unit to convert from.</param> 503 /// <param name="toUnit">Unit to convert to.</param> 504 /// <returns>Converted intensity.</returns> 505 public static float ConvertIntensity(Light light, float intensity, LightUnit fromUnit, LightUnit toUnit) 506 { 507 LightType lightType = light.type; 508 float area = lightType switch 509 { 510 LightType.Rectangle => GetAreaFromRectangleLight(light.areaSize), 511 LightType.Disc => GetAreaFromDiscLight(light.areaSize.x), // Disc radius is stored in areaSize.x 512 LightType.Tube => GetAreaFromTubeLight(light.areaSize.x), // Tube length is stored in areaSize.x 513 _ => 0.0f 514 }; 515 float luxAtDistance = light.luxAtDistance; 516 float solidAngle = lightType switch 517 { 518 LightType.Spot or LightType.Pyramid or LightType.Point => GetSolidAngle(lightType, light.enableSpotReflector, 519 light.spotAngle, light.areaSize.x), // Pyramid aspect ratio is store in areaSize.x 520 _ => 0.0f 521 }; 522 523 return ConvertIntensityInternal(intensity, fromUnit, toUnit, lightType, area, luxAtDistance, solidAngle); 524 } 525 } 526}