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; } // If value at position non-existant => [ 0, 0, 0, 0 ] // If value at position is float => [ v, v, v, v ] // If value at position is float[] => value [Export] public Godot.Collections.Dictionary Tiles { get; set; } 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(); } /// Returns if this terrain grid contains the specified tile position. public bool ContainsTilePos(Vector2I tilePos) => (tilePos.X >= 0) && (tilePos.X < Size.X) && (tilePos.Y >= 0) && (tilePos.Y < Size.Y); /// Transforms a 3D position local to the equivalent tile position. public Vector2I ToTilePos(Vector3 localPos) => new(RoundToInt(localPos.X / TileSize + Size.X / 2.0f), RoundToInt(localPos.Z / TileSize + Size.Y / 2.0f)); public float GetCornerHeight(Vector2I tilePos, Corner corner) => GetTileRaw(tilePos) switch { { VariantType: Variant.Type.Nil } => 0, { VariantType: Variant.Type.Float } num => (float)num, { VariantType: Variant.Type.PackedFloat32Array } nums => ((float[])nums)[(int)corner], _ => throw new Exception("Invalid type in Points dictionary"), }; public void SetCornerHeight(Vector2I tilePos, Corner corner, float value) { if (!ContainsTilePos(tilePos)) return; var existing = GetTileRaw(tilePos) switch { { VariantType: Variant.Type.Nil } => [ 0, 0, 0, 0 ], { VariantType: Variant.Type.Float } num => [ (float)num, (float)num, (float)num, (float)num ], { VariantType: Variant.Type.PackedFloat32Array } nums => (float[])nums, _ => throw new Exception("Invalid type in Points dictionary"), }; existing[(int)corner] = value; if (existing.All(IsZeroApprox)) Tiles?.Remove(tilePos); if (IsEqualApprox(existing[0], existing[1]) && IsEqualApprox(existing[0], existing[2]) && IsEqualApprox(existing[0], existing[3])) SetTileRaw(tilePos, existing[0]); else SetTileRaw(tilePos, existing); } public void SetTileHeight(Vector2I tilePos, float value) { if (!ContainsTilePos(tilePos)) return; if (IsZeroApprox(value)) Tiles?.Remove(tilePos); else SetTileRaw(tilePos, value); } public void AdjustTileHeight(Vector2I tilePos, float relative) { if (relative == 0) return; if (!ContainsTilePos(tilePos)) return; switch (GetTileRaw(tilePos)) { case { VariantType: Variant.Type.Nil }: SetTileRaw(tilePos, relative); break; case { VariantType: Variant.Type.Float } num: var newNum = (float)num + relative; if (IsZeroApprox(newNum)) Tiles?.Remove(tilePos); else SetTileRaw(tilePos, newNum); break; case { VariantType: Variant.Type.PackedFloat32Array } nums: var newNums = (float[])nums; for (var i = 0; i < 4; i++) newNums[i] += relative; SetTileRaw(tilePos, newNums); break; default: throw new Exception("Invalid type in Points dictionary"); }; } public GridCorners GetGridCorners(Vector2I tilePos) { var halfSize = TileSize / 2; var vx = (tilePos.X - Size.X / 2.0f) * TileSize; var vz = (tilePos.Y - Size.Y / 2.0f) * TileSize; return new() { TopLeft = new(vx - halfSize, GetCornerHeight(tilePos, Corner.TopLeft ), vz - halfSize), TopRight = new(vx + halfSize, GetCornerHeight(tilePos, Corner.TopRight ), vz - halfSize), BottomLeft = new(vx - halfSize, GetCornerHeight(tilePos, Corner.BottomLeft ), vz + halfSize), BottomRight = new(vx + halfSize, GetCornerHeight(tilePos, Corner.BottomRight), vz + halfSize), }; } Variant GetTileRaw(Vector2I tilePos) => (Tiles?.TryGetValue(tilePos, out var result) == true) ? result : default; void SetTileRaw(Vector2I tilePos, Variant value) => (Tiles ??= [])[tilePos] = value; 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]; } 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; } public enum Corner { TopLeft, TopRight, BottomLeft, BottomRight, } public readonly struct GridCorners { public Vector3 TopLeft { get; init; } public Vector3 TopRight { get; init; } public Vector3 BottomLeft { get; init; } public Vector3 BottomRight { get; init; } } }