Skip to content

Instantly share code, notes, and snippets.

@BigRoy
Last active April 26, 2023 06:32
Show Gist options
  • Save BigRoy/57a80e9bd9cccdd2c40de445f06437c0 to your computer and use it in GitHub Desktop.
Save BigRoy/57a80e9bd9cccdd2c40de445f06437c0 to your computer and use it in GitHub Desktop.
Get the export settings from Substance Painter project to find the export color spaces configured for the project
"""Substance Painter OCIO management
Adobe Substance 3D Painter supports OCIO color management using a per project
configuration. Output color spaces are defined at the project level
More information see:
- https://substance3d.adobe.com/documentation/spdoc/color-management-223053233.html # noqa
- https://substance3d.adobe.com/documentation/spdoc/color-management-with-opencolorio-225969419.html # noqa
"""
import substance_painter.export
import substance_painter.js
import json
def _convert_stack_path_to_cmd_str(stack_path):
return json.dumps(stack_path)
def get_channels(stack_path):
stack_path = _convert_stack_path_to_cmd_str(stack_path)
cmd = f"alg.mapexport.channelIdentifiers({stack_path})"
return substance_painter.js.evaluate(cmd)
def get_channel_format(stack_path, channel):
stack_path = _convert_stack_path_to_cmd_str(stack_path)
cmd = f"alg.mapexport.channelFormat({stack_path}, '{channel}')"
return substance_painter.js.evaluate(cmd)
def get_document_structure():
cmd = f"alg.mapexport.documentStructure()"
return substance_painter.js.evaluate(cmd)
def _iter_document_stack_channels():
"""Yield all stack paths and channels project"""
for material in get_document_structure()["materials"]:
material_name = material["name"]
for stack in material["stacks"]:
stack_name = stack["name"]
for channel in stack["channels"]:
if stack_name:
stack_path = [material_name, stack_name]
else:
stack_path = material_name
yield stack_path, channel
def _get_first_color_and_data_stack_and_channel():
color_channel = None
data_channel = None
for stack_path, channel in _iter_document_stack_channels():
channel_format = get_channel_format(stack_path, channel)
if channel_format["color"]:
color_channel = (stack_path, channel)
else:
data_channel = (stack_path, channel)
if color_channel and data_channel:
return color_channel, data_channel
return color_channel, data_channel
def get_project_channel_data():
keys = [
"project",
"mesh",
"textureSet",
"sceneMaterial",
"udim",
"colorSpace"
]
query = {key: f"${key}" for key in keys}
config = {
"exportPath": "/",
"exportShaderParams": False,
"defaultExportPreset": "query_preset",
"exportPresets": [{
"name": "query_preset",
# List of maps making up this export preset.
"maps": [{
"fileName": json.dumps(query),
# List of source/destination defining which channels will
# make up the texture file.
"channels": [],
"parameters": {
"fileFormat": "exr",
"bitDepth": "32f",
"dithering": False,
"sizeLog2": 4,
"paddingAlgorithm": "passthrough",
"dilationDistance": 16
}
}]
}],
#"exportParamet
}
def _get_query_output(config):
# Return the basename of the first output path
result = substance_painter.export.list_project_textures(config)
path = next(iter(result.values()))[0]
# strip extension and slash since we know relevant json data starts
# and ends with { and } characters
path = path.strip("/\\.exr")
return json.loads(path)
# Query for each type of channel (color and data)
color_channel, data_channel = _get_first_color_and_data_stack_and_channel()
colorspaces = {}
for key, channel_data in {
"data": data_channel,
"color": color_channel
}.items():
if channel_data is None:
# No channel of that datatype anywhere in the Stack so we're
# unable to identify the output color space of the project
colorspaces[key] = None
continue
stack, channel = channel_data
# Stack must be a string
if not isinstance(stack, str):
# Assume iterable
stack = "/".join(stack)
# Define the temp output config
config["exportList"] = [{"rootPath": stack}]
config_map = config["exportPresets"][0]["maps"][0]
config_map["channels"] = [
{
"destChannel": x,
"srcChannel": x,
"srcMapType": "documentMap",
"srcMapName": channel
} for x in "RGB"
]
if key == "color":
# Query for each bit depth
# Color space definition can have a different OCIO config set
# for 8-bit, 16-bit and 32-bit outputs so we need to check each
# bit depth
for depth in ["8", "16", "16f", "32f"]:
config_map["parameters"]["bitDepth"] = depth # noqa
colorspaces[key + depth] = _get_query_output(config)
else:
# Data channel (not color managed)
colorspaces[key] = _get_query_output(config)
return colorspaces
result = get_project_channel_data()
print(json.dumps(result, indent=4))
@BigRoy
Copy link
Author

BigRoy commented Jan 11, 2023

Example JSON output data:

{
    "data": {
        "project": "test_local_asset1_debug_v009_ocio_udim",
        "mesh": "test_local_asset1_pointcacheMain_v004",
        "textureSet": "DefaultMaterial",
        "sceneMaterial": "DefaultMaterial",
        "udim": "1001",
        "colorspace": "Utility - Raw"
    },
    "color8": {
        "project": "test_local_asset1_debug_v009_ocio_udim",
        "mesh": "test_local_asset1_pointcacheMain_v004",
        "textureSet": "DefaultMaterial",
        "sceneMaterial": "DefaultMaterial",
        "udim": "1001",
        "colorspace": "ACES - ACEScg"
    },
    "color16": {
        "project": "test_local_asset1_debug_v009_ocio_udim",
        "mesh": "test_local_asset1_pointcacheMain_v004",
        "textureSet": "DefaultMaterial",
        "sceneMaterial": "DefaultMaterial",
        "udim": "1001",
        "colorspace": "ACES - ACEScg"
    },
    "color16f": {
        "project": "test_local_asset1_debug_v009_ocio_udim",
        "mesh": "test_local_asset1_pointcacheMain_v004",
        "textureSet": "DefaultMaterial",
        "sceneMaterial": "DefaultMaterial",
        "udim": "1001",
        "colorspace": "ACES - ACEScg"
    },
    "color32f": {
        "project": "test_local_asset1_debug_v009_ocio_udim",
        "mesh": "test_local_asset1_pointcacheMain_v004",
        "textureSet": "DefaultMaterial",
        "sceneMaterial": "DefaultMaterial",
        "udim": "1001",
        "colorspace": "ACES - ACEScg"
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment