subsphere_render/
lib.rs

1mod math;
2
3use math::{mat, vec};
4use subsphere::prelude::*;
5
6/// Renders a [`Scene`] from the perspective of a [`Camera`] to the given target image.
7pub fn render<Sphere: subsphere::Sphere>(
8    scene: &Scene<Sphere>,
9    camera: &Camera,
10    target: &mut image::RgbImage,
11) {
12    // Raytrace the sphere.
13    let size_x = target.width();
14    let size_y = target.height();
15    for (x, y, pixel) in target.enumerate_pixels_mut() {
16        let mut total = [0.0, 0.0, 0.0];
17        for [offset_x, offset_y] in SAMPLES {
18            let view_x = ((x as f64 + offset_x) / size_x as f64) * 2.0 - 1.0;
19            let view_y = 1.0 - ((y as f64 + offset_y) / size_y as f64) * 2.0;
20            let (start, dir) = camera.unproject([view_x, view_y]);
21            if let Some(t) = trace_sphere(start, dir) {
22                let pos = vec::add(start, vec::mul(dir, t));
23                let face = scene.sphere.face_at(pos);
24                let light =
25                    scene.ambient + vec::dot(pos, scene.light_dir).max(0.0) * (1.0 - scene.ambient);
26                let color = scene.faces[face.index()];
27                total = vec::add(total, vec::mul(color, light));
28            } else {
29                total = vec::add(total, scene.background);
30            }
31        }
32        *pixel = to_srgb(vec::div(total, SAMPLES.len() as f64));
33    }
34}
35
36/// The per-pixel sampling positions used for anti-aliasing.
37const SAMPLES: &[[f64; 2]] = &[
38    [0.375, 0.125],
39    [0.875, 0.375],
40    [0.125, 0.625],
41    [0.625, 0.875],
42];
43
44/// Describes a scene to be rendered.
45pub struct Scene<'a, Sphere> {
46    /// The background color for the scene.
47    pub background: Color,
48
49    /// The direction *towards* the light source.
50    pub light_dir: [f64; 3],
51
52    /// The amount of ambient light in the scene, between 0.0 and 1.0.
53    pub ambient: f64,
54
55    /// The sphere to be rendered.
56    ///
57    /// This is placed at the origin of the scene.
58    pub sphere: &'a Sphere,
59
60    /// The colors of `sphere`'s faces.
61    pub faces: Box<[Color]>,
62}
63
64/// Describes a perspective a [`Scene`] can be rendered from.
65pub struct Camera {
66    /// The position of the camera in the scene.
67    pos: [f64; 3],
68
69    /// The forward direction of the camera.
70    forward: [f64; 3],
71
72    /// The extent of the camera's view plane, along each view axis.
73    extent: [[f64; 3]; 2],
74
75    /// The amount that the camera rays diverge from being perpendicular to the view plane.
76    ///
77    /// This will be zero for othographic cameras, and positive for perspective cameras.
78    divergence: f64,
79}
80
81impl Camera {
82    /// Constructs an orthographic camera which looks at the sphere from the given position.
83    ///
84    /// The sphere will always be in the center of the view, with the Z axis pointing upwards,
85    /// along the positive view Y direction.
86    ///
87    /// `extent` specifies the scale factor which converts from view coordinates (in the range
88    /// [-1.0, 1.0]) to world coordinates along each view axis.
89    pub fn ortho(pos: [f64; 3], extent: [f64; 2]) -> Self {
90        Self::perspective(pos, extent, 0.0)
91    }
92
93    /// Constructs a perspective camera which looks at the sphere from the given position.
94    ///
95    /// The sphere will always be in the center of the view, with the Z axis pointing upwards,
96    /// along the positive view Y direction.
97    pub fn perspective(pos: [f64; 3], extent: [f64; 2], divergence: f64) -> Self {
98        let up = [0.0, 0.0, 1.0];
99        let forward = vec::normalize(vec::neg(pos));
100        let extent_x = vec::mul(vec::normalize(vec::cross(forward, up)), extent[0]);
101        let extent_y = vec::mul(vec::normalize(vec::cross(extent_x, forward)), extent[1]);
102        Self {
103            pos,
104            forward,
105            extent: [extent_x, extent_y],
106            divergence
107        }
108    }
109
110    /// Gets the ray which corresponds to the given pixel in view coordinates.
111    fn unproject(&self, coords: [f64; 2]) -> ([f64; 3], [f64; 3]) {
112        let offset = mat::apply(self.extent, coords);
113        let start = vec::add(self.pos, offset);
114        let dir = vec::normalize(vec::add(self.forward, vec::mul(offset, self.divergence)));
115        (start, dir)
116    }
117}
118
119/// The color type used for rendering.
120///
121/// Colors are defined in the linear sRGB color space, with components in the range [0.0, 1.0].
122pub type Color = [f64; 3];
123
124/// Assigns each face of the given sphere an arbitrary color.
125pub fn colorize(sphere: &impl subsphere::Sphere) -> Box<[Color]> {
126    use rand::seq::SliceRandom;
127    use rand::{Rng, SeedableRng};
128    let mut face_colors: Box<[Option<u32>]> = vec![None; sphere.num_faces()].into_boxed_slice();
129
130    // Assign each face a color that is different from its neighbors
131    let mut rng = rand::rngs::SmallRng::seed_from_u64(1);
132    let mut faces = sphere.faces().collect::<Vec<_>>();
133    faces.shuffle(&mut rng);
134    let mut num_colors = 6;
135    'retry: loop {
136        for face in faces.iter() {
137            let index = face.index();
138            let mut available_colors = (1u64 << num_colors) - 1;
139            for side in face.sides() {
140                let neighbor = side.complement().inside();
141                if let Some(color) = face_colors[neighbor.index()] {
142                    available_colors &= !(1 << color);
143                }
144            }
145            if available_colors == 0 {
146                // No colors available, try again with more colors
147                num_colors += 1;
148                continue 'retry;
149            }
150            let mut select = rng.random_range(0..available_colors.count_ones());
151            let mut color = available_colors.trailing_zeros();
152            while select > 0 {
153                available_colors &= !(1 << color);
154                select -= 1;
155                color = available_colors.trailing_zeros();
156            }
157            face_colors[index] = Some(color);
158        }
159        break;
160    }
161
162    // Convert assignments to colors
163    face_colors
164        .into_iter()
165        .map(|i| PALETTE[i.unwrap() as usize])
166        .collect::<Vec<_>>()
167        .into_boxed_slice()
168}
169
170/// The color palette used for coloring faces.
171const PALETTE: &[Color] = &[
172    [1.0, 0.2, 0.2], // Red
173    [0.2, 0.8, 0.2], // Green
174    [0.1, 0.4, 1.0], // Blue
175    [1.0, 1.0, 0.2], // Yellow
176    [1.0, 0.2, 1.0], // Magenta
177    [0.2, 1.0, 1.0], // Cyan
178    [0.7, 0.7, 0.7], // Gray
179];
180
181/// Converts a [`Color`] to an [`image::Rgb<u8>`](image::Rgb).
182fn to_srgb(linear: Color) -> image::Rgb<u8> {
183    image::Rgb(palette::Srgb::<u8>::from(palette::LinSrgb::from(linear)).into())
184}
185
186/// Computes the intersection of a ray with the unit sphere.
187///
188/// Specifically, this returns the smallest non-negative `t`, such that the point `start + dir * t`
189/// is on the sphere, or `None` if the ray does not intersect the sphere.
190fn trace_sphere(start: [f64; 3], dir: [f64; 3]) -> Option<f64> {
191    let a = 1.0;
192    let b = 2.0 * vec::dot(start, dir);
193    let c = vec::dot(start, start) - 1.0;
194    let disc = b * b - 4.0 * a * c;
195    if disc >= 0.0 {
196        let u = -b - b.signum() * disc.sqrt();
197        let t_0 = u / (2.0 * a);
198        let t_1 = 2.0 * c / u;
199        [t_0, t_1]
200            .into_iter()
201            .filter(|&t| t >= 0.0)
202            .min_by(|a, b| a.partial_cmp(b).unwrap())
203    } else {
204        None
205    }
206}