use image::{Rgb, RgbImage};
use nalgebra as na;
#[cfg(all(feature = "simd", nightly))]
use std::simd::{f32x4, num::SimdFloat};
use crate::mesh::Mesh;
#[derive(Debug, Clone)]
pub struct Camera {
pub rotation: na::Matrix3<f32>,
pub translation: na::Vector3<f32>,
pub focal_x: f32,
pub focal_y: f32,
pub cx: f32,
pub cy: f32,
pub width: u32,
pub height: u32,
pub near: f32,
pub far: f32,
}
impl Camera {
#[must_use]
pub fn default_front(width: u32, height: u32) -> Self {
let focal = width as f32 * 1.5;
Self {
rotation: na::Matrix3::identity(),
translation: na::Vector3::new(0.0, 0.0, 0.6),
focal_x: focal,
focal_y: focal,
cx: width as f32 / 2.0,
cy: height as f32 / 2.0,
width,
height,
near: 0.01,
far: 10.0,
}
}
#[inline]
#[must_use]
pub fn world_to_cam(&self, p: &na::Point3<f32>) -> na::Point3<f32> {
na::Point3::from(self.rotation * p.coords + self.translation)
}
#[inline]
#[must_use]
pub fn project(&self, p_cam: &na::Point3<f32>) -> [f32; 2] {
let x = self.focal_x * p_cam.x / p_cam.z + self.cx;
let y = self.focal_y * p_cam.y / p_cam.z + self.cy;
[x, y]
}
#[inline]
#[must_use]
pub fn project_with_recip_z(&self, p_cam: &na::Point3<f32>, recip_z: f32) -> [f32; 2] {
let x = self.focal_x * p_cam.x * recip_z + self.cx;
let y = self.focal_y * p_cam.y * recip_z + self.cy;
[x, y]
}
}
#[cfg(not(all(feature = "simd", nightly)))]
#[inline]
fn normalize_vector(x: f32, y: f32, z: f32) -> (f32, f32, f32) {
let len_sq = x * x + y * y + z * z;
let len = len_sq.sqrt();
if len > 1e-10 {
let recip_len = 1.0 / len;
(x * recip_len, y * recip_len, z * recip_len)
} else {
(x, y, z)
}
}
#[cfg(all(feature = "simd", nightly))]
#[inline]
fn normalize_vector(x: f32, y: f32, z: f32) -> (f32, f32, f32) {
let v = f32x4::from_array([x, y, z, 0.0]);
let len_sq = (v * v).reduce_sum();
let len = len_sq.sqrt();
if len > 1e-10 {
let recip_len = 1.0 / len;
(x * recip_len, y * recip_len, z * recip_len)
} else {
(x, y, z)
}
}
#[cfg(not(all(feature = "simd", nightly)))]
#[inline]
fn interpolate_and_encode_normal(
n0: &na::Vector3<f32>,
n1: &na::Vector3<f32>,
n2: &na::Vector3<f32>,
w0: f32,
w1: f32,
w2: f32,
) -> [u8; 3] {
let nx = n0.x * w0 + n1.x * w1 + n2.x * w2;
let ny = n0.y * w0 + n1.y * w1 + n2.y * w2;
let nz = n0.z * w0 + n1.z * w1 + n2.z * w2;
let (nx_norm, ny_norm, nz_norm) = normalize_vector(nx, ny, nz);
let encode = |v: f32| ((v * 0.5 + 0.5) * 255.0).clamp(0.0, 255.0) as u8;
[encode(nx_norm), encode(ny_norm), encode(nz_norm)]
}
#[cfg(all(feature = "simd", nightly))]
#[inline]
fn interpolate_and_encode_normal(
n0: &na::Vector3<f32>,
n1: &na::Vector3<f32>,
n2: &na::Vector3<f32>,
w0: f32,
w1: f32,
w2: f32,
) -> [u8; 3] {
let nx = n0.x * w0 + n1.x * w1 + n2.x * w2;
let ny = n0.y * w0 + n1.y * w1 + n2.y * w2;
let nz = n0.z * w0 + n1.z * w1 + n2.z * w2;
let (nx_norm, ny_norm, nz_norm) = normalize_vector(nx, ny, nz);
let normal_vec = f32x4::from_array([nx_norm, ny_norm, nz_norm, 0.0]);
let encoded = (normal_vec * f32x4::splat(0.5) + f32x4::splat(0.5)) * f32x4::splat(255.0);
let clamped = encoded.simd_clamp(f32x4::splat(0.0), f32x4::splat(255.0));
let arr = clamped.to_array();
[arr[0] as u8, arr[1] as u8, arr[2] as u8]
}
pub struct NormalMapRenderer;
const TILE_SIZE: i32 = 16;
impl NormalMapRenderer {
#[must_use]
pub fn render(mesh: &Mesh, camera: &Camera) -> RgbImage {
let w = camera.width as usize;
let h = camera.height as usize;
let mut img = RgbImage::new(camera.width, camera.height);
let mut depth_buf = vec![f32::INFINITY; w * h];
for face in &mesh.faces {
let (i0, i1, i2) = (face[0] as usize, face[1] as usize, face[2] as usize);
let n0 = &mesh.normals[i0];
let n1 = &mesh.normals[i1];
let n2 = &mesh.normals[i2];
let p0 = camera.world_to_cam(&mesh.vertices[i0]);
let p1 = camera.world_to_cam(&mesh.vertices[i1]);
let p2 = camera.world_to_cam(&mesh.vertices[i2]);
if p0.z <= camera.near || p1.z <= camera.near || p2.z <= camera.near {
continue;
}
let recip_z0 = 1.0 / p0.z;
let recip_z1 = 1.0 / p1.z;
let recip_z2 = 1.0 / p2.z;
let s0 = camera.project_with_recip_z(&p0, recip_z0);
let s1 = camera.project_with_recip_z(&p1, recip_z1);
let s2 = camera.project_with_recip_z(&p2, recip_z2);
let min_x = s0[0].min(s1[0]).min(s2[0]).max(0.0).floor() as i32;
let max_x = s0[0]
.max(s1[0])
.max(s2[0])
.min((camera.width - 1) as f32)
.ceil() as i32;
let min_y = s0[1].min(s1[1]).min(s2[1]).max(0.0).floor() as i32;
let max_y = s0[1]
.max(s1[1])
.max(s2[1])
.min((camera.height - 1) as f32)
.ceil() as i32;
if min_x > max_x || min_y > max_y {
continue;
}
let area = edge_fn(s0, s1, s2);
if area.abs() < 1e-10 {
continue;
}
let inv_area = 1.0 / area;
let tile_min_x = (min_x / TILE_SIZE) * TILE_SIZE;
let tile_max_x = ((max_x + TILE_SIZE - 1) / TILE_SIZE) * TILE_SIZE;
let tile_min_y = (min_y / TILE_SIZE) * TILE_SIZE;
let tile_max_y = ((max_y + TILE_SIZE - 1) / TILE_SIZE) * TILE_SIZE;
for tile_y in (tile_min_y..tile_max_y).step_by(TILE_SIZE as usize) {
for tile_x in (tile_min_x..tile_max_x).step_by(TILE_SIZE as usize) {
let px_min = tile_x.max(min_x);
let px_max = (tile_x + TILE_SIZE).min(max_x + 1);
let py_min = tile_y.max(min_y);
let py_max = (tile_y + TILE_SIZE).min(max_y + 1);
Self::rasterize_tile(
px_min,
px_max,
py_min,
py_max,
s0,
s1,
s2,
inv_area,
p0.z,
p1.z,
p2.z,
n0,
n1,
n2,
camera.near,
w,
&mut depth_buf,
&mut img,
);
}
}
}
img
}
#[inline]
#[allow(clippy::too_many_arguments)]
fn rasterize_tile(
px_min: i32,
px_max: i32,
py_min: i32,
py_max: i32,
s0: [f32; 2],
s1: [f32; 2],
s2: [f32; 2],
inv_area: f32,
z0: f32,
z1: f32,
z2: f32,
n0: &na::Vector3<f32>,
n1: &na::Vector3<f32>,
n2: &na::Vector3<f32>,
near: f32,
width: usize,
depth_buf: &mut [f32],
img: &mut RgbImage,
) {
let a01 = s0[1] - s1[1];
let a12 = s1[1] - s2[1];
let a20 = s2[1] - s0[1];
for py in py_min..py_max {
let p_y = py as f32 + 0.5;
let p_x_start = px_min as f32 + 0.5;
let mut e0 = edge_fn(s1, s2, [p_x_start, p_y]);
let mut e1 = edge_fn(s2, s0, [p_x_start, p_y]);
let mut e2 = edge_fn(s0, s1, [p_x_start, p_y]);
for px in px_min..px_max {
if e0 >= 0.0 && e1 >= 0.0 && e2 >= 0.0 {
let w0 = e0 * inv_area;
let w1 = e1 * inv_area;
let w2 = e2 * inv_area;
let depth = w0 * z0 + w1 * z1 + w2 * z2;
let idx = py as usize * width + px as usize;
if depth < depth_buf[idx] && depth > near {
depth_buf[idx] = depth;
let rgb = interpolate_and_encode_normal(n0, n1, n2, w0, w1, w2);
img.put_pixel(px as u32, py as u32, Rgb(rgb));
}
}
e0 += a12;
e1 += a20;
e2 += a01;
}
}
}
}
#[inline]
fn edge_fn(a: [f32; 2], b: [f32; 2], p: [f32; 2]) -> f32 {
(b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0])
}