Skip to content

collect_usd_look_assets

CollectUsdLookAssets

Bases: HoudiniInstancePlugin

Collect all assets introduced by the look.

We are looking to collect e.g. all texture resources so we can transfer them with the publish and write then to the publish location.

If possible, we'll also try to identify the colorspace of the asset.

Source code in client/ayon_houdini/plugins/publish/collect_usd_look_assets.py
 50
 51
 52
 53
 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
 85
 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
118
119
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
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
183
184
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
class CollectUsdLookAssets(plugin.HoudiniInstancePlugin):
    """Collect all assets introduced by the look.

    We are looking to collect e.g. all texture resources so we can transfer
    them with the publish and write then to the publish location.

    If possible, we'll also try to identify the colorspace of the asset.

    """
    # TODO: Implement $F frame support (per frame values)
    # TODO: If input image is already a published texture or resource than
    #   preferably we'd keep the link in-tact and NOT update it. We can just
    #   start ignoring AYON URIs

    label = "Collect USD Look Assets"
    order = pyblish.api.CollectorOrder
    hosts = ["houdini"]
    families = ["look"]

    exclude_suffixes = [".usd", ".usda", ".usdc", ".usdz", ".abc", ".vbd"]

    def process(self, instance):

        # Get Sdf.Layers from "Collect ROP Sdf Layers and USD Stage" plug-in
        layers = instance.data.get("layers")
        if layers is None:
            self.log.warning(f"No USD layers found on instance: {instance}")
            return

        layers: List[Sdf.Layer]
        instance_resources = self.get_layer_assets(layers)

        # Define a relative asset remapping for the USD Extractor so that
        # any textures are remapped to their 'relative' publish path.
        # All textures will be in a relative `./resources/` folder
        remap = {}
        for resource in instance_resources:
            source = resource.source
            name = os.path.basename(source)
            remap[os.path.normpath(source)] = f"./resources/{name}"
        instance.data["assetRemap"] = remap

        # Store resources on instance
        resources = instance.data.setdefault("resources", [])
        for resource in instance_resources:
            resources.append(dataclasses.asdict(resource))

        # Log all collected textures
        # Note: It is fine for a single texture to be included more than once
        # where even one of them does not have a color space set, but the other
        # does. For example, there may be a USD UV Texture just for a GL
        # preview material which does not specify an OCIO color
        # space.
        all_files = []
        for resource in instance_resources:
            all_files.append(f"{resource.attribute}:")

            for filepath in resource.files:
                if resource.color_space:
                    file_label = f"- {filepath} ({resource.color_space})"
                else:
                    file_label = f"- {filepath}"
                all_files.append(file_label)

        self.log.info(
            "Collected assets:\n{}".format(
                "\n".join(all_files)
            )
        )

    def get_layer_assets(self, layers: List[Sdf.Layer]) -> List[Resource]:
        # TODO: Correctly resolve paths using Asset Resolver.
        #       Preferably this would use one cached
        #       resolver context to optimize the path resolving.
        # TODO: Fix for timesamples - if timesamples, then `.default` might
        #       not be authored on the spec

        resources: List[Resource] = list()
        for layer in layers:
            for path in get_layer_property_paths(layer):

                spec = layer.GetAttributeAtPath(path)
                if not spec:
                    continue

                if spec.typeName != "asset":
                    continue

                asset: Sdf.AssetPath = spec.default
                base, ext = os.path.splitext(asset.path)
                if ext in self.exclude_suffixes:
                    continue

                filepath = asset.path.replace("\\", "/")

                # Expand <UDIM> to all files of the available files on disk
                # TODO: Add support for `<TILE>`
                # TODO: Add support for `<ATTR:name INDEX:name DEFAULT:value>`
                if "<UDIM>" in filepath.upper():
                    pattern = re.sub(
                        r"<UDIM>",
                        # UDIM is always four digits
                        "[0-9]" * 4,
                        filepath,
                        flags=re.IGNORECASE
                    )
                    files = glob.glob(pattern)
                else:
                    # Single file
                    files = [filepath]

                # Detect the colorspace of the input asset property
                colorspace = self.get_colorspace(spec)

                resource = Resource(
                    attribute=path.pathString,
                    source=asset.path,
                    files=files,
                    color_space=colorspace
                )
                resources.append(resource)

        # Sort by filepath
        resources.sort(key=lambda r: r.source)

        return resources

    def get_colorspace(self, spec: Sdf.AttributeSpec) -> Optional[str]:
        """Return colorspace for a Asset attribute spec.

        There is currently no USD standard on how colorspaces should be
        represented for shaders or asset properties - each renderer's material
        implementations seem to currently use their own way of specifying the
        colorspace on the shader. As such, this comes with some guesswork.

        Args:
            spec (Sdf.AttributeSpec): The asset type attribute to retrieve
                the colorspace for.

        Returns:
            Optional[str]: The colorspace for the given attribute, if any.

        """
        # TODO: Support Karma, V-Ray, Renderman texture colorspaces
        # Materialx image defines colorspace as custom info on the attribute
        if spec.HasInfo("colorSpace"):
            return spec.GetInfo("colorSpace")

        # Arnold materials define the colorspace as a separate primvar
        # TODO: Fix for timesamples - if timesamples, then `.default` might
        #       not be authored on the spec
        prim_path = spec.path.GetPrimPath()
        layer = spec.layer
        for name in COLORSPACE_ATTRS:
            colorspace_property_path = prim_path.AppendProperty(name)
            colorspace_spec = layer.GetAttributeAtPath(
                colorspace_property_path
            )
            if colorspace_spec and colorspace_spec.default:
                return colorspace_spec.default

get_colorspace(spec)

Return colorspace for a Asset attribute spec.

There is currently no USD standard on how colorspaces should be represented for shaders or asset properties - each renderer's material implementations seem to currently use their own way of specifying the colorspace on the shader. As such, this comes with some guesswork.

Parameters:

Name Type Description Default
spec AttributeSpec

The asset type attribute to retrieve the colorspace for.

required

Returns:

Type Description
Optional[str]

Optional[str]: The colorspace for the given attribute, if any.

Source code in client/ayon_houdini/plugins/publish/collect_usd_look_assets.py
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
203
204
205
206
207
208
209
def get_colorspace(self, spec: Sdf.AttributeSpec) -> Optional[str]:
    """Return colorspace for a Asset attribute spec.

    There is currently no USD standard on how colorspaces should be
    represented for shaders or asset properties - each renderer's material
    implementations seem to currently use their own way of specifying the
    colorspace on the shader. As such, this comes with some guesswork.

    Args:
        spec (Sdf.AttributeSpec): The asset type attribute to retrieve
            the colorspace for.

    Returns:
        Optional[str]: The colorspace for the given attribute, if any.

    """
    # TODO: Support Karma, V-Ray, Renderman texture colorspaces
    # Materialx image defines colorspace as custom info on the attribute
    if spec.HasInfo("colorSpace"):
        return spec.GetInfo("colorSpace")

    # Arnold materials define the colorspace as a separate primvar
    # TODO: Fix for timesamples - if timesamples, then `.default` might
    #       not be authored on the spec
    prim_path = spec.path.GetPrimPath()
    layer = spec.layer
    for name in COLORSPACE_ATTRS:
        colorspace_property_path = prim_path.AppendProperty(name)
        colorspace_spec = layer.GetAttributeAtPath(
            colorspace_property_path
        )
        if colorspace_spec and colorspace_spec.default:
            return colorspace_spec.default

CollectUsdLookResourceTransfers

Bases: HoudiniInstancePlugin

Define the publish direct file transfers for any found resources.

This ensures that any source texture will end up in the published look in the resourcesDir.

Source code in client/ayon_houdini/plugins/publish/collect_usd_look_assets.py
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
class CollectUsdLookResourceTransfers(plugin.HoudiniInstancePlugin):
    """Define the publish direct file transfers for any found resources.

    This ensures that any source texture will end up in the published look
    in the `resourcesDir`.

    """
    label = "Collect USD Look Transfers"
    order = pyblish.api.CollectorOrder + 0.496
    hosts = ["houdini"]
    families = ["look"]

    def process(self, instance):

        resources_dir = instance.data["resourcesDir"]
        transfers = instance.data.setdefault("transfers", [])
        for resource in instance.data.get("resources", []):
            for src in resource["files"]:
                dest = os.path.join(resources_dir, os.path.basename(src))
                transfers.append((src, dest))
                self.log.debug("Registering transfer: %s -> %s", src, dest)

get_layer_property_paths(layer)

Return all property paths from a layer

Source code in client/ayon_houdini/plugins/publish/collect_usd_look_assets.py
36
37
38
39
40
41
42
43
44
45
46
47
def get_layer_property_paths(layer: Sdf.Layer) -> List[Sdf.Path]:
    """Return all property paths from a layer"""
    paths = []

    def collect_paths(path):
        if not path.IsPropertyPath():
            return
        paths.append(path)

    layer.Traverse("/", collect_paths)

    return paths