The Problem
A triangle in 3D space is defined by three vertices — v0, v1, and v2 — at arbitrary positions. The same triangle could appear at many different positions and rotations in the world, yet its shape (relative side lengths and angles) never changes.
To compare or process triangle geometry, we need a canonical 2D representation: a coordinate frame built from the triangle itself, where v0 sits at the origin and the base edge lies along the X-axis. The 2D coordinate of v2 in this frame fully describes the shape — no matter where the triangle was in 3D space.
Pin v0 at the origin. The base edge v0→v1 defines the local X-axis. Dividing by its length gives a unit vector $\hat{u}_1$ — our X-axis direction.
v0 = [1.0, 2.0, 1.0]
v1 = [5.0, 2.0, 1.0]
u1 = v1 − v0 = [4.00, 0.00, 0.00]
L = |u1| = 4.000
û1 = u1/L = [1.000, 0.000, 0.000]
Express v2 relative to v0 to get the offset $\vec{u}_2$. The dot product of $\vec{u}_2$ with the unit base $\hat{u}_1$ gives the component that lies along the base — the local X-coordinate.
v2 = [1.0, 5.0, 1.0]
u2 = v2 − v0 = [0.00, 3.00, 0.00]
# dot product = shadow on base axis
x = u2 · û1 = 0.000
Subtract the shadow component from $\vec{u}_2$, leaving only the part perpendicular to the base. Its length is the local Y-coordinate — the triangle's height.
shadow = x · û1 = [0.00, 0.00, 0.00]
u2⊥ = u2 − shadow = [0.00, 3.00, 0.00]
# height = length of remainder
y = |u2⊥| = 3.000
Why Does the Dot Product Give Us the Shadow?
The dot product of any vector $\vec{u}_2$ with a unit vector $\hat{u}_1$ returns the scalar length of $\vec{u}_2$'s shadow cast onto the line defined by $\hat{u}_1$. Formally:
where $\theta$ is the angle between them. When $\theta = 0°$ the vectors are parallel and $x = \lVert\vec{u}_2\rVert$ (full length). When $\theta = 90°$ the vectors are perpendicular and $x = 0$ (no shadow).
Subtracting this shadow from $\vec{u}_2$ leaves a vector exactly perpendicular to $\hat{u}_1$ — the height component. Together, shadow and height give us a complete, orthogonal decomposition of $\vec{u}_2$ in the triangle's own frame.
Result: Canonical 2D Coordinates
These three 2D coordinates fully encode the triangle's geometry. Two triangles with the same $v_0',\, v_1',\, v_2'$ are congruent — no matter where they were in 3D space.
What Is J? — The Map That Takes Source Edges to Target Edges
After Part 1, both triangles live in their own 2D frames. Pack each triangle's two edge vectors as columns of a matrix $M$. Then $J$ is simply the map that makes source edges become target edges:
Because $M$ is upper-triangular, each entry of $J$ has a direct geometric meaning:
L = base length (v0 → v1 along X) x = apex horizontal offset from v0 y = apex height above the base
Example: Ls=4, xs=1, ys=3 → source v2=(1, 3): base 4 wide, apex 1 right & 3 up from origin. Set Lt=8, xt=2, yt=6 and J ≈ 2·I — uniform 2× scale.
Scale ×2 — uniform stretch, σ_t = 2
Stretch X — base doubled, height fixed
Shear — off-diagonal J entry > 0
Near-flat — y_t → 0, σ_t → ∞
ARAP-like — nearly rigid, σ_t ≈ 1
Live Jacobian $J = M_t \, M_s^{-1}$
σmin = —
M_s = [[4.00, 1.00], [0, 3.00]]
M_t = [[4.00, 1.00], [0, 3.00]]
# J = M_t @ inv(M_s)
J[0,0] = Lt/Ls = 1.000 ← base scale
J[1,1] = yt/ys = 1.000 ← height scale
J[0,1] = (xt·Ls−Lt·xs)/(Ls·ys) = 0.000 ← shear
J[1,0] = 0 ← always zero
det J = J00·J11 = 1.000
σ_t = max(σ_max,1/σ_min) = 1.000
Classifying Jacobians: Rigid, Conformal, Non-Conformal
Any 2×2 Jacobian $J$ can be factored as $J = R(\theta)\,\mathrm{diag}(s_x,\,s_y)$ — first scale each axis independently, then rotate the whole result. Three components, three geometric meanings:
sx=0.5: base halves → mesh compressed horizontally
sx=1: no horizontal change
sy=0.5: height halves → mesh squashed toward base
sy=1: no vertical change
$\sigma_{\max} = s_x = 2$
$\sigma_{\min} = s_y = 0.5$
Singular values = sx, sy regardless of θ. Rotation doesn't stretch — only diag(sx, sy) does.
- Rigid (isometric): $s_x = s_y = 1$ — distances and angles preserved; mesh triangles keep their exact shape
- Conformal (angle-preserving): $s_x = s_y \ne 1$ — angles preserved, area scales uniformly; mesh scales but triangles stay similar
- Non-conformal: $s_x \ne s_y$ — angles distorted; mesh triangles are squashed or stretched along one axis
The 4-triangle mesh below makes $J$ concrete. Select a triangle, drag the sliders, and watch only that triangle deform while the others stay fixed — just like per-face Jacobians in a real UV solver.
Real Example: UV Parameterization
UV parameterization cuts a 3D surface and unfolds it flat. Each 3D triangle maps to a 2D triangle in the UV plane — and the Jacobian of that map is exactly what Parts 1 & 2 computed.
Project v₀,v₁,v₂ to local frame (Part 1):
Ms = [[Ls, xs], [0, ys]]
Pack UV coords into (Part 2):
Mt = [[Lt, xt], [0, yt]]
ARAP → 0: near-isometric (no stretch)
ACAP → 0: conformal (no shear)
Live Jacobian — T1 $J = R(\theta)\,\mathrm{diag}(s_x,\,s_y)$
σmin = —
ACAP = $(\sigma_{\max}{-}\sigma_{\min})^2/2$ → 0 iff conformal
Both singular values equal 1. The Jacobian is a pure rotation: $J = R(\theta)$. Distances and angles are preserved.
Mesh triangles keep their shape — no stretching or shearing. Both ARAP and ACAP energies are zero.
Both singular values are equal but $\ne 1$. The Jacobian is a scaled rotation: $J = s\,R(\theta)$. Angles preserved; area scales uniformly.
Mesh scales uniformly — triangles grow or shrink but stay similar. ACAP energy = 0; ARAP energy ≠ 0.
Singular values differ: $\sigma_{\max} \ne \sigma_{\min}$. Space is stretched more along one axis — angles are not preserved.
Mesh stretches differently along each axis — triangles become squashed or elongated. Both ARAP and ACAP energies are non-zero.
The Full Pipeline
Every idea in this tutorial feeds the next. Starting from raw 3D coordinates, three steps turn a pair of triangles into a single number measuring distortion:
Align the base edge with the X-axis. Every 3D triangle collapses to three numbers $(L,\,x,\,y)$ — base length, apex offset, apex height. The 3D embedding becomes irrelevant.
$J = M_t M_s^{-1}$ is the unique linear map sending source edges to target edges. Its entries directly encode base scale, height scale, and shear. Apply it to any point in the source triangle to find where it lands in the target.
Factor $J = R(\theta)\,\mathrm{diag}(\sigma_{\max},\sigma_{\min})$. The singular values are the semi-axes of the ellipse a unit circle maps to — they measure pure stretch independent of rotation.
Key Takeaways
By projecting each triangle into its own 2D frame, the comparison between triangles is independent of their position or orientation in 3D space.
J[0,0] = base scale, J[1,1] = height scale, J[0,1] = shear, J[1,0] = 0 always. Every distortion question reduces to reading entries of a 2×2 matrix.
They are the semi-axes of the ellipse a unit circle maps to under J — independent of rotation θ. ARAP penalises deviation from (1,1); ACAP penalises deviation from σ_max = σ_min.
Unwrapping a 3D mesh assigns a J to every triangle. Minimising ARAP energy globally gives near-isometric UV maps; minimising ACAP gives angle-preserving (conformal) maps.