You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
279 lines
10 KiB
279 lines
10 KiB
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)); |
|
}
|
|
|