|
|
|
[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<Vector3>(); // 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);
|
|
|
|
|
|
|
|
// 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 sorted = tile.Height.ToArray(); Array.Sort(sorted);
|
|
|
|
var minDiff = Abs(sorted[0] - sorted[2]); // Difference between lowest and 3rd lowest point.
|
|
|
|
var maxDiff = Abs(sorted[3] - sorted[1]); // Difference between highest and 3rd highest point.
|
|
|
|
var first = (Corner)sorted[(minDiff > maxDiff) ? 0 : 3];
|
|
|
|
|
|
|
|
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<Vector3> ToPositions(TilePos pos)
|
|
|
|
=> ToPositions(pos, Data.GetTileOrDefault(pos));
|
|
|
|
|
|
|
|
public Corners<Vector3> 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));
|
|
|
|
}
|
|
|
|
}
|