Skip to content

lib

Library of functions useful for 3dsmax pipeline.

convert_unit_scale()

Convert system unit scale in 3dsMax for fbx export

Returns:

Name Type Description
str

unit scale

Source code in client/ayon_max/api/lib.py
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
def convert_unit_scale():
    """Convert system unit scale in 3dsMax
    for fbx export

    Returns:
        str: unit scale
    """
    unit_scale_dict = {
        "millimeters": "mm",
        "centimeters": "cm",
        "meters": "m",
        "kilometers": "km"
    }
    current_unit_scale = rt.Execute("units.MetricType as string")
    return unit_scale_dict[current_unit_scale]

get_all_children(parent, node_type=None)

Handy function to get all the children of a given node

Parameters:

Name Type Description Default
parent 3dsmax Node1

Node to get all children of.

required
node_type None, runtime.class

give class to check for e.g. rt.FFDBox/rt.GeometryClass etc.

None

Returns:

Name Type Description
list

list of all children of the parent node

Source code in client/ayon_max/api/lib.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
def get_all_children(parent, node_type=None):
    """Handy function to get all the children of a given node

    Args:
        parent (3dsmax Node1): Node to get all children of.
        node_type (None, runtime.class): give class to check for
            e.g. rt.FFDBox/rt.GeometryClass etc.

    Returns:
        list: list of all children of the parent node
    """
    def list_children(node):
        children = []
        for c in node.Children:
            children.append(c)
            children = children + list_children(c)
        return children
    child_list = list_children(parent)

    return ([x for x in child_list if rt.SuperClassOf(x) == node_type]
            if node_type else child_list)

get_current_renderer()

Notes

Get current renderer for Max

Returns:

Type Description

"{Current Renderer}:{Current Renderer}"

e.g. "Redshift_Renderer:Redshift_Renderer"

Source code in client/ayon_max/api/lib.py
156
157
158
159
160
161
162
163
164
165
def get_current_renderer():
    """
    Notes:
        Get current renderer for Max

    Returns:
        "{Current Renderer}:{Current Renderer}"
        e.g. "Redshift_Renderer:Redshift_Renderer"
    """
    return rt.renderers.production

get_fps_for_current_context()

Get fps that should be set for current context.

Todos
  • Skip project value.
  • Merge logic with 'get_frame_range' and 'reset_scene_resolution' -> all the values in the functions can be collected at one place as they have same requirements.

Returns:

Type Description

Union[int, float]: FPS value.

Source code in client/ayon_max/api/lib.py
293
294
295
296
297
298
299
300
301
302
303
304
305
306
def get_fps_for_current_context():
    """Get fps that should be set for current context.

    Todos:
        - Skip project value.
        - Merge logic with 'get_frame_range' and 'reset_scene_resolution' ->
            all the values in the functions can be collected at one place as
            they have same requirements.

    Returns:
        Union[int, float]: FPS value.
    """
    task_entity = get_current_task_entity(fields={"attrib"})
    return task_entity["attrib"]["fps"]

get_frame_range(task_entity=None)

Get the current task frame range and handles

Parameters:

Name Type Description Default
task_entity dict

Task Entity.

None

Returns:

Name Type Description
dict Union[Dict[str, Any], None]

with frame start, frame end, handle start, handle end.

Source code in client/ayon_max/api/lib.py
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
def get_frame_range(task_entity=None) -> Union[Dict[str, Any], None]:
    """Get the current task frame range and handles

    Args:
        task_entity (dict): Task Entity.

    Returns:
        dict: with frame start, frame end, handle start, handle end.
    """
    # Set frame start/end
    if task_entity is None:
        task_entity = get_current_task_entity(fields={"attrib"})
    task_attributes = task_entity["attrib"]
    frame_start = int(task_attributes["frameStart"])
    frame_end = int(task_attributes["frameEnd"])
    handle_start = int(task_attributes["handleStart"])
    handle_end = int(task_attributes["handleEnd"])
    frame_start_handle = frame_start - handle_start
    frame_end_handle = frame_end + handle_end

    return {
        "frameStart": frame_start,
        "frameEnd": frame_end,
        "handleStart": handle_start,
        "handleEnd": handle_end,
        "frameStartHandle": frame_start_handle,
        "frameEndHandle": frame_end_handle,
    }

get_main_window()

Acquire Max's main window

Source code in client/ayon_max/api/lib.py
26
27
28
29
30
31
32
33
34
35
36
37
def get_main_window():
    """Acquire Max's main window"""
    from qtpy import QtWidgets
    top_widgets = QtWidgets.QApplication.topLevelWidgets()
    name = "QmaxApplicationWindow"
    for widget in top_widgets:
        if (
            widget.inherits("QMainWindow")
            and widget.metaObject().className() == name
        ):
            return widget
    raise RuntimeError('Count not find 3dsMax main window.')

get_max_version()

Args: get max version date for deadline

Returns:

Type Description

(25000, 62, 0, 25, 0, 0, 997, 2023, "")

max_info[7] = max version date

Source code in client/ayon_max/api/lib.py
387
388
389
390
391
392
393
394
395
396
397
def get_max_version():
    """
    Args:
    get max version date for deadline

    Returns:
        #(25000, 62, 0, 25, 0, 0, 997, 2023, "")
        max_info[7] = max version date
    """
    max_info = rt.MaxVersion()
    return max_info[7]

get_namespace(container_name)

Get the namespace and name of the sub-container

Parameters:

Name Type Description Default
container_name str

the name of master container

required

Raises:

Type Description
RuntimeError

when there is no master container found

Returns:

Name Type Description
namespace str

namespace of the sub-container

name str

name of the sub-container

Source code in client/ayon_max/api/lib.py
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
def get_namespace(container_name):
    """Get the namespace and name of the sub-container

    Args:
        container_name (str): the name of master container

    Raises:
        RuntimeError: when there is no master container found

    Returns:
        namespace (str): namespace of the sub-container
        name (str): name of the sub-container
    """
    node = rt.getNodeByName(container_name)
    if not node:
        raise RuntimeError("Master Container Not Found..")
    name = rt.getUserProp(node, "name")
    namespace = rt.getUserProp(node, "namespace")
    return namespace, name

get_plugins()

Get all loaded plugins in 3dsMax

Returns:

Name Type Description
plugin_info_list list

a list of loaded plugins

Source code in client/ayon_max/api/lib.py
545
546
547
548
549
550
551
552
553
554
555
556
557
558
def get_plugins() -> list:
    """Get all loaded plugins in 3dsMax

    Returns:
        plugin_info_list: a list of loaded plugins
    """
    manager = rt.PluginManager
    count = manager.pluginDllCount
    plugin_info_list = []
    for p in range(1, count + 1):
        plugin_info = manager.pluginDllName(p)
        plugin_info_list.append(plugin_info)

    return plugin_info_list

get_tyflow_export_operators()

Get Tyflow Export Particles Operators.

Returns:

Name Type Description
list

Particle operators

Source code in client/ayon_max/api/lib.py
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
def get_tyflow_export_operators():
    """Get Tyflow Export Particles Operators.

    Returns:
        list: Particle operators

    """
    operators = []
    members = [obj for obj in rt.Objects if rt.ClassOf(obj) == rt.tyFlow]
    for member in members:
        obj = member.baseobject
        anim_names = rt.GetSubAnimNames(obj)
        for anim_name in anim_names:
            sub_anim = rt.GetSubAnim(obj, anim_name)
            if not rt.isKindOf(sub_anim, rt.tyEvent):
                continue
            node_names = rt.GetSubAnimNames(sub_anim)
            for node_name in node_names:
                node_sub_anim = rt.GetSubAnim(sub_anim, node_name)
                if rt.hasProperty(node_sub_anim, "exportMode"):
                    operators.append(node_sub_anim)
    return operators

is_headless()

Check if 3dsMax runs in batch mode. If it returns True, it runs in 3dsbatch.exe If it returns False, it runs in 3dsmax.exe

Source code in client/ayon_max/api/lib.py
400
401
402
403
404
405
def is_headless():
    """Check if 3dsMax runs in batch mode.
    If it returns True, it runs in 3dsbatch.exe
    If it returns False, it runs in 3dsmax.exe
    """
    return rt.maxops.isInNonInteractiveMode()

lsattr(attr, value=None, root=None)

List nodes having attribute with specified value.

Parameters:

Name Type Description Default
attr str

Attribute name to match.

required
value (str, Optional)

Value to match, of omitted, all nodes with specified attribute are returned no matter of value.

None
root (str, Optional)

Root node name. If omitted, scene root is used.

None

Returns:

Type Description
list

list of nodes.

Source code in client/ayon_max/api/lib.py
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
def lsattr(
        attr: str,
        value: Union[str, None] = None,
        root: Union[str, None] = None) -> list:
    """List nodes having attribute with specified value.

    Args:
        attr (str): Attribute name to match.
        value (str, Optional): Value to match, of omitted, all nodes
            with specified attribute are returned no matter of value.
        root (str, Optional): Root node name. If omitted, scene root is used.

    Returns:
        list of nodes.
    """
    root = rt.RootNode if root is None else rt.GetNodeByName(root)

    def output_node(node, nodes):
        nodes.append(node)
        for child in node.Children:
            output_node(child, nodes)

    nodes = []
    output_node(root, nodes)
    return [
        n for n in nodes
        if rt.GetUserProp(n, attr) == value
    ] if value else [
        n for n in nodes
        if rt.GetUserProp(n, attr)
    ]

object_transform_set(container_children)

A function which allows to store the transform of previous loaded object(s) Args: container_children(list): A list of nodes

Returns:

Name Type Description
transform_set dict

A dict with all transform data of

the previous loaded object(s)

Source code in client/ayon_max/api/lib.py
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
def object_transform_set(container_children):
    """A function which allows to store the transform of
    previous loaded object(s)
    Args:
        container_children(list): A list of nodes

    Returns:
        transform_set (dict): A dict with all transform data of
        the previous loaded object(s)
    """
    transform_set = {}
    for node in container_children:
        name = f"{node}.transform"
        transform_set[name] = node.pos
        name = f"{node}.scale"
        transform_set[name] = node.scale
    return transform_set

render_resolution(width, height)

Set render resolution option during context

Parameters:

Name Type Description Default
width int

render width

required
height int

render height

required
Source code in client/ayon_max/api/lib.py
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
@contextlib.contextmanager
def render_resolution(width, height):
    """Set render resolution option during context

    Args:
        width (int): render width
        height (int): render height
    """
    current_renderWidth = rt.renderWidth
    current_renderHeight = rt.renderHeight
    try:
        rt.renderWidth = width
        rt.renderHeight = height
        yield
    finally:
        rt.renderWidth = current_renderWidth
        rt.renderHeight = current_renderHeight

reset_colorspace()

OCIO Configuration Supports in 3dsMax 2024+

Source code in client/ayon_max/api/lib.py
415
416
417
418
419
420
421
422
423
424
425
426
427
428
def reset_colorspace():
    """OCIO Configuration
    Supports in 3dsMax 2024+

    """
    if int(get_max_version()) < 2024:
        return

    max_config_data = colorspace.get_current_context_imageio_config_preset()
    if max_config_data:
        ocio_config_path = max_config_data["path"]
        colorspace_mgr = rt.ColorPipelineMgr
        colorspace_mgr.Mode = rt.Name("OCIO_Custom")
        colorspace_mgr.OCIOConfigPath = ocio_config_path

reset_frame_range(fps=True)

Set frame range to current folder. This is part of 3dsmax documentation:

A System Global variable which lets you get and

set an Interval value that defines the start and end frames of the Active Time Segment.

frameRate: A System Global variable which lets you get and set an Integer value that defines the current scene frame rate in frames-per-second.

Source code in client/ayon_max/api/lib.py
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
def reset_frame_range(fps: bool = True):
    """Set frame range to current folder.
    This is part of 3dsmax documentation:

    animationRange: A System Global variable which lets you get and
        set an Interval value that defines the start and end frames
        of the Active Time Segment.
    frameRate: A System Global variable which lets you get
            and set an Integer value that defines the current
            scene frame rate in frames-per-second.
    """
    if fps:
        rt.frameRate = float(get_fps_for_current_context())

    frame_range = get_frame_range()

    set_timeline(
        frame_range["frameStartHandle"], frame_range["frameEndHandle"])
    set_render_frame_range(
        frame_range["frameStartHandle"], frame_range["frameEndHandle"])

    project_name = get_current_project_name()
    settings = get_project_settings(project_name).get("max")
    auto_key_default_key_time = settings.get(
        "auto_key_default", {}).get("defualt_key_time")
    rt.maxOps.autoKeyDefaultKeyTime = auto_key_default_key_time

reset_scene_resolution(task_entity=None)

Apply the scene resolution from the project definition

scene resolution can be overwritten by a folder if the folder.attrib contains any information regarding scene resolution.

Source code in client/ayon_max/api/lib.py
220
221
222
223
224
225
226
227
228
229
230
231
232
def reset_scene_resolution(task_entity=None):
    """Apply the scene resolution from the project definition

    scene resolution can be overwritten by a folder if the folder.attrib
    contains any information regarding scene resolution.
    """
    if task_entity is None:
        task_entity = get_current_task_entity(fields={"attrib"})
    task_attributes = task_entity["attrib"]
    width = int(task_attributes["resolutionWidth"])
    height = int(task_attributes["resolutionHeight"])

    set_scene_resolution(width, height)

set_context_setting()

Apply the project settings from the project definition

Settings can be overwritten by an folder if the folder.attrib contains any information regarding those settings.

Examples of settings

frame range resolution

Returns:

Type Description

None

Source code in client/ayon_max/api/lib.py
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
def set_context_setting():
    """Apply the project settings from the project definition

    Settings can be overwritten by an folder if the folder.attrib contains
    any information regarding those settings.

    Examples of settings:
        frame range
        resolution

    Returns:
        None
    """
    reset_scene_resolution()
    reset_frame_range()
    validate_unit_scale()
    reset_colorspace()

set_render_frame_range(start_frame, end_frame)

Note

Frame range can be specified in different types. Possible values are: * 1 - Single frame. * 2 - Active time segment ( animationRange ). * 3 - User specified Range. * 4 - User specified Frame pickup string (for example 1,3,5-12).

Todo

Current type is hard-coded, there should be a custom setting for this.

Source code in client/ayon_max/api/lib.py
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
def set_render_frame_range(start_frame, end_frame):
    """
    Note:
        Frame range can be specified in different types. Possible values are:
        * `1` - Single frame.
        * `2` - Active time segment ( animationRange ).
        * `3` - User specified Range.
        * `4` - User specified Frame pickup string (for example `1,3,5-12`).

    Todo:
        Current type is hard-coded, there should be a custom setting for this.
    """
    rt.rendTimeType = 3
    if start_frame is not None and end_frame is not None:
        rt.rendStart = int(start_frame)
        rt.rendEnd = int(end_frame)

set_scene_resolution(width, height)

Set the render resolution

Parameters:

Name Type Description Default
width(int)

value of the width

required
height(int)

value of the height

required

Returns:

Type Description

None

Source code in client/ayon_max/api/lib.py
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
def set_scene_resolution(width: int, height: int):
    """Set the render resolution

    Args:
        width(int): value of the width
        height(int): value of the height

    Returns:
        None

    """
    # make sure the render dialog is closed
    # for the update of resolution
    # Changing the Render Setup dialog settings should be done
    # with the actual Render Setup dialog in a closed state.
    if rt.renderSceneDialog.isOpen():
        rt.renderSceneDialog.close()

    rt.renderWidth = width
    rt.renderHeight = height

set_timeline(frameStart, frameEnd)

Set frame range for timeline editor in Max

Source code in client/ayon_max/api/lib.py
408
409
410
411
412
def set_timeline(frameStart, frameEnd):
    """Set frame range for timeline editor in Max
    """
    rt.animationRange = rt.interval(int(frameStart), int(frameEnd))
    return rt.animationRange

set_unit_scale(project_settings=None)

Function to set unit scale in Metric

Source code in client/ayon_max/api/lib.py
340
341
342
343
344
345
346
347
348
def set_unit_scale(project_settings=None):
    """Function to set unit scale in Metric
    """
    if project_settings is None:
        project_name = get_current_project_name()
        project_settings = get_project_settings(project_name).get("max")
    scene_scale = project_settings["unit_scale_settings"]["scene_unit_scale"]
    rt.units.DisplayType = rt.Name("Metric")
    rt.units.MetricType = rt.Name(scene_scale)

suspended_refresh()

Suspended refresh for scene and modify panel redraw.

Source code in client/ayon_max/api/lib.py
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
@contextlib.contextmanager
def suspended_refresh():
    """Suspended refresh for scene and modify panel redraw.
    """
    if is_headless():
        yield
        return
    rt.disableSceneRedraw()
    rt.suspendEditing()
    try:
        yield

    finally:
        rt.enableSceneRedraw()
        rt.resumeEditing()

unique_namespace(namespace, format='%02d', prefix='', suffix='', con_suffix='CON')

Return unique namespace

Parameters:

Name Type Description Default
namespace str

Name of namespace to consider

required
format str

Formatting of the given iteration number

'%02d'
suffix str

Only consider namespaces with this suffix.

''
con_suffix

max only, for finding the name of the master container

'CON'

unique_namespace("bar")

bar01

unique_namespace(":hello")

:hello01

unique_namespace("bar:", suffix="_NS")

bar01_NS:

Source code in client/ayon_max/api/lib.py
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
def unique_namespace(namespace, format="%02d",
                     prefix="", suffix="", con_suffix="CON"):
    """Return unique namespace

    Arguments:
        namespace (str): Name of namespace to consider
        format (str, optional): Formatting of the given iteration number
        suffix (str, optional): Only consider namespaces with this suffix.
        con_suffix: max only, for finding the name of the master container

    >>> unique_namespace("bar")
    # bar01
    >>> unique_namespace(":hello")
    # :hello01
    >>> unique_namespace("bar:", suffix="_NS")
    # bar01_NS:

    """

    def current_namespace():
        current = namespace
        # When inside a namespace Max adds no trailing :
        if not current.endswith(":"):
            current += ":"
        return current

    # Always check against the absolute namespace root
    # There's no clash with :x if we're defining namespace :a:x
    ROOT = ":" if namespace.startswith(":") else current_namespace()

    # Strip trailing `:` tokens since we might want to add a suffix
    start = ":" if namespace.startswith(":") else ""
    end = ":" if namespace.endswith(":") else ""
    namespace = namespace.strip(":")
    if ":" in namespace:
        # Split off any nesting that we don't uniqify anyway.
        parents, namespace = namespace.rsplit(":", 1)
        start += parents + ":"
        ROOT += start

    iteration = 1
    increment_version = True
    while increment_version:
        nr_namespace = namespace + format % iteration
        unique = prefix + nr_namespace + suffix
        container_name = f"{unique}:{namespace}{con_suffix}"
        if not rt.getNodeByName(container_name):
            name_space = start + unique + end
            increment_version = False
            return name_space
        else:
            increment_version = True
        iteration += 1

update_modifier_node_names(event, node)

Update the name of the nodes after renaming

Parameters:

Name Type Description Default
event MXSWrapperBase

Event Name ( Mandatory argument for rt.NodeEventCallback)

required
node list

Event Number ( Mandatory argument for rt.NodeEventCallback)

required
Source code in client/ayon_max/api/lib.py
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
def update_modifier_node_names(event, node):
    """Update the name of the nodes after renaming

    Args:
        event (pymxs.MXSWrapperBase): Event Name (
            Mandatory argument for rt.NodeEventCallback)
        node (list): Event Number (
            Mandatory argument for rt.NodeEventCallback)

    """
    containers = [
        obj
        for obj in rt.Objects
        if (
            rt.ClassOf(obj) == rt.Container
            and rt.getUserProp(obj, "id") == "pyblish.avalon.instance"
            and rt.getUserProp(obj, "productType") not in {
                "workfile", "tyflow"
            }
        )
    ]
    if not containers:
        return
    for container in containers:
        ayon_data = container.modifiers[0].openPypeData
        updated_node_names = [str(node.node) for node
                              in ayon_data.all_handles]
        rt.setProperty(ayon_data, "sel_list", updated_node_names)

validate_unit_scale(project_settings=None)

Apply the unit scale setting to 3dsMax

Source code in client/ayon_max/api/lib.py
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
def validate_unit_scale(project_settings=None):
    """Apply the unit scale setting to 3dsMax
    """

    if is_headless():
        return
    if project_settings is None:
        project_name = get_current_project_name()
        project_settings = get_project_settings(project_name).get("max")
    scene_scale_enabled = project_settings["unit_scale_settings"]["enabled"]
    if not scene_scale_enabled:
        log.info("Using default scale display type.")
        rt.units.DisplayType = rt.Name("Generic")
        return
    scene_scale = project_settings["unit_scale_settings"]["scene_unit_scale"]
    if rt.units.DisplayType == rt.Name("Metric") and (
        rt.units.MetricType == rt.Name(scene_scale)
    ):
        return

    parent = get_main_window()
    dialog = SimplePopup(parent=parent)
    dialog.setWindowTitle("Wrong Unit Scale")
    dialog.set_message("Scene units do not match studio/project preferences.")
    dialog.set_button_text("Fix")
    dialog.setStyleSheet(load_stylesheet())

    dialog.on_clicked.connect(partial(set_unit_scale, project_settings))
    dialog.show()