Compare commits

..

4 Commits

  1. BIN
      assets/textures/terrain_blend/bricky_inner.png
  2. 14
      assets/textures/terrain_blend/bricky_inner.png.import
  3. BIN
      assets/textures/terrain_blend/bricky_outer.png
  4. 14
      assets/textures/terrain_blend/bricky_outer.png.import
  5. BIN
      assets/textures/terrain_blend/bricky_straight.png
  6. 14
      assets/textures/terrain_blend/bricky_straight.png.import
  7. BIN
      assets/textures/terrain_blend/default.png
  8. 14
      assets/textures/terrain_blend/default.png.import
  9. BIN
      assets/textures/terrain_blend/grassy_inner.png
  10. 14
      assets/textures/terrain_blend/grassy_inner.png.import
  11. BIN
      assets/textures/terrain_blend/grassy_outer.png
  12. 14
      assets/textures/terrain_blend/grassy_outer.png.import
  13. BIN
      assets/textures/terrain_blend/grassy_straight.png
  14. 14
      assets/textures/terrain_blend/grassy_straight.png.import
  15. 1561
      level.tscn
  16. 29
      terrain/SideAndCornerExtensions.cs
  17. 68
      terrain/Terrain+Editing.cs
  18. 270
      terrain/Terrain.cs
  19. 129
      terrain/Tile.cs
  20. 15
      terrain/terrain_material.tres
  21. 16
      terrain/terrain_shader.gdshader

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

@ -0,0 +1,14 @@
[remap]
importer="image"
type="Image"
uid="uid://n7hbvgjiktlg"
path="res://.godot/imported/bricky_inner.png-b94122a758411344f70f7c2760d21808.image"
[deps]
source_file="res://assets/textures/terrain_blend/bricky_inner.png"
dest_files=["res://.godot/imported/bricky_inner.png-b94122a758411344f70f7c2760d21808.image"]
[params]

Binary file not shown.

After

Width:  |  Height:  |  Size: 639 B

@ -0,0 +1,14 @@
[remap]
importer="image"
type="Image"
uid="uid://c8ntkfa6t0t4a"
path="res://.godot/imported/bricky_outer.png-8d14c741e6e00049a6f23c5a7f0ba31a.image"
[deps]
source_file="res://assets/textures/terrain_blend/bricky_outer.png"
dest_files=["res://.godot/imported/bricky_outer.png-8d14c741e6e00049a6f23c5a7f0ba31a.image"]
[params]

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 B

@ -0,0 +1,14 @@
[remap]
importer="image"
type="Image"
uid="uid://cl5gio1e8md5p"
path="res://.godot/imported/bricky_straight.png-6b7be3b817156c5655dc51c502e77349.image"
[deps]
source_file="res://assets/textures/terrain_blend/bricky_straight.png"
dest_files=["res://.godot/imported/bricky_straight.png-6b7be3b817156c5655dc51c502e77349.image"]
[params]

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 B

@ -0,0 +1,14 @@
[remap]
importer="image"
type="Image"
uid="uid://bd65fhvitmpis"
path="res://.godot/imported/default.png-16f623a6df352f7770c4e23ca5d9106b.image"
[deps]
source_file="res://assets/textures/terrain_blend/default.png"
dest_files=["res://.godot/imported/default.png-16f623a6df352f7770c4e23ca5d9106b.image"]
[params]

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

@ -0,0 +1,14 @@
[remap]
importer="image"
type="Image"
uid="uid://cglldg71qap5f"
path="res://.godot/imported/grassy_inner.png-dbbcf6e38e5a9f6ac5aa0bd1d57058a3.image"
[deps]
source_file="res://assets/textures/terrain_blend/grassy_inner.png"
dest_files=["res://.godot/imported/grassy_inner.png-dbbcf6e38e5a9f6ac5aa0bd1d57058a3.image"]
[params]

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

@ -0,0 +1,14 @@
[remap]
importer="image"
type="Image"
uid="uid://cxwk1srj6obxh"
path="res://.godot/imported/grassy_outer.png-23c05e3ec7820ad524433733076341dc.image"
[deps]
source_file="res://assets/textures/terrain_blend/grassy_outer.png"
dest_files=["res://.godot/imported/grassy_outer.png-23c05e3ec7820ad524433733076341dc.image"]
[params]

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

@ -0,0 +1,14 @@
[remap]
importer="image"
type="Image"
uid="uid://cqgah04b0buil"
path="res://.godot/imported/grassy_straight.png-f11f767de0d78d943ad64f3bc3a0dc4c.image"
[deps]
source_file="res://assets/textures/terrain_blend/grassy_straight.png"
dest_files=["res://.godot/imported/grassy_straight.png-f11f767de0d78d943ad64f3bc3a0dc4c.image"]
[params]

File diff suppressed because it is too large Load Diff

@ -0,0 +1,29 @@
public static class SideAndCornerExtensions
{
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)),
};
}

@ -1,9 +1,9 @@
[Tool]
public partial class Terrain
{
bool _unhandledMouseMotion = false; // Used to detect when mouse moves off the terrain.
TilePos? _tileHover = null; // Position of currently hovered tile.
bool _isSelecting = false; // Whether left mouse is held down to select tiles.
bool _unhandledMotion = false; // Used to detect when mouse moves off the terrain.
TilePos? _tileHover = null; // Position of currently hovered tile.
bool _isSelecting = false; // Whether left mouse is held down to select tiles.
(TilePos, TilePos)? _selection = null;
public override void _Input(InputEvent ev)
@ -37,11 +37,14 @@ public partial class Terrain
var innerPos = selection.GetTileFor(innerCorner);
var outerPos = innerPos.GetNeighbor(innerCorner);
var innerHeight = GetCornerHeights(innerPos)[innerCorner];
var outerHeight = GetCornerHeights(outerPos)[outerCorner];
var outerTile = GetTile(outerPos);
var innerHeight = GetTile(innerPos).Height[innerCorner];
var outerHeight = outerTile.Height[outerCorner];
if (IsEqualApprox(outerHeight, innerHeight))
SetCornerHeight(outerPos, outerCorner, innerHeight + amount);
if (IsEqualApprox(outerHeight, innerHeight)) {
outerTile.Height[outerCorner] = innerHeight + amount;
SetTile(outerPos, outerTile);
}
}
// Raise connected sides.
@ -49,40 +52,59 @@ public partial class Terrain
foreach (var innerPos in selection.GetTilesFor(side)) {
var outerPos = innerPos.GetNeighbor(side);
var innerHeights = GetCornerHeights(innerPos);
var outerHeights = GetCornerHeights(outerPos);
var innerTile = GetTile(innerPos);
var outerTile = GetTile(outerPos);
var (innerCorner1, innerCorner2) = side.GetCorners();
var (outerCorner1, outerCorner2) = side.GetOpposite().GetCorners();
var current = outerHeights;
var changed = false;
var matchingCorners = new[]{ (innerCorner1, outerCorner1), (innerCorner2, outerCorner2) };
foreach (var (innerCorner, outerCorner) in matchingCorners) {
var innerHeight = innerHeights[innerCorner];
var outerHeight = outerHeights[outerCorner];
var innerHeight = innerTile.Height[innerCorner];
var outerHeight = outerTile.Height[outerCorner];
if (IsEqualApprox(outerHeight, innerHeight)) {
current = current.With(outerCorner, innerHeight + amount);
outerTile.Height[outerCorner] = innerHeight + amount;
changed = true;
}
}
if (changed) SetCornerHeights(outerPos, current);
if (changed) SetTile(outerPos, outerTile);
}
}
// Raise selected tiles themselves.
foreach (var tile in selection.GetAllTiles())
AdjustTileHeight(tile, amount);
foreach (var pos in selection.GetAllTiles()) {
var tile = GetTile(pos);
tile.Height.Adjust(amount);
SetTile(pos, tile);
}
UpdateMeshAndShape();
NotifyPropertyListChanged();
}
if ((ev is InputEventMouseButton { ButtonIndex: var wheel2, Pressed: var pressed2, CtrlPressed: true })
&& (wheel2 is MouseButton.WheelUp or MouseButton.WheelDown) && (_selection != null))
{
GetViewport().SetInputAsHandled();
if (!pressed2) return;
var amount = (wheel2 == MouseButton.WheelUp) ? 1 : -1;
var selection = TileRegion.From(_selection.Value);
foreach (var pos in selection.GetAllTiles()) {
var tile = GetTile(pos);
tile.TexturePrimary = PosMod(tile.TexturePrimary + amount, 4);
SetTile(pos, tile);
}
UpdateMeshAndShape();
NotifyPropertyListChanged();
}
if (ev is InputEventMouseMotion)
_unhandledMouseMotion = true;
_unhandledMotion = true;
}
public override void _InputEvent(Camera3D camera, InputEvent ev, Vector3 position, Vector3 normal, int shapeIdx)
@ -99,7 +121,7 @@ public partial class Terrain
}
if (ev is InputEventMouseMotion) {
_unhandledMouseMotion = false;
_unhandledMotion = false;
_tileHover = tilePos;
if (_isSelecting) _selection = _selection.Value with { Item2 = tilePos };
}
@ -113,7 +135,7 @@ public partial class Terrain
_isSelecting = false;
}
if (_unhandledMouseMotion)
if (_unhandledMotion)
_tileHover = null;
if ((_tileHover != null) || (_selection != null)) {
@ -135,7 +157,7 @@ public partial class Terrain
}
if (_tileHover is TilePos hover) {
var corners = GetCornersPosition(hover);
var corners = GetTileCornerPositions(hover);
var margin = 0.1f;
mesh.SurfaceSetColor(Colors.Black);
AddQuad(corners.TopLeft + new Vector3(-margin, 0, -margin),
@ -147,7 +169,7 @@ public partial class Terrain
mesh.SurfaceSetColor(Colors.Blue);
if (_selection is (TilePos, TilePos) selection)
foreach (var pos in TileRegion.From(selection).GetAllTiles()) {
var corners = GetCornersPosition(pos);
var corners = GetTileCornerPositions(pos);
AddQuad(corners.TopLeft, corners.TopRight, corners.BottomLeft, corners.BottomRight);
}

@ -26,6 +26,18 @@ public partial class Terrain
}
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);
@ -35,83 +47,6 @@ public partial class Terrain
=> 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()
{
@ -138,33 +73,51 @@ public partial class Terrain
AddPoint(v3, uv3);
}
var rnd = new Random();
// 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 corners = GetCornersPosition(new(x, 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 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.
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;
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));
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));
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));
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));
corners.BottomLeft , new(0.0f, 1.0f),
corners.TopLeft , new(0.0f, 0.0f));
}
}
@ -175,11 +128,25 @@ public partial class Terrain
}
public Corners<Vector3> GetTileCornerPositions(TilePos pos)
=> GetTileCornerPositions(pos, GetTile(pos));
public Corners<Vector3> 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));
}
ImmediateMesh GetOrCreateMesh(string name)
{
var meshInstance = (MeshInstance3D)GetNodeOrNull(name);
if (meshInstance == null) {
meshInstance = new() { Name = name, Mesh = new ImmediateMesh() };
meshInstance = new(){ Name = name, Mesh = new ImmediateMesh() };
AddChild(meshInstance);
meshInstance.Owner = this;
}
@ -190,125 +157,10 @@ public partial class Terrain
{
var collisionShape = (CollisionShape3D)GetNodeOrNull(name);
if (collisionShape == null) {
collisionShape = new() { Name = name, Shape = new ConcavePolygonShape3D() };
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)),
};
}

@ -0,0 +1,129 @@
using Dictionary = Godot.Collections.Dictionary;
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 struct Tile
{
public Corners<float> Height;
// TODO: Replace with enum or something more permanent?
public int TexturePrimary;
public int TextureSecondary;
public int TextureBlend;
public static Tile FromDictionary(Dictionary dict)
{
if (dict == null) return default;
float topLeft, topRight, bottomRight, bottomLeft;
switch (dict["heights"]) {
case { VariantType: Variant.Type.Float } variant:
var height = (float)variant;
(topLeft, topRight, bottomRight, bottomLeft) = (height, height, height, height);
break;
case { VariantType: Variant.Type.PackedFloat32Array } variant:
var heights = (float[])variant;
(topLeft, topRight, bottomRight, bottomLeft) = (heights[0], heights[1], heights[2], heights[3]);
break;
default: throw new Exception("Invalid variant type");
};
int texturePrimary, textureSecondary, textureBlend;
switch (dict["texture"]) {
case { VariantType: Variant.Type.Int } variant:
var texture = (int)variant;
(texturePrimary, textureSecondary, textureBlend) = (texture, 0, 0);
break;
case { VariantType: Variant.Type.PackedInt32Array } variant:
var textures = (int[])variant;
(texturePrimary, textureSecondary, textureBlend) = (textures[0], textures[1], textures[2]);
break;
default: throw new Exception("Invalid variant type");
};
return new(){
Height = new(topLeft, topRight, bottomRight, bottomLeft),
TexturePrimary = texturePrimary,
TextureSecondary = textureSecondary,
TextureBlend = textureBlend,
};
}
public readonly Dictionary ToDictionary()
{
if (Height.IsZeroApprox() && (TexturePrimary == 0) && (TextureBlend == 0))
return null;
return new(){
["heights"] = Height.IsEqualApprox() ? Height.TopLeft : new[]{ Height.TopLeft, Height.TopRight, Height.BottomRight, Height.BottomLeft },
["texture"] = (TextureBlend == 0) ? TexturePrimary : new[]{ TexturePrimary, TextureSecondary, TextureBlend },
};
}
}
public struct Corners<T>(T topLeft, T topRight, T bottomRight, T bottomLeft)
{
public T TopLeft = topLeft;
public T TopRight = topRight;
public T BottomRight = bottomRight;
public T BottomLeft = 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)),
};
set { 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 class CornersExtensions
{
public static void Adjust(this ref Corners<float> self, float amount)
{
self.TopLeft += amount;
self.TopRight += amount;
self.BottomRight += amount;
self.BottomLeft += amount;
}
public static bool IsZeroApprox(this Corners<float> self)
=> Mathf.IsZeroApprox(self.TopLeft ) && Mathf.IsZeroApprox(self.TopRight )
&& Mathf.IsZeroApprox(self.BottomRight) && Mathf.IsZeroApprox(self.BottomLeft);
public static bool IsEqualApprox(this Corners<float> self)
=> Mathf.IsEqualApprox(self.TopLeft, self.TopRight )
&& Mathf.IsEqualApprox(self.TopLeft, self.BottomRight)
&& Mathf.IsEqualApprox(self.TopLeft, self.BottomLeft );
}

@ -1,15 +1,28 @@
[gd_resource type="ShaderMaterial" load_steps=7 format=4 uid="uid://doe8owgx4jeu1"]
[gd_resource type="ShaderMaterial" load_steps=15 format=4 uid="uid://doe8owgx4jeu1"]
[ext_resource type="Shader" path="res://terrain/terrain_shader.gdshader" id="1_mf55c"]
[ext_resource type="Image" uid="uid://b0jp1dyxugbr7" path="res://assets/textures/terrain/grass.png" id="5_mau31"]
[ext_resource type="Image" uid="uid://bpo7mkr6sctqr" path="res://assets/textures/terrain/dirt.png" id="6_xc3ue"]
[ext_resource type="Image" uid="uid://dqyqg6yt7yk3k" path="res://assets/textures/terrain/rock.png" id="7_84wdo"]
[ext_resource type="Image" uid="uid://bkwjxg6g2itag" path="res://assets/textures/terrain/sand.png" id="8_3fpcn"]
[ext_resource type="Image" uid="uid://bd65fhvitmpis" path="res://assets/textures/terrain_blend/default.png" id="5_qdlds"]
[ext_resource type="Image" uid="uid://cqgah04b0buil" path="res://assets/textures/terrain_blend/grassy_straight.png" id="6_s3ccy"]
[ext_resource type="Image" uid="uid://cxwk1srj6obxh" path="res://assets/textures/terrain_blend/grassy_outer.png" id="7_wgbkx"]
[ext_resource type="Image" uid="uid://cglldg71qap5f" path="res://assets/textures/terrain_blend/grassy_inner.png" id="8_dm7dx"]
[ext_resource type="Image" uid="uid://cl5gio1e8md5p" path="res://assets/textures/terrain_blend/bricky_straight.png" id="9_fwpox"]
[ext_resource type="Image" uid="uid://c8ntkfa6t0t4a" path="res://assets/textures/terrain_blend/bricky_outer.png" id="10_kq5au"]
[ext_resource type="Image" uid="uid://n7hbvgjiktlg" path="res://assets/textures/terrain_blend/bricky_inner.png" id="11_0i2i8"]
[sub_resource type="Texture2DArray" id="Texture2DArray_3nq13"]
_images = Array[Image]([ExtResource("5_mau31"), ExtResource("6_xc3ue"), ExtResource("7_84wdo"), ExtResource("8_3fpcn")])
[sub_resource type="Texture2DArray" id="Texture2DArray_3nq14"]
_images = Array[Image]([ExtResource("5_qdlds"), ExtResource("6_s3ccy"), ExtResource("7_wgbkx"), ExtResource("8_dm7dx"), ExtResource("9_fwpox"), ExtResource("10_kq5au"), ExtResource("11_0i2i8")])
[resource]
render_priority = 0
shader = ExtResource("1_mf55c")
shader_parameter/textures = SubResource("Texture2DArray_3nq13")
shader_parameter/blend_textures = SubResource("Texture2DArray_3nq14")

@ -1,10 +1,22 @@
shader_type spatial;
render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_burley, specular_schlick_ggx;
uniform sampler2DArray textures : source_color, filter_linear_mipmap, repeat_enable;
uniform sampler2DArray textures : source_color, filter_linear_mipmap, repeat_enable;
uniform sampler2DArray blend_textures : source_color, filter_nearest_mipmap, repeat_enable;
void fragment() {
ALBEDO = texture(textures, vec3(UV, UV2.x)).rgb;
int num_textures = textureSize(textures, 0).z;
int num_blend_textures = textureSize(blend_textures, 0).z;
float tex_primary_index = COLOR.r * float(num_textures);
float tex_secondary_index = COLOR.g * float(num_textures);
float tex_blend_index = COLOR.b * float(num_blend_textures);
vec4 tex_primary = texture(textures, vec3(UV, tex_primary_index));
vec4 tex_secondary = texture(textures, vec3(UV, tex_secondary_index));
vec4 blend = texture(blend_textures, vec3(UV, tex_blend_index));
ALBEDO = mix(tex_secondary, tex_primary, blend.a).rgb;
METALLIC = 0.0;
SPECULAR = 0.5;

Loading…
Cancel
Save