Improve terrain editing, smooth slopes

main
copygirl 4 months ago
parent b69fb40628
commit cc55db6801
  1. 132
      terrain/Terrain+Editing.cs
  2. 219
      terrain/Terrain.cs

@ -2,9 +2,9 @@
public partial class Terrain public partial class Terrain
{ {
bool _unhandledMouseMotion = false; // Used to detect when mouse moves off the terrain. bool _unhandledMouseMotion = false; // Used to detect when mouse moves off the terrain.
Vector2I? _tileHover = null; // Position of currently hovered tile. TilePos? _tileHover = null; // Position of currently hovered tile.
bool _isSelecting = false; // Whether left mouse is held down to select tiles. bool _isSelecting = false; // Whether left mouse is held down to select tiles.
(Vector2I Start, Vector2I End)? _selection = null; (TilePos, TilePos)? _selection = null;
public override void _Input(InputEvent ev) public override void _Input(InputEvent ev)
{ {
@ -15,17 +15,69 @@ public partial class Terrain
GetViewport().SetInputAsHandled(); GetViewport().SetInputAsHandled();
} }
if ((ev is InputEventMouseButton { ButtonIndex: var wheel, ShiftPressed: true }) if ((ev is InputEventMouseButton { ButtonIndex: var wheel, Pressed: var pressed, ShiftPressed: true })
&& (wheel is MouseButton.WheelUp or MouseButton.WheelDown)) && (wheel is MouseButton.WheelUp or MouseButton.WheelDown) && (_selection != null))
{ {
// NOTE: Potential bug in the Godot editor?
// Does it zoom both when mouse wheel is "pressed" and "released"?
// Because just cancelling one of them still causes zooming to occur.
GetViewport().SetInputAsHandled();
if (!pressed) return;
const float AdjustHeight = 0.5f; const float AdjustHeight = 0.5f;
var value = (wheel == MouseButton.WheelUp) ? 1.0f : -1.0f; var value = (wheel == MouseButton.WheelUp)
foreach (var tile in GetSelectedTiles()) ? AdjustHeight : -AdjustHeight;
AdjustTileHeight(tile, value * AdjustHeight);
if (_selection != null) var selection = TileRegion.From(_selection.Value);
// Raise connected corners.
foreach (var innerCorner in Enum.GetValues<Corner>()) {
var outerCorner = innerCorner.GetOpposite();
var innerPos = selection.GetTileFor(innerCorner);
var outerPos = innerPos.GetNeighbor(innerCorner);
var innerHeight = GetCornerHeights(innerPos)[innerCorner];
var outerHeight = GetCornerHeights(outerPos)[outerCorner];
if (IsEqualApprox(outerHeight, innerHeight))
SetCornerHeight(outerPos, outerCorner, innerHeight + value);
}
// Raise connected sides.
foreach (var side in Enum.GetValues<Side>()) {
foreach (var innerPos in selection.GetTilesFor(side)) {
var outerPos = innerPos.GetNeighbor(side);
var innerHeights = GetCornerHeights(innerPos);
var outerHeights = GetCornerHeights(outerPos);
var (innerCorner1, innerCorner2) = side.GetCorners();
var (outerCorner1, outerCorner2) = side.GetOpposite().GetCorners();
var current = outerHeights;
var changed = false;
foreach (var (innerCorner, outerCorner) in new[]{ (innerCorner1, outerCorner1), (innerCorner2, outerCorner2) }) {
var innerHeight = innerHeights[innerCorner];
var outerHeight = outerHeights[outerCorner];
if (IsEqualApprox(outerHeight, innerHeight)) {
current = current.With(outerCorner, innerHeight + value);
changed = true;
}
}
if (changed) SetCornerHeights(outerPos, current);
}
}
// Raise selected tiles themselves.
foreach (var tile in selection.GetAllTiles())
AdjustTileHeight(tile, value);
UpdateMeshAndShape(); UpdateMeshAndShape();
NotifyPropertyListChanged(); NotifyPropertyListChanged();
GetViewport().SetInputAsHandled();
} }
if (ev is InputEventMouseMotion) if (ev is InputEventMouseMotion)
@ -40,7 +92,7 @@ public partial class Terrain
var tilePos = ToTilePos(localPos); var tilePos = ToTilePos(localPos);
if (ev is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: true }) { if (ev is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: true }) {
_selection = (tilePos, tilePos); _selection = new(tilePos, tilePos);
_isSelecting = true; _isSelecting = true;
GetViewport().SetInputAsHandled(); GetViewport().SetInputAsHandled();
} }
@ -48,7 +100,7 @@ public partial class Terrain
if (ev is InputEventMouseMotion) { if (ev is InputEventMouseMotion) {
_unhandledMouseMotion = false; _unhandledMouseMotion = false;
_tileHover = tilePos; _tileHover = tilePos;
if (_isSelecting) _selection = _selection.Value with { End = tilePos }; if (_isSelecting) _selection = _selection.Value with { Item2 = tilePos };
} }
} }
@ -82,8 +134,8 @@ public partial class Terrain
AddLine(bottomLeft , topLeft ); AddLine(bottomLeft , topLeft );
} }
if (_tileHover is Vector2I hover) { if (_tileHover is TilePos hover) {
var corners = GetGridCorners(hover); var corners = GetCornersPosition(hover);
var margin = 0.1f; var margin = 0.1f;
mesh.SurfaceSetColor(Colors.Black); mesh.SurfaceSetColor(Colors.Black);
AddQuad( AddQuad(
@ -95,8 +147,9 @@ public partial class Terrain
} }
mesh.SurfaceSetColor(Colors.Blue); mesh.SurfaceSetColor(Colors.Blue);
foreach (var tilePos in GetSelectedTiles()) { if (_selection is (TilePos, TilePos) selection)
var corners = GetGridCorners(tilePos); foreach (var pos in TileRegion.From(selection).GetAllTiles()) {
var corners = GetCornersPosition(pos);
AddQuad(corners.TopLeft, corners.TopRight, corners.BottomLeft, corners.BottomRight); AddQuad(corners.TopLeft, corners.TopRight, corners.BottomLeft, corners.BottomRight);
} }
@ -119,15 +172,48 @@ public partial class Terrain
} }
} }
IEnumerable<Vector2I> GetSelectedTiles()
readonly record struct TileRegion(int Left, int Top, int Right, int Bottom)
{ {
if (_selection is not (Vector2I start, Vector2I end)) yield break; public TilePos TopLeft => new(Left , Top);
// Ensure start.X/Y is smaller than end.X/Y. public TilePos TopRight => new(Right, Top);
(start, end) = (new(Min(start.X, end.X), Min(start.Y, end.Y)), public TilePos BottomRight => new(Right, Bottom);
new(Max(start.X, end.X), Max(start.Y, end.Y))); public TilePos BottomLeft => new(Left , Bottom);
// Go over all tiles in the range and yield each one.
for (var x = start.X; x <= end.X; x++) public int Width => Right - Left + 1;
for (var y = start.Y; y <= end.Y; y++) public int Height => Bottom - Top + 1;
public static TileRegion From((TilePos, TilePos) selection)
=> From(selection.Item1, selection.Item2);
public static TileRegion From(TilePos a, TilePos b)
=> new(Min(a.X, b.X), Min(a.Y, b.Y), Max(a.X, b.X), Max(a.Y, b.Y));
public TilePos GetTileFor(Corner corner)
=> corner switch {
Corner.TopLeft => TopLeft,
Corner.TopRight => TopRight,
Corner.BottomRight => BottomRight,
Corner.BottomLeft => BottomLeft,
_ => throw new ArgumentException($"Invalid Corner value '{corner}'", nameof(corner)),
};
public IEnumerable<TilePos> GetTilesFor(Side side)
{
var (left, top, right, bottom) = this;
return side switch {
Side.Left => Enumerable.Range(Top, Height).Select(y => new TilePos(left, y)),
Side.Top => Enumerable.Range(Left, Width).Select(x => new TilePos(x, top)),
Side.Right => Enumerable.Range(Top, Height).Select(y => new TilePos(right, y)),
Side.Bottom => Enumerable.Range(Left, Width).Select(x => new TilePos(x, bottom)),
_ => throw new ArgumentException($"Invalid Side value '{side}'", nameof(side)),
};
}
public IEnumerable<TilePos> GetAllTiles()
{
for (var x = Left; x <= Right; x++)
for (var y = Top; y <= Bottom; y++)
yield return new(x, y); yield return new(x, y);
} }
} }
}

@ -31,92 +31,91 @@ public partial class Terrain
} }
/// <summary> Returns if this terrain grid contains the specified tile position. </summary> public bool Contains(TilePos pos)
public bool ContainsTilePos(Vector2I tilePos) => (pos.X >= 0) && (pos.X < Size.X)
=> (tilePos.X >= 0) && (tilePos.X < Size.X) && (pos.Y >= 0) && (pos.Y < Size.Y);
&& (tilePos.Y >= 0) && (tilePos.Y < Size.Y);
/// <summary> Transforms a 3D position local to the equivalent tile position. </summary> /// <summary> Transforms a 3D position local to the equivalent tile position. </summary>
public Vector2I ToTilePos(Vector3 localPos) public TilePos ToTilePos(Vector3 localPos)
=> new(RoundToInt(localPos.X / TileSize + Size.X / 2.0f), => new(RoundToInt(localPos.X / TileSize + Size.X / 2.0f),
RoundToInt(localPos.Z / TileSize + Size.Y / 2.0f)); RoundToInt(localPos.Z / TileSize + Size.Y / 2.0f));
public float GetCornerHeight(Vector2I tilePos, Corner corner) public Corners<float> GetCornerHeights(TilePos pos)
=> GetTileRaw(tilePos) switch { => GetTileRaw(pos) switch {
{ VariantType: Variant.Type.Nil } => 0, { VariantType: Variant.Type.Nil } => default,
{ VariantType: Variant.Type.Float } num => (float)num, { VariantType: Variant.Type.Float } v => Corners<float>.From((float)v),
{ VariantType: Variant.Type.PackedFloat32Array } nums => ((float[])nums)[(int)corner], { VariantType: Variant.Type.PackedFloat32Array } v => Corners<float>.From((float[])v),
_ => throw new Exception("Invalid type in Points dictionary"), _ => throw new Exception("Invalid type in Points dictionary"),
}; };
public void SetCornerHeight(Vector2I tilePos, Corner corner, float value) public void SetCornerHeight(TilePos pos, Corner corner, float value)
=> SetCornerHeights(pos, default(Corners<float?>).With(corner, value));
public void SetCornerHeights(TilePos pos, Corners<float> values)
=> SetCornerHeights(pos, new Corners<float?>(values.TopLeft, values.TopRight, values.BottomRight, values.BottomLeft));
public void SetCornerHeights(TilePos pos, Corners<float?> values)
{ {
if (!ContainsTilePos(tilePos)) return; if (!Contains(pos)) return;
var existing = GetTileRaw(tilePos) switch {
{ VariantType: Variant.Type.Nil } => [ 0, 0, 0, 0 ], var current = GetCornerHeights(pos);
{ VariantType: Variant.Type.Float } num => [ (float)num, (float)num, (float)num, (float)num ], var changed = false;
{ VariantType: Variant.Type.PackedFloat32Array } nums => (float[])nums,
_ => throw new Exception("Invalid type in Points dictionary"), foreach (var corner in Enum.GetValues<Corner>())
}; if (values[corner] is float value)
existing[(int)corner] = value; { current = current.With(corner, value); changed = true; }
if (existing.All(IsZeroApprox)) if (!changed) return;
Tiles?.Remove(tilePos);
if (IsEqualApprox(existing[0], existing[1]) if (current.IsZeroApprox()) RemoveTileRaw(pos);
&& IsEqualApprox(existing[0], existing[2]) else if (current.IsEqualApprox()) SetTileRaw(pos, current.TopLeft);
&& IsEqualApprox(existing[0], existing[3])) else SetTileRaw(pos, current.ToArray());
SetTileRaw(tilePos, existing[0]); }
else
SetTileRaw(tilePos, existing); public void SetTileHeight(TilePos pos, float value)
}
public void SetTileHeight(Vector2I tilePos, float value)
{ {
if (!ContainsTilePos(tilePos)) return; if (!Contains(pos)) return;
if (IsZeroApprox(value)) Tiles?.Remove(tilePos); if (IsZeroApprox(value)) RemoveTileRaw(pos);
else SetTileRaw(tilePos, value); else SetTileRaw(pos, value);
} }
public void AdjustTileHeight(Vector2I tilePos, float relative) public void AdjustTileHeight(TilePos pos, float relative)
{ {
if (relative == 0) return; if (relative == 0) return;
if (!ContainsTilePos(tilePos)) return; if (!Contains(pos)) return;
switch (GetTileRaw(tilePos)) { switch (GetTileRaw(pos)) {
case { VariantType: Variant.Type.Nil }: case { VariantType: Variant.Type.Nil }:
SetTileRaw(tilePos, relative); SetTileRaw(pos, relative);
break; break;
case { VariantType: Variant.Type.Float } num: case { VariantType: Variant.Type.Float } num:
var newNum = (float)num + relative; var newNum = (float)num + relative;
if (IsZeroApprox(newNum)) Tiles?.Remove(tilePos); if (IsZeroApprox(newNum)) RemoveTileRaw(pos);
else SetTileRaw(tilePos, newNum); else SetTileRaw(pos, newNum);
break; break;
case { VariantType: Variant.Type.PackedFloat32Array } nums: case { VariantType: Variant.Type.PackedFloat32Array } nums:
var newNums = (float[])nums; var newNums = (float[])nums;
for (var i = 0; i < 4; i++) newNums[i] += relative; for (var i = 0; i < 4; i++) newNums[i] += relative;
SetTileRaw(tilePos, newNums); SetTileRaw(pos, newNums);
break; break;
default: throw new Exception("Invalid type in Points dictionary"); default: throw new Exception("Invalid type in Points dictionary");
}; };
} }
public GridCorners GetGridCorners(Vector2I tilePos) public Corners<Vector3> GetCornersPosition(TilePos pos)
{ {
var halfSize = TileSize / 2; var heights = GetCornerHeights(pos);
var vx = (tilePos.X - Size.X / 2.0f) * TileSize; var vx = (pos.X - Size.X / 2.0f) * TileSize;
var vz = (tilePos.Y - Size.Y / 2.0f) * TileSize; var vz = (pos.Y - Size.Y / 2.0f) * TileSize;
var half = TileSize / 2;
return new() { return new(new(vx - half, heights[Corner.TopLeft ], vz - half),
TopLeft = new(vx - halfSize, GetCornerHeight(tilePos, Corner.TopLeft ), vz - halfSize), new(vx + half, heights[Corner.TopRight ], vz - half),
TopRight = new(vx + halfSize, GetCornerHeight(tilePos, Corner.TopRight ), vz - halfSize), new(vx + half, heights[Corner.BottomRight], vz + half),
BottomLeft = new(vx - halfSize, GetCornerHeight(tilePos, Corner.BottomLeft ), vz + halfSize), new(vx - half, heights[Corner.BottomLeft ], vz + half));
BottomRight = new(vx + halfSize, GetCornerHeight(tilePos, Corner.BottomRight), vz + halfSize),
};
} }
Variant GetTileRaw(Vector2I tilePos) Variant GetTileRaw(TilePos pos)
=> (Tiles?.TryGetValue(tilePos, out var result) == true) ? result : default; => (Tiles?.TryGetValue(pos.ToVector2I(), out var result) == true) ? result : default;
void SetTileRaw(TilePos pos, Variant value)
void SetTileRaw(Vector2I tilePos, Variant value) => (Tiles ??= [])[pos.ToVector2I()] = value;
=> (Tiles ??= [])[tilePos] = value; void RemoveTileRaw(TilePos pos)
=> Tiles?.Remove(pos.ToVector2I());
void UpdateMeshAndShape() void UpdateMeshAndShape()
@ -146,7 +145,7 @@ public partial class Terrain
for (var x = 0; x < Size.X; x++) { for (var x = 0; x < Size.X; x++) {
for (var z = 0; z < Size.Y; z++) { for (var z = 0; z < Size.Y; z++) {
var corners = GetGridCorners(new(x, z)); var corners = GetCornersPosition(new(x, z));
AddTriangle(corners.TopLeft , new(0.0f, 0.0f), AddTriangle(corners.TopLeft , new(0.0f, 0.0f),
corners.TopRight , new(1.0f, 0.0f), corners.TopRight , new(1.0f, 0.0f),
corners.BottomLeft , new(0.0f, 1.0f)); corners.BottomLeft , new(0.0f, 1.0f));
@ -186,19 +185,105 @@ public partial class Terrain
} }
public enum Corner public readonly record struct TilePos(int X, int Y)
{
public TilePos GetNeighbor(Side side)
=> side switch {
Side.Left => new(X - 1, Y),
Side.Top => new(X, Y - 1),
Side.Right => new(X + 1, Y),
Side.Bottom => new(X, Y + 1),
_ => throw new ArgumentException($"Invalid Side value '{side}'", nameof(side)),
};
public TilePos GetNeighbor(Corner corner)
=> corner switch {
Corner.TopLeft => new(X - 1, Y - 1),
Corner.TopRight => new(X + 1, Y - 1),
Corner.BottomRight => new(X + 1, Y + 1),
Corner.BottomLeft => new(X - 1, Y + 1),
_ => throw new ArgumentException($"Invalid Corner value '{corner}'", nameof(corner)),
};
public static TilePos From(Vector2I value) => new(value.X, value.Y);
public Vector2I ToVector2I() => new(X, Y);
}
public readonly record struct Corners<T>
(T TopLeft, T TopRight, T BottomRight, T BottomLeft)
{ {
TopLeft, public T this[Corner corner] {
TopRight, readonly get => corner switch {
BottomLeft, Corner.TopLeft => TopLeft,
BottomRight, Corner.TopRight => TopRight,
Corner.BottomRight => BottomRight,
Corner.BottomLeft => BottomLeft,
_ => throw new ArgumentException($"Invalid Corner value '{corner}'", nameof(corner)),
};
init { switch (corner) {
case Corner.TopLeft : TopLeft = value; break;
case Corner.TopRight : TopRight = value; break;
case Corner.BottomRight : BottomRight = value; break;
case Corner.BottomLeft : BottomLeft = value; break;
default: throw new ArgumentException($"Invalid Corner value '{corner}'", nameof(corner));
} }
}
public static Corners<T> From(T value) => new()
{ TopLeft = value, TopRight = value, BottomRight = value, BottomLeft = value };
public static Corners<T> From(T[] values) => new()
{ TopLeft = values[0], TopRight = values[1], BottomRight = values[2], BottomLeft = values[3] };
public Corners<T> With(Corner corner, T value)
=> new((corner == Corner.TopLeft) ? value : TopLeft,
(corner == Corner.TopRight) ? value : TopRight,
(corner == Corner.BottomRight) ? value : BottomRight,
(corner == Corner.BottomLeft) ? value : BottomLeft);
public readonly T[] ToArray()
=> [ TopLeft, TopRight, BottomRight, BottomLeft ];
}
} }
public readonly struct GridCorners public static class CornersExtensions
{ {
public Vector3 TopLeft { get; init; } public static bool IsZeroApprox(this Terrain.Corners<float> self)
public Vector3 TopRight { get; init; } => Mathf.IsZeroApprox(self.TopLeft) && Mathf.IsZeroApprox(self.TopRight)
public Vector3 BottomLeft { get; init; } && Mathf.IsZeroApprox(self.BottomRight) && Mathf.IsZeroApprox(self.BottomLeft);
public Vector3 BottomRight { get; init; }
public static bool IsEqualApprox(this Terrain.Corners<float> self)
=> Mathf.IsEqualApprox(self.TopLeft, self.TopRight)
&& Mathf.IsEqualApprox(self.TopLeft, self.BottomRight)
&& Mathf.IsEqualApprox(self.TopLeft, self.BottomLeft);
} }
// TODO: Put this in a different file.
public static class TerrainExtensions
{
public static (Corner, Corner) GetCorners(this Side side)
=> side switch {
Side.Left => (Corner.TopLeft, Corner.BottomLeft),
Side.Top => (Corner.TopLeft, Corner.TopRight),
Side.Right => (Corner.TopRight, Corner.BottomRight),
Side.Bottom => (Corner.BottomLeft, Corner.BottomRight),
_ => throw new ArgumentException($"Invalid Side value '{side}'", nameof(side)),
};
public static Side GetOpposite(this Side side)
=> side switch {
Side.Left => Side.Right,
Side.Top => Side.Bottom,
Side.Right => Side.Left,
Side.Bottom => Side.Top,
_ => throw new ArgumentException($"Invalid Side value '{side}'", nameof(side)),
};
public static Corner GetOpposite(this Corner corner)
=> corner switch {
Corner.TopLeft => Corner.BottomRight,
Corner.TopRight => Corner.BottomLeft,
Corner.BottomRight => Corner.TopLeft,
Corner.BottomLeft => Corner.TopRight,
_ => throw new ArgumentException($"Invalid Corner value '{corner}'", nameof(corner)),
};
} }

Loading…
Cancel
Save