A game about forced loneliness, made by TACStudios
1using UnityEngine;
2using System;
3using UnityEngine.UIElements;
4using UnityEditor.UIElements;
5using UnityEngine.Experimental.Rendering;
6
7namespace UnityEditor.Rendering.LookDev
8{
9 /// <summary>
10 /// Lighting environment used in LookDev
11 /// </summary>
12 public class Environment : ScriptableObject
13 {
14 [SerializeField]
15 string m_CubemapGUID;
16 Cubemap m_Cubemap;
17
18 internal bool isCubemapOnly { get; private set; } = false;
19
20 /// <summary>
21 /// Offset on the longitude. Affect both sky and sun position in Shadow part
22 /// </summary>
23 public float rotation = 0.0f;
24 /// <summary>
25 /// Exposure to use with this Sky
26 /// </summary>
27 public float exposure = 0f;
28
29 // Setup default position to be on the sun in the default HDRI.
30 // This is important as the defaultHDRI don't call the set brightest spot function on first call.
31 [SerializeField]
32 float m_Latitude = 60.0f; // [-90..90]
33 [SerializeField]
34 float m_Longitude = 299.0f; // [0..360[
35
36 /// <summary>
37 /// The shading tint to used when computing shadow from sun
38 /// </summary>
39 public Color shadowColor = new Color(0.7f, 0.7f, 0.7f);
40
41 /// <summary>
42 /// The cubemap used for this part of the lighting environment
43 /// </summary>
44 public Cubemap cubemap
45 {
46 get
47 {
48 if (m_Cubemap == null || m_Cubemap.Equals(null))
49 LoadCubemap();
50 return m_Cubemap;
51 }
52 set
53 {
54 m_Cubemap = value;
55 m_CubemapGUID = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(m_Cubemap));
56 }
57 }
58
59 /// <summary>
60 /// The Latitude position of the sun casting shadows
61 /// </summary>
62 public float sunLatitude
63 {
64 get => m_Latitude;
65 set => m_Latitude = ClampLatitude(value);
66 }
67
68 /// <summary>
69 /// The Longitude position of the sun casting shadows
70 /// </summary>
71 public float sunLongitude
72 {
73 get => m_Longitude;
74 set => m_Longitude = ClampLongitude(value);
75 }
76
77 internal static float ClampLatitude(float value) => Mathf.Clamp(value, -90, 90);
78
79 internal static float ClampLongitude(float value)
80 {
81 value = value % 360f;
82 if (value < 0.0)
83 value += 360f;
84 return value;
85 }
86
87 internal void UpdateSunPosition(Light sun)
88 => sun.transform.rotation = Quaternion.Euler(sunLatitude, rotation + sunLongitude, 0f);
89
90 /// <summary>
91 /// Compute sun position to be brightest spot of the sky
92 /// </summary>
93 public void ResetToBrightestSpot()
94 => EnvironmentElement.ResetToBrightestSpot(this);
95
96 void LoadCubemap()
97 {
98 m_Cubemap = null;
99
100 GUID storedGUID;
101 GUID.TryParse(m_CubemapGUID, out storedGUID);
102 if (!storedGUID.Empty())
103 {
104 string path = AssetDatabase.GUIDToAssetPath(m_CubemapGUID);
105 m_Cubemap = AssetDatabase.LoadAssetAtPath<Cubemap>(path);
106 }
107 }
108
109 internal void CopyTo(Environment other)
110 {
111 other.cubemap = cubemap;
112 other.exposure = exposure;
113 other.rotation = rotation;
114 other.sunLatitude = sunLatitude;
115 other.sunLongitude = sunLongitude;
116 other.shadowColor = shadowColor;
117 other.name = name + " (copy)";
118 }
119
120 /// <summary>
121 /// Implicit conversion operator to runtime version of sky datas
122 /// </summary>
123 public UnityEngine.Rendering.LookDev.Sky sky
124 => new UnityEngine.Rendering.LookDev.Sky()
125 {
126 cubemap = cubemap,
127 longitudeOffset = rotation,
128 exposure = exposure
129 };
130
131 internal static Environment GetTemporaryEnvironmentForCubemap(Cubemap cubemap)
132 {
133 Environment result = ScriptableObject.CreateInstance<Environment>();
134 result.cubemap = cubemap;
135 result.isCubemapOnly = true;
136 return result;
137 }
138
139 internal bool HasCubemapAssetChanged(Cubemap cubemap)
140 {
141 if (cubemap == null)
142 return !String.IsNullOrEmpty(m_CubemapGUID);
143
144 return m_CubemapGUID != AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(cubemap));
145 }
146 }
147
148 [CustomEditor(typeof(Environment))]
149 class EnvironmentEditor : Editor
150 {
151 //display nothing
152 public sealed override VisualElement CreateInspectorGUI() => null;
153
154 // Don't use ImGUI
155 public sealed override void OnInspectorGUI() { }
156
157 //but make preview in Project window
158 override public Texture2D RenderStaticPreview(string assetPath, UnityEngine.Object[] subAssets, int width, int height)
159 => EnvironmentElement.GetLatLongThumbnailTexture(target as Environment, width);
160 }
161
162 interface IBendable<T>
163 {
164 void Bind(T data);
165 }
166
167 class EnvironmentElement : VisualElement, IBendable<Environment>
168 {
169 internal const int k_SkyThumbnailWidth = 200;
170 internal const int k_SkyThumbnailHeight = 100;
171 static Material s_cubeToLatlongMaterial;
172 static Material cubeToLatlongMaterial
173 {
174 get
175 {
176 if (s_cubeToLatlongMaterial == null || s_cubeToLatlongMaterial.Equals(null))
177 {
178 s_cubeToLatlongMaterial = new Material(Shader.Find("Hidden/LookDev/CubeToLatlong"));
179 }
180 return s_cubeToLatlongMaterial;
181 }
182 }
183
184 VisualElement environmentParams;
185 Environment environment;
186
187 Image latlong;
188 ObjectField skyCubemapField;
189 FloatField skyRotationOffset;
190 FloatField skyExposureField;
191 Vector2Field sunPosition;
192 ColorField shadowColor;
193 TextField environmentName;
194
195 Action OnChangeCallback;
196
197 public Environment target => environment;
198
199 public EnvironmentElement() => Create(withPreview: true);
200 public EnvironmentElement(bool withPreview, Action OnChangeCallback = null)
201 {
202 this.OnChangeCallback = OnChangeCallback;
203 Create(withPreview);
204 }
205
206 public EnvironmentElement(Environment environment)
207 {
208 Create(withPreview: true);
209 Bind(environment);
210 }
211
212 void Create(bool withPreview)
213 {
214 if (withPreview)
215 {
216 latlong = new Image();
217 latlong.style.width = k_SkyThumbnailWidth;
218 latlong.style.height = k_SkyThumbnailHeight;
219 Add(latlong);
220 }
221
222 environmentParams = GetDefaultInspector();
223 Add(environmentParams);
224 }
225
226 public void Bind(Environment environment)
227 {
228 this.environment = environment;
229 if (environment == null || environment.Equals(null))
230 return;
231
232 if (latlong != null && !latlong.Equals(null))
233 latlong.image = GetLatLongThumbnailTexture();
234 skyCubemapField.SetValueWithoutNotify(environment.cubemap);
235 skyRotationOffset.SetValueWithoutNotify(environment.rotation);
236 skyExposureField.SetValueWithoutNotify(environment.exposure);
237 sunPosition.SetValueWithoutNotify(new Vector2(environment.sunLongitude, environment.sunLatitude));
238 shadowColor.SetValueWithoutNotify(environment.shadowColor);
239 environmentName.SetValueWithoutNotify(environment.name);
240 }
241
242 public void Bind(Environment environment, Image deportedLatlong)
243 {
244 latlong = deportedLatlong;
245 Bind(environment);
246 }
247
248 static public Vector2 PositionToLatLong(Vector2 position)
249 {
250 Vector2 result = new Vector2();
251 result.x = position.y * Mathf.PI * 0.5f * Mathf.Rad2Deg;
252 result.y = (position.x * 0.5f + 0.5f) * 2f * Mathf.PI * Mathf.Rad2Deg;
253
254 if (result.x < -90.0f) result.x = -90f;
255 if (result.x > 90.0f) result.x = 90f;
256
257 return result;
258 }
259
260 public static void ResetToBrightestSpot(Environment environment)
261 {
262 cubeToLatlongMaterial.SetTexture("_MainTex", environment.cubemap);
263 cubeToLatlongMaterial.SetVector("_WindowParams", new Vector4(10000, -1000.0f, 2, 0.0f)); // Neutral value to not clip
264 cubeToLatlongMaterial.SetVector("_CubeToLatLongParams", new Vector4(Mathf.Deg2Rad * environment.rotation, 0.5f, 1.0f, 3.0f)); // We use LOD 3 to take a region rather than a single pixel in the map
265 cubeToLatlongMaterial.SetPass(0);
266
267 int width = k_SkyThumbnailWidth;
268 int height = width >> 1;
269
270 RenderTexture temporaryRT = new RenderTexture(width, height, 0, GraphicsFormat.R8G8B8A8_SRGB);
271 Texture2D brightestPointTexture = new Texture2D(width, height, GraphicsFormat.R16G16B16A16_SFloat, TextureCreationFlags.None);
272
273 // Convert cubemap to a 2D LatLong to read on CPU
274 Graphics.Blit(environment.cubemap, temporaryRT, cubeToLatlongMaterial);
275 brightestPointTexture.ReadPixels(new Rect(0, 0, width, height), 0, 0, false);
276 brightestPointTexture.Apply();
277
278 // CPU read back
279 // From Doc: The returned array is a flattened 2D array, where pixels are laid out left to right, bottom to top (i.e. row after row)
280 Color[] color = brightestPointTexture.GetPixels();
281 RenderTexture.active = null;
282 temporaryRT.Release();
283
284 float maxLuminance = 0.0f;
285 int maxIndex = 0;
286 for (int index = height * width - 1; index >= 0; --index)
287 {
288 Color pixel = color[index];
289 float luminance = pixel.r * 0.2126729f + pixel.g * 0.7151522f + pixel.b * 0.0721750f;
290 if (maxLuminance < luminance)
291 {
292 maxLuminance = luminance;
293 maxIndex = index;
294 }
295 }
296 Vector2 sunPosition = PositionToLatLong(new Vector2(((maxIndex % width) / (float)(width - 1)) * 2f - 1f, ((maxIndex / width) / (float)(height - 1)) * 2f - 1f));
297 environment.sunLatitude = sunPosition.x;
298 environment.sunLongitude = sunPosition.y - environment.rotation;
299 }
300
301 public Texture2D GetLatLongThumbnailTexture()
302 => GetLatLongThumbnailTexture(environment, k_SkyThumbnailWidth);
303
304 public static Texture2D GetLatLongThumbnailTexture(Environment environment, int width)
305 {
306 int height = width >> 1;
307 RenderTexture oldActive = RenderTexture.active;
308 RenderTexture temporaryRT = new RenderTexture(width, height, 0, GraphicsFormat.R8G8B8A8_SRGB);
309 RenderTexture.active = temporaryRT;
310 cubeToLatlongMaterial.SetTexture("_MainTex", environment.cubemap);
311 cubeToLatlongMaterial.SetVector("_WindowParams",
312 new Vector4(
313 height, //height
314 -1000f, //y position, -1000f to be sure to not have clipping issue (we should not clip normally but don't want to create a new shader)
315 2f, //margin value
316 1f)); //Pixel per Point
317 cubeToLatlongMaterial.SetVector("_CubeToLatLongParams",
318 new Vector4(
319 Mathf.Deg2Rad * environment.rotation, //rotation of the environment in radian
320 1f, //alpha
321 1f, //intensity
322 0f)); //LOD
323 cubeToLatlongMaterial.SetPass(0);
324 GL.LoadPixelMatrix(0, width, height, 0);
325 GL.Clear(true, true, Color.black);
326 Rect skyRect = new Rect(0, 0, width, height);
327 Renderer.DrawFullScreenQuad(skyRect);
328
329 Texture2D result = new Texture2D(width, height, GraphicsFormat.R8G8B8A8_SRGB, TextureCreationFlags.None);
330 result.ReadPixels(new Rect(0, 0, width, height), 0, 0, false);
331 result.Apply(false);
332 RenderTexture.active = oldActive;
333 UnityEngine.Object.DestroyImmediate(temporaryRT);
334 return result;
335 }
336
337 public VisualElement GetDefaultInspector()
338 {
339 VisualElement inspector = new VisualElement() { name = "inspector" };
340
341 VisualElement header = new VisualElement() { name = "inspector-header" };
342 header.Add(new Image()
343 {
344 image = CoreEditorUtils.LoadIcon(@"Packages/com.unity.render-pipelines.core/Editor/LookDev/Icons/", "Environment", forceLowRes: true)
345 });
346 environmentName = new TextField();
347 environmentName.isDelayed = true;
348 environmentName.RegisterValueChangedCallback(evt =>
349 {
350 string path = AssetDatabase.GetAssetPath(environment);
351 environment.name = evt.newValue;
352 AssetDatabase.SetLabels(environment, new string[] { evt.newValue });
353 EditorUtility.SetDirty(environment);
354 AssetDatabase.ImportAsset(path);
355 environmentName.name = environment.name;
356 });
357 header.Add(environmentName);
358 inspector.Add(header);
359
360 Foldout foldout = new Foldout()
361 {
362 text = "Environment Settings"
363 };
364 skyCubemapField = new ObjectField("Sky with Sun")
365 {
366 tooltip = "A cubemap that will be used as the sky."
367 };
368 skyCubemapField.allowSceneObjects = false;
369 skyCubemapField.objectType = typeof(Cubemap);
370 skyCubemapField.RegisterValueChangedCallback(evt =>
371 {
372 var tmp = environment.cubemap;
373 RegisterChange(ref tmp, evt.newValue as Cubemap, updatePreview: true, customResync: () => environment.cubemap = tmp);
374 });
375 foldout.Add(skyCubemapField);
376
377 skyRotationOffset = new FloatField("Rotation")
378 {
379 tooltip = "Rotation offset on the longitude of the sky."
380 };
381 skyRotationOffset.RegisterValueChangedCallback(evt
382 => RegisterChange(ref environment.rotation, Environment.ClampLongitude(evt.newValue), skyRotationOffset, updatePreview: true));
383 foldout.Add(skyRotationOffset);
384
385 skyExposureField = new FloatField("Exposure")
386 {
387 tooltip = "The exposure to apply with this sky."
388 };
389 skyExposureField.RegisterValueChangedCallback(evt
390 => RegisterChange(ref environment.exposure, evt.newValue));
391 foldout.Add(skyExposureField);
392 var style = foldout.Q<Toggle>().style;
393 style.marginLeft = 3;
394 style.unityFontStyleAndWeight = FontStyle.Bold;
395 inspector.Add(foldout);
396
397 sunPosition = new Vector2Field("Sun Position")
398 {
399 tooltip = "The sun position as (Longitude, Latitude)\nThe button compute brightest position in the sky with sun."
400 };
401 sunPosition.Q("unity-x-input").Q<FloatField>().formatString = "n1";
402 sunPosition.Q("unity-y-input").Q<FloatField>().formatString = "n1";
403 sunPosition.RegisterValueChangedCallback(evt =>
404 {
405 var tmpContainer = new Vector2(
406 environment.sunLongitude,
407 environment.sunLatitude);
408 var tmpNewValue = new Vector2(
409 Environment.ClampLongitude(evt.newValue.x),
410 Environment.ClampLatitude(evt.newValue.y));
411 RegisterChange(ref tmpContainer, tmpNewValue, sunPosition, customResync: () =>
412 {
413 environment.sunLongitude = tmpContainer.x;
414 environment.sunLatitude = tmpContainer.y;
415 });
416 });
417 foldout.Add(sunPosition);
418
419 Button sunToBrightess = new Button(() =>
420 {
421 ResetToBrightestSpot(environment);
422 sunPosition.SetValueWithoutNotify(new Vector2(
423 Environment.ClampLongitude(environment.sunLongitude),
424 Environment.ClampLatitude(environment.sunLatitude)));
425 })
426 {
427 name = "sunToBrightestButton"
428 };
429 sunToBrightess.Add(new Image()
430 {
431 image = CoreEditorUtils.LoadIcon(@"Packages/com.unity.render-pipelines.core/Editor/LookDev/Icons/", "SunPosition", forceLowRes: true)
432 });
433 sunToBrightess.AddToClassList("sun-to-brightest-button");
434 var vector2Input = sunPosition.Q(className: "unity-vector2-field__input");
435 vector2Input.Remove(sunPosition.Q(className: "unity-composite-field__field-spacer"));
436 vector2Input.Add(sunToBrightess);
437
438 shadowColor = new ColorField("Shadow Tint")
439 {
440 tooltip = "The wanted shadow tint to be used when computing shadow."
441 };
442 shadowColor.RegisterValueChangedCallback(evt
443 => RegisterChange(ref environment.shadowColor, evt.newValue));
444 foldout.Add(shadowColor);
445
446 style = foldout.Q<Toggle>().style;
447 style.marginLeft = 3;
448 style.unityFontStyleAndWeight = FontStyle.Bold;
449 inspector.Add(foldout);
450
451 return inspector;
452 }
453
454 void RegisterChange<TValueType>(ref TValueType reflectedVariable, TValueType newValue, BaseField<TValueType> resyncField = null, bool updatePreview = false, Action customResync = null)
455 {
456 if (environment == null || environment.Equals(null))
457 return;
458 reflectedVariable = newValue;
459 resyncField?.SetValueWithoutNotify(newValue);
460 customResync?.Invoke();
461 if (updatePreview && latlong != null && !latlong.Equals(null))
462 latlong.image = GetLatLongThumbnailTexture(environment, k_SkyThumbnailWidth);
463 EditorUtility.SetDirty(environment);
464 OnChangeCallback?.Invoke();
465 }
466 }
467}