|
|
|
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<Texture2D> 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<Vector2I, Variant> 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();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool Contains(TilePos pos)
|
|
|
|
=> (pos.X >= 0) && (pos.X < Size.X)
|
|
|
|
&& (pos.Y >= 0) && (pos.Y < Size.Y);
|
|
|
|
|
|
|
|
/// <summary> Transforms a 3D position local to the equivalent tile position. </summary>
|
|
|
|
public TilePos ToTilePos(Vector3 localPos)
|
|
|
|
=> new(RoundToInt(localPos.X / TileSize + Size.X / 2.0f),
|
|
|
|
RoundToInt(localPos.Z / TileSize + Size.Y / 2.0f));
|
|
|
|
|
|
|
|
public Corners<float> GetCornerHeights(TilePos pos)
|
|
|
|
=> GetTileRaw(pos) switch {
|
|
|
|
{ VariantType: Variant.Type.Nil } => default,
|
|
|
|
{ VariantType: Variant.Type.Float } v => Corners<float>.From((float)v),
|
|
|
|
{ VariantType: Variant.Type.PackedFloat32Array } v => Corners<float>.From((float[])v),
|
|
|
|
_ => throw new Exception("Invalid type in Points dictionary"),
|
|
|
|
};
|
|
|
|
|
|
|
|
public void SetCornerHeight(TilePos pos, Corner corner, float value)
|
|
|
|
=> SetCornerHeights(pos, default(Corners<float?>).With(corner, value));
|
|
|
|
public void SetCornerHeights(TilePos pos, Corners<float> values)
|
|
|
|
=> SetCornerHeights(pos, new Corners<float?>(values.TopLeft, values.TopRight, values.BottomRight, values.BottomLeft));
|
|
|
|
public void SetCornerHeights(TilePos pos, Corners<float?> values)
|
|
|
|
{
|
|
|
|
if (!Contains(pos)) return;
|
|
|
|
|
|
|
|
var current = GetCornerHeights(pos);
|
|
|
|
var changed = false;
|
|
|
|
|
|
|
|
foreach (var corner in Enum.GetValues<Corner>())
|
|
|
|
if (values[corner] is float value)
|
|
|
|
{ current = current.With(corner, value); changed = true; }
|
|
|
|
if (!changed) return;
|
|
|
|
|
|
|
|
if (current.IsZeroApprox()) RemoveTileRaw(pos);
|
|
|
|
else if (current.IsEqualApprox()) SetTileRaw(pos, current.TopLeft);
|
|
|
|
else SetTileRaw(pos, current.ToArray());
|
|
|
|
}
|
|
|
|
|
|
|
|
public void SetTileHeight(TilePos pos, float value)
|
|
|
|
{
|
|
|
|
if (!Contains(pos)) return;
|
|
|
|
if (IsZeroApprox(value)) RemoveTileRaw(pos);
|
|
|
|
else SetTileRaw(pos, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
public void AdjustTileHeight(TilePos pos, float relative)
|
|
|
|
{
|
|
|
|
if (relative == 0) return;
|
|
|
|
if (!Contains(pos)) return;
|
|
|
|
switch (GetTileRaw(pos)) {
|
|
|
|
case { VariantType: Variant.Type.Nil }:
|
|
|
|
SetTileRaw(pos, relative);
|
|
|
|
break;
|
|
|
|
case { VariantType: Variant.Type.Float } num:
|
|
|
|
var newNum = (float)num + relative;
|
|
|
|
if (IsZeroApprox(newNum)) RemoveTileRaw(pos);
|
|
|
|
else SetTileRaw(pos, newNum);
|
|
|
|
break;
|
|
|
|
case { VariantType: Variant.Type.PackedFloat32Array } nums:
|
|
|
|
var newNums = (float[])nums;
|
|
|
|
for (var i = 0; i < 4; i++) newNums[i] += relative;
|
|
|
|
SetTileRaw(pos, newNums);
|
|
|
|
break;
|
|
|
|
default: throw new Exception("Invalid type in Points dictionary");
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
public Corners<Vector3> GetCornersPosition(TilePos pos)
|
|
|
|
{
|
|
|
|
var heights = GetCornerHeights(pos);
|
|
|
|
var vx = (pos.X - Size.X / 2.0f) * TileSize;
|
|
|
|
var vz = (pos.Y - Size.Y / 2.0f) * TileSize;
|
|
|
|
var half = TileSize / 2;
|
|
|
|
return new(new(vx - half, heights[Corner.TopLeft ], vz - half),
|
|
|
|
new(vx + half, heights[Corner.TopRight ], vz - half),
|
|
|
|
new(vx + half, heights[Corner.BottomRight], vz + half),
|
|
|
|
new(vx - half, heights[Corner.BottomLeft ], vz + half));
|
|
|
|
}
|
|
|
|
|
|
|
|
Variant GetTileRaw(TilePos pos)
|
|
|
|
=> (Tiles?.TryGetValue(pos.ToVector2I(), out var result) == true) ? result : default;
|
|
|
|
void SetTileRaw(TilePos pos, Variant value)
|
|
|
|
=> (Tiles ??= [])[pos.ToVector2I()] = value;
|
|
|
|
void RemoveTileRaw(TilePos pos)
|
|
|
|
=> Tiles?.Remove(pos.ToVector2I());
|
|
|
|
|
|
|
|
|
|
|
|
void UpdateMeshAndShape()
|
|
|
|
{
|
|
|
|
var mesh = GetOrCreateMesh("MeshInstance");
|
|
|
|
var shape = GetOrCreateShape("CollisionShape");
|
|
|
|
|
|
|
|
mesh.ClearSurfaces();
|
|
|
|
mesh.SurfaceBegin(Mesh.PrimitiveType.Triangles);
|
|
|
|
var points = new List<Vector3>();
|
|
|
|
|
|
|
|
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 = GetCornersPosition(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 readonly record struct TilePos(int X, int Y)
|
|
|
|
{
|
|
|
|
public TilePos GetNeighbor(Side side)
|
|
|
|
=> side switch {
|
|
|
|
Side.Left => new(X - 1, Y),
|
|
|
|
Side.Top => new(X, Y - 1),
|
|
|
|
Side.Right => new(X + 1, Y),
|
|
|
|
Side.Bottom => new(X, Y + 1),
|
|
|
|
_ => throw new ArgumentException($"Invalid Side value '{side}'", nameof(side)),
|
|
|
|
};
|
|
|
|
|
|
|
|
public TilePos GetNeighbor(Corner corner)
|
|
|
|
=> corner switch {
|
|
|
|
Corner.TopLeft => new(X - 1, Y - 1),
|
|
|
|
Corner.TopRight => new(X + 1, Y - 1),
|
|
|
|
Corner.BottomRight => new(X + 1, Y + 1),
|
|
|
|
Corner.BottomLeft => new(X - 1, Y + 1),
|
|
|
|
_ => throw new ArgumentException($"Invalid Corner value '{corner}'", nameof(corner)),
|
|
|
|
};
|
|
|
|
|
|
|
|
public static TilePos From(Vector2I value) => new(value.X, value.Y);
|
|
|
|
public Vector2I ToVector2I() => new(X, Y);
|
|
|
|
}
|
|
|
|
|
|
|
|
public readonly record struct Corners<T>
|
|
|
|
(T TopLeft, T TopRight, T BottomRight, T BottomLeft)
|
|
|
|
{
|
|
|
|
public T this[Corner corner] {
|
|
|
|
readonly get => corner switch {
|
|
|
|
Corner.TopLeft => TopLeft,
|
|
|
|
Corner.TopRight => TopRight,
|
|
|
|
Corner.BottomRight => BottomRight,
|
|
|
|
Corner.BottomLeft => BottomLeft,
|
|
|
|
_ => throw new ArgumentException($"Invalid Corner value '{corner}'", nameof(corner)),
|
|
|
|
};
|
|
|
|
init { switch (corner) {
|
|
|
|
case Corner.TopLeft : TopLeft = value; break;
|
|
|
|
case Corner.TopRight : TopRight = value; break;
|
|
|
|
case Corner.BottomRight : BottomRight = value; break;
|
|
|
|
case Corner.BottomLeft : BottomLeft = value; break;
|
|
|
|
default: throw new ArgumentException($"Invalid Corner value '{corner}'", nameof(corner));
|
|
|
|
} }
|
|
|
|
}
|
|
|
|
|
|
|
|
public static Corners<T> From(T value) => new()
|
|
|
|
{ TopLeft = value, TopRight = value, BottomRight = value, BottomLeft = value };
|
|
|
|
public static Corners<T> From(T[] values) => new()
|
|
|
|
{ TopLeft = values[0], TopRight = values[1], BottomRight = values[2], BottomLeft = values[3] };
|
|
|
|
|
|
|
|
public Corners<T> With(Corner corner, T value)
|
|
|
|
=> new((corner == Corner.TopLeft) ? value : TopLeft,
|
|
|
|
(corner == Corner.TopRight) ? value : TopRight,
|
|
|
|
(corner == Corner.BottomRight) ? value : BottomRight,
|
|
|
|
(corner == Corner.BottomLeft) ? value : BottomLeft);
|
|
|
|
|
|
|
|
public readonly T[] ToArray()
|
|
|
|
=> [ TopLeft, TopRight, BottomRight, BottomLeft ];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public static class CornersExtensions
|
|
|
|
{
|
|
|
|
public static bool IsZeroApprox(this Terrain.Corners<float> self)
|
|
|
|
=> Mathf.IsZeroApprox(self.TopLeft) && Mathf.IsZeroApprox(self.TopRight)
|
|
|
|
&& Mathf.IsZeroApprox(self.BottomRight) && Mathf.IsZeroApprox(self.BottomLeft);
|
|
|
|
|
|
|
|
public static bool IsEqualApprox(this Terrain.Corners<float> self)
|
|
|
|
=> Mathf.IsEqualApprox(self.TopLeft, self.TopRight)
|
|
|
|
&& Mathf.IsEqualApprox(self.TopLeft, self.BottomRight)
|
|
|
|
&& Mathf.IsEqualApprox(self.TopLeft, self.BottomLeft);
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: Put this in a different file.
|
|
|
|
public static class TerrainExtensions
|
|
|
|
{
|
|
|
|
public static (Corner, Corner) GetCorners(this Side side)
|
|
|
|
=> side switch {
|
|
|
|
Side.Left => (Corner.TopLeft, Corner.BottomLeft),
|
|
|
|
Side.Top => (Corner.TopLeft, Corner.TopRight),
|
|
|
|
Side.Right => (Corner.TopRight, Corner.BottomRight),
|
|
|
|
Side.Bottom => (Corner.BottomLeft, Corner.BottomRight),
|
|
|
|
_ => throw new ArgumentException($"Invalid Side value '{side}'", nameof(side)),
|
|
|
|
};
|
|
|
|
|
|
|
|
public static Side GetOpposite(this Side side)
|
|
|
|
=> side switch {
|
|
|
|
Side.Left => Side.Right,
|
|
|
|
Side.Top => Side.Bottom,
|
|
|
|
Side.Right => Side.Left,
|
|
|
|
Side.Bottom => Side.Top,
|
|
|
|
_ => throw new ArgumentException($"Invalid Side value '{side}'", nameof(side)),
|
|
|
|
};
|
|
|
|
|
|
|
|
public static Corner GetOpposite(this Corner corner)
|
|
|
|
=> corner switch {
|
|
|
|
Corner.TopLeft => Corner.BottomRight,
|
|
|
|
Corner.TopRight => Corner.BottomLeft,
|
|
|
|
Corner.BottomRight => Corner.TopLeft,
|
|
|
|
Corner.BottomLeft => Corner.TopRight,
|
|
|
|
_ => throw new ArgumentException($"Invalid Corner value '{corner}'", nameof(corner)),
|
|
|
|
};
|
|
|
|
}
|