Posted on

Table of Contents

Field of View (FOV)

From our last blog, we learned that to perform perspective projection, we need to define the frustum's boundaries: left, right, bottom, top, far, and near. For a camera, the Field of View (FOV) is crucial as it defines the width of the viewable area. In computer graphics, we utilize the fov-Y and the Aspect Ratio, where the aspect ratio is the width divided by the height.

Assuming symmetry: tanfovY2=tn \tan\frac{fovY}{2} = \frac{t}{|n|}

aspect=rt aspect = \frac{r}{t}

Canonical Cube to Screen

After applying Model transformation (placement of objects), View transformation (placement of the camera), and Projection transformation (Orthographic and Perspective Projection), we need to map the Canonical Cube ([1,1]3[-1,1]^3) to screen. This process involves placing an object into a 2D array of pixels. A Pixel, short for Picture Element, can be considered as a uniformly sized little square.

Rasterization is the process of drawing onto screen pixels.

screen_space

Here, we define coordinates starting from the left bottom corner. The Pixel locate at [x,y][x,y] and x,yZx,y \in \Z, start with zero.

What we do now is transform our canonical cube [1,1]3[-1,1]^3 to screen [0,width]×[0,height][0,\text{width}]\times[0,\text{height}] using matrix: Mviewport=[width200width20height20height200100001] M_{viewport} = \begin{bmatrix} \frac{\text{width}}{2} & 0 & 0 & \frac{\text{width}}{2} \cr 0 & \frac{\text{height}}{2} & 0 & \frac{\text{height}}{2} \cr 0 & 0 & 1 & 0 \cr 0 & 0 & 0 & 1 \cr \end{bmatrix}

Triangles

We can decompose any polygon into a mesh of triangles. Triangles offer numerous benefits:

  • Planar
  • Well-defined interior
  • Easy to do interpolation

Now, assume we have a triangle, and we know the coordinates of its corners after the transformations. How do we determine what our pixel should look like, especially at the edge of the triangle?

Sampling

We evaluate the triangle signal at our pixel, thus discretizing it.

Assume sampling at the centre of the pixel:

use rayon::prelude::*;

fn main() {
    let x_max = 1024;
    let y_max = 768;
    let mut image = vec![vec![false; y_max]; x_max];
    
    // assume triangle
    let triangle = [(0, 0), (1024, 0), (512, 768)];
    
    // AABB
    let min_x = triangle.iter().min_by_key(|v| v.0).unwrap().0;
    let max_x = triangle.iter().max_by_key(|v| v.0).unwrap().0;
    let min_y = triangle.iter().min_by_key(|v| v.1).unwrap().1;
    let max_y = triangle.iter().max_by_key(|v| v.1).unwrap().1;
    
    // pre-calc edge
    let edges = [
      (triangle[1].0 - triangle[0].0, triangle[1].1 - triangle[0].1),
      (triangle[2].0 - triangle[1].0, triangle[2].1 - triangle[1].1),
      (triangle[0].0 - triangle[2].0, triangle[0].1 - triangle[2].1),
    ];
    
    (min_x..=max_x).par_iter_mut().enumerate().for_each(|(x, row)| {
        for y in min_y..=max_y {
            image[x as usize][y as usize]= is_inside(&triangle, x as usize, y as usize);
        }
    });
}

fn is_inside(edges: &[(i32,i32);3],triangle: &[(i32, i32); 3], p:(usize,usize) -> bool {
   // p to edge
    let to_p = [
        (p.0 - triangle[0].0, p.1 - triangle[0].1),
        (p.0 - triangle[1].0, p.1 - triangle[1].1),
        (p.0 - triangle[2].0, p.1 - triangle[2].1),
    ];

    let cross = [
        edges[0].0 * to_p[0].1 - edges[0].1 * to_p[0].0,
        edges[1].0 * to_p[1].1 - edges[1].1 * to_p[1].0,
        edges[2].0 * to_p[2].1 - edges[2].1 * to_p[2].0,
    ];

    // if same sign, means inside
     cross[0].signum() == cross[1].signum() && cross[1].signum() == cross[2].signum()
}

basic_sampling

This approach may cause aliasing, due to our sampling rate not being high enough for our signal.