|
|
|
using System.IO;
|
|
|
|
|
|
|
|
public partial class TerrainEditingControls
|
|
|
|
{
|
|
|
|
// Set by the terrain editing plugin.
|
|
|
|
public EditorUndoRedoManager EditorUndoRedo { get; set; }
|
|
|
|
|
|
|
|
Terrain _currentTerrain = null;
|
|
|
|
|
|
|
|
Material _editToolMaterial;
|
|
|
|
public override void _EnterTree()
|
|
|
|
{
|
|
|
|
_editToolMaterial = new StandardMaterial3D {
|
|
|
|
AlbedoColor = Colors.Blue,
|
|
|
|
ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded,
|
|
|
|
DepthDrawMode = BaseMaterial3D.DepthDrawModeEnum.Disabled,
|
|
|
|
NoDepthTest = true,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
public override void _ExitTree()
|
|
|
|
=> ClearEditToolMesh();
|
|
|
|
|
|
|
|
public override void _Input(InputEvent ev)
|
|
|
|
{
|
|
|
|
var viewport = !Engine.IsEditorHint() ? GetViewport()
|
|
|
|
: EditorInterface.Singleton.GetEditorViewport3D();
|
|
|
|
|
|
|
|
if (Engine.IsEditorHint()) {
|
|
|
|
// Make sure to transform the input event to the 3D scene's viewport.
|
|
|
|
var container = viewport.GetParent<SubViewportContainer>();
|
|
|
|
ev = ev.XformedBy(container.GetGlobalTransform().AffineInverse());
|
|
|
|
if (ev is InputEventMouse m) m.GlobalPosition = m.Position;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (ev is InputEventMouse mouse) {
|
|
|
|
if (viewport.GetVisibleRect().HasPoint(mouse.Position)
|
|
|
|
&& (RayCastTerrain(mouse) is var (terrain, position))) {
|
|
|
|
var (tile, corner) = FindClosestTile(terrain, position);
|
|
|
|
|
|
|
|
var drawSize = (ToolShape == ToolShape.Corner) ? 1 : DrawSize;
|
|
|
|
var isEven = (drawSize % 2) == 0;
|
|
|
|
var radius = FloorToInt(drawSize / 2.0f);
|
|
|
|
|
|
|
|
// Offset tile position by corner.
|
|
|
|
if (isEven) tile = corner switch {
|
|
|
|
Corner.TopLeft => new(tile.X + 0, tile.Y + 0),
|
|
|
|
Corner.TopRight => new(tile.X + 1, tile.Y + 0),
|
|
|
|
Corner.BottomRight => new(tile.X + 1, tile.Y + 1),
|
|
|
|
Corner.BottomLeft => new(tile.X + 0, tile.Y + 1),
|
|
|
|
_ => throw new InvalidOperationException(),
|
|
|
|
};
|
|
|
|
|
|
|
|
IEnumerable<TilePos> GetTilesInSquare() {
|
|
|
|
var minX = tile.X - radius;
|
|
|
|
var minY = tile.Y - radius;
|
|
|
|
var maxX = tile.X + radius - (isEven ? 1 : 0);
|
|
|
|
var maxY = tile.Y + radius - (isEven ? 1 : 0);
|
|
|
|
for (var x = minX; x <= maxX; x++)
|
|
|
|
for (var y = minY; y <= maxY; y++)
|
|
|
|
yield return new(x, y);
|
|
|
|
}
|
|
|
|
|
|
|
|
IEnumerable<TilePos> GetTilesInRadius() {
|
|
|
|
var center = isEven
|
|
|
|
? new Vector2(tile.X , tile.Y )
|
|
|
|
: new Vector2(tile.X + 0.5f, tile.Y + 0.5f);
|
|
|
|
var distanceSqr = Pow(isEven ? radius - 0.25f : radius + 0.25f, 2);
|
|
|
|
return GetTilesInSquare()
|
|
|
|
.Where(tile => center.DistanceSquaredTo(
|
|
|
|
new Vector2(tile.X + 0.5f, tile.Y + 0.5f)) < distanceSqr);
|
|
|
|
}
|
|
|
|
|
|
|
|
var tiles = (ToolShape switch {
|
|
|
|
// TODO: Edit corner, not full tile.
|
|
|
|
ToolShape.Corner => [tile],
|
|
|
|
ToolShape.Circle => GetTilesInRadius(),
|
|
|
|
ToolShape.Square => GetTilesInSquare(),
|
|
|
|
_ => throw new InvalidOperationException(),
|
|
|
|
}).ToHashSet();
|
|
|
|
|
|
|
|
// TODO: Handle different tool modes, such as flatten and painting.
|
|
|
|
// TODO: Allow click-dragging which doesn't affect already changed tiles / corners.
|
|
|
|
// TODO: Make mesh generation generate vertical walls between disconnected corners.
|
|
|
|
// TODO: Use ArrayMesh instead of ImmediateMesh.
|
|
|
|
|
|
|
|
// Raise / lower the terrain if left / right mouse button is pressed.
|
|
|
|
if (mouse is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: true }) {
|
|
|
|
GetViewport().SetInputAsHandled();
|
|
|
|
|
|
|
|
var cornersToChange = new HashSet<(TilePos Position, Corner Corner)>();
|
|
|
|
|
|
|
|
// Raise selected tiles themselves.
|
|
|
|
foreach (var pos in tiles)
|
|
|
|
foreach (var corner2 in Enum.GetValues<Corner>())
|
|
|
|
cornersToChange.Add((pos, corner2));
|
|
|
|
|
|
|
|
// If the Connected toggle button is active, move "connected" corners.
|
|
|
|
// Connected corners are the ones that are at the same height as ones already being moved.
|
|
|
|
if (ConnectedToggle.ButtonPressed) {
|
|
|
|
foreach (var pos in tiles) {
|
|
|
|
var tile2 = terrain.GetTile(pos);
|
|
|
|
foreach (var corner2 in Enum.GetValues<Corner>()) {
|
|
|
|
var height = tile2.Height[corner2];
|
|
|
|
foreach (var (neighborPos, neighborCorner) in GetNeighbors(pos, corner2)) {
|
|
|
|
if (tiles.Contains(neighborPos)) continue;
|
|
|
|
var neighborHeight = terrain.GetTile(neighborPos).Height[neighborCorner];
|
|
|
|
if (neighborHeight == height) cornersToChange.Add((neighborPos, neighborCorner));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const float AdjustHeight = 0.5f;
|
|
|
|
var flatten = ToolMode == ToolMode.Flatten;
|
|
|
|
var amount = flatten ? terrain.GetTile(tile).Height[corner]
|
|
|
|
: UpDownToggle.ButtonPressed ? +AdjustHeight
|
|
|
|
: -AdjustHeight;
|
|
|
|
|
|
|
|
var tilesPrevious = new List<(TilePos, Corners<float>)>();
|
|
|
|
var tilesChanged = new List<(TilePos, Corners<float>)>();
|
|
|
|
foreach (var group in cornersToChange.GroupBy(c => c.Position, c => c.Corner)) {
|
|
|
|
var pos = group.Key;
|
|
|
|
var tile2 = terrain.GetTile(pos);
|
|
|
|
tilesPrevious.Add((pos, tile2.Height));
|
|
|
|
|
|
|
|
var newHeight = tile2.Height;
|
|
|
|
foreach (var corner2 in group) {
|
|
|
|
if (flatten) newHeight[corner2] = amount;
|
|
|
|
else newHeight[corner2] += amount;
|
|
|
|
}
|
|
|
|
tilesChanged.Add((pos, newHeight));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (EditorUndoRedo is EditorUndoRedoManager undo) {
|
|
|
|
var name = "Modify terrain"; // TODO: Change name depending on tool mode.
|
|
|
|
undo.CreateAction(name, customContext: terrain, backwardUndoOps: false);
|
|
|
|
|
|
|
|
undo.AddDoMethod(this, nameof(TerrainModifyHeight), terrain, Pack(tilesChanged));
|
|
|
|
undo.AddDoMethod(terrain, GodotObject.MethodName.NotifyPropertyListChanged);
|
|
|
|
undo.AddDoMethod(terrain, nameof(Terrain.UpdateMeshAndShape));
|
|
|
|
|
|
|
|
undo.AddUndoMethod(this, nameof(TerrainModifyHeight), terrain, Pack(tilesPrevious));
|
|
|
|
undo.AddUndoMethod(terrain, GodotObject.MethodName.NotifyPropertyListChanged);
|
|
|
|
undo.AddUndoMethod(terrain, nameof(Terrain.UpdateMeshAndShape));
|
|
|
|
|
|
|
|
undo.CommitAction(true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
UpdateEditToolMesh(terrain, tiles);
|
|
|
|
} else {
|
|
|
|
ClearEditToolMesh();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public void TerrainModifyHeight(Terrain terrain, byte[] data)
|
|
|
|
{
|
|
|
|
foreach (var (pos, corners) in Unpack(data)) {
|
|
|
|
var tile = terrain.GetTile(pos);
|
|
|
|
tile.Height = corners;
|
|
|
|
terrain.SetTile(pos, tile);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static byte[] Pack(IEnumerable<(TilePos Position, Corners<float> Corners)> data) {
|
|
|
|
using var stream = new MemoryStream();
|
|
|
|
using var writer = new BinaryWriter(stream);
|
|
|
|
foreach (var (pos, corners) in data) {
|
|
|
|
writer.Write(pos.X);
|
|
|
|
writer.Write(pos.Y);
|
|
|
|
writer.Write(corners.TopLeft);
|
|
|
|
writer.Write(corners.TopRight);
|
|
|
|
writer.Write(corners.BottomRight);
|
|
|
|
writer.Write(corners.BottomLeft);
|
|
|
|
}
|
|
|
|
return stream.ToArray();
|
|
|
|
}
|
|
|
|
|
|
|
|
static IEnumerable<(TilePos Position, Corners<float> Corners)> Unpack(byte[] data)
|
|
|
|
{
|
|
|
|
using var stream = new MemoryStream(data);
|
|
|
|
using var reader = new BinaryReader(stream);
|
|
|
|
while (stream.Position < stream.Length) {
|
|
|
|
var x = reader.ReadInt32();
|
|
|
|
var y = reader.ReadInt32();
|
|
|
|
var corners = new Corners<float>(
|
|
|
|
reader.ReadSingle(), reader.ReadSingle(),
|
|
|
|
reader.ReadSingle(), reader.ReadSingle());
|
|
|
|
yield return (new(x, y), corners);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void UpdateEditToolMesh(Terrain terrain, IEnumerable<TilePos> tiles)
|
|
|
|
{
|
|
|
|
if (terrain != _currentTerrain) ClearEditToolMesh();
|
|
|
|
_currentTerrain = terrain;
|
|
|
|
|
|
|
|
var mesh = terrain.GetOrCreateMesh("EditToolMesh");
|
|
|
|
mesh.ClearSurfaces();
|
|
|
|
mesh.SurfaceBegin(Mesh.PrimitiveType.Lines);
|
|
|
|
|
|
|
|
void AddLine(Vector3 start, Vector3 end) {
|
|
|
|
mesh.SurfaceAddVertex(start);
|
|
|
|
mesh.SurfaceAddVertex(end);
|
|
|
|
}
|
|
|
|
|
|
|
|
void AddQuad(Vector3 topLeft , Vector3 topRight,
|
|
|
|
Vector3 bottomRight, Vector3 bottomLeft) {
|
|
|
|
AddLine(topLeft , topRight );
|
|
|
|
AddLine(topRight , bottomRight);
|
|
|
|
AddLine(bottomRight, bottomLeft );
|
|
|
|
AddLine(bottomLeft , topLeft );
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach (var tile in tiles) {
|
|
|
|
var (topLeft, topRight, bottomRight, bottomLeft)
|
|
|
|
= terrain.GetTileCornerPositions(tile);
|
|
|
|
AddQuad(topLeft, topRight, bottomRight, bottomLeft);
|
|
|
|
}
|
|
|
|
|
|
|
|
mesh.SurfaceEnd();
|
|
|
|
mesh.SurfaceSetMaterial(0, _editToolMaterial);
|
|
|
|
}
|
|
|
|
|
|
|
|
void ClearEditToolMesh()
|
|
|
|
=> _currentTerrain?.GetNodeOrNull("EditToolMesh")?.QueueFree();
|
|
|
|
|
|
|
|
(Terrain Terrain, Vector3 Position)? RayCastTerrain(InputEventMouse ev)
|
|
|
|
{
|
|
|
|
// Ray is cast from the editor camera's view.
|
|
|
|
var camera = EditorInterface.Singleton.GetEditorViewport3D().GetCamera3D();
|
|
|
|
var from = camera.ProjectRayOrigin(ev.Position);
|
|
|
|
var to = from + camera.ProjectRayNormal(ev.Position) * camera.Far;
|
|
|
|
|
|
|
|
// Actual collision is done in the edited scene though.
|
|
|
|
var root = (Node3D)EditorInterface.Singleton.GetEditedSceneRoot();
|
|
|
|
var space = root.GetWorld3D().DirectSpaceState;
|
|
|
|
var query = PhysicsRayQueryParameters3D.Create(from, to);
|
|
|
|
|
|
|
|
var result = space.IntersectRay(query);
|
|
|
|
var collider = (GodotObject)result.GetValueOrDefault("collider");
|
|
|
|
return (collider is Terrain terrain)
|
|
|
|
? (terrain, (Vector3)result["position"])
|
|
|
|
: null;
|
|
|
|
}
|
|
|
|
|
|
|
|
static (TilePos, Corner) FindClosestTile(Terrain terrain, Vector3 position)
|
|
|
|
{
|
|
|
|
var local = terrain.ToLocal(position);
|
|
|
|
|
|
|
|
var tileX = local.X / terrain.TileSize + 0.5 + terrain.Size.X / 2;
|
|
|
|
var tileY = local.Z / terrain.TileSize + 0.5 + terrain.Size.Y / 2;
|
|
|
|
var tile = new TilePos(FloorToInt(tileX), FloorToInt(tileY));
|
|
|
|
|
|
|
|
var cornerX = RoundToInt(PosMod(tileX, 1));
|
|
|
|
var cornerY = RoundToInt(PosMod(tileY, 1));
|
|
|
|
var corner = (cornerX, cornerY) switch {
|
|
|
|
(0, 0) => Corner.TopLeft,
|
|
|
|
(1, 0) => Corner.TopRight,
|
|
|
|
(1, 1) => Corner.BottomRight,
|
|
|
|
(0, 1) => Corner.BottomLeft,
|
|
|
|
_ => throw new InvalidOperationException(),
|
|
|
|
};
|
|
|
|
|
|
|
|
return (tile, corner);
|
|
|
|
}
|
|
|
|
|
|
|
|
static readonly Dictionary<Corner, (int X, int Y, Corner Opposite)[]> _offsetLookup = new(){
|
|
|
|
[Corner.TopLeft ] = [(-1, -1, Corner.BottomRight), (-1, 0, Corner.TopRight ), (0, -1, Corner.BottomLeft )],
|
|
|
|
[Corner.TopRight ] = [(+1, -1, Corner.BottomLeft ), (+1, 0, Corner.TopLeft ), (0, -1, Corner.BottomRight)],
|
|
|
|
[Corner.BottomRight] = [(+1, +1, Corner.TopLeft ), (+1, 0, Corner.BottomLeft ), (0, +1, Corner.TopRight )],
|
|
|
|
[Corner.BottomLeft ] = [(-1, +1, Corner.TopRight ), (-1, 0, Corner.BottomRight), (0, +1, Corner.TopLeft )],
|
|
|
|
};
|
|
|
|
static IEnumerable<(TilePos, Corner)> GetNeighbors(TilePos pos, Corner corner)
|
|
|
|
=> _offsetLookup[corner].Select(e => (new TilePos(pos.X + e.X, pos.Y + e.Y), e.Opposite));
|
|
|
|
}
|