Terrain storage changes

- Empty (default) tiles will not be meshed,
  they have to first be "drawn in" somehow
- Terrain data is stored in ... `TerrainData`
  Can be exported to separate resource file
- Each chunks is stored in a `TerrainChunk`
- Height now stored as a short,
  and `TileStep` can be adjusted
- Corners implements IEquatable
main
copygirl 3 months ago
parent 6a710a7c05
commit 52cc3fa28d
  1. 10
      addons/terrain-editing/terrain_editing_controls.gd
  2. 1890
      level.tscn
  3. 29
      level_terrain_data.tres
  4. 119
      terrain/Terrain+Editing.cs
  5. 245
      terrain/Terrain.cs
  6. 31
      terrain/TerrainChunk.cs
  7. 21
      terrain/TerrainData.cs
  8. 148
      terrain/Tile.cs

@ -28,8 +28,8 @@ var tool_shape : ToolShape:
set(value): set_pressed(tool_shape_buttons, value); tool_shape_changed()
## Gets or sets the currently selected texture to paint with.
var texture : int:
get: return index_of_pressed(paint_texture_buttons)
set(value): set_pressed(paint_texture_buttons, value)
get: return index_of_pressed(paint_texture_buttons) + 1
set(value): set_pressed(paint_texture_buttons, value - 1)
## Gets or sets the current draw size for CIRCLE or SQUARE shapes.
var draw_size : int:
get: return roundi(-draw_size_slider.value)
@ -45,9 +45,9 @@ var is_connected : bool:
func _ready() -> void:
# Update 'tool_mode', 'tool_shape' or 'texture' when any of the buttons are pressed.
for i in len(tool_mode_buttons ): tool_mode_buttons [i].pressed.connect(func(): tool_mode = i)
for i in len(tool_shape_buttons ): tool_shape_buttons [i].pressed.connect(func(): tool_shape = i)
for i in len(paint_texture_buttons): paint_texture_buttons[i].pressed.connect(func(): texture = i)
for i in len(tool_mode_buttons ): tool_mode_buttons [i].pressed.connect(func(): tool_mode = i )
for i in len(tool_shape_buttons ): tool_shape_buttons [i].pressed.connect(func(): tool_shape = i )
for i in len(paint_texture_buttons): paint_texture_buttons[i].pressed.connect(func(): texture = i + 1)
# Update 'draw_size_label' whenever the slider changes.
draw_size_slider.value_changed.connect(func(_value):

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

@ -3,10 +3,12 @@ using System.Runtime.InteropServices;
public partial class Terrain
{
// These mirror the modes / shapes in 'terrain_editing_controls.gd'.
enum ToolMode { Height, Flatten, Paint }
enum ToolShape { Corner, Circle, Square }
// Set by the terrain editing plugin.
// Enables access to the in-editor undo/redo system.
public EditorUndoRedoManager EditorUndoRedo { get; set; }
// Dummy value to satisfy the overly careful compiler.
@ -34,7 +36,7 @@ public partial class Terrain
var toolMode = (ToolMode)(int)controls.Get("tool_mode");
var toolShape = (ToolShape)(int)controls.Get("tool_shape");
var texture = (int)controls.Get("texture");
var texture = (byte)(int)controls.Get("texture");
var drawSize = (int)controls.Get("draw_size");
var isRaise = (bool)controls.Get("is_raise");
@ -49,37 +51,37 @@ public partial class Terrain
// Offset hover tile position by corner.
// FIXME: This causes FLATTEN to calculate the wrong height in some cases.
if (isEven) hover.Position = hover.Corner switch {
Corner.TopLeft => hover.Position.Offset(0, 0),
Corner.TopRight => hover.Position.Offset(1, 0),
Corner.BottomRight => hover.Position.Offset(1, 1),
Corner.BottomLeft => hover.Position.Offset(0, 1),
if (isEven) hover.Position += hover.Corner switch {
Corner.TopLeft => (0, 0),
Corner.TopRight => (1, 0),
Corner.BottomRight => (1, 1),
Corner.BottomLeft => (0, 1),
_ => throw new InvalidOperationException(),
};
IEnumerable<TilePos> GetTilesInSquare() {
var min = hover.Position.Offset(-radius, -radius);
var max = hover.Position.Offset(+radius, +radius);
if (isEven) max = max.Offset(-1, -1);
var min = hover.Position + (-radius, -radius);
var max = hover.Position + (+radius, +radius);
if (isEven) max += (-1, -1);
for (var x = min.X; x <= max.X; x++)
for (var y = min.Y; y <= max.Y; y++)
yield return new(x, y);
for (var z = min.Z; z <= max.Z; z++)
yield return new(x, z);
}
IEnumerable<TilePos> GetTilesInRadius() {
var center = isEven ? hover.Position.ToVector2I()
: hover.Position.ToCenter();
var distanceSqr = Pow(radius + 0.25f * (isEven ? -1 : 1), 2);
var center = isEven ? (Vector2I)hover.Position
: hover.Position.Center;
var distanceSqr = Pow(radius + (isEven ? -1 : 1) * 0.25f, 2);
return GetTilesInSquare().Where(tile =>
center.DistanceSquaredTo(tile.ToCenter()) < distanceSqr);
center.DistanceSquaredTo(tile.Center) < distanceSqr);
}
// TODO: Allow click-dragging which doesn't affect already changed tiles / corners.
// TODO: Use ArrayMesh instead of ImmediateMesh.
// TODO: Dynamically expand terrain instead of having it be a set size.
// Holds onto all the tiles and which of their corners corners will be affected by this edit operation.
var tilesToChange = new Dictionary<TilePos, Corners<bool>>();
// Don't look at this black magic. The Dictionary type should have this by default I swear!
// Basically, this returns a reference to an entry in the dictionary that can be modified directly.
ref Corners<bool> Tile(TilePos position) => ref CollectionsMarshal.GetValueRefOrAddDefault(tilesToChange, position, out _dummy);
@ -109,9 +111,9 @@ public partial class Terrain
// If the 'connected_toggle' button is active, move "connected" corners.
// This is a simplified version of the code below that only affects the 3 neighboring corners.
if (isConnected) {
var height = GetTile(hover.Position).Height[hover.Corner];
var height = Data.GetTileOrDefault(hover.Position).Height[hover.Corner];
foreach (var neighbor in GetNeighbors(hover.Position, hover.Corner)) {
var neighborHeight = GetTile(neighbor.Position).Height[neighbor.Corner];
var neighborHeight = Data.GetTileOrDefault(neighbor.Position).Height[neighbor.Corner];
if (neighborHeight != height) continue;
Tile(neighbor.Position)[neighbor.Corner] = true;
}
@ -132,12 +134,12 @@ public partial class Terrain
// If the 'connected_toggle' button is active, move "connected" corners.
// Connected corners are the ones that are at the same height as ones already being moved.
if (isConnected) foreach (var pos in tiles) {
var tile = GetTile(pos);
var tile = Data.GetTileOrDefault(pos);
foreach (var corner in Enum.GetValues<Corner>()) {
var height = tile.Height[corner];
foreach (var neighbor in GetNeighbors(pos, corner)) {
if (tiles.Contains(neighbor.Position)) continue;
var neighborHeight = GetTile(neighbor.Position).Height[neighbor.Corner];
var neighborHeight = Data.GetTileOrDefault(neighbor.Position).Height[neighbor.Corner];
if (neighborHeight != height) continue;
Tile(neighbor.Position)[neighbor.Corner] = true;
}
@ -157,11 +159,11 @@ public partial class Terrain
if (toolMode == ToolMode.Paint) {
// TODO: Support blending somehow.
var tilesPrevious = new List<(TilePos, int)>();
var tilesChanged = new List<(TilePos, int)>();
var tilesPrevious = new List<(TilePos, byte)>();
var tilesChanged = new List<(TilePos, byte)>();
foreach (var (pos, corners) in tilesToChange) {
var tile = GetTile(pos);
var tile = Data.GetTileOrDefault(pos);
tilesPrevious.Add((pos, tile.TexturePrimary));
tilesChanged.Add((pos, texture));
}
@ -171,22 +173,28 @@ public partial class Terrain
doArgs = [ PackTextureData(tilesChanged) ];
undoArgs = [ PackTextureData(tilesPrevious) ];
} else {
var tilesPrevious = new List<(TilePos, Corners<float>)>();
var tilesChanged = new List<(TilePos, Corners<float>)>();
var tilesPrevious = new List<(TilePos, Corners<short>)>();
var tilesChanged = new List<(TilePos, Corners<short>)>();
const float AdjustHeight = 0.5f;
var amount = isFlatten ? GetTile(hover.Position).Height[hover.Corner]
: isRaise ? AdjustHeight : -AdjustHeight;
var amount = isFlatten ? Data.GetTileOrDefault(hover.Position).Height[hover.Corner]
: isRaise ? (short)+1 : (short)-1;
foreach (var (pos, corners) in tilesToChange) {
var tile = GetTile(pos);
var tile = Data.GetTileOrDefault(pos);
tilesPrevious.Add((pos, tile.Height));
var newHeight = tile.Height;
if (corners.TopLeft ) newHeight.TopLeft = isFlatten ? amount : newHeight.TopLeft + amount;
if (corners.TopRight ) newHeight.TopRight = isFlatten ? amount : newHeight.TopRight + amount;
if (corners.BottomRight) newHeight.BottomRight = isFlatten ? amount : newHeight.BottomRight + amount;
if (corners.BottomLeft ) newHeight.BottomLeft = isFlatten ? amount : newHeight.BottomLeft + amount;
if (isFlatten) {
if (corners.TopLeft ) newHeight.TopLeft = amount;
if (corners.TopRight ) newHeight.TopRight = amount;
if (corners.BottomRight) newHeight.BottomRight = amount;
if (corners.BottomLeft ) newHeight.BottomLeft = amount;
} else {
if (corners.TopLeft ) newHeight.TopLeft += amount;
if (corners.TopRight ) newHeight.TopRight += amount;
if (corners.BottomRight) newHeight.BottomRight += amount;
if (corners.BottomLeft ) newHeight.BottomLeft += amount;
}
tilesChanged.Add((pos, newHeight));
}
@ -222,20 +230,14 @@ public partial class Terrain
public void DoModifyTerrainHeight(byte[] data)
{
foreach (var (pos, corners) in UnpackHeightData(data)) {
var tile = GetTile(pos);
tile.Height = corners;
SetTile(pos, tile);
}
foreach (var (pos, corners) in UnpackHeightData(data))
Data[pos].Height = corners;
}
public void DoModifyTerrainTexture(byte[] data)
{
foreach (var (pos, texture) in UnpackTextureData(data)) {
var tile = GetTile(pos);
tile.TexturePrimary = texture;
SetTile(pos, tile);
}
foreach (var (pos, texture) in UnpackTextureData(data))
Data[pos].TexturePrimary = texture;
}
@ -254,7 +256,7 @@ public partial class Terrain
}
foreach (var (tile, visible) in tiles) {
var positions = GetTileCornerPositions(tile);
var positions = ToPositions(tile);
foreach (var side in Enum.GetValues<Side>()) {
var (corner1, corner2) = side.GetCorners();
if (!visible[corner1] && !visible[corner2]) continue;
@ -274,8 +276,7 @@ public partial class Terrain
(TilePos Position, Corner Corner) ToTilePos(Vector3 position)
{
var local = ToLocal(position);
var coord = new Vector2(local.X, local.Z) / TileSize + (Size + Vector2.One) / 2;
var pos = TilePos.From(coord);
var coord = new Vector2(local.X, local.Z) / TileSize;
var corner = coord.PosMod(1).RoundToVector2I() switch {
(0, 0) => Corner.TopLeft,
(1, 0) => Corner.TopRight,
@ -283,26 +284,26 @@ public partial class Terrain
(0, 1) => Corner.BottomLeft,
_ => throw new InvalidOperationException(),
};
return (pos, corner);
return ((TilePos)coord, corner);
}
static readonly Dictionary<Corner, (int X, int Y, Corner Opposite)[]> _offsetLookup = new(){
static readonly Dictionary<Corner, (int X, int Z, Corner Opposite)[]> _offsetLookup = new(){
[Corner.TopLeft ] = [(-1, -1, Corner.BottomRight), (-1, 0, Corner.TopRight ), (0, -1, Corner.BottomLeft )],
[Corner.TopRight ] = [(+1, -1, Corner.BottomLeft ), (+1, 0, Corner.TopLeft ), (0, -1, Corner.BottomRight)],
[Corner.BottomRight] = [(+1, +1, Corner.TopLeft ), (+1, 0, Corner.BottomLeft ), (0, +1, Corner.TopRight )],
[Corner.BottomLeft ] = [(-1, +1, Corner.TopRight ), (-1, 0, Corner.BottomRight), (0, +1, Corner.TopLeft )],
};
static IEnumerable<(TilePos Position, Corner Corner)> GetNeighbors(TilePos pos, Corner corner)
=> _offsetLookup[corner].Select(e => (new TilePos(pos.X + e.X, pos.Y + e.Y), e.Opposite));
=> _offsetLookup[corner].Select(e => (new TilePos(pos.X + e.X, pos.Z + e.Z), e.Opposite));
static byte[] PackHeightData(IEnumerable<(TilePos Position, Corners<float> Corners)> data)
static byte[] PackHeightData(IEnumerable<(TilePos Position, Corners<short> Corners)> data)
{
using var stream = new MemoryStream();
using var writer = new BinaryWriter(stream);
foreach (var (pos, corners) in data) {
writer.Write(pos.X);
writer.Write(pos.Y);
writer.Write(pos.Z);
writer.Write(corners.TopLeft);
writer.Write(corners.TopRight);
writer.Write(corners.BottomRight);
@ -311,33 +312,33 @@ public partial class Terrain
return stream.ToArray();
}
static IEnumerable<(TilePos Position, Corners<float> Corners)> UnpackHeightData(byte[] data)
static IEnumerable<(TilePos Position, Corners<short> Corners)> UnpackHeightData(byte[] data)
{
using var stream = new MemoryStream(data);
using var reader = new BinaryReader(stream);
while (stream.Position < stream.Length) {
var x = reader.ReadInt32();
var y = reader.ReadInt32();
var corners = new Corners<float>(
reader.ReadSingle(), reader.ReadSingle(),
reader.ReadSingle(), reader.ReadSingle());
var corners = new Corners<short>(
reader.ReadInt16(), reader.ReadInt16(),
reader.ReadInt16(), reader.ReadInt16());
yield return (new(x, y), corners);
}
}
static byte[] PackTextureData(IEnumerable<(TilePos Position, int Texture)> data)
static byte[] PackTextureData(IEnumerable<(TilePos Position, byte Texture)> data)
{
using var stream = new MemoryStream();
using var writer = new BinaryWriter(stream);
foreach (var (pos, texture) in data) {
writer.Write(pos.X);
writer.Write(pos.Y);
writer.Write((byte)texture);
writer.Write(pos.Z);
writer.Write(texture);
}
return stream.ToArray();
}
static IEnumerable<(TilePos Position, int Texture)> UnpackTextureData(byte[] data)
static IEnumerable<(TilePos Position, byte Texture)> UnpackTextureData(byte[] data)
{
using var stream = new MemoryStream(data);
using var reader = new BinaryReader(stream);

@ -2,38 +2,16 @@
public partial class Terrain
: StaticBody3D
{
[Export] public Vector2I Size { get; set; } = new(64, 64);
[Export] public float TileSize { get; set; } = 2.0f;
[Export] public float TileStep { get; set; } = 0.5f;
[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; }
[Export] public TerrainData Data { get; set; } = new();
[Export] public ShaderMaterial Material { get; set; }
public override void _Ready()
=> UpdateMeshAndShape();
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);
public void UpdateMeshAndShape()
{
var mesh = GetOrCreateMesh("MeshInstance");
@ -41,7 +19,7 @@ public partial class Terrain
mesh.ClearSurfaces();
mesh.SurfaceBegin(Mesh.PrimitiveType.Triangles);
var points = new List<Vector3>();
var points = new List<Vector3>(); // for CollisionShape
void AddPoint(Vector3 pos, Vector2 uv) {
mesh.SurfaceSetUV(uv);
@ -60,103 +38,102 @@ public partial class Terrain
}
// 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 tile = GetTile(new(x, z));
var corners = GetTileCornerPositions(new(x, z), tile);
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 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));
}
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.
mesh.SurfaceSetColor(new(
(float)2 / num_textures,
(float)2 / num_textures,
(float)0 / num_blend_textures
));
void DrawWall(TilePos nbrPos, Side side) {
var nbrTile = GetTile(nbrPos);
var nbrCorners = GetTileCornerPositions(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;
// 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;
}
}
}
if (x < Size.X - 1) DrawWall(new(x + 1, z), Side.Right);
if (z < Size.Y - 1) DrawWall(new(x, z + 1), Side.Bottom);
}
DrawWall(pos + (1, 0), Side.Right);
DrawWall(pos + (0, 1), Side.Bottom);
}
}
mesh.SurfaceEnd();
mesh.SurfaceSetMaterial(0, Material);
@ -165,20 +142,6 @@ 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));
}
public ImmediateMesh GetOrCreateMesh(string name)
{
var meshInstance = (MeshInstance3D)GetNodeOrNull(name);
@ -200,4 +163,18 @@ public partial class Terrain
}
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));
}
}

@ -0,0 +1,31 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
[Tool]
[GlobalClass]
public partial class TerrainChunk
: Resource
{
// ChunkShift of 5 results in a ChunkSize of 32.
public const int Shift = 5;
public const int Mask = ~(~0 << Shift);
public const int Size = Mask + 1;
static readonly int SizeInBytes = Size * Size * Unsafe.SizeOf<Tile>();
[Export] public byte[] Data { get; set; } = new byte[SizeInBytes];
public ref Tile this[TilePos pos] { get {
var tiles = MemoryMarshal.Cast<byte, Tile>(Data);
return ref tiles[GetIndex(pos)];
} }
public static Vector2I ToChunkPos(TilePos pos)
=> new(pos.X >> Shift, pos.Z >> Shift);
public static Vector2I ToTileOffset(Vector2I chunkPos)
=> new(chunkPos.X << Shift, chunkPos.Y << Shift);
static int GetIndex(TilePos pos)
=> (pos.X & Mask) | ((pos.Z & Mask) << Shift);
}

@ -0,0 +1,21 @@
[Tool]
[GlobalClass]
public partial class TerrainData
: Resource
{
[Export] public Godot.Collections.Dictionary<Vector2I, TerrainChunk> Chunks { get; set; } = [];
public ref Tile this[TilePos pos] { get {
var chunkPos = TerrainChunk.ToChunkPos(pos);
if (!Chunks.TryGetValue(chunkPos, out var chunk))
Chunks.Add(chunkPos, chunk = new());
return ref chunk[pos];
} }
public Tile GetTileOrDefault(TilePos pos)
{
var chunkPos = TerrainChunk.ToChunkPos(pos);
return Chunks.TryGetValue(chunkPos, out var chunk)
? chunk[pos] : default;
}
}

@ -1,94 +1,40 @@
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 TilePos Offset(Vector2I value) => Offset(value.X, value.Y);
public TilePos Offset(int x, int y) => new(X + x, Y + y);
public static TilePos From(Vector2I value) => new(value.X, value.Y);
public static TilePos From(Vector2 value) => new(FloorToInt(value.X), FloorToInt(value.Y));
public Vector2I ToVector2I() => new(X, Y);
public Vector2 ToCenter() => new(X + 0.5f, Y + 0.5f);
}
public struct Tile
{
public Corners<float> Height;
public Corners<short> 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;
public byte TexturePrimary;
public byte TextureSecondary;
public byte TextureBlend;
public readonly bool IsDefault
=> Height == default
&& TexturePrimary == 0
&& TextureSecondary == 0
&& TextureBlend == 0;
public readonly override string ToString()
=> $"Tile {{ Height = {Height}, TexturePrimary = {TexturePrimary}, TextureSecondary = {TextureSecondary}, TextureBlend = {TextureBlend} }}";
}
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");
};
public readonly record struct TilePos(int X, int Z)
{
public Vector2 Center => new(X + 0.5f, Z + 0.5f);
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");
};
public static TilePos operator +(TilePos left, Vector2I right) => new(left.X + right.X, left.Z + right.Y);
public static TilePos operator +(TilePos left, (int X, int Y) right) => new(left.X + right.X, left.Z + right.Y);
return new(){
Height = new(topLeft, topRight, bottomRight, bottomLeft),
TexturePrimary = texturePrimary,
TextureSecondary = textureSecondary,
TextureBlend = textureBlend,
};
}
public static TilePos operator -(TilePos left, Vector2I right) => new(left.X - right.X, left.Z - right.Y);
public static TilePos operator -(TilePos left, (int X, int Y) right) => new(left.X - right.X, left.Z - right.Y);
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 static explicit operator TilePos(Vector2I value) => new(value.X, value.Y);
public static explicit operator TilePos(Vector2 value) => new(FloorToInt(value.X), FloorToInt(value.Y));
public static explicit operator Vector2I(TilePos pos) => new(pos.X, pos.Z);
}
public struct Corners<T>(T topLeft, T topRight, T bottomRight, T bottomLeft)
: IEquatable<Corners<T>>
where T : IEquatable<T>
{
public T TopLeft = topLeft;
public T TopRight = topRight;
@ -115,33 +61,23 @@ public struct Corners<T>(T topLeft, T topRight, T bottomRight, T bottomLeft)
} }
}
public readonly void Deconstruct(
out T topLeft , out T topRight,
out T bottomRight, out T bottomLeft)
{
topLeft = TopLeft;
topRight = TopRight;
bottomRight = BottomRight;
bottomLeft = BottomLeft;
}
}
public readonly T[] ToArray()
=> [ TopLeft, TopRight, BottomRight, BottomLeft ];
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 readonly bool Equals(Corners<T> other)
=> TopLeft .Equals(other.TopLeft )
&& TopRight .Equals(other.TopRight )
&& BottomRight.Equals(other.BottomRight)
&& BottomLeft .Equals(other.BottomLeft );
public readonly override bool Equals(object obj)
=> (obj is Corners<T> other) && Equals(other);
public readonly override int GetHashCode()
=> HashCode.Combine(TopLeft, TopRight, BottomRight, BottomLeft);
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 readonly override string ToString()
=> $"Corners(TopLeft: {TopLeft}, TopRight: {TopRight}, BottomRight: {BottomRight}, BottomLeft: {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 );
public static bool operator ==(Corners<T> left, Corners<T> right) => left.Equals(right);
public static bool operator !=(Corners<T> left, Corners<T> right) => !left.Equals(right);
}

Loading…
Cancel
Save