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.
main
copygirl 2 months ago
parent 52cc3fa28d
commit 883e2af317
  1. 0
      addons/terrain-editing/icons/corner_tile.png
  2. 6
      addons/terrain-editing/icons/corner_tile.png.import
  3. BIN
      addons/terrain-editing/icons/erase.png
  4. 34
      addons/terrain-editing/icons/erase.png.import
  5. 23
      addons/terrain-editing/terrain_editing_controls.gd
  6. 9
      addons/terrain-editing/terrain_editing_controls.tscn
  7. 208
      terrain/Terrain+Editing.cs
  8. 11
      utility/CollectionExtensions.cs

Before

Width:  |  Height:  |  Size: 124 B

After

Width:  |  Height:  |  Size: 124 B

@ -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]

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

@ -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

@ -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()
@ -56,15 +56,22 @@ func _ready() -> void:
func tool_mode_changed() -> void:
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)

@ -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

@ -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.
// Holds onto all the tiles and which of their corners corners will be affected by this edit operation.
var tilesToChange = new Dictionary<TilePos, Corners<bool>>();
// Data structure for holding tiles to be modified.
var tilesToChange = new Dictionary<TilePos, TileModification>();
// 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<bool> Tile(TilePos position) => ref CollectionsMarshal.GetValueRefOrAddDefault(tilesToChange, position, out _dummy);
// 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;
if (toolMode == ToolMode.Paint) {
// In PAINT mode, only full tiles are ever affected.
// 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 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<Corner>()) {
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";
method = nameof(DoModifyTerrainTexture);
doArgs = [ PackTextureData(tilesChanged) ];
undoArgs = [ PackTextureData(tilesPrevious) ];
} else {
var tilesPrevious = new List<(TilePos, Corners<short>)>();
var tilesChanged = new List<(TilePos, Corners<short>)>();
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;
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;
}
tilesChanged.Add((pos, newHeight));
} else {
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<bool> 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<TilePos, Corners<bool>> tiles)
void UpdateEditToolMesh(IEnumerable<(TilePos, Corners<bool>)> 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<short> 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<short> 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<short>(
var height = new Corners<short>(
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,
});
}
}
}

@ -0,0 +1,11 @@
public static class CollectionExtensions
{
public static TValue GetOrAddNew<TKey, TValue>(
this Dictionary<TKey, TValue> dict, TKey key)
where TValue : new()
{
if (!dict.TryGetValue(key, out var value))
dict.Add(key, value = new());
return value;
}
}
Loading…
Cancel
Save