You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
314 lines
11 KiB
314 lines
11 KiB
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<Vector2I, Variant> Tiles { get; set; } |
|
|
|
|
|
Material _editToolMaterial; |
|
public override void _Ready() |
|
{ |
|
_editToolMaterial = new StandardMaterial3D { |
|
VertexColorUseAsAlbedo = true, |
|
ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded, |
|
DepthDrawMode = BaseMaterial3D.DepthDrawModeEnum.Disabled, |
|
NoDepthTest = true, |
|
}; |
|
|
|
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); |
|
} |
|
|
|
var rnd = new Random(); |
|
for (var x = 0; x < Size.X; x++) |
|
for (var z = 0; z < Size.Y; z++) { |
|
var corners = GetCornersPosition(new(x, z)); |
|
|
|
// 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 = corners.ToSorted((a, b) => a.Y.CompareTo(b.Y)); |
|
var minDiff = Abs(sorted[0].Value.Y - sorted[2].Value.Y); // Difference between lowest and 3rd lowest point. |
|
var maxDiff = Abs(sorted[3].Value.Y - sorted[1].Value.Y); // Difference between highest and 3rd highest point. |
|
|
|
mesh.SurfaceSetUV2(new(rnd.Next(4), 0.0f)); |
|
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]; |
|
} |
|
|
|
|
|
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] { |
|
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 T[] ToArray() |
|
=> [ TopLeft, TopRight, BottomRight, BottomLeft ]; |
|
|
|
public (Corner Corner, T Value)[] ToSorted(Comparison<T> comparison) |
|
{ |
|
var result = new (Corner Corner, T Value)[] { |
|
(Corner.TopLeft , TopLeft ), |
|
(Corner.TopRight , TopRight ), |
|
(Corner.BottomRight , BottomRight), |
|
(Corner.BottomLeft , BottomLeft ), |
|
}; |
|
Array.Sort(result, (a, b) => comparison(a.Value, b.Value)); |
|
return result; |
|
} |
|
} |
|
} |
|
|
|
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)), |
|
}; |
|
}
|
|
|