Non-square Viewports

In which you shape the viewing volume so that it matches the shape of the viewport.

The renderers you've written so far have a flaw: when the browser window isn't square, the rendered image is distorted. The fix is a carefully crafted orthographic projection.

Distortion occurs when the viewport rectangle and the front face of viewing volume are not similar rectangles. To be similar, they must have the same width:height ratio. This ratio is called the aspect ratio.

The viewport's width and height is determined by the browser window, which you as a developer probably don't have much control over. The user sizes the window. However, you do get to pick the viewing volume. You must make its aspect ratio match the viewport's if you want to eliminate distortion. In particular, you want to make this statement true:

$$ \begin{aligned} \frac{\mathrm{viewport\ width}}{\mathrm{viewport\ height}} = \frac{\mathrm{right} - \mathrm{left}}{\mathrm{top} - \mathrm{bottom}} \end{aligned} $$

In your renderers, you compute the viewport's aspect ratio in onResizeWindow, since that is where you adjust the size of the canvas to match the browser window. You add the following statement after updating the canvas:

const aspectRatio = canvas.width / canvas.height;

Many of your renderers will center the viewing volume directly on the viewer's line of sight. In such cases, your viewing volume has the following symmetry:

$$ \begin{aligned} \mathrm{left} &= -\mathrm{right} \\ \mathrm{bottom} &= -\mathrm{top} \\ \end{aligned} $$

This symmetry allows you to simplify how you compute the aspect ratio of the viewing volume:

$$ \begin{aligned} \frac{\mathrm{right} - \mathrm{left}}{\mathrm{top} - \mathrm{bottom}} &= \frac{\mathrm{right} - -\mathrm{right}}{\mathrm{top} - -\mathrm{top}} \\ &= \frac{2 \times \mathrm{right}}{2 \times \mathrm{top}} \\ &= \frac{\mathrm{right}}{\mathrm{top}} \end{aligned} $$

This then is the statement that you need to make true:

$$ \frac{\mathrm{viewport\ width}}{\mathrm{viewport\ height}} = \frac{\mathrm{right}}{\mathrm{top}} $$

The user decides \(\mathrm{viewport\ width}\) and \(\mathrm{viewport\ height}\). You as a developer decide the value of either \(\mathrm{right}\) or \(\mathrm{top}\). You must solve for the remaining term. If you fix \(\mathrm{right}\), say at 8, then you calculate \(\mathrm{top}\) as follows:

const right = 8;
const top = right / aspectRatio;

If you fix \(\mathrm{top}\), say at 8, then you calculate \(\mathrm{right}\) as follows:

const top = 8;
const right = top * aspectRatio;

Once you have both variables assigned, you use them to generate a projection matrix whose aspect ratio matches the viewport:

const normalizedFromEye = Matrix4.ortho(-right, right, -top, top, near, far);

Try removing distortion from one of your renderers with this orthographic projection.