Visualize to-be-drawn shape

main
copygirl 1 month ago
parent 3fa91b5954
commit f7a9dfa2f1
  1. 410
      addons/terrain-editing/TerrainEditingControls+Editing.cs
  2. 26
      addons/terrain-editing/TerrainEditingControls.tscn
  3. 5
      terrain/Terrain.cs
  4. 10
      terrain/Tile.cs

@ -1,194 +1,191 @@
public partial class TerrainEditingControls
{
bool _unhandledMotion = false; // Used to detect when mouse moves off the terrain.
TilePos? _tileHover = null; // Position of currently hovered tile.
bool _isSelecting = false; // Whether left mouse is held down to select tiles.
(TilePos, TilePos)? _selection = null;
Terrain _currentTerrain = null;
Material _editToolMaterial;
public override void _EnterTree()
{
_editToolMaterial = new StandardMaterial3D {
VertexColorUseAsAlbedo = true,
AlbedoColor = Colors.Blue,
ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded,
DepthDrawMode = BaseMaterial3D.DepthDrawModeEnum.Disabled,
NoDepthTest = true,
};
}
public override void _ExitTree()
=> ClearEditToolMesh();
public override void _Input(InputEvent ev)
{
if (GetTerrain() is not Terrain terrain) return;
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 viewport = EditorInterface.Singleton.GetEditorViewport3D();
var container = viewport.GetParent<SubViewportContainer>();
ev = ev.XformedBy(container.GetGlobalTransform().AffineInverse());
if (ev is InputEventMouse m) m.GlobalPosition = m.Position;
}
if (_isSelecting && (ev is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: false })) {
_isSelecting = false;
GetViewport().SetInputAsHandled();
return;
}
if ((ev is InputEventMouseButton { ButtonIndex: var wheel, Pressed: var pressed, ShiftPressed: true })
&& (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;
var amount = (wheel == MouseButton.WheelUp)
? AdjustHeight : -AdjustHeight;
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 outerTile = terrain.GetTile(outerPos);
var innerHeight = terrain.GetTile(innerPos).Height[innerCorner];
var outerHeight = outerTile.Height[outerCorner];
if (IsEqualApprox(outerHeight, innerHeight)) {
outerTile.Height[outerCorner] = innerHeight + amount;
terrain.SetTile(outerPos, outerTile);
}
}
// Raise connected sides.
foreach (var side in Enum.GetValues<Side>()) {
foreach (var innerPos in selection.GetTilesFor(side)) {
var outerPos = innerPos.GetNeighbor(side);
var innerTile = terrain.GetTile(innerPos);
var outerTile = terrain.GetTile(outerPos);
var (innerCorner1, innerCorner2) = side.GetCorners();
var (outerCorner1, outerCorner2) = side.GetOpposite().GetCorners();
var changed = false;
var matchingCorners = new[]{ (innerCorner1, outerCorner1), (innerCorner2, outerCorner2) };
foreach (var (innerCorner, outerCorner) in matchingCorners) {
var innerHeight = innerTile.Height[innerCorner];
var outerHeight = outerTile.Height[outerCorner];
if (IsEqualApprox(outerHeight, innerHeight)) {
outerTile.Height[outerCorner] = innerHeight + amount;
changed = true;
}
}
if (changed) terrain.SetTile(outerPos, outerTile);
}
}
// Raise selected tiles themselves.
foreach (var pos in selection.GetAllTiles()) {
var tile = terrain.GetTile(pos);
tile.Height.Adjust(amount);
terrain.SetTile(pos, tile);
}
terrain.UpdateMeshAndShape();
terrain.NotifyPropertyListChanged();
return;
}
if ((ev is InputEventMouseButton { ButtonIndex: var wheel2, Pressed: var pressed2, CtrlPressed: true })
&& (wheel2 is MouseButton.WheelUp or MouseButton.WheelDown) && (_selection != null))
{
GetViewport().SetInputAsHandled();
if (!pressed2) return;
var amount = (wheel2 == MouseButton.WheelUp) ? 1 : -1;
var selection = TileRegion.From(_selection.Value);
foreach (var pos in selection.GetAllTiles()) {
var tile = terrain.GetTile(pos);
tile.TexturePrimary = PosMod(tile.TexturePrimary + amount, 4);
terrain.SetTile(pos, tile);
}
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(),
};
terrain.UpdateMeshAndShape();
terrain.NotifyPropertyListChanged();
return;
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);
}
if (ev is InputEventMouseMotion)
_unhandledMotion = true;
if ((ev is InputEventMouse mouse) && (!Engine.IsEditorHint() ||
EditorInterface.Singleton.GetEditorViewport3D()
.GetVisibleRect().HasPoint(mouse.Position)))
OnInputRayCastTerrain(terrain, mouse);
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);
}
void OnInputRayCastTerrain(Terrain terrain, 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");
if (collider == terrain) {
var pos = (Vector3)result["position"];
// var normal = (Vector3)result["normal"];
// var shape = (int)result["shape"];
OnTerrainInput(terrain, ev, pos);
}
UpdateEditToolMesh(terrain, ToolShape switch {
// TODO: Edit corner, not full tile.
ToolShape.Corner => [tile],
ToolShape.Circle => GetTilesInRadius(),
ToolShape.Square => GetTilesInSquare(),
_ => throw new InvalidOperationException(),
});
} else {
ClearEditToolMesh();
}
void OnTerrainInput(Terrain terrain, InputEvent ev, Vector3 position)
{
if (terrain == null) return;
var localPos = terrain.ToLocal(position);
var tilePos = terrain.ToTilePos(localPos);
if (ev is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: true }) {
_selection = new(tilePos, tilePos);
_isSelecting = true;
GetViewport().SetInputAsHandled();
return;
}
if (ev is InputEventMouseMotion) {
_unhandledMotion = false;
_tileHover = tilePos;
if (_isSelecting) _selection = _selection.Value with { Item2 = tilePos };
}
// if (_isSelecting && (ev is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: false })) {
// _isSelecting = false;
// GetViewport().SetInputAsHandled();
// return;
// }
// if ((ev is InputEventMouseButton { ButtonIndex: var wheel, Pressed: var pressed, ShiftPressed: true })
// && (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;
// var amount = (wheel == MouseButton.WheelUp)
// ? AdjustHeight : -AdjustHeight;
// 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 outerTile = terrain.GetTile(outerPos);
// var innerHeight = terrain.GetTile(innerPos).Height[innerCorner];
// var outerHeight = outerTile.Height[outerCorner];
// if (IsEqualApprox(outerHeight, innerHeight)) {
// outerTile.Height[outerCorner] = innerHeight + amount;
// terrain.SetTile(outerPos, outerTile);
// }
// }
// // Raise connected sides.
// foreach (var side in Enum.GetValues<Side>()) {
// foreach (var innerPos in selection.GetTilesFor(side)) {
// var outerPos = innerPos.GetNeighbor(side);
// var innerTile = terrain.GetTile(innerPos);
// var outerTile = terrain.GetTile(outerPos);
// var (innerCorner1, innerCorner2) = side.GetCorners();
// var (outerCorner1, outerCorner2) = side.GetOpposite().GetCorners();
// var changed = false;
// var matchingCorners = new[]{ (innerCorner1, outerCorner1), (innerCorner2, outerCorner2) };
// foreach (var (innerCorner, outerCorner) in matchingCorners) {
// var innerHeight = innerTile.Height[innerCorner];
// var outerHeight = outerTile.Height[outerCorner];
// if (IsEqualApprox(outerHeight, innerHeight)) {
// outerTile.Height[outerCorner] = innerHeight + amount;
// changed = true;
// }
// }
// if (changed) terrain.SetTile(outerPos, outerTile);
// }
// }
// // Raise selected tiles themselves.
// foreach (var pos in selection.GetAllTiles()) {
// var tile = terrain.GetTile(pos);
// tile.Height.Adjust(amount);
// terrain.SetTile(pos, tile);
// }
// terrain.UpdateMeshAndShape();
// terrain.NotifyPropertyListChanged();
// return;
// }
// if ((ev is InputEventMouseButton { ButtonIndex: var wheel2, Pressed: var pressed2, CtrlPressed: true })
// && (wheel2 is MouseButton.WheelUp or MouseButton.WheelDown) && (_selection != null))
// {
// GetViewport().SetInputAsHandled();
// if (!pressed2) return;
// var amount = (wheel2 == MouseButton.WheelUp) ? 1 : -1;
// var selection = TileRegion.From(_selection.Value);
// foreach (var pos in selection.GetAllTiles()) {
// var tile = terrain.GetTile(pos);
// tile.TexturePrimary = PosMod(tile.TexturePrimary + amount, 4);
// terrain.SetTile(pos, tile);
// }
// terrain.UpdateMeshAndShape();
// terrain.NotifyPropertyListChanged();
// return;
// }
// if (ev is InputEventMouseMotion)
// _unhandledMotion = true;
// if ((ev is InputEventMouse mouse) && viewport.GetVisibleRect().HasPoint(mouse.Position))
// OnInputRayCastTerrain(terrain, mouse);
}
public override void _Process(double delta)
void UpdateEditToolMesh(Terrain terrain, IEnumerable<TilePos> tiles)
{
if (GetTerrain() is not Terrain terrain) {
_tileHover = null;
_selection = null;
_isSelecting = false;
terrain = null;
}
if (terrain != _currentTerrain) ClearEditToolMesh();
_currentTerrain = terrain;
if (_unhandledMotion)
_tileHover = null;
if ((_tileHover != null) || (_selection != null)) {
var mesh = terrain.GetOrCreateMesh("EditToolMesh");
mesh.ClearSurfaces();
mesh.SurfaceBegin(Mesh.PrimitiveType.Lines);
@ -199,84 +196,63 @@ public partial class TerrainEditingControls
}
void AddQuad(Vector3 topLeft , Vector3 topRight,
Vector3 bottomLeft, Vector3 bottomRight) {
Vector3 bottomRight, Vector3 bottomLeft) {
AddLine(topLeft , topRight );
AddLine(topRight , bottomRight);
AddLine(bottomRight, bottomLeft );
AddLine(bottomLeft , topLeft );
}
if (_tileHover is TilePos hover) {
var corners = terrain.GetTileCornerPositions(hover);
var margin = 0.1f;
mesh.SurfaceSetColor(Colors.Black);
AddQuad(corners.TopLeft + new Vector3(-margin, 0, -margin),
corners.TopRight + new Vector3(+margin, 0, -margin),
corners.BottomLeft + new Vector3(-margin, 0, +margin),
corners.BottomRight + new Vector3(+margin, 0, +margin));
}
mesh.SurfaceSetColor(Colors.Blue);
if (_selection is (TilePos, TilePos) selection)
foreach (var pos in TileRegion.From(selection).GetAllTiles()) {
var corners = terrain.GetTileCornerPositions(pos);
AddQuad(corners.TopLeft, corners.TopRight, corners.BottomLeft, corners.BottomRight);
foreach (var tile in tiles) {
var (topLeft, topRight, bottomRight, bottomLeft)
= terrain.GetTileCornerPositions(tile);
AddQuad(topLeft, topRight, bottomRight, bottomLeft);
}
mesh.SurfaceEnd();
mesh.SurfaceSetMaterial(0, _editToolMaterial);
} else {
var meshInstance = (MeshInstance3D)GetNodeOrNull("EditToolMesh");
var mesh = (ImmediateMesh)meshInstance?.Mesh;
mesh?.ClearSurfaces();
}
}
public Terrain GetTerrain() => EditorInterface.Singleton.GetSelection()
.GetSelectedNodes().OfType<Terrain>().FirstOrDefault();
void ClearEditToolMesh()
=> _currentTerrain?.GetNodeOrNull("EditToolMesh")?.QueueFree();
readonly record struct TileRegion(int Left, int Top, int Right, int Bottom)
(Terrain Terrain, Vector3 Position)? RayCastTerrain(InputEventMouse ev)
{
public TilePos TopLeft => new(Left , Top);
public TilePos TopRight => new(Right, Top);
public TilePos BottomRight => new(Right, Bottom);
public TilePos BottomLeft => new(Left , Bottom);
public int Width => Right - Left + 1;
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)),
};
// 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;
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)),
};
// 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;
}
public IEnumerable<TilePos> GetAllTiles()
static (TilePos, Corner) FindClosestTile(Terrain terrain, Vector3 position)
{
for (var x = Left; x <= Right; x++)
for (var y = Top; y <= Bottom; y++)
yield return new(x, y);
}
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);
}
}

@ -78,7 +78,7 @@ horizontal_alignment = 1
custom_minimum_size = Vector2(0, 80)
layout_mode = 2
size_flags_horizontal = 1
min_value = -16.0
min_value = -8.0
max_value = -1.0
value = -1.0
@ -87,6 +87,12 @@ layout_mode = 2
[node name="Grass" type="Button" parent="."]
layout_mode = 2
theme_override_colors/icon_normal_color = Color(1, 1, 1, 1)
theme_override_colors/icon_focus_color = Color(1, 1, 1, 1)
theme_override_colors/icon_pressed_color = Color(1, 1, 1, 1)
theme_override_colors/icon_hover_color = Color(1, 1, 1, 1)
theme_override_colors/icon_hover_pressed_color = Color(1, 1, 1, 1)
theme_override_colors/icon_disabled_color = Color(0.753984, 0.753984, 0.753984, 0.501961)
theme_override_constants/icon_max_width = 16
disabled = true
toggle_mode = true
@ -94,6 +100,12 @@ icon = SubResource("ImageTexture_btyvd")
[node name="Dirt" type="Button" parent="."]
layout_mode = 2
theme_override_colors/icon_normal_color = Color(1, 1, 1, 1)
theme_override_colors/icon_focus_color = Color(1, 1, 1, 1)
theme_override_colors/icon_pressed_color = Color(1, 1, 1, 1)
theme_override_colors/icon_hover_color = Color(1, 1, 1, 1)
theme_override_colors/icon_hover_pressed_color = Color(1, 1, 1, 1)
theme_override_colors/icon_disabled_color = Color(0.753984, 0.753984, 0.753984, 0.501961)
theme_override_constants/icon_max_width = 16
disabled = true
toggle_mode = true
@ -103,6 +115,12 @@ flat = true
[node name="Rock" type="Button" parent="."]
layout_mode = 2
theme_override_colors/icon_normal_color = Color(1, 1, 1, 1)
theme_override_colors/icon_focus_color = Color(1, 1, 1, 1)
theme_override_colors/icon_pressed_color = Color(1, 1, 1, 1)
theme_override_colors/icon_hover_color = Color(1, 1, 1, 1)
theme_override_colors/icon_hover_pressed_color = Color(1, 1, 1, 1)
theme_override_colors/icon_disabled_color = Color(0.753984, 0.753984, 0.753984, 0.501961)
theme_override_constants/icon_max_width = 16
disabled = true
toggle_mode = true
@ -112,6 +130,12 @@ flat = true
[node name="Sand" type="Button" parent="."]
layout_mode = 2
theme_override_colors/icon_normal_color = Color(1, 1, 1, 1)
theme_override_colors/icon_focus_color = Color(1, 1, 1, 1)
theme_override_colors/icon_pressed_color = Color(1, 1, 1, 1)
theme_override_colors/icon_hover_color = Color(1, 1, 1, 1)
theme_override_colors/icon_hover_pressed_color = Color(1, 1, 1, 1)
theme_override_colors/icon_disabled_color = Color(0.753984, 0.753984, 0.753984, 0.501961)
theme_override_constants/icon_max_width = 16
disabled = true
toggle_mode = true

@ -33,11 +33,6 @@ public partial class Terrain
=> (pos.X >= 0) && (pos.X < Size.X)
&& (pos.Y >= 0) && (pos.Y < Size.Y);
/// <summary> Transforms a 3D position local to the equivalent tile position. </summary>
public TilePos ToTilePos(Vector3 localPos)
=> new(RoundToInt(localPos.X / TileSize + Size.X / 2.0f),
RoundToInt(localPos.Z / TileSize + Size.Y / 2.0f));
public void UpdateMeshAndShape()
{

@ -106,6 +106,16 @@ public struct Corners<T>(T topLeft, T topRight, T bottomRight, T bottomLeft)
default: throw new ArgumentException($"Invalid Corner value '{corner}'", nameof(corner));
} }
}
public readonly void Deconstruct(
out T topLeft , out T topRight,
out T bottomRight, out T bottomLeft)
{
topLeft = TopLeft;
topRight = TopRight;
bottomRight = BottomRight;
bottomLeft = BottomLeft;
}
}
public static class CornersExtensions

Loading…
Cancel
Save