TerrainEditor.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. using UnityEditor;
  2. using UnityEngine;
  3. using UnityEngine.Experimental.Rendering;
  4. using UnityEngine.Rendering;
  5. namespace FlatKit {
  6. public class TerrainEditor : StylizedSurfaceEditor, ITerrainLayerCustomUI {
  7. private class StylesLayer
  8. {
  9. public readonly GUIContent warningHeightBasedBlending = new GUIContent("Height-based blending is disabled if you have more than four TerrainLayer materials!");
  10. public readonly GUIContent enableHeightBlend = new GUIContent("Enable Height-based Blend", "Blend terrain layers based on height values.");
  11. public readonly GUIContent heightTransition = new GUIContent("Height Transition", "Size in world units of the smooth transition between layers.");
  12. public readonly GUIContent enableInstancedPerPixelNormal = new GUIContent("Enable Per-pixel Normal", "Enable per-pixel normal when the terrain uses instanced rendering.");
  13. public readonly GUIContent diffuseTexture = new GUIContent("Diffuse");
  14. public readonly GUIContent colorTint = new GUIContent("Color Tint");
  15. public readonly GUIContent opacityAsDensity = new GUIContent("Opacity as Density", "Enable Density Blend (if unchecked, opacity is used as Smoothness)");
  16. public readonly GUIContent normalMapTexture = new GUIContent("Normal Map");
  17. public readonly GUIContent normalScale = new GUIContent("Normal Scale");
  18. public readonly GUIContent maskMapTexture = new GUIContent("Mask", "R: Metallic\nG: AO\nB: Height\nA: Smoothness");
  19. public readonly GUIContent maskMapTextureWithoutHeight = new GUIContent("Mask Map", "R: Metallic\nG: AO\nA: Smoothness");
  20. public readonly GUIContent channelRemapping = new GUIContent("Channel Remapping");
  21. public readonly GUIContent defaultValues = new GUIContent("Channel Default Values");
  22. public readonly GUIContent metallic = new GUIContent("R: Metallic");
  23. public readonly GUIContent ao = new GUIContent("G: AO");
  24. public readonly GUIContent height = new GUIContent("B: Height");
  25. public readonly GUIContent heightParametrization = new GUIContent("Parametrization");
  26. public readonly GUIContent heightAmplitude = new GUIContent("Amplitude (cm)");
  27. public readonly GUIContent heightBase = new GUIContent("Base (cm)");
  28. public readonly GUIContent heightMin = new GUIContent("Min (cm)");
  29. public readonly GUIContent heightMax = new GUIContent("Max (cm)");
  30. public readonly GUIContent heightCm = new GUIContent("B: Height (cm)");
  31. public readonly GUIContent smoothness = new GUIContent("A: Smoothness");
  32. }
  33. static StylesLayer s_Styles = null;
  34. private static StylesLayer styles {
  35. get {
  36. if (s_Styles == null) s_Styles = new StylesLayer();
  37. return s_Styles;
  38. }
  39. }
  40. // Height blend params
  41. MaterialProperty enableHeightBlend = null;
  42. const string kEnableHeightBlend = "_EnableHeightBlend";
  43. MaterialProperty heightTransition = null;
  44. const string kHeightTransition = "_HeightTransition";
  45. // Per-pixel Normal (while instancing)
  46. MaterialProperty enableInstancedPerPixelNormal = null;
  47. const string kEnableInstancedPerPixelNormal = "_EnableInstancedPerPixelNormal";
  48. private bool m_ShowChannelRemapping = false;
  49. enum HeightParametrization
  50. {
  51. Amplitude,
  52. MinMax
  53. };
  54. private HeightParametrization m_HeightParametrization = HeightParametrization.Amplitude;
  55. private static bool DoesTerrainUseMaskMaps(TerrainLayer[] terrainLayers)
  56. {
  57. for (int i = 0; i < terrainLayers.Length; ++i)
  58. {
  59. if (terrainLayers[i].maskMapTexture != null)
  60. return true;
  61. }
  62. return false;
  63. }
  64. protected void FindMaterialProperties(MaterialProperty[] props)
  65. {
  66. enableHeightBlend = FindProperty(kEnableHeightBlend, props, false);
  67. heightTransition = FindProperty(kHeightTransition, props, false);
  68. enableInstancedPerPixelNormal = FindProperty(kEnableInstancedPerPixelNormal, props, false);
  69. }
  70. static public void SetupMaterialKeywords(Material material)
  71. {
  72. bool enableHeightBlend = (material.HasProperty(kEnableHeightBlend) && material.GetFloat(kEnableHeightBlend) > 0);
  73. CoreUtils.SetKeyword(material, "_TERRAIN_BLEND_HEIGHT", enableHeightBlend);
  74. bool enableInstancedPerPixelNormal = material.GetFloat(kEnableInstancedPerPixelNormal) > 0.0f;
  75. CoreUtils.SetKeyword(material, "_TERRAIN_INSTANCED_PERPIXEL_NORMAL", enableInstancedPerPixelNormal);
  76. }
  77. static public bool TextureHasAlpha(Texture2D inTex)
  78. {
  79. if (inTex != null)
  80. {
  81. return GraphicsFormatUtility.HasAlphaChannel(GraphicsFormatUtility.GetGraphicsFormat(inTex.format, true));
  82. }
  83. return false;
  84. }
  85. public override void OnGUI(MaterialEditor materialEditorIn, MaterialProperty[] properties) {
  86. base.OnGUI(materialEditorIn, properties);
  87. FindMaterialProperties(properties);
  88. bool optionsChanged = false;
  89. EditorGUI.BeginChangeCheck();
  90. {
  91. if (enableHeightBlend != null)
  92. {
  93. //EditorGUI.indentLevel++;
  94. materialEditorIn.ShaderProperty(enableHeightBlend, styles.enableHeightBlend);
  95. if (enableHeightBlend.floatValue > 0)
  96. {
  97. EditorGUI.indentLevel++;
  98. EditorGUILayout.HelpBox(styles.warningHeightBasedBlending.text, MessageType.Info);
  99. materialEditorIn.ShaderProperty(heightTransition, styles.heightTransition);
  100. EditorGUI.indentLevel--;
  101. }
  102. //EditorGUI.indentLevel--;
  103. }
  104. EditorGUILayout.Space();
  105. }
  106. if (EditorGUI.EndChangeCheck())
  107. {
  108. optionsChanged = true;
  109. }
  110. bool enablePerPixelNormalChanged = false;
  111. // Since Instanced Per-pixel normal is actually dependent on instancing enabled or not, it is not
  112. // important to check it in the GUI. The shader will make sure it is enabled/disabled properly.s
  113. if (enableInstancedPerPixelNormal != null)
  114. {
  115. //EditorGUI.indentLevel++;
  116. EditorGUI.BeginChangeCheck();
  117. materialEditorIn.ShaderProperty(enableInstancedPerPixelNormal, styles.enableInstancedPerPixelNormal);
  118. enablePerPixelNormalChanged = EditorGUI.EndChangeCheck();
  119. //EditorGUI.indentLevel--;
  120. }
  121. if (optionsChanged || enablePerPixelNormalChanged)
  122. {
  123. foreach (var obj in materialEditorIn.targets)
  124. {
  125. SetupMaterialKeywords((Material)obj);
  126. }
  127. }
  128. // We should always do this call at the end
  129. materialEditorIn.serializedObject.ApplyModifiedProperties();
  130. }
  131. bool ITerrainLayerCustomUI.OnTerrainLayerGUI(TerrainLayer terrainLayer, Terrain terrain)
  132. {
  133. var terrainLayers = terrain.terrainData.terrainLayers;
  134. // Don't use the member field enableHeightBlend as ShaderGUI.OnGUI might not be called if the material UI is folded.
  135. // heightblend shouldn't be available if we are in multi-pass mode, because it is guaranteed to be broken.
  136. bool heightBlendAvailable = (terrainLayers.Length <= 4);
  137. bool heightBlend = heightBlendAvailable && terrain.materialTemplate.HasProperty(kEnableHeightBlend) && (terrain.materialTemplate.GetFloat(kEnableHeightBlend) > 0);
  138. terrainLayer.diffuseTexture = EditorGUILayout.ObjectField(styles.diffuseTexture, terrainLayer.diffuseTexture, typeof(Texture2D), false) as Texture2D;
  139. TerrainLayerUtility.ValidateDiffuseTextureUI(terrainLayer.diffuseTexture);
  140. var diffuseRemapMin = terrainLayer.diffuseRemapMin;
  141. var diffuseRemapMax = terrainLayer.diffuseRemapMax;
  142. EditorGUI.BeginChangeCheck();
  143. bool enableDensity = false;
  144. if (terrainLayer.diffuseTexture != null)
  145. {
  146. var rect = GUILayoutUtility.GetLastRect();
  147. rect.y += 16 + 4;
  148. rect.width = EditorGUIUtility.labelWidth + 64;
  149. rect.height = 16;
  150. ++EditorGUI.indentLevel;
  151. var diffuseTint = new Color(diffuseRemapMax.x, diffuseRemapMax.y, diffuseRemapMax.z);
  152. diffuseTint = EditorGUI.ColorField(rect, styles.colorTint, diffuseTint, true, false, false);
  153. diffuseRemapMax.x = diffuseTint.r;
  154. diffuseRemapMax.y = diffuseTint.g;
  155. diffuseRemapMax.z = diffuseTint.b;
  156. diffuseRemapMin.x = diffuseRemapMin.y = diffuseRemapMin.z = 0;
  157. if (!heightBlend)
  158. {
  159. rect.y = rect.yMax + 2;
  160. enableDensity = EditorGUI.Toggle(rect, styles.opacityAsDensity, diffuseRemapMin.w > 0);
  161. }
  162. --EditorGUI.indentLevel;
  163. }
  164. diffuseRemapMax.w = 1;
  165. diffuseRemapMin.w = enableDensity ? 1 : 0;
  166. if (EditorGUI.EndChangeCheck())
  167. {
  168. terrainLayer.diffuseRemapMin = diffuseRemapMin;
  169. terrainLayer.diffuseRemapMax = diffuseRemapMax;
  170. }
  171. // Display normal map UI
  172. terrainLayer.normalMapTexture = EditorGUILayout.ObjectField(styles.normalMapTexture, terrainLayer.normalMapTexture, typeof(Texture2D), false) as Texture2D;
  173. TerrainLayerUtility.ValidateNormalMapTextureUI(terrainLayer.normalMapTexture, TerrainLayerUtility.CheckNormalMapTextureType(terrainLayer.normalMapTexture));
  174. if (terrainLayer.normalMapTexture != null)
  175. {
  176. var rect = GUILayoutUtility.GetLastRect();
  177. rect.y += 16 + 4;
  178. rect.width = EditorGUIUtility.labelWidth + 64;
  179. rect.height = 16;
  180. ++EditorGUI.indentLevel;
  181. terrainLayer.normalScale = EditorGUI.FloatField(rect, styles.normalScale, terrainLayer.normalScale);
  182. --EditorGUI.indentLevel;
  183. }
  184. // Display the mask map UI and the remap controls
  185. terrainLayer.maskMapTexture = EditorGUILayout.ObjectField(heightBlend ? styles.maskMapTexture : styles.maskMapTextureWithoutHeight, terrainLayer.maskMapTexture, typeof(Texture2D), false) as Texture2D;
  186. TerrainLayerUtility.ValidateMaskMapTextureUI(terrainLayer.maskMapTexture);
  187. var maskMapRemapMin = terrainLayer.maskMapRemapMin;
  188. var maskMapRemapMax = terrainLayer.maskMapRemapMax;
  189. var smoothness = terrainLayer.smoothness;
  190. var metallic = terrainLayer.metallic;
  191. ++EditorGUI.indentLevel;
  192. EditorGUI.BeginChangeCheck();
  193. m_ShowChannelRemapping = EditorGUILayout.Foldout(m_ShowChannelRemapping, terrainLayer.maskMapTexture != null ? s_Styles.channelRemapping : s_Styles.defaultValues);
  194. if (m_ShowChannelRemapping)
  195. {
  196. if (terrainLayer.maskMapTexture != null)
  197. {
  198. float min, max;
  199. min = maskMapRemapMin.x; max = maskMapRemapMax.x;
  200. EditorGUILayout.MinMaxSlider(s_Styles.metallic, ref min, ref max, 0, 1);
  201. maskMapRemapMin.x = min; maskMapRemapMax.x = max;
  202. min = maskMapRemapMin.y; max = maskMapRemapMax.y;
  203. EditorGUILayout.MinMaxSlider(s_Styles.ao, ref min, ref max, 0, 1);
  204. maskMapRemapMin.y = min; maskMapRemapMax.y = max;
  205. if (heightBlend)
  206. {
  207. EditorGUILayout.LabelField(styles.height);
  208. ++EditorGUI.indentLevel;
  209. m_HeightParametrization = (HeightParametrization)EditorGUILayout.EnumPopup(styles.heightParametrization, m_HeightParametrization);
  210. if (m_HeightParametrization == HeightParametrization.Amplitude)
  211. {
  212. // (height - heightBase) * amplitude
  213. float amplitude = Mathf.Max(maskMapRemapMax.z - maskMapRemapMin.z, Mathf.Epsilon); // to avoid divide by zero
  214. float heightBase = maskMapRemapMin.z / amplitude;
  215. amplitude = EditorGUILayout.FloatField(styles.heightAmplitude, amplitude * 100) / 100;
  216. heightBase = EditorGUILayout.FloatField(styles.heightBase, heightBase * 100) / 100;
  217. maskMapRemapMin.z = heightBase * amplitude;
  218. maskMapRemapMax.z = (1.0f - heightBase) * amplitude;
  219. }
  220. else
  221. {
  222. maskMapRemapMin.z = EditorGUILayout.FloatField(styles.heightMin, maskMapRemapMin.z * 100) / 100;
  223. maskMapRemapMax.z = EditorGUILayout.FloatField(styles.heightMax, maskMapRemapMax.z * 100) / 100;
  224. }
  225. --EditorGUI.indentLevel;
  226. }
  227. min = maskMapRemapMin.w; max = maskMapRemapMax.w;
  228. EditorGUILayout.MinMaxSlider(s_Styles.smoothness, ref min, ref max, 0, 1);
  229. maskMapRemapMin.w = min; maskMapRemapMax.w = max;
  230. }
  231. else
  232. {
  233. metallic = EditorGUILayout.Slider(s_Styles.metallic, metallic, 0, 1);
  234. // AO and Height are still exclusively controlled via the maskRemap controls
  235. // metallic and smoothness have their own values as fields within the LayerData.
  236. maskMapRemapMax.y = EditorGUILayout.Slider(s_Styles.ao, maskMapRemapMax.y, 0, 1);
  237. if (heightBlend)
  238. {
  239. maskMapRemapMax.z = EditorGUILayout.FloatField(s_Styles.heightCm, maskMapRemapMax.z * 100) / 100;
  240. }
  241. // There's a possibility that someone could slide max below the existing min value
  242. // so we'll just protect against that by locking the min value down a little bit.
  243. // In the case of height (Z), we are trying to set min to no lower than zero value unless
  244. // max goes negative. Zero is a good sensible value for the minimum. For AO (Y), we
  245. // don't need this extra protection step because the UI blocks us from going negative
  246. // anyway. In both cases, pushing the slider below the min value will lock them together,
  247. // but min will be "left behind" if you go back up.
  248. maskMapRemapMin.y = Mathf.Min(maskMapRemapMin.y, maskMapRemapMax.y);
  249. maskMapRemapMin.z = Mathf.Min(Mathf.Max(0, maskMapRemapMin.z), maskMapRemapMax.z);
  250. if (TextureHasAlpha(terrainLayer.diffuseTexture))
  251. {
  252. GUIStyle warnStyle = new GUIStyle(GUI.skin.label);
  253. warnStyle.wordWrap = true;
  254. GUILayout.Label("Smoothness is controlled by diffuse alpha channel", warnStyle);
  255. }
  256. else
  257. smoothness = EditorGUILayout.Slider(s_Styles.smoothness, smoothness, 0, 1);
  258. }
  259. }
  260. if (EditorGUI.EndChangeCheck())
  261. {
  262. terrainLayer.maskMapRemapMin = maskMapRemapMin;
  263. terrainLayer.maskMapRemapMax = maskMapRemapMax;
  264. terrainLayer.smoothness = smoothness;
  265. terrainLayer.metallic = metallic;
  266. }
  267. --EditorGUI.indentLevel;
  268. EditorGUILayout.Space();
  269. TerrainLayerUtility.TilingSettingsUI(terrainLayer);
  270. return true;
  271. }
  272. }
  273. }