Perspective projection is a fundamental concept in 3D graphics. This transformation, akin to morphing a view frustum shape into a cube—known as Normalized Device Coordinates (NDC)—, generates a realistic foreshortening effect when applied to all scene vertices. The visualization below showcases this transformation on a set of cajas, rendered with a custom shader for shape morphing and viewed from a third-person perspective using a secondary camera, which enables the display of the main view frustum.

Shaders

Shaders, particularly the vertex shader, are essential for creating visualizations relying on advanced transformations—those not covered by the functions found on the p5 transform group or any composition of them. Executing these through software alone would not only be exceedingly cumbersome but also likely inefficient. The vertex shader demonstrates the application of GPU capabilities to carry out the necessary vertex transformations crucial for visualizing perspective transformation to NDC.

#version 300 es
// vertex shader excerpt
precision highp float;

uniform mat4 uModelMatrix; // <- model matrix bound with p5.treegl bindMatrices
uniform mat4 uViewFrustumMatrix;
uniform mat4 uEyeFrustumMatrix;
uniform float d; // morph param in [0, 1]
uniform float ndc;
uniform float n; // near plane

vec4 worldPosition(vec4 position4) {
  vec4 wPosition4 = uModelMatrix * position4; // model to world
  vec4 fPosition4 = uViewFrustumMatrix * wPosition4; // world to frustum
  vec2 xy = -(fPosition4.xy / fPosition4.z) * (1.0 + ndc) * n;
  fPosition4.xy = mix(fPosition4.xy, xy, d);
  return uEyeFrustumMatrix * fPosition4; // frustum to world
}

The worldPosition function in the vertex shader performs several operations to visualizing the desired transformation:

  1. Model transformation: It converts each vertex from model space to world space using the uModelMatrix matrix which is bound in the setup.
  2. Frustum space transformation: It then transforms these world space coordinates into frustum space using the uViewFrustumMatrix matrix.
  3. Perspective division and morphing: The shader modifies each frustum space vertex xy coordinates based on perspective division, using n (the near plane of the view frustum) and the ndc parameter, which ranges from 0 to 1, controlling the size of the NDC (0 aligns it with the view frustum near plane and 1 with the far plane). The morph parameter d allows for a dynamic blend between the original and the morphed positions (NDC), demonstrating the effect of foreshortening.
  4. Conversion back to world space: After applying the perspective transformation, it converts the coordinates back to world space using the uEyeFrustumMatrix matrix.

Sketch

The sketch couples shaders with an interactive 3D environment using the p5.treegl library. It features four main functions: setup, for initializing the scene; draw, which renders the visual elements, including viewFrustum; update, that adjusts the perspective to NDC transformation parameters based on the frustum camera’s position and orientation; and setUniforms, which passes these parameters to the shader.

Update

The update function sets the frustum camera’s position and retrieves its eye and view matrices using the p5.treegl eMatrix and vMatrix functions.

let e, v, p // p5.Matrix instances
const n = 81, ndc = 0.5, fovy = 0.75, z = 158

function update() {
  frustumCam.camera(0, 0, z, ...Tree.k, ...Tree.j) // Update frustum camera
  v = frustumCam.vMatrix() // Get frustum view (world to frustum transform)
  e = frustumCam.eMatrix() // Get frustum eye (frustum to world transform)
  const f = n * (1 + 2 * tan(fovy / 2) * (1 + ndc)) // Calculate far plane
  p.perspective(fovy, 1, n, f) // Set perspective projection
}

setUniforms

The setUniforms passes matrix uniforms alongside the other parameters to the shader.

function setUniforms(shader) {
  shader.setUniform('d', d.value()) // Emit morph parameter
  shader.setUniform('n', n) // Emit near plane
  shader.setUniform('ndc', ndc) // Emit NDC parameter
  shader.setUniform('uViewFrustumMatrix', v.mat4) // Emit frustum view matrix
  shader.setUniform('uEyeFrustumMatrix', e.mat4) // Emit frustum eye matrix
}

p5.treegl API references

References

  1. Jordan Santell’s 3D Projection: An insightful guide on 3D projection, featuring a perspective projection visualization movie.
  2. Song Ho Ahn’s Projection Matrix Notes: Detailed mathematical derivation of the projection matrix.