├── Cargo.toml ├── README.md ├── docs ├── index.html └── macroquad-forestfire.wasm └── src └── main.rs /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "macroquad-forestfire" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [profile.dev.package.'*'] 9 | opt-level = 3 10 | 11 | [dependencies] 12 | macroquad = "0.4" 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # macroquad-forestfire 2 | Forest Fire Model, SOC cellular automaton 3 | 4 | Space or 2-touch for controls 5 | 6 | q to quit 7 | 8 | left click or drag to start fires 9 | 10 | **[WASM Demo](https://gerstacker.github.io/macroquad-forestfire/)** 11 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Forest Fires: Q to quit, Space for controls 9 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /docs/macroquad-forestfire.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gerstacker/macroquad-forestfire/71982d7c16eed17149fc0d1f253ba0f92c76647a/docs/macroquad-forestfire.wasm -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use macroquad::prelude::*; 2 | 3 | use macroquad::ui::{hash, root_ui, widgets}; 4 | use std::process::exit; 5 | 6 | struct DebounceToggle bool>(F, usize); 7 | 8 | impl bool> DebounceToggle { 9 | fn new(f: F) -> DebounceToggle { 10 | DebounceToggle(f, 0) 11 | } 12 | fn get(&mut self) -> bool { 13 | let DebounceToggle(f, ref mut state) = self; 14 | 15 | *state = match (*state, f()) { 16 | (0, true) => 1, 17 | (1, false) => 2, 18 | (2, true) => 3, 19 | (3, false) => 0, 20 | (_, _) => *state, 21 | }; 22 | 23 | *state == 2 24 | } 25 | } 26 | 27 | struct PoissonProcess(f32); 28 | 29 | impl PoissonProcess { 30 | fn new() -> PoissonProcess { 31 | PoissonProcess(0.0) 32 | } 33 | fn draw(&mut self, avgper: f32) -> usize { 34 | let PoissonProcess(ref mut acc) = self; 35 | 36 | let ur = ((1.0 + rand::rand() as f64) / u32::MAX as f64) as f32; 37 | let er = -avgper * ur.ln(); 38 | let newacc = *acc + er; 39 | let faf = newacc.floor(); 40 | *acc = newacc - faf; 41 | faf as usize 42 | } 43 | } 44 | 45 | fn rand_range_usize(low: usize, high: usize) -> usize { 46 | let r = rand::rand() as f64 / (u32::MAX as f64 + 1f64); 47 | return low + (r * (high - low) as f64).floor() as usize; 48 | } 49 | 50 | struct Fire(usize, usize, usize); 51 | 52 | struct CellField { 53 | arr: Vec, 54 | ystride: usize, 55 | } 56 | 57 | impl CellField { 58 | fn new(w: usize, h: usize) -> CellField { 59 | let nx = (w + 7) / 8; 60 | let ny = (h + 7) / 8; 61 | CellField { 62 | arr: vec![0; nx * ny], 63 | ystride: nx, 64 | } 65 | } 66 | fn indices(&self, x: usize, y: usize) -> (usize, usize) { 67 | let (ox, ix) = (x / 8, x % 8); 68 | let (oy, iy) = (y / 8, y % 8); 69 | let s = iy * 8 + ix; 70 | return (oy * self.ystride + ox, s); 71 | } 72 | fn get(&self, x: usize, y: usize) -> bool { 73 | let (off, s) = self.indices(x, y); 74 | return (self.arr[off] & (1 << s)) != 0; 75 | } 76 | fn set(&mut self, x: usize, y: usize) { 77 | let (off, s) = self.indices(x, y); 78 | self.arr[off] |= 1 << s; 79 | } 80 | fn clr(&mut self, x: usize, y: usize) { 81 | let (off, s) = self.indices(x, y); 82 | self.arr[off] &= !(1 << s); 83 | } 84 | } 85 | 86 | fn conf() -> Conf { 87 | Conf { 88 | window_title: String::from("Forest Fires: or double touch for controls"), 89 | high_dpi: false, 90 | ..Default::default() 91 | } 92 | } 93 | 94 | #[macroquad::main(conf)] 95 | async fn main() { 96 | let fireprob: f32 = 1e-7; 97 | let treeprob: f32 = 1e-4; 98 | 99 | let mut logfireprob: f32 = fireprob.log10(); 100 | let mut logtreeprob: f32 = treeprob.log10(); 101 | let mut colorspeed: f32 = 5.; 102 | let mut firemaxage: f32 = 12.; 103 | let mut eightconn: bool = true; 104 | 105 | let w = screen_width() as usize; 106 | let h = screen_height() as usize; 107 | 108 | let mut cellfield = CellField::new(w, h); 109 | let mut fires: Vec = Vec::new(); 110 | 111 | let mut image = Image::gen_image_color(w as u16, h as u16, BLACK); 112 | 113 | let alive_color = Color::new(0.0, 0.5, 0.0, 1.0); 114 | 115 | for y in 0..h { 116 | for x in 0..w { 117 | if rand_range_usize(0, 4 as usize) == 0 { 118 | cellfield.set(x, y); 119 | image.set_pixel(x as u32, y as u32, alive_color); 120 | } 121 | } 122 | } 123 | let texture = Texture2D::from_image(&image); 124 | 125 | let ngh: [[i32; 2]; 8] = [ 126 | [-1, 0], 127 | [1, 0], 128 | [0, -1], 129 | [0, 1], 130 | [-1, -1], 131 | [-1, 1], 132 | [1, -1], 133 | [1, 1], 134 | ]; 135 | 136 | let mut frno: usize = 0; 137 | 138 | let mut showpopup = DebounceToggle::new(|| is_key_down(KeyCode::Space) || touches().len() == 2); 139 | let mut recording: bool = false; 140 | let mut rfrm: usize = 0; 141 | let mut recskip: f32 = 1.; 142 | 143 | let mut colorphase: f32 = 0.; 144 | 145 | let mut fireproc = PoissonProcess::new(); 146 | let mut treeproc = PoissonProcess::new(); 147 | 148 | simulate_mouse_with_touch(false); 149 | 150 | loop { 151 | clear_background(BLACK); 152 | 153 | if is_key_down(KeyCode::Q) { 154 | exit(0); 155 | } 156 | 157 | if showpopup.get() { 158 | widgets::Window::new(hash!(), vec2(100., 100.), vec2(300., 200.)) 159 | .label(&format!("Step {}", frno)) 160 | .ui(&mut *root_ui(), |ui| { 161 | ui.slider(hash!(), "logfireprob", -10f32..-5f32, &mut logfireprob); 162 | ui.slider(hash!(), "logtreeprob", -10f32..-2f32, &mut logtreeprob); 163 | ui.slider(hash!(), "colorspeed", 0f32..10f32, &mut colorspeed); 164 | ui.slider(hash!(), "firemaxage", 0f32..20f32, &mut firemaxage); 165 | ui.checkbox(hash!(), "8-connected", &mut eightconn); 166 | 167 | ui.tree_node(hash!(), "Save PNG", |ui| { 168 | let btext: String = match recording { 169 | false => "Start Recording".to_string(), 170 | true => format!("Recording {}", rfrm).to_string(), 171 | }; 172 | if ui.button(None, btext) { 173 | rfrm = 0; 174 | recording = !recording; 175 | } 176 | ui.slider(hash!(), "recskip", 1f32..10f32, &mut recskip); 177 | }); 178 | }); 179 | } 180 | 181 | let w = image.width(); 182 | let h = image.height(); 183 | let mut numngh: usize = 4; 184 | if eightconn { 185 | numngh = 8; 186 | } 187 | 188 | let mut newfires: Vec = Vec::new(); 189 | 190 | // propagate new fires, age out old fires 191 | for Fire(x, y, age) in &fires { 192 | if *age < firemaxage.floor() as usize { 193 | newfires.push(Fire(*x, *y, *age + 1)); 194 | } else { 195 | image.set_pixel(*x as u32, *y as u32, BLACK); 196 | } 197 | for j in 0..numngh { 198 | let nx = *x as i32 + ngh[j][0]; 199 | let ny = *y as i32 + ngh[j][1]; 200 | if nx >= 0 && nx < w as i32 && ny >= 0 && ny < h as i32 { 201 | let cx = nx as usize; 202 | let cy = ny as usize; 203 | if cellfield.get(cx, cy) { 204 | newfires.push(Fire(cx, cy, 0)); 205 | cellfield.clr(cx, cy); 206 | } 207 | } 208 | } 209 | } 210 | 211 | // spontaneous fires 212 | for _ in 0..fireproc.draw(10f32.powf(logfireprob) * h as f32 * w as f32) { 213 | newfires.push(Fire(rand_range_usize(0, w), rand_range_usize(0, h), 0)); 214 | } 215 | 216 | if is_mouse_button_down(MouseButton::Left) { 217 | let (mouse_x, mouse_y) = mouse_position(); 218 | let mx = clamp(mouse_x as usize, 0, w - 1); 219 | let my = clamp(mouse_y as usize, 0, h - 1); 220 | newfires.push(Fire(mx, my, 0)); 221 | } 222 | 223 | if touches().len() == 1 { 224 | let touchpos = touches()[0].position; 225 | 226 | let mx = clamp(touchpos.x as usize, 0, w - 1); 227 | let my = clamp(touchpos.y as usize, 0, h - 1); 228 | newfires.push(Fire(mx, my, 0)); 229 | } 230 | 231 | // new trees 232 | colorphase += colorspeed * 6.28 / 10000.; 233 | let g = colorphase.cos().abs(); 234 | let b = colorphase.sin().abs(); 235 | for _ in 0..treeproc.draw(10f32.powf(logtreeprob) * h as f32 * w as f32) { 236 | let x = rand_range_usize(0, w); 237 | let y = rand_range_usize(0, h); 238 | if !cellfield.get(x, y) { 239 | image.set_pixel(x as u32, y as u32, Color::new(0.0, g, b, 1.0)); 240 | } 241 | cellfield.set(x, y); 242 | } 243 | 244 | for Fire(x, y, age) in &newfires { 245 | let grn: f32 = *age as f32 / firemaxage; 246 | image.set_pixel(*x as u32, *y as u32, Color::new(1., grn, 0., 1.0)); 247 | } 248 | 249 | if false { 250 | newfires.sort_by(|Fire(x1, y1, _), Fire(x2, y2, _)| { 251 | cellfield 252 | .indices(*x2, *y2) 253 | .0 254 | .cmp(&cellfield.indices(*x1, *y1).0) 255 | }); 256 | } 257 | 258 | fires = newfires; 259 | 260 | texture.update(&image); 261 | 262 | draw_texture(&texture, 0., 0., WHITE); 263 | 264 | if recording && frno % recskip.floor() as usize == 0 { 265 | image.export_png(format!("frm{:05}.png", rfrm).as_str()); 266 | rfrm += 1; 267 | } 268 | 269 | frno = frno + 1; 270 | next_frame().await 271 | } 272 | } 273 | --------------------------------------------------------------------------------