Compare commits
10
Commits
75ae9056d3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c362699b1 | ||
|
|
8395993edf | ||
|
|
b9810bf873 | ||
|
|
20c4871971 | ||
|
|
5ad1253b90 | ||
|
|
fc399095bd | ||
|
|
57b20fe697 | ||
|
|
5510db8e9a | ||
|
|
e188964a41 | ||
|
|
ccaf143b9f |
Generated
+74
-15
@@ -116,7 +116,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "advanced_erosion_terrain_heightmap_generation"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"eframe",
|
||||
@@ -124,6 +124,7 @@ dependencies = [
|
||||
"image",
|
||||
"rand",
|
||||
"rayon",
|
||||
"rfd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -461,6 +462,15 @@ dependencies = [
|
||||
"objc2 0.5.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
|
||||
dependencies = [
|
||||
"objc2 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blocking"
|
||||
version = "1.6.2"
|
||||
@@ -755,6 +765,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"block2 0.6.2",
|
||||
"libc",
|
||||
"objc2 0.6.4",
|
||||
]
|
||||
|
||||
@@ -1536,10 +1548,23 @@ checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
"image-webp",
|
||||
"moxcms",
|
||||
"num-traits",
|
||||
"png",
|
||||
"tiff",
|
||||
"zune-core",
|
||||
"zune-jpeg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image-webp"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
|
||||
dependencies = [
|
||||
"byteorder-lite",
|
||||
"quick-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1939,7 +1964,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"libc",
|
||||
"objc2 0.5.2",
|
||||
"objc2-core-data",
|
||||
@@ -1955,6 +1980,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"block2 0.6.2",
|
||||
"objc2 0.6.4",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
@@ -1968,7 +1994,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-core-location",
|
||||
"objc2-foundation 0.2.2",
|
||||
@@ -1980,7 +2006,7 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
|
||||
dependencies = [
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
]
|
||||
@@ -1992,7 +2018,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
]
|
||||
@@ -2027,7 +2053,7 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
|
||||
dependencies = [
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
"objc2-metal",
|
||||
@@ -2039,7 +2065,7 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781"
|
||||
dependencies = [
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-contacts",
|
||||
"objc2-foundation 0.2.2",
|
||||
@@ -2058,7 +2084,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"dispatch",
|
||||
"libc",
|
||||
"objc2 0.5.2",
|
||||
@@ -2092,7 +2118,7 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
|
||||
dependencies = [
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-app-kit 0.2.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
@@ -2105,7 +2131,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
]
|
||||
@@ -2117,7 +2143,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
"objc2-metal",
|
||||
@@ -2140,7 +2166,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-cloud-kit",
|
||||
"objc2-core-data",
|
||||
@@ -2160,7 +2186,7 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
|
||||
dependencies = [
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
]
|
||||
@@ -2172,7 +2198,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-core-location",
|
||||
"objc2-foundation 0.2.2",
|
||||
@@ -2339,6 +2365,12 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pollster"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.13.1"
|
||||
@@ -2558,6 +2590,33 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
|
||||
|
||||
[[package]]
|
||||
name = "rfd"
|
||||
version = "0.17.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220"
|
||||
dependencies = [
|
||||
"block2 0.6.2",
|
||||
"dispatch2",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"log",
|
||||
"objc2 0.6.4",
|
||||
"objc2-app-kit 0.3.2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.2",
|
||||
"percent-encoding",
|
||||
"pollster",
|
||||
"raw-window-handle",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"web-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "1.1.0"
|
||||
@@ -3953,7 +4012,7 @@ dependencies = [
|
||||
"android-activity",
|
||||
"atomic-waker",
|
||||
"bitflags 2.11.1",
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"bytemuck",
|
||||
"calloop 0.13.0",
|
||||
"cfg_aliases",
|
||||
|
||||
+3
-2
@@ -1,15 +1,16 @@
|
||||
[package]
|
||||
name = "advanced_erosion_terrain_heightmap_generation"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
arboard = "3.6"
|
||||
eframe = "0.33"
|
||||
egui = "0.33"
|
||||
image = { version = "0.25", default-features = false, features = ["png"] }
|
||||
image = { version = "0.25", default-features = false, features = ["jpeg", "png", "webp"] }
|
||||
rand = "0.9"
|
||||
rayon = "1.10"
|
||||
rfd = "0.17.2"
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# advanced_erosion_terrain_heightmap_generation
|
||||
# Grim Rune Terrain Generator
|
||||
|
||||
A standalone Rust terrain generator using `egui`, using a terrain generation method by Runevision (Skovbo Johansen).
|
||||
|
||||
@@ -16,7 +16,7 @@ git clone https://codeberg.org/Grimsace/advanced_erosion_terrain_heightmap_gener
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
## Running:
|
||||
## Easy Way:
|
||||
|
||||
Download the latest release!
|
||||
|
||||
|
||||
+379
-46
@@ -18,12 +18,16 @@ enum LeftTab {
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
enum PreviewFocus {
|
||||
TerrainComposite,
|
||||
TerrainColor,
|
||||
Heightmap,
|
||||
Bumpmap,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TexturePair {
|
||||
struct TextureSet {
|
||||
terrain_composite: TextureHandle,
|
||||
terrain_color: TextureHandle,
|
||||
heightmap: TextureHandle,
|
||||
bumpmap: TextureHandle,
|
||||
}
|
||||
@@ -33,12 +37,14 @@ 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,
|
||||
copy_receiver: Option<Receiver<Result<String, String>>>,
|
||||
copy_in_progress: bool,
|
||||
export_receiver: Option<Receiver<Result<String, String>>>,
|
||||
export_in_progress: bool,
|
||||
status_message: String,
|
||||
}
|
||||
|
||||
@@ -47,7 +53,7 @@ impl TerrainApp {
|
||||
Self {
|
||||
params: GenerationParams::default(),
|
||||
generation_tab: LeftTab::Generation,
|
||||
preview_focus: PreviewFocus::Bumpmap,
|
||||
preview_focus: PreviewFocus::TerrainComposite,
|
||||
hovered_preview: None,
|
||||
textures: None,
|
||||
generated: None,
|
||||
@@ -55,6 +61,8 @@ impl TerrainApp {
|
||||
generation_in_progress: false,
|
||||
copy_receiver: None,
|
||||
copy_in_progress: false,
|
||||
export_receiver: None,
|
||||
export_in_progress: false,
|
||||
status_message: String::from("Adjust parameters and click Generate."),
|
||||
}
|
||||
}
|
||||
@@ -88,7 +96,7 @@ impl TerrainApp {
|
||||
match receiver.try_recv() {
|
||||
Ok(Ok(images)) => {
|
||||
self.status_message = format!(
|
||||
"Generated {}x{} heightmap and bumpmap.",
|
||||
"Generated {}x{} heightmap, bumpmap, terrain color, and composite.",
|
||||
images.width, images.height
|
||||
);
|
||||
self.install_textures(ctx, &images);
|
||||
@@ -139,11 +147,173 @@ impl TerrainApp {
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_export(&mut self, ctx: &Context) {
|
||||
let Some(receiver) = &self.export_receiver else {
|
||||
return;
|
||||
};
|
||||
|
||||
match receiver.try_recv() {
|
||||
Ok(Ok(message)) => {
|
||||
self.status_message = message;
|
||||
self.export_receiver = None;
|
||||
self.export_in_progress = false;
|
||||
}
|
||||
Ok(Err(error)) => {
|
||||
self.status_message = error;
|
||||
self.export_receiver = None;
|
||||
self.export_in_progress = false;
|
||||
}
|
||||
Err(TryRecvError::Empty) => {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
Err(TryRecvError::Disconnected) => {
|
||||
self.status_message = String::from("Export worker disconnected.");
|
||||
self.export_receiver = None;
|
||||
self.export_in_progress = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn start_export(&mut self, preview: PreviewFocus) {
|
||||
if self.export_in_progress {
|
||||
self.status_message = String::from("Export already in progress...");
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(generated) = &self.generated else {
|
||||
self.status_message = String::from("No generated image is available to export.");
|
||||
return;
|
||||
};
|
||||
|
||||
let label = preview_label(preview).to_lowercase().replace(' ', "_");
|
||||
let default_name = format!("{}.png", label);
|
||||
|
||||
let files = rfd::FileDialog::new()
|
||||
.add_filter("PNG Image", &["png"])
|
||||
.add_filter("JPEG Image", &["jpg", "jpeg"])
|
||||
.add_filter("WebP Image", &["webp"])
|
||||
.set_file_name(default_name)
|
||||
.save_file();
|
||||
|
||||
let Some(path) = files else {
|
||||
return;
|
||||
};
|
||||
|
||||
let generated = generated.clone();
|
||||
let (tx, rx) = mpsc::channel();
|
||||
self.export_receiver = Some(rx);
|
||||
self.export_in_progress = true;
|
||||
self.status_message = format!("Exporting to {}...", path.display());
|
||||
|
||||
thread::spawn(move || {
|
||||
let result = (|| -> Result<String, String> {
|
||||
let format = match path.extension().and_then(|s| s.to_str()) {
|
||||
Some("jpg") | Some("jpeg") => image::ImageFormat::Jpeg,
|
||||
Some("webp") => image::ImageFormat::WebP,
|
||||
_ => image::ImageFormat::Png,
|
||||
};
|
||||
|
||||
match preview {
|
||||
PreviewFocus::TerrainComposite => generated
|
||||
.terrain_composite_image
|
||||
.save_with_format(&path, format),
|
||||
PreviewFocus::TerrainColor => generated
|
||||
.terrain_color_image
|
||||
.save_with_format(&path, format),
|
||||
PreviewFocus::Heightmap => {
|
||||
generated.heightmap_image.save_with_format(&path, format)
|
||||
}
|
||||
PreviewFocus::Bumpmap => {
|
||||
generated.bumpmap_image.save_with_format(&path, format)
|
||||
}
|
||||
}
|
||||
.map_err(|e| format!("Failed to save image: {}", e))?;
|
||||
|
||||
Ok(format!("Successfully exported to {}.", path.display()))
|
||||
})();
|
||||
let _ = tx.send(result);
|
||||
});
|
||||
}
|
||||
|
||||
fn start_batch_export(&mut self) {
|
||||
if self.export_in_progress {
|
||||
self.status_message = String::from("Export already in progress...");
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(generated) = &self.generated else {
|
||||
self.status_message = String::from("No generated images available to export.");
|
||||
return;
|
||||
};
|
||||
|
||||
let folder = rfd::FileDialog::new()
|
||||
.set_title("Select Export Folder")
|
||||
.pick_folder();
|
||||
let Some(path) = folder else {
|
||||
return;
|
||||
};
|
||||
|
||||
let generated = generated.clone();
|
||||
let (tx, rx) = mpsc::channel();
|
||||
self.export_receiver = Some(rx);
|
||||
self.export_in_progress = true;
|
||||
self.status_message = format!("Exporting all layers to {}...", path.display());
|
||||
|
||||
thread::spawn(move || {
|
||||
let result = (|| -> Result<String, String> {
|
||||
let save = |img_rgba: Option<&image::RgbaImage>,
|
||||
img_gray: Option<&image::GrayImage>,
|
||||
name: &str| {
|
||||
let full_path = path.join(format!("{}.png", name));
|
||||
if let Some(img) = img_rgba {
|
||||
img.save(&full_path)
|
||||
} else if let Some(img) = img_gray {
|
||||
img.save(&full_path)
|
||||
} else {
|
||||
return Err(format!("No image data for {}", name));
|
||||
}
|
||||
.map_err(|e| format!("Failed to save {}: {}", name, e))
|
||||
};
|
||||
|
||||
save(
|
||||
Some(&generated.terrain_composite_image),
|
||||
None,
|
||||
"terrain_composite",
|
||||
)?;
|
||||
save(Some(&generated.terrain_color_image), None, "terrain_color")?;
|
||||
save(None, Some(&generated.heightmap_image), "heightmap")?;
|
||||
save(None, Some(&generated.bumpmap_image), "bumpmap")?;
|
||||
save(None, Some(&generated.grass_mask_image), "mask_grass")?;
|
||||
save(None, Some(&generated.water_mask_image), "mask_water")?;
|
||||
save(None, Some(&generated.rock_mask_image), "mask_rock")?;
|
||||
save(None, Some(&generated.snow_mask_image), "mask_snow")?;
|
||||
|
||||
Ok(format!(
|
||||
"Successfully exported 8 layers to {}.",
|
||||
path.display()
|
||||
))
|
||||
})();
|
||||
let _ = tx.send(result);
|
||||
});
|
||||
}
|
||||
|
||||
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(TexturePair {
|
||||
self.textures = Some(TextureSet {
|
||||
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()),
|
||||
});
|
||||
@@ -160,11 +330,12 @@ 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::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."),
|
||||
};
|
||||
@@ -173,25 +344,34 @@ impl TerrainApp {
|
||||
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..."),
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
fn preview_context_menu(response: &Response) -> bool {
|
||||
fn preview_context_menu(response: &Response) -> (bool, bool) {
|
||||
let mut should_copy = false;
|
||||
let mut should_export = false;
|
||||
response.context_menu(|ui| {
|
||||
if ui.button("Copy").clicked() {
|
||||
should_copy = true;
|
||||
ui.close();
|
||||
}
|
||||
if ui.button("Export").clicked() {
|
||||
should_export = true;
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
should_copy
|
||||
(should_copy, should_export)
|
||||
}
|
||||
|
||||
fn overlay_copy_button(ui: &mut Ui, anchor_rect: egui::Rect, preview: PreviewFocus) -> bool {
|
||||
@@ -231,6 +411,43 @@ impl TerrainApp {
|
||||
clicked
|
||||
}
|
||||
|
||||
fn overlay_export_button(ui: &mut Ui, anchor_rect: egui::Rect, preview: PreviewFocus) -> bool {
|
||||
let button_size = Vec2::new(28.0, 28.0);
|
||||
let button_rect = Align2::RIGHT_TOP
|
||||
.align_size_within_rect(button_size, anchor_rect.shrink2(Vec2::new(8.0 + 32.0, 8.0)));
|
||||
let response = ui.interact(
|
||||
button_rect,
|
||||
ui.make_persistent_id(("main_export_button", preview == PreviewFocus::Bumpmap)),
|
||||
Sense::click(),
|
||||
);
|
||||
|
||||
let fill = if response.hovered() {
|
||||
Color32::from_rgba_premultiplied(70, 90, 120, 220)
|
||||
} else {
|
||||
Color32::from_rgba_premultiplied(40, 40, 40, 180)
|
||||
};
|
||||
ui.painter()
|
||||
.rect_filled(button_rect, CornerRadius::same(6), fill);
|
||||
ui.painter().rect_stroke(
|
||||
button_rect,
|
||||
CornerRadius::same(6),
|
||||
Stroke::new(1.0, Color32::from_rgba_premultiplied(255, 255, 255, 60)),
|
||||
StrokeKind::Outside,
|
||||
);
|
||||
ui.painter().text(
|
||||
button_rect.center(),
|
||||
Align2::CENTER_CENTER,
|
||||
"📥",
|
||||
FontId::proportional(14.0),
|
||||
Color32::WHITE,
|
||||
);
|
||||
|
||||
let clicked = response.clicked();
|
||||
response.on_hover_text("Export");
|
||||
let _ = preview;
|
||||
clicked
|
||||
}
|
||||
|
||||
fn tab_button(ui: &mut Ui, current: &mut LeftTab, tab: LeftTab, label: &str) {
|
||||
let selected = *current == tab;
|
||||
if ui.selectable_label(selected, label).clicked() {
|
||||
@@ -273,6 +490,17 @@ impl TerrainApp {
|
||||
self.start_generation();
|
||||
}
|
||||
|
||||
ui.add_space(8.0);
|
||||
if ui
|
||||
.add_sized(
|
||||
[ui.available_width(), 32.0],
|
||||
egui::Button::new("Export All Layers"),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
self.start_batch_export();
|
||||
}
|
||||
|
||||
if self.generation_in_progress {
|
||||
ui.add_space(8.0);
|
||||
ui.spinner();
|
||||
@@ -281,6 +509,10 @@ impl TerrainApp {
|
||||
ui.add_space(4.0);
|
||||
ui.spinner();
|
||||
}
|
||||
if self.export_in_progress {
|
||||
ui.add_space(4.0);
|
||||
ui.spinner();
|
||||
}
|
||||
|
||||
ui.add_space(8.0);
|
||||
ui.label(&self.status_message);
|
||||
@@ -342,10 +574,30 @@ 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_int_slider(
|
||||
ui,
|
||||
"Water Table Height",
|
||||
&mut self.params.water_table_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) {
|
||||
let mut pending_copy = None;
|
||||
let mut pending_export = None;
|
||||
let mut pending_focus = None;
|
||||
let mut pending_hover = None;
|
||||
let preview_focus = self.preview_focus;
|
||||
@@ -357,6 +609,8 @@ 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();
|
||||
|
||||
@@ -365,6 +619,8 @@ impl TerrainApp {
|
||||
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,
|
||||
};
|
||||
@@ -374,29 +630,32 @@ impl TerrainApp {
|
||||
if main_response.hovered() {
|
||||
pending_hover = Some(preview_focus);
|
||||
}
|
||||
if Self::preview_context_menu(&main_response)
|
||||
|| Self::overlay_copy_button(ui, main_response.rect, preview_focus)
|
||||
{
|
||||
|
||||
let (context_copy, context_export) = Self::preview_context_menu(&main_response);
|
||||
if context_copy || Self::overlay_copy_button(ui, main_response.rect, preview_focus) {
|
||||
pending_copy = Some(preview_focus);
|
||||
}
|
||||
if context_export || Self::overlay_export_button(ui, main_response.rect, preview_focus)
|
||||
{
|
||||
pending_export = Some(preview_focus);
|
||||
}
|
||||
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| {
|
||||
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,
|
||||
};
|
||||
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);
|
||||
@@ -412,20 +671,30 @@ impl TerrainApp {
|
||||
if secondary_response.hovered() {
|
||||
pending_hover = Some(secondary_focus);
|
||||
}
|
||||
if Self::preview_context_menu(&secondary_response) {
|
||||
let (context_copy, context_export) =
|
||||
Self::preview_context_menu(&secondary_response);
|
||||
if context_copy {
|
||||
pending_copy = Some(secondary_focus);
|
||||
}
|
||||
if context_export {
|
||||
pending_export = Some(secondary_focus);
|
||||
}
|
||||
if secondary_response.clicked() {
|
||||
pending_focus = Some(secondary_focus);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if let Some(preview) = pending_copy {
|
||||
self.copy_preview(preview);
|
||||
}
|
||||
if let Some(preview) = pending_export {
|
||||
self.start_export(preview);
|
||||
}
|
||||
if let Some(preview) = pending_focus {
|
||||
self.preview_focus = preview;
|
||||
}
|
||||
@@ -438,6 +707,7 @@ impl eframe::App for TerrainApp {
|
||||
self.hovered_preview = None;
|
||||
self.poll_generation(ctx);
|
||||
self.poll_copy(ctx);
|
||||
self.poll_export(ctx);
|
||||
self.ui_left_panel(ctx);
|
||||
self.ui_right_panel(ctx);
|
||||
|
||||
@@ -473,23 +743,69 @@ 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::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),
|
||||
}
|
||||
}
|
||||
|
||||
fn preview_label(preview: PreviewFocus) -> &'static str {
|
||||
match preview {
|
||||
PreviewFocus::TerrainComposite => "Composite",
|
||||
PreviewFocus::TerrainColor => "Terrain Color",
|
||||
PreviewFocus::Heightmap => "Heightmap",
|
||||
PreviewFocus::Bumpmap => "Bumpmap",
|
||||
}
|
||||
}
|
||||
|
||||
fn secondary_previews(preview: PreviewFocus) -> [PreviewFocus; 3] {
|
||||
match preview {
|
||||
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,
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
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 +850,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())
|
||||
}
|
||||
|
||||
+227
-8
@@ -1,4 +1,4 @@
|
||||
use image::{GrayImage, Luma};
|
||||
use image::{GrayImage, Luma, Rgba, RgbaImage};
|
||||
use rayon::prelude::*;
|
||||
|
||||
const TAU: f32 = 6.283_185_5;
|
||||
@@ -21,6 +21,9 @@ pub struct GenerationParams {
|
||||
pub erosion_gully_weight: f32,
|
||||
pub erosion_detail: f32,
|
||||
pub erosion_octaves: usize,
|
||||
pub snowline_height: u8,
|
||||
pub water_table_height: u8,
|
||||
pub rock_slope_angle_degrees: f32,
|
||||
}
|
||||
|
||||
impl Default for GenerationParams {
|
||||
@@ -35,6 +38,9 @@ impl Default for GenerationParams {
|
||||
erosion_gully_weight: 0.5,
|
||||
erosion_detail: 1.5,
|
||||
erosion_octaves: 5,
|
||||
snowline_height: 220,
|
||||
water_table_height: 0,
|
||||
rock_slope_angle_degrees: 15.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,6 +51,16 @@ pub struct GeneratedImages {
|
||||
pub height: usize,
|
||||
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)]
|
||||
pub water_mask_image: GrayImage,
|
||||
#[allow(dead_code)]
|
||||
pub rock_mask_image: GrayImage,
|
||||
#[allow(dead_code)]
|
||||
pub snow_mask_image: GrayImage,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
@@ -123,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);
|
||||
@@ -192,7 +212,14 @@ fn noised(p: Vec2, random_seed: u64) -> (f32, Vec2) {
|
||||
(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 deriv = Vec2::default();
|
||||
let mut nf = freq;
|
||||
@@ -207,7 +234,14 @@ fn fractal_noise(p: Vec2, freq: f32, octaves: usize, lacunarity: f32, gain: f32,
|
||||
(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 offset_tau = offset * TAU;
|
||||
|
||||
@@ -241,7 +275,10 @@ fn phacelle_noise(p: Vec2, norm_dir: Vec2, freq: f32, offset: f32, normalization
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -274,10 +311,8 @@ pub fn generate(params: &GenerationParams) -> GeneratedImages {
|
||||
.enumerate()
|
||||
.for_each(|(y, row)| {
|
||||
for (x, value) in row.iter_mut().enumerate() {
|
||||
let p = Vec2::new(
|
||||
x as f32 / width as f32,
|
||||
y as f32 / height as f32,
|
||||
) * params.generation_scale;
|
||||
let p = Vec2::new(x as f32 / width as f32, y as f32 / height as f32)
|
||||
* params.generation_scale;
|
||||
|
||||
let height_freq = 3.0;
|
||||
let height_amp = 0.125;
|
||||
@@ -369,15 +404,43 @@ pub fn generate(params: &GenerationParams) -> GeneratedImages {
|
||||
let bumpmap = apply_shading(&heightmap, width, height, 0.5);
|
||||
let heightmap_image = grayscale_image_from_values(&heightmap_normalized, 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.water_table_height,
|
||||
params.rock_slope_angle_degrees,
|
||||
);
|
||||
let terrain_composite_image = build_soft_light_composite(
|
||||
&terrain_layers.terrain_color_image,
|
||||
&bumpmap_image,
|
||||
&heightmap_image,
|
||||
&terrain_layers.water_mask_image,
|
||||
);
|
||||
|
||||
GeneratedImages {
|
||||
width,
|
||||
height,
|
||||
heightmap_image,
|
||||
bumpmap_image,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
struct TerrainLayers {
|
||||
terrain_color_image: RgbaImage,
|
||||
grass_mask_image: GrayImage,
|
||||
water_mask_image: GrayImage,
|
||||
rock_mask_image: GrayImage,
|
||||
snow_mask_image: GrayImage,
|
||||
}
|
||||
|
||||
fn normalize_values(values: &[f32]) -> Vec<f32> {
|
||||
let mut min_val = f32::INFINITY;
|
||||
let mut max_val = f32::NEG_INFINITY;
|
||||
@@ -407,6 +470,162 @@ fn grayscale_image_from_values(values: &[f32], width: usize, height: usize) -> G
|
||||
image
|
||||
}
|
||||
|
||||
fn build_terrain_layers(
|
||||
heightmap: &[f32],
|
||||
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 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([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);
|
||||
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 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 {
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
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 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);
|
||||
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<f32> {
|
||||
let mut output = vec![0.0; width * height];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user