Curves in Curver

A Short Technical Overview

December 15, 2014 - Admin Tags: ,

In this post I briefly describe some aspects of curves in Curver. Curves are the main components in Curver. The idea is that users should be able to quickly create and manipulate curves to create clean line-art (typically from sketches). For this to feel natural, Curver must implement a work-flow that is dictated not by technical representations, but by visual sense. Here is some technical information for the interested.

Curve Structure

A curve in Curver is a list of points. These points have position and thickness. They are not control points in a mathematical curve as typical in lots of other applications. With dynamic tessellation, this allows for easy implementation of a sculpting-like approach to curve manipulation.

Curves can be open or closed. A closed curve is a curve where the last point is linked to the first. Besides the obvious use, closed curves are also the base for shapes, but more about shapes later.

Programmatically, a curve has a list of points (nodes). Initially I used a home-made intrusive list container, but recently when I picked up the development of Curver again, I switched to std::list. Using the standard library instead of a custom container has many advantages that are obvious to most programmers. However, a month later I switched back to the intrusive list container. I may later write a post about why I switched back, but the main reason, which I think should always have the highest priority (especially for a lone programmer who has very little spare time such as myself), is productivity.

Curve nodes are pooled, as they are created and destroyed thousands of time in a typical usage session.


Curver used DX9. It currently uses the D3DX effect framework, and though the framework will probably be sufficient for Curver, I may switch away from it later down the road.

At this stage of early development, Curver renders curves individually (without batching) by filling a dynamic vertex buffer with curve geometry each frame. The same vertex buffer is used for all curves, so it's always at least as large as needed to render the largest curve.

For each curve point, two vertices are sent to the graphics card. The Following is HLSL code describing the data sent per vertex:

struct TInputVertex
    float4 pos         : POSITION;  // Position of this vertex. z is the width of the curve node.
    float2 tex0        : TEXCOORD0; // Tex coords. Vertex shader also uses tex0.y to properly position the vertex.
                                    // It's assumed to be either 0 or 1. tex0.x is the node masking value [0,1].
    float2 prevPos    : TEXCOORD1;
    float2 nextPos    : TEXCOORD2;

The curve itself is rendered as a triangle strip. The vertex shader computes the positions of the vertices from the position of the node, the width of the node, and the curve direction at the node, as calculated using the positions of the next and previous points.

Alpha-based Anti-Aliasing

While the HW super sampling which is now commonly supported on most graphics cards does a good job at anti-aliasing, implementing mathematically-based anti-aliasing is feasible for 2D curves, and provides better results. Even on the highest settings, the HW's AA was showing artifacts when curves get extra slim!

Curver uses alpha-blending. The vertex shader widens the curve by one additional pixel on each side, and the pixel shader calculates the alpha based on coverage. I will write about this later.

Corners and Extra Width

On sharp corners, the curve will look like it's collapsed if the angle was not taken into consideration when the vertex shader computes the positions of the vertices associated with a point on the curve. This effect is similar to the effect in 3D when a joint (an elbow for example) is bent to a sharp corner - the arm seems to collapse. The first video demonstrates this, while the second demonstrates the fix. They are placed close together for easier visual comparison.

To fix this, the width is adjusted in the vertex shader. It took me a while to figure this out, and I'm not sure the calculations are accurate, but here is what I came up with using my rusty trigonometry:

	// pos.z is the width of the curve node.
	// w is the curve width.
	float w = IN.pos.z * g_CurveWidth;
	float fDot = dot( -inDir, outDir );
	float theta = acos( fDot ) / 2;
	float y = w / tan(theta);
	float t = sqrt( w * w + y * y );
	w = min( t, w * 8 );

The last line ensures that on very sharp corners, the width will not be increased too much.

That's it for now. Please feel free to comment below if you have anything to ask or say!