Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Substance Painter: Support for exporting maps/layer stacks with specific channels #532

Merged
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f08443f
supports exporting layer stack with specific channel & output with sp…
moonyuet May 21, 2024
f3b1d34
check substance version when adding use selection into setting & tool…
moonyuet May 22, 2024
597f4a2
code clean up & rename the contextmanager function to set_layer_stack…
moonyuet May 22, 2024
2dd6c12
rename the contextlib function
moonyuet May 22, 2024
9570d0b
use dict as item values for export channel settings
moonyuet May 22, 2024
a97a33a
add channel mapping setting into ayon project setting
moonyuet May 23, 2024
ced2ac0
Update client/ayon_core/hosts/substancepainter/api/lib.py
moonyuet May 23, 2024
d970539
code tweaks - big roy's comment
moonyuet May 23, 2024
e9a56f3
implement backward compatibility for the channel setting in creator
moonyuet May 23, 2024
b70e0b3
renaming filtered_nodes to excluded_nodes & make sure the code implme…
moonyuet May 23, 2024
63398b1
make sure the code doesn't break the integration
moonyuet May 23, 2024
b578547
make sure the export channel filtering function is working
moonyuet May 23, 2024
f3033a0
rename channel name to channel map
moonyuet May 23, 2024
cc600bd
add roughness, roughness, height option into the channel_mapping
moonyuet May 23, 2024
6d03f7b
support to validate the texture maps filtering when no texture map af…
moonyuet May 24, 2024
fb2e41c
support to validate multiple export channel filtering
moonyuet May 24, 2024
bd3be36
edit error message
moonyuet May 24, 2024
339eea0
improve the validation on invalid channel function
moonyuet May 24, 2024
9f09b75
Merge branch 'develop' into enhancement/AY-5104_Substance-selective-e…
moonyuet May 28, 2024
0ebda1d
code tweaks and clean up --BigRoy's comment
moonyuet May 28, 2024
119428b
improve publish validation message
moonyuet May 28, 2024
447edbe
cosmetic fix
moonyuet May 28, 2024
5f950b7
improve debug msg
moonyuet May 28, 2024
a392567
improve debug msg
moonyuet May 28, 2024
3b074c9
code tweaks - big roy's comment
moonyuet May 29, 2024
eeb4544
code tweaks - big roy's comment
moonyuet May 29, 2024
984592d
Merge branch 'develop' into enhancement/AY-5104_Substance-selective-e…
moonyuet May 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions client/ayon_core/hosts/substancepainter/api/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import json
from collections import defaultdict

import contextlib
import substance_painter
import substance_painter.project
import substance_painter.resource
import substance_painter.js
Expand Down Expand Up @@ -640,3 +642,92 @@ def _setup_prompt():
return

return project_mesh


def get_export_presets_by_filtering(export_preset_name, channel_type_names):
moonyuet marked this conversation as resolved.
Show resolved Hide resolved
"""Function to get export presets included with specific channels
requested by users.
moonyuet marked this conversation as resolved.
Show resolved Hide resolved

Args:
export_preset_name (str): Name of export preset
channel_type_list (list): A list of channel type requested by users

Returns:
dict: export preset data
"""

target_maps = []

export_presets = get_export_presets()
export_preset_nice_name = export_presets[export_preset_name]
resource_presets = substance_painter.export.list_resource_export_presets()
preset = next(
(
preset for preset in resource_presets
if preset.resource_id.name == export_preset_nice_name
), None
)
if preset is None:
return {}

maps = preset.list_output_maps()
for channel_map in maps:
for channel_name in channel_type_names:
if not channel_map.get("fileName"):
continue

if channel_name in channel_map["fileName"]:
target_maps.append(channel_map)
# Create a new preset
return {
"exportPresets": [
{
"name": export_preset_name,
"maps": target_maps
}
],
}


@contextlib.contextmanager
def set_layer_stack_opacity(node_ids, channel_types):
"""Function to set the opacity of the layer stack during
context
Args:
node_ids (list[int]): Substance painter root layer node ids
channel_types (list[str]): Channel type names as defined as
attributes in `substance_painter.textureset.ChannelType`
"""
# Do nothing
if not node_ids or not channel_types:
yield
return

stack = substance_painter.textureset.get_active_stack()
stack_root_layers = (
substance_painter.layerstack.get_root_layer_nodes(stack)
)
all_selected_nodes = []
for node_id in node_ids:
node = substance_painter.layerstack.get_node_by_uid(int(node_id))
all_selected_nodes.append(node)
moonyuet marked this conversation as resolved.
Show resolved Hide resolved
node_ids = set(node_ids) # lookup
excluded_nodes = [
node for node in stack_root_layers
if node.uid() not in node_ids
]

original_opacity_values = []
for node in excluded_nodes:
for channel in channel_types:
chan = getattr(substance_painter.textureset.ChannelType, channel)
original_opacity_values.append((chan, node.get_opacity(chan)))
try:
for node in excluded_nodes:
for channel, _ in original_opacity_values:
node.set_opacity(0.0, channel)
yield
finally:
for node in excluded_nodes:
for channel, opacity in original_opacity_values:
node.set_opacity(opacity, channel)
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
"""Creator plugin for creating textures."""

from ayon_core.pipeline import CreatedInstance, Creator, CreatorError
from ayon_core.lib import (
EnumDef,
Expand All @@ -15,8 +14,11 @@
set_instances,
remove_instance
)
from ayon_core.hosts.substancepainter.api.lib import get_export_presets
from ayon_core.hosts.substancepainter.api.lib import (
get_export_presets
)
moonyuet marked this conversation as resolved.
Show resolved Hide resolved

import substance_painter
import substance_painter.project


Expand All @@ -28,9 +30,16 @@ class CreateTextures(Creator):
icon = "picture-o"

default_variant = "Main"
channel_mapping = []

def create(self, product_name, instance_data, pre_create_data):
def apply_settings(self, project_settings):
settings = project_settings["substancepainter"].get("create", []) # noqa
if settings:
self.channel_mapping = settings["CreateTextures"].get(
"channel_mapping", [])


def create(self, product_name, instance_data, pre_create_data):
if not substance_painter.project.is_open():
raise CreatorError("Can't create a Texture Set instance without "
"an open project.")
Expand All @@ -42,11 +51,20 @@ def create(self, product_name, instance_data, pre_create_data):
"exportFileFormat",
"exportSize",
"exportPadding",
"exportDilationDistance"
"exportDilationDistance",
"useCustomExportPreset",
"exportChannel"
]:
if key in pre_create_data:
creator_attributes[key] = pre_create_data[key]

if pre_create_data.get("use_selection"):
stack = substance_painter.textureset.get_active_stack()

instance_data["selected_node_id"] = [
node_number.uid() for node_number in
substance_painter.layerstack.get_selected_nodes(stack)]
moonyuet marked this conversation as resolved.
Show resolved Hide resolved

instance = self.create_instance_in_context(product_name,
instance_data)
set_instance(
Expand Down Expand Up @@ -88,8 +106,53 @@ def create_instance_in_context_from_existing(self, data):
return instance

def get_instance_attr_defs(self):
if self.channel_mapping:
export_channel_enum = {
item["value"]: item["name"]
for item in self.channel_mapping
}
else:
export_channel_enum = {
"BaseColor": "Base Color",
"Metallic": "Metallic",
"Roughness": "Roughness",
"SpecularEdgeColor": "Specular Edge Color",
"Emissive": "Emissive",
"Opacity": "Opacity",
"Displacement": "Displacement",
"Glossiness": "Glossiness",
"Anisotropylevel": "Anisotropy Level",
"AO": "Ambient Occulsion",
"Anisotropyangle": "Anisotropy Angle",
"Transmissive": "Transmissive",
"Reflection": "Reflection",
"Diffuse": "Diffuse",
"Ior": "Index of Refraction",
"Specularlevel": "Specular Level",
"BlendingMask": "Blending Mask",
"Translucency": "Translucency",
"Scattering": "Scattering",
"ScatterColor": "Scatter Color",
"SheenOpacity": "Sheen Opacity",
"SheenRoughness": "Sheen Roughness",
"SheenColor": "Sheen Color",
"CoatOpacity": "Coat Opacity",
"CoatColor": "Coat Color",
"CoatRoughness": "Coat Roughness",
"CoatSpecularLevel": "Coat Specular Level",
"CoatNormal": "Coat Normal",
}

return [
EnumDef("exportChannel",
items=export_channel_enum,
multiselection=True,
default=None,
label="Export Channel(s)",
tooltip="Choose the channel which you "
"want to solely export. The value "
"is 'None' by default which exports "
"all channels"),
EnumDef("exportPresetUrl",
items=get_export_presets(),
label="Output Template"),
Expand Down Expand Up @@ -149,7 +212,6 @@ def get_instance_attr_defs(self):
},
default=None,
label="Size"),

EnumDef("exportPadding",
items={
"passthrough": "No padding (passthrough)",
Expand All @@ -172,4 +234,10 @@ def get_instance_attr_defs(self):

def get_pre_create_attr_defs(self):
# Use same attributes as for instance attributes
return self.get_instance_attr_defs()
attr_defs = []
if substance_painter.application.version_info()[0] >= 10:
attr_defs.append(
BoolDef("use_selection", label="Use selection",
tooltip="Select Layer Stack(s) for exporting")
)
return attr_defs + self.get_instance_attr_defs()
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from ayon_core.pipeline import publish
from ayon_core.hosts.substancepainter.api.lib import (
get_parsed_export_maps,
get_export_presets_by_filtering,
strip_template
)
from ayon_core.pipeline.create import get_product_name
Expand Down Expand Up @@ -207,5 +208,8 @@ def get_export_config(self, instance):
for key, value in dict(parameters).items():
if value is None:
parameters.pop(key)

channel_layer = creator_attrs.get("exportChannel", [])
if channel_layer:
maps = get_export_presets_by_filtering(preset_url, channel_layer)
config.update(maps)
return config
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import substance_painter.export

from ayon_core.pipeline import KnownPublishError, publish
from ayon_core.hosts.substancepainter.api.lib import set_layer_stack_opacity


class ExtractTextures(publish.Extractor,
Expand All @@ -25,19 +25,24 @@ class ExtractTextures(publish.Extractor,
def process(self, instance):

config = instance.data["exportConfig"]
result = substance_painter.export.export_project_textures(config)

if result.status != substance_painter.export.ExportStatus.Success:
raise KnownPublishError(
"Failed to export texture set: {}".format(result.message)
)

# Log what files we generated
for (texture_set_name, stack_name), maps in result.textures.items():
# Log our texture outputs
self.log.info(f"Exported stack: {texture_set_name} {stack_name}")
for texture_map in maps:
self.log.info(f"Exported texture: {texture_map}")
creator_attrs = instance.data["creator_attributes"]
export_channel = creator_attrs.get("exportChannel", [])
node_ids = instance.data.get("selected_node_id", [])

with set_layer_stack_opacity(node_ids, export_channel):
result = substance_painter.export.export_project_textures(config)

if result.status != substance_painter.export.ExportStatus.Success:
raise KnownPublishError(
"Failed to export texture set: {}".format(result.message)
)

# Log what files we generated
for (texture_set_name, stack_name), maps in result.textures.items():
# Log our texture outputs
self.log.info(f"Exported stack: {texture_set_name} {stack_name}")
for texture_map in maps:
self.log.info(f"Exported texture: {texture_map}")

# We'll insert the color space data for each image instance that we
# added into this texture set. The collector couldn't do so because
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,16 @@ def process(self, instance):
# 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 the texture set {}".format(
moonyuet marked this conversation as resolved.
Show resolved Hide resolved
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(
Expand Down Expand Up @@ -108,3 +113,41 @@ def process(self, instance):
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", [])
tmp_export_channel = copy.deepcopy(export_channel)
invalid_channel = []
if export_channel:
for export_preset in config.get("exportPresets", {}):
if not export_preset.get("maps", {}):
raise PublishValidationError(
"No Texture Map Exported with texture set:{}.".format(
moonyuet marked this conversation as resolved.
Show resolved Hide resolved
instance.name)
)
map_names = [channel_map["fileName"] for channel_map
in export_preset["maps"]]
for channel in tmp_export_channel:
# Check if channel is found in at least one map
for map_name in map_names:
if channel in map_name:
break
else:
invalid_channel.append(channel)

return invalid_channel
2 changes: 1 addition & 1 deletion server_addon/substancepainter/package.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
name = "substancepainter"
title = "Substance Painter"
version = "0.1.1"
version = "0.1.2"
Loading