Environment.cs 18 KB

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