#[macro_use] extern crate askama; #[macro_use] extern crate log; extern crate actix_web; extern crate image; extern crate rand; #[macro_use] extern crate serde_derive; extern crate serde; extern crate stderrlog; #[macro_use] extern crate structopt; extern crate rtiow; use actix_web::Path as WebPath; use std::fs; use std::path::Path; use std::path::PathBuf; use std::time::SystemTime; use actix_web::middleware; use actix_web::server; use actix_web::App; use actix_web::HttpRequest; use actix_web::HttpResponse; use actix_web::Result; use askama::Template; use rand::SeedableRng; use rand::XorShiftRng; use structopt::StructOpt; use rtiow::lode::Noise; use rtiow::vec3::Vec3; #[derive(Debug, StructOpt)] #[structopt( name = "noise_explorer", about = "CLI for exploring Perlin noise" )] struct Opt { /// Output directory #[structopt(parse(from_os_str), default_value = "/tmp/perlin")] pub output: PathBuf, /// HTTP listen address #[structopt(long = "addr", default_value = "0.0.0.0:8889")] pub addr: String, /// Width of noise images #[structopt(short = "w", long = "width", default_value = "128")] pub width: u32, /// Height of noise images #[structopt(short = "h", long = "height", default_value = "128")] pub height: u32, } #[derive(Debug, Deserialize)] #[serde(tag = "type")] enum NoiseType { Scale(f32), Turbulence(usize), Marble { period: Vec3, power: f32, size: usize, }, } impl NoiseType { fn to_url(&self) -> String { match &self { NoiseType::Scale(scale) => format!("scale/{}", scale), NoiseType::Turbulence(turbulence) => format!("turbulence/{}", turbulence), NoiseType::Marble { period, power, size, } => format!( "marble/period/{},{},{}/power/{}/size/{}", period.x, period.y, period.z, power, size, ), } } fn parameters(&self) -> Vec<(&str, String)> { match &self { NoiseType::Scale(scale) => vec![("Scale", scale.to_string())], NoiseType::Turbulence(turbulence) => vec![("Turbulence", turbulence.to_string())], NoiseType::Marble { period, power, size, } => vec![ ("Period", period.to_string()), ("Power", power.to_string()), ("Size", size.to_string()), ], } } } #[derive(Debug, Deserialize)] struct NoiseParams { width: u32, height: u32, noise: NoiseType, } #[derive(Debug, Deserialize)] struct NoiseParamsScale { width: u32, height: u32, scale: f32, } #[derive(Debug, Deserialize)] struct NoiseParamsTurbulence { width: u32, height: u32, turbulence: usize, } #[derive(Debug, Deserialize)] struct NoiseParamsMarble { width: u32, height: u32, x: f32, y: f32, z: f32, power: f32, size: usize, } impl NoiseParams { fn compact_string(&self) -> String { let mut s = format!("w{}-h{}", self.width, self.height); match self.noise { NoiseType::Scale(scale) => s.push_str(&format!("-s{:.2}", scale)), NoiseType::Turbulence(turbulence) => s.push_str(&format!("-t{}", turbulence)), NoiseType::Marble { period, power, size, } => s.push_str(&format!( "-m{}-{}-{}", period.to_string().replace(" ", "-"), power, size )), } s } fn url(&self) -> String { format!( "/noise/{}/{}/{}", self.width, self.height, self.noise.to_url(), ) } fn big_url(&self) -> String { format!("/noise/1024/1024/{}", self.noise.to_url()) } fn image_name(&self) -> String { format!("noise-{}.png", self.compact_string()) } } fn render_noise(noise_params: &NoiseParams) -> image::GrayImage { const SEED: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; let ref mut rng: XorShiftRng = SeedableRng::from_seed(SEED); let mut img = image::GrayImage::new(noise_params.width, noise_params.height); let tex = Noise::new(rng); for x in 0..img.width() { for y in 0..img.height() { let u = x as f32 / 2.; let v = y as f32 / 2.; let p = Vec3::new(u, v, 1.); let luma = match noise_params.noise { NoiseType::Scale(scale) => tex.value(p, scale), NoiseType::Turbulence(turbulence) => tex.turbulence(p, turbulence), NoiseType::Marble { period, power, size, } => tex.marble(p, period, power, size), }; if luma > 1. { info!("Hot pixel @ {}x{}: {}", x, y, luma); } if luma < 0. { info!("Cold pixel @ {}x{}: {}", x, y, luma); } let luma = (luma * 255.) as u8; img.put_pixel(x, y, image::Luma([luma])); } } img } fn render_noise_to_disk

(output_dir: P, noise_params: &NoiseParams) -> Result<(), std::io::Error> where P: AsRef, { let img = render_noise(noise_params); let ref output_dir: &Path = output_dir.as_ref(); fs::create_dir_all(output_dir)?; let path = output_dir.join(noise_params.image_name()); info!("Saving {}", path.display()); img.save(path) } struct Specimen { title: String, params: Vec, } #[derive(Template)] #[template(path = "index.html")] struct IndexTemplate { render_time: u64, specimens: Vec, } fn render_html(specimens: Vec) -> Result { let index = IndexTemplate { render_time: SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap() .as_secs(), specimens, }; index.render() } fn build_specimens(width: u32, height: u32) -> Vec { let mut specimens = Vec::new(); let mut specimen = Specimen { title: "Unperturbed".into(), params: Vec::new(), }; { let params = NoiseParams { width, height, noise: NoiseType::Scale(1.), }; specimen.params.push(params); } specimens.push(specimen); let mut specimen = Specimen { title: "Varying Scale Factor <1".into(), params: Vec::new(), }; let debug = false; if !debug { for scale in 1..10 { let params = NoiseParams { width, height, noise: NoiseType::Scale(scale as f32 / 10.), }; specimen.params.push(params); } specimens.push(specimen); let mut specimen = Specimen { title: "Varying Scale Factor >1".into(), params: Vec::new(), }; for scale in 1..10 { let params = NoiseParams { width, height, noise: NoiseType::Scale(scale as f32), }; specimen.params.push(params); } specimens.push(specimen); let mut specimen = Specimen { title: "Varying Turbulence Factor".into(), params: Vec::new(), }; for turbulence in 0..9 { let params = NoiseParams { width, height, noise: NoiseType::Turbulence(1 << turbulence), }; specimen.params.push(params); } specimens.push(specimen); let max_size = 8; for power in 1..12 { let mut specimen = Specimen { title: format!( "Marble power {}, varying size 2-{}", power, 1 << (max_size - 1) ), params: Vec::new(), }; for size in 1..max_size { let params = NoiseParams { width, height, noise: NoiseType::Marble { period: Vec3::new(5., 10., 0.), power: power as f32, size: 1 << size, }, }; specimen.params.push(params); } specimens.push(specimen); } let mut specimen = Specimen { title: "Marble varying power, size = 32".into(), params: Vec::new(), }; for power in 1..10 { let params = NoiseParams { width, height, noise: NoiseType::Marble { period: Vec3::new(0., 1., 0.), power: power as f32, size: 32, }, }; specimen.params.push(params); } specimens.push(specimen); } specimens } fn style(_req: &HttpRequest) -> Result { let bytes = include_bytes!("../../templates/style.css"); Ok(HttpResponse::Ok().content_type("text/css").body(&bytes[..])) } fn index(_req: &HttpRequest) -> Result { // TODO(wathiede): get these from app state? let width = 128; let height = 128; let specimens = build_specimens(width, height); Ok(HttpResponse::Ok() .content_type("text/html") .body(render_html(specimens).unwrap())) } fn noise(np: NoiseParams) -> Result { let img = render_noise(&np); let mut buf = Vec::new(); let img = image::DynamicImage::ImageLuma8(img); // TODO(wathiede): remove unwrap with proper error propagation. img.write_to(&mut buf, image::ImageOutputFormat::PNG) .unwrap(); Ok(HttpResponse::Ok().content_type("image/png").body(buf)) } fn noise_scale(np: WebPath) -> Result { noise(NoiseParams { width: np.width, height: np.height, noise: NoiseType::Scale(np.scale), }) } fn noise_turbulence(np: WebPath) -> Result { noise(NoiseParams { width: np.width, height: np.height, noise: NoiseType::Turbulence(np.turbulence), }) } fn noise_marble(np: WebPath) -> Result { noise(NoiseParams { width: np.width, height: np.height, noise: NoiseType::Marble { period: Vec3::new(np.x, np.y, np.z), power: np.power, size: np.size, }, }) } fn main() -> Result<(), std::io::Error> { stderrlog::new() .verbosity(2) .timestamp(stderrlog::Timestamp::Millisecond) .init() .unwrap(); let opt = Opt::from_args(); server::new(|| { App::new() .resource("/noise/{width}/{height}/scale/{scale}", |r| { r.with(noise_scale); }).resource("/noise/{width}/{height}/turbulence/{turbulence}", |r| { r.with(noise_turbulence); }).resource( "/noise/{width}/{height}/marble/period/{x},{y},{z}/power/{power}/size/{size}", |r| { r.with(noise_marble); }, ).resource("/style.css", |r| r.f(style)) .resource("/", |r| r.f(index)) .middleware(middleware::Logger::default()) }).bind(opt.addr) .unwrap() .run(); Ok(()) } #[cfg(test)] mod tests { use std::str; use actix_web::http::Method; use actix_web::test::TestServer; use actix_web::HttpMessage; use actix_web::Path; use super::NoiseParamsMarble; use super::NoiseParamsScale; use super::NoiseParamsTurbulence; #[test] fn noise_param_from_req() { let mut srv = TestServer::build().start(|app| { app.resource("/noise/{width}/{height}/scale/{scale}", |r| { r.with(|p: Path| format!("{:?}", p)); }); app.resource("/noise/{width}/{height}/turbulence/{turbulence}", |r| { r.with(|p: Path| format!("{:?}", p)); }); app.resource( "/noise/{width}/{height}/marble/period/{x},{y},{z}/power/{power}/size/{size}", |r| { r.with(|p: Path| format!("{:?}", p)); }, ); }); let request = srv .client(Method::GET, "/noise/32/32/scale/1") .finish() .unwrap(); let response = srv.execute(request.send()).unwrap(); assert!(response.status().is_success(), "Response {:?}", response); let bytes = srv.execute(response.body()).unwrap(); let body = str::from_utf8(&bytes).unwrap(); assert_eq!( body, "NoiseParamsScale { width: 32, height: 32, scale: 1.0 }" ); let request = srv .client(Method::GET, "/noise/32/32/turbulence/5") .finish() .unwrap(); let response = srv.execute(request.send()).unwrap(); assert!(response.status().is_success(), "Response {:?}", response); let bytes = srv.execute(response.body()).unwrap(); let body = str::from_utf8(&bytes).unwrap(); assert_eq!( body, "NoiseParamsTurbulence { width: 32, height: 32, turbulence: 5 }" ); // TODO(wathiede): this isn't working, probably because Vec3 Deserialize is not working. let request = srv .client( Method::GET, "/noise/32/32/marble/period/1.,2.,3./power/32./size/4", ).finish() .unwrap(); let response = srv.execute(request.send()).unwrap(); assert!(response.status().is_success(), "Response {:?}", response); let bytes = srv.execute(response.body()).unwrap(); let body = str::from_utf8(&bytes).unwrap(); assert_eq!( body, "NoiseParamsMarble { width: 32, height: 32, x: 1.0, y: 2.0, z: 3.0, power: 32.0, size: 4 }" ); } }