sdf-2d/documentation-readme.md
Andras Schmelczer 5de6827db1
All checks were successful
Check / check (push) Successful in 26s
Publish / build (push) Successful in 30s
Publish / publish-npm (push) Has been skipped
Remove old links
2026-10-28 22:48:02 +00:00

5.6 KiB

SDF-2D logo Documentation

The motivation behind this library and more in-depth information about the rendering techniques utilised can be found in my thesis.

Usage (1st option)

  • To start using cutting-edge 2D graphics, you first need a renderer instance. You can get one by calling the {@link compile!compile | compile function}.
    • For this, you have to provide one or more {@link drawables/drawable-descriptor!DrawableDescriptor | DrawableDescriptors}.
    • Optionally, the default compile settings can be overridden using {@link graphics/rendering/settings/startup-settings!StartupSettings}.
  • Once you have a renderer, you can start drawing objects through the {@link graphics/rendering/renderer/renderer!Renderer} interface.

Usage (2nd option)

If you're planning to create animated content, use the {@link run-animation!runAnimation | runAnimation function} to save yourself from writing boilerplate code. See its {@link run-animation!runAnimation | documentation} for more details.

Extending drawables

Iñigo Quilez has a great collection of 2D SDFs

  • Subclass {@link drawables/drawable!Drawable}
  • Implement its abstract methods
  • Add a static descriptor property of type {@link drawables/drawable-descriptor!DrawableDescriptor} to your class
  • Follow the instructions given in Usage

Custom drawable example

The function named by sdf.distanceFunctionName must have the signature float distanceFunction(in vec2 target, out vec4 color) in both GLSL 100 and GLSL 300 ES. It returns the signed distance (negative inside, zero on the boundary, positive outside) and writes the closest drawable's RGBA color to color on every code path.

This complete example defines a custom orange circle, maps its serialized properties to shader uniforms, and compiles and renders it. Use it in a JavaScript module with sdf-2d and gl-matrix installed. Documentation CI runs this exact code with both WebGL 1 and WebGL 2.

import { compile, Drawable } from 'sdf-2d';
import { vec2 } from 'gl-matrix';

class CustomCircle extends Drawable {
  constructor(center, radius) {
    super();
    this.center = center;
    this.radius = radius;
  }

  static descriptor = {
    propertyUniformMapping: {
      center: 'customCircleCenters',
      radius: 'customCircleRadii',
    },
    uniformCountMacroName: 'CUSTOM_CIRCLE_COUNT',
    shaderCombinationSteps: [0, 1, 4],
    // Unused array entries are filled with this zero-radius circle and moved away.
    empty: new CustomCircle([0, 0], 0),
    sdf: {
      distanceFunctionName: 'customCircleDistance',
      shader: `
        uniform vec2 customCircleCenters[CUSTOM_CIRCLE_COUNT];
        uniform float customCircleRadii[CUSTOM_CIRCLE_COUNT];

        float customCircleDistance(in vec2 target, out vec4 color) {
          color = vec4(1.0, 0.25, 0.0, 1.0);
          float minDistance = 1000.0;
          for (int i = 0; i < CUSTOM_CIRCLE_COUNT; i++) {
            float d = distance(target, customCircleCenters[i]) - customCircleRadii[i];
            minDistance = min(minDistance, d);
          }
          return minDistance;
        }
      `,
    },
  };

  minDistance(target) {
    return vec2.distance(this.center, target) - this.radius;
  }

  getObjectToSerialize(transform2d, transform1d) {
    return {
      center: vec2.transformMat2d(vec2.create(), this.center, transform2d),
      radius: this.radius * transform1d,
    };
  }
}

const canvas = document.createElement('canvas');
canvas.width = 320;
canvas.height = 240;
document.body.appendChild(canvas);

const renderer = await compile(canvas, [CustomCircle.descriptor]);
renderer.setRuntimeSettings({ ambientLight: [1, 1, 1], motionBlur: 0 });
renderer.addDrawable(new CustomCircle([160, 120], 50));
renderer.renderDrawables();

// Call renderer.destroy() when the renderer is no longer needed.

Useful to know

Math

The vec2, vec3, and vec4 types seen in the documentation come from the glMatrix library and are equivalent to regular JS Arrays or Float32Arrays, so feel free to pass [x, y] to functions that expect a vec2.

Coordinates

Wherever positions need to be specified, the y axis grows upwards. This means that when you set the view area, the origin is at the bottom-left corner of the display.

Tile-based rendering

To optimise the evaluation of the distance field, the display is divided into a grid of tiles. The shaders for each tile are compiled to support a fixed maximum number of objects. When using the built-in drawables, this means that beyond a certain number of on-screen objects, new ones may stop appearing.

Mitigating this is easy. Instead of the following code:

this.renderer = await compile(canvas, [Circle.descriptor, CircleLight.descriptor]);

modify it to something like this:

this.renderer = await compile(canvas, [
  {
    ...Circle.descriptor,
    shaderCombinationSteps: [0, 1, 2, 24, 64],
  },
  {
    ...CircleLight.descriptor,
    shaderCombinationSteps: [0, 1, 2, 4],
  },
]);

Using very large numbers is not advised, for both compatibility and performance reasons.

Steps are especially useful for tile-based rendering: at any given moment, one tile may be empty or contain just a few objects, while another holds a large cluster. The compiled shaders only account for the number of objects actually present on each tile.