use super::LedPanel;
use crate::math::{Matrix4, Point3, Vector3};
use crate::{tracking::CameraPose, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerspectiveCorrectionConfig {
pub fov: f64,
pub near_plane: f64,
pub far_plane: f64,
pub subpixel_accuracy: bool,
}
impl Default for PerspectiveCorrectionConfig {
fn default() -> Self {
Self {
fov: 90.0,
near_plane: 0.1,
far_plane: 100.0,
subpixel_accuracy: true,
}
}
}
pub struct PerspectiveCorrection {
config: PerspectiveCorrectionConfig,
}
impl PerspectiveCorrection {
pub fn new() -> Result<Self> {
Ok(Self {
config: PerspectiveCorrectionConfig::default(),
})
}
pub fn with_config(config: PerspectiveCorrectionConfig) -> Result<Self> {
Ok(Self { config })
}
pub fn compute_transform(
&self,
camera_pose: &CameraPose,
panel: &LedPanel,
) -> Result<Matrix4<f64>> {
let view = self.build_view_matrix(camera_pose);
let aspect = panel.aspect_ratio();
let projection = self.build_projection_matrix(aspect);
Ok(projection * view)
}
fn build_view_matrix(&self, camera_pose: &CameraPose) -> Matrix4<f64> {
let eye = camera_pose.position;
let forward = camera_pose.forward();
let up = camera_pose.up();
let target = eye + forward;
Self::look_at(&eye, &target, &up)
}
fn build_projection_matrix(&self, aspect: f64) -> Matrix4<f64> {
let fov_rad = self.config.fov.to_radians();
let f = 1.0 / (fov_rad / 2.0).tan();
let mut proj = Matrix4::zeros();
proj.data[0][0] = f / aspect;
proj.data[1][1] = f;
proj.data[2][2] = (self.config.far_plane + self.config.near_plane)
/ (self.config.near_plane - self.config.far_plane);
proj.data[2][3] = (2.0 * self.config.far_plane * self.config.near_plane)
/ (self.config.near_plane - self.config.far_plane);
proj.data[3][2] = -1.0;
proj
}
fn look_at(eye: &Point3<f64>, target: &Point3<f64>, up: &Vector3<f64>) -> Matrix4<f64> {
let f = (target - eye).normalize();
let s = f.cross(up).normalize();
let u = s.cross(&f);
let mut view = Matrix4::identity();
view.data[0][0] = s.x;
view.data[0][1] = s.y;
view.data[0][2] = s.z;
view.data[1][0] = u.x;
view.data[1][1] = u.y;
view.data[1][2] = u.z;
view.data[2][0] = -f.x;
view.data[2][1] = -f.y;
view.data[2][2] = -f.z;
view.data[0][3] = -s.dot(&eye.coords());
view.data[1][3] = -u.dot(&eye.coords());
view.data[2][3] = f.dot(&eye.coords());
view
}
pub fn correct_pixel(
&self,
pixel_x: usize,
pixel_y: usize,
panel: &LedPanel,
camera_pose: &CameraPose,
) -> Result<(f64, f64)> {
let x = (pixel_x as f64 / panel.resolution.0 as f64) * panel.width;
let y = (pixel_y as f64 / panel.resolution.1 as f64) * panel.height;
let world_pos = panel.position + Vector3::new(x, y, 0.0);
let transform = self.compute_transform(camera_pose, panel)?;
let transformed = transform * world_pos.to_homogeneous();
let w = if transformed[3].abs() > 1e-15 {
transformed[3]
} else {
1.0
};
let screen_x = transformed[0] / w;
let screen_y = transformed[1] / w;
Ok((screen_x, screen_y))
}
#[must_use]
pub fn config(&self) -> &PerspectiveCorrectionConfig {
&self.config
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::math::UnitQuaternion;
#[test]
fn test_perspective_correction_creation() {
let correction = PerspectiveCorrection::new();
assert!(correction.is_ok());
}
#[test]
fn test_perspective_with_config() {
let config = PerspectiveCorrectionConfig::default();
let correction = PerspectiveCorrection::with_config(config);
assert!(correction.is_ok());
}
#[test]
fn test_build_projection_matrix() {
let correction = PerspectiveCorrection::new().expect("should succeed in test");
let proj = correction.build_projection_matrix(16.0 / 9.0);
assert!(proj[(0, 0)] != 0.0);
assert!(proj[(1, 1)] != 0.0);
}
#[test]
fn test_look_at_matrix() {
let eye = Point3::new(0.0, 0.0, 5.0);
let target = Point3::new(0.0, 0.0, 0.0);
let up = Vector3::new(0.0, 1.0, 0.0);
let view = PerspectiveCorrection::look_at(&eye, &target, &up);
assert!(view[(3, 3)] != 0.0);
}
#[test]
fn test_compute_transform() {
let correction = PerspectiveCorrection::new().expect("should succeed in test");
let pose = CameraPose::new(Point3::new(0.0, 0.0, 5.0), UnitQuaternion::identity(), 0);
let panel = LedPanel::new(Point3::origin(), 5.0, 3.0, (1920, 1080), 2.5);
let transform = correction.compute_transform(&pose, &panel);
assert!(transform.is_ok());
}
}