Foundations to FrontierPart VII: Applied mathematics and computationBezhig

W06: Linear algebra becomes graphics and spatial computation

5 exercises, each with a complete solution. 1 code listing. 1 note from the engine.

On this page
  1. Prerequisites and observable goals
  2. The central question: which space contains the quantity?
  3. Order matters because the output of one map is the next input
  4. Derive barycentric coordinates rather than guess weights
  5. Why screen-linear attributes can be wrong under perspective
  6. Geometry, spatial indexing, and verification
  7. Independent practice
  8. Complete solutions: W06
  9. Retention and readiness

Prerequisites and observable goals#

Before

A01–A02, spatial vectors, C08, and T02: matrices, geometric representations, and parallel-computing concepts.

After

Keep coordinate spaces and homogeneous conventions explicit; derive barycentric and perspective-correct interpolation; connect geometric correctness to numerical and performance evidence.

The central question: which space contains the quantity?#

A coordinate tuple is a representation relative to a basis, origin, and units. The same physical point may have model, world, camera, clip, normalized-device, and screen coordinates. Applying a matrix without naming its source and destination spaces can produce a plausible image that is nevertheless mathematically inconsistent. Treat a transform as a typed function from one representation to another.

Use column vectors in this workshop. In two dimensions, represent a point as (x,y,1)T(x,y,1)^T and a direction as (dx,dy,0)T(d_x,d_y,0)^T. A homogeneous translation matrix

T=(10tx01ty001)T=\begin{pmatrix}1&0&t_x\\0&1&t_y\\0&0&1\end{pmatrix}

moves points but not directions. The final component encodes this distinction; it is not decorative padding. A general projective output (X,Y,W)(X,Y,W) with W≠0W\ne0 becomes Cartesian (X/W,Y/W)(X/W,Y/W). If W=0W=0, the same division is unavailable and the representation has a different projective meaning.

Order matters because the output of one map is the next input#

Let scaling be S=diag⁡(2,3,1)S=\diag(2,3,1) and translation (5,−1)(5,-1). For point (1,2,1)T(1,2,1)^T, TSTS first scales to (2,6,1)(2,6,1) and then translates to (7,5,1)(7,5,1). In the opposite order, STST first translates to (6,1,1)(6,1,1) and then scales to (12,3,1)(12,3,1). Matrices generally do not commute. Row-vector conventions reverse how composed matrices are written; copying a formula from another convention without changing the interpretation is a representation error, not a mysterious graphics bug.

The same idea explains normals. If tangent vectors transform by an invertible linear map AA, a transformed normal n′n' must satisfy (n′)T(Av)=0(n')^T(Av)=0 whenever nTv=0n^Tv=0. Choosing n′=A−Tnn'=A^{-T}n gives (A−Tn)TAv=nTv(A^{-T}n)^TAv=n^Tv. Normalize afterward when a unit normal is required. Transforming normals with AA works in some special cases, such as rotations, but not arbitrary nonuniform scaling. Singular transforms require separate treatment because the inverse does not exist.

Derive barycentric coordinates rather than guess weights#

For a nondegenerate triangle with vertices a,b,c∈R2a,b,c\in\R^2, write a point as

p=αa+βb+γc,α+β+γ=1.p=\alpha a+\beta b+\gamma c,\qquad \alpha+\beta+\gamma=1.

Subtract aa and eliminate α\alpha:

(∣∣b−ac−a∣∣)(βγ)=p−a,α=1−β−γ.\begin{pmatrix}|&|\\b-a&c-a\\|&|\end{pmatrix} \binom\beta\gamma=p-a,\qquad \alpha=1-\beta-\gamma.

The determinant is twice the signed triangle area. A zero determinant means a degenerate triangle and no unique two-dimensional barycentric coordinates. A very small determinant relative to coordinate scales indicates sensitivity; a floating-point nonzero check is not a robust geometric predicate for every adversarial mesh.

For a=(0,0),b=(4,0),c=(0,4)a=(0,0),b=(4,0),c=(0,4) and p=(1,1)p=(1,1), the equations are 4β=1,4γ=14\beta=1,4\gamma=1, so weights are (1/2,1/4,1/4)(1/2,1/4,1/4). Their sum is one and the reconstructed position is exactly (1,1)(1,1). A point inside the closed triangle has nonnegative weights. An outside point may have a negative weight even though the sum and reconstruction still hold. Interpolation and containment are different uses of the same representation.

Why screen-linear attributes can be wrong under perspective#

Perspective division is nonlinear. Suppose the vertices’ pre-division homogeneous components are wi>0w_i>0, and screen-space barycentric weights are λi\lambda_i. An attribute that is affine in the corresponding pre-projection geometry must be interpolated as

a(p)=∑iλiai/wi∑iλi/wi.a(p)=\frac{\sum_i\lambda_i a_i/w_i}{\sum_i\lambda_i/w_i}.

To see the normalization, let pre-projection weights be μi\mu_i. Projection gives screen weights proportional to μiwi\mu_iw_i, so λi=μiwi/∑jμjwj\lambda_i=\mu_iw_i/\sum_j\mu_jw_j. Solving for μi\mu_i and imposing ∑iμi=1\sum_i\mu_i=1 yields μi=(λi/wi)/∑jλj/wj\mu_i=(\lambda_i/w_i)/\sum_j\lambda_j/w_j. Substituting a=∑iμiaia=\sum_i\mu_i a_i gives the formula. This derivation assumes a consistent projective representation and a valid visible triangle; clipping and sign-changing ww require additional rules.

Use the weights (1/2,1/4,1/4)(1/2,1/4,1/4), w=(1,2,4)w=(1,2,4), and scalar attributes (0,8,16)(0,8,16). The numerator is 0+1+1=20+1+1=2 and denominator 1/2+1/8+1/16=11/161/2+1/8+1/16=11/16, giving 32/11≈2.909132/11\approx2.9091. Naive screen-linear interpolation gives 0+2+4=60+2+4=6, a different result. The issue is not that one is more aesthetically pleasing; they interpolate under different coordinate relationships.

Geometry, spatial indexing, and verification#

A spatial index usually produces candidates, not a geometric theorem for free. A grid bucket overlapping a query can contain points outside the exact region; candidate filtering must be followed by the final predicate. J05 makes this distinction explicit for axis-aligned boxes. In triangle work, a bounding box supplies a broad filter and barycentric coordinates supply a more precise test, with boundary and numerical policies documented.

Parallel execution adds another layer. Independent pixel or particle outputs can often be partitioned among workers, but shared reductions change accumulation order. Floating-point addition is not associative, so bitwise agreement with a sequential order may be unavailable even when both approximate the same real sum. Choose a tolerance or reproducibility contract appropriate to the use, test against an analytically justified or higher-accuracy reference, and include transfer/setup costs when claiming acceleration. A faster image with different geometry or different precision is not automatically a like-for-like speedup.

Independent practice#

F foundation C standard course work T transfer S optional stretch L runnable lab or project. Attempt the complete set before opening its solutions.

W06.01CSolution

With the workshop’s column-vector convention, apply scaling (2,3)(2,3) and then translation (5,−1)(5,-1) to point (1,2)(1,2) and direction (1,2)(1,2). Reverse the order for the point. Explain the three different results using input/output spaces.

W06.02TSolution

Let a tangent be v=(1,1)v=(1,1) and an orthogonal normal n=(1,−1)n=(1,-1). Apply A=diag⁡(2,1)A=\diag(2,1). Compare transforming both by AA with transforming the normal by A−TA^{-T}. Check orthogonality explicitly and explain the invertibility condition.

W06.03CSolution

Find barycentric coordinates of (2,1)(2,1) relative to a=(0,0),b=(4,0),c=(0,4)a=(0,0),b=(4,0),c=(0,4). Reconstruct the point, decide whether it is inside, and then repeat the containment decision for (4,2)(4,2).

W06.04TSolution

Use screen weights (1/2,1/4,1/4)(1/2,1/4,1/4), w=(1,2,4)w=(1,2,4), and attributes (0,8,16)(0,8,16) to derive both perspective-correct and naive interpolation. Explain when the two formulas agree for every attribute and identify one invalid denominator case excluded by the lab contract.

W06.05LSolution

Implement barycentric and perspective in integration_code/starters/graphics.py. Validate dimensions, finite values, a nonzero triangle determinant, nonnegative normalized interpolation weights, positive ww, and consistent attribute widths. Test reconstruction, vertices, an outside point, a degenerate triangle, and the exact rational interpolation example. State why the implementation is not a robust production rasterizer.

Optional small hints

For .02, orthogonality is a dot-product condition, not equal lengths. For .03, the weights for b,cb,c are the coordinates divided by 4. For .04, recover and normalize the pre-projection weights.

Complete solutions: W06#

Compare the first step where your work differs. A corrected answer is not yet an independent success.

W06.01Show solution

The homogeneous coordinate determines whether translation acts

Point (1,2,1)(1,2,1) under TSTS becomes (7,5,1)(7,5,1); direction (1,2,0)(1,2,0) becomes (2,6,0)(2,6,0) because translation multiplies its zero last component. The point under STST becomes (12,3,1)(12,3,1). The first composition maps model coordinates through scaling into the translated world frame; reversing it scales the translation too. None is contradictory: they are different maps with an explicit multiplication convention. A graphics API’s storage order is a separate issue from whether vectors are mathematically columns or rows.

Back to W06.01
W06.02Show solution

Preserve the annihilated tangent directions

The transformed tangent is Av=(2,1)Av=(2,1). Transforming the normal by AA gives (2,−1)(2,-1), whose dot product with the tangent is 4−1=34-1=3, not zero. The inverse-transpose gives (1/2,−1)(1/2,-1), whose dot product is 1−1=01-1=0. It can then be normalized without changing orthogonality. The derivation uses A−1A^{-1}; if AA is singular, it may collapse tangent dimensions and a separate geometric normal construction is required. Rotation matrices are a special case where inverse-transpose equals the original matrix.

Back to W06.02
W06.03Show solution

Interpolation coordinates also encode a containment test

For (2,1)(2,1), β=1/2,γ=1/4,α=1/4\beta=1/2,\gamma=1/4,\alpha=1/4. They sum to one and give (4β,4γ)=(2,1)(4\beta,4\gamma)=(2,1). All are nonnegative, so the point is inside. For (4,2)(4,2), β=1,γ=1/2,α=−1/2\beta=1,\gamma=1/2,\alpha=-1/2. Reconstruction still holds, but the negative weight places the point outside the closed triangle. Testing only that weights sum to one would not distinguish the two cases.

Back to W06.03
W06.04Show solution

Normalize after accounting for projective scale

The normalized pre-projection weights are (8/11,2/11,1/11)(8/11,2/11,1/11) because the unnormalized values are (1/2,1/8,1/16)(1/2,1/8,1/16) with sum 11/1611/16. Their attribute combination is 0+16/11+16/11=32/110+16/11+16/11=32/11. Naive screen interpolation is 6. For a given nontrivial set of weights, agreement for every possible attribute requires equal ww on all vertices with nonzero weights; then the common reciprocal cancels. Vertices with zero weights do not matter for that point. A zero ww makes an individual reciprocal invalid; mixed signs can also produce a zero total denominator, which the positive-ww contract excludes. Visibility/clipping is not implemented by simply ignoring a bad denominator.

Back to W06.04
W06.05Show solution

Reference and numerical boundaries

The barycentric solver computes the 2×22\times2 determinant and solves for two weights, deriving the third from the sum. The interpolator supports multiple attribute components and divides each weighted numerator by the shared normalization. It rejects invalid shapes before arithmetic rather than allowing silently truncated zip operations.

integration_code/solutions/graphics.pyPython
"""W06: triangle coordinates and perspective interpolation, CPU-only."""
import math


def barycentric(point,a,b,c):
    if any(len(p)!=2 or any(not math.isfinite(x) for x in p) for p in (point,a,b,c)):
        raise ValueError("finite two-dimensional points required")
    ux,uy=b[0]-a[0],b[1]-a[1]
    vx,vy=c[0]-a[0],c[1]-a[1]
    px,py=point[0]-a[0],point[1]-a[1]
    determinant=ux*vy-uy*vx
    if not math.isfinite(determinant) or determinant==0:
        raise ValueError("degenerate or unrepresentable triangle")
    beta=(px*vy-py*vx)/determinant
    gamma=(ux*py-uy*px)/determinant
    result=(1-beta-gamma,beta,gamma)
    if not all(math.isfinite(x) for x in result):raise ValueError("nonfinite weights")
    return result


def perspective(weights,w,attributes):
    if len(weights)!=3 or len(w)!=3 or len(attributes)!=3 or not attributes[0]:
        raise ValueError("three vertices and nonempty attributes required")
    width=len(attributes[0])
    if width>64 or any(len(row)!=width for row in attributes):raise ValueError("attribute shape")
    if any(not math.isfinite(x) for x in [*weights,*w,*[v for row in attributes for v in row]]):
        raise ValueError("finite inputs required")
    if any(x<0 for x in weights) or not math.isclose(math.fsum(weights),1.,rel_tol=0.,abs_tol=1e-12):
        raise ValueError("inside-triangle weights summing to one required")
    if any(x<=0 for x in w):raise ValueError("positive clip w required in this bounded lab")
    coefficients=[a/b for a,b in zip(weights,w)]
    denominator=math.fsum(coefficients)
    if not math.isfinite(denominator) or denominator<=0:raise ValueError("invalid denominator")
    result=[math.fsum(c*row[k] for c,row in zip(coefficients,attributes))/denominator
            for k in range(width)]
    if not all(math.isfinite(x) for x in result):raise ValueError("nonfinite attribute")
    return result


if __name__=='__main__':
    print(barycentric((1,1),(0,0),(4,0),(0,4)))
    print(perspective([.5,.25,.25],[1,2,4],[[0],[8],[16]]))

Tests compare reconstruction and the analytic 32/1132/11 value, as well as shape and domain rejection. An exact-zero determinant check does not address near-degenerate triangles, floating-point robustness, clipping, edge tie rules, depth buffering, antialiasing, or parallel rasterization. Those are substantial additional contracts. The lab teaches the geometry that a production implementation must preserve, not the claim that a short script is a complete renderer.

Back to W06.05

Retention and readiness#

Label the coordinate space and units on every vector in a graphics derivation. Reconstruct a point from barycentric weights, and explain why perspective division changes interpolation. In a performance report, state which geometric and precision contract stayed fixed.