render normals

This commit is contained in:
wires 2025-04-24 09:26:08 -04:00
parent 38e28a2d57
commit bfaf3c2cb6
Signed by: wires
SSH key fingerprint: SHA256:9GtP+M3O2IivPDlw1UY872UPUuJH2gI0yG6ExBxaaiM
6 changed files with 127 additions and 33 deletions

24
core/src/camera.rs Normal file
View file

@ -0,0 +1,24 @@
use crate::geometry::{Point, Ray, Transform, Vector};
#[derive(Debug, Clone)]
pub struct Camera {
/// camera to world transform
transform: Transform,
}
impl Camera {
pub fn new(eye: Point, center: Point, up: Vector, fov: f32) -> Self {
let f = (center - eye).length();
let tan = (fov / 2.0).to_radians().tan();
let transform =
Transform::look_at(eye, center, up).inverse() * Transform::scale(tan * f, tan * f, f);
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)
}
}

View file

@ -1,6 +1,6 @@
use std::{ use std::{
fmt, fmt,
ops::{Add, Div, Mul, Sub}, ops::{Add, Deref, Div, Mul, Sub},
}; };
pub mod shapes; pub mod shapes;
@ -31,6 +31,14 @@ impl From<(f32, f32, f32)> for Vector {
} }
} }
impl Deref for Vector {
type Target = glam::Vec3A;
#[inline]
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Vector { impl Vector {
pub const ZERO: Self = Self(glam::Vec3A::ZERO); pub const ZERO: Self = Self(glam::Vec3A::ZERO);
pub const I: Self = Self(glam::Vec3A::X); pub const I: Self = Self(glam::Vec3A::X);
@ -49,6 +57,10 @@ impl Vector {
self.0.length_squared() self.0.length_squared()
} }
pub fn normalize(self) -> Vector {
Self(self.0.normalize())
}
pub fn dot(self, other: Vector) -> f32 { pub fn dot(self, other: Vector) -> f32 {
self.0.dot(other.0) self.0.dot(other.0)
} }
@ -199,6 +211,7 @@ impl Mul<Point> for Transform {
} }
} }
#[derive(Debug)]
pub struct Ray { pub struct Ray {
origin: Point, origin: Point,
direction: Vector, direction: Vector,

View file

@ -6,24 +6,24 @@ use super::{Point, Ray, Vector};
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct Hit { pub(crate) struct Hit {
point: Point, pub point: Point,
normal: Vector, pub normal: Vector,
t: f32, pub t: f32,
} }
#[enum_dispatch] #[enum_dispatch]
pub(crate) trait Hittable { pub(crate) trait Hittable {
fn intersect(&self, ray: Ray, t_max: f32) -> Option<Hit>; fn intersect(&self, ray: &Ray, t_max: f32) -> Option<Hit>;
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct Sphere { pub struct Sphere {
center: Point, center: Point,
radius: f32, radius: f32,
} }
impl Hittable for Sphere { impl Hittable for Sphere {
fn intersect(&self, ray: Ray, t_max: f32) -> Option<Hit> { fn intersect(&self, ray: &Ray, t_max: f32) -> Option<Hit> {
let oc = self.center - ray.origin; let oc = self.center - ray.origin;
let a = ray.direction.length_squared(); let a = ray.direction.length_squared();
let h = ray.direction.dot(oc); let h = ray.direction.dot(oc);
@ -41,19 +41,19 @@ impl Hittable for Sphere {
} }
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct Plane { pub struct Plane {
normal: Vector, normal: Vector,
d: f32, d: f32,
} }
impl Hittable for Plane { impl Hittable for Plane {
fn intersect(&self, ray: Ray, t_max: f32) -> Option<Hit> { fn intersect(&self, ray: &Ray, t_max: f32) -> Option<Hit> {
let n_dot_r = self.normal.dot(ray.direction); let n_dot_r = self.normal.dot(ray.direction);
(n_dot_r.abs() > f32::EPSILON) (n_dot_r.abs() > f32::EPSILON)
.then(|| { .then(|| {
let n_dot_o = self.normal.dot(ray.origin.to_vector()); let n_dot_o = self.normal.dot(ray.origin.to_vector());
-(n_dot_o + self.d) / n_dot_r -(n_dot_o - self.d) / n_dot_r
}) })
.filter(|t| *t < t_max && *t > 0.0) .filter(|t| *t < t_max && *t > 0.0)
.map(|t| Hit { .map(|t| Hit {
@ -65,6 +65,7 @@ impl Hittable for Plane {
} }
#[enum_dispatch(Hittable)] #[enum_dispatch(Hittable)]
#[derive(Clone)]
pub enum Shape { pub enum Shape {
Sphere, Sphere,
Plane, Plane,

View file

@ -3,40 +3,62 @@ pub mod impl_bytemuck;
pub mod color; pub mod color;
mod camera;
mod geometry; mod geometry;
use core::f32;
pub use camera::Camera;
pub use geometry::{Point, Transform, Vector, shapes::Shape}; pub use geometry::{Point, Transform, Vector, shapes::Shape};
use color::Rgba; use color::Rgba;
use geometry::{
Ray,
shapes::{Hit, Hittable},
};
pub struct Scene; pub struct Scene {
camera: Camera,
contents: Vec<Shape>,
}
impl Scene { impl Scene {
pub fn new() -> Self { pub fn new(camera: Camera, contents: Vec<Shape>) -> Self {
Self Self { camera, contents }
} }
pub fn render(&self, width: usize, height: usize) -> Box<[Rgba]> { pub fn render(&self, width: usize, height: usize) -> Box<[Rgba]> {
let ray = self.camera.ray(0.0, 0.0);
dbg!(&ray);
dbg!(self.intersect(ray));
let mut data = Box::new_uninit_slice(width * height); let mut data = Box::new_uninit_slice(width * height);
let w = width as f32;
let h = height as f32;
let aspect_ratio = h / w;
for (i, pixel) in data.iter_mut().enumerate() { for (i, pixel) in data.iter_mut().enumerate() {
let x = (i % width) as f32; let x = ((i % width) as f32 * 2.0 / w) - 1.0;
let y = (i / width) as f32; let y = (1.0 - (i / width) as f32 * 2.0 / h) * aspect_ratio;
pixel.write(Rgba::new( let rgb = self
x / (width - 1) as f32, .intersect(self.camera.ray(x, y))
y / (height - 1) as f32, .map(|h| h.normal.normalize() * 0.5 + Vector::new(0.5, 0.5, 0.5))
0.5, .unwrap_or(Vector::ZERO);
1.0,
)); pixel.write(Rgba::new(rgb.x, rgb.y, rgb.z, 1.0));
} }
unsafe { data.assume_init() } unsafe { data.assume_init() }
} }
}
impl Default for Scene { fn intersect(&self, ray: Ray) -> Option<Hit> {
fn default() -> Self { let mut hit: Option<Hit> = None;
Self::new() for shape in &self.contents {
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);
}
}
hit
} }
} }

View file

@ -1,10 +1,20 @@
#!python #!python
from PIL import Image
from mechthild import Scene, Shape, Camera
from mechthild import Shape WIDTH = 800
HEIGHT = 600
s1 = Shape.sphere((0.0, 0.5, 0.0), 0.5)
s2 = Shape.sphere((1.5, 2.0, -1.0), 2.0)
s1 = Shape.sphere((0.75, 1.0, 1.0), 1.0)
s2 = Shape.sphere((-1.5, 2.0, -2.0), 2.0)
ground = Shape.plane((0.0, 1.0, 0.0), 0.0) ground = Shape.plane((0.0, 1.0, 0.0), 0.0)
print(f"{s1}\n{s2}\n{ground}") 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])
render = scene.render(WIDTH, HEIGHT)
rgba = bytes([int(v * 255) for v in render])
image = Image.frombuffer('RGBA', (WIDTH, HEIGHT), rgba)
image.save("test.png")

View file

@ -1,8 +1,30 @@
use bytemuck::allocation::cast_slice_box; use bytemuck::allocation::cast_slice_box;
use mechthild_core::{Scene, Shape, color::Rgba}; use mechthild_core::{Camera, Scene, Shape, color::Rgba};
use pyo3::prelude::*; use pyo3::prelude::*;
#[pyclass(name = "Camera")]
#[derive(Clone)]
struct PyCamera(Camera);
#[pymethods]
impl PyCamera {
#[new]
pub fn new(
eye: (f32, f32, f32),
center: (f32, f32, f32),
up: (f32, f32, f32),
fov: f32,
) -> Self {
Self(Camera::new(eye.into(), center.into(), up.into(), fov))
}
pub fn __str__(&self) -> PyResult<String> {
Ok(format!("{:#?}", self.0))
}
}
#[pyclass(name = "Shape")] #[pyclass(name = "Shape")]
#[derive(Clone)]
struct PyShape(Shape); struct PyShape(Shape);
#[pymethods] #[pymethods]
@ -28,8 +50,9 @@ struct PyScene(Scene);
#[pymethods] #[pymethods]
impl PyScene { impl PyScene {
#[new] #[new]
pub fn new() -> Self { pub fn new(camera: &PyCamera, contents: Vec<PyShape>) -> Self {
Self(Scene::new()) let contents = contents.into_iter().map(|s| s.0).collect();
Self(Scene::new(camera.0.clone(), contents))
} }
pub fn render(&self, width: usize, height: usize) -> Vec<f32> { pub fn render(&self, width: usize, height: usize) -> Vec<f32> {
@ -42,6 +65,7 @@ impl PyScene {
/// A Python module implemented in Rust. /// A Python module implemented in Rust.
#[pymodule] #[pymodule]
fn mechthild(m: &Bound<'_, PyModule>) -> PyResult<()> { fn mechthild(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyCamera>()?;
m.add_class::<PyShape>()?; m.add_class::<PyShape>()?;
m.add_class::<PyScene>()?; m.add_class::<PyScene>()?;
Ok(()) Ok(())