| ... | ... | @@ -0,0 +1,360 @@ |
| 1 | //! Snowflakes grown by Reiter's hexagonal automaton ("A local cellular model for snow |
| 2 | //! crystal growth", 2005), painted through Snowbound's draw crate. |
| 3 | //! |
| 4 | //! `cargo run --release -p draw --example snowflakes -- [first seed] [out dir]` writes |
| 5 | //! `gallery.png`, twelve flakes from consecutive seeds, and `growing.gif`, the first one freezing. |
| 6 | |
| 7 | use std::{f32::consts::PI, fmt::Write as _, path::PathBuf, time::Duration}; |
| 8 | |
| 9 | use draw::{Layer, PathStyle, Primitive, Renderer, srgb}; |
| 10 | |
| 11 | /// Hex cells from the centre to the edge of the grid. |
| 12 | const RADIUS: i32 = 110; |
| 13 | const SIDE: usize = 2 * RADIUS as usize + 1; |
| 14 | const NEIGHBOURS: [(i32, i32); 6] = [(1, 0), (-1, 0), (0, 1), (0, -1), (1, -1), (-1, 1)]; |
| 15 | const TILE: u32 = 512; |
| 16 | const BACKGROUND: [u8; 3] = [9, 16, 36]; |
| 17 | |
| 18 | struct Weather { |
| 19 | /// How quickly vapour spreads. |
| 20 | alpha: f32, |
| 21 | /// Vapour everywhere at the start, and at the grid's edge always. |
| 22 | beta: f32, |
| 23 | /// Vapour condensing onto the crystal each step. |
| 24 | gamma: f32, |
| 25 | } |
| 26 | |
| 27 | impl Weather { |
| 28 | fn from_seed(seed: u64) -> Self { |
| 29 | let mut state = seed.wrapping_mul(0x9e37_79b9_7f4a_7c15) ^ 0x5eed; |
| 30 | let mut next = || { |
| 31 | state = state.wrapping_add(0x9e37_79b9_7f4a_7c15); |
| 32 | let mut z = state; |
| 33 | z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9); |
| 34 | z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb); |
| 35 | ((z ^ (z >> 31)) >> 40) as f32 / (1u64 << 24) as f32 |
| 36 | }; |
| 37 | Self { |
| 38 | alpha: 0.7 + 1.6 * next(), |
| 39 | beta: 0.3 + 0.6 * next(), |
| 40 | gamma: if next() < 0.2 { |
| 41 | 0.0 |
| 42 | } else { |
| 43 | 10f32.powf(-4.0 + 1.6 * next()) |
| 44 | }, |
| 45 | } |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | /// Axial cell `(q, r)` with every `|q|`, `|r|`, `|q + r|` within `RADIUS`. |
| 50 | fn index(q: i32, r: i32) -> Option<usize> { |
| 51 | (q.abs() <= RADIUS && r.abs() <= RADIUS && (q + r).abs() <= RADIUS) |
| 52 | .then(|| (q + RADIUS) as usize * SIDE + (r + RADIUS) as usize) |
| 53 | } |
| 54 | |
| 55 | fn cells() -> impl Iterator<Item = (i32, i32, usize)> { |
| 56 | (-RADIUS..=RADIUS) |
| 57 | .flat_map(|q| (-RADIUS..=RADIUS).map(move |r| (q, r))) |
| 58 | .filter_map(|(q, r)| index(q, r).map(|i| (q, r, i))) |
| 59 | } |
| 60 | |
| 61 | /// Water at each cell; a cell holding 1 or more is ice. |
| 62 | type Field = Vec<f32>; |
| 63 | |
| 64 | /// Grows a flake until its arms near the grid's edge, offering `frame` each step. |
| 65 | fn grow(weather: &Weather, mut frame: impl FnMut(&Field)) -> Field { |
| 66 | let Weather { alpha, beta, gamma } = *weather; |
| 67 | let mut water = vec![beta; SIDE * SIDE]; |
| 68 | water[index(0, 0).unwrap()] = 1.0; |
| 69 | let mut diffusing = vec![0.0; SIDE * SIDE]; |
| 70 | let mut spread = diffusing.clone(); |
| 71 | let mut held = diffusing.clone(); |
| 72 | let neighbours = |q: i32, r: i32| NEIGHBOURS.map(|(dq, dr)| index(q + dq, r + dr)); |
| 73 | for _ in 0..60_000 { |
| 74 | for (q, r, i) in cells() { |
| 75 | let receptive = |
| 76 | water[i] >= 1.0 || neighbours(q, r).iter().flatten().any(|&n| water[n] >= 1.0); |
| 77 | (diffusing[i], held[i]) = if receptive { |
| 78 | (0.0, water[i] + gamma) |
| 79 | } else { |
| 80 | (water[i], 0.0) |
| 81 | }; |
| 82 | } |
| 83 | let mut reach = 0; |
| 84 | for (q, r, i) in cells() { |
| 85 | let around: f32 = neighbours(q, r) |
| 86 | .iter() |
| 87 | .map(|n| n.map_or(beta, |n| diffusing[n])) |
| 88 | .sum(); |
| 89 | spread[i] = diffusing[i] + alpha / 12.0 * (around - 6.0 * diffusing[i]); |
| 90 | water[i] = spread[i] + held[i]; |
| 91 | if water[i] >= 1.0 { |
| 92 | reach = reach.max(q.abs().max(r.abs()).max((q + r).abs())); |
| 93 | } |
| 94 | } |
| 95 | frame(&water); |
| 96 | if reach >= RADIUS - 6 { |
| 97 | break; |
| 98 | } |
| 99 | } |
| 100 | water |
| 101 | } |
| 102 | |
| 103 | /// The farthest ice from the centre, in cells across. |
| 104 | fn extent(water: &Field) -> f32 { |
| 105 | cells() |
| 106 | .filter(|&(_, _, i)| water[i] >= 1.0) |
| 107 | .map(|(q, r, _)| { |
| 108 | 3f32.sqrt() * ((q as f32 + r as f32 / 2.0).powi(2) + (0.75 * (r * r) as f32)).sqrt() |
| 109 | }) |
| 110 | .fold(1.0, f32::max) |
| 111 | } |
| 112 | |
| 113 | /// The flake as stacked translucent layers: a glow, then ice thickening toward white. |
| 114 | fn paint( |
| 115 | water: &Field, |
| 116 | reach: f32, |
| 117 | centre: [f32; 2], |
| 118 | size: f32, |
| 119 | ) -> Vec<(String, PathStyle, [[f32; 4]; 2])> { |
| 120 | let hexagon = |q: i32, r: i32, path: &mut String| { |
| 121 | let x = centre[0] + size * 3f32.sqrt() * (q as f32 + r as f32 / 2.0); |
| 122 | let y = centre[1] + size * 1.5 * r as f32; |
| 123 | for corner in 0..6 { |
| 124 | let angle = PI / 3.0 * corner as f32 + PI / 6.0; |
| 125 | let command = if corner == 0 { 'M' } else { 'L' }; |
| 126 | let _ = write!( |
| 127 | path, |
| 128 | "{command}{:.2} {:.2}", |
| 129 | x + size * angle.cos(), |
| 130 | y + size * angle.sin() |
| 131 | ); |
| 132 | } |
| 133 | path.push('Z'); |
| 134 | }; |
| 135 | let ice: Vec<_> = cells().filter(|&(_, _, i)| water[i] >= 1.0).collect(); |
| 136 | let heaviest = ice.iter().map(|&(_, _, i)| water[i]).fold(1.0, f32::max); |
| 137 | let thickness = |i: usize| ((water[i] - 1.0) / (heaviest - 1.0).max(1e-6)).sqrt(); |
| 138 | const BANDS: usize = 7; |
| 139 | let mut layers = Vec::new(); |
| 140 | let mut all = String::new(); |
| 141 | ice.iter().for_each(|&(q, r, _)| hexagon(q, r, &mut all)); |
| 142 | let glow = [0.25, 0.55, 1.0, 0.45]; |
| 143 | layers.push(( |
| 144 | all.clone(), |
| 145 | PathStyle::Shadow(size * reach * 0.04 + 6.0), |
| 146 | [glow, glow], |
| 147 | )); |
| 148 | layers.push(( |
| 149 | all, |
| 150 | PathStyle::Fill, |
| 151 | [[0.32, 0.55, 0.95, 0.55], [0.18, 0.35, 0.80, 0.55]], |
| 152 | )); |
| 153 | for band in 1..BANDS { |
| 154 | let mut path = String::new(); |
| 155 | for &(q, r, i) in &ice { |
| 156 | if thickness(i) * BANDS as f32 >= band as f32 { |
| 157 | hexagon(q, r, &mut path); |
| 158 | } |
| 159 | } |
| 160 | if !path.is_empty() { |
| 161 | layers.push(( |
| 162 | path, |
| 163 | PathStyle::Fill, |
| 164 | [[0.95, 0.98, 1.0, 0.24], [0.80, 0.90, 1.0, 0.20]], |
| 165 | )); |
| 166 | } |
| 167 | } |
| 168 | layers |
| 169 | } |
| 170 | |
| 171 | struct Gpu { |
| 172 | renderer: Renderer, |
| 173 | texture: wgpu::Texture, |
| 174 | readback: wgpu::Buffer, |
| 175 | } |
| 176 | |
| 177 | impl Gpu { |
| 178 | fn new() -> Self { |
| 179 | let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle()); |
| 180 | let adapter = |
| 181 | pollster::block_on(instance.request_adapter(&Default::default())).expect("a GPU"); |
| 182 | let (device, queue) = |
| 183 | pollster::block_on(adapter.request_device(&Default::default())).expect("a device"); |
| 184 | let format = wgpu::TextureFormat::Rgba8UnormSrgb; |
| 185 | let texture = device.create_texture(&wgpu::TextureDescriptor { |
| 186 | label: Some("Flake"), |
| 187 | size: wgpu::Extent3d { |
| 188 | width: TILE, |
| 189 | height: TILE, |
| 190 | depth_or_array_layers: 1, |
| 191 | }, |
| 192 | mip_level_count: 1, |
| 193 | sample_count: 1, |
| 194 | dimension: wgpu::TextureDimension::D2, |
| 195 | format, |
| 196 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, |
| 197 | view_formats: &[], |
| 198 | }); |
| 199 | let readback = device.create_buffer(&wgpu::BufferDescriptor { |
| 200 | label: Some("Flake readback"), |
| 201 | size: u64::from(TILE * TILE * 4), |
| 202 | usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, |
| 203 | mapped_at_creation: false, |
| 204 | }); |
| 205 | Self { |
| 206 | renderer: Renderer::new(device, queue, format), |
| 207 | texture, |
| 208 | readback, |
| 209 | } |
| 210 | } |
| 211 | |
| 212 | /// One tile of sRGB RGBA rows. |
| 213 | fn render(&mut self, water: &Field, reach: f32) -> image::RgbaImage { |
| 214 | let size = TILE as f32 * 0.46 / reach; |
| 215 | let centre = [TILE as f32 / 2.0; 2]; |
| 216 | let layers = paint(water, reach, centre, size); |
| 217 | let primitives: Vec<_> = layers |
| 218 | .iter() |
| 219 | .map(|(data, style, colors)| Primitive::Path { |
| 220 | data, |
| 221 | origin: [0.0; 2], |
| 222 | style: *style, |
| 223 | colors: *colors, |
| 224 | }) |
| 225 | .collect(); |
| 226 | let [red, green, blue] = BACKGROUND; |
| 227 | self.renderer.clear_glyph_cache(); |
| 228 | self.renderer |
| 229 | .draw( |
| 230 | &draw::Target::from(self.texture.create_view(&Default::default())), |
| 231 | [TILE; 2], |
| 232 | srgb(red, green, blue), |
| 233 | &[Layer { |
| 234 | scale: 1.0, |
| 235 | origin: [0.0; 2], |
| 236 | clip: None, |
| 237 | backdrop: None, |
| 238 | round: None, |
| 239 | motion: None, |
| 240 | primitives: &primitives, |
| 241 | }], |
| 242 | ) |
| 243 | .expect("a frame"); |
| 244 | let device = self.renderer.device(); |
| 245 | let mut encoder = device.create_command_encoder(&Default::default()); |
| 246 | encoder.copy_texture_to_buffer( |
| 247 | self.texture.as_image_copy(), |
| 248 | wgpu::TexelCopyBufferInfo { |
| 249 | buffer: &self.readback, |
| 250 | layout: wgpu::TexelCopyBufferLayout { |
| 251 | offset: 0, |
| 252 | bytes_per_row: Some(TILE * 4), |
| 253 | rows_per_image: Some(TILE), |
| 254 | }, |
| 255 | }, |
| 256 | wgpu::Extent3d { |
| 257 | width: TILE, |
| 258 | height: TILE, |
| 259 | depth_or_array_layers: 1, |
| 260 | }, |
| 261 | ); |
| 262 | self.renderer.queue().submit([encoder.finish()]); |
| 263 | let (sender, receiver) = std::sync::mpsc::channel(); |
| 264 | self.readback |
| 265 | .map_async(wgpu::MapMode::Read, .., move |result| { |
| 266 | sender.send(result).unwrap() |
| 267 | }); |
| 268 | device |
| 269 | .poll(wgpu::PollType::Wait { |
| 270 | submission_index: None, |
| 271 | timeout: Some(Duration::from_secs(10)), |
| 272 | }) |
| 273 | .unwrap(); |
| 274 | receiver |
| 275 | .recv_timeout(Duration::from_secs(10)) |
| 276 | .unwrap() |
| 277 | .unwrap(); |
| 278 | let pixels = self.readback.get_mapped_range(..).unwrap().to_vec(); |
| 279 | self.readback.unmap(); |
| 280 | image::RgbaImage::from_raw(TILE, TILE, pixels).unwrap() |
| 281 | } |
| 282 | } |
| 283 | |
| 284 | fn main() { |
| 285 | let mut args = std::env::args().skip(1); |
| 286 | let first: u64 = args |
| 287 | .next() |
| 288 | .map_or(1, |seed| seed.parse().expect("a numeric seed")); |
| 289 | let out = PathBuf::from(args.next().unwrap_or_else(|| ".".into())); |
| 290 | std::fs::create_dir_all(&out).unwrap(); |
| 291 | |
| 292 | let seeds: Vec<u64> = (first..first + 12).collect(); |
| 293 | let mut growing = Vec::new(); |
| 294 | let flakes: Vec<Field> = std::thread::scope(|scope| { |
| 295 | let handles: Vec<_> = seeds |
| 296 | .iter() |
| 297 | .map(|&seed| scope.spawn(move || grow(&Weather::from_seed(seed), |_| {}))) |
| 298 | .collect(); |
| 299 | // Snapshots at a widening interval, halved whenever they pass 400. |
| 300 | let (mut step, mut every) = (0, 1); |
| 301 | let shown = grow(&Weather::from_seed(first), |water| { |
| 302 | if step % every == 0 { |
| 303 | growing.push(water.clone()); |
| 304 | if growing.len() > 400 { |
| 305 | growing = growing.iter().step_by(2).cloned().collect(); |
| 306 | every *= 2; |
| 307 | } |
| 308 | } |
| 309 | step += 1; |
| 310 | }); |
| 311 | growing.push(shown.clone()); |
| 312 | let mut flakes: Vec<_> = handles |
| 313 | .into_iter() |
| 314 | .map(|handle| handle.join().unwrap()) |
| 315 | .collect(); |
| 316 | flakes[0] = shown; |
| 317 | flakes |
| 318 | }); |
| 319 | |
| 320 | let mut gpu = Gpu::new(); |
| 321 | let mut gallery = image::RgbaImage::new(TILE * 4, TILE * 3); |
| 322 | for (n, (water, seed)) in flakes.iter().zip(&seeds).enumerate() { |
| 323 | let Weather { alpha, beta, gamma } = Weather::from_seed(*seed); |
| 324 | println!("seed {seed}: alpha {alpha:.2} beta {beta:.3} gamma {gamma:.5}"); |
| 325 | let tile = gpu.render(water, extent(water)); |
| 326 | image::imageops::overlay( |
| 327 | &mut gallery, |
| 328 | &tile, |
| 329 | i64::from(n as u32 % 4 * TILE), |
| 330 | i64::from(n as u32 / 4 * TILE), |
| 331 | ); |
| 332 | } |
| 333 | gallery.save(out.join("gallery.png")).unwrap(); |
| 334 | |
| 335 | // Ninety frames spread over the growth, the finished flake held at the end. |
| 336 | let reach = extent(growing.last().unwrap()); |
| 337 | let picks: Vec<usize> = (0..90).map(|f| f * (growing.len() - 1) / 89).collect(); |
| 338 | let file = std::fs::File::create(out.join("growing.gif")).unwrap(); |
| 339 | let mut encoder = image::codecs::gif::GifEncoder::new_with_speed(file, 10); |
| 340 | encoder |
| 341 | .set_repeat(image::codecs::gif::Repeat::Infinite) |
| 342 | .unwrap(); |
| 343 | for (n, &pick) in picks.iter().enumerate() { |
| 344 | let tile = image::imageops::resize( |
| 345 | &gpu.render(&growing[pick], reach), |
| 346 | 320, |
| 347 | 320, |
| 348 | image::imageops::FilterType::Triangle, |
| 349 | ); |
| 350 | let delay = if n + 1 == picks.len() { 2500 } else { 50 }; |
| 351 | encoder |
| 352 | .encode_frame(image::Frame::from_parts( |
| 353 | tile, |
| 354 | 0, |
| 355 | 0, |
| 356 | image::Delay::from_numer_denom_ms(delay, 1), |
| 357 | )) |
| 358 | .unwrap(); |
| 359 | } |
| 360 | } |