Search Unity

Showcase Importing multiple sprites from an SVG

Discussion in 'UI Toolkit' started by superpig, Sep 6, 2022.

  1. superpig

    superpig

    Drink more water! Unity Technologies

    Joined:
    Jan 16, 2011
    Posts:
    4,660
    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 :)

    Code (CSharp):
    1.  
    2. using System.Collections.Generic;
    3. using System.IO;
    4. using System.Linq;
    5. using Unity.VectorGraphics;
    6. using UnityEditor.AssetImporters;
    7. using UnityEngine;
    8.  
    9. // Set up this class as the importer for *.multi.svg files - the regular importer will be used for plain *.svg files.
    10. // This 'composite extension' handling requires Unity 2022.1, but if you're willing to use an explicitly different
    11. // extension (like "*.multisvg") then the rest of the code should still work in some earlier versions.
    12. [ScriptedImporter(1, "multi.svg")]
    13. public class MultiSpriteSVGImporter : ScriptedImporter
    14. {
    15.     [SerializeField] private float _stepDistance = 100f;
    16.     [SerializeField] private float _maxCordDeviation = 0.5f;
    17.     [SerializeField] private float _maxTanAngleDeviation = 0.1f;
    18.     [SerializeField] private float _samplingStepSize = 0.01f;
    19.     [SerializeField] private float _pixelsPerUnit = 10.0f;
    20.     [SerializeField] private int _gradientResolution = 128;
    21.     [SerializeField] private bool _flipYAxis = true;
    22.  
    23.     public override void OnImportAsset(AssetImportContext ctx)
    24.     {
    25.         // Load and parse the SVG itself
    26.         SVGParser.SceneInfo svg;
    27.         using (var reader = new StreamReader(ctx.assetPath))
    28.             svg = SVGParser.ImportSVG(reader);
    29.  
    30.         // Reuse tesselation options for all sprites
    31.         var tessellationOptions = new VectorUtils.TessellationOptions()
    32.         {
    33.             StepDistance = _stepDistance,
    34.             MaxCordDeviation = _maxCordDeviation,
    35.             MaxTanAngleDeviation = _maxTanAngleDeviation,
    36.             SamplingStepSize = _samplingStepSize
    37.         };
    38.  
    39.         foreach(var (childScene, spriteName) in BuildSpriteScenes(svg))
    40.         {
    41.             // Tesselate and make a sprite out of it
    42.             var geometry = VectorUtils.TessellateScene(childScene, tessellationOptions, nodeOpacities: svg.NodeOpacity);
    43.             var sprite = VectorUtils.BuildSprite(geometry, _pixelsPerUnit, VectorUtils.Alignment.Center, Vector2.zero, (ushort)_gradientResolution, _flipYAxis);
    44.  
    45.             sprite.name = spriteName;
    46.            
    47.             // Add it to the asset
    48.             ctx.AddObjectToAsset(sprite.name, sprite);
    49.         }
    50.  
    51.         // Create a dummy GameObject to serve as the 'main asset' so that all the sprites can have their names set
    52.         // without having to worry about the name of the file itself
    53.         var dummyGO = new GameObject();
    54.         ctx.AddObjectToAsset("_dummy", dummyGO);
    55.         ctx.SetMainObject(dummyGO);
    56.     }
    57.  
    58.     /// <summary>
    59.     /// Produce the Scene for each sprite that should be generated from the given SVG file.
    60.     /// </summary>
    61.     /// <param name="svg">The SVG scene.</param>
    62.     /// <returns>A tuple of the Scene instance and the name of the sprite that should be generated from it.</returns>
    63.     /// <remarks>
    64.     /// This returns an enumerable of Scenes, rather than just SceneNode, so that you could easily
    65.     /// change it to pack multiple subtrees of SceneNodes into a single sprite if that's what you want to do.
    66.     /// </remarks>
    67.     private IEnumerable<(Scene, string)> BuildSpriteScenes(SVGParser.SceneInfo svg)
    68.     {
    69.         // Walk down the tree until finding the first node with >1 children
    70.         // (which we assume is the Layer node that has the different sprites as child elements)
    71.         // If your SVG is structured differently then you could do something different here
    72.         var node = svg.Scene.Root;
    73.         while (node.Children.Count == 1)
    74.             node = node.Children[0];
    75.  
    76.         // If we wanted to filter any nodes out, we'd do it here, but I don't.
    77.  
    78.         // Pack each child into its own little scene
    79.         return node.Children.Select(childNode =>
    80.         {
    81.             var scene = new Scene();
    82.             scene.Root = childNode;
    83.             return (scene, svg.NodeIDs.Where(n => n.Value == childNode).First().Key);
    84.         });
    85.     }
    86. }
    87.  
    88.  
    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 ;)