Using SkiaSharp in Unity
Monday, August 28, 2017 (updated May 6, 2018)
Comments: 14 (latest April 29, 2022)
For the past two weeks I've been trying to make SkiaSharp work inside Unity. It has been zero fun but I got it to work. Painfully and creakily, but it works. I suspect I'm the first person to do this so I'm going to document the process here.
Yes, this is for a game project. I'll let you know when it's closer to release.
Background: Skia is an open-source 2D graphics library from Google. It lets you fill and stroke vector shapes (polygons, circles, spline curves, text). 2D graphics features are easy for native apps and web pages, but Unity doesn't have any such feature.
(There's LineRenderer but that's very limited. No polygon fill, no true curves.)
Skia is a native library -- you can download compiled libraries for Mac, Win, Linux, etc. Then the Xamarin people created SkiaSharp, which is a C# wrapper for Skia. Problem solved, right? Drop the library into Unity, build Mac and Windows apps, go.
Nope. It was a headache. But I made it work, in a very clunky way.
UPDATE, SEPT 4: Many thanks to Marshall Quander, who read my original blog post and clued me into the right way to set things up. Or, at least, the less-wrong way. This post is much shorter now!
I'm not going to describe every blind alley. (See the end of this post for a taste.) Instead, I will give a recipe for creating a tiny Unity project which displays some 2D graphics. Follow along!
UPDATE, NOV 19: Added a section about disposing of texture objects so you don't leak memory.
Orientation: the library files
SkiaSharp consists of two libraries. You will need both:
- The managed library (compiled C# code). This is called
- The native library (compiled C++ code). This is called
libSkiaSharp.bundleon Mac, and I guess it would be
You now have these files:
Where I got these files.
If you just want to make the demo work, skip ahead to the Recipe section. This is archeology.
You can download most of these files from SkiaSharp's nuget page. Hit Manual Download, rename the resulting
skiasharp.1.59.1.nupkg file to
skiasharp.1.59.1.zip, and unzip it.
Critical note: for Windows, you must use the native DLLs from
runtimes/win7-x86. I spent days trying to get the one from
runtimes/win10-x64 to work on my 64-bit Windows 10 machine. It would... not... load. Stick with the win7 versions.
The other critical note: for Mac, you have to rename their
libSkiaSharp.dylib file to
libSkiaSharp.bundle. This is clearly silly, since the standard OSX suffix for dynamically-loadable libraries is
.dylib and has been forever. But Unity doesn't like that.
.bundle works, so we go with that.
managed/SkiaSharp.dll file is (essentially) the same one that nuget distributes as
lib/net45/SkiaSharp.dll. As I said, this isn't really portable. The reason is that it tries to import
libSkiaSharp.dll with that file suffix hardwired in. I had to build a new version which imports
libSkiaSharp, without a file suffix. C# is smart enough to pick the right suffix.
But wait, you ask, what about
SkiaSharp.dll.config? This is a config file from the nuget package which is supposed to paper over all those suffix problems. Yeah, well, that seemed to work for command-line C# builds, but I couldn't make it work inside Unity. So I'm ignoring it.
Okay, on with the recipe. I am doing this on a Mac, in case it matters. (I hope it doesn't matter, but...)
Launch Unity 2017.1.0f3.
Create a new 2D project called
Select Edit / Project Settings / Player to open PlayerSettings in the inspector pane. Select the Other Settings subpane. Under Configuration, set Scripting Runtime Version to "Experimental (.NET 4.6 Equivalent)". Unity will then insist on restarting.
This is necessary because the SkiaSharp library is built for .NET 4.5. At least, the version that I got to work is, and I'm scared to change the recipe any further.
- Select GameObject / UI / Raw Image. Unity will create a white square. In the hierarchy window, you'll see a Canvas object containing a RawImage.
If you don't see the white square, make sure you've selected the Game tab in the central window.
Hit Save and save your scene with the name
Main(in the Assets folder).
Select the Canvas. In the inspector, set UI Scale Mode to "Scale With Screen Size". This isn't necessary but this mode makes the most sense to me.
In the bottom pane, switch to the Project tab. Select the
Assetsfolder. Select Assets / Create / Folder to create a new folder inside Assets; name it
SkiaSharp. Then create folders called
Assets/SkiaSharp/x64folder. Select it in the bottom pane. In the import inspector, select the Platform Settings / Editor (Unity) pane, and set CPU to
x86_64. Then select the Platform Settings / Standalone (⬇) pane, and uncheck
x86. Then hit the Apply button.
Assets/SkiaSharp/x86folder. Select it in the bottom pane. In the import inspector, select the Platform Settings / Editor (Unity) pane, and set CPU to
x86. Then select the Platform Settings / Standalone (⬇) pane, and uncheck
x86_64. Then hit the Apply button.
Assets/SkiaSharpfolder. Don't mess with its import settings. It's not necessary, and when I tried it I broke my Unity project entirely.
When you are adjusting the import settings, make sure you're adjusting them for the right file each time! It's easy to leave the wrong file selected. The Information section at the bottom of the inspector pane will show the one you're editing.
In the hierarchy window, select the RawImage component. In the inspector, hit Add Component; select Scripts / Raw Image Draw.
Pray sincerely to the .NET gods and hit the Run button. You should see:
RawImageDraw.cs code draws these shapes and lines on a transparent background. (The dark-blue background color comes from the Main Camera component; the RawImage is drawn on top of that.)
Note that the triangle and semicircle are drawn in translucent yellow (50% opacity).
- You should now be able to build Mac and Windows apps. Both
x86_64should work on both platforms. (Although all Macs are 64-bit-capable, and have been for the last ten years, so you can ignore Mac x86.)
The pain points
The big one: What about Linux? Sorry, the SkiaSharp project doesn't offer a compiled Linux library. (See README.) If someone builds one, let me know.
A second problem: Unity does some incomprehensible caching of libraries. Sometimes your app will refuse to run in the editor even though you have the right libraries installed. Sometimes it will run correctly even though you don't have the right libraries installed. (Even if you restart Unity!) This made writing this post very, very horrible. If you follow these instructions and they don't work for you, all I can say is that I sympathize.
Four months into my project, I got around to profiling the memory usage. Surprise! It leaks texture objects like a flooding aqueduct.
I didn't realize (and therefore my code doesn't account for) the fact that Unity doesn't garbage-collect textures. I create a Texture2D and put it into a RawImage, but when the RawImage is destroyed, the Texture2D lingers. Even though nothing is referencing it any more.
To avoid problems, if you throw away the RawImage, you must call
In my project, I created a subclass of RawImage called OwnedImage, which takes care of this in its own
OnDestroy() method. I leave the implementation as an exercise.
Some more notes
To understand the Skia calls in
RawImageDraw.cs, see the SkiaSharp reference docs.
I always draw into a Unity RawImage object. That is, I take the Texture2D created by the Skia render process and put it into a RawImage. You can probably do other stuff with that Texture2D, but I haven't experimented.
If you create a RawImage in your Unity project and then draw into it at run time, the player might see a flash of a blank white square. To avoid this, create a small transparent PNG in your Assets and set the RawImage to that texture.
Remember that the RawImage size is completely independent of the Skia canvas size. In this example, we render a 256x256 pixel canvas and then drop it into the RawImage, which is scaled to a percentage of your window size. (Because of the "Scale With Screen Size" canvas setting.) Choosing a canvas size is outside the scope of this recipe; I don't have any advice for you.
I hope this is helpful! And I hope it stays helpful for more than a few months! (I know how it is to find five-year-old programming advice on a blog somewhere which no longer works...)
Again, please comment or contact me if you have anything to add.
Old bad version
Don't follow the instructions in this last section. I am preserving my notes from my original attempt at making this work. I made it work, but it required hand-tweaking of your game packages after building. (On both Mac and Windows.)
Before Marshall Quander explained how to build the right managed library, I was building two
SkiaSharp.dll files -- one for Mac, one for Windows. One of them linked to the native
libSkiaSharp.dll library, one to the native
libSkiaSharp.dylib library. In theory, a managed library is portable and you'd use the same one on every platform, but in practice I couldn't make that work.
Or rather, I thought of a way to make it work, but it would cost performance on every graphics call, and why bother? We're already going to have to install a different native library on every build. Might as well install a different managed library too.
To make life even worse, I couldn't even include both
SkiaSharp.dll files in the same project. They conflicted at build time. Yes, Unity has import settings which are supposed to let you include separate Mac and Windows libraries such that right one loads on each platform. Guess what? They don’t work. You have to install just one set in your project and stick to editing on the same platform all the time.
In the course of getting that to work, I wound up putting both the Mac and Windows native libraries into nonstandard locations. The Mac library went into
SkiaDemo.app/Contents/Frameworks/MonoEmbedRuntime/osx; the Win library went into
SkiaDemo_Data/Mono. Why these locations? (Neither appears in a normal Unity-build app at all.) Basically, these were the first locations I found that worked. I was so worn out by the experimentation process that I just put a pin in that solution and moved on.
Of course, Unity wasn't putting the files in my special locations. (In fact Unity wasn't putting the Mac library anywhere, since it was a
.dylib and Unity doesn't recognize those.) So I had to manually copy the files into place. I wrote a Python script to help with this, not that Python is particularly comfortable for most Unity developers.
Then I came up with a nice hack based on PostProcessBuildAttribute, which made the file-copying happen magically inside the Unity build process.
I was pretty pleased with the PostProcessBuildAttribute trick, in fact. But it's not necessary now that I've got everything else sorted out. Oh well. Maybe I'll use it for something else someday.