Skip to content

Expose API + Graphing Utilities for Shader Generator Graphs #2790

Open
kwokcb wants to merge 14 commits into
AcademySoftwareFoundation:mainfrom
kwokcb:graph_genshader_graphs
Open

Expose API + Graphing Utilities for Shader Generator Graphs #2790
kwokcb wants to merge 14 commits into
AcademySoftwareFoundation:mainfrom
kwokcb:graph_genshader_graphs

Conversation

@kwokcb

@kwokcb kwokcb commented Feb 23, 2026

Copy link
Copy Markdown
Contributor

API Exposure Change

The graphs produced by shader generation are hard to debug / visualize as they are using a different data model.

The proposed change here is to be able to create a graph diagram using Python or Javascript by
exposing more of the internal C++ API.

As an example usage Mermaid graph generation is provided as a utility.

API Modifications

  • Add Python and Javascript wrappers ShaderNode, ShaderInput and ShaderOutput
  • Add access to get graph nodes from Shader
  • Add graph creation utility in Python inside of generateshader.py.
  • Add graph creation utility in Javascript as a JS module utility function

Tests

  • Python graph generation added to CI tests. (see some examples below)
  • Javascript graph generation add to unit tests.
Running 1 test using 1 worker

Generating shader for essl...
Shader graph diagram generated.    # New graph gen test
Generating shader for genglsl...
Generating shader for genmsl...

Sample Results

( Markdown wrappers are added as part of generateshader.py )

e.g. Velvet (no input expansion)

graph TB
    geomprop_Nworld["geomprop_Nworld (TEXTURE|GEOMETRIC)"]
    style geomprop_Nworld fill:#4B0336,stroke:#222222,stroke-width:1px,color:#ffffff
    geomprop_Tworld["geomprop_Tworld (TEXTURE|GEOMETRIC)"]
    style geomprop_Tworld fill:#4B0336,stroke:#222222,stroke-width:1px,color:#ffffff
    SR_velvet["SR_velvet (SHADER|SURFACE|CLOSURE)"]
    style SR_velvet fill:#0e7865,stroke:#222222,stroke-width:1px,color:#ffffff
    Velvet["Velvet (MATERIAL|SHADER|SURFACE|CLOSURE)"]
    style Velvet fill:#7ccba2,stroke:#222222,stroke-width:1px,color:#ffffff
    geomprop_Nworld --"out --> coat_normal"--> SR_velvet
    geomprop_Nworld --"out --> normal"--> SR_velvet
    geomprop_Tworld --"out --> tangent"--> SR_velvet
    SR_velvet --"out --> surfaceshader"--> Velvet
Loading

e.g. Tiled Brass (input expansion)

graph LR
    geomprop_Nworld["geomprop_Nworld (TEXTURE|GEOMETRIC)"]
    style geomprop_Nworld fill:#4B0336,stroke:#222222,stroke-width:1px,color:#ffffff
    geomprop_Tworld["geomprop_Tworld (TEXTURE|GEOMETRIC)"]
    style geomprop_Tworld fill:#4B0336,stroke:#222222,stroke-width:1px,color:#ffffff
    geomprop_UV0["geomprop_UV0 (TEXTURE|GEOMETRIC)"]
    style geomprop_UV0 fill:#4B0336,stroke:#222222,stroke-width:1px,color:#ffffff
    NG_brass1/image_roughness["NG_brass1/image_roughness (TEXTURE|FILETEXTURE|SAMPLE2D)"]
    style NG_brass1/image_roughness fill:#be5ae6,stroke:#222222,stroke-width:1px,color:#ffffff
    NG_brass1/image_color["NG_brass1/image_color (TEXTURE|FILETEXTURE|SAMPLE2D)"]
    style NG_brass1/image_color fill:#be5ae6,stroke:#222222,stroke-width:1px,color:#ffffff
    image_color_out_cm["image_color_out_cm (TEXTURE)"]
    style image_color_out_cm fill:#c27ff8,stroke:#222222,stroke-width:1px,color:#ffffff
    SR_brass1["SR_brass1 (SHADER|SURFACE|CLOSURE)"]
    style SR_brass1 fill:#0e7865,stroke:#222222,stroke-width:1px,color:#ffffff
    Tiled_Brass["Tiled_Brass (MATERIAL|SHADER|SURFACE|CLOSURE)"]
    style Tiled_Brass fill:#7ccba2,stroke:#222222,stroke-width:1px,color:#ffffff
    geomprop_Nworld --"out --> coat_normal"--> SR_brass1
    geomprop_Nworld --"out --> normal"--> SR_brass1
    geomprop_Nworld/space["space = 2"] --> geomprop_Nworld
    geomprop_Tworld --"out --> tangent"--> SR_brass1
    geomprop_Tworld/space["space = 2"] --> geomprop_Tworld
    geomprop_Tworld/index["index = 0"] --> geomprop_Tworld
    geomprop_UV0 --"out --> texcoord"--> NG_brass1/image_roughness
    geomprop_UV0 --"out --> texcoord"--> NG_brass1/image_color
    geomprop_UV0/index["index = 0"] --> geomprop_UV0
    NG_brass1/image_roughness --"out --> specular_roughness"--> SR_brass1
    NG_brass1/image_roughness --"out --> coat_roughness"--> SR_brass1
    NG_brass1/image_roughness/file["file = ../../../Images/brass_roughness.jpg"] --> NG_brass1/image_roughness
    NG_brass1/image_roughness/default["default = 0"] --> NG_brass1/image_roughness
    NG_brass1/image_roughness/uvtiling["uvtiling = 1, 1"] --> NG_brass1/image_roughness
    NG_brass1/image_roughness/uvoffset["uvoffset = 0, 0"] --> NG_brass1/image_roughness
    NG_brass1/image_roughness/realworldimagesize["realworldimagesize = 1, 1"] --> NG_brass1/image_roughness
    NG_brass1/image_roughness/realworldtilesize["realworldtilesize = 1, 1"] --> NG_brass1/image_roughness
    NG_brass1/image_roughness/filtertype["filtertype = 1"] --> NG_brass1/image_roughness
    NG_brass1/image_roughness/frameoffset["frameoffset = 0"] --> NG_brass1/image_roughness
    NG_brass1/image_roughness/frameendaction["frameendaction = 0"] --> NG_brass1/image_roughness
    NG_brass1/image_color --"out --> in"--> image_color_out_cm
    NG_brass1/image_color/file["file = ../../../Images/brass_color.jpg"] --> NG_brass1/image_color
    NG_brass1/image_color/default["default = 0, 0, 0"] --> NG_brass1/image_color
    NG_brass1/image_color/uvtiling["uvtiling = 1, 1"] --> NG_brass1/image_color
    NG_brass1/image_color/uvoffset["uvoffset = 0, 0"] --> NG_brass1/image_color
    NG_brass1/image_color/realworldimagesize["realworldimagesize = 1, 1"] --> NG_brass1/image_color
    NG_brass1/image_color/realworldtilesize["realworldtilesize = 1, 1"] --> NG_brass1/image_color
    NG_brass1/image_color/filtertype["filtertype = 1"] --> NG_brass1/image_color
    NG_brass1/image_color/frameoffset["frameoffset = 0"] --> NG_brass1/image_color
    NG_brass1/image_color/frameendaction["frameendaction = 0"] --> NG_brass1/image_color
    image_color_out_cm --"out --> coat_color"--> SR_brass1
    image_color_out_cm/in["in = 0, 0, 0"] --> image_color_out_cm
    SR_brass1 --"out --> surfaceshader"--> Tiled_Brass
    SR_brass1/base["base = 1"] --> SR_brass1
    SR_brass1/base_color["base_color = 1, 1, 1"] --> SR_brass1
    SR_brass1/diffuse_roughness["diffuse_roughness = 0"] --> SR_brass1
    SR_brass1/metalness["metalness = 1"] --> SR_brass1
    SR_brass1/specular["specular = 0"] --> SR_brass1
    SR_brass1/specular_color["specular_color = 1, 1, 1"] --> SR_brass1
    SR_brass1/specular_roughness["specular_roughness = 0.2"] --> SR_brass1
    SR_brass1/specular_IOR["specular_IOR = 1.5"] --> SR_brass1
    SR_brass1/specular_anisotropy["specular_anisotropy = 0"] --> SR_brass1
    SR_brass1/specular_rotation["specular_rotation = 0"] --> SR_brass1
    SR_brass1/transmission["transmission = 0"] --> SR_brass1
    SR_brass1/transmission_color["transmission_color = 1, 1, 1"] --> SR_brass1
    SR_brass1/transmission_depth["transmission_depth = 0"] --> SR_brass1
    SR_brass1/transmission_scatter["transmission_scatter = 0, 0, 0"] --> SR_brass1
    SR_brass1/transmission_scatter_anisotropy["transmission_scatter_anisotropy = 0"] --> SR_brass1
    SR_brass1/transmission_dispersion["transmission_dispersion = 0"] --> SR_brass1
    SR_brass1/transmission_extra_roughness["transmission_extra_roughness = 0"] --> SR_brass1
    SR_brass1/subsurface["subsurface = 0"] --> SR_brass1
    SR_brass1/subsurface_color["subsurface_color = 1, 1, 1"] --> SR_brass1
    SR_brass1/subsurface_radius["subsurface_radius = 1, 1, 1"] --> SR_brass1
    SR_brass1/subsurface_scale["subsurface_scale = 1"] --> SR_brass1
    SR_brass1/subsurface_anisotropy["subsurface_anisotropy = 0"] --> SR_brass1
    SR_brass1/sheen["sheen = 0"] --> SR_brass1
    SR_brass1/sheen_color["sheen_color = 1, 1, 1"] --> SR_brass1
    SR_brass1/sheen_roughness["sheen_roughness = 0.3"] --> SR_brass1
    SR_brass1/coat["coat = 1"] --> SR_brass1
    SR_brass1/coat_color["coat_color = 1, 1, 1"] --> SR_brass1
    SR_brass1/coat_roughness["coat_roughness = 0.1"] --> SR_brass1
    SR_brass1/coat_anisotropy["coat_anisotropy = 0"] --> SR_brass1
    SR_brass1/coat_rotation["coat_rotation = 0"] --> SR_brass1
    SR_brass1/coat_IOR["coat_IOR = 1.5"] --> SR_brass1
    SR_brass1/coat_affect_color["coat_affect_color = 0"] --> SR_brass1
    SR_brass1/coat_affect_roughness["coat_affect_roughness = 0"] --> SR_brass1
    SR_brass1/thin_film_thickness["thin_film_thickness = 0"] --> SR_brass1
    SR_brass1/thin_film_IOR["thin_film_IOR = 1.5"] --> SR_brass1
    SR_brass1/emission["emission = 0"] --> SR_brass1
    SR_brass1/emission_color["emission_color = 1, 1, 1"] --> SR_brass1
    SR_brass1/opacity["opacity = 1, 1, 1"] --> SR_brass1
    SR_brass1/thin_walled["thin_walled = false"] --> SR_brass1
Loading

Visualize input sockets (exposed uniforms) vs internal nodes for BoomBox:

  • Reduced interface (less expose uniforms)
graph LR
    geomprop_Tworld["geomprop_Tworld (TEXTURE|GEOMETRIC)"]
    style geomprop_Tworld fill:#4B0349,stroke:#222222,stroke-width:1px,color:#ffffff
    geomprop_Nworld["geomprop_Nworld (TEXTURE|GEOMETRIC)"]
    style geomprop_Nworld fill:#4B0349,stroke:#222222,stroke-width:1px,color:#ffffff
    geomprop_UV0["geomprop_UV0 (TEXTURE|GEOMETRIC)"]
    style geomprop_UV0 fill:#4B0349,stroke:#222222,stroke-width:1px,color:#ffffff
    image_basecolor["image_basecolor (TEXTURE|FILETEXTURE|SAMPLE2D)"]
    style image_basecolor fill:#3d174c,stroke:#222222,stroke-width:1px,color:#ffffff
    image_orm["image_orm (TEXTURE|FILETEXTURE|SAMPLE2D)"]
    style image_orm fill:#3d174c,stroke:#222222,stroke-width:1px,color:#ffffff
    image_normal["image_normal (TEXTURE|FILETEXTURE|SAMPLE2D)"]
    style image_normal fill:#3d174c,stroke:#222222,stroke-width:1px,color:#ffffff
    image_emission["image_emission (TEXTURE|FILETEXTURE|SAMPLE2D)"]
    style image_emission fill:#3d174c,stroke:#222222,stroke-width:1px,color:#ffffff
    image_basecolor_outcolor_cm["image_basecolor_outcolor_cm (TEXTURE)"]
    style image_basecolor_outcolor_cm fill:#6b5e76,stroke:#222222,stroke-width:1px,color:#ffffff
    image_orm_channels["image_orm_channels (TEXTURE)"]
    style image_orm_channels fill:#6b5e76,stroke:#222222,stroke-width:1px,color:#ffffff
    image_emission_out_cm["image_emission_out_cm (TEXTURE)"]
    style image_emission_out_cm fill:#6b5e76,stroke:#222222,stroke-width:1px,color:#ffffff
    SR_boombox["SR_boombox (SHADER|SURFACE|CLOSURE)"]
    style SR_boombox fill:#0e7865,stroke:#222222,stroke-width:1px,color:#ffffff
    Material_boombox["Material_boombox (MATERIAL|SHADER|SURFACE|CLOSURE)"]
    style Material_boombox fill:#7ccba2,stroke:#222222,stroke-width:1px,color:#ffffff
    geomprop_Tworld --"out --> tangent"--> SR_boombox
    geomprop_Tworld/space["space = 2"] --"space"--> geomprop_Tworld
    geomprop_Tworld/index["index = 0"] --"index"--> geomprop_Tworld
    geomprop_Nworld --"out --> clearcoat_normal"--> SR_boombox
    geomprop_Nworld/space["space = 2"] --"space"--> geomprop_Nworld
    geomprop_UV0 --"out --> texcoord"--> image_basecolor
    geomprop_UV0 --"out --> texcoord"--> image_orm
    geomprop_UV0 --"out --> texcoord"--> image_normal
    geomprop_UV0 --"out --> texcoord"--> image_emission
    geomprop_UV0/index["index = 0"] --"index"--> geomprop_UV0
    image_basecolor --"outcolor --> in"--> image_basecolor_outcolor_cm
    image_basecolor --"outa --> alpha"--> SR_boombox
    image_basecolor/file["file = image_basecolor_file"] --"file"--> image_basecolor
    image_basecolor/default["default = 0, 0, 0, 0"] --"default"--> image_basecolor
    image_basecolor/pivot["pivot = 0, 1"] --"pivot"--> image_basecolor
    image_basecolor/scale["scale = 1, 1"] --"scale"--> image_basecolor
    image_basecolor/rotate["rotate = 0"] --"rotate"--> image_basecolor
    image_basecolor/offset["offset = 0, 0"] --"offset"--> image_basecolor
    image_basecolor/operationorder["operationorder = 1"] --"operationorder"--> image_basecolor
    image_basecolor/uaddressmode["uaddressmode = 2"] --"uaddressmode"--> image_basecolor
    image_basecolor/vaddressmode["vaddressmode = 2"] --"vaddressmode"--> image_basecolor
    image_basecolor/filtertype["filtertype = 1"] --"filtertype"--> image_basecolor
    image_basecolor/color["color = 1, 1, 1, 1"] --"color"--> image_basecolor
    image_basecolor/geomcolor["geomcolor = 1, 1, 1, 1"] --"geomcolor"--> image_basecolor
    image_orm --"out --> in"--> image_orm_channels
    image_orm/file["file = image_orm_file"] --"file"--> image_orm
    image_orm/default["default = 0, 0, 0"] --"default"--> image_orm
    image_orm/pivot["pivot = 0, 1"] --"pivot"--> image_orm
    image_orm/scale["scale = 1, 1"] --"scale"--> image_orm
    image_orm/rotate["rotate = 0"] --"rotate"--> image_orm
    image_orm/offset["offset = 0, 0"] --"offset"--> image_orm
    image_orm/operationorder["operationorder = 0"] --"operationorder"--> image_orm
    image_orm/uaddressmode["uaddressmode = 2"] --"uaddressmode"--> image_orm
    image_orm/vaddressmode["vaddressmode = 2"] --"vaddressmode"--> image_orm
    image_orm/filtertype["filtertype = 1"] --"filtertype"--> image_orm
    image_normal --"out --> normal"--> SR_boombox
    image_normal/file["file = image_normal_file"] --"file"--> image_normal
    image_normal/default["default = 0.5, 0.5, 1"] --"default"--> image_normal
    image_normal/pivot["pivot = 0, 1"] --"pivot"--> image_normal
    image_normal/scale["scale = 1, 1"] --"scale"--> image_normal
    image_normal/rotate["rotate = 0"] --"rotate"--> image_normal
    image_normal/offset["offset = 0, 0"] --"offset"--> image_normal
    image_normal/operationorder["operationorder = 0"] --"operationorder"--> image_normal
    image_normal/uaddressmode["uaddressmode = 2"] --"uaddressmode"--> image_normal
    image_normal/vaddressmode["vaddressmode = 2"] --"vaddressmode"--> image_normal
    image_normal/filtertype["filtertype = 1"] --"filtertype"--> image_normal
    image_emission --"out --> in"--> image_emission_out_cm
    image_emission/file["file = image_emission_file"] --"file"--> image_emission
    image_emission/factor["factor = 1, 1, 1"] --"factor"--> image_emission
    image_emission/default["default = 0, 0, 0"] --"default"--> image_emission
    image_emission/pivot["pivot = 0, 1"] --"pivot"--> image_emission
    image_emission/scale["scale = 1, 1"] --"scale"--> image_emission
    image_emission/rotate["rotate = 0"] --"rotate"--> image_emission
    image_emission/offset["offset = 0, 0"] --"offset"--> image_emission
    image_emission/operationorder["operationorder = 0"] --"operationorder"--> image_emission
    image_emission/uaddressmode["uaddressmode = 2"] --"uaddressmode"--> image_emission
    image_emission/vaddressmode["vaddressmode = 2"] --"vaddressmode"--> image_emission
    image_emission/filtertype["filtertype = 1"] --"filtertype"--> image_emission
    image_basecolor_outcolor_cm --"out --> base_color"--> SR_boombox
    image_basecolor_outcolor_cm/in["image_basecolor_outcolor = 0, 0, 0"] --"in"--> image_basecolor_outcolor_cm
    image_orm_channels --"outx --> occlusion"--> SR_boombox
    image_orm_channels --"outy --> roughness"--> SR_boombox
    image_orm_channels --"outz --> metallic"--> SR_boombox
    image_orm_channels/in["image_orm_out = 0, 0, 0"] --"in"--> image_orm_channels
    image_emission_out_cm --"out --> emissive"--> SR_boombox
    image_emission_out_cm/in["image_emission_out = 0, 0, 0"] --"in"--> image_emission_out_cm
    SR_boombox --"out --> surfaceshader"--> Material_boombox
    SR_boombox/base_color["image_basecolor_outcolor_cm_out = 1, 1, 1"] --"base_color"--> SR_boombox
    SR_boombox/metallic["image_orm_channels_outz = 1"] --"metallic"--> SR_boombox
    SR_boombox/roughness["image_orm_channels_outy = 1"] --"roughness"--> SR_boombox
    SR_boombox/occlusion["image_orm_channels_outx = 1"] --"occlusion"--> SR_boombox
    SR_boombox/transmission["transmission = 0"] --"transmission"--> SR_boombox
    SR_boombox/specular["specular = 1"] --"specular"--> SR_boombox
    SR_boombox/specular_color["specular_color = 1, 1, 1"] --"specular_color"--> SR_boombox
    SR_boombox/ior["ior = 1.5"] --"ior"--> SR_boombox
    SR_boombox/alpha["image_basecolor_outa = 1"] --"alpha"--> SR_boombox
    SR_boombox/alpha_mode["alpha_mode = 0"] --"alpha_mode"--> SR_boombox
    SR_boombox/alpha_cutoff["alpha_cutoff = 0.5"] --"alpha_cutoff"--> SR_boombox
    SR_boombox/iridescence["iridescence = 0"] --"iridescence"--> SR_boombox
    SR_boombox/iridescence_ior["iridescence_ior = 1.3"] --"iridescence_ior"--> SR_boombox
    SR_boombox/iridescence_thickness["iridescence_thickness = 100"] --"iridescence_thickness"--> SR_boombox
    SR_boombox/sheen_color["sheen_color = 0, 0, 0"] --"sheen_color"--> SR_boombox
    SR_boombox/sheen_roughness["sheen_roughness = 0"] --"sheen_roughness"--> SR_boombox
    SR_boombox/clearcoat["clearcoat = 0"] --"clearcoat"--> SR_boombox
    SR_boombox/clearcoat_roughness["clearcoat_roughness = 0"] --"clearcoat_roughness"--> SR_boombox
    SR_boombox/emissive["image_emission_out_cm_out = 0, 0, 0"] --"emissive"--> SR_boombox
    SR_boombox/emissive_strength["emissive_strength = 1"] --"emissive_strength"--> SR_boombox
    SR_boombox/thickness["thickness = 0"] --"thickness"--> SR_boombox
    SR_boombox/attenuation_color["attenuation_color = 1, 1, 1"] --"attenuation_color"--> SR_boombox
    SR_boombox/anisotropy_strength["anisotropy_strength = 0"] --"anisotropy_strength"--> SR_boombox
    SR_boombox/anisotropy_rotation["anisotropy_rotation = 0"] --"anisotropy_rotation"--> SR_boombox
    SR_boombox/dispersion["dispersion = 0"] --"dispersion"--> SR_boombox
Loading

versus Full interface (expose more uniforms)

graph LR
    geomprop_Tworld["geomprop_Tworld (TEXTURE|GEOMETRIC)"]
    style geomprop_Tworld fill:#4B0349,stroke:#222222,stroke-width:1px,color:#ffffff
    geomprop_Nworld["geomprop_Nworld (TEXTURE|GEOMETRIC)"]
    style geomprop_Nworld fill:#4B0349,stroke:#222222,stroke-width:1px,color:#ffffff
    geomprop_UV0["geomprop_UV0 (TEXTURE|GEOMETRIC)"]
    style geomprop_UV0 fill:#4B0349,stroke:#222222,stroke-width:1px,color:#ffffff
    image_basecolor["image_basecolor (TEXTURE|FILETEXTURE|SAMPLE2D)"]
    style image_basecolor fill:#3d174c,stroke:#222222,stroke-width:1px,color:#ffffff
    image_orm["image_orm (TEXTURE|FILETEXTURE|SAMPLE2D)"]
    style image_orm fill:#3d174c,stroke:#222222,stroke-width:1px,color:#ffffff
    image_normal["image_normal (TEXTURE|FILETEXTURE|SAMPLE2D)"]
    style image_normal fill:#3d174c,stroke:#222222,stroke-width:1px,color:#ffffff
    image_emission["image_emission (TEXTURE|FILETEXTURE|SAMPLE2D)"]
    style image_emission fill:#3d174c,stroke:#222222,stroke-width:1px,color:#ffffff
    image_basecolor_outcolor_cm["image_basecolor_outcolor_cm (TEXTURE)"]
    style image_basecolor_outcolor_cm fill:#6b5e76,stroke:#222222,stroke-width:1px,color:#ffffff
    image_orm_channels["image_orm_channels (TEXTURE)"]
    style image_orm_channels fill:#6b5e76,stroke:#222222,stroke-width:1px,color:#ffffff
    image_emission_out_cm["image_emission_out_cm (TEXTURE)"]
    style image_emission_out_cm fill:#6b5e76,stroke:#222222,stroke-width:1px,color:#ffffff
    SR_boombox["SR_boombox (SHADER|SURFACE|CLOSURE)"]
    style SR_boombox fill:#0e7865,stroke:#222222,stroke-width:1px,color:#ffffff
    Material_boombox["Material_boombox (MATERIAL|SHADER|SURFACE|CLOSURE)"]
    style Material_boombox fill:#7ccba2,stroke:#222222,stroke-width:1px,color:#ffffff
    geomprop_Tworld --"out --> tangent"--> SR_boombox
    geomprop_Tworld/space["space = 2"] --"space"--> geomprop_Tworld
    geomprop_Tworld/index["index = 0"] --"index"--> geomprop_Tworld
    geomprop_Nworld --"out --> clearcoat_normal"--> SR_boombox
    geomprop_Nworld/space["space = 2"] --"space"--> geomprop_Nworld
    geomprop_UV0 --"out --> texcoord"--> image_basecolor
    geomprop_UV0 --"out --> texcoord"--> image_orm
    geomprop_UV0 --"out --> texcoord"--> image_normal
    geomprop_UV0 --"out --> texcoord"--> image_emission
    geomprop_UV0/index["geomprop_UV0_index = 0 [SOCKET]"] --"index"--> geomprop_UV0
    style geomprop_UV0/index fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_basecolor --"outcolor --> in"--> image_basecolor_outcolor_cm
    image_basecolor --"outa --> alpha"--> SR_boombox
    image_basecolor/file["image_basecolor_file = boombox/BoomBox_baseColor.png [SOCKET]"] --"file"--> image_basecolor
    style image_basecolor/file fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_basecolor/default["image_basecolor_default = 0, 0, 0, 0 [SOCKET]"] --"default"--> image_basecolor
    style image_basecolor/default fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_basecolor/pivot["image_basecolor_pivot = 0, 1 [SOCKET]"] --"pivot"--> image_basecolor
    style image_basecolor/pivot fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_basecolor/scale["image_basecolor_scale = 1, 1 [SOCKET]"] --"scale"--> image_basecolor
    style image_basecolor/scale fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_basecolor/rotate["image_basecolor_rotate = 0 [SOCKET]"] --"rotate"--> image_basecolor
    style image_basecolor/rotate fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_basecolor/offset["image_basecolor_offset = 0, 0 [SOCKET]"] --"offset"--> image_basecolor
    style image_basecolor/offset fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_basecolor/operationorder["image_basecolor_operationorder = 1 [SOCKET]"] --"operationorder"--> image_basecolor
    style image_basecolor/operationorder fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_basecolor/uaddressmode["image_basecolor_uaddressmode = 2 [SOCKET]"] --"uaddressmode"--> image_basecolor
    style image_basecolor/uaddressmode fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_basecolor/vaddressmode["image_basecolor_vaddressmode = 2 [SOCKET]"] --"vaddressmode"--> image_basecolor
    style image_basecolor/vaddressmode fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_basecolor/filtertype["image_basecolor_filtertype = 1 [SOCKET]"] --"filtertype"--> image_basecolor
    style image_basecolor/filtertype fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_basecolor/color["image_basecolor_color = 1, 1, 1, 1 [SOCKET]"] --"color"--> image_basecolor
    style image_basecolor/color fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_basecolor/geomcolor["image_basecolor_geomcolor = 1, 1, 1, 1 [SOCKET]"] --"geomcolor"--> image_basecolor
    style image_basecolor/geomcolor fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_orm --"out --> in"--> image_orm_channels
    image_orm/file["image_orm_file = boombox/BoomBox_occlusionRoughnessMetallic.png [SOCKET]"] --"file"--> image_orm
    style image_orm/file fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_orm/default["image_orm_default = 0, 0, 0 [SOCKET]"] --"default"--> image_orm
    style image_orm/default fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_orm/pivot["image_orm_pivot = 0, 1 [SOCKET]"] --"pivot"--> image_orm
    style image_orm/pivot fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_orm/scale["image_orm_scale = 1, 1 [SOCKET]"] --"scale"--> image_orm
    style image_orm/scale fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_orm/rotate["image_orm_rotate = 0 [SOCKET]"] --"rotate"--> image_orm
    style image_orm/rotate fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_orm/offset["image_orm_offset = 0, 0 [SOCKET]"] --"offset"--> image_orm
    style image_orm/offset fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_orm/operationorder["image_orm_operationorder = 0 [SOCKET]"] --"operationorder"--> image_orm
    style image_orm/operationorder fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_orm/uaddressmode["image_orm_uaddressmode = 2 [SOCKET]"] --"uaddressmode"--> image_orm
    style image_orm/uaddressmode fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_orm/vaddressmode["image_orm_vaddressmode = 2 [SOCKET]"] --"vaddressmode"--> image_orm
    style image_orm/vaddressmode fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_orm/filtertype["image_orm_filtertype = 1 [SOCKET]"] --"filtertype"--> image_orm
    style image_orm/filtertype fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_normal --"out --> normal"--> SR_boombox
    image_normal/file["image_normal_file = boombox/BoomBox_normal.png [SOCKET]"] --"file"--> image_normal
    style image_normal/file fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_normal/default["image_normal_default = 0.5, 0.5, 1 [SOCKET]"] --"default"--> image_normal
    style image_normal/default fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_normal/pivot["image_normal_pivot = 0, 1 [SOCKET]"] --"pivot"--> image_normal
    style image_normal/pivot fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_normal/scale["image_normal_scale = 1, 1 [SOCKET]"] --"scale"--> image_normal
    style image_normal/scale fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_normal/rotate["image_normal_rotate = 0 [SOCKET]"] --"rotate"--> image_normal
    style image_normal/rotate fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_normal/offset["image_normal_offset = 0, 0 [SOCKET]"] --"offset"--> image_normal
    style image_normal/offset fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_normal/operationorder["image_normal_operationorder = 0 [SOCKET]"] --"operationorder"--> image_normal
    style image_normal/operationorder fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_normal/uaddressmode["image_normal_uaddressmode = 2 [SOCKET]"] --"uaddressmode"--> image_normal
    style image_normal/uaddressmode fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_normal/vaddressmode["image_normal_vaddressmode = 2 [SOCKET]"] --"vaddressmode"--> image_normal
    style image_normal/vaddressmode fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_normal/filtertype["image_normal_filtertype = 1 [SOCKET]"] --"filtertype"--> image_normal
    style image_normal/filtertype fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_emission --"out --> in"--> image_emission_out_cm
    image_emission/file["image_emission_file = boombox/BoomBox_emissive.png [SOCKET]"] --"file"--> image_emission
    style image_emission/file fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_emission/factor["image_emission_factor = 1, 1, 1 [SOCKET]"] --"factor"--> image_emission
    style image_emission/factor fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_emission/default["image_emission_default = 0, 0, 0 [SOCKET]"] --"default"--> image_emission
    style image_emission/default fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_emission/pivot["image_emission_pivot = 0, 1 [SOCKET]"] --"pivot"--> image_emission
    style image_emission/pivot fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_emission/scale["image_emission_scale = 1, 1 [SOCKET]"] --"scale"--> image_emission
    style image_emission/scale fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_emission/rotate["image_emission_rotate = 0 [SOCKET]"] --"rotate"--> image_emission
    style image_emission/rotate fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_emission/offset["image_emission_offset = 0, 0 [SOCKET]"] --"offset"--> image_emission
    style image_emission/offset fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_emission/operationorder["image_emission_operationorder = 0 [SOCKET]"] --"operationorder"--> image_emission
    style image_emission/operationorder fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_emission/uaddressmode["image_emission_uaddressmode = 2 [SOCKET]"] --"uaddressmode"--> image_emission
    style image_emission/uaddressmode fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_emission/vaddressmode["image_emission_vaddressmode = 2 [SOCKET]"] --"vaddressmode"--> image_emission
    style image_emission/vaddressmode fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_emission/filtertype["image_emission_filtertype = 1 [SOCKET]"] --"filtertype"--> image_emission
    style image_emission/filtertype fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    image_basecolor_outcolor_cm --"out --> base_color"--> SR_boombox
    image_basecolor_outcolor_cm/in["image_basecolor_outcolor = 0, 0, 0"] --"in"--> image_basecolor_outcolor_cm
    image_orm_channels --"outx --> occlusion"--> SR_boombox
    image_orm_channels --"outy --> roughness"--> SR_boombox
    image_orm_channels --"outz --> metallic"--> SR_boombox
    image_orm_channels/in["image_orm_out = 0, 0, 0"] --"in"--> image_orm_channels
    image_emission_out_cm --"out --> emissive"--> SR_boombox
    image_emission_out_cm/in["image_emission_out = 0, 0, 0"] --"in"--> image_emission_out_cm
    SR_boombox --"out --> surfaceshader"--> Material_boombox
    SR_boombox/base_color["image_basecolor_outcolor_cm_out = 1, 1, 1"] --"base_color"--> SR_boombox
    SR_boombox/metallic["image_orm_channels_outz = 1"] --"metallic"--> SR_boombox
    SR_boombox/roughness["image_orm_channels_outy = 1"] --"roughness"--> SR_boombox
    SR_boombox/occlusion["image_orm_channels_outx = 1"] --"occlusion"--> SR_boombox
    SR_boombox/transmission["SR_boombox_transmission = 0 [SOCKET]"] --"transmission"--> SR_boombox
    style SR_boombox/transmission fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    SR_boombox/specular["SR_boombox_specular = 1 [SOCKET]"] --"specular"--> SR_boombox
    style SR_boombox/specular fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    SR_boombox/specular_color["SR_boombox_specular_color = 1, 1, 1 [SOCKET]"] --"specular_color"--> SR_boombox
    style SR_boombox/specular_color fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    SR_boombox/ior["SR_boombox_ior = 1.5 [SOCKET]"] --"ior"--> SR_boombox
    style SR_boombox/ior fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    SR_boombox/alpha["image_basecolor_outa = 1"] --"alpha"--> SR_boombox
    SR_boombox/alpha_mode["SR_boombox_alpha_mode = 0 [SOCKET]"] --"alpha_mode"--> SR_boombox
    style SR_boombox/alpha_mode fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    SR_boombox/alpha_cutoff["SR_boombox_alpha_cutoff = 0.5 [SOCKET]"] --"alpha_cutoff"--> SR_boombox
    style SR_boombox/alpha_cutoff fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    SR_boombox/iridescence["SR_boombox_iridescence = 0 [SOCKET]"] --"iridescence"--> SR_boombox
    style SR_boombox/iridescence fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    SR_boombox/iridescence_ior["SR_boombox_iridescence_ior = 1.3 [SOCKET]"] --"iridescence_ior"--> SR_boombox
    style SR_boombox/iridescence_ior fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    SR_boombox/iridescence_thickness["SR_boombox_iridescence_thickness = 100 [SOCKET]"] --"iridescence_thickness"--> SR_boombox
    style SR_boombox/iridescence_thickness fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    SR_boombox/sheen_color["SR_boombox_sheen_color = 0, 0, 0 [SOCKET]"] --"sheen_color"--> SR_boombox
    style SR_boombox/sheen_color fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    SR_boombox/sheen_roughness["SR_boombox_sheen_roughness = 0 [SOCKET]"] --"sheen_roughness"--> SR_boombox
    style SR_boombox/sheen_roughness fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    SR_boombox/clearcoat["SR_boombox_clearcoat = 0 [SOCKET]"] --"clearcoat"--> SR_boombox
    style SR_boombox/clearcoat fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    SR_boombox/clearcoat_roughness["SR_boombox_clearcoat_roughness = 0 [SOCKET]"] --"clearcoat_roughness"--> SR_boombox
    style SR_boombox/clearcoat_roughness fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    SR_boombox/emissive["image_emission_out_cm_out = 0, 0, 0"] --"emissive"--> SR_boombox
    SR_boombox/emissive_strength["SR_boombox_emissive_strength = 1 [SOCKET]"] --"emissive_strength"--> SR_boombox
    style SR_boombox/emissive_strength fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    SR_boombox/thickness["SR_boombox_thickness = 0 [SOCKET]"] --"thickness"--> SR_boombox
    style SR_boombox/thickness fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    SR_boombox/attenuation_color["SR_boombox_attenuation_color = 1, 1, 1 [SOCKET]"] --"attenuation_color"--> SR_boombox
    style SR_boombox/attenuation_color fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    SR_boombox/anisotropy_strength["SR_boombox_anisotropy_strength = 0 [SOCKET]"] --"anisotropy_strength"--> SR_boombox
    style SR_boombox/anisotropy_strength fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    SR_boombox/anisotropy_rotation["SR_boombox_anisotropy_rotation = 0 [SOCKET]"] --"anisotropy_rotation"--> SR_boombox
    style SR_boombox/anisotropy_rotation fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
    SR_boombox/dispersion["SR_boombox_dispersion = 0 [SOCKET]"] --"dispersion"--> SR_boombox
    style SR_boombox/dispersion fill:#053333,stroke:#222222,stroke-width:2px,color:#FFFFFF
Loading

@jstone-lucasfilm

Copy link
Copy Markdown
Member

Thanks for putting this together, @kwokcb! I like the idea of adding functionality that allows us to visualize the generated ShaderGraph, and Mermaid is a compelling choice since GitHub and VS Code render it inline with no extra tooling.

Before we commit to a direction, though, I'd like to propose an alternative that might make this more reusable over the long term. The generated graph isn't really hidden in C++: Shader::getGraph is already public, and the graph is simply unbound in Python and JavaScript. So rather than adding a Mermaid serializer to the core, we could expose the existing read-only ShaderGraph traversal to the bindings and generate the Mermaid in Python. That would keep the core API unchanged and let the same traversal feed other representations later (e.g. DOT, JSON) without further C++ changes.

Does that sound like a reasonable path forward to you as well?

@kwokcb

kwokcb commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

I assumed there is a reason to not expose ShaderGraph, which also means exposing ShaderNode, and its input (ShaderInput) and output (ShaderOutput)ports (which could be confusing as there is already a ShaderPort API exposure which is a base class exposure parts of the inputs API only) --- so will leave a ping with @niklasharrysson if your around.

It seems this increases the API "surface area" unduly since there has never been any request to expose the graph before.

I also don't know if this would hinder longer term "visitor" refactor work so pinging @ld-kerley -- i.e. is the graph is a form that may change over time. I'm hesitant to add another construct like an iterator to hide exposure of internals.

Finally this means that the same logic needs to be repeated externally for every implementation language instead of a single call.

--

An alternative is to specify the output format as an argument. This leaves it open to extend later on if required without an API signature change.

@kwokcb

kwokcb commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

@jstone-lucasfilm, Had a rethink on this as I was using this for nestd nodegraph debugging and needed to access the graph for external validation. Thus I've added the API as you proposed and added sample utilities in JS and Python. A bit more maintenance but it's easier to maintain and enhance this way.

Comment thread python/Scripts/generateshader.py Outdated
import MaterialX.PyMaterialXGenShader as mx_gen_shader


def createMermaidGraph(shader, showInputValues):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Externalized API usage for Mermaid graphs in Python. This could be a module level utility function instead / as well for general availabiity. (which I had to do for JS).

(function () {
onModuleReady(function () {
// Generate a Mermaid graph string for a generated Shader using JS-side APIs.
function _mxEscapeMermaidLabel(value) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Externalized API usage for Mermaid graphs in Javascript.

.def("setAttribute", static_cast<void (mx::Shader::*)(const std::string&)>(&mx::Shader::setAttribute))
.def("setAttribute", static_cast<void (mx::Shader::*)(const std::string&, mx::ValuePtr)>(&mx::Shader::setAttribute));

py::class_<mx::ShaderNode, mx::ShaderNodePtr>(mod, "ShaderNode")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ShaderNode exposure

.def("setName", &mx::ShaderPort::setName)
.def("getName", &mx::ShaderPort::getName)
.def("getFullName", &mx::ShaderPort::getFullName)
.def("getNode", static_cast<mx::ShaderNode* (mx::ShaderPort::*)()>(&mx::ShaderPort::getNode), py::return_value_policy::reference)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't actually need to expose a graph, just get nodes in graph as that's the only interface that's required on the ShaderGraph class.

.def("isUniform", &mx::ShaderPort::isUniform)
.def("isEmitted", &mx::ShaderPort::isEmitted);

py::class_<mx::ShaderInput, mx::ShaderInputPtr, mx::ShaderPort>(mod, "ShaderInput")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add Input and Outputs to access connectivity.



set(JS_LINK_FLAGS_GENSHADER "${JS_LINK_FLAGS_CORE} --preload-file ${PROJECT_SOURCE_DIR}/libraries@libraries ")
set(JS_LINK_FLAGS_GENSHADER "${JS_LINK_FLAGS_CORE} --post-js ${GENSHADER}post.js --preload-file ${PROJECT_SOURCE_DIR}/libraries@libraries ")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Look for all post.js files.

@kwokcb kwokcb changed the title Mermaid graphing util for ShaderGraphs. Expose API + Graphing Utilities for Shader Generator Graphs Jul 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants