Skip to content

collect_look

Maya look collector.

CollectLook

Bases: MayaInstancePlugin

Collect look data for instance.

For the shapes/transforms of the referenced object to collect look for retrieve the user-defined attributes (like V-ray attributes) and their values as they were created in the current scene.

For the members of the instance collect the sets (shadingEngines and other sets, e.g. VRayDisplacement) they are in along with the exact membership relations.

Collects

lookAttributes (list): Nodes in instance with their altered attributes lookSetRelations (list): Sets and their memberships lookSets (list): List of set names included in the look

Source code in client/ayon_maya/plugins/publish/collect_look.py
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
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
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
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
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
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
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
class CollectLook(plugin.MayaInstancePlugin):
    """Collect look data for instance.

    For the shapes/transforms of the referenced object to collect look for
    retrieve the user-defined attributes (like V-ray attributes) and their
    values as they were created in the current scene.

    For the members of the instance collect the sets (shadingEngines and
    other sets, e.g. VRayDisplacement) they are in along with the exact
    membership relations.

    Collects:
        lookAttributes (list): Nodes in instance with their altered attributes
        lookSetRelations (list): Sets and their memberships
        lookSets (list): List of set names included in the look

    """

    order = pyblish.api.CollectorOrder + 0.2
    families = ["look"]
    label = "Collect Look"

    def process(self, instance):
        """Collect the Look in the instance with the correct layer settings"""
        renderlayer = instance.data.get("renderlayer", "defaultRenderLayer")
        with lib.renderlayer(renderlayer):
            self.collect(instance)

    def collect(self, instance):
        """Collect looks.

        Args:
            instance (pyblish.api.Instance): Instance to collect.

        """
        self.log.debug("Looking for look associations "
                       "for %s" % instance.data['name'])

        # Discover related object sets
        self.log.debug("Gathering sets ...")
        sets = self.collect_sets(instance)

        # Lookup set (optimization)
        instance_lookup = set(cmds.ls(instance, long=True))

        self.log.debug("Gathering set relations ...")
        # Ensure iteration happen in a list to allow removing keys from the
        # dict within the loop
        for obj_set in list(sets):
            self.log.debug("From {}".format(obj_set))
            # Get all nodes of the current objectSet (shadingEngine)
            for member in cmds.ls(cmds.sets(obj_set, query=True), long=True):
                member_data = self.collect_member_data(member,
                                                       instance_lookup)
                if member_data:
                    # Add information of the node to the members list
                    sets[obj_set]["members"].append(member_data)

            # Remove sets that didn't have any members assigned in the end
            # Thus the data will be limited to only what we need.
            if not sets[obj_set]["members"]:
                self.log.debug(
                    "Removing redundant set information: {}".format(obj_set)
                )
                sets.pop(obj_set, None)

        self.log.debug("Gathering attribute changes to instance members..")
        attributes = self.collect_attributes_changed(instance)

        # Store data on the instance
        instance.data["lookData"] = {
            "attributes": attributes,
            "relationships": sets
        }

        # Collect file nodes used by shading engines (if we have any)
        files = []
        look_sets = list(sets.keys())
        if look_sets:
            self.log.debug("Found look sets: {}".format(look_sets))
            files = self.collect_file_nodes(look_sets)

        self.log.debug("Collected file nodes:\n{}".format(files))

        # Collect texture resources if any file nodes are found
        resources = []
        for node in files:
            resources.extend(self.collect_resources(node))
        instance.data["resources"] = resources
        self.log.debug("Collected resources: {}".format(resources))

        # Log warning when no relevant sets were retrieved for the look.
        if (
            not instance.data["lookData"]["relationships"]
            and "model" not in self.families
        ):
            self.log.warning("No sets found for the nodes in the "
                             "instance: %s" % instance[:])

        # Ensure unique shader sets
        # Add shader sets to the instance for unify ID validation
        instance.extend(shader for shader in look_sets if shader
                        not in instance_lookup)

        self.log.debug("Collected look for %s" % instance)

    def collect_file_nodes(self, look_sets):
        """Get the entire node chain of the look sets and return file nodes

        Arguments:
            look_sets (List[str]): List of sets and shading engines relevant
                to the look.

        Returns:
            List[str]: List of file node names.

        """

        shader_attrs = [
            "surfaceShader",
            "volumeShader",
            "displacementShader",
            "aiSurfaceShader",
            "aiVolumeShader",
            "rman__surface",
            "rman__displacement"
        ]

        # Get all material attrs for all look sets to retrieve their inputs
        existing_attrs = []
        for look_set in look_sets:
            for attr in shader_attrs:
                if cmds.attributeQuery(attr, node=look_set, exists=True):
                    existing_attrs.append("{}.{}".format(look_set, attr))

        materials = cmds.listConnections(existing_attrs,
                                         source=True,
                                         destination=False) or []

        self.log.debug("Found materials:\n{}".format(materials))

        # Get the entire node chain of the look sets
        # history = cmds.listHistory(look_sets, allConnections=True)
        # if materials list is empty, listHistory() will crash with
        # RuntimeError
        history = set()
        if materials:
            history.update(cmds.listHistory(materials, allConnections=True))

        # Since we retrieved history only of the connected materials connected
        # to the look sets above we now add direct history for some of the
        # look sets directly handling render attribute sets

        # Maya (at least 2024) crashes with Warning when render set type
        # isn't available. cmds.ls() will return empty list
        if RENDER_SET_TYPES:
            render_sets = cmds.ls(look_sets, type=RENDER_SET_TYPES)
            if render_sets:
                history.update(
                    cmds.listHistory(render_sets,
                                     future=False,
                                     pruneDagObjects=True)
                    or []
                )

        # Get file nodes in the material history
        files = cmds.ls(list(history),
                        # It's important only node types are passed that
                        # exist (e.g. for loaded plugins) because otherwise
                        # the result will turn back empty
                        type=list(FILE_NODES.keys()),
                        long=True)

        # Sort for log readability
        files.sort()

        return files

    def collect_sets(self, instance):
        """Collect all objectSets which are of importance for publishing

        It checks if all nodes in the instance are related to any objectSet
        which need to be

        Args:
            instance (pyblish.api.Instance): publish instance containing all
                nodes to be published.

        Returns:
            dict
        """

        sets = {}
        for node in instance:
            related_sets = lib.get_related_sets(node)
            if not related_sets:
                continue

            for objset in related_sets:
                if objset in sets:
                    continue

                sets[objset] = {"uuid": lib.get_id(objset), "members": list()}

        return sets

    def collect_member_data(self, member, instance_members):
        """Get all information of the node
        Args:
            member (str): the name of the node to check
            instance_members (set): the collected instance members

        Returns:
            dict

        """
        node, components = (member.rsplit(".", 1) + [None])[:2]
        if components and not cmds.objectType(node, isAType="shape"):
            # Components are always on shapes. When only a single shape is
            # parented under a transform Maya returns it as e.g. `cube.f[0:4]`
            # instead of `cubeShape.f[0:4]` so we expand that to the shape
            node = cmds.listRelatives(node, shapes=True, fullPath=True)[0]

        # Only include valid members of the instance
        if node not in instance_members:
            return

        node_id = lib.get_id(node)
        if not node_id:
            self.log.error("Member '{}' has no attribute 'cbId'".format(node))
            return

        member_data = {"name": node, "uuid": node_id}
        if components:
            member_data["components"] = components

        return member_data

    def collect_attributes_changed(self, instance):
        """Collect all userDefined attributes which have changed

        Each node gets checked for user defined attributes which have been
        altered during development. Each changes gets logged in a dictionary

        [{name: node,
          uuid: uuid,
          attributes: {attribute: value}}]

        Args:
            instance (list): all nodes which will be published

        Returns:
            list
        """

        attributes = []
        for node in instance:

            # Collect changes to "custom" attributes
            node_attrs = get_look_attrs(node)

            # Only include if there are any properties we care about
            if not node_attrs:
                continue

            self.log.debug(
                "Node \"{0}\" attributes: {1}".format(node, node_attrs)
            )

            node_attributes = {}
            for attr in node_attrs:
                if not cmds.attributeQuery(attr, node=node, exists=True):
                    continue
                attribute = "{}.{}".format(node, attr)
                # We don't support mixed-type attributes yet.
                if cmds.attributeQuery(attr, node=node, multi=True):
                    self.log.warning("Attribute '{}' is mixed-type and is "
                                     "not supported yet.".format(attribute))
                    continue

                attribute_type = cmds.getAttr(attribute, type=True)
                if attribute_type == "message":
                    continue

                # Maya has a tendency to return string attribute values as
                # `None` if it is an empty string and the attribute has never
                # been set but is still at default value.
                value = cmds.getAttr(attribute, asString=True)
                if value is None:
                    # If the attribute type is `string` we will convert it
                    # to enforce an empty string value
                    if attribute_type == "string":
                        value = ""
                    else:
                        continue

                node_attributes[attr] = value
            # Only include if there are any properties we care about
            if not node_attributes:
                continue
            attributes.append({"name": node,
                               "uuid": lib.get_id(node),
                               "attributes": node_attributes})

        return attributes

    def collect_resources(self, node):
        """Collect the link to the file(s) used (resource)
        Args:
            node (str): name of the node

        Returns:
            dict
        """
        if cmds.nodeType(node) not in FILE_NODES:
            self.log.error(
                "Unsupported file node: {}".format(cmds.nodeType(node)))
            raise AssertionError("Unsupported file node")

        self.log.debug(
            "Collecting resource: {} ({})".format(node, cmds.nodeType(node))
        )

        attributes = get_attributes(FILE_NODES, cmds.nodeType(node), node)
        for attribute in attributes:
            source = cmds.getAttr("{}.{}".format(
                node,
                attribute
            ))

            self.log.debug("  - file source: {}".format(source))
            color_space_attr = "{}.colorSpace".format(node)
            try:
                color_space = cmds.getAttr(color_space_attr)
            except ValueError:
                # node doesn't have colorspace attribute
                color_space = "Raw"

            # Compare with the computed file path, e.g. the one with
            # the <UDIM> pattern in it, to generate some logging information
            # about this difference
            # Only for file nodes with `fileTextureName` attribute
            if attribute == "fileTextureName":
                computed_source = cmds.getAttr(
                    "{}.computedFileTextureNamePattern".format(node)
                )
                if source != computed_source:
                    self.log.debug("Detected computed file pattern difference "
                                   "from original pattern: {0} "
                                   "({1} -> {2})".format(node,
                                                         source,
                                                         computed_source))

            # renderman allows nodes to have filename attribute empty while
            # you can have another incoming connection from different node.
            if not source and cmds.nodeType(node) in PXR_NODES:
                self.log.debug("Renderman: source is empty, skipping...")
                continue
            # We replace backslashes with forward slashes because V-Ray
            # can't handle the UDIM files with the backslashes in the
            # paths as the computed patterns
            source = source.replace("\\", "/")

            files = get_file_node_files(node)
            if len(files) == 0:
                self.log.debug("No valid files found from node `%s`" % node)

            self.log.debug("collection of resource done:")
            self.log.debug("  - node: {}".format(node))
            self.log.debug("  - attribute: {}".format(attribute))
            self.log.debug("  - source: {}".format(source))
            self.log.debug("  - file: {}".format(files))
            self.log.debug("  - color space: {}".format(color_space))

            # Define the resource
            yield {
                "node": node,
                # here we are passing not only attribute, but with node again
                # this should be simplified and changed extractor.
                "attribute": "{}.{}".format(node, attribute),
                "source": source,  # required for resources
                "files": files,
                "color_space": color_space
            }

collect(instance)

Collect looks.

Parameters:

Name Type Description Default
instance Instance

Instance to collect.

required
Source code in client/ayon_maya/plugins/publish/collect_look.py
301
302
303
304
305
306
307
308
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
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
def collect(self, instance):
    """Collect looks.

    Args:
        instance (pyblish.api.Instance): Instance to collect.

    """
    self.log.debug("Looking for look associations "
                   "for %s" % instance.data['name'])

    # Discover related object sets
    self.log.debug("Gathering sets ...")
    sets = self.collect_sets(instance)

    # Lookup set (optimization)
    instance_lookup = set(cmds.ls(instance, long=True))

    self.log.debug("Gathering set relations ...")
    # Ensure iteration happen in a list to allow removing keys from the
    # dict within the loop
    for obj_set in list(sets):
        self.log.debug("From {}".format(obj_set))
        # Get all nodes of the current objectSet (shadingEngine)
        for member in cmds.ls(cmds.sets(obj_set, query=True), long=True):
            member_data = self.collect_member_data(member,
                                                   instance_lookup)
            if member_data:
                # Add information of the node to the members list
                sets[obj_set]["members"].append(member_data)

        # Remove sets that didn't have any members assigned in the end
        # Thus the data will be limited to only what we need.
        if not sets[obj_set]["members"]:
            self.log.debug(
                "Removing redundant set information: {}".format(obj_set)
            )
            sets.pop(obj_set, None)

    self.log.debug("Gathering attribute changes to instance members..")
    attributes = self.collect_attributes_changed(instance)

    # Store data on the instance
    instance.data["lookData"] = {
        "attributes": attributes,
        "relationships": sets
    }

    # Collect file nodes used by shading engines (if we have any)
    files = []
    look_sets = list(sets.keys())
    if look_sets:
        self.log.debug("Found look sets: {}".format(look_sets))
        files = self.collect_file_nodes(look_sets)

    self.log.debug("Collected file nodes:\n{}".format(files))

    # Collect texture resources if any file nodes are found
    resources = []
    for node in files:
        resources.extend(self.collect_resources(node))
    instance.data["resources"] = resources
    self.log.debug("Collected resources: {}".format(resources))

    # Log warning when no relevant sets were retrieved for the look.
    if (
        not instance.data["lookData"]["relationships"]
        and "model" not in self.families
    ):
        self.log.warning("No sets found for the nodes in the "
                         "instance: %s" % instance[:])

    # Ensure unique shader sets
    # Add shader sets to the instance for unify ID validation
    instance.extend(shader for shader in look_sets if shader
                    not in instance_lookup)

    self.log.debug("Collected look for %s" % instance)

collect_attributes_changed(instance)

Collect all userDefined attributes which have changed

Each node gets checked for user defined attributes which have been altered during development. Each changes gets logged in a dictionary

[{name: node, uuid: uuid, attributes: {attribute: value}}]

Parameters:

Name Type Description Default
instance list

all nodes which will be published

required

Returns:

Type Description

list

Source code in client/ayon_maya/plugins/publish/collect_look.py
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
def collect_attributes_changed(self, instance):
    """Collect all userDefined attributes which have changed

    Each node gets checked for user defined attributes which have been
    altered during development. Each changes gets logged in a dictionary

    [{name: node,
      uuid: uuid,
      attributes: {attribute: value}}]

    Args:
        instance (list): all nodes which will be published

    Returns:
        list
    """

    attributes = []
    for node in instance:

        # Collect changes to "custom" attributes
        node_attrs = get_look_attrs(node)

        # Only include if there are any properties we care about
        if not node_attrs:
            continue

        self.log.debug(
            "Node \"{0}\" attributes: {1}".format(node, node_attrs)
        )

        node_attributes = {}
        for attr in node_attrs:
            if not cmds.attributeQuery(attr, node=node, exists=True):
                continue
            attribute = "{}.{}".format(node, attr)
            # We don't support mixed-type attributes yet.
            if cmds.attributeQuery(attr, node=node, multi=True):
                self.log.warning("Attribute '{}' is mixed-type and is "
                                 "not supported yet.".format(attribute))
                continue

            attribute_type = cmds.getAttr(attribute, type=True)
            if attribute_type == "message":
                continue

            # Maya has a tendency to return string attribute values as
            # `None` if it is an empty string and the attribute has never
            # been set but is still at default value.
            value = cmds.getAttr(attribute, asString=True)
            if value is None:
                # If the attribute type is `string` we will convert it
                # to enforce an empty string value
                if attribute_type == "string":
                    value = ""
                else:
                    continue

            node_attributes[attr] = value
        # Only include if there are any properties we care about
        if not node_attributes:
            continue
        attributes.append({"name": node,
                           "uuid": lib.get_id(node),
                           "attributes": node_attributes})

    return attributes

collect_file_nodes(look_sets)

Get the entire node chain of the look sets and return file nodes

Parameters:

Name Type Description Default
look_sets List[str]

List of sets and shading engines relevant to the look.

required

Returns:

Type Description

List[str]: List of file node names.

Source code in client/ayon_maya/plugins/publish/collect_look.py
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
def collect_file_nodes(self, look_sets):
    """Get the entire node chain of the look sets and return file nodes

    Arguments:
        look_sets (List[str]): List of sets and shading engines relevant
            to the look.

    Returns:
        List[str]: List of file node names.

    """

    shader_attrs = [
        "surfaceShader",
        "volumeShader",
        "displacementShader",
        "aiSurfaceShader",
        "aiVolumeShader",
        "rman__surface",
        "rman__displacement"
    ]

    # Get all material attrs for all look sets to retrieve their inputs
    existing_attrs = []
    for look_set in look_sets:
        for attr in shader_attrs:
            if cmds.attributeQuery(attr, node=look_set, exists=True):
                existing_attrs.append("{}.{}".format(look_set, attr))

    materials = cmds.listConnections(existing_attrs,
                                     source=True,
                                     destination=False) or []

    self.log.debug("Found materials:\n{}".format(materials))

    # Get the entire node chain of the look sets
    # history = cmds.listHistory(look_sets, allConnections=True)
    # if materials list is empty, listHistory() will crash with
    # RuntimeError
    history = set()
    if materials:
        history.update(cmds.listHistory(materials, allConnections=True))

    # Since we retrieved history only of the connected materials connected
    # to the look sets above we now add direct history for some of the
    # look sets directly handling render attribute sets

    # Maya (at least 2024) crashes with Warning when render set type
    # isn't available. cmds.ls() will return empty list
    if RENDER_SET_TYPES:
        render_sets = cmds.ls(look_sets, type=RENDER_SET_TYPES)
        if render_sets:
            history.update(
                cmds.listHistory(render_sets,
                                 future=False,
                                 pruneDagObjects=True)
                or []
            )

    # Get file nodes in the material history
    files = cmds.ls(list(history),
                    # It's important only node types are passed that
                    # exist (e.g. for loaded plugins) because otherwise
                    # the result will turn back empty
                    type=list(FILE_NODES.keys()),
                    long=True)

    # Sort for log readability
    files.sort()

    return files

collect_member_data(member, instance_members)

Get all information of the node Args: member (str): the name of the node to check instance_members (set): the collected instance members

Returns:

Type Description

dict

Source code in client/ayon_maya/plugins/publish/collect_look.py
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
def collect_member_data(self, member, instance_members):
    """Get all information of the node
    Args:
        member (str): the name of the node to check
        instance_members (set): the collected instance members

    Returns:
        dict

    """
    node, components = (member.rsplit(".", 1) + [None])[:2]
    if components and not cmds.objectType(node, isAType="shape"):
        # Components are always on shapes. When only a single shape is
        # parented under a transform Maya returns it as e.g. `cube.f[0:4]`
        # instead of `cubeShape.f[0:4]` so we expand that to the shape
        node = cmds.listRelatives(node, shapes=True, fullPath=True)[0]

    # Only include valid members of the instance
    if node not in instance_members:
        return

    node_id = lib.get_id(node)
    if not node_id:
        self.log.error("Member '{}' has no attribute 'cbId'".format(node))
        return

    member_data = {"name": node, "uuid": node_id}
    if components:
        member_data["components"] = components

    return member_data

collect_resources(node)

Collect the link to the file(s) used (resource) Args: node (str): name of the node

Returns:

Type Description

dict

Source code in client/ayon_maya/plugins/publish/collect_look.py
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
def collect_resources(self, node):
    """Collect the link to the file(s) used (resource)
    Args:
        node (str): name of the node

    Returns:
        dict
    """
    if cmds.nodeType(node) not in FILE_NODES:
        self.log.error(
            "Unsupported file node: {}".format(cmds.nodeType(node)))
        raise AssertionError("Unsupported file node")

    self.log.debug(
        "Collecting resource: {} ({})".format(node, cmds.nodeType(node))
    )

    attributes = get_attributes(FILE_NODES, cmds.nodeType(node), node)
    for attribute in attributes:
        source = cmds.getAttr("{}.{}".format(
            node,
            attribute
        ))

        self.log.debug("  - file source: {}".format(source))
        color_space_attr = "{}.colorSpace".format(node)
        try:
            color_space = cmds.getAttr(color_space_attr)
        except ValueError:
            # node doesn't have colorspace attribute
            color_space = "Raw"

        # Compare with the computed file path, e.g. the one with
        # the <UDIM> pattern in it, to generate some logging information
        # about this difference
        # Only for file nodes with `fileTextureName` attribute
        if attribute == "fileTextureName":
            computed_source = cmds.getAttr(
                "{}.computedFileTextureNamePattern".format(node)
            )
            if source != computed_source:
                self.log.debug("Detected computed file pattern difference "
                               "from original pattern: {0} "
                               "({1} -> {2})".format(node,
                                                     source,
                                                     computed_source))

        # renderman allows nodes to have filename attribute empty while
        # you can have another incoming connection from different node.
        if not source and cmds.nodeType(node) in PXR_NODES:
            self.log.debug("Renderman: source is empty, skipping...")
            continue
        # We replace backslashes with forward slashes because V-Ray
        # can't handle the UDIM files with the backslashes in the
        # paths as the computed patterns
        source = source.replace("\\", "/")

        files = get_file_node_files(node)
        if len(files) == 0:
            self.log.debug("No valid files found from node `%s`" % node)

        self.log.debug("collection of resource done:")
        self.log.debug("  - node: {}".format(node))
        self.log.debug("  - attribute: {}".format(attribute))
        self.log.debug("  - source: {}".format(source))
        self.log.debug("  - file: {}".format(files))
        self.log.debug("  - color space: {}".format(color_space))

        # Define the resource
        yield {
            "node": node,
            # here we are passing not only attribute, but with node again
            # this should be simplified and changed extractor.
            "attribute": "{}.{}".format(node, attribute),
            "source": source,  # required for resources
            "files": files,
            "color_space": color_space
        }

collect_sets(instance)

Collect all objectSets which are of importance for publishing

It checks if all nodes in the instance are related to any objectSet which need to be

Parameters:

Name Type Description Default
instance Instance

publish instance containing all nodes to be published.

required

Returns:

Type Description

dict

Source code in client/ayon_maya/plugins/publish/collect_look.py
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
def collect_sets(self, instance):
    """Collect all objectSets which are of importance for publishing

    It checks if all nodes in the instance are related to any objectSet
    which need to be

    Args:
        instance (pyblish.api.Instance): publish instance containing all
            nodes to be published.

    Returns:
        dict
    """

    sets = {}
    for node in instance:
        related_sets = lib.get_related_sets(node)
        if not related_sets:
            continue

        for objset in related_sets:
            if objset in sets:
                continue

            sets[objset] = {"uuid": lib.get_id(objset), "members": list()}

    return sets

process(instance)

Collect the Look in the instance with the correct layer settings

Source code in client/ayon_maya/plugins/publish/collect_look.py
295
296
297
298
299
def process(self, instance):
    """Collect the Look in the instance with the correct layer settings"""
    renderlayer = instance.data.get("renderlayer", "defaultRenderLayer")
    with lib.renderlayer(renderlayer):
        self.collect(instance)

CollectModelRenderSets

Bases: CollectLook

Collect render attribute sets for model instance.

Collects additional render attribute sets so they can be published with model.

Source code in client/ayon_maya/plugins/publish/collect_look.py
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
class CollectModelRenderSets(CollectLook):
    """Collect render attribute sets for model instance.

    Collects additional render attribute sets so they can be
    published with model.

    """

    order = pyblish.api.CollectorOrder + 0.21
    families = ["model"]
    label = "Collect Model Render Sets"

    def collect_sets(self, instance):
        """Collect all related objectSets except shadingEngines

        Args:
            instance (pyblish.api.Instance): publish instance containing all
                nodes to be published.

        Returns:
            dict
        """

        sets = {}
        for node in instance:
            related_sets = lib.get_related_sets(node)
            if not related_sets:
                continue

            for objset in related_sets:
                if objset in sets:
                    continue

                if cmds.objectType(objset, isAType="shadingEngine"):
                    continue

                sets[objset] = {"uuid": lib.get_id(objset), "members": list()}

        return sets

collect_sets(instance)

Collect all related objectSets except shadingEngines

Parameters:

Name Type Description Default
instance Instance

publish instance containing all nodes to be published.

required

Returns:

Type Description

dict

Source code in client/ayon_maya/plugins/publish/collect_look.py
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
def collect_sets(self, instance):
    """Collect all related objectSets except shadingEngines

    Args:
        instance (pyblish.api.Instance): publish instance containing all
            nodes to be published.

    Returns:
        dict
    """

    sets = {}
    for node in instance:
        related_sets = lib.get_related_sets(node)
        if not related_sets:
            continue

        for objset in related_sets:
            if objset in sets:
                continue

            if cmds.objectType(objset, isAType="shadingEngine"):
                continue

            sets[objset] = {"uuid": lib.get_id(objset), "members": list()}

    return sets

get_file_node_files(node)

Return the file paths related to the file node

Note

Will only return existing files. Returns an empty list if not valid existing files are linked.

Returns:

Name Type Description
list

List of full file paths.

Source code in client/ayon_maya/plugins/publish/collect_look.py
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
def get_file_node_files(node):
    """Return the file paths related to the file node

    Note:
        Will only return existing files. Returns an empty list
        if not valid existing files are linked.

    Returns:
        list: List of full file paths.

    """
    paths = get_file_node_paths(node)

    # For sequences get all files and filter to only existing files
    result = []
    for path in paths:
        if node_uses_image_sequence(node, path):
            glob_pattern = seq_to_glob(path)
            result.extend(glob.glob(glob_pattern))
        elif os.path.exists(path):
            result.append(path)

    return result

get_file_node_paths(node)

Get the file path used by a Maya file node.

Parameters:

Name Type Description Default
node str

Name of the Maya file node

required

Returns:

Name Type Description
list

the file paths in use

Source code in client/ayon_maya/plugins/publish/collect_look.py
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
def get_file_node_paths(node):
    # type: (str) -> list
    """Get the file path used by a Maya file node.

    Args:
        node (str): Name of the Maya file node

    Returns:
        list: the file paths in use

    """
    # if the path appears to be sequence, use computedFileTextureNamePattern,
    # this preserves the <> tag
    if cmds.attributeQuery('computedFileTextureNamePattern',
                           node=node,
                           exists=True):
        plug = '{0}.computedFileTextureNamePattern'.format(node)
        texture_pattern = cmds.getAttr(plug)

        patterns = ["<udim>",
                    "<tile>",
                    "u<u>_v<v>",
                    "<f>",
                    "<frame0",
                    "<uvtile>"]
        lower = texture_pattern.lower()
        if any(pattern in lower for pattern in patterns):
            return [texture_pattern]

    try:
        file_attributes = get_attributes(
            FILE_NODES, cmds.nodeType(node), node)
    except AttributeError:
        file_attributes = "fileTextureName"

    files = []
    for file_attr in file_attributes:
        if cmds.attributeQuery(file_attr, node=node, exists=True):
            files.append(cmds.getAttr("{}.{}".format(node, file_attr)))

    return files

get_look_attrs(node)

Returns attributes of a node that are important for the look.

These are the "changed" attributes (those that have edits applied in the current scene).

Returns:

Name Type Description
list

Attribute names to extract

Source code in client/ayon_maya/plugins/publish/collect_look.py
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
def get_look_attrs(node):
    """Returns attributes of a node that are important for the look.

    These are the "changed" attributes (those that have edits applied
    in the current scene).

    Returns:
        list: Attribute names to extract

    """
    # When referenced get only attributes that are "changed since file open"
    # which includes any reference edits, otherwise take *all* user defined
    # attributes
    is_referenced = cmds.referenceQuery(node, isNodeReferenced=True)
    result = cmds.listAttr(node, userDefined=True,
                           changedSinceFileOpen=is_referenced) or []

    # `cbId` is added when a scene is saved, ignore by default
    if "cbId" in result:
        result.remove("cbId")

    # For shapes allow render stat changes
    if cmds.objectType(node, isAType="shape"):
        attrs = cmds.listAttr(node, changedSinceFileOpen=True) or []
        for attr in attrs:
            if (
                    attr in SHAPE_ATTRS       # maya shape render stats
                    or attr.startswith('ai')  # arnold
                    or attr.startswith("rs")  # redshift
            ):
                result.append(attr)
    return result

node_uses_image_sequence(node, node_path)

Return whether file node uses an image sequence or single image.

Determine if a node uses an image sequence or just a single image, not always obvious from its file path alone.

Parameters:

Name Type Description Default
node str

Name of the Maya node

required
node_path str

The file path of the node

required

Returns:

Name Type Description
bool

True if node uses an image sequence

Source code in client/ayon_maya/plugins/publish/collect_look.py
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
def node_uses_image_sequence(node, node_path):
    # type: (str, str) -> bool
    """Return whether file node uses an image sequence or single image.

    Determine if a node uses an image sequence or just a single image,
    not always obvious from its file path alone.

    Args:
        node (str): Name of the Maya node
        node_path (str): The file path of the node

    Returns:
        bool: True if node uses an image sequence

    """

    # useFrameExtension indicates an explicit image sequence
    try:
        use_frame_extension = cmds.getAttr('%s.useFrameExtension' % node)
    except ValueError:
        use_frame_extension = False
    if use_frame_extension:
        return True

    # The following tokens imply a sequence
    patterns = ["<udim>", "<tile>", "<uvtile>",
                "u<u>_v<v>", "<frame0", "<f4>"]
    node_path_lowered = node_path.lower()
    return any(pattern in node_path_lowered for pattern in patterns)

seq_to_glob(path)

Takes an image sequence path and returns it in glob format, with the frame number replaced by a '*'.

Image sequences may be numerical sequences, e.g. /path/to/file.1001.exr will return as /path/to/file.*.exr.

Image sequences may also use tokens to denote sequences, e.g. /path/to/texture..tif will return as /path/to/texture.*.tif.

Parameters:

Name Type Description Default
path str

the image sequence path

required

Returns:

Name Type Description
str

Return glob string that matches the filename pattern.

Source code in client/ayon_maya/plugins/publish/collect_look.py
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
def seq_to_glob(path):
    """Takes an image sequence path and returns it in glob format,
    with the frame number replaced by a '*'.

    Image sequences may be numerical sequences, e.g. /path/to/file.1001.exr
    will return as /path/to/file.*.exr.

    Image sequences may also use tokens to denote sequences, e.g.
    /path/to/texture.<UDIM>.tif will return as /path/to/texture.*.tif.

    Args:
        path (str): the image sequence path

    Returns:
        str: Return glob string that matches the filename pattern.

    """

    if path is None:
        return path

    # If any of the patterns, convert the pattern
    patterns = {
        "<udim>": "<udim>",
        "<tile>": "<tile>",
        "<uvtile>": "<uvtile>",
        "#": "#",
        "u<u>_v<v>": "<u>|<v>",
        "<frame0": "<frame0\d+>",
        "<f>": "<f>"
    }

    lower = path.lower()
    has_pattern = False
    for pattern, regex_pattern in patterns.items():
        if pattern in lower:
            path = re.sub(regex_pattern, "*", path, flags=re.IGNORECASE)
            has_pattern = True

    if has_pattern:
        return path

    base = os.path.basename(path)
    matches = list(re.finditer(r'\d+', base))
    if matches:
        match = matches[-1]
        new_base = '{0}*{1}'.format(base[:match.start()],
                                    base[match.end():])
        head = os.path.dirname(path)
        return os.path.join(head, new_base)
    else:
        return path