DirectX Factor

Direct2D Geometries and Their Manipulations

Charles Petzold

Download the Code Sample

Charles PetzoldHigh school geometry comes in two distinct flavors: Euclidean geometry is oriented around constructions, theorems and proofs, while analytic geometry describes geometric figures numerically using points on a coordinate system, often called the Cartesian coordinate system in honor of the pioneer of analytic geometry, René Descartes.

It’s analytic geometry that forms the basis of the whole vector wing of computer graphics, so it’s right and proper that an interface named ID2D1Geometry (and the six interfaces that derive from it) pretty much sits in the center of Direct2D vector graphics.

In the previous installment of this column ( msdn.microsoft.com/magazine/dn342879), I discussed how to use the ID2D1PathGeometry to render lines in a finger-painting application that runs under Windows 8. Now I’d like to step back and explore geometries in broader detail, and in particular investigate some of the intriguing methods defined by ID2D1Geometry for manipulating geometries to make different geometries.

Even if you’re familiar with using geometries in the Windows Runtime (WinRT), ID2D1Geometry provides facilities that aren’t exposed in the WinRT Geometry class.

Overview

Geometries are basically collections of coordinate points. These coordinate points aren’t tied to any particular device, so geometries are inherently device-independent objects. For that reason, you create geometries of various sorts by calling methods on the ID2D1Factory object, which is the device-independent Direct2D factory. The six geometry-creation methods are shown in Figure 1, along with the interface type of the objects they create.

Figure 1 The Six Geometry Methods and Interfaces

ID2D1Factory MethodCreated Object (Derived from ID2D1Geometry)
CreateRectangleGeometryID2D1RectangleGeometry
CreateRoundedRectangleGeometryID2D1RoundedRectangleGeometry
CreateEllipseGeometryID2D1EllipseGeometry
CreatePathGeometryID2D1PathGeometry
CreateTransformedGeometryID2D1TransformedGeometry
CreateGeometryGroupID2D1GeometryGroup    

With the exception of CreatePathGeometry, these methods create immutable objects: All the information necessary to create the object is passed to the creation method, and you can’t change anything about the geometry after it has been created.

The only reason the ID2D1PathGeometry object is different is because CreatePathGeometry returns an object that’s essentially empty. I’ll describe how you fill it up shortly.

CreateTransformedGeometry accepts an existing ID2D1Geometry object and an affine transform matrix. The resultant geometry is translated, scaled, rotated or skewed by that matrix. The Create­GeometryGroup method accepts an array of ID2D1Geometry objects and creates a geometry that is the composite of all the individual geometries.

If you use the Visual Studio Direct2D (XAML) project template to create a Windows Store application that accesses DirectX, you’ll generally create geometry objects in the CreateDeviceIndependent­Resources override of the rendering class and use them throughout the lifetime of the program, in particular during the Render override.

The ID2D1RenderTarget interface (from which interfaces such as ID2D1DeviceContext derive) defines two methods for rendering geometries on a drawing surface: DrawGeometry and FillGeometry.

The DrawGeometry method draws the lines and curves of the geometry with a specified brush, stroke thickness, and stroke style, allowing solid, dotted, dashed, or custom dash-patterned lines. The FillGeometry method fills enclosed areas of a geometry with a brush and an optional opacity mask. You can also use geometries for clipping, which involves calling PushLayer on the render target with a D2D1_LAYER_PARAMETERS structure that includes the geometry.

Sometimes you need to animate a geometry. The most efficient approach is applying a matrix transform to the render target before rendering the geometry. However, if this isn’t adequate, you’ll need to re-create the geometry, probably during the Update method in the rendering class. (Or, you might want to create a bunch of geometries at the outset and save them.) Although re-creating geometries increases the rendering overhead, it’s certainly necessary sometimes. But, for the most part, if you don’t need to re-create geometries, don’t do so.

The Geometry Sink

The ID2D1PathGeometry object is a collection of straight lines and curves, specifically cubic and quadratic Bezier curves and arcs, which are curves on the circumference of an ellipse. You can control whether these lines and curves are connected, and whether they define enclosed areas.

Filling a path geometry with lines and curves involves the use of an object of type ID2D1GeometrySink. This particular sink is not for washing geometries! Think of it as a receptacle—a destination for lines and curves that are then retained by the path geometry.

Here’s how to build a path geometry:

  1. Create an ID2D1PathGeometry object by calling CreatePathGeometry on an ID2D1Factory object.
  2. Call Open on the ID2D1PathGeometry to obtain a new ID2D1GeometrySink object.
  3. Call methods on the ID2D1GeometrySink to add lines and curves to the path geometry.
  4. Call Close on the ID2D1GeometrySink.

The ID2D1PathGeometry isn’t usable until Close has been called on the ID2D1GeometrySink object. After Close is called, the ID2D1PathGeometry is immutable: Its contents can’t be altered in any way, and you can’t call Open again. The geometry sink no longer has a purpose, and you can get rid of it.

The third item in the list usually involves the most extensive code. A path geometry is a collection of figures; each figure is a series of connected lines and curves, called segments. When calling functions on the ID2D1GeometrySink, you begin with an optional call to SetFillMode to indicate the algorithm used for filling enclosed areas. Then, for each series of connected lines and curves in the path geometry:

  1. Call BeginFigure, indicating the first point and whether enclosed areas will be filled.
  2. Call methods beginning with the word Add to add connected lines, Bezier curves and arcs to the figure.
  3. Call EndFigure, indicating whether the last point should be automatically connected to the first point with a straight line.

When you look at the documentation of ID2D1GeometrySink, take note that the interface derives from ID2D1SimplifiedGeometrySink, which actually defines the most important methods. More on this differentiation shortly.

Because the contents of an ID2D1PathGeometry are immutable once you’ve defined its path, if you need to alter an ID2D1Path­Geometry you’ll have to re-create it and go through the path definition process again. However, if you’re just adding additional figures to the beginning or end of an existing path geometry, then a shortcut is available: You can create a new path geometry, add some figures to it, and then transfer the contents of the existing path geometry into the new path geometry by calling the Stream function of the existing path geometry with the new ID2D1GeometrySink. You can then add additional figures before closing.

Drawing and Filling

Now I’m ready to show you some code. The downloadable Geometry­Experimentation project was created in Visual Studio 2012 using the Windows Store Direct2D (XAML) template. I renamed the SimpleTextRenderer class to GeometryVarietiesRenderer, and I removed all the code and markup associated with the sample text rendering.

In the XAML file, I defined a bunch of radio buttons for various geometry-rendering techniques. Each radio button is associated with a member of a RenderingOption enumeration I defined. A big switch and case statement in the Render override uses members of this RenderingOption enumeration to govern what code is executed.

All the geometry creation occurs during the CreateDevice­IndependentResources override. One geometry that the program creates is a five-pointed star. The somewhat-generalized method that builds this geometry is shown in Figure 2. It consists of one figure with four line segments, but the last point is automatically connected to the first point.

Figure 2 A Method to Build a Five-Pointed Star Geometry

HRESULT GeometryVarietiesRenderer::CreateFivePointedStar(
  float radius, ID2D1PathGeometry** ppPathGeometry)
{
  if (ppPathGeometry == nullptr)
    return E_POINTER;
  HRESULT hr = m_d2dFactory->CreatePathGeometry(ppPathGeometry);
  ComPtr<ID2D1GeometrySink> geometrySink;
  if (SUCCEEDED(hr))
  {
    hr = (*ppPathGeometry)->Open(&geometrySink);
  }
  if (SUCCEEDED(hr))
  {
    geometrySink->BeginFigure(Point2F(0, -radius), 
      D2D1_FIGURE_BEGIN_FILLED);
    for (float angle = 2 * XM_2PI / 5; 
        angle < 2 * XM_2PI; angle += 2 * XM_2PI / 5)
    {
      float sin, cos;
      D2D1SinCos(angle, &sin, &cos);
      geometrySink->AddLine(Point2F(radius * sin, -radius * cos));
    }
    geometrySink->EndFigure(D2D1_FIGURE_END_CLOSED);
    hr = geometrySink->Close();
  }
  return hr;
}

Figure 3 shows much of the CreateDeviceIndependentResources method. (To keep the listing simple, I removed the handling of errant HRESULT values.) The method begins by creating a geometry that resembles a square-wave, a call to create the five-pointed star, and a call to another method to create an infinity sign. Two of these geometries are transformed, and all three are combined in the CreateGeometryGroup method (called at the bottom of Figure 3) into a member named m_geometryGroup.

Figure 3 Much of the CreateDeviceIndependentResources Override

void GeometryVarietiesRenderer::CreateDeviceIndependentResources()
{
  DirectXBase::CreateDeviceIndependentResources();
  // Create square-wave geometry
  HRESULT hr = m_d2dFactory->CreatePathGeometry(&m_squareWaveGeometry);
  ComPtr<ID2D1GeometrySink> geometrySink;
  hr = m_squareWaveGeometry->Open(&geometrySink);
  geometrySink->BeginFigure(Point2F(-250, 50), 
    D2D1_FIGURE_BEGIN_HOLLOW);
  geometrySink->AddLine(Point2F(-250, -50));
  geometrySink->AddLine(Point2F(-150, -50));
  geometrySink->AddLine(Point2F(-150,  50));
  geometrySink->AddLine(Point2F(-50,   50));
  geometrySink->AddLine(Point2F(-50,  -50));
  geometrySink->AddLine(Point2F( 50,  -50));
  geometrySink->AddLine(Point2F( 50,   50));
  geometrySink->AddLine(Point2F(150,   50));
  geometrySink->AddLine(Point2F(150,  -50));
  geometrySink->AddLine(Point2F(250,  -50));
  geometrySink->AddLine(Point2F(250,   50));
  geometrySink->EndFigure(D2D1_FIGURE_END_OPEN);
  hr = geometrySink->Close();
  // Create star geometry and translate it
  ComPtr<ID2D1PathGeometry> starGeometry;
  hr = CreateFivePointedStar(150, &starGeometry);
  hr = m_d2dFactory->CreateTransformedGeometry(starGeometry.Get(),
           Matrix3x2F::Translation(0, -200),
           &m_starGeometry);
  // Create infinity geometry and translate it
  ComPtr<ID2D1PathGeometry> infinityGeometry;
  hr = CreateInfinitySign(100, &infinityGeometry);
  hr = m_d2dFactory->CreateTransformedGeometry(infinityGeometry.Get(),
            Matrix3x2F::Translation(0, 200),
            &m_infinityGeometry);
  // Create geometry group
  CreateGeometryGroup();
  ...
}

The CreateDeviceIndependentResources method also creates two stroke styles with rounded ends and joins. One is solid and the other is dotted.

The CreateDeviceDependentResources method creates two brushes: black for drawing and red for filling.

When the program starts up, the first radio button is checked, and DrawGeometry is called:

m_d2dContext->DrawGeometry(m_geometryGroup.Get(),
                           m_blackBrush.Get());

The result is shown in Figure 4, looking oddly like the logo of some weird graphics cult.

The Startup Screen of GeometryExperimentation
Figure 4 The Startup Screen of GeometryExperimentation

Because the program combined the three separate geometries to simplify the rendering, they’ll all be rendered with the same brush. In a real program, you’d probably maintain a bunch of individual geometries and color them all differently.

The first few options demonstrate how the geometry can be drawn with a thicker stroke and a styled line, including a dotted line. (Throughout the program, stroke thicknesses of 1, 10 and 20 pixels are used, and should be easily distinguishable visually.) 

When demonstrating path geometries and animation in XAML, I like to apply a XAML-based animation to the dash offset of a stroke style, causing dots to travel around the geometry. You can do something similar in DirectX (as the Animated Dot Offset option demonstrates), but you need to explicitly recreate the ID2D1StrokeStyle object during each screen refresh. This happens in the Update method.

Enclosed areas of the geometry can also be filled with a brush:

m_d2dContext->FillGeometry(m_geometryGroup.Get(),
                           m_redBrush.Get());

The result is shown in Figure 5. There are no enclosed areas in the square wave. The interior pentagon of the five-pointed star isn’t filled because the filling mode is set to an algorithm known as Alternate. You can use the pair of radio buttons at the bottom left of Figure 5 to select Winding Fill Mode to fill the pentagon. The fill mode needs to be specified when creating the ID2D1GeometryGroup, so the m_geometryGroup object needs to be re-created when either of those two radio buttons are clicked. If the geometries overlap in the geometry group, then intersecting areas are filled also based on that fill mode.

Filling Geometries
Figure 5 Filling Geometries

Often you’ll want to both draw and fill a geometry. Generally you’ll want to call FillGeometry first and then DrawGeometry to keep the stroke fully visible.

Simplified and Simplifying Geometries

Suppose your application creates an ID2D1PathGeometry dynamically, perhaps from user input, and you’d like to “interrogate” the path geometry to extract all the figures and segments. Perhaps you’d like to save this information in a XAML format as a series of standard PathGeometry, PathFigure, LineSegment and BezierSegment tags.

At first, it doesn’t appear as if this is possible. The ID2D1PathGeometry has GetFigureCount and GetSegmentCount methods, but no methods to actually extract those figures and segments.

But recall the Stream method. This method accepts an ID2D1­GeometrySink and copies the contents of the path geometry into that sink. The key here is that you can write your own class that implements the ID2D1GeometrySink interface, and pass an instance of that class to the Stream method. Within this custom class, you can handle all the calls to BeginFigure, AddLine and so forth, and do whatever you want with them.

Of course, such a class isn’t trivial. It would need implementations of all the methods in ID2D1GeometrySink, as well as ID2D1­SimplifiedGeometrySink and IUnknown.

However, there’s a way to make this job somewhat easier: The ID2D1Geometry interface defines a method named Simplify that converts any geometry object into a “simplified” geometry that contains only straight lines and cubic Bezier splines. This feat is possible because quadratic Bezier splines and arcs can be approximated by cubic Bezier splines. This means your custom class need only implement the ID2D1SimplifiedGeometrySink and IUnknown methods. Simply pass an instance of this custom class to the Simplify method of any geometry.

You can also use Simplify to copy the simplified contents of a geometry to a new path geometry. Here’s the code in Geometry­Experimentation that does this (excluding the HRESULT checking):

m_d2dFactory->CreatePathGeometry(&m_simplifiedGeometry);
ComPtr<ID2D1GeometrySink> geometrySink;
m_simplifiedGeometry->Open(&geometrySink);
m_geometryGroup->Simplify(D2D1_GEOMETRY_SIMPLIFICATION_OPTION_CUBICS_AND_LINES,
                          IdentityMatrix(), geometrySink.Get());
geometrySink->Close();

Notice the first argument to the Simplify method indicates that you want both cubic Beziers and straight lines in the simplified path geometry. You can restrict that to only lines, in which case the Bezier curves are approximated by a series of straight lines. This is a process called “flattening.” If flattening is performed with a lot of precision you can’t tell the difference visually, but you can also specify a flattening tolerance so the Bezier curve is not approximated very well. Here’s some code in GeometryExperimentation that creates a “grossly simplified” geometry:

m_d2dFactory->CreatePathGeometry(&m_grosslySimplifiedGeometry);
m_grosslySimplifiedGeometry->Open(&geometrySink);
m_geometryGroup->Simplify(D2D1_GEOMETRY_SIMPLIFICATION_OPTION_LINES,
                           IdentityMatrix(), 20, geometrySink.Get());
geometrySink->Close();

The star and square wave look the same, but the infinity sign is no longer quite so smooth, as shown in Figure 6. By default, the flattening tolerance is 0.25.

A Grossly Flattened Infinity Sign
Figure 6 A Grossly Flattened Infinity Sign

More Geometry Manipulations

The ID2D1Geometry interface defines three additional methods that are similar to Simplify in that they calculate a new geometry and write it into an ID2D1SimplifiedGeometrySink. I’ll discuss Outline and Widen here, but not CombineWithGeometry (because it only does something interesting with multiple overlapping geometries, and I have none in this program).

As you’ve seen, path geometries can have intersecting segments. The infinity sign has an intersecting segment right in the center, and the five-pointed star has a bunch of intersecting segments. The Outline method defined by ID2D1Geometry creates a new path geometry based on an existing path geometry that eliminates those intersections but retains the same enclosed areas.

Here’s the code in GeometryExperimentation that converts the geometry group into an outlined path geometry:

m_d2dFactory->CreatePathGeometry(&m_outlinedGeometry);
m_outlinedGeometry->Open(&geometrySink);
m_geometryGroup->Outline(IdentityMatrix(), geometrySink.Get());
geometrySink->Close();

Because this m_outlinedGeometry object defines the same filled areas as m_geometryGroup, it’s different depending on whether m_geometryGroup was created using an Alternate Fill Mode or Winding Fill Mode. 

The original geometry group has a total of three figures: one for the star, one for the square wave and one for the infinity sign. If this geometry group is created with the Alternate Fill Mode, the outlined geometry contains eight figures: Five for the five points of the star, one for the square wave and two for the infinity sign. But visually it seems the same. If the geometry group is created with the Winding Fill Mode, however, the outlined geometry has a total of four figures: the infinity sign has two figures just as with the Alternate Fill Mode, but because the entire interior of the star is filled, the star consists of just one figure, as shown in Figure 7.

An Outlined Path Geometry
Figure 7 An Outlined Path Geometry

Defining such a geometry from scratch would be mathematically rather difficult, but the Outline method makes it quite easy. Because the path geometry defined by Outline contains no intersecting segments, the fill mode has no effect.

I find the Widen method to be the most interesting of all. To understand what Widen does, consider a path geometry that contains just a single straight line between two points. When this geometry is drawn, it’s stroked with a particular line thickness, so it’s actually rendered as a filled rectangle. If the line style includes rounded ends, this rectangle is adorned with two filled semicircles.

The Widen method computes a path geometry that describes the outline of this rendered object. To do this, Widen requires arguments specifying the desired stroke thickness and stroke style, just like DrawGeometry. Here’s the code in the GeometryExperimentation project:

m_d2dFactory->CreatePathGeometry(&m_widenedGeometry);
m_widenedGeometry->Open(&geometrySink);
m_geometryGroup->Widen(20, m_roundedStrokeStyle.Get(),
                        IdentityMatrix(), geometrySink.Get());
geometrySink->Close();

Notice the 20-pixel stroke thickness. The program then draws this widened geometry using a one-pixel stroke thickness:

m_d2dContext->DrawGeometry(m_widenedGeometry.Get(),
                           m_blackBrush.Get(), 1);

The result is shown in Figure 8. I find the artifacts created by this process extremely interesting. It’s as if I’m somehow peering into the innards of the line-drawing algorithm.

A Widened Path Geometry
Figure 8 A Widened Path Geometry

The GeometryExperimentation program also allows filling the widened path geometry:

m_d2dContext->FillGeometry(m_widenedGeometry.Get(),
                           m_redBrush.Get());

Filling a path geometry that has been widened with a particular stroke width and stroke style is visually identical to stroking the original path with that width and style. The only visual difference between the Draw Rounded Wide Stroke and Fill Widened options in GeometryExperimentation is the color, because I use black for drawing and red for filling.

You might want to fill and draw a widened path geometry, but you’d probably prefer the internal artifacts are removed. Doing so is exceptionally easy. Simply apply the Outline method to the widened path geometry:

m_d2dFactory->CreatePathGeometry(&m_outlinedWidenedGeometry);
m_outlinedWidenedGeometry->Open(&geometrySink);
m_widenedGeometry->Outline(IdentityMatrix(), geometrySink.Get());
geometrySink->Close();

Now when you fill and draw the outlined widened path geometry, you get the image in Figure 9. It’s free of artifacts and visually quite different from anything else rendered by this program, and different from anything I’d be brave enough to code from scratch.

An Outlined Widened Path Geometry, Stroked and Filled
Figure 9 An Outlined Widened Path Geometry, Stroked and Filled

Any Others?

There’s another method that writes geometry data to an ID2D1SimplifiedGeometrySink, but you might have to go hunting for it. It’s not among the Direct2D interfaces. It’s in DirectWrite, and it’s the GetGlyphRunOutline method of IDWriteFontFace. This powerful method generates path geometries from text character outlines, and it’s simply too fun to ignore. Stay tuned.


Charles Petzold is a longtime contributor to MSDN Magazine and the author of “Programming Windows, 6th edition” (O’Reilly Media, 2012), a book about writing applications for Windows 8. His Web site is charlespetzold.com.

Thanks to the following technical experts for reviewing this article: Wessam Bahnassi (In|Framez Technology), Worachai Chaoweeraprasit (Microsoft), Anthony Hodsdon (Microsoft) and Michael B. McLaughlin (Bob Taco Industries)

Anthony Hodsdon has been a developer on the Direct2D team since its inception in Windows 7. His professional interests include geometry (both analytic and Euclidean) and numerical algorithms.

Worachai Chaoweeraprasit leads the development of Direct2D and DirectWrite. He loves good typography and mind-bendingly fast graphics. He taught his third-grader in his free time an easier way to program than moving blocks--he told him it's simply called C.

Michael B. McLaughlin is a Visual C++ MVP and the proprietor of Bob Taco Industries, a micro-ISV and consulting firm. He was formerly an XNA/DirectX MVP and is a retired lawyer. His website is bobtacoindustries.com and his Twitter handle is @mikebmcl.

Wessam Bahnassi is a software engineer with great passion for computer graphics and games. He worked as a Lead Rendering Engineering at Electronic Arts for 7 years. Now he is Lead Programmer at In|Framez Technology Corp., a supervisor of the Arabic Game Developer Network, a DirectX MVP since 2003, and now Visual C++ MVP.

 

Rate: