Discovering Channel Identifiers of a Substance Shader

Dear community,

The following code example demonstrates how to discover the channel identifiers of the "Channel" parameter of a Substance shader, so that the channel can be changed programmatically for a substance asset unknown at the time of writing the script.

9b05b0e3-39d3-471b-855f-4c0e1e3dcdea-image.png

This question reached us via mail, but since answering it requires no confidential data, we are sharing the solution here. The "trick" is to traverse the description of the shader, as these identifiers depend on the substance.

Cheers,
Ferdinand

The result (the example script will randomly select a channel, but with the data provided, channels can also be selected by their name or a substring match as for example "diffuse"):

substance_channels.gif

The code:

"""Example for discovering the channels of a Substance shader.

The solution is a bit hacky by traversing the description of the shader but should work.
"""

import c4d
import random

def GetSubstanceChannels(shader: c4d.BaseShader) -> dict[int:str]:
    """Returns all channels of the substance loaded into #shader as a dictionary of id-label pairs.
    """
    if not isinstance(shader, c4d.BaseShader) or (shader.GetType() != c4d.Xsubstance):
        raise TypeError(f"{shader} is not a substance shader.")

    # Get the data for the "Channel" dropdown element from the description of the shader.
    description = shader.GetDescription(c4d.DESCFLAGS_DESC_NONE)
    channelData = description.GetParameter(c4d.SUBSTANCESHADER_CHANNEL)

    # Get the elements in the drop down menu.
    elements = channelData[c4d.DESC_CYCLE]
    if not isinstance(elements, c4d.BaseContainer):
        raise RuntimeError(f"Could not access Channel parameter description in {shader}.")
    
    # Pack the data into a dictionary and return it.
    return {id: label for id, label in elements}

def main(doc: c4d.documents.BaseDocument):
    """
    """
    # Get the active material.
    material = doc.GetActiveMaterial()
    if not isinstance(material, c4d.BaseMaterial):
        raise RuntimeError("Please select a material.")

    # Get the substance shader loaded into the color channel of the material.
    shader = material[c4d.MATERIAL_COLOR_SHADER]
    channelData = GetSubstanceChannels(shader)

    for id, label in channelData.items():
        print (f"id: {id}, label: {label}")

    # To select a specific channel, one would have to do a string comparison here to find keywords as
    # "Color" or "Metal" in the channel label. I am just randomly selecting a channel instead.
    channelId = random.choice(tuple(channelData.keys()))
    channelLabel = channelData[channelId]
    print (f"Setting substance to channel '{channelLabel}({channelId})'")
    shader[c4d.SUBSTANCESHADER_CHANNEL] = channelId

    c4d.EventAdd()

if __name__=='__main__':
    main(doc)