How to Render Rounded Rectangles in WebGL Using Signed Distance Fields

This article explains how to use WebGL shaders and signed distance functions to draw rectangles and rounded rectangles, covering basic shader setup, SDF formulas for circles, boxes, and rounded boxes, and applying antialiasing techniques to achieve smooth edges for dynamic UI cards.

WeChatFE
WeChatFE
WeChatFE
How to Render Rounded Rectangles in WebGL Using Signed Distance Fields
As business needs grow, some cards in the subscription message stream have been dynamically transformed, with UI rendering handled by a WebGL-based renderer. This article introduces the underlying implementation, focusing on drawing rounded rectangles with WebGL, which serve as building blocks for card rendering.

WebGL Drawing Rectangles

Assuming you are already familiar with the basics of WebGL, drawing graphics primarily relies on a WebGL program and the writing of shaders. After creating a WebGL program, you can use the browser's WebGL API to create a context, pass data to the program, and finally issue draw commands.

WebGL describes geometry using vertices and primitives. A vertex is a point of a shape (e.g., three vertices for a triangle). Primitives are the drawable units (points, lines, triangles, etc.) determined by the drawing mode.

Drawing a shape typically uses two shaders: a vertex shader that processes vertex information and a fragment shader that processes pixel information.

Example shader code for drawing a rectangle:

// Vertex Shader
attribute vec4 a_Position;
void main() {
    gl_Position = a_Position;
}
// Fragment Shader
precision mediump float;
uniform vec4 u_FragColor;
void main() {
    gl_FragColor = u_FragColor;
}

After creating the program, pass vertex data and a color (red in this example) to render a red rectangle on the screen:

// Pass vertex coordinates
var vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
  -0.5, 0.5, 0.5, 0.5, 0.5, -0.5, // Triangle 1
  -0.5, 0.5, 0.5, -0.5, -0.5, -0.5 // Triangle 2
]), gl.STATIC_DRAW);

// Pass color value
var u_FragColor = gl.getUniformLocation(gl.program, 'u_FragColor');
gl.uniform4fv(u_FragColor, [1.0, 0.0, 0.0, 1.0]); // rgba

// Clear screen
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);

// Draw result
gl.drawArrays(gl.TRIANGLES, 0, 6);

Using SDF to Implement Rounded Rectangles

The idea is to keep pixels inside the shape's outline and set pixels outside to transparent, as illustrated below.

A signed distance function (SDF) computes the shortest distance from a point to a shape's boundary and assigns a sign: negative inside, positive outside, zero on the edge.

Basic 2D shape SDF code examples:

Circle

/**
* Circle: p is any point on the canvas, r is the radius
*/
float sdfCircle( vec2 p, float r )
{
    return length(p) - r;
}

The circle SDF simply subtracts the radius from the distance to the center.

Rectangle

/**
* Rectangle: p is any point on the canvas, b is the top‑right corner of the rectangle
*/
float sdfBox( in vec2 p, in vec2 b )
{
    vec2 d = abs(p) - b;
    return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}

The rectangle SDF maps the point to the first quadrant with abs(p), computes the vector d from the corner, and combines outside and inside distances.

Four region analysis (p is the canvas point, b is the corner):

Region ① (p.x > b.x, p.y < b.y):

max(vec2(Px - Bx, Py - By), 0.0) = vec2(Px - Bx, 0.0);
length(vec2(Px - Bx, 0.0)) = Px - Bx;
min(max(Px - Bx, Py - By), 0.0) = 0;
// Result: Px - Bx

Region ② (p.x < b.x, p.y > b.y):

max(vec2(Px - Bx, Py - By), 0.0) = vec2(0.0, Py - By);
length(vec2(0.0, Py - By)) = Py - By;
min(max(Px - Bx, Py - By), 0.0) = 0;
// Result: Py - By

Region ③ (both differences positive):

max(vec2(Px - Bx, Py - By), 0.0) = vec2(Px - Bx, Py - By);
length(vec2(Px - Bx, Py - By)) = sqrt((Px‑Bx)²+(Py‑By)²);
min(max(Px - Bx, Py - By), 0.0) = 0;
// Result: distance to corner

Region ④ (both differences negative):

max(vec2(Px - Bx, Py - By), 0.0) = vec2(0.0, 0.0);
length(vec2(Px - Bx, Py - By)) = 0.0;
min(max(Px - Bx, Py - By), 0.0) = max(Px - Bx, Py - By);
// Result: max(Px‑Bx, Py‑By)

Inglorious‑Quilez (iq) condensed the rectangle SDF to a single formula:

length(max(d, 0.0)) + min(max(d.x, d.y), 0.0)

Rounded Rectangle

/**
* Rounded rectangle: p is any point, b is the top‑right corner, r is the corner radius
*/
float sdfRoundedBox( in vec2 p, in vec2 b, in vec4 r )
{
    r.xy = (p.x > 0.0) ? r.xy : r.zw;
    r.x = (p.y > 0.0) ? r.x : r.y;
    vec2 d = abs(p) - b + r.x;
    return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0) - r.x;
}

The rounded‑rectangle SDF is similar to the rectangle SDF, subtracting the radius from the inner rectangle's distance.

After computing the SDF distance, set pixel opacity based on the sign:

pixel_opacity = distance_to_edge < 0 ? 1 : 0;

Antialiasing

The preliminary implementation yields jagged edges, as shown on the left, while the right side shows a smooth outline.

Because opacity is set to fully opaque or fully transparent, edge pixels lack interpolation.

To achieve smooth edges, adjust the opacity function using a clamp, assuming distance is measured in pixels:

pixel_opacity = clamp( 0.5 - distance_to_edge, 0, 1 );

This maps distances within one pixel to a 0‑1 range, leaving farther pixels unchanged.

Two cases are considered: edge aligned with pixel grid (left) and edge not aligned (right).

GPU pixels have an actual size with a center and edges.

During drawing, the GPU occupies each pixel's center position.

When the edge is between pixels, the opacity becomes 0.5, reflecting half‑inside, half‑outside coverage.

Moving the edge slightly left or right increases or decreases opacity accordingly, solving the jagged‑edge problem.

Summary

By combining SDF with antialiasing, WebGL can render arbitrary rounded rectangles (including circles and squares as special cases). These shapes can be filled with colors or textures (e.g., text converted to textures), allowing complex UI cards to be assembled from multiple rounded‑rectangle pieces.

Shader capabilities can be further extended with common filters such as linear gradients and Gaussian blur, enabling more expressive cards, as demonstrated below.

References

WebGL fundamentals: https://webglfundamentals.org/

跟月影学可视化: https://time.geekbang.org/column/intro/100053801?tab=intro

draw-a-rectangle: https://github.com/bonigarcia/webgl-examples/blob/master/basic_concepts/draw-a-rectangle.html

Signed Distance Field: https://zhuanlan.zhihu.com/p/26217154

2D distance functions: https://iquilezles.org/articles/distfunctions2d/

2D basic shapes SDF detailed (part 1): https://blog.csdn.net/qq_41368247/article/details/106194092

Antialiasing with a signed distance field: https://mortoray.com/2015/06/19/antialiasing-with-a-signed-distance-field/

Antialiasing For SDF Textures: https://drewcassidy.me/2020/06/26/sdf-antialiasing/

Improved Alpha-Tested Magnification for Vector Textures and Special Effects: https://steamcdn-a.akamaihd.net/apps/valve/2007/SIGGRAPH2007_AlphaTestedMagnification.pdf

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

WebGLShaderAntialiasingRounded RectangleSigned Distance Function
WeChatFE
Written by

WeChatFE

Tencent WeChat Public Platform Frontend Team

0 followers
Reader feedback

How this landed with the community

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.