In a word, to comport oneself with perfect propriety in Polygonal society, one ought to be a Polygon oneself. Such at least is the painful teaching of my experience.
--- Edwin A. Abbott
Now that we have a deeper understanding of the simple shapes, brushes, and strokes that Direct2D provides, we will focus on creating geometrical objects.
A Direct2D geometry is an ID2D1Geometry object. This object can be a simple geometry, like a rectangle, rounded rectangle or an ellipse, a path geometry, or a composite geometry.
Why do we want to create new objects, if it is already so easy to draw simple shapes? Well, Direct2D geometries enable the description of complex two-dimensional figures, and they pack a lot of power, such as being able to define hit-test regions, clip regions, and even animation paths. We will talk about those advanced features in later tutorials.
Direct2D geometries are immutable and device-independent resources created by ID2D1Factory. Generally, we will create those geometries one time and keep them for the life of the application, or until they have to be changed.
To draw Direct2D geometries, the DrawGeometry or FillGeometry methods must be called. We will now give an overview of the different possible Direct2D geometries.
Simple Geometrical Figures
To start with, we will create rectangles, rounded rectangles and ellipses, once again, but this time as an intrinsic geometrical object, instead of a shape.
To store the geometries, we will create three new COM pointers in the header file of the Direct2D class:
Take note though that drawing geometries might be slower than drawing primitives.
Path Geometries
Path geometries can be used to describe complex geometric figures composed of segments such as arcs, curves, and lines.
Path geometries are represented by the ID2D1PathGeometry interface. To instantiate a path geometry, the CreatePathGeometry method is used. To populate a path geometry with figures and segments, the Open method must be called to retrieve an ID2D1GeometrySink, which can then be used to add figures and segments to the path geometry.
To recapitulate, creating a path geometry requires four steps:
Create an empty path geometry.
Open the geometry and retrieve a sink.
Fill the sink with points, lines, and curves.
Close the geometry.
Let us try to learn by example, but be warned, trying to draw a path geometry that is not fully constructed yet, hits a breakpoint.
We will recreate the examples from the MSDN, and we will start with the two mountains:
Now we can create the empty path geometries:
So far, so easy.
Now we can add points to the geometries. We open the geometry, and then we first set the fill mode to D2D1_FILL_MODE_WINDING, using the D2D1_FILL_MODE method; check the MSDN for a detailed explanation. Adding the points to the geometry is straightforward, we simply fill an array with 2D-coordinates and then add that array to the sink using the AddLines method:
Drawing the mountain is straightforward:
The creation of the path geometry for the right mountain is left as an exercise to the reader.
To create the little river and the sun, we need the power of arc and Bézier segments.
Arc Segments
Creating arcs is actually quite difficult, thankfully Direct2D takes care of the difficult mathematics behind all of this. Basically, arc segments are parts of a Path Geometry, and all we have to do is to add points and tell Direct2D how to join those points together (note that there are quite many possibilities how to join two points by an elliptical arc). To define the elliptical arc, we have to fill out a D2D1_ARC_SEGMENT structure:
D2D1_POINT_2F point
Those coordinates specify the end point of the segment. Note that the starting point is automatically known to the geometry sink.
This member specifies whether the given arc is larger than 180 degrees.
Okay then, before finishing our little drawing, let us see how arcs work on a simpler example, perhaps. We will simply create four arcs with different properties.
First, we need four new COM pointer, obviously:
By now we know how to create path geometries, but to avoid having to type the same code over and over again, we will create a little helper function in the Direct2D class to create arc segments:
As you can see, the helper function is completely straightforward, we simply fill out an arc segment description structure and use the AddArc method to add our arc to the Path Geometry object.
We want to illustrate why all those variables are needed to define an arc segment, and thus we will fix a starting and an end point and showcase four possible arcs running through those two points; two counterclockwise and two clockwise arcs, one small and one large, in each case:
Now all that is left is to draw the arcs. We will draw the larger arcs with a blue brush, and the smaller ones with a red brush. To highlight the starting (yellow) and end (green) points, we draw an ellipse at their positions, with a high opaque value:
We now have enough knowledge to create the sun for our little scenery. We will also create a new brush to give the sun a small radial effect.
To create the flares and the river, we need to learn how to use Bézier segments.
Bézier Segments
The last, but definitely not least, segment types that Path Geometries support, are Bézier segments, named after the French engineer Pierre Bézier.
A Bézier curve is a parametric curve. In vector graphics, Bézier curves are used to model smooth curves, and Paths, combinations of linked Bézier curves, are not bound by the limits of rasterized images and are intuitive to modify.
We will not talk much about the mathematics behind Bézier curves until later in more advanced tutorials. For now, we will simply learn how to use Bézier curves in Direct2D.
Direct2D supports quadratic and cubic Bézier curves, the difference being that quadratic Bézier curves only have one control point, whereas cubic Bézier curves have two of those. You can think of the control points as magnets, or black holes: Imagine a straight line between two points, and imagine the line being pulled towards the magnets or black holes.
We will learn by example again. We create another COM pointer to a Path Geometry and create the empty geometry:
Now to fill the geometry with a Bézier segment, we have to use the AddBezier which takes a Bezier segment structure, which represents a cubic Bézier segment, as input:
A cubic Bézier curve is defined by four points: a start point (the last point in the Geometry Path figure), an end point (point3), and two control points (point1 and point2).
The two control points of a cubic Bézier curve behave like magnets, attracting portions of what would otherwise be a straight line toward themselves and producing a curve. The first control point, point1, affects the beginning portion of the curve; the second control point, point2, affects the ending portion of the curve.
Let’s see an example:
This code creates the following figure: The start and end points are highlighted with a yellow ellipse, the control points are highlighted by a red ellipse, the line between the start and end points is drawn in blue, and the actual Bézier segment is drawn with a red brush:
Okay, this is fun, let us add a few more points:
And we get the following crazy figure:
Bézier curves are fascinating, but as promised, we won’t talk about the astonishing mathematical properties until later.
For now, we will finish our scenery, adding flares to the sun and a little river flowing from the mountains.
First, we add the flares to the sun geometry:
Note that a Geometry Path may consist of multiple figures.
We draw the scene like this:
And to finish this tutorial, we will create the little river:
An array containing the geometry objects to add to the geometry group. The number of elements in this array is indicated by the geometriesCount parameter.
UINT geometriesCount
This integer specifies the number of elements in geometries.
ID2D1GeometryGroup **geometryGroup
When this method returns, this parameter contains the address of a pointer to the newly created geometry group.
As an example let us add the two mountains to the same group:
Please note that I cheated a little bit in this last example, I translated the right mountain a bit further to the right to avoid overlapping, as that is a topic for a later tutorial.
This was interesting! Direct2D allows us to use even quite difficult concepts such as Bézier curves in a very intuitive manner!
In the next tutorial, we will learn about how to transform those geometries.
You can download the source code of the final example in this tutorial from here. I made a few changes to the Direct2D class, there are more standard brushes now, and only standard brushes, as well as standard strokes. The method to show the FPS information now also accepts a brush, or rather a ComPtr to a brush, to specify the colour of the text.