public partial class TerrainEditingControls { 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(); 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 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 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); } UpdateEditToolMesh(terrain, ToolShape switch { // TODO: Edit corner, not full tile. ToolShape.Corner => [tile], ToolShape.Circle => GetTilesInRadius(), ToolShape.Square => GetTilesInSquare(), _ => throw new InvalidOperationException(), }); } else { ClearEditToolMesh(); } } // 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) && viewport.GetVisibleRect().HasPoint(mouse.Position)) // OnInputRayCastTerrain(terrain, mouse); } void UpdateEditToolMesh(Terrain terrain, IEnumerable 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); } }