Ben Traje
← Back to 3ds-max

Export 3ds Max Materials and Geometry Assignments to JSON using Python

07 Feb 26 (13d ago)

Nothing special. Just exports material and geometry assignments to JSON to check if the materials I recreated in Maya, Cinema4D and Blender uses the same textures as the original.

It saves the JSON right next to script.

There is a progress with OpenPBR Material but for the last 2 decades there is really no easy way to converting materials from one DCC to another even with the same renderer.

import pymxs
import json
import os
import sys

def get_clean_name(name_str):
    """
    Helper: Converts string to lowercase and replaces spaces with underscores.
    """
    if not name_str: return ""
    return name_str.lower().replace(" ", "_")

def get_assigned_objects(mat):
    """
    Returns a list of scene object names that have this material assigned.
    """
    rt = pymxs.runtime
    obj_list = []
    
    # refs.dependents gets everything referencing this material (objects, modifiers, etc.)
    # immediateOnly=False ensures we find objects even if there's a wrapper in between.
    deps = rt.refs.dependents(mat, immediateOnly=False)
    
    for d in deps:
        # 1. Check if it is a valid scene node (Object, Light, Camera, etc.)
        if rt.isValidNode(d):
            # 2. Check if it is Geometry (excludes Lights/Cameras/Helpers if you only want meshes)
            # SuperClassID 3 == GeometryClass
            if rt.superClassOf(d) == rt.GeometryClass:
                # 3. Verify the material is actually assigned to this object 
                # (avoids listing objects that just use the material in a modifier)
                if d.material == mat:
                    obj_list.append(d.name)
    
    # Remove duplicates just in case
    return list(set(obj_list))

def get_node_value(node):
    rt = pymxs.runtime
    
    # 1. MultiTile (Tile 1 only)
    if rt.isProperty(node, "filenames"):
        try:
            f_list = node.filenames
            if f_list and len(f_list) > 0:
                return f"UDIM Sequence (Tile1): {f_list[0]}"
        except:
            pass

    # 2. Bitmaps
    if rt.isProperty(node, "filename"):
        return f"Bitmap: {node.filename}"

    # 3. Colors
    elif rt.isProperty(node, "color"):
        c = node.color
        return f"Color: RGB({int(c.r)}, {int(c.g)}, {int(c.b)})"
    
    # 4. HDRI
    elif rt.isProperty(node, "HDRIMapName"):
        return f"HDRI: {node.HDRIMapName}"

    return None

def get_texture_data(map_node):
    rt = pymxs.runtime
    raw_name = map_node.name
    
    node_data = {
        "name": raw_name,
        "name_02": get_clean_name(raw_name),
        "type": str(rt.classOf(map_node)),
        "value": get_node_value(map_node),
        "connections": {}
    }

    try:
        num_sub_tex = rt.getNumSubTexmaps(map_node)
    except:
        num_sub_tex = 0

    if num_sub_tex > 0:
        is_multitile = "MultiTile" in str(rt.classOf(map_node))
        loop_range = range(1, 2) if is_multitile else range(1, num_sub_tex + 1)

        for i in loop_range:
            sub_tex = rt.getSubTexmap(map_node, i)
            if sub_tex is not None and sub_tex != rt.undefined:
                slot_name = rt.getSubTexmapSlotName(map_node, i)
                node_data["connections"][slot_name] = get_texture_data(sub_tex)

    return node_data

def get_material_data(mat):
    rt = pymxs.runtime
    raw_name = mat.name
    
    mat_data = {
        "name": raw_name,
        "name_02": get_clean_name(raw_name),
        "type": str(rt.classOf(mat)),
        "assigned_geometries": get_assigned_objects(mat), # <--- NEW FIELD
        "sub_materials": [],
        "maps": {}
    }

    # Recurse Sub-Materials
    try:
        num_subs = rt.getNumSubMtls(mat)
    except:
        num_subs = 0

    if num_subs > 0:
        for i in range(1, num_subs + 1):
            sub_mat = rt.getSubMtl(mat, i)
            if sub_mat:
                mat_data["sub_materials"].append(get_material_data(sub_mat))

    # Texture Recursion
    try:
        num_maps = rt.getNumSubTexmaps(mat)
    except:
        num_maps = 0

    if num_maps > 0:
        for i in range(1, num_maps + 1):
            tex_node = rt.getSubTexmap(mat, i)
            if tex_node is not None and tex_node != rt.undefined:
                slot_name = rt.getSubTexmapSlotName(mat, i)
                mat_data["maps"][slot_name] = get_texture_data(tex_node)

    return mat_data

def save_to_json():
    rt = pymxs.runtime
    scene_data = []

    print("--- GATHERING DATA WITH GEOMETRIES ---")
    
    for mat in rt.scenematerials:
        if mat:
            scene_data.append(get_material_data(mat))

    # --- SAVE TO FILE ---
    try:
        script_dir = os.path.dirname(os.path.realpath(__file__))
    except NameError:
        script_dir = "C:\\Temp"
        if not os.path.exists(script_dir):
            os.makedirs(script_dir)

    file_name = "scene_materials.json"
    full_path = os.path.join(script_dir, file_name)

    print(f"--- SAVING JSON TO: {full_path} ---")

    try:
        with open(full_path, 'w', encoding='utf-8') as f:
            json.dump(scene_data, f, ensure_ascii=False, indent=4)
        print("Successfully saved.")
    except Exception as e:
        print(f"Error saving file: {e}")

# Execute
save_to_json()