- Moved more code into editor plugin (GDScript) - Moved remaining code back to Terrain script (C#) - Editing controls themselves are now fully GDScript - Plugin (GDScript) just calls `Terrain.EditorInput` (C#) - Made some more changes and additions where sensiblemain
@ -1,5 +1,8 @@ |
|||||||
public static class SideAndCornerExtensions |
public static class GodotExtensions |
||||||
{ |
{ |
||||||
|
public static Vector2I RoundToVector2I(this Vector2 vector) |
||||||
|
=> new(RoundToInt(vector.X), RoundToInt(vector.Y)); |
||||||
|
|
||||||
public static (Corner, Corner) GetCorners(this Side side) |
public static (Corner, Corner) GetCorners(this Side side) |
||||||
=> side switch { |
=> side switch { |
||||||
Side.Left => (Corner.TopLeft, Corner.BottomLeft), |
Side.Left => (Corner.TopLeft, Corner.BottomLeft), |
Before Width: | Height: | Size: 193 B After Width: | Height: | Size: 193 B |
Before Width: | Height: | Size: 139 B After Width: | Height: | Size: 139 B |
Before Width: | Height: | Size: 133 B After Width: | Height: | Size: 133 B |
Before Width: | Height: | Size: 129 B After Width: | Height: | Size: 129 B |
Before Width: | Height: | Size: 124 B After Width: | Height: | Size: 124 B |
Before Width: | Height: | Size: 113 B After Width: | Height: | Size: 113 B |
Before Width: | Height: | Size: 168 B After Width: | Height: | Size: 168 B |
Before Width: | Height: | Size: 135 B After Width: | Height: | Size: 135 B |
Before Width: | Height: | Size: 149 B After Width: | Height: | Size: 149 B |
Before Width: | Height: | Size: 169 B After Width: | Height: | Size: 169 B |
Before Width: | Height: | Size: 147 B After Width: | Height: | Size: 147 B |
@ -0,0 +1,13 @@ |
|||||||
|
@tool |
||||||
|
extends Button |
||||||
|
|
||||||
|
@export var on_texture : Texture2D |
||||||
|
@export var off_texture : Texture2D |
||||||
|
@export var modifier_key : Key |
||||||
|
|
||||||
|
func _toggled(on: bool) -> void: |
||||||
|
icon = on_texture if on else off_texture |
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void: |
||||||
|
if event is InputEventKey and event.keycode == modifier_key: |
||||||
|
button_pressed = !button_pressed |
@ -0,0 +1,89 @@ |
|||||||
|
@tool |
||||||
|
class_name TerrainEditingControls |
||||||
|
extends VBoxContainer |
||||||
|
|
||||||
|
enum ToolMode { HEIGHT, FLATTEN, PAINT } |
||||||
|
enum ToolShape { CORNER, CIRCLE, SQUARE } |
||||||
|
|
||||||
|
@onready var tool_mode_buttons : Array[Button] = [ $Height, $Flatten, $Paint ] |
||||||
|
@onready var tool_shape_buttons : Array[Button] = [ $Corner, $Circle, $Square ] |
||||||
|
@onready var paint_texture_buttons : Array[Button] = [ $Grass, $Dirt, $Rock, $Sand ] |
||||||
|
|
||||||
|
@onready var draw_size_label : Label = $SizeLabel |
||||||
|
@onready var draw_size_slider : Slider = $SizeSlider |
||||||
|
@onready var raise_lower_toggle : Button = $RaiseLower |
||||||
|
@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") |
||||||
|
|
||||||
|
|
||||||
|
## Gets or sets the currently active tool mode (HEIGHT, FLATTEN, PAINT). |
||||||
|
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). |
||||||
|
var tool_shape : ToolShape: |
||||||
|
get: return index_of_pressed(tool_shape_buttons) |
||||||
|
set(value): set_pressed(tool_shape_buttons, value); tool_shape_changed() |
||||||
|
## Gets or sets the currently selected texture to paint with. |
||||||
|
var texture : int: |
||||||
|
get: return index_of_pressed(paint_texture_buttons) |
||||||
|
set(value): set_pressed(paint_texture_buttons, value) |
||||||
|
## Gets or sets the current draw size for CIRCLE or SQUARE shapes. |
||||||
|
var draw_size : int: |
||||||
|
get: return roundi(-draw_size_slider.value) |
||||||
|
set(value): draw_size_slider.value = -value |
||||||
|
|
||||||
|
## Gets whether the raise/lower button is currently active. |
||||||
|
var is_raise : bool: |
||||||
|
get: return raise_lower_toggle.button_pressed |
||||||
|
## Gets whether the raise/lower button is currently active. |
||||||
|
var is_connected : bool: |
||||||
|
get: return connected_toggle.button_pressed |
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void: |
||||||
|
# Update 'tool_mode', 'tool_shape' or 'texture' when any of the buttons are pressed. |
||||||
|
for i in len(tool_mode_buttons ): tool_mode_buttons [i].pressed.connect(func(): tool_mode = i) |
||||||
|
for i in len(tool_shape_buttons ): tool_shape_buttons [i].pressed.connect(func(): tool_shape = i) |
||||||
|
for i in len(paint_texture_buttons): paint_texture_buttons[i].pressed.connect(func(): texture = i) |
||||||
|
|
||||||
|
# Update 'draw_size_label' whenever the slider changes. |
||||||
|
draw_size_slider.value_changed.connect(func(_value): |
||||||
|
draw_size_label.text = str(draw_size)) |
||||||
|
|
||||||
|
|
||||||
|
func tool_mode_changed() -> void: |
||||||
|
var is_height := (tool_mode == ToolMode.HEIGHT) |
||||||
|
var is_paint := (tool_mode == ToolMode.PAINT) |
||||||
|
|
||||||
|
# In 'PAINT' 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 |
||||||
|
|
||||||
|
for button in paint_texture_buttons: button.disabled = !is_paint |
||||||
|
raise_lower_toggle.disabled = !is_height |
||||||
|
connected_toggle.disabled = is_paint |
||||||
|
|
||||||
|
func tool_shape_changed() -> void: |
||||||
|
var is_corner := (tool_shape == ToolShape.CORNER) |
||||||
|
draw_size_slider.editable = !is_corner; |
||||||
|
|
||||||
|
|
||||||
|
## Returns the index of the first pressed (toggled on) button. |
||||||
|
func index_of_pressed(buttons: Array[Button]) -> int: |
||||||
|
for i in len(buttons): |
||||||
|
var button := buttons[i] |
||||||
|
if button.button_pressed: |
||||||
|
return i |
||||||
|
return 0 |
||||||
|
|
||||||
|
## Sets the pressed state (toggled on) of the button |
||||||
|
## with the specified index, unsetting all others. |
||||||
|
func set_pressed(buttons: Array[Button], value: int) -> void: |
||||||
|
for i in len(buttons): |
||||||
|
var button := buttons[i] |
||||||
|
var is_pressed := (value == i) |
||||||
|
button.button_pressed = is_pressed |
||||||
|
button.flat = !is_pressed |
@ -0,0 +1,245 @@ |
|||||||
|
using System.IO; |
||||||
|
|
||||||
|
public partial class Terrain |
||||||
|
{ |
||||||
|
enum ToolMode { Height, Flatten, Paint } |
||||||
|
enum ToolShape { Corner, Circle, Square } |
||||||
|
|
||||||
|
// Set by the terrain editing plugin. |
||||||
|
public EditorUndoRedoManager EditorUndoRedo { get; set; } |
||||||
|
|
||||||
|
Material _editToolMaterial; |
||||||
|
public override void _EnterTree() |
||||||
|
{ |
||||||
|
_editToolMaterial = new StandardMaterial3D { |
||||||
|
AlbedoColor = Colors.Blue, |
||||||
|
ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded, |
||||||
|
DepthDrawMode = BaseMaterial3D.DepthDrawModeEnum.Disabled, |
||||||
|
NoDepthTest = true, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
public bool EditorInput(InputEventMouse ev, Vector3 position, Control controls) |
||||||
|
{ |
||||||
|
var prevent_default = false; |
||||||
|
|
||||||
|
var toolMode = (ToolMode)(int)controls.Get("tool_mode"); |
||||||
|
var toolShape = (ToolShape)(int)controls.Get("tool_shape"); |
||||||
|
var texture = (int)controls.Get("texture"); |
||||||
|
var drawSize = (int)controls.Get("draw_size"); |
||||||
|
|
||||||
|
var isRaise = (bool)controls.Get("is_raise"); |
||||||
|
var isConnected = (bool)controls.Get("is_connected"); |
||||||
|
var isFlatten = toolMode == ToolMode.Flatten; |
||||||
|
var isCorner = toolShape == ToolShape.Corner; |
||||||
|
|
||||||
|
var hover = ToTilePos(position); |
||||||
|
if (isCorner) drawSize = 1; |
||||||
|
var isEven = (drawSize % 2) == 0; |
||||||
|
var radius = FloorToInt(drawSize / 2.0f); |
||||||
|
|
||||||
|
// Offset hover tile position by corner. |
||||||
|
if (isEven) hover.Position = hover.Corner switch { |
||||||
|
Corner.TopLeft => hover.Position.Offset(0, 0), |
||||||
|
Corner.TopRight => hover.Position.Offset(1, 0), |
||||||
|
Corner.BottomRight => hover.Position.Offset(1, 1), |
||||||
|
Corner.BottomLeft => hover.Position.Offset(0, 1), |
||||||
|
_ => throw new InvalidOperationException(), |
||||||
|
}; |
||||||
|
|
||||||
|
IEnumerable<TilePos> GetTilesInSquare() { |
||||||
|
var min = hover.Position.Offset(-radius, -radius); |
||||||
|
var max = hover.Position.Offset(+radius, +radius); |
||||||
|
if (isEven) max = max.Offset(-1, -1); |
||||||
|
for (var x = min.X; x <= max.X; x++) |
||||||
|
for (var y = min.Y; y <= max.Y; y++) |
||||||
|
yield return new(x, y); |
||||||
|
} |
||||||
|
|
||||||
|
IEnumerable<TilePos> GetTilesInRadius() { |
||||||
|
var center = isEven ? hover.Position.ToVector2I() |
||||||
|
: hover.Position.ToCenter(); |
||||||
|
var distanceSqr = Pow(radius + 0.25f * (isEven ? -1 : 1), 2); |
||||||
|
return GetTilesInSquare().Where(tile => |
||||||
|
center.DistanceSquaredTo(tile.ToCenter()) < distanceSqr); |
||||||
|
} |
||||||
|
|
||||||
|
var tiles = (toolShape switch { |
||||||
|
ToolShape.Corner => [ hover.Position ], |
||||||
|
ToolShape.Circle => GetTilesInRadius(), |
||||||
|
ToolShape.Square => GetTilesInSquare(), |
||||||
|
_ => throw new InvalidOperationException(), |
||||||
|
}).ToHashSet(); |
||||||
|
|
||||||
|
// TODO: Handle different tool modes, such as painting. |
||||||
|
// TODO: Finally allow editing single corners. |
||||||
|
// TODO: Allow click-dragging which doesn't affect already changed tiles / corners. |
||||||
|
// TODO: Make mesh generation generate vertical walls between disconnected corners. |
||||||
|
// TODO: Use ArrayMesh instead of ImmediateMesh. |
||||||
|
// TODO: Dynamically expand terrain instead of having it be a set size. |
||||||
|
|
||||||
|
// Raise / lower the terrain when left mouse button is pressed. |
||||||
|
if (ev is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: true }) { |
||||||
|
prevent_default = true; |
||||||
|
|
||||||
|
var cornersToChange = new HashSet<(TilePos Position, Corner Corner)>(); |
||||||
|
|
||||||
|
// Raise selected tiles themselves. |
||||||
|
foreach (var pos in tiles) |
||||||
|
foreach (var corner2 in Enum.GetValues<Corner>()) |
||||||
|
cornersToChange.Add((pos, corner2)); |
||||||
|
|
||||||
|
if (isConnected) { |
||||||
|
// 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. |
||||||
|
foreach (var pos in tiles) { |
||||||
|
var tile = GetTile(pos); |
||||||
|
foreach (var corner in Enum.GetValues<Corner>()) { |
||||||
|
var height = tile.Height[corner]; |
||||||
|
foreach (var (neighborPos, neighborCorner) in GetNeighbors(pos, corner)) { |
||||||
|
if (tiles.Contains(neighborPos)) continue; |
||||||
|
var neighborHeight = GetTile(neighborPos).Height[neighborCorner]; |
||||||
|
if (neighborHeight == height) cornersToChange.Add((neighborPos, neighborCorner)); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const float AdjustHeight = 0.5f; |
||||||
|
var amount = isFlatten ? GetTile(hover.Position).Height[hover.Corner] |
||||||
|
: isRaise ? AdjustHeight : -AdjustHeight; |
||||||
|
|
||||||
|
var tilesPrevious = new List<(TilePos, Corners<float>)>(); |
||||||
|
var tilesChanged = new List<(TilePos, Corners<float>)>(); |
||||||
|
foreach (var group in cornersToChange.GroupBy(c => c.Position, c => c.Corner)) { |
||||||
|
var pos = group.Key; |
||||||
|
var tile = GetTile(pos); |
||||||
|
tilesPrevious.Add((pos, tile.Height)); |
||||||
|
|
||||||
|
var newHeight = tile.Height; |
||||||
|
foreach (var corner in group) { |
||||||
|
if (isFlatten) newHeight[corner] = amount; |
||||||
|
else newHeight[corner] += amount; |
||||||
|
} |
||||||
|
tilesChanged.Add((pos, newHeight)); |
||||||
|
} |
||||||
|
|
||||||
|
if (EditorUndoRedo is EditorUndoRedoManager undo) { |
||||||
|
var name = "Modify terrain height"; // TODO: Change name depending on tool mode. |
||||||
|
undo.CreateAction(name, backwardUndoOps: false); |
||||||
|
|
||||||
|
undo.AddDoMethod(this, nameof(DoModifyTerrainHeight), Pack(tilesChanged)); |
||||||
|
undo.AddDoMethod(this, nameof(UpdateMeshAndShape)); |
||||||
|
undo.AddDoMethod(this, GodotObject.MethodName.NotifyPropertyListChanged); |
||||||
|
|
||||||
|
undo.AddUndoMethod(this, nameof(DoModifyTerrainHeight), Pack(tilesPrevious)); |
||||||
|
undo.AddUndoMethod(this, nameof(UpdateMeshAndShape)); |
||||||
|
undo.AddUndoMethod(this, GodotObject.MethodName.NotifyPropertyListChanged); |
||||||
|
|
||||||
|
undo.CommitAction(true); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
UpdateEditToolMesh(tiles); |
||||||
|
return prevent_default; |
||||||
|
} |
||||||
|
|
||||||
|
public void EditorUnfocus() |
||||||
|
=> ClearEditToolMesh(); |
||||||
|
|
||||||
|
public void DoModifyTerrainHeight(byte[] data) |
||||||
|
{ |
||||||
|
foreach (var (pos, corners) in Unpack(data)) { |
||||||
|
var tile = GetTile(pos); |
||||||
|
tile.Height = corners; |
||||||
|
SetTile(pos, tile); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
void UpdateEditToolMesh(IEnumerable<TilePos> tiles) |
||||||
|
{ |
||||||
|
var mesh = GetOrCreateMesh("EditToolMesh"); |
||||||
|
mesh.ClearSurfaces(); |
||||||
|
mesh.SurfaceBegin(Mesh.PrimitiveType.Lines); |
||||||
|
|
||||||
|
void AddLine(Vector3 start, Vector3 end) { |
||||||
|
mesh.SurfaceAddVertex(start); |
||||||
|
mesh.SurfaceAddVertex(end); |
||||||
|
} |
||||||
|
|
||||||
|
void AddQuad(Vector3 topLeft , Vector3 topRight , |
||||||
|
Vector3 bottomRight, Vector3 bottomLeft) { |
||||||
|
AddLine(topLeft , topRight ); |
||||||
|
AddLine(topRight , bottomRight); |
||||||
|
AddLine(bottomRight, bottomLeft ); |
||||||
|
AddLine(bottomLeft , topLeft ); |
||||||
|
} |
||||||
|
|
||||||
|
foreach (var tile in tiles) { |
||||||
|
var (topLeft, topRight, bottomRight, bottomLeft) |
||||||
|
= GetTileCornerPositions(tile); |
||||||
|
AddQuad(topLeft, topRight, bottomRight, bottomLeft); |
||||||
|
} |
||||||
|
|
||||||
|
mesh.SurfaceEnd(); |
||||||
|
mesh.SurfaceSetMaterial(0, _editToolMaterial); |
||||||
|
} |
||||||
|
|
||||||
|
void ClearEditToolMesh() |
||||||
|
=> GetNodeOrNull("EditToolMesh")?.QueueFree(); |
||||||
|
|
||||||
|
|
||||||
|
(TilePos Position, Corner Corner) ToTilePos(Vector3 position) |
||||||
|
{ |
||||||
|
var local = ToLocal(position); |
||||||
|
var coord = new Vector2(local.X, local.Z) / TileSize + (Size + Vector2.One) / 2; |
||||||
|
var pos = TilePos.From(coord); |
||||||
|
var corner = coord.PosMod(1).RoundToVector2I() switch { |
||||||
|
(0, 0) => Corner.TopLeft, |
||||||
|
(1, 0) => Corner.TopRight, |
||||||
|
(1, 1) => Corner.BottomRight, |
||||||
|
(0, 1) => Corner.BottomLeft, |
||||||
|
_ => throw new InvalidOperationException(), |
||||||
|
}; |
||||||
|
return (pos, corner); |
||||||
|
} |
||||||
|
|
||||||
|
static readonly Dictionary<Corner, (int X, int Y, Corner Opposite)[]> _offsetLookup = new(){ |
||||||
|
[Corner.TopLeft ] = [(-1, -1, Corner.BottomRight), (-1, 0, Corner.TopRight ), (0, -1, Corner.BottomLeft )], |
||||||
|
[Corner.TopRight ] = [(+1, -1, Corner.BottomLeft ), (+1, 0, Corner.TopLeft ), (0, -1, Corner.BottomRight)], |
||||||
|
[Corner.BottomRight] = [(+1, +1, Corner.TopLeft ), (+1, 0, Corner.BottomLeft ), (0, +1, Corner.TopRight )], |
||||||
|
[Corner.BottomLeft ] = [(-1, +1, Corner.TopRight ), (-1, 0, Corner.BottomRight), (0, +1, Corner.TopLeft )], |
||||||
|
}; |
||||||
|
static IEnumerable<(TilePos, 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<float> Corners)> data) |
||||||
|
{ |
||||||
|
using var stream = new MemoryStream(); |
||||||
|
using var writer = new BinaryWriter(stream); |
||||||
|
foreach (var (pos, corners) in data) { |
||||||
|
writer.Write(pos.X); |
||||||
|
writer.Write(pos.Y); |
||||||
|
writer.Write(corners.TopLeft); |
||||||
|
writer.Write(corners.TopRight); |
||||||
|
writer.Write(corners.BottomRight); |
||||||
|
writer.Write(corners.BottomLeft); |
||||||
|
} |
||||||
|
return stream.ToArray(); |
||||||
|
} |
||||||
|
|
||||||
|
static IEnumerable<(TilePos Position, Corners<float> Corners)> 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<float>( |
||||||
|
reader.ReadSingle(), reader.ReadSingle(), |
||||||
|
reader.ReadSingle(), reader.ReadSingle()); |
||||||
|
yield return (new(x, y), corners); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -1,17 +0,0 @@ |
|||||||
[Tool] |
|
||||||
public partial class ModifierToggleButton : Button |
|
||||||
{ |
|
||||||
[Export] public Texture2D OnTexture { get; set; } |
|
||||||
[Export] public Texture2D OffTexture { get; set; } |
|
||||||
[Export] public Key ModifierKey { get; set; } |
|
||||||
|
|
||||||
|
|
||||||
public override void _Ready() |
|
||||||
=> Toggled += (on) => Icon = on ? OnTexture : OffTexture; |
|
||||||
|
|
||||||
public override void _Input(InputEvent ev) |
|
||||||
{ |
|
||||||
if ((ev is InputEventKey { Keycode: var key }) && (key == ModifierKey)) |
|
||||||
ButtonPressed = !ButtonPressed; |
|
||||||
} |
|
||||||
} |
|
@ -1,279 +0,0 @@ |
|||||||
using System.IO; |
|
||||||
|
|
||||||
public partial class TerrainEditingControls |
|
||||||
{ |
|
||||||
// Set by the terrain editing plugin. |
|
||||||
public EditorUndoRedoManager EditorUndoRedo { get; set; } |
|
||||||
|
|
||||||
Terrain _currentTerrain = null; |
|
||||||
|
|
||||||
Material _editToolMaterial; |
|
||||||
public override void _EnterTree() |
|
||||||
{ |
|
||||||
_editToolMaterial = new StandardMaterial3D { |
|
||||||
AlbedoColor = Colors.Blue, |
|
||||||
ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded, |
|
||||||
DepthDrawMode = BaseMaterial3D.DepthDrawModeEnum.Disabled, |
|
||||||
NoDepthTest = true, |
|
||||||
}; |
|
||||||
} |
|
||||||
|
|
||||||
public override void _ExitTree() |
|
||||||
=> ClearEditToolMesh(); |
|
||||||
|
|
||||||
public override void _Input(InputEvent ev) |
|
||||||
{ |
|
||||||
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 container = viewport.GetParent<SubViewportContainer>(); |
|
||||||
ev = ev.XformedBy(container.GetGlobalTransform().AffineInverse()); |
|
||||||
if (ev is InputEventMouse m) m.GlobalPosition = m.Position; |
|
||||||
} |
|
||||||
|
|
||||||
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(), |
|
||||||
}; |
|
||||||
|
|
||||||
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); |
|
||||||
} |
|
||||||
|
|
||||||
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); |
|
||||||
} |
|
||||||
|
|
||||||
var tiles = (ToolShape switch { |
|
||||||
// TODO: Edit corner, not full tile. |
|
||||||
ToolShape.Corner => [tile], |
|
||||||
ToolShape.Circle => GetTilesInRadius(), |
|
||||||
ToolShape.Square => GetTilesInSquare(), |
|
||||||
_ => throw new InvalidOperationException(), |
|
||||||
}).ToHashSet(); |
|
||||||
|
|
||||||
// TODO: Handle different tool modes, such as flatten and painting. |
|
||||||
// TODO: Allow click-dragging which doesn't affect already changed tiles / corners. |
|
||||||
// TODO: Make mesh generation generate vertical walls between disconnected corners. |
|
||||||
// TODO: Use ArrayMesh instead of ImmediateMesh. |
|
||||||
|
|
||||||
// Raise / lower the terrain if left / right mouse button is pressed. |
|
||||||
if (mouse is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: true }) { |
|
||||||
GetViewport().SetInputAsHandled(); |
|
||||||
|
|
||||||
var cornersToChange = new HashSet<(TilePos Position, Corner Corner)>(); |
|
||||||
|
|
||||||
// Raise selected tiles themselves. |
|
||||||
foreach (var pos in tiles) |
|
||||||
foreach (var corner2 in Enum.GetValues<Corner>()) |
|
||||||
cornersToChange.Add((pos, corner2)); |
|
||||||
|
|
||||||
// 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. |
|
||||||
if (ConnectedToggle.ButtonPressed) { |
|
||||||
foreach (var pos in tiles) { |
|
||||||
var tile2 = terrain.GetTile(pos); |
|
||||||
foreach (var corner2 in Enum.GetValues<Corner>()) { |
|
||||||
var height = tile2.Height[corner2]; |
|
||||||
foreach (var (neighborPos, neighborCorner) in GetNeighbors(pos, corner2)) { |
|
||||||
if (tiles.Contains(neighborPos)) continue; |
|
||||||
var neighborHeight = terrain.GetTile(neighborPos).Height[neighborCorner]; |
|
||||||
if (neighborHeight == height) cornersToChange.Add((neighborPos, neighborCorner)); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
var isFlatten = ToolMode == ToolMode.Flatten; |
|
||||||
var isRaise = RaiseLowerToggle.ButtonPressed; |
|
||||||
|
|
||||||
const float AdjustHeight = 0.5f; |
|
||||||
var amount = isFlatten ? terrain.GetTile(tile).Height[corner] |
|
||||||
: isRaise ? AdjustHeight : -AdjustHeight; |
|
||||||
|
|
||||||
var tilesPrevious = new List<(TilePos, Corners<float>)>(); |
|
||||||
var tilesChanged = new List<(TilePos, Corners<float>)>(); |
|
||||||
foreach (var group in cornersToChange.GroupBy(c => c.Position, c => c.Corner)) { |
|
||||||
var pos = group.Key; |
|
||||||
var tile2 = terrain.GetTile(pos); |
|
||||||
tilesPrevious.Add((pos, tile2.Height)); |
|
||||||
|
|
||||||
var newHeight = tile2.Height; |
|
||||||
foreach (var corner2 in group) { |
|
||||||
if (isFlatten) newHeight[corner2] = amount; |
|
||||||
else newHeight[corner2] += amount; |
|
||||||
} |
|
||||||
tilesChanged.Add((pos, newHeight)); |
|
||||||
} |
|
||||||
|
|
||||||
if (EditorUndoRedo is EditorUndoRedoManager undo) { |
|
||||||
var name = "Modify terrain"; // TODO: Change name depending on tool mode. |
|
||||||
undo.CreateAction(name, customContext: terrain, backwardUndoOps: false); |
|
||||||
|
|
||||||
undo.AddDoMethod(this, nameof(TerrainModifyHeight), terrain, Pack(tilesChanged)); |
|
||||||
undo.AddDoMethod(terrain, GodotObject.MethodName.NotifyPropertyListChanged); |
|
||||||
undo.AddDoMethod(terrain, nameof(Terrain.UpdateMeshAndShape)); |
|
||||||
|
|
||||||
undo.AddUndoMethod(this, nameof(TerrainModifyHeight), terrain, Pack(tilesPrevious)); |
|
||||||
undo.AddUndoMethod(terrain, GodotObject.MethodName.NotifyPropertyListChanged); |
|
||||||
undo.AddUndoMethod(terrain, nameof(Terrain.UpdateMeshAndShape)); |
|
||||||
|
|
||||||
undo.CommitAction(true); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
UpdateEditToolMesh(terrain, tiles); |
|
||||||
} else { |
|
||||||
ClearEditToolMesh(); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
public void TerrainModifyHeight(Terrain terrain, byte[] data) |
|
||||||
{ |
|
||||||
foreach (var (pos, corners) in Unpack(data)) { |
|
||||||
var tile = terrain.GetTile(pos); |
|
||||||
tile.Height = corners; |
|
||||||
terrain.SetTile(pos, tile); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
static byte[] Pack(IEnumerable<(TilePos Position, Corners<float> Corners)> data) { |
|
||||||
using var stream = new MemoryStream(); |
|
||||||
using var writer = new BinaryWriter(stream); |
|
||||||
foreach (var (pos, corners) in data) { |
|
||||||
writer.Write(pos.X); |
|
||||||
writer.Write(pos.Y); |
|
||||||
writer.Write(corners.TopLeft); |
|
||||||
writer.Write(corners.TopRight); |
|
||||||
writer.Write(corners.BottomRight); |
|
||||||
writer.Write(corners.BottomLeft); |
|
||||||
} |
|
||||||
return stream.ToArray(); |
|
||||||
} |
|
||||||
|
|
||||||
static IEnumerable<(TilePos Position, Corners<float> Corners)> 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<float>( |
|
||||||
reader.ReadSingle(), reader.ReadSingle(), |
|
||||||
reader.ReadSingle(), reader.ReadSingle()); |
|
||||||
yield return (new(x, y), corners); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
void UpdateEditToolMesh(Terrain terrain, IEnumerable<TilePos> tiles) |
|
||||||
{ |
|
||||||
if (terrain != _currentTerrain) ClearEditToolMesh(); |
|
||||||
_currentTerrain = terrain; |
|
||||||
|
|
||||||
var mesh = terrain.GetOrCreateMesh("EditToolMesh"); |
|
||||||
mesh.ClearSurfaces(); |
|
||||||
mesh.SurfaceBegin(Mesh.PrimitiveType.Lines); |
|
||||||
|
|
||||||
void AddLine(Vector3 start, Vector3 end) { |
|
||||||
mesh.SurfaceAddVertex(start); |
|
||||||
mesh.SurfaceAddVertex(end); |
|
||||||
} |
|
||||||
|
|
||||||
void AddQuad(Vector3 topLeft , Vector3 topRight, |
|
||||||
Vector3 bottomRight, Vector3 bottomLeft) { |
|
||||||
AddLine(topLeft , topRight ); |
|
||||||
AddLine(topRight , bottomRight); |
|
||||||
AddLine(bottomRight, bottomLeft ); |
|
||||||
AddLine(bottomLeft , topLeft ); |
|
||||||
} |
|
||||||
|
|
||||||
foreach (var tile in tiles) { |
|
||||||
var (topLeft, topRight, bottomRight, bottomLeft) |
|
||||||
= terrain.GetTileCornerPositions(tile); |
|
||||||
AddQuad(topLeft, topRight, bottomRight, bottomLeft); |
|
||||||
} |
|
||||||
|
|
||||||
mesh.SurfaceEnd(); |
|
||||||
mesh.SurfaceSetMaterial(0, _editToolMaterial); |
|
||||||
} |
|
||||||
|
|
||||||
void ClearEditToolMesh() |
|
||||||
=> _currentTerrain?.GetNodeOrNull("EditToolMesh")?.QueueFree(); |
|
||||||
|
|
||||||
(Terrain Terrain, Vector3 Position)? RayCastTerrain(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"); |
|
||||||
return (collider is Terrain terrain) |
|
||||||
? (terrain, (Vector3)result["position"]) |
|
||||||
: null; |
|
||||||
} |
|
||||||
|
|
||||||
static (TilePos, Corner) FindClosestTile(Terrain terrain, Vector3 position) |
|
||||||
{ |
|
||||||
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); |
|
||||||
} |
|
||||||
|
|
||||||
static readonly Dictionary<Corner, (int X, int Y, Corner Opposite)[]> _offsetLookup = new(){ |
|
||||||
[Corner.TopLeft ] = [(-1, -1, Corner.BottomRight), (-1, 0, Corner.TopRight ), (0, -1, Corner.BottomLeft )], |
|
||||||
[Corner.TopRight ] = [(+1, -1, Corner.BottomLeft ), (+1, 0, Corner.TopLeft ), (0, -1, Corner.BottomRight)], |
|
||||||
[Corner.BottomRight] = [(+1, +1, Corner.TopLeft ), (+1, 0, Corner.BottomLeft ), (0, +1, Corner.TopRight )], |
|
||||||
[Corner.BottomLeft ] = [(-1, +1, Corner.TopRight ), (-1, 0, Corner.BottomRight), (0, +1, Corner.TopLeft )], |
|
||||||
}; |
|
||||||
static IEnumerable<(TilePos, Corner)> GetNeighbors(TilePos pos, Corner corner) |
|
||||||
=> _offsetLookup[corner].Select(e => (new TilePos(pos.X + e.X, pos.Y + e.Y), e.Opposite)); |
|
||||||
} |
|
@ -1,118 +0,0 @@ |
|||||||
[Tool] |
|
||||||
public partial class TerrainEditingControls |
|
||||||
: VBoxContainer |
|
||||||
{ |
|
||||||
public (ToolMode , Button)[] ToolModeButtons { get; private set; } |
|
||||||
public (ToolShape, Button)[] ToolShapeButtons { get; private set; } |
|
||||||
public Slider DrawSizeSlider { get; private set; } |
|
||||||
public Button[] PaintTextureButtons { get; private set; } |
|
||||||
|
|
||||||
public Button RaiseLowerToggle { get; private set; } |
|
||||||
public Button ConnectedToggle { get; private set; } |
|
||||||
|
|
||||||
public ToolMode ToolMode { get => GetToolMode (); set => SetToolMode (value); } |
|
||||||
public ToolShape ToolShape { get => GetToolShape(); set => SetToolShape(value); } |
|
||||||
public int DrawSize { get => GetDrawSize (); set => SetDrawSize (value); } |
|
||||||
public int Texture { get => GetTexture (); set => SetTexture (value); } |
|
||||||
|
|
||||||
[Export] public Texture2D CornerTextureNormal { get; set; } |
|
||||||
[Export] public Texture2D CornerTexturePaint { get; set; } |
|
||||||
|
|
||||||
public override void _Ready() |
|
||||||
{ |
|
||||||
ToolModeButtons = [ |
|
||||||
(ToolMode.Height , GetNode<Button>("Height" )), |
|
||||||
(ToolMode.Flatten, GetNode<Button>("Flatten")), |
|
||||||
(ToolMode.Paint , GetNode<Button>("Paint" )), |
|
||||||
]; |
|
||||||
ToolShapeButtons = [ |
|
||||||
(ToolShape.Corner, GetNode<Button>("Corner")), |
|
||||||
(ToolShape.Circle, GetNode<Button>("Circle")), |
|
||||||
(ToolShape.Square, GetNode<Button>("Square")), |
|
||||||
]; |
|
||||||
PaintTextureButtons = [ |
|
||||||
GetNode<Button>("Grass"), |
|
||||||
GetNode<Button>("Dirt"), |
|
||||||
GetNode<Button>("Rock"), |
|
||||||
GetNode<Button>("Sand"), |
|
||||||
]; |
|
||||||
|
|
||||||
foreach (var (mode, button) in ToolModeButtons) |
|
||||||
button.Pressed += () => SetToolMode(mode); |
|
||||||
foreach (var (shape, button) in ToolShapeButtons) |
|
||||||
button.Pressed += () => SetToolShape(shape); |
|
||||||
foreach (var (i, button) in PaintTextureButtons.Select((b, i) => (i, b))) |
|
||||||
button.Pressed += () => SetTexture(i + 1); |
|
||||||
|
|
||||||
var drawSizeLabel = GetNode<Label> ("SizeLabel"); |
|
||||||
DrawSizeSlider = GetNode<Slider>("SizeSlider"); |
|
||||||
DrawSizeSlider.ValueChanged += (_) => drawSizeLabel.Text = $"{DrawSize}"; |
|
||||||
|
|
||||||
RaiseLowerToggle = GetNode<Button>("RaiseLower"); |
|
||||||
ConnectedToggle = GetNode<Button>("Connected"); |
|
||||||
} |
|
||||||
|
|
||||||
ToolMode GetToolMode() |
|
||||||
=> ToolModeButtons?.First(x => x.Item2.ButtonPressed).Item1 ?? ToolMode.Height; |
|
||||||
void SetToolMode(ToolMode value) |
|
||||||
{ |
|
||||||
if (ToolModeButtons == null) return; |
|
||||||
foreach (var (mode, button) in ToolModeButtons) |
|
||||||
button.Flat = !(button.ButtonPressed = value == mode); |
|
||||||
foreach (var button in PaintTextureButtons) |
|
||||||
button.Disabled = value != ToolMode.Paint; |
|
||||||
|
|
||||||
// In paint mode, `ToolShape.Corner` affects a single tile regardless |
|
||||||
// of `DrawSize`. This changes the button's icon to communicate that. |
|
||||||
ToolShapeButtons[0].Item2.Icon = (value != ToolMode.Paint) |
|
||||||
? CornerTextureNormal : CornerTexturePaint; |
|
||||||
|
|
||||||
RaiseLowerToggle.Disabled = value is ToolMode.Flatten or ToolMode.Paint; |
|
||||||
ConnectedToggle.Disabled = value is ToolMode.Paint; |
|
||||||
} |
|
||||||
|
|
||||||
ToolShape GetToolShape() |
|
||||||
=> ToolShapeButtons?.First(x => x.Item2.ButtonPressed).Item1 ?? ToolShape.Circle; |
|
||||||
void SetToolShape(ToolShape value) |
|
||||||
{ |
|
||||||
if (ToolShapeButtons == null) return; |
|
||||||
foreach (var (shape, button) in ToolShapeButtons) |
|
||||||
button.Flat = !(button.ButtonPressed = value == shape); |
|
||||||
DrawSizeSlider.Editable = value != ToolShape.Corner; |
|
||||||
} |
|
||||||
|
|
||||||
int GetDrawSize() |
|
||||||
=> RoundToInt(-DrawSizeSlider?.Value ?? 1); |
|
||||||
void SetDrawSize(int value) |
|
||||||
{ |
|
||||||
if (DrawSizeSlider == null) return; |
|
||||||
DrawSizeSlider.Value = -value; |
|
||||||
} |
|
||||||
|
|
||||||
int GetTexture() |
|
||||||
=> PaintTextureButtons? |
|
||||||
.Select((b, i) => (Index: i + 1, Button: b)) |
|
||||||
.First(x => x.Button.ButtonPressed).Index ?? 1; |
|
||||||
void SetTexture(int value) |
|
||||||
{ |
|
||||||
if (PaintTextureButtons == null) return; |
|
||||||
if ((value < 1) || (value > PaintTextureButtons.Length)) |
|
||||||
throw new ArgumentOutOfRangeException(nameof(value)); |
|
||||||
foreach (var (index, button) in PaintTextureButtons.Select((b, i) => (i, b))) |
|
||||||
button.Flat = !(button.ButtonPressed = value == index + 1); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
public enum ToolMode |
|
||||||
{ |
|
||||||
Height, |
|
||||||
Flatten, |
|
||||||
Paint, |
|
||||||
} |
|
||||||
|
|
||||||
public enum ToolShape |
|
||||||
{ |
|
||||||
Corner, |
|
||||||
Circle, |
|
||||||
Square, |
|
||||||
} |
|