Compare commits
No commits in common. "c608369dcf48b52b8adee27a066e55f8ab3b2149" and "f7de1c67af530863c307c98ffc617e6e48484cd5" have entirely different histories.
c608369dcf
...
f7de1c67af
5 changed files with 489 additions and 2611 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -1,221 +0,0 @@
|
||||||
shader_type canvas_item;
|
|
||||||
|
|
||||||
// Textures
|
|
||||||
uniform sampler2D height_map : filter_nearest;
|
|
||||||
|
|
||||||
// Thresholds
|
|
||||||
uniform float water_threshold = 0.0;
|
|
||||||
uniform float rock_threshold = 0.05;
|
|
||||||
uniform float snow_height_threshold = 0.8;
|
|
||||||
uniform float desert_temperature_threshold = 0.7;
|
|
||||||
uniform float desert_moisture_threshold = 0.3;
|
|
||||||
|
|
||||||
// Noise parameters
|
|
||||||
uniform float temperature_noise_frequency = 0.01;
|
|
||||||
uniform float temperature_noise_amplitude = 0.1;
|
|
||||||
uniform float moisture_noise_frequency = 0.01;
|
|
||||||
uniform float moisture_noise_amplitude = 0.1;
|
|
||||||
|
|
||||||
// Debug mode
|
|
||||||
uniform int debug_type = 0; // 0=None, 1=Slope, 2=Height, 3=Temperature, 4=Moisture
|
|
||||||
|
|
||||||
// Colors
|
|
||||||
const vec3 COLOR_WATER = vec3(0.263, 0.502, 0.690); // #4380b0
|
|
||||||
const vec3 COLOR_GRASS = vec3(0.745, 0.835, 0.541); // #bed58a
|
|
||||||
const vec3 COLOR_DESERT = vec3(0.929, 0.788, 0.686); // #edc9af
|
|
||||||
const vec3 COLOR_BEACH = vec3(0.502, 0.502, 0.502); // #808080
|
|
||||||
const vec3 COLOR_SNOW = vec3(0.957, 0.957, 0.957); // #f4f4f4
|
|
||||||
const vec3 COLOR_ROCK = vec3(0.894, 0.827, 0.651); // #e4d3a6
|
|
||||||
|
|
||||||
// Better hash function for noise
|
|
||||||
vec2 hash22(vec2 p, float seed) {
|
|
||||||
vec3 p3 = fract(vec3(p.xyx) * vec3(443.897, 441.423, 437.195));
|
|
||||||
p3 += dot(p3, p3.yzx + 19.19 + seed);
|
|
||||||
return fract((p3.xx + p3.yz) * p3.zy);
|
|
||||||
}
|
|
||||||
|
|
||||||
float hash12(vec2 p, float seed) {
|
|
||||||
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
|
|
||||||
p3 += dot(p3, p3.yzx + 33.33 + seed);
|
|
||||||
return fract((p3.x + p3.y) * p3.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perlin noise
|
|
||||||
float perlin_noise(vec2 p, float seed) {
|
|
||||||
vec2 i = floor(p);
|
|
||||||
vec2 f = fract(p);
|
|
||||||
|
|
||||||
// Quintic interpolation
|
|
||||||
vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
|
|
||||||
|
|
||||||
// Get gradients
|
|
||||||
float a = hash12(i + vec2(0.0, 0.0), seed);
|
|
||||||
float b = hash12(i + vec2(1.0, 0.0), seed);
|
|
||||||
float c = hash12(i + vec2(0.0, 1.0), seed);
|
|
||||||
float d = hash12(i + vec2(1.0, 1.0), seed);
|
|
||||||
|
|
||||||
// Mix
|
|
||||||
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y) * 2.0 - 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// FBM with 12 octaves
|
|
||||||
float fbm(vec2 p, float freq, float seed) {
|
|
||||||
float value = 0.0;
|
|
||||||
float amplitude = 0.5;
|
|
||||||
float frequency = freq;
|
|
||||||
|
|
||||||
for(int i = 0; i < 12; i++) {
|
|
||||||
value += amplitude * perlin_noise(p * frequency, seed);
|
|
||||||
frequency *= 2.0;
|
|
||||||
amplitude *= 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get height at UV coordinates
|
|
||||||
float get_height(vec2 uv) {
|
|
||||||
return texture(height_map, uv).r;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get heat value with latitude falloff
|
|
||||||
float get_heat(vec2 uv) {
|
|
||||||
vec2 heightmap_size = vec2(textureSize(height_map, 0));
|
|
||||||
vec2 heightmap_coords = uv * heightmap_size;
|
|
||||||
|
|
||||||
// Original C# uses 1/frequency for FastNoiseLite, so we invert
|
|
||||||
float actualFreq = 1.0 / temperature_noise_frequency;
|
|
||||||
float heat = fbm(heightmap_coords, actualFreq, 1738.0);
|
|
||||||
heat *= temperature_noise_amplitude;
|
|
||||||
|
|
||||||
// Latitude falloff (sine wave from poles to equator)
|
|
||||||
float latitude_factor = sin(3.14159265 * uv.y);
|
|
||||||
heat = latitude_factor + heat;
|
|
||||||
|
|
||||||
return heat;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get moisture value with latitude falloff
|
|
||||||
float get_moisture(vec2 uv) {
|
|
||||||
vec2 heightmap_size = vec2(textureSize(height_map, 0));
|
|
||||||
vec2 heightmap_coords = uv * heightmap_size;
|
|
||||||
|
|
||||||
// Original C# uses 1/frequency for FastNoiseLite, so we invert
|
|
||||||
float actualFreq = 1.0 / moisture_noise_frequency;
|
|
||||||
float moisture = fbm(heightmap_coords, actualFreq, 1337.0);
|
|
||||||
moisture *= moisture_noise_amplitude;
|
|
||||||
|
|
||||||
// Latitude falloff (sine wave from poles to equator)
|
|
||||||
float latitude_factor = sin(3.14159265 * uv.y);
|
|
||||||
moisture = latitude_factor + moisture;
|
|
||||||
|
|
||||||
return moisture;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if position is sea
|
|
||||||
bool is_sea(vec2 uv) {
|
|
||||||
return get_height(uv) <= water_threshold;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if position is desert
|
|
||||||
bool is_desert(vec2 uv, float heat, float moisture) {
|
|
||||||
return moisture < desert_moisture_threshold && heat > desert_temperature_threshold;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate max height difference with neighbors (for slope)
|
|
||||||
float get_max_slope(vec2 uv) {
|
|
||||||
vec2 heightmap_size = vec2(textureSize(height_map, 0));
|
|
||||||
vec2 texel_size = 1.0 / heightmap_size;
|
|
||||||
|
|
||||||
float center_height = get_height(uv);
|
|
||||||
float max_diff = 0.0;
|
|
||||||
|
|
||||||
// Check 8 neighbors
|
|
||||||
for(int x = -1; x <= 1; x++) {
|
|
||||||
for(int y = -1; y <= 1; y++) {
|
|
||||||
if(x == 0 && y == 0) continue;
|
|
||||||
|
|
||||||
vec2 neighbor_uv = uv + vec2(float(x), float(y)) * texel_size;
|
|
||||||
float neighbor_height = get_height(neighbor_uv);
|
|
||||||
float diff = abs(center_height - neighbor_height);
|
|
||||||
max_diff = max(max_diff, diff);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return max_diff;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if any neighbor is sea (for beach detection)
|
|
||||||
bool borders_sea(vec2 uv) {
|
|
||||||
vec2 heightmap_size = vec2(textureSize(height_map, 0));
|
|
||||||
vec2 texel_size = 1.0 / heightmap_size;
|
|
||||||
|
|
||||||
for(int x = -1; x <= 1; x++) {
|
|
||||||
for(int y = -1; y <= 1; y++) {
|
|
||||||
if(x == 0 && y == 0) continue;
|
|
||||||
|
|
||||||
vec2 neighbor_uv = uv + vec2(float(x), float(y)) * texel_size;
|
|
||||||
if(is_sea(neighbor_uv)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void fragment() {
|
|
||||||
vec2 uv = UV;
|
|
||||||
|
|
||||||
float height_value = get_height(uv);
|
|
||||||
float heat_value = get_heat(uv);
|
|
||||||
float moisture_value = get_moisture(uv);
|
|
||||||
float slope = get_max_slope(uv);
|
|
||||||
|
|
||||||
vec3 color = COLOR_GRASS; // Default
|
|
||||||
|
|
||||||
if(debug_type == 0) {
|
|
||||||
// Normal rendering
|
|
||||||
|
|
||||||
// Water
|
|
||||||
if(is_sea(uv)) {
|
|
||||||
color = COLOR_WATER;
|
|
||||||
}
|
|
||||||
// Desert
|
|
||||||
else if(is_desert(uv, heat_value, moisture_value)) {
|
|
||||||
color = COLOR_DESERT;
|
|
||||||
}
|
|
||||||
// Beach (borders sea)
|
|
||||||
else if(borders_sea(uv)) {
|
|
||||||
color = COLOR_BEACH;
|
|
||||||
}
|
|
||||||
// Snow (high altitude, not desert)
|
|
||||||
else if(height_value > snow_height_threshold && !is_desert(uv, heat_value, moisture_value)) {
|
|
||||||
color = COLOR_SNOW;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rock (steep areas) - override everything except water
|
|
||||||
if(slope > rock_threshold && !is_sea(uv)) {
|
|
||||||
color = COLOR_ROCK;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if(debug_type == 1) {
|
|
||||||
// Slope debug
|
|
||||||
float gray = slope * 5.0;
|
|
||||||
color = vec3(gray);
|
|
||||||
}
|
|
||||||
else if(debug_type == 2) {
|
|
||||||
// Height debug
|
|
||||||
color = vec3(height_value);
|
|
||||||
}
|
|
||||||
else if(debug_type == 3) {
|
|
||||||
// Temperature debug
|
|
||||||
color = vec3(heat_value);
|
|
||||||
}
|
|
||||||
else if(debug_type == 4) {
|
|
||||||
// Moisture debug
|
|
||||||
color = vec3(moisture_value);
|
|
||||||
}
|
|
||||||
|
|
||||||
COLOR = vec4(color, 1.0);
|
|
||||||
}
|
|
||||||
304
Source/Tile.cs
Normal file
304
Source/Tile.cs
Normal file
|
|
@ -0,0 +1,304 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
285
Source/World.cs
285
Source/World.cs
|
|
@ -1,5 +1,12 @@
|
||||||
using Godot;
|
using Godot;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Godot.Collections;
|
||||||
|
using Environment = Godot.Environment;
|
||||||
|
|
||||||
public enum DebugType
|
public enum DebugType
|
||||||
{
|
{
|
||||||
|
|
@ -13,21 +20,52 @@ public enum DebugType
|
||||||
[Tool]
|
[Tool]
|
||||||
public partial class World : Node2D
|
public partial class World : Node2D
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
|
private int _chunksX = 32;
|
||||||
|
[Export]
|
||||||
|
public int ChunkXCount
|
||||||
|
{
|
||||||
|
get => _chunksX;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_chunksX = value;
|
||||||
|
LoadTiles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector2I ChunksPerAxis
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
|
||||||
|
if (HeightMapImage == null)
|
||||||
|
{
|
||||||
|
return new Vector2I(_chunksX, _chunksX);
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2I heightMapResolution = new Vector2I(HeightMapImage.GetWidth(), HeightMapImage.GetHeight());
|
||||||
|
int chunksY = (int)(heightMapResolution.Y / (heightMapResolution.X / _chunksX));
|
||||||
|
return new Vector2I(_chunksX, chunksY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _tileSize = 96;
|
||||||
|
[Export]
|
||||||
|
public int TileSize
|
||||||
|
{
|
||||||
|
get => _tileSize;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_tileSize = value;
|
||||||
|
LoadTiles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Export]
|
[Export]
|
||||||
public Texture2D HeightMapTexture;
|
public Texture2D HeightMapTexture;
|
||||||
|
|
||||||
[Export]
|
private float _rockThreshold = 0.05f;
|
||||||
public int MapWidth
|
|
||||||
{
|
|
||||||
get => _mapWidth;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_mapWidth = value;
|
|
||||||
SetupShader();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private int _mapWidth = 3072;
|
|
||||||
|
|
||||||
[Export]
|
[Export]
|
||||||
public float RockThreshold
|
public float RockThreshold
|
||||||
{
|
{
|
||||||
|
|
@ -35,11 +73,11 @@ public partial class World : Node2D
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_rockThreshold = value;
|
_rockThreshold = value;
|
||||||
UpdateShaderParameters();
|
LoadTiles();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private float _rockThreshold = 0.05f;
|
|
||||||
|
|
||||||
|
private float _snowHeightThreshold = 0.8f;
|
||||||
[Export]
|
[Export]
|
||||||
public float SnowHeightThreshold
|
public float SnowHeightThreshold
|
||||||
{
|
{
|
||||||
|
|
@ -47,11 +85,11 @@ public partial class World : Node2D
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_snowHeightThreshold = value;
|
_snowHeightThreshold = value;
|
||||||
UpdateShaderParameters();
|
LoadTiles();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private float _snowHeightThreshold = 0.8f;
|
|
||||||
|
|
||||||
|
private float _waterThreshold = 0.0f;
|
||||||
[Export]
|
[Export]
|
||||||
public float WaterThreshold
|
public float WaterThreshold
|
||||||
{
|
{
|
||||||
|
|
@ -59,11 +97,11 @@ public partial class World : Node2D
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_waterThreshold = value;
|
_waterThreshold = value;
|
||||||
UpdateShaderParameters();
|
LoadTiles();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private float _waterThreshold = 0.0f;
|
|
||||||
|
|
||||||
|
private float _desertTemperatureThreshold = 0.7f;
|
||||||
[Export]
|
[Export]
|
||||||
public float DesertTemperatureThreshold
|
public float DesertTemperatureThreshold
|
||||||
{
|
{
|
||||||
|
|
@ -71,11 +109,11 @@ public partial class World : Node2D
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_desertTemperatureThreshold = value;
|
_desertTemperatureThreshold = value;
|
||||||
UpdateShaderParameters();
|
LoadTiles();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private float _desertTemperatureThreshold = 0.7f;
|
|
||||||
|
|
||||||
|
private float _desertMoistureThreshold = 0.3f;
|
||||||
[Export]
|
[Export]
|
||||||
public float DesertMoistureThreshold
|
public float DesertMoistureThreshold
|
||||||
{
|
{
|
||||||
|
|
@ -83,11 +121,11 @@ public partial class World : Node2D
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_desertMoistureThreshold = value;
|
_desertMoistureThreshold = value;
|
||||||
UpdateShaderParameters();
|
LoadTiles();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private float _desertMoistureThreshold = 0.3f;
|
|
||||||
|
|
||||||
|
private float _temperatureNoiseFrequency = 0.01f;
|
||||||
[Export]
|
[Export]
|
||||||
public float TemperatureNoiseFrequency
|
public float TemperatureNoiseFrequency
|
||||||
{
|
{
|
||||||
|
|
@ -95,11 +133,11 @@ public partial class World : Node2D
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_temperatureNoiseFrequency = value;
|
_temperatureNoiseFrequency = value;
|
||||||
UpdateShaderParameters();
|
LoadTiles();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private float _temperatureNoiseFrequency = 0.01f;
|
|
||||||
|
|
||||||
|
private float _moistureNoiseFrequency = 0.01f;
|
||||||
[Export]
|
[Export]
|
||||||
public float MoistureNoiseFrequency
|
public float MoistureNoiseFrequency
|
||||||
{
|
{
|
||||||
|
|
@ -107,35 +145,35 @@ public partial class World : Node2D
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_moistureNoiseFrequency = value;
|
_moistureNoiseFrequency = value;
|
||||||
UpdateShaderParameters();
|
LoadTiles();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private float _moistureNoiseFrequency = 0.01f;
|
|
||||||
|
|
||||||
|
private float _TemperatureNoiseAmplitude = 0.1f;
|
||||||
[Export]
|
[Export]
|
||||||
public float TemperatureNoiseAmplitude
|
public float TemperatureNoiseAmplitude
|
||||||
{
|
{
|
||||||
get => _temperatureNoiseAmplitude;
|
get => _TemperatureNoiseAmplitude;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_temperatureNoiseAmplitude = value;
|
_TemperatureNoiseAmplitude = value;
|
||||||
UpdateShaderParameters();
|
LoadTiles();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private float _temperatureNoiseAmplitude = 0.1f;
|
|
||||||
|
|
||||||
|
private float _MoistureNoiseAmplitude = 0.1f;
|
||||||
[Export]
|
[Export]
|
||||||
public float MoistureNoiseAmplitude
|
public float MoistureNoiseAmplitude
|
||||||
{
|
{
|
||||||
get => _moistureNoiseAmplitude;
|
get => _MoistureNoiseAmplitude;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_moistureNoiseAmplitude = value;
|
_MoistureNoiseAmplitude = value;
|
||||||
UpdateShaderParameters();
|
LoadTiles();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private float _moistureNoiseAmplitude = 0.1f;
|
|
||||||
|
|
||||||
|
private DebugType _debugType = DebugType.None;
|
||||||
[Export]
|
[Export]
|
||||||
public DebugType DebugType
|
public DebugType DebugType
|
||||||
{
|
{
|
||||||
|
|
@ -143,121 +181,136 @@ public partial class World : Node2D
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_debugType = value;
|
_debugType = value;
|
||||||
UpdateShaderParameters();
|
LoadTiles();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private DebugType _debugType = DebugType.None;
|
|
||||||
|
|
||||||
private Sprite2D _terrainSprite;
|
|
||||||
private ShaderMaterial _shaderMaterial;
|
|
||||||
|
|
||||||
|
ConcurrentDictionary<Vector2I, Tile> _tiles = new ConcurrentDictionary<Vector2I, Tile>();
|
||||||
|
private CancellationTokenSource _cancellationTokenSource;
|
||||||
|
private static readonly int MaxDegreeOfParallelism = Math.Max(1, (int)(System.Environment.ProcessorCount * 0.5));
|
||||||
|
|
||||||
|
public World()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
|
|
||||||
// If the height map image is not assigned, log an error and return
|
// If the height map image is not assigned, log an error and return
|
||||||
if (HeightMapTexture == null)
|
if (HeightMapTexture == null)
|
||||||
{
|
{
|
||||||
GD.PrintErr("Height map texture is not assigned.");
|
GD.PrintErr("Height map image is not assigned.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
SetupShader();
|
LoadTiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetupShader()
|
private void LoadTiles()
|
||||||
{
|
{
|
||||||
// Don't run during initialization if not ready
|
// Check if HeightMapImage is available
|
||||||
if (HeightMapTexture == null) return;
|
if (HeightMapImage == null)
|
||||||
|
|
||||||
// Clear any existing terrain sprite
|
|
||||||
if (_terrainSprite != null)
|
|
||||||
{
|
{
|
||||||
_terrainSprite.QueueFree();
|
GD.PrintErr("Cannot load tiles: HeightMapImage is null");
|
||||||
_terrainSprite = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the shader
|
|
||||||
Shader terrainShader = GD.Load<Shader>("res://Shaders/terrain.gdshader");
|
|
||||||
if (terrainShader == null)
|
|
||||||
{
|
|
||||||
GD.PrintErr("Failed to load terrain shader.");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create shader material
|
// Cancel any existing generation
|
||||||
_shaderMaterial = new ShaderMaterial();
|
_cancellationTokenSource?.Cancel();
|
||||||
_shaderMaterial.Shader = terrainShader;
|
_cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
var cancellationToken = _cancellationTokenSource.Token;
|
||||||
|
|
||||||
// Get heightmap dimensions
|
// Clear existing tiles
|
||||||
Image heightImage = HeightMapTexture.GetImage();
|
foreach (var tile in _tiles.Values)
|
||||||
if (heightImage == null)
|
|
||||||
{
|
{
|
||||||
GD.PrintErr("Failed to get image from HeightMapTexture.");
|
tile.Visible = false;
|
||||||
return;
|
|
||||||
|
// Delete and delete its children
|
||||||
|
foreach (Node child in tile.GetChildren())
|
||||||
|
{
|
||||||
|
child.QueueFree();
|
||||||
|
}
|
||||||
|
tile.QueueFree();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
_tiles.Clear();
|
||||||
|
|
||||||
Vector2I heightMapSize = new Vector2I(heightImage.GetWidth(), heightImage.GetHeight());
|
int scale = 1;
|
||||||
|
|
||||||
// Calculate output resolution
|
// Store local copies of parameters to avoid them changing mid-generation
|
||||||
// Height is always calculated based on heightmap aspect ratio
|
int tileSize = TileSize;
|
||||||
int outputWidth = MapWidth;
|
int chunksX = _chunksX;
|
||||||
float aspectRatio = (float)heightMapSize.Y / heightMapSize.X;
|
|
||||||
int outputHeight = (int)(outputWidth * aspectRatio);
|
|
||||||
|
|
||||||
// Create a sprite to display the shader
|
// Get the resolution of the height map image
|
||||||
_terrainSprite = new Sprite2D();
|
Vector2I heightMapResolution = new Vector2I(HeightMapImage.GetWidth(), HeightMapImage.GetHeight());
|
||||||
_terrainSprite.Material = _shaderMaterial;
|
|
||||||
_terrainSprite.TextureFilter = TextureFilterEnum.Nearest;
|
|
||||||
|
|
||||||
// Create a white texture with the desired output size
|
// Calculate the number of chunks in the Y direction based on the aspect ratio
|
||||||
// The shader will render over this
|
int chunksY = (int)(heightMapResolution.Y / (heightMapResolution.X / _chunksX));
|
||||||
Image whiteImage = Image.CreateEmpty(outputWidth, outputHeight, false, Image.Format.Rgba8);
|
|
||||||
whiteImage.Fill(Colors.White);
|
|
||||||
_terrainSprite.Texture = ImageTexture.CreateFromImage(whiteImage);
|
|
||||||
|
|
||||||
// Center the sprite
|
// Loop through each chunk position and create a Tile
|
||||||
_terrainSprite.Centered = false;
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (int x = 0; x < chunksX; x++)
|
||||||
|
{
|
||||||
|
if (cancellationToken.IsCancellationRequested) return;
|
||||||
|
|
||||||
AddChild(_terrainSprite);
|
for (int y = 0; y < chunksY; y++)
|
||||||
|
{
|
||||||
|
if (cancellationToken.IsCancellationRequested) return;
|
||||||
|
|
||||||
|
Vector2I chunkId = new Vector2I(x, y);
|
||||||
|
|
||||||
|
Tile tile = new Tile(new Vector2I(tileSize, tileSize), chunkId, this);
|
||||||
|
tile.CancellationToken = cancellationToken;
|
||||||
|
tile.Position = new Vector2(chunkId.X * tileSize, chunkId.Y * tileSize) * scale;
|
||||||
|
tile.Scale = new Vector2(scale, scale);
|
||||||
|
|
||||||
|
if (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
CallDeferred("add_child", tile);
|
||||||
|
_tiles[chunkId] = tile;
|
||||||
|
// GD.Print("Loaded tile at chunk " + chunkId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// GD.Print("Tile generation cancelled");
|
||||||
|
}
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
// Update all shader parameters
|
|
||||||
UpdateShaderParameters();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateShaderParameters()
|
private Image _heightMapCache;
|
||||||
|
public Image HeightMapImage
|
||||||
{
|
{
|
||||||
if (_shaderMaterial == null)
|
get
|
||||||
{
|
{
|
||||||
GD.PrintErr("ShaderMaterial is null, cannot update parameters");
|
// If not cached, load and cache it
|
||||||
return;
|
if (_heightMapCache == null && HeightMapTexture != null)
|
||||||
|
{
|
||||||
|
_heightMapCache = HeightMapTexture.GetImage();
|
||||||
|
|
||||||
|
// If GetImage() returns null, try loading directly from file
|
||||||
|
if (_heightMapCache == null && HeightMapTexture.ResourcePath != "")
|
||||||
|
{
|
||||||
|
_heightMapCache = Image.LoadFromFile(HeightMapTexture.ResourcePath);
|
||||||
|
|
||||||
|
if (_heightMapCache == null)
|
||||||
|
{
|
||||||
|
GD.PushError($"Could not load heightmap from {HeightMapTexture.ResourcePath}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _heightMapCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (HeightMapTexture == null)
|
|
||||||
{
|
|
||||||
GD.PrintErr("HeightMapTexture is null, cannot update parameters");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
GD.Print($"Setting height_map texture: {HeightMapTexture.ResourcePath}");
|
|
||||||
|
|
||||||
// Set the heightmap texture
|
|
||||||
_shaderMaterial.SetShaderParameter("height_map", HeightMapTexture);
|
|
||||||
|
|
||||||
// Set all threshold parameters
|
|
||||||
_shaderMaterial.SetShaderParameter("water_threshold", WaterThreshold);
|
|
||||||
_shaderMaterial.SetShaderParameter("rock_threshold", RockThreshold);
|
|
||||||
_shaderMaterial.SetShaderParameter("snow_height_threshold", SnowHeightThreshold);
|
|
||||||
_shaderMaterial.SetShaderParameter("desert_temperature_threshold", DesertTemperatureThreshold);
|
|
||||||
_shaderMaterial.SetShaderParameter("desert_moisture_threshold", DesertMoistureThreshold);
|
|
||||||
|
|
||||||
// Set noise parameters
|
|
||||||
_shaderMaterial.SetShaderParameter("temperature_noise_frequency", TemperatureNoiseFrequency);
|
|
||||||
_shaderMaterial.SetShaderParameter("temperature_noise_amplitude", TemperatureNoiseAmplitude);
|
|
||||||
_shaderMaterial.SetShaderParameter("moisture_noise_frequency", MoistureNoiseFrequency);
|
|
||||||
_shaderMaterial.SetShaderParameter("moisture_noise_amplitude", MoistureNoiseAmplitude);
|
|
||||||
|
|
||||||
// Set debug mode
|
|
||||||
_shaderMaterial.SetShaderParameter("debug_type", (int)DebugType);
|
|
||||||
|
|
||||||
GD.Print("Shader parameters updated successfully");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ RockThreshold = 0.04
|
||||||
SnowHeightThreshold = 0.32
|
SnowHeightThreshold = 0.32
|
||||||
WaterThreshold = 0.24
|
WaterThreshold = 0.24
|
||||||
DesertTemperatureThreshold = 0.84
|
DesertTemperatureThreshold = 0.84
|
||||||
DesertMoistureThreshold = 0.98
|
DesertMoistureThreshold = 0.96
|
||||||
TemperatureNoiseFrequency = 1500.0
|
TemperatureNoiseFrequency = 1500.0
|
||||||
MoistureNoiseFrequency = 1500.0
|
MoistureNoiseFrequency = 1500.0
|
||||||
MoistureNoiseAmplitude = 0.02
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue