A game about forced loneliness, made by TACStudios
1using System;
2using UnityEngine;
3using UnityEngine.Rendering;
4
5namespace UnityEditor.Rendering
6{
7 internal static class LightIntensitySlider
8 {
9 // Note: To have the right icons along the skin, we do not use the editor resource loading mechanism at the moment. This could be revisited once this is converted to UITK.
10 static Texture2D GetLightUnitIcon(string name)
11 {
12 return CoreEditorUtils.LoadIcon(@"Packages/com.unity.render-pipelines.core/Editor/Lighting/Icons/LightUnitIcons", name, ".png");
13 }
14
15 // TODO: Move all light unit icons from the package into the built-in resources.
16 static Texture2D Candlelight = GetLightUnitIcon("Candlelight");
17 static Texture2D DecorativeLight = GetLightUnitIcon("DecorativeLight");
18 static Texture2D ExteriorLight = GetLightUnitIcon("ExteriorLight");
19 static Texture2D InteriorLight = GetLightUnitIcon("InteriorLight");
20 static Texture2D Moonlight = GetLightUnitIcon("Moonlight");
21 static Texture2D Overcast = GetLightUnitIcon("Overcast");
22 static Texture2D SunriseSunset = GetLightUnitIcon("SunriseSunset");
23 static Texture2D BrightSky = GetLightUnitIcon("BrightSky");
24
25 static GUIStyle k_IconButton = new ("IconButton");
26
27 private static readonly LightUnitSliderUIRange[] LumenRanges =
28 {
29 new (Candlelight, "Candle", new Vector2(0, 15), 12.5f),
30 new (DecorativeLight, "Decorative", new Vector2(15, 300), 100),
31 new (InteriorLight, "Interior", new Vector2(300, 3000), 1000),
32 new (ExteriorLight, "Exterior", new Vector2(3000, 40000), 10000),
33 };
34 private static readonly float[] LumenDistribution = { 0f, 0.25f, 0.5f, 0.75f, 1f };
35
36 private static readonly LightUnitSliderUIRange[] LuxRanges =
37 {
38 new (Moonlight, "Moon", new Vector2(0, 1), 0.5f),
39 new (SunriseSunset, "Low Sun", new Vector2(1, 10000), 5000),
40 new (Overcast, "Cloudy", new Vector2(10000, 80000), 20000),
41 new (BrightSky, "High Sun", new Vector2(80000, 130000), 100000),
42 };
43 private static readonly float[] LuxDistribution = { 0.0f, 0.05f, 0.5f, 0.9f, 1.0f };
44
45 private const float ConstantNitsToLumenArea = 200.0f;
46
47 internal static void Draw(ISerializedLight serialized, Editor owner, Rect baseRect)
48 {
49 // Calculate UI rects
50 Rect sliderRect, iconRect;
51 {
52 sliderRect = baseRect;
53 sliderRect.width -= EditorGUIUtility.singleLineHeight;
54
55 iconRect = baseRect;
56 iconRect.x += sliderRect.width;
57 iconRect.width = EditorGUIUtility.singleLineHeight;
58 }
59
60 Light light = serialized.settings.light;
61 LightType lightType = serialized.settings.lightType.GetEnumValue<LightType>();
62 LightUnit nativeUnit = LightUnitUtils.GetNativeLightUnit(lightType);
63 LightUnit lightUnit = serialized.settings.lightUnit.GetEnumValue<LightUnit>();
64 bool usesLuxBasedRange = lightType == LightType.Directional;
65
66 LightUnitSliderUIRange[] ranges = usesLuxBasedRange ? LuxRanges : LumenRanges;
67 float[] distribution = usesLuxBasedRange ? LuxDistribution : LumenDistribution;
68
69 // Verify that ui light unit is in fact supported or revert to native.
70 lightUnit = LightUnitUtils.IsLightUnitSupported(lightType, lightUnit) ? lightUnit : nativeUnit;
71
72 Debug.Assert(ranges.Length == distribution.Length - 1);
73
74 // This intensity is in the native light unit for the light's type
75 float nativeIntensity = serialized.settings.intensity.floatValue;
76 // This is the intensity above converted to the unit (either lux or lumen) that's the basis of the ranges/distribution for this light type
77 float convertedIntensity;
78 bool isSpotReflectorRelevant = (lightType == LightType.Pyramid || lightType == LightType.Spot) &&
79 lightUnit == LightUnit.Lumen &&
80 serialized.settings.enableSpotReflector.boolValue;
81
82 if (lightType == LightType.Pyramid || lightType == LightType.Spot || lightType == LightType.Box)
83 {
84 // For Box light, we want to use the Lumen style ranges,
85 // but Lumen is not defined for Box lights,
86 // so we just pretend its native type is Candela.
87 float solidAngle = LightUnitUtils.SphereSolidAngle;
88 if (isSpotReflectorRelevant)
89 {
90 // If spot reflector matters for this type of light,
91 // calculate lumen as if spot reflector is on;
92 // This prevents the slider from moving around when solid angle params change.
93 solidAngle = LightUnitUtils.GetSolidAngle(lightType, true, light.spotAngle, light.areaSize.x);
94 }
95 convertedIntensity = LightUnitUtils.CandelaToLumen(nativeIntensity, solidAngle);
96 }
97 else if (nativeUnit == LightUnit.Nits && lightUnit != LightUnit.Lumen)
98 {
99 convertedIntensity = LightUnitUtils.NitsToLumen(nativeIntensity, ConstantNitsToLumenArea);
100 }
101 else
102 {
103 LightUnit toUnit = usesLuxBasedRange ? LightUnit.Lux : LightUnit.Lumen;
104 convertedIntensity = LightUnitUtils.ConvertIntensity(light, nativeIntensity, nativeUnit, toUnit);
105 }
106
107 // Check which preset level we are in. If we're within a preset range,
108 // this index will contain the index of that preset. If we're below all
109 // preset ranges, the value will be -2, and if we're above all preset
110 // ranges, the value will be -1. Also calculate the min and max values
111 // of the slider.
112 int rangeIndex = -3;
113 float minValue = float.MaxValue;
114 float maxValue = float.MinValue;
115 for (int i = 0; i < ranges.Length; i++)
116 {
117 var l = ranges[i];
118 if (convertedIntensity >= l.value.x && convertedIntensity <= l.value.y)
119 {
120 rangeIndex = i;
121 }
122
123 minValue = Mathf.Min(minValue, l.value.x);
124 maxValue = Mathf.Max(maxValue, l.value.y);
125 }
126
127 if (rangeIndex < 0)
128 {
129 // ^ The current value doesn't lie within a preset range.
130 // If it is less than the minimum preset, it is below the
131 // whole slider's range, otherwise, it is above it.
132 rangeIndex = (convertedIntensity < minValue) ? -2 : -1;
133 }
134
135 // Draw the slider
136 float sliderValue;
137 using (new EditorGUI.IndentLevelScope(-EditorGUI.indentLevel))
138 {
139 if (rangeIndex == -2)
140 {
141 // ^ The current value is below the slider's range
142 sliderValue = 0f;
143 }
144 else if (rangeIndex == -1)
145 {
146 // ^ The current value is above the slider's range
147 sliderValue = 1f;
148 }
149 else
150 {
151 // Map the intensity value into the [0, 1] range via a non-linear piecewise mapping
152 Vector2 r = ranges[rangeIndex].value;
153 Vector2 d = new Vector2(distribution[rangeIndex], distribution[rangeIndex + 1]);
154 sliderValue = (d.x - d.y) / (r.x - r.y) * (convertedIntensity - r.x) + d.x;
155 }
156
157 EditorGUI.BeginChangeCheck();
158 float newSliderValue = GUI.HorizontalSlider(sliderRect, sliderValue, 0f, 1f);
159 if (EditorGUI.EndChangeCheck())
160 {
161 bool newRangeFound = false;
162 for (int i = 0; i < ranges.Length; i++)
163 {
164 if (newSliderValue >= distribution[i] && newSliderValue <= distribution[i + 1])
165 {
166 rangeIndex = i;
167 newRangeFound = true;
168 break;
169 }
170 }
171
172 Debug.Assert(newRangeFound);
173 // Map the slider value in the [0, 1] range to the intensity value via a non-linear piecewise
174 // mapping
175 Vector2 r = ranges[rangeIndex].value;
176 Vector2 d = new Vector2(distribution[rangeIndex], distribution[rangeIndex + 1]);
177 float newConvertedIntensity = (r.x - r.y) / (d.x - d.y) * (newSliderValue - d.x) + r.x;
178
179 if (lightType == LightType.Pyramid || lightType == LightType.Spot || lightType == LightType.Box)
180 {
181 float solidAngle = LightUnitUtils.SphereSolidAngle;
182 if (isSpotReflectorRelevant)
183 {
184 solidAngle = LightUnitUtils.GetSolidAngle(lightType, true, light.spotAngle, light.areaSize.x);
185 }
186 serialized.settings.intensity.floatValue = LightUnitUtils.LumenToCandela(newConvertedIntensity, solidAngle);
187 }
188 else if (nativeUnit == LightUnit.Nits && lightUnit != LightUnit.Lumen)
189 {
190 serialized.settings.intensity.floatValue = LightUnitUtils.LumenToNits(newConvertedIntensity, ConstantNitsToLumenArea);
191 }
192 else
193 {
194 LightUnit fromUnit = usesLuxBasedRange ? LightUnit.Lux : LightUnit.Lumen;
195 serialized.settings.intensity.floatValue = LightUnitUtils.ConvertIntensity(light, newConvertedIntensity, fromUnit, nativeUnit);
196 }
197 }
198 }
199
200 GUIContent GetTooltip(string rangeName, float intensity)
201 {
202 float uiIntensity;
203
204 if (lightType == LightType.Box)
205 {
206 float candelaIntensity = LightUnitUtils.LumenToCandela(intensity, LightUnitUtils.SphereSolidAngle);
207 uiIntensity = candelaIntensity;
208 }
209 else if (nativeUnit == LightUnit.Nits && lightUnit != LightUnit.Lumen)
210 {
211 uiIntensity = LightUnitUtils.LumenToNits(intensity, ConstantNitsToLumenArea);
212 }
213 else
214 {
215 LightUnit fromUnit = usesLuxBasedRange ? LightUnit.Lux : LightUnit.Lumen;
216 uiIntensity = LightUnitUtils.ConvertIntensity(light, intensity, fromUnit, lightUnit);
217 }
218
219 string formatValue = uiIntensity < 100 ? $"{uiIntensity:n}" : $"{uiIntensity:n0}";
220 return new GUIContent(string.Empty, $"{rangeName} | {formatValue} {lightUnit.ToString()}");
221 }
222
223 // Draw the markers on the slider
224 for (int i = 0; i < ranges.Length; i++)
225 {
226 const float kMarkerWidth = 2f;
227 const float kMarkerHeight = 2f;
228 const float kMarkerTooltipSize = 16f;
229
230 var markerRect = new Rect(
231 sliderRect.x + distribution[i + 1] * sliderRect.width - kMarkerWidth * 0.5f,
232 sliderRect.y + (EditorGUIUtility.singleLineHeight / 2f) - 1,
233 kMarkerWidth,
234 kMarkerHeight
235 );
236
237 // Draw marker by manually drawing the rect, and an empty label with the tooltip.
238 Color kDarkThemeColor = new Color32(153, 153, 153, 255);
239 Color kLiteThemeColor = new Color32(97, 97, 97, 255);
240 EditorGUI.DrawRect(markerRect, EditorGUIUtility.isProSkin ? kDarkThemeColor : kLiteThemeColor);
241
242 // Scale the marker tooltip for easier discovery
243 Rect markerTooltipRect = new(
244 markerRect.x - kMarkerTooltipSize * 0.5f,
245 markerRect.y - kMarkerTooltipSize * 0.5f,
246 kMarkerTooltipSize * (i < ranges.Length - 1 ? 1f : 0.5f),
247 kMarkerTooltipSize
248 );
249
250 // Temporarily remove indent level, otherwise our custom-positioned tooltip label field will also be
251 // indented
252 int indent = EditorGUI.indentLevel;
253 EditorGUI.indentLevel = 0;
254 EditorGUI.LabelField(markerTooltipRect, GetTooltip(ranges[i].content.tooltip, ranges[i].value.y));
255 EditorGUI.indentLevel = indent;
256 }
257
258 GUIContent content;
259 Vector2 range;
260 if (rangeIndex < 0)
261 {
262 string tooltip = usesLuxBasedRange ? "Higher than Sunlight" : "Very high intensity light";
263 content = new GUIContent(EditorGUIUtility.TrIconContent("console.warnicon").image, tooltip);
264 float minOrMaxValue = (convertedIntensity < minValue) ? minValue : maxValue;
265 range = new Vector2(-1, minOrMaxValue);
266 }
267 else
268 {
269 content = ranges[rangeIndex].content;
270 range = ranges[rangeIndex].value;
271 }
272 // Draw the context menu feedback before the icon
273 GUI.Box(iconRect, GUIContent.none, k_IconButton);
274 // Draw the icon
275 {
276 var oldColor = GUI.color;
277 GUI.color = Color.clear;
278 EditorGUI.DrawTextureTransparent(iconRect, content.image);
279 GUI.color = oldColor;
280 }
281 // Draw the thumbnail tooltip and the knob tooltip
282 {
283 // Temporarily remove indent level, otherwise our custom-positioned tooltip label field will also be
284 // indented
285 int indent = EditorGUI.indentLevel;
286 EditorGUI.indentLevel = 0;
287
288 EditorGUI.LabelField(iconRect, GetTooltip(content.tooltip, range.y));
289
290 const float knobSize = 10f;
291 Rect knobRect = new(
292 sliderRect.x + (sliderRect.width - knobSize) * sliderValue,
293 sliderRect.y + (sliderRect.height - knobSize) * 0.5f,
294 knobSize,
295 knobSize
296 );
297 EditorGUI.LabelField(knobRect, GetTooltip(content.tooltip, convertedIntensity));
298
299 EditorGUI.indentLevel = indent;
300 }
301 // Handle events for context menu
302 var e = Event.current;
303 if (e.type == EventType.MouseDown && e.button == 0)
304 {
305 if (iconRect.Contains(e.mousePosition))
306 {
307 var menuPosition = iconRect.position + iconRect.size;
308 var menu = new GenericMenu();
309
310 for (int i = ranges.Length - 1; i >= 0; --i)
311 {
312 // Indicate a checkmark if the value is within this preset range.
313 LightUnitSliderUIRange preset = ranges[i];
314 float nativePresetValue;
315 if (lightType == LightType.Pyramid || lightType == LightType.Spot || lightType == LightType.Box)
316 {
317 float solidAngle = LightUnitUtils.SphereSolidAngle;
318 if (isSpotReflectorRelevant)
319 {
320 solidAngle = LightUnitUtils.GetSolidAngle(lightType, true, light.spotAngle, light.areaSize.x);
321 }
322 nativePresetValue = LightUnitUtils.LumenToCandela(preset.presetValue, solidAngle);
323 }
324 else if (nativeUnit == LightUnit.Nits && lightUnit != LightUnit.Lumen)
325 {
326 nativePresetValue = LightUnitUtils.LumenToNits(preset.presetValue, ConstantNitsToLumenArea);
327 }
328 else
329 {
330 LightUnit fromUnit = usesLuxBasedRange ? LightUnit.Lux : LightUnit.Lumen;
331 nativePresetValue = LightUnitUtils.ConvertIntensity(light, preset.presetValue, fromUnit, nativeUnit);
332 }
333
334 menu.AddItem(
335 EditorGUIUtility.TrTextContent(preset.content.tooltip),
336 rangeIndex == i,
337 () => SetIntensityValue(serialized, nativePresetValue)
338 );
339 }
340
341 menu.DropDown(new Rect(menuPosition, Vector2.zero));
342 e.Use();
343 }
344 }
345 }
346
347 static void SetIntensityValue(ISerializedLight serialized, float intensity)
348 {
349 serialized.Update();
350 serialized.settings.intensity.floatValue = intensity;
351 serialized.Apply();
352 }
353 }
354}