Frontend Development 9 min read

Understanding and Implementing Distance Fields in WebGL (Part 2)

This article explains the concept of distance fields, shows how to compute distance fields for rectangles, lines, and line segments in GLSL, and demonstrates sampling techniques and curve rendering using WebGL shaders with complete code examples.

ByteFE
ByteFE
ByteFE
Understanding and Implementing Distance Fields in WebGL (Part 2)

Continuing from the previous tutorial, this article introduces the distance‑field composition method, which defines a scalar field representing the distance from each pixel to a shape, and applies it to draw various geometric primitives in WebGL.

The simplest distance field is a circle, defined by the distance from each pixel to the circle’s centre. A rectangle’s distance field is similarly straightforward and can be visualized with the following fragment shader:

#version 300 es
precision highp float;
out vec4 FragColor;
uniform vec2 resolution;
void main() {
  vec2 st = gl_FragCoord.xy / resolution;
  vec2 center = vec2(0.5);
  st = abs(st - center);
  float d = max(st.x, st.y);
  FragColor.rgb = smoothstep(d - 0.015, d, 0.2) * vec3(1.0);
  FragColor.a = 1.0;
}

To compute the distance from a point to an arbitrary line defined by two points A and B , the cross‑product of vectors AP and AB divided by the length of AB gives the perpendicular distance. The corresponding GLSL function is:

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

Using this function, a line can be rendered with:

void main() {
  vec2 st = gl_FragCoord.xy / resolution;
  float d = sdf_line(vec2(0), vec2(1.0), st);
  float d2 = sdf_line(vec2(1.0, 0.0), vec2(0.0, 1.0), st);
  FragColor.rgb = (stroke(d, 0.0, 0.03, 0.2) + stroke(d2, 0.0, 0.03, 0.2)) * vec3(1.0);
  FragColor.a = 1.0;
}

For a line segment, the distance is the line distance when the projection of the point lies between the segment’s endpoints; otherwise it is the shorter distance to either endpoint. The GLSL implementation is:

float sdf_seg(vec2 a, vec2 b, vec2 st) {
  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(a, b, st);
  }
  return min(length(ap), length(bp));
}

To draw continuous curves, three consecutive points A , B , and C are sampled; the distance from a pixel to the curve is the minimum of the distances to the two segments AB and BC :

float sdf_plot(vec2 a, vec2 b, vec2 c, vec2 st) {
  float d1 = sdf_seg(a, b, st);
  float d2 = sdf_seg(b, c, st);
  return min(d1, d2);
}

A macro PLOT is defined to evaluate a function at three neighboring sample points and feed them into sdf_plot :

#ifndef PLOT
#define PLOT(f, st, step) sdf_plot(vec2(st.x - step, f(st.x - step)), vec2(st.x, f(st.x)), vec2(st.x + step, f(st.x + step)), st)
#endif

Various example functions ( f1 to f5 ) are provided, and the final fragment shader combines the distance fields of axes and several curves, applying a stroke function to render them with adjustable thickness and smoothness. The coordinate system is expanded from (0,0)-(1,1) to (-10,-10)-(10,10) using:

st = mix(vec2(-10, -10), vec2(10, 10), st);

This technique allows for flexible scaling of the drawing space, which will be further explored in the next lecture.

GraphicsGLSLWebGLShaderDistance Field
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.