Files
Frontiers/Source/Tile.cs

305 lines
8.4 KiB
C#

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Godot;
public partial class Tile : Node2D
{
private Vector2I _size;
private Vector2I _chunkId;
private World _world;
public CancellationToken CancellationToken { get; set; }
public Tile(Vector2I size, Vector2I chunkId, World world)
{
_size = size;
_chunkId = chunkId;
_world = world;
}
public override void _Ready()
{
loadTile();
}
void loadTile()
{
Image image = Image.CreateEmpty(_size.X, _size.Y, false, Image.Format.Rgba8);
Vector2I heightMapResolution = new Vector2I(
_world.HeightMapImage.GetWidth(),
_world.HeightMapImage.GetHeight()
);
Vector2I mapResolution = _world.ChunksPerAxis * _world.TileSize;
Vector2I tileOffset = new Vector2I(
_chunkId.X * _world.TileSize,
_chunkId.Y * _world.TileSize
);
// Store parameters locally to avoid changes during generation
float rockThreshold = _world.RockThreshold;
float snowHeightThreshold = _world.SnowHeightThreshold;
float waterThreshold = _world.WaterThreshold;
float desertMoistureThreshold = _world.DesertMoistureThreshold;
float desertTemperatureThreshold = _world.DesertTemperatureThreshold;
DebugType debugType = _world.DebugType;
// Use limited parallelism instead of unlimited Task.Run
var parallelOptions = new ParallelOptions
{
MaxDegreeOfParallelism = Math.Max(1, (int)(System.Environment.ProcessorCount * 0.5)),
CancellationToken = CancellationToken
};
Task.Run(() =>
{
try
{
Parallel.For(0, _size.X, parallelOptions, x =>
{
if (CancellationToken.IsCancellationRequested) return;
for (int y = 0; y < _size.Y; y++)
{
if (CancellationToken.IsCancellationRequested) return;
Vector2I pixelOffset = new Vector2I(
x + tileOffset.X,
y + tileOffset.Y
);
float heightValue = GetValueAtMapPosition(pixelOffset);
float heatValue = GetHeatAtMapPosition(pixelOffset);
float moistureValue = GetMoistureAtMapPosition(pixelOffset);
// Get the max difference between the coord and its neighbors
float maxDiff = 0.0f;
float centerHeight = heightValue;
bool bordersSea = false;
for (int offsetX = -1; offsetX <= 1; offsetX++)
{
for (int offsetY = -1; offsetY <= 1; offsetY++)
{
if (offsetX == 0 && offsetY == 0)
continue;
Vector2I neighborCoords = new Vector2I(
pixelOffset.X + offsetX,
pixelOffset.Y + offsetY
);
float neighborHeight = GetValueAtMapPosition(neighborCoords);
if (IsSeaAtMapPosition(neighborCoords))
{
bordersSea = true;
}
float diff = Mathf.Abs(centerHeight - neighborHeight);
if (diff > maxDiff)
{
maxDiff = diff;
}
}
}
if (debugType == DebugType.None)
{
// Grass (default)
image.SetPixel(x, y, Color.FromString("#bed58a", Colors.Purple));
if (IsSeaAtMapPosition(pixelOffset))
{
// Water
image.SetPixel(x, y, Color.FromString("#4380b0", Colors.Purple));
continue;
}
if (isDesertAtMapPosition(pixelOffset))
{
// Desert
image.SetPixel(x, y, Color.FromString("#edc9af", Colors.Purple));
}
if (bordersSea)
{
// Beach
image.SetPixel(x, y, Color.FromString("#808080", Colors.Purple));
continue;
}
if (heightValue > snowHeightThreshold && !isDesertAtMapPosition(pixelOffset))
{
// Snow
image.SetPixel(x, y, Color.FromString("#f4f4f4", Colors.Purple));
}
// We want steep areas to be grey for rock.
if (maxDiff > rockThreshold)
{
// Steep area
image.SetPixel(x, y, Color.FromString("#e4d3a6", Colors.Purple));
continue;
}
}
else if (debugType == DebugType.Height)
{
float grayValue = heightValue;
image.SetPixel(x, y, new Color(grayValue, grayValue, grayValue));
}
else if (debugType == DebugType.Slope)
{
float grayValue = maxDiff * 5.0f;
image.SetPixel(x, y, new Color(grayValue, grayValue, grayValue));
} else if (debugType == DebugType.Temperature)
{
image.SetPixel(x, y, new Color(heatValue, heatValue, heatValue));
} else if (debugType == DebugType.Moisture)
{
image.SetPixel(x, y, new Color(moistureValue, moistureValue, moistureValue));
}
}
});
if (!CancellationToken.IsCancellationRequested)
{
// Create a texture from the image and assign it to a Sprite2D
Texture2D texture = ImageTexture.CreateFromImage(image);
Sprite2D sprite = new Sprite2D();
sprite.Texture = texture;
sprite.TextureFilter = TextureFilterEnum.Nearest;
CallDeferred("add_child", sprite);
}
}
catch (OperationCanceledException)
{
// Generation was cancelled, do nothing
}
}, CancellationToken);
}
// Terrain Query Methods
private Vector2I ToHeightMapCoordinates(Vector2I position)
{
Vector2I heightMapResolution = new Vector2I(
_world.HeightMapImage.GetWidth(),
_world.HeightMapImage.GetHeight()
);
Vector2I mapResolution = _world.ChunksPerAxis * _world.TileSize;
Vector2 mapDelta = (Vector2) position / (Vector2) mapResolution;
Vector2I heightMapCoords = new Vector2I(
(int)(mapDelta.X * heightMapResolution.X),
(int)(mapDelta.Y * heightMapResolution.Y)
);
// Clamp to valid range
heightMapCoords.X = Mathf.Clamp(heightMapCoords.X, 0, heightMapResolution.X - 1);
heightMapCoords.Y = Mathf.Clamp(heightMapCoords.Y, 0, heightMapResolution.Y - 1);
return heightMapCoords;
}
private float GetValueAtMapPosition(Vector2I position)
{
Vector2I heightMapCoords = ToHeightMapCoordinates(position);
Color hmColor = _world.HeightMapImage.GetPixel(heightMapCoords.X, heightMapCoords.Y);
return hmColor.R;
}
private bool isSeaAtMapPosition(Vector2I position)
{
float heightValue = GetValueAtMapPosition(position);
return heightValue <= _world.WaterThreshold;
}
private bool isDesertAtMapPosition(Vector2I position)
{
float heatValue = GetHeatAtMapPosition(position);
float moistureValue = GetMoistureAtMapPosition(position);
return moistureValue < _world.DesertMoistureThreshold && heatValue > _world.DesertTemperatureThreshold;
}
private bool IsSeaAtMapPosition(Vector2I position)
{
float heightValue = GetValueAtMapPosition(position);
return heightValue <= _world.WaterThreshold;
}
private FastNoiseLite _heatNoise = null;
private float GetHeatAtMapPosition(Vector2I position)
{
if (_heatNoise == null)
{
_heatNoise = new FastNoiseLite();
_heatNoise.SetNoiseType(FastNoiseLite.NoiseTypeEnum.Perlin);
_heatNoise.SetFrequency(1/_world.TemperatureNoiseFrequency);
_heatNoise.SetFractalType(FastNoiseLite.FractalTypeEnum.Fbm);
_heatNoise.SetFractalOctaves(12);
_heatNoise.SetSeed(1738);
}
Vector2I heightMapCoords = ToHeightMapCoordinates(position);
float heat = _heatNoise.GetNoise2D(heightMapCoords.X, heightMapCoords.Y);
heat *= _world.TemperatureNoiseAmplitude;
// Fall off towards the poles. Use sine function for a globe effect.
float mapResolutionY = _world.HeightMapImage.GetHeight();
float latitudeFactor = Mathf.Sin(Mathf.Pi * heightMapCoords.Y / mapResolutionY);
heat = latitudeFactor + heat;
return heat;
}
private FastNoiseLite _moistureNoise = null;
private float GetMoistureAtMapPosition(Vector2I position)
{
if (_moistureNoise == null)
{
_moistureNoise = new FastNoiseLite();
_moistureNoise.SetNoiseType(FastNoiseLite.NoiseTypeEnum.Perlin);
_moistureNoise.SetFrequency(1/_world.MoistureNoiseFrequency);
_moistureNoise.SetFractalType(FastNoiseLite.FractalTypeEnum.Fbm);
_moistureNoise.SetFractalOctaves(12);
_moistureNoise.SetSeed(1337);
}
Vector2I heightMapCoords = ToHeightMapCoordinates(position);
float moisture = _moistureNoise.GetNoise2D(heightMapCoords.X, heightMapCoords.Y);
moisture *= _world.MoistureNoiseAmplitude;
// Fall off towards the poles. Use sine function for a globe effect.
float mapResolutionY = _world.HeightMapImage.GetHeight();
float latitudeFactor = Mathf.Sin(Mathf.Pi * heightMapCoords.Y / mapResolutionY);
moisture = latitudeFactor + moisture;
return moisture;
}
}