A Custom glTF Importer for Unity

I've never really liked Unity's handling of Blender (.blend) files, so after doing a bit of research, I decided to write a custom importer for glTF files. It actually turned out to be less work than I thought it would be, and being able to extend the importer with additional custom properties has been very useful!

What's Wrong with FBX?

Well for starters, the FBX (Filmbox) file format is proprietary (owned by Autodesk) and generally requires the use of Autodesk's C++ SDK to read and write FBX files. Because of the licenses involved with the SDK, the Blender Foundation was forced to basically reverse engineer the file format in order to provide an importer and exporter for Blender. Sadly, this resulted in the FBX exporter having a few annoying bugs that I've had to fix in the past.

Annoyingly, Unity uses Blender's FBX exporter to "import" .blend files into their Editor. It does this by running Blender with a Python script that calls the FBX exporter (found in Unity\Editor\Data\Tools\Unity-BlenderToFBX.py) for each .blend file in the project. One of the issues with this is that the script uses the ASCII (text) version of the .FBX file format instead of the binary version which has had more support in Blender. This conversion process also does a fairly terrible job of handling the coordinate system differences between Blender and Unity which you may have noticed before (models likely have a non-zero default rotation applied).

I worked around this for a while by using a modified FBX exporter script for Blender as well as a custom version of Unity's Unity-BlenderToFBX.py file and even wrote a simple C# script to handle updating the files each time Unity or Blender was updated.

One other issue that I have with FBX is that it's intended to be a full interchange format between modelling programs. This means that the data is stored in a way that might be friendly to a modelling program, but it requires additional processing to make it optimal for a game engine to use resulting in slower imports.

Why glTF?

Well, here's the short description of the format from the glTF website:

glTF™ (GL Transmission Format) is a royalty-free specification for the efficient transmission and loading of 3D scenes and models by applications. glTF minimizes both the size of 3D assets, and the runtime processing needed to unpack and use those assets. glTF defines an extensible, common publishing format for 3D content tools and services that streamlines authoring workflows and enables interoperable use of content across the industry.

So, it's a royalty-free specification developed by the Khronos Group with a primary goal being to quickly present 3D scenes and models. The data stored in glTF files can be quickly read and converted to most game or rendering engines with minimal processing. It's also easy to extend with custom properties which I'll cover that in just a bit.

All the details about the glTF file format can be found in their GitHub repository including links to existing importers and exporters as well as sample files. And the good news is that Blender already includes a glTF exporter capable of exporting meshes (rigid and skinned), PBR materials, scene hierarchies, and animations.

A (Very) Brief glTF Overview

The actual glTF specification is the likely the best way to learn about the file format, so I'll be quite brief here. There are variations of the format represented by two file extensions: .gltf and .glb.

.gltf files are actually just JSON files that describe one or more scenes including node hierarchies, materials, cameras, meshes, animations, etc. Large data arrays for these objects are stored in either a separate .bin file or embedded directly into the JSON using base64 encoding. Textures may be stored as individual files (as .png or .jpg) or they may also be stored inside the JSON file using base64 encoding.

.glb files (at least those exported by Blender) combine the JSON data, buffer data, and images into a single binary file. While this might be helpful for uploading to SketchFab or possibly for runtime mods, the inability to edit textures makes this variation less than ideal for game development. Since the JSON data is the same (other than being embedded), it's still pretty easy to support this format if desired.

The JSON data is fairly straightforward to understand just by looking through the spec and a few examples. The Properties Reference section of the spec describes each of the object types and their required fields and default values. The only thing that might seem a bit odd at first is the use of Accessors and BufferViews that simply specify the datatype and starting offset into the binary data for things such as vertices and animation keys.

One important thing to note about glTF files is that the coordinate system is right-handed, Y-Up (vs. Unity's left-handed Y-Up coordinate system). glTF models should face down the +Z axis as the specification states that model viewers should orient their default camera pointing down the -Z axis. Note that in Blender models should face down the -Y axis in order to export to glTF correctly.

Why Another glTF Importer for Unity?

The Khronos Group maintains their own importer for Unity called UnityGLTF. It's a great resource, but there are a few things that I wanted to improve on:

  • UnityGLTF has a dependency on the Newtonsoft.Json package which I'd definitely like to avoid. It's actually quite easy to make a set of classes that mimic the glTF JSON schema and use Unity's built-in JsonUtility.FromJson or JsonUtility.FromJsonOverwrite methods to handle deserialization of all JSON data and avoid the extra dependency.
  • UnityGLTF includes an exporter as well. This might be useful for some people, but it's not something I need, so I'd like to avoid including it.
  • In a similar vein, UnityGLTF has to be everything to everyone (or at least that's how everyone seems to view it). There are several feature requests for things that I know I'll never need and this just leads to code bloat and the potential for additional bugs.
  • It appears that UnityGLTF currently only supports importing materials into Unity's standard (non-scriptable) rendering pipeline. Since I'll be using either the Universal or HD pipeline, I would have to modify their importer. With a custom importer, I can import materials exactly as needed including how existing materials are handled.
  • UnityGLTF still has several optimizations (both performance and memory) that could be made. I'd like importing to be as fast as possible and require as few GC allocations as possible, so being in control of that is important to me. I'm perfectly fine with using a few unsafe blocks and memory-mapping binary data files to eke out as much performance as possible.

Because it is a custom importer, it's also easy to export additional properties from Blender. These properties can then be used to automatically add colliders and NavMesh settings to imported models without having to remember to add them manually.

So to sum up, I'm not a huge fan of third party libraries having had many headaches with them in the past. For something as critical as models and animations, I'm okay with writing an importer especially when it should save some work and time in the future.

Implementation Details

Once I have time to test the importer more thoroughly, I'll create a repository for it on GitHub for anyone wishing to customize it to their needs. For now, I'll briefly cover a few of the important details. Feel free to leave a comment if you are interested in additional details.

The glTF Object Model

To start with, I created several classes that map directly to the objects in the glTF specification. These classes are pretty minimal with fields whose names match the properties in the spec. The most important thing to remember is that each class needs to have the [Serializable] attribute for Unity's JSON importer to work properly. Here's an example of the Node class that I use (with comments removed).

[Serializable]
public class Node
{
public int camera = -1;
public int[] children;
public int skin = -1;
public float[] matrix;
public int mesh = -1;
public float[] rotation;
public float[] scale;
public float[] translation;
public float[] weights;
public string name;
}

I use default values of -1 for all fields that represent indices and since the spec defines the rotation, scale, and translation values as arrays in JSON, I do the same. Note that rotation values are always quaternions.

Loading a .gltf JSON file and memory mapping its binary data (.bin file) is then a simple matter of doing:

public static Gltf LoadGltf( string filename )
{
// Load the JSON .gltf file.
var json = File.ReadAllText( filename, Encoding.UTF8 );
var glTF = new Gltf { _filename = filename };
JsonUtility.FromJsonOverwrite( json, glTF );

// Memory map the associated .bin file.
// Note: .bin files may not exist if binary data was not needed.
var binFilename = Path.ChangeExtension( filename, ".bin" );
if( File.Exists( binFilename ) )
glTF.MemoryMapBinaryData( binFilename );
return glTF;
}

For anyone unfamiliar with memory mapped files, be sure to check out the MemoryMappedFile documentation on MSDN. The MemoryMapBinaryData method shown below opens the file for read-only memory-mapped access, creates a view accessor, and then stores a byte* that can be used to directly access the binary data:

void MemoryMapBinaryData( string filename )
{
_binFile = MemoryMappedFile.CreateFromFile( filename, FileMode.Open );
_binView = _binFile.CreateViewAccessor();
unsafe
{
_binaryData = null;
_binView.SafeMemoryMappedViewHandle.AcquirePointer( ref _binaryData );
}
}

Also note that it is very important that the Gltf class that wraps this access implement the IDispose interface and properly release the acquired pointer and dispose the view accessor and file. It's also very important to call Dispose or use a using statement for immediate cleanup of the Gltf instance where it's created (see next code snippet).

Importing glTF Objects into Unity

The first step here is to create a new class that derives from Unity's ScriptedImporter class and load the glTF data inside the overridden OnImportAsset method:

using UnityEditor.Experimental.AssetImporters;
...
[ScriptedImporter( 1, "gltf" )]
public partial class GltfImporter : ScriptedImporter
{
/// <summary>Called by Unity when an asset is imported.</summary>
public override void OnImportAsset( AssetImportContext context )
{
// Load the glTF file and import its contents.
using( var gltf = Gltf.Gltf.LoadGltf( context.assetPath ) )
Import( context, gltf );
}
...

Note: To avoid name collisions with Unity, I put all of the Gltf object model classes inside a Gltf namespace including the root Gltf class. It looks a little odd in the line above, but it ended up being the cleanest approach.

The Import method simply calls methods that loop over the arrays of meshes, materials, nodes, and animations from the glTF file and converts each object into its Unity counterpart.

void Import( AssetImportContext context, Gltf.Gltf gltf)
{
...
ImportMaterials();
ImportMeshes();
ImportNodes();
ImportAnimations();
...

Once the objects are converted, it then adds the root GameObject to the Unity asset and sets it as the "main" object. Meshes and animation clips are also added to the asset. Materials are created as separate asset files only if they are not already found in the same directory as the glTF file. If a material's name starts with a "'/", is is assumed to exist in the global materials directory and is not automatically created.

...
_context.AddObjectToAsset( "_main", _root );
_context.SetMainObject( _root );

foreach( var meshInfo in _meshInfos )
_context.AddObjectToAsset( meshInfo.mesh.name, meshInfo.mesh );

foreach( var animationClip in _animationClips )
_context.AddObjectToAsset( animationClip.name, animationClip );
...

Coordinate System Conversions

Converting the objects is fairly straight forward, but as previously mentioned, glTF uses a different coordinate system than Unity. Luckily this is a pretty simple transformation:

  • Positions and directions (including tangents) need to negate their x components.
  • Quaternion rotations need to negate both their x and w components.
  • Triangle vertex indices need to be reversed to preserve the proper winding when changing from a right-handed to a left-handed coordinate system.
  • Bind pose matrices used for skinning need to be pre and post multiplied by (-1, 1, 1). This can be done simply by negating the following values after copying the glTF matrix:
bindPose.m01 = -bindPose.m01;
bindPose.m02 = -bindPose.m02;
bindPose.m03 = -bindPose.m03;
bindPose.m10 = -bindPose.m10;
bindPose.m20 = -bindPose.m20;
bindPose.m30 = -bindPose.m30;

Custom Properties

Custom properties are quite easy to import from Blender. Simply create a nested, serializable Extras class inside the Node class and add additional properties to it. Then custom properties with the same name that are added to objects in Blender will be automatically imported:

[Serializable]
public class Node
{
...
[Serializable]
public class Extras
{
public bool myProperty;
}
public Extras extras;
...
}

In Blender, it's possible to add additional properties from the bottom of the Object Properties panel (the orange box icon) using the Custom Properties sub-panel. This can get a bit tedious and is prone to typos, so a nicer way to add custom properties in Blender is to extend a type (Object, Mesh, Action, etc.) with the desired properties (note that currently custom properties for actions do not export to glTF, but this should be fixed shortly):

# Add custom properties to Objects (glTF Nodes).
bpy.types.Object.myProperty = BoolProperty(name="My Property", default=True,
description="My property's description.")

A custom sub-panel can then be created to display the properties inside Blender's Object Properties panel:

# A custom UI Panel for exported Objects (glTF Nodes).
class ObjectExportedProperties(bpy.types.Panel):
bl_label = "Exported Properties"
bl_idname = "OBJECT_PT_exported_properties"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "object"
bl_order = 1000

@classmethod
def poll(self, context):
return (context.object is not None)

def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.row().prop(context.object, "myProperty")

Updating Git's .gitattributes

If you happen to be using Git for your version control, it's likely that you'll need to add an entry in your .gitattributes file for the glTF .bin files. Since the .gltf files are actually text (JSON), there shouldn't be a need to add an entry for them. You can check out our .gitattributes for Unity Projects file or simply add an entry to yours:

*.bin  filter=lfs diff=lfs merge=lfs -text

Future Improvements

I was originally hoping to simply replace Unity's built-in importer for .blend files, but unfortunately Unity does not allow this. It's not a huge issue, as I finally decided that having Unity launch Blender to do conversions has its drawback. Others on the project may not have Blender installed and having it load large sculpted models is a waste of time when only a low-poly version is needed for import.

Sadly, it's also not possible to disable the automatic import of .blend files which is quite annoying. In the next article, I'll discuss a simple work-around for this issue as I find it very convenient to have Blender files located inside the Assets/ folder where they can be easily loaded from Unity's Project window.