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
SkiaSharp.dll
. - The native library (compiled C++ code). This is called
libSkiaSharp.dll
on Windows,libSkiaSharp.bundle
on Mac, and I guess it would belibSkiaSharp.so
on Linux.
I have handily collected all the files you need here. Download
SkiaComponents.zip
and unpack it.
You now have these files:
managed/SkiaSharp.dll
native/mac/libSkiaSharp.bundle
native/win-x86/libSkiaSharp.dll
native/win-x64/libSkiaSharp.dll
RawImageDraw.cs
README.md
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-x64
and 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.
The
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.The recipe
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
SkiaDemo
. -
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
Assets
folder. Select Assets / Create / Folder to create a new folder inside Assets; name itSkiaSharp
. Then create folders calledx64
andx86
insideAssets/SkiaSharp
-
Copy
managed/SkiaSharp.dll
to yourAssets/SkiaSharp
folder. -
Copy
native/win-x64/libSkiaSharp.dll
to yourAssets/SkiaSharp/x64
folder. Select it in the bottom pane. In the import inspector, select the Platform Settings / Editor (Unity) pane, and set CPU tox86_64
. Then select the Platform Settings / Standalone (⬇) pane, and uncheckx86
. Then hit the Apply button. -
Copy
native/win-x86/libSkiaSharp.dll
to yourAssets/SkiaSharp/x86
folder. Select it in the bottom pane. In the import inspector, select the Platform Settings / Editor (Unity) pane, and set CPU tox86
. Then select the Platform Settings / Standalone (⬇) pane, and uncheckx86_64
. Then hit the Apply button. -
Copy
native/mac/libSkiaSharp.bundle
to yourAssets/SkiaSharp
folder. 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.
-
Copy
RawImageDraw.cs
to yourAssets
folder. -
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:
The
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
andx86_64
should 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.
Deallocating textures
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
Destroy(texture)
.
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. -
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.
Followup: I have been pointed at https://docs.unity3d.com/Manual/BuildPlayerPipeline.html , which should let me automate the build postprocessing. Will investigate.
ReplyDeleteMarshall Quander has explained how to make the managed library (SkiaSharp.dll) truly cross-platform, so you don't have to fuss with different versions of that. In short: compile it with *no* file suffix on the DllImport line, and it will figure out the right suffix for your platform!
ReplyDelete"That was easy."
With this change, the Windows native library loads correctly from where Unity puts it, so there's no need to post-install that. (Or maybe that always worked right, and I just didn't test enough...)
So now the only post-processing needed is to install the Mac native library. I'm still looking to see if BuildPlayerPipeline can handle that.
I will update the post this weekend to explain the simpler procedure.
Well. Have you ever had a problem with xinput1_3.dll https://fix4dll.com/xinput1_3_dll download when installing programs?
ReplyDeleteAnd how did you solve it, if not secret? Cuz last my workday "dll not found" error prevented me completing my project, and I slightly miss the deadline. After that, I had to work a little overtime and yet download xinput1_3.dll, after I copied it to the appropriate place using the instruction, so I had fixed dll errors such way.
I'm afraid that problem is not related to the problems I describe in this post.
DeleteHave you had success running SkiaSharp in Unity on iOS ?
ReplyDeleteI haven't tried it. On iOS, I use native Cocoa code rather than Unity.
DeleteThere is no precompiled Skia binary for iOS, so you'd have to compile it yourself. And it might be too slow to be practical.
Hey Andrew. This is great. I followed your guidelines and everything is working fine. I do have and issue when trying to render the shapes in Update(). If I put any piece of SK code in there (like drawing a circle) unity just shut down after I press the play button. Any ideas of how to draw the shapes on Update() or to perform changes dinamically?
ReplyDeleteHi! It's been a while since I looked at this code, but I didn't do anything different on Update(). I made a Redraw() function that did all the same work and called it from Start(), Update(), OnPointerClick(), or wherever.
DeleteHey Andrew, this is great. I set up everything as you said and its working fine. I do have and issue when drawing shapes on Update() to change the parameters in runtime (like the size or the color). Any idea of how to do this?
ReplyDeleteThread on Win10 DLLs: https://forum.unity.com/threads/hololens-native-plugin-fails-to-load.662581/
ReplyDeleteI have not experimented in this area further, but the info may be helpful.
Hi, did you could be able to use Skiasharp with android?
ReplyDeleteI have no information on Android. There's an Android build of it, so I'm sure it's possible.
DeleteHello, could you please help
ReplyDeleteWhich file and how should I change to build a universal SkiaSharp.dll?
(I have little experience in this)
I'm afraid I haven't looked at this in several years.
Delete