using UnityEngine; using UnityEngine.TerrainTools; using UnityEditor.ShortcutManagement; namespace UnityEditor.TerrainTools { internal class BridgeTool : TerrainPaintTool { #if UNITY_2019_1_OR_NEWER [Shortcut("Terrain/Select Bridge Tool", typeof(TerrainToolShortcutContext))] // tells shortcut manager what to call the shortcut and what to pass as args static void SelectShortcut(ShortcutArguments args) { TerrainToolShortcutContext context = (TerrainToolShortcutContext)args.context; // gets interface to modify state of TerrainTools context.SelectPaintTool(); // set active tool TerrainToolsAnalytics.OnShortcutKeyRelease("Select Bridge Tool"); } #endif [SerializeField] IBrushUIGroup m_commonUI; private IBrushUIGroup commonUI { get { if( m_commonUI == null ) { LoadSettings(); m_commonUI = new DefaultBrushUIGroup( "BridgeTool", UpdateAnalyticParameters, DefaultBrushUIGroup.Feature.NoScatter ); m_commonUI.OnEnterToolMode(); } return m_commonUI; } } Terrain m_StartTerrain = null; private Vector3 m_StartPoint; Material m_Material = null; Material GetPaintMaterial() { if (m_Material == null) m_Material = new Material(Shader.Find("Hidden/TerrainTools/SetExactHeight")); return m_Material; } [System.Serializable] class BridgeToolSerializedProperties { public AnimationCurve widthProfile; public AnimationCurve heightProfile; public AnimationCurve strengthProfile; public AnimationCurve jitterProfile; public void SetDefaults() { widthProfile = AnimationCurve.Linear(0, 1, 1, 1); heightProfile = AnimationCurve.Linear(0, 0, 1, 0); strengthProfile = AnimationCurve.Linear(0, 1, 1, 1); jitterProfile = AnimationCurve.Linear(0, 0, 1, 0); } } BridgeToolSerializedProperties bridgeToolProperties = new BridgeToolSerializedProperties(); public override string GetName() { return "Sculpt/Bridge"; } public override string GetDescription() { return "Connects two points on a terrain creating a bridge.\n\n" + "Hold Ctrl + Click to select the starting position.\n" + "Release Ctrl + Click to set the end position."; } public override void OnEnterToolMode() { base.OnEnterToolMode(); commonUI.OnEnterToolMode(); } public override void OnExitToolMode() { base.OnExitToolMode(); commonUI.OnExitToolMode(); } public override void OnSceneGUI(Terrain terrain, IOnSceneGUI editContext) { commonUI.OnSceneGUI2D(terrain, editContext); if (editContext.hitValidTerrain || commonUI.isInUse) { commonUI.OnSceneGUI(terrain, editContext); if (Event.current.type != EventType.Repaint) { return; } var previewMaterial = Utility.GetDefaultPreviewMaterial(commonUI.hasEnabledFilters); float endWidth = Mathf.Abs(bridgeToolProperties.widthProfile.Evaluate(1.0f)); BrushTransform brushXform = TerrainPaintUtility.CalculateBrushTransform(terrain, commonUI.raycastHitUnderCursor.textureCoord, commonUI.brushSize * endWidth, commonUI.brushRotation); PaintContext ctx = TerrainPaintUtility.BeginPaintHeightmap(terrain, brushXform.GetBrushXYBounds(), 1); var brushMask = RTUtils.GetTempHandle(ctx.sourceRenderTexture.width, ctx.sourceRenderTexture.height, 0, FilterUtility.defaultFormat); Utility.GenerateAndSetFilterRT(commonUI, ctx.sourceRenderTexture, brushMask, previewMaterial); var texelCtx = Utility.CollectTexelValidity(ctx.originTerrain, brushXform.GetBrushXYBounds()); Utility.SetupMaterialForPaintingWithTexelValidityContext(ctx, texelCtx, brushXform, previewMaterial); TerrainPaintUtilityEditor.DrawBrushPreview(ctx, TerrainBrushPreviewMode.SourceRenderTexture, editContext.brushTexture, brushXform, previewMaterial, 0); texelCtx.Cleanup(); ctx.Cleanup(); RTUtils.Release(brushMask); } if (Event.current.type != EventType.Repaint) { return; } //display a brush preview at the bridge starting location, using starting size from width profile if (m_StartTerrain != null) { float startWidth = Mathf.Abs(bridgeToolProperties.widthProfile.Evaluate(0.0f)); float brushSize = commonUI.brushSize * startWidth; Material previewMaterial = Utility.GetDefaultPreviewMaterial(); BrushTransform brushTransform = TerrainPaintUtility.CalculateBrushTransform(m_StartTerrain, m_StartPoint, brushSize, commonUI.brushRotation); PaintContext sampleContext = TerrainPaintUtility.BeginPaintHeightmap(m_StartTerrain, brushTransform.GetBrushXYBounds()); var texelCtx = Utility.CollectTexelValidity(sampleContext.originTerrain, brushTransform.GetBrushXYBounds()); Utility.SetupMaterialForPaintingWithTexelValidityContext(sampleContext, texelCtx, brushTransform, previewMaterial); TerrainPaintUtilityEditor.DrawBrushPreview(sampleContext, TerrainBrushPreviewMode.SourceRenderTexture, editContext.brushTexture, brushTransform, previewMaterial, 0); texelCtx.Cleanup(); sampleContext.Cleanup(); } } bool m_ShowBridgeControls = true; public override void OnInspectorGUI(Terrain terrain, IOnInspectorGUI editContext) { EditorGUI.BeginChangeCheck(); commonUI.OnInspectorGUI(terrain, editContext); m_ShowBridgeControls = TerrainToolGUIHelper.DrawHeaderFoldoutForBrush(Styles.controlHeader, m_ShowBridgeControls, bridgeToolProperties.SetDefaults); if (m_ShowBridgeControls) { //"Controls the width of the bridge over the length of the stroke" bridgeToolProperties.widthProfile = EditorGUILayout.CurveField(Styles.widthProfileContent, bridgeToolProperties.widthProfile); bridgeToolProperties.heightProfile = EditorGUILayout.CurveField(Styles.heightProfileContent, bridgeToolProperties.heightProfile); bridgeToolProperties.strengthProfile = EditorGUILayout.CurveField(Styles.strengthProfileContent, bridgeToolProperties.strengthProfile); bridgeToolProperties.jitterProfile = EditorGUILayout.CurveField(Styles.jitterProfileContent, bridgeToolProperties.jitterProfile); } if (EditorGUI.EndChangeCheck()) { SaveSetting(); Save(true); TerrainToolsAnalytics.OnParameterChange(); editContext.Repaint(RepaintFlags.Scene); } } private Vector2 transformToWorld(Terrain t, Vector2 uvs) { Vector3 tilePos = t.GetPosition(); return new Vector2(tilePos.x, tilePos.z) + uvs * new Vector2(t.terrainData.size.x, t.terrainData.size.z); } private Vector2 transformToUVSpace(Terrain originTile, Vector2 worldPos) { Vector3 originTilePos = originTile.GetPosition(); Vector2 uvPos = new Vector2((worldPos.x - originTilePos.x) / originTile.terrainData.size.x, (worldPos.y - originTilePos.z) / originTile.terrainData.size.z); return uvPos; } private void ApplyBrushInternal(Terrain terrain, Vector2 uv, Texture brushTexture, float brushSpacing) { //get the target position & height float targetHeight = terrain.terrainData.GetInterpolatedHeight(uv.x, uv.y) / terrain.terrainData.size.y; Vector3 targetPos = new Vector3(uv.x, uv.y, targetHeight); if (terrain != m_StartTerrain) { //figure out the stroke vector in uv,height space Vector2 targetWorld = transformToWorld(terrain, uv); Vector2 targetUVs = transformToUVSpace(m_StartTerrain, targetWorld); targetPos.x = targetUVs.x; targetPos.y = targetUVs.y; } Vector3 stroke = targetPos - m_StartPoint; float strokeLength = stroke.magnitude; int numSplats = (int)(strokeLength / (0.1f * Mathf.Max(brushSpacing, 0.01f))); Terrain currTerrain = m_StartTerrain; Material mat = GetPaintMaterial(); Vector2 posOffset = new Vector2(0.0f, 0.0f); Vector2 currUV = new Vector2(); Vector4 brushParams = new Vector4(); Vector2 jitterVec = new Vector2(-stroke.z, stroke.x); //perpendicular to stroke direction jitterVec.Normalize(); for (int i = 0; i < numSplats; i++) { float pct = (float)i / (float)numSplats; float widthScale = bridgeToolProperties.widthProfile.Evaluate(pct); float heightOffset = bridgeToolProperties.heightProfile.Evaluate(pct) / currTerrain.terrainData.size.y; float strengthScale = bridgeToolProperties.strengthProfile.Evaluate(pct); float jitterOffset = bridgeToolProperties.jitterProfile.Evaluate(pct) / Mathf.Max(currTerrain.terrainData.size.x, currTerrain.terrainData.size.z); Vector3 currPos = m_StartPoint + pct * stroke; //add in jitter offset (needs to happen before tile correction) currPos.x += posOffset.x + jitterOffset * jitterVec.x; currPos.y += posOffset.y + jitterOffset * jitterVec.y; if (currPos.x >= 1.0f && (currTerrain.rightNeighbor != null)) { currTerrain = currTerrain.rightNeighbor; currPos.x -= 1.0f; posOffset.x -= 1.0f; } if(currPos.x <= 0.0f && (currTerrain.leftNeighbor != null)) { currTerrain = currTerrain.leftNeighbor; currPos.x += 1.0f; posOffset.x += 1.0f; } if(currPos.y >= 1.0f && (currTerrain.topNeighbor != null)) { currTerrain = currTerrain.topNeighbor; currPos.y -= 1.0f; posOffset.y -= 1.0f; } if(currPos.y <= 0.0f && (currTerrain.bottomNeighbor != null)) { currTerrain = currTerrain.bottomNeighbor; currPos.y += 1.0f; posOffset.y += 1.0f; } currUV.x = currPos.x; currUV.y = currPos.y; int finalBrushSize = (int)(widthScale * (float)commonUI.brushSize); float finalHeight = (m_StartPoint + pct * stroke).z + heightOffset; using(IBrushRenderWithTerrain brushRenderWithTerrain = new BrushRenderWithTerrainUIGroup(commonUI, "BridgeTool", brushTexture)) { if(brushRenderWithTerrain.CalculateBrushTransform(currTerrain, currUV, finalBrushSize, out BrushTransform brushTransform)) { Rect brushBounds = brushTransform.GetBrushXYBounds(); PaintContext paintContext = brushRenderWithTerrain.AcquireHeightmap(true, currTerrain, brushBounds); mat.SetTexture("_BrushTex", brushTexture); brushParams.x = commonUI.brushStrength * strengthScale; brushParams.y = 0.5f * finalHeight; mat.SetVector("_BrushParams", brushParams); var brushMask = RTUtils.GetTempHandle(paintContext.sourceRenderTexture.width, paintContext.sourceRenderTexture.height, 0, FilterUtility.defaultFormat); Utility.GenerateAndSetFilterRT(commonUI, paintContext.sourceRenderTexture, brushMask, mat); brushRenderWithTerrain.SetupTerrainToolMaterialProperties(paintContext, brushTransform, mat); brushRenderWithTerrain.RenderBrush(paintContext, mat, 0); RTUtils.Release(brushMask); } } } } public override bool OnPaint(Terrain terrain, IOnPaint editContext) { commonUI.OnPaint(terrain, editContext); Vector2 uv = editContext.uv; if(Event.current.shift) { return true; } //grab the starting position & height if (Event.current.control) { TerrainData terrainData = terrain.terrainData; float height = terrainData.GetInterpolatedHeight(uv.x, uv.y) / terrainData.size.y; m_StartPoint = new Vector3(uv.x, uv.y, height); m_StartTerrain = terrain; return true; } else if (!m_StartTerrain || (Event.current.type == EventType.MouseDrag)) { return true; } else { ApplyBrushInternal(terrain, uv, editContext.brushTexture, commonUI.brushSpacing); return false; } } private static class Styles { public static readonly GUIContent controlHeader = EditorGUIUtility.TrTextContent("Bridge Tool Controls"); public static readonly GUIContent widthProfileContent = EditorGUIUtility.TrTextContent("Width Profile", "A multiplier that controls the width of the bridge over the length of the stroke"); public static readonly GUIContent heightProfileContent = EditorGUIUtility.TrTextContent("Height Offset Profile", "Adds a height offset to the bridge along the length of the stroke (World Units)"); public static readonly GUIContent strengthProfileContent = EditorGUIUtility.TrTextContent("Strength Profile", "A multiplier that controls influence of the bridge along the length of the stroke"); public static readonly GUIContent jitterProfileContent = EditorGUIUtility.TrTextContent("Horizontal Offset Profile", "Adds an offset perpendicular to the stroke direction (World Units)"); } private void SaveSetting() { string bridgeToolData = JsonUtility.ToJson(bridgeToolProperties); EditorPrefs.SetString("Unity.TerrainTools.Bridge", bridgeToolData); } private void LoadSettings() { string bridgeToolData = EditorPrefs.GetString("Unity.TerrainTools.Bridge"); bridgeToolProperties.SetDefaults(); JsonUtility.FromJsonOverwrite(bridgeToolData, bridgeToolProperties); } //Analytics Setup private TerrainToolsAnalytics.IBrushParameter[] UpdateAnalyticParameters() => new TerrainToolsAnalytics.IBrushParameter[]{ new TerrainToolsAnalytics.BrushParameter{Name = Styles.widthProfileContent.text, Value = bridgeToolProperties.widthProfile.keys}, new TerrainToolsAnalytics.BrushParameter{Name = Styles.heightProfileContent.text, Value = bridgeToolProperties.heightProfile.keys}, new TerrainToolsAnalytics.BrushParameter{Name = Styles.strengthProfileContent.text, Value = bridgeToolProperties.strengthProfile.keys}, new TerrainToolsAnalytics.BrushParameter{Name = Styles.jitterProfileContent.text, Value = bridgeToolProperties.jitterProfile.keys}, }; } }