diff --git a/assignment-0/.gitignore b/assignment-0/.gitignore index aa58f4e..c5348e3 100644 --- a/assignment-0/.gitignore +++ b/assignment-0/.gitignore @@ -1,3 +1,4 @@ /target +input out.ppm diff --git a/assignment-0/Cargo.lock b/assignment-0/Cargo.lock index 3f25d9c..26e0ebe 100644 --- a/assignment-0/Cargo.lock +++ b/assignment-0/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + [[package]] name = "anyhow" version = "1.0.68" @@ -14,7 +23,9 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", + "lazy_static", "rand", + "regex", ] [[package]] @@ -141,6 +152,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.139" @@ -153,6 +170,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + [[package]] name = "once_cell" version = "1.17.0" @@ -243,6 +266,23 @@ dependencies = [ "getrandom", ] +[[package]] +name = "regex" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + [[package]] name = "rustix" version = "0.36.7" diff --git a/assignment-0/Cargo.toml b/assignment-0/Cargo.toml index aa0f266..44b72b0 100644 --- a/assignment-0/Cargo.toml +++ b/assignment-0/Cargo.toml @@ -1,11 +1,12 @@ [package] +authors = ["Michael Zhang "] name = "assignment-0" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] anyhow = "1.0.68" -clap = { version = "4.1.1", features = ["derive"] } +clap = { version = "4.1.1", features = ["derive", "cargo"] } +lazy_static = "1.4.0" rand = "0.8.5" +regex = "1.7.1" diff --git a/assignment-0/README.md b/assignment-0/README.md new file mode 100644 index 000000000..e0d30be --- /dev/null +++ b/assignment-0/README.md @@ -0,0 +1,31 @@ +Assignment 0: Generating turbulence with Rust! +=== + +This program generates PPM images. + +Using the program +--- + +The binary provided has been built for Ubuntu, and has been tested on CSELabs. +Run `assignment-0 --help` to find out about all the options for using this +program. + +Compiling the source +--- + +This project is a standard Rust program, and can be built with just a Rust +compiler. The version of Rust used is the current stable at the time of writing, +1.66. + +Build the program using: + +``` +cargo build --release +``` + +The binary can be found in the `target` directory. + +Contact +--- + +Author: Michael Zhang diff --git a/assignment-0/src/ppm.rs b/assignment-0/src/image.rs similarity index 67% rename from assignment-0/src/ppm.rs rename to assignment-0/src/image.rs index 668a30e..88f3e4f 100644 --- a/assignment-0/src/ppm.rs +++ b/assignment-0/src/image.rs @@ -1,14 +1,22 @@ use std::io::{Result, Write}; +/// A 24-bit pixel represented by a red, green, and blue value. pub struct Pixel(pub u8, pub u8, pub u8); -pub struct Ppm { +/// A representation of an image +pub struct Image { + /// Width in pixels pub(crate) width: usize, + + /// Height in pixels pub(crate) height: usize, + + /// Pixel data in row-major form. pub(crate) data: Vec, } -impl Ppm { +impl Image { + /// Write the image in PPM format to a file. pub fn write(&self, mut w: impl Write) -> Result<()> { // Header let header = format!("P3 {} {} 255\n", self.width, self.height); diff --git a/assignment-0/src/main.rs b/assignment-0/src/main.rs index 3c787df..1cf627f 100644 --- a/assignment-0/src/main.rs +++ b/assignment-0/src/main.rs @@ -1,48 +1,111 @@ -use std::{fs::File, path::PathBuf}; +use std::{ + fs::File, + io::Read, + path::{Path, PathBuf}, +}; -use anyhow::Result; +use anyhow::{bail, Context, Result}; use clap::{Parser, ValueEnum}; +use lazy_static::lazy_static; +use regex::Regex; use turbulence::generate_turbulence; use crate::value_noise::generate_noise; -mod ppm; +mod image; mod turbulence; mod value_noise; mod vec2; +/// Noise image generator. #[derive(Parser)] +#[clap(author, version, about, long_about = None)] struct Opt { + /// Path to the input file to use. + /// + /// The input file should follow this format: + /// + /// imsize [width] [height] + /// + /// Where `imsize' is a keyword, and `width' and `height' are integer values + /// denoting the desired size of the image to be generated. + #[clap()] + input_path: PathBuf, + + /// The algorithm to use for pattern generation. #[clap(long = "algorithm", default_value = "turbulence")] algorithm: Algorithm, - #[clap(long = "size", default_value = "1024")] - size: usize, - + /// The output image path. #[clap(short = 'o', long = "out", default_value = "out.ppm")] output_path: PathBuf, } #[derive(ValueEnum, Clone)] enum Algorithm { + /// Generates noise, which is linearly interpolated randomness. Noise, + + /// Fractal sum made with signed noise, used to resemble smoke. Turbulence, } fn main() -> Result<()> { let opt = Opt::parse(); + let (width, height) = parse_input_file(&opt.input_path)?; + + // We will use the default thread-local rng. Future work could include + // allowing the rng to be seedable from the command line let rng = rand::thread_rng(); + // Pick the algorithm to use based on the option passed via command line let ppm = match opt.algorithm { - Algorithm::Noise => generate_noise(opt.size, opt.size, rng), - Algorithm::Turbulence => generate_turbulence(opt.size, opt.size, rng), + Algorithm::Noise => generate_noise(width, height, rng), + Algorithm::Turbulence => generate_turbulence(width, height, rng), }; + // Write the PPM image to the output file specified { - let file = File::create(opt.output_path)?; - ppm.write(file)?; + let file = File::create(&opt.output_path).with_context(|| { + format!("Could not create file {:?}.", opt.output_path) + })?; + + ppm.write(file).with_context(|| { + format!("Could not write to file {:?}.", opt.output_path) + })?; } Ok(()) } + +lazy_static! { + static ref INPUT_PATTERN: Regex = + Regex::new(r"imsize\s+(?P\d+)\s+(?P\d+)") + .expect("Failed to compile regex."); +} + +/// Parse the input file, returning the target image dimensions +fn parse_input_file(path: impl AsRef) -> Result<(usize, usize)> { + let path = path.as_ref(); + + // Read the contents from the file + let contents = { + let mut contents = String::new(); + let mut file = File::open(path)?; + file.read_to_string(&mut contents)?; + contents + }; + + // Parse the contents using regex + let matches = match INPUT_PATTERN.captures(&contents) { + Some(v) => v, + None => bail!("Invalid input file syntax."), + }; + + // Extract the parsed captures and convert to int + let width = matches.name("width").unwrap().as_str().parse()?; + let height = matches.name("height").unwrap().as_str().parse()?; + + Ok((width, height)) +} diff --git a/assignment-0/src/turbulence.rs b/assignment-0/src/turbulence.rs index d8b69b2..23dc9b6 100644 --- a/assignment-0/src/turbulence.rs +++ b/assignment-0/src/turbulence.rs @@ -1,8 +1,9 @@ +// Follows this algorithm: // https://www.scratchapixel.com/lessons/procedural-generation-virtual-worlds/procedural-patterns-noise-part-1/simple-pattern-examples.html use rand::RngCore; -use crate::ppm::{Pixel, Ppm}; +use crate::image::{Image, Pixel}; use crate::value_noise::ValueNoise; use crate::vec2::Vec2; @@ -10,7 +11,7 @@ pub fn generate_turbulence( width: usize, height: usize, rng: impl RngCore, -) -> Ppm { +) -> Image { let mut noise_map = vec![0.0; width * height]; let noise = ValueNoise::new(rng); @@ -43,7 +44,7 @@ pub fn generate_turbulence( Pixel(v, v, v) }) .collect(); - Ppm { + Image { width, height, data, diff --git a/assignment-0/src/value_noise.rs b/assignment-0/src/value_noise.rs index d738f89..559f832 100644 --- a/assignment-0/src/value_noise.rs +++ b/assignment-0/src/value_noise.rs @@ -1,16 +1,17 @@ +// Follows this algorithm: // https://www.scratchapixel.com/lessons/procedural-generation-virtual-worlds/procedural-patterns-noise-part-1/creating-simple-2D-noise.html use std::mem; use rand::{Rng, RngCore}; -use crate::ppm::{Pixel, Ppm}; +use crate::image::{Pixel, Image}; use crate::vec2::Vec2; const MAX_TABLE_SIZE: usize = 256; const MAX_TABLE_SIZE_MASK: usize = MAX_TABLE_SIZE - 1; -pub fn generate_noise(width: usize, height: usize, rng: impl RngCore) -> Ppm { +pub fn generate_noise(width: usize, height: usize, rng: impl RngCore) -> Image { let mut noise_map = vec![0.0; width * height]; let noise = ValueNoise::new(rng); let frequency = 0.05; @@ -29,7 +30,7 @@ pub fn generate_noise(width: usize, height: usize, rng: impl RngCore) -> Ppm { Pixel(v, v, v) }) .collect(); - Ppm { + Image { width, height, data, diff --git a/assignment-0/src/vec2.rs b/assignment-0/src/vec2.rs index c114dd9..da604a4 100644 --- a/assignment-0/src/vec2.rs +++ b/assignment-0/src/vec2.rs @@ -1,5 +1,6 @@ use std::ops::Mul; +/// A generic 2-dimensional point class. #[derive(Copy, Clone)] pub struct Vec2 { pub x: T, diff --git a/flake.nix b/flake.nix index 6402583..77f36b9 100644 --- a/flake.nix +++ b/flake.nix @@ -9,7 +9,7 @@ overlays = [ fenix.overlays.default ]; }; - toolchain = pkgs.fenix.default; + toolchain = pkgs.fenix.stable; in rec { devShell = pkgs.mkShell { packages = (with pkgs; [ cargo-watch cargo-deny cargo-edit ])