├── sky.jpg ├── .gitignore ├── doc ├── ui_2d.jpg ├── ui_3d.jpg ├── ex_ue5.jpg ├── ui_gen.jpg ├── ex_hills.jpg ├── ex_island.jpg ├── ui_export.jpg ├── ui_masks.jpg ├── ui_project.jpg └── ex_continent.jpg ├── CREDITS.md ├── src ├── generators │ ├── normalize.rs │ ├── island.rs │ ├── hills.rs │ ├── mod.rs │ ├── fbm.rs │ ├── mudslide.rs │ ├── landmass.rs │ ├── mid_point.rs │ └── water_erosion.rs ├── fps.rs ├── panel_save.rs ├── exporter.rs ├── panel_export.rs ├── panel_2dview.rs ├── worldgen.rs ├── panel_generator.rs ├── main.rs ├── panel_maskedit.rs └── panel_3dview.rs ├── Cargo.toml ├── ex_hills.wgen ├── .vscode └── launch.json ├── ex_island.wgen ├── ex_continent.wgen ├── LICENSE ├── CHANGELOG.md └── README.md /sky.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jice-nospam/wgen/HEAD/sky.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *_x*_y*.png 3 | *_x*_y*.exr 4 | *.wgen 5 | /media -------------------------------------------------------------------------------- /doc/ui_2d.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jice-nospam/wgen/HEAD/doc/ui_2d.jpg -------------------------------------------------------------------------------- /doc/ui_3d.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jice-nospam/wgen/HEAD/doc/ui_3d.jpg -------------------------------------------------------------------------------- /doc/ex_ue5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jice-nospam/wgen/HEAD/doc/ex_ue5.jpg -------------------------------------------------------------------------------- /doc/ui_gen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jice-nospam/wgen/HEAD/doc/ui_gen.jpg -------------------------------------------------------------------------------- /doc/ex_hills.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jice-nospam/wgen/HEAD/doc/ex_hills.jpg -------------------------------------------------------------------------------- /doc/ex_island.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jice-nospam/wgen/HEAD/doc/ex_island.jpg -------------------------------------------------------------------------------- /doc/ui_export.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jice-nospam/wgen/HEAD/doc/ui_export.jpg -------------------------------------------------------------------------------- /doc/ui_masks.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jice-nospam/wgen/HEAD/doc/ui_masks.jpg -------------------------------------------------------------------------------- /doc/ui_project.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jice-nospam/wgen/HEAD/doc/ui_project.jpg -------------------------------------------------------------------------------- /doc/ex_continent.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jice-nospam/wgen/HEAD/doc/ex_continent.jpg -------------------------------------------------------------------------------- /CREDITS.md: -------------------------------------------------------------------------------- 1 | wgen by jice () 2 | 3 | water erosion algorithm adapted from [Implementation of a method for hydraulic erosion](https://www.firespark.de/resources/downloads/implementation%20of%20a%20methode%20for%20hydraulic%20erosion.pdf) by Hans Theobald Beyer. 4 | 5 | # contributors 6 | 7 | * [StefanUlbrich](https://github.com/StefanUlbrich) 8 | -------------------------------------------------------------------------------- /src/generators/normalize.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use super::normalize; 4 | 5 | #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] 6 | pub struct NormalizeConf { 7 | pub min: f32, 8 | pub max: f32, 9 | } 10 | 11 | impl Default for NormalizeConf { 12 | fn default() -> Self { 13 | Self { min: 0.0, max: 1.0 } 14 | } 15 | } 16 | 17 | pub fn gen_normalize(hmap: &mut [f32], conf: &NormalizeConf) { 18 | normalize(hmap, conf.min, conf.max); 19 | } 20 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "worldgen" 3 | version = "0.4.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | eframe = "0.29.1" 8 | egui_extras = { version = "0.29.1", features = ["image"] } 9 | epaint = "0.29.1" 10 | egui_glow = "0.29.1" 11 | # glow = "0.14.2" 12 | three-d = { version = "0.18.2", default-features = false } 13 | image = { version = "0.25.5", default-features = false, features = [ 14 | "png", 15 | "jpeg", 16 | ] } 17 | exr = "1.5.2" 18 | rand = "0.9.0" 19 | noise = { version = "0.9", default-features = false } 20 | serde = { version = "1.0", features = ["derive"] } 21 | ron = "0.8.1" 22 | num_cpus = "1.16.0" 23 | rfd = "0.15.3" 24 | -------------------------------------------------------------------------------- /ex_hills.wgen: -------------------------------------------------------------------------------- 1 | (version:"0.3.1",steps:[(disabled:false,mask:None,typ:Hills((nb_hill:600,base_radius:16.0,radius_var:0.7,height:0.3))),(disabled:false,mask:None,typ:Normalize((min:0.0,max:1.0))),(disabled:false,mask:None,typ:MudSlide((iterations:5.0,max_erosion_alt:0.9,strength:0.33,water_level:0.12))),(disabled:false,mask:None,typ:WaterErosion((drop_amount:0.5,erosion_strength:0.08,evaporation:0.05,capacity:6.0,min_slope:0.05,deposition:0.06,inertia:0.5,radius:4.0)))],cur_step:(disabled:false,mask:None,typ:WaterErosion((drop_amount:0.5,erosion_strength:0.1,evaporation:0.05,capacity:8.0,min_slope:0.05,deposition:0.1,inertia:0.4,radius:4.0))),selected_step:3,move_to_pos:0,hovered:false,seed:3735928559) -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "(Windows) Launch", 9 | "type": "cppvsdbg", 10 | "request": "launch", 11 | "program": "${workspaceRoot}/target/debug/worldgen.exe", 12 | "args": [], 13 | "stopAtEntry": false, 14 | "cwd": "${workspaceRoot}", 15 | "environment": [], 16 | "externalConsole": true 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /src/fps.rs: -------------------------------------------------------------------------------- 1 | use std::time::{Duration, Instant}; 2 | 3 | pub struct FpsCounter { 4 | last: Instant, 5 | fps_counter: usize, 6 | fps: usize, 7 | } 8 | 9 | impl Default for FpsCounter { 10 | fn default() -> Self { 11 | Self { 12 | last: Instant::now(), 13 | fps_counter: 0, 14 | fps: 0, 15 | } 16 | } 17 | } 18 | 19 | impl FpsCounter { 20 | pub fn new_frame(&mut self) { 21 | self.fps_counter += 1; 22 | if self.last.elapsed() >= Duration::from_secs(1) { 23 | self.fps = self.fps_counter; 24 | self.fps_counter = 0; 25 | self.last = Instant::now(); 26 | } 27 | } 28 | pub fn fps(&self) -> usize { 29 | self.fps 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ex_island.wgen: -------------------------------------------------------------------------------- 1 | (version:"0.3.1",steps:[(disabled:false,mask:None,typ:MidPoint((roughness:0.7))),(disabled:false,mask:None,typ:Normalize((min:0.0,max:1.0))),(disabled:false,mask:None,typ:LandMass((land_proportion:0.6,water_level:0.12,plain_factor:2.5,shore_height:0.1))),(disabled:false,mask:None,typ:Island((coast_range:50.0))),(disabled:false,mask:None,typ:Normalize((min:0.0,max:1.0))),(disabled:false,mask:None,typ:WaterErosion((drop_amount:0.5,erosion_strength:0.08,evaporation:0.05,capacity:6.0,min_slope:0.05,deposition:0.06,inertia:0.5,radius:4.0))),(disabled:false,mask:None,typ:MudSlide((iterations:5.0,max_erosion_alt:0.9,strength:0.4,water_level:0.12)))],cur_step:(disabled:false,mask:None,typ:MudSlide((iterations:5.0,max_erosion_alt:0.9,strength:0.4,water_level:0.12))),selected_step:6,move_to_pos:0,hovered:false,seed:3735928559) -------------------------------------------------------------------------------- /ex_continent.wgen: -------------------------------------------------------------------------------- 1 | (version:"0.3.1",steps:[(disabled:false,mask:None,typ:Hills((nb_hill:50,base_radius:16.0,radius_var:0.7,height:0.3))),(disabled:false,mask:None,typ:Normalize((min:0.0,max:1.0))),(disabled:false,mask:None,typ:Fbm((mulx:16.0,muly:16.0,addx:0.0,addy:0.0,octaves:10.0,delta:0.0,scale:0.2))),(disabled:false,mask:None,typ:Normalize((min:0.0,max:1.0))),(disabled:false,mask:None,typ:Island((coast_range:25.0))),(disabled:false,mask:None,typ:LandMass((land_proportion:0.6,water_level:0.12,plain_factor:1.71,shore_height:0.1))),(disabled:false,mask:None,typ:Normalize((min:0.0,max:1.0))),(disabled:false,mask:None,typ:WaterErosion((drop_amount:0.5,erosion_strength:0.08,evaporation:0.05,capacity:6.0,min_slope:0.05,deposition:0.06,inertia:0.4,radius:4.0))),(disabled:false,mask:None,typ:Normalize((min:0.0,max:1.0))),(disabled:false,mask:None,typ:MudSlide((iterations:5.0,max_erosion_alt:1.0,strength:0.4,water_level:0.0)))],cur_step:(disabled:false,mask:None,typ:MudSlide((iterations:5.0,max_erosion_alt:0.9,strength:0.4,water_level:0.12))),selected_step:9,move_to_pos:0,hovered:false,seed:3735928559) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 jice 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/panel_save.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use eframe::egui; 4 | 5 | use crate::panel_export::TEXTEDIT_WIDTH; 6 | pub struct PanelSaveLoad { 7 | /// the name of the file to load or save 8 | pub file_path: String, 9 | /// the program's current directory 10 | cur_dir: PathBuf, 11 | } 12 | 13 | pub enum SaveLoadAction { 14 | Save, 15 | Load, 16 | } 17 | 18 | impl Default for PanelSaveLoad { 19 | fn default() -> Self { 20 | let cur_dir = std::env::current_dir().unwrap(); 21 | let file_path = format!("{}/my_terrain.wgen", cur_dir.display()); 22 | Self { file_path, cur_dir } 23 | } 24 | } 25 | 26 | impl PanelSaveLoad { 27 | pub fn get_file_path(&self) -> &str { 28 | &self.file_path 29 | } 30 | pub fn render(&mut self, ui: &mut egui::Ui) -> Option { 31 | let mut action = None; 32 | ui.heading("Save/load project"); 33 | ui.horizontal(|ui| { 34 | ui.label("File path"); 35 | if ui.button("Pick...").clicked() { 36 | if let Some(path) = rfd::FileDialog::new() 37 | .set_directory(&self.cur_dir) 38 | .pick_file() 39 | { 40 | self.file_path = path.display().to_string(); 41 | self.cur_dir = if path.is_file() { 42 | path.parent().unwrap().to_path_buf() 43 | } else { 44 | path 45 | }; 46 | } 47 | } 48 | }); 49 | ui.add(egui::TextEdit::singleline(&mut self.file_path).desired_width(TEXTEDIT_WIDTH)); 50 | ui.horizontal(|ui| { 51 | if ui.button("Load!").clicked() { 52 | action = Some(SaveLoadAction::Load); 53 | } 54 | if ui.button("Save!").clicked() { 55 | action = Some(SaveLoadAction::Save); 56 | } 57 | }); 58 | action 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/generators/island.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc::Sender; 2 | 3 | use eframe::egui; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::ThreadMessage; 7 | 8 | use super::{get_min_max, report_progress}; 9 | 10 | #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] 11 | pub struct IslandConf { 12 | pub coast_range: f32, 13 | } 14 | 15 | impl Default for IslandConf { 16 | fn default() -> Self { 17 | Self { coast_range: 50.0 } 18 | } 19 | } 20 | 21 | pub fn render_island(ui: &mut egui::Ui, conf: &mut IslandConf) { 22 | ui.horizontal(|ui| { 23 | ui.label("coast range %"); 24 | ui.add( 25 | egui::DragValue::new(&mut conf.coast_range) 26 | .speed(0.1) 27 | .range(0.1..=50.0), 28 | ); 29 | }); 30 | } 31 | 32 | pub fn gen_island( 33 | size: (usize, usize), 34 | hmap: &mut [f32], 35 | conf: &IslandConf, 36 | export: bool, 37 | tx: Sender, 38 | min_progress_step: f32, 39 | ) { 40 | let coast_h_dist = size.0 as f32 * conf.coast_range / 100.0; 41 | let coast_v_dist = size.1 as f32 * conf.coast_range / 100.0; 42 | let (min, _) = get_min_max(hmap); 43 | let mut progress = 0.0; 44 | for x in 0..size.0 { 45 | for y in 0..coast_v_dist as usize { 46 | let h_coef = y as f32 / coast_v_dist as f32; 47 | let h = hmap[x + y * size.0]; 48 | hmap[x + y * size.0] = (h - min) * h_coef + min; 49 | let h = hmap[x + (size.1 - 1 - y) * size.0]; 50 | hmap[x + (size.1 - 1 - y) * size.0] = (h - min) * h_coef + min; 51 | } 52 | let new_progress = 0.5 * x as f32 / size.0 as f32; 53 | if new_progress - progress >= min_progress_step { 54 | progress = new_progress; 55 | report_progress(progress, export, tx.clone()); 56 | } 57 | } 58 | for y in 0..size.1 { 59 | for x in 0..coast_h_dist as usize { 60 | let h_coef = x as f32 / coast_h_dist as f32; 61 | let h = hmap[x + y * size.0]; 62 | hmap[x + y * size.0] = (h - min) * h_coef + min; 63 | let h = hmap[(size.0 - 1 - x) + y * size.0]; 64 | hmap[(size.0 - 1 - x) + y * size.0] = (h - min) * h_coef + min; 65 | } 66 | let new_progress = 0.5 + 0.5 * y as f32 / size.0 as f32; 67 | if new_progress - progress >= min_progress_step { 68 | progress = new_progress; 69 | report_progress(progress, export, tx.clone()); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.4.0] - Unreleased 4 | 5 | ### Changed 6 | 7 | - exports to single channel EXR (slightly smaller files) 8 | - upgraded to egui 0.29, three_d 0.18 9 | 10 | ## [0.3.1] - 2022-10-25 11 | 12 | ### Added 13 | 14 | - seamless flag on exporter for game engines not supporting multi-texture heightmaps 15 | - now you can export to either 16 bits PNG (preferred format for Unreal Engine) or 16 bits float OpenExr format (for Godot) 16 | - added shore height parameter to landmass generator to avoid z fighting issues between the land mesh and a water plane 17 | 18 | ### Fixed 19 | 20 | - changing the height scale in the 3D preview preserves the water level 21 | 22 | ## [0.3.0] - 2022-10-06 23 | 24 | ### Added 25 | 26 | - editable masks to each step. Makes it possible to apply a step only on some part of the map 27 | 28 | ### Changed 29 | 30 | - improved overall performance and UI responsiveness 31 | 32 | ### Fixed 33 | 34 | - Horizontal rotation in the 3D view 35 | 36 | ## [0.2.0] - 2022-08-24 37 | 38 | ### Changed 39 | 40 | - improved water erosion algorithm 41 | - thanks to egui 0.19, UI is now responsive and adapts to any resolution 42 | - fbm generator is now multi-threaded and much faster 43 | - export and load/save panels now use a file dialog instead of a simple textbox 44 | 45 | ### Fixed 46 | 47 | - seed is now set correctly when loading a project 48 | - landmass works even if input is not normalized 49 | - 2d and 3d previews work when loading a project with less steps than current project 50 | - hills doesn't crash anymore when using radius variation == 0.0 51 | - worldgen doesn't crash anymore if there is an error while loading/saving a project or exporting a heightmap 52 | 53 | ## [0.1.0] - 2022-08-05 54 | 55 | ### Added 56 | 57 | - Initial release 58 | - 16 bits grayscale tiled PNG exporter 59 | - save/restore projects to/from [RON](https://github.com/ron-rs/ron) files 60 | - generators : 61 | - Hills : superposition of hemispheric hills 62 | - Fbm : fractal brownian motion 63 | - MidPoint : square-diamond mid-point deplacement 64 | - Normalize : scale the heightmap to range 0.0..1.0 65 | - LandMass : scale the terrain so that a defined proportion is above a defined water level. Also applies a x^3 curve above water level to have a nice plain/mountain ratio 66 | - MudSlide : smoothen the terrain by simulating earth sliding along slopes 67 | - WaterErosion : carves rivers by simulating rain drops dragging earth along slopes 68 | - Island : lower the altitude along the borders of the map 69 | - 2D preview : 70 | - 64x64 to 512x512 grayscale normalized preview (whatever your terrain height, the preview will always range from black to white) 71 | - possibility to preview the map at any point of the generator by selecting a step 72 | - 3D preview : 73 | - 3D mesh preview using the same resolution as the 2D preview (from 64x64 to 512x512) 74 | - skybox (actually a sky cylinder) 75 | - constrained camera (left click : tilt, right click : pan, middle click : zoom) 76 | - water plane with user selectable height 77 | -------------------------------------------------------------------------------- /src/generators/hills.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc::Sender; 2 | 3 | use eframe::egui; 4 | use rand::{prelude::*, rngs::StdRng}; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use crate::ThreadMessage; 8 | 9 | use super::report_progress; 10 | 11 | #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] 12 | pub struct HillsConf { 13 | pub nb_hill: usize, 14 | pub base_radius: f32, 15 | pub radius_var: f32, 16 | pub height: f32, 17 | } 18 | 19 | impl Default for HillsConf { 20 | fn default() -> Self { 21 | Self { 22 | nb_hill: 600, 23 | base_radius: 16.0, 24 | radius_var: 0.7, 25 | height: 0.3, 26 | } 27 | } 28 | } 29 | 30 | pub fn render_hills(ui: &mut egui::Ui, conf: &mut HillsConf) { 31 | ui.horizontal(|ui| { 32 | ui.label("count"); 33 | ui.add( 34 | egui::DragValue::new(&mut conf.nb_hill) 35 | .speed(1.0) 36 | .range(1.0..=5000.0), 37 | ); 38 | ui.label("radius"); 39 | ui.add( 40 | egui::DragValue::new(&mut conf.base_radius) 41 | .speed(1.0) 42 | .range(1.0..=255.0), 43 | ); 44 | }); 45 | ui.horizontal(|ui| { 46 | ui.label("radius variation"); 47 | ui.add( 48 | egui::DragValue::new(&mut conf.radius_var) 49 | .speed(0.01) 50 | .range(0.0..=1.0), 51 | ); 52 | }); 53 | } 54 | 55 | pub fn gen_hills( 56 | seed: u64, 57 | size: (usize, usize), 58 | hmap: &mut [f32], 59 | conf: &HillsConf, 60 | export: bool, 61 | tx: Sender, 62 | min_progress_step: f32, 63 | ) { 64 | let mut rng = StdRng::seed_from_u64(seed); 65 | let real_radius = conf.base_radius * size.0 as f32 / 200.0; 66 | let hill_min_radius = real_radius * (1.0 - conf.radius_var); 67 | let hill_max_radius = real_radius * (1.0 + conf.radius_var); 68 | let mut progress = 0.0; 69 | for i in 0..conf.nb_hill { 70 | let radius: f32 = if conf.radius_var == 0.0 { 71 | hill_min_radius 72 | } else { 73 | rng.random_range(hill_min_radius..hill_max_radius) 74 | }; 75 | let xh: f32 = rng.random_range(0.0..size.0 as f32); 76 | let yh: f32 = rng.random_range(0.0..size.1 as f32); 77 | let radius2 = radius * radius; 78 | let coef = conf.height / radius2; 79 | let minx = (xh - radius).max(0.0) as usize; 80 | let maxx = (xh + radius).min(size.0 as f32) as usize; 81 | let miny = (yh - radius).max(0.0) as usize; 82 | let maxy = (yh + radius).min(size.1 as f32) as usize; 83 | for px in minx..maxx { 84 | let xdist = (px as f32 - xh).powi(2); 85 | for py in miny..maxy { 86 | let z = radius2 - xdist - (py as f32 - yh).powi(2); 87 | if z > 0.0 { 88 | hmap[px + py * size.0] += z * coef; 89 | } 90 | } 91 | } 92 | let new_progress = i as f32 / conf.nb_hill as f32; 93 | if new_progress - progress >= min_progress_step { 94 | progress = new_progress; 95 | report_progress(progress, export, tx.clone()); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/generators/mod.rs: -------------------------------------------------------------------------------- 1 | mod fbm; 2 | mod hills; 3 | mod island; 4 | mod landmass; 5 | mod mid_point; 6 | mod mudslide; 7 | mod normalize; 8 | mod water_erosion; 9 | 10 | use std::sync::mpsc::Sender; 11 | 12 | pub use fbm::{gen_fbm, render_fbm, FbmConf}; 13 | pub use hills::{gen_hills, render_hills, HillsConf}; 14 | pub use island::{gen_island, render_island, IslandConf}; 15 | pub use landmass::{gen_landmass, render_landmass, LandMassConf}; 16 | pub use mid_point::{gen_mid_point, render_mid_point, MidPointConf}; 17 | pub use mudslide::{gen_mudslide, render_mudslide, MudSlideConf}; 18 | pub use normalize::{gen_normalize, NormalizeConf}; 19 | pub use water_erosion::{gen_water_erosion, render_water_erosion, WaterErosionConf}; 20 | 21 | use crate::ThreadMessage; 22 | 23 | const DIRX: [i32; 9] = [0, -1, 0, 1, -1, 1, -1, 0, 1]; 24 | const DIRY: [i32; 9] = [0, -1, -1, -1, 0, 0, 1, 1, 1]; 25 | 26 | pub fn vec_get_safe(v: &Vec, off: usize) -> T 27 | where 28 | T: Default + Copy, 29 | { 30 | if off < v.len() { 31 | return v[off]; 32 | } 33 | T::default() 34 | } 35 | 36 | pub fn get_min_max(v: &[f32]) -> (f32, f32) { 37 | let mut min = v[0]; 38 | let mut max = v[0]; 39 | for val in v.iter().skip(1) { 40 | if *val > max { 41 | max = *val; 42 | } else if *val < min { 43 | min = *val; 44 | } 45 | } 46 | (min, max) 47 | } 48 | 49 | pub fn normalize(v: &mut [f32], target_min: f32, target_max: f32) { 50 | let (min, max) = get_min_max(v); 51 | let invmax = if min == max { 52 | 0.0 53 | } else { 54 | (target_max - target_min) / (max - min) 55 | }; 56 | for val in v { 57 | *val = target_min + (*val - min) * invmax; 58 | } 59 | } 60 | 61 | pub fn _blur(v: &mut [f32], size: (usize, usize)) { 62 | const FACTOR: usize = 8; 63 | let small_size: (usize, usize) = ( 64 | (size.0 + FACTOR - 1) / FACTOR, 65 | (size.1 + FACTOR - 1) / FACTOR, 66 | ); 67 | let mut low_res = vec![0.0; small_size.0 * small_size.1]; 68 | for x in 0..size.0 { 69 | for y in 0..size.1 { 70 | let value = v[x + y * size.0]; 71 | let ix = x / FACTOR; 72 | let iy = y / FACTOR; 73 | low_res[ix + iy * small_size.0] += value; 74 | } 75 | } 76 | let coef = 1.0 / FACTOR as f32; 77 | for x in 0..size.0 { 78 | for y in 0..size.1 { 79 | v[x + y * size.0] = 80 | _interpolate(&low_res, x as f32 * coef, y as f32 * coef, small_size); 81 | } 82 | } 83 | } 84 | 85 | pub fn _interpolate(v: &[f32], x: f32, y: f32, size: (usize, usize)) -> f32 { 86 | let ix = x as usize; 87 | let iy = y as usize; 88 | let dx = x.fract(); 89 | let dy = y.fract(); 90 | 91 | let val_nw = v[ix + iy * size.0]; 92 | let val_ne = if ix < size.0 - 1 { 93 | v[ix + 1 + iy * size.0] 94 | } else { 95 | val_nw 96 | }; 97 | let val_sw = if iy < size.1 - 1 { 98 | v[ix + (iy + 1) * size.0] 99 | } else { 100 | val_nw 101 | }; 102 | let val_se = if ix < size.0 - 1 && iy < size.1 - 1 { 103 | v[ix + 1 + (iy + 1) * size.0] 104 | } else { 105 | val_nw 106 | }; 107 | let val_n = (1.0 - dx) * val_nw + dx * val_ne; 108 | let val_s = (1.0 - dx) * val_sw + dx * val_se; 109 | (1.0 - dy) * val_n + dy * val_s 110 | } 111 | 112 | fn report_progress(progress: f32, export: bool, tx: Sender) { 113 | if export { 114 | tx.send(ThreadMessage::ExporterStepProgress(progress)) 115 | .unwrap(); 116 | } else { 117 | tx.send(ThreadMessage::GeneratorStepProgress(progress)) 118 | .unwrap(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/generators/fbm.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc::Sender; 2 | 3 | use eframe::egui; 4 | use noise::{Fbm, MultiFractal, NoiseFn, Perlin}; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use crate::ThreadMessage; 8 | 9 | use super::report_progress; 10 | 11 | #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] 12 | pub struct FbmConf { 13 | pub mulx: f32, 14 | pub muly: f32, 15 | pub addx: f32, 16 | pub addy: f32, 17 | pub octaves: f32, 18 | pub delta: f32, 19 | pub scale: f32, 20 | } 21 | 22 | impl Default for FbmConf { 23 | fn default() -> Self { 24 | Self { 25 | mulx: 2.20, 26 | muly: 2.20, 27 | addx: 0.0, 28 | addy: 0.0, 29 | octaves: 6.0, 30 | delta: 0.0, 31 | scale: 2.05, 32 | } 33 | } 34 | } 35 | 36 | pub fn render_fbm(ui: &mut egui::Ui, conf: &mut FbmConf) { 37 | ui.horizontal(|ui| { 38 | ui.label("scale x"); 39 | ui.add( 40 | egui::DragValue::new(&mut conf.mulx) 41 | .speed(0.1) 42 | .range(0.0..=100.0), 43 | ); 44 | ui.label("y"); 45 | ui.add( 46 | egui::DragValue::new(&mut conf.muly) 47 | .speed(0.1) 48 | .range(0.0..=100.0), 49 | ); 50 | ui.label("octaves"); 51 | ui.add( 52 | egui::DragValue::new(&mut conf.octaves) 53 | .speed(0.5) 54 | .range(1.0..=Fbm::::MAX_OCTAVES as f32), 55 | ); 56 | }); 57 | ui.horizontal(|ui| { 58 | ui.label("offset x"); 59 | ui.add( 60 | egui::DragValue::new(&mut conf.addx) 61 | .speed(0.1) 62 | .range(0.0..=200.0), 63 | ); 64 | ui.label("y"); 65 | ui.add( 66 | egui::DragValue::new(&mut conf.addy) 67 | .speed(0.1) 68 | .range(0.0..=200.0), 69 | ); 70 | ui.label("scale"); 71 | ui.add( 72 | egui::DragValue::new(&mut conf.scale) 73 | .speed(0.01) 74 | .range(0.01..=10.0), 75 | ); 76 | }); 77 | } 78 | 79 | pub fn gen_fbm( 80 | seed: u64, 81 | size: (usize, usize), 82 | hmap: &mut [f32], 83 | conf: &FbmConf, 84 | export: bool, 85 | tx: Sender, 86 | min_progress_step: f32, 87 | ) { 88 | let xcoef = conf.mulx / 400.0; 89 | let ycoef = conf.muly / 400.0; 90 | let mut progress = 0.0; 91 | let num_threads = num_cpus::get(); 92 | std::thread::scope(|s| { 93 | let size_per_job = size.1 / num_threads; 94 | for (i, chunk) in hmap.chunks_mut(size_per_job * size.0).enumerate() { 95 | // FIXME: Why was this here 96 | // let i = i; 97 | let fbm = Fbm::::new(seed as u32).set_octaves(conf.octaves as usize); 98 | let tx = tx.clone(); 99 | s.spawn(move || { 100 | let yoffset = i * size_per_job; 101 | let lasty = size_per_job.min(size.1 - yoffset); 102 | for y in 0..lasty { 103 | let f1 = ((y + yoffset) as f32 * 512.0 / size.1 as f32 + conf.addy) * ycoef; 104 | let mut offset = y * size.0; 105 | for x in 0..size.0 { 106 | let f0 = (x as f32 * 512.0 / size.0 as f32 + conf.addx) * xcoef; 107 | let value = 108 | conf.delta + fbm.get([f0 as f64, f1 as f64]) as f32 * conf.scale; 109 | chunk[offset] += value; 110 | offset += 1; 111 | } 112 | if i == 0 { 113 | let new_progress = (y + 1) as f32 / size_per_job as f32; 114 | if new_progress - progress >= min_progress_step { 115 | progress = new_progress; 116 | report_progress(progress, export, tx.clone()) 117 | } 118 | } 119 | } 120 | }); 121 | } 122 | }); 123 | } 124 | -------------------------------------------------------------------------------- /src/generators/mudslide.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc::Sender; 2 | 3 | use eframe::egui; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::ThreadMessage; 7 | 8 | use super::{report_progress, vec_get_safe, DIRX, DIRY}; 9 | 10 | #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] 11 | pub struct MudSlideConf { 12 | iterations: f32, 13 | max_erosion_alt: f32, 14 | strength: f32, 15 | water_level: f32, 16 | } 17 | 18 | impl Default for MudSlideConf { 19 | fn default() -> Self { 20 | Self { 21 | iterations: 5.0, 22 | max_erosion_alt: 0.9, 23 | strength: 0.4, 24 | water_level: 0.12, 25 | } 26 | } 27 | } 28 | 29 | pub fn render_mudslide(ui: &mut egui::Ui, conf: &mut MudSlideConf) { 30 | ui.horizontal(|ui| { 31 | ui.label("iterations"); 32 | ui.add( 33 | egui::DragValue::new(&mut conf.iterations) 34 | .speed(0.5) 35 | .range(1.0..=10.0), 36 | ); 37 | ui.label("max altitude"); 38 | ui.add( 39 | egui::DragValue::new(&mut conf.max_erosion_alt) 40 | .speed(0.01) 41 | .range(0.0..=1.0), 42 | ); 43 | }); 44 | ui.horizontal(|ui| { 45 | ui.label("strength"); 46 | ui.add( 47 | egui::DragValue::new(&mut conf.strength) 48 | .speed(0.01) 49 | .range(0.0..=1.0), 50 | ); 51 | ui.label("water level"); 52 | ui.add( 53 | egui::DragValue::new(&mut conf.water_level) 54 | .speed(0.01) 55 | .range(0.0..=1.0), 56 | ); 57 | }); 58 | } 59 | 60 | pub fn gen_mudslide( 61 | size: (usize, usize), 62 | hmap: &mut Vec, 63 | conf: &MudSlideConf, 64 | export: bool, 65 | tx: Sender, 66 | min_progress_step: f32, 67 | ) { 68 | for i in 0..conf.iterations as usize { 69 | mudslide(size, hmap, i, conf, export, tx.clone(), min_progress_step); 70 | } 71 | } 72 | 73 | fn mudslide( 74 | size: (usize, usize), 75 | hmap: &mut Vec, 76 | iteration: usize, 77 | conf: &MudSlideConf, 78 | export: bool, 79 | tx: Sender, 80 | min_progress_step: f32, 81 | ) { 82 | let sand_coef = 1.0 / (1.0 - conf.water_level); 83 | let mut new_hmap = vec![0.0; size.0 * size.1]; 84 | let mut progress = 0.0; 85 | for y in 0..size.1 { 86 | let yoff = y * size.0; 87 | for x in 0..size.0 { 88 | let h = vec_get_safe(hmap, x + yoff); 89 | if h < conf.water_level - 0.01 || h >= conf.max_erosion_alt { 90 | new_hmap[x + y * size.0] = h; 91 | continue; 92 | } 93 | let mut sum_delta1 = 0.0; 94 | let mut sum_delta2 = 0.0; 95 | let mut nb1 = 1.0; 96 | let mut nb2 = 1.0; 97 | for i in 1..9 { 98 | let ix = (x as i32 + DIRX[i]) as usize; 99 | let iy = (y as i32 + DIRY[i]) as usize; 100 | if ix < size.0 && iy < size.1 { 101 | let ih = vec_get_safe(hmap, ix + iy * size.0); 102 | if ih < h { 103 | if i == 1 || i == 3 || i == 6 || i == 8 { 104 | // diagonal neighbour 105 | sum_delta1 += (ih - h) * 0.4; 106 | nb1 += 1.0; 107 | } else { 108 | // adjacent neighbour 109 | sum_delta2 += (ih - h) * 1.6; 110 | nb2 += 1.0; 111 | } 112 | } 113 | } 114 | } 115 | // average height difference with lower neighbours 116 | let mut dh = sum_delta1 / nb1 + sum_delta2 / nb2; 117 | dh *= conf.strength; 118 | let hcoef = (h - conf.water_level) * sand_coef; 119 | dh *= 1.0 - hcoef * hcoef * hcoef; // less smoothing at high altitudes 120 | new_hmap[x + y * size.0] = h + dh; 121 | } 122 | let new_progress = iteration as f32 / conf.iterations as f32 123 | + (y as f32 / size.1 as f32) / conf.iterations as f32; 124 | if new_progress - progress >= min_progress_step { 125 | progress = new_progress; 126 | report_progress(progress, export, tx.clone()); 127 | } 128 | } 129 | *hmap = new_hmap; 130 | } 131 | -------------------------------------------------------------------------------- /src/exporter.rs: -------------------------------------------------------------------------------- 1 | use std::{path::Path, sync::mpsc::Sender}; 2 | 3 | use crate::{ 4 | panel_export::{ExportFileType, PanelExport}, 5 | worldgen::{Step, WorldGenerator}, 6 | ThreadMessage, 7 | }; 8 | 9 | pub fn export_heightmap( 10 | // random number generator's seed to use 11 | seed: u64, 12 | // list of generator steps with their configuration and optional masks 13 | steps: &[Step], 14 | // size and number of files to export, file name pattern 15 | export_data: &PanelExport, 16 | // channel to send feedback messages to the main thread 17 | tx: Sender, 18 | // minimum amount of progress to report (below this value, the global %age won't change) 19 | min_progress_step: f32, 20 | ) -> Result<(), String> { 21 | let file_width = export_data.export_width as usize; 22 | let file_height = export_data.export_height as usize; 23 | let mut wgen = WorldGenerator::new( 24 | seed, 25 | ( 26 | (export_data.export_width * export_data.tiles_h) as usize, 27 | (export_data.export_height * export_data.tiles_v) as usize, 28 | ), 29 | ); 30 | wgen.generate(steps, tx, min_progress_step); 31 | 32 | let (min, max) = wgen.get_min_max(); 33 | let coef = if max - min > std::f32::EPSILON { 34 | 1.0 / (max - min) 35 | } else { 36 | 1.0 37 | }; 38 | 39 | for ty in 0..export_data.tiles_v as usize { 40 | for tx in 0..export_data.tiles_h as usize { 41 | let offset_x = if export_data.seamless { 42 | tx * (file_width - 1) 43 | } else { 44 | tx * file_width 45 | }; 46 | let offset_y = if export_data.seamless { 47 | ty * (file_height - 1) 48 | } else { 49 | ty * file_height 50 | }; 51 | let path = format!( 52 | "{}_x{}_y{}.{}", 53 | export_data.file_path, 54 | tx, 55 | ty, 56 | export_data.file_type.to_string() 57 | ); 58 | match export_data.file_type { 59 | ExportFileType::PNG => write_png( 60 | file_width, 61 | file_height, 62 | offset_x, 63 | offset_y, 64 | &wgen, 65 | min, 66 | coef, 67 | &path, 68 | )?, 69 | ExportFileType::EXR => write_exr( 70 | file_width, 71 | file_height, 72 | offset_x, 73 | offset_y, 74 | &wgen, 75 | min, 76 | coef, 77 | &path, 78 | )?, 79 | } 80 | } 81 | } 82 | Ok(()) 83 | } 84 | 85 | fn write_png( 86 | file_width: usize, 87 | file_height: usize, 88 | offset_x: usize, 89 | offset_y: usize, 90 | wgen: &WorldGenerator, 91 | min: f32, 92 | coef: f32, 93 | path: &str, 94 | ) -> Result<(), String> { 95 | let mut buf = vec![0u8; file_width * file_height * 2]; 96 | for py in 0..file_height { 97 | for px in 0..file_width { 98 | let mut h = wgen.combined_height(px + offset_x, py + offset_y); 99 | h = (h - min) * coef; 100 | let offset = (px + py * file_width) * 2; 101 | let pixel = (h * 65535.0) as u16; 102 | let upixel = pixel.to_ne_bytes(); 103 | buf[offset] = upixel[0]; 104 | buf[offset + 1] = upixel[1]; 105 | } 106 | } 107 | image::save_buffer( 108 | &Path::new(&path), 109 | &buf, 110 | file_width as u32, 111 | file_height as u32, 112 | image::ColorType::L16, 113 | ) 114 | .map_err(|e| format!("Error while saving {}: {}", &path, e)) 115 | } 116 | 117 | fn write_exr( 118 | file_width: usize, 119 | file_height: usize, 120 | offset_x: usize, 121 | offset_y: usize, 122 | wgen: &WorldGenerator, 123 | min: f32, 124 | coef: f32, 125 | path: &str, 126 | ) -> Result<(), String> { 127 | use exr::prelude::*; 128 | 129 | let channel = SpecificChannels::new( 130 | (ChannelDescription::named("Y", SampleType::F16),), 131 | |Vec2(px, py)| { 132 | let h = wgen.combined_height(px + offset_x, py + offset_y); 133 | let h = f16::from_f32((h - min) * coef); 134 | (h,) 135 | }, 136 | ); 137 | 138 | Image::from_encoded_channels( 139 | (file_width, file_height), 140 | Encoding { 141 | compression: Compression::ZIP1, 142 | blocks: Blocks::ScanLines, 143 | line_order: LineOrder::Increasing, 144 | }, 145 | channel, 146 | ) 147 | .write() 148 | .to_file(path) 149 | .map_err(|e| format!("Error while saving {}: {}", &path, e)) 150 | } 151 | -------------------------------------------------------------------------------- /src/generators/landmass.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc::Sender; 2 | 3 | use eframe::egui; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::ThreadMessage; 7 | 8 | use super::{normalize, report_progress}; 9 | 10 | #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] 11 | pub struct LandMassConf { 12 | /// what proportion of the map should be above water 0.0-1.0 13 | pub land_proportion: f32, 14 | /// height of the water plane 15 | pub water_level: f32, 16 | /// apply h^plain_factor above sea level for sharper mountains and flatter plains 17 | pub plain_factor: f32, 18 | /// lower everything under water level by this value to avoid z fighting between land and water plane near shores 19 | pub shore_height: f32, 20 | } 21 | 22 | impl Default for LandMassConf { 23 | fn default() -> Self { 24 | Self { 25 | land_proportion: 0.6, 26 | water_level: 0.12, 27 | plain_factor: 2.5, 28 | shore_height: 0.05, 29 | } 30 | } 31 | } 32 | 33 | pub fn render_landmass(ui: &mut egui::Ui, conf: &mut LandMassConf) { 34 | ui.horizontal(|ui| { 35 | ui.label("land proportion") 36 | .on_hover_text("what proportion of the map should be above water"); 37 | ui.add( 38 | egui::DragValue::new(&mut conf.land_proportion) 39 | .speed(0.01) 40 | .range(0.0..=1.0), 41 | ); 42 | ui.label("water level") 43 | .on_hover_text("height of the water plane"); 44 | ui.add( 45 | egui::DragValue::new(&mut conf.water_level) 46 | .speed(0.01) 47 | .range(0.0..=1.0), 48 | ); 49 | }); 50 | ui.horizontal(|ui| { 51 | ui.label("plain factor") 52 | .on_hover_text("increase for sharper mountains and flatter plains"); 53 | ui.add( 54 | egui::DragValue::new(&mut conf.plain_factor) 55 | .speed(0.01) 56 | .range(1.0..=4.0), 57 | ); 58 | ui.label("shore height") 59 | .on_hover_text("lower underwater land by this value"); 60 | ui.add( 61 | egui::DragValue::new(&mut conf.shore_height) 62 | .speed(0.01) 63 | .range(0.0..=0.1), 64 | ); 65 | }); 66 | } 67 | 68 | pub fn gen_landmass( 69 | size: (usize, usize), 70 | hmap: &mut [f32], 71 | conf: &LandMassConf, 72 | export: bool, 73 | tx: Sender, 74 | min_progress_step: f32, 75 | ) { 76 | let mut height_count: [f32; 256] = [0.0; 256]; 77 | let mut progress = 0.0; 78 | normalize(hmap, 0.0, 1.0); 79 | for y in 0..size.1 { 80 | let yoff = y * size.0; 81 | for x in 0..size.0 { 82 | let h = hmap[x + yoff]; 83 | let ih = (h * 255.0) as usize; 84 | height_count[ih] += 1.0; 85 | } 86 | let new_progress = 0.33 * y as f32 / size.1 as f32; 87 | if new_progress - progress >= min_progress_step { 88 | progress = new_progress; 89 | report_progress(progress, export, tx.clone()); 90 | } 91 | } 92 | let mut water_level = 0; 93 | let mut water_cells = 0.0; 94 | let target_water_cells = (size.0 * size.1) as f32 * (1.0 - conf.land_proportion); 95 | while water_level < 256 && water_cells < target_water_cells { 96 | water_cells += height_count[water_level]; 97 | water_level += 1; 98 | } 99 | let new_water_level = water_level as f32 / 255.0; 100 | let land_coef = (1.0 - conf.water_level) / (1.0 - new_water_level); 101 | let water_coef = conf.water_level / new_water_level; 102 | // water level should be raised/lowered to newWaterLevel 103 | for y in 0..size.1 { 104 | let yoff = y * size.0; 105 | for x in 0..size.0 { 106 | let mut h = hmap[x + yoff]; 107 | if h > new_water_level { 108 | h = conf.water_level + (h - new_water_level) * land_coef; 109 | } else { 110 | h = h * water_coef - conf.shore_height; 111 | } 112 | hmap[x + yoff] = h; 113 | } 114 | let new_progress = 0.33 + 0.33 * y as f32 / size.1 as f32; 115 | if new_progress - progress >= min_progress_step { 116 | progress = new_progress; 117 | report_progress(progress, export, tx.clone()); 118 | } 119 | } 120 | // fix land/mountain ratio using h^plain_factor curve above sea level 121 | for y in 0..size.1 { 122 | let yoff = y * size.0; 123 | for x in 0..size.0 { 124 | let mut h = hmap[x + yoff]; 125 | if h >= conf.water_level { 126 | let coef = (h - conf.water_level) / (1.0 - conf.water_level); 127 | let coef = coef.powf(conf.plain_factor); 128 | h = conf.water_level + coef * (1.0 - conf.water_level); 129 | hmap[x + y * size.0] = h; 130 | } 131 | } 132 | let new_progress = 0.66 + 0.33 * y as f32 / size.1 as f32; 133 | if new_progress - progress >= min_progress_step { 134 | progress = new_progress; 135 | report_progress(progress, export, tx.clone()); 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/panel_export.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use eframe::egui; 4 | 5 | pub const TEXTEDIT_WIDTH: f32 = 240.0; 6 | 7 | #[derive(Clone)] 8 | pub enum ExportFileType { 9 | PNG, 10 | EXR, 11 | } 12 | 13 | impl std::fmt::Display for ExportFileType { 14 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 15 | write!( 16 | f, 17 | "{}", 18 | match self { 19 | Self::PNG => "png", 20 | Self::EXR => "exr", 21 | } 22 | ) 23 | } 24 | } 25 | 26 | #[derive(Clone)] 27 | pub struct PanelExport { 28 | /// width of each image in pixels 29 | pub export_width: f32, 30 | /// height of each image in pixels 31 | pub export_height: f32, 32 | /// number of horizontal tiles 33 | pub tiles_h: f32, 34 | /// number of vertical tiles 35 | pub tiles_v: f32, 36 | /// image filename prefix 37 | pub file_path: String, 38 | /// should we repeat the same pixel row on two adjacent tiles ? 39 | /// not needed for unreal engine which handles multi-textures heightmaps 40 | /// might be needed for other engines (for example godot heightmap terrain plugin) 41 | pub seamless: bool, 42 | /// format to export, either png or exr 43 | pub file_type: ExportFileType, 44 | /// to disable the exporter ui during export 45 | pub enabled: bool, 46 | /// program's current directory 47 | cur_dir: PathBuf, 48 | } 49 | 50 | impl Default for PanelExport { 51 | fn default() -> Self { 52 | let cur_dir = std::env::current_dir().unwrap(); 53 | let file_path = format!("{}/wgen", cur_dir.display()); 54 | Self { 55 | export_width: 1024.0, 56 | export_height: 1024.0, 57 | tiles_h: 1.0, 58 | tiles_v: 1.0, 59 | file_path, 60 | seamless: false, 61 | file_type: ExportFileType::PNG, 62 | enabled: true, 63 | cur_dir, 64 | } 65 | } 66 | } 67 | 68 | impl PanelExport { 69 | pub fn render(&mut self, ui: &mut egui::Ui, progress: f32, progress_text: &str) -> bool { 70 | let mut export = false; 71 | ui.horizontal(|ui| { 72 | ui.heading("Export heightmaps"); 73 | if !self.enabled { 74 | ui.spinner(); 75 | } 76 | }); 77 | ui.add(egui::ProgressBar::new(progress).text(progress_text)); 78 | ui.add_enabled_ui(self.enabled, |ui| { 79 | ui.horizontal(|ui| { 80 | ui.label("Tile size"); 81 | ui.add(egui::DragValue::new(&mut self.export_width).speed(1.0)); 82 | ui.label(" x "); 83 | ui.add(egui::DragValue::new(&mut self.export_height).speed(1.0)); 84 | }); 85 | ui.horizontal(|ui| { 86 | ui.label("Tiles"); 87 | ui.add(egui::DragValue::new(&mut self.tiles_h).speed(1.0)); 88 | ui.label(" x "); 89 | ui.add(egui::DragValue::new(&mut self.tiles_v).speed(1.0)); 90 | }); 91 | ui.horizontal(|ui| { 92 | ui.label("Export file path"); 93 | if ui.button("Pick...").clicked() { 94 | if let Some(path) = rfd::FileDialog::new() 95 | .set_directory(&self.cur_dir) 96 | .pick_file() 97 | { 98 | self.file_path = path.display().to_string(); 99 | if self.file_path.ends_with(".png") { 100 | self.file_path = 101 | self.file_path.strip_suffix(".png").unwrap().to_owned(); 102 | } else if self.file_path.ends_with(".exr") { 103 | self.file_path = 104 | self.file_path.strip_suffix(".exr").unwrap().to_owned(); 105 | } 106 | self.cur_dir = if path.is_file() { 107 | path.parent().unwrap().to_path_buf() 108 | } else { 109 | path 110 | }; 111 | } 112 | } 113 | }); 114 | ui.horizontal(|ui| { 115 | ui.add( 116 | egui::TextEdit::singleline(&mut self.file_path) 117 | .desired_width(TEXTEDIT_WIDTH - 80.0), 118 | ); 119 | ui.label("_x*_y*."); 120 | if ui 121 | .button(&self.file_type.to_string()) 122 | .on_hover_text("change the exported file format") 123 | .clicked() 124 | { 125 | match self.file_type { 126 | ExportFileType::PNG => self.file_type = ExportFileType::EXR, 127 | ExportFileType::EXR => self.file_type = ExportFileType::PNG, 128 | } 129 | } 130 | }); 131 | ui.horizontal(|ui| { 132 | ui.checkbox(&mut self.seamless, "seamless") 133 | .on_hover_text("whether pixel values are repeated on two adjacent tiles"); 134 | export = ui.button("Export!").clicked(); 135 | }); 136 | }); 137 | export 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/generators/mid_point.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc::Sender; 2 | 3 | use eframe::egui; 4 | use rand::{rngs::StdRng, Rng, SeedableRng}; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use crate::ThreadMessage; 8 | 9 | use super::report_progress; 10 | 11 | #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] 12 | pub struct MidPointConf { 13 | pub roughness: f32, 14 | } 15 | 16 | impl Default for MidPointConf { 17 | fn default() -> Self { 18 | Self { roughness: 0.7 } 19 | } 20 | } 21 | 22 | pub struct ProgressTracking { 23 | count: usize, 24 | progress: f32, 25 | min_progress_step: f32, 26 | export: bool, 27 | } 28 | 29 | pub fn render_mid_point(ui: &mut egui::Ui, conf: &mut MidPointConf) { 30 | ui.horizontal(|ui| { 31 | ui.label("roughness"); 32 | ui.add( 33 | egui::DragValue::new(&mut conf.roughness) 34 | .speed(0.01) 35 | .range(0.01..=1.0), 36 | ); 37 | }); 38 | } 39 | 40 | pub fn gen_mid_point( 41 | seed: u64, 42 | size: (usize, usize), 43 | hmap: &mut Vec, 44 | conf: &MidPointConf, 45 | export: bool, 46 | tx: Sender, 47 | min_progress_step: f32, 48 | ) { 49 | let mut rng = StdRng::seed_from_u64(seed); 50 | hmap[0] = rng.random_range(0.0..1.0); 51 | hmap[size.0 - 1] = rng.random_range(0.0..1.0); 52 | hmap[size.0 * (size.1 - 1)] = rng.random_range(0.0..1.0); 53 | hmap[size.0 * size.1 - 1] = rng.random_range(0.0..1.0); 54 | let mut track = ProgressTracking { 55 | count: size.0 * size.1 * 2, 56 | progress: 0.0, 57 | min_progress_step, 58 | export, 59 | }; 60 | diamond_square( 61 | hmap, 62 | &mut rng, 63 | size, 64 | size.0 / 2, 65 | conf.roughness, 66 | &mut track, 67 | tx, 68 | ); 69 | } 70 | 71 | fn check_progress(track: &mut ProgressTracking, size: (usize, usize), tx: Sender) { 72 | let new_progress = 1.0 - track.count as f32 / (size.0 * size.1 * 2) as f32; 73 | if new_progress - track.progress >= track.min_progress_step { 74 | track.progress = new_progress; 75 | report_progress(track.progress, track.export, tx); 76 | } 77 | } 78 | 79 | pub fn diamond_square( 80 | hmap: &mut Vec, 81 | rng: &mut StdRng, 82 | size: (usize, usize), 83 | cur_size: usize, 84 | roughness: f32, 85 | track: &mut ProgressTracking, 86 | tx: Sender, 87 | ) { 88 | let half = cur_size / 2; 89 | if half < 1 { 90 | return; 91 | } 92 | for y in (half..size.1).step_by(cur_size) { 93 | for x in (half..size.0).step_by(cur_size) { 94 | square_step(hmap, rng, x, y, size, half, roughness); 95 | track.count -= 1; 96 | check_progress(track, size, tx.clone()); 97 | } 98 | } 99 | let mut col = 0; 100 | for x in (0..size.0).step_by(half) { 101 | col += 1; 102 | if col % 2 == 1 { 103 | for y in (half..size.1).step_by(cur_size) { 104 | diamond_step(hmap, rng, x, y, size, half, roughness); 105 | track.count -= 1; 106 | check_progress(track, size, tx.clone()); 107 | } 108 | } else { 109 | for y in (0..size.1).step_by(cur_size) { 110 | diamond_step(hmap, rng, x, y, size, half, roughness); 111 | track.count -= 1; 112 | check_progress(track, size, tx.clone()); 113 | } 114 | } 115 | } 116 | diamond_square(hmap, rng, size, cur_size / 2, roughness * 0.5, track, tx); 117 | } 118 | 119 | fn square_step( 120 | hmap: &mut [f32], 121 | rng: &mut StdRng, 122 | x: usize, 123 | y: usize, 124 | size: (usize, usize), 125 | reach: usize, 126 | roughness: f32, 127 | ) { 128 | let mut count = 0; 129 | let mut avg = 0.0; 130 | if x >= reach && y >= reach { 131 | avg += hmap[x - reach + (y - reach) * size.0]; 132 | count += 1; 133 | } 134 | if x >= reach && y + reach < size.1 { 135 | avg += hmap[x - reach + (y + reach) * size.0]; 136 | count += 1; 137 | } 138 | if x + reach < size.0 && y >= reach { 139 | avg += hmap[x + reach + (y - reach) * size.0]; 140 | count += 1; 141 | } 142 | if x + reach < size.0 && y + reach < size.1 { 143 | avg += hmap[x + reach + (y + reach) * size.0]; 144 | count += 1; 145 | } 146 | avg /= count as f32; 147 | avg += rng.random_range(-roughness..roughness); 148 | hmap[x + y * size.0] = avg; 149 | } 150 | 151 | fn diamond_step( 152 | hmap: &mut [f32], 153 | rng: &mut StdRng, 154 | x: usize, 155 | y: usize, 156 | size: (usize, usize), 157 | reach: usize, 158 | roughness: f32, 159 | ) { 160 | let mut count = 0; 161 | let mut avg = 0.0; 162 | if x >= reach { 163 | avg += hmap[x - reach + y * size.0]; 164 | count += 1; 165 | } 166 | if x + reach < size.0 { 167 | avg += hmap[x + reach + y * size.0]; 168 | count += 1; 169 | } 170 | if y >= reach { 171 | avg += hmap[x + (y - reach) * size.0]; 172 | count += 1; 173 | } 174 | if y + reach < size.1 { 175 | avg += hmap[x + (y + reach) * size.0]; 176 | count += 1; 177 | } 178 | avg /= count as f32; 179 | avg += rng.random_range(-roughness..roughness); 180 | hmap[x + y * size.0] = avg; 181 | } 182 | -------------------------------------------------------------------------------- /src/panel_2dview.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui; 2 | use egui_extras::RetainedImage; 3 | use epaint::{Color32, ColorImage}; 4 | 5 | use crate::{fps::FpsCounter, panel_maskedit::PanelMaskEdit, worldgen::ExportMap}; 6 | 7 | pub enum Panel2dAction { 8 | /// inform the main program that the preview size has changed. terrain/3d view must be recomputed 9 | ResizePreview(usize), 10 | /// inform the main program that mask must be copied to the generator panel 11 | MaskUpdated, 12 | /// inform the main program that mask must be deleted in the generator panel 13 | MaskDelete, 14 | } 15 | pub struct Panel2dView { 16 | /// preview image of the heightmap 17 | img: ColorImage, 18 | /// minimum value in the heightmap 19 | min: f32, 20 | /// maximum value in the heightmap 21 | max: f32, 22 | /// are we displaying the mask editor ? 23 | mask_mode: bool, 24 | /// size of the preview canvas in pixels 25 | image_size: usize, 26 | /// size of the heightmap 27 | preview_size: usize, 28 | /// should we update the preview every time a step is computed ? 29 | pub live_preview: bool, 30 | /// utility to display FPS 31 | fps_counter: FpsCounter, 32 | /// egui renderable image 33 | ui_img: Option, 34 | /// mask editor subpanel 35 | mask_editor: PanelMaskEdit, 36 | } 37 | 38 | impl Panel2dView { 39 | pub fn new(image_size: usize, preview_size: u32, hmap: &ExportMap) -> Self { 40 | let mut panel = Panel2dView { 41 | img: ColorImage::new([image_size, image_size], Color32::BLACK), 42 | min: 0.0, 43 | max: 0.0, 44 | image_size, 45 | mask_mode: false, 46 | live_preview: true, 47 | preview_size: preview_size as usize, 48 | fps_counter: FpsCounter::default(), 49 | ui_img: None, 50 | mask_editor: PanelMaskEdit::new(image_size), 51 | }; 52 | panel.refresh(image_size, preview_size, Some(hmap)); 53 | panel 54 | } 55 | pub fn get_current_mask(&self) -> Option> { 56 | self.mask_editor.get_mask() 57 | } 58 | pub fn display_mask(&mut self, image_size: usize, preview_size: u32, mask: Option>) { 59 | self.image_size = image_size; 60 | self.preview_size = preview_size as usize; 61 | self.mask_editor.display_mask(image_size, mask); 62 | self.mask_mode = true; 63 | } 64 | pub fn refresh(&mut self, image_size: usize, preview_size: u32, hmap: Option<&ExportMap>) { 65 | self.image_size = image_size; 66 | self.mask_mode = false; 67 | self.preview_size = preview_size as usize; 68 | if self.img.width() != image_size { 69 | self.img = ColorImage::new([self.image_size, self.image_size], Color32::BLACK); 70 | } 71 | if let Some(hmap) = hmap { 72 | let (min, max) = hmap.get_min_max(); 73 | let coef = if max - min > std::f32::EPSILON { 74 | 1.0 / (max - min) 75 | } else { 76 | 1.0 77 | }; 78 | self.min = min; 79 | self.max = max; 80 | let mut idx = 0; 81 | for y in 0..image_size { 82 | let py = ((y * preview_size as usize) as f32 / image_size as f32) as usize; 83 | for x in 0..image_size { 84 | let px = ((x * preview_size as usize) as f32 / image_size as f32) as usize; 85 | let mut h = hmap.height(px as usize, py as usize); 86 | h = (h - min) * coef; 87 | self.img.pixels[idx] = Color32::from_gray((h * 255.0).clamp(0.0, 255.0) as u8); 88 | idx += 1; 89 | } 90 | } 91 | }; 92 | self.ui_img = Some(RetainedImage::from_color_image("hmap", self.img.clone())); 93 | } 94 | pub fn render(&mut self, ui: &mut egui::Ui) -> Option { 95 | let mut action = None; 96 | let old_size = self.preview_size; 97 | self.fps_counter.new_frame(); 98 | if self.mask_mode { 99 | action = self.mask_editor.render(ui, &self.img); 100 | } else { 101 | ui.vertical(|ui| { 102 | if let Some(ref img) = self.ui_img { 103 | img.show(ui); 104 | } 105 | ui.horizontal(|ui| { 106 | ui.label(format!("Height range : {} - {}", self.min, self.max)); 107 | }); 108 | }); 109 | } 110 | ui.label(format!("FPS : {}", self.fps_counter.fps())); 111 | ui.horizontal(|ui| { 112 | ui.label("Preview size"); 113 | egui::ComboBox::from_label("") 114 | .selected_text(format!("{}x{}", self.preview_size, self.preview_size)) 115 | .show_ui(ui, |ui| { 116 | ui.selectable_value(&mut self.preview_size, 64, "64x64"); 117 | ui.selectable_value(&mut self.preview_size, 128, "128x128"); 118 | ui.selectable_value(&mut self.preview_size, 256, "256x256"); 119 | ui.selectable_value(&mut self.preview_size, 512, "512x512"); 120 | }); 121 | ui.label("Live preview"); 122 | ui.checkbox(&mut self.live_preview, ""); 123 | }); 124 | if self.preview_size != old_size { 125 | action = Some(Panel2dAction::ResizePreview(self.preview_size)); 126 | } 127 | action 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WGEN - a simple heightmap generator 2 | 3 | There are a lot of great terrain generators out there but most of them have a free version with a terrain size limitation. 4 | 5 | This is a much simpler generator, but it can export maps as big as you want. 6 | 7 | Continent example, using a rough hill pattern and a high frequency FBM : 8 | ![continent example](https://raw.githubusercontent.com/jice-nospam/wgen/main/doc/ex_continent.jpg) 9 | 10 | Island example, using mid-point deplacement algorithm : 11 | ![island example](https://raw.githubusercontent.com/jice-nospam/wgen/main/doc/ex_island.jpg) 12 | 13 | Smoother landscape using only hills generator : 14 | ![hills example](https://raw.githubusercontent.com/jice-nospam/wgen/main/doc/ex_hills.jpg) 15 | 16 | Exemple of (untextured) 4K x 4K landscape imported in Unreal Engine 5 : 17 | ![UE5 example](https://raw.githubusercontent.com/jice-nospam/wgen/main/doc/ex_ue5.jpg) 18 | 19 | If you like this project and want to support its development, feel free to donate at [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://paypal.me/guldendraak) 20 | 21 | # Manual 22 | ## Generators 23 | This is where you control the world generation. You can stack several "generators" that applies some modification to the heightmap. 24 | Select the generator with the dropdown button, then press `New step` button to add it to the stack. 25 | You can click on a step label in the stack to select it and display its parameters. Click the `Refresh` button once you changed the parameters values to recompute the heightmap from this step. 26 | 27 | ![Generators UI](https://raw.githubusercontent.com/jice-nospam/wgen/main/doc/ui_gen.jpg) 28 | 29 | The current version features those generators : 30 | - Hills : superposition of hemispheric hills to generate a smooth terrain 31 | - Fbm : fractal brownian motion can be used to add noise to an existing terrain or as first step to generate a continent-like terrain. 32 | - MidPoint : square-diamond mid-point deplacement generates a realistic looking heightmap 33 | - Normalize : scales the heightmap back to the range 0.0..1.0. Some generators work better with a normalized heightmap. Check your heightmap values range in the 2D preview. 34 | - LandMass : scale the terrain so that a defined proportion is above a defined water level. Also applies a x^3 curve above water level to have a nice plain/mountain ratio and can lower underwater terrain to have a crisp coast line 35 | - MudSlide : smoothen the terrain by simulating earth sliding along slopes 36 | - WaterErosion : carves rivers by simulating rain drops dragging earth along slopes 37 | - Island : lower the altitude along the borders of the map 38 | 39 | ## Masks 40 | You can add a mask to a generator step by clicking the square next to the generator name. 41 | You can then edit the mask using a painting brush. The generator effect will be scaled depending on the mask color. 42 | ![Masks UI](https://raw.githubusercontent.com/jice-nospam/wgen/main/doc/ui_masks.jpg) 43 | 44 | ## Terrain preview 45 | You have a 2D preview displaying the heightmap (at current selected step in the generators UI). You can change the preview heightmap size from 64x64 for very fast computation to 512x512 for a more precise visualization. If `live preview` button is checked, the 2D preview will be updated at every step during computation. 46 | 47 | ![3D preview UI](https://raw.githubusercontent.com/jice-nospam/wgen/main/doc/ui_2d.jpg) 48 | 49 | You also have a 3D preview displaying the final 3D mesh. The mesh uses the same resolution as the 2D preview. 50 | You can change the view by dragging the mouse cursor in the view : 51 | - rotate the terrain with left button 52 | - zoom with middle button 53 | - pan with right button 54 | 55 | You can also display a water plane with configurable height and a grid to help visualize the terrain. 56 | 57 | ![3D preview UI](https://raw.githubusercontent.com/jice-nospam/wgen/main/doc/ui_3d.jpg) 58 | 59 | ## Save/Load project 60 | Here you can save the current generator configuration (all the steps with their parameters) in a plain text file using RON format. You can also load a previously saved project, erasing the current configuration. 61 | 62 | ![Save project UI](https://raw.githubusercontent.com/jice-nospam/wgen/main/doc/ui_project.jpg) 63 | 64 | ## Exporter 65 | You can export the resulting heightmap as a single image file or several tiled files using this panel. 66 | 67 | ![Export UI](https://raw.githubusercontent.com/jice-nospam/wgen/main/doc/ui_export.jpg) 68 | 69 | You can click on the file extension to chose another format (16 bits PNG or EXR currently supported). 70 | 71 | File names will be generated using _x?_y? pattern, for example for 2x2 tiles : 72 | * ..._x0_y0.png 73 | * ..._x1_y0.png 74 | * ..._x0_y1.png 75 | * ..._x1_y1.png 76 | 77 | If the seamless checkbox is checked, the same row of pixels will be repeated on the border of two adjacent tiles. 78 | This is not needed if you export to unreal engine as it natively supports multi-textures heightmaps. 79 | This might be needed for other engines where each tile is an independant terrain object that needs to have matching border vertices with the adjacent object. 80 | 81 | # Engines guide 82 | ## Unreal Engine 5 83 | Unreal natively support multi-textures heightmap. All you have to do is to choose the texture size (preferably 1024x1024 or 2048x2048 PNG) and adjust the number of tiles to match your total terrain size. The seamless flag should be unchecked as Unreal automatically joins the tile borders. 84 | 85 | ## Godot 3 86 | As of version 3.5, Godot only support 8bits PNG so using the PNG format will result in posterization of the heightmap and a staircase effect. So the prefered format here when using the Heightmap Terrain plugin is a single square EXR file with a "power of two plus one" size (1025x1025, 2049x2049 or 4097x4097). The EXR file contains values between 0.0 and 1.0 and might look completely flat in Godot, so increase the y scale of your HTerrain object to something near 500. -------------------------------------------------------------------------------- /src/generators/water_erosion.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc::Sender; 2 | 3 | use eframe::egui; 4 | use rand::{rngs::StdRng, Rng, SeedableRng}; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use crate::ThreadMessage; 8 | 9 | use super::report_progress; 10 | 11 | // water erosion algorithm adapted from https://www.firespark.de/resources/downloads/implementation%20of%20a%20methode%20for%20hydraulic%20erosion.pdf 12 | const MAX_PATH_LENGTH: usize = 40; 13 | const DEFAULT_EVAPORATION: f32 = 0.05; 14 | const DEFAULT_CAPACITY: f32 = 8.0; 15 | const DEFAULT_MIN_SLOPE: f32 = 0.05; 16 | const DEFAULT_DEPOSITION: f32 = 0.1; 17 | const DEFAULT_INERTIA: f32 = 0.4; 18 | const DEFAULT_DROP_AMOUNT: f32 = 0.5; 19 | const DEFAULT_EROSION_STRENGTH: f32 = 0.1; 20 | const DEFAULT_RADIUS: f32 = 4.0; 21 | 22 | /// a drop of water 23 | struct Drop { 24 | /// position on the grid 25 | pub pos: (f32, f32), 26 | /// water amount 27 | pub water: f32, 28 | /// movement direction 29 | pub dir: (f32, f32), 30 | /// maximum sediment capacity of the drop 31 | pub capacity: f32, 32 | /// amount of accumulated sediment 33 | pub sediment: f32, 34 | /// velocity 35 | pub speed: f32, 36 | } 37 | 38 | impl Drop { 39 | pub fn grid_offset(&self, grid_width: usize) -> usize { 40 | self.pos.0.round() as usize + self.pos.1.round() as usize * grid_width 41 | } 42 | } 43 | 44 | #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] 45 | pub struct WaterErosionConf { 46 | drop_amount: f32, 47 | erosion_strength: f32, 48 | evaporation: f32, 49 | capacity: f32, 50 | min_slope: f32, 51 | deposition: f32, 52 | inertia: f32, 53 | radius: f32, 54 | } 55 | 56 | impl Default for WaterErosionConf { 57 | fn default() -> Self { 58 | Self { 59 | drop_amount: DEFAULT_DROP_AMOUNT, 60 | erosion_strength: DEFAULT_EROSION_STRENGTH, 61 | evaporation: DEFAULT_EVAPORATION, 62 | capacity: DEFAULT_CAPACITY, 63 | min_slope: DEFAULT_MIN_SLOPE, 64 | deposition: DEFAULT_DEPOSITION, 65 | inertia: DEFAULT_INERTIA, 66 | radius: DEFAULT_RADIUS, 67 | } 68 | } 69 | } 70 | 71 | pub fn render_water_erosion(ui: &mut egui::Ui, conf: &mut WaterErosionConf) { 72 | ui.horizontal(|ui| { 73 | ui.label("drop amount") 74 | .on_hover_text("Amount of drops simulated"); 75 | ui.add( 76 | egui::DragValue::new(&mut conf.drop_amount) 77 | .speed(0.01) 78 | .range(0.1..=2.0), 79 | ); 80 | ui.label("erosion strength") 81 | .on_hover_text("How much soil is eroded by the drop"); 82 | ui.add( 83 | egui::DragValue::new(&mut conf.erosion_strength) 84 | .speed(0.01) 85 | .range(0.01..=1.0), 86 | ); 87 | }); 88 | ui.horizontal(|ui| { 89 | ui.label("drop capacity") 90 | .on_hover_text("How much sediment a drop can contain"); 91 | ui.add( 92 | egui::DragValue::new(&mut conf.capacity) 93 | .speed(0.5) 94 | .range(2.0..=32.0), 95 | ); 96 | ui.label("inertia") 97 | .on_hover_text("Inertia of the drop. Increase for smoother result"); 98 | ui.add( 99 | egui::DragValue::new(&mut conf.inertia) 100 | .speed(0.01) 101 | .range(0.01..=0.5), 102 | ); 103 | }); 104 | ui.horizontal(|ui| { 105 | ui.label("deposition") 106 | .on_hover_text("Amount of sediment deposited"); 107 | ui.add( 108 | egui::DragValue::new(&mut conf.deposition) 109 | .speed(0.01) 110 | .range(0.01..=1.0), 111 | ); 112 | ui.label("evaporation") 113 | .on_hover_text("How fast the drop evaporate. Increase for smoother results"); 114 | ui.add( 115 | egui::DragValue::new(&mut conf.evaporation) 116 | .speed(0.01) 117 | .range(0.01..=0.5), 118 | ); 119 | }); 120 | ui.horizontal(|ui| { 121 | ui.label("radius").on_hover_text("Erosion radius"); 122 | ui.add( 123 | egui::DragValue::new(&mut conf.radius) 124 | .speed(0.1) 125 | .range(1.0..=10.0), 126 | ); 127 | ui.label("minimum slope") 128 | .on_hover_text("Minimum height for the drop capacity calculation"); 129 | ui.add( 130 | egui::DragValue::new(&mut conf.min_slope) 131 | .speed(0.001) 132 | .range(0.001..=0.1), 133 | ); 134 | }); 135 | } 136 | 137 | pub fn gen_water_erosion( 138 | seed: u64, 139 | size: (usize, usize), 140 | hmap: &mut [f32], 141 | conf: &WaterErosionConf, 142 | export: bool, 143 | tx: Sender, 144 | min_progress_step: f32, 145 | ) { 146 | let mut progress = 0.0; 147 | let mut rng = StdRng::seed_from_u64(seed); 148 | // maximum drop count is 2 per cell 149 | let drop_count = ((size.1 * 2) as f32 * conf.drop_amount) as usize; 150 | // compute erosion weight depending on radius 151 | let mut erosion_weight = 0.0; 152 | for y in (-conf.radius).round() as i32..conf.radius.round() as i32 { 153 | for x in (-conf.radius).round() as i32..conf.radius.round() as i32 { 154 | let dist = ((x * x + y * y) as f32).sqrt(); 155 | if dist < conf.radius { 156 | erosion_weight += conf.radius - dist; 157 | } 158 | } 159 | } 160 | // use a double loop to check progress every size.0 drops 161 | for y in 0..drop_count { 162 | for _ in 0..size.0 { 163 | let mut drop = Drop { 164 | pos: ( 165 | rng.random_range(0..size.0 - 1) as f32, 166 | rng.random_range(0..size.1 - 1) as f32, 167 | ), 168 | dir: (0.0, 0.0), 169 | sediment: 0.0, 170 | water: 1.0, 171 | capacity: conf.capacity, 172 | speed: 0.0, 173 | }; 174 | let mut off = drop.grid_offset(size.0); 175 | let mut count = 0; 176 | while count < MAX_PATH_LENGTH { 177 | let oldh = hmap[off]; 178 | let old_off = off; 179 | // interpolate slope at old position 180 | let h00 = oldh; 181 | let h10 = hmap[off + 1]; 182 | let h01 = hmap[off + size.0]; 183 | let h11 = hmap[off + 1 + size.0]; 184 | let old_u = drop.pos.0.fract(); 185 | let old_v = drop.pos.1.fract(); 186 | // weight for each cell surrounding the drop position 187 | let w00 = (1.0 - old_u) * (1.0 - old_v); 188 | let w10 = old_u * (1.0 - old_v); 189 | let w01 = (1.0 - old_u) * old_v; 190 | let w11 = old_u * old_v; 191 | // get slope direction 192 | let mut gx = (h00 - h10) * (1.0 - old_v) + (h01 - h11) * old_v; 193 | let mut gy = (h00 - h01) * (1.0 - old_u) + (h10 - h11) * old_u; 194 | (gx, gy) = normalize_dir(gx, gy, &mut rng); 195 | // interpolate between old direction and new one to account for inertia 196 | gx = (drop.dir.0 - gx) * conf.inertia + gx; 197 | gy = (drop.dir.1 - gy) * conf.inertia + gy; 198 | (drop.dir.0, drop.dir.1) = normalize_dir(gx, gy, &mut rng); 199 | let old_x = drop.pos.0; 200 | let old_y = drop.pos.1; 201 | // compute the droplet new position 202 | drop.pos.0 += drop.dir.0; 203 | drop.pos.1 += drop.dir.1; 204 | let ix = drop.pos.0.round() as usize; 205 | let iy = drop.pos.1.round() as usize; 206 | if ix >= size.0 - 1 || iy >= size.1 - 1 { 207 | // out of the map 208 | break; 209 | } 210 | off = drop.grid_offset(size.0); 211 | // interpolate height at new drop position 212 | let u = drop.pos.0.fract(); 213 | let v = drop.pos.1.fract(); 214 | let new_h00 = hmap[off]; 215 | let new_h10 = hmap[off + 1]; 216 | let new_h01 = hmap[off + size.0]; 217 | let new_h11 = hmap[off + 1 + size.0]; 218 | let newh = (new_h00 * (1.0 - u) + new_h10 * u) * (1.0 - v) 219 | + (new_h01 * (1.0 - u) + new_h11 * u) * v; 220 | let hdif = newh - oldh; 221 | if hdif >= 0.0 { 222 | // going uphill : deposit sediment at old position 223 | let deposit = drop.sediment.min(hdif); 224 | hmap[old_off] += deposit * w00; 225 | hmap[old_off + 1] += deposit * w10; 226 | hmap[old_off + size.0] += deposit * w01; 227 | hmap[old_off + 1 + size.0] += deposit * w11; 228 | drop.sediment -= deposit; 229 | drop.speed = 0.0; 230 | if drop.sediment <= 0.0 { 231 | // no more sediment. stop the path 232 | break; 233 | } 234 | } else { 235 | drop.capacity = 236 | conf.min_slope.max(-hdif) * drop.water * conf.capacity * drop.speed; 237 | if drop.sediment > drop.capacity { 238 | // too much sediment in the drop. deposit 239 | let deposit = (drop.sediment - drop.capacity) * conf.deposition; 240 | hmap[old_off] += deposit * w00; 241 | hmap[old_off + 1] += deposit * w10; 242 | hmap[old_off + size.0] += deposit * w01; 243 | hmap[old_off + 1 + size.0] += deposit * w11; 244 | drop.sediment -= deposit; 245 | } else { 246 | // erode 247 | let amount = 248 | ((drop.capacity - drop.sediment) * conf.erosion_strength).min(-hdif); 249 | for y in (old_y - conf.radius).round() as i32 250 | ..(old_y + conf.radius).round() as i32 251 | { 252 | if y < 0 || y >= size.1 as i32 { 253 | continue; 254 | } 255 | let dy = old_y - y as f32; 256 | for x in (old_x - conf.radius).round() as i32 257 | ..(old_x + conf.radius).round() as i32 258 | { 259 | if x < 0 || x >= size.0 as i32 { 260 | continue; 261 | } 262 | let dx = old_x - x as f32; 263 | let dist = (dx * dx + dy * dy).sqrt(); 264 | if dist < conf.radius { 265 | let off = x as usize + y as usize * size.0; 266 | hmap[off] -= amount * (conf.radius - dist) / erosion_weight; 267 | } 268 | } 269 | } 270 | drop.sediment += amount; 271 | } 272 | } 273 | drop.speed = (drop.speed * drop.speed + hdif.abs()).sqrt(); 274 | drop.water *= 1.0 - conf.evaporation; 275 | count += 1; 276 | } 277 | } 278 | let new_progress = y as f32 / drop_count as f32; 279 | if new_progress - progress >= min_progress_step { 280 | progress = new_progress; 281 | report_progress(progress, export, tx.clone()); 282 | } 283 | } 284 | } 285 | 286 | fn normalize_dir(dx: f32, dy: f32, rng: &mut StdRng) -> (f32, f32) { 287 | let len = (dx * dx + dy * dy).sqrt(); 288 | if len < std::f32::EPSILON { 289 | // random direction 290 | let angle = rng.random_range(0.0..std::f32::consts::PI * 2.0); 291 | (angle.cos(), angle.sin()) 292 | } else { 293 | (dx / len, dy / len) 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /src/worldgen.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc::Sender; 2 | use std::time::Instant; 3 | use std::{fmt::Display, sync::mpsc::Receiver}; 4 | 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use crate::generators::{ 8 | gen_fbm, gen_hills, gen_island, gen_landmass, gen_mid_point, gen_mudslide, gen_normalize, 9 | gen_water_erosion, get_min_max, FbmConf, HillsConf, IslandConf, LandMassConf, MidPointConf, 10 | MudSlideConf, NormalizeConf, WaterErosionConf, 11 | }; 12 | use crate::{log, ThreadMessage, MASK_SIZE}; 13 | 14 | #[derive(Debug)] 15 | /// commands sent by the main thread to the world generator thread 16 | pub enum WorldGenCommand { 17 | /// recompute a specific step : step index, step conf, live preview, min progress step to report 18 | ExecuteStep(usize, Step, bool, f32), 19 | /// remove a step 20 | DeleteStep(usize), 21 | /// enable a step 22 | EnableStep(usize), 23 | /// disable a step 24 | DisableStep(usize), 25 | /// change the heightmap size 26 | SetSize(usize), 27 | /// return the heightmap for a given step 28 | GetStepMap(usize), 29 | /// change the random number generator seed 30 | SetSeed(u64), 31 | /// remove all steps 32 | Clear, 33 | /// cancel previous undone ExecuteStep commands from a specific step 34 | Abort(usize), 35 | } 36 | 37 | #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] 38 | /// Each value contains its own configuration 39 | pub enum StepType { 40 | Hills(HillsConf), 41 | Fbm(FbmConf), 42 | Normalize(NormalizeConf), 43 | LandMass(LandMassConf), 44 | MudSlide(MudSlideConf), 45 | WaterErosion(WaterErosionConf), 46 | Island(IslandConf), 47 | MidPoint(MidPointConf), 48 | } 49 | #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] 50 | pub struct Step { 51 | /// should we skip this step when computing the heightmap ? 52 | pub disabled: bool, 53 | /// this step mask 54 | pub mask: Option>, 55 | /// step type with its configuration 56 | pub typ: StepType, 57 | } 58 | 59 | impl Default for Step { 60 | fn default() -> Self { 61 | Self { 62 | disabled: false, 63 | mask: None, 64 | typ: StepType::Normalize(NormalizeConf::default()), 65 | } 66 | } 67 | } 68 | 69 | impl Display for Step { 70 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 71 | let debug_val = format!("{:?}", self.typ); 72 | let val: Vec<&str> = debug_val.split('(').collect(); 73 | write!(f, "{}", val[0]) 74 | } 75 | } 76 | 77 | pub struct ExportMap { 78 | size: (usize, usize), 79 | h: Vec, 80 | } 81 | 82 | impl ExportMap { 83 | pub fn get_min_max(&self) -> (f32, f32) { 84 | get_min_max(&self.h) 85 | } 86 | pub fn get_size(&self) -> (usize, usize) { 87 | self.size 88 | } 89 | pub fn height(&self, x: usize, y: usize) -> f32 { 90 | let off = x + y * self.size.0; 91 | if off < self.size.0 * self.size.1 { 92 | return self.h[off]; 93 | } 94 | 0.0 95 | } 96 | pub fn borrow(&self) -> &Vec { 97 | &self.h 98 | } 99 | } 100 | 101 | #[derive(Clone)] 102 | struct HMap { 103 | h: Vec, 104 | disabled: bool, 105 | } 106 | 107 | #[derive(Clone)] 108 | pub struct WorldGenerator { 109 | seed: u64, 110 | world_size: (usize, usize), 111 | hmap: Vec, 112 | } 113 | 114 | struct InnerStep { 115 | index: usize, 116 | step: Step, 117 | live: bool, 118 | min_progress_step: f32, 119 | } 120 | 121 | fn do_command( 122 | msg: WorldGenCommand, 123 | wgen: &mut WorldGenerator, 124 | steps: &mut Vec, 125 | tx: Sender, 126 | ) { 127 | log(&format!("wgen<={:?}", msg)); 128 | match msg { 129 | WorldGenCommand::Clear => { 130 | wgen.clear(); 131 | } 132 | WorldGenCommand::SetSeed(new_seed) => { 133 | wgen.seed = new_seed; 134 | } 135 | WorldGenCommand::ExecuteStep(index, step, live, min_progress_step) => { 136 | steps.push(InnerStep { 137 | index, 138 | step, 139 | live, 140 | min_progress_step, 141 | }); 142 | } 143 | WorldGenCommand::DeleteStep(index) => { 144 | wgen.hmap.remove(index); 145 | } 146 | WorldGenCommand::DisableStep(index) => { 147 | wgen.hmap[index].disabled = true; 148 | } 149 | WorldGenCommand::EnableStep(index) => { 150 | wgen.hmap[index].disabled = false; 151 | } 152 | WorldGenCommand::GetStepMap(index) => tx 153 | .send(ThreadMessage::GeneratorStepMap( 154 | index, 155 | wgen.get_step_export_map(index), 156 | )) 157 | .unwrap(), 158 | WorldGenCommand::Abort(from_idx) => { 159 | let mut i = 0; 160 | while i < steps.len() { 161 | if steps[i].index >= from_idx { 162 | steps.remove(i); 163 | } else { 164 | i += 1; 165 | } 166 | } 167 | } 168 | WorldGenCommand::SetSize(size) => { 169 | *wgen = WorldGenerator::new(wgen.seed, (size, size)); 170 | } 171 | } 172 | } 173 | 174 | pub fn generator_thread( 175 | seed: u64, 176 | size: usize, 177 | rx: Receiver, 178 | tx: Sender, 179 | ) { 180 | let mut wgen = WorldGenerator::new(seed, (size, size)); 181 | let mut steps = Vec::new(); 182 | loop { 183 | if steps.is_empty() { 184 | // blocking wait 185 | if let Ok(msg) = rx.recv() { 186 | let tx = tx.clone(); 187 | do_command(msg, &mut wgen, &mut steps, tx); 188 | } 189 | } 190 | while let Ok(msg) = rx.try_recv() { 191 | let tx = tx.clone(); 192 | do_command(msg, &mut wgen, &mut steps, tx); 193 | } 194 | if !steps.is_empty() { 195 | let InnerStep { 196 | index, 197 | step, 198 | live, 199 | min_progress_step, 200 | } = steps.remove(0); 201 | let tx2 = tx.clone(); 202 | wgen.execute_step(index, &step, false, tx2, min_progress_step); 203 | if steps.is_empty() { 204 | log("wgen=>Done"); 205 | tx.send(ThreadMessage::GeneratorDone(wgen.get_export_map())) 206 | .unwrap(); 207 | } else { 208 | log(&format!("wgen=>GeneratorStepDone({})", index)); 209 | tx.send(ThreadMessage::GeneratorStepDone( 210 | index, 211 | if live { 212 | Some(wgen.get_step_export_map(index)) 213 | } else { 214 | None 215 | }, 216 | )) 217 | .unwrap(); 218 | } 219 | } 220 | } 221 | } 222 | impl WorldGenerator { 223 | pub fn new(seed: u64, world_size: (usize, usize)) -> Self { 224 | Self { 225 | seed, 226 | world_size, 227 | hmap: Vec::new(), 228 | } 229 | } 230 | pub fn get_export_map(&self) -> ExportMap { 231 | self.get_step_export_map(if self.hmap.is_empty() { 232 | 0 233 | } else { 234 | self.hmap.len() - 1 235 | }) 236 | } 237 | pub fn get_step_export_map(&self, step: usize) -> ExportMap { 238 | ExportMap { 239 | size: self.world_size, 240 | h: if step >= self.hmap.len() { 241 | vec![0.0; self.world_size.0 * self.world_size.1] 242 | } else { 243 | self.hmap[step].h.clone() 244 | }, 245 | } 246 | } 247 | 248 | pub fn combined_height(&self, x: usize, y: usize) -> f32 { 249 | let off = x + y * self.world_size.0; 250 | if !self.hmap.is_empty() && off < self.world_size.0 * self.world_size.1 { 251 | return self.hmap[self.hmap.len() - 1].h[off]; 252 | } 253 | 0.0 254 | } 255 | pub fn clear(&mut self) { 256 | *self = WorldGenerator::new(self.seed, self.world_size); 257 | } 258 | 259 | fn execute_step( 260 | &mut self, 261 | index: usize, 262 | step: &Step, 263 | export: bool, 264 | tx: Sender, 265 | min_progress_step: f32, 266 | ) { 267 | let now = Instant::now(); 268 | let len = self.hmap.len(); 269 | if index >= len { 270 | let vecsize = self.world_size.0 * self.world_size.1; 271 | self.hmap.push(if len == 0 { 272 | HMap { 273 | h: vec![0.0; vecsize], 274 | disabled: false, 275 | } 276 | } else { 277 | HMap { 278 | h: self.hmap[len - 1].h.clone(), 279 | disabled: false, 280 | } 281 | }); 282 | } else if index > 0 { 283 | self.hmap[index].h = self.hmap[index - 1].h.clone(); 284 | } else { 285 | self.hmap[index].h.fill(0.0); 286 | } 287 | { 288 | let hmap = &mut self.hmap[index]; 289 | match step { 290 | Step { 291 | typ: StepType::Hills(conf), 292 | disabled, 293 | .. 294 | } => { 295 | if !*disabled { 296 | gen_hills( 297 | self.seed, 298 | self.world_size, 299 | &mut hmap.h, 300 | conf, 301 | export, 302 | tx, 303 | min_progress_step, 304 | ); 305 | } 306 | } 307 | Step { 308 | typ: StepType::Fbm(conf), 309 | disabled, 310 | .. 311 | } => { 312 | if !*disabled { 313 | gen_fbm( 314 | self.seed, 315 | self.world_size, 316 | &mut hmap.h, 317 | conf, 318 | export, 319 | tx, 320 | min_progress_step, 321 | ); 322 | } 323 | } 324 | Step { 325 | typ: StepType::MidPoint(conf), 326 | disabled, 327 | .. 328 | } => { 329 | if !*disabled { 330 | gen_mid_point( 331 | self.seed, 332 | self.world_size, 333 | &mut hmap.h, 334 | conf, 335 | export, 336 | tx, 337 | min_progress_step, 338 | ); 339 | } 340 | } 341 | Step { 342 | typ: StepType::Normalize(conf), 343 | disabled, 344 | .. 345 | } => { 346 | if !*disabled { 347 | gen_normalize(&mut hmap.h, conf); 348 | } 349 | } 350 | Step { 351 | typ: StepType::LandMass(conf), 352 | disabled, 353 | .. 354 | } => { 355 | if !*disabled { 356 | gen_landmass( 357 | self.world_size, 358 | &mut hmap.h, 359 | conf, 360 | export, 361 | tx, 362 | min_progress_step, 363 | ); 364 | } 365 | } 366 | Step { 367 | typ: StepType::MudSlide(conf), 368 | disabled, 369 | .. 370 | } => { 371 | if !*disabled { 372 | gen_mudslide( 373 | self.world_size, 374 | &mut hmap.h, 375 | conf, 376 | export, 377 | tx, 378 | min_progress_step, 379 | ); 380 | } 381 | } 382 | Step { 383 | typ: StepType::WaterErosion(conf), 384 | disabled, 385 | .. 386 | } => { 387 | if !*disabled { 388 | gen_water_erosion( 389 | self.seed, 390 | self.world_size, 391 | &mut hmap.h, 392 | conf, 393 | export, 394 | tx, 395 | min_progress_step, 396 | ); 397 | } 398 | } 399 | Step { 400 | typ: StepType::Island(conf), 401 | disabled, 402 | .. 403 | } => { 404 | if !*disabled { 405 | gen_island( 406 | self.world_size, 407 | &mut hmap.h, 408 | conf, 409 | export, 410 | tx, 411 | min_progress_step, 412 | ); 413 | } 414 | } 415 | } 416 | } 417 | if let Some(ref mask) = step.mask { 418 | if index > 0 { 419 | let prev = self.hmap[index - 1].h.clone(); 420 | apply_mask(self.world_size, mask, Some(&prev), &mut self.hmap[index].h); 421 | } else { 422 | apply_mask(self.world_size, mask, None, &mut self.hmap[index].h); 423 | } 424 | } 425 | 426 | log(&format!( 427 | "Executed {} in {:.2}s", 428 | step, 429 | now.elapsed().as_secs_f32() 430 | )); 431 | } 432 | 433 | pub fn generate(&mut self, steps: &[Step], tx: Sender, min_progress_step: f32) { 434 | self.clear(); 435 | for (i, step) in steps.iter().enumerate() { 436 | let tx2 = tx.clone(); 437 | self.execute_step(i, step, true, tx2, min_progress_step); 438 | tx.send(ThreadMessage::ExporterStepDone(i)).unwrap(); 439 | } 440 | } 441 | 442 | pub fn get_min_max(&self) -> (f32, f32) { 443 | if self.hmap.is_empty() { 444 | (0.0, 0.0) 445 | } else { 446 | get_min_max(&self.hmap[self.hmap.len() - 1].h) 447 | } 448 | } 449 | } 450 | 451 | fn apply_mask(world_size: (usize, usize), mask: &[f32], prev: Option<&[f32]>, h: &mut [f32]) { 452 | let mut off = 0; 453 | let (min, _) = if prev.is_none() { 454 | get_min_max(h) 455 | } else { 456 | (0.0, 0.0) 457 | }; 458 | for y in 0..world_size.1 { 459 | let myf = (y * MASK_SIZE) as f32 / world_size.0 as f32; 460 | let my = myf as usize; 461 | let yalpha = myf.fract(); 462 | for x in 0..world_size.0 { 463 | let mxf = (x * MASK_SIZE) as f32 / world_size.0 as f32; 464 | let mx = mxf as usize; 465 | let xalpha = mxf.fract(); 466 | let mut mask_value = mask[mx + my * MASK_SIZE]; 467 | if mx + 1 < MASK_SIZE { 468 | mask_value = (1.0 - xalpha) * mask_value + xalpha * mask[mx + 1 + my * MASK_SIZE]; 469 | if my + 1 < MASK_SIZE { 470 | let bottom_left_mask = mask[mx + (my + 1) * MASK_SIZE]; 471 | let bottom_right_mask = mask[mx + 1 + (my + 1) * MASK_SIZE]; 472 | let bottom_mask = 473 | (1.0 - xalpha) * bottom_left_mask + xalpha * bottom_right_mask; 474 | mask_value = (1.0 - yalpha) * mask_value + yalpha * bottom_mask; 475 | } 476 | } 477 | if let Some(prev) = prev { 478 | h[off] = (1.0 - mask_value) * prev[off] + mask_value * h[off]; 479 | } else { 480 | h[off] = (1.0 - mask_value) * min + mask_value * (h[off] - min); 481 | } 482 | off += 1; 483 | } 484 | } 485 | } 486 | -------------------------------------------------------------------------------- /src/panel_generator.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui::{self, CursorIcon, Id, LayerId, Order, Sense}; 2 | use epaint::Color32; 3 | use serde::{Deserialize, Serialize}; 4 | use std::{ 5 | fs::File, 6 | io::{Read, Write}, 7 | }; 8 | 9 | use crate::{ 10 | generators::{ 11 | render_fbm, render_hills, render_island, render_landmass, render_mid_point, 12 | render_mudslide, render_water_erosion, FbmConf, HillsConf, IslandConf, LandMassConf, 13 | MidPointConf, MudSlideConf, NormalizeConf, WaterErosionConf, 14 | }, 15 | worldgen::{Step, StepType}, 16 | VERSION, 17 | }; 18 | 19 | /// actions to do by the main program 20 | pub enum GeneratorAction { 21 | /// recompute heightmap from a specific step (deleteStep, stepIndex) 22 | Regen(bool, usize), 23 | /// disable a step and recompute the heightmap 24 | Disable(usize), 25 | /// enable a step and recompute the heightmap 26 | Enable(usize), 27 | /// display a specific step heightmap in the 2D preview 28 | DisplayLayer(usize), 29 | /// display a specific step mask in the 2D preview 30 | DisplayMask(usize), 31 | /// change the RNG seed 32 | SetSeed(u64), 33 | /// remove all steps 34 | Clear, 35 | } 36 | 37 | #[derive(Serialize, Deserialize)] 38 | pub struct PanelGenerator { 39 | /// to avoid loading a save from another version 40 | version: String, 41 | #[serde(skip)] 42 | /// is the world generator currently computing the heightmap? 43 | pub is_running: bool, 44 | #[serde(skip)] 45 | /// are we currently displaying a mask or a heightmap ? 46 | pub mask_selected: bool, 47 | /// generator steps with their configuration and masks 48 | pub steps: Vec, 49 | /// current selected step. used for combo box. must be outside of steps in case steps is empty 50 | cur_step: Step, 51 | /// current selected step index 52 | pub selected_step: usize, 53 | /// current drag n drop destination 54 | move_to_pos: usize, 55 | /// is the drag n drop zone currently hovered by the mouse cursor? 56 | hovered: bool, 57 | /// random number generator's seed 58 | pub seed: u64, 59 | } 60 | 61 | impl Default for PanelGenerator { 62 | fn default() -> Self { 63 | Self { 64 | version: VERSION.to_owned(), 65 | is_running: false, 66 | mask_selected: false, 67 | steps: Vec::new(), 68 | cur_step: Step { 69 | typ: StepType::Hills(HillsConf::default()), 70 | ..Default::default() 71 | }, 72 | selected_step: 0, 73 | move_to_pos: 0, 74 | hovered: false, 75 | seed: 0xdeadbeef, 76 | } 77 | } 78 | } 79 | 80 | fn render_step_gui(ui: &mut egui::Ui, id: Id, body: impl FnOnce(&mut egui::Ui)) -> Option { 81 | let is_being_dragged = ui.ctx().is_being_dragged(id); 82 | if !is_being_dragged { 83 | ui.scope(body); 84 | } else { 85 | let layer_id = LayerId::new(Order::Tooltip, id); 86 | let response = ui.with_layer_id(layer_id, body).response; 87 | ui.output_mut(|i| i.cursor_icon = CursorIcon::Grabbing); 88 | if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() { 89 | let mut delta = pointer_pos - response.rect.center(); 90 | delta.x += 60.0; 91 | ui.ctx().translate_layer(layer_id, delta); 92 | return Some(delta.y); 93 | } 94 | } 95 | None 96 | } 97 | 98 | impl PanelGenerator { 99 | pub fn enabled_steps(&self) -> usize { 100 | self.steps.iter().filter(|s| !s.disabled).count() 101 | } 102 | fn render_header(&mut self, ui: &mut egui::Ui, progress: f32) -> Option { 103 | let mut action = None; 104 | ui.horizontal(|ui| { 105 | ui.heading("Generators"); 106 | if self.is_running { 107 | ui.spinner(); 108 | } 109 | }); 110 | ui.add(egui::ProgressBar::new(progress).show_percentage()); 111 | ui.horizontal(|ui| { 112 | if ui.button("Clear").clicked() { 113 | self.steps.clear(); 114 | action = Some(GeneratorAction::Clear) 115 | } 116 | ui.label("Seed"); 117 | let old_seed = self.seed; 118 | let old_size = ui.spacing().interact_size.x; 119 | ui.spacing_mut().interact_size.x = 100.0; 120 | ui.add(egui::DragValue::new(&mut self.seed).speed(1.0)); 121 | ui.spacing_mut().interact_size.x = old_size; 122 | if self.seed != old_seed { 123 | action = Some(GeneratorAction::SetSeed(self.seed)); 124 | } 125 | }); 126 | action 127 | } 128 | /// render UI to add a new step 129 | fn render_new_step(&mut self, ui: &mut egui::Ui) -> Option { 130 | let mut action = None; 131 | ui.horizontal(|ui| { 132 | if ui.button("New step").clicked() { 133 | self.steps.push(self.cur_step.clone()); 134 | self.selected_step = self.steps.len() - 1; 135 | action = Some(GeneratorAction::Regen(false, self.selected_step)) 136 | } 137 | egui::ComboBox::from_label("") 138 | .selected_text(format!("{}", self.cur_step)) 139 | .show_ui(ui, |ui| { 140 | ui.selectable_value( 141 | &mut self.cur_step, 142 | Step { 143 | typ: StepType::Hills(HillsConf::default()), 144 | ..Default::default() 145 | }, 146 | "Hills", 147 | ) 148 | .on_hover_text("Add round hills to generate a smooth land"); 149 | ui.selectable_value( 150 | &mut self.cur_step, 151 | Step { 152 | typ: StepType::Fbm(FbmConf::default()), 153 | ..Default::default() 154 | }, 155 | "Fbm", 156 | ) 157 | .on_hover_text("Add fractional brownian motion to generate a mountainous land"); 158 | ui.selectable_value( 159 | &mut self.cur_step, 160 | Step { 161 | typ: StepType::MidPoint(MidPointConf::default()), 162 | ..Default::default() 163 | }, 164 | "MidPoint", 165 | ) 166 | .on_hover_text("Use mid point deplacement to generate a mountainous land"); 167 | ui.selectable_value( 168 | &mut self.cur_step, 169 | Step { 170 | typ: StepType::Normalize(NormalizeConf::default()), 171 | ..Default::default() 172 | }, 173 | "Normalize", 174 | ) 175 | .on_hover_text("Scale the terrain back to the 0.0-1.0 range"); 176 | ui.selectable_value( 177 | &mut self.cur_step, 178 | Step { 179 | typ: StepType::LandMass(LandMassConf::default()), 180 | ..Default::default() 181 | }, 182 | "LandMass", 183 | ) 184 | .on_hover_text( 185 | "Scale the terrain so that only a proportion of land is above water level", 186 | ); 187 | ui.selectable_value( 188 | &mut self.cur_step, 189 | Step { 190 | typ: StepType::MudSlide(MudSlideConf::default()), 191 | ..Default::default() 192 | }, 193 | "MudSlide", 194 | ) 195 | .on_hover_text("Simulate mud sliding and smoothing the terrain"); 196 | ui.selectable_value( 197 | &mut self.cur_step, 198 | Step { 199 | typ: StepType::WaterErosion(WaterErosionConf::default()), 200 | ..Default::default() 201 | }, 202 | "WaterErosion", 203 | ) 204 | .on_hover_text("Simulate rain falling and carving rivers"); 205 | ui.selectable_value( 206 | &mut self.cur_step, 207 | Step { 208 | typ: StepType::Island(IslandConf::default()), 209 | ..Default::default() 210 | }, 211 | "Island", 212 | ) 213 | .on_hover_text("Lower height on the map borders"); 214 | }); 215 | }); 216 | action 217 | } 218 | /// render the list of steps of current project 219 | fn render_step_list( 220 | &mut self, 221 | ui: &mut egui::Ui, 222 | to_remove: &mut Option, 223 | to_move: &mut Option, 224 | ) -> Option { 225 | let mut action = None; 226 | let len = self.steps.len(); 227 | // let dragging = ui.ctx().dragged_id.is_some() 228 | let dragging = ui.memory(|m| m.is_anything_being_dragged()) && self.hovered; 229 | let response = ui 230 | .scope(|ui| { 231 | for (i, step) in self.steps.iter_mut().enumerate() { 232 | if dragging && self.move_to_pos == i { 233 | ui.separator(); 234 | } 235 | let item_id = Id::new("wgen").with(i); 236 | if let Some(dy) = render_step_gui(ui, item_id, |ui| { 237 | ui.horizontal(|ui| { 238 | let response = ui 239 | .button("▓") 240 | .on_hover_text("Drag this to change step order"); 241 | let response = ui.interact(response.rect, item_id, Sense::drag()); 242 | if response.hovered() { 243 | ui.output_mut(|o| o.cursor_icon = CursorIcon::Grab); 244 | } 245 | if ui.button("⊗").on_hover_text("Delete this step").clicked() { 246 | *to_remove = Some(i); 247 | } 248 | if ui 249 | .button(egui::RichText::new("👁").color(if step.disabled { 250 | Color32::from_rgb(0, 0, 0) 251 | } else { 252 | Color32::from_rgb(200, 200, 200) 253 | })) 254 | .on_hover_text(if step.disabled { 255 | "Enable this step" 256 | } else { 257 | "Disable this step" 258 | }) 259 | .clicked() 260 | { 261 | step.disabled = !step.disabled; 262 | if step.disabled { 263 | action = Some(GeneratorAction::Disable(i)); 264 | } else { 265 | action = Some(GeneratorAction::Enable(i)); 266 | } 267 | } 268 | if ui 269 | .button(if step.mask.is_none() { "⬜" } else { "⬛" }) 270 | .on_hover_text("Add a mask to this step") 271 | .clicked() 272 | { 273 | self.mask_selected = true; 274 | self.selected_step = i; 275 | } 276 | if ui 277 | .selectable_label( 278 | self.selected_step == i && !self.mask_selected, 279 | step.to_string(), 280 | ) 281 | .clicked() 282 | { 283 | self.selected_step = i; 284 | self.mask_selected = false; 285 | } 286 | }); 287 | }) { 288 | *to_move = Some(i); 289 | let dest = i as i32 + (dy / 20.0) as i32; 290 | self.move_to_pos = dest.clamp(0, len as i32) as usize; 291 | } 292 | } 293 | }) 294 | .response; 295 | self.hovered = response.hovered(); 296 | action 297 | } 298 | /// render the configuration UI for currently selected step 299 | fn render_curstep_conf(&mut self, ui: &mut egui::Ui) -> Option { 300 | let mut action = None; 301 | match &mut self.steps[self.selected_step] { 302 | Step { 303 | typ: StepType::Hills(conf), 304 | .. 305 | } => render_hills(ui, conf), 306 | Step { 307 | typ: StepType::LandMass(conf), 308 | .. 309 | } => render_landmass(ui, conf), 310 | Step { 311 | typ: StepType::MudSlide(conf), 312 | .. 313 | } => render_mudslide(ui, conf), 314 | Step { 315 | typ: StepType::Fbm(conf), 316 | .. 317 | } => render_fbm(ui, conf), 318 | Step { 319 | typ: StepType::WaterErosion(conf), 320 | .. 321 | } => render_water_erosion(ui, conf), 322 | Step { 323 | typ: StepType::Island(conf), 324 | .. 325 | } => render_island(ui, conf), 326 | Step { 327 | typ: StepType::MidPoint(conf), 328 | .. 329 | } => render_mid_point(ui, conf), 330 | Step { 331 | typ: StepType::Normalize(_), 332 | .. 333 | } => (), 334 | } 335 | if ui.button("Refresh").clicked() { 336 | action = Some(GeneratorAction::Regen(false, self.selected_step)); 337 | self.mask_selected = false; 338 | } 339 | action 340 | } 341 | pub fn render(&mut self, ui: &mut egui::Ui, progress: f32) -> Option { 342 | let previous_selected_step = self.selected_step; 343 | let previous_mask_selected = self.mask_selected; 344 | let mut action = self.render_header(ui, progress); 345 | action = action.or(self.render_new_step(ui)); 346 | ui.end_row(); 347 | let mut to_remove = None; 348 | let mut to_move = None; 349 | action = action.or(self.render_step_list(ui, &mut to_remove, &mut to_move)); 350 | ui.separator(); 351 | if !self.steps.is_empty() { 352 | action = action.or(self.render_curstep_conf(ui)); 353 | } 354 | if action.is_none() 355 | && (previous_selected_step != self.selected_step 356 | || previous_mask_selected != self.mask_selected) 357 | { 358 | if self.mask_selected { 359 | action = Some(GeneratorAction::DisplayMask(self.selected_step)); 360 | } else { 361 | action = Some(GeneratorAction::DisplayLayer(self.selected_step)); 362 | } 363 | } 364 | if let Some(i) = to_remove { 365 | self.steps.remove(i); 366 | if self.selected_step >= self.steps.len() { 367 | self.selected_step = if self.steps.is_empty() { 368 | 0 369 | } else { 370 | self.steps.len() - 1 371 | }; 372 | } 373 | action = Some(GeneratorAction::Regen(true, i)); 374 | self.mask_selected = false; 375 | } 376 | if ui.input(|i| i.pointer.any_released()) { 377 | if let Some(i) = to_move { 378 | if i != self.move_to_pos { 379 | let step = self.steps.remove(i); 380 | let dest = if self.move_to_pos > i { 381 | self.move_to_pos - 1 382 | } else { 383 | self.move_to_pos 384 | }; 385 | self.steps.insert(dest, step); 386 | action = Some(GeneratorAction::Regen(false, i)); 387 | self.mask_selected = false; 388 | } 389 | } 390 | } 391 | action 392 | } 393 | pub fn load(&mut self, file_path: &str) -> Result<(), String> { 394 | let mut file = File::open(file_path).map_err(|_| "Unable to open the file")?; 395 | let mut contents = String::new(); 396 | file.read_to_string(&mut contents) 397 | .map_err(|_| "Unable to read the file")?; 398 | let gen_data: PanelGenerator = 399 | ron::from_str(&contents).map_err(|e| format!("Cannot parse the file : {}", e))?; 400 | if gen_data.version != VERSION { 401 | return Err(format!( 402 | "Bad file version. Expected {}, found {}", 403 | VERSION, gen_data.version 404 | )); 405 | } 406 | *self = gen_data; 407 | Ok(()) 408 | } 409 | pub fn save(&self, file_path: &str) -> Result<(), String> { 410 | let data = ron::to_string(self).unwrap(); 411 | let mut buffer = File::create(file_path).map_err(|_| "Unable to create the file")?; 412 | write!(buffer, "{}", data).map_err(|_| "Unable to write to the file")?; 413 | Ok(()) 414 | } 415 | } 416 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate exr; 2 | extern crate image; 3 | extern crate noise; 4 | extern crate rand; 5 | 6 | mod exporter; 7 | mod fps; 8 | mod generators; 9 | mod panel_2dview; 10 | mod panel_3dview; 11 | mod panel_export; 12 | mod panel_generator; 13 | mod panel_maskedit; 14 | mod panel_save; 15 | mod worldgen; 16 | 17 | use eframe::egui::{self, Visuals}; 18 | use epaint::emath; 19 | use exporter::export_heightmap; 20 | use std::sync::mpsc::{self, Receiver, Sender}; 21 | use std::thread; 22 | use std::time::Instant; 23 | 24 | use panel_2dview::{Panel2dAction, Panel2dView}; 25 | use panel_3dview::Panel3dView; 26 | use panel_export::PanelExport; 27 | use panel_generator::{GeneratorAction, PanelGenerator}; 28 | use panel_save::{PanelSaveLoad, SaveLoadAction}; 29 | use worldgen::{generator_thread, ExportMap, WorldGenCommand, WorldGenerator}; 30 | 31 | pub const VERSION: &str = env!("CARGO_PKG_VERSION"); 32 | pub const MASK_SIZE: usize = 64; 33 | 34 | /// messages sent to the main thread by either world generator or exporter threads 35 | pub enum ThreadMessage { 36 | /// from world generator : all steps have been computed => update 2D/3D previews 37 | GeneratorDone(ExportMap), 38 | /// from world generator : update progress bar 39 | GeneratorStepProgress(f32), 40 | /// from world generator : one step has been computed => update 2D preview if live preview enabled 41 | GeneratorStepDone(usize, Option), 42 | /// from world generator : return the heightmap for a specific step 43 | GeneratorStepMap(usize, ExportMap), 44 | /// from exporter : one step has been computed 45 | ExporterStepDone(usize), 46 | /// from exporter : export is finished 47 | ExporterDone(Result<(), String>), 48 | /// from exporter : update progress bar 49 | ExporterStepProgress(f32), 50 | } 51 | 52 | fn main() { 53 | let options = eframe::NativeOptions { 54 | multisampling: 8, 55 | depth_buffer: 24, 56 | renderer: eframe::Renderer::Glow, 57 | vsync: true, 58 | viewport: egui::ViewportBuilder::default().with_maximized(true), 59 | ..Default::default() 60 | }; 61 | println!( 62 | "wgen v{} - {} cpus {} cores", 63 | VERSION, 64 | num_cpus::get(), 65 | num_cpus::get_physical() 66 | ); 67 | eframe::run_native( 68 | "wgen", 69 | options, 70 | Box::new(|_cc| Ok(Box::new(MyApp::default()))), 71 | ) 72 | .or_else(|e| { 73 | eprintln!("Error: {}", e); 74 | Ok::<(), ()>(()) 75 | }) 76 | .ok(); 77 | } 78 | 79 | struct MyApp { 80 | /// size in pixels of the 2D preview canvas 81 | image_size: usize, 82 | /// size of the preview heightmap (from 64x64 to 512x512) 83 | preview_size: usize, 84 | /// current world generator progress 85 | progress: f32, 86 | /// exporter progress 87 | exporter_progress: f32, 88 | /// exporter progress bar text 89 | exporter_text: String, 90 | /// exporter current step 91 | exporter_cur_step: usize, 92 | /// random number generator's seed 93 | seed: u64, 94 | // ui widgets 95 | gen_panel: PanelGenerator, 96 | export_panel: PanelExport, 97 | panel_3d: Panel3dView, 98 | panel_2d: Panel2dView, 99 | load_save_panel: PanelSaveLoad, 100 | // thread communication 101 | /// channel to receive messages from either world generator or exporter 102 | thread2main_rx: Receiver, 103 | /// channel to send messages to the world generator thread 104 | main2wgen_tx: Sender, 105 | /// channel to send messages to the main thread from the exporter thread 106 | exp2main_tx: Sender, 107 | /// an error to display in a popup 108 | err_msg: Option, 109 | /// are we editing a mask ? 110 | mask_step: Option, 111 | /// last time the mask was updated 112 | last_mask_updated: f64, 113 | } 114 | 115 | impl Default for MyApp { 116 | fn default() -> Self { 117 | let preview_size = 128; 118 | let image_size = 790; //368; 119 | let seed = 0xdeadbeef; 120 | let wgen = WorldGenerator::new(seed, (preview_size, preview_size)); 121 | let panel_2d = Panel2dView::new(image_size, preview_size as u32, &wgen.get_export_map()); 122 | // generator -> main channel 123 | let (exp2main_tx, thread2main_rx) = mpsc::channel(); 124 | // main -> generator channel 125 | let (main2gen_tx, gen_rx) = mpsc::channel(); 126 | let gen_tx = exp2main_tx.clone(); 127 | thread::spawn(move || { 128 | generator_thread(seed, preview_size, gen_rx, gen_tx); 129 | }); 130 | Self { 131 | image_size, 132 | preview_size, 133 | seed, 134 | panel_2d, 135 | panel_3d: Panel3dView::new(image_size as f32), 136 | progress: 1.0, 137 | exporter_progress: 1.0, 138 | exporter_text: String::new(), 139 | exporter_cur_step: 0, 140 | mask_step: None, 141 | gen_panel: PanelGenerator::default(), 142 | export_panel: PanelExport::default(), 143 | load_save_panel: PanelSaveLoad::default(), 144 | thread2main_rx, 145 | main2wgen_tx: main2gen_tx, 146 | exp2main_tx, 147 | err_msg: None, 148 | last_mask_updated: 0.0, 149 | } 150 | } 151 | } 152 | 153 | impl MyApp { 154 | fn export(&mut self) { 155 | let steps = self.gen_panel.steps.clone(); 156 | let export_panel = self.export_panel.clone(); 157 | let seed = self.seed; 158 | let tx = self.exp2main_tx.clone(); 159 | let min_progress_step = 0.01 * self.gen_panel.enabled_steps() as f32; 160 | thread::spawn(move || { 161 | let res = export_heightmap(seed, &steps, &export_panel, tx.clone(), min_progress_step); 162 | tx.send(ThreadMessage::ExporterDone(res)).unwrap(); 163 | }); 164 | } 165 | fn regen(&mut self, must_delete: bool, from_idx: usize) { 166 | self.progress = from_idx as f32 / self.gen_panel.enabled_steps() as f32; 167 | self.main2wgen_tx 168 | .send(WorldGenCommand::Abort(from_idx)) 169 | .unwrap(); 170 | let len = self.gen_panel.steps.len(); 171 | if must_delete { 172 | self.main2wgen_tx 173 | .send(WorldGenCommand::DeleteStep(from_idx)) 174 | .unwrap(); 175 | } 176 | if len == 0 { 177 | return; 178 | } 179 | for i in from_idx.min(len - 1)..len { 180 | self.main2wgen_tx 181 | .send(WorldGenCommand::ExecuteStep( 182 | i, 183 | self.gen_panel.steps[i].clone(), 184 | self.panel_2d.live_preview, 185 | 0.01 * self.gen_panel.enabled_steps() as f32, 186 | )) 187 | .unwrap(); 188 | } 189 | self.gen_panel.is_running = true; 190 | } 191 | fn set_seed(&mut self, new_seed: u64) { 192 | self.seed = new_seed; 193 | self.main2wgen_tx 194 | .send(WorldGenCommand::SetSeed(new_seed)) 195 | .unwrap(); 196 | self.regen(false, 0); 197 | } 198 | fn resize(&mut self, new_size: usize) { 199 | if self.preview_size == new_size { 200 | return; 201 | } 202 | self.preview_size = new_size; 203 | self.main2wgen_tx 204 | .send(WorldGenCommand::SetSize(new_size)) 205 | .unwrap(); 206 | self.regen(false, 0); 207 | } 208 | fn render_left_panel(&mut self, ctx: &egui::Context) { 209 | egui::SidePanel::left("Generation").show(ctx, |ui| { 210 | ui.label(format!("wgen {}", VERSION)); 211 | ui.separator(); 212 | if self 213 | .export_panel 214 | .render(ui, self.exporter_progress, &self.exporter_text) 215 | { 216 | self.export_panel.enabled = false; 217 | self.exporter_progress = 0.0; 218 | self.exporter_cur_step = 0; 219 | self.export(); 220 | } 221 | ui.separator(); 222 | match self.load_save_panel.render(ui) { 223 | Some(SaveLoadAction::Load) => { 224 | if let Err(msg) = self.gen_panel.load(self.load_save_panel.get_file_path()) { 225 | let err_msg = format!( 226 | "Error while reading project {} : {}", 227 | self.load_save_panel.get_file_path(), 228 | msg 229 | ); 230 | println!("{}", err_msg); 231 | self.err_msg = Some(err_msg); 232 | } else { 233 | self.main2wgen_tx.send(WorldGenCommand::Clear).unwrap(); 234 | self.set_seed(self.gen_panel.seed); 235 | } 236 | } 237 | Some(SaveLoadAction::Save) => { 238 | if let Err(msg) = self.gen_panel.save(self.load_save_panel.get_file_path()) { 239 | let err_msg = format!( 240 | "Error while writing project {} : {}", 241 | self.load_save_panel.get_file_path(), 242 | msg 243 | ); 244 | println!("{}", err_msg); 245 | self.err_msg = Some(err_msg); 246 | } 247 | } 248 | None => (), 249 | } 250 | ui.separator(); 251 | egui::ScrollArea::vertical().show(ui, |ui| { 252 | match self.gen_panel.render(ui, self.progress) { 253 | Some(GeneratorAction::Clear) => { 254 | self.main2wgen_tx.send(WorldGenCommand::Clear).unwrap(); 255 | } 256 | Some(GeneratorAction::SetSeed(new_seed)) => { 257 | self.set_seed(new_seed); 258 | } 259 | Some(GeneratorAction::Regen(must_delete, from_idx)) => { 260 | self.regen(must_delete, from_idx); 261 | } 262 | Some(GeneratorAction::Disable(idx)) => { 263 | self.main2wgen_tx 264 | .send(WorldGenCommand::DisableStep(idx)) 265 | .unwrap(); 266 | self.regen(false, idx); 267 | } 268 | Some(GeneratorAction::Enable(idx)) => { 269 | self.main2wgen_tx 270 | .send(WorldGenCommand::EnableStep(idx)) 271 | .unwrap(); 272 | self.regen(false, idx); 273 | } 274 | Some(GeneratorAction::DisplayLayer(step)) => { 275 | self.main2wgen_tx 276 | .send(WorldGenCommand::GetStepMap(step)) 277 | .unwrap(); 278 | } 279 | Some(GeneratorAction::DisplayMask(step)) => { 280 | self.mask_step = Some(step); 281 | let mask = if let Some(ref mask) = self.gen_panel.steps[step].mask { 282 | Some(mask.clone()) 283 | } else { 284 | Some(vec![1.0; MASK_SIZE * MASK_SIZE]) 285 | }; 286 | self.panel_2d 287 | .display_mask(self.image_size, self.preview_size as u32, mask); 288 | } 289 | None => (), 290 | } 291 | }); 292 | }); 293 | } 294 | fn render_central_panel(&mut self, ctx: &egui::Context) { 295 | egui::CentralPanel::default().show(ctx, |ui| { 296 | ui.heading("Terrain preview"); 297 | ui.horizontal(|ui| { 298 | egui::CollapsingHeader::new("2d preview") 299 | .default_open(true) 300 | .show(ui, |ui| match self.panel_2d.render(ui) { 301 | Some(Panel2dAction::ResizePreview(new_size)) => { 302 | self.resize(new_size); 303 | self.mask_step = None; 304 | self.gen_panel.mask_selected = false; 305 | } 306 | Some(Panel2dAction::MaskUpdated) => { 307 | self.last_mask_updated = ui.input(|r| r.time); 308 | } 309 | Some(Panel2dAction::MaskDelete) => { 310 | if let Some(step) = self.mask_step { 311 | self.gen_panel.steps[step].mask = None; 312 | } 313 | self.last_mask_updated = 0.0; 314 | } 315 | None => (), 316 | }); 317 | egui::CollapsingHeader::new("3d preview") 318 | .default_open(true) 319 | .show(ui, |ui| { 320 | self.panel_3d.render(ui); 321 | }); 322 | }); 323 | }); 324 | } 325 | fn handle_threads_messages(&mut self) { 326 | match self.thread2main_rx.try_recv() { 327 | Ok(ThreadMessage::GeneratorStepProgress(progress)) => { 328 | let progstep = 1.0 / self.gen_panel.enabled_steps() as f32; 329 | self.progress = (self.progress / progstep).floor() * progstep; 330 | self.progress += progress * progstep; 331 | } 332 | Ok(ThreadMessage::GeneratorDone(hmap)) => { 333 | log("main<=Done"); 334 | self.panel_2d 335 | .refresh(self.image_size, self.preview_size as u32, Some(&hmap)); 336 | self.gen_panel.selected_step = self.gen_panel.steps.len() - 1; 337 | self.panel_3d.update_mesh(&hmap); 338 | self.gen_panel.is_running = false; 339 | self.progress = 1.0; 340 | } 341 | Ok(ThreadMessage::GeneratorStepDone(step, hmap)) => { 342 | log(&format!("main<=GeneratorStepDone({})", step)); 343 | if let Some(ref hmap) = hmap { 344 | self.panel_2d 345 | .refresh(self.image_size, self.preview_size as u32, Some(hmap)); 346 | } 347 | self.gen_panel.selected_step = step; 348 | self.progress = (step + 1) as f32 / self.gen_panel.enabled_steps() as f32 349 | } 350 | Ok(ThreadMessage::GeneratorStepMap(_idx, hmap)) => { 351 | // display heightmap from a specific step in the 2d preview 352 | if let Some(step) = self.mask_step { 353 | // mask was updated, recompute terrain 354 | self.regen(false, step); 355 | self.mask_step = None; 356 | } 357 | self.panel_2d 358 | .refresh(self.image_size, self.preview_size as u32, Some(&hmap)); 359 | } 360 | Ok(ThreadMessage::ExporterStepProgress(progress)) => { 361 | let progstep = 1.0 / self.gen_panel.enabled_steps() as f32; 362 | self.exporter_progress = (self.exporter_progress / progstep).floor() * progstep; 363 | self.exporter_progress += progress * progstep; 364 | self.exporter_text = format!( 365 | "{}% {}/{} {}", 366 | (self.exporter_progress * 100.0) as usize, 367 | self.exporter_cur_step + 1, 368 | self.gen_panel.steps.len(), 369 | self.gen_panel.steps[self.exporter_cur_step] 370 | ); 371 | } 372 | Ok(ThreadMessage::ExporterStepDone(step)) => { 373 | log(&format!("main<=ExporterStepDone({})", step)); 374 | self.exporter_progress = (step + 1) as f32 / self.gen_panel.enabled_steps() as f32; 375 | self.exporter_cur_step = step + 1; 376 | if step + 1 == self.gen_panel.steps.len() { 377 | self.exporter_text = 378 | format!("Saving {}...", self.export_panel.file_type.to_string()); 379 | } else { 380 | self.exporter_text = format!( 381 | "{}% {}/{} {}", 382 | (self.exporter_progress * 100.0) as usize, 383 | step + 1, 384 | self.gen_panel.steps.len(), 385 | self.gen_panel.steps[self.exporter_cur_step] 386 | ); 387 | } 388 | } 389 | Ok(ThreadMessage::ExporterDone(res)) => { 390 | if let Err(msg) = res { 391 | let err_msg = format!("Error while exporting heightmap : {}", msg); 392 | println!("{}", err_msg); 393 | self.err_msg = Some(err_msg); 394 | } 395 | log("main<=ExporterDone"); 396 | self.exporter_progress = 1.0; 397 | self.export_panel.enabled = true; 398 | self.exporter_cur_step = 0; 399 | self.exporter_text = String::new(); 400 | } 401 | Err(_) => {} 402 | } 403 | } 404 | } 405 | 406 | impl eframe::App for MyApp { 407 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 408 | let wsize = ctx.input(|i| { 409 | if let Some(rect) = i.viewport().inner_rect { 410 | rect.size() 411 | } else { 412 | emath::Vec2::new(0.0, 0.0) 413 | } 414 | }); 415 | let new_size = ((wsize.x - 340.0) * 0.5) as usize; 416 | if new_size != self.image_size && new_size != 0 { 417 | // handle window resizing 418 | self.image_size = new_size; 419 | self.panel_2d 420 | .refresh(self.image_size, self.preview_size as u32, None); 421 | self.panel_3d = Panel3dView::new(self.image_size as f32); 422 | self.regen(false, 0); 423 | } 424 | ctx.set_visuals(Visuals::dark()); 425 | self.handle_threads_messages(); 426 | self.render_left_panel(ctx); 427 | self.render_central_panel(ctx); 428 | if self.last_mask_updated > 0.0 && ctx.input(|i| i.time) - self.last_mask_updated >= 0.5 { 429 | if let Some(step) = self.mask_step { 430 | // mask was updated, copy mask to generator step 431 | if let Some(mask) = self.panel_2d.get_current_mask() { 432 | self.gen_panel.steps[step].mask = Some(mask); 433 | } 434 | } 435 | self.last_mask_updated = 0.0; 436 | } 437 | 438 | if let Some(ref err_msg) = self.err_msg { 439 | // display error popup 440 | let mut open = true; 441 | egui::Window::new("Error") 442 | .resizable(false) 443 | .collapsible(false) 444 | .open(&mut open) 445 | .show(ctx, |ui| { 446 | ui.scope(|ui| { 447 | ui.visuals_mut().override_text_color = Some(egui::Color32::RED); 448 | ui.label(err_msg); 449 | }); 450 | }); 451 | if !open { 452 | self.err_msg = None; 453 | } 454 | } 455 | } 456 | } 457 | 458 | pub fn log(msg: &str) { 459 | thread_local! { 460 | pub static LOGTIME: Instant = Instant::now(); 461 | } 462 | LOGTIME.with(|log_time| { 463 | println!( 464 | "{:03.3} {}", 465 | log_time.elapsed().as_millis() as f32 / 1000.0, 466 | msg 467 | ); 468 | }); 469 | } 470 | -------------------------------------------------------------------------------- /src/panel_maskedit.rs: -------------------------------------------------------------------------------- 1 | use eframe::{ 2 | egui::{self, PointerButton}, 3 | emath, 4 | }; 5 | use epaint::{Color32, ColorImage, Pos2, Rect}; 6 | use three_d::{ 7 | vec3, Blend, Camera, ColorMaterial, CpuMaterial, CpuMesh, CpuTexture, Cull, DepthTest, Gm, 8 | Indices, Mat4, Mesh, Object, Positions, Srgba, TextureData, Viewport, 9 | }; 10 | 11 | use crate::{panel_2dview::Panel2dAction, MASK_SIZE}; 12 | 13 | /// maximum size of the brush relative to the canvas 14 | const MAX_BRUSH_SIZE: f32 = 0.25; 15 | 16 | #[derive(Clone, Copy)] 17 | pub struct BrushConfig { 18 | /// value painted with middle mouse button 19 | pub value: f32, 20 | /// brush radius from a single 'pixel' in the heightmap (0.0) to 25% of heightmap's size (1.0) 21 | pub size: f32, 22 | /// brush radius where the opacity starts to falloff from no falloff(0.0) to center of the brush (1.0) 23 | pub falloff: f32, 24 | /// how fast the brush updates the mask 0.0: slow, 1.0: fast 25 | pub opacity: f32, 26 | } 27 | pub struct PanelMaskEdit { 28 | /// preview canvas size in pixels 29 | image_size: usize, 30 | /// the mask as a MASK_SIZE x MASK_SIZE f32 matrix 31 | mask: Option>, 32 | /// the brush parameters 33 | conf: BrushConfig, 34 | /// should the mesh used to render the mask be updated to reflect changes in mask ? 35 | mesh_updated: bool, 36 | /// are we rendering a new mask for the first time ? 37 | new_mask: bool, 38 | /// should the mesh used to render the brush be updated to reflect a change in brush falloff ? 39 | brush_updated: bool, 40 | /// are we currently modifying the mask (cursor is in canvas and one mouse button is pressed) 41 | is_painting: bool, 42 | /// used to compute the brush impact on the mask depending on elapsed time 43 | prev_frame_time: f64, 44 | /// how transparent we want the heightmap to appear on top of the mask 45 | pub heightmap_transparency: f32, 46 | } 47 | 48 | impl PanelMaskEdit { 49 | pub fn new(image_size: usize) -> Self { 50 | PanelMaskEdit { 51 | image_size, 52 | mask: None, 53 | conf: BrushConfig { 54 | value: 0.5, 55 | size: 0.5, 56 | falloff: 0.5, 57 | opacity: 0.5, 58 | }, 59 | mesh_updated: false, 60 | new_mask: true, 61 | is_painting: false, 62 | brush_updated: false, 63 | prev_frame_time: -1.0, 64 | heightmap_transparency: 0.5, 65 | } 66 | } 67 | pub fn get_mask(&self) -> Option> { 68 | self.mask.clone() 69 | } 70 | pub fn display_mask(&mut self, image_size: usize, mask: Option>) { 71 | self.image_size = image_size; 72 | self.mesh_updated = true; 73 | self.new_mask = true; 74 | self.mask = mask.or_else(|| Some(vec![1.0; MASK_SIZE * MASK_SIZE])); 75 | } 76 | pub fn render( 77 | &mut self, 78 | ui: &mut egui::Ui, 79 | heightmap_img: &ColorImage, 80 | ) -> Option { 81 | let mut action = None; 82 | ui.vertical(|ui| { 83 | egui::Frame::dark_canvas(ui.style()).show(ui, |ui| { 84 | self.render_3dview(ui, heightmap_img, self.image_size as u32); 85 | }); 86 | if self.is_painting { 87 | action = Some(Panel2dAction::MaskUpdated); 88 | ui.ctx().request_repaint(); 89 | } else { 90 | self.prev_frame_time = -1.0; 91 | } 92 | ui.label("mouse buttons : left increase, right decrease, middle set brush value"); 93 | ui.horizontal(|ui| { 94 | ui.label("brush size"); 95 | ui.add( 96 | egui::DragValue::new(&mut self.conf.size) 97 | .speed(0.01) 98 | .range(1.0 / (MASK_SIZE as f32)..=1.0), 99 | ); 100 | ui.label("falloff"); 101 | let old_falloff = self.conf.falloff; 102 | ui.add( 103 | egui::DragValue::new(&mut self.conf.falloff) 104 | .speed(0.01) 105 | .range(0.0..=1.0), 106 | ); 107 | ui.label("value"); 108 | ui.add( 109 | egui::DragValue::new(&mut self.conf.value) 110 | .speed(0.01) 111 | .range(0.0..=1.0), 112 | ); 113 | ui.label("opacity"); 114 | ui.add( 115 | egui::DragValue::new(&mut self.conf.opacity) 116 | .speed(0.01) 117 | .range(0.0..=1.0), 118 | ); 119 | // need to update the brush mesh ? 120 | self.brush_updated = old_falloff != self.conf.falloff; 121 | }); 122 | ui.horizontal(|ui| { 123 | ui.label("heightmap opacity"); 124 | ui.add( 125 | egui::DragValue::new(&mut self.heightmap_transparency) 126 | .speed(0.01) 127 | .range(0.0..=1.0), 128 | ); 129 | }); 130 | if ui 131 | .button("Clear mask") 132 | .on_hover_text("Delete this mask") 133 | .clicked() 134 | { 135 | action = Some(Panel2dAction::MaskDelete); 136 | if let Some(ref mut mask) = self.mask { 137 | mask.fill(1.0); 138 | self.mesh_updated = true; 139 | } 140 | } 141 | }); 142 | action 143 | } 144 | fn render_3dview(&mut self, ui: &mut egui::Ui, heightmap_img: &ColorImage, image_size: u32) { 145 | let (rect, response) = ui.allocate_exact_size( 146 | egui::Vec2::splat(self.image_size as f32), 147 | egui::Sense::drag(), 148 | ); 149 | let lbutton = ui.input(|i| i.pointer.button_down(PointerButton::Primary)); 150 | let rbutton = ui.input(|i| i.pointer.button_down(PointerButton::Secondary)); 151 | let mbutton = ui.input(|i| i.pointer.button_down(PointerButton::Middle)); 152 | let mut mouse_pos = ui.input(|i| i.pointer.hover_pos()); 153 | let to_screen = emath::RectTransform::from_to( 154 | Rect::from_min_size(Pos2::ZERO, response.rect.square_proportions()), 155 | response.rect, 156 | ); 157 | let from_screen = to_screen.inverse(); 158 | let mut mesh_updated = self.mesh_updated; 159 | let new_mask = self.new_mask; 160 | let hmap_transp = self.heightmap_transparency; 161 | let heightmap_img = if new_mask { 162 | Some(heightmap_img.clone()) 163 | } else { 164 | None 165 | }; 166 | let brush_updated = self.brush_updated; 167 | let brush_config = self.conf; 168 | let time = if self.prev_frame_time == -1.0 { 169 | self.prev_frame_time = ui.input(|i| i.time); 170 | 0.0 171 | } else { 172 | let t = ui.input(|i| i.time); 173 | let elapsed = t - self.prev_frame_time; 174 | self.prev_frame_time = t; 175 | elapsed 176 | }; 177 | if let Some(pos) = mouse_pos { 178 | // mouse position in canvas from 0.0,0.0 (bottom left) to 1.0,1.0 (top right) 179 | let canvas_pos = from_screen * pos; 180 | mouse_pos = Some(canvas_pos); 181 | self.is_painting = (lbutton || rbutton || mbutton) && in_canvas(canvas_pos); 182 | if self.is_painting && time > 0.0 { 183 | self.update_mask(canvas_pos, lbutton, rbutton, brush_config, time as f32); 184 | mesh_updated = true; 185 | } 186 | } 187 | let mask = if mesh_updated { 188 | self.mask.clone() 189 | } else { 190 | None 191 | }; 192 | let callback = egui::PaintCallback { 193 | rect, 194 | callback: std::sync::Arc::new(egui_glow::CallbackFn::new(move |info, painter| { 195 | with_three_d_context(painter.gl(), |three_d, renderer| { 196 | if new_mask { 197 | if let Some(ref heightmap_img) = heightmap_img { 198 | renderer.set_heightmap(three_d, heightmap_img, image_size); 199 | } 200 | } 201 | if brush_updated { 202 | renderer.update_brush(three_d, brush_config); 203 | } 204 | if mesh_updated { 205 | renderer.update_model(three_d, &mask); 206 | } 207 | renderer.render(three_d, &info, mouse_pos, brush_config, hmap_transp); 208 | }); 209 | })), 210 | }; 211 | ui.painter().add(callback); 212 | self.mesh_updated = false; 213 | self.new_mask = false; 214 | } 215 | 216 | fn update_mask( 217 | &mut self, 218 | canvas_pos: Pos2, 219 | lbutton: bool, 220 | rbutton: bool, 221 | brush_config: BrushConfig, 222 | time: f32, 223 | ) { 224 | if let Some(ref mut mask) = self.mask { 225 | let mx = canvas_pos.x * MASK_SIZE as f32; 226 | let my = canvas_pos.y * MASK_SIZE as f32; 227 | let brush_radius = brush_config.size * MASK_SIZE as f32 * MAX_BRUSH_SIZE; 228 | let falloff_dist = (1.0 - brush_config.falloff) * brush_radius; 229 | let minx = (mx - brush_radius).max(0.0) as usize; 230 | let maxx = ((mx + brush_radius) as usize).min(MASK_SIZE); 231 | let miny = (my - brush_radius).max(0.0) as usize; 232 | let maxy = ((my + brush_radius) as usize).min(MASK_SIZE); 233 | let opacity_factor = 0.5 + brush_config.opacity; 234 | let (target_value, time_coef) = if lbutton { 235 | (0.0, 10.0) 236 | } else if rbutton { 237 | // for some unknown reason, white color is faster than black! 238 | (1.0, 3.0) 239 | } else { 240 | // mbutton 241 | (brush_config.value, 5.0) 242 | }; 243 | let brush_coef = 1.0 / (brush_radius - falloff_dist); 244 | let coef = time * time_coef * opacity_factor; 245 | for y in miny..maxy { 246 | let dy = y as f32 - my; 247 | let yoff = y * MASK_SIZE; 248 | for x in minx..maxx { 249 | let dx = x as f32 - mx; 250 | // distance from brush center 251 | let dist = (dx * dx + dy * dy).sqrt(); 252 | if dist >= brush_radius { 253 | // out of the brush 254 | continue; 255 | } 256 | let alpha = if dist < falloff_dist { 257 | 1.0 258 | } else { 259 | 1.0 - (dist - falloff_dist) * brush_coef 260 | }; 261 | let current_value = mask[x + yoff]; 262 | mask[x + yoff] = current_value + coef * alpha * (target_value - current_value); 263 | } 264 | } 265 | } 266 | } 267 | } 268 | 269 | fn in_canvas(canvas_pos: Pos2) -> bool { 270 | canvas_pos.x >= 0.0 && canvas_pos.x <= 1.0 && canvas_pos.y >= 0.0 && canvas_pos.y <= 1.0 271 | } 272 | 273 | fn with_three_d_context( 274 | gl: &std::sync::Arc, 275 | f: impl FnOnce(&three_d::Context, &mut Renderer) -> R, 276 | ) -> R { 277 | use std::cell::RefCell; 278 | thread_local! { 279 | pub static THREE_D: RefCell> = RefCell::new(None); 280 | } 281 | #[allow(unsafe_code)] 282 | unsafe { 283 | use egui_glow::glow::HasContext as _; 284 | gl.disable(egui_glow::glow::DEPTH_TEST); 285 | gl.enable(egui_glow::glow::BLEND); 286 | if !cfg!(target_arch = "wasm32") { 287 | gl.disable(egui_glow::glow::FRAMEBUFFER_SRGB); 288 | } 289 | } 290 | THREE_D.with(|context| { 291 | let mut context = context.borrow_mut(); 292 | let (three_d, renderer) = context.get_or_insert_with(|| { 293 | let three_d = three_d::Context::from_gl_context(gl.clone()).unwrap(); 294 | let renderer = Renderer::new(&three_d); 295 | (three_d, renderer) 296 | }); 297 | 298 | f(three_d, renderer) 299 | }) 300 | } 301 | pub struct Renderer { 302 | mask_model: Gm, 303 | brush_mesh: CpuMesh, 304 | brush_model: Gm, 305 | heightmap_model: Gm, 306 | mask_mesh: CpuMesh, 307 | material: ColorMaterial, 308 | } 309 | 310 | impl Renderer { 311 | pub fn new(three_d: &three_d::Context) -> Self { 312 | let mut material = ColorMaterial::new( 313 | three_d, 314 | &CpuMaterial { 315 | roughness: 1.0, 316 | metallic: 0.0, 317 | albedo: Srgba::WHITE, 318 | ..Default::default() 319 | }, 320 | ); 321 | material.render_states.cull = Cull::None; 322 | material.render_states.depth_test = DepthTest::Always; 323 | material.render_states.blend = Blend::TRANSPARENCY; 324 | let mask_mesh = build_mask(); 325 | let mask_model = Gm::new(Mesh::new(three_d, &mask_mesh), material.clone()); 326 | let brush_mesh = build_brush(0.5); 327 | let brush_model = Gm::new(Mesh::new(three_d, &brush_mesh), material.clone()); 328 | let heightmap_model = Gm::new(Mesh::new(three_d, &CpuMesh::square()), material.clone()); 329 | Self { 330 | mask_model, 331 | brush_mesh, 332 | brush_model, 333 | mask_mesh, 334 | heightmap_model, 335 | material, 336 | } 337 | } 338 | pub fn update_brush(&mut self, three_d: &three_d::Context, brush_conf: BrushConfig) { 339 | if let Positions::F32(ref mut vertices) = self.brush_mesh.positions { 340 | let inv_fall = 1.0 - brush_conf.falloff; 341 | // update position of inner opaque ring 342 | for i in 0..32 { 343 | let angle = std::f32::consts::PI * 2.0 * (i as f32) / 32.0; 344 | vertices[i + 1] = vec3(angle.cos() * inv_fall, angle.sin() * inv_fall, 0.0); 345 | } 346 | } 347 | self.brush_model = Gm::new(Mesh::new(three_d, &self.brush_mesh), self.material.clone()); 348 | } 349 | pub fn update_model(&mut self, three_d: &three_d::Context, mask: &Option>) { 350 | if let Some(mask) = mask { 351 | if let Some(ref mut colors) = self.mask_mesh.colors { 352 | let mut idx = 0; 353 | for y in 0..MASK_SIZE { 354 | let yoff = (MASK_SIZE - 1 - y) * MASK_SIZE; 355 | for x in 0..MASK_SIZE { 356 | let rgb_val = (mask[yoff + x] * 255.0).clamp(0.0, 255.0) as u8; 357 | colors[idx].r = rgb_val; 358 | colors[idx].g = rgb_val; 359 | colors[idx].b = rgb_val; 360 | idx += 1; 361 | } 362 | } 363 | } 364 | self.mask_model = Gm::new(Mesh::new(three_d, &self.mask_mesh), self.material.clone()); 365 | } 366 | } 367 | pub fn render( 368 | &mut self, 369 | _three_d: &three_d::Context, 370 | info: &egui::PaintCallbackInfo, 371 | mouse_pos: Option, 372 | brush_conf: BrushConfig, 373 | hmap_transp: f32, 374 | ) { 375 | // Set where to paint 376 | let viewport = info.viewport_in_pixels(); 377 | let viewport = Viewport { 378 | x: viewport.left_px, 379 | y: viewport.from_bottom_px, 380 | width: viewport.width_px as _, 381 | height: viewport.height_px as _, 382 | }; 383 | 384 | let target = vec3(0.0, 0.0, 0.0); 385 | let campos = vec3(0.0, 0.0, 1.0); 386 | 387 | let camera = Camera::new_orthographic( 388 | viewport, 389 | campos, 390 | target, 391 | vec3(0.0, 1.0, 0.0), 392 | 10.0, 393 | 0.0, 394 | 1000.0, 395 | ); 396 | 397 | self.mask_model.render(&camera, &[]); 398 | if let Some(mouse_pos) = mouse_pos { 399 | let transfo = Mat4::from_translation(vec3( 400 | mouse_pos.x * 10.0 - 5.0, 401 | 5.0 - mouse_pos.y * 10.0, 402 | 0.1, 403 | )); 404 | let scale = Mat4::from_scale(brush_conf.size * 10.0 * MAX_BRUSH_SIZE); 405 | self.brush_model.set_transformation(transfo * scale); 406 | self.brush_model.render(&camera, &[]); 407 | } 408 | let transfo = Mat4::from_scale(5.0); 409 | self.heightmap_model.set_transformation(transfo); 410 | self.heightmap_model.material.color.a = (hmap_transp * 255.0) as u8; 411 | self.heightmap_model.render(&camera, &[]); 412 | } 413 | 414 | fn set_heightmap( 415 | &mut self, 416 | three_d: &three_d::Context, 417 | heightmap_img: &ColorImage, 418 | image_size: u32, 419 | ) { 420 | self.heightmap_model = build_heightmap(three_d, heightmap_img, image_size); 421 | } 422 | } 423 | 424 | /// build a circular mesh with a double ring : one opaque 32 vertices inner ring and one transparent 64 vertices outer ring 425 | fn build_brush(falloff: f32) -> CpuMesh { 426 | const VERTICES_COUNT: usize = 1 + 32 + 64; 427 | let mut colors = Vec::with_capacity(VERTICES_COUNT); 428 | let mut vertices = Vec::with_capacity(VERTICES_COUNT); 429 | let mut indices = Vec::with_capacity(3 * 32 + 9 * 32); 430 | vertices.push(vec3(0.0, 0.0, 0.0)); 431 | let inv_fall = 1.0 - falloff; 432 | // inner opaque ring 433 | for i in 0..32 { 434 | let angle = std::f32::consts::PI * 2.0 * (i as f32) / 32.0; 435 | vertices.push(vec3(angle.cos() * inv_fall, angle.sin() * inv_fall, 0.0)); 436 | } 437 | // outer transparent ring 438 | for i in 0..64 { 439 | let angle = std::f32::consts::PI * 2.0 * (i as f32) / 64.0; 440 | vertices.push(vec3(angle.cos(), angle.sin(), 0.0)); 441 | } 442 | for _ in 0..33 { 443 | colors.push(Srgba::RED); 444 | } 445 | for _ in 0..64 { 446 | colors.push(Srgba::new(255, 0, 0, 0)); 447 | } 448 | // inner ring 449 | for i in 0..32 { 450 | indices.push(0); 451 | indices.push(1 + i); 452 | indices.push(1 + (1 + i) % 32); 453 | } 454 | // outer ring, 32 vertices inside, 64 vertices outside 455 | for i in 0..32 { 456 | indices.push(1 + i); 457 | indices.push(33 + 2 * i); 458 | indices.push(33 + (2 * i + 1) % 64); 459 | 460 | indices.push(1 + i); 461 | indices.push(1 + (i + 1) % 32); 462 | indices.push(33 + (2 * i + 1) % 64); 463 | 464 | indices.push(1 + (i + 1) % 32); 465 | indices.push(33 + (2 * i + 1) % 64); 466 | indices.push(33 + (2 * i + 2) % 64); 467 | } 468 | CpuMesh { 469 | // name: "brush".to_string(), 470 | positions: Positions::F32(vertices), 471 | indices: Indices::U16(indices), 472 | colors: Some(colors), 473 | ..Default::default() 474 | } 475 | } 476 | 477 | fn build_mask() -> CpuMesh { 478 | let mut vertices = Vec::with_capacity(MASK_SIZE * MASK_SIZE); 479 | let mut indices = Vec::with_capacity(6 * (MASK_SIZE - 1) * (MASK_SIZE - 1)); 480 | let mut colors = Vec::with_capacity(MASK_SIZE * MASK_SIZE); 481 | for y in 0..MASK_SIZE { 482 | let vy = y as f32 / (MASK_SIZE - 1) as f32 * 10.0 - 5.0; 483 | for x in 0..MASK_SIZE { 484 | let vx = x as f32 / (MASK_SIZE - 1) as f32 * 10.0 - 5.0; 485 | vertices.push(three_d::vec3(vx, vy, 0.0)); 486 | colors.push(Srgba::WHITE); 487 | } 488 | } 489 | for y in 0..MASK_SIZE - 1 { 490 | let y_offset = y * MASK_SIZE; 491 | for x in 0..MASK_SIZE - 1 { 492 | let off = x + y_offset; 493 | indices.push((off) as u32); 494 | indices.push((off + MASK_SIZE) as u32); 495 | indices.push((off + 1) as u32); 496 | indices.push((off + MASK_SIZE) as u32); 497 | indices.push((off + MASK_SIZE + 1) as u32); 498 | indices.push((off + 1) as u32); 499 | } 500 | } 501 | CpuMesh { 502 | positions: Positions::F32(vertices), 503 | indices: Indices::U32(indices), 504 | colors: Some(colors), 505 | ..Default::default() 506 | } 507 | } 508 | 509 | /// build a simple textured square to display the heightmap 510 | fn build_heightmap( 511 | three_d: &three_d::Context, 512 | heightmap_img: &ColorImage, 513 | image_size: u32, 514 | ) -> Gm { 515 | let mesh = CpuMesh::square(); 516 | let mut material = ColorMaterial::new( 517 | three_d, 518 | &CpuMaterial { 519 | roughness: 1.0, 520 | metallic: 0.0, 521 | albedo: Srgba::new(255, 255, 255, 128), 522 | albedo_texture: Some(CpuTexture { 523 | width: image_size, 524 | height: image_size, 525 | data: TextureData::RgbaU8( 526 | heightmap_img.pixels.iter().map(Color32::to_array).collect(), 527 | ), 528 | ..Default::default() 529 | }), 530 | ..Default::default() 531 | }, 532 | ); 533 | material.render_states.cull = Cull::None; 534 | material.render_states.depth_test = DepthTest::Always; 535 | material.render_states.blend = Blend::TRANSPARENCY; 536 | Gm::new(Mesh::new(three_d, &mesh), material) 537 | } 538 | -------------------------------------------------------------------------------- /src/panel_3dview.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui::{self, PointerButton}; 2 | use image::EncodableLayout; 3 | use three_d::{ 4 | core::prelude::Srgba, degrees, radians, vec2, vec3, AmbientLight, Camera, ClearState, 5 | CpuMaterial, CpuMesh, CpuTexture, Cull, DirectionalLight, Gm, Indices, InnerSpace, Mat3, Mat4, 6 | Mesh, PhysicalMaterial, Positions, TextureData, Vec3, 7 | }; 8 | 9 | use crate::worldgen::ExportMap; 10 | 11 | const ZSCALE: f32 = 200.0; 12 | const XY_SCALE: f32 = 500.0; 13 | const PANEL3D_SIZE: f32 = 256.0; 14 | const WATER_LEVEL_DELTA: f32 = 3.0; 15 | 16 | #[derive(Default, Clone)] 17 | pub struct MeshData { 18 | size: (usize, usize), 19 | vertices: Vec, 20 | indices: Vec, 21 | normals: Vec, 22 | uv: Vec, 23 | } 24 | 25 | #[derive(Clone, Copy)] 26 | pub struct Panel3dViewConf { 27 | /// camera x and y orbit angles 28 | pub orbit: three_d::Vec2, 29 | /// camera x and y pan distances 30 | pub pan: three_d::Vec2, 31 | /// camera zoom in degrees (y field of view is 90 - zoom) 32 | pub zoom: f32, 33 | /// vertical scale to apply to the heightmap 34 | pub hscale: f32, 35 | /// water plane z position 36 | pub water_level: f32, 37 | /// do we display the water plane ? 38 | pub show_water: bool, 39 | /// do we display the skybox ? 40 | pub show_skybox: bool, 41 | } 42 | 43 | pub struct Panel3dView { 44 | size: f32, 45 | conf: Panel3dViewConf, 46 | mesh_data: MeshData, 47 | mesh_updated: bool, 48 | } 49 | 50 | impl Default for Panel3dView { 51 | fn default() -> Self { 52 | Self { 53 | size: PANEL3D_SIZE, 54 | conf: Panel3dViewConf { 55 | pan: three_d::Vec2::new(0.0, 0.0), 56 | orbit: three_d::Vec2::new(std::f32::consts::FRAC_PI_2, std::f32::consts::FRAC_PI_4), 57 | zoom: 60.0, 58 | hscale: 100.0, 59 | water_level: 40.0, 60 | show_water: true, 61 | show_skybox: true, 62 | }, 63 | mesh_data: Default::default(), 64 | mesh_updated: false, 65 | } 66 | } 67 | } 68 | 69 | impl Panel3dView { 70 | pub fn new(size: f32) -> Self { 71 | Self { 72 | size, 73 | ..Default::default() 74 | } 75 | } 76 | pub fn render(&mut self, ui: &mut egui::Ui) { 77 | ui.vertical(|ui| { 78 | egui::Frame::dark_canvas(ui.style()).show(ui, |ui| { 79 | self.render_3dview(ui); 80 | }); 81 | ui.horizontal(|ui| { 82 | ui.label("Height scale %"); 83 | ui.add( 84 | egui::DragValue::new(&mut self.conf.hscale) 85 | .speed(1.0) 86 | .range(std::ops::RangeInclusive::new(10.0, 200.0)), 87 | ); 88 | }); 89 | ui.horizontal(|ui| { 90 | ui.label("Show water plane"); 91 | let old_show_water = self.conf.show_water; 92 | ui.checkbox(&mut self.conf.show_water, ""); 93 | if old_show_water != self.conf.show_water { 94 | self.update_water_level(self.conf.show_water, self.conf.water_level); 95 | } 96 | ui.label("Water height"); 97 | let old_water_level = self.conf.water_level; 98 | ui.add_enabled( 99 | self.conf.show_water, 100 | egui::DragValue::new(&mut self.conf.water_level) 101 | .speed(0.1) 102 | .range(std::ops::RangeInclusive::new(0.0, 100.0)), 103 | ); 104 | if old_water_level != self.conf.water_level { 105 | self.update_water_level(false, old_water_level); 106 | self.update_water_level(true, self.conf.water_level); 107 | } 108 | ui.label("Show skybox"); 109 | ui.checkbox(&mut self.conf.show_skybox, ""); 110 | }); 111 | }); 112 | } 113 | 114 | pub fn update_water_level(&mut self, show: bool, level: f32) { 115 | let sign = if show { 1.0 } else { -1.0 }; 116 | for v in self.mesh_data.vertices.iter_mut() { 117 | let delta = v.z - level; 118 | if delta > 0.0 { 119 | v.z += sign * WATER_LEVEL_DELTA; 120 | } else { 121 | v.z -= sign * WATER_LEVEL_DELTA; 122 | } 123 | } 124 | self.mesh_updated = true; 125 | } 126 | 127 | pub fn update_mesh(&mut self, hmap: &ExportMap) { 128 | let size = hmap.get_size(); 129 | self.mesh_data.size = size; 130 | self.mesh_data.vertices = Vec::with_capacity(size.0 * size.1); 131 | let grid_size = (XY_SCALE / size.0 as f32, XY_SCALE / size.1 as f32); 132 | let off_x = -0.5 * grid_size.0 * size.0 as f32; 133 | let off_y = -0.5 * grid_size.1 * size.1 as f32; 134 | let (min, max) = hmap.get_min_max(); 135 | let coef = ZSCALE 136 | * if max - min > std::f32::EPSILON { 137 | 1.0 / (max - min) 138 | } else { 139 | 1.0 140 | }; 141 | let ucoef = 1.0 / size.0 as f32; 142 | let vcoef = 1.0 / size.1 as f32; 143 | for y in 0..size.1 { 144 | let vy = y as f32 * grid_size.1 + off_y; 145 | for x in 0..size.0 { 146 | let vx = x as f32 * grid_size.0 + off_x; 147 | let mut vz = hmap.height(x, y); 148 | vz = (vz - min) * coef; 149 | self.mesh_data.vertices.push(three_d::vec3(vx, -vy, vz)); 150 | self.mesh_data 151 | .uv 152 | .push(three_d::vec2(x as f32 * ucoef, y as f32 * vcoef)); 153 | } 154 | } 155 | if self.conf.show_water { 156 | self.update_water_level(true, self.conf.water_level); 157 | } 158 | self.mesh_data.indices = Vec::with_capacity(6 * (size.1 - 1) * (size.0 - 1)); 159 | for y in 0..size.1 - 1 { 160 | let y_offset = y * size.0; 161 | for x in 0..size.0 - 1 { 162 | let off = x + y_offset; 163 | self.mesh_data.indices.push((off) as u32); 164 | self.mesh_data.indices.push((off + size.0) as u32); 165 | self.mesh_data.indices.push((off + 1) as u32); 166 | self.mesh_data.indices.push((off + size.0) as u32); 167 | self.mesh_data.indices.push((off + size.0 + 1) as u32); 168 | self.mesh_data.indices.push((off + 1) as u32); 169 | } 170 | } 171 | let mut cpu_mesh = three_d::CpuMesh { 172 | positions: three_d::Positions::F32(self.mesh_data.vertices.clone()), 173 | indices: three_d::Indices::U32(self.mesh_data.indices.clone()), 174 | ..Default::default() 175 | }; 176 | cpu_mesh.compute_normals(); 177 | self.mesh_data.normals = cpu_mesh.normals.take().unwrap(); 178 | self.mesh_updated = true; 179 | } 180 | 181 | fn render_3dview(&mut self, ui: &mut egui::Ui) { 182 | let (rect, response) = 183 | ui.allocate_exact_size(egui::Vec2::splat(self.size), egui::Sense::drag()); 184 | let lbutton = ui.input(|i| i.pointer.button_down(PointerButton::Primary)); 185 | let rbutton = ui.input(|i| i.pointer.button_down(PointerButton::Secondary)); 186 | let mbutton = ui.input(|i| i.pointer.button_down(PointerButton::Middle)); 187 | if lbutton { 188 | self.conf.orbit[0] += response.drag_delta().x * 0.01; 189 | self.conf.orbit[1] += response.drag_delta().y * 0.01; 190 | self.conf.orbit[1] = self.conf.orbit[1].clamp(0.15, std::f32::consts::FRAC_PI_2 - 0.05); 191 | } else if rbutton { 192 | self.conf.pan[0] += response.drag_delta().x * 0.5; 193 | self.conf.pan[1] += response.drag_delta().y * 0.5; 194 | self.conf.pan[1] = self.conf.pan[1].clamp(0.0, 140.0); 195 | } else if mbutton { 196 | self.conf.zoom += response.drag_delta().y * 0.15; 197 | } 198 | 199 | // Clone locals so we can move them into the paint callback: 200 | let conf = self.conf; 201 | let mesh_updated = self.mesh_updated; 202 | let mesh_data: Option = if mesh_updated { 203 | Some(self.mesh_data.clone()) 204 | } else { 205 | None 206 | }; 207 | 208 | let callback = egui::PaintCallback { 209 | rect, 210 | callback: std::sync::Arc::new(egui_glow::CallbackFn::new(move |info, painter| { 211 | with_three_d_context(painter.gl(), |three_d, renderer| { 212 | if mesh_updated { 213 | renderer.update_model(three_d, &mesh_data); 214 | } 215 | renderer.render( 216 | three_d, 217 | &info, 218 | conf, 219 | FrameInput::new(&three_d, &info, painter), 220 | ); 221 | }); 222 | })), 223 | }; 224 | ui.painter().add(callback); 225 | self.mesh_updated = false; 226 | } 227 | } 228 | /// 229 | /// Translates from egui input to three-d input 230 | /// 231 | pub struct FrameInput<'a> { 232 | screen: three_d::RenderTarget<'a>, 233 | viewport: three_d::Viewport, 234 | scissor_box: three_d::ScissorBox, 235 | } 236 | 237 | impl FrameInput<'_> { 238 | pub fn new( 239 | context: &three_d::Context, 240 | info: &egui::PaintCallbackInfo, 241 | painter: &egui_glow::Painter, 242 | ) -> Self { 243 | use three_d::*; 244 | 245 | // Disable sRGB textures for three-d 246 | #[cfg(not(target_arch = "wasm32"))] 247 | #[allow(unsafe_code)] 248 | unsafe { 249 | use egui_glow::glow::HasContext as _; 250 | context.disable(egui_glow::glow::FRAMEBUFFER_SRGB); 251 | } 252 | 253 | // Constructs a screen render target to render the final image to 254 | let screen = painter.intermediate_fbo().map_or_else( 255 | || { 256 | RenderTarget::screen( 257 | context, 258 | info.viewport.width() as u32, 259 | info.viewport.height() as u32, 260 | ) 261 | }, 262 | |fbo| { 263 | RenderTarget::from_framebuffer( 264 | context, 265 | info.viewport.width() as u32, 266 | info.viewport.height() as u32, 267 | fbo, 268 | ) 269 | }, 270 | ); 271 | 272 | // Set where to paint 273 | let viewport = info.viewport_in_pixels(); 274 | let viewport = Viewport { 275 | x: viewport.left_px, 276 | y: viewport.from_bottom_px, 277 | width: viewport.width_px as u32, 278 | height: viewport.height_px as u32, 279 | }; 280 | 281 | // Respect the egui clip region (e.g. if we are inside an `egui::ScrollArea`). 282 | let clip_rect = info.clip_rect_in_pixels(); 283 | let scissor_box = ScissorBox { 284 | x: clip_rect.left_px, 285 | y: clip_rect.from_bottom_px, 286 | width: clip_rect.width_px as u32, 287 | height: clip_rect.height_px as u32, 288 | }; 289 | Self { 290 | screen, 291 | scissor_box, 292 | viewport, 293 | } 294 | } 295 | } 296 | 297 | fn with_three_d_context( 298 | gl: &std::sync::Arc, 299 | f: impl FnOnce(&three_d::Context, &mut Renderer) -> R, 300 | ) -> R { 301 | use std::cell::RefCell; 302 | thread_local! { 303 | pub static THREE_D: RefCell> = RefCell::new(None); 304 | } 305 | #[allow(unsafe_code)] 306 | unsafe { 307 | use egui_glow::glow::HasContext as _; 308 | gl.enable(egui_glow::glow::DEPTH_TEST); 309 | if !cfg!(target_arch = "wasm32") { 310 | gl.disable(egui_glow::glow::FRAMEBUFFER_SRGB); 311 | } 312 | gl.clear(egui_glow::glow::DEPTH_BUFFER_BIT); 313 | gl.clear_depth_f32(1.0); 314 | gl.depth_func(egui_glow::glow::LESS); 315 | } 316 | THREE_D.with(|context| { 317 | let mut context = context.borrow_mut(); 318 | let (three_d, renderer) = context.get_or_insert_with(|| { 319 | let three_d = three_d::Context::from_gl_context(gl.clone()).unwrap(); 320 | let renderer = Renderer::new(&three_d); 321 | (three_d, renderer) 322 | }); 323 | 324 | f(three_d, renderer) 325 | }) 326 | } 327 | pub struct Renderer { 328 | terrain_mesh: CpuMesh, 329 | terrain_model: Gm, 330 | terrain_material: PhysicalMaterial, 331 | water_model: Gm, 332 | directional: DirectionalLight, 333 | ambient: AmbientLight, 334 | sky: Gm, 335 | } 336 | 337 | impl Renderer { 338 | pub fn new(three_d: &three_d::Context) -> Self { 339 | let terrain_mesh = CpuMesh::square(); 340 | let mut terrain_material = PhysicalMaterial::new_opaque( 341 | three_d, 342 | &CpuMaterial { 343 | roughness: 1.0, 344 | metallic: 0.0, 345 | albedo: Srgba::new_opaque(45, 30, 25), 346 | ..Default::default() 347 | }, 348 | ); 349 | terrain_material.render_states.cull = Cull::Back; 350 | let terrain_model = Gm::new(Mesh::new(three_d, &terrain_mesh), terrain_material.clone()); 351 | let water_model = build_water_plane(three_d); 352 | Self { 353 | terrain_mesh, 354 | terrain_model, 355 | terrain_material, 356 | water_model, 357 | sky: build_sky(three_d), 358 | directional: DirectionalLight::new( 359 | three_d, 360 | 1.5, 361 | Srgba::new_opaque(255, 222, 180), 362 | vec3(-0.5, 0.5, -0.5).normalize(), 363 | ), 364 | ambient: AmbientLight::new(&three_d, 0.5, Srgba::WHITE), 365 | } 366 | } 367 | pub fn update_model(&mut self, three_d: &three_d::Context, mesh_data: &Option) { 368 | if let Some(mesh_data) = mesh_data { 369 | let mut rebuild = false; 370 | if let Positions::F32(ref mut vertices) = self.terrain_mesh.positions { 371 | rebuild = vertices.len() != mesh_data.vertices.len(); 372 | *vertices = mesh_data.vertices.clone(); 373 | } 374 | if rebuild { 375 | self.terrain_mesh.indices = Indices::U32(mesh_data.indices.clone()); 376 | self.terrain_mesh.normals = Some(mesh_data.normals.clone()); 377 | self.terrain_mesh.uvs = Some(mesh_data.uv.clone()); 378 | self.terrain_mesh.tangents = None; 379 | } 380 | self.terrain_model = Gm::new( 381 | Mesh::new(three_d, &self.terrain_mesh), 382 | self.terrain_material.clone(), 383 | ); 384 | } 385 | } 386 | pub fn render( 387 | &mut self, 388 | _three_d: &three_d::Context, 389 | _info: &egui::PaintCallbackInfo, 390 | conf: Panel3dViewConf, 391 | frame_input: FrameInput<'_>, 392 | ) { 393 | // Set where to paint 394 | let viewport = frame_input.viewport; 395 | 396 | let target = vec3(0.0, 0.0, 0.0); 397 | let campos = vec3(XY_SCALE * 2.0, 0.0, 0.0); 398 | 399 | let mut camera = Camera::new_perspective( 400 | viewport, 401 | campos, 402 | target, 403 | vec3(0.0, 0.0, 1.0), 404 | degrees((90.0 - conf.zoom * 0.8).clamp(1.0, 90.0)), 405 | 0.1, 406 | XY_SCALE * 10.0, 407 | ); 408 | 409 | camera.rotate_around_with_fixed_up(target, 0.0, conf.orbit[1]); 410 | 411 | let up = camera.up(); 412 | let right_direction = camera.right_direction(); 413 | camera.translate(conf.pan[1] * up - conf.pan[0] * right_direction); 414 | let camz = camera.position().z; 415 | if camz < conf.water_level + 10.0 { 416 | camera.translate(vec3(0.0, 0.0, conf.water_level + 10.0 - camz)); 417 | } 418 | 419 | let mut transfo = Mat4::from_angle_z(radians(conf.orbit[0] * 2.0)); 420 | transfo.z[2] = conf.hscale / 100.0; 421 | self.terrain_model.set_transformation(transfo); 422 | 423 | let light_transfo = Mat3::from_angle_z(radians(conf.orbit[0] * 2.0)); 424 | self.directional.direction = light_transfo * vec3(-0.5, 0.5, -0.5); 425 | self.directional 426 | .generate_shadow_map(1024, &[&self.terrain_model]); 427 | // Get the screen render target to be able to render something on the screen 428 | frame_input 429 | .screen 430 | // Clear the color and depth of the screen render target 431 | .clear_partially(frame_input.scissor_box, ClearState::depth(1.0)); 432 | frame_input.screen.render_partially( 433 | frame_input.scissor_box, 434 | &camera, 435 | &[&self.terrain_model], 436 | &[&self.ambient, &self.directional], 437 | ); 438 | 439 | if conf.show_water { 440 | let mut water_transfo = 441 | Mat4::from_translation(Vec3::new(0.0, 0.0, conf.water_level * conf.hscale * 0.01)); 442 | water_transfo.x[0] = XY_SCALE * 10.0; 443 | water_transfo.y[1] = XY_SCALE * 10.0; 444 | self.water_model.set_transformation(water_transfo); 445 | 446 | frame_input.screen.render_partially( 447 | frame_input.scissor_box, 448 | &camera, 449 | &[&self.water_model], 450 | &[&self.ambient, &self.directional], 451 | ); 452 | } 453 | if conf.show_skybox { 454 | let transfo = Mat4::from_angle_z(radians(conf.orbit[0] * 2.0)); 455 | self.sky.set_transformation(transfo); 456 | frame_input.screen.render_partially( 457 | frame_input.scissor_box, 458 | &camera, 459 | &[&self.sky], 460 | &[], 461 | ); 462 | } 463 | 464 | frame_input.screen.into_framebuffer(); // Take back the screen fbo, we will continue to use it. 465 | } 466 | } 467 | 468 | const SKY_BYTES: &[u8] = include_bytes!("../sky.jpg"); 469 | 470 | fn build_sky(three_d: &three_d::Context) -> Gm { 471 | let img = image::load_from_memory(SKY_BYTES).unwrap(); 472 | let buffer = img.as_rgb8().unwrap().as_bytes(); 473 | let mut data = Vec::new(); 474 | let mut i = 0; 475 | while i < (img.height() * img.width() * 3) as usize { 476 | let r = buffer[i]; 477 | let g = buffer[i + 1]; 478 | let b = buffer[i + 2]; 479 | i += 3; 480 | data.push([r, g, b]); 481 | } 482 | const SUBDIV: u32 = 32; 483 | let mut sky2 = uv_wrapping_cylinder(SUBDIV); 484 | sky2.transform(Mat4::from_nonuniform_scale( 485 | ZSCALE * 5.0, 486 | XY_SCALE * 2.0, 487 | XY_SCALE * 2.0, 488 | )) 489 | .unwrap(); 490 | sky2.transform(Mat4::from_angle_y(degrees(-90.0))).unwrap(); 491 | sky2.transform(Mat4::from_angle_z(degrees(90.0))).unwrap(); 492 | let mut sky_material = PhysicalMaterial::new_opaque( 493 | three_d, 494 | &CpuMaterial { 495 | roughness: 1.0, 496 | metallic: 0.0, 497 | emissive: Srgba::WHITE, 498 | emissive_texture: Some(CpuTexture { 499 | width: img.width(), 500 | height: img.height(), 501 | data: TextureData::RgbU8(data), 502 | ..Default::default() 503 | }), 504 | ..Default::default() 505 | }, 506 | ); 507 | // water_material.render_states.depth_test = DepthTest::Greater; 508 | sky_material.render_states.cull = Cull::Front; 509 | Gm::new(Mesh::new(three_d, &sky2), sky_material) 510 | } 511 | fn build_water_plane(three_d: &three_d::Context) -> Gm { 512 | let water_mesh = CpuMesh::square(); 513 | 514 | let mut water_material = PhysicalMaterial::new_opaque( 515 | three_d, 516 | &CpuMaterial { 517 | roughness: 0.1, 518 | metallic: 0.2, 519 | albedo: Srgba::new_opaque(50, 60, 150), 520 | ..Default::default() 521 | }, 522 | ); 523 | // water_material.render_states.depth_test = DepthTest::Greater; 524 | water_material.render_states.cull = Cull::Back; 525 | Gm::new(Mesh::new(three_d, &water_mesh), water_material) 526 | } 527 | fn uv_wrapping_cylinder(angle_subdivisions: u32) -> CpuMesh { 528 | let length_subdivisions = 1; 529 | let mut positions = Vec::new(); 530 | let mut indices = Vec::new(); 531 | for i in 0..length_subdivisions + 1 { 532 | let x = i as f32 / length_subdivisions as f32; 533 | for j in 0..angle_subdivisions + 1 { 534 | let angle = 2.0 * std::f32::consts::PI * j as f32 / angle_subdivisions as f32; 535 | 536 | positions.push(vec3(x, angle.cos(), angle.sin())); 537 | } 538 | } 539 | for i in 0..length_subdivisions { 540 | for j in 0..angle_subdivisions { 541 | indices.push((i * (angle_subdivisions + 1) + j) as u16); 542 | indices.push((i * (angle_subdivisions + 1) + (j + 1)) as u16); 543 | indices.push(((i + 1) * (angle_subdivisions + 1) + (j + 1)) as u16); 544 | 545 | indices.push((i * (angle_subdivisions + 1) + j) as u16); 546 | indices.push(((i + 1) * (angle_subdivisions + 1) + (j + 1)) as u16); 547 | indices.push(((i + 1) * (angle_subdivisions + 1) + j) as u16); 548 | } 549 | } 550 | let mut uvs = Vec::new(); 551 | for i in 0..angle_subdivisions + 1 { 552 | let u = i as f32 / angle_subdivisions as f32; 553 | uvs.push(vec2(u, 1.0)); 554 | } 555 | for i in 0..angle_subdivisions + 1 { 556 | let u = i as f32 / angle_subdivisions as f32; 557 | uvs.push(vec2(u, 0.0)); 558 | } 559 | let mut mesh = CpuMesh { 560 | // name: "cylinder".to_string(), 561 | positions: Positions::F32(positions), 562 | indices: Indices::U16(indices), 563 | uvs: Some(uvs), 564 | ..Default::default() 565 | }; 566 | mesh.compute_normals(); 567 | mesh 568 | } 569 | --------------------------------------------------------------------------------