Skip to content

Mermaid graphing util for ShaderGraphs.#2790

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

Mermaid graphing util for ShaderGraphs.#2790
kwokcb wants to merge 9 commits into
AcademySoftwareFoundation:mainfrom
kwokcb:graph_genshader_graphs

Conversation

@kwokcb

@kwokcb kwokcb commented Feb 23, 2026

Copy link
Copy Markdown
Contributor

Utility Proposal

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 a ShaderGraph for a Shader.
In this case it will produce a Mermaid graph which can be viewed in various containers such as Markdown and HTML.

Modifications

  • Add a "create Mermaid graph" to ShaderGraph.
    • Optional is showing all input values.
  • Expose this on the public Shader class as ShaderGraph is hidden.
  • Add Python and Javascript wrappers at the Shader level.
  • Add "create graph" option to generateshader.py.

Tests

  • Python graph generation added to CI tests.
  • Javascript graph generation add to unit tests.

Sample Results

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

e.g. Velvet (no input expansion)

graph LR
    geomprop_Nworld["geomprop_Nworld"]
    geomprop_Tworld["geomprop_Tworld"]
    SR_velvet["SR_velvet"]
    Velvet["Velvet"]
    geomprop_Nworld --"out --> coat_normal"--> SR_velvet
    geomprop_Nworld --"out --> normal"--> SR_velvet
    geomprop_Nworld/space["space = 2"] --> geomprop_Nworld
    geomprop_Tworld --"out --> tangent"--> SR_velvet
    geomprop_Tworld/space["space = 2"] --> geomprop_Tworld
    geomprop_Tworld/index["index = 0"] --> geomprop_Tworld
    SR_velvet --"out --> surfaceshader"--> Velvet    
    Velvet --"out --> out"--> Velvet
Loading

e.g. Tiled Brass (input expansion)

graph LR
    geomprop_Nworld["geomprop_Nworld"]
    geomprop_Tworld["geomprop_Tworld"]
    geomprop_UV0["geomprop_UV0"]
    NG_brass1/image_roughness["NG_brass1/image_roughness"]
    NG_brass1/image_color["NG_brass1/image_color"]
    SR_brass1["SR_brass1"]
    Tiled_Brass["Tiled_Brass"]
    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 --> coat_color"--> SR_brass1
    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
    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
    Tiled_Brass --"out --> out"--> Tiled_Brass

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.

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