Skip to content

Tutorial: 'Spline' Basics

Hapaxia edited this page Oct 6, 2020 · 2 revisions

Spline Tutorial

Basics


Selba Ward Tutorials


Introduction

Spline is a C++ class for displaying a spline to a render window. A Spline is a composite line, which is made up of multiple smaller lines.

This allows the ability to draw polylines of any thickness with any amount of curvature.

Note that this tutorial makes use of C++11 features and Spline itself uses C++11 features so a C++11 compatible compiler is required.

Starting Point

We're going to need a basic program to use as a starting point. The following program includes SFML and then uses it to create a window with a loop that continues until the window is closed, also processing the window's events:

#include <SFML/Graphics.hpp>

int main()
{
    sf::RenderWindow window(sf::VideoMode(800, 600), "Spline Basics Tutorial");
	
	while (window.isOpen())
	{
	    sf::Event event;
		while (window.pollEvent(event))
		{
			switch (event.type)
			{
			case sf::Event::Closed:
				window.close();
				break;
			}
		}
		
		window.clear();
		window.display();
	}
	
	return EXIT_SUCCESS;
}

This is a simple, working SFML render window used for graphical displays and is the minimal required. If you use SFML, you should already be aware of this "skeleton" program.

First Steps

The first things we need to do are include Spline and create a Spline object.

To include Spline, we simply add:
#include <SelbaWard/Spline.hpp>

To create an instance of a Spline object, we simply use:
sw::Spline spline;
and "spline" becomes the name of our new Spline object.

Drawing

To see that spline, it needs to be drawn to the window. A Spline can be drawn like any standard SFML drawable:
window.draw(spline);

If you run the program so far, you will see only the empty black window. An empty Spline is not a visible object. The object first needs vertices to define its path.

Vertices

Any number of vertices can be added to a Spline. The positions of the vertices define the points through which the Spline passes.

A Spline needs more than one vertex to be defined since a Spline is one or more connected lines and lines cannot be defined by a single vertex so therefore a Spline with only one vertex is still invisible.

Let's add two vertices:

spline.addVertex(sf::Vector2f(100.f, 500.f));
spline.addVertex(sf::Vector2f(700.f, 500.f));

Vertices are added by passing the position of that vertex to the addVertex method; as you can see, that position is an sf::Vector2f - a two dimensional vector provided by SFML.

Since a Spline can be particularly complicated (even though ours is simple at the moment), we need to manually update it when we've finished setting it up:
spline.update();

addVertex takes an sf::Vector2f, as you can see above, but this can also be provided using list initialisation. We will change ours to that format as it's clearer. Change the two lines addVertex lines above to:

spline.addVertex({ 100.f, 500.f });
spline.addVertex({ 700.f, 500.f });

If you run the program at this point, you will see a single pixel -width horizontal line (white) near the bottom of the window.

In a window of 800x600, horizontal positions of 100 to 700 are near the left edge to near the right edge and a vertical position of 500 is near the bottom edge.

With two vertices, we have a single line that goes through those two positions.

However, if we add just one more vertex, we get two lines since the start of the second line uses the same vertex as the end of the first line:
spline.addVertex({ 400.f, 300.f });

Remember to add this line before the update method.

Running now, the program displays the horizontal line and a connected diagonal line from the bottom-right to the centre of the window.

We can add multiple vertices at once by passing a vector of sf::Vectors to the method addVertices. We can also pass this using list initialisation so the following two sf::Vector2fs (each inside braces) will be inside an std::vector (another pair of braces).

Adding another two vertices, we can see that a Spline adds another line for every further single vertex added:

spline.addVertices({ { 700.f, 300.f }, { 400.f, 100.f } });

Running the program at this point, we now see four connected lines (we have five vertices) that form a sort of jagged "3" shape (and similar to half of a tree shape).

Thickness

The lines within the Spline so far are all "infinitely thin"; they effectively have no thickness whatsoever. However, when drawn, they pixels that occupy the line are filled entirely so that the line appears to have a thickness of one pixel.

With Spline, you can specify a thickness of its lines and it will no longer be "infinitely thin" but this specified thickness instead.

Let's set its thickness to 10 so we can clearly see its effect:
spline.setThickness(10);

You can set this thickness to anything and that is the thickness of its lines. If you set it to one, it will be similar to - but not identical to - the infinitely thin default line. To reset its thickness back to the "infinitely thin" lines, simply use the value zero.

Run it and see what difference it makes.

Actually, we will change that thickness so that it's even more obvious what the shape looks like:
spline.setThickness(40);

At this point, it should be obvious how it is drawn: the four thick lines have the width (thickness) of 40 (specified) and each corner has a sharp point where the lines' edges meet. Also, the angle of the start and end edges are perpendicular to the angle of the line to which it is attached. These features can be customised but we will save that for a future tutorial. Feel free to have a look through the documentation for more information.

Interpolation

By default, the vertices are not interpolated. This means that each line of the Spline is drawn between each vertex directly. However, with interpolation, the would-be line between two vertices are split up into pieces. Then, a line is drawn between each piece.

Just interpolating on its own causes no visual difference so we will make some changes so that we can see "behind the scenes".

First, change the thickness back to zero:
spline.setThickness(0);

Just removing any thickness setting would leave it at its default setting of zero.

Then, change the primitive type:
spline.setPrimitiveType(sf::PrimitiveType::Points);

The primitive type can be any of SFML's primitive types (sf::PrimitiveType) but some will be more use than others unless you want to push the Spline to do something it wasn't really designed to do. The default primitive type is sf::PrimitiveType::LineStrip

If you run the program now, you can see (very small) points at each vertex. Five of them, in fact, since we have five vertices.

This may be difficult to see at very high resolutions. These points are a single pixel.

Now, we can set the interpolation and the results will be visible:
spline.setInterpolationSteps(1u);

This adds a single point between each pair of vertices.

Let's increase the value to make it more obvious:
spline.setInterpolationSteps(50u);

Yes, that's more obvious!

Now, it looks more like a dotted line. What just happened is it added 50 more points between each pair of vertices. This means that when it draws lines instead of points, it will be drawing 51 lines between those two vertices!

Okay. Now change the primitive type back (or remove the line to change it):
spline.setPrimitiveType(sf::PrimitiveType::LineStrip);

Run it now and... Oh. It looks like the original four lines again.

Even though it looks like them, it isn't. It's actually drawing 51 lines for each of those four lines so there are actually 204 lines here! The thing is, they all line up perfectly so they look like they are a part of the same line.

Obviously this looks useless but interpolation is important for a couple of features so seeing what it actually does underneath is important to understand what is happening.

Lightning

A Spline can offset each vertex - or technically, each interpolated point/position - by a random amount. They are offset it by its normal - the direction perpendicular to its tangent (the direction of the line at this point).

First, we need to activate this feature:
spline.setRandomNormalOffsetsActivated(true);

This is required since v1.6 if random normal offsets are used. It is disabled by default so that many calculations using random numbers are not made unless required.

Let's see it in action. Let's allow it a range of 1:
spline.setRandomNormalOffsetRange(1.f);

Okay. It makes the line look slightly wobbly. It sort of looks like it's "glitching" slightly. Not very impressive. Then again, with a range of one, it can only "wobble" from one pixel to the next.

Instead, let's make that offset larger:
spline.setRandomNormalOffsetRange(10.f);

That's better. It now kind of looks like fork lightning.

Feel free to try it at higher values: 30 (more prominent lightning), 100 (noisy zig-zag).

The point is offset from its original position by a maximum of half of the specified range. The range goes from one side of the line to the other. For example, the range 10 goes from -5 to 5. This keeps the overall shape centred around the original line.

It's important to remember that these offsets are applied at each vertex and also at each interpolated position. This is why we interpolated the Spline first.

Try it. Set the interpolation steps to 0 but leave the random normal offset range at 10 and it almost looks like nothing happens. It could look like it did (but unlikely) or it might look like the vertices are just slightly in the wrong place (more likely).

In fact, try the interpolation steps at 1 again and notice that the lines look like they're broken in half. The more interpolated positions, the more positions we have to offset randomly.

You can also try a higher value for interpolations just to see the effect. 250, for example, would make it look like a noisy waveform! It's similar to the noisy zig-zagging earlier but smaller and more dense.

Remember to reset it back to 50 again if you change it.

Colour

A Spline can also have its colour set to whatever you choose.

First, let's duplicate our Spline twice so that we have 3 of them (identical):

sw::Spline spline2{ spline };
sw::Spline spline3{ spline };

This can go before or after the update but you must remember that all three must be updated before drawn. In our case, before the loop starts. We shall put it before the update.

Then, we need to update the splines so update those two as well:

spline2.update();
spline3.update();

We will put this directly after the original Spline's update.

We also need to draw all three Splines so we draw the other two Spline after the original one:

window.draw(spline2);
window.draw(spline3);

You can run it now and see that there are three identical Splines and they are all white. Note, though, that the random offsets for each Spline are completely different; they are random, after all!

Now, we can change the colours of these Splines:

spline.setColor(sf::Color::Blue);
spline2.setColor(sf::Color::Cyan);
spline3.setColor(sf::Color::White);

This goes before the updates.

We don't technically need to set the colour of the third spline since we set it to white and it defaults to that anyway but it's good practice to set it to white if you want it to be white.

Run the program now. You can see three separate "lightning" Splines following the same path but have different offsets and different colours.

Both interpolation and the random normal offsets work with thick Splines too. Let's make all the Splines thick and set each thickness separately:

spline.setThickness(20.f);
spline2.setThickness(10.f);
spline3.setThickness(5.f);

This can be places anywhere after the creation of the other two Splines but before the updating of the three Splines. We placed it after the colour setting lines.

Note that Spline considers any specified thickness as a "thick" Spline. A zero (infinitely thin) thickness is not considered a thick line. This means that a thickness of a half is also considered thick even though it could be "thinner" than what is drawn by a "non-thick" line.

Note also that we no longer need the previous "zero" thickness setting or the primitive type setting. The primitive type doesn't affect "thick" Splines.

Closing

A Spline can be automatically "closed". A closed Spline is one that attaches the last vertex to the first vertex to form a "loop".

To close all three Splines, we simply use:

spline.setClosed(true);
spline2.setClosed(true);
spline3.setClosed(true);

Place this just before the update lines.

We can "un-close" - or open - the Spline by setting closed to false: spline.setClosed(false);

Running the program now displays the same three Splines as before except they now draw a "wobbly" line from the end of the Spline back to the beginning of it to form a sort of capital b shape.

The Spline looks it follows a continuous loop after closing.

Conclusion

You can try experimenting with different colours and different thickness. You can try moving the vertices around to different positions.

Another thing to try is add the ability to update all three splines from within the main program loop. You can update them all when a key is pressed or maybe a few time per second; it's up to you. However, when you do so, you'll notice that every time they are updated - even without any modifications to it - they change; that is because the normal offsets are randomised on every update. This could useful to animate "electrical" lines.

Before any of your own customisations, the final program should look something like this:
Final

That concludes the tutorial on the basics.


Selba Ward Tutorials


The Final Program

#include <SFML/Graphics.hpp>
#include <SelbaWard/Spline.hpp>

int main()
{
	sf::RenderWindow window(sf::VideoMode(800, 600), "Spline Basics Tutorial");
	sw::Spline spline;

	spline.addVertex({ 100.f, 500.f });
	spline.addVertex({ 700.f, 500.f });
	spline.addVertex({ 400.f, 300.f });
	spline.addVertices({ { 700.f, 300.f }, { 400.f, 100.f } });
	//spline.setThickness(0);
	//spline.setPrimitiveType(sf::PrimitiveType::LineStrip);
	spline.setInterpolationSteps(50u);
	spline.setRandomNormalOffsetsActivated(true);
	spline.setRandomNormalOffsetRange(30.f);

	sw::Spline spline2{ spline };
	sw::Spline spline3{ spline };

	spline.setColor(sf::Color::Blue);
	spline2.setColor(sf::Color::Cyan);
	spline3.setColor(sf::Color::White);

	spline.setThickness(20.f);
	spline2.setThickness(10.f);
	spline3.setThickness(5.f);

	spline.setClosed(true);
	spline2.setClosed(true);
	spline3.setClosed(true);

	spline.update();
	spline2.update();
	spline3.update();

	while (window.isOpen())
	{
		sf::Event event;
		while (window.pollEvent(event))
		{
			switch (event.type)
			{
			case sf::Event::Closed:
				window.close();
				break;
			}
		}

		window.clear();
		window.draw(spline);
		window.draw(spline2);
		window.draw(spline3);
		window.display();
	}
	return EXIT_SUCCESS;
}

Selba Ward Tutorials