added basic terrain coloring
This commit is contained in:
+126
-62
@@ -20,12 +20,14 @@ enum LeftTab {
|
||||
enum PreviewFocus {
|
||||
Heightmap,
|
||||
Bumpmap,
|
||||
TerrainColor,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TexturePair {
|
||||
struct TextureSet {
|
||||
heightmap: TextureHandle,
|
||||
bumpmap: TextureHandle,
|
||||
terrain_color: TextureHandle,
|
||||
}
|
||||
|
||||
pub struct TerrainApp {
|
||||
@@ -33,7 +35,7 @@ pub struct TerrainApp {
|
||||
generation_tab: LeftTab,
|
||||
preview_focus: PreviewFocus,
|
||||
hovered_preview: Option<PreviewFocus>,
|
||||
textures: Option<TexturePair>,
|
||||
textures: Option<TextureSet>,
|
||||
generated: Option<GeneratedImages>,
|
||||
generation_receiver: Option<Receiver<Result<GeneratedImages, String>>>,
|
||||
generation_in_progress: bool,
|
||||
@@ -47,7 +49,7 @@ impl TerrainApp {
|
||||
Self {
|
||||
params: GenerationParams::default(),
|
||||
generation_tab: LeftTab::Generation,
|
||||
preview_focus: PreviewFocus::Bumpmap,
|
||||
preview_focus: PreviewFocus::TerrainColor,
|
||||
hovered_preview: None,
|
||||
textures: None,
|
||||
generated: None,
|
||||
@@ -88,7 +90,7 @@ impl TerrainApp {
|
||||
match receiver.try_recv() {
|
||||
Ok(Ok(images)) => {
|
||||
self.status_message = format!(
|
||||
"Generated {}x{} heightmap and bumpmap.",
|
||||
"Generated {}x{} heightmap, bumpmap, and terrain color.",
|
||||
images.width, images.height
|
||||
);
|
||||
self.install_textures(ctx, &images);
|
||||
@@ -142,10 +144,16 @@ 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_color = rgba_to_color_image(&images.terrain_color_image);
|
||||
|
||||
self.textures = Some(TexturePair {
|
||||
self.textures = Some(TextureSet {
|
||||
heightmap: ctx.load_texture("heightmap_preview", heightmap, TextureOptions::default()),
|
||||
bumpmap: ctx.load_texture("bumpmap_preview", bumpmap, TextureOptions::default()),
|
||||
terrain_color: ctx.load_texture(
|
||||
"terrain_color_preview",
|
||||
terrain_color,
|
||||
TextureOptions::default(),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -160,13 +168,11 @@ impl TerrainApp {
|
||||
return;
|
||||
};
|
||||
|
||||
let image = match preview {
|
||||
PreviewFocus::Heightmap => generated.heightmap_image.clone(),
|
||||
PreviewFocus::Bumpmap => generated.bumpmap_image.clone(),
|
||||
};
|
||||
let image = preview_image_data(generated, preview);
|
||||
let success_message = match preview {
|
||||
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();
|
||||
@@ -175,10 +181,11 @@ impl TerrainApp {
|
||||
self.status_message = match preview {
|
||||
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 || {
|
||||
let result = copy_gray_image_to_clipboard(&image).map(|_| success_message);
|
||||
let result = copy_image_to_clipboard(&image).map(|_| success_message);
|
||||
let _ = tx.send(result);
|
||||
});
|
||||
}
|
||||
@@ -342,6 +349,19 @@ impl TerrainApp {
|
||||
0.1,
|
||||
);
|
||||
labeled_int_slider(ui, "Octaves", &mut self.params.erosion_octaves, 1..=8);
|
||||
labeled_int_slider(
|
||||
ui,
|
||||
"Snowline Height",
|
||||
&mut self.params.snowline_height,
|
||||
0..=255,
|
||||
);
|
||||
labeled_float_slider(
|
||||
ui,
|
||||
"Rock Slope Angle",
|
||||
&mut self.params.rock_slope_angle_degrees,
|
||||
0.0..=89.0,
|
||||
1.0,
|
||||
);
|
||||
}
|
||||
|
||||
fn ui_right_panel(&mut self, ctx: &Context) {
|
||||
@@ -359,6 +379,7 @@ impl TerrainApp {
|
||||
};
|
||||
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);
|
||||
@@ -367,6 +388,7 @@ impl TerrainApp {
|
||||
let main_texture = match preview_focus {
|
||||
PreviewFocus::Heightmap => &heightmap_texture,
|
||||
PreviewFocus::Bumpmap => &bumpmap_texture,
|
||||
PreviewFocus::TerrainColor => &terrain_color_texture,
|
||||
};
|
||||
|
||||
let main_response =
|
||||
@@ -381,44 +403,44 @@ impl TerrainApp {
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
|
||||
let thumb_width = (available.x * 0.28).clamp(120.0, 240.0);
|
||||
let thumb_width = (available.x * 0.26).clamp(120.0, 220.0);
|
||||
let panel_width = (thumb_width + 32.0).min(available.x);
|
||||
let secondary_texture = match preview_focus {
|
||||
PreviewFocus::Heightmap => &bumpmap_texture,
|
||||
PreviewFocus::Bumpmap => &heightmap_texture,
|
||||
};
|
||||
let secondary_focus = match preview_focus {
|
||||
PreviewFocus::Heightmap => PreviewFocus::Bumpmap,
|
||||
PreviewFocus::Bumpmap => PreviewFocus::Heightmap,
|
||||
};
|
||||
let secondary_label = match secondary_focus {
|
||||
PreviewFocus::Heightmap => "Heightmap",
|
||||
PreviewFocus::Bumpmap => "Bumpmap",
|
||||
};
|
||||
let secondary_previews = secondary_previews(preview_focus);
|
||||
|
||||
ui.horizontal_centered(|ui| {
|
||||
Frame::group(ui.style()).show(ui, |ui| {
|
||||
ui.set_min_width(panel_width);
|
||||
ui.set_max_width(panel_width);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(secondary_label).strong());
|
||||
ui.add_space(6.0);
|
||||
let secondary_response = render_clickable_image(
|
||||
ui,
|
||||
secondary_texture,
|
||||
Vec2::new(thumb_width, bottom_strip_height - 32.0),
|
||||
true,
|
||||
);
|
||||
if secondary_response.hovered() {
|
||||
pending_hover = Some(secondary_focus);
|
||||
}
|
||||
if Self::preview_context_menu(&secondary_response) {
|
||||
pending_copy = Some(secondary_focus);
|
||||
}
|
||||
if secondary_response.clicked() {
|
||||
pending_focus = Some(secondary_focus);
|
||||
}
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
for secondary_focus in secondary_previews {
|
||||
let secondary_texture = match secondary_focus {
|
||||
PreviewFocus::Heightmap => &heightmap_texture,
|
||||
PreviewFocus::Bumpmap => &bumpmap_texture,
|
||||
PreviewFocus::TerrainColor => &terrain_color_texture,
|
||||
};
|
||||
let secondary_label = preview_label(secondary_focus);
|
||||
|
||||
Frame::group(ui.style()).show(ui, |ui| {
|
||||
ui.set_min_width(panel_width);
|
||||
ui.set_max_width(panel_width);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(secondary_label).strong());
|
||||
ui.add_space(6.0);
|
||||
let secondary_response = render_clickable_image(
|
||||
ui,
|
||||
secondary_texture,
|
||||
Vec2::new(thumb_width, bottom_strip_height - 32.0),
|
||||
true,
|
||||
);
|
||||
if secondary_response.hovered() {
|
||||
pending_hover = Some(secondary_focus);
|
||||
}
|
||||
if Self::preview_context_menu(&secondary_response) {
|
||||
pending_copy = Some(secondary_focus);
|
||||
}
|
||||
if secondary_response.clicked() {
|
||||
pending_focus = Some(secondary_focus);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -473,23 +495,48 @@ fn gray_to_color_image(image: &image::GrayImage) -> ColorImage {
|
||||
ColorImage::new(size, pixels)
|
||||
}
|
||||
|
||||
fn labeled_int_slider(
|
||||
fn rgba_to_color_image(image: &image::RgbaImage) -> ColorImage {
|
||||
let size = [image.width() as usize, image.height() as usize];
|
||||
let mut pixels = Vec::with_capacity(size[0] * size[1]);
|
||||
for pixel in image.pixels() {
|
||||
let [r, g, b, a] = pixel.0;
|
||||
pixels.push(Color32::from_rgba_unmultiplied(r, g, b, a));
|
||||
}
|
||||
ColorImage::new(size, pixels)
|
||||
}
|
||||
|
||||
fn preview_image_data(images: &GeneratedImages, preview: PreviewFocus) -> ImageData<'static> {
|
||||
match preview {
|
||||
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::Heightmap => "Heightmap",
|
||||
PreviewFocus::Bumpmap => "Bumpmap",
|
||||
PreviewFocus::TerrainColor => "Terrain Color",
|
||||
}
|
||||
}
|
||||
|
||||
fn secondary_previews(preview: PreviewFocus) -> [PreviewFocus; 2] {
|
||||
match preview {
|
||||
PreviewFocus::Heightmap => [PreviewFocus::TerrainColor, PreviewFocus::Bumpmap],
|
||||
PreviewFocus::Bumpmap => [PreviewFocus::TerrainColor, PreviewFocus::Heightmap],
|
||||
PreviewFocus::TerrainColor => [PreviewFocus::Heightmap, PreviewFocus::Bumpmap],
|
||||
}
|
||||
}
|
||||
|
||||
fn labeled_int_slider<Num: egui::emath::Numeric>(
|
||||
ui: &mut Ui,
|
||||
label: &str,
|
||||
value: &mut usize,
|
||||
range: std::ops::RangeInclusive<usize>,
|
||||
value: &mut Num,
|
||||
range: std::ops::RangeInclusive<Num>,
|
||||
) {
|
||||
ui.label(label);
|
||||
let mut slider_value = *value as u32;
|
||||
if ui
|
||||
.add(Slider::new(
|
||||
&mut slider_value,
|
||||
*range.start() as u32..=*range.end() as u32,
|
||||
))
|
||||
.changed()
|
||||
{
|
||||
*value = slider_value as usize;
|
||||
}
|
||||
ui.add(Slider::new(value, range));
|
||||
}
|
||||
|
||||
fn labeled_float_slider(
|
||||
@@ -534,19 +581,36 @@ fn render_clickable_image(
|
||||
response
|
||||
}
|
||||
|
||||
fn copy_gray_image_to_clipboard(image: &image::GrayImage) -> Result<(), String> {
|
||||
fn gray_image_to_clipboard_data(image: &image::GrayImage) -> ImageData<'static> {
|
||||
let mut rgba = Vec::with_capacity((image.width() * image.height() * 4) as usize);
|
||||
for pixel in image.pixels() {
|
||||
let gray = pixel.0[0];
|
||||
rgba.extend_from_slice(&[gray, gray, gray, 255]);
|
||||
}
|
||||
|
||||
let data = ImageData {
|
||||
ImageData {
|
||||
width: image.width() as usize,
|
||||
height: image.height() as usize,
|
||||
bytes: rgba.into(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn rgba_image_to_clipboard_data(image: &image::RgbaImage) -> ImageData<'static> {
|
||||
let mut rgba = Vec::with_capacity((image.width() * image.height() * 4) as usize);
|
||||
for pixel in image.pixels() {
|
||||
rgba.extend_from_slice(&pixel.0);
|
||||
}
|
||||
|
||||
ImageData {
|
||||
width: image.width() as usize,
|
||||
height: image.height() as usize,
|
||||
bytes: rgba.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_image_to_clipboard(image: &ImageData<'static>) -> Result<(), String> {
|
||||
let mut clipboard = Clipboard::new().map_err(|error| error.to_string())?;
|
||||
clipboard.set_image(data).map_err(|error| error.to_string())
|
||||
clipboard
|
||||
.set_image(image.clone())
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user