added basic terrain coloring
This commit is contained in:
+104
-40
@@ -20,12 +20,14 @@ enum LeftTab {
|
|||||||
enum PreviewFocus {
|
enum PreviewFocus {
|
||||||
Heightmap,
|
Heightmap,
|
||||||
Bumpmap,
|
Bumpmap,
|
||||||
|
TerrainColor,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct TexturePair {
|
struct TextureSet {
|
||||||
heightmap: TextureHandle,
|
heightmap: TextureHandle,
|
||||||
bumpmap: TextureHandle,
|
bumpmap: TextureHandle,
|
||||||
|
terrain_color: TextureHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TerrainApp {
|
pub struct TerrainApp {
|
||||||
@@ -33,7 +35,7 @@ pub struct TerrainApp {
|
|||||||
generation_tab: LeftTab,
|
generation_tab: LeftTab,
|
||||||
preview_focus: PreviewFocus,
|
preview_focus: PreviewFocus,
|
||||||
hovered_preview: Option<PreviewFocus>,
|
hovered_preview: Option<PreviewFocus>,
|
||||||
textures: Option<TexturePair>,
|
textures: Option<TextureSet>,
|
||||||
generated: Option<GeneratedImages>,
|
generated: Option<GeneratedImages>,
|
||||||
generation_receiver: Option<Receiver<Result<GeneratedImages, String>>>,
|
generation_receiver: Option<Receiver<Result<GeneratedImages, String>>>,
|
||||||
generation_in_progress: bool,
|
generation_in_progress: bool,
|
||||||
@@ -47,7 +49,7 @@ impl TerrainApp {
|
|||||||
Self {
|
Self {
|
||||||
params: GenerationParams::default(),
|
params: GenerationParams::default(),
|
||||||
generation_tab: LeftTab::Generation,
|
generation_tab: LeftTab::Generation,
|
||||||
preview_focus: PreviewFocus::Bumpmap,
|
preview_focus: PreviewFocus::TerrainColor,
|
||||||
hovered_preview: None,
|
hovered_preview: None,
|
||||||
textures: None,
|
textures: None,
|
||||||
generated: None,
|
generated: None,
|
||||||
@@ -88,7 +90,7 @@ impl TerrainApp {
|
|||||||
match receiver.try_recv() {
|
match receiver.try_recv() {
|
||||||
Ok(Ok(images)) => {
|
Ok(Ok(images)) => {
|
||||||
self.status_message = format!(
|
self.status_message = format!(
|
||||||
"Generated {}x{} heightmap and bumpmap.",
|
"Generated {}x{} heightmap, bumpmap, and terrain color.",
|
||||||
images.width, images.height
|
images.width, images.height
|
||||||
);
|
);
|
||||||
self.install_textures(ctx, &images);
|
self.install_textures(ctx, &images);
|
||||||
@@ -142,10 +144,16 @@ impl TerrainApp {
|
|||||||
fn install_textures(&mut self, ctx: &Context, images: &GeneratedImages) {
|
fn install_textures(&mut self, ctx: &Context, images: &GeneratedImages) {
|
||||||
let heightmap = gray_to_color_image(&images.heightmap_image);
|
let heightmap = gray_to_color_image(&images.heightmap_image);
|
||||||
let bumpmap = gray_to_color_image(&images.bumpmap_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()),
|
heightmap: ctx.load_texture("heightmap_preview", heightmap, TextureOptions::default()),
|
||||||
bumpmap: ctx.load_texture("bumpmap_preview", bumpmap, 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;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let image = match preview {
|
let image = preview_image_data(generated, preview);
|
||||||
PreviewFocus::Heightmap => generated.heightmap_image.clone(),
|
|
||||||
PreviewFocus::Bumpmap => generated.bumpmap_image.clone(),
|
|
||||||
};
|
|
||||||
let success_message = match preview {
|
let success_message = match preview {
|
||||||
PreviewFocus::Heightmap => String::from("Heightmap copied to clipboard."),
|
PreviewFocus::Heightmap => String::from("Heightmap copied to clipboard."),
|
||||||
PreviewFocus::Bumpmap => String::from("Bumpmap 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();
|
let (tx, rx) = mpsc::channel();
|
||||||
@@ -175,10 +181,11 @@ impl TerrainApp {
|
|||||||
self.status_message = match preview {
|
self.status_message = match preview {
|
||||||
PreviewFocus::Heightmap => String::from("Copying heightmap to clipboard..."),
|
PreviewFocus::Heightmap => String::from("Copying heightmap to clipboard..."),
|
||||||
PreviewFocus::Bumpmap => String::from("Copying bumpmap to clipboard..."),
|
PreviewFocus::Bumpmap => String::from("Copying bumpmap to clipboard..."),
|
||||||
|
PreviewFocus::TerrainColor => String::from("Copying terrain color to clipboard..."),
|
||||||
};
|
};
|
||||||
|
|
||||||
thread::spawn(move || {
|
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);
|
let _ = tx.send(result);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -342,6 +349,19 @@ impl TerrainApp {
|
|||||||
0.1,
|
0.1,
|
||||||
);
|
);
|
||||||
labeled_int_slider(ui, "Octaves", &mut self.params.erosion_octaves, 1..=8);
|
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) {
|
fn ui_right_panel(&mut self, ctx: &Context) {
|
||||||
@@ -359,6 +379,7 @@ impl TerrainApp {
|
|||||||
};
|
};
|
||||||
let heightmap_texture = textures.heightmap.clone();
|
let heightmap_texture = textures.heightmap.clone();
|
||||||
let bumpmap_texture = textures.bumpmap.clone();
|
let bumpmap_texture = textures.bumpmap.clone();
|
||||||
|
let terrain_color_texture = textures.terrain_color.clone();
|
||||||
|
|
||||||
let available = ui.available_size();
|
let available = ui.available_size();
|
||||||
let bottom_strip_height = (available.y * 0.24).clamp(120.0, 220.0);
|
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 {
|
let main_texture = match preview_focus {
|
||||||
PreviewFocus::Heightmap => &heightmap_texture,
|
PreviewFocus::Heightmap => &heightmap_texture,
|
||||||
PreviewFocus::Bumpmap => &bumpmap_texture,
|
PreviewFocus::Bumpmap => &bumpmap_texture,
|
||||||
|
PreviewFocus::TerrainColor => &terrain_color_texture,
|
||||||
};
|
};
|
||||||
|
|
||||||
let main_response =
|
let main_response =
|
||||||
@@ -381,22 +403,20 @@ impl TerrainApp {
|
|||||||
}
|
}
|
||||||
ui.add_space(8.0);
|
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 panel_width = (thumb_width + 32.0).min(available.x);
|
||||||
let secondary_texture = match preview_focus {
|
let secondary_previews = secondary_previews(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",
|
|
||||||
};
|
|
||||||
|
|
||||||
ui.horizontal_centered(|ui| {
|
ui.horizontal_centered(|ui| {
|
||||||
|
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| {
|
Frame::group(ui.style()).show(ui, |ui| {
|
||||||
ui.set_min_width(panel_width);
|
ui.set_min_width(panel_width);
|
||||||
ui.set_max_width(panel_width);
|
ui.set_max_width(panel_width);
|
||||||
@@ -420,6 +440,8 @@ impl TerrainApp {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -473,23 +495,48 @@ fn gray_to_color_image(image: &image::GrayImage) -> ColorImage {
|
|||||||
ColorImage::new(size, pixels)
|
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,
|
ui: &mut Ui,
|
||||||
label: &str,
|
label: &str,
|
||||||
value: &mut usize,
|
value: &mut Num,
|
||||||
range: std::ops::RangeInclusive<usize>,
|
range: std::ops::RangeInclusive<Num>,
|
||||||
) {
|
) {
|
||||||
ui.label(label);
|
ui.label(label);
|
||||||
let mut slider_value = *value as u32;
|
ui.add(Slider::new(value, range));
|
||||||
if ui
|
|
||||||
.add(Slider::new(
|
|
||||||
&mut slider_value,
|
|
||||||
*range.start() as u32..=*range.end() as u32,
|
|
||||||
))
|
|
||||||
.changed()
|
|
||||||
{
|
|
||||||
*value = slider_value as usize;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn labeled_float_slider(
|
fn labeled_float_slider(
|
||||||
@@ -534,19 +581,36 @@ fn render_clickable_image(
|
|||||||
response
|
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);
|
let mut rgba = Vec::with_capacity((image.width() * image.height() * 4) as usize);
|
||||||
for pixel in image.pixels() {
|
for pixel in image.pixels() {
|
||||||
let gray = pixel.0[0];
|
let gray = pixel.0[0];
|
||||||
rgba.extend_from_slice(&[gray, gray, gray, 255]);
|
rgba.extend_from_slice(&[gray, gray, gray, 255]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = ImageData {
|
ImageData {
|
||||||
width: image.width() as usize,
|
width: image.width() as usize,
|
||||||
height: image.height() as usize,
|
height: image.height() as usize,
|
||||||
bytes: rgba.into(),
|
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())?;
|
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())
|
||||||
}
|
}
|
||||||
|
|||||||
+116
-8
@@ -1,4 +1,4 @@
|
|||||||
use image::{GrayImage, Luma};
|
use image::{GrayImage, Luma, Rgba, RgbaImage};
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
|
|
||||||
const TAU: f32 = 6.283_185_5;
|
const TAU: f32 = 6.283_185_5;
|
||||||
@@ -21,6 +21,8 @@ pub struct GenerationParams {
|
|||||||
pub erosion_gully_weight: f32,
|
pub erosion_gully_weight: f32,
|
||||||
pub erosion_detail: f32,
|
pub erosion_detail: f32,
|
||||||
pub erosion_octaves: usize,
|
pub erosion_octaves: usize,
|
||||||
|
pub snowline_height: u8,
|
||||||
|
pub rock_slope_angle_degrees: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for GenerationParams {
|
impl Default for GenerationParams {
|
||||||
@@ -35,6 +37,8 @@ impl Default for GenerationParams {
|
|||||||
erosion_gully_weight: 0.5,
|
erosion_gully_weight: 0.5,
|
||||||
erosion_detail: 1.5,
|
erosion_detail: 1.5,
|
||||||
erosion_octaves: 5,
|
erosion_octaves: 5,
|
||||||
|
snowline_height: 220,
|
||||||
|
rock_slope_angle_degrees: 15.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,6 +49,13 @@ pub struct GeneratedImages {
|
|||||||
pub height: usize,
|
pub height: usize,
|
||||||
pub heightmap_image: GrayImage,
|
pub heightmap_image: GrayImage,
|
||||||
pub bumpmap_image: GrayImage,
|
pub bumpmap_image: GrayImage,
|
||||||
|
pub terrain_color_image: RgbaImage,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub grass_mask_image: GrayImage,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub rock_mask_image: GrayImage,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub snow_mask_image: GrayImage,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Default)]
|
#[derive(Copy, Clone, Debug, Default)]
|
||||||
@@ -192,7 +203,14 @@ fn noised(p: Vec2, random_seed: u64) -> (f32, Vec2) {
|
|||||||
(val, Vec2::new(deriv_x, deriv_y))
|
(val, Vec2::new(deriv_x, deriv_y))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fractal_noise(p: Vec2, freq: f32, octaves: usize, lacunarity: f32, gain: f32, random_seed: u64) -> (f32, Vec2) {
|
fn fractal_noise(
|
||||||
|
p: Vec2,
|
||||||
|
freq: f32,
|
||||||
|
octaves: usize,
|
||||||
|
lacunarity: f32,
|
||||||
|
gain: f32,
|
||||||
|
random_seed: u64,
|
||||||
|
) -> (f32, Vec2) {
|
||||||
let mut val = 0.0;
|
let mut val = 0.0;
|
||||||
let mut deriv = Vec2::default();
|
let mut deriv = Vec2::default();
|
||||||
let mut nf = freq;
|
let mut nf = freq;
|
||||||
@@ -207,7 +225,14 @@ fn fractal_noise(p: Vec2, freq: f32, octaves: usize, lacunarity: f32, gain: f32,
|
|||||||
(val, deriv)
|
(val, deriv)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn phacelle_noise(p: Vec2, norm_dir: Vec2, freq: f32, offset: f32, normalization: f32, random_seed: u64) -> (Vec2, Vec2) {
|
fn phacelle_noise(
|
||||||
|
p: Vec2,
|
||||||
|
norm_dir: Vec2,
|
||||||
|
freq: f32,
|
||||||
|
offset: f32,
|
||||||
|
normalization: f32,
|
||||||
|
random_seed: u64,
|
||||||
|
) -> (Vec2, Vec2) {
|
||||||
let side_dir = Vec2::new(-norm_dir.y, norm_dir.x) * (freq * TAU);
|
let side_dir = Vec2::new(-norm_dir.y, norm_dir.x) * (freq * TAU);
|
||||||
let offset_tau = offset * TAU;
|
let offset_tau = offset * TAU;
|
||||||
|
|
||||||
@@ -241,7 +266,10 @@ fn phacelle_noise(p: Vec2, norm_dir: Vec2, freq: f32, offset: f32, normalization
|
|||||||
}
|
}
|
||||||
|
|
||||||
let interpolated = phase_dir * (1.0 / weight_sum);
|
let interpolated = phase_dir * (1.0 / weight_sum);
|
||||||
let magnitude = interpolated.dot(interpolated).sqrt().max(1.0 - normalization);
|
let magnitude = interpolated
|
||||||
|
.dot(interpolated)
|
||||||
|
.sqrt()
|
||||||
|
.max(1.0 - normalization);
|
||||||
(interpolated * (1.0 / magnitude), side_dir)
|
(interpolated * (1.0 / magnitude), side_dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,10 +302,8 @@ pub fn generate(params: &GenerationParams) -> GeneratedImages {
|
|||||||
.enumerate()
|
.enumerate()
|
||||||
.for_each(|(y, row)| {
|
.for_each(|(y, row)| {
|
||||||
for (x, value) in row.iter_mut().enumerate() {
|
for (x, value) in row.iter_mut().enumerate() {
|
||||||
let p = Vec2::new(
|
let p = Vec2::new(x as f32 / width as f32, y as f32 / height as f32)
|
||||||
x as f32 / width as f32,
|
* params.generation_scale;
|
||||||
y as f32 / height as f32,
|
|
||||||
) * params.generation_scale;
|
|
||||||
|
|
||||||
let height_freq = 3.0;
|
let height_freq = 3.0;
|
||||||
let height_amp = 0.125;
|
let height_amp = 0.125;
|
||||||
@@ -369,15 +395,33 @@ pub fn generate(params: &GenerationParams) -> GeneratedImages {
|
|||||||
let bumpmap = apply_shading(&heightmap, width, height, 0.5);
|
let bumpmap = apply_shading(&heightmap, width, height, 0.5);
|
||||||
let heightmap_image = grayscale_image_from_values(&heightmap_normalized, width, height);
|
let heightmap_image = grayscale_image_from_values(&heightmap_normalized, width, height);
|
||||||
let bumpmap_image = grayscale_image_from_values(&bumpmap, width, height);
|
let bumpmap_image = grayscale_image_from_values(&bumpmap, width, height);
|
||||||
|
let terrain_layers = build_terrain_layers(
|
||||||
|
&heightmap_normalized,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
params.snowline_height,
|
||||||
|
params.rock_slope_angle_degrees,
|
||||||
|
);
|
||||||
|
|
||||||
GeneratedImages {
|
GeneratedImages {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
heightmap_image,
|
heightmap_image,
|
||||||
bumpmap_image,
|
bumpmap_image,
|
||||||
|
terrain_color_image: terrain_layers.terrain_color_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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct TerrainLayers {
|
||||||
|
terrain_color_image: RgbaImage,
|
||||||
|
grass_mask_image: GrayImage,
|
||||||
|
rock_mask_image: GrayImage,
|
||||||
|
snow_mask_image: GrayImage,
|
||||||
|
}
|
||||||
|
|
||||||
fn normalize_values(values: &[f32]) -> Vec<f32> {
|
fn normalize_values(values: &[f32]) -> Vec<f32> {
|
||||||
let mut min_val = f32::INFINITY;
|
let mut min_val = f32::INFINITY;
|
||||||
let mut max_val = f32::NEG_INFINITY;
|
let mut max_val = f32::NEG_INFINITY;
|
||||||
@@ -407,6 +451,70 @@ fn grayscale_image_from_values(values: &[f32], width: usize, height: usize) -> G
|
|||||||
image
|
image
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_terrain_layers(
|
||||||
|
heightmap: &[f32],
|
||||||
|
width: usize,
|
||||||
|
height: usize,
|
||||||
|
snowline_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 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 rock_color = Rgba([122, 112, 98, 255]);
|
||||||
|
let snow_color = Rgba([245, 248, 252, 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 left = sample_height(heightmap, width, height, x.saturating_sub(1), y);
|
||||||
|
let right = sample_height(heightmap, width, height, (x + 1).min(width - 1), y);
|
||||||
|
let up = sample_height(heightmap, width, height, x, y.saturating_sub(1));
|
||||||
|
let down = sample_height(heightmap, width, height, x, (y + 1).min(height - 1));
|
||||||
|
|
||||||
|
let dx = (right - left) * 0.5 * width as f32;
|
||||||
|
let dy = (down - up) * 0.5 * height as f32;
|
||||||
|
let slope_angle = dx.hypot(dy).atan();
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
let is_rock = slope_angle >= slope_threshold_radians;
|
||||||
|
|
||||||
|
grass_mask_image.put_pixel(x as u32, y as u32, Luma([255]));
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
terrain_color_image.put_pixel(x as u32, y as u32, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TerrainLayers {
|
||||||
|
terrain_color_image,
|
||||||
|
grass_mask_image,
|
||||||
|
rock_mask_image,
|
||||||
|
snow_mask_image,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_shading(heightmap: &[f32], width: usize, height: usize, strength: f32) -> Vec<f32> {
|
fn apply_shading(heightmap: &[f32], width: usize, height: usize, strength: f32) -> Vec<f32> {
|
||||||
let mut output = vec![0.0; width * height];
|
let mut output = vec![0.0; width * height];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user