use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct TorusParams {
pub major_radius: f64,
pub minor_radius: f64,
pub u_steps: u32,
pub v_steps: u32,
}
impl Default for TorusParams {
fn default() -> Self {
Self {
major_radius: 3.0,
minor_radius: 1.0,
u_steps: 64,
v_steps: 32,
}
}
}
pub fn torus_point(u: f64, v: f64, params: &TorusParams) -> (f64, f64, f64) {
let r = params.major_radius;
let r2 = params.minor_radius;
let x = (r + r2 * v.cos()) * u.cos();
let y = (r + r2 * v.cos()) * u.sin();
let z = r2 * v.sin();
(x, y, z)
}
#[derive(Debug, Clone)]
pub struct SphereParams {
pub radius: f64,
pub u_steps: u32,
pub v_steps: u32,
}
impl Default for SphereParams {
fn default() -> Self {
Self {
radius: 3.0,
u_steps: 64,
v_steps: 32,
}
}
}
pub fn sphere_point(u: f64, v: f64, params: &SphereParams) -> (f64, f64, f64) {
let r = params.radius;
let x = r * v.sin() * u.cos();
let y = r * v.sin() * u.sin();
let z = r * v.cos();
(x, y, z)
}
pub fn project_orthographic(
point: (f64, f64, f64),
width: u32,
height: u32,
) -> Option<(usize, usize)> {
let (x, y, z) = point;
if z < 0.0 {
return None;
}
let hw = width as f64 / 2.0;
let hh = height as f64 / 2.0;
let col = (x + hw).round() as i64;
let row = (hh - y).round() as i64;
if col < 0 || col >= width as i64 || row < 0 || row >= height as i64 {
return None;
}
Some((col as usize, row as usize))
}
fn sample_texture(texture: &[Vec<[u8; 3]>], u_norm: f64, v_norm: f64) -> [u8; 3] {
if texture.is_empty() || texture[0].is_empty() {
return [0; 3];
}
let h = texture.len();
let w = texture[0].len();
let row = ((v_norm.rem_euclid(1.0)) * h as f64).floor() as usize % h;
let col = ((u_norm.rem_euclid(1.0)) * w as f64).floor() as usize % w;
texture[row][col]
}
pub fn render_torus(
texture: &Vec<Vec<[u8; 3]>>,
params: &TorusParams,
width: u32,
height: u32,
) -> Vec<Vec<[u8; 3]>> {
let bg = [20u8, 20, 20];
let mut image = vec![vec![bg; width as usize]; height as usize];
for ui in 0..params.u_steps {
for vi in 0..params.v_steps {
let u = 2.0 * PI * ui as f64 / params.u_steps as f64;
let v = 2.0 * PI * vi as f64 / params.v_steps as f64;
let point = torus_point(u, v, params);
let scale =
(width.min(height) as f64) / (2.0 * (params.major_radius + params.minor_radius));
let scaled = (point.0 * scale, point.1 * scale, point.2 * scale);
if let Some((col, row)) = project_orthographic(scaled, width, height) {
let u_norm = ui as f64 / params.u_steps as f64;
let v_norm = vi as f64 / params.v_steps as f64;
image[row][col] = sample_texture(texture, u_norm, v_norm);
}
}
}
image
}
pub fn render_sphere(
texture: &Vec<Vec<[u8; 3]>>,
params: &SphereParams,
width: u32,
height: u32,
) -> Vec<Vec<[u8; 3]>> {
let bg = [20u8, 20, 20];
let mut image = vec![vec![bg; width as usize]; height as usize];
let scale = (width.min(height) as f64) / (2.0 * params.radius);
for ui in 0..params.u_steps {
for vi in 0..params.v_steps {
let u = 2.0 * PI * ui as f64 / params.u_steps as f64;
let v = PI * vi as f64 / params.v_steps as f64;
let point = sphere_point(u, v, params);
let scaled = (point.0 * scale, point.1 * scale, point.2 * scale);
if let Some((col, row)) = project_orthographic(scaled, width, height) {
let u_norm = ui as f64 / params.u_steps as f64;
let v_norm = vi as f64 / params.v_steps as f64;
image[row][col] = sample_texture(texture, u_norm, v_norm);
}
}
}
image
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn torus_point_at_origin_angle() {
let params = TorusParams {
major_radius: 3.0,
minor_radius: 1.0,
u_steps: 32,
v_steps: 16,
};
let (x, y, z) = torus_point(0.0, 0.0, ¶ms);
assert!((x - 4.0).abs() < 1e-10, "x={x}");
assert!(y.abs() < 1e-10, "y={y}");
assert!(z.abs() < 1e-10, "z={z}");
}
#[test]
fn torus_point_distance_from_center_is_major_radius() {
let params = TorusParams::default();
for i in 0..8 {
let u = 2.0 * PI * i as f64 / 8.0;
let (x, y, _z) = torus_point(u, 0.0, ¶ms);
let dist = (x * x + y * y).sqrt();
let expected = params.major_radius + params.minor_radius;
assert!(
(dist - expected).abs() < 1e-9,
"dist={dist} expected={expected}"
);
}
}
#[test]
fn sphere_point_at_north_pole() {
let params = SphereParams::default();
let (x, y, z) = sphere_point(0.0, 0.0, ¶ms);
assert!(x.abs() < 1e-10);
assert!(y.abs() < 1e-10);
assert!((z - params.radius).abs() < 1e-10, "z={z}");
}
#[test]
fn sphere_point_on_surface() {
let params = SphereParams {
radius: 2.0,
u_steps: 16,
v_steps: 8,
};
for ui in 0..8 {
for vi in 0..4 {
let u = 2.0 * PI * ui as f64 / 8.0;
let v = PI * vi as f64 / 4.0;
let (x, y, z) = sphere_point(u, v, ¶ms);
let r = (x * x + y * y + z * z).sqrt();
assert!((r - params.radius).abs() < 1e-9, "r={r}");
}
}
}
#[test]
fn orthographic_center() {
let pt = (0.0, 0.0, 1.0);
let result = project_orthographic(pt, 100, 100);
assert!(result.is_some());
let (col, row) = result.unwrap();
assert_eq!(col, 50);
assert_eq!(row, 50);
}
#[test]
fn orthographic_behind_view_returns_none() {
let pt = (0.0, 0.0, -1.0);
assert!(project_orthographic(pt, 100, 100).is_none());
}
#[test]
fn orthographic_out_of_bounds_returns_none() {
let pt = (200.0, 0.0, 1.0);
assert!(project_orthographic(pt, 100, 100).is_none());
}
fn checker_texture(size: usize) -> Vec<Vec<[u8; 3]>> {
(0..size)
.map(|r| {
(0..size)
.map(|c| {
if (r + c) % 2 == 0 {
[255u8, 255, 255]
} else {
[0u8, 0, 0]
}
})
.collect()
})
.collect()
}
#[test]
fn render_torus_dimensions() {
let texture = checker_texture(32);
let params = TorusParams {
major_radius: 2.0,
minor_radius: 0.5,
u_steps: 16,
v_steps: 8,
};
let img = render_torus(&texture, ¶ms, 64, 64);
assert_eq!(img.len(), 64);
assert_eq!(img[0].len(), 64);
}
#[test]
fn render_torus_paints_some_pixels() {
let texture = checker_texture(16);
let params = TorusParams {
major_radius: 2.0,
minor_radius: 0.5,
u_steps: 64,
v_steps: 32,
};
let bg = [20u8, 20, 20];
let img = render_torus(&texture, ¶ms, 128, 128);
let painted = img.iter().flatten().any(|&px| px != bg);
assert!(painted, "torus render produced blank image");
}
#[test]
fn render_sphere_dimensions() {
let texture = checker_texture(32);
let params = SphereParams {
radius: 2.0,
u_steps: 16,
v_steps: 8,
};
let img = render_sphere(&texture, ¶ms, 64, 64);
assert_eq!(img.len(), 64);
assert_eq!(img[0].len(), 64);
}
#[test]
fn render_sphere_paints_some_pixels() {
let texture = checker_texture(16);
let params = SphereParams {
radius: 2.0,
u_steps: 64,
v_steps: 32,
};
let bg = [20u8, 20, 20];
let img = render_sphere(&texture, ¶ms, 128, 128);
let painted = img.iter().flatten().any(|&px| px != bg);
assert!(painted, "sphere render produced blank image");
}
#[test]
fn sample_texture_wraps() {
let tex = checker_texture(4);
let c1 = sample_texture(&tex, 0.0, 0.0);
let c2 = sample_texture(&tex, 1.0, 0.0);
assert_eq!(c1, c2);
}
}