|
|
|
public partial class TerrainEditingControls
|
|
|
|
{
|
|
|
|
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 painting.
|
|
|
|
// TODO: Add a flatten tool mode: Flattens everything selected to nearest corner height.
|
|
|
|
// TODO: Allow click-dragging which doesn't affect already changed tiles / corners.
|
|
|
|
// TODO: Support undo and redo.
|
|
|
|
// TODO: Support "disconnected" mode which can create vertical cliffs.
|
|
|
|
|
|
|
|
// Raise / lower the terrain if left / right mouse button is pressed.
|
|
|
|
if ((mouse is InputEventMouseButton { ButtonIndex: var button, Pressed: true })
|
|
|
|
&& (button is MouseButton.Left or MouseButton.Right))
|
|
|
|
{
|
|
|
|
GetViewport().SetInputAsHandled();
|
|
|
|
|
|
|
|
const float AdjustHeight = 0.5f;
|
|
|
|
var amount = (button == MouseButton.Left)
|
|
|
|
? AdjustHeight : -AdjustHeight;
|
|
|
|
|
|
|
|
// Find corners that are "connected" and should be raised.
|
|
|
|
var corners = new HashSet<(TilePos Position, Corner Corner)>();
|
|
|
|
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) corners.Add((neighborPos, neighborCorner));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Raise connected corners.
|
|
|
|
foreach (var group in corners.GroupBy(e => e.Position, e => e.Corner)) {
|
|
|
|
var pos = group.Key;
|
|
|
|
var tile2 = terrain.GetTile(pos);
|
|
|
|
foreach (var corner2 in group)
|
|
|
|
tile2.Height[corner2] += amount;
|
|
|
|
terrain.SetTile(pos, tile2);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Raise selected tiles themselves.
|
|
|
|
foreach (var pos in tiles) {
|
|
|
|
var tile2 = terrain.GetTile(pos);
|
|
|
|
tile2.Height.Adjust(amount);
|
|
|
|
terrain.SetTile(pos, tile2);
|
|
|
|
}
|
|
|
|
|
|
|
|
terrain.UpdateMeshAndShape();
|
|
|
|
terrain.NotifyPropertyListChanged();
|
|
|
|
}
|
|
|
|
|
|
|
|
UpdateEditToolMesh(terrain, tiles);
|
|
|
|
} else {
|
|
|
|
ClearEditToolMesh();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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));
|
|
|
|
}
|