raytracers/rtiow/src/bin/noise_explorer.rs

511 lines
14 KiB
Rust

#[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<P>(output_dir: P, noise_params: &NoiseParams) -> Result<(), std::io::Error>
where
P: AsRef<Path>,
{
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<NoiseParams>,
}
#[derive(Template)]
#[template(path = "index.html")]
struct IndexTemplate {
render_time: u64,
specimens: Vec<Specimen>,
}
fn render_html(specimens: Vec<Specimen>) -> Result<String, askama::Error> {
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<Specimen> {
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<HttpResponse> {
let bytes = include_bytes!("../../templates/style.css");
Ok(HttpResponse::Ok().content_type("text/css").body(&bytes[..]))
}
fn index(_req: &HttpRequest) -> Result<HttpResponse> {
// 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<HttpResponse> {
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<NoiseParamsScale>) -> Result<HttpResponse> {
noise(NoiseParams {
width: np.width,
height: np.height,
noise: NoiseType::Scale(np.scale),
})
}
fn noise_turbulence(np: WebPath<NoiseParamsTurbulence>) -> Result<HttpResponse> {
noise(NoiseParams {
width: np.width,
height: np.height,
noise: NoiseType::Turbulence(np.turbulence),
})
}
fn noise_marble(np: WebPath<NoiseParamsMarble>) -> Result<HttpResponse> {
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<NoiseParamsScale>| format!("{:?}", p));
});
app.resource("/noise/{width}/{height}/turbulence/{turbulence}", |r| {
r.with(|p: Path<NoiseParamsTurbulence>| format!("{:?}", p));
});
app.resource(
"/noise/{width}/{height}/marble/period/{x},{y},{z}/power/{power}/size/{size}",
|r| {
r.with(|p: Path<NoiseParamsMarble>| 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 }"
);
}
}