using UnityEngine; using UnityEngine.TerrainTools; using UnityEditor.ShortcutManagement; using UnityEngine.Experimental.Rendering; namespace UnityEditor.TerrainTools { //[FilePathAttribute("Library/TerrainTools/Stamp", FilePathAttribute.Location.ProjectFolder)] internal class StampTool : TerrainPaintTool { #if UNITY_2019_1_OR_NEWER [Shortcut("Terrain/Select Stamp Tool", typeof(TerrainToolShortcutContext))] static void SelectShortcut(ShortcutArguments args) { TerrainToolShortcutContext context = (TerrainToolShortcutContext)args.context; context.SelectPaintTool(); TerrainToolsAnalytics.OnShortcutKeyRelease("Select Stamp Tool"); } [ClutchShortcut("Terrain/Adjust Mesh Stamp Transform", typeof(TerrainToolShortcutContext), KeyCode.C)] static void StrengthBrushShortcut(ShortcutArguments args) { if (args.stage == ShortcutStage.Begin && s_StampToolProperties.mode == StampToolMode.Mesh) { s_EditTransform = true; } else if (args.stage == ShortcutStage.End && s_StampToolProperties.mode == StampToolMode.Mesh) { s_EditTransform = false; TerrainToolsAnalytics.OnShortcutKeyRelease("Adjust Mesh Stamp Transform"); } } #endif static class Styles { public static readonly GUIContent controls = EditorGUIUtility.TrTextContent("Stamp Tool Controls"); public static readonly GUIContent description = EditorGUIUtility.TrTextContent("Left click to stamp the brush onto the terrain.\n\nHold control and mousewheel to adjust height."); public static readonly GUIContent height = EditorGUIUtility.TrTextContent("Stamp Height", "You can set the Stamp Height manually or you can hold control and mouse wheel on the terrain to adjust it."); public static readonly GUIContent blendAmount = EditorGUIUtility.TrTextContent("Blend Amount", "Blends between replacing and offsetting the existing heights under the stamp."); public static readonly GUIContent stampToolBehavior = EditorGUIUtility.TrTextContent("Behavior", "Stamping behavior."); public static readonly GUIContent minBehavior = EditorGUIUtility.TrTextContent("Min", "Stamp where the terrain's height is greater than the input height."); public static readonly GUIContent maxBehavior = EditorGUIUtility.TrTextContent("Max", "Stamp where the terrain's height is less than the input height."); public static readonly GUIContent setBehavior = EditorGUIUtility.TrTextContent("Set", "Stamp the terrain setting it's height to the input height"); public static readonly GUIContent stampToolMode = EditorGUIUtility.TrTextContent("Stamp Mode", "Select the mode used to stamp your terrain."); public static readonly GUIContent brushMode = EditorGUIUtility.TrTextContent("Brush", "Stamp the terrain using a brush."); public static readonly GUIContent meshMode = EditorGUIUtility.TrTextContent("Mesh", "Stamp the terrain using a mesh."); public static readonly GUIContent stampHeightContent = EditorGUIUtility.TrTextContent("Height Offset", "The height to stamp the mesh into the terrain at."); public static readonly GUIContent stampScaleContent = EditorGUIUtility.TrTextContent("Scale", "The scale of the mesh."); public static readonly GUIContent stampRotationContent = EditorGUIUtility.TrTextContent("Rotation", "The rotation of the mesh."); public static readonly GUIContent meshContent = EditorGUIUtility.TrTextContent("Mesh", "The mesh to stamp."); public static readonly GUIContent settings = EditorGUIUtility.TrTextContent("Mesh Stamp Settings"); public static readonly GUIContent transformSettings = EditorGUIUtility.TrTextContent("Transform Settings:"); public static readonly GUIContent resetTransformContent = EditorGUIUtility.TrTextContent("Reset", "Resets the mesh's rotation, scale, and height to their default state."); public static readonly string nullMeshString = "Must assign a mesh to use with the Mesh Stamp Tool."; } enum StampToolBehavior { Min, Max, Set, } [System.Serializable] public enum StampToolMode { Mesh, Brush } private enum ShaderPasses { BrushPreviewFrontFaces = 0, BrushPreviewBackFaces, DepthPassFrontFaces, DepthPassBackFaces, StampToHeightmap } static class RenderTextureIDs { public static int cameraView = "cameraView".GetHashCode(); public static int meshStamp = "meshStamp".GetHashCode(); public static int meshStampPreview = "meshStampPreview".GetHashCode(); public static int meshStampMask = "meshStampMask".GetHashCode(); public static int sourceHeight = "sourceHeight".GetHashCode(); public static int combinedHeight = "combinedHeight".GetHashCode(); } [System.Serializable] class StampToolSerializedProperties { public StampToolBehavior behavior; public StampToolMode mode; public float stampHeight; public float blendAmount; //Mesh stamp public Quaternion rotation; public Vector3 scale; public string meshAssetGUID; public bool showToolSettings; public void SetDefaults() { behavior = StampToolBehavior.Set; mode = StampToolMode.Brush; stampHeight = 100.0f; blendAmount = 0.0f; rotation = Quaternion.identity; scale = Vector3.one; meshAssetGUID = null; showToolSettings = true; } } private Mesh m_ActiveMesh; public Mesh activeMesh { get { if (m_ActiveMesh == null && !string.IsNullOrEmpty(s_StampToolProperties.meshAssetGUID)) { m_ActiveMesh = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(s_StampToolProperties.meshAssetGUID)); } return m_ActiveMesh; } set { m_ActiveMesh = value; } } static StampToolSerializedProperties s_StampToolProperties = new StampToolSerializedProperties(); static bool s_PrevEditTransform = false; static bool s_EditTransform = false; [System.NonSerialized] bool m_Initialized = false; bool m_DebugOrtho = true; float m_HandleHeightOffsetWS; RTHandleCollection m_RtCollection; Vector3 m_BaseHandlePos; [SerializeField] IBrushUIGroup m_CommonUI; private IBrushUIGroup commonUI { get { if (m_CommonUI == null) { LoadSettings(); m_CommonUI = new DefaultBrushUIGroup("StampTool", UpdateAnalyticParameters, DefaultBrushUIGroup.Feature.NoSpacing); m_CommonUI.OnEnterToolMode(); } return m_CommonUI; } } private Material m_Material = null; private Material GetMaterial() { if (m_Material == null) { m_Material = new Material(Shader.Find("Hidden/TerrainEngine/PaintHeightTool")); } return m_Material; } private void Init() { if (!m_Initialized) { m_RtCollection = new RTHandleCollection(); m_RtCollection.AddRTHandle(RenderTextureIDs.cameraView, "cameraView", GraphicsFormat.R8G8B8A8_UNorm); m_RtCollection.AddRTHandle(RenderTextureIDs.meshStamp, "meshStamp", GraphicsFormat.R16_SFloat); m_RtCollection.AddRTHandle(RenderTextureIDs.meshStampPreview, "meshStampPreview", GraphicsFormat.R16_SFloat); m_RtCollection.AddRTHandle(RenderTextureIDs.meshStampMask, "meshStampMask", GraphicsFormat.R16_UNorm); m_RtCollection.AddRTHandle(RenderTextureIDs.sourceHeight, "sourceHeight", GraphicsFormat.R16_UNorm); m_RtCollection.AddRTHandle(RenderTextureIDs.combinedHeight, "combinedHeight", GraphicsFormat.R16_UNorm); m_Initialized = true; } } /// /// Allows overriding for unit testing purposes /// /// internal void ChangeCommonUI(IBrushUIGroup uiGroup) { m_CommonUI = uiGroup; } public override string GetName() { return "Stamp Terrain"; } public override string GetDescription() { return "Projects the shape of a 3d mesh or brush mask onto the Terrain.\n\n" + "Hold Ctrl + Scroll with the mouse wheel to change the Stamp Height.\n" + "Hold Ctrl + Click to subtract the mesh.\n" + "Hold C to rotate, scale, or set a height offset of the mesh using an interactive gizmo."; } internal void SetStampHeight(float height) { s_StampToolProperties.SetDefaults(); s_StampToolProperties.stampHeight = height; } private void ApplyBrushInternal(IPaintContextRender renderer, PaintContext paintContext, float brushStrength, Texture brushTexture, BrushTransform brushXform, Terrain terrain) { Material mat = GetMaterial(); float HeightUnderCursor = terrain.SampleHeight(commonUI.raycastHitUnderCursor.point) / (terrain.terrainData.size.y * 2.0f); float height = s_StampToolProperties.stampHeight * brushStrength / (terrain.terrainData.size.y * 2f); Vector4 brushParams = new Vector4((int) s_StampToolProperties.behavior, HeightUnderCursor, height, s_StampToolProperties.blendAmount); mat.SetVector("_BrushParams", brushParams); var brushMask = RTUtils.GetTempHandle(paintContext.sourceRenderTexture.width, paintContext.sourceRenderTexture.height, 0, FilterUtility.defaultFormat); Utility.GenerateAndSetFilterRT(commonUI, paintContext.sourceRenderTexture, brushMask, mat); if (s_StampToolProperties.mode == StampToolMode.Mesh) { Init(); m_RtCollection.ReleaseRTHandles(); m_RtCollection.GatherRTHandles(paintContext.sourceRenderTexture.width, paintContext.sourceRenderTexture.height, 16); Graphics.Blit(paintContext.sourceRenderTexture, m_RtCollection[RenderTextureIDs.sourceHeight]); Matrix4x4 toolMatrix = Matrix4x4.TRS(Vector3.zero, s_StampToolProperties.rotation, s_StampToolProperties.scale); Bounds modelBounds = activeMesh.bounds; float maxModelScale = Mathf.Max(Mathf.Max(modelBounds.size.x, modelBounds.size.y), modelBounds.size.z); float x = .5f; float y = .5f; float xy = Mathf.Sqrt(x * x + y * y); float z = .5f; float xyz = Mathf.Sqrt(xy * xy + z * z); maxModelScale *= xyz; // build the model matrix to transform the mesh with. we want to scale it to fit in the brush bounds and also center it in the brush bounds Matrix4x4 model = toolMatrix * Matrix4x4.Scale(Vector3.one / maxModelScale) * Matrix4x4.Translate(-modelBounds.center); // actually render the mesh to texture to be used with the tool shader MeshUtils.RenderTopdownProjection(activeMesh, model, m_RtCollection[RenderTextureIDs.meshStamp], MeshUtils.defaultProjectionMaterial, MeshUtils.ShaderPass.Height); // this doesn't actually apply any noise to the destination RT but will color the destination RT based on whether the fragment values are (+) or (-) NoiseUtils.BlitPreview2D(m_RtCollection[RenderTextureIDs.meshStamp], m_RtCollection[RenderTextureIDs.meshStampPreview]); Graphics.Blit(paintContext.destinationRenderTexture, m_RtCollection[RenderTextureIDs.combinedHeight]); mat.SetTexture("_BrushTex", m_RtCollection[RenderTextureIDs.meshStamp]); // restore old render target RenderTexture.active = paintContext.oldRenderTexture; } else { mat.SetTexture("_BrushTex", brushTexture); } renderer.SetupTerrainToolMaterialProperties(paintContext, brushXform, mat); renderer.RenderBrush(paintContext, mat, (int)TerrainBuiltinPaintMaterialPasses.StampHeight); RTUtils.Release(brushMask); } public override void OnEnterToolMode() { Init(); base.OnEnterToolMode(); commonUI.OnEnterToolMode(); } public override void OnExitToolMode() { base.OnExitToolMode(); commonUI.OnExitToolMode(); } public override bool OnPaint(Terrain terrain, IOnPaint editContext) { if (s_StampToolProperties.mode == StampToolMode.Mesh && activeMesh == null || s_EditTransform) { return false; } commonUI.OnPaint(terrain, editContext); // ignore mouse drags if (commonUI.allowPaint && Event.current == null || Event.current.type != EventType.MouseDrag) { Texture brushTexture = editContext.brushTexture; using (IBrushRenderUnderCursor brushRender = new BrushRenderUIGroupUnderCursor(commonUI, "Stamp", brushTexture)) { if (brushRender.CalculateBrushTransform(out BrushTransform brushXform)) { PaintContext paintContext = brushRender.AcquireHeightmap(true, brushXform.GetBrushXYBounds()); ApplyBrushInternal(brushRender, paintContext, commonUI.brushStrength, brushTexture, brushXform, terrain); } } } return true; } public override void OnSceneGUI(Terrain terrain, IOnSceneGUI editContext) { Init(); commonUI.OnSceneGUI2D(terrain, editContext); if (!editContext.hitValidTerrain && !commonUI.isInUse && !s_EditTransform && !m_DebugOrtho) { return; } commonUI.OnSceneGUI(terrain, editContext); bool justPressedEditKey = s_EditTransform && !s_PrevEditTransform; bool justReleaseEditKey = s_PrevEditTransform && !s_EditTransform; s_PrevEditTransform = s_EditTransform; if (justPressedEditKey) { (commonUI as BaseBrushUIGroup).LockTerrainUnderCursor(true); m_BaseHandlePos = commonUI.raycastHitUnderCursor.point; m_HandleHeightOffsetWS = 0; } else if (justReleaseEditKey) { (commonUI as BaseBrushUIGroup).UnlockTerrainUnderCursor(); m_HandleHeightOffsetWS = 0; } // don't render mesh previews, etc. if the mesh field has not been populated yet if (s_StampToolProperties.mode == StampToolMode.Mesh && activeMesh == null) { return; } Event evt = Event.current; if (evt.control && (evt.type == EventType.ScrollWheel)) { const float k_mouseWheelToHeightRatio = -0.004f; s_StampToolProperties.stampHeight += Event.current.delta.y * k_mouseWheelToHeightRatio * editContext.raycastHit.distance; evt.Use(); editContext.Repaint(); SaveSettings(); } // We're only doing painting operations, early out if it's not a repaint if (evt.type == EventType.Repaint && commonUI.isRaycastHitUnderCursorValid) { Texture brushTexture = editContext.brushTexture; using (IBrushRenderPreviewUnderCursor brushRender = new BrushRenderPreviewUIGroupUnderCursor(commonUI, "Stamp", brushTexture)) { if (brushRender.CalculateBrushTransform(out BrushTransform brushXform)) { PaintContext paintContext = brushRender.AcquireHeightmap(false, brushXform.GetBrushXYBounds(), 1); Material previewMaterial = Utility.GetDefaultPreviewMaterial(commonUI.hasEnabledFilters); var texelCtx = Utility.CollectTexelValidity(paintContext.originTerrain, brushXform.GetBrushXYBounds()); Utility.SetupMaterialForPaintingWithTexelValidityContext(paintContext, texelCtx, brushXform, previewMaterial); var filterRT = RTUtils.GetTempHandle(paintContext.sourceRenderTexture.width, paintContext.sourceRenderTexture.height, 0, FilterUtility.defaultFormat); Utility.GenerateAndSetFilterRT(commonUI, paintContext.sourceRenderTexture, filterRT, previewMaterial); TerrainPaintUtilityEditor.DrawBrushPreview(paintContext, TerrainBrushPreviewMode.SourceRenderTexture, editContext.brushTexture, brushXform, previewMaterial, 0); // draw result preview { ApplyBrushInternal(brushRender, paintContext, commonUI.brushStrength, brushTexture, brushXform, terrain); // restore old render target RenderTexture.active = paintContext.oldRenderTexture; previewMaterial.SetTexture("_HeightmapOrig", paintContext.sourceRenderTexture); TerrainPaintUtilityEditor.DrawBrushPreview(paintContext, TerrainBrushPreviewMode.DestinationRenderTexture, editContext.brushTexture, brushXform, previewMaterial, 1); } TerrainPaintUtility.ReleaseContextResources(paintContext); m_RtCollection.ReleaseRTHandles(); texelCtx.Cleanup(); RTUtils.Release(filterRT); } } } if (s_EditTransform && s_StampToolProperties.mode == StampToolMode.Mesh) { EditorGUI.BeginChangeCheck(); { Vector3 prevHandlePosWS = m_BaseHandlePos + Vector3.up * m_HandleHeightOffsetWS; // draw transform handles float handleSize = HandleUtility.GetHandleSize(prevHandlePosWS); Quaternion brushRotation = Quaternion.AngleAxis(commonUI.brushRotation, Vector3.up); Matrix4x4 brushRotMat = Matrix4x4.Rotate(brushRotation); Matrix4x4 toolRotMat = Matrix4x4.Rotate(s_StampToolProperties.rotation); Quaternion handleRot = MeshUtils.QuaternionFromMatrix(brushRotMat * toolRotMat); Quaternion newRot = Handles.RotationHandle(handleRot, prevHandlePosWS); s_StampToolProperties.rotation = MeshUtils.QuaternionFromMatrix(brushRotMat.inverse * Matrix4x4.Rotate(newRot)); s_StampToolProperties.scale = Handles.ScaleHandle(s_StampToolProperties.scale, prevHandlePosWS, handleRot, handleSize * 1.5f); Vector3 currHandlePosWS = Handles.Slider(prevHandlePosWS, Vector3.up, handleSize, Handles.ArrowHandleCap, 1f); float deltaHeight = (currHandlePosWS.y - prevHandlePosWS.y); m_HandleHeightOffsetWS += deltaHeight; s_StampToolProperties.stampHeight += deltaHeight; } if (EditorGUI.EndChangeCheck()) { SaveSettings(); editContext.Repaint(); } } } bool m_ShowControls = true; public override void OnInspectorGUI(Terrain terrain, IOnInspectorGUI editContext) { EditorGUI.BeginChangeCheck(); commonUI.OnInspectorGUI(terrain, editContext); m_ShowControls = TerrainToolGUIHelper.DrawHeaderFoldoutForBrush(Styles.controls, m_ShowControls, s_StampToolProperties.SetDefaults); if (!m_ShowControls) { return; } EditorGUI.BeginChangeCheck(); { EditorGUILayout.BeginVertical("GroupBox"); { // brush coordinate space toolbar EditorGUILayout.BeginHorizontal(); { EditorGUILayout.PrefixLabel(Styles.stampToolMode); if (GUILayout.Toggle(s_StampToolProperties.mode == StampToolMode.Brush, Styles.brushMode, GUI.skin.button)) { s_StampToolProperties.mode = StampToolMode.Brush; } if (GUILayout.Toggle(s_StampToolProperties.mode == StampToolMode.Mesh, Styles.meshMode, GUI.skin.button)) { s_StampToolProperties.mode = StampToolMode.Mesh; } } EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); EditorGUILayout.PrefixLabel(Styles.stampToolBehavior); if (GUILayout.Toggle(s_StampToolProperties.behavior == StampToolBehavior.Min, Styles.minBehavior, GUI.skin.button)) { s_StampToolProperties.behavior = StampToolBehavior.Min; } if (GUILayout.Toggle(s_StampToolProperties.behavior == StampToolBehavior.Set, Styles.setBehavior, GUI.skin.button)) { s_StampToolProperties.behavior = StampToolBehavior.Set; } if (GUILayout.Toggle(s_StampToolProperties.behavior == StampToolBehavior.Max, Styles.maxBehavior, GUI.skin.button)) { s_StampToolProperties.behavior = StampToolBehavior.Max; } EditorGUILayout.EndHorizontal(); s_StampToolProperties.stampHeight = EditorGUILayout.Slider(Styles.height, s_StampToolProperties.stampHeight, -terrain.terrainData.size.y, terrain.terrainData.size.y); s_StampToolProperties.blendAmount = EditorGUILayout.Slider(Styles.blendAmount, s_StampToolProperties.blendAmount, 0.0f, 1.0f); s_StampToolProperties.showToolSettings = TerrainToolGUIHelper.DrawDisableableLabelFoldout(Styles.settings, s_StampToolProperties.showToolSettings, s_StampToolProperties.mode != StampToolMode.Brush); EditorGUI.BeginDisabledGroup(s_StampToolProperties.mode == StampToolMode.Brush); { EditorGUI.indentLevel++; if (s_StampToolProperties.showToolSettings) { if (activeMesh == null) { EditorGUILayout.HelpBox(Styles.nullMeshString, MessageType.Warning); } activeMesh = EditorGUILayout.ObjectField(Styles.meshContent, activeMesh, typeof(Mesh), false) as Mesh; s_StampToolProperties.scale = EditorGUILayout.Vector3Field(Styles.stampScaleContent, s_StampToolProperties.scale); s_StampToolProperties.rotation = Quaternion.Euler(EditorGUILayout.Vector3Field(Styles.stampRotationContent, s_StampToolProperties.rotation.eulerAngles)); } EditorGUI.indentLevel--; } EditorGUI.EndDisabledGroup(); } EditorGUILayout.EndVertical(); } if (EditorGUI.EndChangeCheck()) { SaveSettings(); Save(true); TerrainToolsAnalytics.OnParameterChange(); } base.OnInspectorGUI(terrain, editContext); } private void SaveSettings() { s_StampToolProperties.meshAssetGUID = activeMesh == null ? "" : AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(activeMesh)); string stampToolData = JsonUtility.ToJson(s_StampToolProperties); EditorPrefs.SetString("Unity.TerrainTools.Stamp", stampToolData); } private void LoadSettings() { string stampToolData = EditorPrefs.GetString("Unity.TerrainTools.Stamp"); s_StampToolProperties.SetDefaults(); JsonUtility.FromJsonOverwrite(stampToolData, s_StampToolProperties); } //Analytics Setup private TerrainToolsAnalytics.IBrushParameter[] UpdateAnalyticParameters() => new TerrainToolsAnalytics.IBrushParameter[]{ new TerrainToolsAnalytics.BrushParameter{Name = Styles.stampToolBehavior.text, Value = s_StampToolProperties.behavior.ToString()}, new TerrainToolsAnalytics.BrushParameter{Name = Styles.height.text, Value = s_StampToolProperties.stampHeight}, new TerrainToolsAnalytics.BrushParameter{Name = Styles.blendAmount.text, Value = s_StampToolProperties.blendAmount}, new TerrainToolsAnalytics.BrushParameter{Name = Styles.stampScaleContent.text, Value = s_StampToolProperties.scale}, new TerrainToolsAnalytics.BrushParameter{Name = Styles.stampRotationContent.text, Value = s_StampToolProperties.rotation.eulerAngles}, }; } }