Compare commits
2 commits
48abb19464
...
bfaf3c2cb6
Author | SHA1 | Date | |
---|---|---|---|
bfaf3c2cb6 | |||
38e28a2d57 |
8 changed files with 299 additions and 28 deletions
13
Cargo.lock
generated
13
Cargo.lock
generated
|
@ -20,6 +20,18 @@ version = "1.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "enum_dispatch"
|
||||
version = "0.3.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glam"
|
||||
version = "0.30.2"
|
||||
|
@ -52,6 +64,7 @@ name = "mechthild_core"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"enum_dispatch",
|
||||
"glam",
|
||||
]
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ repository.workspace = true
|
|||
|
||||
[dependencies]
|
||||
bytemuck = { version = "1.22.0", optional = true }
|
||||
enum_dispatch = "0.3.13"
|
||||
glam = "0.30.2"
|
||||
|
||||
[features]
|
||||
|
|
24
core/src/camera.rs
Normal file
24
core/src/camera.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -1,9 +1,24 @@
|
|||
use std::ops::{Add, Div, Mul, Sub};
|
||||
use std::{
|
||||
fmt,
|
||||
ops::{Add, Deref, Div, Mul, Sub},
|
||||
};
|
||||
|
||||
pub mod shapes;
|
||||
|
||||
#[repr(transparent)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[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()
|
||||
|
@ -16,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 {
|
||||
pub const ZERO: Self = Self(glam::Vec3A::ZERO);
|
||||
pub const I: Self = Self(glam::Vec3A::X);
|
||||
|
@ -30,6 +53,14 @@ impl Vector {
|
|||
self.0.length()
|
||||
}
|
||||
|
||||
pub fn length_squared(self) -> f32 {
|
||||
self.0.length_squared()
|
||||
}
|
||||
|
||||
pub fn normalize(self) -> Vector {
|
||||
Self(self.0.normalize())
|
||||
}
|
||||
|
||||
pub fn dot(self, other: Vector) -> f32 {
|
||||
self.0.dot(other.0)
|
||||
}
|
||||
|
@ -72,9 +103,19 @@ impl Add for Vector {
|
|||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Point(glam::Vec3A);
|
||||
|
||||
impl fmt::Debug for Point {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_tuple(stringify!(Point))
|
||||
.field(&self.0.x)
|
||||
.field(&self.0.y)
|
||||
.field(&self.0.z)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Point> for glam::Vec3 {
|
||||
fn from(value: Point) -> Self {
|
||||
value.0.into()
|
||||
|
@ -93,6 +134,10 @@ impl Point {
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<Vector> for Point {
|
||||
|
@ -111,10 +156,19 @@ impl Sub for Point {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[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)))
|
||||
|
@ -149,6 +203,15 @@ impl Mul<Vector> for Transform {
|
|||
}
|
||||
}
|
||||
|
||||
impl Mul<Point> for Transform {
|
||||
type Output = Point;
|
||||
|
||||
fn mul(self, rhs: Point) -> Self::Output {
|
||||
Point(self.0.transform_point3a(rhs.0))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Ray {
|
||||
origin: Point,
|
||||
direction: Vector,
|
||||
|
|
91
core/src/geometry/shapes.rs
Normal file
91
core/src/geometry/shapes.rs
Normal file
|
@ -0,0 +1,91 @@
|
|||
use std::fmt;
|
||||
|
||||
use enum_dispatch::enum_dispatch;
|
||||
|
||||
use super::{Point, Ray, Vector};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Hit {
|
||||
pub point: Point,
|
||||
pub normal: Vector,
|
||||
pub t: f32,
|
||||
}
|
||||
|
||||
#[enum_dispatch]
|
||||
pub(crate) trait Hittable {
|
||||
fn intersect(&self, ray: &Ray, t_max: f32) -> Option<Hit>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Sphere {
|
||||
center: Point,
|
||||
radius: f32,
|
||||
}
|
||||
|
||||
impl Hittable for Sphere {
|
||||
fn intersect(&self, ray: &Ray, t_max: f32) -> Option<Hit> {
|
||||
let oc = self.center - ray.origin;
|
||||
let a = ray.direction.length_squared();
|
||||
let h = ray.direction.dot(oc);
|
||||
let c = oc.length_squared() - self.radius * self.radius;
|
||||
let d = h * h - a * c;
|
||||
|
||||
(d > 0.0)
|
||||
.then(|| (h - d.sqrt()) / a)
|
||||
.filter(|t| *t < t_max && *t > 0.0)
|
||||
.map(|t| {
|
||||
let point = ray.at(t);
|
||||
let normal = point - self.center;
|
||||
Hit { point, normal, t }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Plane {
|
||||
normal: Vector,
|
||||
d: f32,
|
||||
}
|
||||
|
||||
impl Hittable for Plane {
|
||||
fn intersect(&self, ray: &Ray, t_max: f32) -> Option<Hit> {
|
||||
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());
|
||||
-(n_dot_o - self.d) / n_dot_r
|
||||
})
|
||||
.filter(|t| *t < t_max && *t > 0.0)
|
||||
.map(|t| Hit {
|
||||
point: ray.at(t),
|
||||
normal: self.normal,
|
||||
t,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[enum_dispatch(Hittable)]
|
||||
#[derive(Clone)]
|
||||
pub enum Shape {
|
||||
Sphere,
|
||||
Plane,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Shape {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Sphere(s) => s.fmt(f),
|
||||
Self::Plane(p) => p.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Shape {
|
||||
pub fn sphere(center: Point, radius: f32) -> Shape {
|
||||
Shape::from(Sphere { center, radius })
|
||||
}
|
||||
|
||||
pub fn plane(normal: Vector, d: f32) -> Shape {
|
||||
Shape::from(Plane { normal, d })
|
||||
}
|
||||
}
|
|
@ -2,38 +2,63 @@
|
|||
pub mod impl_bytemuck;
|
||||
|
||||
pub mod color;
|
||||
|
||||
mod camera;
|
||||
mod geometry;
|
||||
|
||||
use color::Rgba;
|
||||
use core::f32;
|
||||
|
||||
pub struct Scene;
|
||||
pub use camera::Camera;
|
||||
pub use geometry::{Point, Transform, Vector, shapes::Shape};
|
||||
|
||||
use color::Rgba;
|
||||
use geometry::{
|
||||
Ray,
|
||||
shapes::{Hit, Hittable},
|
||||
};
|
||||
|
||||
pub struct Scene {
|
||||
camera: Camera,
|
||||
contents: Vec<Shape>,
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
pub fn new(camera: Camera, contents: Vec<Shape>) -> Self {
|
||||
Self { camera, contents }
|
||||
}
|
||||
|
||||
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 w = width as f32;
|
||||
let h = height as f32;
|
||||
let aspect_ratio = h / w;
|
||||
|
||||
for (i, pixel) in data.iter_mut().enumerate() {
|
||||
let x = (i % width) as f32;
|
||||
let y = (i / width) as f32;
|
||||
let x = ((i % width) as f32 * 2.0 / w) - 1.0;
|
||||
let y = (1.0 - (i / width) as f32 * 2.0 / h) * aspect_ratio;
|
||||
|
||||
pixel.write(Rgba::new(
|
||||
x / (width - 1) as f32,
|
||||
y / (height - 1) as f32,
|
||||
0.5,
|
||||
1.0,
|
||||
));
|
||||
let rgb = self
|
||||
.intersect(self.camera.ray(x, y))
|
||||
.map(|h| h.normal.normalize() * 0.5 + Vector::new(0.5, 0.5, 0.5))
|
||||
.unwrap_or(Vector::ZERO);
|
||||
|
||||
pixel.write(Rgba::new(rgb.x, rgb.y, rgb.z, 1.0));
|
||||
}
|
||||
|
||||
unsafe { data.assume_init() }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Scene {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
fn intersect(&self, ray: Ray) -> Option<Hit> {
|
||||
let mut hit: Option<Hit> = None;
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
19
examples/basic.py
Normal file → Executable file
19
examples/basic.py
Normal file → Executable file
|
@ -1,11 +1,20 @@
|
|||
#!python
|
||||
from PIL import Image
|
||||
from mechthild import Scene
|
||||
from mechthild import Scene, Shape, Camera
|
||||
|
||||
WIDTH = 800
|
||||
HEIGHT = 600
|
||||
|
||||
render_result = Scene().render(WIDTH, HEIGHT)
|
||||
rgba = bytes([int(v * 255) for v in render_result])
|
||||
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)
|
||||
|
||||
img = Image.frombuffer('RGBA', (WIDTH, HEIGHT), rgba)
|
||||
img.save("test.png")
|
||||
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")
|
||||
|
|
|
@ -1,15 +1,58 @@
|
|||
use bytemuck::allocation::cast_slice_box;
|
||||
use mechthild_core::{Scene, color::Rgba};
|
||||
use mechthild_core::{Camera, Scene, Shape, color::Rgba};
|
||||
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")]
|
||||
#[derive(Clone)]
|
||||
struct PyShape(Shape);
|
||||
|
||||
#[pymethods]
|
||||
impl PyShape {
|
||||
#[staticmethod]
|
||||
pub fn sphere(center: (f32, f32, f32), radius: f32) -> Self {
|
||||
Self(Shape::sphere(center.into(), radius))
|
||||
}
|
||||
|
||||
#[staticmethod]
|
||||
pub fn plane(normal: (f32, f32, f32), d: f32) -> Self {
|
||||
Self(Shape::plane(normal.into(), d))
|
||||
}
|
||||
|
||||
pub fn __str__(&self) -> PyResult<String> {
|
||||
Ok(format!("{:?}", self.0))
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "Scene")]
|
||||
struct PyScene(Scene);
|
||||
|
||||
#[pymethods]
|
||||
impl PyScene {
|
||||
#[new]
|
||||
pub fn new() -> Self {
|
||||
Self(Scene::new())
|
||||
pub fn new(camera: &PyCamera, contents: Vec<PyShape>) -> Self {
|
||||
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> {
|
||||
|
@ -22,6 +65,8 @@ impl PyScene {
|
|||
/// A Python module implemented in Rust.
|
||||
#[pymodule]
|
||||
fn mechthild(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyCamera>()?;
|
||||
m.add_class::<PyShape>()?;
|
||||
m.add_class::<PyScene>()?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue