worn_painted_metal

21 nodes, 23 wires — 9 from the library, 12 built in

brushed anisotropic_noise_v2 output X_Amount = 4 Y_Amount = 64 disorder = 0.12 smoothness = 1.5 panels tile_generator pattern_input_1 pattern_input_2 pattern_input_3 pattern_input_4 pattern_input_5 pattern_input_6 background output x_amount = 2 y_amount = 3 scale = 0.97 plate bevel input custom_curve height normal distance = 0.01 smoothing = 4 normal_intensity = 1 seams height_blend height_top height_bottom mask blended_height height_mask height_position = 0.5 contrast = 0.4 opacity = 0.65 scratch scratches_generator output spline_number = 48 spline_width = 0.1 spline_distortion = 0.3 carve blend source destination opacity unique_filter_output blendingmode = substract opacitymult = 0.3 erode slope_blur_grayscale_2 Source Effect Slope_Blur Samples = 16 Intensity = 0.35 tonormal normal input1 unique_filter_output intensity = 6 height output inputNodeOutput unique_filter_output identifier = "height" usage = height (RGBA) normal output inputNodeOutput unique_filter_output identifier = "normal" usage = normal (RGBA) curv curvature_smooth_v2 input curvature convexity concavity radius = 1.5 edges histogram_scan Input_1 Output Position = 0.62 Contrast = 0.8 grunge grunge_scratches_rough output scratch_quantity = 0.45 Contrast = 0.3 chip blend source destination opacity unique_filter_output blendingmode = max opacitymult = 0.75 paint uniform unique_filter_output colorswitch = true outputcolor = [0.09, 0.25, 0.28, 1] steel uniform unique_filter_output colorswitch = true outputcolor = [0.44, 0.42, 0.4, 1] albedo blend source destination opacity unique_filter_output basecolor output inputNodeOutput unique_filter_output identifier = "basecolor" usage = baseColor (RGBA) roughbase levels input1 unique_filter_output levelinlow = [0.3, 0.3, 0.3, 0] levelinhigh = [0.75, 0.75, 0.75, 1] roughmix blend source destination opacity unique_filter_output opacitymult = 0.9 roughness output inputNodeOutput unique_filter_output identifier = "roughness" usage = roughness (RGBA)

The same network, for Designer

Open the graph you want it in, then Windows › Python Editor, paste, Run.

# Generated from a node-graph spec -- worn_painted_metal.
#
# Open the graph you want these nodes in, then: Windows > Python Editor, paste, Run.
# Nodes are added below anything already in the graph; nothing existing is touched.

import os
import sys

import sd
from sd.api.sdbasetypes import ColorRGBA, float2, float4
from sd.api.sdapplication import SDApplicationPath
from sd.api.sdvaluebool import SDValueBool
from sd.api.sdvaluecolorrgba import SDValueColorRGBA
from sd.api.sdvalueenum import SDValueEnum
from sd.api.sdvaluefloat import SDValueFloat
from sd.api.sdvaluefloat4 import SDValueFloat4
from sd.api.sdvalueint import SDValueInt
from sd.api.sdvaluestring import SDValueString
from sd.api.sdvaluearray import SDValueArray
from sd.api.sdtypeusage import SDTypeUsage
from sd.api.sdusage import SDUsage
from sd.api.sdvalueusage import SDValueUsage


def refuse(app, headline, detail):
    """Stop, and make sure it was seen.

    Two channels, because one alone is missable in a different way. stderr first and always:
    the Python Editor tags it [ERR] rather than [MSG], which is why a crash traceback stands out
    from ordinary output, and unlike a dialog it is still there after it has been dismissed.
    Then a modal box, which is the half that interrupts rather than waits to be read.

    The dialog is the optional half. PySide6 6.5 ships in the interpreter's own site-packages
    and sd.api wraps Designer's main window into a QMainWindow for it, so it is available from
    a pasted script -- but anything that goes wrong with it goes wrong after the console line
    has already been written."""
    sys.stderr.write(headline + '\n' + detail + '\n')
    sys.stderr.flush()
    try:
        from PySide6 import QtWidgets
        from sd.api.qtforpythonuimgrwrapper import QtForPythonUIMgrWrapper

        box = QtWidgets.QMessageBox(QtForPythonUIMgrWrapper(app.getUIMgr()).getMainWindow())
        box.setIcon(QtWidgets.QMessageBox.Icon.Warning)
        box.setWindowTitle('Nothing was built')
        box.setText(headline)
        box.setInformativeText(detail)
        box.exec()
    except Exception:
        pass



def resources_dir(app):
    """Where the shipped .sbs packages live. Asking the application beats hardcoding an install
    path, but which of these the answer points at is not worth guessing -- try them in turn."""
    resources = app.getPath(SDApplicationPath.DefaultResourcesDir)
    install = app.getPath(SDApplicationPath.InstallationDir)
    return [
        os.path.join(resources, 'packages'),
        resources,
        os.path.join(install, 'resources', 'packages'),
    ]


def find_resource(app, package_name, identifier, explicit_path=None):
    """Library nodes are graphs inside .sbs packages and no definition manager lists them.
    Match on identifier, never on the resource url -- its ?dependency=NNNN is a per-session uid.

    explicit_path is set for a graph the project itself owns: it sits in the project's alias
    folder rather than the install. This script is regenerated per machine, so an absolute
    path here is written fresh each build rather than committed."""
    package_mgr = app.getPackageMgr()
    filename = package_name + '.sbs'

    for package in package_mgr.getPackages():
        path = (package.getFilePath() or '').replace('\\', '/')
        if path.endswith('/' + filename):
            for resource in package.getChildrenResources(True):
                if resource.getIdentifier() == identifier:
                    return resource

    search = ([explicit_path] if explicit_path else [])
    search += [os.path.join(directory, filename) for directory in resources_dir(app)]
    for candidate in search:
        if not os.path.isfile(candidate):
            continue
        package = package_mgr.loadUserPackage(candidate)
        for resource in package.getChildrenResources(True):
            if resource.getIdentifier() == identifier:
                return resource

    raise RuntimeError('cannot find {0} in {1}.sbs'.format(identifier, package_name))



def free_offset(graph):
    """Drop the new network clear of whatever is already in the graph."""
    lowest = None
    for node in graph.getNodes():
        position = node.getPosition()
        lowest = position.y if lowest is None else max(lowest, position.y)
    return 0.0 if lowest is None else lowest + 192.0


def main():
    app = sd.getContext().getSDApplication()
    graph = app.getUIMgr().getCurrentGraph()
    if graph is None:
        refuse(app, 'No compositing graph is open.',
               'These nodes go into whatever graph you are looking at, and there is nothing '
               'to look at. Open the graph you want them in, then run again.')
        return

    offset = free_offset(graph)
    nodes = {}

    # brushed -- anisotropic_noise_v2
    nodes['brushed'] = graph.newInstanceNode(find_resource(app, 'noise_anisotropic_noise_v2', 'anisotropic_noise_v2'))
    nodes['brushed'].setPosition(float2(0.0, offset + 0.0))
    nodes['brushed'].setInputPropertyValueFromId('X_Amount', SDValueInt.sNew(4))
    nodes['brushed'].setInputPropertyValueFromId('Y_Amount', SDValueInt.sNew(64))
    nodes['brushed'].setInputPropertyValueFromId('disorder', SDValueFloat.sNew(0.12))
    nodes['brushed'].setInputPropertyValueFromId('smoothness', SDValueFloat.sNew(1.5))

    # panels -- tile_generator
    nodes['panels'] = graph.newInstanceNode(find_resource(app, 'pattern_tile_generator', 'tile_generator'))
    nodes['panels'].setPosition(float2(0.0, offset + 144.0))
    nodes['panels'].setInputPropertyValueFromId('x_amount', SDValueInt.sNew(2))
    nodes['panels'].setInputPropertyValueFromId('y_amount', SDValueInt.sNew(3))
    nodes['panels'].setInputPropertyValueFromId('scale', SDValueFloat.sNew(0.97))

    # plate -- bevel
    nodes['plate'] = graph.newInstanceNode(find_resource(app, 'bevel', 'bevel'))
    nodes['plate'].setPosition(float2(192.0, offset + 360.0))
    nodes['plate'].setInputPropertyValueFromId('distance', SDValueFloat.sNew(0.01))
    nodes['plate'].setInputPropertyValueFromId('smoothing', SDValueFloat.sNew(4.0))
    nodes['plate'].setInputPropertyValueFromId('normal_intensity', SDValueFloat.sNew(1.0))

    # seams -- height_blend
    nodes['seams'] = graph.newInstanceNode(find_resource(app, 'height_blend', 'height_blend'))
    nodes['seams'].setPosition(float2(384.0, offset + 360.0))
    nodes['seams'].setInputPropertyValueFromId('height_position', SDValueFloat.sNew(0.5))
    nodes['seams'].setInputPropertyValueFromId('contrast', SDValueFloat.sNew(0.4))
    nodes['seams'].setInputPropertyValueFromId('opacity', SDValueFloat.sNew(0.65))

    # scratch -- scratches_generator
    nodes['scratch'] = graph.newInstanceNode(find_resource(app, 'pattern_scratches_generator', 'scratches_generator'))
    nodes['scratch'].setPosition(float2(0.0, offset + 288.0))
    nodes['scratch'].setInputPropertyValueFromId('spline_number', SDValueInt.sNew(48))
    nodes['scratch'].setInputPropertyValueFromId('spline_width', SDValueFloat.sNew(0.1))
    nodes['scratch'].setInputPropertyValueFromId('spline_distortion', SDValueFloat.sNew(0.3))

    # carve -- blend
    nodes['carve'] = graph.newNode('sbs::compositing::blend')
    nodes['carve'].setPosition(float2(576.0, offset + 360.0))
    nodes['carve'].setInputPropertyValueFromId('blendingmode', SDValueEnum.sFromValue('sbs::compositing::blendingmode', 2))
    nodes['carve'].setInputPropertyValueFromId('opacitymult', SDValueFloat.sNew(0.3))

    # erode -- slope_blur_grayscale_2
    nodes['erode'] = graph.newInstanceNode(find_resource(app, 'slope_blur', 'slope_blur_grayscale_2'))
    nodes['erode'].setPosition(float2(768.0, offset + 360.0))
    nodes['erode'].setInputPropertyValueFromId('Samples', SDValueInt.sNew(16))
    nodes['erode'].setInputPropertyValueFromId('Intensity', SDValueFloat.sNew(0.35))

    # tonormal -- normal
    nodes['tonormal'] = graph.newNode('sbs::compositing::normal')
    nodes['tonormal'].setPosition(float2(960.0, offset + 360.0))
    nodes['tonormal'].setInputPropertyValueFromId('intensity', SDValueFloat.sNew(6.0))

    # height -- output
    nodes['height'] = graph.newNode('sbs::compositing::output')
    nodes['height'].setPosition(float2(960.0, offset + 504.0))
    nodes['height'].setAnnotationPropertyValueFromId('identifier', SDValueString.sNew('height'))
    usages = SDValueArray.sNew(SDTypeUsage.sNew(), 0)
    usages.pushBack(SDValueUsage.sNew(SDUsage.sNew('height', 'RGBA', '')))
    nodes['height'].setAnnotationPropertyValueFromId('usages', usages)

    # normal -- output
    nodes['normal'] = graph.newNode('sbs::compositing::output')
    nodes['normal'].setPosition(float2(1152.0, offset + 432.0))
    nodes['normal'].setAnnotationPropertyValueFromId('identifier', SDValueString.sNew('normal'))
    usages = SDValueArray.sNew(SDTypeUsage.sNew(), 0)
    usages.pushBack(SDValueUsage.sNew(SDUsage.sNew('normal', 'RGBA', '')))
    nodes['normal'].setAnnotationPropertyValueFromId('usages', usages)

    # curv -- curvature_smooth_v2
    nodes['curv'] = graph.newInstanceNode(find_resource(app, 'curvature_smooth_v2', 'curvature_smooth_v2'))
    nodes['curv'].setPosition(float2(1152.0, offset + 288.0))
    nodes['curv'].setInputPropertyValueFromId('radius', SDValueFloat.sNew(1.5))

    # edges -- histogram_scan
    nodes['edges'] = graph.newInstanceNode(find_resource(app, 'histogram_scan', 'histogram_scan'))
    nodes['edges'].setPosition(float2(1344.0, offset + 360.0))
    nodes['edges'].setInputPropertyValueFromId('Position', SDValueFloat.sNew(0.62))
    nodes['edges'].setInputPropertyValueFromId('Contrast', SDValueFloat.sNew(0.8))

    # grunge -- grunge_scratches_rough
    nodes['grunge'] = graph.newInstanceNode(find_resource(app, 'grunge_scratches_rough', 'grunge_scratches_rough'))
    nodes['grunge'].setPosition(float2(0.0, offset + 432.0))
    nodes['grunge'].setInputPropertyValueFromId('scratch_quantity', SDValueFloat.sNew(0.45))
    nodes['grunge'].setInputPropertyValueFromId('Contrast', SDValueFloat.sNew(0.3))

    # chip -- blend
    nodes['chip'] = graph.newNode('sbs::compositing::blend')
    nodes['chip'].setPosition(float2(1536.0, offset + 360.0))
    nodes['chip'].setInputPropertyValueFromId('blendingmode', SDValueEnum.sFromValue('sbs::compositing::blendingmode', 5))
    nodes['chip'].setInputPropertyValueFromId('opacitymult', SDValueFloat.sNew(0.75))

    # paint -- uniform
    nodes['paint'] = graph.newNode('sbs::compositing::uniform')
    nodes['paint'].setPosition(float2(0.0, offset + 576.0))
    nodes['paint'].setInputPropertyValueFromId('colorswitch', SDValueBool.sNew(True))
    nodes['paint'].setInputPropertyValueFromId('outputcolor', SDValueColorRGBA.sNew(ColorRGBA(0.09, 0.25, 0.28, 1.0)))

    # steel -- uniform
    nodes['steel'] = graph.newNode('sbs::compositing::uniform')
    nodes['steel'].setPosition(float2(0.0, offset + 720.0))
    nodes['steel'].setInputPropertyValueFromId('colorswitch', SDValueBool.sNew(True))
    nodes['steel'].setInputPropertyValueFromId('outputcolor', SDValueColorRGBA.sNew(ColorRGBA(0.44, 0.42, 0.4, 1.0)))

    # albedo -- blend
    nodes['albedo'] = graph.newNode('sbs::compositing::blend')
    nodes['albedo'].setPosition(float2(1728.0, offset + 288.0))

    # basecolor -- output
    nodes['basecolor'] = graph.newNode('sbs::compositing::output')
    nodes['basecolor'].setPosition(float2(1920.0, offset + 288.0))
    nodes['basecolor'].setAnnotationPropertyValueFromId('identifier', SDValueString.sNew('basecolor'))
    usages = SDValueArray.sNew(SDTypeUsage.sNew(), 0)
    usages.pushBack(SDValueUsage.sNew(SDUsage.sNew('baseColor', 'RGBA', '')))
    nodes['basecolor'].setAnnotationPropertyValueFromId('usages', usages)

    # roughbase -- levels
    nodes['roughbase'] = graph.newNode('sbs::compositing::levels')
    nodes['roughbase'].setPosition(float2(960.0, offset + 216.0))
    nodes['roughbase'].setInputPropertyValueFromId('levelinlow', SDValueFloat4.sNew(float4(0.3, 0.3, 0.3, 0.0)))
    nodes['roughbase'].setInputPropertyValueFromId('levelinhigh', SDValueFloat4.sNew(float4(0.75, 0.75, 0.75, 1.0)))

    # roughmix -- blend
    nodes['roughmix'] = graph.newNode('sbs::compositing::blend')
    nodes['roughmix'].setPosition(float2(1728.0, offset + 432.0))
    nodes['roughmix'].setInputPropertyValueFromId('opacitymult', SDValueFloat.sNew(0.9))

    # roughness -- output
    nodes['roughness'] = graph.newNode('sbs::compositing::output')
    nodes['roughness'].setPosition(float2(1920.0, offset + 432.0))
    nodes['roughness'].setAnnotationPropertyValueFromId('identifier', SDValueString.sNew('roughness'))
    usages = SDValueArray.sNew(SDTypeUsage.sNew(), 0)
    usages.pushBack(SDValueUsage.sNew(SDUsage.sNew('roughness', 'RGBA', '')))
    nodes['roughness'].setAnnotationPropertyValueFromId('usages', usages)

    nodes['panels'].newPropertyConnectionFromId('output', nodes['plate'], 'input')
    nodes['plate'].newPropertyConnectionFromId('height', nodes['seams'], 'height_bottom')
    nodes['brushed'].newPropertyConnectionFromId('output', nodes['seams'], 'height_top')
    nodes['seams'].newPropertyConnectionFromId('blended_height', nodes['carve'], 'destination')
    nodes['scratch'].newPropertyConnectionFromId('output', nodes['carve'], 'source')
    nodes['carve'].newPropertyConnectionFromId('unique_filter_output', nodes['erode'], 'Source')
    nodes['scratch'].newPropertyConnectionFromId('output', nodes['erode'], 'Effect')
    nodes['erode'].newPropertyConnectionFromId('Slope_Blur', nodes['height'], 'inputNodeOutput')
    nodes['erode'].newPropertyConnectionFromId('Slope_Blur', nodes['tonormal'], 'input1')
    nodes['tonormal'].newPropertyConnectionFromId('unique_filter_output', nodes['normal'], 'inputNodeOutput')
    nodes['tonormal'].newPropertyConnectionFromId('unique_filter_output', nodes['curv'], 'input')
    nodes['curv'].newPropertyConnectionFromId('curvature', nodes['edges'], 'Input_1')
    nodes['edges'].newPropertyConnectionFromId('Output', nodes['chip'], 'destination')
    nodes['grunge'].newPropertyConnectionFromId('output', nodes['chip'], 'source')
    nodes['paint'].newPropertyConnectionFromId('unique_filter_output', nodes['albedo'], 'destination')
    nodes['steel'].newPropertyConnectionFromId('unique_filter_output', nodes['albedo'], 'source')
    nodes['chip'].newPropertyConnectionFromId('unique_filter_output', nodes['albedo'], 'opacity')
    nodes['albedo'].newPropertyConnectionFromId('unique_filter_output', nodes['basecolor'], 'inputNodeOutput')
    nodes['erode'].newPropertyConnectionFromId('Slope_Blur', nodes['roughbase'], 'input1')
    nodes['roughbase'].newPropertyConnectionFromId('unique_filter_output', nodes['roughmix'], 'destination')
    nodes['grunge'].newPropertyConnectionFromId('output', nodes['roughmix'], 'source')
    nodes['chip'].newPropertyConnectionFromId('unique_filter_output', nodes['roughmix'], 'opacity')
    nodes['roughmix'].newPropertyConnectionFromId('unique_filter_output', nodes['roughness'], 'inputNodeOutput')

    print('added {0} nodes and {1} wires'.format(21, 23))


main()

Notes

Pin colours are this page’s, not Designer’s — the canvas palette lives in the application binary, not in any readable resource. They separate what a pin carries: a colour texture, a grayscale one, either, or a plain number.

Built-in filters accept whichever of colour or grayscale the graph is already carrying, so their pins are drawn as either rather than guessed at.