using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.Serialization.Formatters.Binary; using System.Text; using UnityEditor.EditorTools; using UnityEditor.ShortcutManagement; using UnityEngine; using UnityEngine.Experimental.Rendering; using UnityEditor.Overlays; using UnityEngine.UIElements; using Cursor = UnityEngine.Cursor; using UnityEngine.TerrainTools; 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; internal bool m_HasBrushSize; // tells you which of the controllers are null or not internal bool m_HasBrushRotation; internal bool m_HasBrushStrength; internal bool m_HasBrushSpacing; internal bool m_HasBrushScatter; [ 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(); } // If there is no filter stack or if it failed to load if( m_BrushMaskFilterStack == null ) { // 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; 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; private void SetFilterContext(TerrainData terrainData, Vector3 position, float scale, float rotation) { // set the filter context properties filterContext.brushPos = position; filterContext.brushSize = scale; filterContext.brushRotation = rotation; // bind properties for filters to read/write to filterContext.diffuseTextures = terrainData.terrainLayers; 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); } /// /// Generates the brush mask. /// /// /// **Note:** Using this version of the overloaded method allows for the use of the Layer Filter /// /// The terrain in focus. /// The brushRender object used for acquiring the heightmap and splatmap 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, IBrushRenderUnderCursor brushRender, RenderTexture destinationRenderTexture, Vector3 position, float scale, float rotation) { filterContext.ReleaseRTHandles(); FilterUtility.LayerFilterActiveState = true; if (brushRender.CalculateBrushTransform(out BrushTransform brushTransform)) { Rect brushRect = brushTransform.GetBrushXYBounds(); using (new ActiveRenderTextureScope(null)) { TerrainData terrainData = terrain.terrainData; SetFilterContext(terrainData, position, scale, rotation); // bind terrain texture data PaintContext heightContext = brushRender.AcquireHeightmap(false, brushRect); filterContext.rtHandleCollection.AddRTHandle(0, FilterContext.Keywords.Heightmap, heightContext.sourceRenderTexture.graphicsFormat); List layerFilters = brushMaskFilterStack.filters.OfType().ToList(); if(layerFilters.Count > 0) { // bind layer filter alphamap textures for (int i = 0; i < layerFilters.Count; i++) { LayerFilter layerFilter = layerFilters[i]; if (layerFilter.layerIndex >= terrainData.terrainLayers.Length) continue; PaintContext paintContext = brushRender.AcquireTexture(false, brushRect, terrain.terrainData.terrainLayers[layerFilter.layerIndex]); layerFilter.splatmapKeyword = FilterContext.Keywords.Splatmap + i; filterContext.rtHandleCollection.AddRTHandle(1 + i, layerFilter.splatmapKeyword, paintContext.sourceRenderTexture.graphicsFormat); brushRender.Release(paintContext); } filterContext.rtHandleCollection.GatherRTHandles(heightContext.sourceRenderTexture.width, heightContext.sourceRenderTexture.height); // blit the gathered rthandles foreach(LayerFilter layerFilter in layerFilters) { if (layerFilter.layerIndex >= terrainData.terrainLayers.Length) continue; PaintContext paintContext = brushRender.AcquireTexture(false, brushRect, terrain.terrainData.terrainLayers[layerFilter.layerIndex]); Graphics.Blit(paintContext.sourceRenderTexture, filterContext.rtHandleCollection[layerFilter.splatmapKeyword]); brushRender.Release(paintContext); } Graphics.Blit(heightContext.sourceRenderTexture, filterContext.rtHandleCollection[FilterContext.Keywords.Heightmap]); } else { filterContext.rtHandleCollection.GatherRTHandles(heightContext.sourceRenderTexture.width, heightContext.sourceRenderTexture.height); Graphics.Blit(heightContext.sourceRenderTexture, filterContext.rtHandleCollection[FilterContext.Keywords.Heightmap]); } brushMaskFilterStack.Eval(filterContext, heightContext.sourceRenderTexture, destinationRenderTexture); } filterContext.ReleaseRTHandles(); } } /// /// Generates the brush mask. /// /// /// **Note:** Use /// if Layer Filtering capabilities are desired. /// /// 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(); FilterUtility.LayerFilterActiveState = false; using (new ActiveRenderTextureScope(null)) { SetFilterContext(terrain.terrainData, position, scale, rotation); // 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); } /// /// Generates the brush mask. /// /// The brushRender object used for acquiring the heightmap and splatmap texture to blit from. /// The destination render texture for bliting to. public void GenerateBrushMask(IBrushRenderUnderCursor brushRender, RenderTexture destinationRenderTexture) { GenerateBrushMask(terrainUnderCursor, brushRender, destinationRenderTexture, raycastHitUnderCursor.point, brushSize, brushRotation); } /// /// Returns the brush name. /// public string brushName => m_Name; /// /// Does the commonUI have a size controller? /// public bool hasBrushSize => m_HasBrushSize; /// /// Does the commonUI have a rotation controller? /// public bool hasBrushRotation => m_HasBrushRotation; /// /// Does the commonUI have a strength controller? /// public bool hasBrushStrength => m_HasBrushStrength; /// /// Does the commonUI have a spacing controller? /// public bool hasBrushSpacing => m_HasBrushSpacing; /// /// Does the commonUI have a scatter controller? /// public bool hasBrushScatter => m_HasBrushScatter; /// /// 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; } } /// /// The size of the brush without jitter. /// /// Gets a value of 100 if the brush size controller isn't initialized. public float brushSizeVal { get { return m_BrushSizeController?.brushSizeVal ?? 100.0f; } } /// /// Gets and sets the brush min size. /// /// Gets a value of 0 if the brush size controller isn't initialized. public float brushSizeMin { get { return m_BrushSizeController?.brushSizeMin ?? 0.0f; } set { m_BrushSizeController.brushSizeMin = value; } } /// /// Gets and sets the brush max size. /// /// Gets a value of 1 if the brush size controller isn't initialized. public float brushSizeMax { get { return m_BrushSizeController?.brushSizeMax ?? 1.0f; } set { m_BrushSizeController.brushSizeMax = value; } } /// /// Gets and sets the brush size jitter. /// /// Gets a value of 0 if the brush size controller isn't initialized. public float brushSizeJitter { get { return m_BrushSizeController?.brushSizeJitter ?? 0.0f; } set { m_BrushSizeController.brushSizeJitter = value; } } /// /// Gets and sets the brush rotation. /// /// Gets a value of 0 if the brush rotation controller isn't initialized. public float brushRotation { get { return m_BrushRotationController?.brushRotation ?? 0.0f; } set { m_BrushRotationController.brushRotation = value; } } /// /// The strength of the brush without jitter. /// /// Gets a value of 0 if the brush rotation controller isn't initialized. public float brushRotationVal { get { return m_BrushRotationController?.brushRotationVal ?? 0.0f; } } /// /// Gets and sets the brush rotation jitter. /// /// Gets a value of 0 if the brush rotation controller isn't initialized. public float brushRotationJitter { get { return m_BrushRotationController?.brushRotationJitter ?? 0.0f; } set { m_BrushRotationController.brushRotationJitter = value; } } /// /// Gets and sets the brush strength. /// /// Gets a value of 1 if the brush strength controller isn't initialized. public float brushStrength { get { return m_BrushStrengthController?.brushStrength ?? 1.0f; } set { m_BrushStrengthController.brushStrength = value; } } /// /// The strength of the brush without jitter. /// /// Gets a value of 1 if the brush strength controller isn't initialized. public float brushStrengthVal { get { return m_BrushStrengthController?.brushStrengthVal ?? 1.0f; } } /// /// Gets and sets the brush min strength. /// /// Gets a value of 0 if the brush strength controller isn't initialized. public float brushStrengthMin { get { return m_BrushStrengthController?.brushStrengthMin ?? 0.0f; } set { m_BrushStrengthController.brushStrengthMin = value; } } /// /// Gets and sets the brush max strength. /// /// Gets a value of 1 if the brush strength controller isn't initialized. public float brushStrengthMax { get { return m_BrushStrengthController?.brushStrengthMax ?? 1.0f; } set { m_BrushStrengthController.brushStrengthMax = value; } } /// /// Gets and sets the brush strength jitter. /// /// Gets a value of 0 if the brush strength controller isn't initialized. public float brushStrengthJitter { get { return m_BrushStrengthController?.brushStrengthJitter ?? 0.0f; } set { m_BrushStrengthController.brushStrengthJitter = value; } } /// /// Returns the brush spacing. /// /// Returns a value of 0 if the brush spacing controller isn't initialized. public float brushSpacing { get { return m_BrushSpacingController?.brushSpacing ?? 0.0f; } set { m_BrushSpacingController.brushSpacing = value; } } /// /// Returns the brush scatter. /// /// Returns a value of 0 if the brush scattering controller isn't initialized. public float brushScatter { get { return m_BrushScatterController?.brushScatter ?? 0.0f; } set { m_BrushScatterController.brushScatter = value; } } 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) { OnInspectorGUI(terrain, editContext, false); } /// /// Renders the brush's GUI within the inspector view. /// /// The terrain in focus. /// The editcontext used to show the brush GUI. /// The bool to mark true when showing UI specific for overlays. /// The brushflags to use when displaying the brush GUI. /// public virtual void OnInspectorGUI(Terrain terrain, IOnInspectorGUI editContext, bool overlays, BrushGUIEditFlags brushFlags = BrushGUIEditFlags.SelectAndInspect, BrushOverlaysGUIFlags brushOverlaysFlags = BrushOverlaysGUIFlags.All) { // get flag booleans for overlays bool showNone = brushOverlaysFlags.Equals(BrushOverlaysGUIFlags.None); bool showFilter = (brushOverlaysFlags & BrushOverlaysGUIFlags.Filter) != 0; bool showStrength = (brushOverlaysFlags & BrushOverlaysGUIFlags.Strength) != 0; bool showSize = (brushOverlaysFlags & BrushOverlaysGUIFlags.Size) != 0; bool showRotation = (brushOverlaysFlags & BrushOverlaysGUIFlags.Rotation) != 0; bool showSpacing = (brushOverlaysFlags & BrushOverlaysGUIFlags.Spacing) != 0; bool showScatter = (brushOverlaysFlags & BrushOverlaysGUIFlags.Scatter) != 0; if (overlays && showNone) return; // if show nothing for overlays, then return if (brushFlags != BrushGUIEditFlags.None && !overlays) { editContext.ShowBrushesGUI(0, brushFlags); } EditorGUI.BeginChangeCheck(); if (showFilter) { // only show drop down for not overlays if (!overlays) m_ShowBrushMaskFilters = TerrainToolGUIHelper.DrawHeaderFoldout(Styles.brushMask, m_ShowBrushMaskFilters); if (m_ShowBrushMaskFilters) { brushMaskFilterStackView.OnGUI(); } } // only show drop down for not overlays if (!overlays) m_ShowModifierControls = TerrainToolGUIHelper.DrawHeaderFoldout(Styles.stroke, m_ShowModifierControls); if (m_ShowModifierControls) { if(m_BrushStrengthController != null && showStrength) { EditorGUILayout.BeginVertical(Styles.kGroupBox); m_BrushStrengthController.OnInspectorGUI(terrain, editContext); EditorGUILayout.EndVertical(); } if(m_BrushSizeController != null && showSize) { EditorGUILayout.BeginVertical(Styles.kGroupBox); m_BrushSizeController.OnInspectorGUI(terrain, editContext); EditorGUILayout.EndVertical(); } if(m_BrushRotationController != null && showRotation) { EditorGUILayout.BeginVertical(Styles.kGroupBox); m_BrushRotationController?.OnInspectorGUI(terrain, editContext); EditorGUILayout.EndVertical(); } // if NOT overlays then draw spacing and scatter together. else draw scatter and spacing separately if (overlays) { if (((m_BrushSpacingController != null) || (m_BrushScatterController != null)) && showSpacing) { EditorGUILayout.BeginVertical(Styles.kGroupBox); m_BrushSpacingController?.OnInspectorGUI(terrain, editContext); EditorGUILayout.EndVertical(); } if (((m_BrushSpacingController != null) || (m_BrushScatterController != null)) && showScatter) { EditorGUILayout.BeginVertical(Styles.kGroupBox); m_BrushScatterController?.OnInspectorGUI(terrain, editContext); EditorGUILayout.EndVertical(); } } else { 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. /// private static string brushInfo = ""; // adding this to grab brush info 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'); brushInfo = trimmedText; BrushInfoIsAccessed(); Handles.EndGUI(); } } // adding this to grab brush info public static string getBrushInfoText() { return brushInfo; } public static event Action brushInfoAccessed; internal static void BrushInfoIsAccessed() { if (brushInfoAccessed == null) return; brushInfoAccessed(); } [Overlay(typeof(SceneView), k_Id, "Brush Info")] class BrushInfoOverlay : Overlay, ITransientOverlay { const string k_Id = "brush-info-overlay"; private Label m_Label; public bool visible { get { var currTool = BrushesOverlay.ActiveTerrainTool as TerrainPaintToolWithOverlaysBase; if (currTool == null) return false; return currTool.HasBrushAttributes && BrushesOverlay.IsSelectedObjectTerrain(); } } void UpdateText() { if (m_Label != null) m_Label.text = getBrushInfoText(); } void EmptyText() { if (m_Label != null) m_Label.text = ""; } public override VisualElement CreatePanelContent() { brushInfoAccessed += UpdateText; Selection.selectionChanged += EmptyText; return m_Label = new Label(getBrushInfoText()); } public override void OnWillBeDestroyed() { base.OnWillBeDestroyed(); brushInfoAccessed -= UpdateText; Selection.selectionChanged -= EmptyText; } } /// /// 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; } } }