MaterialXUSD 0.0.1
Utilities for using MaterialX with USD
Loading...
Searching...
No Matches
materialxusd_utils.py
1have_materialx = False
2try:
3 import MaterialX as mx
4 have_materialx = True
5except ImportError:
6 print('MaterialX not available. Please install MaterialX to use this utility.')
7 have_materialx = False
8import logging
9
11 '''
12 @brief A collection of support utilities for working with MaterialX and USD.
13 '''
14
15 def __init__(self):
16 '''
17 @brief Constructor.
18 '''
19 self._stdlib, self._libFiles = self.load_standard_libraries()
20 logging.basicConfig(level=logging.INFO)
21 self.logger = logging.getLogger('MXUSDUTIL')
22
24 '''Load standard MaierialX libraries.
25 @return: The standard library and the list of library files.
26 '''
27 stdlib = mx.createDocument()
28 libFiles = mx.loadLibraries(mx.getDefaultDataLibraryFolders(), mx.getDefaultDataSearchPath(), stdlib)
29 return stdlib, libFiles
30
32 '''
33 @brief Get standard MaierialX libraries.
34 @return: The standard library and the list of library files.
35 '''
36 return self._stdlib
37
38 '''
39 @brief A collection of support utilities for working with MaterialX and USD.
40 '''
41 def create_document(self, path: str):
42 '''
43 @brief Create a MaterialX document from a file path.
44 @param path The path to the MaterialX document.
45 @return The MaterialX document if successful, None otherwise.
46 '''
47 doc = mx.createDocument()
48 mx.readFromXmlFile(doc, mx.FilePath(path))
49 return doc
50
51
52 def write_document(self, doc: mx.Document, path: str):
53 '''
54 @brief Write a MaterialX document to a file.
55 @param doc The MaterialX document.
56 @param path The path to write the MaterialX document.
57 @return True if successful, False otherwise.
58 '''
59 return mx.writeToXmlFile(doc, path)
60
61
62 def create_FileSearchPath(self, search_paths: list):
63 '''
64 @brief Create a MaterialX file search path from a list of string paths.
65 @param search_paths A list of string paths.
66 @return The MaterialX file search path.
67 '''
68 #self.logger.info(f'> Creating file search path: {search_paths}')
69 search_path = mx.FileSearchPath()
70 for path in search_paths:
71 search_path.append(path)
72 return search_path
73
74
75 def resolve_image_file_paths(self, doc: mx.Document, search_paths: mx.FileSearchPath):
76 '''
77 @brief Resolve image file paths in a MaterialX document.
78 @param doc The MaterialX document.
79 @param search_paths The MaterialX file search path.
80 '''
81 mx.flattenFilenames(doc, search_paths)
82
83 def add_nodegraph_output_qualifier_on_shaders(self, doc: mx.Document):
84 '''
85 @brief Add nodegraph output qualifier on shaders in the MaterialX document if not already set.
86 USD appears to require this for shader inputs to be connected to outputs on a nodegraph
87 sometimes -- when the output name is not "out" ?
88 @param doc The MaterialX document.
89 @return The number of explicit outputs added.
90 '''
91 explicit_outputs_added = 0
92 surfaceshader_nodes = doc.getChildren()
93 for surfaceshader_node in surfaceshader_nodes:
94 if surfaceshader_node.getType() not in ['surfaceshader']:
95 continue
96
97 # Scan all inputs on the shader node
98 for input in surfaceshader_node.getInputs():
99 # Check for nodegraph output qualifier
100 nodegraph_string = input.getNodeGraphString()
101 if nodegraph_string:
102 if not input.getOutputString():
103 nodegraph = doc.getNodeGraph(nodegraph_string)
104 if nodegraph:
105 outputs = nodegraph.getOutputs()
106 if outputs:
107 input.setOutputString(outputs[0].getName())
108 explicit_outputs_added += 1
109 self.logger.debug(f'>> Add output qualifier for shader input {mx.prettyPrint(input)}')
110
111 return explicit_outputs_added
112
113
114 def add_materials_for_shaders(self, doc: mx.Document):
115 '''
116 @brief Add materials for shaders at the root level of a MaterialX document. Nodegraphs are not considered as this is not supported by USD.
117 @param doc The MaterialX document.
118 @param logger The logger to use for output.
119 @return The number of materials added.
120 '''
121 # If has materials skip
122 materials = doc.getMaterialNodes()
123 if len(materials):
124 return 0
125
126 material_count = 0
127
128 surfaceshader_nodes = doc.getChildren()
129 for surfaceshader_node in surfaceshader_nodes:
130 if surfaceshader_node.getType() not in ['surfaceshader']:
131 continue
132 self.logger.info(f'>> Scan shader: {surfaceshader_node.getName()}')
133 downstream_ports = surfaceshader_node.getDownstreamPorts()
134 if not downstream_ports:
135 # Add a material for the shader
136 material_name = doc.createValidChildName('material_' + surfaceshader_node.getName())
137 material_node = doc.addMaterialNode(material_name, surfaceshader_node)
138 if material_node:
139 material_count += 1
140
141 return material_count
142
143
144 def add_downstream_materials(self, doc: mx.Document, logger=None):
145 '''
146 @brief Add downstream materials to the MaterialX graph.
147 @param doc The MaterialX document.
148 @return The number of materials added.
149 '''
150 # If has materials skip
151 material_count = len(doc.getMaterialNodes())
152 if material_count > 0:
153 return 0
154
155 nodegraphs = doc.getNodeGraphs()
156 if not nodegraphs:
157 return 0
158
159 # Only support these types of graph outputs
160 supported_output_types = [ 'float', 'vector2', 'vector3', 'vector4', 'integer', 'boolean', 'color3', 'color4' ]
161
162 for graph in nodegraphs:
163 if graph.hasSourceUri():
164 continue
165
166
167 graph_outputs = graph.getOutputs()
168 if not graph_outputs:
169 continue
170
171 self.logger.debug(f'Scan graph: {graph.getName()}')
172
173 # Use does not support these nodes so need to do it the hard way....
174 usd_supports_convert_to_surface_shader = False
175
176 downstream_ports = graph.getDownstreamPorts()
177 for output in graph_outputs:
178 # See if output name path is in downstream ports
179 match = False
180 for port in downstream_ports:
181 if port.getNamePath() == output.getNamePath():
182 match = True
183 break
184 if match:
185 downstream_ports.remove(port)
186
187 #downstream_port_count = 0
188 #for port in downstream_ports:
189 # match = False
190 # for output in graph_outputs:
191 # if port.getName() == output.getName():
192 # match = True
193 # break
194 # if not match:
195 # downstream_port_count += 1
196
197 if downstream_ports:
198 self.logger.info('>>> Downstream port:' + ",".join( [port.getNamePath() for port in downstream_ports]))
199 if len(downstream_ports) == 0:
200 # Add a material per output
201 #
202 is_multi_output = len(graph_outputs) > 1
203 for output in graph_outputs:
204 #if downstream_ports:
205 # for port in downstream_ports:
206 # if port.getNamePath() == output.getNamePath():
207 # self.logger.info('---- SKIP OUTPUT :', output.getNamePath())
208 # continue
209
210 output_name = output.getName()
211 output_type = output.getType()
212
213 #self.logger.info(f'>>> Scan output: {output_name}. type: {output_type}')
214
215 # Special case for surfaceshader outputs. Just add in a downstream material
216 if output_type == 'surfaceshader':
217 graph_parent = graph.getParent()
218 connected_ss = output.getConnectedNode()
219 if not graph_parent or not connected_ss:
220 continue
221
222 self.logger.info(f'Extract unsupported shader inside nodegraph: {connected_ss.getNamePath()}')
223
224 shadernode_name = graph_parent.createValidChildName(connected_ss.getName())
225 shadernode_nodedef = connected_ss.getNodeDef()
226 shadernode = graph_parent.addNodeInstance(shadernode_nodedef, shadernode_name)
227 shadernode.copyContentFrom(connected_ss)
228
229 # For every connected input on the surfaceshader node
230 # create a nodegraph output
231 for ss_input in connected_ss.getInputs():
232 ss_input_input = ss_input.getNodeName() if ss_input.getNodeName() else ss_input.getInterfaceName()
233 if not ss_input_input:
234 continue
235 ss_input_type = ss_input.getType()
236 ss_input_output = graph.addOutput(graph.createValidChildName('out'), ss_input_type)
237 ss_input_output.setNodeName(ss_input_input)
238 if ss_input.getOutputString():
239 ss_input_output.setOutputString(ss_input.getOutputString())
240
241 # Connect new graph output to new shader node's input to
242 shadernode_input = shadernode.getInput(ss_input.getName())
243 if shadernode_input:
244 shadernode_input.removeAttribute('nodename')
245 shadernode_input.removeAttribute('value')
246 shadernode_input.setNodeGraphString(graph.getName())
247 shadernode_input.setOutputString(ss_input_output.getName())
248
249 # Should do this after scanning all surfaceshader nodes...
250 graph.removeNode(connected_ss.getName())
251 graph.removeOutput(output_name)
252
253 # Add a material for the shader
254 material_name = doc.createValidChildName(graph.getName() + '_' + output_name)
255 material_node = doc.addMaterialNode(material_name)
256 if material_node:
257 self.logger.info(f"Added material node: {material_node.getName()}, for graph shader output: {output_name}")
258 material_node_input = material_node.addInput(output_type, output_type)
259 #material_node_input.setNodeGraphString(graph.getName())
260 #material_node_input.setOutputString(output_name)
261 material_node_input.setNodeName(shadernode_name)
262 material_node_input.removeAttribute('value')
263 material_count += 1
264
265 elif output_type in supported_output_types:
266
267 if usd_supports_convert_to_surface_shader:
268 # Create a new material node
269 shadernode_name = doc.createValidChildName('SHD_' + graph.getName() + '_' + output_name)
270 materialnode_name = doc.createValidChildName('MAT' + graph.getName() + '_' + output_name)
271
272 convert_definition = 'ND_convert_' + output_type + '_color3'
273 convert_node = doc.getNodeDef(convert_definition)
274 if not convert_node:
275 self.logger.info(f'>>>> Failed to find conversion definition: {convert_definition}')
276 else:
277 shadernode = doc.addNodeInstance(convert_node, shadernode_name)
278 shadernode.removeAttribute('nodedef')
279 new_input = shadernode.addInput('in', output_type)
280 new_input.setNodeGraphString(graph.getName())
281 new_input.removeAttribute('value')
282 #if is_multi_output:
283 # ISSUE: USD does not handle nodegraph without an explicit output propoerly
284 # so always added in the output string !
285 new_input.setOutputString(output_name)
286 materialnode = doc.addMaterialNode(materialnode_name, shadernode)
287
288 if materialnode:
289 material_count += 1
290
291 else:
292 #self.logger.info(f'Scan: {graph.getName()} output: {output_name} type: {output_type}')
293
294 # If not color3 or float add a convert node and connect it to the current upstream node
295 # and then add in a new output which is of type color3
296 if output_type != 'color3' and output_type != 'float':
297
298 convert_definition = 'ND_convert_' + output_type + '_color3'
299 convert_nodedef = doc.getNodeDef(convert_definition)
300 if not convert_nodedef:
301 self.logger.info(">>>> Failed to find conversion definition: %s" % convert_definition)
302 continue
303
304 # Find upstream node or interface input
305 convert_upstream = None
306 if len(output.getNodeName()) > 0:
307 convert_upstream = output.getNodeName()
308 elif output.hasInterfaceName():
309 convert_upstream = output.getInterfaceName()
310 if not convert_upstream:
311 self.logger.info("> Failed to find upstream node for output: %s" % output.getName())
312 continue
313
314 # Insert convert node
315 convert_node = graph.addNodeInstance(convert_nodedef, graph.createValidChildName(f'convert_{convert_upstream}'))
316 convert_node.removeAttribute('nodedef')
317 convert_input = convert_node.addInput('in', output_type)
318 if len(output.getNodeName()) > 0:
319 convert_input.setNodeName(output.getNodeName())
320 elif output.hasInterfaceName():
321 convert_input.setInterfaceName(output.getInterfaceName())
322 convert_input.removeAttribute('value')
323
324 # Overwrite the upstream connection on the output
325 # and change it's type
326 output.setNodeName(convert_node.getName())
327 output.removeAttribute('value')
328 output.setType('color3')
329 output_type = 'color3'
330
331 # Create downstream (umlit) shader
332 shadernode_name = doc.createValidChildName('shader_' + graph.getName() + '_' + output_name)
333 materialnode_name = doc.createValidChildName(graph.getName() + '_' + output_name)
334 unlitDefinition = 'ND_surface_unlit'
335 unlitNode = doc.getNodeDef(unlitDefinition)
336 shadernode = doc.addNodeInstance(unlitNode, shadernode_name)
337 shadernode.removeAttribute('nodedef')
338
339 # Connect upstream output to shader input (based on type)
340 new_input = None
341 if output_type == 'color3':
342 new_input = shadernode.addInput('emission_color', output_type)
343 else:
344 new_input = shadernode.addInput('emission', output_type)
345 new_input.setNodeGraphString(graph.getName())
346 new_input.removeAttribute('value')
347 #if is_multi_output:
348 # ISSUE: USD does not handle nodegraph without an explicit output propoerly
349 # so always added in the output string !
350 new_input.setOutputString(output_name)
351
352 # Add downstream material node connected to shadernode
353 materialnode = doc.addMaterialNode(materialnode_name, shadernode)
354
355 if materialnode:
356 material_count += 1
357
358 return material_count
359
360
361 def add_explicit_geometry_stream(self, graph: mx.GraphElement):
362 '''
363 @brief Add explicit geometry stream nodes for inputs with defaultgeomprop specified
364 in nodes definition. Do this for unconnected inputs only.
365 @param graph The MaterialX graph element.
366 @return The number of implicit nodes added.
367 '''
368
369 graph_default_nodes = {}
370
371 for node in graph.getNodes():
372 if node.hasSourceUri() or (node.getCategory() in ["nodedef"]):
373 continue
374
375 nodedef = node.getNodeDef(node.getType())
376 #self.logger.info('Node:', node.getName(), 'NodeDef:', nodedef.getName() if nodedef else "None")
377 if not nodedef:
378 continue
379
380 for nodedef_input in nodedef.getInputs():
381 node_input = node.getInput(nodedef_input.getName())
382 # Skip if is a connected input
383 if node_input:
384 if node_input.getInterfaceName() or node_input.getNodeName() or node_input.getNodeGraphString():
385 continue
386
387 # Skip if no defaultgeomprop
388 defaultgeomprop = nodedef_input.getDefaultGeomProp()
389 if not defaultgeomprop:
390 continue
391
392 # Firewall. USD does not appear to handle bitangent properly so
393 # skip it for now.
394 if defaultgeomprop.getGeomProp() == "bitangent":
395 #self.logger.info(f'> WARNING: Skipping adding explicit bitangent node for: "{node.getNamePath()}"')
396 continue
397
398 # Fix this up to get information from the defaultgromprop e.g.
399 # - texcoord <geompropdef name="UV0" type="vector2" geomprop="texcoord" index="0">
400 defaultgeomprop_name = defaultgeomprop.getName()
401 defaultgeomprop_prop = defaultgeomprop.getGeomProp()
402 defaultgeomprop_type = defaultgeomprop.getType()
403 defaultgeomprop_index = defaultgeomprop.getIndex()
404 defaultgeomprop_space = defaultgeomprop.getSpace()
405
406 if not node_input:
407 node_input = node.addInput(nodedef_input.getName(), nodedef_input.getType())
408 if defaultgeomprop_name not in graph_default_nodes:
409 upstream_default_node = graph.addNode(defaultgeomprop_prop,
410 graph.createValidChildName(defaultgeomprop_name),
411 defaultgeomprop_type)
412 upstream_default_node.addInputsFromNodeDef()
413
414 # Set space and set index
415 index_input = upstream_default_node.getInput("index")
416 if index_input:
417 index_input.setValue(defaultgeomprop_index, 'integer')
418 space_input = upstream_default_node.getInput("space")
419 if space_input:
420 space_input.setValue(defaultgeomprop_space, 'string')
421
422 #self.logger.info(f'> Added upstream node "{upstream_default_node.getNamePath()}" : {upstream_default_node}')
423 graph_default_nodes[defaultgeomprop_name] = upstream_default_node
424 else:
425 upstream_default_node = graph_default_nodes[defaultgeomprop_name]
426 #self.logger.info('Use upstream node for defaultgromprop:', nodedef_input.getName(), defaultgeomprop)
427 node_input.setNodeName(upstream_default_node.getName())
428 node_input.removeAttribute('value')
429
430 implicit_nodes_added = len(graph_default_nodes)
431 if graph.getCategory() not in "nodegraph":
432 for child_graph in graph.getNodeGraphs():
433 if child_graph.hasSourceUri():
434 continue
435 implicit_nodes_added += self.add_explicit_geometry_stream(child_graph)
436
437 return implicit_nodes_added
438
439
440 def encapsulate_top_level_nodes(self, doc: mx.Document, nodegraph_name:str="top_level_nodes", remove_original:bool=True):
441 """
442 @brief Encapsulate top level nodes in a nodegraph. Remap any connections to the top level nodes
443 to outputs on a new nodegraph.
444 @param doc The MaterialX document.
445 @param nodegraph_name The name of the new nodegraph to encapsulate the top level nodes. Default is 'top_level_nodes'.
446 @param remove_original If True, remove the original top level nodes from the document. Default is True.
447 @return The number of top level nodes found
448 """
449 connections_made = 0
450 top_level_nodes_found = 0
451
452 # Find all children of document which are no material or shader nodes.
453 top_level_nodes = []
454 top_level_connections = []
455 for elem in doc.getNodes():
456
457 # skips elements that are part of the stdlib
458 if elem.hasSourceUri():
459 continue
460
461 if (elem.getName()
462 and (elem.getType() not in ["material", "surfaceshader"])
463 and elem.getCategory() not in ["nodegraph", "nodedef"]):
464 #self.logger.info("Finding top level nodes: ", elem.getName(), elem.getType())
465 top_level_nodes.append(elem)
466
467 elif elem.getType() in ["surfaceshader"]:
468 for input_port in elem.getInputs():
469 upstream_node_name = input_port.getNodeName()
470 if len(upstream_node_name) > 0:
471 upstream_output_name = input_port.getOutputString()
472
473 # Go through node outputs and nodedef outputs if needed if it's multi-output as
474 # we have to find the output name for usdMtlx to make the connection properly.
475 # It does not seem to hanlde upstream multioutputs properly and tries to connect to the first output
476 # or not connect at all ?
477 if not upstream_output_name:
478 upstream_node = doc.getDescendant(upstream_node_name)
479 upstream_node_outputs = upstream_node.getOutputs()
480 if len(upstream_node_outputs) > 1:
481 self.logger.debug(f"Find an output of name: {upstream_node_outputs[0].getName()}")
482 upstream_output_name = upstream_node_outputs[0].getName()
483 else:
484 upstream_node_nodedef = upstream_node.getNodeDef()
485 upstream_node_outputs = upstream_node_nodedef.getActiveOutputs()
486 if len(upstream_node_outputs) > 1:
487 self.logger.debug(f"Find an output of name: {upstream_node_outputs[0].getName()}")
488 upstream_output_name = upstream_node_outputs[0].getName()
489
490 #self.logger.info("Store connection: ", upstream_node_name, "<--", input_port.getNamePath())
491 top_level_connections.append([upstream_node_name, input_port.getNamePath(), upstream_output_name])
492
493 #self.logger.info("Top level connections: ", top_level_connections)
494 top_level_nodes_found = len(top_level_nodes)
495 if top_level_nodes_found == 0:
496 return top_level_nodes_found
497
498 # create nodegraph
499 ng_name = doc.createValidChildName(nodegraph_name)
500 ng = doc.addNodeGraph(ng_name)
501 for node in top_level_nodes:
502 #self.logger.info("Adding node: ", node.getName())
503 new_node = ng.addNode(node.getCategory(), mx.createValidName(node.getName()), node.getType())
504 new_node.copyContentFrom(node)
505 for connect in top_level_connections:
506 if connect[0] == node.getName():
507 the_input = doc.getDescendant(connect[1])
508 if not the_input:
509 continue
510
511 # Create a new output on the graph
512 new_output = ng.addOutput(ng.createValidChildName("out"), the_input.getType())
513 new_output.setNodeName(new_node.getName())
514 if len(connect[2]) > 0:
515 new_output.setOutputString(connect[2])
516 #self.logger.info("Create new output: ", mx.prettyPrint(new_output))
517 the_input.setNodeGraphString(ng_name)
518 the_input.setOutputString(new_output.getName())
519 the_input.removeAttribute("nodename")
520 the_input.removeAttribute("value")
521 #self.logger.info(f"Reconnecting {the_input.getNamePath()} {connect[1]} to {mx.prettyPrint(the_input)}")
522 connections_made += 1
523
524 if remove_original:
525 for node in top_level_nodes:
526 doc.removeChild(node.getName())
527
528 return top_level_nodes_found
529
530
531 def encapsulate_top_level_nodes_file(self, input_path:str, new_input_path:str, nodegraph_name:str='top_level_nodes', remove_original_nodes:bool =True):
532 '''
533 @brief Encapsulate top level nodes in a nodegraph. Remap any connections to the top level nodes
534 to outputs on a new nodegraph.
535 @param input_path The path to the MaterialX document.
536 @param new_input_path The path to write the modified MaterialX document.
537 @param nodegraph_name The name of the new nodegraph to encapsulate the top level nodes. Default is 'top_level_nodes'.
538 @param remove_original_nodes If True, remove the original top level nodes from the document. Default is True.
539 @return The modified MaterialX document if top level connections were found, None otherwise.
540 '''
541 doc = self.create_document(input_path)
542 top_level_nodes_found = self.encapsulate_top_level_nodes(doc, nodegraph_name, remove_original_nodes)
543 if top_level_nodes_found:
544 self.logger.info(f'> Encapsulated {top_level_nodes_found} top level nodes in a new nodegraph.')
545 if new_input_path:
546 print(f'> Writing modified MaterialX document to: {new_input_path}')
547 self.write_document(doc, new_input_path)
548 return doc
549 return None
550
551
552
A collection of support utilities for working with MaterialX and USD.
create_FileSearchPath(self, list search_paths)
Create a MaterialX file search path from a list of string paths.
resolve_image_file_paths(self, mx.Document doc, mx.FileSearchPath search_paths)
Resolve image file paths in a MaterialX document.
add_explicit_geometry_stream(self, mx.GraphElement graph)
Add explicit geometry stream nodes for inputs with defaultgeomprop specified in nodes definition.
create_document(self, str path)
Create a MaterialX document from a file path.
write_document(self, mx.Document doc, str path)
Write a MaterialX document to a file.
encapsulate_top_level_nodes_file(self, str input_path, str new_input_path, str nodegraph_name='top_level_nodes', bool remove_original_nodes=True)
Encapsulate top level nodes in a nodegraph.
get_standard_libraries(self)
Get standard MaierialX libraries.
add_materials_for_shaders(self, mx.Document doc)
Add materials for shaders at the root level of a MaterialX document.
encapsulate_top_level_nodes(self, mx.Document doc, str nodegraph_name="top_level_nodes", bool remove_original=True)
Encapsulate top level nodes in a nodegraph.
add_nodegraph_output_qualifier_on_shaders(self, mx.Document doc)
Add nodegraph output qualifier on shaders in the MaterialX document if not already set.
add_downstream_materials(self, mx.Document doc, logger=None)
Add downstream materials to the MaterialX graph.
load_standard_libraries(self)
Load standard MaierialX libraries.