Development Design

1. Development Design

The overall development system is shown below:

dev_diagram

Available Components

Not all components have been made public at time of writing. The following are:

Numerous Python utilities exist in the pymaterialx folder of the MaterialX Elements github repo. These have not been formalized. Many are explained in the published Jupyter notebooks. This includes Blender and OpenUSD data-model conversion, command line rendering, graph editing, and so forth.

Note

At time of writing not all documentation for all components are included. Please stay-tuned for updates as they become available.

2. Node Graph Editor

dev_diagram

2.1 Graph Representation

To allow for interactive graph viewing and editing an underlying representation and a library which can handle user display and interaction is required.

The decision was made to use the litegraph library due based on the following requirements:

  1. The ability to handle custom data types.
  2. The ability to handle custom node definitions including custom input and output ports with custom types.
  3. The ability to handle custom-meta data at the node or port level.
  4. The ability to provide custom UI styling including coloring of nodes, ports, links, and control over visibility of various elements.
  5. The ability to have a read-only as well as an editing mode.
  6. The ability to replace / provide a custom property editor.
  7. The ability to monitor the graph for basic interactions such as value, link changes, node add/remove, rename etc, as well as add custom monitor.
  8. The ability to "run" the graph by connection procedural input nodes into a shader graph.
  9. The ability to override or replace serialization.
  10. The ability to provide support for cut/copy/paste and an command system for logging, undo/redo.
  11. The ability to control graph layout.

2.2 MaterialX Definitions

Custom definitions can be dynamically creted / modified via a mechanism which takes as input one or more MaterialX definitions (nodedef) and generate Javascript functions which define all node characteristics.

This Javascript code is then executed as require to dynamically create / update a set of node definitions which can be instantiated. By default "standard" libraries which come as part of the MaterialX distribution is used. Additional definitions can be added via MaterialX documents provided by the user.

Below is an example of custom definitions from Maya (Lambert), and Houdini ("facing ratio") being loaded in and nodes instantiated in a sample graph. The OpenPBR node definition which is part of the 1.39 release of MaterialX was also loaded in using a 1.38.9 version of MaterialX.

dev_diagram

Note that it is also possible to disallow allow this and only load in definitions from the Javascript definition code. This has the advantage of being able to load in content saved in native litegraph format with the only dependence being the execution of the Javascript definition code.

The following is an example of dynamically generated Javascript code for the float version of ramplr Characteristics of node: 1. UI elements have been include here such as node colors, and icon and swatch previews in additional to MaterialX provided UI meta-data such as ui min, ui max etc. 2. Explicit nodedef ids are included along with node name, type, group etc. 3. Default values are explicitly provided including default geometric stream bindings. 4. Meta-data for color management or real-world units is included. 5. Callbacks for node specific changes are included here. In this example a specific "change" monitor is added. See the "Change Management" section for more details.

LiteGraph definition registration is shown at the end of the example. This will hook into all places where definitions are used including the provided node creation UI.

/**
  * @brief mtlx_procedural2d_ramplr_float
  * @details Library: mtlx. Category: constant. Type: float
  *   LiteGraph id: mtlx/procedural2d/ramplr_float
  */
function mtlx_procedural2d_ramplr_float() {
  this.nodedef_icon = '';
  this.nodedef_name = 'ND_ramplr_float';
  this.nodedef_type = 'float';
  this.nodedef_node = 'ramplr';
  this.nodedef_href = 'https://kwokcb.github.io/MaterialX_Learn/documents/definitions/ramplr.html';
  this.nodedef_swatch = 'https://kwokcb.github.io/MaterialX_Learn/resources/mtlx/nodedef_materials/material_ramplr_float_out_genglsl.png';
  this.nodedef_group = 'procedural2d';
  this.addInput('valuel','float');
  this.addProperty('valuel', 0.0, 'float',{"colorspace":"","unit":"","unittype":"","uiname":"","uimin":null,"uimax":null,"uifolder":"","defaultgeomprop":""});
  this.addInput('valuer','float');
  this.addProperty('valuer', 0.0, 'float',{"colorspace":"","unit":"","unittype":"","uiname":"","uimin":null,"uimax":null,"uifolder":"","defaultgeomprop":""});
  this.addInput('texcoord','vector2');
  this.addProperty('texcoord', [0.0, 0.0], 'vector2',{"colorspace":"","unit":"","unittype":"","uiname":"","uimin":null,"uimax":null,"uifolder":"","defaultgeomprop":"UV0"});
  this.addOutput('out','float');
  this.title = 'ramplr_float';
  this.desc = "MaterialX:mtlx/procedural2d/ramplr_float";
  this.onNodeCreated = function() {
  // Handled globally
}
  this.onRemoved = function() {
  // Handled globally
  }
  this.onPropertyChanged = function(name, value, prev_value) {
 MxShadingGraphEditor.theEditor.monitor.onPropertyChanged(this.title, name, value, prev_value, this);
  }
  this.onPropertyInfoChanged = function(name, info, value, prev_value) {
 MxShadingGraphEditor.theEditor.monitor.onPropertyInfoChanged(this.title, name, info, value, prev_value, this);
  }
  this.onConnectOutput = function(slot, input_type, input, target_node, target_slot) {
 MxShadingGraphEditor.theEditor.monitor.onConnectOutput(slot, input_type, input, target_node, target_slot, this);
  }
  this.onConnectInput = function(target_slot, output_type, output, source, slot) {
 MxShadingGraphEditor.theEditor.monitor.onConnectInput(target_slot, output_type, output, source, slot, this);
  }
  this.bgcolor = '#111';
  this.color = '#222';
  this.shape = LiteGraph.ROUND_SHAPE;
  this.boxcolor = '#161';
}
mtlx_procedural2d_ramplr_float.nodedef_name = 'ND_ramplr_float';
mtlx_procedural2d_ramplr_float.nodedef_node = 'ramplr';
mtlx_procedural2d_ramplr_float.nodedef_href = 'https://kwokcb.github.io/MaterialX_Learn/documents/definitions/ramplr.html';
LiteGraph.registerNodeType('mtlx/procedural2d/ramplr_float',mtlx_procedural2d_ramplr_float);

2.3 Document Management and Change Management

The design of document and change management is shown in the following diagram:

dev_diagram

The left-hand side shows how documents are managed. This includes how definitions are handled. The right-handed side shows how graph updates can be monitored with an example connection to a renderer.

2.4 Document Management

This section goes into the details for: 1. Producing MaterialX documents are used for interop, runtime graph handling, and rendering. 2. Producing runtime (Javascript) node definitions as described in the previous section

By default the MaterialX standard definitions are loaded in and kept in a MaterialX library document. This is fairly common practice. Any additional definitions that are loaded in from a working document are added to a one or more MaterialX additional library documents.

  1. Javascript definition code is generated from all library documents.
  2. Definitions and functional nodegraphs are removed to produce a document which can be parsed to generate the runtime graph which can be viewed or edited by the user. The extraction logic is denoted as the Extract Definitions and Extract Func. Graphs process boxes in the diagram.
  3. To produce a document which can be saved, used for interop, or used rendering or shader generation the runtime graph must be converted back to a MaterialX working document.
    • This working document must have any node definition merged into it as these are required to instantiate MaterialX node instances.
    • This document is marked as the Renderable Doc in the diagram.
    • When:
      • Saving to string or file will strip the definitions as a user option.
      • Rendering and code generation uses the working document as is.

2.5 File References

Secondary references are disallowed for web access. This will affect two areas. It is recommended to pre-package content and unpacking contents as required. At time of writing unpacking logic is not provided (e.g. such as for a USDz or glTF binary).

Note that MaterialX itself has no packaging mechanism nor any way to embed resources into a document.

Referenced Files

Direct references to an additional files via the include mechanism for XML is ignored as this is disallowed.

Reference Images

Any file textures cannot be resolved as again these references are disallowed.

2.6 Change Management

There are a few categories of changes that are worthwhile to monitor to perform updates. This includes:

  1. Value and link changes
  2. Name / identifier changes for nodes (including input and output nodes)
  3. Meta-data attribute changes which can affect appearance (e.g. color management or units)
  4. Node addition / deletiong
  5. String changes which affect appearance. This includes enumeration changes, filename changes, geometric stream name changes.

Two test cases will be examined: 1. Property Editor Updates 2. Rendering Updates

Property Editor Updates

In general the editor will be the one triggering changes so each attribute or value change should trigger the appropriate notification. This is not done directly but indirectly when value, attribute or link changes occur.

The sample editor provided does however keep track of selection context so this is additional monitoring logic added for this purpose.

Rendering Updates

Assuming that shader code have been produced and compiled into runtime shaders, it is possible to directly "poke" into the shader's uniforms on node input value changes.

This is possible by keeping track of shader reflection data which provides a correspondence between the runtime node path and the shader uniform. This is provide as part of MaterialX code generation and used by the sample renderer provided. ( Refer to the shader generation /reflection utility for more details)

This does not handle any string or filename changes which require a remapping to a shader uniform. - In general the string changes will require shader code to be regenerated and recompiled to produce a new runtime shader. It would be better if there are no nodes with string inputs but currently even the standard library has instances of these. - Note that string changes includes color management and unit changes. This entails more than a remapping as code is injected into the graph at code generation time. - filename changes requires access to an external resource which must be loaded and converted into a hardware texture and then bound.

At time of writing value changes which require shader builds are automatically performed. These are indicated by dotted the lines for boxes and data flow links in the diagram.