From 5ad1253b9054a9e7ec6bd1da0749aad1db4d4047 Mon Sep 17 00:00:00 2001 From: grimsace Date: Wed, 22 Apr 2026 11:59:35 -0500 Subject: [PATCH] added a composite image --- src/app.rs | 72 ++++++++++++++++++++++++++++++++++++------------ src/generator.rs | 48 ++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 17 deletions(-) diff --git a/src/app.rs b/src/app.rs index 5feadd3..88b8faa 100644 --- a/src/app.rs +++ b/src/app.rs @@ -18,16 +18,18 @@ enum LeftTab { #[derive(Copy, Clone, Eq, PartialEq)] enum PreviewFocus { + TerrainComposite, + TerrainColor, Heightmap, Bumpmap, - TerrainColor, } #[derive(Clone)] struct TextureSet { + terrain_composite: TextureHandle, + terrain_color: TextureHandle, heightmap: TextureHandle, bumpmap: TextureHandle, - terrain_color: TextureHandle, } pub struct TerrainApp { @@ -49,7 +51,7 @@ impl TerrainApp { Self { params: GenerationParams::default(), generation_tab: LeftTab::Generation, - preview_focus: PreviewFocus::TerrainColor, + preview_focus: PreviewFocus::TerrainComposite, hovered_preview: None, textures: None, generated: None, @@ -90,7 +92,7 @@ impl TerrainApp { match receiver.try_recv() { Ok(Ok(images)) => { self.status_message = format!( - "Generated {}x{} heightmap, bumpmap, and terrain color.", + "Generated {}x{} heightmap, bumpmap, terrain color, and composite.", images.width, images.height ); self.install_textures(ctx, &images); @@ -144,16 +146,22 @@ impl TerrainApp { fn install_textures(&mut self, ctx: &Context, images: &GeneratedImages) { let heightmap = gray_to_color_image(&images.heightmap_image); let bumpmap = gray_to_color_image(&images.bumpmap_image); + let terrain_composite = rgba_to_color_image(&images.terrain_composite_image); let terrain_color = rgba_to_color_image(&images.terrain_color_image); self.textures = Some(TextureSet { - heightmap: ctx.load_texture("heightmap_preview", heightmap, TextureOptions::default()), - bumpmap: ctx.load_texture("bumpmap_preview", bumpmap, TextureOptions::default()), + terrain_composite: ctx.load_texture( + "terrain_composite_preview", + terrain_composite, + TextureOptions::default(), + ), terrain_color: ctx.load_texture( "terrain_color_preview", terrain_color, TextureOptions::default(), ), + heightmap: ctx.load_texture("heightmap_preview", heightmap, TextureOptions::default()), + bumpmap: ctx.load_texture("bumpmap_preview", bumpmap, TextureOptions::default()), }); } @@ -170,18 +178,24 @@ impl TerrainApp { let image = preview_image_data(generated, preview); let success_message = match preview { + PreviewFocus::TerrainComposite => { + String::from("Terrain composite copied to clipboard.") + } + PreviewFocus::TerrainColor => String::from("Terrain color copied to clipboard."), PreviewFocus::Heightmap => String::from("Heightmap copied to clipboard."), PreviewFocus::Bumpmap => String::from("Bumpmap copied to clipboard."), - PreviewFocus::TerrainColor => String::from("Terrain color copied to clipboard."), }; let (tx, rx) = mpsc::channel(); self.copy_receiver = Some(rx); self.copy_in_progress = true; self.status_message = match preview { + PreviewFocus::TerrainComposite => { + String::from("Copying terrain composite to clipboard...") + } + PreviewFocus::TerrainColor => String::from("Copying terrain color to clipboard..."), PreviewFocus::Heightmap => String::from("Copying heightmap to clipboard..."), PreviewFocus::Bumpmap => String::from("Copying bumpmap to clipboard..."), - PreviewFocus::TerrainColor => String::from("Copying terrain color to clipboard..."), }; thread::spawn(move || { @@ -377,18 +391,20 @@ impl TerrainApp { }); return; }; + let terrain_composite_texture = textures.terrain_composite.clone(); + let terrain_color_texture = textures.terrain_color.clone(); let heightmap_texture = textures.heightmap.clone(); let bumpmap_texture = textures.bumpmap.clone(); - let terrain_color_texture = textures.terrain_color.clone(); let available = ui.available_size(); let bottom_strip_height = (available.y * 0.24).clamp(120.0, 220.0); let main_height = (available.y - bottom_strip_height - 8.0).max(100.0); let main_texture = match preview_focus { + PreviewFocus::TerrainComposite => &terrain_composite_texture, + PreviewFocus::TerrainColor => &terrain_color_texture, PreviewFocus::Heightmap => &heightmap_texture, PreviewFocus::Bumpmap => &bumpmap_texture, - PreviewFocus::TerrainColor => &terrain_color_texture, }; let main_response = @@ -411,9 +427,10 @@ impl TerrainApp { ui.horizontal(|ui| { for secondary_focus in secondary_previews { let secondary_texture = match secondary_focus { + PreviewFocus::TerrainComposite => &terrain_composite_texture, + PreviewFocus::TerrainColor => &terrain_color_texture, PreviewFocus::Heightmap => &heightmap_texture, PreviewFocus::Bumpmap => &bumpmap_texture, - PreviewFocus::TerrainColor => &terrain_color_texture, }; let secondary_label = preview_label(secondary_focus); @@ -507,25 +524,46 @@ fn rgba_to_color_image(image: &image::RgbaImage) -> ColorImage { fn preview_image_data(images: &GeneratedImages, preview: PreviewFocus) -> ImageData<'static> { match preview { + PreviewFocus::TerrainComposite => { + rgba_image_to_clipboard_data(&images.terrain_composite_image) + } + PreviewFocus::TerrainColor => rgba_image_to_clipboard_data(&images.terrain_color_image), PreviewFocus::Heightmap => gray_image_to_clipboard_data(&images.heightmap_image), PreviewFocus::Bumpmap => gray_image_to_clipboard_data(&images.bumpmap_image), - PreviewFocus::TerrainColor => rgba_image_to_clipboard_data(&images.terrain_color_image), } } fn preview_label(preview: PreviewFocus) -> &'static str { match preview { + PreviewFocus::TerrainComposite => "Composite", + PreviewFocus::TerrainColor => "Terrain Color", PreviewFocus::Heightmap => "Heightmap", PreviewFocus::Bumpmap => "Bumpmap", - PreviewFocus::TerrainColor => "Terrain Color", } } -fn secondary_previews(preview: PreviewFocus) -> [PreviewFocus; 2] { +fn secondary_previews(preview: PreviewFocus) -> [PreviewFocus; 3] { match preview { - PreviewFocus::Heightmap => [PreviewFocus::TerrainColor, PreviewFocus::Bumpmap], - PreviewFocus::Bumpmap => [PreviewFocus::TerrainColor, PreviewFocus::Heightmap], - PreviewFocus::TerrainColor => [PreviewFocus::Heightmap, PreviewFocus::Bumpmap], + PreviewFocus::TerrainComposite => [ + PreviewFocus::TerrainColor, + PreviewFocus::Heightmap, + PreviewFocus::Bumpmap, + ], + PreviewFocus::TerrainColor => [ + PreviewFocus::TerrainComposite, + PreviewFocus::Heightmap, + PreviewFocus::Bumpmap, + ], + PreviewFocus::Heightmap => [ + PreviewFocus::TerrainComposite, + PreviewFocus::TerrainColor, + PreviewFocus::Bumpmap, + ], + PreviewFocus::Bumpmap => [ + PreviewFocus::TerrainComposite, + PreviewFocus::TerrainColor, + PreviewFocus::Heightmap, + ], } } diff --git a/src/generator.rs b/src/generator.rs index 3b51280..f5774c5 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -50,6 +50,7 @@ pub struct GeneratedImages { pub heightmap_image: GrayImage, pub bumpmap_image: GrayImage, pub terrain_color_image: RgbaImage, + pub terrain_composite_image: RgbaImage, #[allow(dead_code)] pub grass_mask_image: GrayImage, #[allow(dead_code)] @@ -402,6 +403,8 @@ pub fn generate(params: &GenerationParams) -> GeneratedImages { params.snowline_height, params.rock_slope_angle_degrees, ); + let terrain_composite_image = + build_soft_light_composite(&terrain_layers.terrain_color_image, &bumpmap_image); GeneratedImages { width, @@ -409,6 +412,7 @@ pub fn generate(params: &GenerationParams) -> GeneratedImages { heightmap_image, bumpmap_image, terrain_color_image: terrain_layers.terrain_color_image, + terrain_composite_image, grass_mask_image: terrain_layers.grass_mask_image, rock_mask_image: terrain_layers.rock_mask_image, snow_mask_image: terrain_layers.snow_mask_image, @@ -515,6 +519,50 @@ fn build_terrain_layers( } } +fn build_soft_light_composite(color_image: &RgbaImage, bumpmap_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 r = soft_light_channel(base[0] as f32 / 255.0, blend); + let g = soft_light_channel(base[1] as f32 / 255.0, blend); + let b = soft_light_channel(base[2] as f32 / 255.0, blend); + + composite.put_pixel( + x, + y, + Rgba([ + (r * 255.0).round() as u8, + (g * 255.0).round() as u8, + (b * 255.0).round() as u8, + base[3], + ]), + ); + } + } + + composite +} + +fn soft_light_channel(base: f32, blend: f32) -> f32 { + let base = clamp01(base); + let blend = clamp01(blend); + let result = if blend <= 0.5 { + base - (1.0 - 2.0 * blend) * base * (1.0 - base) + } else { + let d = if base <= 0.25 { + ((16.0 * base - 12.0) * base + 4.0) * base + } else { + base.sqrt() + }; + base + (2.0 * blend - 1.0) * (d - base) + }; + clamp01(result) +} + fn apply_shading(heightmap: &[f32], width: usize, height: usize, strength: f32) -> Vec { let mut output = vec![0.0; width * height];