more radical reorganization!

rendering is now performed in a separate thread. this is of little use
now but lays the groundwork for adding parallel rendering. i just need
to think about data ownership a little harder to make the tile renderer
nice.
This commit is contained in:
wires 2025-06-01 00:21:11 -04:00
parent 1141566e6f
commit d8a88e81c8
Signed by: wires
SSH key fingerprint: SHA256:9GtP+M3O2IivPDlw1UY872UPUuJH2gI0yG6ExBxaaiM
13 changed files with 442 additions and 207 deletions

124
Cargo.lock generated
View file

@ -8,24 +8,12 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "bytemuck"
version = "1.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "0.1.10" version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "enum_dispatch" name = "enum_dispatch"
version = "0.3.13" version = "0.3.13"
@ -44,7 +32,7 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "032213946b4eaae09117ec63f020322b78ca7a31d8aa2cf64df3032e1579690f" checksum = "032213946b4eaae09117ec63f020322b78ca7a31d8aa2cf64df3032e1579690f"
dependencies = [ dependencies = [
"cfg-if 0.1.10", "cfg-if",
"fasthash-sys", "fasthash-sys",
"num-traits", "num-traits",
"seahash", "seahash",
@ -77,9 +65,6 @@ name = "glam"
version = "0.30.2" version = "0.30.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0e9b6647e9b41d3a5ef02964c6be01311a7f2472fea40595c635c6d046c259e" checksum = "d0e9b6647e9b41d3a5ef02964c6be01311a7f2472fea40595c635c6d046c259e"
dependencies = [
"bytemuck",
]
[[package]] [[package]]
name = "heck" name = "heck"
@ -99,23 +84,33 @@ version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "matrixmultiply"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08"
dependencies = [
"autocfg",
"rawpointer",
]
[[package]] [[package]]
name = "mechthild_core" name = "mechthild_core"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bytemuck",
"enum_dispatch", "enum_dispatch",
"fasthash", "fasthash",
"glam", "glam",
"range2d", "ndarray",
"num-traits",
] ]
[[package]] [[package]]
name = "mechthild_py" name = "mechthild_py"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bytemuck",
"mechthild_core", "mechthild_core",
"numpy",
"pyo3", "pyo3",
] ]
@ -128,6 +123,39 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "ndarray"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841"
dependencies = [
"matrixmultiply",
"num-complex",
"num-integer",
"num-traits",
"portable-atomic",
"portable-atomic-util",
"rawpointer",
]
[[package]]
name = "num-complex"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
"num-traits",
]
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@ -137,6 +165,22 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "numpy"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29f1dee9aa8d3f6f8e8b9af3803006101bb3653866ef056d530d53ae68587191"
dependencies = [
"libc",
"ndarray",
"num-complex",
"num-integer",
"num-traits",
"pyo3",
"pyo3-build-config",
"rustc-hash",
]
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.3" version = "1.21.3"
@ -149,6 +193,15 @@ version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
[[package]]
name = "portable-atomic-util"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
dependencies = [
"portable-atomic",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.95" version = "1.0.95"
@ -160,11 +213,10 @@ dependencies = [
[[package]] [[package]]
name = "pyo3" name = "pyo3"
version = "0.24.2" version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5203598f366b11a02b13aa20cab591229ff0a89fd121a308a5df751d5fc9219" checksum = "f239d656363bcee73afef85277f1b281e8ac6212a1d42aa90e55b90ed43c47a4"
dependencies = [ dependencies = [
"cfg-if 1.0.0",
"indoc", "indoc",
"libc", "libc",
"memoffset", "memoffset",
@ -178,9 +230,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-build-config" name = "pyo3-build-config"
version = "0.24.2" version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99636d423fa2ca130fa5acde3059308006d46f98caac629418e53f7ebb1e9999" checksum = "755ea671a1c34044fa165247aaf6f419ca39caa6003aee791a0df2713d8f1b6d"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"target-lexicon", "target-lexicon",
@ -188,9 +240,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-ffi" name = "pyo3-ffi"
version = "0.24.2" version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78f9cf92ba9c409279bc3305b5409d90db2d2c22392d443a87df3a1adad59e33" checksum = "fc95a2e67091e44791d4ea300ff744be5293f394f1bafd9f78c080814d35956e"
dependencies = [ dependencies = [
"libc", "libc",
"pyo3-build-config", "pyo3-build-config",
@ -198,9 +250,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-macros" name = "pyo3-macros"
version = "0.24.2" version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b999cb1a6ce21f9a6b147dcf1be9ffedf02e0043aec74dc390f3007047cecd9" checksum = "a179641d1b93920829a62f15e87c0ed791b6c8db2271ba0fd7c2686090510214"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"pyo3-macros-backend", "pyo3-macros-backend",
@ -210,9 +262,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-macros-backend" name = "pyo3-macros-backend"
version = "0.24.2" version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "822ece1c7e1012745607d5cf0bcb2874769f0f7cb34c4cde03b9358eb9ef911a" checksum = "9dff85ebcaab8c441b0e3f7ae40a6963ecea8a9f5e74f647e33fcf5ec9a1e89e"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
@ -259,10 +311,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
[[package]] [[package]]
name = "range2d" name = "rawpointer"
version = "0.2.0" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7169c4e8549f72214a10fe6a64e24bf5e2d75ca9378d0ac7934afb9acbdfc313" checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
[[package]] [[package]]
name = "rdrand" name = "rdrand"
@ -273,6 +325,12 @@ dependencies = [
"rand_core 0.3.1", "rand_core 0.3.1",
] ]
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]] [[package]]
name = "seahash" name = "seahash"
version = "3.0.7" version = "3.0.7"

View file

@ -7,12 +7,11 @@ license.workspace = true
repository.workspace = true repository.workspace = true
[dependencies] [dependencies]
bytemuck = { version = "1.22.0", optional = true }
enum_dispatch = "0.3.13" enum_dispatch = "0.3.13"
fasthash = "0.4.0" fasthash = "0.4.0"
glam = "0.30.2" glam = "0.30.2"
range2d = "0.2.0" ndarray = "0.16.1"
num-traits = "0.2.19"
[features] [features]
fast-math = ["glam/fast-math"] fast-math = ["glam/fast-math"]
bytemuck = ["dep:bytemuck", "glam/bytemuck"]

View file

@ -1,8 +1,4 @@
use crate::{ use crate::geometry::{Point, Point2, Ray, Transform, Vector, ray, vec3};
color::Rgba,
geometry::{Point, Point2, Ray, Transform, Transform2, Vector, point2, ray, vec3},
sampling::PixelSample,
};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Camera { pub struct Camera {
@ -27,45 +23,3 @@ impl Camera {
) )
} }
} }
pub struct Film {
width: u32,
height: u32,
transform: Transform2,
data: Box<[Rgba]>,
}
impl Film {
pub fn new(width: u32, height: u32) -> Self {
let size = width as usize * height as usize;
let data = vec![Rgba::BLACK; size].into_boxed_slice();
let w = width as f32;
let h = height as f32;
let ar = h / w;
let transform = Transform2::translate(-1.0, ar) * Transform2::scale(2.0 / w, -2.0 / w);
Self {
width,
height,
transform,
data,
}
}
pub fn add_sample(&mut self, x: u32, y: u32, color: Rgba, weight: f32) {
let i = y as usize * self.width as usize + x as usize;
self.data[i] += color * weight;
}
pub fn get_camera_sample(&self, x: u32, y: u32, sampler: &mut PixelSample) -> Point2 {
let p = point2(x as f32, y as f32) + sampler.get_2d().into();
self.transform * p
}
pub fn into_data(self) -> Box<[Rgba]> {
self.data
}
}

View file

@ -1,75 +1,99 @@
use std::{ use std::fmt;
fmt, use std::ops::{Add, AddAssign, Div, DivAssign, Index, Mul, MulAssign};
ops::{Add, AddAssign, Mul},
}; use glam::Vec3A;
use crate::Vector;
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
#[repr(C)] pub struct Rgb(Vec3A);
pub struct Rgba {
r: f32, impl Rgb {
g: f32, pub const BLACK: Self = rgb(0.0, 0.0, 0.0);
b: f32,
a: f32,
} }
pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Rgba { impl num_traits::Zero for Rgb {
Rgba { r, g, b, a } fn zero() -> Self {
Self(Vec3A::ZERO)
}
fn is_zero(&self) -> bool {
self.0 == Vec3A::ZERO
}
} }
impl Rgba { pub const fn rgb(r: f32, g: f32, b: f32) -> Rgb {
pub const BLACK: Self = rgba(0.0, 0.0, 0.0, 1.0); Rgb(glam::vec3a(r, g, b))
} }
impl fmt::Debug for Rgba { impl From<Vector> for Rgb {
fn from(v: Vector) -> Self {
Self(v.into())
}
}
impl fmt::Debug for Rgb {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple(stringify!(Rgba)) f.debug_tuple(stringify!(Rgb))
.field(&self.r) .field(&self.0.x)
.field(&self.g) .field(&self.0.y)
.field(&self.b) .field(&self.0.z)
.field(&self.a)
.finish() .finish()
} }
} }
impl Add for Rgba { impl Index<usize> for Rgb {
type Output = Self; type Output = f32;
fn add(self, Self { r, g, b, a }: Self) -> Self::Output { #[inline]
Self { fn index(&self, index: usize) -> &Self::Output {
r: self.r + r, match index {
g: self.g + g, 0 => &self.0.x,
b: self.b + b, 1 => &self.0.y,
a: self.a + a, 2 => &self.0.z,
_ => panic!("index out of bounds"),
} }
} }
} }
impl AddAssign for Rgba { impl Add for Rgb {
fn add_assign(&mut self, Self { r, g, b, a }: Self) { type Output = Self;
self.r += r;
self.g += g; fn add(self, rhs: Self) -> Self::Output {
self.b += b; Self(self.0 + rhs.0)
self.a += a;
} }
} }
impl Mul<f32> for Rgba { impl AddAssign for Rgb {
fn add_assign(&mut self, rhs: Self) {
self.0 += rhs.0
}
}
impl Mul<f32> for Rgb {
type Output = Self; type Output = Self;
fn mul(self, rhs: f32) -> Self::Output { fn mul(self, rhs: f32) -> Self::Output {
Self { Self(self.0 * rhs)
r: self.r * rhs,
g: self.g * rhs,
b: self.b * rhs,
a: self.a * rhs,
}
} }
} }
impl Mul<Rgba> for f32 { impl MulAssign<f32> for Rgb {
type Output = Rgba; fn mul_assign(&mut self, rhs: f32) {
self.0 *= rhs
fn mul(self, rhs: Rgba) -> Self::Output { }
rhs * self }
impl Div<f32> for Rgb {
type Output = Self;
fn div(self, rhs: f32) -> Self::Output {
Self(self.0 / rhs)
}
}
impl DivAssign<f32> for Rgb {
fn div_assign(&mut self, rhs: f32) {
self.0 /= rhs
} }
} }

37
core/src/film.rs Normal file
View file

@ -0,0 +1,37 @@
use ndarray::{Array3, Ix2, iter::LanesMut};
use crate::{geometry::Transform2, sampling::Sampler};
pub struct Film {
pub width: u32,
pub height: u32,
pub transform: Transform2,
pub data: Array3<f32>,
}
impl Film {
pub fn new(width: u32, height: u32) -> Self {
let w = width as f32;
let h = height as f32;
let ar = h / w;
let transform = Transform2::translate(-1.0, ar) * Transform2::scale(2.0 / w, -2.0 / w);
let data = Array3::zeros((height as usize, width as usize, 3));
Self {
width,
height,
transform,
data,
}
}
pub fn sampler(&self, samples: u32, seed: u32) -> Sampler {
Sampler::new(self.width, self.height, samples, seed)
}
pub fn lanes_mut(&mut self) -> LanesMut<'_, f32, Ix2> {
self.data.lanes_mut(ndarray::Axis(2))
}
}

View file

@ -21,6 +21,10 @@ impl Vector {
pub const J: Self = Self(glam::Vec3A::Y); pub const J: Self = Self(glam::Vec3A::Y);
pub const K: Self = Self(glam::Vec3A::Z); pub const K: Self = Self(glam::Vec3A::Z);
pub fn splat(v: f32) -> Self {
Self(glam::Vec3A::splat(v))
}
pub fn length(self) -> f32 { pub fn length(self) -> f32 {
self.0.length() self.0.length()
} }
@ -80,6 +84,12 @@ impl Deref for Vector {
} }
} }
impl From<Vector> for glam::Vec3A {
fn from(value: Vector) -> Self {
value.0
}
}
impl From<Vector> for glam::Vec3 { impl From<Vector> for glam::Vec3 {
fn from(value: Vector) -> Self { fn from(value: Vector) -> Self {
value.0.into() value.0.into()

View file

@ -1,6 +0,0 @@
use bytemuck::{Pod, Zeroable};
use crate::color::Rgba;
unsafe impl Pod for Rgba {}
unsafe impl Zeroable for Rgba {}

View file

@ -1,66 +1,129 @@
use range2d::Range2D; use std::{
sync::mpsc::{self, Receiver, Sender, channel},
#[cfg(feature = "bytemuck")] thread::{self, JoinHandle},
pub mod impl_bytemuck; };
pub mod color;
mod camera; mod camera;
mod color;
mod film;
mod geometry; mod geometry;
mod sampling; mod sampling;
mod scene;
pub use camera::Camera; pub use camera::Camera;
pub use film::Film;
pub use geometry::{Point, Shape, Transform, Vector}; pub use geometry::{Point, Shape, Transform, Vector};
pub use scene::Scene;
use camera::Film; use color::Rgb;
use color::{Rgba, rgba}; use geometry::{Point2, Transform2, point2};
use geometry::{Hit, Hittable, Ray, vec3}; use sampling::PixelSample;
use sampling::Sampler;
pub struct Scene { pub struct RenderSession {
thread: RenderThread,
}
impl RenderSession {
pub fn new(scene: Scene, camera: Camera, film: Film, samples: u32, seed: u32) -> Self {
Self {
thread: RenderThread::spawn(scene, camera, film, samples, seed),
}
}
pub fn stop(self) -> thread::Result<RenderState> {
self.thread.stop()
}
pub fn is_finished(&self) -> bool {
self.thread.is_finished()
}
pub fn join(self) -> thread::Result<RenderState> {
self.thread.join()
}
}
pub enum RenderState {
Stopped(Film),
Finished(Film),
}
struct RenderThread {
handle: JoinHandle<RenderState>,
shutdown: Sender<()>,
}
impl RenderThread {
fn spawn(scene: Scene, camera: Camera, film: Film, samples: u32, seed: u32) -> Self {
let (tx, rx) = channel::<()>();
let handle = thread::spawn(move || render_thread(scene, camera, film, samples, seed, rx));
Self {
handle,
shutdown: tx,
}
}
fn is_finished(&self) -> bool {
self.handle.is_finished()
}
fn join(self) -> thread::Result<RenderState> {
self.handle.join()
}
fn stop(self) -> thread::Result<RenderState> {
let _ = self.shutdown.send(());
self.handle.join()
}
}
fn render_thread(
scene: Scene,
camera: Camera, camera: Camera,
objects: Vec<Shape>, mut film: Film,
} samples: u32,
seed: u32,
shutdown: Receiver<()>,
) -> RenderState {
let mut sampler = film.sampler(samples, seed);
let t = film.transform;
let w = film.width as usize;
impl Scene { for (i, mut c) in film.lanes_mut().into_iter().enumerate() {
pub fn new(camera: Camera, objects: Vec<Shape>) -> Self { let mut rgb = Rgb::BLACK;
Self { camera, objects }
}
pub fn render(&self, width: u32, height: u32, samples: u32, seed: u32) -> Box<[Rgba]> { let x = i % w;
let mut sampler = Sampler::new(width, height, samples, seed); let y = i / w;
let mut film = Film::new(width, height);
for (y, x) in Range2D::new(0..height, 0..width) { for mut pixel_sample in sampler.pixel(x as u32, y as u32) {
let mut rgb = Vector::ZERO; let p_film = get_camera_sample(x as u32, y as u32, &mut pixel_sample, t);
let ray = camera.ray(p_film);
for mut pixel_sample in sampler.pixel(x, y) { if let Some(h) = scene.intersect(ray) {
let p_film = film.get_camera_sample(x, y, &mut pixel_sample); rgb += dir_color(h.normal);
let ray = self.camera.ray(p_film);
if let Some(h) = self.intersect(ray) {
rgb += h.normal.normalize() * 0.5 + vec3(0.5, 0.5, 0.5);
}
} }
rgb /= samples as f32; use mpsc::TryRecvError as E;
match shutdown.try_recv() {
film.add_sample(x, y, rgba(rgb.x, rgb.y, rgb.z, 0.0), 1.0); Ok(_) | Err(E::Disconnected) => return RenderState::Stopped(film),
} Err(E::Empty) => (),
film.into_data()
}
fn intersect(&self, r: Ray) -> Option<Hit> {
let mut hit: Option<Hit> = None;
for obj in &self.objects {
let t_max = hit.as_ref().map(|h| h.t).unwrap_or(f32::INFINITY);
if let Some(h) = obj.intersect(&r, t_max) {
hit = Some(h)
} }
} }
hit rgb /= samples as f32;
for i in 0..3 {
c[i] = rgb[i];
}
} }
RenderState::Finished(film)
}
fn get_camera_sample(x: u32, y: u32, sampler: &mut PixelSample, transform: Transform2) -> Point2 {
transform * (point2(x as f32, y as f32) + sampler.get_2d().into())
}
fn dir_color(v: Vector) -> Rgb {
(v.normalize() * 0.5 + Vector::splat(0.5)).into()
} }

View file

@ -20,6 +20,7 @@ const PERMUTATIONS: [[u8; 4]; 24] = [
[3, 0, 1, 2], [3, 0, 2, 1], [3, 1, 0, 2], [3, 1, 2, 0], [3, 2, 0, 1], [3, 2, 1, 0], [3, 0, 1, 2], [3, 0, 2, 1], [3, 1, 0, 2], [3, 1, 2, 0], [3, 2, 0, 1], [3, 2, 1, 0],
]; ];
#[derive(Clone)]
pub struct Sampler { pub struct Sampler {
seed: u32, seed: u32,
samples: u32, samples: u32,

26
core/src/scene.rs Normal file
View file

@ -0,0 +1,26 @@
use crate::{
Shape,
geometry::{Hit, Hittable, Ray},
};
#[derive(Debug, Clone)]
pub struct Scene(Vec<Shape>);
impl Scene {
pub fn new(objects: Vec<Shape>) -> Self {
Self(objects)
}
pub fn intersect(&self, r: Ray) -> Option<Hit> {
let mut hit: Option<Hit> = None;
for obj in &self.0 {
let t_max = hit.as_ref().map(|h| h.t).unwrap_or(f32::INFINITY);
if let Some(h) = obj.intersect(&r, t_max) {
hit = Some(h)
}
}
hit
}
}

View file

@ -1,10 +1,14 @@
#!python #!python
import argparse
from PIL import Image from PIL import Image
from mechthild import Scene, Shape, Camera from mechthild import Session, Shape, Camera
WIDTH = 800 parser = argparse.ArgumentParser()
HEIGHT = 600 parser.add_argument('width', type=int, default=400, nargs='?')
SAMPLES = 32 parser.add_argument('height', type=int, default=400, nargs='?')
parser.add_argument('samples', type=int, default=32, nargs='?')
args = parser.parse_args()
s1 = Shape.sphere((0.75, 1.0, 1.0), 1.0) s1 = Shape.sphere((0.75, 1.0, 1.0), 1.0)
s2 = Shape.sphere((-1.5, 2.0, -2.0), 2.0) s2 = Shape.sphere((-1.5, 2.0, -2.0), 2.0)
@ -12,10 +16,10 @@ ground = Shape.plane((0.0, 1.0, 0.0), 0.0)
camera = Camera((0.0, 1.5, 5.0), (0.0, 1.0, 0.0), (0.0, 1.0, 0.0), 80.0) camera = Camera((0.0, 1.5, 5.0), (0.0, 1.0, 0.0), (0.0, 1.0, 0.0), 80.0)
scene = Scene(camera, [s1, s2, ground]) session = Session([s1, s2, ground], camera, args.width, args.height, args.samples)
render = scene.render(WIDTH, HEIGHT, SAMPLES) film = (session.get_output() * 255).astype('uint8')
rgba = bytes([int(v * 255) for v in render])
image = Image.frombuffer('RGBA', (WIDTH, HEIGHT), rgba) image = Image.fromarray(film)
image.save("test.png") image.save("test.png")

View file

@ -6,12 +6,11 @@ authors.workspace = true
license.workspace = true license.workspace = true
repository.workspace = true repository.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib] [lib]
name = "mechthild" name = "mechthild"
crate-type = ["cdylib"] crate-type = ["cdylib"]
[dependencies] [dependencies]
pyo3 = "0.24.0" pyo3 = { version = "0.25.0", features = ["extension-module"]}
mechthild_core = { path = "../core", features = ["bytemuck"] } numpy = "0.25.0"
bytemuck = { version = "1.22.0", features = ["extern_crate_alloc"] } mechthild_core = { path = "../core" }

View file

@ -1,5 +1,7 @@
use bytemuck::allocation::cast_slice_box; use std::{mem, panic};
use mechthild_core::{Camera, Scene, Shape, color::Rgba};
use mechthild_core::{Camera, Film, RenderSession, RenderState, Scene, Shape};
use numpy::{Ix3, PyArray};
use pyo3::prelude::*; use pyo3::prelude::*;
#[pyclass(name = "Camera")] #[pyclass(name = "Camera")]
@ -44,22 +46,86 @@ impl PyShape {
} }
} }
#[pyclass(name = "Scene")] #[pyclass(name = "Session")]
struct PyScene(Scene); struct PySession(Session);
#[pymethods] enum Session {
impl PyScene { Running(RenderSession),
#[new] Finished(Film),
pub fn new(camera: &PyCamera, contents: Vec<PyShape>) -> Self { Stopped(Film),
let contents = contents.into_iter().map(|s| s.0).collect(); // i hate this
Self(Scene::new(camera.0.clone(), contents)) Empty,
}
impl Default for Session {
fn default() -> Self {
Self::Empty
}
}
impl Session {
fn new(scene: Scene, camera: Camera, film: Film, samples: u32, seed: u32) -> Self {
Self::Running(RenderSession::new(scene, camera, film, samples, seed))
} }
#[pyo3(signature = (width, height, samples, seed = 0))] fn take(&mut self) -> Option<RenderSession> {
pub fn render(&self, width: u32, height: u32, samples: u32, seed: u32) -> Vec<f32> { let session = mem::take(self);
let result = self.0.render(width, height, samples, seed);
cast_slice_box::<Rgba, f32>(result).into_vec() if let Self::Running(s) = session {
Some(s)
} else {
*self = session;
None
}
}
}
#[pymethods]
impl PySession {
#[new]
#[pyo3(signature = (objects, camera, width, height, samples, seed = 0))]
pub fn new(
objects: Vec<PyShape>,
camera: &PyCamera,
width: u32,
height: u32,
samples: u32,
seed: u32,
) -> Self {
let film = Film::new(width, height);
let scene = Scene::new(objects.into_iter().map(|s| s.0).collect());
Self(Session::new(scene, camera.0.clone(), film, samples, seed))
}
pub fn wait(&mut self) {
if let Some(session) = self.0.take() {
*self = match session.join() {
Ok(RenderState::Finished(f)) => Self(Session::Finished(f)),
Ok(RenderState::Stopped(f)) => Self(Session::Stopped(f)),
Err(e) => panic::resume_unwind(e),
}
}
}
pub fn stop(&mut self) {
if let Some(session) = self.0.take() {
*self = match session.stop() {
Ok(RenderState::Finished(f)) => Self(Session::Finished(f)),
Ok(RenderState::Stopped(f)) => Self(Session::Stopped(f)),
Err(e) => panic::resume_unwind(e),
}
}
}
pub fn get_output<'py>(&mut self, py: Python<'py>) -> Bound<'py, PyArray<f32, Ix3>> {
self.wait();
match &self.0 {
Session::Finished(f) => PyArray::from_array(py, &f.data),
Session::Stopped(f) => PyArray::from_array(py, &f.data),
_ => unreachable!(),
}
} }
} }
@ -67,6 +133,6 @@ impl PyScene {
fn mechthild(m: &Bound<'_, PyModule>) -> PyResult<()> { fn mechthild(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyCamera>()?; m.add_class::<PyCamera>()?;
m.add_class::<PyShape>()?; m.add_class::<PyShape>()?;
m.add_class::<PyScene>()?; m.add_class::<PySession>()?;
Ok(()) Ok(())
} }