using System; using System.Collections.Generic; using System.IO; using System.Runtime.Serialization.Formatters.Binary; using System.Text; using UnityEditor.ShortcutManagement; using UnityEngine; using UnityEngine.Experimental.Rendering; namespace UnityEditor.TerrainTools { /// /// Provides methods for altering brush data. /// public abstract class BaseBrushUIGroup : IBrushUIGroup, IBrushEventHandler, IBrushTerrainCache { private bool m_ShowBrushMaskFilters = true; private bool m_ShowModifierControls = true; private static readonly BrushShortcutHandler s_ShortcutHandler = new BrushShortcutHandler(); private readonly string m_Name; private readonly HashSet m_ConsumedEvents = new HashSet(); private readonly List m_Controllers = new List(); private IBrushSizeController m_BrushSizeController = null; private IBrushRotationController m_BrushRotationController = null; private IBrushStrengthController m_BrushStrengthController = null; private IBrushSpacingController m_BrushSpacingController = null; private IBrushScatterController m_BrushScatterController = null; private IBrushModifierKeyController m_BrushModifierKeyController = null; private IBrushSmoothController m_BrushSmoothController = null; [ SerializeField ] private FilterStack m_BrushMaskFilterStack = null; /// /// Gets the brush mask's . /// public FilterStack brushMaskFilterStack { get { if( m_BrushMaskFilterStack == null ) { if( File.Exists( getFilterStackFilePath ) ) { m_BrushMaskFilterStack = LoadFilterStack(); } else { // create the first filterstack if this is the first time this tool is being used // because a save file has not been made yet for the filterstack m_BrushMaskFilterStack = ScriptableObject.CreateInstance< FilterStack >(); } } return m_BrushMaskFilterStack; } } private FilterStackView m_BrushMaskFilterStackView = null; /// /// Gets the brush mask's . /// public FilterStackView brushMaskFilterStackView { get { // need to make the UI if the view hasnt been created yet or if the reference to the FilterStack SerializedObject has // been lost, like when entering and exiting Play Mode if( m_BrushMaskFilterStackView == null || m_BrushMaskFilterStackView.serializedFilterStack.targetObject == null ) { m_BrushMaskFilterStackView = new FilterStackView(new GUIContent("Brush Mask Filters"), new SerializedObject( brushMaskFilterStack ) ); m_BrushMaskFilterStackView.FilterContext = filterContext; m_BrushMaskFilterStackView.onChanged += SaveFilterStack; } return m_BrushMaskFilterStackView; } } FilterContext m_FilterContext; private FilterContext filterContext { get { if (m_FilterContext != null) return m_FilterContext; m_FilterContext = new FilterContext(FilterUtility.defaultFormat, Vector3.zero, 1f, 0f); return m_FilterContext; } } /// /// Checks if Filters are enabled. /// public bool hasEnabledFilters => brushMaskFilterStack.hasEnabledFilters; /// /// Generates the brush mask. /// /// The terrain in focus. /// The source render texture to blit from. /// The destination render texture for bliting to. /// The brush's position. /// The brush's scale. /// The brush's rotation. public void GenerateBrushMask(Terrain terrain, RenderTexture sourceRenderTexture, RenderTexture destinationRenderTexture, Vector3 position, float scale, float rotation) { filterContext.ReleaseRTHandles(); using(new ActiveRenderTextureScope(null)) { // set the filter context properties filterContext.brushPos = position; filterContext.brushSize = scale; filterContext.brushRotation = rotation; // bind properties for filters to read/write to var terrainData = terrain.terrainData; filterContext.floatProperties[FilterContext.Keywords.TerrainScale] = Mathf.Sqrt(terrainData.size.x * terrainData.size.x + terrainData.size.z * terrainData.size.z); filterContext.vectorProperties["_TerrainSize"] = new Vector4(terrainData.size.x, terrainData.size.y, terrainData.size.z, 0.0f); // bind terrain texture data filterContext.rtHandleCollection.AddRTHandle(0, FilterContext.Keywords.Heightmap, sourceRenderTexture.graphicsFormat); filterContext.rtHandleCollection.GatherRTHandles(sourceRenderTexture.width, sourceRenderTexture.height); Graphics.Blit(sourceRenderTexture, filterContext.rtHandleCollection[FilterContext.Keywords.Heightmap]); brushMaskFilterStack.Eval(filterContext, sourceRenderTexture, destinationRenderTexture); } filterContext.ReleaseRTHandles(); } /// /// Generates the brush mask. /// /// The terrain in focus. /// The source render texture to blit from. /// The destination render texture for bliting to. /// public void GenerateBrushMask(Terrain terrain, RenderTexture sourceRenderTexture, RenderTexture destinationRenderTexture) { GenerateBrushMask(terrain, sourceRenderTexture, destinationRenderTexture, raycastHitUnderCursor.point, brushSize, brushRotation); } /// /// Generates the brush mask. /// /// The source render texture to blit from. /// The destination render texture for bliting to. /// public void GenerateBrushMask(RenderTexture sourceRenderTexture, RenderTexture destinationRenderTexture) { GenerateBrushMask(terrainUnderCursor, sourceRenderTexture, destinationRenderTexture); } /// /// Returns the brush name. /// public string brushName => m_Name; /// /// Gets and sets the brush size. /// /// Gets a value of 100 if the brush size controller isn't initialized. public float brushSize { get { return m_BrushSizeController?.brushSize ?? 100.0f; } set { m_BrushSizeController.brushSize = value; } } /// /// Gets and sets the brush rotation. /// /// Gets a value of 0 if the brush size controller isn't initialized. public float brushRotation { get { return m_BrushRotationController?.brushRotation ?? 0.0f; } set { m_BrushRotationController.brushRotation = value; } } /// /// Gets and sets the brush strength. /// /// Gets a value of 1 if the brush size controller isn't initialized. public float brushStrength { get { return m_BrushStrengthController?.brushStrength ?? 1.0f; } set { m_BrushStrengthController.brushStrength = value; } } /// /// Returns the brush spacing. /// /// Returns a value of 0 if the brush size controller isn't initialized. public float brushSpacing => m_BrushSpacingController?.brushSpacing ?? 0.0f; /// /// Returns the brush scatter. /// /// Returns a value of 0 if the brush size controller isn't initialized. public float brushScatter => m_BrushScatterController?.brushScatter ?? 0.0f; private bool isSmoothing { get { if (m_BrushSmoothController != null) { return Event.current != null && Event.current.shift; } return false; } } /// /// Checks if painting is allowed. /// public virtual bool allowPaint => (m_BrushSpacingController?.allowPaint ?? true) && !isSmoothing; /// /// Inverts the brush strength. /// public bool InvertStrength => m_BrushModifierKeyController?.ModifierActive(BrushModifierKey.BRUSH_MOD_INVERT) ?? false; /// /// Checks if the brush is in use. /// public bool isInUse { get { foreach(IBrushController c in m_Controllers) { if(c.isInUse) { return true; } } return false; } } private static class Styles { public static GUIStyle Box { get; private set; } public static readonly GUIContent brushMask = EditorGUIUtility.TrTextContent("Brush Mask"); public static readonly GUIContent multipleControls = EditorGUIUtility.TrTextContent("Multiple Controls"); public static readonly GUIContent stroke = EditorGUIUtility.TrTextContent("Stroke"); public static readonly string kGroupBox = "GroupBox"; static Styles() { Box = new GUIStyle(EditorStyles.helpBox); Box.normal.textColor = Color.white; } } Func m_analyticsCallback; /// /// Initializes and returns an instance of BaseBrushUIGroup. /// /// The name of the brush. /// The brush's analytics function. protected BaseBrushUIGroup(string name, Func analyticsCall = null) { m_Name = name; m_analyticsCallback = analyticsCall; } #if UNITY_2019_1_OR_NEWER [ClutchShortcut("Terrain/Adjust Brush Strength (SceneView)", typeof(TerrainToolShortcutContext), KeyCode.A)] static void StrengthBrushShortcut(ShortcutArguments args) { s_ShortcutHandler.HandleShortcutChanged(args, BrushShortcutType.Strength); } [ClutchShortcut("Terrain/Adjust Brush Size (SceneView)", typeof(TerrainToolShortcutContext), KeyCode.S)] static void ResizeBrushShortcut(ShortcutArguments args) { s_ShortcutHandler.HandleShortcutChanged(args, BrushShortcutType.Size); } [ClutchShortcut("Terrain/Adjust Brush Rotation (SceneView)", typeof(TerrainToolShortcutContext), KeyCode.D)] private static void RotateBrushShortcut(ShortcutArguments args) { s_ShortcutHandler.HandleShortcutChanged(args, BrushShortcutType.Rotation); } #endif /// /// Adds a generic controller of type to the brush's controller list. /// /// A generic controller type of IBrushController. /// The new controller to add. /// Returns the new generic controller. protected TController AddController(TController newController) where TController: IBrushController { m_Controllers.Add(newController); return newController; } /// /// Adds a rotation controller of type to the brush's controller list. /// /// A generic controller type of IBrushRotationController. /// The new controller to add. /// Returns the new rotation controller. protected TController AddRotationController(TController newController) where TController : IBrushRotationController { m_BrushRotationController = AddController(newController); return newController; } /// /// Adds a size controller of type to the brush's controller list. /// /// A generic controller type of IBrushSizeController. /// The new controller to add. /// Returns the new size controller. protected TController AddSizeController(TController newController) where TController : IBrushSizeController { m_BrushSizeController = AddController(newController); return newController; } /// /// Adds a strength controller of type to the brush's controller list. /// /// A generic controller type of IBrushStrengthController. /// The new controller to add. /// Returns the new strength controller. protected TController AddStrengthController(TController newController) where TController : IBrushStrengthController { m_BrushStrengthController = AddController(newController); return newController; } /// /// Adds a spacing controller of type to the brush's controller list. /// /// A generic controller type of IBrushSpacingController. /// The new controller to add. /// Returns the new spacing controller. protected TController AddSpacingController(TController newController) where TController : IBrushSpacingController { m_BrushSpacingController = AddController(newController); return newController; } /// /// Adds a scatter controller of type to the brush's controller list. /// /// A generic controller type of IBrushScatterController. /// The new controller to add. /// Returns the new scatter controller. protected TController AddScatterController(TController newController) where TController : IBrushScatterController { m_BrushScatterController = AddController(newController); return newController; } /// /// Adds a modifier key controller of type to the brush's controller list. /// /// A generic controller type of IBrushModifierKeyController. /// The new controller to add. /// Returns the new modifier key controller. protected TController AddModifierKeyController(TController newController) where TController : IBrushModifierKeyController { m_BrushModifierKeyController = newController; return newController; } /// /// Adds a smoothing controller of type to the brush's controller list. /// /// A generic controller type of IBrushSmoothController. /// The new controller to add. /// Returns the new smoothing controller. protected TController AddSmoothingController(TController newController) where TController : IBrushSmoothController { m_BrushSmoothController = newController; return newController; } private bool m_RepaintRequested; /// /// Registers a new event to be used witin . /// /// The event to add. public void RegisterEvent(Event newEvent) { m_ConsumedEvents.Add(newEvent); } /// /// Calls the Use function of the registered events. /// /// The terrain in focus. /// The editcontext to repaint. /// public void ConsumeEvents(Terrain terrain, IOnSceneGUI editContext) { // Consume all of the events we've handled... foreach(Event currentEvent in m_ConsumedEvents) { currentEvent.Use(); } m_ConsumedEvents.Clear(); // Repaint everything if we need to... if(m_RepaintRequested) { EditorWindow view = EditorWindow.GetWindow(); editContext.Repaint(); view.Repaint(); m_RepaintRequested = false; } } /// /// Sets the repaint request to true. /// public void RequestRepaint() { m_RepaintRequested = true; } /// /// Renders the brush's GUI within the inspector view. /// /// The terrain in focus. /// The editcontext used to show the brush GUI. /// The brushflags to use when displaying the brush GUI. public virtual void OnInspectorGUI(Terrain terrain, IOnInspectorGUI editContext, BrushGUIEditFlags brushFlags = BrushGUIEditFlags.SelectAndInspect) { if (brushFlags != BrushGUIEditFlags.None) { editContext.ShowBrushesGUI(0, brushFlags); } EditorGUI.BeginChangeCheck(); m_ShowBrushMaskFilters = TerrainToolGUIHelper.DrawHeaderFoldout(Styles.brushMask, m_ShowBrushMaskFilters); if (m_ShowBrushMaskFilters) { brushMaskFilterStackView.OnGUI(); } m_ShowModifierControls = TerrainToolGUIHelper.DrawHeaderFoldout(Styles.stroke, m_ShowModifierControls); if (m_ShowModifierControls) { if(m_BrushStrengthController != null) { EditorGUILayout.BeginVertical(Styles.kGroupBox); m_BrushStrengthController.OnInspectorGUI(terrain, editContext); EditorGUILayout.EndVertical(); } if(m_BrushSizeController != null) { EditorGUILayout.BeginVertical(Styles.kGroupBox); m_BrushSizeController.OnInspectorGUI(terrain, editContext); EditorGUILayout.EndVertical(); } if(m_BrushRotationController != null) { EditorGUILayout.BeginVertical(Styles.kGroupBox); m_BrushRotationController?.OnInspectorGUI(terrain, editContext); EditorGUILayout.EndVertical(); } if((m_BrushSpacingController != null) || (m_BrushScatterController != null)) { EditorGUILayout.BeginVertical(Styles.kGroupBox); m_BrushSpacingController?.OnInspectorGUI(terrain, editContext); m_BrushScatterController?.OnInspectorGUI(terrain, editContext); EditorGUILayout.EndVertical(); } } if (EditorGUI.EndChangeCheck()) TerrainToolsAnalytics.OnParameterChange(); } private string getFilterStackFilePath { get { return Application.persistentDataPath + "/TerrainTools_" + m_Name + "_FilterStack.filterstack"; } } private FilterStack LoadFilterStack() { UnityEngine.Object[] obs = UnityEditorInternal.InternalEditorUtility.LoadSerializedFileAndForget( getFilterStackFilePath ); if( obs != null && obs.Length > 0 ) { return obs[ 0 ] as FilterStack; } return null; } private void SaveFilterStack( FilterStack filterStack ) { List< UnityEngine.Object > objList = new List< UnityEngine.Object >(); objList.Add( filterStack ); objList.AddRange( filterStack.filters ); filterStack.filters.ForEach( ( f ) => { var l = f.GetObjectsToSerialize(); if( l != null && l.Count > 0 ) { objList.AddRange( l ); } } ); // write to the file UnityEditorInternal.InternalEditorUtility.SaveToSerializedFileAndForget(objList.ToArray(), getFilterStackFilePath, true ); } /// /// Defines data when the brush is selected. /// /// public virtual void OnEnterToolMode() { m_BrushModifierKeyController?.OnEnterToolMode(); m_Controllers.ForEach((controller) => controller.OnEnterToolMode(s_ShortcutHandler)); TerrainToolsAnalytics.m_OriginalParameters = m_analyticsCallback?.Invoke(); } /// /// Defines data when the brush is deselected. /// /// public virtual void OnExitToolMode() { m_Controllers.ForEach((controller) => controller.OnExitToolMode(s_ShortcutHandler)); m_BrushModifierKeyController?.OnExitToolMode(); SaveFilterStack(brushMaskFilterStack); } /// /// Checks if the brush strokes are being recorded. /// public static bool isRecording = false; /// /// Provides methods for the brush's painting. /// [Serializable] public class OnPaintOccurrence { [NonSerialized] internal static List history = new List(); [NonSerialized] private static float prevRealTime; /// /// Initializes and returns an instance of OnPaintOccurrence. /// /// The brush's texture. /// The brush's size. /// The brush's strength. /// The brush's rotation. /// The cursor's X position within UV space. /// The cursor's Y position within UV space. public OnPaintOccurrence(Texture brushTexture, float brushSize, float brushStrength, float brushRotation, float uvX, float uvY) { this.xPos = uvX; this.yPos = uvY; this.brushTextureAssetPath = AssetDatabase.GetAssetPath(brushTexture); this.brushStrength = brushStrength; this.brushSize = brushSize; if (history.Count == 0) { duration = 0; } else { duration = Time.realtimeSinceStartup - prevRealTime; } prevRealTime = Time.realtimeSinceStartup; } /// /// The cursor's X position within UV space. /// [SerializeField] public float xPos; /// /// The cursor's Y position within UV space. /// [SerializeField] public float yPos; /// /// The asset file path of the brush texture in use. /// [SerializeField] public string brushTextureAssetPath; /// /// The brush strength. /// [SerializeField] public float brushStrength; /// /// The brush rotation. /// [SerializeField] public float brushRotation; /// /// The brush size. /// [SerializeField] public float brushSize; /// /// The total duration of painting. /// [SerializeField] public float duration; } /// /// Triggers events when painting on a terrain. /// /// The terrain in focus. /// The editcontext to reference. public virtual void OnPaint(Terrain terrain, IOnPaint editContext) { filterContext.ReleaseRTHandles(); // Manage brush capture history for playback in tests if (isRecording) { OnPaintOccurrence.history.Add(new OnPaintOccurrence(editContext.brushTexture, brushSize, brushStrength, brushRotation, editContext.uv.x, editContext.uv.y)); } m_Controllers.ForEach((controller) => controller.OnPaint(terrain, editContext)); if (isSmoothing) { Vector2 uv = editContext.uv; m_BrushSmoothController.kernelSize = (int)Mathf.Max(1, 0.1f * m_BrushSizeController.brushSize); m_BrushSmoothController.OnPaint(terrain, editContext, brushSize, brushRotation, brushStrength, uv); } /// Ensure that we re-randomize where the next scatter operation will place the brush, /// that way we can render the preview in a representative manner. m_BrushScatterController?.RequestRandomisation(); TerrainToolsAnalytics.UpdateAnalytics(this, m_analyticsCallback); filterContext.ReleaseRTHandles(); } /// /// Triggers events to render a 2D GUI within the Scene view. /// /// The terrain in focus. /// The editcontext to reference. /// public virtual void OnSceneGUI2D(Terrain terrain, IOnSceneGUI editContext) { StringBuilder builder = new StringBuilder(); Handles.BeginGUI(); { AppendBrushInfo(terrain, editContext, builder); string text = builder.ToString(); string trimmedText = text.Trim('\n', '\r', ' ', '\t'); GUILayout.Box(trimmedText, Styles.Box, GUILayout.ExpandWidth(false)); Handles.EndGUI(); } } /// /// Triggers events to render objects and displays within Scene view. /// /// The terrain in focus. /// The editcontext to reference. /// public virtual void OnSceneGUI(Terrain terrain, IOnSceneGUI editContext) { filterContext.ReleaseRTHandles(); Event currentEvent = Event.current; int controlId = GUIUtility.GetControlID(TerrainToolGUIHelper.s_TerrainEditorHash, FocusType.Passive); if(canUpdateTerrainUnderCursor) { isRaycastHitUnderCursorValid = editContext.hitValidTerrain; terrainUnderCursor = terrain; raycastHitUnderCursor = editContext.raycastHit; } m_Controllers.ForEach((controller) => controller.OnSceneGUI(currentEvent, controlId, terrain, editContext)); ConsumeEvents(terrain, editContext); if (!isRecording && OnPaintOccurrence.history.Count != 0) { SaveBrushData(); } brushMaskFilterStackView.OnSceneGUI(editContext.sceneView); if( editContext.hitValidTerrain && Event.current.keyCode == KeyCode.F && Event.current.type != EventType.Layout ) { SceneView.currentDrawingSceneView.Frame( new Bounds() { center = raycastHitUnderCursor.point, size = new Vector3( brushSize, 1, brushSize ) }, false ); Event.current.Use(); } filterContext.ReleaseRTHandles(); } private void SaveBrushData() { // Copy paintOccurrenceHistory to temp variable to prevent re-activating this condition List tmpPaintOccurrenceHistory = new List(OnPaintOccurrence.history); OnPaintOccurrence.history.Clear(); string fileName = EditorUtility.SaveFilePanelInProject("Save input playback", "PaintHistory", "txt", ""); if (fileName == "") { return; } FileStream file; if (File.Exists(fileName)) file = File.OpenWrite(fileName); else file = File.Create(fileName); BinaryFormatter binaryFormatter = new BinaryFormatter(); binaryFormatter.Serialize(file, tmpPaintOccurrenceHistory); file.Close(); } /// /// Adds basic information to the selected brush. /// /// The Terrain in focus. /// The IOnSceneGUI to reference. /// The StringBuilder containing the brush information. public virtual void AppendBrushInfo(Terrain terrain, IOnSceneGUI editContext, StringBuilder builder) { builder.AppendLine($"Brush: {m_Name}"); builder.AppendLine(); m_Controllers.ForEach((controller) => controller.AppendBrushInfo(terrain, editContext, builder)); builder.AppendLine(); builder.AppendLine(validationMessage); } /// /// Scatters the location of the brush's stamp operation. /// /// The terrain in reference. /// The UV location to scatter at. /// Returns false if there aren't any terrains to scatter the stamp on. public bool ScatterBrushStamp(ref Terrain terrain, ref Vector2 uv) { if(m_BrushScatterController == null) { bool invalidTerrain = terrain == null; return !invalidTerrain; } else { Vector2 scatteredUv = m_BrushScatterController.ScatterBrushStamp(uv, brushSize); Terrain scatteredTerrain = terrain; // Ensure that our UV is over a valid terrain AND in the range 0-1... while((scatteredTerrain != null) && (scatteredUv.x < 0.0f)) { scatteredTerrain = scatteredTerrain.leftNeighbor; scatteredUv.x += 1.0f; } while((scatteredTerrain != null) && (scatteredUv.x > 1.0f)) { scatteredTerrain = scatteredTerrain.rightNeighbor; scatteredUv.x -= 1.0f; } while((scatteredTerrain != null) && scatteredUv.y < 0.0f) { scatteredTerrain = scatteredTerrain.bottomNeighbor; scatteredUv.y += 1.0f; } while((scatteredTerrain != null) && (scatteredUv.y > 1.0f)) { scatteredTerrain = scatteredTerrain.topNeighbor; scatteredUv.y -= 1.0f; } // Did we run out of terrains? if(scatteredTerrain == null) { return false; } else { terrain = scatteredTerrain; uv = scatteredUv; return true; } } } /// /// Activates a modifier key controller. /// /// The modifier key to activate. /// Returns false when the modifier key controller is null. public bool ModifierActive(BrushModifierKey k) { return m_BrushModifierKeyController?.ModifierActive(k) ?? false; } private int m_TerrainUnderCursorLockCount = 0; /// /// Handles the locking of the terrain cursor in it's current position. /// /// This method is commonly used when utilizing shortcuts. /// Whether the cursor is visible within the scene. When the value is true the cursor is visible. /// public void LockTerrainUnderCursor(bool cursorVisible) { if (m_TerrainUnderCursorLockCount == 0) { Cursor.visible = cursorVisible; } m_TerrainUnderCursorLockCount++; } /// /// Handles unlocking of the terrain cursor. /// /// public void UnlockTerrainUnderCursor() { if (m_TerrainUnderCursorLockCount > 0) { m_TerrainUnderCursorLockCount--; } else if (m_TerrainUnderCursorLockCount == 0) { // Last unlock enables the cursor... Cursor.visible = true; } else if (m_TerrainUnderCursorLockCount < 0) { m_TerrainUnderCursorLockCount = 0; throw new ArgumentOutOfRangeException(nameof(m_TerrainUnderCursorLockCount), "Cannot reduce m_TerrainUnderCursorLockCount below zero. Possible mismatch between lock/unlock calls."); } } /// /// Checks if the cursor is currently locked and can not be updated. /// public bool canUpdateTerrainUnderCursor => m_TerrainUnderCursorLockCount == 0; /// /// Gets and sets the terrain in focus. /// public Terrain terrainUnderCursor { get; protected set; } /// /// Gets and sets the value associated to whether there is a raycast hit detecting a terrain under the cursor. /// public bool isRaycastHitUnderCursorValid { get; private set; } /// /// Gets and sets the raycast hit that was under the cursor's position. /// public RaycastHit raycastHitUnderCursor { get; protected set; } /// /// Gets and sets the message for validating terrain parameters. /// public virtual string validationMessage { get; set; } } }