Frontend Development 13 min read

Understanding Distance Fields for Triangles, Regular Polygons, and Ellipses in WebGL

This article explains how to construct signed distance fields for triangles, regular polygons and ellipses using GLSL shaders in WebGL, covering the mathematical definitions, interior‑exterior detection, code implementations, and techniques for repeating patterns to create complex procedural graphics.

ByteFE
ByteFE
ByteFE
Understanding Distance Fields for Triangles, Regular Polygons, and Ellipses in WebGL

Building on the previous two chapters about distance‑field composition, we now show how to create signed distance fields (SDF) for triangles, regular polygons, and ellipses, which are essential primitives for procedural graphics in WebGL.

Triangle SDF : The distance from a point to a triangle is defined as the shortest distance to any of its three edges. Inside the triangle the distance is normalized by the incircle radius l , and the sign of the distance is determined by the consistency of the edge‑vector cross‑product signs. The GLSL implementation is provided below:

float sdf_line(vec2 st, vec2 a, vec2 b) {
  vec2 ap = st - a;
  vec2 ab = b - a;
  return ((ap.x * ab.y) - (ab.x * ap.y)) / length(ab);
}

float sdf_seg(vec2 st, vec2 a, vec2 b) {
  vec2 ap = st - a;
  vec2 ab = b - a;
  vec2 bp = st - b;
  float l = length(ab);
  float proj = dot(ap, ab) / l;
  if(proj >= 0.0 && proj <= l) {
    return sdf_line(st, a, b);
  }
  return min(length(ap), length(bp));
}

/**
  Triangle SDF
 */
float sdf_triangle(vec2 st, vec2 a, vec2 b, vec2 c) {
  vec2 va = a - b;
  vec2 vb = b - c;
  vec2 vc = c - a;

  float d1 = sdf_line(st, a, b);
  float d2 = sdf_line(st, b, c);
  float d3 = sdf_line(st, c, a);

  // incircle radius
  float l = abs(va.x * vb.y - va.y * vb.x) / (length(va) + length(vb) + length(vc));
  
  // inside: positive distance
  if(d1 >= 0.0 && d2 >= 0.0 && d3 >= 0.0 || d1 <= 0.0 && d2 <= 0.0 && d3 <= 0.0) {
    return min(abs(d1), min(abs(d2), abs(d3))) / l;
  }
  
  // outside: negative distance
  d1 = sdf_seg(st, a, b);
  d2 = sdf_seg(st, b, c);
  d3 = sdf_seg(st, c, a);
  return -min(abs(d1), min(abs(d2), abs(d3))) / l;
}

Note that the function signature has been adjusted so that the texture coordinate st is always the first parameter, making it easier to compose different shape SDFs.

Regular Polygon SDF : For a regular n -gon we define the distance as the shortest distance to any edge, normalized by the distance from the center to an edge ( l ). The implementation uses a custom atan2 , rotation matrix, and edge selection based on the angular sector.

#ifndef PI
#define PI 3.141592653589793
#endif

#ifndef FLT_EPSILON
#define FLT_EPSILON 0.000001
#endif

vec2 transform(vec2 v0, mat3 matrix) {
  return vec2(matrix * vec3(v0, 1.0));
}

vec2 rotate(vec2 v0, vec2 origin, float ang) {
  float sinA = sin(ang);
  float cosA = cos(ang);
  mat3 m = mat3(cosA, -sinA, 0, sinA, cosA, 0, 0, 0, 1);
  return transform(v0 - origin, m) + origin;
}

vec2 rotate(vec2 v0, float ang) {
  return rotate(v0, vec2(0.0), ang);
}

float atan2(float dy, float dx) {
  float ax = abs(dx);
  float ay = abs(dy);
  float a = min(ax, ay) / (max(ax, ay) + FLT_EPSILON);
  float s = a * a;
  float r = ((-0.0464964749 * s + 0.15931422) * s - 0.327622764) * s * a + a;
  if(ay > ax) r = 1.57079637 - r;
  if(dx < 0.0) r = PI - r;
  if(dy < 0.0) r = -r;
  return r;
}

float atan2(vec2 v) {
  return atan2(v.y, v.x);
}

/**
  Angle between two vectors, 0~2*PI
 */
float angle(vec2 v1, vec2 v2) {
  float ang = atan2(v1) - atan2(v2);
  if(ang < 0.0) ang += 2.0 * PI;
  return ang;
}

/**
  Regular polygon SDF
 */
float regular_polygon(vec2 st, vec2 center, float r, float rotation, const int edges) {
  vec2 p = st - center;
  vec2 v0 = vec2(0, r); // first vertex
  v0 = rotate(v0, -rotation);

  float a = 2.0 * PI / float(edges); // angle per edge

  float ang = angle(v0, p); // current angle
  ang = floor(ang / a); // sector index

  vec2 v1 = rotate(v0, a * ang); // left vertex
  vec2 v2 = rotate(v0, a * (ang + 1.0)); // right vertex

  float c_a = cos(0.5 * a);

  float l = r * c_a;
  float d = sdf_line(p, v1, v2);

  return d / l; 
}

The code above enables drawing any regular polygon, for example a 7‑sided shape, by supplying the appropriate edges value.

Ellipse and Elliptical Sector SDF : An ellipse is defined by the standard equation, and its SDF is computed similarly to the circle case. For an elliptical sector we combine the ellipse SDF with line SDFs that represent the sector boundaries, handling inside/outside cases with sign logic.

float sdf_ellipse(vec2 st, vec2 c, float a, float b) {
  vec2 p = st - c;
  return 1.0 - sqrt(pow(p.x / a, 2.0) + pow(p.y / b, 2.0));
}

float sdf_ellipse(vec2 st, vec2 c, float a, float b, float sAng, float eAng) {
  vec2 ua = vec2(cos(sAng), sin(sAng));
  vec2 ub = vec2(cos(eAng), sin(eAng));

  float d1 = sdf_line(st, c, ua + c);
  float d2 = sdf_line(st, c, ub + c);

  float d3 = sdf_ellipse(st, c, a, b);
  float r = min(a, b);

  vec2 v = st - c;
  float ang = angle(v, vec2(1.0, 0));

  if(eAng - sAng > 2.0 * PI) {
    return d3;
  }

  if(ang >= sAng && ang <= eAng) { // middle part
    float m = max(a, b);
    float d11 = sdf_seg(st, c, ua * m + c);
    float d12 = sdf_seg(st, c, ub * m + c);
    if(d3 >= 0.0) {
      return min(abs(d11 / r), min(abs(d12 / r), d3));
    }
    return d3;
  }
  
  float pa = dot(ua, v); // projection on first radius
  float pb = dot(ub, v);

  if(pa < 0.0 && pb < 0.0) {
    return -length(st - c) / r;
  }

  if(d1 > 0.0 && pa >= 0.0) {
    vec2 va = pa * ua;
    float da = pow(va.x / a, 2.0) + pow(va.y / b, 2.0);
    if(d3 > 0.0 || da <= pow(1.0 + abs(d1 / r), 2.0)) {
      return -abs(d1 / r);
    } else {
      return d3;
    }
  }

  if(d2 < 0.0 && pb >= 0.0) {
    vec2 vb = pb * ub;
    float db = pow(vb.x / a, 2.0) + pow(vb.y / b, 2.0);
    if(d3 >= 0.0 || db <= pow(1.0 + abs(d2 / r), 2.0)) {
      return -abs(d2 / r);
    } else {
      return d3;
    }
  }
}

By combining these primitives with repetition techniques—scaling st or the distance value and applying fract() —we can generate intricate patterns such as traditional Chinese motifs or tiled geometric designs using only a few lines of shader code.

Overall, the article demonstrates that with a solid grasp of distance‑field mathematics and GLSL, developers can create complex, high‑performance procedural graphics in WebGL.

GLSLWebGLShadergeometryDistance FieldsProcedural Graphics
ByteFE
Written by

ByteFE

Cutting‑edge tech, article sharing, and practical insights from the ByteDance frontend team.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.