public partial class TerrainEditingControls { bool _unhandledMotion = false; // Used to detect when mouse moves off the terrain. TilePos? _tileHover = null; // Position of currently hovered tile. bool _isSelecting = false; // Whether left mouse is held down to select tiles. (TilePos, TilePos)? _selection = null; Material _editToolMaterial; public override void _EnterTree() { _editToolMaterial = new StandardMaterial3D { VertexColorUseAsAlbedo = true, ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded, DepthDrawMode = BaseMaterial3D.DepthDrawModeEnum.Disabled, NoDepthTest = true, }; } public override void _Input(InputEvent ev) { if (GetTerrain() is not Terrain terrain) return; if (Engine.IsEditorHint()) { // Make sure to transform the input event to the 3D scene's viewport. var viewport = EditorInterface.Singleton.GetEditorViewport3D(); var container = viewport.GetParent(); ev = ev.XformedBy(container.GetGlobalTransform().AffineInverse()); if (ev is InputEventMouse m) m.GlobalPosition = m.Position; } if (_isSelecting && (ev is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: false })) { _isSelecting = false; GetViewport().SetInputAsHandled(); return; } if ((ev is InputEventMouseButton { ButtonIndex: var wheel, Pressed: var pressed, ShiftPressed: true }) && (wheel is MouseButton.WheelUp or MouseButton.WheelDown) && (_selection != null)) { // NOTE: Potential bug in the Godot editor? // Does it zoom both when mouse wheel is "pressed" and "released"? // Because just cancelling one of them still causes zooming to occur. GetViewport().SetInputAsHandled(); if (!pressed) return; const float AdjustHeight = 0.5f; var amount = (wheel == MouseButton.WheelUp) ? AdjustHeight : -AdjustHeight; var selection = TileRegion.From(_selection.Value); // Raise connected corners. foreach (var innerCorner in Enum.GetValues()) { var outerCorner = innerCorner.GetOpposite(); var innerPos = selection.GetTileFor(innerCorner); var outerPos = innerPos.GetNeighbor(innerCorner); var outerTile = terrain.GetTile(outerPos); var innerHeight = terrain.GetTile(innerPos).Height[innerCorner]; var outerHeight = outerTile.Height[outerCorner]; if (IsEqualApprox(outerHeight, innerHeight)) { outerTile.Height[outerCorner] = innerHeight + amount; terrain.SetTile(outerPos, outerTile); } } // Raise connected sides. foreach (var side in Enum.GetValues()) { foreach (var innerPos in selection.GetTilesFor(side)) { var outerPos = innerPos.GetNeighbor(side); var innerTile = terrain.GetTile(innerPos); var outerTile = terrain.GetTile(outerPos); var (innerCorner1, innerCorner2) = side.GetCorners(); var (outerCorner1, outerCorner2) = side.GetOpposite().GetCorners(); var changed = false; var matchingCorners = new[]{ (innerCorner1, outerCorner1), (innerCorner2, outerCorner2) }; foreach (var (innerCorner, outerCorner) in matchingCorners) { var innerHeight = innerTile.Height[innerCorner]; var outerHeight = outerTile.Height[outerCorner]; if (IsEqualApprox(outerHeight, innerHeight)) { outerTile.Height[outerCorner] = innerHeight + amount; changed = true; } } if (changed) terrain.SetTile(outerPos, outerTile); } } // Raise selected tiles themselves. foreach (var pos in selection.GetAllTiles()) { var tile = terrain.GetTile(pos); tile.Height.Adjust(amount); terrain.SetTile(pos, tile); } terrain.UpdateMeshAndShape(); terrain.NotifyPropertyListChanged(); return; } if ((ev is InputEventMouseButton { ButtonIndex: var wheel2, Pressed: var pressed2, CtrlPressed: true }) && (wheel2 is MouseButton.WheelUp or MouseButton.WheelDown) && (_selection != null)) { GetViewport().SetInputAsHandled(); if (!pressed2) return; var amount = (wheel2 == MouseButton.WheelUp) ? 1 : -1; var selection = TileRegion.From(_selection.Value); foreach (var pos in selection.GetAllTiles()) { var tile = terrain.GetTile(pos); tile.TexturePrimary = PosMod(tile.TexturePrimary + amount, 4); terrain.SetTile(pos, tile); } terrain.UpdateMeshAndShape(); terrain.NotifyPropertyListChanged(); return; } if (ev is InputEventMouseMotion) _unhandledMotion = true; if ((ev is InputEventMouse mouse) && (!Engine.IsEditorHint() || EditorInterface.Singleton.GetEditorViewport3D() .GetVisibleRect().HasPoint(mouse.Position))) OnInputRayCastTerrain(terrain, mouse); } void OnInputRayCastTerrain(Terrain terrain, 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"); if (collider == terrain) { var pos = (Vector3)result["position"]; // var normal = (Vector3)result["normal"]; // var shape = (int)result["shape"]; OnTerrainInput(terrain, ev, pos); } } void OnTerrainInput(Terrain terrain, InputEvent ev, Vector3 position) { if (terrain == null) return; var localPos = terrain.ToLocal(position); var tilePos = terrain.ToTilePos(localPos); if (ev is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: true }) { _selection = new(tilePos, tilePos); _isSelecting = true; GetViewport().SetInputAsHandled(); return; } if (ev is InputEventMouseMotion) { _unhandledMotion = false; _tileHover = tilePos; if (_isSelecting) _selection = _selection.Value with { Item2 = tilePos }; } } public override void _Process(double delta) { if (GetTerrain() is not Terrain terrain) { _tileHover = null; _selection = null; _isSelecting = false; terrain = null; } if (_unhandledMotion) _tileHover = null; if ((_tileHover != null) || (_selection != null)) { 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 bottomLeft, Vector3 bottomRight) { AddLine(topLeft , topRight ); AddLine(topRight , bottomRight); AddLine(bottomRight, bottomLeft ); AddLine(bottomLeft , topLeft ); } if (_tileHover is TilePos hover) { var corners = terrain.GetTileCornerPositions(hover); var margin = 0.1f; mesh.SurfaceSetColor(Colors.Black); AddQuad(corners.TopLeft + new Vector3(-margin, 0, -margin), corners.TopRight + new Vector3(+margin, 0, -margin), corners.BottomLeft + new Vector3(-margin, 0, +margin), corners.BottomRight + new Vector3(+margin, 0, +margin)); } mesh.SurfaceSetColor(Colors.Blue); if (_selection is (TilePos, TilePos) selection) foreach (var pos in TileRegion.From(selection).GetAllTiles()) { var corners = terrain.GetTileCornerPositions(pos); AddQuad(corners.TopLeft, corners.TopRight, corners.BottomLeft, corners.BottomRight); } mesh.SurfaceEnd(); mesh.SurfaceSetMaterial(0, _editToolMaterial); } else { var meshInstance = (MeshInstance3D)GetNodeOrNull("EditToolMesh"); var mesh = (ImmediateMesh)meshInstance?.Mesh; mesh?.ClearSurfaces(); } } public Terrain GetTerrain() => EditorInterface.Singleton.GetSelection() .GetSelectedNodes().OfType().FirstOrDefault(); readonly record struct TileRegion(int Left, int Top, int Right, int Bottom) { public TilePos TopLeft => new(Left , Top); public TilePos TopRight => new(Right, Top); public TilePos BottomRight => new(Right, Bottom); public TilePos BottomLeft => new(Left , Bottom); public int Width => Right - Left + 1; public int Height => Bottom - Top + 1; public static TileRegion From((TilePos, TilePos) selection) => From(selection.Item1, selection.Item2); public static TileRegion From(TilePos a, TilePos b) => new(Min(a.X, b.X), Min(a.Y, b.Y), Max(a.X, b.X), Max(a.Y, b.Y)); public TilePos GetTileFor(Corner corner) => corner switch { Corner.TopLeft => TopLeft, Corner.TopRight => TopRight, Corner.BottomRight => BottomRight, Corner.BottomLeft => BottomLeft, _ => throw new ArgumentException($"Invalid Corner value '{corner}'", nameof(corner)), }; public IEnumerable GetTilesFor(Side side) { var (left, top, right, bottom) = this; return side switch { Side.Left => Enumerable.Range(Top, Height).Select(y => new TilePos(left, y)), Side.Top => Enumerable.Range(Left, Width).Select(x => new TilePos(x, top)), Side.Right => Enumerable.Range(Top, Height).Select(y => new TilePos(right, y)), Side.Bottom => Enumerable.Range(Left, Width).Select(x => new TilePos(x, bottom)), _ => throw new ArgumentException($"Invalid Side value '{side}'", nameof(side)), }; } public IEnumerable GetAllTiles() { for (var x = Left; x <= Right; x++) for (var y = Top; y <= Bottom; y++) yield return new(x, y); } } }