using System.Collections.Generic; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.SceneManagement; namespace MTE { internal partial class GrassPainter : IEditor { public int Id { get; } = 6; public bool Enabled { get; set; } = true; public string Name { get; } = "GrassPainter"; public Texture Icon { get; } = EditorGUIUtility.IconContent("TerrainInspector.TerrainToolPlants").image; public bool WantMouseMove { get; } = false; public bool WillEditMesh { get; } = false; #region Parameters #region Constant // default const float DefaultBrushSize = 1; const float DefaultBrushOpacity = 0.5f; const float DefaultBrushDirection = 0; const bool DefaultUseRandomDirection = true; const int DefaultReduction = 100; // min/max const float MinBrushSize = 0.1f; const float MaxBrushSize = 10f; const float MinBrushOpacity = 0.0f; const float MaxBrushOpacity = 1.0f; private const int MinReduction = 1; private const int MaxReduction = 100; // limit private const int MaxPositionNumber = 50; #endregion public float brushSize; public float brushOpacity; public int reduction; private GrassDetail SelectedGrassDetail => grassDetailList[SelectedGrassIndex]; /// /// Brush size (unit: 1 BrushUnit) /// public float BrushSize { get { return brushSize; } set { value = Mathf.Clamp(value, MinBrushSize, MaxBrushSize); if (!MathEx.AmostEqual(brushSize, value)) { brushSize = value; EditorPrefs.SetFloat("MTE_GrassPainter.brushSize", value); } } } //real brush size private float BrushSizeInU3D { get { return BrushSize * Settings.BrushUnit; } } /// /// /// public float BrushOpacity { get { return brushOpacity; } set { if (Mathf.Abs(brushOpacity - value) > 0.0001f) { brushOpacity = value; EditorPrefs.SetFloat("MTE_GrassPainter.brushOpacity", value); } } } /// /// Removing strength (percent) /// public int Reduction { get { return this.reduction; } set { if (this.reduction != value) { this.reduction = value; EditorPrefs.SetInt("MTE_GrassPainter.reduction", value); } } } /// /// Selected grass texture index /// public int SelectedGrassIndex { get; set; } private float brushDirection = 0; /// /// Brush direction, angle to north(+z) /// public float BrushDirection { get { return this.brushDirection; } set { value = Mathf.Clamp(value, 0, 2 * Mathf.PI); if (!MathEx.AmostEqual(value, this.brushDirection)) { EditorPrefs.SetFloat("MTE_GrassPainter.brushDirection", this.brushDirection); this.brushDirection = value; } } } private bool useRandomDirection; /// /// /// public bool UseRandomDirection { get { return this.useRandomDirection; } set { if (value != useRandomDirection) { useRandomDirection = value; EditorPrefs.SetBool("MTE_GrassPainter.useRandomDirection", value); } } } #endregion public static GrassPainter Instance; private List grassDetailList = null; internal void LoadGrassDetailList() { if (detailListBox == null) { detailListBox = new GrassDetailListBox(); } var path = Res.DetailDir + "SavedGrassDetailList.asset"; var relativePath = Utility.GetUnityPath(path); var obj = AssetDatabase.LoadAssetAtPath(relativePath); if (obj != null && obj.grassDetailList != null) { grassDetailList = obj.grassDetailList; detailListBox.SetEditingTarget(grassDetailList); MTEDebug.LogFormat("GrassDetailList loaded from {0}", path); } else { obj = ScriptableObject.CreateInstance(); obj.grassDetailList = new List(4); AssetDatabase.CreateAsset(obj, relativePath); EditorUtility.SetDirty(obj); grassDetailList = obj.grassDetailList; detailListBox.SetEditingTarget(grassDetailList); MTEDebug.LogFormat("No GrassDetailList found in {0}, created a new SavedGrassDetailList.asset.", path); } } public GrassPainter() { MTEContext.EnableEvent += (sender, args) => { if (MTEContext.editor == this) { LoadSavedParamter(); LoadGrassDetailList(); CheckIfCanAttachGrassLoader(); ForceReloadGrass(); } }; MTEContext.EditTypeChangedEvent += (sender, args) => { if (MTEContext.editor == this) { LoadSavedParamter(); LoadGrassDetailList(); CheckIfCanAttachGrassLoader(); ForceReloadGrass(); } }; MTEContext.SelectionChangedEvent += (sender, args) => { if (MTEContext.editor == this) { CheckIfCanAttachGrassLoader(); } }; MTEContext.MeshColliderUpdatedEvent += (sender, args) => { UpdateAllGrasses(); }; // Load default parameters brushSize = DefaultBrushSize; brushOpacity = DefaultBrushOpacity; brushDirection = DefaultBrushDirection; useRandomDirection = DefaultUseRandomDirection; this.reduction = DefaultReduction; GrassPainter.Instance = this; } private void ForceReloadGrass() { // force reload the grass loader var foundGrassLoader = MTEContext.TheGrassLoader; if (foundGrassLoader != null) { GrassEditorUtil.ReloadGrassesFromFile(foundGrassLoader); } } public HashSet DefineHotkeys() { return new HashSet { new Hotkey(this, KeyCode.LeftBracket, () => { BrushSize -= 1; MTEEditorWindow.Instance.Repaint(); }), new Hotkey(this, KeyCode.RightBracket, () => { BrushSize += 1; MTEEditorWindow.Instance.Repaint(); }), new Hotkey(this, KeyCode.Minus, () => { BrushOpacity -= 0.01f; MTEEditorWindow.Instance.Repaint(); }), new Hotkey(this, KeyCode.Equals, () => { BrushOpacity += 0.01f; MTEEditorWindow.Instance.Repaint(); }) }; } private void LoadSavedParamter() { // Load parameters from the EditorPrefs brushSize = EditorPrefs.GetFloat("MTE_GrassPainter.brushSize", DefaultBrushSize); brushOpacity = EditorPrefs.GetFloat("MTE_GrassPainter.brushOpacity", DefaultBrushOpacity); brushDirection = EditorPrefs.GetFloat("MTE_GrassPainter.brushDirection", DefaultBrushDirection); useRandomDirection = EditorPrefs.GetBool("MTE_GrassPainter.useRandomDirection", DefaultUseRandomDirection); reduction = EditorPrefs.GetInt("MTE_GrassPainter.reduction", DefaultReduction); } public string Header { get { return StringTable.Get(C.PaintGrass_Header); } } public string Description { get { return StringTable.Get(C.PaintGrass_Description); } } public void DoArgsGUI() { if (!MTEContext.TheGrassLoader) { EditorGUILayout.HelpBox(StringTable.Get(C.Warning_NoGrassLoader), MessageType.Warning); EditorGUILayout.BeginHorizontal(); { if (GUILayout.Button(StringTable.Get(C.CreateGrassLoader), GUILayout.Width(100), GUILayout.Height(40))) { CreateGrassContainer(); } GUILayout.Space(20); EditorGUILayout.LabelField( StringTable.Get(C.Info_ToolDescription_CreateGrassLoader), MTEStyles.labelFieldWordwrap); } EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); { GUI.enabled = CanAttachGrassLoader; if (GUILayout.Button(StringTable.Get(C.AttachGrassLoader), GUILayout.Width(100), GUILayout.Height(40))) { AttachGrassLoader(); } GUILayout.Space(20); EditorGUILayout.LabelField(StringTable.Get(C.Info_ToolDescription_AttachGrassLoader), MTEStyles.labelFieldWordwrap); GUI.enabled = true; if (!CanAttachGrassLoader) { EditorGUILayout.BeginHorizontal(); { var content = EditorGUIUtility.IconContent("console.warnicon"); content.tooltip = CannotAttachGrassReason; GUILayout.Label(content, "button"); } EditorGUILayout.EndHorizontal(); } } EditorGUILayout.EndHorizontal(); return; } // Grasses if (!Settings.CompactGUI) { GUILayout.Label(StringTable.Get(C.Grasses), MTEStyles.SubHeader); } // grass detail list SelectedGrassIndex = detailListBox.DoGUI(SelectedGrassIndex); //Settings if (!Settings.CompactGUI) { EditorGUILayout.Space(); GUILayout.Label(StringTable.Get(C.Settings), MTEStyles.SubHeader); } BrushSize = EditorGUILayoutEx.Slider(StringTable.Get(C.Size), "-", "+", BrushSize, MinBrushSize, MaxBrushSize); BrushOpacity = EditorGUILayoutEx.Slider(StringTable.Get(C.Density), "[", "]", BrushOpacity, MinBrushOpacity, MaxBrushOpacity); Reduction = EditorGUILayoutEx.IntSlider(StringTable.Get(C.Reduction), Reduction, MinReduction, MaxReduction); EditorGUILayout.BeginHorizontal(); { var label = new GUIContent(StringTable.Get(C.Direction)); var size = GUIStyle.none.CalcSize(label); EditorGUILayout.LabelField(label, GUILayout.Width(size.x + 10), GUILayout.MinWidth(60)); EditorGUILayout.BeginVertical(); UseRandomDirection = GUILayout.Toggle(UseRandomDirection, StringTable.Get(C.Random)); if (!UseRandomDirection) { EditorGUILayout.LabelField(string.Format("{0}°", Mathf.Rad2Deg * BrushDirection)); EditorGUILayout.HelpBox(StringTable.Get(C.Info_HowToRotate), MessageType.Info); } EditorGUILayout.EndVertical(); } EditorGUILayout.EndHorizontal(); // Tools if (!Settings.CompactGUI) { EditorGUILayout.Space(); GUILayout.Label(StringTable.Get(C.Tools), MTEStyles.SubHeader); } EditorGUILayout.BeginVertical(); { EditorGUILayout.BeginHorizontal(); { if (GUILayout.Button(StringTable.Get(C.BakePointCloudToMesh), GUILayout.Width(100), GUILayout.Height(40))) { BakePointCloudToMesh(); } GUILayout.Space(20); EditorGUILayout.LabelField( StringTable.Get(C.Info_ToolDescription_BakePointCloudToMesh), MTEStyles.labelFieldWordwrap); } EditorGUILayout.EndHorizontal(); } EditorGUILayout.EndVertical(); GUILayout.FlexibleSpace(); EditorGUILayout.HelpBox(StringTable.Get(C.Info_WillBeSavedInstantly), MessageType.Info, true); } private HashSet highlightedRenderers = new HashSet(); List editingItems = new List(); public void OnSceneGUI() { var e = Event.current; if (e.commandName == "UndoRedoPerformed") { SceneView.RepaintAll(); return; } if (!(EditorWindow.mouseOverWindow is SceneView)) { MTEDebug.Log("Mouse not in SceneView."); ClearHighlight(); return; } if(!UseRandomDirection && e.control) { RaycastHit hit; Ray ray1 = HandleUtility.GUIPointToWorldRay(e.mousePosition); if (Physics.Raycast(ray1, out hit, Mathf.Infinity, 1 << MTEContext.TargetLayer//only hit target layer )) { //check tag if (!hit.transform.CompareTag(MTEContext.TargetTag)) { return; } Handles.ArrowHandleCap(0, hit.point, Quaternion.Euler(0, BrushDirection * Mathf.Rad2Deg, 0), 10 * Settings.PointSize, EventType.Repaint); } } // do nothing when mouse middle/right button, control/alt key is pressed if (e.button != 0 || e.alt) return; // no grass if (grassDetailList == null || grassDetailList.Count == 0) { MTEDebug.Log("Return: No grass detail."); return; } // grass loader not specified if (MTEContext.TheGrassLoader == null) { MTEDebug.Log("Return: No grass loader."); return; } HandleUtility.AddDefaultControl(0); RaycastHit raycastHit; Ray ray = HandleUtility.GUIPointToWorldRay(e.mousePosition); if (Physics.Raycast(ray, out raycastHit, Mathf.Infinity, 1 << MTEContext.TargetLayer//only hit target layer )) { //check tag if (!raycastHit.transform.CompareTag(MTEContext.TargetTag)) { return; } if (Settings.ShowBrushRect) { Utility.ShowBrushRect(raycastHit.point, BrushSizeInU3D); } var hitPoint = raycastHit.point; Handles.color = Color.green; Handles.DrawWireDisc(hitPoint, raycastHit.normal, BrushSizeInU3D); if (!UseRandomDirection) { ClearHighlight(); if (e.control) { GrassMap.GetGrassItemsInCircle(hitPoint, BrushSizeInU3D, editingItems); foreach (var grassItem in editingItems) { var renderer = grassItem.gameObject.GetComponent(); Utility.SetHighlight(renderer, true); highlightedRenderers.Add(renderer); } } } // not using random direction // hold control key and scroll wheel to change // 1. grasses' rotationY // 2. brush direction if (!UseRandomDirection && e.control && !e.isKey && e.type == EventType.ScrollWheel) { float oldDirection = BrushDirection; float direction = oldDirection; ChangeDirection(e.delta.y, ref direction); if (Mathf.Abs(direction - oldDirection) > Mathf.Epsilon) { UpdateGrasses(editingItems, Mathf.Rad2Deg * direction); MTEEditorWindow.Instance.Repaint(); BrushDirection = direction; } e.Use(); } else if (e.type == EventType.MouseDown || e.type == EventType.MouseDrag) { if (e.type == EventType.MouseDown) { grassPaintTransation = new Undo.UndoTransaction( e.shift ? "Grass Painter: Delete Grass Instances" : "Grass Painter: Create Grass Instances" ); Undo.UndoRedoManager.Instance().StartTransaction(grassPaintTransation); } if (!e.shift) {//adding grasses MTEDebug.Log("1: generate grass positions"); var grassDetail = grassDetailList[this.SelectedGrassIndex]; grassPositions.Clear(); if (grassDetail.MaxWidth > this.BrushSizeInU3D) {//TODO single mode: click and create a single grass; dragging is not allowed. grassPositions.Add(new Vector2(hitPoint.x, hitPoint.z)); } else { var positionNumber = Mathf.CeilToInt( Mathf.PI * this.BrushSizeInU3D * this.BrushSizeInU3D / this.BrushOpacity); positionNumber = Mathf.Min(MaxPositionNumber, positionNumber); MathEx.UniformPointsInCircle( new Vector2(hitPoint.x, hitPoint.z), this.BrushSizeInU3D, positionNumber, ref grassPositions); } MTEDebug.Log("2: added grass positions number = " + grassPositions.Count); CreateGrassInstances(); } else {//removing grasses removeList.Clear(); GrassMap.GetGrassItemsInCircle(hitPoint, BrushSizeInU3D, removeList); int removeCount = Mathf.CeilToInt(this.reduction / 100.0f * this.removeList.Count); if (removeCount != 0) { var grassItemsRemoved = this.removeList.TakeRandom(removeCount); RemoveGrassInstances(grassItemsRemoved); } } } // auto save when mouse up if (e.type == EventType.MouseUp && e.button == 0) { SaveGrass(); MTEDebug.Log("5: saved grass asset file"); if (grassPaintTransation != null) { Undo.UndoRedoManager.Instance().EndTransaction(grassPaintTransation); Utility.RefreshHistoryViewer(); grassPaintTransation = null; } } } SceneView.RepaintAll(); } Undo.UndoTransaction grassPaintTransation; private void CreateGrassInstances() { List createdInstances = new List(grassPositions.Count); var grassDetail = SelectedGrassDetail; int createdGrassObjectNumber = 0; for (int j = 0; j < grassPositions.Count; j++) { var width = Random.Range(grassDetail.MinWidth, grassDetail.MaxWidth); var height = Random.Range(grassDetail.MinHeight, grassDetail.MaxHeight); var rotationY = UseRandomDirection ? Random.Range(0f, 180f) : Mathf.Rad2Deg * this.BrushDirection; var grassPosition = grassPositions[j]; RaycastHit hit; if (Physics.Raycast( new Ray(new Vector3(grassPosition.x, 10000, grassPosition.y), new Vector3(0, -1f, 0)), out hit, Mathf.Infinity, 1 << MTEContext.TargetLayer //only hit target layer )) { //only consider target tag if (!hit.transform.CompareTag(MTEContext.TargetTag)) { return; } GrassItem grassItem = null; if (grassDetail.GrassType == GrassType.OneQuad) { grassItem = CreateGrassQuad( grassDetail.Material, hit.point, rotationY, width, height); } else if(grassDetail.GrassType == GrassType.ThreeQuad) { grassItem = CreateGrassStar( grassDetail.Material, hit.point, rotationY, width, height); } else { throw new System.ArgumentOutOfRangeException( $"Unknown grass type {grassDetail.GrassType}"); } createdGrassObjectNumber++; createdInstances.Add(grassItem); } } MTEDebug.Log("4: created grass object number = " + createdGrassObjectNumber); if (createdInstances.Count > 0) { Undo.UndoRedoManager.Instance().Push(a => { UndoCreate(a); }, createdInstances); } } private void RemoveGrassInstances(IEnumerable grassItemsRemoved) { List removedItems = new List(); foreach (var grassItem in grassItemsRemoved) { if (grassItem.Star != null) { MTEContext.TheGrassLoader.grassInstanceList.grasses.Remove(grassItem.Star); } else if (grassItem.Quad != null) { MTEContext.TheGrassLoader.grassInstanceList.quads.Remove(grassItem.Quad); } Object.DestroyImmediate(grassItem.gameObject); grassItem.gameObject = null; GrassMap.Remove(grassItem); removedItems.Add(grassItem); } Undo.UndoRedoManager.Instance().Push(a => { RedoCreate(removedItems); }, removedItems); } private GrassItem CreateGrassQuad( Material material, Vector3 position, float rotationY, float width, float height) { GameObject grassObject; MeshRenderer grassMeshRenderer; //not used Mesh grassMesh; //not used var rotation = Quaternion.Euler(0, rotationY, 0); GrassUtil.GenerateGrassQuadObject(position, rotation, width, height, material, out grassObject, out grassMeshRenderer, out grassMesh); MTEDebug.Log("3: created quad grass object " + grassObject.GetInstanceID()); grassObject.transform.SetParent(MTEContext.TheGrassLoader.transform, true); GrassQuad quad = new GrassQuad(); quad.Init(material, position, rotationY, width, height); MTEContext.TheGrassLoader.grassInstanceList.quads.Add(quad); var grassItem = new GrassItem(quad, grassObject); GrassMap.Insert(grassItem); return grassItem; } private GrassItem CreateGrassStar( Material material, Vector3 position, float rotationY, float width, float height) { GameObject grassObject; MeshRenderer grassMeshRenderer; //not used Mesh grassMesh; //not used var rotation = Quaternion.Euler(0, rotationY, 0); GrassUtil.GenerateGrassStarObject(position, rotation, width, height, material, out grassObject, out grassMeshRenderer, out grassMesh); MTEDebug.Log("3: created star grass object " + grassObject.GetInstanceID()); grassObject.transform.SetParent(MTEContext.TheGrassLoader.transform, true); GrassStar grassInstance = new GrassStar(); grassInstance.Init(material, position, rotationY, width, height); MTEContext.TheGrassLoader.grassInstanceList.grasses.Add(grassInstance); var grassItem = new GrassItem(grassInstance, grassObject); GrassMap.Insert(grassItem); return grassItem; } private void UndoCreate(List createdInstances) { List removedItems = new List(createdInstances.Count); //remove created grass instances foreach (var grassItem in createdInstances) { if (grassItem == null) { continue; } if (grassItem.gameObject) { Object.DestroyImmediate(grassItem.gameObject); grassItem.gameObject = null; } GrassMap.Remove(grassItem); if (grassItem.Star == null && grassItem.Quad == null) { continue; } if (grassItem.Star != null) { MTEContext.TheGrassLoader.grassInstanceList.grasses.Remove(grassItem.Star); } else { MTEContext.TheGrassLoader.grassInstanceList.quads.Remove(grassItem.Quad); } removedItems.Add(grassItem); } Undo.UndoRedoManager.Instance().Push(a => { RedoCreate(removedItems); }, createdInstances); } private void RedoCreate(List removedObjects) { List createdItems = new List(removeList.Count); foreach (var undoData in removedObjects) { if (undoData.Quad == null && undoData.Star == null) {//ignore invalid grass item continue; } GrassItem grassItem = null; if (undoData.Quad != null) { var quad = undoData.Quad; grassItem = CreateGrassQuad( quad.Material, quad.Position, quad.RotationY, quad.Width, quad.Height); } else if(undoData.Star != null) { var star = undoData.Star; grassItem = CreateGrassStar( star.Material, star.Position, star.RotationY, star.Width, star.Height); } else { MTEDebug.LogWarning("Ignored a null grass item when undo/redo."); continue; } createdItems.Add(grassItem); } Undo.UndoRedoManager.Instance().Push(a => { UndoCreate(a); }, createdItems); } private void ClearHighlight() { foreach (var renderer in highlightedRenderers) { if (renderer) { Utility.SetHighlight(renderer, false); } } highlightedRenderers.Clear(); } private void ChangeDirection(float delta, ref float direction) { if(delta > 0) { direction -= Mathf.PI / 12; } else if(delta < 0) { direction += Mathf.PI / 12; } if(direction < 0) { direction += 2*Mathf.PI; } if (direction > 2*Mathf.PI) { direction -= 2*Mathf.PI; } } private void SaveGrass() { EditorUtility.SetDirty(MTEContext.TheGrassLoader.grassInstanceList); } /// /// Update height of grass items /// private static void UpdateGrasses(IEnumerable items) { bool updated = false; foreach (var item in items) { var pos2D = new Vector2(item.Position2D.x, item.Position2D.y); var rayOrigin = new Vector3(pos2D.x, 99999f, pos2D.y); var ray = new Ray(rayOrigin, Vector3.down); RaycastHit hit; if (Physics.Raycast(ray, out hit, Mathf.Infinity, 1 << MTEContext.TargetLayer//only hit target layer )) { if (!hit.transform.CompareTag(MTEContext.TargetTag)) { return; } item.Height = hit.point.y; updated = true; } } if (updated) { GrassPainter.Instance.SaveGrass(); } } /// /// Update rotation (Y) of grass items /// private static void UpdateGrasses(IEnumerable items, float rotationY) { foreach (var item in items) { var pos2D = new Vector2(item.Position2D.x, item.Position2D.y); var rayOrigin = new Vector3(pos2D.x, 99999f, pos2D.y); var ray = new Ray(rayOrigin, Vector3.down); if (Physics.Raycast(ray, Mathf.Infinity, ~MTEContext.TargetLayer)) { item.RotationY = rotationY; } } GrassPainter.Instance.SaveGrass(); } /// /// Update height of grass items inside a circular region /// /// center of the circular region /// radius of the circular region public void UpdateGrass(Vector3 center, float radius) { var items = new List(); GrassMap.GetGrassItemsInCircle(center, radius, items); UpdateGrasses(items); } /// /// Update height of all grass items /// public void UpdateAllGrasses() { var items = GrassMap.GetAllGrassItems(); UpdateGrasses(items); } private void BakePointCloudToMesh() { bool confirmed = EditorUtility.DisplayDialog( StringTable.Get(C.Warning), StringTable.Get(C.Warning_Confirm_UnrecoverableOperation), StringTable.Get(C.Yes), StringTable.Get(C.No)); if (!confirmed) { return; } if (!MTEContext.TheGrassLoader) { EditorUtility.DisplayDialog( StringTable.Get(C.Warning), StringTable.Get(C.Warning_NoGrassLoader_CannotBakePointCloudToMesh), StringTable.Get(C.OK)); return; } MTEContext.TheGrassLoader.RemoveOldGrasses(); MTEContext.TheGrassLoader.GenerateGrasses(new GrassGenerationSettings { UseStaticBatch = false, HideGrassObjectInEditor = false }); bool removeGrassLoader = EditorUtility.DisplayDialog( StringTable.Get(C.Info), StringTable.Get(C.Info_RemoveGrassLoader), StringTable.Get(C.Yes), StringTable.Get(C.No)); if (removeGrassLoader) { UnityEngine.Object.DestroyImmediate(MTEContext.TheGrassLoader); } EditorSceneManager.MarkSceneDirty(SceneManager.GetActiveScene()); } } }