3ds Max Python How Tos

repository·master·Indexed 43 days ago

https://github.com/adn-devtech/3dsmax-python-howtos

A collection of Python 3 tutorials and practical examples for 3ds Max development. It focuses on building professional-grade tools using pip packaging and PySide/Qt, featuring utilities like menuhook for menu registration, mxthread for main-thread execution, and mxs2py for transpiling MAXScript to Python.

Tokens
21.2K
Snippets
77
Records
95
Agent score
54%

What's inside 3ds Max Python How Tos

  1. Overview of 3ds Max Python How Tos

    master

    This repository provides practical Python 3 development examples and tutorials specifically for 3ds Max.

    Key characteristics of the project:

    • Pip-based distribution: All tutorials are implemented as pip packages (including setup.py and LICENSE). This approach is used to make tools easily installable, shareable, and capable of managing external dependencies.
    • Pythonic implementation: While some samples are translations of official MAXScript How Tos, they are implemented using Python best practices, such as using PySide (Qt) for UI instead of traditional 3ds Max UI mechanisms.
  2. Execute code on the 3ds Max main thread with mxthread

    master

    The mxthread module allows Python worker threads to queue work to be executed by the 3ds Max main thread. This is essential because most pymxs operations and certain UI/print functions can only be safely called from the main thread.

    mxthread handles the complexity of passing functions to the main thread, waiting for execution to complete, retrieving return values, and propagating exceptions back to the caller thread.

    Key Features

    • Return Values: Functions executed on the main thread can return values directly to the caller thread.
    • Exception Propagation: If the code on the main thread raises an exception, it is caught and re-raised in the worker thread.
    • Thread Safety: Uses Qt mechanisms (Signals, Slots, and QWaitCondition) to bridge the threads.
    from mxthread import on_main_thread, run_on_main_thread
    
    @on_main_thread
    def my_task(val):
        return val * 2
    
    # Called from a worker thread
    result = my_task(10)
    print(result)  # Outputs 20
  3. Use mxs2py to transpile MAXScript to Python

    master

    mxs2py is an experimental transpiler that parses MAXScript programs into a syntax tree and generates equivalent Python code. It is designed to help Python developers understand and use MAXScript samples by providing automatic translation.

    Limitations and Warnings

    • Experimental Status: The project is in an experimental state.
    • Parsing Errors: Error reporting for invalid MAXScript syntax is poor; ensure your input code is valid MAXScript.
    • Unsupported Constructs: Some MAXScript constructs (e.g., plugin, tool, custom attributes) are parsed but may have nonexistent or limited Python support. When a construct cannot be translated, a warning is emitted within the generated Python code.
    • Code Quality: Generated Python code may be functionally correct but syntactically "awkward."
    • Dependencies: The generated code relies on mxsshim.py, which is an early and incomplete emulation layer for MAXScript constructs in Python.
    from mxs2py import topy
    
    (output, _) = topy("rotate $ (angleaxis 90 [1,0,0])")
    print(output)
  4. How zdepthchannel generates a voxel landscape

    master

    The zdepthchannel tool demonstrates how to access Z-buffer depth information from the 3ds Max scanline renderer to create 3D geometry.

    Workflow:

    1. Cleanup: Deletes existing objects named with the prefix VoxelBox to clear previous runs.
    2. Rendering: Renders the active viewport at a low resolution (e.g., 32x32) while requesting an additional Z-depth channel. The Virtual Frame Buffer (VFB) is disabled during this process.
    3. Mask Extraction: Converts the Z-depth channel into a grayscale mask bitmap.
    4. Geometry Generation: Iterates through every pixel of the rendered bitmap and its corresponding Z-depth mask. For each pixel, it creates a rt.box where:
      • The height is derived from the Z-depth value.
      • The wirecolor is set to the pixel's RGB color.
      • The position is mapped from bitmap coordinates to 3ds Max world space (mirroring the Y-axis to align the bitmap's top-left origin with the 3ds Max coordinate system).
    5. Progress Tracking: Uses rt.progressStart, rt.progressupdate, and rt.progressEnd to provide visual feedback in the status bar.
  5. The 3dsMax startup entry point mechanism

    master

    The pystartup mechanism uses pkg_resources (part of setuptools) to discover and execute startup functions from installed packages.

    Workflow:

    1. The MAXScript pystartup.ms executes a Python block via python.execute.
    2. The Python code imports pkg_resources.
    3. It iterates through pkg_resources.working_set to inspect all installed distributions.
    4. For each distribution, it searches for an entry point named 'startup' within the '3dsMax' group using pkg_resources.get_entry_info(dist, '3dsMax', 'startup').
    5. If found, the entry point is loaded via .load() and the resulting function is called.

    Requirements:

    • pip and setuptools must be installed in the 3ds Max Python environment to provide pkg_resources.
    # Conceptual logic used by pystartup
    import pkg_resources
    
    for dist in pkg_resources.working_set: 
        entrypt = pkg_resources.get_entry_info(dist, '3dsMax', 'startup')
        if entrypt is not None:
            fcn = entrypt.load()
            fcn()
  6. How quickpreview records animation frames

    master

    The quickpreview tool automates the process of recording a preview of an animation by following these steps:

    1. Setup Output: Defines an AVI output path within the 3ds Max Previews system directory using rt.getDir(rt.Name("preview")).
    2. Initialize Bitmap: Retrieves the current viewport size via rt.getViewSize() and creates a matching bitmap file.
    3. Frame Iteration: Loops through the rt.animationRange.
    4. Capture: For each frame, it sets the rt.sliderTime, captures the viewport's Device Independent Bitmap (DIB) using rt.gw.getViewportDib(), and copies that DIB into the main animation bitmap.
    5. Save & Cleanup: Saves each frame to disk, closes the bitmap file, and forces garbage collection with rt.gc() to clear memory.
    6. Playback: Launches the 3ds Max RAMPlayer to play the generated AVI file.
  7. How to create a PySide dialog for 3ds Max

    master

    To create a custom PySide dialog that is properly attached to the 3ds Max main window, subclass QDialog and set its parent using rt.windows.getMAXHWND(). This ensures the dialog behaves correctly within the 3ds Max UI environment.

    Key steps:

    1. Find the 3ds Max window handle using rt.windows.getMAXHWND().
    2. Pass this handle to the QDialog constructor as the parent.
    3. Use layouts (like QVBoxLayout) to organize widgets such as QLabel, QLineEdit, and QPushButton.
    class PyMaxDialog(QDialog):
        """
        Custom dialog attached to the 3ds Max main window
        """
        def __init__(self, click, parent=QWidget.find(rt.windows.getMAXHWND())):
            super(PyMaxDialog, self).__init__(parent)
            self.setWindowTitle('Rename')
    
            main_layout = QVBoxLayout()
            label = QLabel("Enter new base name")
            main_layout.addWidget(label)
    
            edit = QLineEdit()
            main_layout.addWidget(edit)
    
            btn = QPushButton("Rename selected objects")
            # Connect the button click to the provided callback function
            btn.clicked.connect(lambda : click(edit.text()))
            main_layout.addWidget(btn)
    
            self.setLayout(main_layout)
            self.resize(250, 100)
  8. How to hook a Python function to a 3ds Max menu item

    master

    To add a custom command to the 3ds Max interface, use the menuhook package. The menuhook.register function allows you to map a Python function to a specific menu path, providing a label and tooltip for the UI element.

    Key parameters for menuhook.register:

    • name: A unique identifier for the hook.
    • category: A category string.
    • callback: The Python function to execute.
    • menu: A list of strings representing the menu hierarchy (e.g., ["&Scripting", "Python3 Development"]).
    • text: The label displayed in the menu.
    • tooltip: The text shown when hovering over the menu item.
    import menuhook
    
    def my_function():
        print("Hello from the menu!")
    
    menuhook.register(
        "my_tool_id",
        "my_category",
        my_function,
        menu=["&Scripting", "My Custom Menu"],
        text="Run My Function",
        tooltip="Description of what this does"
    )
  9. Implement a background Worker thread with Qt Signals

    master

    To update a UI from a thread, define a class inheriting from QThread. Use a Signal to communicate progress back to the main thread.

    Key components of the Worker implementation:

    • progress = Signal(int): An integer signal used to emit the current progress value.
    • aborted = False: A boolean flag used to check if the thread should stop execution.
    • The run loop: Inside the loop, emit the signal using self.progress.emit(value) and check self.aborted to allow for graceful exits.
    class Worker(QThread):
        progress = Signal(int)
        aborted = False
        def __init__(self):
            QThread.__init__(self)
    
        def run(self):
            for i in range(MINRANGE, MAXRANGE):
                self.progress.emit(i)
                time.sleep(0.5)
                if self.aborted:
                    return
            self.progress.emit(MAXRANGE)
  10. How findChild prevents dialog duplication

    master

    The findChild method is used to traverse the object tree of a parent widget (in this case, the 3ds Max main window) to find a child of a specific type that matches a given object name.

    By assigning a unique identifier via setObjectName to your dialog class, you can query the MAIN_WINDOW to see if that specific object already exists in memory. This allows you to implement a 'singleton' UI pattern where clicking a tool button repeatedly either brings the existing window to the front or creates it if it's the first time, rather than spawning dozens of overlapping windows.

  11. Automate 2025 Menu Configuration via Callbacks

    master

    To ensure your custom menu items are correctly injected into the 3ds Max 2025 menu system whenever the UI regenerates, you should use pymxs.runtime.callbacks.

    1. Define a menu configuration function (e.g., menu_func) that retrieves the menu manager via rt.callbacks.notificationparam() and calls your registration logic.
    2. Use rt.callbacks.addscript with a unique ID to hook this function into the cuiRegisterMenus event. This ensures that whenever the menu system needs to rebuild, your items are re-added.
    from pymxs import runtime as rt
    from menuhook import register_howtos_menu_2025
    
    def menu_func():
        menumgr = rt.callbacks.notificationparam()
        register_howtos_menu_2025(menumgr)
    
    cuiregid = rt.name("cuiRegisterMenus")
    howtoid = rt.name("pyScriptHowtoMenu")
    
    # Remove existing callback to prevent duplicates, then add
    rt.callbacks.removescripts(id=cuiregid)
    rt.callbacks.addscript(cuiregid, menu_func, id=howtoid)