BLOG / MAYA API · TOOLDEV
Building a high-performance, zero-dependency Skin I/O tool: tackling API component types, sparse JSON data, and managing complex deformation stacks for rapid autorig iteration.

FIG 01 — gcSkinclusterIO: NATIVE SKIN WEIGHT EXPORT / IMPORT
While developing a custom autorig for my University Final Degree Project, I hit a major bottleneck: iteration speed. Every time the rig's skeleton updated or proportions changed, I had to manually transfer skin weights.
Relying on excellent external plugins like ngSkinTools or mGear wasn't an option for me. I wanted a native, zero-dependency pipeline tool that could instantly export and import complex deformation stacks across different rig versions without manual intervention — lightning-fast, highly optimized, and fully integrated into my own codebase. This led to the creation of my custom SkinIO manager.
When dealing with a dense character mesh of 50,000 vertices and 100 joints, you are iterating through 5 million potential weight values. Using standard maya.cmds.skinPercent to query or set these values evaluates them as strings through Maya's command engine, which creates an enormous bottleneck.
By switching to Maya's API 2.0 (maya.api.OpenMaya), the tool accesses the raw memory array of weights directly. Using MFnSkinCluster.getWeights(), querying millions of weights happens in a fraction of a second.
A robust autorig doesn't just skin polygons. It skins NURBS curves for facial ribbons, and NURBS surfaces for wing membranes or sliding setups. Generalizing the tool meant understanding how the API handles different geometric components.
Meshes and curves are straightforward — they use a single index (vertex 0, 1, 2…). I used MFnSingleIndexedComponent for kMeshVertComponent and kCurveCVComponent. NURBS surfaces, however, are a grid — double-indexed in U and V. To handle this gracefully, I dynamically calculate the total point count (numCVsInU * numCVsInV) and create an MFnDoubleIndexedComponent for kSurfaceCVComponent.
# Handling different API component types based on geometry def _get_geometry_components(self, dag_path): component = None count = 0 if dag_path.hasFn(om.MFn.kMesh): # Single indexed component for meshes fn_mesh = om.MFnMesh(dag_path) count = fn_mesh.numVertices fn_single = om.MFnSingleIndexedComponent() component = fn_single.create(om.MFn.kMeshVertComponent) fn_single.setCompleteData(count) elif dag_path.hasFn(om.MFn.kNurbsSurface): # Double indexed component for surfaces (U, V) fn_surf = om.MFnNurbsSurface(dag_path) num_u = fn_surf.numCVsInU num_v = fn_surf.numCVsInV count = num_u * num_v fn_double = om.MFnDoubleIndexedComponent() component = fn_double.create(om.MFn.kSurfaceCVComponent) fn_double.setCompleteData(num_u, num_v) return count, component
A standard character mesh is "sparse". If a skeleton has 200 joints, a single vertex on the pinky toe is only influenced by 1 or 2 of them. Storing 0.0 weights for the other 198 joints across thousands of vertices creates massive, slow-to-read files.
To fix this, my exporter applies a strict tolerance threshold (1e-5). As it slices the flattened API array using a stride based on the influence count, it aggressively filters out negligible weights. This sparse dictionary mapping reduces file sizes by up to 90% compared to raw dumps.
# Extracting sparse data using a stride and tolerance flat_weights = list(weights_marray) sparse_weights = {} stride = len(inf_names) for inf_idx, inf_name in enumerate(inf_names): # Slice the flat array for this specific influence inf_vals = flat_weights[inf_idx::stride] # Keep only indices and weights above the tolerance (1e-5) j_indices = [i for i, v in enumerate(inf_vals) if v > self.tolerance] j_weights = [round(inf_vals[i], 5) for i in j_indices] if j_indices: sparse_weights[inf_name] = {"ix": j_indices, "vw": j_weights}
A common debate is whether to serialize data as binary (fast, small) or ASCII (JSON). I specifically chose JSON for flexibility and debugging.
If topology slightly changes during production, a binary file might crash or corrupt the load. With an ASCII JSON file, I can easily write a secondary script to read the JSON, map the old vertex indices to the new topology using proximity algorithms, and inject the patched data safely. To offset the larger file size inherent to ASCII, I strip out unnecessary whitespace during the dump using separators=(',', ':').
In complex rigs, a single skinCluster is rarely enough. We stack base skins and corrective skins. If these layers are applied in the wrong order upon import, the rig breaks.
The tool iterates through the node history using cmds.listHistory to capture the exact stack order. During import, after all skinClusters are rebuilt, it actively reorders them using cmds.reorderDeformers to guarantee the original deformation logic remains intact.
By leveraging OpenMaya, we get the speed of C++ under the hood while maintaining the flexibility of Python. Building this custom tool gave my autorig the independence and iteration speed it desperately needed for my final degree project — proving that sometimes, building your own foundational pipeline tools is the most effective way forward.
Free to use and modify for your own pipelines.