Skip to content

Commit

Permalink
refactor: use typestates for camera configuration
Browse files Browse the repository at this point in the history
This will make it easier to develop a clean API around additional camera
settings like optional lighting.
  • Loading branch information
cbgbt committed Nov 24, 2024
1 parent 0582a1a commit 31e39d0
Show file tree
Hide file tree
Showing 14 changed files with 321 additions and 228 deletions.
2 changes: 1 addition & 1 deletion pyraydeon/examples/py_cubes.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ def bounding_box(self):
znear = 0.1
zfar = 10.0

cam = Camera.look_at(eye, focus, up).perspective(fovy, width, height, znear, zfar)
cam = Camera().look_at(eye, focus, up).perspective(fovy, width, height, znear, zfar)

paths = scene.render(cam)

Expand Down
2 changes: 1 addition & 1 deletion pyraydeon/examples/py_rhombohedron.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ def paths(self, cam):
znear = 0.1
zfar = 20.0

cam = Camera.look_at(eye, focus, up).perspective(fovy, width, height, znear, zfar)
cam = Camera().look_at(eye, focus, up).perspective(fovy, width, height, znear, zfar)

paths = scene.render(cam)

Expand Down
2 changes: 1 addition & 1 deletion pyraydeon/examples/triangles.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def bounding_box(self):
znear = 0.1
zfar = 10.0

cam = Camera.look_at(eye, focus, up).perspective(fovy, width, height, znear, zfar)
cam = Camera().look_at(eye, focus, up).perspective(fovy, width, height, znear, zfar)

paths = scene.render(cam)

Expand Down
27 changes: 12 additions & 15 deletions pyraydeon/src/scene.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,30 @@ use raydeon::WorldSpace;
use crate::linear::{ArbitrarySpace, Point2, Point3, Vec3};
use crate::shapes::Geometry;

pywrap!(Camera, raydeon::Camera);
pywrap!(Camera, raydeon::Camera<raydeon::Perspective, raydeon::Observation>);

#[pymethods]
impl Camera {
#[staticmethod]
#[new]
fn new() -> Self {
raydeon::Camera::default().into()
}

fn look_at(
&self,
eye: &Bound<'_, PyAny>,
center: &Bound<'_, PyAny>,
up: &Bound<'_, PyAny>,
) -> PyResult<LookingCamera> {
) -> PyResult<Camera> {
let eye = Point3::try_from(eye)?;
let center = Vec3::try_from(center)?;
let up = Vec3::try_from(up)?;
Ok(raydeon::Camera::look_at(eye.cast_unit(), center.cast_unit(), up.cast_unit()).into())
Ok(self
.0
.look_at(eye.cast_unit(), center.cast_unit(), up.cast_unit())
.into())
}

fn __repr__(slf: &Bound<'_, Self>) -> PyResult<String> {
let class_name = slf.get_type().qualname()?;
Ok(format!("{}<{:?}>", class_name, slf.borrow().0))
}
}

pywrap!(LookingCamera, raydeon::scene::LookingCamera);

#[pymethods]
impl LookingCamera {
fn perspective(&self, fovy: f64, width: f64, height: f64, znear: f64, zfar: f64) -> Camera {
self.0.perspective(fovy, width, height, znear, zfar).into()
}
Expand Down Expand Up @@ -137,7 +135,6 @@ impl LineSegment3D {

pub(crate) fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<Camera>()?;
// `LookingCamera` remains "private"
m.add_class::<Scene>()?;
m.add_class::<LineSegment2D>()?;
m.add_class::<LineSegment3D>()?;
Expand Down
5 changes: 4 additions & 1 deletion pyraydeon/src/shapes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,10 @@ impl raydeon::Shape<WorldSpace> for PythonGeometry {
collision_geometry
}

fn paths(&self, cam: &raydeon::Camera) -> Vec<raydeon::path::LineSegment3D<WorldSpace>> {
fn paths(
&self,
cam: &raydeon::Camera<raydeon::Perspective, raydeon::Observation>,
) -> Vec<raydeon::path::LineSegment3D<WorldSpace>> {
let segments: Option<_> = Python::with_gil(|py| {
let inner = self.slf.bind(py);
let cam = Camera::from(*cam);
Expand Down
4 changes: 3 additions & 1 deletion raydeon/examples/cube.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ fn main() {
let znear = 0.1;
let zfar = 10.0;

let camera = Camera::look_at(eye, focus, up).perspective(fovy, width, height, znear, zfar);
let camera = Camera::new()
.look_at(eye, focus, up)
.perspective(fovy, width, height, znear, zfar);

let paths = scene.attach_camera(camera).render();

Expand Down
4 changes: 3 additions & 1 deletion raydeon/examples/geom_perf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ fn main() {

let scene = Scene::new(generate_scene());

let camera = Camera::look_at(eye, focus, up).perspective(fovy, width, height, znear, zfar);
let camera = Camera::new()
.look_at(eye, focus, up)
.perspective(fovy, width, height, znear, zfar);

let paths = scene.attach_camera(camera).render();

Expand Down
4 changes: 3 additions & 1 deletion raydeon/examples/triangles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ fn main() {
let znear = 0.1;
let zfar = 10.0;

let camera = Camera::look_at(eye, focus, up).perspective(fovy, width, height, znear, zfar);
let camera = Camera::new()
.look_at(eye, focus, up)
.perspective(fovy, width, height, znear, zfar);

let paths = scene.attach_camera(camera).render();

Expand Down
265 changes: 265 additions & 0 deletions raydeon/src/camera.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
use euclid::Transform3D;

use crate::*;

#[derive(Debug, Copy, Clone)]
pub struct Observation {
pub eye: WPoint3,
pub center: WVec3,
pub up: WVec3,
}

impl Default for Observation {
fn default() -> Self {
Self::new((0.0, 0.0, 1.0), (0.0, 0.0, 0.0), (0.0, 1.0, 0.0))
}
}

/// Type parameter for a camera that isn't yet looking anywhere
#[derive(Debug, Copy, Clone)]
pub struct NoObservation;

impl Observation {
pub fn new(eye: impl Into<WPoint3>, center: impl Into<WVec3>, up: impl Into<WVec3>) -> Self {
let eye = eye.into();
let center = center.into();
let up = up.into().normalize();

Self { eye, center, up }
}

#[rustfmt::skip]
pub fn look_matrix(&self) -> WCTransform {
let Observation { eye, center, up, .. } = *self;
let f = (center - eye.to_vector()).normalize();
let s = f.cross(up).normalize();
let u = s.cross(f).normalize();

CWTransform::from_array(
// euclid used to let us specify things in column major order and now it doesn't.
// So we're just transposing it here.
CWTransform::new(
s.x, u.x, -f.x, eye.x,
s.y, u.y, -f.y, eye.y,
s.z, u.z, -f.z, eye.z,
0.0, 0.0, 0.0, 1.0,
)
.to_array_transposed(),
)
.inverse()
.unwrap()
}
}

#[derive(Debug, Copy, Clone)]
pub struct Perspective {
pub fovy: f64,
pub width: f64,
pub height: f64,
pub aspect: f64,
pub znear: f64,
pub zfar: f64,
}

impl Default for Perspective {
fn default() -> Self {
Self::new(45.0, 1920.0, 1080.0, 0.1, 100.0)
}
}

/// Type parameter for a camera that doesn't yet have a defined perspective
#[derive(Debug, Copy, Clone)]
pub struct NoPerspective;

impl Perspective {
pub fn new(fovy: f64, width: f64, height: f64, znear: f64, zfar: f64) -> Self {
let aspect = width / height;
Self {
fovy,
width,
height,
aspect,
znear,
zfar,
}
}
}

#[derive(Debug, Copy, Clone)]
pub struct Camera<P, O> {
pub observation: O,
pub perspective: P,
}

impl Camera<NoPerspective, NoObservation> {
pub fn new() -> Self {
Self {
observation: NoObservation,
perspective: NoPerspective,
}
}
}

impl Default for Camera<Perspective, Observation> {
fn default() -> Self {
Self {
observation: Observation::default(),
perspective: Perspective::default(),
}
}
}

impl<P, O> Camera<P, O> {
pub fn look_at(
self,
eye: impl Into<WPoint3>,
center: impl Into<WVec3>,
up: impl Into<WVec3>,
) -> Camera<P, Observation> {
let Camera { perspective, .. } = self;
let observation = Observation::new(eye.into(), center.into(), up.into());
Camera {
observation,
perspective,
}
}

pub fn perspective(
self,
fovy: f64,
width: f64,
height: f64,
znear: f64,
zfar: f64,
) -> Camera<Perspective, O> {
let Camera { observation, .. } = self;
let perspective = Perspective::new(fovy, width, height, znear, zfar);
Camera {
observation,
perspective,
}
}
}

impl Camera<Perspective, Observation> {
#[must_use]
pub fn canvas_transformation(&self) -> Transform3D<f64, WorldSpace, CanvasSpace> {
let p = &self.perspective;
let ymax = p.znear * (p.fovy * std::f64::consts::PI / 360.0).tan();
let xmax = ymax * p.aspect;

let frustum = frustum(-xmax, xmax, -ymax, ymax, p.znear, p.zfar);
self.observation.look_matrix().then(&frustum)
}

#[must_use]
pub fn camera_transformation(&self) -> Transform3D<f64, WorldSpace, CameraSpace> {
self.canvas_transformation()
.then_translate(Vec3::new(1.0, 1.0, 0.0))
.then_scale(
self.perspective.width / 2.0,
self.perspective.height / 2.0,
0.0,
)
.with_destination()
}

/// Chops a line segment into subsegments based on distance from camera
pub fn chop_segment(
&self,
segment: &LineSegment3D<WorldSpace>,
) -> Vec<LineSegment3D<WorldSpace>> {
let p1 = segment.p1.to_vector();
let p2 = segment.p2.to_vector();

// Transform the points to camera space, then chop based on the pixel length
let transformation = self.camera_transformation();
let canvas_points = transformation
.transform_point3d(p1.to_point())
.and_then(|p1t| {
transformation
.transform_point3d(p2.to_point())
.map(|p2t| (p1t.xy(), p2t.xy()))
});

// The pixel fidelity of the drawing instrument.
// TODO: Make this configurable
let pen_px_size = 4;

let chunk_count = canvas_points
.map(|(p1t, p2t)| {
let rough_chop_size = (p2t - p1t).length() / (pen_px_size as f64 / 2.0);
rough_chop_size.round_ties_even() as u32
})
.unwrap_or_else(|| {
let rough_chop_size = self.min_step_size();
((p2 - p1).length() / rough_chop_size).round_ties_even() as u32
});

if chunk_count == 0 {
return vec![];
}
if chunk_count == 1 {
return vec![*segment];
}

let segment_diff = p2 - p1;
let segment_length = segment_diff.length();

let true_chunk_len = segment_length / chunk_count as f64;
let true_chunk_len = f64::min(true_chunk_len, segment_length);

let segment_dir = segment_diff.normalize();
let chunk_vec = segment_dir * true_chunk_len;
(0..chunk_count)
.map(|segment_ndx| {
let p1 = segment.p1 + (chunk_vec * (segment_ndx as f64));
let p2 = p1 + chunk_vec;
LineSegment3D::tagged(p1, p2, segment.tag)
})
.collect()
}
}

impl<O> Camera<Perspective, O> {
#[must_use]
fn min_step_size(&self) -> f64 {
let p = &self.perspective;
let ymax = p.znear * (p.fovy * std::f64::consts::PI / 360.0).tan();
let xmax = ymax * p.aspect;

// TODO: We can apply scaling here based on pen size
let effective_dims: Vec2<()> = Vec2::new(p.width, p.height);

let znear_dims = Vec2::new(xmax, ymax) * 2.0;
let est_min_pix = znear_dims.component_div(effective_dims);
f64::min(est_min_pix.x, est_min_pix.y)
}
}

#[rustfmt::skip]
fn frustum(
l: f64,
r: f64,
b: f64,
t: f64,
n: f64,
f: f64,
) -> Transform3D<f64, CameraSpace, CanvasSpace> {
let t1 = 2.0 * n;
let t2 = r - l;
let t3 = t - b;
let t4 = f - n;

// euclid used to let us specify things in column major order and now it doesn't.
// So we're just transposing it here.
Transform3D::from_array(
Transform3D::<f64, CameraSpace, CanvasSpace>::new(
t1 / t2, 0.0, (r + l) / t2, 0.0,
0.0, t1 / t3, (t + b) / t3, 0.0,
0.0, 0.0, (-f - n) / t4,(-t1 * f) / t4,
0.0, 0.0, -1.0, 0.0,
)
.to_array_transposed(),
)
}
Loading

0 comments on commit 31e39d0

Please sign in to comment.