[Tool] public partial class Terrain : StaticBody3D { [Export] public float TileSize { get; set; } = 2.0f; [Export] public float TileStep { get; set; } = 0.5f; [Export] public TerrainData Data { get; set; } = new(); [Export] public ShaderMaterial Material { get; set; } public override void _Ready() => UpdateMeshAndShape(); public void UpdateMeshAndShape() { var mesh = GetOrCreateMesh("MeshInstance"); var shape = GetOrCreateShape("CollisionShape"); mesh.ClearSurfaces(); mesh.SurfaceBegin(Mesh.PrimitiveType.Triangles); var points = new List(); // for CollisionShape 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 stone_texture = 3; // These are floats to ensure floating point division is used when calling 'SurfaceSetColor'. var num_textures = 4.0f; var num_blend_textures = 7.0f; void SetTexture(int primary, int secondary = 1, int blend = 0) => mesh.SurfaceSetColor(new( (primary - 1) / num_textures, (secondary - 1) / num_textures, blend / num_blend_textures )); foreach (var (chunkPos, chunk) in Data.Chunks) { var offset = TerrainChunk.ToTileOffset(chunkPos); for (var x = 0; x < TerrainChunk.Size; x++) for (var z = 0; z < TerrainChunk.Size; z++) { var pos = new TilePos(x, z) + offset; var tile = chunk[pos]; if (tile.IsDefault) continue; var corners = ToPositions(pos, tile); SetTexture(tile.TexturePrimary, tile.TextureSecondary, tile.TextureBlend); 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)); } // Set stone texture for walls. SetTexture(stone_texture); void DrawWall(TilePos nbrPos, Side side) { var nbrTile = Data.GetTileOrDefault(nbrPos); var nbrCorners = ToPositions(nbrPos, nbrTile); var (c1, c2) = side.GetCorners(); var (c3, c4) = side.GetOpposite().GetCorners(); var corner1 = corners[c1]; // "TopRight" var corner2 = corners[c2]; // "TopLeft" var corner3 = nbrCorners[c3]; // "BottomLeft" var corner4 = nbrCorners[c4]; // "BottomRight" var equal1 = IsEqualApprox(corner1.Y, corner4.Y); var equal2 = IsEqualApprox(corner2.Y, corner3.Y); switch (equal1, equal2) { case (true, true): // Both corners are connected, no wall needed. break; case (true, false): AddTriangle(corner1, new(1.0f, corner1.Y / TileSize), corner3, new(0.0f, corner3.Y / TileSize), corner2, new(0.0f, corner2.Y / TileSize)); break; case (false, true): AddTriangle(corner1, new(1.0f, corner1.Y / TileSize), corner4, new(1.0f, corner4.Y / TileSize), corner2, new(0.0f, corner2.Y / TileSize)); break; case (false, false): // FIXME: In some configurations this creates a shape we don't want. // Need to find a way to detect this, and switch the way triangles make up a quad. AddTriangle(corner1, new(1.0f, corner1.Y / TileSize), corner4, new(1.0f, corner4.Y / TileSize), corner2, new(0.0f, corner2.Y / TileSize)); AddTriangle(corner2, new(0.0f, corner2.Y / TileSize), corner4, new(1.0f, corner4.Y / TileSize), corner3, new(0.0f, corner3.Y / TileSize)); break; } } DrawWall(pos + (1, 0), Side.Right); DrawWall(pos + (0, 1), Side.Bottom); } } mesh.SurfaceEnd(); mesh.SurfaceSetMaterial(0, Material); shape.Data = [.. points]; } 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; } public Corners ToPositions(TilePos pos) => ToPositions(pos, Data.GetTileOrDefault(pos)); public Corners ToPositions(TilePos pos, Tile tile) { var x = pos.X * TileSize; var z = pos.Z * TileSize; return new(new(x , tile.Height.TopLeft * TileStep, z ), new(x + TileSize, tile.Height.TopRight * TileStep, z ), new(x + TileSize, tile.Height.BottomRight * TileStep, z + TileSize), new(x , tile.Height.BottomLeft * TileStep, z + TileSize)); } }