Implement terrain painting

main
copygirl 2 months ago
parent 9dd2d0794b
commit 6f54e2706e
  1. 276
      level.tscn
  2. 122
      terrain/Terrain+Editing.cs
  3. 2
      terrain/Terrain.cs

@ -1494,6 +1494,38 @@ Vector2i(40, 30): {
"heights": 0.0, "heights": 0.0,
"texture": 1 "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): { Vector2i(41, 19): {
"heights": PackedFloat32Array(0, 0, 3, 3), "heights": PackedFloat32Array(0, 0, 3, 3),
"texture": 2 "texture": 2
@ -1530,6 +1562,38 @@ Vector2i(41, 27): {
"heights": PackedFloat32Array(0, -1, 0, 0), "heights": PackedFloat32Array(0, -1, 0, 0),
"texture": 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): { Vector2i(42, 19): {
"heights": PackedFloat32Array(0, 0, 4.5, 3), "heights": PackedFloat32Array(0, 0, 4.5, 3),
"texture": 2 "texture": 2
@ -1558,6 +1622,38 @@ Vector2i(42, 27): {
"heights": PackedFloat32Array(-0.5, -0.5, 0, 0), "heights": PackedFloat32Array(-0.5, -0.5, 0, 0),
"texture": 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): { Vector2i(43, 19): {
"heights": PackedFloat32Array(0, 0, 4.5, 4.5), "heights": PackedFloat32Array(0, 0, 4.5, 4.5),
"texture": 2 "texture": 2
@ -1584,16 +1680,48 @@ Vector2i(43, 26): {
}, },
Vector2i(43, 27): { Vector2i(43, 27): {
"heights": PackedFloat32Array(1.5, 0, 1, 0), "heights": PackedFloat32Array(1.5, 0, 1, 0),
"texture": 0 "texture": 2
}, },
Vector2i(43, 28): { Vector2i(43, 28): {
"heights": PackedFloat32Array(0, 1, 1, 0), "heights": PackedFloat32Array(0, 1, 1, 0),
"texture": 0 "texture": 2
}, },
Vector2i(43, 29): { Vector2i(43, 29): {
"heights": PackedFloat32Array(0, 1, 0, 0), "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 "texture": 0
}, },
Vector2i(43, 40): {
"heights": PackedFloat32Array(1, 1, 2, 2),
"texture": 0
},
Vector2i(43, 41): {
"heights": 2.0,
"texture": 2
},
Vector2i(44, 19): { Vector2i(44, 19): {
"heights": PackedFloat32Array(0, 0, 3, 4.5), "heights": PackedFloat32Array(0, 0, 3, 4.5),
"texture": 2 "texture": 2
@ -1612,16 +1740,48 @@ Vector2i(44, 22): {
}, },
Vector2i(44, 27): { Vector2i(44, 27): {
"heights": PackedFloat32Array(0, 0, 2, 1), "heights": PackedFloat32Array(0, 0, 2, 1),
"texture": 0 "texture": 2
}, },
Vector2i(44, 28): { Vector2i(44, 28): {
"heights": PackedFloat32Array(2, 4, 5.5, 4.5), "heights": PackedFloat32Array(2, 4, 5.5, 4.5),
"texture": 0 "texture": 2
}, },
Vector2i(44, 29): { Vector2i(44, 29): {
"heights": PackedFloat32Array(1, 2, 0, 0), "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 "texture": 0
}, },
Vector2i(44, 40): {
"heights": PackedFloat32Array(1, 2, 2, 2),
"texture": 2
},
Vector2i(44, 41): {
"heights": 2.0,
"texture": 2
},
Vector2i(45, 19): { Vector2i(45, 19): {
"heights": PackedFloat32Array(0, 0, 0, 3), "heights": PackedFloat32Array(0, 0, 0, 3),
"texture": 2 "texture": 2
@ -1644,27 +1804,123 @@ Vector2i(45, 25): {
}, },
Vector2i(45, 27): { Vector2i(45, 27): {
"heights": 2.0, "heights": 2.0,
"texture": 0 "texture": 2
}, },
Vector2i(45, 28): { Vector2i(45, 28): {
"heights": PackedFloat32Array(3.5, 3.5, 4.5, 4.5), "heights": PackedFloat32Array(3.5, 3.5, 4.5, 4.5),
"texture": 0 "texture": 2
}, },
Vector2i(45, 29): { Vector2i(45, 29): {
"heights": PackedFloat32Array(2, 1, 0, 0), "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): { Vector2i(46, 27): {
"heights": PackedFloat32Array(0, 0, 0, 1), "heights": PackedFloat32Array(0, 0, 0, 1),
"texture": 0 "texture": 2
}, },
Vector2i(46, 28): { Vector2i(46, 28): {
"heights": PackedFloat32Array(1, 0, 0, 1), "heights": PackedFloat32Array(1, 0, 0, 1),
"texture": 0 "texture": 2
}, },
Vector2i(46, 29): { Vector2i(46, 29): {
"heights": PackedFloat32Array(1, 0, 0, 0), "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
} }
} }

@ -48,6 +48,7 @@ public partial class Terrain
var radius = FloorToInt(drawSize / 2.0f); var radius = FloorToInt(drawSize / 2.0f);
// Offset hover tile position by corner. // 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 { if (isEven) hover.Position = hover.Corner switch {
Corner.TopLeft => hover.Position.Offset(0, 0), Corner.TopLeft => hover.Position.Offset(0, 0),
Corner.TopRight => hover.Position.Offset(1, 0), Corner.TopRight => hover.Position.Offset(1, 0),
@ -73,30 +74,51 @@ public partial class Terrain
center.DistanceSquaredTo(tile.ToCenter()) < distanceSqr); 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: Allow click-dragging which doesn't affect already changed tiles / corners.
// TODO: Use ArrayMesh instead of ImmediateMesh. // TODO: Use ArrayMesh instead of ImmediateMesh.
// TODO: Dynamically expand terrain instead of having it be a set size. // 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. // 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>>(); var tilesToChange = new Dictionary<TilePos, Corners<bool>>();
ref Corners<bool> GetTileToChange(TilePos position)
// Don't look at this black magic. The Dictionary type should have this by default I swear! // Don't look at this black magic. The Dictionary type should have this by default I swear!
=> ref CollectionsMarshal.GetValueRefOrAddDefault(tilesToChange, position, out _dummy); // 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);
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. // 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) { if (isConnected) {
var height = GetTile(hover.Position).Height[hover.Corner]; var height = GetTile(hover.Position).Height[hover.Corner];
foreach (var neighbor in GetNeighbors(hover.Position, hover.Corner)) { foreach (var neighbor in GetNeighbors(hover.Position, hover.Corner)) {
var neighborHeight = GetTile(neighbor.Position).Height[neighbor.Corner]; var neighborHeight = GetTile(neighbor.Position).Height[neighbor.Corner];
if (neighborHeight != height) continue; if (neighborHeight != height) continue;
GetTileToChange(neighbor.Position)[neighbor.Corner] = true; Tile(neighbor.Position)[neighbor.Corner] = true;
} }
} }
} else { } else {
var tiles = (toolShape switch { var tiles = (toolShape switch {
ToolShape.Circle => GetTilesInRadius(), ToolShape.Circle => GetTilesInRadius(),
ToolShape.Square => GetTilesInSquare(), ToolShape.Square => GetTilesInSquare(),
@ -105,7 +127,7 @@ public partial class Terrain
// Modify selected tiles themselves. // Modify selected tiles themselves.
foreach (var pos in tiles) foreach (var pos in tiles)
GetTileToChange(pos) = new(true); tilesToChange.Add(pos, new(true));
// If the 'connected_toggle' button is active, move "connected" corners. // 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. // Connected corners are the ones that are at the same height as ones already being moved.
@ -117,22 +139,45 @@ public partial class Terrain
if (tiles.Contains(neighbor.Position)) continue; if (tiles.Contains(neighbor.Position)) continue;
var neighborHeight = GetTile(neighbor.Position).Height[neighbor.Corner]; var neighborHeight = GetTile(neighbor.Position).Height[neighbor.Corner];
if (neighborHeight != height) continue; 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 }) { if (ev is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: true }) {
prevent_default = true; prevent_default = true;
string action;
StringName method;
Variant[] doArgs;
Variant[] undoArgs;
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<float>)>();
var tilesChanged = new List<(TilePos, Corners<float>)>();
const float AdjustHeight = 0.5f; const float AdjustHeight = 0.5f;
var amount = isFlatten ? GetTile(hover.Position).Height[hover.Corner] var amount = isFlatten ? GetTile(hover.Position).Height[hover.Corner]
: isRaise ? AdjustHeight : -AdjustHeight; : isRaise ? AdjustHeight : -AdjustHeight;
var tilesPrevious = new List<(TilePos, Corners<float>)>();
var tilesChanged = new List<(TilePos, Corners<float>)>();
foreach (var (pos, corners) in tilesToChange) { foreach (var (pos, corners) in tilesToChange) {
var tile = GetTile(pos); var tile = GetTile(pos);
tilesPrevious.Add((pos, tile.Height)); tilesPrevious.Add((pos, tile.Height));
@ -145,15 +190,22 @@ public partial class Terrain
tilesChanged.Add((pos, newHeight)); 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) { if (EditorUndoRedo is EditorUndoRedoManager undo) {
var name = "Modify terrain height"; // TODO: Change name depending on tool mode. undo.CreateAction(action, backwardUndoOps: false);
undo.CreateAction(name, backwardUndoOps: false);
undo.AddDoMethod(this, nameof(DoModifyTerrainHeight), Pack(tilesChanged)); undo.AddDoMethod(this, method, doArgs);
undo.AddDoMethod(this, nameof(UpdateMeshAndShape)); undo.AddDoMethod(this, nameof(UpdateMeshAndShape));
undo.AddDoMethod(this, GodotObject.MethodName.NotifyPropertyListChanged); 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, nameof(UpdateMeshAndShape));
undo.AddUndoMethod(this, GodotObject.MethodName.NotifyPropertyListChanged); undo.AddUndoMethod(this, GodotObject.MethodName.NotifyPropertyListChanged);
@ -170,13 +222,22 @@ public partial class Terrain
public void DoModifyTerrainHeight(byte[] data) public void DoModifyTerrainHeight(byte[] data)
{ {
foreach (var (pos, corners) in Unpack(data)) { foreach (var (pos, corners) in UnpackHeightData(data)) {
var tile = GetTile(pos); var tile = GetTile(pos);
tile.Height = corners; tile.Height = corners;
SetTile(pos, tile); 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<TilePos, Corners<bool>> tiles) void UpdateEditToolMesh(Dictionary<TilePos, Corners<bool>> tiles)
{ {
@ -234,7 +295,8 @@ public partial class Terrain
static IEnumerable<(TilePos Position, Corner Corner)> GetNeighbors(TilePos pos, Corner corner) 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)); => _offsetLookup[corner].Select(e => (new TilePos(pos.X + e.X, pos.Y + e.Y), e.Opposite));
static byte[] Pack(IEnumerable<(TilePos Position, Corners<float> Corners)> data)
static byte[] PackHeightData(IEnumerable<(TilePos Position, Corners<float> Corners)> data)
{ {
using var stream = new MemoryStream(); using var stream = new MemoryStream();
using var writer = new BinaryWriter(stream); using var writer = new BinaryWriter(stream);
@ -249,7 +311,7 @@ public partial class Terrain
return stream.ToArray(); return stream.ToArray();
} }
static IEnumerable<(TilePos Position, Corners<float> Corners)> Unpack(byte[] data) static IEnumerable<(TilePos Position, Corners<float> Corners)> UnpackHeightData(byte[] data)
{ {
using var stream = new MemoryStream(data); using var stream = new MemoryStream(data);
using var reader = new BinaryReader(stream); using var reader = new BinaryReader(stream);
@ -262,4 +324,28 @@ public partial class Terrain
yield return (new(x, y), corners); 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);
}
}
} }

@ -142,6 +142,8 @@ public partial class Terrain
corner2, new(0.0f, corner2.Y / TileSize)); corner2, new(0.0f, corner2.Y / TileSize));
break; break;
case (false, false): 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), AddTriangle(corner1, new(1.0f, corner1.Y / TileSize),
corner4, new(1.0f, corner4.Y / TileSize), corner4, new(1.0f, corner4.Y / TileSize),
corner2, new(0.0f, corner2.Y / TileSize)); corner2, new(0.0f, corner2.Y / TileSize));

Loading…
Cancel
Save