Suppose you want to render the edges of a mesh like in the in the figure:

There are many ways to do this. Here I will use barycentric coordinates. Barycentric coordinates provide a very useful way to represent points relatively to the vertices of a convex polygon, giving us a tool to do all sorts of calculations easily without the need to perform geometric computations.

Barycentric Coordinates

Given a triangle ABCABC, with vertices AA, BB, and CC - and an arbitrary point PP. The barycentric coordinates of PP have three components λi\lambda_i, each relative to a different vertex. This new coordinate system is constructed from the normalized areas \triangle of the sub-triangles ABPABP, BCPBCP, and PCAPCA.

The barycentric coordinates or PP are defined as

λ0=ABPABC,λ1=BCPABC,λ2=PCAABC.\lambda_0 = \frac{\triangle ABP}{\triangle ABC}, \quad \lambda_1 = \frac{\triangle BCP}{\triangle ABC}, \quad \lambda_2 = \frac{\triangle PCA}{\triangle ABC}.

Notice that, since the areas are normalized by ABC\triangle ABC, we have λi=1\sum \lambda_i = 1. This way we can save some computations by using λ2=1λ0λ1.\lambda_2 = 1 - \lambda_0 - \lambda_1.

Thus, the barycentric coordinates (λ0,λ1,λ2\lambda_0, \lambda_1, \lambda_2) represent PP. The cartesian coordinates of PP can be retrieved from the relation of each λi\lambda_i and their respective vertex position AA, BB or CC. In other words, PP is the result of a linear combination of the vertices AA, BB, and CC.

P=λ0C+λ1A+λ2BP = \lambda_0 C + \lambda_1 A + \lambda_2 B

Always remember the sub-triangles you form with PP. Making PP coincide one of the vertices is the same as having one of those triangles equal ABCABC and the other two vanish. You may also realize that each λi\lambda_i is associated to an edge eie_i and an opposite vertex vjv_j. The value of λi\lambda_i is the signed distance of PP to the line formed by eie_i, and this distance is +1+1 at vjv_j. The following figure may help :)

Here are some observations:

  • If P=AP = A, then the barycentric coordinates are (0,1,00,1,0);
  • If P=BP = B, then the barycentric coordinates are (0,0,10,0,1);
  • If P=CP = C, then the barycentric coordinates are (1,0,01,0,0);
  • Any point PP inside the triangle will have coordinates λi0\lambda_i \geq 0;
  • If PP lies on edge ABAB, then λ0=0\lambda_0 = 0;
  • If PP lies on edge BCBC, then λ1=0\lambda_1 = 0;
  • If PP lies on edge CACA, then λ2=0\lambda_2 = 0;

As you may expect now, we will determine how close PP is from any edge using barycentric coordinates! But before that, here is a simple code to compute the barycentric coodinates of PP:

// we receive the triangle vertices A, B, and C, 
// and we want to compute the coordinates for the given P
hermes::point3 barycentricCoordinates(
				const hermes::point3& A,
				const hermes::point3& B,
				const hermes::point3& C,
				const hermes::point3& P) {
	// the area of a triangle can calculated with the cross product
	// note: we don't need to divide by two here, because when we
	//       divide the areas later the 2's get cancelled out.

	// ABC area using edge vectors AB and CB
	auto ABC_area = hermes::cross(B - A, C - B).length();
	// ABP area using edge vectors AB and BP
	auto ABP_area = hermes::cross(B - A, P - B).length();
	// BCP area using edge vectors BC and CP
	auto BCP_area = hermes::cross(C - B, P - C).length();
	// lambdas are then computed as
	auto lambda_0 = ABP_area / ABC_area;
	auto lambda_1 = BCP_area / ABC_area;
	auto lambda_2 = 1 - lambda_0 - lambda_1;
	return {lambda_0, lambda_1, lambda_2};
}

Detecting Edges in Fragment Shader (GLSL)

The trick now is to determine the barycentric coordinates of our fragment. Because with those coordinates, we can determine if it belongs to an edge or not.

Since we know that the barycentric coordinates of the three vertices of ABCABC are A=(0,1,0)B=(0,0,1)C=(1,0,0)A = (0,1,0) \quad B = (0,0,1) \quad C = (1,0,0) we can just assign those values as vertex attributes and let the hardware interpolate them throughout the fragments.

Now that we have the barycentric coordinates of our fragment, we need to get the distance to the closest edge. A fragment will be considered an edge if it is closer than the edge width WW. In which space the WW is defined is up to the method we use. Perhaps the most naive one is to define WW in the barycentric coordinates space, this way an fragment can be easily classified as edge using

minλiW\min{\lambda_i} \leq W

Depending on the angle of the triangle and the camera view plane, you may also get edges to desapear due to interpolation issues. A way to get around this problem is to weight WW with the total change in barycentric coordinate values on the fragment:

λi(dF(λi)dx+dF(λi)dy)W\lambda_i \leq (|\frac{dF(\lambda_i)}{dx}| + |\frac{dF(\lambda_i)}{dy}|) * W

In GLSL, there is a function called fwidth that computes the sum of the absolute value of derivatives for a given variable accross fragments. The above test can be coded in GLSL like this:

in vec3 bc; // fragment barycentric coordinates
uniform float W; // edge width 
void main() {
	// compute bc derivatives
	vec3 d = fwidth(bc);
	// here we use a step function perform the check
	vec3 f = step(d * W, bc);
	// check if any lambda is passed the test
	if(min(min(f.x, f.y), f.z) < 1) {
		// edge!
	}
}

Using OSL (Open Shading Language)

Here I’ll take the chance to give an example using OSL. In order to compute the barycentric coordinates of our point PP, we can use some variables that OSL provides for us:

Variable Description
P position of the point being shaded
u and v 2D parametric coordinates - for the current primitive - of P
dPdu and dPdv Partial derivatives tangent to the surface ate P

The vertices of our primitive (assuming a triangle here) can then be calculated like this:

A = P - (u * dPdu) - (v * dPdv)
B = A + dPdu
C = A + dPdv

Now, we again have the 3 vertices AA, BB, and CC, and a point PP. The rest goes as usual.

Compensating Triangle Shapes

Until now, we’ve been defining WW on barycentric coordinates space. This method works fine for fairly regular triangles (all sides with equal size). However, streched triangles will show edges with varying width. See the edges on the bunny ears for example:

We might improve our results a little bit by trying to normalize things. Lets think about the actual sizes of our triangle: the side vectors aa, bb, and cc, and in particular, the heights HaH_a, HbH_b, and HcH_c:

In barycentric coordinates terms, all these heights measure 11 (that is why things get weird in streched triangles). However, we can bring our distances back to the space where our vertices live in. This can be done by scaling each λi\lambda_i to its respective height HiH_i. Remember that each vertex is associated to an λi\lambda_i: Aλ1Bλ2Cλ0A \rightarrow \lambda_1 \quad B \rightarrow \lambda_2 \quad C \rightarrow \lambda_0

So, the distance DaD_a to edge ABAB is Hcλ0H_c * \lambda_0, and the other two are Db=Haλ2D_b = H_a * \lambda_2 Dc=Hbλ1D_c = H_b * \lambda_1 where DbD_b is the distance to edge BCBC, and DcD_c is the distance to edge CACA. The heights can be computed using simple vector projection: Ha=abbbbaH_a = || \frac{a\cdot b}{b \cdot b} b - a|| Hb=bccccbH_b = || \frac{b\cdot c}{c \cdot c} c - b|| Hc=caaaacH_c = || \frac{c\cdot a}{a \cdot a} a - c||

Now, our fragment test becomes: minDa,b,cW\min D_{a,b,c} \leq W Shader-Based Wireframe Drawing