Rasagar/Library/PackageCache/com.unity.terrain-tools/Editor/TerrainTools/DetailsScatterToolOvl.cs

1035 lines
49 KiB
C#
Raw Normal View History

2024-08-26 13:07:20 -07:00
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor.ShortcutManagement;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.TerrainTools;
using UnityEditorInternal;
namespace UnityEditor.TerrainTools
{
internal class DetailScatterToolOvl : TerrainToolsPaintTool<DetailScatterToolOvl>
{
[Shortcut("Terrain/Select Detail ScatterTool", typeof(TerrainToolShortcutContext))]
static void SelectShortcut(ShortcutArguments args)
{
TerrainToolShortcutContext context = (TerrainToolShortcutContext)args.context;
context.SelectPaintToolWithOverlays<DetailScatterToolOvl>();
TerrainToolsAnalytics.OnShortcutKeyRelease("Select Detail ScatterTool");
}
public override bool HasToolSettings => true;
public override bool HasBrushFilters => true;
public override bool HasBrushMask => true;
public override bool HasBrushAttributes => true;
Terrain m_SelectedTerrain;
DetailBrushRepresentation m_BrushRep;
[NonReorderable]
ReorderableList m_ReordableDetailsList;
bool m_ShowDetailControls = true;
int m_PreviouslySelectedIndex;
int m_MouseOnPatchIndex = -1;
List<DetailUIData> m_DetailDataList = new List<DetailUIData>();
internal List<DetailUIData> detailDataList => m_DetailDataList;
private const string k_MissingDetailPrototype = "Detail Prototype is missing.";
private const string k_MissingDetailTexture = "Detail Texture is missing.";
Material m_Material = null;
Material scatterMaterial
{
get
{
if (m_Material == null)
m_Material = new Material(Shader.Find("Hidden/TerrainEngine/DetailScatter"));
return m_Material;
}
}
IBrushUIGroup commonUI
{
get
{
if (m_commonUI == null)
{
m_commonUI = new DefaultBrushUIGroup("DetailsScatterTool", UpdateAnalyticParameters, DefaultBrushUIGroup.Feature.NoSmoothing);
m_commonUI.OnEnterToolMode();
}
return m_commonUI;
}
}
/// <summary>
/// Allows overriding for unit testing purposes
/// </summary>
/// <param name="uiGroup"></param>
internal void ChangeCommonUI(IBrushUIGroup uiGroup)
{
m_commonUI = uiGroup;
}
private class Styles
{
public readonly GUIContent editDetails = EditorGUIUtility.TrTextContent("Edit Details...", "Add or remove detail meshes");
public readonly GUIContent detailControlHeader = EditorGUIUtility.TrTextContent("Paint Details Control");
public Texture settingsIcon = EditorGUIUtility.IconContent("SettingsIcon").image;
public GUIStyle largeSquare = new GUIStyle("Button")
{
fixedHeight = 22
};
public GUIStyle buttonIcon = new GUIStyle()
{
alignment = TextAnchor.MiddleCenter,
fontStyle = FontStyle.Bold,
normal = new GUIStyleState()
{
background = null
},
hover = new GUIStyleState()
{
background = null
},
stretchWidth = true
};
public GUIStyle prototypeLabel = new GUIStyle(GUI.skin.box);
public GUIStyle toggleColor = new GUIStyle(GUI.skin.button);
public enum ViewType
{
List,
Grid
}
public ViewType viewType = ViewType.List;
public readonly GUIContent viewTypesLabel = EditorGUIUtility.TrTextContent("View", "Choose between two different detail control views.");
public readonly GUIContent listViewLabel = EditorGUIUtility.TrTextContent("List", "View the detail controls in a list. Allows for greater access to parameters.");
public readonly GUIContent gridViewLabel = EditorGUIUtility.TrTextContent("Grid", "View the detail controls in a grid. Allows for a simpler UI.");
public readonly GUIContent detailSelectionWarning = EditorGUIUtility.TrTextContentWithIcon("Select the \"+\" button in order to add a Detail to scatter with.", MessageType.Info);
//List View
public readonly GUIContent previewLabel = EditorGUIUtility.TrTextContent("Preview", "Detail preview image");
public readonly GUIContent targetDensityLabel = EditorGUIUtility.TrTextContent("Target Density", "Clamps the scattered density to a percentage of the Detail Density.");
public readonly GUIContent elementPrototypeLabel = EditorGUIUtility.TrTextContent("Detail Prefab", "");
public readonly GUIContent elementMinWidthLabel = EditorGUIUtility.TrTextContent("Min Width", "");
public readonly GUIContent elementMaxWidthLabel = EditorGUIUtility.TrTextContent("Max Width", "");
public readonly GUIContent elementMinHeightLabel = EditorGUIUtility.TrTextContent("Min Height", "");
public readonly GUIContent elementMaxHeightLabel = EditorGUIUtility.TrTextContent("Max Height", "");
public readonly GUIContent elementNoiseSeedLabel = EditorGUIUtility.TrTextContent("Noise Seed", "Specifies the random seed value for detail object placement.");
public readonly GUIContent elementNoiseSpreadLabel = EditorGUIUtility.TrTextContent("Noise Spread", "Controls the spatial frequency of the noise pattern used to vary the scale and color of the detail objects.");
public readonly GUIContent elementDetailDensityLabel = EditorGUIUtility.TrTextContent("Detail Density", "Controls detail density for this detail prototype, relative to it's size. Only enabled in \"Coverage\" detail scatter mode.");
public readonly GUIContent elementAlignToGround = EditorGUIUtility.TrTextContent("Align To Ground (%)", "Rotate detail axis to ground normal direction.");
public readonly GUIContent elementPositionJitter = EditorGUIUtility.TrTextContent("Position Jitter (%)", "Controls the randomness of the detail distribution, from ordered to random. Only available when legacy distribution in Quality Settings is turned off.");
public readonly GUIContent elementHolePaddingLabel = EditorGUIUtility.TrTextContent("Hole Edge Padding (%)", "Controls how far away detail objects are from the edge of the hole area.\n\nSpecify this value as a percentage of the detail width, which determines the radius of the circular area around the detail object used for hole testing.");
public readonly Color prototypeColor = new Color(0.82745f, 1.07450f, 1.23333f);
//Distribution Slider
public readonly GUIContent distributionLabel = EditorGUIUtility.TrTextContent("Target Coverage Distribution", "Visualizes the ratio of multiple detail's scatter target coverage.");
}
private static Styles s_Styles;
internal class DetailUIData
{
public bool isSelected;
public bool isSettingsExpanded;
}
public override int IconIndex
{
get { return (int) FoliageIndex.PaintDetails; }
}
public override TerrainCategory Category
{
get { return TerrainCategory.Foliage; }
}
public override string OnIcon => "TerrainOverlays/PaintDetails_On.png";
public override string OffIcon => "TerrainOverlays/PaintDetails.png";
public override string GetName()
{
return "Paint Details";
}
public override string GetDescription()
{
return "Paints the selected detail prototype onto the terrain";
}
public override void OnEnterToolMode()
{
base.OnEnterToolMode();
commonUI.OnEnterToolMode();
}
public override void OnExitToolMode()
{
base.OnExitToolMode();
commonUI.OnExitToolMode();
PaintDetailsToolUtility.ResetDetailsUtilityData();
}
public override void OnSceneGUI(Terrain terrain, IOnSceneGUI editContext)
{
commonUI.OnSceneGUI2D(terrain, editContext);
commonUI.OnSceneGUI(terrain, editContext);
//Grab m_MouseOnPatchIndex here to avoid calling again in OnRenderBrushPreview
m_MouseOnPatchIndex = PaintDetailsToolUtility.ClampedDetailPatchesGUI(terrain, out var detailMinMaxHeight, out var clampedDetailPatchIconScreenPositions);
//Don't render the brush preview the scene isn't being repainted. There's a performance loss.
if (Event.current.type != EventType.Repaint)
{
return;
}
Texture brushTexture = editContext.brushTexture;
using (IBrushRenderPreviewUnderCursor brushRender = new BrushRenderPreviewUIGroupUnderCursor(commonUI, "DetailScatterTool", brushTexture))
{
if (brushRender.CalculateBrushTransform(out BrushTransform brushTransform))
{
RenderTexture tmpRT = RenderTexture.active;
Rect brushTransformBounds = brushTransform.GetBrushXYBounds();
PaintContext heightmapContext = brushRender.AcquireHeightmap(false, brushTransformBounds, 1);
var previewMaterial = Utility.GetDefaultPreviewMaterial(commonUI.hasEnabledFilters);
var texelCtx = Utility.CollectTexelValidity(heightmapContext.originTerrain, brushTransform.GetBrushXYBounds());
Utility.SetupMaterialForPaintingWithTexelValidityContext(heightmapContext, texelCtx, brushTransform, previewMaterial);
var filterRT = RTUtils.GetTempHandle(heightmapContext.sourceRenderTexture.width,
heightmapContext.sourceRenderTexture.height, 0, FilterUtility.defaultFormat);
Utility.GenerateAndSetFilterRT(commonUI, brushRender, filterRT, previewMaterial);
brushRender.RenderBrushPreview(heightmapContext, TerrainBrushPreviewMode.SourceRenderTexture, brushTransform, previewMaterial, 0);
texelCtx.Cleanup();
RTUtils.Release(filterRT);
}
}
PaintDetailsToolUtility.DrawClampedDetailPatchGUI(m_MouseOnPatchIndex, clampedDetailPatchIconScreenPositions, detailMinMaxHeight, terrain, editContext);
}
public override void OnToolSettingsGUI(Terrain terrain, IOnInspectorGUI editContext, bool overlays)
{
DetailScatterGUI(terrain, editContext, true);
}
private void DetailScatterGUI(Terrain terrain, IOnInspectorGUI editContext, bool overlays)
{
if (s_Styles == null)
s_Styles = new Styles();
m_ShowDetailControls = TerrainToolGUIHelper.DrawHeaderFoldout(s_Styles.detailControlHeader, m_ShowDetailControls);
if (m_ShowDetailControls)
{
DetailPrototype[] prototypes = terrain.terrainData.detailPrototypes;
//Reset toggle list if it's not synced with the terrain
if (m_SelectedTerrain != terrain || prototypes.Length != m_DetailDataList.Count)
{
UpdateDetailUIData(terrain);
}
if (m_ReordableDetailsList == null)
{
InitReordableLayerSelection();
}
DetailsControlGUI(terrain, overlays);
}
m_SelectedTerrain = terrain;
}
public override void OnInspectorGUI(Terrain terrain, IOnInspectorGUI editContext)
{
if (m_commonUI == null)
return;
if (s_Styles == null)
s_Styles = new Styles();
//Brush selector
m_commonUI.OnInspectorGUI(terrain, editContext);
DetailScatterGUI(terrain, editContext, false);
}
public override bool OnPaint(Terrain terrain, IOnPaint editContext)
{
commonUI.OnPaint(terrain, editContext);
if(!m_commonUI.allowPaint)
{
return true;
}
Texture2D brushTexture = editContext.brushTexture as Texture2D;
if (brushTexture == null)
{
Debug.LogError("Brush texture is not a Texture2D.");
return false;
}
if (m_BrushRep == null)
{
m_BrushRep = new DetailBrushRepresentation();
}
float eraseStrength = 1;
if (Event.current != null && (Event.current.shift || Event.current.control))
{
eraseStrength = -eraseStrength;
}
//Create an array of indices that are selected
int[] layers = m_DetailDataList.Select((v, i) => new { Value = v, Index = i })
.Where(b => b.Value.isSelected == true)
.Select(b => b.Index).ToArray();
PaintTreesDetailsContext ctx = PaintTreesDetailsContext.Create(terrain, editContext.uv);
for (int t = 0; t < ctx.neighborTerrains.Length; ++t)
{
TerrainData terrainData = ctx.neighborTerrains[t]?.terrainData;
if (terrainData == null)
{
continue;
}
using (IBrushRenderUnderCursor brushRender = new BrushRenderUIGroupUnderCursor(commonUI, "DetailsScatterTool", brushTexture))
{
if (brushRender.CalculateBrushTransform(out BrushTransform brushTransform))
{
ApplyBrushInternal(brushTexture, brushRender);
PaintContext paintContext = brushRender.AcquireHeightmap(false, brushTransform.GetBrushXYBounds());
}
}
int size = (int)Mathf.Max(1.0f, m_commonUI.brushSize * ((float)terrainData.detailResolution / terrainData.size.x));
DetailBrushBounds brushBounds = new DetailBrushBounds(terrainData, ctx, size, t);
if (brushBounds.bounds.height + brushBounds.bounds.width == 0)
{
continue;
}
m_BrushRep.Update(brushTexture, size, true);
if (eraseStrength < 0.0F && !Event.current.control) //If erasing a list of all the layers within the terrain
{
layers = terrainData.GetSupportedLayers(brushBounds.min, brushBounds.bounds.size);
}
TerrainPaintUtilityEditor.UpdateTerrainDataUndo(terrainData, "Terrain - Detail Edit");
for (int i = 0; i < layers.Length; i++)
{
int layerIndex;
if (Event.current != null && !Event.current.shift && !Event.current.control)
{
layerIndex = PaintDetailsToolUtility.FindDetailPrototype(ctx.neighborTerrains[t], m_SelectedTerrain, layers[i]);
if (layerIndex == -1)
{
layerIndex = PaintDetailsToolUtility.CopyDetailPrototype(ctx.neighborTerrains[t], m_SelectedTerrain, layers[i]);
}
}
else
{
layerIndex = layers[i];
}
int[,] alphamap = terrainData.GetDetailLayer(brushBounds.min, brushBounds.bounds.size, layerIndex);
for (int y = 0; y < brushBounds.bounds.height; y++)
{
for (int x = 0; x < brushBounds.bounds.width; x++)
{
Vector2Int brushOffset = brushBounds.GetBrushOffset(x, y);
float opa = m_commonUI.brushStrength * m_BrushRep.GetStrength(brushOffset.x, brushOffset.y);
float targetValue = Mathf.Lerp(alphamap[y, x], eraseStrength * terrainData.detailPrototypes[layerIndex].targetCoverage * terrainData.maxDetailScatterPerRes, opa);
alphamap[y, x] = Mathf.Min(Mathf.RoundToInt(targetValue - .5f + UnityEngine.Random.value), terrainData.maxDetailScatterPerRes);
}
}
terrainData.SetDetailLayer(brushBounds.min, layerIndex, alphamap);
}
}
return false;
}
void ApplyBrushInternal(Texture2D brushTex, IBrushRenderUnderCursor brushRender)
{
Texture2D mask = brushTex;
if (mask != null)
{
Texture2D readableTexture = null;
if (!mask.isReadable)
{
readableTexture = new Texture2D(mask.width, mask.height, mask.format, mask.mipmapCount > 1);
Graphics.CopyTexture(mask, readableTexture);
readableTexture.Apply();
}
else
{
readableTexture = mask;
}
brushRender.CalculateBrushTransform(out BrushTransform brushTransform);
PaintContext paintContext = brushRender.AcquireHeightmap(false, brushTransform.GetBrushXYBounds());
RenderTexture renderTexture = RenderTexture.GetTemporary(readableTexture.width, readableTexture.height, 16, readableTexture.graphicsFormat);
RenderTexture oldRT = RenderTexture.active;
Material mat = scatterMaterial;
var brushMask = RTUtils.GetTempHandle(paintContext.sourceRenderTexture.width, paintContext.sourceRenderTexture.height, 0, FilterUtility.defaultFormat);
Utility.GenerateAndSetFilterRT(commonUI, brushRender, brushMask, mat);
mat.SetTexture("_BrushTex", readableTexture);
brushRender.SetupTerrainToolMaterialProperties(paintContext, brushTransform, mat);
Graphics.Blit(readableTexture, renderTexture, mat, 0);
RenderTexture.active = renderTexture;
brushTex.ReadPixels(new Rect(0, 0, brushTex.width, brushTex.height), 0, 0);
RenderTexture.active = oldRT;
RenderTexture.ReleaseTemporary(renderTexture);
RTUtils.Release(brushMask);
}
}
void DetailsControlGUI(Terrain terrain, bool overlay)
{
DetailPrototype[] prototypes = terrain.terrainData.detailPrototypes;
EditorGUILayout.BeginHorizontal("Box");
GUILayout.Label(s_Styles.viewTypesLabel);
if (GUILayout.Toggle(s_Styles.viewType == Styles.ViewType.List, s_Styles.listViewLabel, GUI.skin.button))
{
s_Styles.viewType = Styles.ViewType.List;
}
if (GUILayout.Toggle(s_Styles.viewType == Styles.ViewType.Grid, s_Styles.gridViewLabel, GUI.skin.button))
{
s_Styles.viewType = Styles.ViewType.Grid;
}
EditorGUILayout.EndHorizontal();
//Show distribution slider limit warning
if (m_DetailDataList.Count == 0)
{
EditorGUILayout.HelpBox(s_Styles.detailSelectionWarning);
}
//Multi-Detail Selection GUI
if (s_Styles.viewType == Styles.ViewType.List)
{
int minWidth = overlay ? 350 : 0;
EditorGUILayout.BeginHorizontal(GUILayout.MinWidth(minWidth));
m_ReordableDetailsList.DoLayoutList();
EditorGUILayout.EndHorizontal();
}
else
{
DrawGridSelection(terrain, prototypes);
}
//Distribution Slider
EditorGUILayout.LabelField("Target Density Distribution");
int[] layers = m_DetailDataList.Select((v, i) => new { Value = v, Index = i })
.Where(b => b.Value.isSelected == true)
.Select(b => b.Index).ToArray();
var sliderBarPosition = GUILayoutUtility.GetRect(0, 30, GUILayout.ExpandWidth(true));
var distributionElements = DistributionSliderGUI.CreateDistributionInfos(layers.Length, sliderBarPosition,
i => GetPrototypeName(prototypes[layers[i]]),
i => prototypes[layers[i]].targetCoverage,
i => layers[i]);
if (DistributionSliderGUI.DrawSlider(sliderBarPosition, distributionElements, prototypes))
{
m_SelectedTerrain.terrainData.detailPrototypes = prototypes; //Necessary to update the settings
EditorUtility.SetDirty(m_SelectedTerrain);
}
//Show distribution slider limit warning
if (layers.Length > DistributionSliderGUI.k_MaxDistributionSliderCount)
{
EditorGUILayout.HelpBox("The distribution slider only supports the first 8 prototypes ", MessageType.Info);
}
}
readonly int m_DistributionPrototypeCardId = "PrototypeCardIDHash".GetHashCode();
const int k_CardSize = 80;
const int k_CardPreviewOffset = 5;
void DrawGridSelection(Terrain terrain, DetailPrototype[] prototypes)
{
var detailIcons = PaintDetailsToolUtility.LoadDetailIcons(prototypes);
float inspectorWidth = EditorGUIUtility.currentViewWidth;
int prototypeCount = prototypes.Length + 1;
int columns = (int)MathF.Floor(inspectorWidth / k_CardSize);
int rows = (int)MathF.Ceiling((prototypeCount * k_CardSize) / inspectorWidth);
rows = Mathf.Max(rows, rows + (prototypeCount) - (rows * columns)); //Make sure the column and row count is equivalent to the amount of prototypes being displayed
bool finalindexReached = false;
for (int y = 0; y < rows; y++)
{
EditorGUILayout.BeginHorizontal();
for (int x = 0; x < columns; x++)
{
int index = x + (y * columns);
if (index >= prototypes.Length)
{
finalindexReached = true;
break;
}
bool prevSelectedValue = m_DetailDataList[index].isSelected;
Rect prototypeCardRect = GUILayoutUtility.GetRect(k_CardSize, k_CardSize);
Rect prototypePreviewRect = new Rect(
prototypeCardRect.x + k_CardPreviewOffset,
prototypeCardRect.y + k_CardPreviewOffset,
k_CardSize - (k_CardPreviewOffset * 2),
k_CardSize - (k_CardPreviewOffset * 2));
Rect prototypeCheckBoxRect = new Rect(prototypePreviewRect.x + 2, prototypePreviewRect.y + 9, 0, 0);
Color tempColor = GUI.backgroundColor;
GUI.backgroundColor = m_DetailDataList[index].isSelected ? s_Styles.prototypeColor : tempColor; //new Color(0.479f, 0.708f, 0.983f, 1.0f)
GUI.Box(prototypeCardRect, "", GUI.skin.button);
GUI.backgroundColor = tempColor;
if(detailIcons[index]?.image != null)
{
EditorGUI.DrawPreviewTexture(prototypePreviewRect, detailIcons[index].image);
}
else
{
EditorGUI.DrawTextureAlpha(prototypePreviewRect, EditorGUIUtility.IconContent("SceneAsset Icon").image);
}
GUI.Toggle(prototypeCheckBoxRect, prevSelectedValue, String.Empty);
Event currentEvent = Event.current;
MouseClickOperations(currentEvent, terrain.terrainData, prototypeCardRect, prototypeCardRect, prototypes, detailIcons, index);
if (currentEvent.type == EventType.Repaint)
{
Rect toggleRect = GUILayoutUtility.GetLastRect();
string prototypeName = prototypes[index].usePrototypeMesh ? prototypes[index].prototype.name : prototypes[index].prototypeTexture.name;
GUIContent labelText = EditorGUIUtility.TrTextContent(prototypeName, prototypeName + " ");
EditorGUI.LabelField(new Rect(toggleRect.x, toggleRect.yMax - 22, toggleRect.width, 22), labelText, GUI.skin.box);
}
}
if(finalindexReached)
{
Rect addDetailButtonRect = GUILayoutUtility.GetRect(k_CardSize, k_CardSize);
if (GUI.Button(addDetailButtonRect, EditorGUIUtility.IconContent("d_Toolbar Plus@2x")))
{
DisplayDetailAddMenu();
}
}
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
}
}
void InitReordableLayerSelection()
{
m_ReordableDetailsList = new ReorderableList(m_DetailDataList, typeof(DetailUIData), false, true, true, true);
m_ReordableDetailsList.elementHeightCallback = GetElementHeight;
m_ReordableDetailsList.drawHeaderCallback = DrawHeader;
m_ReordableDetailsList.drawElementCallback = DrawElement;
m_ReordableDetailsList.drawElementBackgroundCallback = DrawElementBackground;
m_ReordableDetailsList.onAddCallback = DisplayDetailAddMenu;
m_ReordableDetailsList.onRemoveCallback = RemoveElement;
}
const int k_ElementHeight = 85;
const int k_GrassTextureElementHeight = 385;
const int k_GPUEnabledElementHeight = 365;
const int k_GPUDisabledElementHeight = 410;
float GetElementHeight(int index)
{
if(m_SelectedTerrain == null || index >= m_SelectedTerrain.terrainData.detailPrototypes.Length)
return 0;
DetailPrototype prototype = m_SelectedTerrain.terrainData.detailPrototypes[index];
if (m_DetailDataList[index].isSettingsExpanded)
{
if(prototype.usePrototypeMesh)
{
return prototype.useInstancing ? k_GPUEnabledElementHeight : k_GPUDisabledElementHeight;
}
else
{
return k_GrassTextureElementHeight;
}
}
return k_ElementHeight;
}
void DisplayDetailAddMenu(ReorderableList list = null)
{
MenuCommand item = new MenuCommand(m_SelectedTerrain, m_PreviouslySelectedIndex);
if (ValidateDetailTexture())
{
GenericMenu menu = new GenericMenu();
menu.AddItem(new GUIContent("Detail Mesh"), false, () =>
{
TerrainWizard.DisplayTerrainWizard<TerrainDetailMeshWizard>("Add Detail Mesh", "Add").ResetDefaults((Terrain)item.context, -1);
});
menu.AddItem(new GUIContent("Grass Texture"), false, () =>
{
TerrainWizard.DisplayTerrainWizard<TerrainDetailTextureWizard>("Add Grass Texture", "Add").ResetDefaults((Terrain)item.context, -1);
});
menu.ShowAsContext();
}
else
{
TerrainWizard.DisplayTerrainWizard<TerrainDetailMeshWizard>("Add Detail Mesh", "Add").ResetDefaults((Terrain)item.context, -1);
}
}
void RemoveElement(ReorderableList list)
{
int[] selectedIndecies = m_DetailDataList.Select((v, i) => new { Value = v, Index = i })
.Where(b => b.Value.isSelected == true)
.Select(b => b.Index).ToArray();
Array.Reverse(selectedIndecies);
foreach(int index in selectedIndecies)
{
Undo.RegisterCompleteObjectUndo(m_SelectedTerrain.terrainData, "Remove detail object");
m_SelectedTerrain.terrainData.RemoveDetailPrototype(index);
}
}
const float k_PreviewLabelWidth = 100f;
void DrawHeader(Rect rect)
{
Rect previewLabelRect = new Rect(
rect.x,
rect.y,
k_PreviewLabelWidth,
rect.height);
GUI.Label(previewLabelRect, s_Styles.previewLabel);
}
const int k_ElementPadding = 4;
const int k_ElementPrototypeCardSize = 80;
const int k_ElementPrototypePreviewPadding = 4;
const int k_ElementOptionsMenuXOffest = 15;
const int k_ElementOptionsMenuSize = 30;
const int k_MinTargetCoverageValue = 0;
const int k_MaxTargetCoverageValue = 100;
Vector2 m_ElementPadding = new Vector2(8, 6);
Texture originalPrototypeThumbnail;
//The empty method is needed to properly draw detail elements without the selection highlight
void DrawElement(Rect rect, int index, bool selected, bool focused) { }
void DrawElementBackground(Rect rect, int index, bool selected, bool focused)
{
if (m_SelectedTerrain == null || m_DetailDataList.Count == 0)
return;
TerrainData terrainData = m_SelectedTerrain.terrainData;
//Prototype Selection
float prototypeCardSize = k_ElementPrototypeCardSize - k_ElementPrototypePreviewPadding;
Rect prototypeCardRect = new Rect(
rect.x + 5,
rect.y + 6,
prototypeCardSize,
prototypeCardSize);
Rect prototypeCardPreviewRect = new Rect(
prototypeCardRect.x + k_ElementPrototypePreviewPadding,
prototypeCardRect.y + k_ElementPrototypePreviewPadding,
prototypeCardRect.width - (k_ElementPrototypePreviewPadding * 2),
prototypeCardRect.height - (k_ElementPrototypePreviewPadding * 2));
Rect prototypeCardCheckBoxRect = new Rect(
prototypeCardPreviewRect.x + 2,
prototypeCardPreviewRect.y + 9,
0,
0);
DetailPrototype[] prototypes = terrainData.detailPrototypes;
if (index >= prototypes.Length)
{
return;
}
var detailIcons = PaintDetailsToolUtility.LoadDetailIcons(prototypes);
Color tempColor = GUI.backgroundColor;
GUI.backgroundColor = m_DetailDataList[index].isSelected ? s_Styles.prototypeColor : tempColor;
GUI.Box(prototypeCardRect, String.Empty, GUI.skin.button);
GUI.backgroundColor = tempColor;
Texture thumbnail = prototypes[index].Validate() ? detailIcons[index]?.image : originalPrototypeThumbnail;
if (thumbnail != null)
{
EditorGUI.DrawPreviewTexture(prototypeCardPreviewRect, thumbnail);
}
else
{
EditorGUI.DrawTextureAlpha(prototypeCardPreviewRect, EditorGUIUtility.IconContent("SceneAsset Icon").image);
}
GUI.Toggle(prototypeCardCheckBoxRect, m_DetailDataList[index].isSelected, String.Empty);
Event currentEvent = Event.current;
MouseClickOperations(currentEvent, terrainData, prototypeCardRect, rect, prototypes, detailIcons, index);
float prototypeRectOffset = rect.width - (prototypeCardRect.width + m_ElementPadding.x + 8);
//Prototype name
Rect prototypeNameRect = new Rect(
prototypeCardRect.x + prototypeCardRect.width + k_ElementPadding,
prototypeCardRect.y - 2,
prototypeRectOffset - k_ElementOptionsMenuXOffest,
EditorGUIUtility.singleLineHeight);
GUI.Label(prototypeNameRect, GetPrototypeName(prototypes[index]));
//Prototype options menu
Rect optionsMenuRect = new Rect(
rect.xMax - ((k_ElementOptionsMenuSize / 2) + k_ElementPadding),
prototypeNameRect.y,
k_ElementOptionsMenuSize,
k_ElementOptionsMenuSize);
if (GUI.Button(optionsMenuRect, EditorGUIUtility.IconContent("d__Menu@2x"), EditorStyles.iconButton))
{
originalPrototypeThumbnail = detailIcons[index].image;
DisplayPrototypeEditMenu(terrainData, prototypes, index);
}
//Settings - Box
Rect settingsBoxRect = new Rect(
prototypeNameRect.x + 2,
prototypeNameRect.y + EditorGUIUtility.singleLineHeight + 5,
prototypeRectOffset,
rect.height - (prototypeNameRect.height + ((k_ElementPadding * 3) + 3)
));
GUI.Box(settingsBoxRect, "", "GroupBox");
//Settings - Foldout handle
Rect settingsFoldoutHandle = new Rect(
settingsBoxRect.x + 10,
settingsBoxRect.y + 20,
k_ElementOptionsMenuXOffest,
k_ElementOptionsMenuXOffest
);
m_DetailDataList[index].isSettingsExpanded = GUI.Toggle(settingsFoldoutHandle, m_DetailDataList[index].isSettingsExpanded, GUIContent.none, EditorStyles.foldout);
if (m_DetailDataList[index].isSettingsExpanded)
{
DrawDetailSettings(settingsBoxRect, settingsFoldoutHandle, prototypes, index);
}
//Settings - Slider
EditorGUI.BeginChangeCheck();
Rect settingsSliderRect = new Rect(
settingsFoldoutHandle.x + settingsFoldoutHandle.width + k_ElementPadding,
settingsFoldoutHandle.y - 3,
settingsBoxRect.width - 38,
settingsFoldoutHandle.height + 3
);
float targetCoverage = prototypes[index].targetCoverage * 100f;
EditorGUIUtility.labelWidth = GUI.skin.label.CalcSize(s_Styles.targetDensityLabel).x + 3;
targetCoverage = EditorGUI.IntSlider(settingsSliderRect, s_Styles.targetDensityLabel, (int)targetCoverage,
k_MinTargetCoverageValue, k_MaxTargetCoverageValue);
EditorGUIUtility.labelWidth = 0; //Reset to the default labelWidth
prototypes[index].targetCoverage = targetCoverage / 100f;
if (EditorGUI.EndChangeCheck())
{
terrainData.detailPrototypes = prototypes; //Necessary to trigger the settings to be updated
EditorUtility.SetDirty(m_SelectedTerrain);
}
ReorderableList.defaultBehaviours.DrawElementBackground(rect, index, false, false, true);
}
void DrawDetailSettings(Rect settingsBoxRect, Rect previousReferenceRect, DetailPrototype[] prototypes, int index)
{
DetailPrototype prototype = prototypes[index];
EditorGUI.BeginChangeCheck();
GetSettingElementRect(settingsBoxRect, previousReferenceRect, out Rect prototypeField, true);
if (prototype.usePrototypeMesh)
{
GameObject previousPrototype = prototype.prototype;
prototype.prototype = (GameObject)EditorGUI.ObjectField(prototypeField, s_Styles.elementPrototypeLabel, prototype.prototype, typeof(GameObject), false);
if (!prototype.Validate(out string errorMessage))
{
prototype.prototype = previousPrototype;
EditorUtility.DisplayDialog("Can't assign prototype", errorMessage, "OK");
}
}
else
{
prototype.prototypeTexture = (Texture2D)EditorGUI.ObjectField(prototypeField, s_Styles.elementPrototypeLabel, prototype.prototypeTexture, typeof(GameObject), false);
}
GetSettingElementRect(settingsBoxRect, previousReferenceRect, out Rect densityField);
EditorGUI.BeginDisabledGroup(m_SelectedTerrain.terrainData.detailScatterMode != DetailScatterMode.CoverageMode);
prototype.density = EditorGUI.Slider(densityField, s_Styles.elementDetailDensityLabel, prototype.density, 0, 3);
EditorGUI.EndDisabledGroup();
GetSettingElementRect(settingsBoxRect, previousReferenceRect, out Rect alignToGroundField);
prototype.alignToGround = EditorGUI.Slider(alignToGroundField, s_Styles.elementAlignToGround, prototype.alignToGround * 100, 0, 100) / 100;
GetSettingElementRect(settingsBoxRect, previousReferenceRect, out Rect positionJitter);
GUI.enabled = !QualitySettings.useLegacyDetailDistribution;
prototype.positionJitter = EditorGUI.Slider(positionJitter, s_Styles.elementPositionJitter, prototype.positionJitter * 100, 0, 100) / 100;
GUI.enabled = true;
GetSettingElementRect(settingsBoxRect, previousReferenceRect, out Rect minWidthField);
prototype.minWidth = Mathf.Max(0f, EditorGUI.FloatField(minWidthField, s_Styles.elementMinWidthLabel, prototype.minWidth));
GetSettingElementRect( settingsBoxRect, previousReferenceRect, out Rect maxWidthField);
prototype.maxWidth = Mathf.Max(prototype.minWidth, EditorGUI.FloatField(maxWidthField, s_Styles.elementMaxWidthLabel, prototype.maxWidth));
GetSettingElementRect(settingsBoxRect, previousReferenceRect, out Rect minHeightField);
prototype.minHeight = Mathf.Max(0f, EditorGUI.FloatField(minHeightField, s_Styles.elementMinHeightLabel, prototype.minHeight));
GetSettingElementRect(settingsBoxRect, previousReferenceRect, out Rect maxHeightField);
prototype.maxHeight = Mathf.Max(prototype.minHeight, EditorGUI.FloatField(maxHeightField, s_Styles.elementMaxHeightLabel, prototype.maxHeight));
GetSettingElementRect(settingsBoxRect, previousReferenceRect, out Rect noiseSeedField);
prototype.noiseSeed = EditorGUI.IntField(noiseSeedField, s_Styles.elementNoiseSeedLabel, prototype.noiseSeed);
GetSettingElementRect(settingsBoxRect, previousReferenceRect, out Rect noiseSpreadField);
prototype.noiseSpread = EditorGUI.FloatField(noiseSpreadField, s_Styles.elementNoiseSpreadLabel, prototype.noiseSpread);
GetSettingElementRect(settingsBoxRect, previousReferenceRect, out Rect holePaddingField);
float holePadding = prototype.holeEdgePadding * 100;
holePadding = EditorGUI.Slider(holePaddingField, s_Styles.elementHolePaddingLabel, holePadding, 0, 100);
prototype.holeEdgePadding = holePadding / 100;
if (prototype.usePrototypeMesh)
{
GetSettingElementRect(settingsBoxRect, previousReferenceRect, out Rect renderModeField);
if (prototype.useInstancing)
{
EditorGUI.BeginDisabledGroup(true);
EditorGUI.EnumPopup(renderModeField, "Render Mode", TerrainDetailMeshRenderMode.VertexLit);
EditorGUI.EndDisabledGroup();
GetSettingElementRect(settingsBoxRect, previousReferenceRect, out Rect useInstancingField);
prototype.useInstancing = EditorGUI.Toggle(useInstancingField, "Use GPU Instancing", prototype.useInstancing);
}
else
{
TerrainDetailMeshRenderMode meshRenderMode = prototype.renderMode == DetailRenderMode.Grass ? TerrainDetailMeshRenderMode.Grass : TerrainDetailMeshRenderMode.VertexLit;
meshRenderMode = (TerrainDetailMeshRenderMode)EditorGUI.EnumPopup(renderModeField, "Render Mode", meshRenderMode);
prototype.renderMode = meshRenderMode == TerrainDetailMeshRenderMode.Grass ? DetailRenderMode.Grass : DetailRenderMode.VertexLit;
GetSettingElementRect(settingsBoxRect, previousReferenceRect, out Rect healthyColorField);
prototype.healthyColor = EditorGUI.ColorField(healthyColorField, "Healthy Color", prototype.healthyColor);
GetSettingElementRect(settingsBoxRect, previousReferenceRect, out Rect dryColorField);
prototype.dryColor = EditorGUI.ColorField(dryColorField, "Dry Color", prototype.dryColor);
GetSettingElementRect(settingsBoxRect, previousReferenceRect, out Rect useInstancingField);
prototype.useInstancing = EditorGUI.Toggle(useInstancingField, "Use GPU Instancing", prototype.useInstancing);
}
}
else //Grass Texture
{
GetSettingElementRect(settingsBoxRect, previousReferenceRect, out Rect healthyColorField);
prototype.healthyColor = EditorGUI.ColorField(healthyColorField, "Healthy Color", prototype.healthyColor);
GetSettingElementRect(settingsBoxRect, previousReferenceRect, out Rect dryColorField);
prototype.dryColor = EditorGUI.ColorField(dryColorField, "Dry Color", prototype.dryColor);
GetSettingElementRect(settingsBoxRect, previousReferenceRect, out Rect useInstancingField);
bool billboard = prototype.renderMode == DetailRenderMode.GrassBillboard;
billboard = EditorGUI.Toggle(useInstancingField, "Billboard", billboard);
prototype.renderMode = billboard ? DetailRenderMode.GrassBillboard : DetailRenderMode.Grass;
}
if (EditorGUI.EndChangeCheck())
{
m_SelectedTerrain.terrainData.detailPrototypes = prototypes; //Necessary to update the settings
EditorUtility.SetDirty(m_SelectedTerrain);
}
}
int m_ElementLevel = 1;
void GetSettingElementRect(Rect settingsBoxRect, Rect previousReferenceRect, out Rect fieldRect, bool firstElement = false)
{
if(firstElement)
{
m_ElementLevel = 1;
}
fieldRect = new Rect(
previousReferenceRect.x + k_ElementPadding,
previousReferenceRect.y + (EditorGUIUtility.singleLineHeight * m_ElementLevel) + (k_ElementPadding * m_ElementLevel),
settingsBoxRect.width - 24,
EditorGUIUtility.singleLineHeight
);
m_ElementLevel++;
}
void DisplayPrototypeEditMenu(TerrainData terrainData, DetailPrototype[] prototypes, int index)
{
GenericMenu menu = new GenericMenu();
menu.AddItem(new GUIContent("Edit"), false, () =>
{
//Taken from TerrainMenus.EditDetail(MenuCommand)
MenuCommand item = new MenuCommand(m_SelectedTerrain, index);
if (prototypes[index].usePrototypeMesh)
{
TerrainWizard.DisplayTerrainWizard<TerrainDetailMeshWizard>("Edit Detail Mesh", "Apply").ResetDefaults((Terrain)item.context, item.userData);
}
else
{
TerrainWizard.DisplayTerrainWizard<TerrainDetailTextureWizard>("Edit Grass Texture", "Apply").ResetDefaults((Terrain)item.context, item.userData);
}
});
menu.AddItem(new GUIContent("Remove"), false, () =>
{
Undo.RegisterCompleteObjectUndo(terrainData, "Remove detail object");
terrainData.RemoveDetailPrototype(index);
});
menu.ShowAsContext();
}
void DisplayDetailAddMenu()
{
MenuCommand item = new MenuCommand(m_SelectedTerrain, m_PreviouslySelectedIndex);
if (ValidateDetailTexture())
{
GenericMenu menu = new GenericMenu();
menu.AddItem(new GUIContent("Detail Mesh"), false, () =>
{
TerrainWizard.DisplayTerrainWizard<TerrainDetailMeshWizard>("Add Detail Mesh", "Add").ResetDefaults((Terrain)item.context, -1);
});
menu.AddItem(new GUIContent("Grass Texture"), false, () =>
{
TerrainWizard.DisplayTerrainWizard<TerrainDetailTextureWizard>("Add Grass Texture", "Add").ResetDefaults((Terrain)item.context, -1);
});
menu.ShowAsContext();
}
else
{
TerrainWizard.DisplayTerrainWizard<TerrainDetailMeshWizard>("Add Detail Mesh", "Add").ResetDefaults((Terrain)item.context, -1);
}
}
void MouseClickOperations(Event currentEvent, TerrainData terrainData, Rect leftClickAreaRect, Rect rightClickAreaRect, DetailPrototype[] prototypes, GUIContent[] detailIcons, int index)
{
EventType eventType = currentEvent.GetTypeForControl(GUIUtility.GetControlID(m_DistributionPrototypeCardId, FocusType.Passive));
if (eventType == EventType.MouseDown)
{
if (currentEvent.button == 0 && leftClickAreaRect.Contains(currentEvent.mousePosition)) //Left click operation
{
m_DetailDataList[index].isSelected = !m_DetailDataList[index].isSelected;
//Shift multi-select
if (currentEvent.shift)
{
int start = Mathf.Min(index, m_PreviouslySelectedIndex);
int steps = Mathf.Abs(index - m_PreviouslySelectedIndex);
for (int i = start; i <= steps + start; i++)
{
m_DetailDataList[i].isSelected = m_DetailDataList[index].isSelected;
}
}
currentEvent.Use();
m_PreviouslySelectedIndex = index;
}
if (currentEvent.button == 1 && rightClickAreaRect.Contains(currentEvent.mousePosition)) //Right click operation
{
originalPrototypeThumbnail = detailIcons[index].image;
DisplayPrototypeEditMenu(terrainData, prototypes, index);
}
}
}
bool ValidateDetailTexture() => GraphicsSettings.currentRenderPipeline == null
|| GraphicsSettings.currentRenderPipeline.terrainDetailGrassBillboardShader != null
|| GraphicsSettings.currentRenderPipeline.terrainDetailGrassShader != null;
// Used for internal unit tests only.
internal void SetSelectedTerrain(Terrain terrain)
{
m_SelectedTerrain = terrain;
}
internal void UpdateDetailUIData(Terrain ctxTerrain)
{
if (ctxTerrain == null || m_SelectedTerrain == null)
return;
int[] layers = m_DetailDataList.Select((v, i) => new { Value = v, Index = i })
.Where(b => b.Value.isSelected == true)
.Select(b => b.Index).ToArray();
int oldDetailListCount = m_DetailDataList.Count;
m_DetailDataList.Clear();
for (int i = 0; i < ctxTerrain.terrainData.detailPrototypes.Length; i++)
{
m_DetailDataList.Add(
new DetailUIData() {
isSelected = m_SelectedTerrain == ctxTerrain && i + 1 > oldDetailListCount // Set the selection boolean to true if it's a newly added detail
}
);
}
for (int i = 0; i < layers.Length; i++)
{
int index = layers[i];
int detailPrototype = PaintDetailsToolUtility.FindDetailPrototype(ctxTerrain, m_SelectedTerrain, index);
if (detailPrototype != -1)
{
m_DetailDataList[detailPrototype].isSelected = true;
}
}
}
//Analytics Setup
private TerrainToolsAnalytics.IBrushParameter[] UpdateAnalyticParameters()
{
if (m_SelectedTerrain == null || s_Styles == null || m_DetailDataList == null || m_DetailDataList.Count == 0)
{
//Return an empty object for comparing data
return new TerrainToolsAnalytics.IBrushParameter[] { };
}
else
{
return new TerrainToolsAnalytics.IBrushParameter[]
{
new TerrainToolsAnalytics.BrushParameter<int>{Name = "Details Count", Value = m_DetailDataList.Count},
new TerrainToolsAnalytics.BrushParameter<int>{Name = "Selected Details", Value = m_DetailDataList.Count(b => b.isSelected == true)},
new TerrainToolsAnalytics.BrushParameter<string>{Name = "View", Value = s_Styles.viewType.ToString()},
new TerrainToolsAnalytics.BrushParameter<float>{Name = "Average Target Coverage", Value = m_SelectedTerrain.terrainData.detailPrototypes.Select(x => x.targetCoverage).Average()},
};
}
}
public static string GetPrototypeName(DetailPrototype prototype)
{
return prototype.usePrototypeMesh
? prototype.prototype == null
? k_MissingDetailPrototype
: prototype.prototype.name
: prototype.prototypeTexture == null
? k_MissingDetailTexture
: prototype.prototypeTexture.name;
}
}
}