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 { /// <summary> /// Provides methods for altering brush data. /// </summary> public abstract class BaseBrushUIGroup : IBrushUIGroup, IBrushEventHandler, IBrushTerrainCache { private bool m_ShowBrushMaskFilters = true; private bool m_ShowModifierControls = true; private static readonly BrushShortcutHandler<BrushShortcutType> s_ShortcutHandler = new BrushShortcutHandler<BrushShortcutType>(); private readonly string m_Name; private readonly HashSet<Event> m_ConsumedEvents = new HashSet<Event>(); private readonly List<IBrushController> m_Controllers = new List<IBrushController>(); 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; /// <summary> /// Gets the brush mask's <see cref="FilterStack"/>. /// </summary> 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; /// <summary> /// Gets the brush mask's <see cref="FilterStackView"/>. /// </summary> 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; } } /// <summary> /// Checks if Filters are enabled. /// </summary> public bool hasEnabledFilters => brushMaskFilterStack.hasEnabledFilters; /// <summary> /// Generates the brush mask. /// </summary> /// <param name="terrain">The terrain in focus.</param> /// <param name="sourceRenderTexture">The source render texture to blit from.</param> /// <param name="destinationRenderTexture">The destination render texture for bliting to.</param> /// <param name="position">The brush's position.</param> /// <param name="scale">The brush's scale.</param> /// <param name="rotation">The brush's rotation.</param> 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(); } /// <summary> /// Generates the brush mask. /// </summary> /// <param name="terrain">The terrain in focus.</param> /// <param name="sourceRenderTexture">The source render texture to blit from.</param> /// <param name="destinationRenderTexture">The destination render texture for bliting to.</param> /// <seealso cref="GenerateBrushMask(Terrain, RenderTexture, RenderTexture, Vector3, float, float)"/> public void GenerateBrushMask(Terrain terrain, RenderTexture sourceRenderTexture, RenderTexture destinationRenderTexture) { GenerateBrushMask(terrain, sourceRenderTexture, destinationRenderTexture, raycastHitUnderCursor.point, brushSize, brushRotation); } /// <summary> /// Generates the brush mask. /// </summary> /// <param name="sourceRenderTexture">The source render texture to blit from.</param> /// <param name="destinationRenderTexture">The destination render texture for bliting to.</param> /// <seealso cref="GenerateBrushMask(Terrain, RenderTexture, RenderTexture, Vector3, float, float)"/> public void GenerateBrushMask(RenderTexture sourceRenderTexture, RenderTexture destinationRenderTexture) { GenerateBrushMask(terrainUnderCursor, sourceRenderTexture, destinationRenderTexture); } /// <summary> /// Returns the brush name. /// </summary> public string brushName => m_Name; /// <summary> /// Gets and sets the brush size. /// </summary> /// <remarks>Gets a value of 100 if the brush size controller isn't initialized.</remarks> public float brushSize { get { return m_BrushSizeController?.brushSize ?? 100.0f; } set { m_BrushSizeController.brushSize = value; } } /// <summary> /// Gets and sets the brush rotation. /// </summary> /// <remarks>Gets a value of 0 if the brush size controller isn't initialized.</remarks> public float brushRotation { get { return m_BrushRotationController?.brushRotation ?? 0.0f; } set { m_BrushRotationController.brushRotation = value; } } /// <summary> /// Gets and sets the brush strength. /// </summary> /// <remarks>Gets a value of 1 if the brush size controller isn't initialized.</remarks> public float brushStrength { get { return m_BrushStrengthController?.brushStrength ?? 1.0f; } set { m_BrushStrengthController.brushStrength = value; } } /// <summary> /// Returns the brush spacing. /// </summary> /// <remarks>Returns a value of 0 if the brush size controller isn't initialized.</remarks> public float brushSpacing => m_BrushSpacingController?.brushSpacing ?? 0.0f; /// <summary> /// Returns the brush scatter. /// </summary> /// <remarks>Returns a value of 0 if the brush size controller isn't initialized.</remarks> 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; } } /// <summary> /// Checks if painting is allowed. /// </summary> public virtual bool allowPaint => (m_BrushSpacingController?.allowPaint ?? true) && !isSmoothing; /// <summary> /// Inverts the brush strength. /// </summary> public bool InvertStrength => m_BrushModifierKeyController?.ModifierActive(BrushModifierKey.BRUSH_MOD_INVERT) ?? false; /// <summary> /// Checks if the brush is in use. /// </summary> 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<TerrainToolsAnalytics.IBrushParameter[]> m_analyticsCallback; /// <summary> /// Initializes and returns an instance of BaseBrushUIGroup. /// </summary> /// <param name="name">The name of the brush.</param> /// <param name="analyticsCall">The brush's analytics function.</param> protected BaseBrushUIGroup(string name, Func<TerrainToolsAnalytics.IBrushParameter[]> 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 /// <summary> /// Adds a generic controller of type <see cref="IBrushController"/> to the brush's controller list. /// </summary> /// <typeparam name="TController">A generic controller type of IBrushController.</typeparam> /// <param name="newController">The new controller to add.</param> /// <returns>Returns the new generic controller.</returns> protected TController AddController<TController>(TController newController) where TController: IBrushController { m_Controllers.Add(newController); return newController; } /// <summary> /// Adds a rotation controller of type <see cref="IBrushRotationController"/> to the brush's controller list. /// </summary> /// <typeparam name="TController">A generic controller type of IBrushRotationController.</typeparam> /// <param name="newController">The new controller to add.</param> /// <returns>Returns the new rotation controller.</returns> protected TController AddRotationController<TController>(TController newController) where TController : IBrushRotationController { m_BrushRotationController = AddController(newController); return newController; } /// <summary> /// Adds a size controller of type <see cref="IBrushSizeController"/> to the brush's controller list. /// </summary> /// <typeparam name="TController">A generic controller type of IBrushSizeController.</typeparam> /// <param name="newController">The new controller to add.</param> /// <returns>Returns the new size controller.</returns> protected TController AddSizeController<TController>(TController newController) where TController : IBrushSizeController { m_BrushSizeController = AddController(newController); return newController; } /// <summary> /// Adds a strength controller of type <see cref="IBrushStrengthController"/> to the brush's controller list. /// </summary> /// <typeparam name="TController">A generic controller type of IBrushStrengthController.</typeparam> /// <param name="newController">The new controller to add.</param> /// <returns>Returns the new strength controller.</returns> protected TController AddStrengthController<TController>(TController newController) where TController : IBrushStrengthController { m_BrushStrengthController = AddController(newController); return newController; } /// <summary> /// Adds a spacing controller of type <see cref="IBrushSpacingController"/> to the brush's controller list. /// </summary> /// <typeparam name="TController">A generic controller type of IBrushSpacingController.</typeparam> /// <param name="newController">The new controller to add.</param> /// <returns>Returns the new spacing controller.</returns> protected TController AddSpacingController<TController>(TController newController) where TController : IBrushSpacingController { m_BrushSpacingController = AddController(newController); return newController; } /// <summary> /// Adds a scatter controller of type <see cref="IBrushScatterController"/> to the brush's controller list. /// </summary> /// <typeparam name="TController">A generic controller type of IBrushScatterController.</typeparam> /// <param name="newController">The new controller to add.</param> /// <returns>Returns the new scatter controller.</returns> protected TController AddScatterController<TController>(TController newController) where TController : IBrushScatterController { m_BrushScatterController = AddController(newController); return newController; } /// <summary> /// Adds a modifier key controller of type <see cref="IBrushModifierKeyController"/> to the brush's controller list. /// </summary> /// <typeparam name="TController">A generic controller type of IBrushModifierKeyController.</typeparam> /// <param name="newController">The new controller to add.</param> /// <returns>Returns the new modifier key controller.</returns> protected TController AddModifierKeyController<TController>(TController newController) where TController : IBrushModifierKeyController { m_BrushModifierKeyController = newController; return newController; } /// <summary> /// Adds a smoothing controller of type <see cref="IBrushSmoothController"/> to the brush's controller list. /// </summary> /// <typeparam name="TController">A generic controller type of IBrushSmoothController.</typeparam> /// <param name="newController">The new controller to add.</param> /// <returns>Returns the new smoothing controller.</returns> protected TController AddSmoothingController<TController>(TController newController) where TController : IBrushSmoothController { m_BrushSmoothController = newController; return newController; } private bool m_RepaintRequested; /// <summary> /// Registers a new event to be used witin <see cref="OnSceneGUI(Terrain, IOnSceneGUI)"/>. /// </summary> /// <param name="newEvent">The event to add.</param> public void RegisterEvent(Event newEvent) { m_ConsumedEvents.Add(newEvent); } /// <summary> /// Calls the Use function of the registered events. /// </summary> /// <param name="terrain">The terrain in focus.</param> /// <param name="editContext">The editcontext to repaint.</param> /// <seealso cref="RegisterEvent(Event)"/> 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<SceneView>(); editContext.Repaint(); view.Repaint(); m_RepaintRequested = false; } } /// <summary> /// Sets the repaint request to <c>true</c>. /// </summary> public void RequestRepaint() { m_RepaintRequested = true; } /// <summary> /// Renders the brush's GUI within the inspector view. /// </summary> /// <param name="terrain">The terrain in focus.</param> /// <param name="editContext">The editcontext used to show the brush GUI.</param> /// <param name="brushFlags">The brushflags to use when displaying the brush GUI.</param> 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 ); } /// <summary> /// Defines data when the brush is selected. /// </summary> /// <seealso cref="OnExitToolMode"/> public virtual void OnEnterToolMode() { m_BrushModifierKeyController?.OnEnterToolMode(); m_Controllers.ForEach((controller) => controller.OnEnterToolMode(s_ShortcutHandler)); TerrainToolsAnalytics.m_OriginalParameters = m_analyticsCallback?.Invoke(); } /// <summary> /// Defines data when the brush is deselected. /// </summary> /// <seealso cref="OnEnterToolMode"/> public virtual void OnExitToolMode() { m_Controllers.ForEach((controller) => controller.OnExitToolMode(s_ShortcutHandler)); m_BrushModifierKeyController?.OnExitToolMode(); SaveFilterStack(brushMaskFilterStack); } /// <summary> /// Checks if the brush strokes are being recorded. /// </summary> public static bool isRecording = false; /// <summary> /// Provides methods for the brush's painting. /// </summary> [Serializable] public class OnPaintOccurrence { [NonSerialized] internal static List<OnPaintOccurrence> history = new List<OnPaintOccurrence>(); [NonSerialized] private static float prevRealTime; /// <summary> /// Initializes and returns an instance of OnPaintOccurrence. /// </summary> /// <param name="brushTexture">The brush's texture.</param> /// <param name="brushSize">The brush's size.</param> /// <param name="brushStrength">The brush's strength.</param> /// <param name="brushRotation">The brush's rotation.</param> /// <param name="uvX">The cursor's X position within UV space.</param> /// <param name="uvY">The cursor's Y position within UV space.</param> 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; } /// <summary> /// The cursor's X position within UV space. /// </summary> [SerializeField] public float xPos; /// <summary> /// The cursor's Y position within UV space. /// </summary> [SerializeField] public float yPos; /// <summary> /// The asset file path of the brush texture in use. /// </summary> [SerializeField] public string brushTextureAssetPath; /// <summary> /// The brush strength. /// </summary> [SerializeField] public float brushStrength; /// <summary> /// The brush rotation. /// </summary> [SerializeField] public float brushRotation; /// <summary> /// The brush size. /// </summary> [SerializeField] public float brushSize; /// <summary> /// The total duration of painting. /// </summary> [SerializeField] public float duration; } /// <summary> /// Triggers events when painting on a terrain. /// </summary> /// <param name="terrain">The terrain in focus.</param> /// <param name="editContext">The editcontext to reference.</param> 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(); } /// <summary> /// Triggers events to render a 2D GUI within the Scene view. /// </summary> /// <param name="terrain">The terrain in focus.</param> /// <param name="editContext">The editcontext to reference.</param> /// <seealso cref="OnSceneGUI(Terrain, IOnSceneGUI)"/> 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(); } } /// <summary> /// Triggers events to render objects and displays within Scene view. /// </summary> /// <param name="terrain">The terrain in focus.</param> /// <param name="editContext">The editcontext to reference.</param> /// <seealso cref="OnSceneGUI(Terrain, IOnSceneGUI)"/> 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<OnPaintOccurrence> tmpPaintOccurrenceHistory = new List<OnPaintOccurrence>(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(); } /// <summary> /// Adds basic information to the selected brush. /// </summary> /// <param name="terrain">The Terrain in focus.</param> /// <param name="editContext">The IOnSceneGUI to reference.</param> /// <param name="builder">The StringBuilder containing the brush information.</param> 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); } /// <summary> /// Scatters the location of the brush's stamp operation. /// </summary> /// <param name="terrain">The terrain in reference.</param> /// <param name="uv">The UV location to scatter at.</param> /// <returns>Returns false if there aren't any terrains to scatter the stamp on.</returns> 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; } } } /// <summary> /// Activates a modifier key controller. /// </summary> /// <param name="k">The modifier key to activate.</param> /// <returns>Returns false when the modifier key controller is null.</returns> public bool ModifierActive(BrushModifierKey k) { return m_BrushModifierKeyController?.ModifierActive(k) ?? false; } private int m_TerrainUnderCursorLockCount = 0; /// <summary> /// Handles the locking of the terrain cursor in it's current position. /// </summary> /// <remarks>This method is commonly used when utilizing shortcuts.</remarks> /// <param name="cursorVisible">Whether the cursor is visible within the scene. When the value is <c>true</c> the cursor is visible.</param> /// <seealso cref="UnlockTerrainUnderCursor"/> public void LockTerrainUnderCursor(bool cursorVisible) { if (m_TerrainUnderCursorLockCount == 0) { Cursor.visible = cursorVisible; } m_TerrainUnderCursorLockCount++; } /// <summary> /// Handles unlocking of the terrain cursor. /// </summary> /// <seealso cref="LockTerrainUnderCursor(bool)"/> 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."); } } /// <summary> /// Checks if the cursor is currently locked and can not be updated. /// </summary> public bool canUpdateTerrainUnderCursor => m_TerrainUnderCursorLockCount == 0; /// <summary> /// Gets and sets the terrain in focus. /// </summary> public Terrain terrainUnderCursor { get; protected set; } /// <summary> /// Gets and sets the value associated to whether there is a raycast hit detecting a terrain under the cursor. /// </summary> public bool isRaycastHitUnderCursorValid { get; private set; } /// <summary> /// Gets and sets the raycast hit that was under the cursor's position. /// </summary> public RaycastHit raycastHitUnderCursor { get; protected set; } /// <summary> /// Gets and sets the message for validating terrain parameters. /// </summary> public virtual string validationMessage { get; set; } } }