[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 ShaderMaterial Material { 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; } public override void _Ready() => UpdateMeshAndShape(); public Tile GetTile(TilePos pos) => (Tiles?.TryGetValue(pos.ToVector2I(), out var result) == true) ? Tile.FromDictionary(result.AsGodotDictionary()) : default; public void SetTile(TilePos pos, Tile value) { var key = pos.ToVector2I(); var dict = value.ToDictionary(); if (dict == null) Tiles.Remove(key); else Tiles[key] = dict; } public bool Contains(TilePos pos) => (pos.X >= 0) && (pos.X < Size.X) && (pos.Y >= 0) && (pos.Y < Size.Y); public 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); } // TODO: Don't hardcode. var num_textures = 4; var num_blend_textures = 7; var rnd = new Random(); for (var x = 0; x < Size.X; x++) for (var z = 0; z < Size.Y; z++) { var tile = GetTile(new(x, z)); var corners = GetTileCornerPositions(new(x, z), tile); // Randomly select two different textures and one blend texture. mesh.SurfaceSetColor(new( (float)tile.TexturePrimary / num_textures, (float)tile.TextureSecondary / num_textures, (float)tile.TextureBlend / num_blend_textures )); var sorted = new (Corner Corner, float Height)[] { (Corner.TopLeft , tile.Height.TopLeft ), (Corner.TopRight , tile.Height.TopRight ), (Corner.BottomRight , tile.Height.BottomRight), (Corner.BottomLeft , tile.Height.BottomLeft ), }; Array.Sort(sorted, (a, b) => a.Height.CompareTo(b.Height)); // Find the "ideal way" to split the quad for the tile into two triangles. // This is done by finding the corner with the least variance between its neighboring corners. var minDiff = Abs(sorted[0].Height - sorted[2].Height); // Difference between lowest and 3rd lowest point. var maxDiff = Abs(sorted[3].Height - sorted[1].Height); // Difference between highest and 3rd highest point. var first = sorted[(minDiff > maxDiff) ? 0 : 3].Corner; if (first is Corner.TopLeft or Corner.BottomRight) { 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)); } else { AddTriangle(corners.TopRight , new(1.0f, 0.0f), corners.BottomRight, new(1.0f, 1.0f), corners.TopLeft , new(0.0f, 0.0f)); AddTriangle(corners.BottomRight, new(1.0f, 1.0f), corners.BottomLeft , new(0.0f, 1.0f), corners.TopLeft , new(0.0f, 0.0f)); } } mesh.SurfaceEnd(); mesh.SurfaceSetMaterial(0, Material); shape.Data = [.. points]; } public Corners GetTileCornerPositions(TilePos pos) => GetTileCornerPositions(pos, GetTile(pos)); public Corners GetTileCornerPositions(TilePos pos, Tile tile) { var half = TileSize / 2; var vx = (pos.X - Size.X / 2.0f) * TileSize; var vz = (pos.Y - Size.Y / 2.0f) * TileSize; return new(new(vx - half, tile.Height.TopLeft , vz - half), new(vx + half, tile.Height.TopRight , vz - half), new(vx + half, tile.Height.BottomRight, vz + half), new(vx - half, tile.Height.BottomLeft , vz + half)); } public 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; } public 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; } }