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}