Urgent Python Scripting Issue in Substance 3D Designer - Node Creation & Connections (Version 14.1.2
Hello Substance 3D Designer Support Team / Community,
I am experiencing a critical issue trying to run a Python script in Adobe Substance 3D Designer, Version 14.1.2 build 8986 Release (04/09/2025).
Edition Windows 11 Home
Version 24H2
Installed on 12/6/2024
OS build 26100.4351
Experience Windows Feature Experience Pack 1000.26100.107.0
My primary goal is to programmatically create a network of standard nodes and establish connections between them within an existing, opened Substance graph. I am trying to automate the setup of a workflow involving nodes like bitmap, levels, blur_hq, normal_blend, normal, and output.
I'm encountering persistent AttributeError and ModuleNotFoundError messages that indicate significant discrepancies in the Python API for my specific build of Substance Designer, compared to common documentation or expected behavior.
Here is a summary of the issues encountered, with the most recent error being the critical blocker:
Initial API Access:
- ModuleNotFoundError: No module named 'sd.api.sdApplication' (Resolved by changing import to from sd.api import sdapplication)
- AttributeError: type object 'SDApplication' has no attribute 'get' (Resolved by changing SDApplication.get() to sd.getContext().getSDApplication())
- AttributeError: 'Context' object has no attribute 'getApplication'. Did you mean: 'getSDApplication'? (Resolved by changing getApplication() to getSDApplication())
- AttributeError: 'SDApplication' object has no attribute 'getName' (Resolved by simply removing .getName() call as it was not accessible directly)
- AttributeError: 'Context' object has no attribute 'getQtForPythonUIMgr' (Resolved by removing the ui_mgr line and changing how current_graph is obtained)
Node Creation Issues:
- AttributeError: type object 'SDNode' has no attribute 'sNew' (Resolved by changing sdnode.SDNode.sNew() to current_graph.newNode())
- TypeError: SDGraph.newNode() missing 1 required positional argument: 'sdDefinitionId' (Resolved by passing the node definition string, e.g., 'sbs::compositing::bitmap', directly to current_graph.newNode())
Property Setting Issues:
- AttributeError: 'SDSBSCompNode' object has no attribute 'setIdentifier' and AttributeError: 'SDSBSCompNode' object has no attribute 'setLabel' (These methods do not appear to exist on the SDSBSCompNode returned by newNode(). All calls to setIdentifier() and setLabel() were removed from the script to bypass this error.)
- AttributeError: 'SDSBSCompNode' object has no attribute 'getProperty' (This occurs when trying to get properties like output_type for the input nodes. I also attempted direct attribute assignment, e.g., node.output_type = ..., but the issue persisted as getProperty is called internally by set(). All getProperty() and direct assignment attempts for identifier/label/group were ultimately removed to isolate the core connection issue.)
Positioning Issues:
- AttributeError: module 'sd.api' has no attribute 'sdPosition' (This persisted even after trying import sd.api.sdposition as sdposition_module and sd.api.sdPosition directly. It seems setPosition requires a ctypes instance like LP_float2.)
- TypeError: byref() argument must be a ctypes instance, not 'tuple' / TypeError: expected LP_float2 instance instead of pointer to c_float_Array_2 (When attempting to use setPosition with (x,y) tuples or ctypes structures, the internal API explicitly required an LP_float2 instance. All setPosition calls were ultimately removed from the script to bypass this error, as it's too complex to reliably create the required ctypes object without specific API headers.)
CURRENT CRITICAL BLOCKER:
- AttributeError: type object 'SDConnection' has no attribute 'sNew' (This is the most recent error, preventing any connections from being made, occurring on line 47: sdconnection.SDConnection.sNew(...))
Here is the Python script I am attempting to run:
import sd from sd.api import sdapplication from sd.api import sdgraph from sd.api import sdnode from sd.api import sdproperty from sd.api import sdtype from sd.api import sdconnection from sd.api import sdpackage def add_normal_smoother_nodes_to_current_graph(): """ Adds the normal smoother node network to the currently opened graph. """ app = sd.getContext().getSDApplication() context = app.mContext current_graph = app.getQtForPythonUIMgr().getCurrentGraph() if not current_graph: print("ERROR: No graph is currently opened. Please open an empty graph first.") return graph_name = current_graph.getIdentifier() print(f"Adding normal smoother nodes to graph: {graph_name}") # --- NODE CREATION --- # All setIdentifier, setLabel, and setPosition calls were removed due to errors. # Nodes will be created with their default identifiers/labels. # Normal Map Input normal_input_node = current_graph.newNode('sbs::compositing::bitmap') # Property setting for output_type (This line failed last time, but is included for full context) normal_input_node.getProperty(sdproperty.SDPropertyID.sNew("output_type"), sdproperty.SDPropertyCategory.Output).set(sdtype.sNew(sdtype.SDTypeEnum.Float4)) # Curvature Map Input curvature_input_node = current_graph.newNode('sbs::compositing::bitmap') # Property setting for output_type (This line failed last time, but is included for full context) curvature_input_node.getProperty(sdproperty.SDPropertyID.sNew("output_type"), sdproperty.SDPropertyCategory.Output).set(sdtype.sNew(sdtype.SDTypeEnum.Float1)) # --- Processing Nodes --- # Curvature Mask Processing (Levels to create a sharp edge mask) levels_node = current_graph.newNode('sbs::compositing::levels') # Connect Curvature Input to Levels # THIS IS THE LINE WHERE THE LAST ERROR OCCURRED: sdconnection.SDConnection.sNew() sdconnection.SDConnection.sNew(levels_node.getProperty(sdproperty.SDPropertyID.sNew("input_image"), sdproperty.SDPropertyCategory.Input), curvature_input_node.getProperty(sdproperty.SDPropertyID.sNew("output_image"), sdproperty.SDPropertyCategory.Output)) # Blur the generated mask (optional, but good for blending softness) mask_blur_node = current_graph.newNode('sbs::compositing::blur_hq') # Connect Levels to Mask Blur sdconnection.SDConnection.sNew(mask_blur_node.getProperty(sdproperty.SDPropertyID.sNew("input_image"), sdproperty.SDPropertyCategory.Input), levels_node.getProperty(sdproperty.SDPropertyID.sNew("output_image"), sdproperty.SDPropertyCategory.Output)) # Normal Map Blur (for the smoothed version) normal_blur_node = current_graph.newNode('sbs::compositing::blur_hq') # Connect Original Normal Map to Normal Map Blur sdconnection.SDConnection.sNew(normal_blur_node.getProperty(sdproperty.SDPropertyID.sNew("input_image"), sdproperty.SDPropertyCategory.Input), normal_input_node.getProperty(sdproperty.SDPropertyID.sNew("output_image"), sdproperty.SDPropertyCategory.Output)) # Blend Node (Original Normal + Blurred Normal, masked by Curvature) normal_blend_node = current_graph.newNode('sbs::compositing::normal_blend') # Connect inputs to Normal Blend sdconnection.SDConnection.sNew(normal_blend_node.getProperty(sdproperty.SDPropertyID.sNew("input_image_foreground"), sdproperty.SDPropertyCategory.Input), normal_blur_node.getProperty(sdproperty.SDPropertyID.sNew("output_image"), sdproperty.SDPropertyCategory.Output)) sdconnection.SDConnection.sNew(normal_blend_node.getProperty(sdproperty.SDPropertyID.sNew("input_image_background"), sdproperty.SDPropertyCategory.Input), normal_input_node.getProperty(sdproperty.SDPropertyID.sNew("output_image"), sdproperty.SDPropertyCategory.Output)) sdconnection.SDConnection.sNew(normal_blend_node.getProperty(sdproperty.SDPropertyID.sNew("input_image_mask"), sdproperty.SDPropertyCategory.Input), mask_blur_node.getProperty(sdproperty.SDPropertyID.sNew("output_image"), sdproperty.SDPropertyCategory.Output)) # Re-normalize the final output (critical for correct normal maps) final_normal_node = current_graph.newNode('sbs::compositing::normal') # Connect Normal Blend to Final Normal Node sdconnection.SDConnection.sNew(final_normal_node.getProperty(sdproperty.SDPropertyID.sNew("input_image"), sdproperty.SDPropertyCategory.Input), normal_blend_node.getProperty(sdproperty.SDPropertyID.sNew("output_image"), sdproperty.SDPropertyCategory.Output)) # --- Create Output Node --- output_node = current_graph.newNode('sbs::compositing::output') output_node.getProperty(sdproperty.SDPropertyID.sNew("output_type"), sdproperty.SDPropertyCategory.Output).set(sdtype.sNew(sdtype.SDTypeEnum.Float4)) output_node.getProperty(sdproperty.SDPropertyID.sNew("identifier"), sdproperty.SDPropertyID.sNew("normal_smoothed")) output_node.getProperty(sdproperty.SDPropertyID.sNew("label"), sdproperty.SDPropertyID.sNew("Normal Smoothed")) output_node.getProperty(sdproperty.SDPropertyID.sNew("group"), sdproperty.SDPropertyID.sNew("Outputs")) # Connect Final Normal to Output sdconnection.SDConnection.sNew(output_node.getProperty(sdproperty.SDPropertyID.sNew("input_image"), sdproperty.SDPropertyCategory.Input), final_normal_node.getProperty(sdproperty.SDPropertyID.sNew("output_image"), sdproperty.SDPropertyCategory.Output)) print(f"Nodes for normal smoother added to graph '{graph_name}' successfully.") # Run the function to add nodes add_normal_smoother_nodes_to_current_graph()
