diff --git a/src/app.rs b/src/app.rs index 88b8faa..08d9eff 100644 --- a/src/app.rs +++ b/src/app.rs @@ -369,6 +369,12 @@ impl TerrainApp { &mut self.params.snowline_height, 0..=255, ); + labeled_int_slider( + ui, + "Water Table Height", + &mut self.params.water_table_height, + 0..=255, + ); labeled_float_slider( ui, "Rock Slope Angle", diff --git a/src/generator.rs b/src/generator.rs index f5774c5..edae44c 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -22,6 +22,7 @@ pub struct GenerationParams { pub erosion_detail: f32, pub erosion_octaves: usize, pub snowline_height: u8, + pub water_table_height: u8, pub rock_slope_angle_degrees: f32, } @@ -38,6 +39,7 @@ impl Default for GenerationParams { erosion_detail: 1.5, erosion_octaves: 5, snowline_height: 220, + water_table_height: 0, rock_slope_angle_degrees: 15.0, } } @@ -54,6 +56,8 @@ pub struct GeneratedImages { #[allow(dead_code)] pub grass_mask_image: GrayImage, #[allow(dead_code)] + pub water_mask_image: GrayImage, + #[allow(dead_code)] pub rock_mask_image: GrayImage, #[allow(dead_code)] pub snow_mask_image: GrayImage, @@ -135,6 +139,10 @@ fn clamp01(x: f32) -> f32 { clamp(x, 0.0, 1.0) } +fn mix_u8(a: u8, b: u8, t: f32) -> u8 { + mix(a as f32, b as f32, clamp01(t)).round() as u8 +} + fn hash22(p: Vec2, random_seed: u64) -> Vec2 { let scrambled_x = splitmix64(random_seed ^ 0x9E37_79B9_7F4A_7C15); let scrambled_y = splitmix64(random_seed ^ 0xBF58_476D_1CE4_E5B9); @@ -401,10 +409,15 @@ pub fn generate(params: &GenerationParams) -> GeneratedImages { width, height, params.snowline_height, + params.water_table_height, params.rock_slope_angle_degrees, ); - let terrain_composite_image = - build_soft_light_composite(&terrain_layers.terrain_color_image, &bumpmap_image); + let terrain_composite_image = build_soft_light_composite( + &terrain_layers.terrain_color_image, + &bumpmap_image, + &heightmap_image, + &terrain_layers.water_mask_image, + ); GeneratedImages { width, @@ -414,6 +427,7 @@ pub fn generate(params: &GenerationParams) -> GeneratedImages { terrain_color_image: terrain_layers.terrain_color_image, terrain_composite_image, grass_mask_image: terrain_layers.grass_mask_image, + water_mask_image: terrain_layers.water_mask_image, rock_mask_image: terrain_layers.rock_mask_image, snow_mask_image: terrain_layers.snow_mask_image, } @@ -422,6 +436,7 @@ pub fn generate(params: &GenerationParams) -> GeneratedImages { struct TerrainLayers { terrain_color_image: RgbaImage, grass_mask_image: GrayImage, + water_mask_image: GrayImage, rock_mask_image: GrayImage, snow_mask_image: GrayImage, } @@ -460,22 +475,28 @@ fn build_terrain_layers( width: usize, height: usize, snowline_height: u8, + water_table_height: u8, rock_slope_angle_degrees: f32, ) -> TerrainLayers { let mut terrain_color_image = RgbaImage::new(width as u32, height as u32); let mut grass_mask_image = GrayImage::new(width as u32, height as u32); + let mut water_mask_image = GrayImage::new(width as u32, height as u32); let mut rock_mask_image = GrayImage::new(width as u32, height as u32); let mut snow_mask_image = GrayImage::new(width as u32, height as u32); - let grass_color = Rgba([92, 140, 66, 255]); + let low_grass_color = [78, 146, 64]; + let high_grass_color = [150, 156, 88]; + let water_color = Rgba([40, 70, 150, 255]); + let sand_color = [210, 195, 145]; let rock_color = Rgba([122, 112, 98, 255]); - let snow_color = Rgba([245, 248, 252, 255]); + let snow_color = Rgba([255, 250, 250, 255]); let slope_threshold_radians = rock_slope_angle_degrees.clamp(0.0, 89.9).to_radians(); for y in 0..height { for x in 0..width { let idx = y * width + x; let height_value = heightmap[idx].clamp(0.0, 1.0); + let u8_height = (height_value * 255.0).round() as u8; let left = sample_height(heightmap, width, height, x.saturating_sub(1), y); let right = sample_height(heightmap, width, height, (x + 1).min(width - 1), y); @@ -486,26 +507,56 @@ fn build_terrain_layers( let dy = (down - up) * 0.5 * height as f32; let slope_angle = dx.hypot(dy).atan(); + let grass_t = clamp01((height_value - 0.08) / 0.52); + let grass_color = [ + mix_u8(low_grass_color[0], high_grass_color[0], grass_t), + mix_u8(low_grass_color[1], high_grass_color[1], grass_t), + mix_u8(low_grass_color[2], high_grass_color[2], grass_t), + ]; + let is_water = if water_table_height == 0 { + false + } else { + u8_height < water_table_height + }; + + let sand_width = 4.0; + let sand_t = if water_table_height == 0 || is_water { + 0.0 + } else { + let diff = (u8_height - water_table_height) as f32; + (1.0 - (diff / sand_width)).clamp(0.0, 1.0) + }; + + let base_color = Rgba([ + mix_u8(grass_color[0], sand_color[0], sand_t), + mix_u8(grass_color[1], sand_color[1], sand_t), + mix_u8(grass_color[2], sand_color[2], sand_t), + 255, + ]); + let is_snow = if snowline_height == 0 { true } else if snowline_height == u8::MAX { false } else { - (height_value * 255.0).round() > snowline_height as f32 + u8_height > snowline_height }; let is_rock = slope_angle >= slope_threshold_radians; grass_mask_image.put_pixel(x as u32, y as u32, Luma([255])); + water_mask_image.put_pixel(x as u32, y as u32, Luma([if is_water { 255 } else { 0 }])); rock_mask_image.put_pixel(x as u32, y as u32, Luma([if is_rock { 255 } else { 0 }])); snow_mask_image.put_pixel(x as u32, y as u32, Luma([if is_snow { 255 } else { 0 }])); - let mut color = grass_color; - if is_rock { - color = rock_color; - } - if is_snow { - color = snow_color; - } + let color = if is_water { + water_color + } else if is_snow { + snow_color + } else if is_rock { + rock_color + } else { + base_color + }; terrain_color_image.put_pixel(x as u32, y as u32, color); } @@ -514,18 +565,30 @@ fn build_terrain_layers( TerrainLayers { terrain_color_image, grass_mask_image, + water_mask_image, rock_mask_image, snow_mask_image, } } -fn build_soft_light_composite(color_image: &RgbaImage, bumpmap_image: &GrayImage) -> RgbaImage { +fn build_soft_light_composite( + color_image: &RgbaImage, + bumpmap_image: &GrayImage, + heightmap_image: &GrayImage, + water_mask_image: &GrayImage, +) -> RgbaImage { let mut composite = RgbaImage::new(color_image.width(), color_image.height()); for y in 0..color_image.height() { for x in 0..color_image.width() { let base = color_image.get_pixel(x, y).0; - let blend = bumpmap_image.get_pixel(x, y).0[0] as f32 / 255.0; + let is_water = water_mask_image.get_pixel(x, y).0[0] > 0; + + let blend = if is_water { + heightmap_image.get_pixel(x, y).0[0] as f32 / 255.0 + } else { + bumpmap_image.get_pixel(x, y).0[0] as f32 / 255.0 + }; let r = soft_light_channel(base[0] as f32 / 255.0, blend); let g = soft_light_channel(base[1] as f32 / 255.0, blend);