Drawing smooth lines with Cocos2dx

Andrey Suvorov
5 min readApr 3, 2019

--

In this topic, I will show you how to draw smooth lines like with Cocos2dx.

The problem

Anyone experienced enough in computer graphics knows that Drawing Lines is Hard! These could be especially challenging for WebGL and OpenGL ES on mobile platforms. In order to succeed in that, you’ll need to deal with aliasing somehow and the fact that OpenGL can’t draw curves with an arbitrary thickness.

A solution to this might be implementing tesselation algorithm like this (tesselation is basically a smart way to represent line by a set of triangles):

Or to use one of the existed rendering engine, which implements it for you, for example:

In native development, we can use Skia, which implement tricky AA solution for paths, but when it comes to Cocos2dx game engine we might notice the lack of ready-made solutions. Multiple times these issues have been raised on the cocos forum: one, two, three, four, etc. But none of the suggested solutions worked for me, so some time ago I’ve have written my own. You can partly track down my research after that from this topic at cocos forum:

DrawNode

With standard DrawNodewe can draw something like this:

node = DrawNode::create(9);
node->drawCardinalSpline(pts, tension, segments, Color4F::RED);
for (int i = 0; i < pts->count(); ++i) {
node->drawPoint(pts->getControlPointAtIndex(i), 10, Color4F::BLUE);
}
Not so pretty, right?

This image has the following artifacts:

  1. Stepped curve borders (aliasing)
  2. Some cut holes inside the line.

Varying the number of segments or draw-then-scale can eliminate an only small fraction of all the artifacts, mostly because the problem is not exactly in smoothing the line edges but rather in correct representation of it with a set of triangles — tessellation.

Triangle

First, let's extend the standard DrawNode class with an additional draw method for a triangle:

This method differs from the original one that allowed us to draw only monochrome triangles:

void DrawNode::drawTriangle(const Vec2 &p1, const Vec2 &p2, const Vec2 &p3, const Color4F &color) {...}

Implementation is pretty similar on the original code, just pass the arguments:

Why does it important for our purpose? Because if we assign different colors for different vertices, OpenGL will provide color interpolation for our triangle:

Color gradient interpolation

And this also works for alpha channel value (opacity). So with only that method we already can go pretty far. We can eliminate the line’s feathers if we’ll draw triangles like these:

Same color, different opacity

By varying the alpha value we can make gradients from full color to fully transparent at line’s edge. By using only this method I’ve been able to achieve these results (screenshot from an Android app):

Smooooth!

This curve was drawn by setting up the coordinates and colors for every one triangle vertices. This looks solid, but do I really need an engine, if I do triangle drawing all by myself?

Segment

So I continued my research about how cocos draw things and after some experiments, I’ve found the only one thing which cocos draw strangely smooth — segment:

I highlight geometry for you

As you can see, it is drawn from 6 triangles and what allows its line edges to be so smooth is this line of the fragment shader:

gl_FragColor = v_color*smoothstep(0.0, 0.1, 1.0 - length(v_texcoord));

when DrawNode sets the texture offsets at drawSegment.

Cardinal Splines

We can reuse such a cool primitive inside cardinal splines:

I’ve got the code from DrawNode::drawCardinalSpline, fix the bug at cardinal splines math, which is apparently been there since the beginning of DrawNode, and start drawing segments with segments, instead of some edgy poly primitives. And that's what I’ve got:

If you zoom up these images, you would see that at large widths we’ve got good results, but at thin lines, there are still some artifacts.

Tesselation

For such cases I’ve implemented a simple tessellation algorithm:

I’ve started with the line segment:

and then adjust the line joints:

I used 4 triangles for each line segment and set the border vertices opacity to zero. Such a scheme doesn’t work well with wide lines — we need more triangle layers for this, but fortunately enough I only need this for thin lines:

And by combining both approaches

void AwesomeNode::drawACardinalSpline(PointArray *config, float tension, unsigned int segments,float w, const Color4F &color) {

auto *vertices = calculateVertices(config, tension, segments);
if (w < LINE_SIZE_THRESHOLD) {
tessellation(vertices, segments, w, color);
} else {
for (int i = 2; i <= segments + 1; ++i) {
drawSegment(vertices[i - 2], vertices[i - 1], w / 2, color);
}
}

CC_SAFE_DELETE_ARRAY(vertices);
}

we’ll have the results you can see at the header of this post.

Final solution

These experiments resulted in a library called AwesomeNode, which you can find here:

You just need to copy-paste AwesomeNode.h and AwesomeNode.cpp into your cocos2d-x based project and you’re all set for drawing extra-smooth lines! (Don’t forget to add AwesomeNode.cpp to LOCAL_SRC_FILES section at Android.mk on Android).

If you have any questions I would love to answer it here, or you may open an issue or send PR to me at GitHub.

--

--