[Tool] public partial class Terrain : StaticBody3D { [Export] public Vector2I Size { get; set; } = new(64, 64); [Export] public float TileSize { get; set; } = 2.0f; [Export] public Godot.Collections.Array Textures { get; set; } public bool IsEditing { get; } = Engine.IsEditorHint(); Vector2I? _gridHover = null; Vector2I? _gridSelectionStart = null; Vector2I? _gridSelectionEnd = null; bool _isSelecting = false; Material _editToolMaterial; Material _terrainMaterial; public override void _Ready() { _editToolMaterial = new StandardMaterial3D { VertexColorUseAsAlbedo = true, ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded, DepthDrawMode = BaseMaterial3D.DepthDrawModeEnum.Disabled, NoDepthTest = true, }; _terrainMaterial = new StandardMaterial3D { AlbedoTexture = Textures?.FirstOrDefault(), }; UpdateMeshAndShape(); } Vector2I ToGridPos(Vector3 localPos) => new(RoundToInt(localPos.X / TileSize + Size.X / 2.0f), RoundToInt(localPos.Z / TileSize + Size.Y / 2.0f)); float GetCornerHeight(Vector2I gridPos, Corner corner) { return 0.0f; } GridCorners GetGridCorners(Vector2I gridPos) { var halfSize = TileSize / 2; var vx = (gridPos.X - Size.X / 2.0f) * TileSize; var vz = (gridPos.Y - Size.Y / 2.0f) * TileSize; return new() { TopLeft = new(vx - halfSize, GetCornerHeight(gridPos, Corner.TopLeft ), vz - halfSize), TopRight = new(vx + halfSize, GetCornerHeight(gridPos, Corner.TopRight ), vz - halfSize), BottomLeft = new(vx - halfSize, GetCornerHeight(gridPos, Corner.BottomLeft ), vz + halfSize), BottomRight = new(vx + halfSize, GetCornerHeight(gridPos, Corner.BottomRight), vz + halfSize), }; } void UpdateMeshAndShape() { var mesh = GetOrCreateMesh("MeshInstance"); var shape = GetOrCreateShape("CollisionShape"); mesh.ClearSurfaces(); mesh.SurfaceBegin(Mesh.PrimitiveType.Triangles); var points = new List(); void AddPoint(Vector3 pos, Vector2 uv) { mesh.SurfaceSetUV(uv); mesh.SurfaceAddVertex(pos); points.Add(pos); } void AddTriangle(Vector3 v1, Vector2 uv1, Vector3 v2, Vector2 uv2, Vector3 v3, Vector2 uv3) { var dir = (v3 - v1).Cross(v2 - v1); mesh.SurfaceSetNormal(dir.Normalized()); AddPoint(v1, uv1); AddPoint(v2, uv2); AddPoint(v3, uv3); } for (var x = 0; x < Size.X; x++) { for (var z = 0; z < Size.Y; z++) { var corners = GetGridCorners(new(x, z)); AddTriangle(corners.TopLeft , new(0.0f, 0.0f), corners.TopRight , new(1.0f, 0.0f), corners.BottomLeft , new(0.0f, 1.0f)); AddTriangle(corners.TopRight , new(1.0f, 0.0f), corners.BottomRight, new(1.0f, 1.0f), corners.BottomLeft , new(0.0f, 1.0f)); } } mesh.SurfaceEnd(); mesh.SurfaceSetMaterial(0, _terrainMaterial); shape.Data = [.. points]; } public override void _MouseEnter() { } public override void _MouseExit() { _gridHover = null; } public override void _InputEvent(Camera3D camera, InputEvent @event, Vector3 position, Vector3 normal, int shapeIdx) { if (!IsEditing) return; var localPos = ToLocal(position); var gridPos = ToGridPos(localPos); if (@event is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: var pressed }) { if (pressed) _gridSelectionStart = _gridSelectionEnd = gridPos; _isSelecting = pressed; camera.GetViewport().SetInputAsHandled(); } if (@event is InputEventMouseMotion) { _gridHover = gridPos; if (_isSelecting) _gridSelectionEnd = gridPos; } } public override void _Process(double delta) { if (!IsEditing) _gridHover = _gridSelectionStart = _gridSelectionEnd = null; if ((_gridHover != null) || (_gridSelectionStart != null)) { var mesh = 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 (_gridHover is Vector2I hover) { var corners = GetGridCorners(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) ); } if (_gridSelectionStart is Vector2I start) { var end = _gridSelectionEnd ?? start; (start, end) = (new(Min(start.X, end.X), Min(start.Y, end.Y)), new(Max(start.X, end.X), Max(start.Y, end.Y))); mesh.SurfaceSetColor(Colors.Blue); for (var x = start.X; x <= end.X; x++) for (var y = start.Y; y <= end.Y; y++) { var corners = GetGridCorners(new(x, y)); 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(); } } ImmediateMesh GetOrCreateMesh(string name) { var meshInstance = (MeshInstance3D)GetNodeOrNull(name); if (meshInstance == null) { meshInstance = new() { Name = name, Mesh = new ImmediateMesh() }; AddChild(meshInstance); meshInstance.Owner = this; } return (ImmediateMesh)meshInstance.Mesh; } ConcavePolygonShape3D GetOrCreateShape(string name) { var collisionShape = (CollisionShape3D)GetNodeOrNull(name); if (collisionShape == null) { collisionShape = new() { Name = name, Shape = new ConcavePolygonShape3D() }; AddChild(collisionShape); collisionShape.Owner = this; } return (ConcavePolygonShape3D)collisionShape.Shape; } enum Corner { TopLeft, TopRight, BottomLeft, BottomRight, } readonly struct GridCorners { public Vector3 TopLeft { get; init; } public Vector3 TopRight { get; init; } public Vector3 BottomLeft { get; init; } public Vector3 BottomRight { get; init; } } }