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<=>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}