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 { [Shortcut("Terrain/Select Detail ScatterTool", typeof(TerrainToolShortcutContext))] static void SelectShortcut(ShortcutArguments args) { TerrainToolShortcutContext context = (TerrainToolShortcutContext)args.context; context.SelectPaintToolWithOverlays(); 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 m_DetailDataList = new List(); internal List 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; } } /// /// Allows overriding for unit testing purposes /// /// 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("Add Detail Mesh", "Add").ResetDefaults((Terrain)item.context, -1); }); menu.AddItem(new GUIContent("Grass Texture"), false, () => { TerrainWizard.DisplayTerrainWizard("Add Grass Texture", "Add").ResetDefaults((Terrain)item.context, -1); }); menu.ShowAsContext(); } else { TerrainWizard.DisplayTerrainWizard("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("Edit Detail Mesh", "Apply").ResetDefaults((Terrain)item.context, item.userData); } else { TerrainWizard.DisplayTerrainWizard("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("Add Detail Mesh", "Add").ResetDefaults((Terrain)item.context, -1); }); menu.AddItem(new GUIContent("Grass Texture"), false, () => { TerrainWizard.DisplayTerrainWizard("Add Grass Texture", "Add").ResetDefaults((Terrain)item.context, -1); }); menu.ShowAsContext(); } else { TerrainWizard.DisplayTerrainWizard("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{Name = "Details Count", Value = m_DetailDataList.Count}, new TerrainToolsAnalytics.BrushParameter{Name = "Selected Details", Value = m_DetailDataList.Count(b => b.isSelected == true)}, new TerrainToolsAnalytics.BrushParameter{Name = "View", Value = s_Styles.viewType.ToString()}, new TerrainToolsAnalytics.BrushParameter{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; } } }