Over the past few days, I’ve been working on an asset builder to build my Blender models. Here’s the what, why, and how.
What is an Asset Builder?
An asset builder automatically processes content authored by artists, designers, etc. into data that can be readily used by a game engine. In the case below, I am specifically talking about 3D models, textures, and animations authored in Blender that are converted into a data format that is usable in Unity3D.
Why Do You Need an Asset Builder for Unity?
Generally, for Unity, you actually don’t need an asset builder. When you place an asset like a blend file or fbx file in the Assets folder in your Unity project, the file gets automatically processed into models, skeletons/armatures/avatars, and animation clips, ready to be used in your game. I really didn’t want to spend time on writing build tools. I was hoping Unity’s processors would be good enough. For my purposes though, I had a few problems getting my models from Blender to Unity.
- Placing a .blend file directly into Unity doesn’t give you full control over how the models/skeleton/animations are processed. It’s possible that the results may not be fully optimized.
- As a result of #1, manually exporting from Blender to Unity, although straightforward, is time consuming and repetitive. These qualities suggest that this task can be easily scripted and automated for efficiency. The manual process usually goes like this:
- Open the Export to FBX Menu in Blender.
- Select FBX export settings.
- Navigate to the destination Unity Asset directory and name the file. (I’m using Blender’s Rigify addon, so the RigifyToUnity asset requires that the file name have an identifier like “rigify” in it.)
- Click on Export.
- Repeat 10-100 times a day.
- This last point is a result of a personal preference that I have in regards to source control. I prefer to keep my .blend file separate from the exported FBX file that resides in the Unity Asset directory. The reasons for this are as follows:
- FBX files are generally smaller than .blend files, due to some optimization options that the FBX format provides, like exporting only deformed bones. Enabling some of these options can significantly reduce the size of the data necessary for Unity.
- I keep my source assets (3d model .blend files, textures (PSDs, XCFs, SVGs, PDNs, etc.)) in an SVN repo, and my code in a Perforce depot. I do this because my Perforce depot is getting quite huge already, and adding source assets to the depot will just make it even larger. That said, I don’t want to keep large .blend files in my code depot.
As a side effect of this structure, I now have two files representative of the same asset; one in my working art directory and the other in my Unity asset directory. However, I plan on only keeping the source .blend file in source control, and the exported FBX file is considered part of my asset build, so it doesn’t have to be stored in source control. However, if later down the road, I absolutely need to keep the FBX files in source control, they will at least be the fraction of the size of the .blend files in my Perforce depot.
I admit this can sound confusing to those not familiar with all the systems that I mentioned. Luckily, item 3 above does not apply to everyone, and hopefully can just be ignored by a majority of those developers who have a slicker source control setup.
How Do You Implement an Asset Builder?
The general answer is, “It depends. Every project’s requirements are different, so the build system has to be custom built.” With that said, the best I can do is describe how I implemented my asset builder.
Python and batch files. That’s the quick answer.
Python is great. And especially with Blender’s support of Python, it makes the asset build solution even more feasible. (I know that Maya and 3ds max also support Python, but I’m not sure to what extent, though I wouldn’t doubt that it’s possible to use Python for an asset build solution in those 3d packages as well.)
Requirements
The first step, of course, is to define your requirements. I’m on a Win7 PC.
I want to:
- Automate the conversion of a <name>.blend file from my working art directory to a <name>_rigify.fbx file in the Unity Asset directory, while preserving the subtree that the .blend file resides in (i.e.
<art>/<game>/models/hero/hero.blend -> <Unity project>/Assets/models/hero/hero_rigify.fbx
).
- Be able to click on a batch file to launch the autobuild step in #1.
- Be able to press a shortcut in Blender to launch the autobuild step in #1.
- Be able to click on a batch file to run an autobuild on the entire models directory, which simply calls the autobuild step in #1 for each model in that directory.
Implementation
As you may guess, item 1 above is the bulk of the work. Items 2, 3, and 4 are just hooks into the work done in item 1.
Since there are so many facets to setting up build scripts, I’m just going to attack this explanation as best as I can. It’ll be all over the place, but I’ll try to be as complete as possible.
Command Line Scripts
Add a directory to your path that contains all your art tool scripts. For example, d:/art/_tools/
. This way, your batch files can be accessible from anywhere in the system. It’s also probably a good idea to add an environment variable for your tools path, like ART_TOOLS_PATH = d:/art/_tools/
.
The main key to getting this to all work is Blender’s command line mode:
blender.exe mymodel.blend -b --python pythonScript.py -- param1 param2 ...
This allows you to run Blender in background mode (-b) and execute a Python script that Blender can read, passing in any number of parameters to the script that you may require.
You can just wrap this up in a batch file (i.e. exportfbx.bat
) that contains something like this:
blender.exe %1 -b --python %ART_TOOLS_PATH%/myBuildScript.py -- %2
where %1 is the name of the .blend file passed to the batch file, and %2 can be the destination path.
This way, you always have the option to write a batch file for an individual model, call it say, build.bat
that sits alongside your .blend file in say, <art>/<game>/models/hero/hero.blend
, that does something like this:
exportfbx.bat hero.blend d:/myUnityGame/Assets/
And you may notice that this batch file satisfies our requirement #2! Yay!
But if you’ve also noticed, we have to step back to requirement #1. We need to implement something in myBuildScript.py
for Blender to actually execute. My script basically looks like this:
import bpy
import sys
def main():
argv = sys.argv
argv = argv[argv.index('--') + 1:] # get all args after '--'
if (len(argv) < 1):
print('ERROR: Target game asset path not specified.')
return
bpy.ops.wm.exportfbx('EXEC_DEFAULT', targetGameAssetPath=argv[0], copyTextures=True)
if __name__ == "__main__":
main()
As you can see, it looks like a basic Python script. The key thing to note is that we are importing the bpy module for Blender to use, and the key line is:
bpy.ops.wm.exportfbx('EXEC_DEFAULT', targetGameAssetPath=argv[0])
where:
wm.exportfbx
is the name of our Blender operator.
targetGameAssetPath
is the asset path passed in as a parameter.
So, wm.exportfbx
needs some explanation, and for that, I have to switch to “Blender talk”.
The Blender Operator
Blender’s plugin system is basically in the form of Python scripts that get stored in an addons
directory. This is usually stored in Blender’s appdata folder (on Win7, it’s in Users/<user>/AppData/Roaming/Blender Foundation/Blender/<ver>/scripts/addons/
) or in a script folder that you can specify in Blender.
A very basic operator script that does nothing looks like:
import bpy
class MyExportFBXOperator(bpy.types.Operator):
bl_idname = 'wm.exportfbx'
bl_label = 'My Export FBX'
targetGameAssetPath = bpy.props.StringProperty()
def execute(self, context):
# Do something here
return {'FINISHED'}
def register():
bpy.utils.register_class(MyExportFBXOperator)
def unregister():
bpy.utils.unregister_class(MyExportFBXOperator)
if __name__ == "__main__":
register()
It’s some boilerplate code for registering the operator with Blender. But here are the things to note:
bl_idname = 'wm.exportfbx'
This is the name of the command that is used by the call in myBuildScript.py
above, so this must match.
targetGameAssetPath
is a string property that gets initialized from the passed parameter in myBuildScript.py
.
- The
execute()
function is where the work is actually done.
So, at this point, we have access to the Blender file proper. We can access the scenes, objects, bones, textures, etc. I’m not going into the implementation of the execute()
function, but ultimately, what you want to call is something like the following:
bpy.ops.export_scene.fbx(filepath=outFile, axis_forward='-Z', axis_up='Y', object_types={'ARMATURE','MESH'}, use_armature_deform_only=True)
There are many options to bpy.ops.export_scene.fbx
, so it will depend on what you need for your project. outFile
is simply the remapped filePath to the target Unity Asset directory. I like to export only the ‘MESH’ and ‘ARMATURE’ types, because that’s all you really need for character animations.
Well, this satisfies requirement #1!
Exporting Textures
But I do need to mention something else, which is a bonus of sorts. bpy.ops.export_scene.fbx
will only export the model, armature, and animations, but you’ll most likely want to also export the textures associated with the file.
I found a great post on the Blender Stack Exchange that describes how you can get the list of linked textures. If that link ever goes down, here is the key piece of code that I used:
ob = bpy.context.object
if ob:
for mat_slot in ob.material_slots:
for mtex_slot in mat_slot.material.texture_slots:
if mtex_slot:
if hasattr(mtex_slot.texture , 'image'):
print("\t\t%s" % mtex_slot.texture.image.filepath)
With that, you can simply copy the textures from your source art directory to the target Unity Asset directory as necessary.
Blender Shortcut
For requirement #3, the MyExportFBXOperator
script above is almost set up for usage in the Blender UI. The only thing missing is the following header:
bl_info = {
"name": "My Export FBX",
"category": "Import-Export"
}
The “name” field will appear in the Blender UI, and it will be in the “Import-Export” category. In Blender, you can open up User Preferences > Add-ons > Categories:User, and you’ll see your operator script listed. Just check the box to enable it. Once it’s enabled, you can go to User Preferences > Input, and add wm.exportfbx
to any shortcut that you want! The details on this are beyond the scope of this post, but there are plenty of articles and youtube videos describing how to do this.
Full AutoBuild
Lastly, requirement #4 is just another script that walks your models directory for each .blend file that you want to export, and calls exportfbx.bat
. Truthfully, I haven’t implemented this part yet, but if anyone complains =), I may just write up another post on it.
EDIT: [2016.06.30] I’ve had a request for the script file that processes all models in a directory tree, and since I actually have it done, here it is!
Download
# build_all.py
# Copyright (c) 2016 Under the Weather, LLC
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software
# and associated documentation files (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all copies
# or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
# FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import sys
import os
import subprocess
def main():
argv = sys.argv
argv = argv[argv.index('--') + 1:] # get all args after '--'
if (len(argv) < 2):
print('ERROR: Source and Target game asset path not specified.')
return
sourceGamePath = argv[0]
targetGamePath = argv[1]
optionStr = ''
options = argv[2:]
for option in options:
optionStr += option + ' '
print('============================================================')
print('BUILD ALL')
print('SOURCE: ' + sourceGamePath)
print('TARGET: ' + targetGamePath)
print('OPTIONS: ' + optionStr)
print('============================================================')
no_build_token = '_' # If the .blend filename has this token (or even string), they will be excluded from the build
for root, dirs, files in os.walk(sourceGamePath):
for name in files:
ext = os.path.splitext(name)
if ext[1].lower() == '.blend':
path = os.path.split(name)
fname = path[1]
if fname.find(no_build_token) == -1:
absPath = os.path.join(root, name)
print('Processing Blender file: ' + absPath)
command = 'exportfbx.bat ' + absPath + ' ' + targetGamePath + ' ' + optionStr
print('command: ' + command)
subprocess.call(command, shell=True)
if __name__ == "__main__":
main()
And now for a bit of explanation. The script accepts no less than 2 arguments; the source and the target paths. The script also supports a string of additional options/parameters, if necessary for whatever reason you may have. You'll also notice the no_build_token
, which is essentially a character or string used to identify any blender files that are not intended to be exported. And that's it!
Conclusion
I seriously didn't think I was going to type that much. It was pretty frustrating for me to find a lot of fragmented information on the Blender and Unity pipeline, so I decided to put as much as I know all together here. There are definitely aspects of this system that need attention, such as custom material exports from Blender, and multiple armature and animation exports, but I think a lot of what I covered in this post can get some people started with more efficient game development.
Make it fun!
Info:
- Unity 5.0.1f1
- Blender 2.74
- Python 2.6.4