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.
204 lines
6.4 KiB
204 lines
6.4 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 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(); |
|
} |
|
|
|
|
|
/// <summary> Returns if this terrain grid contains the specified tile position. </summary> |
|
public bool ContainsTilePos(Vector2I tilePos) |
|
=> (tilePos.X >= 0) && (tilePos.X < Size.X) |
|
&& (tilePos.Y >= 0) && (tilePos.Y < Size.Y); |
|
|
|
/// <summary> Transforms a 3D position local to the equivalent tile position. </summary> |
|
public Vector2I ToTilePos(Vector3 localPos) |
|
=> new(RoundToInt(localPos.X / TileSize + Size.X / 2.0f), |
|
RoundToInt(localPos.Z / TileSize + Size.Y / 2.0f)); |
|
|
|
public float GetCornerHeight(Vector2I tilePos, Corner corner) |
|
=> GetTileRaw(tilePos) switch { |
|
{ VariantType: Variant.Type.Nil } => 0, |
|
{ VariantType: Variant.Type.Float } num => (float)num, |
|
{ VariantType: Variant.Type.PackedFloat32Array } nums => ((float[])nums)[(int)corner], |
|
_ => throw new Exception("Invalid type in Points dictionary"), |
|
}; |
|
|
|
public void SetCornerHeight(Vector2I tilePos, Corner corner, float value) |
|
{ |
|
if (!ContainsTilePos(tilePos)) return; |
|
var existing = GetTileRaw(tilePos) switch { |
|
{ VariantType: Variant.Type.Nil } => [ 0, 0, 0, 0 ], |
|
{ VariantType: Variant.Type.Float } num => [ (float)num, (float)num, (float)num, (float)num ], |
|
{ VariantType: Variant.Type.PackedFloat32Array } nums => (float[])nums, |
|
_ => throw new Exception("Invalid type in Points dictionary"), |
|
}; |
|
existing[(int)corner] = value; |
|
if (existing.All(IsZeroApprox)) |
|
Tiles?.Remove(tilePos); |
|
if (IsEqualApprox(existing[0], existing[1]) |
|
&& IsEqualApprox(existing[0], existing[2]) |
|
&& IsEqualApprox(existing[0], existing[3])) |
|
SetTileRaw(tilePos, existing[0]); |
|
else |
|
SetTileRaw(tilePos, existing); |
|
} |
|
|
|
public void SetTileHeight(Vector2I tilePos, float value) |
|
{ |
|
if (!ContainsTilePos(tilePos)) return; |
|
if (IsZeroApprox(value)) Tiles?.Remove(tilePos); |
|
else SetTileRaw(tilePos, value); |
|
} |
|
|
|
public void AdjustTileHeight(Vector2I tilePos, float relative) |
|
{ |
|
if (relative == 0) return; |
|
if (!ContainsTilePos(tilePos)) return; |
|
switch (GetTileRaw(tilePos)) { |
|
case { VariantType: Variant.Type.Nil }: |
|
SetTileRaw(tilePos, relative); |
|
break; |
|
case { VariantType: Variant.Type.Float } num: |
|
var newNum = (float)num + relative; |
|
if (IsZeroApprox(newNum)) Tiles?.Remove(tilePos); |
|
else SetTileRaw(tilePos, newNum); |
|
break; |
|
case { VariantType: Variant.Type.PackedFloat32Array } nums: |
|
var newNums = (float[])nums; |
|
for (var i = 0; i < 4; i++) newNums[i] += relative; |
|
SetTileRaw(tilePos, newNums); |
|
break; |
|
default: throw new Exception("Invalid type in Points dictionary"); |
|
}; |
|
} |
|
|
|
public GridCorners GetGridCorners(Vector2I tilePos) |
|
{ |
|
var halfSize = TileSize / 2; |
|
var vx = (tilePos.X - Size.X / 2.0f) * TileSize; |
|
var vz = (tilePos.Y - Size.Y / 2.0f) * TileSize; |
|
|
|
return new() { |
|
TopLeft = new(vx - halfSize, GetCornerHeight(tilePos, Corner.TopLeft ), vz - halfSize), |
|
TopRight = new(vx + halfSize, GetCornerHeight(tilePos, Corner.TopRight ), vz - halfSize), |
|
BottomLeft = new(vx - halfSize, GetCornerHeight(tilePos, Corner.BottomLeft ), vz + halfSize), |
|
BottomRight = new(vx + halfSize, GetCornerHeight(tilePos, Corner.BottomRight), vz + halfSize), |
|
}; |
|
} |
|
|
|
Variant GetTileRaw(Vector2I tilePos) |
|
=> (Tiles?.TryGetValue(tilePos, out var result) == true) ? result : default; |
|
|
|
void SetTileRaw(Vector2I tilePos, Variant value) |
|
=> (Tiles ??= [])[tilePos] = value; |
|
|
|
|
|
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 = GetGridCorners(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 enum Corner |
|
{ |
|
TopLeft, |
|
TopRight, |
|
BottomLeft, |
|
BottomRight, |
|
} |
|
|
|
public readonly struct GridCorners |
|
{ |
|
public Vector3 TopLeft { get; init; } |
|
public Vector3 TopRight { get; init; } |
|
public Vector3 BottomLeft { get; init; } |
|
public Vector3 BottomRight { get; init; } |
|
} |
|
}
|
|
|