Files
FlaxEngine/Source/Editor/Tools/Terrain/SculptTerrainGizmoMode.cs

380 lines
12 KiB
C#

// Copyright (c) Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using FlaxEditor.Gizmo;
using FlaxEditor.SceneGraph.Actors;
using FlaxEditor.Tools.Terrain.Brushes;
using FlaxEditor.Tools.Terrain.Sculpt;
using FlaxEditor.Tools.Terrain.Undo;
using FlaxEditor.Viewport;
using FlaxEditor.Viewport.Modes;
using FlaxEngine;
namespace FlaxEditor.Tools.Terrain
{
/// <summary>
/// Terrain carving tool mode.
/// </summary>
/// <seealso cref="FlaxEditor.Viewport.Modes.EditorGizmoMode" />
[HideInEditor]
public class SculptTerrainGizmoMode : EditorGizmoMode
{
private IntPtr _cachedHeightmapData;
private int _cachedHeightmapDataSize;
private EditTerrainMapAction _activeAction;
/// <summary>
/// The terrain carving gizmo.
/// </summary>
public SculptTerrainGizmo Gizmo;
/// <summary>
/// The tool modes.
/// </summary>
public enum ModeTypes
{
/// <summary>
/// The sculpt mode.
/// </summary>
[Tooltip("Sculpt tool mode. Edits terrain heightmap by moving area affected by brush up or down.")]
Sculpt,
/// <summary>
/// The smooth mode.
/// </summary>
[Tooltip("Sculpt tool mode that smooths the terrain heightmap area affected by brush.")]
Smooth,
/// <summary>
/// The flatten mode.
/// </summary>
[Tooltip("Sculpt tool mode that flattens the terrain heightmap area affected by brush to the target value.")]
Flatten,
/// <summary>
/// The noise mode.
/// </summary>
[Tooltip("Sculpt tool mode that applies the noise to the terrain heightmap area affected by brush.")]
Noise,
/// <summary>
/// The holes mode.
/// </summary>
[Tooltip("Terrain holes creating tool mode edits terrain holes mask by changing area affected by brush.")]
Holes,
}
/// <summary>
/// The brush types.
/// </summary>
public enum BrushTypes
{
/// <summary>
/// The circle brush.
/// </summary>
CircleBrush,
}
private readonly Mode[] _modes =
{
new SculptMode(),
new SmoothMode(),
new FlattenMode(),
new NoiseMode(),
new HolesMode(),
};
private readonly Brush[] _brushes =
{
new CircleBrush(),
};
private ModeTypes _modeType = ModeTypes.Sculpt;
private BrushTypes _brushType = BrushTypes.CircleBrush;
/// <summary>
/// Occurs when tool mode gets changed.
/// </summary>
public event Action ToolModeChanged;
/// <summary>
/// Gets the current tool mode (enum).
/// </summary>
public ModeTypes ToolModeType
{
get => _modeType;
set
{
if (_modeType != value)
{
if (_activeAction != null)
throw new InvalidOperationException("Cannot change sculpt tool mode during terrain editing.");
_modeType = value;
ToolModeChanged?.Invoke();
}
}
}
/// <summary>
/// Gets the current tool mode.
/// </summary>
public Mode CurrentMode => _modes[(int)_modeType];
/// <summary>
/// Gets the sculpt mode instance.
/// </summary>
public SculptMode SculptMode => _modes[(int)ModeTypes.Sculpt] as SculptMode;
/// <summary>
/// Gets the smooth mode instance.
/// </summary>
public SmoothMode SmoothMode => _modes[(int)ModeTypes.Smooth] as SmoothMode;
/// <summary>
/// Occurs when tool brush gets changed.
/// </summary>
public event Action ToolBrushChanged;
/// <summary>
/// Gets the current tool brush (enum).
/// </summary>
public BrushTypes ToolBrushType
{
get => _brushType;
set
{
if (_brushType != value)
{
if (_activeAction != null)
throw new InvalidOperationException("Cannot change sculpt tool brush type during terrain editing.");
_brushType = value;
ToolBrushChanged?.Invoke();
}
}
}
/// <summary>
/// Gets the current brush.
/// </summary>
public Brush CurrentBrush => _brushes[(int)_brushType];
/// <summary>
/// Gets the circle brush instance.
/// </summary>
public CircleBrush CircleBrush => _brushes[(int)BrushTypes.CircleBrush] as CircleBrush;
/// <summary>
/// The last valid cursor position of the brush (in world space).
/// </summary>
public Vector3 CursorPosition { get; private set; }
/// <summary>
/// Flag used to indicate whenever last cursor position of the brush is valid.
/// </summary>
public bool HasValidHit { get; private set; }
/// <summary>
/// Describes the terrain patch link.
/// </summary>
public struct PatchLocation
{
/// <summary>
/// The patch coordinates.
/// </summary>
public Int2 PatchCoord;
}
/// <summary>
/// The selected terrain patches collection that are under cursor (affected by the brush).
/// </summary>
public readonly List<PatchLocation> PatchesUnderCursor = new List<PatchLocation>();
/// <summary>
/// Describes the terrain chunk link.
/// </summary>
public struct ChunkLocation
{
/// <summary>
/// The patch coordinates.
/// </summary>
public Int2 PatchCoord;
/// <summary>
/// The chunk coordinates.
/// </summary>
public Int2 ChunkCoord;
}
/// <summary>
/// The selected terrain chunk collection that are under cursor (affected by the brush).
/// </summary>
public readonly List<ChunkLocation> ChunksUnderCursor = new List<ChunkLocation>();
/// <summary>
/// Gets the selected terrain actor (see <see cref="Modules.SceneEditingModule"/>).
/// </summary>
public FlaxEngine.Terrain SelectedTerrain
{
get
{
var sceneEditing = Editor.Instance.SceneEditing;
var terrainNode = sceneEditing.SelectionCount == 1 ? sceneEditing.Selection[0] as TerrainNode : null;
return (FlaxEngine.Terrain)terrainNode?.Actor;
}
}
/// <summary>
/// Gets the world bounds of the brush located at the current cursor position (defined by <see cref="CursorPosition"/>). Valid only if <see cref="HasValidHit"/> is set to true.
/// </summary>
public BoundingBox CursorBrushBounds
{
get
{
const float brushExtentY = 10000.0f;
float brushSizeHalf = CurrentBrush.Size * 0.5f;
Vector3 center = CursorPosition;
BoundingBox box;
box.Minimum = new Vector3(center.X - brushSizeHalf, center.Y - brushSizeHalf - brushExtentY, center.Z - brushSizeHalf);
box.Maximum = new Vector3(center.X + brushSizeHalf, center.Y + brushSizeHalf + brushExtentY, center.Z + brushSizeHalf);
return box;
}
}
/// <summary>
/// Gets the heightmap temporary scratch memory buffer used to modify terrain samples. Allocated memory is unmanaged by GC.
/// </summary>
/// <param name="size">The minimum buffer size (in bytes).</param>
/// <returns>The allocated memory using <see cref="Marshal"/> interface.</returns>
public IntPtr GetHeightmapTempBuffer(int size)
{
if (_cachedHeightmapDataSize < size)
{
if (_cachedHeightmapData != IntPtr.Zero)
{
Marshal.FreeHGlobal(_cachedHeightmapData);
}
_cachedHeightmapData = Marshal.AllocHGlobal(size);
_cachedHeightmapDataSize = size;
}
return _cachedHeightmapData;
}
/// <summary>
/// Gets the current edit terrain undo system action. Use it to record the data for the undo restoring after terrain editing.
/// </summary>
internal EditTerrainMapAction CurrentEditUndoAction => _activeAction;
/// <inheritdoc />
public override void Init(IGizmoOwner owner)
{
base.Init(owner);
Gizmo = new SculptTerrainGizmo(owner, this);
Gizmo.PaintStarted += OnPaintStarted;
Gizmo.PaintEnded += OnPaintEnded;
}
/// <inheritdoc />
public override void OnActivated()
{
base.OnActivated();
Owner.Gizmos.Active = Gizmo;
ClearCursor();
}
/// <inheritdoc />
public override void OnDeactivated()
{
base.OnDeactivated();
// Free temporary memory buffer
if (_cachedHeightmapData != IntPtr.Zero)
{
Marshal.FreeHGlobal(_cachedHeightmapData);
_cachedHeightmapData = IntPtr.Zero;
_cachedHeightmapDataSize = 0;
}
}
/// <summary>
/// Clears the cursor location information cached within the gizmo mode.
/// </summary>
public void ClearCursor()
{
HasValidHit = false;
PatchesUnderCursor.Clear();
ChunksUnderCursor.Clear();
}
/// <summary>
/// Sets the cursor location in the world space. Updates the brush location and cached affected chunks.
/// </summary>
/// <param name="hitPosition">The cursor hit location on the selected terrain.</param>
public void SetCursor(ref Vector3 hitPosition)
{
HasValidHit = true;
CursorPosition = hitPosition;
PatchesUnderCursor.Clear();
ChunksUnderCursor.Clear();
// Find patches and chunks affected by the brush
var terrain = SelectedTerrain;
if (terrain == null)
throw new InvalidOperationException("Cannot set cursor then no terrain is selected.");
var brushBounds = CursorBrushBounds;
var patchesCount = terrain.PatchesCount;
for (int patchIndex = 0; patchIndex < patchesCount; patchIndex++)
{
terrain.GetPatchBounds(patchIndex, out BoundingBox tmp);
if (!tmp.Intersects(brushBounds))
continue;
terrain.GetPatchCoord(patchIndex, out var patchCoord);
PatchesUnderCursor.Add(new PatchLocation { PatchCoord = patchCoord });
for (int chunkIndex = 0; chunkIndex < FlaxEngine.Terrain.PatchChunksCount; chunkIndex++)
{
terrain.GetChunkBounds(patchIndex, chunkIndex, out tmp);
if (!tmp.Intersects(brushBounds))
continue;
var chunkCoord = new Int2(chunkIndex % FlaxEngine.Terrain.PatchEdgeChunksCount, chunkIndex / FlaxEngine.Terrain.PatchEdgeChunksCount);
ChunksUnderCursor.Add(new ChunkLocation { PatchCoord = patchCoord, ChunkCoord = chunkCoord });
}
}
}
private void OnPaintStarted()
{
if (_activeAction != null)
throw new InvalidOperationException("Terrain paint start/end resynchronization.");
var terrain = SelectedTerrain;
if (CurrentMode.EditHoles)
_activeAction = new EditTerrainHolesMapAction(terrain);
else
_activeAction = new EditTerrainHeightMapAction(terrain);
}
private void OnPaintEnded()
{
if (_activeAction != null)
{
if (_activeAction.HasAnyModification)
{
_activeAction.OnEditingEnd();
Editor.Instance.Undo.AddAction(_activeAction);
}
_activeAction = null;
}
}
}
}