Skip to content

validate_ouput_maps

ValidateOutputMaps

Bases: InstancePlugin

Validate all output maps for Output Template are generated.

Output maps will be skipped by Substance Painter if it is an output map in the Substance Output Template which uses channels that the current substance painter project has not painted or generated.

Source code in client/ayon_substancepainter/plugins/publish/validate_ouput_maps.py
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 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
class ValidateOutputMaps(pyblish.api.InstancePlugin):
    """Validate all output maps for Output Template are generated.

    Output maps will be skipped by Substance Painter if it is an output
    map in the Substance Output Template which uses channels that the current
    substance painter project has not painted or generated.

    """

    order = pyblish.api.ValidatorOrder
    label = "Validate output maps"
    hosts = ["substancepainter"]
    families = ["textureSet"]

    def process(self, instance):

        config = instance.data["exportConfig"]

        # Substance Painter API does not allow to query the actual output maps
        # it will generate without actually exporting the files. So we try to
        # generate the smallest size / fastest export as possible
        config = copy.deepcopy(config)
        invalid_channels = self.get_invalid_channels(instance, config)
        if invalid_channels:
            raise PublishValidationError(
                "Invalid Channel(s): {} found in texture set {}".format(
                    invalid_channels, instance.name
                ))
        parameters = config["exportParameters"][0]["parameters"]
        parameters["sizeLog2"] = [1, 1]     # output 2x2 images (smallest)
        parameters["paddingAlgorithm"] = "passthrough"  # no dilation (faster)
        parameters["dithering"] = False     # no dithering (faster)
        result = substance_painter.export.export_project_textures(config)
        if result.status != substance_painter.export.ExportStatus.Success:
            raise PublishValidationError(
                "Failed to export texture set: {}".format(result.message)
            )

        generated_files = set()
        for texture_maps in result.textures.values():
            for texture_map in texture_maps:
                generated_files.add(os.path.normpath(texture_map))
                # Directly clean up our temporary export
                os.remove(texture_map)

        creator_attributes = instance.data.get("creator_attributes", {})
        allow_skipped_maps = creator_attributes.get("allowSkippedMaps", True)
        error_report_missing = []
        for image_instance in instance:

            # Confirm whether the instance has its expected files generated.
            # We assume there's just one representation and that it is
            # the actual texture representation from the collector.
            representation = next(iter(image_instance.data["representations"]))
            staging_dir = representation["stagingDir"]
            filenames = representation["files"]
            if not isinstance(filenames, (list, tuple)):
                # Convert single file to list
                filenames = [filenames]

            missing = []
            for filename in filenames:
                filepath = os.path.join(staging_dir, filename)
                filepath = os.path.normpath(filepath)
                if filepath not in generated_files:
                    self.log.warning(f"Missing texture: {filepath}")
                    missing.append(filepath)

            if not missing:
                continue

            if allow_skipped_maps:
                # TODO: This is changing state on the instance's which
                #   should not be done during validation.
                self.log.warning(f"Disabling texture instance: "
                                 f"{image_instance}")
                image_instance.data["active"] = False
                image_instance.data["publish"] = False
                image_instance.data["integrate"] = False
                representation.setdefault("tags", []).append("delete")
                continue
            else:
                error_report_missing.append((image_instance, missing))

        if error_report_missing:

            message = (
                "The Texture Set skipped exporting some output maps which are "
                "defined in the Output Template. This happens if the Output "
                "Templates exports maps from channels which you do not "
                "have in your current Substance Painter project.\n\n"
                "To allow this enable the *Allow Skipped Output Maps* setting "
                "on the instance.\n\n"
                f"Instance {instance} skipped exporting output maps:\n"
                ""
            )

            for image_instance, missing in error_report_missing:
                missing_str = ", ".join(missing)
                message += f"- **{image_instance}** skipped: {missing_str}\n"

            raise PublishValidationError(
                message=message,
                title="Missing output maps"
            )

    def get_invalid_channels(self, instance, config):
        """Function to get invalid channel(s) from export channel
        filtering

        Args:
            instance (pyblish.api.Instance): Instance
            config (dict): export config

        Raises:
            PublishValidationError: raise Publish Validation
                Error if any invalid channel(s) found

        Returns:
            list: invalid channel(s)
        """
        creator_attrs = instance.data["creator_attributes"]
        export_channel = creator_attrs.get("exportChannel", [])
        if not export_channel:
            return []

        export_presets = config.get("exportPresets", [])
        if not export_presets:
            return []

        invalid_channels = []
        for channel in export_channel:
            for export_preset in export_presets:
                maps = export_preset.get("maps", [])
                if not maps:
                    raise PublishValidationError(
                        "No Texture Map Exported with texture set: {}.".format(
                            instance.name)
                    )

                included = any(map_includes_channel(
                    channel_map, channel) for channel_map in maps
                )
                if included:
                    break
            else:
                # not found in any export preset
                invalid_channels.append(channel)

        return invalid_channels

get_invalid_channels(instance, config)

Function to get invalid channel(s) from export channel filtering

Parameters:

Name Type Description Default
instance Instance

Instance

required
config dict

export config

required

Raises:

Type Description
PublishValidationError

raise Publish Validation Error if any invalid channel(s) found

Returns:

Name Type Description
list

invalid channel(s)

Source code in client/ayon_substancepainter/plugins/publish/validate_ouput_maps.py
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
def get_invalid_channels(self, instance, config):
    """Function to get invalid channel(s) from export channel
    filtering

    Args:
        instance (pyblish.api.Instance): Instance
        config (dict): export config

    Raises:
        PublishValidationError: raise Publish Validation
            Error if any invalid channel(s) found

    Returns:
        list: invalid channel(s)
    """
    creator_attrs = instance.data["creator_attributes"]
    export_channel = creator_attrs.get("exportChannel", [])
    if not export_channel:
        return []

    export_presets = config.get("exportPresets", [])
    if not export_presets:
        return []

    invalid_channels = []
    for channel in export_channel:
        for export_preset in export_presets:
            maps = export_preset.get("maps", [])
            if not maps:
                raise PublishValidationError(
                    "No Texture Map Exported with texture set: {}.".format(
                        instance.name)
                )

            included = any(map_includes_channel(
                channel_map, channel) for channel_map in maps
            )
            if included:
                break
        else:
            # not found in any export preset
            invalid_channels.append(channel)

    return invalid_channels