Tutorial — Maya Python Batch Export and Naming Checks

Augmented Traveler local tutorial. Back to Maya — Tutorials · presets: Tutorial — FBX Export Presets for Unreal · ZBrush equivalent: Tutorial — ZBrush Python Export Presets

A small Python tool that checks a Maya scene against our rules and batch-exports every top-level asset group to FBX with the studio preset. Use it once the second figure starts and the number of assets makes hand-exporting error-prone.

Draft. Test on the first prop batch. The DevOps engineer owns the script in source control (Position — DevOps Engineer).

1. What it checks

Check Rule
Names Top-level groups match AT_<Name>_<Asset> and use no spaces
Units Scene is in centimetres
History No construction history on meshes
Transforms Meshes have frozen transforms (translate/rotate 0, scale 1)
LODs If any _LOD child exists, _LOD0 exists

2. The script

Save it as Pipeline/Maya/scripts/at_export.py. Add Pipeline/Maya/scripts to PYTHONPATH in each seat's Maya.env.

"""Augmented Traveler: check and batch-export top-level asset groups to FBX."""
import os
import re
import maya.cmds as cmds
import maya.mel as mel

NAME_RE = re.compile(r"^AT_[A-Za-z0-9]+_[A-Za-z0-9_]+$")

def _top_groups():
    tops = cmds.ls(assemblies=True, long=False) or []
    cams = {"persp", "top", "front", "side"}
    return [t for t in tops if t not in cams]

def check_scene():
    problems = []
    if cmds.currentUnit(query=True, linear=True) != "cm":
        problems.append("Scene units are not centimetres")
    for grp in _top_groups():
        if not NAME_RE.match(grp):
            problems.append(f"Bad name: {grp}")
        meshes = cmds.listRelatives(grp, allDescendents=True, type="mesh", fullPath=True) or []
        for shape in meshes:
            xf = cmds.listRelatives(shape, parent=True, fullPath=True)[0]
            hist = [h for h in (cmds.listHistory(xf, pruneDagObjects=True) or [])
                    if cmds.nodeType(h) not in ("skinCluster", "tweak", "groupId", "groupParts", "objectSet")]
            if hist:
                problems.append(f"History on {xf}: {hist[:3]}")
            if not cmds.listConnections(shape, type="skinCluster"):
                t = cmds.xform(xf, q=True, t=True, os=True)
                r = cmds.xform(xf, q=True, ro=True, os=True)
                s = cmds.xform(xf, q=True, s=True, r=True)
                if any(abs(v) > 1e-4 for v in t + r) or any(abs(v - 1) > 1e-4 for v in s):
                    problems.append(f"Unfrozen transform: {xf}")
        children = cmds.listRelatives(grp, children=True) or []
        if any("_LOD" in c for c in children) and not any(c.endswith("_LOD0") for c in children):
            problems.append(f"{grp} has LODs but no _LOD0")
    return problems

def export_all(out_dir, preset_path):
    problems = check_scene()
    if problems:
        for p in problems:
            print("CHECK FAILED:", p)
        cmds.warning(f"{len(problems)} problem(s); nothing exported. See Script Editor.")
        return []
    os.makedirs(out_dir, exist_ok=True)
    mel.eval(f'FBXLoadExportPresetFile -f "{preset_path.replace(os.sep, "/")}"')
    written = []
    for grp in _top_groups():
        path = os.path.join(out_dir, f"{grp}.fbx").replace(os.sep, "/")
        cmds.select(grp, replace=True)
        mel.eval(f'FBXExport -f "{path}" -s')
        written.append(path)
        print("Exported", path)
    cmds.select(clear=True)
    return written

3. Use it

import at_export
at_export.check_scene()          # prints problems; fix them first
at_export.export_all(
    r"D:/AT/Export/Props",
    r"D:/AT/Pipeline/Maya/FBXPresets/AT_StaticMesh.fbxexportpreset")

Optional shelf button: drag the two lines above onto a shelf with the paths for that seat.

4. Extending it

5. Check before rolling it out

Sources