From 6f54e2706e78c3051c966d4d0c19a048ce986cf6 Mon Sep 17 00:00:00 2001 From: copygirl Date: Tue, 1 Oct 2024 19:35:13 +0200 Subject: [PATCH] Implement terrain painting --- level.tscn | 276 +++++++++++++++++++++++++++++++++++-- terrain/Terrain+Editing.cs | 150 +++++++++++++++----- terrain/Terrain.cs | 2 + 3 files changed, 386 insertions(+), 42 deletions(-) diff --git a/level.tscn b/level.tscn index 50e1704..cb70642 100644 --- a/level.tscn +++ b/level.tscn @@ -1494,6 +1494,38 @@ Vector2i(40, 30): { "heights": 0.0, "texture": 1 }, +Vector2i(40, 34): { +"heights": PackedFloat32Array(0, 0, 1, 0), +"texture": 3 +}, +Vector2i(40, 35): { +"heights": PackedFloat32Array(0, 1, 1, 0), +"texture": 3 +}, +Vector2i(40, 36): { +"heights": 1.5, +"texture": 2 +}, +Vector2i(40, 37): { +"heights": 2.0, +"texture": 2 +}, +Vector2i(40, 38): { +"heights": 2.0, +"texture": 2 +}, +Vector2i(40, 39): { +"heights": 2.0, +"texture": 2 +}, +Vector2i(40, 40): { +"heights": 2.0, +"texture": 2 +}, +Vector2i(40, 41): { +"heights": 1.5, +"texture": 2 +}, Vector2i(41, 19): { "heights": PackedFloat32Array(0, 0, 3, 3), "texture": 2 @@ -1530,6 +1562,38 @@ Vector2i(41, 27): { "heights": PackedFloat32Array(0, -1, 0, 0), "texture": 0 }, +Vector2i(41, 34): { +"heights": PackedFloat32Array(0, 0, 1, 1), +"texture": 3 +}, +Vector2i(41, 35): { +"heights": 1.0, +"texture": 1 +}, +Vector2i(41, 36): { +"heights": 2.0, +"texture": 2 +}, +Vector2i(41, 37): { +"heights": PackedFloat32Array(2, 2, 1, 2), +"texture": 2 +}, +Vector2i(41, 38): { +"heights": PackedFloat32Array(2, 1, 1, 2), +"texture": 0 +}, +Vector2i(41, 39): { +"heights": PackedFloat32Array(2, 1, 1, 2), +"texture": 0 +}, +Vector2i(41, 40): { +"heights": PackedFloat32Array(2, 1, 2, 2), +"texture": 2 +}, +Vector2i(41, 41): { +"heights": 2.0, +"texture": 2 +}, Vector2i(42, 19): { "heights": PackedFloat32Array(0, 0, 4.5, 3), "texture": 2 @@ -1558,6 +1622,38 @@ Vector2i(42, 27): { "heights": PackedFloat32Array(-0.5, -0.5, 0, 0), "texture": 0 }, +Vector2i(42, 34): { +"heights": PackedFloat32Array(0, 0, 1, 1), +"texture": 3 +}, +Vector2i(42, 35): { +"heights": 1.0, +"texture": 1 +}, +Vector2i(42, 36): { +"heights": 2.0, +"texture": 2 +}, +Vector2i(42, 37): { +"heights": PackedFloat32Array(2, 2, 1, 1), +"texture": 0 +}, +Vector2i(42, 38): { +"heights": PackedFloat32Array(1, 1, 3, 1), +"texture": 0 +}, +Vector2i(42, 39): { +"heights": PackedFloat32Array(3, 1, 1, 1), +"texture": 0 +}, +Vector2i(42, 40): { +"heights": PackedFloat32Array(1, 1, 2, 2), +"texture": 0 +}, +Vector2i(42, 41): { +"heights": 2.0, +"texture": 2 +}, Vector2i(43, 19): { "heights": PackedFloat32Array(0, 0, 4.5, 4.5), "texture": 2 @@ -1584,16 +1680,48 @@ Vector2i(43, 26): { }, Vector2i(43, 27): { "heights": PackedFloat32Array(1.5, 0, 1, 0), -"texture": 0 +"texture": 2 }, Vector2i(43, 28): { "heights": PackedFloat32Array(0, 1, 1, 0), -"texture": 0 +"texture": 2 }, Vector2i(43, 29): { "heights": PackedFloat32Array(0, 1, 0, 0), +"texture": 2 +}, +Vector2i(43, 34): { +"heights": PackedFloat32Array(0, 0, 1, 1), +"texture": 3 +}, +Vector2i(43, 35): { +"heights": 1.0, +"texture": 1 +}, +Vector2i(43, 36): { +"heights": 2.0, +"texture": 2 +}, +Vector2i(43, 37): { +"heights": PackedFloat32Array(2, 2, 1, 1), +"texture": 0 +}, +Vector2i(43, 38): { +"heights": PackedFloat32Array(1, 1, 1, 3), +"texture": 0 +}, +Vector2i(43, 39): { +"heights": PackedFloat32Array(1, 3, 1, 1), "texture": 0 }, +Vector2i(43, 40): { +"heights": PackedFloat32Array(1, 1, 2, 2), +"texture": 0 +}, +Vector2i(43, 41): { +"heights": 2.0, +"texture": 2 +}, Vector2i(44, 19): { "heights": PackedFloat32Array(0, 0, 3, 4.5), "texture": 2 @@ -1612,16 +1740,48 @@ Vector2i(44, 22): { }, Vector2i(44, 27): { "heights": PackedFloat32Array(0, 0, 2, 1), -"texture": 0 +"texture": 2 }, Vector2i(44, 28): { "heights": PackedFloat32Array(2, 4, 5.5, 4.5), -"texture": 0 +"texture": 2 }, Vector2i(44, 29): { "heights": PackedFloat32Array(1, 2, 0, 0), +"texture": 2 +}, +Vector2i(44, 34): { +"heights": PackedFloat32Array(0, 0, 1, 1), +"texture": 3 +}, +Vector2i(44, 35): { +"heights": 1.0, +"texture": 1 +}, +Vector2i(44, 36): { +"heights": 2.0, +"texture": 2 +}, +Vector2i(44, 37): { +"heights": PackedFloat32Array(2, 2, 2, 1), +"texture": 2 +}, +Vector2i(44, 38): { +"heights": PackedFloat32Array(1, 2, 2, 1), +"texture": 0 +}, +Vector2i(44, 39): { +"heights": PackedFloat32Array(1, 2, 2, 1), "texture": 0 }, +Vector2i(44, 40): { +"heights": PackedFloat32Array(1, 2, 2, 2), +"texture": 2 +}, +Vector2i(44, 41): { +"heights": 2.0, +"texture": 2 +}, Vector2i(45, 19): { "heights": PackedFloat32Array(0, 0, 0, 3), "texture": 2 @@ -1644,27 +1804,123 @@ Vector2i(45, 25): { }, Vector2i(45, 27): { "heights": 2.0, -"texture": 0 +"texture": 2 }, Vector2i(45, 28): { "heights": PackedFloat32Array(3.5, 3.5, 4.5, 4.5), -"texture": 0 +"texture": 2 }, Vector2i(45, 29): { "heights": PackedFloat32Array(2, 1, 0, 0), -"texture": 0 +"texture": 2 +}, +Vector2i(45, 34): { +"heights": PackedFloat32Array(0, 0, 1, 1), +"texture": 3 +}, +Vector2i(45, 35): { +"heights": 1.0, +"texture": 1 +}, +Vector2i(45, 36): { +"heights": 1.5, +"texture": 2 +}, +Vector2i(45, 37): { +"heights": 2.0, +"texture": 2 +}, +Vector2i(45, 38): { +"heights": 2.0, +"texture": 2 +}, +Vector2i(45, 39): { +"heights": 2.0, +"texture": 2 +}, +Vector2i(45, 40): { +"heights": 2.0, +"texture": 2 +}, +Vector2i(45, 41): { +"heights": 1.5, +"texture": 2 }, Vector2i(46, 27): { "heights": PackedFloat32Array(0, 0, 0, 1), -"texture": 0 +"texture": 2 }, Vector2i(46, 28): { "heights": PackedFloat32Array(1, 0, 0, 1), -"texture": 0 +"texture": 2 }, Vector2i(46, 29): { "heights": PackedFloat32Array(1, 0, 0, 0), -"texture": 0 +"texture": 2 +}, +Vector2i(46, 34): { +"heights": PackedFloat32Array(0, 0, 1, 1), +"texture": 3 +}, +Vector2i(46, 35): { +"heights": 1.0, +"texture": 1 +}, +Vector2i(46, 36): { +"heights": 1.0, +"texture": 1 +}, +Vector2i(46, 37): { +"heights": 1.0, +"texture": 1 +}, +Vector2i(46, 38): { +"heights": 1.0, +"texture": 1 +}, +Vector2i(46, 39): { +"heights": 1.0, +"texture": 1 +}, +Vector2i(46, 40): { +"heights": 1.0, +"texture": 1 +}, +Vector2i(46, 41): { +"heights": PackedFloat32Array(1, 1, 0, 0), +"texture": 3 +}, +Vector2i(47, 34): { +"heights": PackedFloat32Array(0, 0, 0, 1), +"texture": 3 +}, +Vector2i(47, 35): { +"heights": PackedFloat32Array(1, 0, 0, 1), +"texture": 3 +}, +Vector2i(47, 36): { +"heights": PackedFloat32Array(1, 0, 0, 1), +"texture": 3 +}, +Vector2i(47, 37): { +"heights": PackedFloat32Array(1, 0, 0, 1), +"texture": 3 +}, +Vector2i(47, 38): { +"heights": PackedFloat32Array(1, 0, 0, 1), +"texture": 3 +}, +Vector2i(47, 39): { +"heights": PackedFloat32Array(1, 0, 0, 1), +"texture": 3 +}, +Vector2i(47, 40): { +"heights": PackedFloat32Array(1, 0, 0, 1), +"texture": 3 +}, +Vector2i(47, 41): { +"heights": PackedFloat32Array(1, 0, 0, 0), +"texture": 3 } } diff --git a/terrain/Terrain+Editing.cs b/terrain/Terrain+Editing.cs index 22dae10..4e746cc 100644 --- a/terrain/Terrain+Editing.cs +++ b/terrain/Terrain+Editing.cs @@ -48,6 +48,7 @@ public partial class Terrain var radius = FloorToInt(drawSize / 2.0f); // Offset hover tile position by corner. + // FIXME: This causes FLATTEN to calculate the wrong height in some cases. if (isEven) hover.Position = hover.Corner switch { Corner.TopLeft => hover.Position.Offset(0, 0), Corner.TopRight => hover.Position.Offset(1, 0), @@ -73,30 +74,51 @@ public partial class Terrain center.DistanceSquaredTo(tile.ToCenter()) < distanceSqr); } - // TODO: Handle different tool modes, such as painting. // TODO: Allow click-dragging which doesn't affect already changed tiles / corners. // TODO: Use ArrayMesh instead of ImmediateMesh. // TODO: Dynamically expand terrain instead of having it be a set size. // Holds onto all the tiles and which of their corners corners will be affected by this edit operation. var tilesToChange = new Dictionary>(); - ref Corners GetTileToChange(TilePos position) - // Don't look at this black magic. The Dictionary type should have this by default I swear! - => ref CollectionsMarshal.GetValueRefOrAddDefault(tilesToChange, position, out _dummy); + // Don't look at this black magic. The Dictionary type should have this by default I swear! + // Basically, this returns a reference to an entry in the dictionary that can be modified directly. + ref Corners Tile(TilePos position) => ref CollectionsMarshal.GetValueRefOrAddDefault(tilesToChange, position, out _dummy); + + if (toolMode == ToolMode.Paint) { + // In PAINT mode, only full tiles are ever affected. + // So this makes populating 'tilesToChange' very straight-forward. + + var tiles = toolShape switch { + // While in PAINT mode, the CORNER shape instead affects + // a single tile, regardless of the current 'draw_size'. + ToolShape.Corner => [ hover.Position ], + ToolShape.Circle => GetTilesInRadius(), + ToolShape.Square => GetTilesInSquare(), + _ => throw new InvalidOperationException(), + }; + + foreach (var pos in tiles) + tilesToChange.Add(pos, new(true)); + + } else if (toolShape == ToolShape.Corner) { + // With the CORNER shape, only a single corner is affected. - if (toolShape == ToolShape.Corner) { // Modify selected corner itself. - GetTileToChange(hover.Position)[hover.Corner] = true; + Tile(hover.Position)[hover.Corner] = true; + // If the 'connected_toggle' button is active, move "connected" corners. + // This is a simplified version of the code below that only affects the 3 neighboring corners. if (isConnected) { var height = GetTile(hover.Position).Height[hover.Corner]; foreach (var neighbor in GetNeighbors(hover.Position, hover.Corner)) { var neighborHeight = GetTile(neighbor.Position).Height[neighbor.Corner]; if (neighborHeight != height) continue; - GetTileToChange(neighbor.Position)[neighbor.Corner] = true; + Tile(neighbor.Position)[neighbor.Corner] = true; } } + } else { + var tiles = (toolShape switch { ToolShape.Circle => GetTilesInRadius(), ToolShape.Square => GetTilesInSquare(), @@ -105,7 +127,7 @@ public partial class Terrain // Modify selected tiles themselves. foreach (var pos in tiles) - GetTileToChange(pos) = new(true); + tilesToChange.Add(pos, new(true)); // 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. @@ -117,43 +139,73 @@ public partial class Terrain if (tiles.Contains(neighbor.Position)) continue; var neighborHeight = GetTile(neighbor.Position).Height[neighbor.Corner]; if (neighborHeight != height) continue; - GetTileToChange(neighbor.Position)[neighbor.Corner] = true; + Tile(neighbor.Position)[neighbor.Corner] = true; } } } + } - // Raise / lower the terrain when left mouse button is pressed. + // Modify the terrain when left mouse button is pressed. if (ev is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: true }) { prevent_default = true; - const float AdjustHeight = 0.5f; - var amount = isFlatten ? GetTile(hover.Position).Height[hover.Corner] - : isRaise ? AdjustHeight : -AdjustHeight; + string action; + StringName method; + Variant[] doArgs; + Variant[] undoArgs; - var tilesPrevious = new List<(TilePos, Corners)>(); - var tilesChanged = new List<(TilePos, Corners)>(); - foreach (var (pos, corners) in tilesToChange) { - var tile = GetTile(pos); - tilesPrevious.Add((pos, tile.Height)); - - var newHeight = tile.Height; - if (corners.TopLeft ) newHeight.TopLeft = isFlatten ? amount : newHeight.TopLeft + amount; - if (corners.TopRight ) newHeight.TopRight = isFlatten ? amount : newHeight.TopRight + amount; - if (corners.BottomRight) newHeight.BottomRight = isFlatten ? amount : newHeight.BottomRight + amount; - if (corners.BottomLeft ) newHeight.BottomLeft = isFlatten ? amount : newHeight.BottomLeft + amount; - tilesChanged.Add((pos, newHeight)); + if (toolMode == ToolMode.Paint) { + // TODO: Support blending somehow. + var tilesPrevious = new List<(TilePos, int)>(); + var tilesChanged = new List<(TilePos, int)>(); + + foreach (var (pos, corners) in tilesToChange) { + var tile = GetTile(pos); + tilesPrevious.Add((pos, tile.TexturePrimary)); + tilesChanged.Add((pos, texture)); + } + + action = "Paint terrain"; + method = nameof(DoModifyTerrainTexture); + doArgs = [ PackTextureData(tilesChanged) ]; + undoArgs = [ PackTextureData(tilesPrevious) ]; + } else { + var tilesPrevious = new List<(TilePos, Corners)>(); + var tilesChanged = new List<(TilePos, Corners)>(); + + const float AdjustHeight = 0.5f; + var amount = isFlatten ? GetTile(hover.Position).Height[hover.Corner] + : isRaise ? AdjustHeight : -AdjustHeight; + + foreach (var (pos, corners) in tilesToChange) { + var tile = GetTile(pos); + tilesPrevious.Add((pos, tile.Height)); + + var newHeight = tile.Height; + if (corners.TopLeft ) newHeight.TopLeft = isFlatten ? amount : newHeight.TopLeft + amount; + if (corners.TopRight ) newHeight.TopRight = isFlatten ? amount : newHeight.TopRight + amount; + if (corners.BottomRight) newHeight.BottomRight = isFlatten ? amount : newHeight.BottomRight + amount; + if (corners.BottomLeft ) newHeight.BottomLeft = isFlatten ? amount : newHeight.BottomLeft + amount; + tilesChanged.Add((pos, newHeight)); + } + + action = isFlatten ? "Flatten terrain" + : isRaise ? "Raise terrain" + : "Lower Terrain"; + method = nameof(DoModifyTerrainHeight); + doArgs = [ PackHeightData(tilesChanged) ]; + undoArgs = [ PackHeightData(tilesPrevious) ]; } if (EditorUndoRedo is EditorUndoRedoManager undo) { - var name = "Modify terrain height"; // TODO: Change name depending on tool mode. - undo.CreateAction(name, backwardUndoOps: false); + undo.CreateAction(action, backwardUndoOps: false); - undo.AddDoMethod(this, nameof(DoModifyTerrainHeight), Pack(tilesChanged)); + undo.AddDoMethod(this, method, doArgs); undo.AddDoMethod(this, nameof(UpdateMeshAndShape)); undo.AddDoMethod(this, GodotObject.MethodName.NotifyPropertyListChanged); - undo.AddUndoMethod(this, nameof(DoModifyTerrainHeight), Pack(tilesPrevious)); + undo.AddUndoMethod(this, method, undoArgs); undo.AddUndoMethod(this, nameof(UpdateMeshAndShape)); undo.AddUndoMethod(this, GodotObject.MethodName.NotifyPropertyListChanged); @@ -170,13 +222,22 @@ public partial class Terrain public void DoModifyTerrainHeight(byte[] data) { - foreach (var (pos, corners) in Unpack(data)) { + foreach (var (pos, corners) in UnpackHeightData(data)) { var tile = GetTile(pos); tile.Height = corners; SetTile(pos, tile); } } + public void DoModifyTerrainTexture(byte[] data) + { + foreach (var (pos, texture) in UnpackTextureData(data)) { + var tile = GetTile(pos); + tile.TexturePrimary = texture; + SetTile(pos, tile); + } + } + void UpdateEditToolMesh(Dictionary> tiles) { @@ -234,7 +295,8 @@ public partial class Terrain static IEnumerable<(TilePos Position, Corner Corner)> GetNeighbors(TilePos pos, Corner corner) => _offsetLookup[corner].Select(e => (new TilePos(pos.X + e.X, pos.Y + e.Y), e.Opposite)); - static byte[] Pack(IEnumerable<(TilePos Position, Corners Corners)> data) + + static byte[] PackHeightData(IEnumerable<(TilePos Position, Corners Corners)> data) { using var stream = new MemoryStream(); using var writer = new BinaryWriter(stream); @@ -249,7 +311,7 @@ public partial class Terrain return stream.ToArray(); } - static IEnumerable<(TilePos Position, Corners Corners)> Unpack(byte[] data) + static IEnumerable<(TilePos Position, Corners Corners)> UnpackHeightData(byte[] data) { using var stream = new MemoryStream(data); using var reader = new BinaryReader(stream); @@ -262,4 +324,28 @@ public partial class Terrain yield return (new(x, y), corners); } } + + static byte[] PackTextureData(IEnumerable<(TilePos Position, int Texture)> data) + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + foreach (var (pos, texture) in data) { + writer.Write(pos.X); + writer.Write(pos.Y); + writer.Write((byte)texture); + } + return stream.ToArray(); + } + + static IEnumerable<(TilePos Position, int Texture)> UnpackTextureData(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 texture = reader.ReadByte(); + yield return (new(x, y), texture); + } + } } diff --git a/terrain/Terrain.cs b/terrain/Terrain.cs index dda52da..609364a 100644 --- a/terrain/Terrain.cs +++ b/terrain/Terrain.cs @@ -142,6 +142,8 @@ public partial class Terrain corner2, new(0.0f, corner2.Y / TileSize)); break; case (false, false): + // FIXME: In some configurations this creates a shape we don't want. + // Need to find a way to detect this, and switch the way triangles make up a quad. AddTriangle(corner1, new(1.0f, corner1.Y / TileSize), corner4, new(1.0f, corner4.Y / TileSize), corner2, new(0.0f, corner2.Y / TileSize));