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;
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:
- Model transformation: It converts each vertex from model space to world space using the
uModelMatrix
matrix which is bound in the setup. - Frustum space transformation: It then transforms these world space coordinates into frustum space using the
uViewFrustumMatrix
matrix. - Perspective division and morphing: The shader modifies each frustum space vertex
xy
coordinates based on perspective division, usingn
(the near plane of the view frustum) and thendc
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 parameterd
allows for a dynamic blend between the original and the morphed positions (NDC), demonstrating the effect of foreshortening. - 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
- grid().
- viewFrustum().
- iMatrix().
- eMatrix().
- vMatrix().
- Constants:
Tree.j
(= [0, 1, 0])
andTree.k
(= [0, 0, 1])
References
- Jordan Santell’s 3D Projection: An insightful guide on 3D projection, featuring a perspective projection visualization movie.
- Song Ho Ahn’s Projection Matrix Notes: Detailed mathematical derivation of the projection matrix.