Batching assets using headless Maya
Disclaimer: The code in this post is written on version 9.0.7700 of Simplygon. If you encounter this post at a later stage, some of the API calls might have changed. However, the core concepts should still remain valid.
Overview
Simplygon has a native plug-in within Maya that can handle exporting and importing of assets. This is not only useful when you are working within the Maya UI, but can also come in handy if you want to batch process many assets and use the built-in functionality in Maya. In this post we will show how you can set up a simple batch script that runs headless in Maya.
Getting started
Before we get to the coding, it's a good idea to add the Maya/bin folder to your Path variable in the environment. This will enable you to run mayapy.exe anywhere.
Now that we have got that out of the way we can start scripting. We're going to create a python file called maya_batch.py that we can start from the command prompt and provide input to.
Getting the Maya python environment up and running
The first thing we need to do is get the Maya environment up and running with the plug-ins we'll be needing. Here's the code for that:
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.
import sys, getopt, os
# We need to initialize the python environment in Maya
import maya.standalone
maya.standalone.initialize("Python")
import maya.cmds as cmds
# We'll need mel for the fbx exporting
import maya.mel as mel
# If we want to be able to load fbx files we need to load the fbx plugin
cmds.loadPlugin("fbxmaya")
# ... we also need to load the Simplygon plug
cmds.loadPlugin("SimplygonMaya2020")
With this we can use mel, cmd, fbx and Simplygon from the command prompt. This suffices for our example, but you might want to expand on that if you want to use other features from Maya.
The boring stuff
Since we want to make this a script that you can call with parameters from the command prompt we need to set up the main function properly. Here's that out of the way:
# Print how to use this code example
def print_instructions():
print('Usage: maya_batch.py -a <asset_file (fbx or mb/ma)> -p <pipeline file>')
def main(argv):
asset_file = ''
pipeline_file = ''
try:
opts, args = getopt.getopt(argv,"a:p:")
except getopt.GetoptError:
print("Could not extract parameters.")
print_instructions()
sys.exit(2)
for opt, arg in opts:
if opt in ("-a"):
asset_file = arg
elif opt in ("-p"):
pipeline_file = arg
if asset_file == '' or pipeline_file == '':
print("Missing argument.")
print_instructions()
sys.exit(2)
# Let's start the batching process
batch_asset(asset_file, pipeline_file)
if __name__== "__main__":
main(sys.argv[1:])
This a really basic implementation of parameter parsing, but will suffice for our current example. Now we will be able to call the script from the command prompt like this:
mayapy maya_batch.py -a myasset.fbx -p mypipeline.json
The interesting stuff will happen in batch_asset
Processing the asset
This is the function that will orchestrate the whole process. It will first import the asset, then start the processing and finally export the results into separate files. Here it is:
def batch_asset(asset_file, pipeline_file):
# Let's make it simple and just export the LODs with the same base name as the original
asset_path_without_ext = os.path.splitext(asset_file.replace('\\', '/'))[0]
# Load the asset file
load_asset(asset_file)
# Process the asset
lods = process_asset(pipeline_file)
# Export the LODs into separate files
export_output(lods, asset_path_without_ext)
In this example we will return the output files in the same directory with _LODx appended to the name, making it easy to handle the output. That's why we're splitting up the file path on the first line. Loading an asset will only be using Maya commands. Here's the function that handles that:
def load_asset(assetFile):
if "fbx" in assetFile:
cmds.file(assetFile, i=True, type="FBX")
else:
cmds.file(assetFile, i=True)
# Select the newly imported asset for processing
cmds.select(all=True)
Nothing fancy here. The only thing worth mentioning is that we're selecting everything after the asset has been loaded, as Simplygon will grab the selection when starting a process.
Processing an asset is also trivial, we will just be using the Simplygon command built into Maya.
def process_asset(pipeline_file):
resulting_objects = cmds.Simplygon(sf=pipeline_file)
return split_lods(resulting_objects)
The split_lods function splits up the returned objects into separate buckets, so that the lods can be exported into separate files. Here's that function:
def split_lods(resulting_objects):
lods = {}
for obj in resulting_objects:
index = int(obj[obj.rfind('_')+1:])
if not index in lods:
lods[index] = []
lods[index].append(obj)
return lods
When objects are returned into Maya from Simplygon _001,_002 etc will be appended to the name. We use that information to extract the lod index and put the object into the correct bucket.
Now that we have processed the objects, we just need to export the lods into separate files. Here's the code for that:
def export_output(lods, base_file_name):
for lod_index in lods:
output_file = base_file_name+"_LOD"+str(lod_index)+".fbx"
cmds.select(lods[lod_index])
mel.eval(('FBXExport -f \"{}\" -s').format(output_file))
This just loops through all the lod buckets and exports the parts in each bucket into a file.
That's the whole thing done. Let's look at it in practice.
Running the script
Now that we have the whole thing in place, let's run it. First we need an asset, we're going to be lazy and create two spheres like this:
We'll save the asset as spheres.fbx I bet you can be more creative when it comes to asset selection.
We also need a pipeline to process the objects with. We can go into Maya and create a 3 step LOD chain and export it like this:
Let's name the pipeline lod_chain.json.
Now we should have a directory with the following files:
- maya_batch.py
- spheres.fbx
- lod_chain.json
That's all we need to run the script. Open up a command prompt and change the directory to your example directory and write the following line in there:
mayapy maya_batch.py -a spheres.fbx -p lod_chain.json
If you're getting an error where mayapy isn't a recognized command, you most likely haven't set the path to the maya/bin folder correctly.
After the script has run, there should be three new files in the directory:
- spheres_LOD1.fbx
- spheres_LOD2.fbx
- spheres_LOD3.fbx
Each of these files is a 50% reduction of the previous step.
Possible improvements
This is a very simple implementation of batching in Maya, there are a million cool things you could expand this. Here are a few suggestions.
Loading the file into the Simplygon API
If you want, you can skip the pipeline exporting and instead export the asset from the Maya format using the Maya Simplygon export command and then load the file into the Python API to control the processing more in detail. You can then import the Simplygon scene back into using the import command.
Handling remeshings and aggregations
If you decide to have remeshings or aggregations in your LOD chain these objects will be named differently thatn the reduced objects, as they group all objects into one. In that case you will need to modify split lods to handle those kinds of objects.
Using weight painting
If you want to control the processing results in more detail, perhaps save more details in the face of the character, you can add that to the pipeline. Check this video for more information on how to do that.
Geometry culling
If you want to cull unecessary geometry you can check this post to see what options you have at your disposal in Simplygon.
The script
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.
import sys, getopt, os
# We need to initialize the python environment in Maya
import maya.standalone
maya.standalone.initialize("Python")
import maya.cmds as cmds
# We'll need mel for the fbx exporting
import maya.mel as mel
# If we want to be able to load fbx files we need to load the fbx plugin
cmds.loadPlugin("fbxmaya")
# ... we also need to load the Simplygon plug
cmds.loadPlugin("SimplygonMaya2020")
def load_asset(assetFile):
print("Importing "+assetFile)
if "fbx" in assetFile:
cmds.file(assetFile, i=True, type="FBX")
else:
cmds.file(assetFile, i=True)
# Select the newly imported asset for processing
cmds.select(all=True)
# Splits the resulting objects into separate list of lods based on the naming (_001, _002 etc)
def split_lods(resulting_objects):
lods = {}
for obj in resulting_objects:
# Crudely relies on naming, error handling would be a nice addition.
index = int(obj[obj.rfind('_')+1:])
if not index in lods:
lods[index] = []
lods[index].append(obj)
return lods
# Processes the currently selected geometry in the scene with the specified pipeline file.
def process_asset(pipeline_file):
print("Processing asset")
resulting_objects = cmds.Simplygon(sf=pipeline_file)
return split_lods(resulting_objects)
# Exports the lods into separate files
def export_output(lods, base_file_name):
for lod_index in lods:
output_file = base_file_name+"_LOD"+str(lod_index)+".fbx"
cmds.select(lods[lod_index])
print("Exporting lod to "+output_file)
mel.eval(('FBXExport -f \"{}\" -s').format(output_file))
# Import, processes and exports the asset with the settings specified in the pipeline file.
def batch_asset(asset_file, pipeline_file):
# Let's make it simple and just export the LODs with the same base name as the original
asset_path_without_ext = os.path.splitext(asset_file.replace('\\', '/'))[0]
# Load the asset file
load_asset(asset_file)
# Process the asset
lods = process_asset(pipeline_file)
# Export the LODs into separeate files
export_output(lods, asset_path_without_ext)
# Print how to use this code example
def print_instructions():
print('Usage: maya_batch.py -a <asset_file (fbx or mb/ma)> -p <pipeline file>')
def main(argv):
asset_file = ''
pipeline_file = ''
try:
opts, args = getopt.getopt(argv,"a:p:")
except getopt.GetoptError:
print("Could not extract parameters.")
print_instructions()
sys.exit(2)
for opt, arg in opts:
if opt in ("-a"):
asset_file = arg
elif opt in ("-p"):
pipeline_file = arg
if asset_file == '' or pipeline_file == '':
print("Missing argument.")
print_instructions()
sys.exit(2)
# Let's start the batching process
batch_asset(asset_file, pipeline_file)
if __name__== "__main__":
main(sys.argv[1:])