Search Unity

Please add this Path Tool to the Unity Terrain Tools in the next version

Discussion in 'World Building' started by Rowlan, Oct 3, 2020.

  1. Rowlan

    Rowlan

    Joined:
    Aug 4, 2016
    Posts:
    4,293
    I was really looking forward to the Environment System, but unfortunately that got delayed recently. So I thought I'd update my Path Paint Tool. But I just noticed that in the latest version of the Unity Terrain Tools there is still no way to paint a path. But there's the Bridge Tool. So I modded the Bridge Tool again to act as a path painter. It's a fairly easy change as you can see yourself, but @Unity please add a tool to paint paths like it's common in all the other game engines.

    The Bridge Tool is both awesome and absolutely beyond useless. Does anyone have a use case for that to paint a star, i. e. always the same stroke from the center? Here's the difference:



    Basically my mod changes the start position to be the last click position instead of always the first one. Or in path paint mode it just performs the bridge tool code while you drag the mouse.

    There's very little code change to make it become a path painter, you can verify the diff yourself:

    https://github.com/Roland09/PathPaintTool-Core/blob/main/Assets/PathTool/Editor/PathTool.cs

    Also that noise tool is way under-advertised, it's really awesome. You should check it out.

    Feel free to use this modified Bridge Tool code in case you don't want to go to github:

    Code (CSharp):
    1. /**
    2. * This is based on the work of Unity (Terrain Tools 3.0.1-preview), that way the license is the same as the original Terrain Tools license.
    3. * Please be aware that Unity is in no way affiliated with this project!
    4. *
    5. * Terrain Tools License:
    6. * -----------------------------------------------------------------------
    7. * Terrain Tools copyright 2019 Unity Technologies ApS
    8. *
    9. * Licensed under the Unity Companion License for Unity-dependent projects--see [Unity Companion License](http://www.unity3d.com/legal/licenses/Unity_Companion_License).
    10. *
    11. * Unless expressly provided otherwise, the Software under this license is made available strictly on an AS IS BASIS WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. Please review the license for details on these and other terms and conditions.
    12. */
    13. using UnityEngine;
    14. using UnityEngine.Experimental.TerrainAPI;
    15. using UnityEditor.ShortcutManagement;
    16.  
    17. namespace UnityEditor.Experimental.TerrainAPI
    18. {
    19.     public class PathTool : TerrainPaintTool<PathTool>
    20.     {
    21. #if UNITY_2019_1_OR_NEWER
    22.         [Shortcut("Terrain/Select Path Tool", typeof(TerrainToolShortcutContext))]                // tells shortcut manager what to call the shortcut and what to pass as args
    23.         static void SelectShortcut(ShortcutArguments args) {
    24.             TerrainToolShortcutContext context = (TerrainToolShortcutContext)args.context;          // gets interface to modify state of TerrainTools
    25.             context.SelectPaintTool<PathTool>();                                                  // set active tool
    26.         }
    27. #endif
    28.  
    29.         [SerializeField]
    30.         IBrushUIGroup m_commonUI;
    31.         private IBrushUIGroup commonUI
    32.         {
    33.             get
    34.             {
    35.                 if( m_commonUI == null )
    36.                 {
    37.                     m_commonUI = new DefaultBrushUIGroup("PathTool", DefaultBrushUIGroup.Feature.NoScatter );
    38.                     m_commonUI.OnEnterToolMode();
    39.                 }
    40.  
    41.                 return m_commonUI;
    42.             }
    43.         }
    44.  
    45.         Terrain m_StartTerrain = null;
    46.         private Vector3 m_StartPoint;
    47.  
    48.         Material m_Material = null;
    49.         Material GetPaintMaterial() {
    50.             if (m_Material == null)
    51.                 m_Material = new Material(Shader.Find("Hidden/TerrainTools/SetExactHeight"));
    52.             return m_Material;
    53.         }
    54.  
    55.         enum PaintMode
    56.         {
    57.             /// <summary>
    58.             /// Paint via dragging the mouse
    59.             /// </summary>
    60.             Paint,
    61.  
    62.             /// <summary>
    63.             /// Similar to Bridge Tool, but uses the last click position as source for the next bridge
    64.             /// </summary>
    65.             Stroke
    66.         }
    67.  
    68.         [System.Serializable]
    69.         class PathToolSerializedProperties
    70.         {
    71.             public PaintMode paintMode;
    72.  
    73.             public AnimationCurve widthProfile;
    74.             public AnimationCurve heightProfile;
    75.             public AnimationCurve strengthProfile;
    76.             public AnimationCurve jitterProfile;
    77.  
    78.             public void SetDefaults()
    79.             {
    80.                 paintMode = PaintMode.Paint;
    81.  
    82.                 widthProfile = AnimationCurve.Linear(0, 1, 1, 1);
    83.                 heightProfile = AnimationCurve.Linear(0, 0, 1, 0);
    84.                 strengthProfile = AnimationCurve.Linear(0, 1, 1, 1);
    85.                 jitterProfile = AnimationCurve.Linear(0, 0, 1, 0);
    86.             }
    87.         }
    88.  
    89.         PathToolSerializedProperties pathToolProperties = new PathToolSerializedProperties();
    90.  
    91.         public override string GetName()
    92.         {
    93.             return "Sculpt/Path";
    94.         }
    95.  
    96.         public override string GetDesc()
    97.         {
    98.             return "Paint Mode: drag brush to paint a path.\nStroke Mode: Control + Click to Set the first start point, click to connect the path.";
    99.         }
    100.  
    101.         public override void OnEnterToolMode() {
    102.             base.OnEnterToolMode();
    103.             commonUI.OnEnterToolMode();
    104.         }
    105.  
    106.         public override void OnExitToolMode() {
    107.             base.OnExitToolMode();
    108.             commonUI.OnExitToolMode();
    109.         }
    110.  
    111.         public override void OnSceneGUI(Terrain terrain, IOnSceneGUI editContext)
    112.         {
    113.             commonUI.OnSceneGUI2D(terrain, editContext);
    114.  
    115.             if (editContext.hitValidTerrain || commonUI.isInUse)
    116.             {
    117.                 commonUI.OnSceneGUI(terrain, editContext);
    118.  
    119.                 if (Event.current.type != EventType.Repaint)
    120.                 {
    121.                     return;
    122.                 }
    123.  
    124.                 if (pathToolProperties != null && pathToolProperties.widthProfile != null)
    125.                 {
    126.                     float endWidth = Mathf.Abs(pathToolProperties.widthProfile.Evaluate(1.0f));
    127.  
    128.                     BrushTransform brushXform = TerrainPaintUtility.CalculateBrushTransform(terrain, commonUI.raycastHitUnderCursor.textureCoord, commonUI.brushSize * endWidth, commonUI.brushRotation);
    129.                     PaintContext ctx = TerrainPaintUtility.BeginPaintHeightmap(terrain, brushXform.GetBrushXYBounds(), 1);
    130.                     TerrainPaintUtilityEditor.DrawBrushPreview(ctx, TerrainPaintUtilityEditor.BrushPreview.SourceRenderTexture, editContext.brushTexture, brushXform, TerrainPaintUtilityEditor.GetDefaultBrushPreviewMaterial(), 0);
    131.                     TerrainPaintUtility.ReleaseContextResources(ctx);
    132.                 }          
    133.             }
    134.  
    135.             if (Event.current.type != EventType.Repaint)
    136.             {
    137.                 return;
    138.             }
    139.  
    140.             switch (pathToolProperties.paintMode)
    141.             {
    142.                 case PaintMode.Paint:
    143.                     // nothing to do, no special indicator
    144.                     break;
    145.  
    146.                 case PaintMode.Stroke:
    147.                     //display a brush preview at first or last clicked path location, using starting size from width profile
    148.                     if (m_StartTerrain != null)
    149.                     {
    150.                         float startWidth = Mathf.Abs(pathToolProperties.widthProfile.Evaluate(0.0f));
    151.  
    152.                         BrushTransform brushTransform = TerrainPaintUtility.CalculateBrushTransform(m_StartTerrain, m_StartPoint, commonUI.brushSize * startWidth, commonUI.brushRotation);
    153.                         PaintContext sampleContext = TerrainPaintUtility.BeginPaintHeightmap(m_StartTerrain, brushTransform.GetBrushXYBounds());
    154.                         TerrainPaintUtilityEditor.DrawBrushPreview(sampleContext, TerrainPaintUtilityEditor.BrushPreview.SourceRenderTexture,
    155.                                                                    editContext.brushTexture, brushTransform, TerrainPaintUtilityEditor.GetDefaultBrushPreviewMaterial(), 0);
    156.                         TerrainPaintUtility.ReleaseContextResources(sampleContext);
    157.                     }
    158.                     break;
    159.  
    160.                 default:
    161.                     throw new System.Exception(string.Format("Unsupported paint mode {0}", pathToolProperties.paintMode));
    162.             }
    163.         }
    164.  
    165.         bool m_ShowPathControls = true;
    166.         bool m_initialized = false;
    167.         public override void OnInspectorGUI(Terrain terrain, IOnInspectorGUI editContext)
    168.         {
    169.  
    170.             if (!m_initialized)
    171.             {
    172.                 LoadSettings();
    173.                 m_initialized = true;
    174.             }
    175.             EditorGUI.BeginChangeCheck();
    176.  
    177.             commonUI.OnInspectorGUI(terrain, editContext);
    178.  
    179.             m_ShowPathControls = TerrainToolGUIHelper.DrawHeaderFoldoutForBrush(Styles.controlHeader, m_ShowPathControls, pathToolProperties.SetDefaults);
    180.  
    181.             if (m_ShowPathControls) {
    182.  
    183.                 pathToolProperties.paintMode = (PaintMode)EditorGUILayout.EnumPopup(Styles.paintModeContent, pathToolProperties.paintMode);
    184.  
    185.                 //"Controls the width of the path over the length of the stroke"
    186.                 pathToolProperties.widthProfile = EditorGUILayout.CurveField(Styles.widthProfileContent, pathToolProperties.widthProfile);
    187.                 pathToolProperties.heightProfile = EditorGUILayout.CurveField(Styles.heightProfileContent, pathToolProperties.heightProfile);
    188.                 pathToolProperties.strengthProfile = EditorGUILayout.CurveField(Styles.strengthProfileContent, pathToolProperties.strengthProfile);
    189.                 pathToolProperties.jitterProfile = EditorGUILayout.CurveField(Styles.jitterProfileContent, pathToolProperties.jitterProfile);
    190.             }
    191.  
    192.             if (EditorGUI.EndChangeCheck())
    193.             {
    194.                 SaveSetting();
    195.                 Save(true);
    196.             }
    197.         }
    198.  
    199.         private Vector2 transformToWorld(Terrain t, Vector2 uvs)
    200.         {
    201.             Vector3 tilePos = t.GetPosition();
    202.             return new Vector2(tilePos.x, tilePos.z) + uvs * new Vector2(t.terrainData.size.x, t.terrainData.size.z);
    203.         }
    204.  
    205.         private Vector2 transformToUVSpace(Terrain originTile, Vector2 worldPos) {
    206.             Vector3 originTilePos = originTile.GetPosition();
    207.             Vector2 uvPos = new Vector2((worldPos.x - originTilePos.x) / originTile.terrainData.size.x,
    208.                                         (worldPos.y - originTilePos.z) / originTile.terrainData.size.z);
    209.             return uvPos;
    210.         }
    211.  
    212.         private void ApplyBrushInternal(Terrain terrain, Vector2 uv, Texture brushTexture, float brushSpacing)
    213.         {
    214.             //get the target position & height
    215.             float targetHeight = terrain.terrainData.GetInterpolatedHeight(uv.x, uv.y) / terrain.terrainData.size.y;
    216.             Vector3 targetPos = new Vector3(uv.x, uv.y, targetHeight);
    217.  
    218.             if (terrain != m_StartTerrain) {
    219.                 //figure out the stroke vector in uv,height space
    220.                 Vector2 targetWorld = transformToWorld(terrain, uv);
    221.                 Vector2 targetUVs = transformToUVSpace(m_StartTerrain, targetWorld);
    222.                 targetPos.x = targetUVs.x;
    223.                 targetPos.y = targetUVs.y;
    224.             }
    225.  
    226.             Vector3 stroke = targetPos - m_StartPoint;
    227.             float strokeLength = stroke.magnitude;
    228.             int numSplats;
    229.  
    230.             switch( pathToolProperties.paintMode)
    231.             {
    232.                 case PaintMode.Paint:
    233.                     numSplats = 1;
    234.                     break;
    235.  
    236.                 case PaintMode.Stroke:
    237.                     numSplats = (int)(strokeLength / (0.1f * Mathf.Max(brushSpacing, 0.01f)));
    238.                     break;
    239.  
    240.                 default:
    241.                     throw new System.Exception( string.Format("Unsupported paint mode {0}", pathToolProperties.paintMode));
    242.             }
    243.  
    244.             Terrain currTerrain = m_StartTerrain;
    245.             Material mat = GetPaintMaterial();
    246.  
    247.             Vector2 posOffset = new Vector2(0.0f, 0.0f);
    248.             Vector2 currUV = new Vector2();
    249.             Vector4 brushParams = new Vector4();
    250.  
    251.             Vector2 jitterVec = new Vector2(-stroke.z, stroke.x); //perpendicular to stroke direction
    252.             jitterVec.Normalize();
    253.  
    254.             for (int i = 0; i < numSplats; i++)
    255.             {
    256.                 float pct = (float)i / (float)numSplats;
    257.  
    258.                 float widthScale = pathToolProperties.widthProfile.Evaluate(pct);
    259.                 float heightOffset = pathToolProperties.heightProfile.Evaluate(pct) / currTerrain.terrainData.size.y;
    260.                 float strengthScale = pathToolProperties.strengthProfile.Evaluate(pct);
    261.                 float jitterOffset = pathToolProperties.jitterProfile.Evaluate(pct) / Mathf.Max(currTerrain.terrainData.size.x, currTerrain.terrainData.size.z);
    262.  
    263.                 Vector3 currPos = m_StartPoint + pct * stroke;
    264.  
    265.                 //add in jitter offset (needs to happen before tile correction)
    266.                 currPos.x += posOffset.x + jitterOffset * jitterVec.x;
    267.                 currPos.y += posOffset.y + jitterOffset * jitterVec.y;
    268.  
    269.                 if (currPos.x >= 1.0f && (currTerrain.rightNeighbor != null)) {
    270.                     currTerrain = currTerrain.rightNeighbor;
    271.                     currPos.x -= 1.0f;
    272.                     posOffset.x -= 1.0f;
    273.                 }
    274.                 if(currPos.x <= 0.0f && (currTerrain.leftNeighbor != null)) {
    275.                     currTerrain = currTerrain.leftNeighbor;
    276.                     currPos.x += 1.0f;
    277.                     posOffset.x += 1.0f;
    278.                 }
    279.                 if(currPos.y >= 1.0f && (currTerrain.topNeighbor != null)) {
    280.                     currTerrain = currTerrain.topNeighbor;
    281.                     currPos.y -= 1.0f;
    282.                     posOffset.y -= 1.0f;
    283.                 }
    284.                 if(currPos.y <= 0.0f && (currTerrain.bottomNeighbor != null)) {
    285.                     currTerrain = currTerrain.bottomNeighbor;
    286.                     currPos.y += 1.0f;
    287.                     posOffset.y += 1.0f;
    288.                 }
    289.  
    290.                 currUV.x = currPos.x;
    291.                 currUV.y = currPos.y;
    292.  
    293.                 int finalBrushSize = (int)(widthScale * (float)commonUI.brushSize);
    294.                 float finalHeight =  (m_StartPoint + pct * stroke).z + heightOffset;
    295.  
    296.                 using(IBrushRenderWithTerrain brushRenderWithTerrain = new BrushRenderWithTerrainUiGroup(commonUI, "PathTool", brushTexture))
    297.                 {
    298.                     if(brushRenderWithTerrain.CalculateBrushTransform(currTerrain, currUV, finalBrushSize, out BrushTransform brushTransform))
    299.                     {
    300.                         Rect brushBounds = brushTransform.GetBrushXYBounds();
    301.                         PaintContext paintContext = brushRenderWithTerrain.AcquireHeightmap(true, currTerrain, brushBounds);
    302.          
    303.                         mat.SetTexture("_BrushTex", brushTexture);
    304.  
    305.                         brushParams.x = commonUI.brushStrength * strengthScale;
    306.                         brushParams.y = 0.5f * finalHeight;
    307.  
    308.                         mat.SetVector("_BrushParams", brushParams);
    309.  
    310.                         FilterContext fc = new FilterContext(terrain, currPos, finalBrushSize, commonUI.brushRotation);
    311.                         fc.renderTextureCollection.GatherRenderTextures(paintContext.sourceRenderTexture.width, paintContext.sourceRenderTexture.height);
    312.                         RenderTexture filterMaskRT = commonUI.GetBrushMask(fc, paintContext.sourceRenderTexture);
    313.                         mat.SetTexture("_FilterTex", filterMaskRT);
    314.  
    315.                         brushRenderWithTerrain.SetupTerrainToolMaterialProperties(paintContext, brushTransform, mat);
    316.                         brushRenderWithTerrain.RenderBrush(paintContext, mat, 0);
    317.                     }
    318.                 }
    319.             }
    320.         }
    321.  
    322.         public override bool OnPaint(Terrain terrain, IOnPaint editContext)
    323.         {
    324.             commonUI.OnPaint(terrain, editContext);
    325.             Vector2 uv = editContext.uv;
    326.  
    327.             if(Event.current.shift) { return true; }
    328.             //grab the starting position & height
    329.             if (Event.current.control)
    330.             {
    331.                 TerrainData terrainData = terrain.terrainData;
    332.                 float height = terrainData.GetInterpolatedHeight(uv.x, uv.y) / terrainData.size.y;
    333.  
    334.                 m_StartPoint = new Vector3(uv.x, uv.y, height);
    335.                 m_StartTerrain = terrain;
    336.                 return true;
    337.             }
    338.  
    339.             switch (pathToolProperties.paintMode)
    340.             {
    341.                 case PaintMode.Paint:
    342.                     if (Event.current.type == EventType.MouseUp)
    343.                     {
    344.                         m_StartTerrain = null;
    345.                         return true;
    346.                     }
    347.                     else if (Event.current.type == EventType.MouseDown)
    348.                     {
    349.                         UpdateStartPoint(terrain, uv);
    350.                         return true;
    351.                     }
    352.                     else if (Event.current.type == EventType.MouseDrag)
    353.                     {
    354.  
    355.                         ApplyBrushInternal(terrain, uv, editContext.brushTexture, commonUI.brushSpacing);
    356.  
    357.                         UpdateStartPoint(terrain, uv);
    358.                         return false;
    359.                     }
    360.                     else
    361.                     {
    362.  
    363.                         ApplyBrushInternal(terrain, uv, editContext.brushTexture, commonUI.brushSpacing);
    364.                         return false;
    365.                     }
    366.  
    367.                 case PaintMode.Stroke:
    368.                     if (Event.current.type == EventType.MouseDown)
    369.                     {
    370.                         ApplyBrushInternal(terrain, uv, editContext.brushTexture, commonUI.brushSpacing);
    371.  
    372.                         UpdateStartPoint(terrain, uv);
    373.  
    374.                         return false;
    375.                     }
    376.                     break;
    377.  
    378.                 default:
    379.                     throw new System.Exception(string.Format("Unsupported paint mode {0}", pathToolProperties.paintMode));
    380.             }
    381.  
    382.             return false;
    383.         }
    384.  
    385.         void UpdateStartPoint(Terrain terrain, Vector2 uv)
    386.         {
    387.             TerrainData terrainData = terrain.terrainData;
    388.             float height = terrainData.GetInterpolatedHeight(uv.x, uv.y) / terrainData.size.y;
    389.  
    390.             m_StartPoint = new Vector3(uv.x, uv.y, height);
    391.             m_StartTerrain = terrain;
    392.         }
    393.  
    394.         private static class Styles
    395.         {
    396.             public static readonly GUIContent controlHeader = EditorGUIUtility.TrTextContent("Path Tool Controls");
    397.  
    398.             public static readonly GUIContent paintModeContent = EditorGUIUtility.TrTextContent("Paint Mode", "Paint via drag or click");
    399.  
    400.             public static readonly GUIContent widthProfileContent = EditorGUIUtility.TrTextContent("Width Profile", "A multiplier that controls the width of the path over the length of the stroke");
    401.             public static readonly GUIContent heightProfileContent = EditorGUIUtility.TrTextContent("Height Offset Profile", "Adds a height offset to the path along the length of the stroke (World Units)");
    402.             public static readonly GUIContent strengthProfileContent = EditorGUIUtility.TrTextContent("Strength Profile", "A multiplier that controls influence of the path along the length of the stroke");
    403.             public static readonly GUIContent jitterProfileContent = EditorGUIUtility.TrTextContent("Horizontal Offset Profile", "Adds an offset perpendicular to the stroke direction (World Units)");
    404.  
    405.         }
    406.  
    407.         private void SaveSetting()
    408.         {
    409.             string pathToolData = JsonUtility.ToJson(pathToolProperties);
    410.             EditorPrefs.SetString("Unity.TerrainTools.Path", pathToolData);
    411.  
    412.         }
    413.  
    414.         private void LoadSettings()
    415.         {
    416.  
    417.             string pathToolData = EditorPrefs.GetString("Unity.TerrainTools.Path");
    418.             pathToolProperties.SetDefaults();
    419.             JsonUtility.FromJsonOverwrite(pathToolData, pathToolProperties);
    420.         }
    421.     }
    422. }
    423.  
     
    Last edited: Oct 6, 2020
    BaharCaneli, Ruchir, wyattt_ and 4 others like this.
  2. Rowlan

    Rowlan

    Joined:
    Aug 4, 2016
    Posts:
    4,293
    In case you haven't tried jitter & spacing of the new Terrain Tools yet, here's how you can benefit from it, i. e. you can paint e. g. a more rocky path with less smoothness and some kind of noise in it by choosing a different brush, jitter and spacing:

    js.gif