MaterialXUSD 0.0.1
Utilities for using MaterialX with USD
Loading...
Searching...
No Matches
materialxusd_custom.py
1#!/usr/bin/env python
2'''
3Conversion utilities to convert from USD to MaterialX and MaterialX to USD.
4'''
5from pxr import Usd, UsdShade, Sdf, Gf, UsdMtlx
6import MaterialX as mx
7
8
10 '''
11 Sample converter from MaterialX to USD.
12 '''
13 def __init__(self, logger=None):
14 '''
15 Constructor.
16 @param logger: Logger object.
17 '''
18 self.loggerlogger = logger
19
20 def log(self, msg, level=0):
21 '''
22 Log a message.
23 @param msg: Message to log.
24 @param level: Log level.
25 '''
26 if self.loggerlogger:
27 if level == 1:
28 self.loggerlogger.warning(msg)
29 elif level == 2:
30 self.loggerlogger.error(msg)
31 elif level == -1:
32 self.loggerlogger.debug(msg)
33 else:
34 self.loggerlogger.info(msg)
35 else:
36 self.log(msg)
37
38 def get_usd_types(self):
39 '''
40 Retrieve a list of USD Sdf value type names.
41 @return: List of USD Sdf value type names.
42 '''
43 types = []
44 for t in dir(Sdf.ValueTypeNames):
45 if not t.startswith("__"):
46 types.append(str(t))
47 return types
48
49 def map_mtlx_to_usd_type(self, mtlx_type : str):
50 '''
51 Map a MaterialX type to a USD Sdf type.The mapping is easier from MaterialX as
52 the number of type variations is much less. Note that one USD type is chosen
53 with no options for choosing things like precision.
54 @param mtlx_type: MaterialX type.
55 @return: Corresponding USD Sdf type.
56 '''
57 mtlx_usd_map = {
58 "filename": Sdf.ValueTypeNames.Asset,
59 "string": Sdf.ValueTypeNames.String,
60 "boolean": Sdf.ValueTypeNames.Bool,
61 "integer": Sdf.ValueTypeNames.Int,
62 "float": Sdf.ValueTypeNames.Float,
63 "color3": Sdf.ValueTypeNames.Color3f,
64 "color4": Sdf.ValueTypeNames.Color4f,
65 "vector2": Sdf.ValueTypeNames.Float2,
66 "vector3": Sdf.ValueTypeNames.Float3,
67 "vector4": Sdf.ValueTypeNames.Float4,
68 "surfaceshader": Sdf.ValueTypeNames.Token,
69 "volumeshader": Sdf.ValueTypeNames.Token,
70 "displacementshader": Sdf.ValueTypeNames.Token,
71 }
72 return mtlx_usd_map.get(mtlx_type, Sdf.ValueTypeNames.Token)
73
74 def map_mtlx_to_usd_value(self, mtlx_type, mtlx_value):
75 '''
76 Map a MaterialX value of a given type to a USD value.
77 TODO: Add all types here. This does not seem to be exposed in Python?
78 See: https://openusd.org/dev/api/struct_usd_mtlx_usd_type_info.html
79 @param mtlx_type: MaterialX type.
80 @param mtlx_value: MaterialX value.
81 @return: Corresponding USD value.
82 '''
83 if mtlx_type == "float":
84 return float(mtlx_value)
85 elif mtlx_type == "integer":
86 return int(mtlx_value)
87 elif mtlx_type == "boolean":
88 return bool(mtlx_value)
89 elif mtlx_type in ("string", "filename"):
90 return str(mtlx_value)
91 elif mtlx_type == "vector2":
92 return Gf.Vec2f(mtlx_value[0], mtlx_value[1])
93 elif mtlx_type in ("color3", "vector3"):
94 return Gf.Vec3f(mtlx_value[0], mtlx_value[1], mtlx_value[2])
95 elif mtlx_type in ("color4", "vector4"):
96 return Gf.Vec4f(mtlx_value[0], mtlx_value[1], mtlx_value[2], mtlx_value[3])
97 elif mtlx_type == "matraix33":
98 return Gf.Matrix3f(mtlx_value[0], mtlx_value[1], mtlx_value[2],
99 mtlx_value[3], mtlx_value[4], mtlx_value[5],
100 mtlx_value[6], mtlx_value[7], mtlx_value[8])
101 elif mtlx_type == "matrix44":
102 return Gf.Matrix4f(mtlx_value[0], mtlx_value[1], mtlx_value[2], mtlx_value[3],
103 mtlx_value[4], mtlx_value[5], mtlx_value[6], mtlx_value[7],
104 mtlx_value[8], mtlx_value[9], mtlx_value[10], mtlx_value[11],
105 mtlx_value[12], mtlx_value[13], mtlx_value[14], mtlx_value[15])
106 return None
107
109 '''
110 Utility to map from MaterialX shader notation to USD notation.
111 @param name: MaterialX shader notation.
112 @return: Corresponding USD notation.
113 '''
114 if name == "surfaceshader":
115 return "surface"
116 elif name == "displacementshader":
117 return "displacement"
118 elif name == "volumeshader":
119 return "volume"
120 return name
121
122 def emit_usd_connections(self, node, stage, root_path):
123 '''
124 Emit connections between MaterialX elements as USD connections for
125 a given MaterialX node.
126 @param node: MaterialX node to examine.
127 @param stage: USD stage to write connection to.
128 @param root_path: Root path for connections.
129 '''
130 if not node:
131 return
132
133 material_path = None
134 if node.getType() == "material":
135 material_path = node.getName()
136
137 value_elements = node.getActiveValueElements() if (node.isA(mx.Node) or node.isA(mx.NodeGraph)) else [ node ]
138 for value_element in value_elements:
139 is_input = value_element.isA(mx.Input)
140 is_output = value_element.isA(mx.Output)
141
142 if is_input or is_output:
143 interface_name = ""
144
145 # Find out what type of element is connected to upstream:
146 # node, nodegraph, or interface input.
147 mtlx_connection = value_element.getAttribute("nodename")
148 if not mtlx_connection:
149 mtlx_connection = value_element.getAttribute("nodegraph")
150 if is_input and not mtlx_connection:
151 mtlx_connection = value_element.getAttribute("interfacename")
152 interface_name = mtlx_connection
153
154 connection_path = ""
155 if mtlx_connection:
156 # Handle input connection by searching for the appropriate parent node.
157 # - If it's an interface input we want the parent nodegraph. Otherwise
158 # we want the node or nodegraph specified above.
159 # - If the parent path is the root (getNamePath() is empty), then this is to
160 # nodes at the root document level.
161 if is_input:
162 parent = node.getParent()
163 if parent.getNamePath():
164 if interface_name:
165 connection_path = root_path + parent.getNamePath()
166 else:
167 connection_path = root_path + parent.getNamePath() + "/" + mtlx_connection
168 else:
169 # The connection is to a prim at the root level so insert a '/' identifier
170 # as getNamePath() will return an empty string at the root Document level.
171 if interface_name:
172 connection_path = root_path
173 else:
174 connection_path = root_path + mtlx_connection
175
176 # Handle output connection by looking for sibling elements
177 else:
178 parent = node.getParent()
179
180 # Connection is to sibling under the same nodegraph
181 if node.isA(mx.NodeGraph):
182 connection_path = root_path + node.getNamePath() + "/" + mtlx_connection
183 else:
184 # Connection is to a nodegraph parent of the current node
185 if parent.getNamePath():
186 connection_path = root_path + parent.getNamePath() + "/" + mtlx_connection
187 # Connection is to the root document.
188 else:
189 connection_path = root_path + mtlx_connection
190
191 # Find the source prim
192 # Assumes that the source is either a nodegraph, a material or a shader
193 connection_path = connection_path.removesuffix("/")
194 source_prim = None
195 source_port = "out"
196 source = stage.GetPrimAtPath(connection_path)
197 if not source and material_path:
198 connection_path = "/" + material_path + connection_path
199 source = stage.GetPrimAtPath(connection_path)
200 if not source:
201 source = stage.GetPrimAtPath("/" + material_path)
202
203 if source:
204 if source.IsA(UsdShade.Material):
205 source_prim = UsdShade.Material(source)
206 elif source.IsA(UsdShade.NodeGraph):
207 source_prim = UsdShade.NodeGraph(source)
208 elif source.IsA(UsdShade.Shader):
209 source_prim = UsdShade.Shader(source)
210
211 # Special case handle interface input vs an output
212 if interface_name:
213 source_port = interface_name
214 else:
215 source_port = value_element.getAttribute("output") or "out"
216
217 # Find destination prim and port and make the appropriate connection.
218 # Assumes that the destination is either a nodegraph, a material or a shader
219 if source_prim:
220 dest = stage.GetPrimAtPath(root_path + node.getNamePath())
221 if dest:
222 port_name = value_element.getName()
223 dest_node = None
224 if dest.IsA(UsdShade.Material):
225 dest_node = UsdShade.Material(dest)
226 elif dest.IsA(UsdShade.NodeGraph):
227 dest_node = UsdShade.NodeGraph(dest)
228 elif dest.IsA(UsdShade.Shader):
229 dest_node = UsdShade.Shader(dest)
230
231 # Find downstream port (input or output)
232 if dest_node:
233 if is_input:
234 # Map from MaterialX to USD connection syntax
235 if dest.IsA(UsdShade.Material):
236 port_name = self.map_mtlx_to_usd_shader_notation(port_name)
237 port_name = "mtlx:" + port_name
238 dest_port = dest_node.GetOutput(port_name)
239 else:
240 dest_port = dest_node.GetInput(port_name)
241 else:
242 dest_port = dest_node.GetOutput(port_name)
243
244 # Make connection to interface input, or node/nodegraph output
245 if dest_port:
246 if interface_name:
247 interface_input = source_prim.GetInput(source_port)
248 if interface_input:
249 if not dest_port.ConnectToSource(interface_input):
250 self.log(f"> Failed to connect: {source.GetPrimPath()} --> {dest_port.GetFullName()}")
251 else:
252 source_prim_api = source_prim.ConnectableAPI()
253 if not dest_port.ConnectToSource(source_prim_api, source_port):
254 self.log(f"> Failed to connect: {source.GetPrimPath()} --> {dest_port.GetFullName()}")
255 else:
256 self.log(f"> Failed to find destination port: {port_name}")
257
258 def emit_usd_value_elements(self, node, usd_node, emit_all_value_elements):
259 '''
260 Emit MaterialX value elements in USD.
261 @param node: MaterialX node with value elements to scan.
262 @param usd_node: UsdShade node to create value elements on.
263 @param emit_all_value_elements: Emit value elements based on node definition, even if not specified on node instance.
264 '''
265 if not node:
266 return
267
268 is_material = node.getType() == "material"
269 node_def = None
270 if node.isA(mx.Node):
271 node_def = node.getNodeDef()
272
273 # Instantiate with all the nodedef inputs (if emit_all_value_elements is True).
274 # Note that outputs are always created.
275 if node_def and not is_material:
276 for value_element in node_def.getActiveValueElements():
277 if value_element.isA(mx.Input):
278 if emit_all_value_elements:
279 mtlx_type = value_element.getType()
280 usd_type = self.map_mtlx_to_usd_type(mtlx_type)
281 port_name = value_element.getName()
282 usd_input = usd_node.CreateInput(port_name, usd_type)
283
284 if value_element.getValueString():
285 mtlx_value = value_element.getValue()
286 usd_value = self.map_mtlx_to_usd_value(mtlx_type, mtlx_value)
287 if usd_value is not None:
288 usd_input.Set(usd_value)
289 color_space = value_element.getAttribute("colorspace")
290 if color_space:
291 usd_input.GetAttr().SetColorSpace(color_space)
292 uifolder = value_element.getAttribute("uifolder")
293 if uifolder:
294 usd_input.SetDisplayGroup(uifolder)
295 uiname = value_element.getAttribute("uiname")
296 if uiname:
297 usd_input.GetAttr().SetDisplayName(uiname)
298
299 elif not is_material and value_element.isA(mx.Output):
300 usd_node.CreateOutput(value_element.getName(), self.map_mtlx_to_usd_type(value_element.getType()))
301
302 # From the given instance add inputs and outputs and set values.
303 # This may override the default value specified on the definition.
304 value_elements = []
305 if node.isA(mx.Node) or node.isA(mx.NodeGraph):
306 value_elements = node.getActiveValueElements()
307 else:
308 value_elements = [ node ]
309 for value_element in value_elements:
310 if value_element.isA(mx.Input):
311 mtlx_type = value_element.getType()
312 usd_type = self.map_mtlx_to_usd_type(mtlx_type)
313 port_name = value_element.getName()
314 if is_material:
315 # Map from Materials to USD notation
316 port_name = self.map_mtlx_to_usd_shader_notation(port_name)
317 usd_input = usd_node.CreateOutput("mtlx:" + port_name, usd_type)
318 else:
319 usd_input = usd_node.CreateInput(port_name, usd_type)
320
321 # Set value. Note that we check the length of the value string
322 # instead of getValue() as a 0 value will be skipped.
323 if value_element.getValueString():
324 mtlx_value = value_element.getValue()
325 usd_value = self.map_mtlx_to_usd_value(mtlx_type, mtlx_value)
326 if usd_value is not None:
327 usd_input.Set(usd_value)
328 color_space = value_element.getAttribute("colorspace")
329 if color_space:
330 usd_input.GetAttr().SetColorSpace(color_space)
331 uifolder = value_element.getAttribute("uifolder")
332 if uifolder:
333 usd_input.SetDisplayGroup(uifolder)
334 uiname = value_element.getAttribute("uiname")
335 if uiname:
336 usd_input.GetAttr().SetDisplayName(uiname)
337
338 elif not is_material and value_element.isA(mx.Output):
339 usd_output = usd_node.GetInput(value_element.getName())
340 if not usd_output:
341 usd_node.CreateOutput(value_element.getName(), self.map_mtlx_to_usd_type(value_element.getType()))
342
343 def set_prim_mtlx_version(self, prim: Usd.Prim, version: str):
344 '''
345 Set the MaterialX version on a prim.
346 See: https://openusd.org/dev/api/class_usd_mtlx_material_x_config_a_p_i.html
347 @param prim: USD prim.
348 @param version: MaterialX version string.
349 @return: True if the version was set, False otherwise.
350 '''
351 error = ""
352 try:
353 #if UsdMtlx.MaterialXConfigAPI.CanApply(prim):
354 mtlx_config_api = UsdMtlx.MaterialXConfigAPI.Apply(prim)
355 version_attr = mtlx_config_api.CreateConfigMtlxVersionAttr()
356 version_attr.Set(version)
357 return True
358 except Exception as e:
359 error = e
360 self.loggerlogger.warning(f"Failed to set MaterialX version on prim: {prim.GetPath()}. {error}")
361 return False
362
363 def emit_usd_shader_graph(self, doc, stage, mtlx_nodes, emit_all_value_elements, root="/MaterialX/Materials/"):
364 '''
365 Emit USD shader graph to a given stage from a list of MaterialX nodes.
366 @param doc: MaterialX source document.
367 @param stage: USD target stage.
368 @param mtlx_nodes: MaterialX shader nodes.
369 @param emit_all_value_elements: Emit value elements based on node definition, even if not specified on node instance.
370 @param root: Root path for the shader graph.
371 '''
372 mtx_version = doc.getVersionString()
373 # Create root primt
374 # Q: Should this be done here. Seems this is not considered valid.
375 declare_version_at_root = False
376 if declare_version_at_root:
377 root_prim = stage.DefinePrim("/MaterialX/Materials")
378 self.set_prim_mtlx_version(root_prim, mtx_version)
379
380 material_path = None
381 for node_name in mtlx_nodes:
382 elem = doc.getDescendant(node_name)
383 if elem.getType() == "material":
384 material_path = elem.getName()
385 break
386
387 # Emit USD nodes
388 for node_name in mtlx_nodes:
389 elem = doc.getDescendant(node_name)
390 usd_path = root + elem.getNamePath()
391
392 node_def = None
393 usd_node = None
394 if elem.getType() == "material":
395 self.log(f"Add material at path: {usd_path}", -1)
396 # Q: Should we set the MTLX version on all nodes / graphs ?
397 usd_node = UsdShade.Material.Define(stage, usd_path)
398 material_prim = usd_node.GetPrim()
399 if not declare_version_at_root:
400 self.set_prim_mtlx_version(material_prim, mtx_version)
401 material_prim.ApplyAPI("MaterialXConfigAPI")
402 elif elem.isA(mx.Node):
403 node_def = elem.getNodeDef()
404 self.log(f"Add node at path: {usd_path}", -1)
405 usd_node = UsdShade.Shader.Define(stage, usd_path)
406 if not declare_version_at_root:
407 self.set_prim_mtlx_version(usd_node.GetPrim(), mtx_version)
408 elif elem.isA(mx.NodeGraph):
409 self.log(f"Add nodegraph at path: {usd_path}", -1)
410 usd_node = UsdShade.NodeGraph.Define(stage, usd_path)
411
412 if usd_node:
413 if node_def:
414 usd_node.SetShaderId(node_def.getName())
415 self.emit_usd_value_elements(elem, usd_node, emit_all_value_elements)
416
417 # Emit connections between USD nodes
418 for node_name in mtlx_nodes:
419 elem = doc.getDescendant(node_name)
420 if elem.getType() == "material" or elem.isA(mx.Node) or elem.isA(mx.NodeGraph):
421 self.emit_usd_connections(elem, stage, root)
422
423 def find_materialx_nodes(self, doc):
424 '''
425 Find all nodes in a MaterialX document.
426 @param doc: MaterialX document.
427 @return: List of node paths.
428 '''
429 visited_nodes = []
430 for elem in doc.traverseTree():
431 if elem.isA(mx.Look) or elem.isA(mx.MaterialAssign):
432 self.loggerlogger.debug(f"Skipping look element: {elem.getNamePath()}")
433 else:
434 path = elem.getNamePath()
435 if path not in visited_nodes:
436 visited_nodes.append(path)
437 return visited_nodes
438
439 def emit_document_metadata(self, doc, stage):
440 '''
441 Emit MaterialX document metadata to the USD stage.
442 @param doc: MaterialX document.
443 @param stage: USD stage.
444 '''
445 root_layer = stage.GetRootLayer()
446 # - color space
447 color_space = doc.getColorSpace()
448 if not color_space:
449 color_space = "lin_rec709"
450
451 custom_layer_data = {"colorSpace": color_space}
452 if root_layer.customLayerData:
453 root_layer.customLayerData.update(custom_layer_data)
454 else:
455 root_layer.customLayerData = custom_layer_data
456
457 def emit(self, mtlx_file_name, emit_all_value_elements):
458 '''
459 Read in a MaterialX file and emit it to a new USD Stage.
460 Dump results for display and save to usda file.
461 @param mtlx_file_name: Name of file containing MaterialX document. Assumed to end in ".mtlx".
462 @param emit_all_value_elements: Emit value elements based on node definition, even if not specified on node instance.
463 @return: USD stage.
464 '''
465 stage = Usd.Stage.CreateInMemory()
466 doc = mx.createDocument()
467 mtlx_file_path = mx.FilePath(mtlx_file_name)
468
469 if not mtlx_file_path.exists():
470 self.log(f"Failed to read file: {mtlx_file_path.asString()}")
471 return
472
473 # Find nodes to transform before importing the definition library
474 mx.readFromXmlFile(doc, mtlx_file_name)
475 mtlx_nodes = self.find_materialx_nodes(doc)
476
477 stdlib = self.create_library_document()
478 doc.setDataLibrary(stdlib)
479
480 self.emit_document_metadata(doc, stage)
481
482 # Translate
483 self.emit_usd_shader_graph(doc, stage, mtlx_nodes, emit_all_value_elements)
484 return stage
485
487 '''
488 Create a MaterialX library document.
489 @return: MaterialX library document.
490 '''
491 stdlib = mx.createDocument()
492 search_path = mx.getDefaultDataSearchPath()
493 mx.loadLibraries(mx.getDefaultDataLibraryFolders(), search_path, stdlib)
494 return stdlib
Sample converter from MaterialX to USD.
emit_usd_value_elements(self, node, usd_node, emit_all_value_elements)
Emit MaterialX value elements in USD.
find_materialx_nodes(self, doc)
Find all nodes in a MaterialX document.
map_mtlx_to_usd_type(self, str mtlx_type)
Map a MaterialX type to a USD Sdf type.The mapping is easier from MaterialX as the number of type var...
log(self, msg, level=0)
Log a message.
map_mtlx_to_usd_shader_notation(self, name)
Utility to map from MaterialX shader notation to USD notation.
emit(self, mtlx_file_name, emit_all_value_elements)
Read in a MaterialX file and emit it to a new USD Stage.
emit_usd_shader_graph(self, doc, stage, mtlx_nodes, emit_all_value_elements, root="/MaterialX/Materials/")
Emit USD shader graph to a given stage from a list of MaterialX nodes.
set_prim_mtlx_version(self, Usd.Prim prim, str version)
Set the MaterialX version on a prim.
get_usd_types(self)
Retrieve a list of USD Sdf value type names.
create_library_document(self)
Create a MaterialX library document.
map_mtlx_to_usd_value(self, mtlx_type, mtlx_value)
Map a MaterialX value of a given type to a USD value.
emit_usd_connections(self, node, stage, root_path)
Emit connections between MaterialX elements as USD connections for a given MaterialX node.
emit_document_metadata(self, doc, stage)
Emit MaterialX document metadata to the USD stage.