Computer Graphics
Barycentric Coordinates
The weighted-average coordinate system that fills every triangle on your screen
Barycentric coordinates express any point inside a triangle as a weighted average of its three vertices, with weights (λ₀, λ₁, λ₂) that always sum to 1. A point is inside the triangle exactly when all three weights are non-negative, and every per-vertex attribute — color, texture coordinates, depth, normals — is filled in with that same weighted blend. Introduced by August Ferdinand Möbius in 1827, they are the mathematical engine of triangle rasterization, computed on modern GPUs through three signed edge functions in O(1) per pixel.
- Coordinates per point3 weights (2 independent)
- Constraintλ₀ + λ₁ + λ₂ = 1
- Inside testall λᵢ ≥ 0
- Cost per pixelO(1) — 3 edge functions
- Interpolation typeAffine (perspective-correct via /w)
- Introduced byA. F. Möbius, 1827
Interactive visualization
Press play, or step through manually. The visualization is yours to drive — try it before reading on.
Watch the 60-second explainer
A condensed visual walkthrough — narrated, captioned, under a minute.
What barycentric coordinates actually are
Pick a triangle with vertices A, B, and C. Any point P in the plane can be written uniquely as a weighted sum of those three vertices:
P = λ₀·A + λ₁·B + λ₂·C, subject to λ₀ + λ₁ + λ₂ = 1.
The three numbers (λ₀, λ₁, λ₂) are the barycentric coordinates of P. The name comes from barycenter — center of mass. If you place a physical mass λ₀ at A, λ₁ at B, and λ₂ at C, then P is exactly the balance point. Möbius framed the whole idea this way in his 1827 Der barycentrische Calcul.
Because the three weights are forced to sum to 1, only two of them are free — barycentric coordinates form a 2-dimensional affine coordinate system that is glued to the triangle rather than to the global x/y axes. The vertices themselves get the cleanest possible coordinates: A is (1, 0, 0), B is (0, 1, 0), C is (0, 0, 1). The centroid is (⅓, ⅓, ⅓), and the midpoint of edge BC is (0, ½, ½).
Weights are area ratios
The single most useful fact: each barycentric weight equals a ratio of areas. Split the triangle into three sub-triangles by connecting P to the three vertices. Then
- λ₀ = Area(P, B, C) / Area(A, B, C) — the sub-triangle opposite vertex A,
- λ₁ = Area(A, P, C) / Area(A, B, C) — opposite B,
- λ₂ = Area(A, B, P) / Area(A, B, C) — opposite C.
The three sub-areas add up to the whole triangle, which is exactly why the weights sum to 1. Move P toward vertex A and the opposite sub-triangle grows to fill the whole triangle, driving λ₀ → 1 while λ₁ and λ₂ shrink to 0. This area-ratio picture is not just intuition — it is literally how the weights are computed, because a 2D signed area is a cross product.
The inside test: three sign checks
Use signed areas (positive when the vertices are wound counter-clockwise). Then barycentric coordinates are defined for the whole plane, and the sign of each weight tells you which side of an edge P is on:
- All three λᵢ > 0 → P is strictly inside the triangle.
- Some λᵢ = 0 → P lies on the edge opposite vertex i.
- Any λᵢ < 0 → P is outside, past the edge opposite vertex i.
That is the entire point-in-triangle test: compute the weights, check three signs. It runs in O(1), branches cleanly, and handles the whole plane without special cases — which is precisely why the GPU evaluates it for every candidate pixel while rasterizing.
Interpolating attributes across the triangle
Here is where barycentric coordinates earn their keep in graphics. Suppose each vertex carries some attribute — a color, a UV texture coordinate, a normal, a depth value. The attribute at any interior point P is the same weighted blend that locates P:
attr(P) = λ₀·attr(A) + λ₁·attr(B) + λ₂·attr(C).
Because the weights are non-negative and sum to 1 inside the triangle, this is a genuine convex combination: the interpolated value can never overshoot the range of the three vertex values. Fill a triangle's three corners with red, green, and blue and you get the classic smooth RGB gradient — every pixel's color is its barycentric mix. Gouraud shading, vertex-color gradients, UV texture mapping, and z-buffer depth all reduce to this single formula. One set of weights per pixel drives all of them at once.
Rasterization: from edge functions to pixels
A triangle rasterizer visits candidate pixels (usually within the triangle's bounding box) and, for each one, asks two questions: is this pixel inside, and if so what are its barycentric weights? Juan Pineda's 1988 paper A Parallel Algorithm for Polygon Rasterization gave the answer that hardware still uses: the edge function.
For an edge from V₀ to V₁, define
E₀₁(P) = (P.x − V₀.x)·(V₁.y − V₀.y) − (P.y − V₀.y)·(V₁.x − V₀.x).
This 2D cross product returns twice the signed area of triangle (V₀, V₁, P). It is affine in P.x and P.y, so stepping one pixel to the right changes it by a constant, and stepping down a row changes it by another constant — the rasterizer can walk the whole bounding box with two additions per pixel and no multiplies. Evaluate all three edge functions; their signs give the inside test, and dividing each by the total signed area (2·Area, itself an edge function of the third vertex) gives the three barycentric weights for free. This is why the inside test and the interpolation weights are computed by the exact same machinery.
A worked example
Take A = (0, 0), B = (4, 0), C = (0, 3), and a query point P = (1, 1). The total signed area is ½·|(B−A) × (C−A)| = ½·(4·3 − 0·0) = 6, so 2·Area = 12.
- λ₀ (opposite A) = signed area(P, B, C) · 2 / 12. Edge B→C with P gives (4−1)(3−1) − ... working it through yields λ₀ = 1 − λ₁ − λ₂.
- λ₁ (opposite B) = 2·area(A, P, C) / 12 = (1·3 − 1·0)/12 · ... = 3/12 = ¼.
- λ₂ (opposite C) = 2·area(A, B, P) / 12 = (4·1 − 0·1)/12 = 4/12 = ⅓.
- λ₀ = 1 − ¼ − ⅓ = 5/12.
Check: 5/12 + 3/12 + 4/12 = 12/12 = 1, and all three are positive, so P is inside. If a vertex color at A is red (1,0,0), B is green (0,1,0), C is blue (0,0,1), then P's color is (5/12, 3/12, 4/12) — a slightly reddish blend, exactly matching how close P sits to each corner.
The catch: perspective-correct interpolation
Barycentric interpolation in screen space is exactly linear in the projected image. But perspective projection is not linear — a receding surface compresses toward the horizon. So if you linearly interpolate texture coordinates using screen-space weights, the texture warps: the infamous wobbly, swimming checkerboard on a tilted floor that plagued the PlayStation 1.
The fix (attributed to Jim Blinn and standard in every modern GPU) is to interpolate attr / w and 1 / w linearly with the barycentric weights, then divide:
attr(P) = ( Σ λᵢ · attrᵢ / wᵢ ) / ( Σ λᵢ / wᵢ ),
where wᵢ is the clip-space w (roughly the camera-space depth) of vertex i. Depth is the happy exception: z/w interpolates linearly and correctly under perspective, which is exactly why hardware z-buffers store post-projection depth rather than raw camera distance.
Barycentric vs bilinear interpolation
| Barycentric (triangle) | Bilinear (quad) | |
|---|---|---|
| Domain | Triangle (3 vertices) | Quad / grid cell (4 corners) |
| Weights | 3, sum to 1 | 4, sum to 1 |
| Function form | Affine (exactly planar) | Product of two linears → quadratic |
| Coordinate-independent? | Yes (intrinsic to triangle) | No (axis-aligned parameterization) |
| Inside test | All λᵢ ≥ 0 | 0 ≤ u ≤ 1 and 0 ≤ v ≤ 1 |
| Primary use | Triangle rasterization, FEM shape functions | Texture filtering, image resampling |
| Cost per sample | O(1) — 3 edge functions | O(1) — 4 texel reads + 3 lerps |
Implementation: computing barycentric coordinates
The clean way is Cramer's rule on the two-vector form. Let v0 = B − A, v1 = C − A, v2 = P − A. Solve P − A = λ₁·v0 + λ₂·v1 for the two free weights, then recover λ₀:
def barycentric(A, B, C, P):
"""Return (l0, l1, l2) for point P w.r.t. triangle ABC.
Inside the triangle iff all three are >= 0."""
v0 = (B[0] - A[0], B[1] - A[1]) # edge A->B
v1 = (C[0] - A[0], C[1] - A[1]) # edge A->C
v2 = (P[0] - A[0], P[1] - A[1]) # A->P
# Cross products = 2x signed sub-triangle areas
d = v0[0] * v1[1] - v1[0] * v0[1] # 2 * signed area of ABC
if d == 0:
return None # degenerate (collinear) triangle
l1 = (v2[0] * v1[1] - v1[0] * v2[1]) / d
l2 = (v0[0] * v2[1] - v2[0] * v0[1]) / d
l0 = 1.0 - l1 - l2
return (l0, l1, l2)
def inside_triangle(A, B, C, P):
bc = barycentric(A, B, C, P)
return bc is not None and all(w >= 0 for w in bc)
def interpolate(A, B, C, P, attrA, attrB, attrC):
"""Linear (screen-space) interpolation of a per-vertex attribute."""
l0, l1, l2 = barycentric(A, B, C, P)
return l0 * attrA + l1 * attrB + l2 * attrC
The scalar d is 2·Area(ABC); it is computed once per triangle and reused for every pixel. In a real rasterizer you would instead precompute the three edge-function increments and step them additively across the bounding box — same math, no per-pixel divide until you actually need normalized weights.
Common misconceptions and pitfalls
- Assuming weights are only defined inside the triangle. They cover the whole plane; outside points simply have one or two negative weights that still sum to 1. The negative sign is the inside test.
- Interpolating UVs linearly under perspective. Screen-space linear interpolation warps textures. You must divide by interpolated 1/w. Depth (z/w) is the exception that is already linear.
- A degenerate (zero-area) triangle. If the three vertices are collinear, 2·Area = 0 and the weights are undefined — you divide by zero. Rasterizers cull these before shading.
- Cracks and double-shading along shared edges. Two triangles sharing an edge must agree on whether an edge pixel belongs to one or the other. GPUs use a consistent fill rule (e.g. top-left) so exactly one triangle owns each boundary pixel.
- Confusing barycentric with bilinear. Barycentric is affine on a triangle; bilinear is quadratic on a quad. They are different interpolants for different primitives.
- Floating-point sign flicker on the boundary. A weight of exactly 0 is fragile in floating point; robust rasterizers snap to fixed-point coordinates so edge tests are exact and deterministic.
Frequently asked questions
What are barycentric coordinates?
Barycentric coordinates are three weights (λ₀, λ₁, λ₂) that express a point P as a weighted average of a triangle's vertices: P = λ₀A + λ₁B + λ₂C, with the constraint λ₀ + λ₁ + λ₂ = 1. Each weight equals the ratio of the sub-triangle area opposite its vertex to the total triangle area. Because the weights sum to 1, only two are independent — they form an affine coordinate system tied to the triangle rather than to the global axes.
How do you test if a point is inside a triangle using barycentric coordinates?
Compute the three barycentric weights of the point. The point lies strictly inside the triangle if and only if all three weights are positive. If one weight is exactly zero the point is on the opposite edge, and if any weight is negative the point is outside. This is the whole inside test: three sign checks after an O(1) computation, which is why GPUs use it for every pixel during rasterization.
How are barycentric coordinates used to interpolate colors and texture coordinates?
Any per-vertex attribute — vertex color, UV texture coordinates, surface normal, or depth — is interpolated at an interior point with the exact same weights that locate the point: attr(P) = λ₀·attr(A) + λ₁·attr(B) + λ₂·attr(C). Because λ₀ + λ₁ + λ₂ = 1, the result is a true convex combination that stays within the range of the vertex values. This single formula is how Gouraud shading, texture mapping, and z-buffer depth all fill a triangle.
What does it mean when a barycentric coordinate is negative?
A negative weight means the point is outside the triangle, on the far side of the edge opposite the vertex whose weight went negative. Geometrically, the signed area of the sub-triangle opposite that vertex has flipped orientation. Barycentric coordinates are defined for the entire plane, not just the triangle interior, so points outside simply have one or two negative weights while the three still sum to 1.
Why do you need perspective-correct interpolation instead of plain barycentric interpolation?
Screen-space barycentric weights interpolate linearly in the projected image, but perspective projection is non-linear, so linearly blending texture coordinates warps the texture (the classic wobbly checkerboard on a receding floor). The fix is to interpolate attribute/w and 1/w linearly with the barycentric weights, then divide: attr = (Σ λᵢ·attrᵢ/wᵢ) / (Σ λᵢ/wᵢ). Depth stored as z/w interpolates correctly and linearly, which is why hardware z-buffers store post-projection depth.
How does the edge function relate to barycentric coordinates?
The edge function E(P) = (P - V₀) × (V₁ - V₀) is a 2D cross product that returns twice the signed area of the triangle formed by P and edge V₀V₁. Each barycentric weight is one edge function divided by the total signed area (2·Area). Edge functions are the workhorse of Pineda's 1988 parallel rasterization algorithm: they are affine in the pixel coordinates, so they can be evaluated with two additions per pixel step, making them ideal for hardware.
What is the difference between barycentric coordinates and bilinear interpolation?
Barycentric interpolation is defined on a triangle with three weights that sum to 1 and is exactly linear (affine) across the whole triangle. Bilinear interpolation is defined on a quad with four corner values and is a product of two linear blends, so it is generally quadratic and not planar. GPUs rasterize triangles, so barycentric interpolation is the native primitive; bilinear interpolation appears mainly inside texture sampling of a 2×2 texel neighborhood.