using System; using System.Collections.Generic; using Godot; using System.Diagnostics; using System.Linq; using System.Threading; using adatonic; using Node = adatonic.Node; using Timer = System.Timers.Timer; public class PlanetHelper { public static float RandF(float min, float max) { return min + (max - min) * Random.Shared.NextSingle(); } public class PlateData(int Id = 0, Color Color = new(), bool IsLandform = false, List Vertices = null) { public int Id { get; set; } = Id; public Color Color { get; set; } = Color; public bool IsLandform { get; set; } = IsLandform; public List Vertices { get; set; } = Vertices; public int CenterVertexId = -1; public float PlateExpansion { get; set; } = RandF(0.5f, 2f); public Vector3 Dir { get; set; } = Vector3.Zero; } public class VertexData(int Id = 0, int PlateId = 0, List Neighbours = null, bool StageComplete = false) { public int Id { get; set; } = Id; public int PlateId { get; set; } = PlateId; public List StrainSamples { get; set; } = new(); public List Neighbours { get; set; } = Neighbours; public bool StageComplete { get; set; } = StageComplete; public bool IsEdge = false; public bool IsTypeEdge = false; public float EdgeDistance = -1f; public float Height = 0f; } public enum StrainType { Tension, Compression, Shear } public class StrainAnalysis { public float Magnitude; public StrainType Type; public float NormalRate; public float ShearRate; } private bool StageComplete = true; private int _plateCount = 14; private float _landRatio = 0.4f; public List Plates = new List(); public List Vertices = new List(); public double StageHangTime = 1.0; public bool AutoRun = false; public bool Advance = false; public int TesselationLevel = 4; Stopwatch _generationStopwatch = new Stopwatch(); private FastNoiseLite _continentalNoise; private FastNoiseLite _mountainNoise; private FastNoiseLite _hfNoise; public enum GenerationStage { NotStarted, Initialization, PlateGeneration, BorderSearch, EdgeDistanceCalculation, EdgeStressCalculation, SpreadStress, HeightCalculation, Completed, } private bool _waiting = false; public GenerationStage Stage = GenerationStage.NotStarted; public GenerationStage StopStage = GenerationStage.Completed; private MeshInstance3D _meshInstance; private TextureRect _textureRect; private ArrayMesh _arrayMesh; public MeshDataTool Mdt; public Oct Octree = new Oct(); public PlanetHelper(MeshInstance3D meshInstance, TextureRect textureRect) { _meshInstance = meshInstance; _arrayMesh = meshInstance.Mesh as ArrayMesh; _textureRect = textureRect; _continentalNoise = new FastNoiseLite(); _mountainNoise = new FastNoiseLite(); _hfNoise = new FastNoiseLite(); Mdt = new MeshDataTool(); Mdt.CreateFromSurface(_arrayMesh, 0); for (int i = 0; i < Mdt.GetVertexCount(); i++) { Octree.Insert(new Node(i, Mdt.GetVertex(i) * 0.001f)); Mdt.SetVertexColor(i, Colors.Black); } if (_meshInstance.GetSurfaceOverrideMaterial(0) is ShaderMaterial shaderMaterial) { shaderMaterial.SetShaderParameter("mode", 1); } if (_textureRect.Material is ShaderMaterial textureShaderMaterial) { textureShaderMaterial.SetShaderParameter("mode", 1); } UpdateMesh(); } public void InitializeGeneration() { Plates = new(); Vertices = new(); Mdt.CreateFromSurface(_arrayMesh, 0); for (int i = 0; i < Mdt.GetVertexCount(); i++) { // Init to black Mdt.SetVertexColor(i, Colors.Black); Vertices.Add(new VertexData(i, -1,GetNeighboringVertices(i, false).OrderBy(v => Guid.NewGuid()).ToList())); } // Initialize Plates for (int i = 0; i < _plateCount; i++) { // Get a random un-assigned vertex. VertexData vertex = Vertices.Where(v => v.PlateId == -1).OrderBy(v => Guid.NewGuid()).First(); vertex.PlateId = i; var color = new Color(RandF(0f, 1f), RandF(0f, 1f), RandF(0f, 1f)); ColorVertex(vertex.Id, color); PlateData plate = new PlateData(i, color, false, [vertex.Id]); plate.Dir = GetRandomTangentialVelocity(Mdt.GetVertex(vertex.Id), RandF(0f, 1f)); Plates.Add(plate); } CompleteStage(); } public IEnumerable GetNeighboringVertices(int vertexId, bool blackOnly = true) { if (Stage != GenerationStage.Initialization) { if (blackOnly) return Vertices[vertexId].Neighbours.Where(n => Vertices[n].PlateId == -1); return Vertices[vertexId].Neighbours; } var verts = Mdt.GetVertexEdges(vertexId).AsEnumerable().SelectMany(edge => [Mdt.GetEdgeVertex(edge, 0), Mdt.GetEdgeVertex(edge, 1)]).Distinct().Where(v => v != vertexId); if (!blackOnly) return verts.Except([vertexId]); return verts.Where(v => Mdt.GetVertexColor(v) == Colors.Black).Except([vertexId]); } public Vector3 GetRandomTangentialVelocity(Vector3 pointOnSphere, float speed) { Vector3 normal = pointOnSphere.Normalized(); Random rand = new Random(); Vector3 randomVec = new Vector3( (float)(rand.NextDouble() - 0.5), // Range -0.5 to 0.5 (float)(rand.NextDouble() - 0.5), (float)(rand.NextDouble() - 0.5) ); Vector3 tangent = randomVec.Cross(normal); if (tangent.Dot(tangent) < 1e-6f) { randomVec = new Vector3(0, 1, 0); tangent = randomVec.Cross(normal); } Vector3 normalizedTangent = tangent.Normalized(); return normalizedTangent * speed; } public void ToggleAutoRun() { AutoRun = !AutoRun; } public void ToggleAdvance() { Advance = !Advance; } public void AdvanceStage() { Advance = false; if (_waiting) return; Timer timer = new(Mathf.Clamp(StageHangTime, 0.1, 10.0)); timer.Elapsed += (o, e) => { GenerationStage stage = Stage + 1; Stage = Stage == StopStage ? GenerationStage.Completed : stage; if (stage == GenerationStage.Completed) { AutoRun = false; _generationStopwatch.Stop(); } else { _generationStopwatch.Restart(); } GD.Print($"Stage Started: '{Stage.ToString()}'"); _waiting = false; StageComplete = false; }; timer.AutoReset = false; timer.Start(); _waiting = true; } public void Process() { if (!StageComplete) { switch (Stage) { default: case GenerationStage.NotStarted: break; case GenerationStage.Completed: break; case GenerationStage.Initialization: InitializeGeneration(); break; case GenerationStage.PlateGeneration: PlateGeneration(); break; case GenerationStage.BorderSearch: BorderSearch(); break; case GenerationStage.EdgeDistanceCalculation: EdgeDistanceCalculation(); break; case GenerationStage.EdgeStressCalculation: EdgeStressCalculation(); break; case GenerationStage.SpreadStress: SpreadStress(); break; case GenerationStage.HeightCalculation: HeightCalculation(); break; } UpdateMesh(); } else { if (AutoRun || Advance) AdvanceStage(); } } public void PlateGeneration() { var availableVerts = Vertices.Where(d => d.StageComplete == false && d.PlateId != -1).OrderBy(v => Guid.NewGuid()).ToList(); foreach (PlateData plateData in Plates) { var plateVerts = availableVerts.Where(d => d.PlateId == plateData.Id); foreach (VertexData vertexData in plateVerts.Take((int)((5 + plateVerts.Count() / 4) * plateData.PlateExpansion))) { int expandTo = GetFreeNeighbourIndex(vertexData); if (expandTo != -1) { Vertices[expandTo].PlateId = plateData.Id; plateData.Vertices.Add(expandTo); ColorVertex(expandTo, plateData.Color); } else { vertexData.StageComplete = true; } } } if (!availableVerts.Any()) { foreach (VertexData vertexData in Vertices) vertexData.StageComplete = false; AssignOceanPlates(Plates); CompleteStage(); } } public void BorderSearch() { var availableVerts = Vertices.Where(d => d.StageComplete == false).Take(2500).ToList(); foreach (VertexData vertexData in availableVerts) { // Do we have any neighbours of another plate? var neighbours = GetNeighboringVertices(vertexData.Id, false).ToList(); if (neighbours .Any(v => Vertices[v].PlateId != vertexData.PlateId)) { vertexData.IsEdge = true; vertexData.IsTypeEdge = neighbours.Any(n => Plates[Vertices[n].PlateId].IsLandform != Plates[vertexData.PlateId].IsLandform); if (vertexData.IsTypeEdge) vertexData.EdgeDistance = 1f; ColorVertex(vertexData.Id, vertexData.IsTypeEdge ? Colors.White : Colors.Black); } else { ColorVertex(vertexData.Id, Plates[vertexData.PlateId].Color); } vertexData.StageComplete = true; } if (!availableVerts.Any()) { foreach (VertexData vertexData in Vertices) vertexData.StageComplete = false; CompleteStage(); } } public void EdgeDistanceCalculation() { var availableVerts = Vertices.Where(d => d.StageComplete == false && d.EdgeDistance > 0f).OrderBy(v => v.EdgeDistance).Take(2500).ToList(); foreach (VertexData vertexData in availableVerts) { var neighbours = GetNeighboringVertices(vertexData.Id, false).ToList(); foreach (int neighbour in neighbours) { if (Vertices[neighbour].EdgeDistance > 0f && Vertices[neighbour].EdgeDistance < vertexData.EdgeDistance + 1f) continue; VertexData neighbourVert = Vertices[neighbour]; neighbourVert.EdgeDistance = vertexData.EdgeDistance + 1f; ColorVertex(neighbourVert.Id, Plates[vertexData.PlateId].Color * 0.8f); } vertexData.StageComplete = true; } if (!availableVerts.Any()) { float maxDistance = Vertices.Max(v => v.EdgeDistance); foreach (VertexData vertexData in Vertices) { vertexData.EdgeDistance /= maxDistance; } foreach (PlateData plateData in Plates) { plateData.CenterVertexId = Vertices.Where(v => v.PlateId == plateData.Id).MaxBy(v => v.EdgeDistance).Id; } foreach (VertexData vertexData in Vertices) vertexData.StageComplete = false; CompleteStage(); } } public void EdgeStressCalculation() { var availableVerts = Vertices.Where(d => d.StageComplete == false && d.IsEdge).Take(2500).ToList(); foreach (VertexData vertexData in availableVerts) { var neighbours = GetNeighboringVertices(vertexData.Id, false).ToList(); foreach (int neighbour in neighbours) { if (!Vertices[neighbour].IsEdge) continue; if (Vertices[neighbour].PlateId == vertexData.PlateId) continue; PlateData plateA = Plates[vertexData.PlateId]; PlateData plateB = Plates[Vertices[neighbour].PlateId]; VertexData centerA = Vertices[plateA.CenterVertexId]; VertexData centerB = Vertices[plateB.CenterVertexId]; Vector3 p1, p2; p1 = Mdt.GetVertex(vertexData.Id).Cross(Mdt.GetVertex(centerA.Id)); p2 = Mdt.GetVertex(neighbour).Cross(Mdt.GetVertex(centerB.Id)); vertexData.StrainSamples.Add(CalculateStrainMagnitude(p1, p2, plateA.Dir, plateB.Dir)); } vertexData.StageComplete = true; var majorStrain = AverageStrainList(vertexData.StrainSamples); switch (majorStrain.Type) { case StrainType.Compression: ColorVertex(vertexData.Id, Colors.Red * majorStrain.Magnitude); break; case StrainType.Shear: ColorVertex(vertexData.Id, Colors.Yellow * majorStrain.Magnitude); break; case StrainType.Tension: ColorVertex(vertexData.Id, Colors.Blue * majorStrain.Magnitude); break; } } if (!availableVerts.Any()) { foreach (VertexData vertexData in Vertices) vertexData.StageComplete = false; CompleteStage(); } } public void SpreadStress() { var availableVerts = Vertices.Where(d => d.StageComplete == false && d.IsEdge && d.StrainSamples.Any()).OrderBy(d => Mathf.Abs(d.StrainSamples.Max(s => s.Magnitude))).Take(2500).ToList(); foreach (VertexData vertexData in availableVerts) { var neighbours = GetNeighboringVertices(vertexData.Id, false).ToList(); var majorStrain = AverageStrainList(vertexData.StrainSamples); foreach (int neighbour in neighbours) { VertexData neighbourVert = Vertices[neighbour]; neighbourVert.IsEdge = true; var newStrain = new StrainAnalysis(); newStrain.Magnitude = majorStrain.Magnitude * 0.9f; newStrain.Type = majorStrain.Type; newStrain.NormalRate = majorStrain.NormalRate * 0.9f; newStrain.ShearRate = majorStrain.ShearRate * 0.9f; neighbourVert.StrainSamples.Add(newStrain); var newAverage = AverageStrainList(neighbourVert.StrainSamples);; switch (majorStrain.Type) { case StrainType.Compression: ColorVertex(neighbourVert.Id, Colors.Red * newAverage.Magnitude); break; case StrainType.Shear: ColorVertex(neighbourVert.Id, Colors.Yellow * newAverage.Magnitude); break; case StrainType.Tension: ColorVertex(neighbourVert.Id, Colors.Blue * newAverage.Magnitude); break; } } if (neighbours.All(n => Vertices[n].IsEdge)) { vertexData.StageComplete = true; } } if (!availableVerts.Any()) { foreach (VertexData vertexData in Vertices) { vertexData.StageComplete = false; } CompleteStage(); } } public void HeightCalculation() { var availableVerts = Vertices.Where(d => d.StageComplete == false).Take(2500).ToList(); foreach (VertexData vertexData in availableVerts) { PlateData plate = Plates[vertexData.PlateId]; float continentalNoise = _continentalNoise.GetNoise3Dv(GetVertexPosition(vertexData.Id)); float mountainNoise = (1.0f + _mountainNoise.GetNoise3Dv(GetVertexPosition(vertexData.Id))) * 0.5f; float hfNoise = _hfNoise.GetNoise3Dv(GetVertexPosition(vertexData.Id)); var majorStrain = AverageStrainList(vertexData.StrainSamples); var normalRate = -majorStrain.NormalRate * majorStrain.Magnitude * (plate.IsLandform ? 1f : 0.5f); var edgeDistance = vertexData.EdgeDistance * (plate.IsLandform ? 1f : -1f); float height = 0.5f; //height *= plate.PlateExpansion; float mult = 2f; height += hfNoise; height = (height + 0.5f * mult) / (1f + mult); height += continentalNoise; height = (height + 0.5f * mult) / (1f + mult); height += edgeDistance * 0.25f; height = (height + 0.5f * mult) / (1f + mult); height += normalRate * 0.35f; height = Mathf.Clamp(height, 0.01f, 0.99f); ColorVertex(vertexData.Id, Colors.White * height); vertexData.StageComplete = true; vertexData.Height = height; } if (!availableVerts.Any()) { GD.Print($"Heights - min:'{Vertices.Min(v => v.Height)}' - max:'{Vertices.Max(v => v.Height)}' - average:'{Vertices.Average(v => v.Height)}'"); ScaleValues(Vertices); foreach (VertexData vertexData in Vertices) ColorVertex(vertexData.Id, Colors.White * vertexData.Height); float oceanPercentage = Vertices.Count(v => v.Height < 0.5f) / (float)Vertices.Count; GD.Print($"Ocean Percentage:'{oceanPercentage}'"); CompleteStage(); if (_meshInstance.GetSurfaceOverrideMaterial(0) is ShaderMaterial shaderMaterial) { shaderMaterial.SetShaderParameter("mode", 2); } if (_textureRect.Material is ShaderMaterial textureShaderMaterial) { textureShaderMaterial.SetShaderParameter("mode", 2); } } } public void ScaleValues(List values) { float maxDistance = Vertices.Max(s => Mathf.Abs(s.Height - 0.5f)); float scale = 0.5f/maxDistance; values.ForEach(v => v.Height = Mathf.Clamp(0.5f + (v.Height - 0.5f) * scale, 0.01f, 0.99f)); GD.Print($"Heights Post Scaling - min:'{Vertices.Min(v => v.Height)}' - max:'{Vertices.Max(v => v.Height)}' - average:'{Vertices.Average(v => v.Height)}'"); } public StrainAnalysis CalculateStrainMagnitude(Vector3 p1, Vector3 p2, Vector3 v1, Vector3 v2) { StrainAnalysis result = new StrainAnalysis(); Vector3 edge = p2 - p1; float edgeLength = edge.Length(); if (edgeLength < float.Epsilon) { result.Magnitude = 0; result.Type = StrainType.Shear; // Default return result; } Vector3 relVelocity = v2 - v1; float relVelMag = relVelocity.Length(); float dot = relVelocity.Dot(edge); result.NormalRate = dot / edgeLength; float normalRateSq = result.NormalRate * result.NormalRate; float shearRateSq = relVelMag * relVelMag - normalRateSq; result.ShearRate = (shearRateSq > 0) ? (float)Math.Sqrt(shearRateSq) : 0; result.Magnitude = (float)Math.Sqrt(normalRateSq + shearRateSq); float absNormal = Math.Abs(result.NormalRate); float absShear = Math.Abs(result.ShearRate); if (absNormal > absShear) { result.Type = result.NormalRate > 0 ? StrainType.Tension : StrainType.Compression; } else { result.Type = StrainType.Shear; } return result; } public static StrainAnalysis AverageStrainList(List strains) { if (strains == null || strains.Count == 0) { return new StrainAnalysis(); } int count = strains.Count; float sumMagnitude = 0; float sumNormalRate = 0; float sumShearRate = 0; int tensionCount = 0; int compressionCount = 0; int shearCount = 0; foreach (var s in strains) { sumMagnitude += s.Magnitude; sumNormalRate += s.NormalRate; sumShearRate += s.ShearRate; switch (s.Type) { case StrainType.Tension: tensionCount++; break; case StrainType.Compression: compressionCount++; break; case StrainType.Shear: shearCount++; break; } } float avgMagnitude = sumMagnitude / count; float avgNormalRate = sumNormalRate / count; float avgShearRate = sumShearRate / count; StrainType averageType = StrainType.Shear; int maxCount = 0; if (tensionCount > maxCount) { maxCount = tensionCount; averageType = StrainType.Tension; } if (compressionCount > maxCount) { maxCount = compressionCount; averageType = StrainType.Compression; } if (shearCount > maxCount) { maxCount = shearCount; averageType = StrainType.Shear; } return new StrainAnalysis { Magnitude = avgMagnitude, Type = averageType, NormalRate = avgNormalRate, ShearRate = avgShearRate }; } public void AssignOceanPlates(List areas) { int n = areas.Count; double totalArea = areas.Sum(a => a.Vertices.Count * a.PlateExpansion); double targetOcean = totalArea * _landRatio; double bestDiff = double.MaxValue; int bestMask = 0; int combinations = 1 << n; for (int mask = 0; mask < combinations; mask++) { int oceanArea = 0; for (int i = 0; i < n; i++) { if ((mask & (1 << i)) != 0) oceanArea += (int)(areas[i].Vertices.Count * areas[i].PlateExpansion); } double diff = Math.Abs(oceanArea - targetOcean); if (diff < bestDiff) { bestDiff = diff; bestMask = mask; } } for (int i = 0; i < n; i++) { areas[i].IsLandform = (bestMask & (1 << i)) != 0; Color color = GetInitialColor(areas[i].IsLandform); areas[i].Color = color; foreach (int v in areas[i].Vertices) { ColorVertex(v, color); } } } public int GetFreeNeighbourIndex(VertexData vertexData) { foreach (int neighbour in vertexData.Neighbours) { if (Vertices[neighbour].PlateId == -1) return neighbour; } return -1; } public Color GetInitialColor(bool isLand) { var color = isLand ? new Color( 0.2f, 1f, 0.2f ) : new Color( 0.2f, 0.2f, 1f ); color.ToHsv(out float h, out float s, out float v); h += RandF(-0.05f, 0.05f); s += RandF(-0.2f, 0.2f); v += RandF(-0.3f, 0.3f); color = Color.FromHsv(h, s, v); return color; } public Vector3 GetVertexPosition(int vertexId) { return Mdt.GetVertex(vertexId); } public void CompleteStage() { StageComplete = true; _generationStopwatch.Stop(); if (Stage != GenerationStage.NotStarted) GD.Print($"'{Stage.ToString()}' took '{_generationStopwatch.Elapsed}'"); } public void ColorVertex(int id, Color color) { Mdt.SetVertexColor(id, color); } public void UpdateMesh() { _arrayMesh.ClearSurfaces(); Mdt.CommitToSurface(_arrayMesh); } }