This is the Big One. SVG is an immensely powerful image format. It yields very small file sizes losslessly, with infinite scalability. Virtually every vector-drawing application has some form of import/export support for SVG, making it extremely portable. It can be trivially edited or even authored by hand in a text editor.
The absence of SVG support in Unreal Engine has been a thorn in my side since I first started using the engine some 5 years ago. Today, we’re going to rectify that.
We’ll start with a brief exploration of the spec, mapping some of the features we aim to support to the Skia API. We’ll create a UMG widget with a clumsy-but-workable API for defining an SVG by hand in the Details panel. Finally, in the next entry, we’ll create a new Unreal asset type, so that we can import SVG files into the Unreal Editor and load them from disk for rendering on-demand.
Investigating the spec (and managing expectations)
As noted by MDN
, “SVG is a huge specification,” and this is but a humble blog post. We will not be exhaustively supporting every nook and cranny of the SVG feature-set, because that would be a gargantuan task. Instead, we’ll isolate a small subset of the most common and useful features for UI design. The goal is to allow for designing relatively simple assets that greatly benefit from rendering in a resolution-agnostic way — icons are a prime example — in a design tool like Inkscape, Figma, or Adobe Illustrator, and bringing them into Unreal Engine for use in UMG widget blueprints.
That said, since we’re building all of this functionality from the ground up, my hope is that by the end of this post you’ll be armed with sufficient knowledge to fill in any gaps that may be required to support your particular use case.
Most of the core SVG shape elements map trivially to Skia types. For example, here’s the rect element:
One somewhat interesting example is the SVG path. Path commands are specified in SVG using a terse and pretty obtuse syntax that looks something like the following (this particular example is Material Design’s “Favorite” icon
):
Mercifully, Skia includes a utility to parse these path commands so we don’t have to worry about doing it ourselves:
First, at least temporarily, we want a way to build an SVG from base elements directly in the Unreal Editor. This will not be very practical for most use cases, because SVGs are rarely authored by hand like this (even though it’s technically possible). But it does give us a useful intermediate stage to work with, allowing us to test our code in real-time by manipulating properties in the UMG designer and hot-reloading when we need to make C++ changes.
Second, we want the ability to drag-and-drop an SVG file into the Unreal Editor, where it will be serialized and stored as a static *.uasset that can be loaded from disk and rendered on-demand. This will require parsing SVG’s textual representation, which is the main reason we’re saving it for later.
These two formats will require slightly different type representations — at least, if we want to do things optimally.
Primer on tagged unions and data locality
The most cache-friendly way to store a list of heterogeneous types in C or C++ is by using a tagged union. If you’re not familiar, a tagged union is basically a type that holds a union of types, which represent the actual data; and an enum (the tag), which indicates which “variant” of the union is actually in use at any given time or for any given instance. It’s a little bit hard to clearly describe, so here’s a simple example:
The example above would print something like this:
The reason an array of tagged unions is relatively cache-efficient is that it allows us to store an arbitrary sequence of heterogeneous types in contiguous memory. A union can only store one of its fields at a time — in our example, a bool, an int64_t, or a float — and thanks to that unusual property, it only needs to occupy as much space as its largest field. In other words, the size of our Foo union is equal to max(sizeof(bool), sizeof(int64_t), sizeof(float)) instead of sizeof(bool) + sizeof(int64_t) + sizeof(float) (plus whatever padding would be required to store them in a given order).
If we wanted to do something like this without a union, we would need to use polymorphism and downcasting instead — something like this:
Needing to store the data as an array of pointers adds an extra layer of indirection: every iteration, we have to jump to the pointer’s location in the array’s memory, and then follow that pointer to the location where the actual data is stored. This means potentially lots of memory fragmentation and CPU cache misses, which is several orders of magnitude slower than an equivalent loop over contiguously-stored elements.
What does this have to do with Unreal?
Unreal Engine has a handy TUnion type, which would be ideal for storing C++ representations of the various types of SVG elements we’ll want to draw — except that TUnion, like most other templated types, cannot be used with the UProperty system due to limitations of the Unreal Header Tool. So to get started, we’ll need to use a polymorphic base UObject-deriving class to allow our SVGs to be constructed in the UMG designer. Later, when we implement SVG asset importing and loading, we’ll investigate using TUnions so we can iterate more efficiently over our SVG elements. To stay relatively agile, we’ll use the same UStructs as wrapped values for both the polymorphic UObject implementation and the tagged union implementation.
Sketching out the SVG element types
For the sake of simplicity, we’ll start with the list of “basic shapes” from MDN’s guide
:
Then an enum to use as a discriminant:
Almost all of these elements also support a common set of “presentation attributes,” which more-or-less correspond to SkPaint properties. We’ll create another container for (a subset of) those — we can always add more later.
Polymorphic UClass wrappers
Our base UClass will have the Abstract specifier, because it shouldn’t be a valid option for an SVG element type, and EditInlineNew, indicating that we should be able to create new instances directly from the Details panel instead of needing to select an existing asset. (These specifiers might seem contradictory, and they sort of are — EditInlineNew will actually only apply to the classes that derive from this one, because that specifier is inherited, while Abstract is not.)
We give it the Type enum tag, which gets a UProperty annotation for serialization, but no specifiers because it shouldn’t be user-editable. This field isn’t technically necessary, since we could query the concrete type through reflection (e.g., if (element->IsA<UChildClass>())) — but checking a simple enum field will be cheaper than going through the reflection system, and arguably a little more ergonomic since we can use a switch statement.
The base class also hosts the style properties, since we want them to be shared by all SVG elements.
Then we can declare the child types. There will be one of these for each of the ESVGElement enumerators and corresponding structs we declared earlier. All of them follow the exact same pattern, so I’ll only spell out a couple of examples here:
The ShowOnlyInnerProperties specifier doesn’t seem to be working as intended as of writing, but I’ll leave it alone in case it gets fixed in a patch at some point.
Implementing the UMG SVG-Builder widget
Finally, we can implement the UMG widget. First, as always, the boilerplate:
We’ll go ahead and add support for the viewBox attribute, but for now we’ll only actually do anything useful with the Width and Height properties.
Add the array for our elements. For now, we’ll work under the assumption that the array won’t be modified at runtime. It would be pretty difficult to detect if individual elements were mutated, added or removed without some heavy polling, and it would make for expensive updates to re-allocate a whole new array instance just to make a setter work with this field. We could hypothetically expose something like a RequestRedraw method on the UMG widget to at least make array updates work manually if necessary — but in all likelihood, YAGNI
.
OnDraw dispatcher
With that, we can start fleshing out our OnDraw implementation. This will basically be a dispatcher: After some setup, we’ll iterate over each element, identify its type, and delegate the actual drawing operation to a specific function for each.
All of those individual functions are going to take the same set of arguments, so instead of declaring all of our draw functions with a ton of identical parameters each, we’ll declare an internal struct as a container for them. That way we don’t need to update every single function signature every time we realize we need to tweak the arguments list.
Alternatively, we could have added a pure-virtual Draw method to some base struct that all of our element structs would override, which is the more traditional way to do dynamic dispatch. The virtual function calls would incur some likely negligible runtime overhead, but mostly this is a stylistic choice and pretty inconsequential either way.
For the arguments struct, we’ll obviously need the SkCanvas. We’ll store it as a reference, which is a little unorthodox — you usually see poiners in member variable declarations, not references — but there’s no reason it should ever actually be null. Using a reference type also encourages us to keep the struct lifetime tightly scoped to the OnDraw function body, since it prevents us from instantiating the struct outside of that function. This is good, because we don’t “own” that SkCanvas. For all we know, it may only be valid for the duration of our function call, so we shouldn’t be hanging on to any references to it when that function is not actively executing.
We’ll add a shared SkPaint instance to the args as well, to avoid needing to create a new one for each element. Similarly, several of the specific draw functions will need to use an SkPath, which dynamically allocates, so we’ll create one of those for our struct and rewind it after each iteration to reset its state while potentially reusing its allocated storage.
Each draw function will also need the scale, and the style for that element. The style will actually be unique per-element, but we can just update the field value in the dispatcher before calling the target function.
Here are the new header declarations:
Note that all of those methods are static, so we could (and likely will) decouple them from this particular class at some point. For now, they’re fine where they are.
Here’s our dispatcher:
Pretty straightforward, but do note that this time, we’re deriving the scale from the difference between our ViewBox and the render target size instead of using the argument from the Slate widget. I haven’t exhaustively tested that logic, so it’s possible that’s not exactly what we want — it may be a better idea to set the Slate widget’s DesiredSizeOverride from our ViewBox properties and continue treating the scale argument as canonical. We can always revisit it if we run into scaling issues later.
Per-element Draw functions
Now we can dig in to the individual draw calls.
Rect
That weird auto& declaration at the top, if you’re not familiar, is a C++17 feature called a “structured binding” (what most languages would call “destructuring”). This is an idiom I’ve brought with me from JavaScript, and it’s arguably a bit fraught to use like this: the types and values of the local declarations depend on the order — not the names — of the corresponding struct fields, which means that changing the layout of FDrawArgs could lead to some slightly confusing error messages in our draw functions. Nevertheless, I don’t want to waste time (or screen real-estate) typing args.<member> dozens of times over the course of these implementations, so I’ll leave it in place for now — this code will ultimately be pretty short-lived anyway.
We don’t really want to tell Skia to waste time trying to paint transparent fill colors or zero-width strokes, so we should check those properties before making the actual draw calls.
SkPaint::setStrokeWidth takes a single scalar value, but our scale vector could theoretically be non-uniform. I don’t really have a great solution in mind for that yet — we could use the compound path technique like we did for the variable borders in our Canvas Rect implementation, but that’s going to be a lot of code to write, because we’ll have to do it for every single one of these elements, so I’m holding off until I can come up with a way to abstract it cleanly and efficiently. For now, I’m using using an arbitrary component of our scale vector and leaving a TODO comment.
Speaking of abstractions — we’re also going to be repeating this pattern of checking the style properties before painting for every single element, and that is something we can abstract pretty easily, so let’s go ahead and do it:
We’re almost there now, but SVG actually supports 2D corner radii for the rect element, so we’ll also need to check for that and adjust accordingly. Here’s the final DrawRect implementation in full:
Circle
For DrawCircle, we run into some trickiness with non-uniform scaling due to the single-scalar radius property. There are several ways to skin this particular cat, but I decided to just trace the unmodified circle as a path and transform it with a scale matrix:
Ellipse, Line
DrawEllipse and DrawLine are refreshingly straightforward, with no real gotchas besides the stroke-width issue that we’re ignoring for now:
PolyLine, Polygon
DrawPolyLine is pretty simple, except that the API is a little unintuitive. There are a couple of techniques that initially look like they could work — SkPath::addPoly and SkCanvas::drawPoints(kLines_PointMode, ...) — but the latter seems intended for drawing multiple discrete line segments from a set of point pairs. We’ll use the former — the boolean argument at the end indicates whether the last point should be connected back to the first.
We’ll also specify the path’s “fill type” property here (via SVG’s “fill rule” attribute), since it’s possible for the line segments to cross over one another, and governing the behavior of those overlaps is the use case for that attribute.
DrawPolygon is identical to DrawPolyLine, except the aforementioned boolean argument will be set to true.
Path
And finally we have DrawPath, which is straightforward except for the need to convert Unreal’s platform-dependent character encoding to char for Skia.
Checking our work
Feel free to start up the editor and drop a Canvas SVG Builder into a widget blueprint to give it a test run. Here’s a replica of the SVG I drew up in Figma to demonstrate tracing a manual path for a rounded rectangle in the last entry, but make sure to put all of the element types we defined through their paces.
If you go through the process of trying to manually re-author an existing SVG in the Unreal Editor, as I’ve just done, you will probably come to the conclusion that this workflow is definitely not sustainable. We need a way to import and load SVG files so we can author them in an external tool and bring them directly into Unreal. Unfortunately, this post has already gone on longer than I intended, so we’ll save that exercise for the next (and likely final) entry in this series.