Importing multiple sprites from an SVG

I was working on a personal project and wanted to use the VectorGraphics package. One limitation of that package is that it doesn’t offer any way to import an SVG into multiple separate sprites, which isn’t great for workflow when you’re just trying to author a page full of sprites in Inkscape or something.

So, inspired by a comment from @mcoted3d , here’s my solution, a custom importer for *.multi.svg files that generates a separate sprite from each top-level group in my SVG. It works in Unity 2022.1 (though you could probably adapt it to earlier versions without much trouble), and uses vectorgraphics version 2.0.0-preview.20.

Just to be 100% clear: this is something I wrote for a personal project, it’s not an official or supported piece of technology from Unity, and if you use it you are doing so entirely at your own risk. But, maybe it can be helpful :slight_smile:

using System.Collections.Generic;
using System.IO;
using System.Linq;
using Unity.VectorGraphics;
using UnityEditor.AssetImporters;
using UnityEngine;

// Set up this class as the importer for *.multi.svg files - the regular importer will be used for plain *.svg files.
// This 'composite extension' handling requires Unity 2022.1, but if you're willing to use an explicitly different
// extension (like "*.multisvg") then the rest of the code should still work in some earlier versions.
[ScriptedImporter(1, "multi.svg")]
public class MultiSpriteSVGImporter : ScriptedImporter
{
    [SerializeField] private float _stepDistance = 100f;
    [SerializeField] private float _maxCordDeviation = 0.5f;
    [SerializeField] private float _maxTanAngleDeviation = 0.1f;
    [SerializeField] private float _samplingStepSize = 0.01f;
    [SerializeField] private float _pixelsPerUnit = 10.0f;
    [SerializeField] private int _gradientResolution = 128;
    [SerializeField] private bool _flipYAxis = true;

    public override void OnImportAsset(AssetImportContext ctx)
    {
        // Load and parse the SVG itself
        SVGParser.SceneInfo svg;
        using (var reader = new StreamReader(ctx.assetPath))
            svg = SVGParser.ImportSVG(reader);

        // Reuse tesselation options for all sprites
        var tessellationOptions = new VectorUtils.TessellationOptions()
        {
            StepDistance = _stepDistance,
            MaxCordDeviation = _maxCordDeviation,
            MaxTanAngleDeviation = _maxTanAngleDeviation,
            SamplingStepSize = _samplingStepSize
        };

        foreach(var (childScene, spriteName) in BuildSpriteScenes(svg))
        {
            // Tesselate and make a sprite out of it
            var geometry = VectorUtils.TessellateScene(childScene, tessellationOptions, nodeOpacities: svg.NodeOpacity);
            var sprite = VectorUtils.BuildSprite(geometry, _pixelsPerUnit, VectorUtils.Alignment.Center, Vector2.zero, (ushort)_gradientResolution, _flipYAxis);

            sprite.name = spriteName;
           
            // Add it to the asset
            ctx.AddObjectToAsset(sprite.name, sprite);
        }

        // Create a dummy GameObject to serve as the 'main asset' so that all the sprites can have their names set
        // without having to worry about the name of the file itself
        var dummyGO = new GameObject();
        ctx.AddObjectToAsset("_dummy", dummyGO);
        ctx.SetMainObject(dummyGO);
    }

    /// <summary>
    /// Produce the Scene for each sprite that should be generated from the given SVG file.
    /// </summary>
    /// <param name="svg">The SVG scene.</param>
    /// <returns>A tuple of the Scene instance and the name of the sprite that should be generated from it.</returns>
    /// <remarks>
    /// This returns an enumerable of Scenes, rather than just SceneNode, so that you could easily
    /// change it to pack multiple subtrees of SceneNodes into a single sprite if that's what you want to do.
    /// </remarks>
    private IEnumerable<(Scene, string)> BuildSpriteScenes(SVGParser.SceneInfo svg)
    {
        // Walk down the tree until finding the first node with >1 children
        // (which we assume is the Layer node that has the different sprites as child elements)
        // If your SVG is structured differently then you could do something different here
        var node = svg.Scene.Root;
        while (node.Children.Count == 1)
            node = node.Children[0];

        // If we wanted to filter any nodes out, we'd do it here, but I don't.

        // Pack each child into its own little scene
        return node.Children.Select(childNode =>
        {
            var scene = new Scene();
            scene.Root = childNode;
            return (scene, svg.NodeIDs.Where(n => n.Value == childNode).First().Key);
        });
    }
}

Feedback is welcome, but I’m not likely to spend time implementing any feature requests or adapting it for your projects; it does what I need for my project so I’m getting on with other things :wink:

1 Like