-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: use typestates for camera configuration
This will make it easier to develop a clean API around additional camera settings like optional lighting.
- Loading branch information
Showing
14 changed files
with
321 additions
and
228 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(), | ||
) | ||
} |
Oops, something went wrong.