Compare commits
No commits in common. 'e1a6c15f5fcacade94431ba4dd1299ea3daa4212' and '3fa91b5954f18f8036bae6f2aa41e5129a2a2e11' have entirely different histories.
e1a6c15f5f
...
3fa91b5954
@ -0,0 +1,282 @@ |
||||
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<SubViewportContainer>(); |
||||
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<Corner>()) { |
||||
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<Side>()) { |
||||
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<Terrain>().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<TilePos> 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<TilePos> GetAllTiles() |
||||
{ |
||||
for (var x = Left; x <= Right; x++) |
||||
for (var y = Top; y <= Bottom; y++) |
||||
yield return new(x, y); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,39 @@ |
||||
#if TOOLS |
||||
[Tool] |
||||
public partial class TerrainEditingPlugin |
||||
: EditorPlugin |
||||
{ |
||||
const string ScenePath = "res://addons/terrain-editing/TerrainEditingControls.tscn"; |
||||
// The container to which the editing controls get added to when Terrain is selected. |
||||
const CustomControlContainer Container = CustomControlContainer.SpatialEditorSideRight; |
||||
|
||||
|
||||
TerrainEditingControls _controls; |
||||
|
||||
public override bool _Handles(GodotObject obj) |
||||
=> obj is Terrain; |
||||
|
||||
public override void _EnterTree() |
||||
{ |
||||
var scene = GD.Load<PackedScene>(ScenePath); |
||||
_controls = scene.Instantiate<TerrainEditingControls>(); |
||||
} |
||||
|
||||
public override void _ExitTree() |
||||
{ |
||||
if (_controls == null) return; |
||||
if (_controls.GetParent() != null) |
||||
RemoveControlFromContainer(Container, _controls); |
||||
_controls.Free(); |
||||
_controls = null; |
||||
} |
||||
|
||||
public override void _MakeVisible(bool visible) |
||||
{ |
||||
if (visible) |
||||
AddControlToContainer(Container, _controls); |
||||
else if (_controls.GetParent() != null) |
||||
RemoveControlFromContainer(Container, _controls); |
||||
} |
||||
} |
||||
#endif |
Before Width: | Height: | Size: 193 B After Width: | Height: | Size: 193 B |
Before Width: | Height: | Size: 129 B After Width: | Height: | Size: 129 B |
Before Width: | Height: | Size: 168 B After Width: | Height: | Size: 168 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 |
@ -1,26 +0,0 @@ |
||||
@tool |
||||
extends EditorPlugin |
||||
|
||||
const CONTAINER := CustomControlContainer.CONTAINER_SPATIAL_EDITOR_SIDE_RIGHT |
||||
|
||||
var controls_scene := preload("res://terrain/editing/TerrainEditingControls.tscn") |
||||
var controls_script := preload("res://terrain/editing/TerrainEditingControls.cs") |
||||
|
||||
var controls : Control; |
||||
|
||||
func _handles(object: Object) -> bool: |
||||
return object.get_script() == controls_script |
||||
|
||||
func _enter_tree() -> void: |
||||
controls = controls_scene.instantiate() |
||||
|
||||
func _exit_tree() -> void: |
||||
if controls.get_parent(): |
||||
remove_control_from_container(CONTAINER, controls) |
||||
controls.free() |
||||
|
||||
func _make_visible(visible: bool) -> void: |
||||
if visible: |
||||
add_control_to_container(CONTAINER, controls) |
||||
elif controls.get_parent(): |
||||
remove_control_from_container(CONTAINER, controls) |
@ -1,258 +0,0 @@ |
||||
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<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); |
||||
} |
||||
|
||||
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<Corner>()) { |
||||
// 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<Side>()) { |
||||
// 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<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); |
||||
} |
||||
} |