Compare commits

..

No commits in common. "master" and "proc-macro-version" have entirely different histories.

479 changed files with 568 additions and 730283 deletions

1
.gitattributes vendored
View file

@ -1 +0,0 @@
assignment-2*/ext/* linguist-vendored

3
.gitignore vendored
View file

@ -1,4 +1 @@
.direnv
.pijul
/target

View file

@ -1,2 +0,0 @@
.git
.DS_Store

1288
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,20 +0,0 @@
[workspace]
members = [
"assignment-0",
"assignment-1a",
"assignment-1b",
"assignment-1c",
"assignment-1d",
"assignment-2a-rust",
]
# For profiling with flamegraphs
[profile.release]
debug = true
# Optimize for size when creating handin
[profile.release-handin]
inherits = "release"
strip = true
lto = true

View file

@ -4,6 +4,16 @@ authors = ["Michael Zhang <zhan4854@umn.edu>"]
version = "0.1.0"
edition = "2021"
# For profiling with flamegraphs
[profile.release]
debug = true
# Optimize for size when creating handin
[profile.release-handin]
inherits = "release"
strip = true
lto = true
[[bin]]
name = "raytracer1b"
path = "src/main.rs"

View file

@ -1,6 +1,6 @@
/target
/assignment-1c
/raytracer1c
/assignment-1b
/raytracer1b
/examples/*.png
*.ppm
*.zip

171
assignment-1c/Cargo.lock generated
View file

@ -2,29 +2,11 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "addr2line"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97"
dependencies = [
"gimli",
]
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "anyhow"
version = "1.0.68"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61"
dependencies = [
"backtrace",
]
checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800"
[[package]]
name = "approx"
@ -42,9 +24,8 @@ dependencies = [
"anyhow",
"base64",
"clap",
"csci5607-macros",
"derivative",
"generator",
"itertools",
"nalgebra",
"num",
"ordered-float",
@ -60,21 +41,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "backtrace"
version = "0.3.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca"
dependencies = [
"addr2line",
"cc",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
]
[[package]]
name = "base64"
version = "0.21.0"
@ -185,6 +151,16 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "csci5607-macros"
version = "0.1.0"
dependencies = [
"anyhow",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "derivative"
version = "2.2.0"
@ -223,19 +199,6 @@ dependencies = [
"libc",
]
[[package]]
name = "generator"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d266041a359dfa931b370ef684cceb84b166beb14f7f0421f4a6a3d0c446d12e"
dependencies = [
"cc",
"libc",
"log",
"rustversion",
"windows",
]
[[package]]
name = "getrandom"
version = "0.2.8"
@ -247,12 +210,6 @@ dependencies = [
"wasi",
]
[[package]]
name = "gimli"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4"
[[package]]
name = "heck"
version = "0.4.1"
@ -290,15 +247,6 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
@ -335,12 +283,6 @@ dependencies = [
"rawpointer",
]
[[package]]
name = "memchr"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "memoffset"
version = "0.7.1"
@ -350,15 +292,6 @@ dependencies = [
"autocfg",
]
[[package]]
name = "miniz_oxide"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa"
dependencies = [
"adler",
]
[[package]]
name = "nalgebra"
version = "0.32.1"
@ -485,15 +418,6 @@ dependencies = [
"libc",
]
[[package]]
name = "object"
version = "0.30.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.17.0"
@ -565,9 +489,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.50"
version = "1.0.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2"
checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6"
dependencies = [
"unicode-ident",
]
@ -639,12 +563,6 @@ dependencies = [
"num_cpus",
]
[[package]]
name = "rustc-demangle"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
[[package]]
name = "rustix"
version = "0.36.7"
@ -659,12 +577,6 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "rustversion"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70"
[[package]]
name = "safe_arch"
version = "0.6.0"
@ -879,19 +791,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1c4bd0a50ac6020f65184721f758dba47bb9fbc2133df715ec74a237b26794a"
dependencies = [
"windows_aarch64_msvc 0.39.0",
"windows_i686_gnu 0.39.0",
"windows_i686_msvc 0.39.0",
"windows_x86_64_gnu 0.39.0",
"windows_x86_64_msvc 0.39.0",
]
[[package]]
name = "windows-sys"
version = "0.42.0"
@ -899,12 +798,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc 0.42.1",
"windows_i686_gnu 0.42.1",
"windows_i686_msvc 0.42.1",
"windows_x86_64_gnu 0.42.1",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc 0.42.1",
"windows_x86_64_msvc",
]
[[package]]
@ -913,48 +812,24 @@ version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
[[package]]
name = "windows_aarch64_msvc"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7711666096bd4096ffa835238905bb33fb87267910e154b18b44eaabb340f2"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
[[package]]
name = "windows_i686_gnu"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "763fc57100a5f7042e3057e7e8d9bdd7860d330070251a73d003563a3bb49e1b"
[[package]]
name = "windows_i686_gnu"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
[[package]]
name = "windows_i686_msvc"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bc7cbfe58828921e10a9f446fcaaf649204dcfe6c1ddd712c5eebae6bda1106"
[[package]]
name = "windows_i686_msvc"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
[[package]]
name = "windows_x86_64_gnu"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6868c165637d653ae1e8dc4d82c25d4f97dd6605eaa8d784b5c6e0ab2a252b65"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.1"
@ -967,12 +842,6 @@ version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
[[package]]
name = "windows_x86_64_msvc"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e4d40883ae9cae962787ca76ba76390ffa29214667a111db9e0a1ad8377e809"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.1"

View file

@ -4,17 +4,29 @@ authors = ["Michael Zhang <zhan4854@umn.edu>"]
version = "0.1.0"
edition = "2021"
[workspace]
members = ["csci5607-macros"]
# For profiling with flamegraphs
[profile.release]
debug = true
# Optimize for size when creating handin
[profile.release-handin]
inherits = "release"
strip = true
lto = true
[[bin]]
name = "raytracer1c"
path = "src/main.rs"
[dependencies]
anyhow = { version = "1.0.68", features = ["backtrace"] }
anyhow = "1.0.68"
base64 = "0.21.0"
clap = { version = "4.1.4", features = ["cargo", "derive"] }
csci5607-macros = { path = "./csci5607-macros" }
derivative = "2.2.0"
generator = "0.7.2"
itertools = "0.10.5"
nalgebra = "0.32.1"
num = { version = "0.4.0", features = ["serde"] }
ordered-float = "3.4.0"

View file

@ -10,6 +10,8 @@ CONVERT := convert
HANDIN := ./hw1c.michael.zhang.zip
BINARY := ./raytracer1c
WRITEUP := ./writeup.pdf
SHOWCASE := ./showcase.png
SOURCES := Cargo.toml $(shell find -name "*.rs")
EXAMPLES := $(shell find examples -name "*.txt")
@ -31,16 +33,22 @@ $(BINARY): $(SOURCES)
cargo build --profile release-handin
mv target/docker/release-handin/raytracer1c $@
$(HANDIN): $(BINARY) Makefile Cargo.toml Cargo.lock README.md $(EXAMPLES_PNG) $(EXAMPLES_PPM)
$(HANDIN): $(BINARY) $(WRITEUP) Makefile Cargo.toml Cargo.lock README.md $(EXAMPLES_PNG) $(EXAMPLES_PPM) $(SHOWCASE)
$(ZIP) -r $@ src examples $^
$(SHOWCASE): examples/soft-shadow-demo.png
cp $< $@
examples/%.ppm: examples/%.txt $(SOURCES)
cargo run --release -- -o $@ $(RAYTRACER_FLAGS) $<
examples/%.png: examples/%.ppm
convert $< $@
writeup.pdf: writeup.md $(EXAMPLES_PNG)
$(PANDOC) -o $@ $<
clean:
rm -rf target/docker \
$(HANDIN) $(BINARY) \
$(HANDIN) $(BINARY) $(WRITEUP) $(SHOWCASE) \
$(EXAMPLES_PPM) $(EXAMPLES_PNG)

View file

@ -0,0 +1,13 @@
[package]
name = "csci5607-macros"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
anyhow = "1.0.69"
proc-macro2 = "1.0.51"
quote = "1.0.23"
syn = "1.0.107"

View file

@ -0,0 +1,65 @@
#[macro_use]
extern crate anyhow;
use anyhow::Result;
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{Data, DataStruct, DeriveInput, Field};
#[proc_macro_derive(Csci5607Parser)]
pub fn derive_answer_fn(tokens: TokenStream) -> TokenStream {
let tokens: TokenStream2 = tokens.into();
match derive_answer_fn_impl(tokens) {
Ok(v) => v.into(),
Err(e) => {
eprintln!("Error: {e}");
panic!("{e:?}");
}
}
}
fn derive_answer_fn_impl(tokens: TokenStream2) -> Result<TokenStream2> {
let derive_input: DeriveInput = syn::parse2(tokens)?;
// Only defined on structs
let struct_input: DataStruct = match derive_input.data {
Data::Struct(v) => v,
_ => bail!("Only defined on structs."),
};
let struct_ident = derive_input.ident;
// Gather struct fields
let mut initializers = TokenStream2::default();
let mut final_constructor = TokenStream2::default();
for field in struct_input.fields {
let field_ident = field.ident;
initializers.extend(quote! { let #field_ident = todo!(); });
final_constructor.extend(quote! { #field_ident, });
// Check if this field is a vec
}
Ok(
quote! {
impl std::str::FromStr for #struct_ident {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
#initializers
for line in contents.lines() {
let mut parts = line.split_whitespace();
let keyword = match parts.next() {
Some(v) => v,
None => continue,
};
}
Ok(#struct_ident { #final_constructor })
}
}
}
.into(),
)
}

View file

@ -1,2 +0,0 @@
# Necessary files
!/earthtexture.ppm

View file

@ -14,4 +14,4 @@ v 1 -1 -4
v 2 1 -6
f 1 2 3
f 1 3 4
f 1 3 4

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

View file

@ -1,14 +0,0 @@
eye 0 0 0
viewdir 0 0 -1
updir 0 1 0
hfov 60
imsize 512 256
bkgcolor 0.1 0.1 0.1
light -2 1 0 1 1 1 1
mtlcolor 0 1 0 1 1 1 0.2 0.6 0.2 20
v 0 1 -4
v -1 -1 -4
v 1 -1 -4
v 2 1 -6
f 1 2 3
f 1 3 4

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View file

@ -1,18 +0,0 @@
eye 0 0 0
viewdir 0 0 -1
updir 0 1 0
hfov 60
imsize 512 256
bkgcolor 0.1 0.1 0.1
light -2 1 0 1 1 1 1
mtlcolor 0 0 1 1 1 1 0.2 0.6 0.2 20
v -1 1 -4
v -1 -1 -4
v 1 -1 -4
v 1 1 -4
vn -1 1 1
vn -1 -1 1
vn 1 -1 1
vn 1 1 1
f 1//1 2//2 3//3
f 1//1 3//3 4//4

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

View file

@ -1,10 +0,0 @@
eye 2 -6 1
viewdir -1 3 -0.5
updir 0 0 1
hfov 50
imsize 512 512
bkgcolor 0.5 0.7 0.9
light 0 1 -1 0 1 1 1
mtlcolor 0 1 0 1 1 1 0.2 0.8 0.1 20
texture earthtexture.ppm
sphere 0 0 0 2

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View file

@ -1,19 +0,0 @@
eye 0 0 0
viewdir 0 0 -1
updir 0 1 0
hfov 60
imsize 512 256
bkgcolor 0.1 0.1 0.1
light -2 1 0 1 1 1 1
mtlcolor 0 0 1 1 1 1 0.2 0.6 0.2 20
texture umn.ppm
v -1 1 -4
v -1 -1 -4
v 1 -1 -4
v 1 1 -4
vt 0 0
vt 0 1
vt 1 1
vt 1 0
f 1/1 2/2 3/3
f 1/1 3/3 4/4

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View file

@ -1,25 +0,0 @@
eye 0 0 0
viewdir 0 0 -1
updir 0 1 0
hfov 60
imsize 512 256
bkgcolor 0.1 0.1 0.1
light -2 1 0 1 1 1 1
mtlcolor 0 0 1 1 1 1 0.2 0.6 0.2 20
texture umn.ppm
v -1 1 -4
v -1 -1 -4
v 1 -1 -4
v 1 1 -4
vn -1 1 1
vn -1 -1 1
vn 1 -1 1
vn 1 1 1
vt 0 0
vt 0 1
vt 1 1
vt 1 0
f 1/1/1 2/2/2 3/3/3
f 1/1/1 3/3/3 4/4/4

Binary file not shown.

Before

Width:  |  Height:  |  Size: 640 KiB

View file

@ -1,56 +0,0 @@
eye 12.5 5 2.5
viewdir -2 -0.5 2
updir 0 1 0
hfov 60
imsize 900 600
bkgcolor 0.5 0.7 0.9
light 1 -1 1 0 1 1 1
mtlcolor 0 1 0 1 1 1 0.2 0.8 0 20
v 10 0 5
v -10 0 5
v -10 0 25
v 10 0 25
v 5 0 12.5
v -5 0 12.5
v -5 5 12.5
v 5 5 12.5
v 5 0 17.5
v -5 0 17.5
v -5 5 17.5
v 5 5 17.5
v 5 7.5 15
v -5 7.5 15
v 5 4.5 12
v -5 4.5 12
v 5 4.5 18
v -5 4.5 18
vt 0 0
vt 0 1
vt 1 1
vt 1 0
vt 2 0
vt 2 1
vt 0.5 0
vt 4 0
vt 4 1
texture grass.ppm
f 1/2 2/3 3/4
f 1/2 3/4 4/1
texture wood.ppm
f 5/2 6/6 7/5
f 5/2 7/5 8/1
f 9/2 11/5 10/6
f 9/2 12/1 11/5
f 6/2 10/3 11/4
f 6/2 11/4 7/1
f 5/3 12/1 9/2
f 5/3 8/4 12/1
f 7/2 11/3 14/7
f 8/3 13/7 12/2
texture redwood.ppm
f 13/1 15/2 16/9
f 13/1 16/9 14/8
f 13/8 14/1 18/2
f 13/8 18/2 17/9
texture soccerball.ppm
sphere -2.5 0.5 9 0.5

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

View file

@ -1,20 +0,0 @@
eye 0 0 0
viewdir 0 0 -1
updir 0 1 0
hfov 60
imsize 512 256
bkgcolor 0.1 0.1 0.1
light -2 1 0 1 1 1 1
mtlcolor 1 1 1 1 1 1 0.2 0.6 0.2 20 1 0
bump normalmap.ppm
sphere -1.6 0 -4 0.5
v -1 1 -4
v -1 -1 -4
v 1 -1 -4
v 1 1 -4
vt 0 0
vt 0 1
vt 1 1
vt 1 0
f 1/1 2/2 3/3
f 1/1 3/3 4/4

View file

@ -1,2 +0,0 @@
#! /bin/sh
PROGRAM_NAME="raytracer1c"; echo "-------- Running Test1.txt --------"; ./$PROGRAM_NAME Test1.txt; echo "-------- Running Test2.txt --------";./$PROGRAM_NAME Test2.txt; echo "-------- Running Test3.txt --------";./$PROGRAM_NAME Test3.txt; echo "-------- Running Test4.txt --------";./$PROGRAM_NAME Test4.txt; echo "-------- Running Test5.txt --------";./$PROGRAM_NAME Test5.txt; echo "-------- Running Test6.txt --------";./$PROGRAM_NAME Test6.txt; echo "-------- Running TestE.txt --------";./$PROGRAM_NAME TestE.txt;

View file

@ -1,27 +0,0 @@
eye 0 0 4
viewdir 0 0 -1
updir 1 1 0
hfov 60
imsize 640 480
bkgcolor 0.4 0.4 0.4
light -2 1 0 1 1 1 1
mtlcolor 1 0.6 1 1 1 1 0.1 0.4 0.4 20
v 0 0 0
v 1 0 -1
v 0 1 -1
v -1 0 -1
v 0 -1 -1
vn 0 0 1
vn 1 0 1
vn 0 1 1
vn -1 0 1
vn 0 -1 1
f 1//1 2//2 3//3
f 1//1 3//3 4//4
f 1//1 4//4 5//5
f 1//1 5//5 2//2

View file

@ -1,45 +0,0 @@
imsize 1024 768
eye 0 0 15
viewdir 0 0 -1
hfov 60
updir 0 1 0
bkgcolor 0.5 0.5 0.5
depthcueing 0.5 0.5 0.5 1 0.4 60 0
light 10 10 -10 1 1 1 1
mtlcolor 0.5 1 0.5 1 1 1 0.2 1 0.1 5
sphere 4.5 4.5 -20 4.5
sphere -4.5 -4.5 -20 4.5
mtlcolor 1 0.5 0.5 1 1 1 0.2 0.8 0 5
sphere -10 0 -30 4
sphere -20 0 -30 4
sphere -30 0 -30 4
sphere -40 0 -30 4
sphere 0 0 -30 4
sphere 10 0 -30 4
sphere 20 0 -30 4
sphere 30 0 -30 4
sphere 40 0 -30 4
sphere -10 -10 -30 4
sphere -20 -10 -30 4
sphere -30 -10 -30 4
sphere -40 -10 -30 4
sphere 0 -10 -30 4
sphere 10 -10 -30 4
sphere 20 -10 -30 4
sphere 30 -10 -30 4
sphere 40 -10 -30 4
sphere -10 10 -30 4
sphere -20 10 -30 4
sphere -30 10 -30 4
sphere -40 10 -30 4
sphere 0 10 -30 4
sphere 10 10 -30 4
sphere 20 10 -30 4
sphere 30 10 -30 4
sphere 40 10 -30 4

View file

@ -1,49 +0,0 @@
CSci 5607, Spring 2023
Assignment 1c: Triangles and Texture
Due: Friday March 3rd
- [x] The program robustly accepts extended scene description files that include
texture images, texture coordinates and surface normal vectors. The
program is able to robustly handle triangle definitions that either
include or do not include per-vertex normal directions and include or do
not include per-vertex texture coordinates, in addition to vertex
locations. The syntax used to define textured and/or smooth-shaded
triangles follows the classic .obj format as described in class. (5 pts)
- [x] The program correctly computes ray/plane intersections, and correctly
performs point-in-triangle testing using barycentric coordinates, enabling
the correct rendering of scenes containing triangles as well as spheres.
(20 pts)
- [x] The program is capable of rendering triangles using flat shading, in which
every pixel in a triangle is assigned the same color, obtained by
correctly evaluating the Phong illumination equation using the unit length
normal of the plane in which the triangle lies. (10 pts)
- [x] The program is capable of rendering triangles using smooth shading, in
which every pixel within a triangle is assigned a unique color, obtained
by evaluating the Phong illumination equation using a unit length normal
direction correctly interpolated from the three normal directions defined
at the three triangle vertices. (15 pts)
- [x] The program is capable of rendering textured spheres. An appropriate
texture coordinate is computed at each ray/sphere intersection point. This
can be done at runtime using a pre-defined, hard-coded algorithm. The
computed texture coordinate is used to retrieve a correctly corresponding
color from the associated texture map, which replaces the objects diffuse
color in the Phong illumination model when it is evaluated at that point.
(25 pts)
- [x] The program is capable of rendering textured triangles. The texture
coordinate at the ray/triangle intersection point is correctly
interpolated from the texture coordinates defined at each of the three
triangle vertices. The interpolated texture coordinate is used to retrieve
a correctly corresponding color from the associated texture map, which
replaces the objects diffuse color in the Phong illumination model when
it is evaluated at that point. (25 pts)
- [ ] Extra credit: The program is capable of reading a normal map from a file
and correctly using the values in that map to appropriately vary the
surface normal direction used when calculating the Phone illumination
equation at each point across the surface. The normal mapping is capable
of being applied to both triangles and spheres. (7 pts)

Binary file not shown.

View file

@ -1,20 +1,11 @@
use std::{
fs::File,
io::{BufRead, BufReader, Read, Write},
path::Path,
};
use std::io::{Result, Write};
use anyhow::{Context, Result};
use generator::{done, Gn};
use itertools::Itertools;
use nalgebra::Vector3;
/// A pixel color represented by a red, green, and blue value in the range 0-1.
pub type Color = Vector3<f64>;
/// A representation of an image
#[derive(Derivative)]
#[derivative(Debug)]
pub struct Image {
/// Width in pixels
pub width: usize,
@ -23,87 +14,10 @@ pub struct Image {
pub height: usize,
/// Pixel data in row-major form.
#[derivative(Debug = "ignore")]
pub data: Vec<Color>,
}
impl Image {
pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
let file = File::open(path)
.with_context(|| format!("Could not open file at {path:?}"))?;
Self::read(file)
}
/// Parse image from a Read
pub fn read(r: impl Read + Send) -> Result<Self> {
let mut line_reader = BufReader::new(r);
let mut header = String::new();
line_reader
.read_line(&mut header)
.context("Could not read line")?;
let parts = header.trim().split(" ").collect::<Vec<_>>();
let width = parts[1].parse::<usize>().context("Could not read width")?;
let height = parts[2].parse::<usize>().context("Could not read height")?;
let max_value = parts[3]
.parse::<usize>()
.context("Could not read max value")?;
// Generator for reading numbers
let numbers = Gn::<()>::new_scoped(move |mut s| {
macro_rules! gen_try {
($expr:expr, $str:expr $(, $($arg:expr),* $(,)?)?) => {
match $expr {
Ok(v) => v,
Err(e) => {
s.yield_(
Err(anyhow::Error::from(e))
.with_context(|| format!($str $(, $($arg,)*)?)),
);
done!();
}
}
};
}
for line in line_reader.lines() {
let line = gen_try!(line, "Could not read line");
let parts = line.trim().split_whitespace();
for part in parts {
let int =
gen_try!(part.parse::<u64>(), "Could not read int from: {}", part);
s.yield_(Ok(int));
}
}
done!()
});
let mut data = Vec::with_capacity(width * height);
for mut chunk in &(numbers).chunks(3) {
let (r, g, b) = match chunk.next_tuple() {
Some(v) => v,
None => bail!("Not enough elements"),
};
let r = r? as f64 / max_value as f64;
let g = g? as f64 / max_value as f64;
let b = b? as f64 / max_value as f64;
let color = Color::new(r, g, b);
data.push(color);
}
Ok(Image {
width,
height,
data,
})
}
/// Write the image in PPM format to a file.
pub fn write(&self, mut w: impl Write) -> Result<()> {
// Header

View file

@ -1,4 +1,6 @@
use nalgebra::{Vector2, Vector3};
#![doc = include_str!("../README.md")]
use nalgebra::Vector3;
#[macro_use]
extern crate anyhow;
@ -12,6 +14,4 @@ pub mod ray;
pub mod scene;
pub mod utils;
pub type Point2 = Vector2<f64>;
pub type Point = Vector3<f64>;
pub type Vector = Vector3<f64>;

View file

@ -4,7 +4,7 @@ extern crate tracing;
use std::fs::File;
use std::path::PathBuf;
use anyhow::{Context, Result};
use anyhow::Result;
use assignment_1c::image::Image;
use assignment_1c::ray::Ray;
use assignment_1c::scene::Scene;
@ -91,7 +91,7 @@ fn main() -> Result<()> {
.iter()
.enumerate()
.filter_map(|(i, object)| {
match object.kind.intersects_ray_at(&scene, &ray) {
match object.kind.intersects_ray_at(&ray) {
Ok(Some(t)) => {
// Return both the t and the sphere, because we want to sort on
// the t but later retrieve attributes from the sphere
@ -113,8 +113,7 @@ fn main() -> Result<()> {
Ok(match earliest_intersection {
// Take the object's material color
Some((obj_idx, intersection_context, object)) => scene
.compute_pixel_color(obj_idx, object, intersection_context)
.context("Could not compute pixel color.")?,
.compute_pixel_color(obj_idx, object.material, intersection_context),
// There was no intersection, so this should default to the scene's
// background color

View file

@ -1,4 +1,4 @@
use crate::{Point, Vector};
use crate::Point;
/// A normalized parametric Ray of the form (origin + direction * time)
///
@ -7,7 +7,7 @@ use crate::{Point, Vector};
#[derive(Debug)]
pub struct Ray {
pub origin: Point,
pub direction: Vector,
pub direction: Point,
}
impl Ray {

View file

@ -3,16 +3,14 @@ use anyhow::Result;
use ordered_float::NotNan;
use crate::utils::compute_rotation_matrix;
use crate::Vector;
use crate::{ray::Ray, Point};
use super::illumination::IntersectionContext;
use super::Scene;
#[derive(Debug)]
pub struct Cylinder {
pub center: Point,
pub direction: Vector,
pub direction: Point,
pub radius: f64,
pub length: f64,
}
@ -24,12 +22,11 @@ impl Cylinder {
/// If there is no intersection point, returns None.
pub fn intersects_ray_at(
&self,
_: &Scene,
ray: &Ray,
) -> Result<Option<IntersectionContext>> {
// Determine rotation matrix for turning the cylinder upright along the
// Z-axis
let target_direction = Vector::new(0.0, 0.0, 1.0);
let target_direction = Point::new(0.0, 0.0, 1.0);
let rotation_matrix =
compute_rotation_matrix(self.direction, target_direction)?;
let inverse_rotation_matrix =
@ -156,7 +153,7 @@ impl Cylinder {
}
let normal_rotated =
Vector::new(0.0, 0.0, rotated_point.z - rotated_cylinder_center.z)
Point::new(0.0, 0.0, rotated_point.z - rotated_cylinder_center.z)
.normalize();
let normal = inverse_rotation_matrix * normal_rotated;
@ -182,7 +179,7 @@ impl Cylinder {
#[cfg(test)]
mod tests {
use crate::{ray::Ray, scene::Scene, Point, Vector};
use crate::{ray::Ray, Point};
use super::Cylinder;
@ -190,7 +187,7 @@ mod tests {
fn test_cylinder() {
let cylinder = Cylinder {
center: Point::new(0.0, 0.0, 0.0),
direction: Vector::new(0.0, 1.0, 0.0),
direction: Point::new(0.0, 1.0, 0.0),
radius: 3.0,
length: 4.0,
};
@ -199,8 +196,7 @@ mod tests {
let end = Point::new(0.0, 2.0, 2.0);
let ray = Ray::from_endpoints(eye, end);
let scene = Scene::default();
let _res = cylinder.intersects_ray_at(&scene, &ray);
// panic!("Result: {res:?}");
let res = cylinder.intersects_ray_at(&ray);
panic!("Result: {res:?}");
}
}

View file

@ -1,11 +1,49 @@
use std::fmt::Debug;
use anyhow::Result;
use crate::image::Color;
use crate::ray::Ray;
use crate::utils::cross;
use crate::Point;
use super::cylinder::Cylinder;
use super::illumination::IntersectionContext;
use super::sphere::Sphere;
use super::Scene;
#[derive(Debug)]
pub enum ObjectKind {
Sphere(Sphere),
Cylinder(Cylinder),
}
impl ObjectKind {
/// Determine where the ray intersects this object, returning the earliest
/// time this happens. Returns None if no intersection occurs.
///
/// Also known as Trace_Ray in the slides, except not the part where it calls
/// Shade_Ray.
pub fn intersects_ray_at(
&self,
ray: &Ray,
) -> Result<Option<IntersectionContext>> {
match self {
ObjectKind::Sphere(sphere) => sphere.intersects_ray_at(ray),
ObjectKind::Cylinder(cylinder) => cylinder.intersects_ray_at(ray),
}
}
}
/// An object in the scene
#[derive(Debug)]
pub struct Object {
pub kind: ObjectKind,
/// Index into the scene's material color list
pub material: usize,
}
#[derive(Debug)]
pub struct Rect {
pub upper_left: Point,

View file

@ -1,6 +1,5 @@
use std::iter;
use anyhow::Result;
use ordered_float::NotNan;
use rand::Rng;
use rayon::prelude::{
@ -8,11 +7,10 @@ use rayon::prelude::{
ParallelIterator,
};
use crate::{image::Color, ray::Ray, utils::dot, Point, Vector};
use crate::{image::Color, ray::Ray, utils::dot, Point};
use super::{
data::{DepthCueing, Light, LightKind},
object::Object,
data::{DepthCueing, Light, LightKind, Object},
Scene,
};
@ -27,34 +25,20 @@ impl Scene {
pub fn compute_pixel_color(
&self,
obj_idx: usize,
object: &Object,
material_idx: usize,
intersection_context: IntersectionContext,
) -> Result<Color> {
let material = match self.materials.get(object.material_idx) {
) -> Color {
// TODO: Does it make sense to make this function fallible from an API
// design standpoint?
let material = match self.materials.get(material_idx) {
Some(v) => v,
None => bail!("Material index not found."),
None => return self.bkg_color,
};
let diffuse_color = match object.texture_idx {
Some(texture_idx) => {
let (u, v) = object
.kind
.get_texture_coord(&self, &intersection_context)?;
let texture = match self.textures.get(texture_idx) {
Some(v) => v,
None => bail!("Texture index {texture_idx} not found."),
};
texture.pixel_at(u, v)
}
None => material.diffuse_color,
};
let ambient_component = material.k_a * diffuse_color;
let ambient_component = material.k_a * material.diffuse_color;
// Diffuse and specular lighting for each separate light
let diffuse_and_specular: Color = self
let diffuse_and_specular: Point = self
.lights
.par_iter()
.map(|light| {
@ -67,7 +51,7 @@ impl Scene {
((light_direction + viewer_direction) / 2.0).normalize();
let diffuse_component = material.k_d
* diffuse_color
* material.diffuse_color
* dot(normal, light_direction).max(0.0);
let specular_component = material.k_s
@ -138,7 +122,7 @@ impl Scene {
// Need to clamp the result so none of the components goes over 1
let clamped_result = color.map(|v| v.min(1.0));
Ok(clamped_result)
clamped_result
}
/// Perform another ray casting to see if there are any objects obstructing
@ -167,14 +151,13 @@ impl Scene {
// This list will be a set of opacities
let intersections = other_objects
.filter_map(|(_, object)| {
let intersection_context =
match object.kind.intersects_ray_at(&self, &ray) {
Ok(v) => v,
Err(err) => {
error!("Error while performing shadow casting: {err}");
None
}
}?;
let intersection_context = match object.kind.intersects_ray_at(&ray) {
Ok(v) => v,
Err(err) => {
error!("Error while performing shadow casting: {err}");
None
}
}?;
let intersection_time = *intersection_context.time;
match light.kind {
@ -205,13 +188,12 @@ impl Scene {
})
.collect::<Vec<_>>();
let average =
intersections.iter().cloned().sum::<f64>() / intersections.len() as f64;
match intersections.is_empty() {
true => 1.0,
false => {
let average = intersections.iter().cloned().sum::<f64>()
/ intersections.len() as f64;
average
}
false => average,
}
}
@ -232,7 +214,7 @@ impl Scene {
let x = rng.gen_range(0.0..JITTER_RADIUS);
let y = rng.gen_range(0.0..JITTER_RADIUS);
let z = rng.gen_range(0.0..JITTER_RADIUS);
let delta = Vector::new(x, y, z);
let delta = Point::new(x, y, z);
light_location + delta
})
.take(JITTER_RAYS)
@ -246,15 +228,14 @@ impl Scene {
direction,
};
let intersection_context =
match object.kind.intersects_ray_at(&self, &ray) {
Ok(Some(v)) => v,
Ok(None) => return false,
Err(err) => {
error!("Error while performing shadow casting: {err}");
return false;
}
};
let intersection_context = match object.kind.intersects_ray_at(&ray) {
Ok(Some(v)) => v,
Ok(None) => return false,
Err(err) => {
error!("Error while performing shadow casting: {err}");
return false;
}
};
let light_time = (location - ray.origin).norm();
let intersection_time = *intersection_context.time;
@ -286,7 +267,7 @@ pub struct IntersectionContext {
/// The normal vector protruding from the surface of the object at the
/// intersection point
#[derivative(PartialEq = "ignore", Ord = "ignore")]
pub normal: Vector,
pub normal: Point,
}
impl Eq for IntersectionContext {}

View file

@ -1,420 +1,189 @@
use std::fs::File;
use std::io::Read;
use std::path::Path;
use std::{fs::File, io::Read, path::Path};
use anyhow::{Context, Result};
use itertools::Itertools;
use nalgebra::{Vector2, Vector3};
use anyhow::Result;
use crate::{
image::{Color, Image},
scene::{
cylinder::Cylinder,
data::{Attenuation, Light, LightKind, Material},
object::{Object, ObjectKind},
data::{Attenuation, Light, LightKind, Material, Object},
sphere::Sphere,
texture::Texture,
triangle::Triangle,
Scene,
},
Point, Vector,
Point,
};
use super::data::DepthCueing;
use super::data::{DepthCueing, ObjectKind};
impl Scene {
/// Parse the input file into a scene
pub fn from_input_file(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
// Scope the read so the file is dropped and closed immediately after the
// contents have been read to memory
let contents = {
let mut contents = String::new();
let mut file = File::open(path)?;
let mut file = File::open(path.as_ref())?;
file.read_to_string(&mut contents)?;
contents
};
let mut scene = Scene::default();
let mut material_idx = None;
let mut texture_idx = None;
let mut material_color = None;
for line in contents.lines() {
// Split lines into words. `parts' is an iterator, which is consumed upon
// iterating, rather than collected into a Vec
let mut parts = line.split_whitespace();
// The keyword is the very first space-separated substring, and tells us
// how to interpret the rest
let keyword = match parts.next() {
Some(v) => v,
None => continue,
};
/// Short for "read", macro for reading something from the iterator and
/// converting it into the appropriate format given by $ty. For this to
/// work, $ty must implement Construct
macro_rules! r {
($ty:ty) => {
<$ty>::construct(&mut parts, ())
.with_context(|| format!("Could not parse {}", stringify!($ty)))?
};
($ty:ty, $($ex:expr),* $(,)?) => {
<$ty>::construct(&mut parts, $($ex,)*)
.with_context(|| format!("Could not parse {}", stringify!($ty)))?
};
if keyword == "imsize" {
let parts = parts
.map(|s| s.parse::<usize>().map_err(|e| e.into()))
.collect::<Result<Vec<_>>>()?;
if let [width, height] = parts[..] {
scene.image_width = width;
scene.image_height = height;
}
} else if keyword == "projection" {
if let Some("parallel") = parts.next() {
scene.parallel_projection = true;
}
/// Shortcut for unwrapping one of the state `Option's
macro_rules! u {
($expr:expr) => {
match $expr {
Some(v) => v,
None => {
bail!(
"Each object must be preceded by a `{}` line",
stringify!($expr)
)
}
}
};
}
// Do float parsing instead
else {
let parts = parts
.map(|s| s.parse::<f64>().map_err(|e| e.into()))
.collect::<Result<Vec<_>>>()?;
match keyword {
"imsize" => {
scene.image_width = r!(usize);
scene.image_height = r!(usize);
}
"projection" => {
if let Some("parallel") = parts.next() {
scene.parallel_projection = true;
let read_vec3 = |start: usize| {
ensure!(parts.len() >= start + 3, "Vec3 requires 3 components.");
Ok(Point::new(parts[start], parts[start + 1], parts[start + 2]))
};
match keyword {
"eye" => scene.eye_pos = read_vec3(0)?,
"viewdir" => scene.view_dir = read_vec3(0)?,
"updir" => scene.up_dir = read_vec3(0)?,
"hfov" => scene.hfov = parts[0],
"bkgcolor" => scene.bkg_color = read_vec3(0)?,
// light x y z w r g b
"light" => {
ensure!(parts.len() == 7, "Light requires 7 params");
let kind = match parts[3] as usize {
0 => LightKind::Directional {
direction: read_vec3(0)?,
},
1 => LightKind::Point {
location: read_vec3(0)?,
attenuation: None,
},
_ => bail!("Invalid w; must be either 0 or 1"),
};
let light = Light {
kind,
color: read_vec3(4)?,
};
scene.lights.push(light);
}
}
"eye" => scene.eye_pos = r!(Vector3<f64>),
"viewdir" => scene.view_dir = r!(Vector3<f64>),
"updir" => scene.up_dir = r!(Vector3<f64>),
// attlight x y z w r g b c1 c2 c3
"attlight" => {
ensure!(parts.len() == 10, "Attenuated light requires 10 params");
"hfov" => scene.hfov = r!(f64),
"bkgcolor" => scene.bkg_color = r!(Color),
let kind = match parts[3] as usize {
// TODO: Is this even defined? Pending TA answer
0 => LightKind::Directional {
direction: read_vec3(0)?,
},
1 => LightKind::Point {
location: read_vec3(0)?,
attenuation: Some(Attenuation {
c1: parts[7],
c2: parts[8],
c3: parts[9],
}),
},
_ => bail!("Invalid w; must be either 0 or 1"),
};
let light = Light {
kind,
color: read_vec3(4)?,
};
scene.lights.push(light);
}
// light x y z w r g b
// attlight x y z w r g b c1 c2 c3
"light" | "attlight" => {
let vec3 = r!(Vector3<f64>);
let w = r!(usize);
let color = r!(Color);
// depthcueing dcr dcg dcb amax amin distmax distmin
"depthcueing" => {
ensure!(parts.len() == 7, "Depth cueing requires 7 params");
let attenuation = match keyword == "attlight" {
true => {
let c = r!(Vector3<f64>);
Some(Attenuation {
c1: c.x,
c2: c.y,
c3: c.z,
})
}
false => None,
};
let color = read_vec3(0)?;
scene.depth_cueing = DepthCueing {
color,
a_max: parts[3],
a_min: parts[4],
dist_max: parts[5],
dist_min: parts[6],
};
}
let kind = match w as usize {
0 => LightKind::Directional { direction: vec3 },
1 => LightKind::Point {
location: vec3,
attenuation,
},
_ => bail!("Invalid w; must be either 0 or 1"),
};
// mtlcolor Odr Odg Odb Osr Osg Osb ka kd ks n
"mtlcolor" => {
ensure!(parts.len() == 10, "Material color requires 10 params");
let light = Light { kind, color };
scene.lights.push(light);
}
let diffuse_color = read_vec3(0)?;
let specular_color = read_vec3(3)?;
// depthcueing dcr dcg dcb amax amin distmax distmin
"depthcueing" => {
let color = r!(Color);
let a_max = r!(f64);
let a_min = r!(f64);
let dist_max = r!(f64);
let dist_min = r!(f64);
let material = Material {
diffuse_color,
specular_color,
k_a: parts[6],
k_d: parts[7],
k_s: parts[8],
exponent: parts[9],
};
scene.depth_cueing = DepthCueing {
color,
a_max,
a_min,
dist_max,
dist_min,
};
}
let idx = scene.materials.len();
material_color = Some(idx);
scene.materials.push(material);
}
// mtlcolor Odr Odg Odb Osr Osg Osb ka kd ks n
"mtlcolor" => {
let diffuse_color = r!(Color);
let specular_color = r!(Color);
let k_a = r!(f64);
let k_d = r!(f64);
let k_s = r!(f64);
let exponent = r!(f64);
let material = Material {
diffuse_color,
specular_color,
k_a,
k_d,
k_s,
exponent,
};
let idx = scene.materials.len();
material_idx = Some(idx);
scene.materials.push(material);
}
"sphere" => {
let center = r!(Point);
let radius = r!(f64);
scene.objects.push(Object {
kind: ObjectKind::Sphere(Sphere { center, radius }),
material_idx: u!(material_idx),
texture_idx,
});
}
"cylinder" => {
let center = r!(Point);
let direction = r!(Vector);
let radius = r!(f64);
let length = r!(f64);
scene.objects.push(Object {
kind: ObjectKind::Cylinder(Cylinder {
center,
direction,
radius,
length,
"sphere" => scene.objects.push(Object {
kind: ObjectKind::Sphere(Sphere {
center: read_vec3(0)?,
radius: parts[3],
}),
material_idx: u!(material_idx),
texture_idx,
});
material: match material_color {
Some(v) => v,
None => {
bail!("Each sphere must be preceded by a `mtlcolor` line")
}
},
}),
"cylinder" => scene.objects.push(Object {
kind: ObjectKind::Cylinder(Cylinder {
center: read_vec3(0)?,
direction: read_vec3(3)?,
radius: parts[6],
length: parts[7],
}),
material: match material_color {
Some(v) => v,
None => {
bail!("Each sphere must be preceded by a `mtlcolor` line")
}
},
}),
_ => bail!("Unknown keyword {keyword}"),
}
// Assignment 1C: Triangles and textures
// v x y z
"v" => scene.triangle_vertices.push(r!(Vector)),
// vn nx ny nz
"vn" => scene.vertex_normals.push(r!(Vector)),
// vt u v
"vt" => scene.texture_vertices.push(r!(Vector2<f64>)),
// f v1 v2 v3
// f v1//n1 v2//n2 v3//n3
"f" => {
let v1 = r!(TriangleVertex);
let v2 = r!(TriangleVertex);
let v3 = r!(TriangleVertex);
let vs = Vector3::new(v1, v2, v3);
let vertices = vs.map(|v| v.vertex_idx);
let normals = vs.map(|v| v.normal_idx);
let normals = match normals.iter().filter(|o| o.is_some()).count() {
0 => None,
n if n == vs.len() => Some(normals.map(|o| o.unwrap())),
_ => bail!("Cannot mix and match having a normal index"),
};
let textures = vs.map(|v| v.texture_idx);
let textures = match textures.iter().filter(|o| o.is_some()).count() {
0 => None,
n if n == vs.len() => Some(textures.map(|o| o.unwrap())),
_ => bail!("Cannot mix and match having a texture index"),
};
let triangle = Triangle {
vertices,
normals,
textures,
};
scene.objects.push(Object {
kind: ObjectKind::Triangle(triangle),
material_idx: u!(material_idx),
texture_idx,
});
}
"texture" => {
let input_parent = path.parent().unwrap().to_path_buf();
let path = match parts.next() {
Some(s) => input_parent.join(s),
None => bail!("Did not provide path."),
};
let idx = scene.textures.len();
texture_idx = Some(idx);
let image = Image::from_file(path)?;
let texture = Texture::new(image, false);
scene.textures.push(texture);
}
"bump" => {
let input_parent = path.parent().unwrap().to_path_buf();
let path = match parts.next() {
Some(s) => input_parent.join(s),
None => bail!("Did not provide path."),
};
let idx = scene.textures.len();
texture_idx = Some(idx);
let image = Image::from_file(path)?;
let normal_map = Texture::new(image, true);
scene.textures.push(normal_map);
}
_ => bail!("Unknown keyword {keyword}"),
}
}
Ok(scene)
}
}
pub trait Construct: Sized {
type Args;
/// Construct an element of this type from an iterator over strings.
fn construct<'a, I>(it: &mut I, args: Self::Args) -> Result<Self>
where
I: Iterator<Item = &'a str>;
}
impl<T: Construct> Construct for Option<T> {
type Args = T::Args;
fn construct<'a, I>(it: &mut I, args: Self::Args) -> Result<Self>
where
I: Iterator<Item = &'a str>,
{
let mut peeker = it.peekable();
if peeker.peek().is_none() {
return Ok(None);
}
T::construct(&mut peeker, args).map(Some)
}
}
macro_rules! impl_construct {
($ty:ty) => {
impl Construct for $ty {
type Args = ();
fn construct<'a, I>(it: &mut I, _: Self::Args) -> Result<Self>
where
I: Iterator<Item = &'a str>,
{
let item = match it.next() {
Some(v) => v,
None => bail!("Ran out of items"),
};
Ok(item.parse()?)
}
}
};
}
impl_construct!(f64);
impl_construct!(usize);
impl Construct for Vector2<f64> {
type Args = ();
fn construct<'a, I>(it: &mut I, _: Self::Args) -> Result<Self>
where
I: Iterator<Item = &'a str>,
{
let (x, y) = match it.next_tuple() {
Some(v) => v,
None => bail!("Expected 2 values"),
};
let x: f64 = x.parse()?;
let y: f64 = y.parse()?;
Ok(Vector2::new(x, y))
}
}
impl Construct for Vector3<f64> {
type Args = ();
fn construct<'a, I>(it: &mut I, _: Self::Args) -> Result<Self>
where
I: Iterator<Item = &'a str>,
{
let (x, y, z) = match it.next_tuple() {
Some(v) => v,
None => bail!("Expected 3 values"),
};
let x: f64 = x.parse()?;
let y: f64 = y.parse()?;
let z: f64 = z.parse()?;
Ok(Vector3::new(x, y, z))
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
struct TriangleVertex {
vertex_idx: usize,
normal_idx: Option<usize>,
texture_idx: Option<usize>,
}
impl Construct for TriangleVertex {
type Args = ();
fn construct<'a, I>(it: &mut I, _: Self::Args) -> Result<Self>
where
I: Iterator<Item = &'a str>,
{
let s = match it.next() {
Some(v) => v,
None => bail!("Waiting on another"),
};
// Note: indexed by 1 not 0, so we will just do the subtraction
// here to avoid having to deal with it later
let parts = s.split("/").collect_vec();
ensure!(parts.len() >= 1 && parts.len() <= 3);
let vertex_idx: usize = parts[0].parse::<usize>()? - 1;
let texture_idx =
match parts.get(1).and_then(|s| (!s.is_empty()).then(|| *s)) {
Some(s) => Some(s.parse::<usize>()? - 1),
None => None,
};
let normal_idx =
match parts.get(2).and_then(|s| (!s.is_empty()).then(|| *s)) {
Some(s) => Some(s.parse::<usize>()? - 1),
None => None,
};
Ok(TriangleVertex {
vertex_idx,
texture_idx,
normal_idx,
})
}
}

View file

@ -2,25 +2,20 @@ pub mod cylinder;
pub mod data;
pub mod illumination;
pub mod input_file;
pub mod object;
pub mod sphere;
pub mod texture;
pub mod triangle;
use nalgebra::Vector2;
use nalgebra::Vector3;
use csci5607_macros::Csci5607Parser;
use crate::image::Color;
use crate::{Point, Point2, Vector};
use crate::{image::Color, Point};
use self::data::{Attenuation, DepthCueing, Light, Material};
use self::object::Object;
use self::texture::{Texture};
use self::data::{Attenuation, DepthCueing, Light, Material, Object};
#[derive(Debug, Default)]
#[derive(Debug, Default, Csci5607Parser)]
pub struct Scene {
pub eye_pos: Point,
pub view_dir: Vector,
pub up_dir: Vector,
pub eye_pos: Vector3<f64>,
pub view_dir: Vector3<f64>,
pub up_dir: Vector3<f64>,
/// Horizontal field of view (in degrees)
pub hfov: f64,
@ -38,16 +33,5 @@ pub struct Scene {
pub lights: Vec<Light>,
pub objects: Vec<Object>,
/// List of textures
pub textures: Vec<Texture>,
/// List of normal maps (Extra credit)
pub normal_maps: Vec<Texture>,
/// Coordinates into a texture image
pub texture_vertices: Vec<Point2>,
/// Triangle vertices
pub triangle_vertices: Vec<Point>,
pub vertex_normals: Vec<Vector>,
pub vertices: Vec<Point>,
}

View file

@ -1,61 +0,0 @@
use anyhow::Result;
use crate::ray::Ray;
use super::cylinder::Cylinder;
use super::illumination::IntersectionContext;
use super::sphere::Sphere;
use super::triangle::Triangle;
use super::Scene;
/// An object in the scene
#[derive(Debug)]
pub struct Object {
pub kind: ObjectKind,
/// Index into the scene's material color list
pub material_idx: usize,
/// If this object has a texture, this is the index into the texture list
pub texture_idx: Option<usize>,
}
#[derive(Debug)]
pub enum ObjectKind {
Sphere(Sphere),
Cylinder(Cylinder),
Triangle(Triangle),
}
impl ObjectKind {
/// Determine where the ray intersects this object, returning the earliest
/// time this happens. Returns None if no intersection occurs.
///
/// Also known as Trace_Ray in the slides, except not the part where it calls
/// Shade_Ray.
pub fn intersects_ray_at(
&self,
scene: &Scene,
ray: &Ray,
) -> Result<Option<IntersectionContext>> {
match self {
ObjectKind::Sphere(sphere) => sphere.intersects_ray_at(scene, ray),
ObjectKind::Cylinder(cylinder) => cylinder.intersects_ray_at(scene, ray),
ObjectKind::Triangle(triangle) => triangle.intersects_ray_at(scene, ray),
}
}
/// Get the (u, v) coordinates in the texture (between 0 and 1) that
/// corresponds to the intersection point
pub fn get_texture_coord(
&self,
scene: &Scene,
ctx: &IntersectionContext,
) -> Result<(f64, f64)> {
match self {
ObjectKind::Sphere(sphere) => sphere.get_texture_coord(ctx),
ObjectKind::Cylinder(cylinder) => todo!(),
ObjectKind::Triangle(triangle) => triangle.get_texture_coord(scene, ctx),
}
}
}

View file

@ -1,12 +1,9 @@
use std::f64::consts::PI;
use anyhow::Result;
use ordered_float::NotNan;
use crate::{ray::Ray, utils::min_f64, Point};
use super::illumination::IntersectionContext;
use super::Scene;
#[derive(Debug)]
pub struct Sphere {
@ -21,7 +18,6 @@ impl Sphere {
/// If there is no intersection point, returns None.
pub fn intersects_ray_at(
&self,
_: &Scene,
ray: &Ray,
) -> Result<Option<IntersectionContext>> {
let a = ray.direction.norm();
@ -74,21 +70,4 @@ impl Sphere {
normal,
}))
}
pub fn get_texture_coord(
&self,
ctx: &IntersectionContext,
) -> Result<(f64, f64)> {
// Reverse engineer the angles from the coordinate of the intersection
let cosp = (ctx.point.z - self.center.z) / self.radius;
let phi = cosp.acos();
let theta =
(ctx.point.y - self.center.y).atan2(ctx.point.x - self.center.x);
// Map theta and phi into 0 - 1 range
let v = phi / PI;
let u = 0.5 + theta / (2.0 * PI);
Ok((u, v))
}
}

View file

@ -1,60 +0,0 @@
use crate::{
image::{Color, Image},
Vector,
};
#[derive(Debug)]
pub struct Texture {
image: Image,
is_normal_map: bool,
}
impl Texture {
pub fn new(image: Image, is_normal_map: bool) -> Self {
Self {
image,
is_normal_map,
}
}
pub fn pixel_at_exact(&self, x: usize, y: usize) -> Color {
// TODO: Debug asserts?
let x = match x {
n if n < self.image.width => n,
_ => self.image.width - 1,
};
let y = match y {
n if n < self.image.height => n,
_ => self.image.height - 1,
};
self.image.data[y * self.image.width + x]
}
/// Returns a pixel at the given coordinate. For non-lattice coordinates,
/// bi-linear interpolation of the image is done.
pub fn pixel_at(&self, u: f64, v: f64) -> Color {
debug_assert!(0.0 <= u && u <= 1.0, "u must be between 0 and 1");
debug_assert!(0.0 <= v && v <= 1.0, "u must be between 0 and 1");
// Slide 121
let x = u * (self.image.width - 1) as f64;
let y = v * (self.image.height - 1) as f64;
let i = x.floor();
let j = y.floor();
let alpha = x - i;
let beta = y - j;
let i = i as usize;
let j = j as usize;
(1.0 - alpha) * (1.0 - beta) * self.pixel_at_exact(i, j)
+ (alpha) * (1.0 - beta) * self.pixel_at_exact(i + 1, j)
+ (1.0 - alpha) * (beta) * self.pixel_at_exact(i, j + 1)
+ (alpha) * (beta) * self.pixel_at_exact(i + 1, j + 1)
}
}

View file

@ -1,184 +0,0 @@
use std::f64::EPSILON;
use anyhow::Result;
use nalgebra::{Matrix2, Vector2, Vector3};
use ordered_float::NotNan;
use crate::ray::Ray;
use crate::utils::{cross, dot};
use crate::{Point, Vector};
use super::illumination::IntersectionContext;
use super::Scene;
#[derive(Debug)]
pub struct Triangle {
/// Indexes into the scene's vertex list
pub vertices: Vector3<usize>,
/// Indexes into the scene's normal coordinates list
pub normals: Option<Vector3<usize>>,
/// Indexes into the scene's texture coordinates list
pub textures: Option<Vector3<usize>>,
}
impl Triangle {
pub fn intersects_ray_at(
&self,
scene: &Scene,
ray: &Ray,
) -> Result<Option<IntersectionContext>> {
let (p0, e1, e2) = self.basis_vectors(scene);
// Solve for the plane equation coefficients A, B, C, D such that:
//
// $$
// Ax + By + Cz + D = 0
// $$
let n = cross(e1, e2);
let a = n.x;
let b = n.y;
let c = n.z;
// Sub in p0 to solve for D
let d = -(a * p0.x + b * p0.y + c * p0.z);
// Find the intersection point
let time = {
let (x0, y0, z0, xd, yd, zd) =
match (ray.origin.as_slice(), ray.direction.as_slice()) {
([x0, y0, z0], [xd, yd, zd]) => (x0, y0, z0, xd, yd, zd),
_ => unreachable!("lol rip no tuple interface"),
};
let denom = a * xd + b * yd + c * zd;
if denom == 0.0 {
// The ray is parallel to the plane, so there is no intersection point.
return Ok(None);
};
-(a * x0 + b * y0 + c * z0 + d) / denom
};
let time = NotNan::new(time)?;
let point = ray.eval(*time);
// Use barycentric coordinates to determine if the point is inside of the
// triangle
// p = p0 + beta * e1 + gamma * e2
// Using the whack linear algebra approach derived on slide 57
let ep = point - p0;
let p = Vector2::new(dot(e1, ep), dot(e2, ep));
let (alpha, beta, gamma) =
self.compute_barycentric_coordinates(scene, p)?;
// Each of alpha, beta, and gamma must be between 0 and 1
if ![alpha, beta, gamma]
.into_iter()
.all(|v| 0.0 - EPSILON <= v && v <= 1.0 + EPSILON)
{
return Ok(None);
}
let normal = match self.normals {
// If surface normals are provided, then interpolate the normals to do
// smooth shading
Some(normals) => {
let n0 = scene.vertex_normals[normals.x];
let n1 = scene.vertex_normals[normals.y];
let n2 = scene.vertex_normals[normals.z];
(alpha * n0 + beta * n1 + gamma * n2).normalize()
}
None => n.normalize(),
};
Ok(Some(IntersectionContext {
time,
point,
normal,
}))
}
/// Get the (u, v) texture coordinates corresponding to the point provided in
/// the intersection context
pub fn get_texture_coord(
&self,
scene: &Scene,
ctx: &IntersectionContext,
) -> Result<(f64, f64)> {
let texture_coordinates = match self.textures {
Some(v) => v,
None => {
bail!("Textured triangle requested without providing coordinates")
}
};
let p0 = scene.texture_vertices[texture_coordinates.x];
let p1 = scene.texture_vertices[texture_coordinates.y];
let p2 = scene.texture_vertices[texture_coordinates.z];
let p = self.convert_point(scene, ctx.point);
let (alpha, beta, gamma) =
self.compute_barycentric_coordinates(scene, p)?;
let u = alpha * p0.x + beta * p1.x + gamma * p2.x;
let v = alpha * p0.y + beta * p1.y + gamma * p2.y;
Ok((u, v))
}
fn convert_point(&self, scene: &Scene, point: Point) -> Vector2<f64> {
let (p0, e1, e2) = self.basis_vectors(scene);
let ep = point - p0;
Vector2::new(dot(e1, ep), dot(e2, ep))
}
/// Fetch the corners of the triangles from the scene
#[inline]
fn corner_coordinates(&self, scene: &Scene) -> (Point, Point, Point) {
let p0 = scene.triangle_vertices[self.vertices.x];
let p1 = scene.triangle_vertices[self.vertices.y];
let p2 = scene.triangle_vertices[self.vertices.z];
(p0, p1, p2)
}
/// Get the new basis vectors using p0 as the origin. Returns (p0, e1, e2)
#[inline]
fn basis_vectors(&self, scene: &Scene) -> (Vector, Vector, Vector) {
let (p0, p1, p2) = self.corner_coordinates(scene);
let e1 = p1 - p0;
let e2 = p2 - p0;
(p0, e1, e2)
}
/// Compute barycentric coordinates
fn compute_barycentric_coordinates(
&self,
scene: &Scene,
p: Vector2<f64>,
) -> Result<(f64, f64, f64)> {
let (_, e1, e2) = self.basis_vectors(scene);
let d = Matrix2::new(dot(e1, e1), dot(e1, e2), dot(e2, e1), dot(e2, e2));
let d_inv = match d.try_inverse() {
Some(v) => v,
// TODO: Whack
None => bail!("No inverse..."),
};
let sol = d_inv * p;
let beta = sol.x;
let gamma = sol.y;
// Slide 46
let alpha = 1.0 - beta - gamma;
Ok((alpha, beta, gamma))
}
}

View file

@ -2,7 +2,7 @@ use anyhow::Result;
use nalgebra::{Matrix3, Vector3};
use ordered_float::NotNan;
use crate::{Vector};
use crate::Point;
/// Finds the minimum of an iterator of f64s, ignoring any NaN values
#[inline]
@ -30,17 +30,17 @@ where
/// Dot-product between two 3D vectors.
#[inline]
pub fn dot(a: Vector, b: Vector) -> f64 {
pub fn dot(a: Point, b: Point) -> f64 {
a.x * b.x + a.y * b.y + a.z * b.z
}
/// Cross-product between two 3D vectors.
#[inline]
pub fn cross(a: Vector, b: Vector) -> Vector {
pub fn cross(a: Point, b: Point) -> Point {
let x = a.y * b.z - a.z * b.y;
let y = a.z * b.x - a.x * b.z;
let z = a.x * b.y - a.y * b.x;
Vector::new(x, y, z)
Point::new(x, y, z)
}
/// Calculate the rotation matrix between the 2 given vectors

173
assignment-1c/writeup.md Normal file
View file

@ -0,0 +1,173 @@
---
geometry: margin=2cm
output: pdf_document
---
# Raytracer part B
This project implements a raytracer with Blinn-Phong illumination and shadows
implemented. The primary formula that is used by this implementation is:
\begin{equation}
I_{\lambda} =
k_a O_{d\lambda} +
\sum_{i=1}^{n_\textrm{lights}} \left(
f_\textrm{att} \cdot
S_i \cdot
IL_{i\lambda} \left[
k_d O_{d\lambda} \max ( 0, \vec{N} \cdot \vec{L_i} ) +
k_s O_{s\lambda} \max ( 0, \vec{N} \cdot \vec{H_i} )^n
\right]
\right)
\end{equation}
Where:
- $I_{\lambda}$ is the final illumination of the pixel on an object
- $k_a$ is the material's ambient reflectivity
- $k_d$ is the material's diffuse reflectivity
- $k_s$ is the material's specular reflectivity
- $n_\textrm{lights}$ is the number of lights
- $f_\textrm{att}$ is the light attenuation factor (1.0 if attenuation is not on)
- $S_i$ is the shadow coefficient for light $i$
- $IL_{i\lambda}$ is the intensity of light $i$
- $O_{d\lambda}$ is the object's diffuse color
- $O_{s\lambda}$ is the object's specular color
- $\vec{N}$ is the normal vector to the object's surface
- $\vec{L_i}$ is the direction from the intersection point to the light $i$
- $\vec{H_i}$ is halfway between the direction to the light $i$ and the
direction to the viewer
- $n$ is the exponent for the specular component
In this report we will look through how these various factors influence the
rendering of the scene. All the images along with their source `.txt` files,
rendered `.ppm` files, and converted `.png` files can be found in the `examples`
directory of this handin.
## Varying $k_a$
$k_a$ is the strength of ambient light. It's used as a coefficient for the
object's diffuse color, which keeps a constant value independent of the
positions of the object, light, and the viewer. In the image below, I varied
$k_a$ between 0.2 and 1. Note how the overall color of the ball increases or
decreases in brightness when all other factors remain constant.
![Varying $k_a$](examples/ka-demo.png){width=360px}
\
## Varying $k_d$
$k_d$ is the strength of the diffuse component. It also affects an object's
diffuse color, but at a strength that's affected by how much of it faces the
light. Much like the dark side of the moon, the parts of the object that aren't
pointed at the light will not receive as much of the light's influence. In the
image below, I varied $k_d$ between 0.2 and 1. Note how the part pointed to the
light changes the strength of the brightness as all other factors remain
constant.
![Varying $k_d$](examples/kd-demo.png){width=360px}
\
## Varying $k_s$
$k_s$ is the specular strength. It uses the object's specular color, which is
like its reflective component. When there is a large specular $k_s$, there's a
shine that appears on the object with a greater intensity. In the image below, I
varied $k_s$ between 0.2 and 1. Note how the whiteness of the light is more
reflective in higher $k_s$ values as other factors remain constant.
![Varying $k_s$](examples/ks-demo.png){width=360px}
\
## Varying $n$
$n$ is the exponent saying how big the radius of the specular highlight should
be. In the equation, increasing the exponent usually leads to smaller shines. In
the image below, I varied $n$ between 2 and 100. Note how the size of the shine
is the same intensity, but more focused but covers a smaller area as $n$
increases.
![Varying $n$](examples/n-demo.png){width=360px}
\
## Multiple lights
Multiple lights are handled by multiplying each light against an intensity
level, and then added together. Unfortunately, this means that the intensity of
each light can't be too bright. We rely on the image to not use lights that are
too bright. Because this may result in color values above 1.0, the final value
is clamped against 1.0. Below is an example of a scene with two lights; one to
the left and one to the right:
![Multiple lights](examples/multiple-lights-demo.png){width=360px}
\
## Shadows
Shadows are implemented by pointing a second ray between the intersection point
of the original view ray and each light. If the light has something obstructing
it in the middle, the light's effect is not used.
The soft shadow effect is realized by jittering rays across an area. In my
implementation, a jitter radius of about 1.0 is used, and 75 rays are shot into
uniformly sampled points within that radius. This also has the side effect that
rays that are closer to the original ray are sampled more frequently. Each of
these rays produces either 0 or 1 depending on if it was obstructed by the
object. Taking the proportion of rays that hit as a coefficient for the shadow,
we can get some soft shadow effects like this:
![Soft shadows](examples/soft-shadow-demo.png){width=360px}
\
## Light attenuation
Light attenuation is when more of the light is applied for objects that are
closer to a particular light source. The function that's applied is an inverse
quadratic formula with respect to the distance the object is from the light:
\begin{equation}
f_\textrm{att}(d) = \frac{1}{c_1 + c_2 d + c_3 d^2}
\end{equation}
Where:
- $f_\textrm{att}$ is the attenuation factor
- $d$ is the distance the object is from the light
- $c_1$, $c_2$, and $c_3$ are user-supplied coefficients
As you can see below, the effect of the light drops off with the distance from
the light (light coming from the left):
![Light attenuation](examples/attenuation-demo.png){width=360px}
\
## Depth Cueing
Depth cueing is when the objects further from the viewer have a lower opacity to
"fade" into the background in some sense. A good example of this can be seen in
the image below; note how the objects are less and less bright the further they
are away from the eye.
![Depth cueing](examples/depth-cueing-demo.png){width=360px}
\
## Shortcomings of the model
The Phong formula is just a model of how light works, and doesn't actually
represent reality. There's not actually rays physically escaping our eyes and
hitting objects; it's actually the other way around, but computing it that way
would not be efficient since we would be factoring in a lot of rays that don't
ever get rendered.
Also, one needs to take care to use reasonable constants. For example, if using
a different specular light color than the diffuse color, then it may produce
some bizarre lighting effects that may not actually look right compare to
reality.
# Arbitrary Objects
Here is an example scene with some objects that demonstrates some of the
features of the raytracer.
![Objects in the scene](examples/objects.png){width=360px}
\

View file

@ -1,2 +0,0 @@
[registries.crates-io]
protocol = "sparse"

View file

@ -1,11 +0,0 @@
/target
/assignment-1*
/raytracer1*
/examples/*.png
*.ppm
*.zip
*.pdf
perf.data*
flamegraph.svg
showcase.png
/out.log

1068
assignment-1d/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,30 +0,0 @@
[package]
name = "assignment-1d"
authors = ["Michael Zhang <zhan4854@umn.edu>"]
version = "0.1.0"
edition = "2021"
[features]
release-handin = ["tracing/release_max_level_info"]
[[bin]]
name = "raytracer1d"
path = "src/main.rs"
[dependencies]
anyhow = { version = "1.0.68", features = ["backtrace"] }
base64 = "0.21.0"
clap = { version = "4.1.4", features = ["cargo", "derive"] }
contracts = "0.6.3"
derivative = "2.2.0"
either = "1.8.1"
generator = "0.7.2"
itertools = "0.10.5"
nalgebra = "0.32.1"
num = { version = "0.4.0", features = ["serde"] }
ordered-float = "3.4.0"
rand = "0.8.5"
rayon = "1.6.1"
tracing = "0.1.37"
tracing-appender = "0.2.2"
tracing-subscriber = { version = "0.3.16", features = ["json"] }

View file

@ -1,53 +0,0 @@
.PHONY: all clean
.PRECIOUS: $(EXAMPLES_PPM)
DEBUG :=
CARGO_FLAGS := --release
RAYTRACER_FLAGS :=
DOCKER := docker
ZIP := zip
PANDOC := pandoc
CONVERT := convert
ifeq ($(DEBUG),1)
RAYTRACER_FLAGS += -vvvv
else
endif
HANDIN := ./hw1d.michael.zhang.zip
BINARY := ./raytracer1d
SOURCES := Cargo.toml $(shell find -name "*.rs")
EXAMPLES := $(shell find examples -name "*.txt")
EXAMPLES_PPM := $(patsubst %.txt,%.ppm,$(EXAMPLES))
EXAMPLES_PNG := $(patsubst %.txt,%.png,$(EXAMPLES))
all: $(HANDIN)
$(BINARY): $(SOURCES)
mkdir -p target/docker
$(DOCKER) run \
--rm \
-v "$(shell pwd)":/usr/src/myapp \
-v cargo-registry:/usr/local/cargo \
--user "$(shell id -u)":"$(shell id -g)" \
-w /usr/src/myapp \
-e CARGO_TARGET_DIR=/usr/src/myapp/target/docker \
rust \
cargo build --profile release-handin --features release-handin
mv target/docker/release-handin/raytracer1d $@
$(HANDIN): $(BINARY) Makefile Cargo.toml Cargo.lock README.md $(EXAMPLES_PNG) $(EXAMPLES_PPM)
$(ZIP) -r $@ src examples $^
examples/%.ppm: examples/%.txt $(SOURCES)
cargo run $(CARGO_FLAGS) -- -o $@ $(RAYTRACER_FLAGS) $<
examples/%.png: examples/%.ppm
convert $< $@
clean:
rm -rf target/docker \
$(HANDIN) $(BINARY) \
$(EXAMPLES_PPM) $(EXAMPLES_PNG)

View file

@ -1,29 +0,0 @@
# Raycaster
## Bundle contents
Writeup is located at `/writeup.pdf`.
The binary can be found at `/raytracer1b`. Run `./raytracer1b --help` to see
how to use it. The binary has been built using the Rust Docker image, which
should have an environment similar to CSELabs. If there is trouble running the
binary, try building from source, as documented below.
Examples are found in the `examples` directory. The text files are the input
sources, and the ppm files are the corresponding outputs. They have been
generated by running this program. For convenience, pngs have also been provided
using imagemagick.
## Showcase image
The showcase image can be found at `/showcase.png`.
## Building from source
The Makefile currently uses Docker to produce a more consistent build. If you
have a Rust+Cargo toolchain installed locally, it's also possible to build the
source using just:
cargo build --release
The binary will be found in `target/release`.

View file

@ -1,2 +0,0 @@
# Necessary files
!/earthtexture.ppm

View file

@ -1,22 +0,0 @@
eye 0 5 0
viewdir 0 0 1
updir 0 1 0
hfov 45
imsize 1080 1080
bkgcolor 0.5 0.7 0.9 1
light 0 -1 0 0 1 1 1
mtlcolor 1 1 1 1 1 1 0.2 0.4 0.6 60 0.2 1.5
sphere 1.25 8 15 1
sphere 0 6 15 1
sphere 1.5 4 15 1
mtlcolor 1 1 1 1 1 1 0.2 0.4 0.6 60 1 0
sphere -1.5 4 15 1
mtlcolor 1 1 1 1 1 1 0.2 0.8 0 20 1 0
v 10 0 5
v -10 0 5
v -10 0 25
v 10 0 25
f 1 2 3
f 1 3 4

View file

@ -1,12 +0,0 @@
eye 0 0 0
viewdir 1 0 0
updir 0 0 1
hfov 60
imsize 1080 1080
bkgcolor 0.5 0.7 0.9 1
light 0 94820000 -28450000 1 0 0 0
mtlcolor 1 1 1 1 1 1 0 0.05 0.1 80 1 30
sphere 3 0 0 1
mtlcolor 0 1 0 1 1 1 1 0 0 1 1 0
texture harbor.ppm
sphere 0 0 0 100000000

View file

@ -1,18 +0,0 @@
eye 0 5 0
viewdir 0 0 1
updir 0 1 0
hfov 45
imsize 1080 1080
bkgcolor 0.5 0.7 0.9 1
light 0 -1 0 0 1 1 1
mtlcolor 1 1 1 1 1 1 0.2 0.4 0.6 60 1 0
sphere -1.5 4 15 1
mtlcolor 1 1 1 1 1 1 0.2 0.8 0 20 1 0
v 10 0 5
v -10 0 5
v -10 0 25
v 10 0 25
f 1 2 3
f 1 3 4

View file

@ -1,18 +0,0 @@
eye 0 5 0
viewdir 0 0 1
updir 0 1 0
hfov 45
imsize 128 128
bkgcolor 0.5 0.7 0.9 1
light 0 -1 0 0 1 1 1
mtlcolor 1 1 1 1 1 1 0.2 0.4 0.6 60 0.2 1.5
sphere 0 6 15 3
mtlcolor 1 1 1 1 1 1 0.2 0.8 0 20 1 0
v 10 0 5
v -10 0 5
v -10 0 25
v 10 0 25
f 1 2 3
f 1 3 4

View file

@ -1,45 +0,0 @@
imsize 640 480
eye 0 0 15
viewdir 0 0 -1
hfov 60
updir 0 1 0
bkgcolor 0.5 0.5 0.5
depthcueing 0.5 0.5 0.5 1 0.4 60 0
light 10 10 -10 1 1 1 1
mtlcolor 0.5 1 0.5 1 1 1 0.2 1 0.1 5 0.5 1
sphere 4.5 4.5 -20 4.5
sphere -4.5 -4.5 -20 4.5
mtlcolor 1 0.5 0.5 1 1 1 0.2 0.8 0 5 0.8 1
sphere -10 0 -30 4
sphere -20 0 -30 4
sphere -30 0 -30 4
sphere -40 0 -30 4
sphere 0 0 -30 4
sphere 10 0 -30 4
sphere 20 0 -30 4
sphere 30 0 -30 4
sphere 40 0 -30 4
sphere -10 -10 -30 4
sphere -20 -10 -30 4
sphere -30 -10 -30 4
sphere -40 -10 -30 4
sphere 0 -10 -30 4
sphere 10 -10 -30 4
sphere 20 -10 -30 4
sphere 30 -10 -30 4
sphere 40 -10 -30 4
sphere -10 10 -30 4
sphere -20 10 -30 4
sphere -30 10 -30 4
sphere -40 10 -30 4
sphere 0 10 -30 4
sphere 10 10 -30 4
sphere 20 10 -30 4
sphere 30 10 -30 4
sphere 40 10 -30 4

View file

@ -1,29 +0,0 @@
imsize 1366 768
eye 0 5 -2
viewdir 0 -0.2 1
hfov 60
updir 0 1 0
bkgcolor 0.4 0.5 0.6
depthcueing 0.5 0.5 0.5 1 0.4 60 0
light 0 -1 0 0 1 1 1
mtlcolor 1 0.6 0.6 1 1 1 0.2 0.4 0.6 60 0.9 2
sphere -1.5 4 15 1
mtlcolor 0.6 0.6 1 1 1 1 0.2 0.4 0.6 60 0.9 2
sphere 0 -1 12 2
mtlcolor 0.6 1 0.6 1 1 1 0.2 0.4 0.6 60 1 2
sphere 6 8 20 3
mtlcolor 1 1 0.6 1 1 1 0.2 0.4 0.6 60 0.9 2
sphere -6 -8 20 4
mtlcolor 0.7 0.6 0.8 0.5 0.5 0.5 0.2 0.8 0.1 20 0.5 1.5
v 10 0 5
v -10 0 5
v -10 0 25
v 10 0 25
f 1 2 3
f 1 3 4

View file

@ -1,11 +0,0 @@
eye 0 0 10
viewdir 0 0 -1
updir 0 1 0
hfov 60
imsize 512 512
bkgcolor 0.5 0.7 0.9 1
light 0 -1 0 0 1 1 1
mtlcolor 0.7 0.2 0.7 1 1 1 0 0.05 0.1 80 0.5 1.5
sphere 1 1 -6 3
sphere -1 -1 1 3

View file

@ -1,127 +0,0 @@
use std::{
fs::File,
io::{BufRead, BufReader, Read, Write},
path::Path,
};
use anyhow::{Context, Result};
use generator::{done, Gn};
use itertools::Itertools;
use nalgebra::Vector3;
/// A pixel color represented by a red, green, and blue value in the range 0-1.
pub type Color = Vector3<f64>;
/// A representation of an image
#[derive(Derivative)]
#[derivative(Debug)]
pub struct Image {
/// Width in pixels
pub width: usize,
/// Height in pixels
pub height: usize,
/// Pixel data in row-major form.
#[derivative(Debug = "ignore")]
pub data: Vec<Color>,
}
impl Image {
pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
let file = File::open(path)
.with_context(|| format!("Could not open file at {path:?}"))?;
Self::read(file)
}
/// Parse image from a Read
pub fn read(r: impl Read + Send) -> Result<Self> {
let mut line_reader = BufReader::new(r);
let mut header = String::new();
line_reader
.read_line(&mut header)
.context("Could not read line")?;
let parts = header.trim().split(" ").collect::<Vec<_>>();
let width = parts[1].parse::<usize>().context("Could not read width")?;
let height = parts[2].parse::<usize>().context("Could not read height")?;
let max_value = parts[3]
.parse::<usize>()
.context("Could not read max value")?;
// Generator for reading numbers
let numbers = Gn::<()>::new_scoped(move |mut s| {
macro_rules! gen_try {
($expr:expr, $str:expr $(, $($arg:expr),* $(,)?)?) => {
match $expr {
Ok(v) => v,
Err(e) => {
s.yield_(
Err(anyhow::Error::from(e))
.with_context(|| format!($str $(, $($arg,)*)?)),
);
done!();
}
}
};
}
for line in line_reader.lines() {
let line = gen_try!(line, "Could not read line");
let parts = line.trim().split_whitespace();
for part in parts {
let int =
gen_try!(part.parse::<u64>(), "Could not read int from: {}", part);
s.yield_(Ok(int));
}
}
done!()
});
let mut data = Vec::with_capacity(width * height);
for mut chunk in &(numbers).chunks(3) {
let (r, g, b) = match chunk.next_tuple() {
Some(v) => v,
None => bail!("Not enough elements"),
};
let r = r? as f64 / max_value as f64;
let g = g? as f64 / max_value as f64;
let b = b? as f64 / max_value as f64;
let color = Color::new(r, g, b);
data.push(color);
}
Ok(Image {
width,
height,
data,
})
}
/// 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);
w.write_all(header.as_bytes())?;
// Pixel data
assert_eq!(self.data.len(), self.width * self.height);
for pixel in self.data.iter() {
let pixel = pixel * 256.0;
let red = pixel.x as u8;
let green = pixel.y as u8;
let blue = pixel.z as u8;
let pixel = format!("{red} {green} {blue}\n");
w.write_all(pixel.as_bytes())?;
}
Ok(())
}
}

View file

@ -1,22 +0,0 @@
use nalgebra::{Vector2, Vector3};
#[macro_use]
extern crate anyhow;
#[macro_use]
extern crate contracts;
#[macro_use]
extern crate derivative;
#[macro_use]
extern crate tracing;
pub mod image;
pub mod ray;
pub mod scene;
pub mod utils;
// Creating a bunch of aliases here to make it more obvious which one I'm
// expecting a variable to be
pub type Point2 = Vector2<f64>;
pub type Point = Vector3<f64>;
pub type Vector = Vector3<f64>;

View file

@ -1,204 +0,0 @@
#[macro_use]
extern crate anyhow;
#[macro_use]
extern crate tracing;
use std::path::PathBuf;
use std::{fs::File, str::FromStr};
use anyhow::Result;
use assignment_1d::{image::Image, ray::Ray, scene::Scene};
use clap::{ArgAction, Parser};
use rayon::prelude::{IntoParallelIterator, ParallelIterator};
use tracing::metadata::LevelFilter;
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::{
fmt::Layer, prelude::__tracing_subscriber_SubscriberExt,
util::SubscriberInitExt,
};
/// Simple raytracer with Blinn-Phong illumination and shadowing.
#[derive(Parser)]
#[clap(author, version, about, long_about = None)]
struct Opt {
/// Path to the input file to use.
#[clap()]
input_path: PathBuf,
/// Path to the output (defaults to the same file name as the input except
/// with an extension of .ppm)
#[clap(short = 'o', long = "output")]
output_path: Option<PathBuf>,
/// Log output in json
#[clap(long = "json")]
use_json: bool,
/// Which file to send logs to (stderr by default)
#[clap(long = "log-output")]
log_output: Option<PathBuf>,
/// Force parallel projection to be used
#[clap(long = "parallel")]
force_parallel: bool,
/// Override distance from eye
#[clap(long = "distance", default_value = "1.0")]
distance: f64,
/// Verbosity
#[clap(short, long, action = ArgAction::Count)]
verbosity: u8,
/// Evaluate at a single pixel
#[clap(short, long = "render-pixel")]
render_pixel: Option<RenderPixel>,
}
fn main() -> Result<()> {
let opt = Opt::parse();
let _guard = setup_logging(&opt);
// Rename the output file if it's not provided
let out_file = opt
.output_path
.unwrap_or_else(|| opt.input_path.with_extension("ppm"));
let mut scene = Scene::from_input_file(&opt.input_path)?;
let distance = opt.distance;
// Force-override parallel projection
if opt.force_parallel {
scene.parallel_projection = true;
}
// Translate image pixels to real-world 3d coords
let translate_pixel = scene.pixel_translation_function(distance);
let evaluate_at_pixel = |px, py| {
let span = trace_span!("main_loop", px = px, py = py);
let _enter = span.enter();
let pixel_in_space = translate_pixel(px, py);
let ray_start = if scene.parallel_projection {
// For a parallel projection, we'll just take the view direction and
// subtract it from the target point. This means every single
// ray will be viewed from a point at infinity, rather than a single eye
// position.
let n = scene.view_dir.normalize();
let view_dir = n * distance;
pixel_in_space - view_dir
} else {
scene.eye_pos
};
let ray = Ray::from_endpoints(ray_start, pixel_in_space);
// let res= rayon::spawn(|| scene.trace_single_ray(ray, 0));
scene.trace_single_ray(scene.eye_pos, ray, 0)
};
// For debugging purposes!
if let Some(RenderPixel(px, py)) = opt.render_pixel {
let pixel_color = evaluate_at_pixel(px, py)?;
println!("Pixel color: {pixel_color}");
return Ok(());
}
// Generate a parallel iterator for pixels
// The iterator preserves order and uses row-major order
let pixels_iter = (0..scene.image_height)
.into_par_iter()
.flat_map(|y| (0..scene.image_width).into_par_iter().map(move |x| (x, y)));
// Loop through every single pixel of the output file
let pixels = pixels_iter
.map(|(px, py)| evaluate_at_pixel(px, py))
.collect::<Result<Vec<_>>>()?;
// Construct and emit image
let image = Image {
width: scene.image_width,
height: scene.image_height,
data: pixels,
};
{
let file = File::create(out_file)?;
image.write(file)?;
}
Ok(())
}
#[derive(Clone)]
struct RenderPixel(usize, usize);
impl FromStr for RenderPixel {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parts = s.split(",").collect::<Vec<_>>();
ensure!(parts.len() == 2, "must be a pair");
let x = parts[0].parse::<usize>()?;
let y = parts[1].parse::<usize>()?;
Ok(RenderPixel(x, y))
}
}
/// A little bit of engineering to make it easy to write conditional builders
/// for logging setup because the tracing-subscriber crate for some reason
/// decided it would be a good idea to have all of its builders be polymorphic?
macro_rules! logsetup_if {
($ident:ident , $cond:expr , $iftrue:expr , $iffalse:expr => { $($body:tt)* }) => {
if ($cond) {
let $ident = $iftrue;
$($body)*
} else {
let $ident = $iffalse;
$($body)*
}
};
}
fn setup_logging(opt: &Opt) -> Option<WorkerGuard> {
let mut result = None;
let level_filter = match opt.verbosity {
0 => LevelFilter::ERROR,
1 => LevelFilter::WARN,
2 => LevelFilter::INFO,
3 => LevelFilter::DEBUG,
_ => LevelFilter::TRACE,
};
let layer = Layer::default();
logsetup_if! (layer, opt.use_json, layer.json(), layer => {
let layer = layer
.with_target(false)
.with_timer(tracing_subscriber::fmt::time::uptime())
.with_level(true);
logsetup_if! (layer, opt.log_output.is_some(), {
let log_output = opt.log_output.clone().unwrap();
let file_appender = tracing_appender::rolling::never(".", log_output);
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
result = Some(guard);
layer.with_writer(non_blocking)
}, layer => {
tracing_subscriber::registry()
.with(layer)
.with(level_filter)
.init();
});
});
result
}

View file

@ -1,60 +0,0 @@
use std::fmt;
use crate::{Point, Vector};
/// A normalized parametric Ray of the form (origin + direction * time)
///
/// That means at any time t: f64, the point represented by origin + direction *
/// time occurs on the ray. This is pretty much a (time -> point) function.
pub struct Ray {
/// The point in space where the ray started
pub origin: Point,
/// The direction the ray is headed
pub direction: Vector,
}
impl fmt::Debug for Ray {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"({:.2}, {:.2}, {:.2}) + t * ({:.2}, {:.2}, {:.2})",
self.origin.x,
self.origin.y,
self.origin.z,
self.direction.x,
self.direction.y,
self.direction.z,
)
}
}
impl Ray {
pub fn new(origin: Point, direction: Vector) -> Self {
Ray { origin, direction }
}
/// Construct a ray from endpoints
pub fn from_endpoints(start: Point, end: Point) -> Self {
let delta = (end - start).normalize();
Ray {
origin: start,
direction: delta,
}
}
/// Evaluate the ray at a certain point in time, yielding a point
pub fn eval(&self, time: f64) -> Point {
self.origin + self.direction * time
}
/// Check if any of the components is NaN
pub fn has_nan(&self) -> bool {
self.origin.x.is_nan()
|| self.origin.y.is_nan()
|| self.origin.z.is_nan()
|| self.direction.x.is_nan()
|| self.direction.y.is_nan()
|| self.direction.z.is_nan()
}
}

View file

@ -1,208 +0,0 @@
use anyhow::Result;
use ordered_float::NotNan;
use crate::utils::compute_rotation_matrix;
use crate::Vector;
use crate::{ray::Ray, Point};
use super::illumination::IntersectionContext;
use super::Scene;
#[derive(Debug)]
pub struct Cylinder {
pub center: Point,
pub direction: Vector,
pub radius: f64,
pub length: f64,
}
impl Cylinder {
/// Given a cylinder, returns the first time at which this ray intersects the
/// cylinder.
///
/// If there is no intersection point, returns None.
pub fn intersects_ray_at(
&self,
_: &Scene,
ray: &Ray,
) -> Result<Option<IntersectionContext>> {
// Determine rotation matrix for turning the cylinder upright along the
// Z-axis
let target_direction = Vector::new(0.0, 0.0, 1.0);
let rotation_matrix =
compute_rotation_matrix(self.direction, target_direction)?;
let inverse_rotation_matrix =
rotation_matrix.try_inverse().ok_or_else(|| {
anyhow!("Rotation matrix for some reason does not have an inverse?")
})?;
// Transform all parameters according to this rotation matrix
let rotated_cylinder_center = rotation_matrix * self.center;
let rotated_ray_origin = rotation_matrix * ray.origin;
let rotated_ray_direction = rotation_matrix * ray.direction;
// Now that we know the cylinder is upright, we can start checking against
// the formula:
//
// (ox + t*rx - cx)^2 + (oy + t*ry - cy)^2 = r^2
//
// where o{xy} is the ray origin, r{xy} is the ray direction, and c{xy} is
// the cylinder center. The z will be taken care of after the fact. To
// solve, we must put it into the form At^2 + Bt + c = 0. The variables
// are:
//
// A: rx^2 + ry^2
// B: 2(rx(ox - cx) + ry(oy - cy))
// C: (cx - ox)^2 + (cy - oy)^2 - r^2
let (a, b, c) = {
let o = rotated_ray_origin;
let r = rotated_ray_direction;
let c = rotated_cylinder_center;
(
r.x.powi(2) + r.y.powi(2),
2.0 * (r.x * (o.x - c.x) + r.y * (o.y - c.y)),
(c.x - o.x).powi(2) + (c.y - o.y).powi(2) - self.radius.powi(2),
)
};
let discriminant = b * b - 4.0 * a * c;
let possible_side_solutions = match discriminant {
// Discriminant < 0, means the equation has no solutions.
d if d < 0.0 => vec![],
// Discriminant == 0
d if d == 0.0 => vec![-b / 2.0 * a],
// Discriminant > 0, 2 solutions available.
d if d > 0.0 => {
vec![
(-b + discriminant.sqrt()) / (2.0 * a),
(-b - discriminant.sqrt()) / (2.0 * a),
]
}
// Probably hit some NaN or Infinity value due to faulty inputs...
_ => bail!("Invalid determinant value: {discriminant}"),
};
// Filter out solutions that don't have a valid Z position.
let side_solutions = possible_side_solutions.into_iter().filter_map(|t| {
let ray_point = ray.eval(t);
let rotated_ray_point = rotation_matrix * ray_point;
let z = rotated_ray_point.z - rotated_cylinder_center.z;
// Check to see if z is between -len/2 and len/2
if z.abs() > self.length / 2.0 {
return None;
}
let time = NotNan::new(t).ok()?;
// The point on the center of the cylinder that corresponds to the z-axis
// point of the intersection
let center_at_z = {
let mut center_point = rotation_matrix * ray_point;
center_point.x = rotated_cylinder_center.x;
center_point.y = rotated_cylinder_center.y;
inverse_rotation_matrix * center_point
};
let normal = (ray_point - center_at_z).normalize();
Some(IntersectionContext {
time,
point: ray_point,
normal,
exiting: todo!(),
})
});
// We also need to add solutions for the two ends of the cylinder, which
// uses a similar method except backwards: check intersection points
// with the correct z-plane and then see if the points are within the
// circle.
//
// Luckily, this means we only need to care about one dimension at first,
// and don't need to perform the quadratic equation method above.
//
// oz + t * rz = cz +- (len / 2)
// t = (-oz + cz +- (len / 2)) / rz
let possible_z_intersections = {
let o = rotated_ray_origin;
let r = rotated_ray_direction;
let c = rotated_cylinder_center;
if r.z == 0.0 {
Vec::new() // No solutions here
} else {
vec![
(-o.z + c.z + self.length / 2.0) / r.z,
(-o.z + c.z - self.length / 2.0) / r.z,
]
}
};
let end_solutions = possible_z_intersections.into_iter().filter_map(|t| {
let ray_point = ray.eval(t);
let rotated_point = rotation_matrix * ray_point;
// Filter out all the solutions where the intersection point does not lie
// in the circle
if rotated_point.x.powi(2) + rotated_point.y.powi(2) > self.radius.powi(2)
{
return None;
}
let normal_rotated =
Vector::new(0.0, 0.0, rotated_point.z - rotated_cylinder_center.z)
.normalize();
let normal = inverse_rotation_matrix * normal_rotated;
let time = NotNan::new(t).ok()?;
Some(IntersectionContext {
time,
point: ray_point,
normal,
exiting: todo!(),
})
});
let solutions = side_solutions
.into_iter()
.chain(end_solutions.into_iter())
// Remove any t < 0, since that means it's behind the viewer and we
// can't see it.
.filter(|ctx| *ctx.time >= 0.0);
// Return the minimum solution
Ok(solutions.min_by_key(|ctx| ctx.time))
}
}
#[cfg(test)]
mod tests {
use crate::{ray::Ray, scene::Scene, Point, Vector};
use super::Cylinder;
#[test]
fn test_cylinder() {
let cylinder = Cylinder {
center: Point::new(0.0, 0.0, 0.0),
direction: Vector::new(0.0, 1.0, 0.0),
radius: 3.0,
length: 4.0,
};
let eye = Point::new(0.0, 3.0, 3.0);
let end = Point::new(0.0, 2.0, 2.0);
let ray = Ray::from_endpoints(eye, end);
let scene = Scene::default();
let _res = cylinder.intersects_ray_at(&scene, &ray);
// panic!("Result: {res:?}");
}
}

View file

@ -1,193 +0,0 @@
use std::fmt::Debug;
use crate::image::Color;
use crate::utils::cross;
use crate::Point;
use super::Scene;
#[derive(Debug)]
pub struct Rect {
pub upper_left: Point,
pub upper_right: Point,
pub lower_left: Point,
pub lower_right: Point,
}
#[derive(Debug)]
pub struct Material {
pub diffuse_color: Point,
pub specular_color: Point,
pub k_a: f64,
pub k_d: f64,
pub k_s: f64,
pub exponent: f64,
/// Opacity
pub alpha: f64,
/// Index of refraction
pub eta: f64,
}
#[derive(Debug)]
pub enum LightKind {
/// A point light source exists at a point and emits light in all directions
Point {
location: Point,
/// Whether light attenuation is enabled for this light
attenuation: Option<Attenuation>,
},
/// A directional light source exists at an infinitely far location but emits
/// light in a specific direction
Directional { direction: Point },
}
#[derive(Debug)]
pub struct Light {
/// The kind of light source, as well as its associated information
pub kind: LightKind,
/// The color, or intensity, of the light source
pub color: Point,
}
impl Light {
/// Get the unit directional vector pointing from the given point to this
/// light source
pub fn direction_from(&self, point: Point) -> Point {
match self.kind {
LightKind::Point { location, .. } => location - point,
LightKind::Directional { direction } => -direction,
}
.normalize()
}
}
#[derive(Debug)]
pub struct DepthCueing {
/// The color to tint (should be the same as the background color, to avoid
/// bizarre visual effects)
pub color: Color,
/// Proportion of the color influenced by the depth tint when the distance is
/// maxed (caps at 1.0)
pub a_max: f64,
/// Proportion of the color influenced by the depth tint when the distance is
/// at the minimum (caps at 1.0)
pub a_min: f64,
/// The max distance that should be affected by the depth tint
pub dist_max: f64,
/// The min distance that should be affected by the depth tint
pub dist_min: f64,
}
/// A default implementation here needs to simulate what would happen if there
/// was no depth cueing. In this case, if we have both a_max and a_min be 1.0,
/// then the original color will always apply and there will be no need for
/// depth color
impl Default for DepthCueing {
fn default() -> Self {
Self {
color: Default::default(),
a_max: 1.0,
a_min: 1.0,
dist_max: 0.0,
dist_min: 0.0,
}
}
}
/// Light attenuation dropoff coefficients
#[derive(Debug)]
pub struct Attenuation {
pub c1: f64,
pub c2: f64,
pub c3: f64,
}
/// A default implementation here needs to simulate what would happen if there
/// was no light attenuation specified. In this case, c1 would just be a
/// constant of 1 and all the coefficients for anything involving distance would
/// be zeroed out
impl Default for Attenuation {
fn default() -> Self {
Self {
c1: 1.0,
c2: 0.0,
c3: 0.0,
}
}
}
impl Scene {
/// Determine the boundaries of the viewing window in world coordinates
pub fn compute_viewing_window(&self, distance: f64) -> Rect {
// Compute viewing directions
let u = cross(self.view_dir, self.up_dir).normalize();
let v = cross(u, self.view_dir).normalize();
// Compute dimensions of viewing window based on field of view
let viewing_width = {
// Divide the angle in 2 since we are trying to use trig rules so we must
// get it from a right triangle
let half_hfov = self.hfov.to_radians() / 2.0;
// tan(hfov / 2) = w / 2d
let w_over_2d = half_hfov.tan();
// To find the viewing width we must multiply by 2d now
w_over_2d * 2.0 * distance
};
let aspect_ratio = self.image_width as f64 / self.image_height as f64;
let viewing_height = viewing_width / aspect_ratio;
// Compute viewing window corners
let n = self.view_dir.normalize();
#[rustfmt::skip] // Don't format, or else this line wraps over
let view_window = Rect {
upper_left: self.eye_pos + n * distance - u * (viewing_width / 2.0) + v * (viewing_height / 2.0),
upper_right: self.eye_pos + n * distance + u * (viewing_width / 2.0) + v * (viewing_height / 2.0),
lower_left: self.eye_pos + n * distance - u * (viewing_width / 2.0) - v * (viewing_height / 2.0),
lower_right: self.eye_pos + n * distance + u * (viewing_width / 2.0) - v * (viewing_height / 2.0),
};
view_window
}
/// Create a pixel translation function based on the viewing window of the
/// current scene
pub fn pixel_translation_function(
&self,
distance: f64,
) -> impl Fn(usize, usize) -> Point {
let view_window = self.compute_viewing_window(distance);
let dx = view_window.upper_right - view_window.upper_left;
let pixel_base_x = dx / self.image_width as f64;
let dy = view_window.lower_left - view_window.upper_left;
let pixel_base_y = dy / self.image_height as f64;
// The final function to be returned
move |px: usize, py: usize| {
let x_component = pixel_base_x * px as f64;
let y_component = pixel_base_y * py as f64;
// Without adding this, we would be getting the top-left of the pixel's
// rectangle. We want the center, so add half of the pixel size as
// well.
let center_offset = (pixel_base_x + pixel_base_y) / 2.0;
view_window.upper_left + x_component + y_component + center_offset
}
}
}

View file

@ -1,519 +0,0 @@
use std::iter;
use anyhow::Result;
use ordered_float::NotNan;
use rand::Rng;
use rayon::prelude::{
IndexedParallelIterator, IntoParallelIterator, IntoParallelRefIterator,
ParallelIterator,
};
use crate::{
image::Color,
ray::Ray,
utils::{
compute_reflection_ray, compute_refraction_lengths, dot, RefractionResult,
},
Point, Vector,
};
use super::{
data::{DepthCueing, Light, LightKind, Material},
object::Object,
Scene,
};
// TODO: Is this a good constant?
const JITTER_CONST: f64 = 0.05;
const ZERO_COLOR: Color = Color::new(0.0, 0.0, 0.0);
// Soft shadows: jitter some rays here to somewhere close to the
// actual location as well, and measure the proportion
// of them that intersect any objects
const SOFT_SHADOW_JITTER_RADIUS: f64 = 1.0;
const JITTER_RAYS: usize = 75;
impl Scene {
/// Determine the color that should be used to fill this pixel.
///
/// - material_idx is the index into the materials list.
/// - intersection_context contains information on vectors where the
/// intersection occurred
///
/// Also known as Shade_Ray in the slides.
pub fn compute_pixel_color(
&self,
obj_idx: usize,
object: &Object,
origin: Point,
incident_ray: Ray,
intersection_context: IntersectionContext,
depth: usize,
) -> Result<Color> {
let span = trace_span!("compute_pixel_color", intersection = ?intersection_context, incident_ray=?incident_ray);
let _enter = span.enter();
let material = match self.materials.get(object.material_idx) {
Some(v) => v,
None => bail!("Material index not found."),
};
let diffuse_color = match object.texture_idx {
Some(texture_idx) => {
let (u, v) = object
.kind
.get_texture_coord(&self, &intersection_context)?;
let texture = match self.textures.get(texture_idx) {
Some(v) => v,
None => bail!("Texture index not found."),
};
texture.pixel_at(u, v)
}
None => material.diffuse_color,
};
let ambient_component = material.k_a * diffuse_color;
// Diffuse and specular lighting for each separate light
let diffuse_and_specular: Color = self
.lights
.par_iter()
.map(|light| {
// The vector pointing in the direction of the light
let light_direction = light.direction_from(intersection_context.point);
let normal = intersection_context.normal.normalize(); // reflection_normal();
// Viewer direction is no longer towards the eye, but to the last origin point, so that
// transmitted rays reflect properly
// let viewer_direction = self.eye_pos - intersection_context.point;
let incoming_ray_direction = (intersection_context.point - origin).normalize();
let halfway_direction =
((light_direction + incoming_ray_direction) / 2.0).normalize();
let diffuse_component = material.k_d
* diffuse_color
* dot(normal, light_direction).max(0.0);
let specular_component = material.k_s
* material.specular_color
* dot(normal, halfway_direction)
.max(0.0)
.powf(material.exponent);
// Shadow coefficient between 0 and 1 to control how bright this pixel
// should be from being in the shadow of another object (could be
// between 0 and 1 when applying soft shadows)
let shadow_coefficient = self.compute_shadow_coefficient(
obj_idx,
intersection_context.point,
light,
);
let attenuation_coefficient = match &light.kind {
LightKind::Point {
location,
attenuation: Some(att),
} => {
let dist = (location - intersection_context.point).norm();
let denom = att.c1 + att.c2 * dist + att.c3 * dist.powi(2);
if denom == 0.0 {
warn!("Light attenuation coefficients produced a denominator of 0. Check your inputs...");
1.0 // Some kind of graceful fallback here
} else {
1.0 / denom
}
}
_ => 1.0,
};
let diffuse_and_specular = diffuse_component + specular_component;
attenuation_coefficient
* shadow_coefficient
* light.color.component_mul(&diffuse_and_specular)
})
.sum();
let (eta_i, eta_t) = match intersection_context.exiting {
// true => (material.eta, 1.0),
_ => (1.0, material.eta),
};
let specular_reflection_component = if material.k_s == 0.0 {
ZERO_COLOR
} else {
self.compute_specular_reflection(
&intersection_context,
&incident_ray,
depth,
)?
};
let transparency_component = if eta_t < 1.0 || material.alpha == 1.0 {
ZERO_COLOR
} else {
self.compute_transparency(
&intersection_context,
&incident_ray,
eta_i,
eta_t,
depth,
)?
};
let fresnel_coefficient = self.compute_fresnel_coefficient(
material,
&incident_ray.direction,
intersection_context.normal,
);
// This is the result of the Phong illumination equation.
let color = (ambient_component + diffuse_and_specular) + {
// This part is all the transparency + reflection stuff
fresnel_coefficient * specular_reflection_component
+ (1.0 - fresnel_coefficient)
* (1.0 - material.alpha)
* transparency_component
};
debug!(
last_time_component = ?(ambient_component + diffuse_and_specular),
?specular_reflection_component,
?transparency_component,
?fresnel_coefficient,
?color,
"color result"
);
// Apply depth cueing to the result
let a_dc = {
// Distance from the viewer
let d_obj = (intersection_context.point - self.eye_pos).norm();
let DepthCueing {
dist_max,
dist_min,
a_max,
a_min,
..
} = self.depth_cueing;
if d_obj < dist_min {
a_max
} else if d_obj < dist_max {
a_min + (a_max - a_min) * (dist_max - d_obj) / (dist_max - dist_min)
} else {
a_min
}
};
let color = a_dc * color + (1.0 - a_dc) * self.depth_cueing.color;
// Need to clamp the result so none of the components goes over 1
let clamped_result = color.map(|v| v.min(1.0));
Ok(clamped_result)
}
/// Perform another ray casting to see if there are any objects obstructing
/// the light source to this particular point
pub fn compute_shadow_coefficient(
&self,
obj_idx: usize,
point: Point,
light: &Light,
) -> f64 {
let light_direction = light.direction_from(point);
let ray = Ray {
origin: point,
direction: light_direction.normalize(),
};
// Small helper for iterating over all of the objects in the scene except
// for the current one
let other_objects = self
.objects
.par_iter()
.enumerate()
.filter(|(i, _)| *i != obj_idx);
#[derive(Clone, Copy)]
struct ShadowResult {
transparent_coefficient: f64,
shadow_opacity: f64,
}
// Get the list of intersections with all the other objects in the scene
// This list will be a set of opacities
let intersections = other_objects
.filter_map(|(_, object)| {
let intersection_context =
match object.kind.intersects_ray_at(&self, &ray) {
Ok(v) => v,
Err(err) => {
error!("Error while performing shadow casting: {err}");
None
}
}?;
let intersection_time = *intersection_context.time;
let material = &self.materials[object.material_idx];
match light.kind {
// In the case of point lights, we must check to see if both t > 0 and
// t is less than the time it took to even get to the light.
LightKind::Point { location, .. } => {
let light_time = (location - ray.origin).norm();
if intersection_time <= 0.0 || intersection_time >= light_time {
None
} else {
Some(ShadowResult {
transparent_coefficient: material.alpha,
shadow_opacity: self
.compute_soft_shadow_coefficient(location, point, object),
})
}
}
// In the case of directional lights, only t > 0 needs to be checked
LightKind::Directional { .. } => {
if intersection_time <= 0.0 {
None
} else {
// The object obstructed the directional light, which means (1 -
// alpha) amount of light passes through
Some(ShadowResult {
transparent_coefficient: material.alpha,
// Opacity is 0 because there's no jitter from an infinitely far
// away light source
shadow_opacity: 0.0,
}) // complete obstruction
}
}
}
})
.collect::<Vec<_>>();
match intersections.is_empty() {
true => 1.0,
false => {
// let average =
// intersections.iter().map(|s| s.shadow_opacity).sum::<f64>()
// / intersections.len() as f64;
// (1 - a_0) * (1 - a_1) * (...)
let transparency = intersections
.iter()
.map(|s| 1.0 - s.transparent_coefficient)
.product::<f64>();
// debug!(
// "average {average}, transparency {transparency} = {}",
// average * transparency
// );
transparency
}
}
}
fn compute_soft_shadow_coefficient(
&self,
light_location: Point,
original_intersection_point: Point,
object: &Object,
) -> f64 {
let mut rng = rand::thread_rng();
let locations = iter::repeat_with(|| {
let x = rng.gen_range(0.0..SOFT_SHADOW_JITTER_RADIUS);
let y = rng.gen_range(0.0..SOFT_SHADOW_JITTER_RADIUS);
let z = rng.gen_range(0.0..SOFT_SHADOW_JITTER_RADIUS);
let delta = Vector::new(x, y, z);
light_location + delta
})
.take(JITTER_RAYS)
.collect::<Vec<_>>();
let num_obstructed_rays = locations
.into_par_iter()
.filter(|location| {
let direction = (location - original_intersection_point).normalize();
let ray = Ray {
origin: original_intersection_point,
direction,
};
let intersection_context =
match object.kind.intersects_ray_at(&self, &ray) {
Ok(Some(v)) => v,
Ok(None) => return false,
Err(err) => {
error!("Error while performing shadow casting: {err}");
return false;
}
};
let light_time = (location - ray.origin).norm();
let intersection_time = *intersection_context.time;
0.0 < intersection_time && intersection_time < light_time
})
.count();
(JITTER_RAYS - num_obstructed_rays) as f64 / JITTER_RAYS as f64
}
fn compute_fresnel_coefficient(
&self,
material: &Material,
incident_ray: &Vector,
normal: Vector,
) -> f64 {
let mut cos_theta_i = dot(*incident_ray, normal);
if cos_theta_i < 0.0 {
cos_theta_i = dot(*incident_ray, -normal);
}
let f0 = ((material.eta - 1.0) / (material.eta + 1.0)).powi(2);
let fr = f0 * 1.0 + (1.0 - f0) * (1.0 - cos_theta_i).powi(5);
if fr < 0.0 || fr > 1.0 {
warn!(
eta = material.eta,
cos_theta_i, f0, fr, "fresnel coefficient outside of 0 - 1"
);
}
fr
}
fn compute_specular_reflection(
&self,
intersection_context: &IntersectionContext,
incident_ray: &Ray,
depth: usize,
) -> Result<Color> {
let reflection_ray = compute_reflection_ray(
incident_ray.direction.clone(),
intersection_context.reflection_normal().normalize(),
);
let origin = intersection_context.point;
let origin = origin + JITTER_CONST * reflection_ray;
let ray = Ray::new(origin, reflection_ray);
self.trace_single_ray(origin, ray, depth + 1)
}
fn compute_transparency(
&self,
intersection_context: &IntersectionContext,
incident_ray: &Ray,
eta_i: f64,
eta_t: f64,
depth: usize,
) -> Result<Color> {
let span =
trace_span!("compute_transparency", eta_i = eta_i, eta_t = eta_t);
let _enter = span.enter();
// Fix the normal direction to account for exiting a material
let normal = intersection_context.reflection_normal().normalize();
let i = incident_ray.direction.normalize();
assert!(eta_t != 0.0, "wtf eta_t is 0");
// This comes in two parts: one is reflection and one is refraction. The
// refraction component will only occur if the angle remains below the
// critical angle. The reflection amount is added in proportion to the
// Fresnel coefficient.
// First, calculate whether or not refraction is happening. If total
// internal reflection occurs, then there's no refraction since there's
// no ray escaping the medium.
let value =
match compute_refraction_lengths(normal, &incident_ray, eta_i, eta_t) {
Some(RefractionResult {
cos_theta_i,
sin_theta_i: _,
sin_theta_t,
cos_theta_t,
}) => {
// Now that we identified that there is refraction happening, transmit
// a ray through the material at the scene behind it in the
// new direction.
// Calculate refraction direction
let a = normal * cos_theta_t;
let s_direction = cos_theta_i * normal - i;
let m_unit = s_direction.normalize();
let b = m_unit * sin_theta_t;
let t = a + b;
// Jitter a bit to reduce acne
// TODO: Is this a good constant?
let origin = intersection_context.point;
let origin = origin + JITTER_CONST * t;
let ray = Ray::new(origin, t);
self.trace_single_ray(origin, ray, depth + 1)?
}
// No extra color from the transmitted ray, since it's completely
// reflected
None => ZERO_COLOR,
};
// Calculate reflection
// let sin_theta_t = (eta_i / eta_t) * sin_theta_i;
// let cos_theta_t = (1.0 - sin_theta_t.powi(2)).sqrt();
// let fresnel_coefficient = self.compute_fresnel_coefficient(&material, i,
// n);
Ok(value)
}
}
/// Information about an intersection
#[derive(Derivative)]
#[derivative(Debug, PartialEq, PartialOrd, Ord)]
pub struct IntersectionContext {
/// The time of the intersection in the parametric ray
///
/// Unfortunately, IEEE floats in Rust don't have total ordering, because
/// NaNs violate ordering properties. The way to remedy this is to ensure we
/// don't have NaNs by wrapping it into this type, which then implements
/// total ordering.
pub time: NotNan<f64>,
/// The intersection point.
#[derivative(PartialEq = "ignore", Ord = "ignore")]
pub point: Point,
/// The normal vector protruding from the surface of the object at the
/// intersection point
#[derivative(PartialEq = "ignore", Ord = "ignore")]
pub normal: Vector,
/// Is this ray exiting the material at the intersection point?
pub exiting: bool,
}
impl Eq for IntersectionContext {}
impl IntersectionContext {
// If we're exiting the material, the normal should face the other direction
// since that's how the reflection works
pub fn reflection_normal(&self) -> Vector {
match self.exiting {
true => -self.normal,
false => self.normal,
}
}
}

View file

@ -1,371 +0,0 @@
pub mod triangle_vertex;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use anyhow::{Context, Result};
use itertools::Itertools;
use nalgebra::Vector3;
use crate::{
image::{Color, Image},
scene::{
cylinder::Cylinder,
data::{Attenuation, Light, LightKind, Material},
input_file::triangle_vertex::TriangleVertex,
object::{Object, ObjectKind},
sphere::Sphere,
texture::{NormalMap, Texture},
triangle::Triangle,
Scene,
},
Point, Vector,
};
use super::data::DepthCueing;
impl Scene {
/// Parse the input file into a scene
pub fn from_input_file(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
// Scope the read so the file is dropped and closed immediately after the
// contents have been read to memory
let contents = {
let mut contents = String::new();
let mut file = File::open(path)?;
file.read_to_string(&mut contents)?;
contents
};
let mut scene = Scene::default();
let mut material_idx = None;
let mut texture_idx = None;
for line in contents.lines() {
// Comments :)
let line = line.trim();
if line.starts_with("#") {
continue;
}
// Split lines into words. `parts' is an iterator, which is consumed upon
// iterating, rather than collected into a Vec
let mut parts = line.split_whitespace();
// The keyword is the very first space-separated substring, and tells us
// how to interpret the rest
let keyword = match parts.next() {
Some(v) => v,
None => continue,
};
/// Short for "read", macro for reading something from the iterator and
/// converting it into the appropriate format given by $ty. For this to
/// work, $ty must implement Construct
macro_rules! r {
($ty:ty) => {
<$ty>::construct(&mut parts, ())
.with_context(|| format!("Could not parse {} ({}:{})", stringify!($ty), file!(), line!()))?
};
($ty:ty, $($ex:expr),* $(,)?) => {
<$ty>::construct(&mut parts, $($ex,)*)
.with_context(|| format!("Could not parse {} ({}:{})", stringify!($ty), file!(), line!()))?
};
}
/// Shortcut for unwrapping one of the state `Option's
macro_rules! u {
($expr:expr) => {
match $expr {
Some(v) => v,
None => {
bail!(
"Each object must be preceded by a `{}` line",
stringify!($expr)
)
}
}
};
}
match keyword {
"imsize" => {
scene.image_width = r!(usize);
scene.image_height = r!(usize);
}
"projection" => {
if let Some("parallel") = parts.next() {
scene.parallel_projection = true;
}
}
"eye" => scene.eye_pos = r!(Vector3<f64>),
"viewdir" => scene.view_dir = r!(Vector3<f64>),
"updir" => scene.up_dir = r!(Vector3<f64>),
"hfov" => scene.hfov = r!(f64),
"bkgcolor" => scene.bkg_color = r!(Color),
// light x y z w r g b
// attlight x y z w r g b c1 c2 c3
"light" | "attlight" => {
let vec3 = r!(Vector3<f64>);
let w = r!(usize);
let color = r!(Color);
let attenuation = match keyword == "attlight" {
true => {
let c = r!(Vector3<f64>);
Some(Attenuation {
c1: c.x,
c2: c.y,
c3: c.z,
})
}
false => None,
};
let kind = match w as usize {
0 => LightKind::Directional { direction: vec3 },
1 => LightKind::Point {
location: vec3,
attenuation,
},
_ => bail!("Invalid w; must be either 0 or 1"),
};
let light = Light { kind, color };
scene.lights.push(light);
}
// depthcueing dcr dcg dcb amax amin distmax distmin
"depthcueing" => {
let color = r!(Color);
let a_max = r!(f64);
let a_min = r!(f64);
let dist_max = r!(f64);
let dist_min = r!(f64);
scene.depth_cueing = DepthCueing {
color,
a_max,
a_min,
dist_max,
dist_min,
};
}
// mtlcolor Odr Odg Odb Osr Osg Osb ka kd ks n alpha eta
"mtlcolor" => {
let diffuse_color = r!(Color);
let specular_color = r!(Color);
let k_a = r!(f64);
let k_d = r!(f64);
let k_s = r!(f64);
let exponent = r!(f64);
let alpha = r!(f64);
let eta = r!(f64);
let material = Material {
diffuse_color,
specular_color,
k_a,
k_d,
k_s,
exponent,
alpha,
eta,
};
let idx = scene.materials.len();
material_idx = Some(idx);
scene.materials.push(material);
}
"sphere" => {
let center = r!(Point);
let radius = r!(f64);
scene.objects.push(Object {
kind: ObjectKind::Sphere(Sphere { center, radius }),
material_idx: u!(material_idx),
texture_idx,
});
}
"cylinder" => {
let center = r!(Point);
let direction = r!(Vector);
let radius = r!(f64);
let length = r!(f64);
scene.objects.push(Object {
kind: ObjectKind::Cylinder(Cylinder {
center,
direction,
radius,
length,
}),
material_idx: u!(material_idx),
texture_idx,
});
}
// Assignment 1C: Triangles and textures
// v x y z
"v" => scene.triangle_vertices.push(r!(Vector)),
// vn nx ny nz
"vn" => scene.vertex_normals.push(r!(Vector)),
// f v1 v2 v3
// f v1//n1 v2//n2 v3//n3
"f" => {
let v1 = r!(TriangleVertex);
let v2 = r!(TriangleVertex);
let v3 = r!(TriangleVertex);
let vs = Vector3::new(v1, v2, v3);
let vertices = vs.map(|v| v.vertex_idx);
let normals = vs.map(|v| v.normal_idx);
let normals = match normals.iter().filter(|o| o.is_some()).count() {
0 => None,
n if n == vs.len() => Some(normals.map(|o| o.unwrap())),
_ => bail!("Cannot mix and match having a normal index"),
};
let textures = vs.map(|v| v.texture_idx);
let textures = match textures.iter().filter(|o| o.is_some()).count() {
0 => None,
n if n == vs.len() => Some(textures.map(|o| o.unwrap())),
_ => bail!("Cannot mix and match having a normal index"),
};
let triangle = Triangle {
vertices,
normals,
textures,
};
scene.objects.push(Object {
kind: ObjectKind::Triangle(triangle),
material_idx: u!(material_idx),
texture_idx,
});
}
"texture" => {
let input_parent = path.parent().unwrap().to_path_buf();
let path = match parts.next() {
Some(s) => input_parent.join(s),
None => bail!("Did not provide path."),
};
let idx = scene.textures.len();
texture_idx = Some(idx);
let image = Image::from_file(path)?;
let texture = Texture::new(image);
scene.textures.push(texture);
}
"bump" => {
let input_parent = path.parent().unwrap().to_path_buf();
let path = match parts.next() {
Some(s) => input_parent.join(s),
None => bail!("Did not provide path."),
};
let idx = scene.textures.len();
texture_idx = Some(idx);
let image = Image::from_file(path)?;
let normal_map = NormalMap::new(image);
scene.normal_maps.push(normal_map);
}
_ => bail!("Unknown keyword {keyword}"),
}
}
Ok(scene)
}
}
pub trait Construct: Sized {
type Args;
/// Construct an element of this type from an iterator over strings.
fn construct<'a, I>(it: &mut I, args: Self::Args) -> Result<Self>
where
I: Iterator<Item = &'a str>;
}
impl<T: Construct> Construct for Option<T> {
type Args = T::Args;
fn construct<'a, I>(it: &mut I, args: Self::Args) -> Result<Self>
where
I: Iterator<Item = &'a str>,
{
let mut peeker = it.peekable();
if peeker.peek().is_none() {
return Ok(None);
}
T::construct(&mut peeker, args).map(Some)
}
}
macro_rules! impl_construct {
($ty:ty) => {
impl Construct for $ty {
type Args = ();
fn construct<'a, I>(it: &mut I, _: Self::Args) -> Result<Self>
where
I: Iterator<Item = &'a str>,
{
let item = match it.next() {
Some(v) => v,
None => bail!(
"Ran out of items for {} ({}:{})",
stringify!($ty),
file!(),
line!()
),
};
Ok(item.parse()?)
}
}
};
}
impl_construct!(f64);
impl_construct!(usize);
impl Construct for Vector3<f64> {
type Args = ();
fn construct<'a, I>(it: &mut I, _: Self::Args) -> Result<Self>
where
I: Iterator<Item = &'a str>,
{
let (x, y, z) = match it.next_tuple() {
Some(v) => v,
None => bail!("Expected 3 values"),
};
let x: f64 = x.parse()?;
let y: f64 = y.parse()?;
let z: f64 = z.parse()?;
Ok(Vector3::new(x, y, z))
}
}

View file

@ -1,49 +0,0 @@
use anyhow::Result;
use itertools::Itertools;
use super::Construct;
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct TriangleVertex {
pub vertex_idx: usize,
pub normal_idx: Option<usize>,
pub texture_idx: Option<usize>,
}
impl Construct for TriangleVertex {
type Args = ();
fn construct<'a, I>(it: &mut I, _: Self::Args) -> Result<Self>
where
I: Iterator<Item = &'a str>,
{
let s = match it.next() {
Some(v) => v,
None => bail!("Waiting on another"),
};
// Note: indexed by 1 not 0, so we will just do the subtraction
// here to avoid having to deal with it later
let parts = s.split("/").collect_vec();
ensure!(parts.len() >= 1 && parts.len() <= 3);
let vertex_idx: usize = parts[0].parse::<usize>()? - 1;
let texture_idx =
match parts.get(1).and_then(|s| (!s.is_empty()).then(|| *s)) {
Some(s) => Some(s.parse::<usize>()? - 1),
None => None,
};
let normal_idx =
match parts.get(2).and_then(|s| (!s.is_empty()).then(|| *s)) {
Some(s) => Some(s.parse::<usize>()? - 1),
None => None,
};
Ok(TriangleVertex {
vertex_idx,
texture_idx,
normal_idx,
})
}
}

View file

@ -1,52 +0,0 @@
pub mod cylinder;
pub mod data;
pub mod illumination;
pub mod input_file;
pub mod object;
pub mod sphere;
pub mod texture;
pub mod tracing;
pub mod triangle;
use crate::image::Color;
use crate::{Point, Point2, Vector};
use self::data::{Attenuation, DepthCueing, Light, Material};
use self::object::Object;
use self::texture::{NormalMap, Texture};
#[derive(Debug, Default)]
pub struct Scene {
pub eye_pos: Point,
pub view_dir: Vector,
pub up_dir: Vector,
/// Horizontal field of view (in degrees)
pub hfov: f64,
pub parallel_projection: bool,
pub image_width: usize,
pub image_height: usize,
/// Background color
pub bkg_color: Color,
pub depth_cueing: DepthCueing,
pub attenuation: Attenuation,
pub materials: Vec<Material>,
pub lights: Vec<Light>,
pub objects: Vec<Object>,
/// List of textures
pub textures: Vec<Texture>,
/// List of normal maps (Extra credit)
pub normal_maps: Vec<NormalMap>,
/// Coordinates into a texture image
pub texture_vertices: Vec<Point2>,
/// Triangle vertices
pub triangle_vertices: Vec<Point>,
pub vertex_normals: Vec<Vector>,
}

View file

@ -1,61 +0,0 @@
use anyhow::Result;
use crate::ray::Ray;
use super::cylinder::Cylinder;
use super::illumination::IntersectionContext;
use super::sphere::Sphere;
use super::triangle::Triangle;
use super::Scene;
/// An object in the scene
#[derive(Debug)]
pub struct Object {
pub kind: ObjectKind,
/// Index into the scene's material color list
pub material_idx: usize,
/// If this object has a texture, this is the index into the texture list
pub texture_idx: Option<usize>,
}
#[derive(Debug)]
pub enum ObjectKind {
Sphere(Sphere),
Cylinder(Cylinder),
Triangle(Triangle),
}
impl ObjectKind {
/// Determine where the ray intersects this object, returning the earliest
/// time this happens. Returns None if no intersection occurs.
///
/// Also known as Trace_Ray in the slides, except not the part where it calls
/// Shade_Ray.
pub fn intersects_ray_at(
&self,
scene: &Scene,
ray: &Ray,
) -> Result<Option<IntersectionContext>> {
match self {
ObjectKind::Sphere(sphere) => sphere.intersects_ray_at(scene, ray),
ObjectKind::Cylinder(cylinder) => cylinder.intersects_ray_at(scene, ray),
ObjectKind::Triangle(triangle) => triangle.intersects_ray_at(scene, ray),
}
}
/// Get the (u, v) coordinates in the texture (between 0 and 1) that
/// corresponds to the intersection point
pub fn get_texture_coord(
&self,
scene: &Scene,
ctx: &IntersectionContext,
) -> Result<(f64, f64)> {
match self {
ObjectKind::Sphere(sphere) => sphere.get_texture_coord(ctx),
ObjectKind::Cylinder(cylinder) => todo!(),
ObjectKind::Triangle(triangle) => triangle.get_texture_coord(scene, ctx),
}
}
}

View file

@ -1,116 +0,0 @@
use std::f64::consts::PI;
use anyhow::Result;
use ordered_float::NotNan;
use crate::{ray::Ray, utils::min_f64, Point};
use super::illumination::IntersectionContext;
use super::Scene;
#[derive(Debug)]
pub struct Sphere {
pub center: Point,
pub radius: f64,
}
impl Sphere {
/// Given a sphere, returns the first time at which this ray intersects the
/// sphere.
///
/// If there is no intersection point, returns None.
pub fn intersects_ray_at(
&self,
_: &Scene,
ray: &Ray,
) -> Result<Option<IntersectionContext>> {
let a = ray.direction.norm();
let b = 2.0
* (ray.direction.x * (ray.origin.x - self.center.x)
+ ray.direction.y * (ray.origin.y - self.center.y)
+ ray.direction.z * (ray.origin.z - self.center.z));
let c = (ray.origin.x - self.center.x).powi(2)
+ (ray.origin.y - self.center.y).powi(2)
+ (ray.origin.z - self.center.z).powi(2)
- self.radius.powi(2);
let discriminant = b * b - 4.0 * a * c;
if discriminant.is_nan() {
warn!("WTF NAN");
}
let time = match discriminant {
// Discriminant < 0, means the equation has no solutions.
d if d < 0.0 => None,
// Discriminant == 0
d if d == 0.0 => Some(-b / (2.0 * a)),
d if d > 0.0 => {
let solution_1 = (-b + discriminant.sqrt()) / (2.0 * a);
let solution_2 = (-b - discriminant.sqrt()) / (2.0 * a);
let solutions = [solution_1, solution_2]
.into_iter()
// Remove any t < 0, since that means it's behind the viewer and we
// can't see it.
.filter(|t| *t >= 0.0);
// Return the minimum solution
min_f64(solutions)
}
// Probably hit some NaN or Infinity value due to faulty inputs...
_ => unreachable!("Invalid determinant value: {discriminant}"),
};
let time = match time.and_then(|t| NotNan::new(t).ok()) {
Some(v) => v,
None => return Ok(None),
};
let point = ray.eval(*time);
let normal = (point - self.center).normalize();
let exiting = {
// To figure out if we're exiting, just test if the origin is inside the
// sphere
let dx = ray.origin.x - self.center.x;
let dy = ray.origin.y - self.center.y;
let dz = ray.origin.z - self.center.z;
dx.powi(2) + dy.powi(2) + dz.powi(2) <= self.radius.powi(2)
};
/* let normal = match exiting {
true => -normal,
false => normal,
}; */
Ok(Some(IntersectionContext {
time,
point,
normal,
exiting,
}))
}
pub fn get_texture_coord(
&self,
ctx: &IntersectionContext,
) -> Result<(f64, f64)> {
// Reverse engineer the angles from the coordinate of the intersection
let cosp = (ctx.point.z - self.center.z) / self.radius;
let phi = cosp.acos();
let theta =
(ctx.point.y - self.center.y).atan2(ctx.point.x - self.center.x);
// Map theta and phi into 0 - 1 range
let v = phi / PI;
let u = 0.5 + theta / (2.0 * PI);
Ok((u, v))
}
}

View file

@ -1,64 +0,0 @@
use crate::{
image::{Color, Image},
Vector,
};
#[derive(Debug)]
pub struct Texture(Image);
impl Texture {
pub fn new(image: Image) -> Self {
Self(image)
}
pub fn pixel_at_exact(&self, x: usize, y: usize) -> Color {
// TODO: Debug asserts?
self.0.data[y * self.0.width + x]
}
/// Returns a pixel at the given coordinate. For non-lattice coordinates,
/// bi-linear interpolation of the image is done.
pub fn pixel_at(&self, u: f64, v: f64) -> Color {
debug_assert!(0.0 <= u && u <= 1.0, "u must be between 0 and 1");
debug_assert!(0.0 <= v && v <= 1.0, "u must be between 0 and 1");
// Slide 121
let x = u * (self.0.width - 1) as f64;
let y = v * (self.0.height - 1) as f64;
let i = x.floor();
let j = y.floor();
let alpha = x - i;
let beta = y - j;
let i = i as usize;
let j = j as usize;
(1.0 - alpha) * (1.0 - beta) * self.pixel_at_exact(i, j)
+ (alpha) * (1.0 - beta) * self.pixel_at_exact(i + 1, j)
+ (1.0 - alpha) * (beta) * self.pixel_at_exact(i, j + 1)
+ (alpha) * (beta) * self.pixel_at_exact(i + 1, j + 1)
}
}
#[derive(Debug)]
pub struct NormalMap(Image);
impl NormalMap {
pub fn new(image: Image) -> Self {
Self(image)
}
pub fn normal_vector_at_exact(&self, x: usize, y: usize) -> Vector {
let vec = self.0.data[y * self.0.width + x];
// So, according to the instructions, this should actually be a value
// between -1 and 1. However, we're reading this in through an image.
// I'm just going to do the lazy thing here (which theoretically
// actually saves cycles) by only doing the transformation when loading
// out of the image
vec.map(|value| 2.0 * value / 255.0 - 1.0)
}
}

View file

@ -1,66 +0,0 @@
use anyhow::Result;
use crate::{image::Color, ray::Ray, Point};
use super::Scene;
const MAX_RECURSION_DEPTH: usize = 10_usize;
impl Scene {
pub fn trace_single_ray(
&self,
origin: Point,
ray: Ray,
depth: usize,
) -> Result<Color> {
if depth > MAX_RECURSION_DEPTH {
return Ok(Color::new(0.0, 0.0, 0.0));
}
let span = trace_span!("trace_ray", ray = ?ray, depth = depth);
let _enter = span.enter();
let intersections = self
.objects
.iter()
.enumerate()
.filter_map(|(i, object)| {
match object.kind.intersects_ray_at(&self, &ray) {
Ok(Some(t)) => {
// Return both the t and the sphere, because we want to sort on
// the t but later retrieve attributes from the sphere
Some(Ok((i, t, object)))
}
Ok(None) => None,
Err(err) => {
error!("Error: {err}");
Some(Err(err))
}
}
})
.collect::<Result<Vec<_>>>()?;
// Sort the list of intersection times by the lowest one.
let earliest_intersection =
intersections.into_iter().min_by_key(|(_, t, _)| t.time);
info!("Ray {ray:?} intersected at: {earliest_intersection:?}");
Ok(match earliest_intersection {
// Take the object's material color
Some((obj_idx, intersection_context, object)) => self
.compute_pixel_color(
obj_idx,
object,
origin,
ray,
intersection_context,
depth,
)?,
// There was no intersection, so this should default to the scene's
// background color
None => self.bkg_color,
})
}
}

View file

@ -1,190 +0,0 @@
use std::f64::EPSILON;
use anyhow::Result;
use nalgebra::{Matrix2, Vector2, Vector3};
use ordered_float::NotNan;
use crate::ray::Ray;
use crate::utils::{cross, dot};
use crate::{Point, Vector};
use super::illumination::IntersectionContext;
use super::Scene;
#[derive(Debug)]
pub struct Triangle {
/// Indexes into the scene's vertex list
pub vertices: Vector3<usize>,
/// Indexes into the scene's normal coordinates list
pub normals: Option<Vector3<usize>>,
/// Indexes into the scene's texture coordinates list
pub textures: Option<Vector3<usize>>,
}
impl Triangle {
pub fn intersects_ray_at(
&self,
scene: &Scene,
ray: &Ray,
) -> Result<Option<IntersectionContext>> {
let (p0, e1, e2) = self.basis_vectors(scene);
// Solve for the plane equation coefficients A, B, C, D such that:
//
// $$
// Ax + By + Cz + D = 0
// $$
let n = cross(e1, e2);
let a = n.x;
let b = n.y;
let c = n.z;
// Sub in p0 to solve for D
let d = -(a * p0.x + b * p0.y + c * p0.z);
// Find the intersection point
let time = {
let (x0, y0, z0, xd, yd, zd) =
match (ray.origin.as_slice(), ray.direction.as_slice()) {
([x0, y0, z0], [xd, yd, zd]) => (x0, y0, z0, xd, yd, zd),
_ => unreachable!("lol rip no tuple interface"),
};
let denom = a * xd + b * yd + c * zd;
if denom == 0.0 {
// The ray is parallel to the plane, so there is no intersection point.
return Ok(None);
};
-(a * x0 + b * y0 + c * z0 + d) / denom
};
// Intersected the plane behind where the ray started
if time < 0.0 {
return Ok(None);
}
let time = NotNan::new(time)?;
let point = ray.eval(*time);
// Use barycentric coordinates to determine if the point is inside of the
// triangle
// p = p0 + beta * e1 + gamma * e2
// Using the whack linear algebra approach derived on slide 57
let ep = point - p0;
let p = Vector2::new(dot(e1, ep), dot(e2, ep));
let (alpha, beta, gamma) =
self.compute_barycentric_coordinates(scene, p)?;
// Each of alpha, beta, and gamma must be between 0 and 1
if ![alpha, beta, gamma]
.into_iter()
.all(|v| 0.0 - EPSILON <= v && v <= 1.0 + EPSILON)
{
return Ok(None);
}
let normal = match self.normals {
// If surface normals are provided, then interpolate the normals to do
// smooth shading
Some(normals) => {
let n0 = scene.vertex_normals[normals.x];
let n1 = scene.vertex_normals[normals.y];
let n2 = scene.vertex_normals[normals.z];
(alpha * n0 + beta * n1 + gamma * n2).normalize()
}
None => n.normalize(),
};
Ok(Some(IntersectionContext {
time,
point,
normal,
exiting: false,
}))
}
/// Get the (u, v) texture coordinates corresponding to the point provided in
/// the intersection context
pub fn get_texture_coord(
&self,
scene: &Scene,
ctx: &IntersectionContext,
) -> Result<(f64, f64)> {
let texture_coordinates = match self.textures {
Some(v) => v,
None => {
bail!("Textured triangle requested without providing coordinates")
}
};
let p0 = scene.texture_vertices[texture_coordinates.x];
let p1 = scene.texture_vertices[texture_coordinates.y];
let p2 = scene.texture_vertices[texture_coordinates.z];
let p = self.convert_point(scene, ctx.point);
let (alpha, beta, gamma) =
self.compute_barycentric_coordinates(scene, p)?;
let u = alpha * p0.x + beta * p1.x + gamma * p2.x;
let v = alpha * p0.y + beta * p1.y + gamma * p2.y;
Ok((u, v))
}
fn convert_point(&self, scene: &Scene, point: Point) -> Vector2<f64> {
let (p0, e1, e2) = self.basis_vectors(scene);
let ep = point - p0;
Vector2::new(dot(e1, ep), dot(e2, ep))
}
/// Fetch the corners of the triangles from the scene
#[inline]
fn corner_coordinates(&self, scene: &Scene) -> (Point, Point, Point) {
let p0 = scene.triangle_vertices[self.vertices.x];
let p1 = scene.triangle_vertices[self.vertices.y];
let p2 = scene.triangle_vertices[self.vertices.z];
(p0, p1, p2)
}
/// Get the new basis vectors using p0 as the origin. Returns (p0, e1, e2)
#[inline]
fn basis_vectors(&self, scene: &Scene) -> (Vector, Vector, Vector) {
let (p0, p1, p2) = self.corner_coordinates(scene);
let e1 = p1 - p0;
let e2 = p2 - p0;
(p0, e1, e2)
}
/// Compute barycentric coordinates
fn compute_barycentric_coordinates(
&self,
scene: &Scene,
p: Vector2<f64>,
) -> Result<(f64, f64, f64)> {
let (_, e1, e2) = self.basis_vectors(scene);
let d = Matrix2::new(dot(e1, e1), dot(e1, e2), dot(e2, e1), dot(e2, e2));
let d_inv = match d.try_inverse() {
Some(v) => v,
// TODO: Whack
None => bail!("No inverse..."),
};
let sol = d_inv * p;
let beta = sol.x;
let gamma = sol.y;
// Slide 46
let alpha = 1.0 - beta - gamma;
Ok((alpha, beta, gamma))
}
}

View file

@ -1,160 +0,0 @@
use anyhow::Result;
use nalgebra::{Matrix3, Vector3};
use ordered_float::NotNan;
use crate::{ray::Ray, Vector};
/// Finds the minimum of an iterator of f64s, ignoring any NaN values
#[inline]
pub fn min_f64<I>(i: I) -> Option<f64>
where
I: Iterator<Item = f64>,
{
i.filter_map(|i| NotNan::new(i).ok())
.min()
.map(|i| i.into_inner())
}
/// Finds the minimum of an iterator of f64s using the given predicate, ignoring
/// any NaN values
#[inline]
pub fn min_f64_by_key<I, F>(i: I, f: F) -> Option<f64>
where
I: Iterator<Item = f64>,
F: FnMut(&NotNan<f64>),
{
i.filter_map(|i| NotNan::new(i).ok())
.min_by_key(f)
.map(|i| i.into_inner())
}
/// Dot-product between two 3D vectors.
#[inline]
pub fn dot(a: Vector, b: Vector) -> f64 {
a.x * b.x + a.y * b.y + a.z * b.z
}
/// Cross-product between two 3D vectors.
#[inline]
pub fn cross(a: Vector, b: Vector) -> Vector {
let x = a.y * b.z - a.z * b.y;
let y = a.z * b.x - a.x * b.z;
let z = a.x * b.y - a.y * b.x;
Vector::new(x, y, z)
}
/// Calculate the rotation matrix between the 2 given vectors
///
/// Based on the method given [here][1].
///
/// [1]: https://math.stackexchange.com/a/897677
pub fn compute_rotation_matrix(
a: Vector3<f64>,
b: Vector3<f64>,
) -> Result<Matrix3<f64>> {
// Special case: if a and b are in the same direction, just return the
// identity matrix.
if a.normalize() == b.normalize() {
return Ok(Matrix3::identity());
}
let cos_t = dot(a, b);
let sin_t = cross(a, b).norm();
let g = Matrix3::new(cos_t, -sin_t, 0.0, sin_t, cos_t, 0.0, 0.0, 0.0, 1.0);
// New basis vectors
let u = a;
let v = (b - cos_t * a).normalize();
let w = cross(b, a);
// Not sure if this is required to be invertible?
let f_inverse = Matrix3::from_columns(&[u, v, w]);
let f = match f_inverse.try_inverse() {
Some(v) => v,
None => {
// So I ran into this case trying to compute the rotation matrix where one
// of the vector endpoints was (0, 0, 0). I'm pretty sure this case makes
// no sense in reality, which means if I ever encounter this case, I
// probably made a mistake somewhere before. So going to just error
// out here and screw recovering.
//
// println!("Failed to compute inverse matrix.");
// println!("- Initial: a = {a}, b = {b}");
// println!("- cos(t) = {cos_t}, sin(t) = {sin_t}");
// println!("- Basis: u = {u}, v = {v}, w = {w}");
bail!("Failed to compute inverse matrix of {f_inverse}\na = {a}\nb = {b}")
}
};
// if (f_inverse * g * f).norm() != 1.0 {
// bail!("WTF {}", (f_inverse * g * f).norm());
// }
Ok(f_inverse * g * f)
}
pub struct RefractionResult {
pub cos_theta_i: f64,
pub sin_theta_i: f64,
pub sin_theta_t: f64,
pub cos_theta_t: f64,
}
/// This function computes the 4 values:
///
/// - cos_theta_i
/// - sin_theta_i
/// - sin_theta_t
/// - cos_theta_t
///
/// If total internal reflection occurs, return None instead.
pub fn compute_refraction_lengths(
normal: Vector,
incident_ray: &Ray,
eta_i: f64,
eta_t: f64,
) -> Option<RefractionResult> {
let i = incident_ray.direction.normalize();
let cos_theta_i = dot(i, normal);
let sin_theta_i = (1.0 - cos_theta_i.powi(2)).sqrt();
if sin_theta_i * eta_i > eta_t {
info!("Total internal reflection encountered.");
return None;
}
let sin_theta_t = (eta_i / eta_t) * sin_theta_i;
let cos_theta_t = (1.0 - sin_theta_t.powi(2)).sqrt();
Some(RefractionResult {
cos_theta_i,
sin_theta_i,
sin_theta_t,
cos_theta_t,
})
}
#[allow(non_snake_case)]
pub fn compute_reflection_ray(incident_ray: Vector, normal: Vector) -> Vector {
let I = (-incident_ray).normalize();
let N = normal.normalize();
2.0 * dot(N, I) * N - I
}
#[cfg(test)]
mod tests {
use crate::{utils::compute_reflection_ray, Vector};
#[test]
fn test_reflection_ray() {
let incident_ray = Vector::new(2.0, -1.0, 2.0);
let normal = Vector::new(0.0, 1.0, 0.0);
assert_eq!(
compute_reflection_ray(incident_ray, normal),
Vector::new(2.0, 1.0, 2.0).normalize()
);
}
}

View file

@ -1,4 +0,0 @@
---
BasedOnStyle: LLVM
AlignAfterOpenBracket: BlockIndent

View file

@ -1,5 +0,0 @@
/build
/result*
.cache
hw2a.michael.zhang.zip

View file

@ -1,79 +0,0 @@
# Set the minimum required version of cmake for this project
cmake_minimum_required (VERSION 3.1)
set(CMAKE_CXX_STANDARD 17)
# Create a project called 'HW2a'
project(HW2a)
# Define in the C++ code what the variable "SRC_DIR" should be equal to the current_path/src
add_definitions( -DSRC_DIR="${CMAKE_CURRENT_SOURCE_DIR}/src" )
# Generate the `compile_commands.json` file.
set(CMAKE_EXPORT_COMPILE_COMMANDS ON CACHE INTERNAL "")
if(CMAKE_EXPORT_COMPILE_COMMANDS)
set(CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES
${CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES})
endif()
# Find OpenGL, and set link library names and include paths
find_package(OpenGL REQUIRED)
set(OPENGL_LIBRARIES ${OPENGL_gl_LIBRARY} ${OPENGL_glu_LIBRARY})
set(OPENGL_INCLUDE_DIRS ${OPENGL_INCLUDE_DIR})
include_directories(${OPENGL_INCLUDE_DIRS})
# Also disable building some of the extra things GLFW has (examples, tests, docs)
set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL " " FORCE)
set(GLFW_BUILD_TESTS OFF CACHE BOOL " " FORCE)
set(GLFW_BUILD_DOCS OFF CACHE BOOL " " FORCE)
# Now actually run cmake on the CMakeLists.txt file found inside of the GLFW directory
add_subdirectory(ext/glfw)
# Make a list of all the source files
set(
SOURCES
src/HW2a.cpp
ext/glad/src/glad.c
)
# Make a list of all the header files (optional-- only necessary to make them appear in IDE)
set(
INCLUDES
src/ShaderStuff.hpp
)
# Make a list of all of the directories to look in when doing #include "whatever.h"
set(
INCLUDE_DIRS
ext/
ext/glfw/include
ext/glad/include
)
set(
LIBS
glfw
${OPENGL_LIBRARIES}
)
# Define what we are trying to produce here (an executable), as
# well as what items are needed to create it (the header and source files)
add_executable(${PROJECT_NAME} ${SOURCES} ${INCLUDES})
# Tell cmake which directories to look in when you #include a file
# Equivalent to the "-I" option for g++
include_directories(${INCLUDE_DIRS})
# Tell cmake which libraries to link to
# Equivalent to the "-l" option for g++
target_link_libraries(${PROJECT_NAME} PRIVATE ${LIBS})
# For Visual Studio only
if (MSVC)
# Do a parallel compilation of this project
target_compile_options(${PROJECT_NAME} PRIVATE "/MP")
# Have this project be the default startup project (the one to build/run when hitting F5)
set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT ${PROJECT_NAME})
endif()

View file

@ -1,14 +0,0 @@
.PHONY: all clean
ZIP := zip
HANDIN := hw2a.michael.zhang.zip
SOURCES := $(shell find -name "*.cpp")
all: $(HANDIN)
$(HANDIN): src/HW2a.cpp examples README.md
$(ZIP) -r $@ $^
clean:
rm -f $(HANDIN)

View file

@ -1,13 +0,0 @@
# Assignment 2A
Compiles but does not run on CSE labs machines due to OpenGL 3.2 missing.
Try with `examples/test.obj`, run the program with
cmake -B build
make -C build
./build/HW2a examples/test.obj
This test has a few triangles in it. Other obj files _should_ work in theory
![](examples/test.png)

View file

@ -1 +0,0 @@
build/compile_commands.json

View file

@ -1,26 +0,0 @@
{ stdenv, cmake, ninja, libglvnd, libGLU, xorg, spdlog }:
stdenv.mkDerivation {
name = "assignment-2a";
src = ./.;
nativeBuildInputs = [ cmake ninja ];
buildInputs = [
libglvnd
libGLU
xorg.libX11
xorg.libXcursor
xorg.libXext
xorg.libXi
xorg.libXinerama
xorg.libXrandr
xorg.libXrender
spdlog
];
preBuild = ''
env
'';
cmakeFlags = [ "-DCMAKE_CURRENT_SOURCE_DIR=${./.}" ];
}

View file

@ -1,11 +0,0 @@
v 2 3 0
v 1.5 4 0
v 3 2 0
v 1.5 4 0
v 3 2 0
v 5 3.4 0
v 3 2 0
v 5 3.4 0
v 6 5.9 0

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Binary file not shown.

View file

@ -1,290 +0,0 @@
#ifndef __khrplatform_h_
#define __khrplatform_h_
/*
** Copyright (c) 2008-2018 The Khronos Group Inc.
**
** Permission is hereby granted, free of charge, to any person obtaining a
** copy of this software and/or associated documentation files (the
** "Materials"), to deal in the Materials without restriction, including
** without limitation the rights to use, copy, modify, merge, publish,
** distribute, sublicense, and/or sell copies of the Materials, and to
** permit persons to whom the Materials are furnished to do so, subject to
** the following conditions:
**
** The above copyright notice and this permission notice shall be included
** in all copies or substantial portions of the Materials.
**
** THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
** MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
*/
/* Khronos platform-specific types and definitions.
*
* The master copy of khrplatform.h is maintained in the Khronos EGL
* Registry repository at https://github.com/KhronosGroup/EGL-Registry
* The last semantic modification to khrplatform.h was at commit ID:
* 67a3e0864c2d75ea5287b9f3d2eb74a745936692
*
* Adopters may modify this file to suit their platform. Adopters are
* encouraged to submit platform specific modifications to the Khronos
* group so that they can be included in future versions of this file.
* Please submit changes by filing pull requests or issues on
* the EGL Registry repository linked above.
*
*
* See the Implementer's Guidelines for information about where this file
* should be located on your system and for more details of its use:
* http://www.khronos.org/registry/implementers_guide.pdf
*
* This file should be included as
* #include <KHR/khrplatform.h>
* by Khronos client API header files that use its types and defines.
*
* The types in khrplatform.h should only be used to define API-specific types.
*
* Types defined in khrplatform.h:
* khronos_int8_t signed 8 bit
* khronos_uint8_t unsigned 8 bit
* khronos_int16_t signed 16 bit
* khronos_uint16_t unsigned 16 bit
* khronos_int32_t signed 32 bit
* khronos_uint32_t unsigned 32 bit
* khronos_int64_t signed 64 bit
* khronos_uint64_t unsigned 64 bit
* khronos_intptr_t signed same number of bits as a pointer
* khronos_uintptr_t unsigned same number of bits as a pointer
* khronos_ssize_t signed size
* khronos_usize_t unsigned size
* khronos_float_t signed 32 bit floating point
* khronos_time_ns_t unsigned 64 bit time in nanoseconds
* khronos_utime_nanoseconds_t unsigned time interval or absolute time in
* nanoseconds
* khronos_stime_nanoseconds_t signed time interval in nanoseconds
* khronos_boolean_enum_t enumerated boolean type. This should
* only be used as a base type when a client API's boolean type is
* an enum. Client APIs which use an integer or other type for
* booleans cannot use this as the base type for their boolean.
*
* Tokens defined in khrplatform.h:
*
* KHRONOS_FALSE, KHRONOS_TRUE Enumerated boolean false/true values.
*
* KHRONOS_SUPPORT_INT64 is 1 if 64 bit integers are supported; otherwise 0.
* KHRONOS_SUPPORT_FLOAT is 1 if floats are supported; otherwise 0.
*
* Calling convention macros defined in this file:
* KHRONOS_APICALL
* KHRONOS_APIENTRY
* KHRONOS_APIATTRIBUTES
*
* These may be used in function prototypes as:
*
* KHRONOS_APICALL void KHRONOS_APIENTRY funcname(
* int arg1,
* int arg2) KHRONOS_APIATTRIBUTES;
*/
#if defined(__SCITECH_SNAP__) && !defined(KHRONOS_STATIC)
# define KHRONOS_STATIC 1
#endif
/*-------------------------------------------------------------------------
* Definition of KHRONOS_APICALL
*-------------------------------------------------------------------------
* This precedes the return type of the function in the function prototype.
*/
#if defined(KHRONOS_STATIC)
/* If the preprocessor constant KHRONOS_STATIC is defined, make the
* header compatible with static linking. */
# define KHRONOS_APICALL
#elif defined(_WIN32)
# define KHRONOS_APICALL __declspec(dllimport)
#elif defined (__SYMBIAN32__)
# define KHRONOS_APICALL IMPORT_C
#elif defined(__ANDROID__)
# define KHRONOS_APICALL __attribute__((visibility("default")))
#else
# define KHRONOS_APICALL
#endif
/*-------------------------------------------------------------------------
* Definition of KHRONOS_APIENTRY
*-------------------------------------------------------------------------
* This follows the return type of the function and precedes the function
* name in the function prototype.
*/
#if defined(_WIN32) && !defined(_WIN32_WCE) && !defined(KHRONOS_STATIC)
/* Win32 but not WinCE */
# define KHRONOS_APIENTRY __stdcall
#else
# define KHRONOS_APIENTRY
#endif
/*-------------------------------------------------------------------------
* Definition of KHRONOS_APIATTRIBUTES
*-------------------------------------------------------------------------
* This follows the closing parenthesis of the function prototype arguments.
*/
#if defined (__ARMCC_2__)
#define KHRONOS_APIATTRIBUTES __softfp
#else
#define KHRONOS_APIATTRIBUTES
#endif
/*-------------------------------------------------------------------------
* basic type definitions
*-----------------------------------------------------------------------*/
#if (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || defined(__GNUC__) || defined(__SCO__) || defined(__USLC__)
/*
* Using <stdint.h>
*/
#include <stdint.h>
typedef int32_t khronos_int32_t;
typedef uint32_t khronos_uint32_t;
typedef int64_t khronos_int64_t;
typedef uint64_t khronos_uint64_t;
#define KHRONOS_SUPPORT_INT64 1
#define KHRONOS_SUPPORT_FLOAT 1
#elif defined(__VMS ) || defined(__sgi)
/*
* Using <inttypes.h>
*/
#include <inttypes.h>
typedef int32_t khronos_int32_t;
typedef uint32_t khronos_uint32_t;
typedef int64_t khronos_int64_t;
typedef uint64_t khronos_uint64_t;
#define KHRONOS_SUPPORT_INT64 1
#define KHRONOS_SUPPORT_FLOAT 1
#elif defined(_WIN32) && !defined(__SCITECH_SNAP__)
/*
* Win32
*/
typedef __int32 khronos_int32_t;
typedef unsigned __int32 khronos_uint32_t;
typedef __int64 khronos_int64_t;
typedef unsigned __int64 khronos_uint64_t;
#define KHRONOS_SUPPORT_INT64 1
#define KHRONOS_SUPPORT_FLOAT 1
#elif defined(__sun__) || defined(__digital__)
/*
* Sun or Digital
*/
typedef int khronos_int32_t;
typedef unsigned int khronos_uint32_t;
#if defined(__arch64__) || defined(_LP64)
typedef long int khronos_int64_t;
typedef unsigned long int khronos_uint64_t;
#else
typedef long long int khronos_int64_t;
typedef unsigned long long int khronos_uint64_t;
#endif /* __arch64__ */
#define KHRONOS_SUPPORT_INT64 1
#define KHRONOS_SUPPORT_FLOAT 1
#elif 0
/*
* Hypothetical platform with no float or int64 support
*/
typedef int khronos_int32_t;
typedef unsigned int khronos_uint32_t;
#define KHRONOS_SUPPORT_INT64 0
#define KHRONOS_SUPPORT_FLOAT 0
#else
/*
* Generic fallback
*/
#include <stdint.h>
typedef int32_t khronos_int32_t;
typedef uint32_t khronos_uint32_t;
typedef int64_t khronos_int64_t;
typedef uint64_t khronos_uint64_t;
#define KHRONOS_SUPPORT_INT64 1
#define KHRONOS_SUPPORT_FLOAT 1
#endif
/*
* Types that are (so far) the same on all platforms
*/
typedef signed char khronos_int8_t;
typedef unsigned char khronos_uint8_t;
typedef signed short int khronos_int16_t;
typedef unsigned short int khronos_uint16_t;
/*
* Types that differ between LLP64 and LP64 architectures - in LLP64,
* pointers are 64 bits, but 'long' is still 32 bits. Win64 appears
* to be the only LLP64 architecture in current use.
*/
#ifdef _WIN64
typedef signed long long int khronos_intptr_t;
typedef unsigned long long int khronos_uintptr_t;
typedef signed long long int khronos_ssize_t;
typedef unsigned long long int khronos_usize_t;
#else
typedef signed long int khronos_intptr_t;
typedef unsigned long int khronos_uintptr_t;
typedef signed long int khronos_ssize_t;
typedef unsigned long int khronos_usize_t;
#endif
#if KHRONOS_SUPPORT_FLOAT
/*
* Float type
*/
typedef float khronos_float_t;
#endif
#if KHRONOS_SUPPORT_INT64
/* Time types
*
* These types can be used to represent a time interval in nanoseconds or
* an absolute Unadjusted System Time. Unadjusted System Time is the number
* of nanoseconds since some arbitrary system event (e.g. since the last
* time the system booted). The Unadjusted System Time is an unsigned
* 64 bit value that wraps back to 0 every 584 years. Time intervals
* may be either signed or unsigned.
*/
typedef khronos_uint64_t khronos_utime_nanoseconds_t;
typedef khronos_int64_t khronos_stime_nanoseconds_t;
#endif
/*
* Dummy value used to pad enum types to 32 bits.
*/
#ifndef KHRONOS_MAX_ENUM
#define KHRONOS_MAX_ENUM 0x7FFFFFFF
#endif
/*
* Enumerated boolean type
*
* Values other than zero should be considered to be true. Therefore
* comparisons should not be made against KHRONOS_TRUE.
*/
typedef enum {
KHRONOS_FALSE = 0,
KHRONOS_TRUE = 1,
KHRONOS_BOOLEAN_ENUM_FORCE_SIZE = KHRONOS_MAX_ENUM
} khronos_boolean_enum_t;
#endif /* __khrplatform_h_ */

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,65 +0,0 @@
image:
- Visual Studio 2015
- Visual Studio 2019
branches:
only:
- ci
- master
- 3.3-stable
skip_tags: true
skip_commits:
files:
- README.md
- LICENSE.md
- docs/*
environment:
matrix:
- GENERATOR: MinGW Makefiles
BUILD_SHARED_LIBS: ON
CFLAGS: -Werror
- GENERATOR: MinGW Makefiles
BUILD_SHARED_LIBS: OFF
CFLAGS: -Werror
- GENERATOR: Visual Studio 10 2010
BUILD_SHARED_LIBS: ON
CFLAGS: /WX
- GENERATOR: Visual Studio 10 2010
BUILD_SHARED_LIBS: OFF
CFLAGS: /WX
- GENERATOR: Visual Studio 16 2019
BUILD_SHARED_LIBS: ON
CFLAGS: /WX
- GENERATOR: Visual Studio 16 2019
BUILD_SHARED_LIBS: OFF
CFLAGS: /WX
matrix:
fast_finish: true
exclude:
- image: Visual Studio 2015
GENERATOR: Visual Studio 16 2019
- image: Visual Studio 2019
GENERATOR: Visual Studio 10 2010
- image: Visual Studio 2019
GENERATOR: MinGW Makefiles
for:
-
matrix:
except:
- GENERATOR: Visual Studio 10 2010
build_script:
- set PATH=%PATH:C:\Program Files\Git\usr\bin=C:\MinGW\bin%
- cmake -S . -B build -G "%GENERATOR%" -DBUILD_SHARED_LIBS=%BUILD_SHARED_LIBS%
- cmake --build build
-
matrix:
only:
- GENERATOR: Visual Studio 10 2010
build_script:
- cmake -S . -B build -G "%GENERATOR%" -DBUILD_SHARED_LIBS=%BUILD_SHARED_LIBS%
- cmake --build build --target glfw
notifications:
- provider: Email
to:
- ci@glfw.org
on_build_failure: true
on_build_success: false

View file

@ -1,5 +0,0 @@
*.m linguist-language=Objective-C
.gitignore export-ignore
.gitattributes export-ignore
.travis.yml export-ignore
.appveyor.yml export-ignore

View file

@ -1,85 +0,0 @@
# External junk
.DS_Store
_ReSharper*
*.opensdf
*.sdf
*.suo
*.dir
*.vcxproj*
*.sln
.vs/
Win32
x64
Debug
Release
MinSizeRel
RelWithDebInfo
*.xcodeproj
# CMake files
Makefile
CMakeCache.txt
CMakeFiles
CMakeScripts
cmake_install.cmake
cmake_uninstall.cmake
# Generated files
docs/Doxyfile
docs/html
docs/warnings.txt
docs/doxygen_sqlite3.db
src/glfw_config.h
src/glfw3.pc
src/glfw3Config.cmake
src/glfw3ConfigVersion.cmake
src/wayland-pointer-constraints-unstable-v1-client-protocol.h
src/wayland-pointer-constraints-unstable-v1-protocol.c
src/wayland-relative-pointer-unstable-v1-client-protocol.h
src/wayland-relative-pointer-unstable-v1-protocol.c
# Compiled binaries
src/libglfw.so
src/libglfw.so.3
src/libglfw.so.3.4
src/libglfw.dylib
src/libglfw.dylib
src/libglfw.3.dylib
src/libglfw.3.4.dylib
src/libglfw3.a
src/glfw3.lib
src/glfw3.dll
src/glfw3dll.lib
src/libglfw3dll.a
examples/*.app
examples/*.exe
examples/boing
examples/gears
examples/heightmap
examples/offscreen
examples/particles
examples/splitview
examples/sharing
examples/triangle-opengl
examples/wave
tests/*.app
tests/*.exe
tests/clipboard
tests/cursor
tests/empty
tests/events
tests/gamma
tests/glfwinfo
tests/icon
tests/iconify
tests/joysticks
tests/monitors
tests/msaa
tests/reopen
tests/tearing
tests/threads
tests/timeout
tests/title
tests/triangle-vulkan
tests/windows

View file

@ -1,104 +0,0 @@
language: c
compiler: clang
branches:
only:
- ci
- master
- 3.3-stable
matrix:
include:
- os: linux
dist: xenial
sudo: false
name: "X11 shared library"
addons:
apt:
packages:
- libxrandr-dev
- libxinerama-dev
- libxcursor-dev
- libxi-dev
env:
- BUILD_SHARED_LIBS=ON
- CFLAGS=-Werror
- os: linux
dist: xenial
sudo: false
name: "X11 static library"
addons:
apt:
packages:
- libxrandr-dev
- libxinerama-dev
- libxcursor-dev
- libxi-dev
env:
- BUILD_SHARED_LIBS=OFF
- CFLAGS=-Werror
- os: linux
dist: xenial
sudo: required
name: "Wayland shared library"
addons:
apt:
sources:
- ppa:kubuntu-ppa/backports
packages:
- extra-cmake-modules
- libwayland-dev
- libxkbcommon-dev
- libegl1-mesa-dev
env:
- USE_WAYLAND=ON
- BUILD_SHARED_LIBS=ON
- CFLAGS=-Werror
- os: linux
dist: xenial
sudo: required
name: "Wayland static library"
addons:
apt:
sources:
- ppa:kubuntu-ppa/backports
packages:
- extra-cmake-modules
- libwayland-dev
- libxkbcommon-dev
- libegl1-mesa-dev
env:
- USE_WAYLAND=ON
- BUILD_SHARED_LIBS=OFF
- CFLAGS=-Werror
- os: osx
sudo: false
name: "Cocoa shared library"
env:
- BUILD_SHARED_LIBS=ON
- CFLAGS=-Werror
- os: osx
sudo: false
name: "Cocoa static library"
env:
- BUILD_SHARED_LIBS=OFF
- CFLAGS=-Werror
script:
- if grep -Inr '\s$' src include docs tests examples CMake *.md .gitattributes .gitignore; then
echo Trailing whitespace found, aborting;
exit 1;
fi
- mkdir build
- cd build
- if test -n "${USE_WAYLAND}"; then
git clone git://anongit.freedesktop.org/wayland/wayland-protocols;
pushd wayland-protocols;
git checkout 1.15 && ./autogen.sh --prefix=/usr && make && sudo make install;
popd;
fi
- cmake -DCMAKE_VERBOSE_MAKEFILE=ON -DBUILD_SHARED_LIBS=${BUILD_SHARED_LIBS} -DGLFW_USE_WAYLAND=${USE_WAYLAND} ..
- cmake --build .
notifications:
email:
recipients:
- ci@glfw.org
on_success: never
on_failure: always

View file

@ -1,33 +0,0 @@
# Usage:
# cmake -P GenerateMappings.cmake <path/to/mappings.h.in> <path/to/mappings.h>
set(source_url "https://raw.githubusercontent.com/gabomdq/SDL_GameControllerDB/master/gamecontrollerdb.txt")
set(source_path "${CMAKE_CURRENT_BINARY_DIR}/gamecontrollerdb.txt")
set(template_path "${CMAKE_ARGV3}")
set(target_path "${CMAKE_ARGV4}")
if (NOT EXISTS "${template_path}")
message(FATAL_ERROR "Failed to find template file ${template_path}")
endif()
file(DOWNLOAD "${source_url}" "${source_path}"
STATUS download_status
TLS_VERIFY on)
list(GET download_status 0 status_code)
list(GET download_status 1 status_message)
if (status_code)
message(FATAL_ERROR "Failed to download ${source_url}: ${status_message}")
endif()
file(STRINGS "${source_path}" lines)
foreach(line ${lines})
if ("${line}" MATCHES "^[0-9a-fA-F].*$")
set(GLFW_GAMEPAD_MAPPINGS "${GLFW_GAMEPAD_MAPPINGS}\"${line}\",\n")
endif()
endforeach()
configure_file("${template_path}" "${target_path}" @ONLY NEWLINE_STYLE UNIX)
file(REMOVE "${source_path}")

View file

@ -1,38 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
<key>CFBundleGetInfoString</key>
<string>${MACOSX_BUNDLE_INFO_STRING}</string>
<key>CFBundleIconFile</key>
<string>${MACOSX_BUNDLE_ICON_FILE}</string>
<key>CFBundleIdentifier</key>
<string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleLongVersionString</key>
<string>${MACOSX_BUNDLE_LONG_VERSION_STRING}</string>
<key>CFBundleName</key>
<string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
<key>CSResourcesFileMapped</key>
<true/>
<key>LSRequiresCarbon</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>${MACOSX_BUNDLE_COPYRIGHT}</string>
<key>NSHighResolutionCapable</key>
<true/>
</dict>
</plist>

Some files were not shown because too many files have changed in this diff Show more