5.6 KiB
Documentation
The motivation behind this library and more in-depth information about the rendering techniques utilised can be found in my thesis.
Links
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
descriptorproperty 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.