reorganize core systems

no change in functionality, but preparing for stuff in the near future
like more accurately modeling film sensors, filtering, color science,
etc.
This commit is contained in:
wires 2025-05-29 16:30:25 -04:00
parent a30b29a130
commit 094f4947dc
Signed by: wires
SSH key fingerprint: SHA256:9GtP+M3O2IivPDlw1UY872UPUuJH2gI0yG6ExBxaaiM
9 changed files with 210 additions and 56 deletions

7
Cargo.lock generated
View file

@ -107,6 +107,7 @@ dependencies = [
"enum_dispatch",
"fasthash",
"glam",
"range2d",
]
[[package]]
@ -257,6 +258,12 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
[[package]]
name = "range2d"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7169c4e8549f72214a10fe6a64e24bf5e2d75ca9378d0ac7934afb9acbdfc313"
[[package]]
name = "rdrand"
version = "0.4.0"

View file

@ -11,6 +11,7 @@ bytemuck = { version = "1.22.0", optional = true }
enum_dispatch = "0.3.13"
fasthash = "0.4.0"
glam = "0.30.2"
range2d = "0.2.0"
[features]
fast-math = ["glam/fast-math"]

View file

@ -1,4 +1,8 @@
use crate::geometry::{Point, Ray, Transform, Vector};
use crate::{
color::Rgba,
geometry::{Point, Point2, Ray, Transform, Transform2, Vector, point2, ray, vec3},
sampling::PixelSample,
};
#[derive(Debug, Clone)]
pub struct Camera {
@ -16,9 +20,52 @@ impl Camera {
Self { transform }
}
pub fn ray(&self, x: f32, y: f32) -> Ray {
let origin = self.transform * Point::ORIGIN;
let direction = self.transform * Vector::new(x, y, 1.0);
Ray::new(origin, direction)
pub fn ray(&self, p_film: Point2) -> Ray {
ray(
self.transform * Point::ORIGIN,
self.transform * vec3(p_film.x, p_film.y, 1.0),
)
}
}
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,4 +1,7 @@
use std::fmt;
use std::{
fmt,
ops::{Add, AddAssign, Mul},
};
#[derive(Clone, Copy)]
#[repr(C)]
@ -9,12 +12,12 @@ pub struct Rgba {
a: f32,
}
impl Rgba {
pub const BLACK: Self = Self::new(0.0, 0.0, 0.0, 1.0);
pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
Self { r, g, b, a }
pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Rgba {
Rgba { r, g, b, a }
}
impl Rgba {
pub const BLACK: Self = rgba(0.0, 0.0, 0.0, 1.0);
}
impl fmt::Debug for Rgba {
@ -27,3 +30,46 @@ impl fmt::Debug for Rgba {
.finish()
}
}
impl Add for Rgba {
type Output = Self;
fn add(self, Self { r, g, b, a }: Self) -> Self::Output {
Self {
r: self.r + r,
g: self.g + g,
b: self.b + b,
a: self.a + a,
}
}
}
impl AddAssign for Rgba {
fn add_assign(&mut self, Self { r, g, b, a }: Self) {
self.r += r;
self.g += g;
self.b += b;
self.a += a;
}
}
impl Mul<f32> for Rgba {
type Output = Self;
fn mul(self, rhs: f32) -> Self::Output {
Self {
r: self.r * rhs,
g: self.g * rhs,
b: self.b * rhs,
a: self.a * rhs,
}
}
}
impl Mul<Rgba> for f32 {
type Output = Rgba;
fn mul(self, rhs: Rgba) -> Self::Output {
rhs * self
}
}

View file

@ -8,7 +8,7 @@ mod shapes;
mod transform;
pub use _2d::*;
pub use shapes::Shape;
pub use shapes::{Hit, Hittable, Shape};
pub use transform::Transform;
#[repr(transparent)]

View file

@ -1,6 +1,6 @@
use std::{
fmt,
ops::{Add, AddAssign, Div, DivAssign, Mul, Sub},
ops::{Add, AddAssign, Deref, Div, DivAssign, Mul, Sub},
};
#[repr(transparent)]
@ -88,6 +88,12 @@ pub const fn point2(x: f32, y: f32) -> Point2 {
Point2(glam::vec2(x, y))
}
impl From<(f32, f32)> for Point2 {
fn from((x, y): (f32, f32)) -> Self {
point2(x, y)
}
}
impl fmt::Debug for Point2 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple(stringify!(Point))
@ -97,9 +103,12 @@ impl fmt::Debug for Point2 {
}
}
impl From<(f32, f32)> for Point2 {
fn from((x, y): (f32, f32)) -> Self {
point2(x, y)
impl Deref for Point2 {
type Target = glam::Vec2;
#[inline]
fn deref(&self) -> &Self::Target {
&self.0
}
}
@ -126,3 +135,54 @@ impl Sub for Point2 {
Vector2(self.0 - rhs.0)
}
}
#[derive(Copy, Clone)]
#[repr(transparent)]
pub struct Transform2(glam::Affine2);
impl fmt::Debug for Transform2 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct(stringify!(Transform))
.field(stringify!(matrix2), &self.0.matrix2)
.field(stringify!(translation), &self.0.translation)
.finish()
}
}
impl Transform2 {
pub fn scale(x: f32, y: f32) -> Self {
Self(glam::Affine2::from_scale(glam::vec2(x, y)))
}
pub fn inverse(self) -> Self {
Self(self.0.inverse())
}
pub fn translate(x: f32, y: f32) -> Self {
Self(glam::Affine2::from_translation(glam::vec2(x, y)))
}
}
impl Mul for Transform2 {
type Output = Transform2;
fn mul(self, rhs: Self) -> Self::Output {
Self(self.0 * rhs.0)
}
}
impl Mul<Vector2> for Transform2 {
type Output = Vector2;
fn mul(self, rhs: Vector2) -> Self::Output {
Vector2(self.0.transform_vector2(rhs.0))
}
}
impl Mul<Point2> for Transform2 {
type Output = Point2;
fn mul(self, rhs: Point2) -> Self::Output {
Point2(self.0.transform_point2(rhs.0))
}
}

View file

@ -5,14 +5,14 @@ use enum_dispatch::enum_dispatch;
use super::{Point, Ray, Vector};
#[derive(Debug)]
pub(crate) struct Hit {
pub struct Hit {
pub point: Point,
pub normal: Vector,
pub t: f32,
}
#[enum_dispatch]
pub(crate) trait Hittable {
pub trait Hittable {
fn intersect(&self, ray: &Ray, t_max: f32) -> Option<Hit>;
}

View file

@ -1,3 +1,8 @@
use core::f32;
use geometry::{Hit, Hittable, Ray, point2, vec3};
use range2d::Range2D;
#[cfg(feature = "bytemuck")]
pub mod impl_bytemuck;
@ -7,70 +12,57 @@ mod camera;
mod geometry;
mod sampling;
use core::f32;
pub use camera::Camera;
pub use geometry::{Point, Transform, Vector, shapes::Shape};
pub use geometry::{Point, Shape, Transform, Vector};
use color::Rgba;
use geometry::{
Ray,
shapes::{Hit, Hittable},
};
use camera::Film;
use color::{Rgba, rgba};
use sampling::Sampler;
pub struct Scene {
camera: Camera,
contents: Vec<Shape>,
objects: Vec<Shape>,
}
impl Scene {
pub fn new(camera: Camera, contents: Vec<Shape>) -> Self {
Self { camera, contents }
pub fn new(camera: Camera, objects: Vec<Shape>) -> Self {
Self { camera, objects }
}
pub fn render(&self, width: u32, height: u32, samples: u32) -> Box<[Rgba]> {
let mut sampler = Sampler::new(width, height, samples, 0);
let mut data = Box::new_uninit_slice(width as usize * height as usize);
let w = width as f32;
let h = height as f32;
let aspect_ratio = h / w;
for (i, pixel) in data.iter_mut().enumerate() {
let bx = i as u32 % width;
let by = i as u32 / width;
pub fn render(&self, width: u32, height: u32, samples: u32, seed: u32) -> Box<[Rgba]> {
let mut sampler = Sampler::new(width, height, samples, seed);
let mut film = Film::new(width, height);
for (y, x) in Range2D::new(0..height, 0..width) {
let mut rgb = Vector::ZERO;
for mut pixel_samples in sampler.pixel(bx, by) {
let (x_offset, y_offset) = pixel_samples.get_2d();
let x = bx as f32 + x_offset;
let y = by as f32 + y_offset;
for mut pixel_sample in sampler.pixel(x, y) {
let p_film = film.get_camera_sample(x, y, &mut pixel_sample);
let ray = self.camera.ray(p_film);
let cx = 2.0 * x / w - 1.0;
let cy = (1.0 - 2.0 * y / h) * aspect_ratio;
if let Some(h) = self.intersect(self.camera.ray(cx, cy)) {
rgb += h.normal.normalize() * 0.5 + Vector::new(0.5, 0.5, 0.5);
if let Some(h) = self.intersect(ray) {
rgb += h.normal.normalize() * 0.5 + vec3(0.5, 0.5, 0.5);
}
}
rgb /= samples as f32;
pixel.write(Rgba::new(rgb.x, rgb.y, rgb.z, 1.0));
film.add_sample(x, y, rgba(rgb.x, rgb.y, rgb.z, 0.0), 1.0);
}
unsafe { data.assume_init() }
film.into_data()
}
fn intersect(&self, ray: Ray) -> Option<Hit> {
fn intersect(&self, r: Ray) -> Option<Hit> {
let mut hit: Option<Hit> = None;
for shape in &self.contents {
for obj in &self.objects {
let t_max = hit.as_ref().map(|h| h.t).unwrap_or(f32::INFINITY);
if let Some(h) = shape.intersect(&ray, t_max) {
hit = Some(h);
if let Some(h) = obj.intersect(&r, t_max) {
hit = Some(h)
}
}
hit
}
}

View file

@ -55,8 +55,9 @@ impl PyScene {
Self(Scene::new(camera.0.clone(), contents))
}
pub fn render(&self, width: u32, height: u32, samples: u32) -> Vec<f32> {
let result = self.0.render(width, height, samples);
#[pyo3(signature = (width, height, samples, seed = 0))]
pub fn render(&self, width: u32, height: u32, samples: u32, seed: u32) -> Vec<f32> {
let result = self.0.render(width, height, samples, seed);
cast_slice_box::<Rgba, f32>(result).into_vec()
}