From a2cca8165b81951f526fa1a894132a59c7a798b4 Mon Sep 17 00:00:00 2001 From: copygirl Date: Sun, 15 Sep 2024 22:31:13 +0200 Subject: [PATCH] Add terrain editing controls - Not functional yet - Remove "input in editor" plugin (since it's no longer necessary) - Move terrain editing code to plugin --- .../ReceiveInputInEditorPlugin.cs | 49 ------- .../ReceiveInputInEditorPlugin.gd | 38 ------ addons/receive-input-in-editor/plugin.cfg | 7 - .../TerrainEditingControls+Editing.cs | 113 ++++++++++++----- .../terrain-editing/TerrainEditingControls.cs | 101 +++++++++++++++ .../TerrainEditingControls.tscn | 120 ++++++++++++++++++ .../terrain-editing/TerrainEditingPlugin.cs | 34 +++++ addons/terrain-editing/icons/circle.png | Bin 0 -> 193 bytes .../terrain-editing/icons/circle.png.import | 34 +++++ addons/terrain-editing/icons/corner.png | Bin 0 -> 129 bytes .../terrain-editing/icons/corner.png.import | 34 +++++ addons/terrain-editing/icons/height.png | Bin 0 -> 168 bytes .../terrain-editing/icons/height.png.import | 34 +++++ addons/terrain-editing/icons/paint.png | Bin 0 -> 169 bytes addons/terrain-editing/icons/paint.png.import | 34 +++++ addons/terrain-editing/icons/square.png | Bin 0 -> 147 bytes .../terrain-editing/icons/square.png.import | 34 +++++ addons/terrain-editing/plugin.cfg | 7 + project.godot | 2 +- terrain/Terrain.cs | 19 +-- terrain/Tile.cs | 2 +- 21 files changed, 517 insertions(+), 145 deletions(-) delete mode 100644 addons/receive-input-in-editor/ReceiveInputInEditorPlugin.cs delete mode 100644 addons/receive-input-in-editor/ReceiveInputInEditorPlugin.gd delete mode 100644 addons/receive-input-in-editor/plugin.cfg rename terrain/Terrain+Editing.cs => addons/terrain-editing/TerrainEditingControls+Editing.cs (67%) create mode 100644 addons/terrain-editing/TerrainEditingControls.cs create mode 100644 addons/terrain-editing/TerrainEditingControls.tscn create mode 100644 addons/terrain-editing/TerrainEditingPlugin.cs create mode 100644 addons/terrain-editing/icons/circle.png create mode 100644 addons/terrain-editing/icons/circle.png.import create mode 100644 addons/terrain-editing/icons/corner.png create mode 100644 addons/terrain-editing/icons/corner.png.import create mode 100644 addons/terrain-editing/icons/height.png create mode 100644 addons/terrain-editing/icons/height.png.import create mode 100644 addons/terrain-editing/icons/paint.png create mode 100644 addons/terrain-editing/icons/paint.png.import create mode 100644 addons/terrain-editing/icons/square.png create mode 100644 addons/terrain-editing/icons/square.png.import create mode 100644 addons/terrain-editing/plugin.cfg diff --git a/addons/receive-input-in-editor/ReceiveInputInEditorPlugin.cs b/addons/receive-input-in-editor/ReceiveInputInEditorPlugin.cs deleted file mode 100644 index 05165c1..0000000 --- a/addons/receive-input-in-editor/ReceiveInputInEditorPlugin.cs +++ /dev/null @@ -1,49 +0,0 @@ -#if TOOLS -[Tool] -public partial class ReceiveInputInEditorPlugin - : EditorPlugin -{ - public override bool _Handles(GodotObject obj) - => obj is Node; - - public override int _Forward3DGuiInput(Camera3D camera, InputEvent ev) - { - var root = (Node3D)EditorInterface.Singleton.GetEditedSceneRoot(); - - var viewport = root.GetViewport(); - // Don't know about any negative effect of changing these and leaving them like that. - viewport.GuiDisableInput = false; // Re-enable input, required for `PushInput` to work at all. - viewport.HandleInputLocally = true; // Let sub-viewport handle its own input, makes `SetInputAsHandled` work as expected? - - // This will propagate input events into the edited scene, - // including regular, GUI, shortcut, unhandled key and unhandled. - viewport.PushInput(ev); - - // Enabling `PhysicsObjectPicking` causes `IsInputHandled()` to always - // return true, and object picking isn't immediate anyway, so let's just - // do it ourselves. This doesn't support touch input, though. - if (!viewport.IsInputHandled() && (ev is InputEventMouse { Position: var mouse })) { - // Ray is cast from the editor camera's view. - var from = camera.ProjectRayOrigin(mouse); - var to = from + camera.ProjectRayNormal(mouse) * camera.Far; - - // Actual collision is done in the edited scene though. - var space = root.GetWorld3D().DirectSpaceState; - var query = PhysicsRayQueryParameters3D.Create(from, to); - - var result = space.IntersectRay(query); - // The collider object isn't necessarily a `CollisionObject3D`. - var collider = (GodotObject)result.GetValueOrDefault("collider"); - if (collider is CollisionObject3D { Visible: true } obj) { - var pos = (Vector3)result["position"]; - var normal = (Vector3)result["normal"]; - var shape = (int)result["shape"]; - obj._InputEvent(camera, ev, pos, normal, shape); - } - } - - // If any node calls `SetInputAsHandled()`, the event is not passed on to other editor gizmos / plugins. - return viewport.IsInputHandled() ? (int)AfterGuiInput.Stop : (int)AfterGuiInput.Pass; - } -} -#endif diff --git a/addons/receive-input-in-editor/ReceiveInputInEditorPlugin.gd b/addons/receive-input-in-editor/ReceiveInputInEditorPlugin.gd deleted file mode 100644 index 777ee0c..0000000 --- a/addons/receive-input-in-editor/ReceiveInputInEditorPlugin.gd +++ /dev/null @@ -1,38 +0,0 @@ -@tool -extends EditorPlugin - -func _handles(obj: Object) -> bool: - return obj is Node - -func _forward_3d_gui_input(camera: Camera3D, event: InputEvent) -> int: - var root := EditorInterface.get_edited_scene_root() as Node3D - - var viewport := root.get_viewport() - # Don't know about any negative effect of changing these and leaving them like that. - viewport.gui_disable_input = false # Re-enable input, required for `push_input` to work at all. - viewport.handle_input_locally = true # Let sub-viewport handle its own input, makes `set_input_as_handled` work as expected? - - # This will propagate input events into the edited scene, - # including regular, GUI, shortcut, unhandled key and unhandled. - viewport.push_input(event) - - # Enabling `physics_object_picking` causes `is_input_handled()` to always - # return true, and object picking isn't immediate anyway, so let's just - # do it ourselves. This doesn't support touch input, though. - if !viewport.is_input_handled() and event is InputEventMouse: - # Ray is cast from the editor camera's view. - var from := camera.project_ray_origin(event.position) - var to := from + camera.project_ray_normal(event.position) * camera.far - - # Actual collision is done in the edited scene though. - var space := root.get_world_3d().direct_space_state - var query := PhysicsRayQueryParameters3D.create(from, to) - - var result := space.intersect_ray(query) - # The collider object isn't necessarily a `CollisionObject3D`. - var obj := result.get("collider") as CollisionObject3D - if obj and obj.visible: - obj._input_event(camera, event, result["position"], result["normal"], result["shape"]) - - # If any node calls `set_input_as_handled()`, the event is not passed on to other editor gizmos / plugins. - return AfterGUIInput.AFTER_GUI_INPUT_STOP if viewport.is_input_handled() else AfterGUIInput.AFTER_GUI_INPUT_PASS diff --git a/addons/receive-input-in-editor/plugin.cfg b/addons/receive-input-in-editor/plugin.cfg deleted file mode 100644 index 9957010..0000000 --- a/addons/receive-input-in-editor/plugin.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[plugin] - -name="Receive Input in Editor" -description="" -author="copygirl" -version="" -script="ReceiveInputInEditorPlugin.cs" diff --git a/terrain/Terrain+Editing.cs b/addons/terrain-editing/TerrainEditingControls+Editing.cs similarity index 67% rename from terrain/Terrain+Editing.cs rename to addons/terrain-editing/TerrainEditingControls+Editing.cs index 1090caf..91e7dae 100644 --- a/terrain/Terrain+Editing.cs +++ b/addons/terrain-editing/TerrainEditingControls+Editing.cs @@ -1,18 +1,37 @@ -[Tool] -public partial class Terrain +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; + void OnEditingReady() + { + _editToolMaterial = new StandardMaterial3D { + VertexColorUseAsAlbedo = true, + ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded, + DepthDrawMode = BaseMaterial3D.DepthDrawModeEnum.Disabled, + NoDepthTest = true, + }; + } + public override void _Input(InputEvent ev) { - if (!IsEditing()) return; + 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 }) { + 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 }) @@ -37,13 +56,13 @@ public partial class Terrain var innerPos = selection.GetTileFor(innerCorner); var outerPos = innerPos.GetNeighbor(innerCorner); - var outerTile = GetTile(outerPos); - var innerHeight = GetTile(innerPos).Height[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; - SetTile(outerPos, outerTile); + terrain.SetTile(outerPos, outerTile); } } @@ -52,8 +71,8 @@ public partial class Terrain foreach (var innerPos in selection.GetTilesFor(side)) { var outerPos = innerPos.GetNeighbor(side); - var innerTile = GetTile(innerPos); - var outerTile = GetTile(outerPos); + var innerTile = terrain.GetTile(innerPos); + var outerTile = terrain.GetTile(outerPos); var (innerCorner1, innerCorner2) = side.GetCorners(); var (outerCorner1, outerCorner2) = side.GetOpposite().GetCorners(); @@ -69,19 +88,20 @@ public partial class Terrain changed = true; } } - if (changed) SetTile(outerPos, outerTile); + if (changed) terrain.SetTile(outerPos, outerTile); } } // Raise selected tiles themselves. foreach (var pos in selection.GetAllTiles()) { - var tile = GetTile(pos); + var tile = terrain.GetTile(pos); tile.Height.Adjust(amount); - SetTile(pos, tile); + terrain.SetTile(pos, tile); } - UpdateMeshAndShape(); - NotifyPropertyListChanged(); + terrain.UpdateMeshAndShape(); + terrain.NotifyPropertyListChanged(); + return; } if ((ev is InputEventMouseButton { ButtonIndex: var wheel2, Pressed: var pressed2, CtrlPressed: true }) @@ -94,30 +114,59 @@ public partial class Terrain var selection = TileRegion.From(_selection.Value); foreach (var pos in selection.GetAllTiles()) { - var tile = GetTile(pos); + var tile = terrain.GetTile(pos); tile.TexturePrimary = PosMod(tile.TexturePrimary + amount, 4); - SetTile(pos, tile); + terrain.SetTile(pos, tile); } - UpdateMeshAndShape(); - NotifyPropertyListChanged(); + 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); + } } - public override void _InputEvent(Camera3D camera, InputEvent ev, Vector3 position, Vector3 normal, int shapeIdx) + void OnTerrainInput(Terrain terrain, InputEvent ev, Vector3 position) { - if (!IsEditing()) return; + if (terrain == null) return; - var localPos = ToLocal(position); - var tilePos = ToTilePos(localPos); + 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) { @@ -129,17 +178,18 @@ public partial class Terrain public override void _Process(double delta) { - if (!IsEditing()) { + 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 = GetOrCreateMesh("EditToolMesh"); + var mesh = terrain.GetOrCreateMesh("EditToolMesh"); mesh.ClearSurfaces(); mesh.SurfaceBegin(Mesh.PrimitiveType.Lines); @@ -157,7 +207,7 @@ public partial class Terrain } if (_tileHover is TilePos hover) { - var corners = GetTileCornerPositions(hover); + var corners = terrain.GetTileCornerPositions(hover); var margin = 0.1f; mesh.SurfaceSetColor(Colors.Black); AddQuad(corners.TopLeft + new Vector3(-margin, 0, -margin), @@ -169,7 +219,7 @@ public partial class Terrain mesh.SurfaceSetColor(Colors.Blue); if (_selection is (TilePos, TilePos) selection) foreach (var pos in TileRegion.From(selection).GetAllTiles()) { - var corners = GetTileCornerPositions(pos); + var corners = terrain.GetTileCornerPositions(pos); AddQuad(corners.TopLeft, corners.TopRight, corners.BottomLeft, corners.BottomRight); } @@ -182,15 +232,8 @@ public partial class Terrain } } - bool IsEditing() - { - if (Engine.IsEditorHint()) { - var selection = EditorInterface.Singleton.GetSelection(); - return selection.GetSelectedNodes().Contains(this); - } else { - return false; - } - } + public Terrain GetTerrain() => EditorInterface.Singleton.GetSelection() + .GetSelectedNodes().OfType().FirstOrDefault(); readonly record struct TileRegion(int Left, int Top, int Right, int Bottom) diff --git a/addons/terrain-editing/TerrainEditingControls.cs b/addons/terrain-editing/TerrainEditingControls.cs new file mode 100644 index 0000000..11179ac --- /dev/null +++ b/addons/terrain-editing/TerrainEditingControls.cs @@ -0,0 +1,101 @@ +[Tool] +public partial class TerrainEditingControls + : VBoxContainer +{ + public (ToolMode , Button)[] ToolModeButtons { get; private set; } + public (ToolShape, Button)[] ToolShapeButtons { get; private set; } + public Button[] PaintTextureButtons { get; private set; } + + public Slider DrawSizeSlider { get; private set; } + + ToolMode _toolMode; + ToolShape _toolShape; + int _texture; + + public ToolMode ToolMode { get => _toolMode ; set => SetToolMode (value); } + public ToolShape ToolShape { get => _toolShape; set => SetToolShape(value); } + public int Texture { get => _texture ; set => SetTexture (value); } + + public int DrawSize { + get => RoundToInt(-DrawSizeSlider?.Value ?? 1); + set => DrawSizeSlider.Value = -value; + } + + public override void _Ready() + { + ToolModeButtons = [ + (ToolMode.Height, GetNode