From 883e2af3174e8cfa5e447e65b1c892448ca2d3bb Mon Sep 17 00:00:00 2001 From: copygirl Date: Wed, 2 Oct 2024 22:14:04 +0200 Subject: [PATCH] Add erase tool, more improvements Changes are now tracked in `tilesToChange`. (No more need for `tilesPrevious` and `tilesChanged`.) Applied to full tiles instead of just height or texture. --- .../{corner_paint.png => corner_tile.png} | Bin ...aint.png.import => corner_tile.png.import} | 6 +- addons/terrain-editing/icons/erase.png | Bin 0 -> 164 bytes addons/terrain-editing/icons/erase.png.import | 34 +++ .../terrain_editing_controls.gd | 27 ++- .../terrain_editing_controls.tscn | 9 +- terrain/Terrain+Editing.cs | 208 ++++++++---------- utility/CollectionExtensions.cs | 11 + 8 files changed, 164 insertions(+), 131 deletions(-) rename addons/terrain-editing/icons/{corner_paint.png => corner_tile.png} (100%) rename addons/terrain-editing/icons/{corner_paint.png.import => corner_tile.png.import} (69%) create mode 100644 addons/terrain-editing/icons/erase.png create mode 100644 addons/terrain-editing/icons/erase.png.import create mode 100644 utility/CollectionExtensions.cs diff --git a/addons/terrain-editing/icons/corner_paint.png b/addons/terrain-editing/icons/corner_tile.png similarity index 100% rename from addons/terrain-editing/icons/corner_paint.png rename to addons/terrain-editing/icons/corner_tile.png diff --git a/addons/terrain-editing/icons/corner_paint.png.import b/addons/terrain-editing/icons/corner_tile.png.import similarity index 69% rename from addons/terrain-editing/icons/corner_paint.png.import rename to addons/terrain-editing/icons/corner_tile.png.import index 45b5ce4..7d84902 100644 --- a/addons/terrain-editing/icons/corner_paint.png.import +++ b/addons/terrain-editing/icons/corner_tile.png.import @@ -3,15 +3,15 @@ importer="texture" type="CompressedTexture2D" uid="uid://dc0q2xn2cgcjw" -path="res://.godot/imported/corner_paint.png-c66b764e062a869b0cd32b525c1718b2.ctex" +path="res://.godot/imported/corner_tile.png-0c44f85b2fc372771ac9f78ccd76ab93.ctex" metadata={ "vram_texture": false } [deps] -source_file="res://addons/terrain-editing/icons/corner_paint.png" -dest_files=["res://.godot/imported/corner_paint.png-c66b764e062a869b0cd32b525c1718b2.ctex"] +source_file="res://addons/terrain-editing/icons/corner_tile.png" +dest_files=["res://.godot/imported/corner_tile.png-0c44f85b2fc372771ac9f78ccd76ab93.ctex"] [params] diff --git a/addons/terrain-editing/icons/erase.png b/addons/terrain-editing/icons/erase.png new file mode 100644 index 0000000000000000000000000000000000000000..5db3ba8af8acac6450abb7fade846dd6f2107497 GIT binary patch literal 164 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`*`6+rAr`$)CwOx;81S$P_eU%Y z;@8;mq33)nThL^;w}*Q3MLCRr3j9^Jc>MJTXAI*RQDLrG!BbNzxV^1KTs}Ny(q?F= zlzN{KUGZU4p5mLl1gTe~DWM4f`Or57 literal 0 HcmV?d00001 diff --git a/addons/terrain-editing/icons/erase.png.import b/addons/terrain-editing/icons/erase.png.import new file mode 100644 index 0000000..ec555b3 --- /dev/null +++ b/addons/terrain-editing/icons/erase.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://7kstj7og5bsh" +path="res://.godot/imported/erase.png-ab9c7058d6cbc29ffa2b36ef2f531c01.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/terrain-editing/icons/erase.png" +dest_files=["res://.godot/imported/erase.png-ab9c7058d6cbc29ffa2b36ef2f531c01.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/terrain-editing/terrain_editing_controls.gd b/addons/terrain-editing/terrain_editing_controls.gd index 0787dc8..9740c00 100644 --- a/addons/terrain-editing/terrain_editing_controls.gd +++ b/addons/terrain-editing/terrain_editing_controls.gd @@ -2,10 +2,10 @@ class_name TerrainEditingControls extends VBoxContainer -enum ToolMode { HEIGHT, FLATTEN, PAINT } +enum ToolMode { HEIGHT, FLATTEN, PAINT, ERASE } enum ToolShape { CORNER, CIRCLE, SQUARE } -@onready var tool_mode_buttons : Array[Button] = [ $Height, $Flatten, $Paint ] +@onready var tool_mode_buttons : Array[Button] = [ $Height, $Flatten, $Paint, $Erase ] @onready var tool_shape_buttons : Array[Button] = [ $Corner, $Circle, $Square ] @onready var paint_texture_buttons : Array[Button] = [ $Grass, $Dirt, $Rock, $Sand ] @@ -15,14 +15,14 @@ enum ToolShape { CORNER, CIRCLE, SQUARE } @onready var connected_toggle : Button = $Connected @onready var corner_texture_default : Texture2D = preload("icons/corner.png") -@onready var corner_texture_paint : Texture2D = preload("icons/corner_paint.png") +@onready var corner_texture_tile : Texture2D = preload("icons/corner_tile.png") -## Gets or sets the currently active tool mode (HEIGHT, FLATTEN, PAINT). +## Gets or sets the currently active tool mode. var tool_mode : ToolMode: get: return index_of_pressed(tool_mode_buttons) set(value): set_pressed(tool_mode_buttons, value); tool_mode_changed() -## Gets or sets the currently selected tool shape (CORNER, CIRCLE, SQUARE). +## Gets or sets the currently selected tool shape. var tool_shape : ToolShape: get: return index_of_pressed(tool_shape_buttons) set(value): set_pressed(tool_shape_buttons, value); tool_shape_changed() @@ -55,16 +55,23 @@ func _ready() -> void: func tool_mode_changed() -> void: - var is_height := (tool_mode == ToolMode.HEIGHT) - var is_paint := (tool_mode == ToolMode.PAINT) + var is_height := (tool_mode == ToolMode.HEIGHT) + var is_flatten := (tool_mode == ToolMode.FLATTEN) + var is_paint := (tool_mode == ToolMode.PAINT) + var is_erase := (tool_mode == ToolMode.ERASE) - # In 'PAINT' mode, 'CORNER' affects a single tile regardless of 'draw_size'. + # In 'PAINT' and 'ERASE' mode, 'CORNER' affects a single tile regardless of 'draw_size'. # This changes the button's icon to a small square to communicate that. - tool_shape_buttons[0].icon = corner_texture_paint if is_paint else corner_texture_default + tool_shape_buttons[0].icon = corner_texture_tile if is_paint or is_erase else corner_texture_default + # Enable texture buttons only in 'PAINT' mode. for button in paint_texture_buttons: button.disabled = !is_paint + + # Enable raise/lower toggle only in 'HEIGHT' mode. raise_lower_toggle.disabled = !is_height - connected_toggle.disabled = is_paint + + # Enable connected toggle only in 'HEIGHT' or 'FLATTEN' mode. + connected_toggle.disabled = !(is_height or is_flatten) func tool_shape_changed() -> void: var is_corner := (tool_shape == ToolShape.CORNER) diff --git a/addons/terrain-editing/terrain_editing_controls.tscn b/addons/terrain-editing/terrain_editing_controls.tscn index 12a6eec..c2aa331 100644 --- a/addons/terrain-editing/terrain_editing_controls.tscn +++ b/addons/terrain-editing/terrain_editing_controls.tscn @@ -1,10 +1,11 @@ -[gd_scene load_steps=17 format=3 uid="uid://bp0wulaxcrutd"] +[gd_scene load_steps=18 format=3 uid="uid://bp0wulaxcrutd"] [ext_resource type="Script" path="res://addons/terrain-editing/terrain_editing_controls.gd" id="1_4e1sk"] [ext_resource type="Script" path="res://addons/terrain-editing/modifier_toggle_button.gd" id="2_61bkl"] [ext_resource type="Texture2D" uid="uid://dqbtbf8pe05qv" path="res://addons/terrain-editing/icons/height.png" id="3_mn5pg"] [ext_resource type="Texture2D" uid="uid://bcb8w33ns56go" path="res://addons/terrain-editing/icons/flatten.png" id="4_c1bhf"] [ext_resource type="Texture2D" uid="uid://btdpyu4n3pgkx" path="res://addons/terrain-editing/icons/paint.png" id="5_547tx"] +[ext_resource type="Texture2D" uid="uid://7kstj7og5bsh" path="res://addons/terrain-editing/icons/erase.png" id="5_i5hs4"] [ext_resource type="Texture2D" uid="uid://btl3jsqeldix2" path="res://addons/terrain-editing/icons/corner.png" id="6_fcc6v"] [ext_resource type="Texture2D" uid="uid://2u1ldmh0osbx" path="res://addons/terrain-editing/icons/circle.png" id="7_b1ydi"] [ext_resource type="Texture2D" uid="uid://btjd1704xtdjv" path="res://addons/terrain-editing/icons/square.png" id="8_w3t42"] @@ -50,6 +51,12 @@ toggle_mode = true icon = ExtResource("5_547tx") flat = true +[node name="Erase" type="Button" parent="."] +layout_mode = 2 +toggle_mode = true +icon = ExtResource("5_i5hs4") +flat = true + [node name="HSeparator" type="HSeparator" parent="."] layout_mode = 2 diff --git a/terrain/Terrain+Editing.cs b/terrain/Terrain+Editing.cs index fd84856..9c6fc58 100644 --- a/terrain/Terrain+Editing.cs +++ b/terrain/Terrain+Editing.cs @@ -4,16 +4,13 @@ using System.Runtime.InteropServices; public partial class Terrain { // These mirror the modes / shapes in 'terrain_editing_controls.gd'. - enum ToolMode { Height, Flatten, Paint } + enum ToolMode { Height, Flatten, Paint, Erase } enum ToolShape { Corner, Circle, Square } // Set by the terrain editing plugin. // Enables access to the in-editor undo/redo system. public EditorUndoRedoManager EditorUndoRedo { get; set; } - // Dummy value to satisfy the overly careful compiler. - static bool _dummy = false; - Material _editToolMaterial; public override void _EnterTree() { @@ -78,16 +75,22 @@ public partial class Terrain // TODO: Allow click-dragging which doesn't affect already changed tiles / corners. // TODO: Use ArrayMesh instead of ImmediateMesh. + // TODO: Support texture blending somehow. + // TODO: Clear empty chunks. + + // Data structure for holding tiles to be modified. + var tilesToChange = new Dictionary(); - // Holds onto all the tiles and which of their corners corners will be affected by this edit operation. - var tilesToChange = new Dictionary>(); + // Utility function to set 'Affected' for the specified position and corner. + void SetCornerAffected((TilePos Position, Corner Corner) pair) + => tilesToChange.GetOrAddNew(pair.Position).Affected[pair.Corner] = true; - // 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); + // Utility function to get the height for the specified position and corner. + short GetCornerHeight((TilePos Position, Corner Corner) pair) + => Data.GetTileOrDefault(pair.Position).Height[pair.Corner]; - if (toolMode == ToolMode.Paint) { - // In PAINT mode, only full tiles are ever affected. + if (toolMode is ToolMode.Paint or ToolMode.Erase) { + // In PAINT or ERASE mode, only full tiles are ever affected. // So this makes populating 'tilesToChange' very straight-forward. var tiles = toolShape switch { @@ -100,23 +103,21 @@ public partial class Terrain }; foreach (var pos in tiles) - tilesToChange.Add(pos, new(true)); + tilesToChange.Add(pos, new(){ Affected = new(true) }); } else if (toolShape == ToolShape.Corner) { // With the CORNER shape, only a single corner is affected. // Modify selected corner itself. - Tile(hover.Position)[hover.Corner] = true; + SetCornerAffected(hover); // 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 = Data.GetTileOrDefault(hover.Position).Height[hover.Corner]; - foreach (var neighbor in GetNeighbors(hover.Position, hover.Corner)) { - var neighborHeight = Data.GetTileOrDefault(neighbor.Position).Height[neighbor.Corner]; - if (neighborHeight != height) continue; - Tile(neighbor.Position)[neighbor.Corner] = true; - } + var height = GetCornerHeight(hover); + foreach (var neighbor in GetNeighbors(hover.Position, hover.Corner)) + if (GetCornerHeight(neighbor) == height) + SetCornerAffected(neighbor); } } else { @@ -129,7 +130,7 @@ public partial class Terrain // Modify selected tiles themselves. foreach (var pos in tiles) - tilesToChange.Add(pos, new(true)); + tilesToChange.Add(pos, new(){ Affected = 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. @@ -137,12 +138,10 @@ public partial class Terrain var tile = Data.GetTileOrDefault(pos); foreach (var corner in Enum.GetValues()) { var height = tile.Height[corner]; - foreach (var neighbor in GetNeighbors(pos, corner)) { - if (tiles.Contains(neighbor.Position)) continue; - var neighborHeight = Data.GetTileOrDefault(neighbor.Position).Height[neighbor.Corner]; - if (neighborHeight != height) continue; - Tile(neighbor.Position)[neighbor.Corner] = true; - } + foreach (var neighbor in GetNeighbors(pos, corner)) + if (!tiles.Contains(neighbor.Position)) + if (GetCornerHeight(neighbor) == height) + SetCornerAffected(neighbor); } } @@ -152,68 +151,52 @@ public partial class Terrain if (ev is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: true }) { prevent_default = true; - string action; - StringName method; - Variant[] doArgs; - Variant[] undoArgs; + // Fill with current tile data. + foreach (var (pos, modified) in tilesToChange) + modified.Original = modified.New = Data.GetTileOrDefault(pos); + string action; if (toolMode == ToolMode.Paint) { - // TODO: Support blending somehow. - var tilesPrevious = new List<(TilePos, byte)>(); - var tilesChanged = new List<(TilePos, byte)>(); - - foreach (var (pos, corners) in tilesToChange) { - var tile = Data.GetTileOrDefault(pos); - tilesPrevious.Add((pos, tile.TexturePrimary)); - tilesChanged.Add((pos, texture)); + action = "Paint terrain"; + foreach (var (_, modified) in tilesToChange) + modified.New.TexturePrimary = texture; + } else if (toolMode == ToolMode.Erase) { + action = "Erase terrain"; + foreach (var (_, modified) in tilesToChange) + modified.New = default; + } else if (isFlatten) { + action = "Flatten terrain"; + var amount = GetCornerHeight(hover); + foreach (var (_, modified) in tilesToChange) { + ref var height = ref modified.New.Height; + if (modified.Affected.TopLeft ) height.TopLeft = amount; + if (modified.Affected.TopRight ) height.TopRight = amount; + if (modified.Affected.BottomRight) height.BottomRight = amount; + if (modified.Affected.BottomLeft ) height.BottomLeft = amount; } - - 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)>(); - - var amount = isFlatten ? Data.GetTileOrDefault(hover.Position).Height[hover.Corner] - : isRaise ? (short)+1 : (short)-1; - - foreach (var (pos, corners) in tilesToChange) { - var tile = Data.GetTileOrDefault(pos); - tilesPrevious.Add((pos, tile.Height)); - - var newHeight = tile.Height; - if (isFlatten) { - if (corners.TopLeft ) newHeight.TopLeft = amount; - if (corners.TopRight ) newHeight.TopRight = amount; - if (corners.BottomRight) newHeight.BottomRight = amount; - if (corners.BottomLeft ) newHeight.BottomLeft = amount; - } else { - if (corners.TopLeft ) newHeight.TopLeft += amount; - if (corners.TopRight ) newHeight.TopRight += amount; - if (corners.BottomRight) newHeight.BottomRight += amount; - if (corners.BottomLeft ) newHeight.BottomLeft += amount; - } - tilesChanged.Add((pos, newHeight)); + action = isRaise ? "Raise terrain" : "Lower terrain"; + var amount = isRaise ? (short)+1 : (short)-1; + foreach (var (_, modified) in tilesToChange) { + ref var height = ref modified.New.Height; + if (modified.Affected.TopLeft ) height.TopLeft += amount; + if (modified.Affected.TopRight ) height.TopRight += amount; + if (modified.Affected.BottomRight) height.BottomRight += amount; + if (modified.Affected.BottomLeft ) height.BottomLeft += amount; } - - action = isFlatten ? "Flatten terrain" - : isRaise ? "Raise terrain" - : "Lower Terrain"; - method = nameof(DoModifyTerrainHeight); - doArgs = [ PackHeightData(tilesChanged) ]; - undoArgs = [ PackHeightData(tilesPrevious) ]; } if (EditorUndoRedo is EditorUndoRedoManager undo) { undo.CreateAction(action, backwardUndoOps: false); - undo.AddDoMethod(this, method, doArgs); + var doData = Pack(tilesToChange.Select(e => (e.Key, e.Value.New))); + var undoData = Pack(tilesToChange.Select(e => (e.Key, e.Value.Original))); + + undo.AddDoMethod(this, nameof(DoModifyTerrain), doData); undo.AddDoMethod(this, nameof(UpdateMeshAndShape)); undo.AddDoMethod(this, GodotObject.MethodName.NotifyPropertyListChanged); - undo.AddUndoMethod(this, method, undoArgs); + undo.AddUndoMethod(this, nameof(DoModifyTerrain), undoData); undo.AddUndoMethod(this, nameof(UpdateMeshAndShape)); undo.AddUndoMethod(this, GodotObject.MethodName.NotifyPropertyListChanged); @@ -221,27 +204,28 @@ public partial class Terrain } } - UpdateEditToolMesh(tilesToChange); + UpdateEditToolMesh(tilesToChange.Select(e => (e.Key, e.Value.Affected))); return prevent_default; } - public void EditorUnfocus() - => ClearEditToolMesh(); - - public void DoModifyTerrainHeight(byte[] data) + class TileModification { - foreach (var (pos, corners) in UnpackHeightData(data)) - Data[pos].Height = corners; + public Corners Affected; + public Tile Original; + public Tile New; } - public void DoModifyTerrainTexture(byte[] data) + public void EditorUnfocus() + => ClearEditToolMesh(); + + public void DoModifyTerrain(byte[] data) { - foreach (var (pos, texture) in UnpackTextureData(data)) - Data[pos].TexturePrimary = texture; + foreach (var (pos, tile) in Unpack(data)) + Data[pos] = tile; } - void UpdateEditToolMesh(Dictionary> tiles) + void UpdateEditToolMesh(IEnumerable<(TilePos, Corners)> tiles) { var mesh = GetOrCreateMesh("EditToolMesh"); mesh.ClearSurfaces(); @@ -297,56 +281,46 @@ public partial class Terrain => _offsetLookup[corner].Select(e => (new TilePos(pos.X + e.X, pos.Z + e.Z), e.Opposite)); - static byte[] PackHeightData(IEnumerable<(TilePos Position, Corners Corners)> data) + static byte[] Pack(IEnumerable<(TilePos Position, Tile tile)> data) { using var stream = new MemoryStream(); using var writer = new BinaryWriter(stream); - foreach (var (pos, corners) in data) { + foreach (var (pos, tile) in data) { writer.Write(pos.X); writer.Write(pos.Z); - writer.Write(corners.TopLeft); - writer.Write(corners.TopRight); - writer.Write(corners.BottomRight); - writer.Write(corners.BottomLeft); + writer.Write(tile.Height.TopLeft); + writer.Write(tile.Height.TopRight); + writer.Write(tile.Height.BottomRight); + writer.Write(tile.Height.BottomLeft); + writer.Write(tile.TexturePrimary); + writer.Write(tile.TextureSecondary); + writer.Write(tile.TextureBlend); } return stream.ToArray(); } - static IEnumerable<(TilePos Position, Corners Corners)> UnpackHeightData(byte[] data) + static IEnumerable<(TilePos Position, Tile tile)> 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( + + var height = new Corners( reader.ReadInt16(), reader.ReadInt16(), reader.ReadInt16(), reader.ReadInt16()); - yield return (new(x, y), corners); - } - } - static byte[] PackTextureData(IEnumerable<(TilePos Position, byte 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.Z); - writer.Write(texture); - } - return stream.ToArray(); - } + var texPrimary = reader.ReadByte(); + var texSecondary = reader.ReadByte(); + var texBlend = reader.ReadByte(); - static IEnumerable<(TilePos Position, byte 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); + yield return (new(x, y), new(){ + Height = height, + TexturePrimary = texPrimary, + TextureSecondary = texSecondary, + TextureBlend = texBlend, + }); } } } diff --git a/utility/CollectionExtensions.cs b/utility/CollectionExtensions.cs new file mode 100644 index 0000000..4515d30 --- /dev/null +++ b/utility/CollectionExtensions.cs @@ -0,0 +1,11 @@ +public static class CollectionExtensions +{ + public static TValue GetOrAddNew( + this Dictionary dict, TKey key) + where TValue : new() + { + if (!dict.TryGetValue(key, out var value)) + dict.Add(key, value = new()); + return value; + } +}