├── 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 |
--------------------------------------------------------------------------------