Skip to content

Commit

Permalink
Load diffuse texture and rework asset validation
Browse files Browse the repository at this point in the history
  • Loading branch information
caspark committed Nov 21, 2021
1 parent ec64b21 commit 6991bb5
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 63 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ egui = "0.14.2"
serde = { version = "1.0", features = ["derive"] }
strum = { version = "0.22", features = ["derive"] }
glam = { version = "0.20.0", features = ["serde"] }
anyhow = "1.0"

[dev-dependencies]
criterion = "0.3"
Expand Down
Binary file added assets/african_head.diffuse.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes.
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ mod model;
pub use colors::*;

pub use canvas::Canvas;
pub use model::{Face, Model, Vertex};
pub use model::{Face, Model, ModelInput, Vertex};
63 changes: 41 additions & 22 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
mod scenes;
mod ui;

use std::path::{Path, PathBuf};

use crate::scenes::{render_scene, RenderScene};
use crab_tv::Canvas;
use anyhow::{bail, Context, Result};
use crab_tv::{Canvas, Model, ModelInput};
use glam::Vec3;
use rgb::RGB8;

Expand All @@ -15,7 +18,7 @@ pub struct RenderConfig {
scene: RenderScene,
width: usize,
height: usize,
model_filename: String,
model: PathBuf,
light_dir: Vec3,
output_filename: String,
display_actual_size: bool,
Expand All @@ -27,19 +30,28 @@ impl RenderConfig {
self.width * self.height
}

pub(crate) fn validate(&self) -> Result<(), String> {
pub(crate) fn validate(&self) -> Result<RenderInput> {
if self.width < 200 {
return Err("Width must be 200 or greater".to_owned());
bail!("Width must be 200 or greater");
} else if self.width > 5000 {
return Err("Width must be 5000 or less".to_owned());
bail!("Width must be 5000 or less");
}
if self.height < 200 {
return Err("Height must be 200 or greater".to_owned());
bail!("Height must be 200 or greater");
} else if self.height > 5000 {
return Err("Height must be 5000 or less".to_owned());
bail!("Height must be 5000 or less");
}

Ok(())
let model_input = Model::validate(&self.model)
.with_context(|| format!("Failed to load model from {}", self.model.display()))?;

Ok(RenderInput {
scene: self.scene,
width: self.width,
height: self.height,
model_input,
light_dir: self.light_dir,
})
}
}
impl RenderConfig {
Expand All @@ -56,8 +68,8 @@ impl RenderConfig {
self
}

pub(crate) fn model_filename(&mut self, model_filename: String) -> &mut Self {
self.model_filename = model_filename;
pub(crate) fn model(&mut self, model: &Path) -> &mut Self {
self.model = model.to_owned();
self
}
pub(crate) fn output_filename(&mut self, output_filename: String) -> &mut Self {
Expand All @@ -79,7 +91,7 @@ impl Default for RenderConfig {
scene: RenderScene::iter().last().unwrap(),
width: 400,
height: 400,
model_filename: "models/african_head.obj".to_owned(),
model: PathBuf::from("assets/african_head"),
light_dir: Vec3::new(0.0, 0.0, -1.0),
output_filename: "target/output.png".to_owned(),
display_actual_size: true,
Expand All @@ -88,8 +100,17 @@ impl Default for RenderConfig {
}
}

#[derive(Clone, Debug)]
pub struct RenderInput {
scene: RenderScene,
width: usize,
height: usize,
model_input: ModelInput,
light_dir: Vec3,
}

enum RenderCommand {
Render { config: RenderConfig },
Render { input: RenderInput },
}

enum RenderResult {
Expand Down Expand Up @@ -125,22 +146,20 @@ fn run_render_loop(
match render_command_rx.recv() {
Err(flume::RecvError::Disconnected) => break, // nothing to do, just quit quietly

Ok(RenderCommand::Render { config }) => {
Ok(RenderCommand::Render { input }) => {
render_result_tx
.send(RenderResult::Reset {
image_height: config.height,
image_width: config.width,
image_height: input.height,
image_width: input.width,
})
.ok()
.expect("sending Reset should succeed");

let mut image = Canvas::new(config.width, config.height);
render_scene(
&mut image,
&config.scene,
&config.model_filename,
config.light_dir,
);
let mut image = Canvas::new(input.width, input.height);

let model = Model::load_obj_file(&input.model_input).expect("Failed to load model");

render_scene(&mut image, &input.scene, &model, input.light_dir).unwrap();

render_result_tx
.send(RenderResult::FullImage {
Expand Down
80 changes: 78 additions & 2 deletions src/model.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
use std::path::{Path, PathBuf};

use anyhow::{anyhow, bail, Context, Result};
use derive_more::Constructor;
use glam::Vec3;
use rgb::RGB8;

#[derive(Clone, Copy, Debug, PartialEq, Constructor)]
pub struct Vertex {
Expand All @@ -12,19 +16,87 @@ pub struct Face {
pub texture_coords: Vec<usize>,
}

type TextureInput = PathBuf;

#[derive(Clone, Debug, Constructor)]
pub struct Texture {
pub width: usize,
pub height: usize,
pub data: Vec<RGB8>,
}

impl Texture {
fn validate(path: &Path) -> Result<TextureInput> {
if !path.exists() {
bail!("Texture file does not exist: {}", path.display());
}
Ok(path.to_owned())
}

fn load_from_file(path: &TextureInput) -> Result<Self> {
println!("Loading texture from file: {}", path.display());
let diffuse_bitmap = lodepng::decode24_file(path)
.with_context(|| format!("Loading texture from '{}' failed", path.display()))?;
Ok(Texture::new(
diffuse_bitmap.width,
diffuse_bitmap.height,
diffuse_bitmap.buffer,
))
}

pub fn get_pixel(&self, x: usize, y: usize) -> RGB8 {
debug_assert!(x < self.width);
debug_assert!(y < self.height);
self.data[y * self.width + x]
}
}

#[derive(Clone, Debug)]
pub struct ModelInput {
model: PathBuf,
diffuse_texture: PathBuf,
}

impl ModelInput {}

#[derive(Clone, Debug)]
pub struct Model {
pub vertices: Vec<Vertex>,
pub faces: Vec<Face>,
pub texture_coords: Vec<Vec3>,
pub diffuse_texture: Texture,
}

impl Model {
pub fn load_from_file<S: AsRef<str>>(path: S) -> std::io::Result<Self> {
pub fn validate(model: &Path) -> Result<ModelInput> {
let model_ext = model
.extension()
.ok_or(anyhow!("Model file '{:?}' must have an extension", model))?;
if model_ext != "obj" {
bail!(
"Model file '{:?}' must be an Obj file that ends in .obj",
model
);
}

let diffuse_texture = Texture::validate(model.with_extension("diffuse.png").as_ref())
.context("Loading diffuse texture failed")?;

Ok(ModelInput {
model: model.to_owned(),
diffuse_texture,
})
}

pub fn load_obj_file(input: &ModelInput) -> Result<Self> {
use std::io::prelude::*;

println!("Loading model from file: {}", input.model.display());
let mut contents = String::new();
std::fs::File::open(path.as_ref())?.read_to_string(&mut contents)?;
std::fs::File::open(&input.model)
.with_context(|| "attempting to open model file")?
.read_to_string(&mut contents)
.with_context(|| "attempting to read model file")?;

let mut vertices = Vec::new();
let mut faces = Vec::new();
Expand Down Expand Up @@ -102,10 +174,14 @@ impl Model {
}
}

let diffuse_texture = Texture::load_from_file(&input.diffuse_texture)
.context("Loading diffuse texture failed")?;

Ok(Self {
vertices,
faces,
texture_coords,
diffuse_texture,
})
}
}
30 changes: 13 additions & 17 deletions src/scenes.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use anyhow::Result;
use glam::{IVec2, Vec3};

use crab_tv::{Canvas, Model, BLUE, CYAN, GREEN, RED, WHITE};
Expand Down Expand Up @@ -29,9 +30,10 @@ pub enum RenderScene {
pub fn render_scene(
image: &mut Canvas,
scene: &RenderScene,
model_filename: &str,
model: &Model,
light_dir: Vec3,
) {
) -> Result<()> {
println!("Rendering scene: {}", scene);
match scene {
RenderScene::FivePixels => {
// pixel in the middle
Expand All @@ -49,9 +51,6 @@ pub fn render_scene(
image.line(IVec2::new(0, 0), IVec2::new(50, 50), GREEN);
}
RenderScene::ModelWireframe => {
println!("Loading model: {}", model_filename);
let model = Model::load_from_file(model_filename).expect("model filename should exist");

image.model_wireframe(&model, WHITE);
}
RenderScene::TriangleLineSweepVerbose => {
Expand Down Expand Up @@ -91,45 +90,42 @@ pub fn render_scene(
image.triangle_barycentric(&t2, GREEN);
}
RenderScene::ModelColoredTriangles => {
println!("Loading model: {}", model_filename);
let model = Model::load_from_file(model_filename).expect("model filename should exist");

image.model_colored_triangles(&model);
}
RenderScene::ModelFlatShaded => {
println!("Loading model: {}", model_filename);
let model = Model::load_from_file(model_filename).expect("model filename should exist");

image.model_flat_shaded(&model, light_dir, false);
}
RenderScene::ModelFlatShadedDepthTested => {
println!("Loading model: {}", model_filename);
let model = Model::load_from_file(model_filename).expect("model filename should exist");

image.model_flat_shaded(&model, light_dir, true);
}
}

image.flip_y();

Ok(())
}

#[cfg(test)]
mod tests {
use std::path::Path;

use strum::IntoEnumIterator;

use super::*;

#[test]
fn every_scene_should_render_without_errors() {
fn every_scene_should_render_without_errors() -> Result<()> {
for scene in RenderScene::iter() {
let mut image = Canvas::new(200, 200);
println!("Rendering scene: {:?}", scene);
render_scene(
&mut image,
&scene,
"models/african_head.obj",
&Model::load_obj_file(&Model::validate(Path::new("assets/african_head").as_ref())?)
.expect("model load should succeed"),
Vec3::new(0.0, 0.0, -1.0),
);
)?;
}
Ok(())
}
}
Loading

0 comments on commit 6991bb5

Please sign in to comment.