Skip to content

collect_multiverse_look

CollectMultiverseLookData

Bases: MayaInstancePlugin

Collect Multiverse Look

Searches through the overrides finding all material overrides. From there it extracts the shading group and then finds all texture files in the shading group network. It also checks for mipmap versions of texture files and adds them to the resources to get published.

Source code in client/ayon_maya/plugins/publish/collect_multiverse_look.py
255
256
257
258
259
260
261
262
263
264
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
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
class CollectMultiverseLookData(plugin.MayaInstancePlugin):
    """Collect Multiverse Look

    Searches through the overrides finding all material overrides. From there
    it extracts the shading group and then finds all texture files in the
    shading group network. It also checks for mipmap versions of texture files
    and adds them to the resources to get published.

    """

    order = pyblish.api.CollectorOrder + 0.2
    label = 'Collect Multiverse Look'
    families = ["mvLook"]

    def process(self, instance):
        # Load plugin first
        cmds.loadPlugin("MultiverseForMaya", quiet=True)
        import multiverse

        self.log.debug("Processing mvLook for '{}'".format(instance))

        nodes = set()
        for node in instance:
            # We want only mvUsdCompoundShape nodes.
            nodes_of_interest = cmds.ls(node,
                                        dag=True,
                                        shapes=False,
                                        type="mvUsdCompoundShape",
                                        noIntermediate=True,
                                        long=True)
            nodes.update(nodes_of_interest)

        sets = {}
        instance.data["resources"] = []
        publishMipMap = instance.data["publishMipMap"]

        for node in nodes:
            self.log.debug("Getting resources for '{}'".format(node))

            # We know what nodes need to be collected, now we need to
            # extract the materials overrides.
            overrides = multiverse.ListMaterialOverridePrims(node)
            for override in overrides:
                matOver = multiverse.GetMaterialOverride(node, override)

                if isinstance(matOver, multiverse.MaterialSourceShadingGroup):
                    # We now need to grab the shadingGroup so add it to the
                    # sets we pass down the pipe.
                    shadingGroup = matOver.shadingGroupName
                    self.log.debug("ShadingGroup = '{}'".format(shadingGroup))
                    sets[shadingGroup] = {"uuid": lib.get_id(
                        shadingGroup), "members": list()}

                    # The SG may reference files, add those too!
                    history = cmds.listHistory(
                        shadingGroup, allConnections=True)

                    # We need to iterate over node_types since `cmds.ls` may
                    # error out if we don't have the appropriate plugin loaded.
                    files = []
                    for node_type in NODETYPES.keys():
                        files += cmds.ls(history,
                                         type=node_type,
                                         long=True)

                    for f in files:
                        resources = self.collect_resource(f, publishMipMap)
                        instance.data["resources"] += resources

                elif isinstance(matOver, multiverse.MaterialSourceUsdPath):
                    # TODO: Handle this later.
                    pass

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

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

        Returns:
            dict
        """

        node_type = cmds.nodeType(node)
        self.log.debug("processing: {}/{}".format(node, node_type))

        if node_type not in NODETYPES:
            self.log.error("Unsupported file node: {}".format(node_type))
            raise AssertionError("Unsupported file node")

        resources = []
        for node_type_attr in NODETYPES[node_type]:
            fname_attrib = node_type_attr.get_fname(node)
            computed_fname_attrib = node_type_attr.get_computed_fname(node)
            colour_space_attrib = node_type_attr.get_colour_space(node)

            source = cmds.getAttr(fname_attrib)
            color_space = "Raw"
            try:
                color_space = cmds.getAttr(colour_space_attrib)
            except ValueError:
                # node doesn't have colorspace attribute, use "Raw" from before
                pass
            # Compare with the computed file path, e.g. the one with the <UDIM>
            # pattern in it, to generate some logging information about this
            # difference
            # computed_attribute = "{}.computedFileTextureNamePattern".format(node)  # noqa
            computed_source = cmds.getAttr(computed_fname_attrib)
            if source != computed_source:
                self.log.debug("Detected computed file pattern difference "
                               "from original pattern: {0} "
                               "({1} -> {2})".format(node,
                                                     source,
                                                     computed_source))

            # 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)
            files = self.handle_files(files, publishMipMap)
            if len(files) == 0:
                self.log.error("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(fname_attrib))
            self.log.debug("  - source: {}".format(source))
            self.log.debug("  - file: {}".format(files))
            self.log.debug("  - color space: {}".format(color_space))

            # Define the resource
            resource = {"node": node,
                        "attribute": fname_attrib,
                        "source": source,  # required for resources
                        "files": files,
                        "color_space": color_space}  # required for resources
            resources.append(resource)
        return resources

    def handle_files(self, files, publishMipMap):
        """This will go through all the files and make sure that they are
        either already mipmapped or have a corresponding mipmap sidecar and
        add that to the list."""
        if not publishMipMap:
            return files

        extra_files = []
        self.log.debug("Expecting MipMaps, going to look for them.")
        for fname in files:
            self.log.debug("Checking '{}' for mipmaps".format(fname))
            if is_mipmap(fname):
                self.log.debug(" - file is already MipMap, skipping.")
                continue

            mipmap = get_mipmap(fname)
            if mipmap:
                self.log.debug(" mipmap found for '{}'".format(fname))
                extra_files.append(mipmap)
            else:
                self.log.warning(" no mipmap found for '{}'".format(fname))
        return files + extra_files

collect_resource(node, publishMipMap)

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_multiverse_look.py
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
def collect_resource(self, node, publishMipMap):
    """Collect the link to the file(s) used (resource)
    Args:
        node (str): name of the node

    Returns:
        dict
    """

    node_type = cmds.nodeType(node)
    self.log.debug("processing: {}/{}".format(node, node_type))

    if node_type not in NODETYPES:
        self.log.error("Unsupported file node: {}".format(node_type))
        raise AssertionError("Unsupported file node")

    resources = []
    for node_type_attr in NODETYPES[node_type]:
        fname_attrib = node_type_attr.get_fname(node)
        computed_fname_attrib = node_type_attr.get_computed_fname(node)
        colour_space_attrib = node_type_attr.get_colour_space(node)

        source = cmds.getAttr(fname_attrib)
        color_space = "Raw"
        try:
            color_space = cmds.getAttr(colour_space_attrib)
        except ValueError:
            # node doesn't have colorspace attribute, use "Raw" from before
            pass
        # Compare with the computed file path, e.g. the one with the <UDIM>
        # pattern in it, to generate some logging information about this
        # difference
        # computed_attribute = "{}.computedFileTextureNamePattern".format(node)  # noqa
        computed_source = cmds.getAttr(computed_fname_attrib)
        if source != computed_source:
            self.log.debug("Detected computed file pattern difference "
                           "from original pattern: {0} "
                           "({1} -> {2})".format(node,
                                                 source,
                                                 computed_source))

        # 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)
        files = self.handle_files(files, publishMipMap)
        if len(files) == 0:
            self.log.error("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(fname_attrib))
        self.log.debug("  - source: {}".format(source))
        self.log.debug("  - file: {}".format(files))
        self.log.debug("  - color space: {}".format(color_space))

        # Define the resource
        resource = {"node": node,
                    "attribute": fname_attrib,
                    "source": source,  # required for resources
                    "files": files,
                    "color_space": color_space}  # required for resources
        resources.append(resource)
    return resources

handle_files(files, publishMipMap)

This will go through all the files and make sure that they are either already mipmapped or have a corresponding mipmap sidecar and add that to the list.

Source code in client/ayon_maya/plugins/publish/collect_multiverse_look.py
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
def handle_files(self, files, publishMipMap):
    """This will go through all the files and make sure that they are
    either already mipmapped or have a corresponding mipmap sidecar and
    add that to the list."""
    if not publishMipMap:
        return files

    extra_files = []
    self.log.debug("Expecting MipMaps, going to look for them.")
    for fname in files:
        self.log.debug("Checking '{}' for mipmaps".format(fname))
        if is_mipmap(fname):
            self.log.debug(" - file is already MipMap, skipping.")
            continue

        mipmap = get_mipmap(fname)
        if mipmap:
            self.log.debug(" mipmap found for '{}'".format(fname))
            extra_files.append(mipmap)
        else:
            self.log.warning(" no mipmap found for '{}'".format(fname))
    return files + extra_files

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_multiverse_look.py
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
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)
    paths = [cmds.workspace(expandName=path) for path in paths]
    if node_uses_image_sequence(node):
        globs = []
        for path in paths:
            globs += glob.glob(seq_to_glob(path))
        return globs
    else:
        return list(filter(lambda x: os.path.exists(x), paths))

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
str

the file path in use

Source code in client/ayon_maya/plugins/publish/collect_multiverse_look.py
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
def get_file_node_paths(node):
    """Get the file path used by a Maya file node.

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

    Returns:
        str: the file path 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]

    return get_file_paths_for_node(node)

get_file_paths_for_node(node)

Gets all the file paths in this node.

Returns all filepaths that this node references. Some node types only reference one, but others, like dlTriplanar, can reference 3.

Parameters:

Name Type Description Default
node str

Name of the Maya node

required

Returns list(str): A list with all evaluated maya attributes for filepaths.

Source code in client/ayon_maya/plugins/publish/collect_multiverse_look.py
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
def get_file_paths_for_node(node):
    """Gets all the file paths in this node.

    Returns all filepaths that this node references. Some node types only
    reference one, but others, like dlTriplanar, can reference 3.

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

    Returns
        list(str): A list with all evaluated maya attributes for filepaths.
    """

    node_type = cmds.nodeType(node)
    if node_type not in NODETYPES:
        return []

    paths = []
    for node_type_attr in NODETYPES[node_type]:
        fname = cmds.getAttr("{}.{}".format(node, node_type_attr.fname))
        paths.append(fname)
    return paths

node_uses_image_sequence(node)

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

Returns:

Name Type Description
bool

True if node uses an image sequence

Source code in client/ayon_maya/plugins/publish/collect_multiverse_look.py
 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
118
119
120
121
122
123
124
125
126
127
128
def node_uses_image_sequence(node):
    """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

    Returns:
        bool: True if node uses an image sequence

    """

    # useFrameExtension indicates an explicit image sequence
    paths = get_file_node_paths(node)
    paths = [path.lower() for path in paths]

    # The following tokens imply a sequence
    patterns = ["<udim>", "<tile>", "<uvtile>", "u<u>_v<v>", "<frame0"]

    def pattern_in_paths(patterns, paths):
        """Helper function for checking to see if a pattern is contained
        in the list of paths"""
        for pattern in patterns:
            for path in paths:
                if pattern in path:
                    return True
        return False

    node_type = cmds.nodeType(node)
    if node_type == 'dlTexture':
        return (cmds.getAttr('{}.useImageSequence'.format(node)) or
                pattern_in_paths(patterns, paths))
    elif node_type == "file":
        return (cmds.getAttr('{}.useFrameExtension'.format(node)) or
                pattern_in_paths(patterns, paths))
    return False

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_multiverse_look.py
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
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
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+>",  # noqa - copied from collect_look.py
        "<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