Compare commits

...

2 commits

Author SHA1 Message Date
094f4947dc
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.
2025-05-29 16:30:25 -04:00
a30b29a130
reorganize some geometry stuff 2025-05-29 11:32:31 -04:00
10 changed files with 470 additions and 154 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 rgba(r: f32, g: f32, b: f32, a: f32) -> Rgba {
Rgba { r, g, b, a }
}
pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
Self { 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

@ -3,52 +3,24 @@ use std::{
ops::{Add, AddAssign, Deref, Div, DivAssign, Mul, Sub},
};
pub mod shapes;
mod _2d;
mod shapes;
mod transform;
pub use _2d::*;
pub use shapes::{Hit, Hittable, Shape};
pub use transform::Transform;
#[repr(transparent)]
#[derive(Clone, Copy)]
pub struct Vector(glam::Vec3A);
impl fmt::Debug for Vector {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple(stringify!(Vector))
.field(&self.0.x)
.field(&self.0.y)
.field(&self.0.z)
.finish()
}
}
impl From<Vector> for glam::Vec3 {
fn from(value: Vector) -> Self {
value.0.into()
}
}
impl From<(f32, f32, f32)> for Vector {
fn from((x, y, z): (f32, f32, f32)) -> Self {
Self::new(x, y, z)
}
}
impl Deref for Vector {
type Target = glam::Vec3A;
#[inline]
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Vector {
pub const ZERO: Self = Self(glam::Vec3A::ZERO);
pub const I: Self = Self(glam::Vec3A::X);
pub const J: Self = Self(glam::Vec3A::Y);
pub const K: Self = Self(glam::Vec3A::Z);
pub fn new(x: f32, y: f32, z: f32) -> Self {
Self(glam::vec3a(x, y, z))
}
pub fn length(self) -> f32 {
self.0.length()
}
@ -68,6 +40,50 @@ impl Vector {
pub fn cross(self, other: Vector) -> Vector {
Self(self.0.cross(other.0))
}
pub fn same_hemisphere(self, other: Vector) -> bool {
self.dot(other) > 0.0
}
}
pub fn vec3(x: f32, y: f32, z: f32) -> Vector {
Vector(glam::vec3a(x, y, z))
}
impl fmt::Debug for Vector {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple(stringify!(Vector))
.field(&self.0.x)
.field(&self.0.y)
.field(&self.0.z)
.finish()
}
}
impl From<(f32, f32, f32)> for Vector {
fn from((x, y, z): (f32, f32, f32)) -> Self {
vec3(x, y, z)
}
}
impl From<Point> for Vector {
fn from(p: Point) -> Self {
Self(p.0)
}
}
impl Deref for Vector {
type Target = glam::Vec3A;
#[inline]
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<Vector> for glam::Vec3 {
fn from(value: Vector) -> Self {
value.0.into()
}
}
impl Mul<f32> for Vector {
@ -118,6 +134,14 @@ impl AddAssign for Vector {
#[derive(Clone, Copy)]
pub struct Point(glam::Vec3A);
impl Point {
pub const ORIGIN: Self = Self(glam::Vec3A::ZERO);
}
pub fn point3(x: f32, y: f32, z: f32) -> Point {
Point(glam::vec3a(x, y, z))
}
impl fmt::Debug for Point {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple(stringify!(Point))
@ -136,19 +160,7 @@ impl From<Point> for glam::Vec3 {
impl From<(f32, f32, f32)> for Point {
fn from((x, y, z): (f32, f32, f32)) -> Self {
Self::new(x, y, z)
}
}
impl Point {
pub const ORIGIN: Self = Self(glam::Vec3A::ZERO);
pub fn new(x: f32, y: f32, z: f32) -> Self {
Self(glam::vec3a(x, y, z))
}
pub fn to_vector(self) -> Vector {
Vector(self.0)
point3(x, y, z)
}
}
@ -168,58 +180,11 @@ impl Sub for Point {
}
}
#[derive(Copy, Clone)]
#[repr(transparent)]
pub struct Transform(glam::Affine3A);
impl fmt::Debug for Transform {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct(stringify!(Transform))
.field(stringify!(matrix3), &self.0.matrix3)
.field(stringify!(translation), &self.0.translation)
.finish()
}
}
impl Transform {
pub fn scale(x: f32, y: f32, z: f32) -> Self {
Self(glam::Affine3A::from_scale(glam::vec3(x, y, z)))
}
pub fn inverse(self) -> Self {
Self(self.0.inverse())
}
pub fn look_at(eye: Point, center: Point, up: Vector) -> Self {
Self(glam::Affine3A::look_at_lh(
eye.into(),
center.into(),
up.into(),
))
}
}
impl Mul for Transform {
type Output = Transform;
fn mul(self, rhs: Self) -> Self::Output {
Self(self.0 * rhs.0)
}
}
impl Mul<Vector> for Transform {
type Output = Vector;
fn mul(self, rhs: Vector) -> Self::Output {
Vector(self.0.transform_vector3a(rhs.0))
}
}
impl Mul<Point> for Transform {
impl Sub<Vector> for Point {
type Output = Point;
fn mul(self, rhs: Point) -> Self::Output {
Point(self.0.transform_point3a(rhs.0))
fn sub(self, rhs: Vector) -> Self::Output {
Point(self.0 - rhs.0)
}
}
@ -230,11 +195,11 @@ pub struct Ray {
}
impl Ray {
pub fn new(origin: Point, direction: Vector) -> Self {
Self { origin, direction }
}
pub fn at(&self, t: f32) -> Point {
self.origin + t * self.direction
}
}
pub fn ray(origin: Point, direction: Vector) -> Ray {
Ray { origin, direction }
}

188
core/src/geometry/_2d.rs Normal file
View file

@ -0,0 +1,188 @@
use std::{
fmt,
ops::{Add, AddAssign, Deref, Div, DivAssign, Mul, Sub},
};
#[repr(transparent)]
#[derive(Clone, Copy)]
pub struct Vector2(glam::Vec2);
impl Vector2 {
pub const ZERO: Self = Self(glam::Vec2::ZERO);
pub const U: Self = Self(glam::Vec2::X);
pub const V: Self = Self(glam::Vec2::Y);
}
pub const fn vec2(x: f32, y: f32) -> Vector2 {
Vector2(glam::vec2(x, y))
}
impl fmt::Debug for Vector2 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple(stringify!(Vector))
.field(&self.0.x)
.field(&self.0.y)
.finish()
}
}
impl From<(f32, f32)> for Vector2 {
fn from((x, y): (f32, f32)) -> Self {
vec2(x, y)
}
}
impl Mul<f32> for Vector2 {
type Output = Self;
fn mul(self, rhs: f32) -> Self::Output {
Self(self.0 * rhs)
}
}
impl Mul<Vector2> for f32 {
type Output = Vector2;
fn mul(self, rhs: Vector2) -> Self::Output {
Vector2(self * rhs.0)
}
}
impl Div<f32> for Vector2 {
type Output = Self;
fn div(self, rhs: f32) -> Self::Output {
Self(self.0 / rhs)
}
}
impl DivAssign<f32> for Vector2 {
fn div_assign(&mut self, rhs: f32) {
*self = *self / rhs;
}
}
impl Add for Vector2 {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self(self.0 + rhs.0)
}
}
impl AddAssign for Vector2 {
fn add_assign(&mut self, rhs: Self) {
*self = *self + rhs;
}
}
#[repr(transparent)]
#[derive(Clone, Copy)]
pub struct Point2(glam::Vec2);
impl Point2 {
pub const ORIGIN: Self = Self(glam::Vec2::ZERO);
}
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))
.field(&self.0.x)
.field(&self.0.y)
.finish()
}
}
impl Deref for Point2 {
type Target = glam::Vec2;
#[inline]
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Add<Vector2> for Point2 {
type Output = Point2;
fn add(self, rhs: Vector2) -> Self::Output {
Self(self.0 + rhs.0)
}
}
impl Sub<Vector2> for Point2 {
type Output = Point2;
fn sub(self, rhs: Vector2) -> Self::Output {
Self(self.0 - rhs.0)
}
}
impl Sub for Point2 {
type Output = Vector2;
fn sub(self, rhs: Self) -> Self::Output {
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>;
}
@ -52,7 +52,7 @@ impl Hittable for Plane {
let n_dot_r = self.normal.dot(ray.direction);
(n_dot_r.abs() > f32::EPSILON)
.then(|| {
let n_dot_o = self.normal.dot(ray.origin.to_vector());
let n_dot_o = self.normal.dot(ray.origin.into());
-(n_dot_o - self.d) / n_dot_r
})
.filter(|t| *t < t_max && *t > 0.0)

View file

@ -0,0 +1,69 @@
use std::{fmt, ops::Mul};
use super::{Point, Ray, Vector};
#[derive(Copy, Clone)]
#[repr(transparent)]
pub struct Transform(glam::Affine3A);
impl fmt::Debug for Transform {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct(stringify!(Transform))
.field(stringify!(matrix3), &self.0.matrix3)
.field(stringify!(translation), &self.0.translation)
.finish()
}
}
impl Transform {
pub fn scale(x: f32, y: f32, z: f32) -> Self {
Self(glam::Affine3A::from_scale(glam::vec3(x, y, z)))
}
pub fn inverse(self) -> Self {
Self(self.0.inverse())
}
pub fn look_at(eye: Point, center: Point, up: Vector) -> Self {
Self(glam::Affine3A::look_at_lh(
eye.into(),
center.into(),
up.into(),
))
}
}
impl Mul for Transform {
type Output = Transform;
fn mul(self, rhs: Self) -> Self::Output {
Self(self.0 * rhs.0)
}
}
impl Mul<Vector> for Transform {
type Output = Vector;
fn mul(self, rhs: Vector) -> Self::Output {
Vector(self.0.transform_vector3a(rhs.0))
}
}
impl Mul<Point> for Transform {
type Output = Point;
fn mul(self, rhs: Point) -> Self::Output {
Point(self.0.transform_point3a(rhs.0))
}
}
impl Mul<Ray> for Transform {
type Output = Ray;
fn mul(self, rhs: Ray) -> Self::Output {
Ray {
origin: self * rhs.origin,
direction: self * rhs.direction,
}
}
}

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()
}