Ben Traje
← Back to 3ds-max

Cross-DCC Rigging: Exporting 3ds Max Skin Weights to mGear (.jSkin) with Python

09 Sep 26 (1d ago)

Transferring character deformation data across different Digital Content Creation (DCC) tools is a common bottleneck in multi-software animation pipelines. If your character models are rigged or weight-painted in Autodesk 3ds Max, but your primary animation and rigging framework runs in Autodesk Maya with mGear, moving skin weights cleanly without loss of precision is critical.

mGear stores its skinning data in a standardized JSON structure with the .jSkin extension.

Using pymxs, you can automate the extraction of 3ds Max skinOps data, validate vertex integrity to prevent silent mismatches, and format the output so it imports directly into Maya via mGear's skinning tools.

Here is an analysis of how to map 3ds Max weights to mGear schema, common vertex count validation traps, and the complete export script.

The Cross-DCC Indexing Problem

When porting skin data between 3ds Max and Maya, you must account for fundamental architecture differences:

  1. 1-Based vs. 0-Based Indices:
    • 3ds Max's skinOps API indexes vertices and bones starting at 1.
    • Maya and mGear's dictionary keys map vertices starting at 0.
    • Every vertex loop in Max must offset index keys using str(i - 1) to match Maya’s internal vertex table.
  2. The Command Panel Dependency:
    • Unlike Maya's independent node graph queries, 3ds Max’s skinOps functions (such as GetVertexWeightCount and GetVertexWeight) require the target object to be selected and the Modify Panel active with the Skin modifier focused. Querying skinOps without entering the Modify panel returns empty arrays or throws a runtime exception.

3ds Max (skinOps)                  mGear JSON Format (.jSkin)
Vertex ID: 1 ───────── (i - 1) ──► "weights": {
Bone: "Spine_01"                    "Spine_01": {
Weight: 0.85                          "0": 0.85
}
}

The Vertex Validation Gate

A common disaster when exporting weights occurs when downstream modifiers (like TurboSmooth, Edit Poly, or symmetry mirrors) sit on top of the modifier stack.

  • If you query rt.getNumVerts(obj.mesh), 3ds Max returns the vertex count of the final evaluated stack.
  • If a TurboSmooth sits above Skin, actual_verts will read 4x or 16x higher than skin_verts, causing false mismatches.
  • Conversely, if an Edit Poly collapsed or welded points below the Skin modifier, the Skin modifier will hold a corrupt internal table that crashes upon import into Maya.

The script guards against this by explicitly comparing the geometry's baseline vertex count against rt.skinOps.GetNumberVertices(skin_mod).

Complete Python Script (pymxs)

Run this script directly in the 3ds Max Python Script Editor. It loops through your specified mesh names, runs the validation checks, and writes out .jSkin files ready for Maya.

import json
import os
import pymxs

rt = pymxs.runtime

def export_mgear_jskin(obj_names, output_dir):
    """
    Exports 3ds Max Skin modifier weights into Maya mGear .jSkin format.
    
    :param obj_names: List of string names of objects in the scene.
    :param output_dir: Destination folder path for .jSkin JSON files.
    """
    if not os.path.exists(output_dir):
        os.makedirs(output_dir)

    for obj_name in obj_names:
        obj = rt.getNodeByName(obj_name)
        if not obj:
            print(f"[Warning] Object '{obj_name}' not found. Skipping.")
            continue

        # Locate Skin Modifier
        skin_mod = None
        for mod in obj.modifiers:
            if rt.classOf(mod) == rt.Skin:
                skin_mod = mod
                break

        if not skin_mod:
            print(f"[Warning] No Skin modifier found on '{obj_name}'. Skipping.")
            continue

        # Activate Modify Panel (Mandatory for skinOps evaluation)
        rt.select(obj)
        rt.setCommandPanelTaskMode(rt.Name("modify"))
        rt.modPanel.setCurrentObject(skin_mod)

        # --- VALIDATION GATE ---
        # Query total vertices registered in the Skin modifier
        skin_verts = rt.skinOps.GetNumberVertices(skin_mod)

        # Query base mesh vertex count (evaluating base object before downstream tessellation)
        base_obj = obj.baseObject
        try:
            actual_verts = rt.getNumVerts(base_obj.mesh)
        except Exception:
            actual_verts = rt.getNumVerts(obj)

        print(f"\n--- Validating '{obj_name}' ---")
        print(f"Base Geometry Vertices : {actual_verts}")
        print(f"Skin Modifier Vertices  : {skin_verts}")

        if actual_verts != skin_verts:
            print(f"[ERROR] Vertex count mismatch on '{obj_name}'!")
            print(f"Skin table ({skin_verts}) does not match base geometry ({actual_verts}).")
            print("Aborting export for this object to prevent corrupt weight data.\n")
            continue

        print(f"Validation passed. Serializing weights for mGear...")
        
        weights_data = {}

        # Loop through vertices (3ds Max uses 1-based indexing)
        for i in range(1, skin_verts + 1):
            num_bones = rt.skinOps.GetVertexWeightCount(skin_mod, i)

            for j in range(1, num_bones + 1):
                weight = rt.skinOps.GetVertexWeight(skin_mod, i, j)

                if weight > 0.0:
                    bone_id = rt.skinOps.GetVertexWeightBoneID(skin_mod, i, j)
                    bone_name = rt.skinOps.GetBoneName(skin_mod, bone_id, 0)

                    if bone_name not in weights_data:
                        weights_data[bone_name] = {}

                    # Offset to 0-based index string for Maya/mGear compatibility
                    vert_idx_str = str(i - 1)
                    weights_data[bone_name][vert_idx_str] = round(float(weight), 6)

        # Structure mGear jSkin schema
        obj_dic = {
            "blendWeights": {},
            "nameSpace": "",
            "normalizeWeights": 1,
            "objName": obj_name,
            "skinClsName": skin_mod.name,
            "skinDataFormat": "compressed",
            "skinningMethod": 0,
            "vertexCount": skin_verts,
            "weights": weights_data
        }

        mgear_json = {
            "bypassObj": [],
            "objDDic": [obj_dic],
            "objs": [obj_name]
        }

        output_filepath = os.path.join(output_dir, f"{obj_name}.jSkin")

        try:
            with open(output_filepath, 'w', encoding='utf-8') as f:
                json.dump(mgear_json, f, indent=4)
            print(f"[SUCCESS] Exported '{obj_name}' -> {output_filepath}\n")
        except IOError as err:
            print(f"[Error] Failed to write file for '{obj_name}': {err}\n")


# --- CONFIGURATION & RUN ---
target_objects = ["body", "Male_Body"]
export_directory = r"C:\temp\mgear_skin_exports"

export_mgear_jskin(target_objects, export_directory)

Importing into Maya via mGear

Once the .jSkin files are saved:

  1. Open your Maya scene containing the target character geometry.
  2. In the top menu, go to mGear > Skin & Weights > Import Skin.
  3. Select the exported .jSkin file.

Because joint names and 0-based vertex indices match the Maya mesh topology, mGear binds the joints, builds the skinCluster, and populates the weight arrays without requiring manual bone re-mapping.