MaterialX / glTF Texture Procedurals  0.0.1
Utilities for interoperability between MaterialX and glTF Texture Procedurals
Loading...
Searching...
No Matches
converter.py
Go to the documentation of this file.
1# converter.py
2
3'''
4@file converter.py
5This module contains the core functionality for MaterialX glTF ProceduralTexture graph conversion.
6'''
7import json
8import MaterialX as mx
9import logging as lg
10
11'''
12Package globals
13'''
14
16KHR_TEXTURE_PROCEDURALS = 'KHR_texture_procedurals'
17
18
20EXT_TEXTURE_PROCEDURALS_MX_1_39 = 'EXT_texture_procedurals_mx_1_39'
21
22
24KHR_MATERIALX_UNLIT = 'KHR_materials_unlit'
25
26
28KHR_EXTENSIONS_BLOCK = 'extensions'
29
30
32KHR_EXTENTIONSUSED_BLOCK = 'extensionsUsed'
33
34
36KHR_ASSET_BLOCK = 'asset'
37
38
40KHR_MATERIALS_BLOCK = 'materials'
41
42
44KHR_TEXTURES_BLOCK = 'textures'
45
46
48KHR_IMAGES_BLOCK = 'images'
49
50
52KHR_IMAGE_SOURCE = 'source'
53
54
56KHR_IMAGE_URI = 'uri'
57
58
60KHR_TEXTURE_PROCEDURALS_TYPE = 'type'
61
62
64KHR_TEXTURE_PROCEDURALS_VALUE = 'value'
65
66
68KHR_TEXTURE_PROCEDURALS_TEXTURE = 'texture'
69
70
72KHR_TEXTURE_PROCEDURALS_INPUTS_BLOCK = 'inputs'
73
74
76KHR_TEXTURE_PROCEDURALS_OUTPUTS_BLOCK = 'outputs'
77
78
80KHR_TEXTURE_PROCEDURALS_NODES_BLOCK = 'nodes'
81
82
84KHR_TEXTURE_PROCEDURALS_NODETYPE = 'nodetype'
85
86
88KHR_TEXTURE_PROCEDURALS_NAME = 'name'
89
90
92KHR_TEXTURE_PROCEDURALS_INPUT = 'input'
93
94
96KHR_TEXTURE_PROCEDURALS_OUTPUT = 'output'
97
98
100KHR_TEXTURE_PROCEDURALS_NODE = 'node'
101
102
104KHR_TEXTURE_PROCEDURALS_INDEX = 'index'
105
106
108KHR_TEXTURE_PROCEDURALS_NODEGROUP = 'nodegroup'
109
110
112KHR_TEXTURE_PROCEDURALS_PROCEDURALS_BLOCK = 'procedurals'
113
114
116MTLX_DEFAULT_MATERIAL_NAME = 'MATERIAL_0'
117
118
120MTLX_MATERIAL_PREFIX = 'MATERIAL_'
121
122
124MTLX_DEFAULT_SHADER_NAME = 'SHADER_0'
125
126
128MTLX_NODEGRAPH_NAME_ATTRIBUTE = 'nodegraph'
129
130
132MTLX_DEFAULT_NODE_NAME = 'NODE_0'
133
134
136MTLX_DEFAULT_INPUT_NAME = 'INPUT_0'
137
138
140MTLX_DEFAULT_OUTPUT_NAME = 'OUTPUT_0'
141
142
144MTLX_DEFAULT_GRAPH_NAME = 'GRAPH_0'
145
146
148MTLX_INTERFACEINPUT_NAME_ATTRIBUTE = 'interfacename'
149
150
152MTLX_NODE_NAME_ATTRIBUTE = 'nodename'
153
154
156MTLX_NODEDEF_NAME_ATTRIBUTE = 'nodedef'
157
158
160MTLX_OUTPUT_ATTRIBUTE = 'output'
161
162
164MTLX_GLTF_PBR_CATEGORY = 'gltf_pbr'
165
166
168MTLX_UNLIT_CATEGORY_STRING = 'surface_unlit'
169
170
172MULTI_OUTPUT_TYPE_STRING = 'multioutput'
173
175 '''
176 @brief Class for converting to convert between glTF Texture Procedurals content and MaterialX
177 '''
178
179 def __init__(self):
180 '''
181 Constructor
182
183 **Attributes**
184 - logger : logging.Logger
185 - Logger instance for the class, used to log information, warnings, and errors.
186
187 - add_asset_info : bool
188 - Option to add asset information during the conversion to MaterialX.
189
190 - supported_types : list of str
191 - List of supported data types. This is fixed to MaterialX 1.39
192
193 - supported_scalar_types : list of str
194 - List of supported scalar data types. This is fixed to MaterialX 1.39
195
196 - array_types : list of str
197 - List of supported array types. This is fixed to MaterialX 1.39.x
198
199 - metadata : list of str
200 - MaterialX and / or 3rd party meta-data to transfer to gltf. Default is MaterialX based metadata for 1.39
201 '''
202 self.logger = lg.getLogger('glTFMtlx')
203 lg.basicConfig(level=lg.INFO)
204
205 # Options for conversion to MaterialX
206 self.add_asset_info = False
207
208 # Options for conversion from Materialx
209 self.supported_types = ['boolean', 'string', 'integer', 'matrix33', 'matrix44', 'vector2', 'vector3', 'vector4', 'float', 'color3', 'color4']
210 self.supported_scalar_types = ['integer', 'matrix33', 'matrix44', 'vector2', 'vector3', 'vector4', 'float', 'color3', 'color4']
211 self.supported_array_typessupported_array_types = ['matrix33', 'matrix44', 'vector2', 'vector3', 'vector4', 'color3', 'color4']
212 self.standard_ui_metadata = ['xpos', 'ypos', 'width', 'height', 'uicolor']
213 self.supported_metadata = ['colorspace', 'unit', 'unittype',
214 'uiname', 'uimin', 'uimax', 'uisoftmin', 'uisoftmax', 'uistep', 'uifolder', 'uiadvanced', 'uivisible',
215 'defaultgeomprop', 'uniform',
216 'doc'] + self.standard_ui_metadata
217 self.supported_graph_metadata = ['colorspace', 'unit', 'unittype', 'uiname', 'doc'] + self.standard_ui_metadata
218
219 def set_debug(self, debug):
220 '''
221 Set the debug flag for the converter.
222 @param debug: The debug flag.
223 '''
224 if debug:
225 self.logger.setLevel(lg.DEBUG)
226 else:
227 self.logger.setLevel(lg.INFO)
228
230 '''
231 Get the standard UI metadata defined by MaterialX.
232 @return The standard UI metadata.
233 '''
234 return self.standard_ui_metadata
235
236 def set_metadata(self, metadata):
237 '''
238 Set the supported metadata for the converter.
239 @param metadata: The metadata to set.
240 '''
241 self.supported_metadata = metadata
242
243 def get_metadata(self):
244 '''
245 Get the supported metadata for the converter.
246 @return The metadata.
247 '''
248 return self.supported_metadata
249
251 '''
252 Get the supported graph metadata for the converter.
253 @return The metadata.
254 '''
255 return self.supported_graph_metadata
256
258 '''
259 Get the target type that MaterialX can be converted to.
260 @return The target type supported by the converter.
261 '''
262 return 'gltf'
263
265 '''
266 Get the source type thay can be converted to MaterialX.
267 @return The source type supported by the converter.
268 '''
269 return 'gltf'
270
271 def string_to_scalar(self, value, type):
272 '''
273 Convert a supported MaterialX value string to a JSON scalar value.
274 @param value: The string value to convert.
275 @param type: The type of the value.
276 @return The converted scalar value if successful, otherwise the original string value.
277 '''
278 return_value = value
279
280 SCALAR_SEPARATOR = ','
281 if type in self.supported_scalar_types:
282 split_value = value.split(SCALAR_SEPARATOR)
283
284 if len(split_value) > 1:
285 return_value = list(map(float, split_value))
286 else:
287 if type == 'integer':
288 return_value = int(value)
289 else:
290 return_value = float(value)
291
292 return return_value
293
294 def initialize_glTF_texture(self, texture, name, uri, images):
295 '''
296 Initialize a new glTF image entry and texture entry which references the image entry.
297
298 @param texture: The glTF texture entry to initialize.
299 @param name: The name of the texture entry.
300 @param uri: The URI of the image entry.
301 @param images: The list of images to append the new image entry to.
302 '''
303 image = {}
304 image[KHR_TEXTURE_PROCEDURALS_NAME] = name
305
306 # Assuming mx.FilePath and mx.FormatPosix equivalents exist in Python context
307 # uri_path = mx.createFilePath(uri) # Assuming mx.FilePath is a class in the context
308 # image[KHR_IMAGE_URI] = uri_path.asString(mx.FormatPosix) # Assuming asString method and FormatPosix constant exist
309 image[KHR_IMAGE_URI] = uri
310
311 images.append(image)
312
313 texture[KHR_TEXTURE_PROCEDURALS_NAME] = name
314 texture[KHR_IMAGE_SOURCE] = len(images) - 1
315
316 def add_fallback_texture(self, json, fallback):
317 '''
318 Add a fallback texture to the glTF JSON object.
319 @param json: The JSON object to add the fallback texture to.
320 @param fallback: The fallback texture URI.
321 @return The index of the fallback texture if successful, otherwise -1.
322 '''
323 fallback_texture_index = -1
324 fallback_image_index = -1
325
326 images_block = json.get(KHR_IMAGES_BLOCK, [])
327 if KHR_IMAGES_BLOCK not in json:
328 json[KHR_IMAGES_BLOCK] = images_block
329
330 for i, image in enumerate(images_block):
331 if image[KHR_IMAGE_URI] == fallback:
332 fallback_image_index = i
333 break
334
335 if fallback_image_index == -1:
336 image = {
337 KHR_IMAGE_URI: fallback,
338 KHR_TEXTURE_PROCEDURALS_NAME: 'KHR_texture_procedural_fallback'
339 }
340 images_block.append(image)
341 fallback_image_index = len(images_block) - 1
342
343 texture_array = json.get(KHR_TEXTURES_BLOCK, [])
344 if KHR_TEXTURES_BLOCK not in json:
345 json[KHR_TEXTURES_BLOCK] = texture_array
346
347 for i, texture in enumerate(texture_array):
348 if texture[KHR_IMAGE_SOURCE] == fallback_image_index:
349 fallback_texture_index = i
350 break
351
352 if fallback_texture_index == -1:
353 texture_array.append({KHR_IMAGE_SOURCE: fallback_image_index})
354 fallback_texture_index = len(texture_array) - 1
355
356 return fallback_texture_index
357
358 def materialX_graph_to_glTF(self, graph, json):
359 '''
360 Export a MaterialX nodegraph to a glTF procedural graph.
361 @param graph: The MaterialX nodegraph to export.
362 @param json: The JSON object to export the procedural graph to.
363 meaning to export all materials.
364 @return The procedural graph JSON object if successful, otherwise None.
365 '''
366 no_result = [None, None, None]
367
368 graph_outputs = graph.getOutputs()
369 if len(graph_outputs) == 0:
370 self.logger.info(f'> No graph outputs found on graph: {graph.getNamePath()}')
371 return no_result
372
373 debug = False
374 use_paths = False
375
376 images_block = json.get(KHR_IMAGES_BLOCK, [])
377 if KHR_IMAGES_BLOCK not in json:
378 json[KHR_IMAGES_BLOCK] = images_block
379
380 texture_array = json.get(KHR_TEXTURES_BLOCK, [])
381 if KHR_TEXTURES_BLOCK not in json:
382 json[KHR_TEXTURES_BLOCK] = texture_array
383
384 # Dictionaries used to compute index for node, input, and output references.
385 # Key is a the path to the items
386 nodegraph_nodes = {}
387 nodegraph_inputs = {}
388 nodegraph_outputs = {}
389
390 # Set up extensions
391 extensions = json.get(KHR_EXTENSIONS_BLOCK, {})
392 if KHR_EXTENSIONS_BLOCK not in json:
393 json[KHR_EXTENSIONS_BLOCK] = extensions
394
395 KHR_texture_procedurals = extensions.get(KHR_TEXTURE_PROCEDURALS, {})
396 if KHR_TEXTURE_PROCEDURALS not in extensions:
397 extensions[KHR_TEXTURE_PROCEDURALS] = KHR_texture_procedurals
398
399 if KHR_TEXTURE_PROCEDURALS_PROCEDURALS_BLOCK not in KHR_texture_procedurals:
400 KHR_texture_procedurals[KHR_TEXTURE_PROCEDURALS_PROCEDURALS_BLOCK] = []
401
402 procs = KHR_texture_procedurals[KHR_TEXTURE_PROCEDURALS_PROCEDURALS_BLOCK]
403 nodegraph = {
404 'name': graph.getNamePath() if use_paths else graph.getName(),
405 'nodetype': graph.getCategory()
406 }
407
408 nodegraph[KHR_TEXTURE_PROCEDURALS_TYPE] = MULTI_OUTPUT_TYPE_STRING if len(graph_outputs) > 1 else graph_outputs[0].getType()
409 nodegraph[KHR_TEXTURE_PROCEDURALS_INPUTS_BLOCK] = []
410 nodegraph[KHR_TEXTURE_PROCEDURALS_OUTPUTS_BLOCK] = []
411 nodegraph[KHR_TEXTURE_PROCEDURALS_NODES_BLOCK] = []
412 procs.append(nodegraph)
413
414 metadata = self.get_metadata()
415
416 # Set nodegraph metadata
417 graph_metadata = self.get_graph_metadata()
418 for meta in graph_metadata:
419 if graph.getAttribute(meta):
420 nodegraph[meta] = graph.getAttribute(meta)
421
422 # Add nodes to to dictonary. Use path as this is globally unique
423 #
424 for node in graph.getNodes():
425 json_node = {'name': node.getNamePath() if use_paths else node.getName()}
426 nodegraph[KHR_TEXTURE_PROCEDURALS_NODES_BLOCK].append(json_node)
427 nodegraph_nodes[node.getNamePath()] = len(nodegraph[KHR_TEXTURE_PROCEDURALS_NODES_BLOCK]) - 1
428
429 # Add inputs to the graph
430 #
431 for input in graph.getInputs():
432 json_node = {
433 'name': input.getNamePath() if use_paths else input.getName(),
434 'nodetype': input.getCategory()
435 }
436
437 for meta in metadata:
438 if input.getAttribute(meta):
439 json_node[meta] = input.getAttribute(meta)
440
441 # Only values are allowed for graph inputs
442 if input.getValue() is not None:
443 input_type = input.getAttribute(mx.TypedElement.TYPE_ATTRIBUTE)
444 json_node[KHR_TEXTURE_PROCEDURALS_TYPE] = input_type
445 if input_type == mx.FILENAME_TYPE_STRING:
446 texture = {}
447 filename = input.getResolvedValueString()
448 # Initialize file texture
449 self.initialize_glTF_texture(texture, input.getNamePath(), filename, images_block)
450 texture_array.append(texture)
451 json_node[KHR_TEXTURE_PROCEDURALS_TEXTURE] = len(texture_array) - 1
452 else:
453 value = input.getValueString()
454 value = self.string_to_scalar(value, input_type)
455 json_node[KHR_TEXTURE_PROCEDURALS_VALUE] = value
456 nodegraph[KHR_TEXTURE_PROCEDURALS_INPUTS_BLOCK].append(json_node)
457
458 # Add input to dictionary
459 nodegraph_inputs[input.getNamePath()] = len(nodegraph[KHR_TEXTURE_PROCEDURALS_INPUTS_BLOCK]) - 1
460 else:
461 self.logger.error('> No value or invalid connection specified for input. Input skipped:', input.getNamePath())
462
463 # Add outputs to the graph
464 #
465 for output in graph_outputs:
466 json_node = {KHR_TEXTURE_PROCEDURALS_NAME: output.getNamePath() if use_paths else output.getName()}
467 nodegraph[KHR_TEXTURE_PROCEDURALS_OUTPUTS_BLOCK].append(json_node)
468 nodegraph_outputs[output.getNamePath()] = len(nodegraph[KHR_TEXTURE_PROCEDURALS_OUTPUTS_BLOCK]) - 1
469
470 json_node[KHR_TEXTURE_PROCEDURALS_NODETYPE] = output.getCategory()
471 json_node[KHR_TEXTURE_PROCEDURALS_TYPE] = output.getType()
472
473 # Add additional attributes to the output
474 for meta in metadata:
475 if output.getAttribute(meta):
476 json_node[meta] = output.getAttribute(meta)
477
478 # Add connection if any. Only interfacename and nodename
479 # are supported.
480 connection = output.getAttribute(MTLX_INTERFACEINPUT_NAME_ATTRIBUTE)
481 if len(connection) == 0:
482 connection = output.getAttribute(MTLX_NODE_NAME_ATTRIBUTE)
483
484 connection_node = graph.getChild(connection)
485 if connection_node:
486 connection_path = connection_node.getNamePath()
487 if debug:
488 json_node['debug_connection_path'] = connection_path
489
490 # Add an input or node connection
491 if nodegraph_inputs.get(connection_path) is not None:
492 json_node[KHR_TEXTURE_PROCEDURALS_INPUT] = nodegraph_inputs[connection_path]
493 elif nodegraph_nodes.get(connection_path) is not None:
494 json_node[KHR_TEXTURE_PROCEDURALS_NODE] = nodegraph_nodes[connection_path]
495 else:
496 self.logger.error(f'> Invalid output connection to: {connection_path}')
497
498 # Add output qualifier if any
499 output_string = output.getAttribute(MTLX_OUTPUT_ATTRIBUTE)
500 if len(output_string) > 0:
501 json_node[KHR_TEXTURE_PROCEDURALS_OUTPUT] = output_string
502
503 # Add nodes to the graph
504 for node in graph.getNodes():
505 json_node = nodegraph[KHR_TEXTURE_PROCEDURALS_NODES_BLOCK][nodegraph_nodes[node.getNamePath()]]
506 json_node[KHR_TEXTURE_PROCEDURALS_NODETYPE] = node.getCategory()
507 nodedef = node.getNodeDef()
508
509 # Skip unsupported nodes
510 if not nodedef:
511 self.logger.error(f'> Missing nodedef for node: {node.getNamePath()}')
512 continue
513
514 if debug and nodedef and nodedef.getNodeGroup():
515 json_node[KHR_TEXTURE_PROCEDURALS_NODEGROUP] = nodedef.getNodeGroup()
516
517 for attr_name in node.getAttributeNames():
518 json_node[attr_name] = node.getAttribute(attr_name)
519
520 # Add node inputs
521 #
522 inputs = []
523 for input in node.getInputs():
524 input_item = {
525 'name': input.getName(),
526 'nodetype': 'input'
527 }
528
529 for meta in metadata:
530 if input.getAttribute(meta):
531 input_item[meta] = input.getAttribute(meta)
532
533 input_type = input.getAttribute(mx.TypedElement.TYPE_ATTRIBUTE)
534 input_item[KHR_TEXTURE_PROCEDURALS_TYPE] = input_type
535
536 # Add connection. Connections superscede values.
537 # Only interfacename and nodename are supported.
538 is_interface = True
539 connection = input.getAttribute(MTLX_INTERFACEINPUT_NAME_ATTRIBUTE)
540 if not connection:
541 is_interface = False
542 connection = input.getAttribute(MTLX_NODE_NAME_ATTRIBUTE)
543
544 if connection:
545 connection_node = graph.getChild(connection)
546 if connection_node:
547 connection_path = connection_node.getNamePath()
548 if debug:
549 input_item['debug_connection_path'] = connection_path
550
551 if is_interface and nodegraph_inputs.get(connection_path) is not None:
552 input_item[KHR_TEXTURE_PROCEDURALS_INPUT] = nodegraph_inputs[connection_path]
553 elif nodegraph_nodes.get(connection_path) is not None:
554 input_item[KHR_TEXTURE_PROCEDURALS_NODE] = nodegraph_nodes[connection_path]
555
556 output_string = input.getAttribute(MTLX_OUTPUT_ATTRIBUTE)
557 if output_string:
558 connected_node_outputs = connection_node.getOutputs()
559 for i, connected_output in enumerate(connected_node_outputs):
560 if connected_output.getName() == output_string:
561 input_item[KHR_TEXTURE_PROCEDURALS_OUTPUT] = i
562 break
563 else:
564 self.logger.error(f'> Invalid input connection to: '
565 '{connection} from input: {input.getNamePath()} '
566 'node: {node.getNamePath()}')
567
568 # Node input value if any
569 elif input.getValue() is not None:
570 if input_type == mx.FILENAME_TYPE_STRING:
571 texture = {}
572 filename = input.getResolvedValueString()
573 self.initialize_glTF_texture(texture, input.getNamePath(), filename, images_block)
574 texture_array.append(texture)
575 input_item[KHR_TEXTURE_PROCEDURALS_TEXTURE] = len(texture_array) - 1
576 else:
577 value = input.getValueString()
578 value = self.string_to_scalar(value, input_type)
579 input_item[KHR_TEXTURE_PROCEDURALS_VALUE] = value
580
581 inputs.append(input_item)
582
583 # Add node inputs list
584 if inputs:
585 json_node[KHR_TEXTURE_PROCEDURALS_INPUTS_BLOCK] = inputs
586
587 # Find explicit node outputs
588 outputs = []
589 for output in node.getOutputs():
590 output_item = {
591 'nodetype': KHR_TEXTURE_PROCEDURALS_OUTPUT,
592 'name': output.getName(),
593 KHR_TEXTURE_PROCEDURALS_TYPE: output.getType()
594 }
595 outputs.append(output_item)
596
597 # Add implicit outputs (based on nodedef)
598 if nodedef:
599 for output in nodedef.getOutputs():
600 if not any(output_item[KHR_TEXTURE_PROCEDURALS_NAME] == output.getName() for output_item in outputs):
601 output_item = {
602 'nodetype': KHR_TEXTURE_PROCEDURALS_OUTPUT,
603 'name': output.getName(),
604 KHR_TEXTURE_PROCEDURALS_TYPE: output.getType()
605 }
606 outputs.append(output_item)
607 else:
608 self.logger.warning(f'> Missing nodedef for node: {node.getNamePath()}')
609
610 # Add to node outputs list
611 if outputs:
612 json_node[KHR_TEXTURE_PROCEDURALS_OUTPUTS_BLOCK] = outputs
613
614 return [procs, nodegraph_outputs, nodegraph_nodes]
615
616 def materialX_to_glTF(self, mtlx_doc):
617 '''
618 @brief Convert a MaterialX document to glTF.
619 @param mtlx_doc: The MaterialX document to convert.
620 @return glTF JSON string and status message.
621 '''
622
623 status = ''
624 if not mtlx_doc:
625 status = 'Invalid document to convert'
626 return None, status
627
628 materials = []
629 mx_materials = mtlx_doc.getMaterialNodes()
630 if len(mx_materials) == 0:
631 self.logger.warning('> No materials found in document')
632 #return None, status # No MaterialX materials found in the document
633
634 json_data = {}
635 json_asset = {
636 "version": "2.0",
637 "generator": "MaterialX 1.39 / glTF 2.0 Texture Procedural Converter"
638 }
639
640 # Supported input mappings
641 input_maps = {}
642 input_maps[MTLX_GLTF_PBR_CATEGORY] = [
643 # Contains:
644 # <MaterialX input name>, <gltf input name>, [<gltf parent block>]
645 ['base_color', 'baseColorTexture', 'pbrMetallicRoughness']
646 ]
647 input_maps[MTLX_UNLIT_CATEGORY_STRING] = [['emission_color', 'baseColorTexture', 'pbrMetallicRoughness']]
648
649 pbr_nodes = {}
650 fallback_texture_index = -1
651 fallback_image_data = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4z/AfAAQAAf/zKSWvAAAAAElFTkSuQmCC'
652 procs = []
653 export_graph_names = []
654
655 extensions_used = [KHR_TEXTURE_PROCEDURALS, EXT_TEXTURE_PROCEDURALS_MX_1_39]
656
657 # Scan for materials
658 for mxMaterial in mx_materials:
659 mtlx_shaders = mx.getShaderNodes(mxMaterial)
660
661 # Scan for shaders for the material
662 for shader_node in mtlx_shaders:
663 category = shader_node.getCategory()
664 path = shader_node.getNamePath()
665 is_pbr = (category == MTLX_GLTF_PBR_CATEGORY)
666 is_unlit = (category == MTLX_UNLIT_CATEGORY_STRING)
667
668 if (is_pbr or is_unlit) and pbr_nodes.get(path) is None:
669 # Add fallback if not already added
670 if fallback_texture_index == -1:
671 fallback_texture_index = self.add_fallback_texture(json_data, fallback_image_data)
672
673 self.logger.info(f'> Convert shader to glTF: {shader_node.getNamePath()}. Category: {category}')
674 pbr_nodes[path] = shader_node
675
676 material = {}
677
678 material[KHR_TEXTURE_PROCEDURALS_NAME] = path
679 if is_unlit:
680 material[KHR_EXTENSIONS_BLOCK] = {}
681 material[KHR_EXTENSIONS_BLOCK][KHR_MATERIALX_UNLIT] = {}
682 # Append if not found
683 if KHR_MATERIALX_UNLIT not in extensions_used:
684 extensions_used.append(KHR_MATERIALX_UNLIT)
685
686 shader_node_input = None
687 shader_node_output = ''
688 input_pairs = input_maps[category]
689
690 # Scan through support inupt channels
691 for input_pair in input_pairs:
692 shader_node_input = shader_node.getInput(input_pair[0])
693 shader_node_output = input_pair[1]
694
695 if not shader_node_input:
696 continue
697
698 # Check for upstream nodegraph connection. Skip if not found
699 nodegraph_name = shader_node_input.getNodeGraphString()
700 if len(nodegraph_name) == 0:
701 continue
702
703 # Check for upstream nodegraph output connection.
704 nodegraph_output = shader_node_input.getOutputString()
705
706 # Determine the parent of the input
707 parent = material
708 if len(input_pair[2]) > 0:
709 if input_pair[2] not in material:
710 material[input_pair[2]] = {}
711 parent = material[input_pair[2]]
712
713 # Check for an existing converted graph and / or output index
714 # in the "procedurals" list
715 graph_index = -1
716 output_index = -1
717 outputs_length = 0
718 if procs:
719 for i, proc in enumerate(procs):
720 if proc[KHR_TEXTURE_PROCEDURALS_NAME] == nodegraph_name:
721 graph_index = i
722 outputs_length = len(nodegraph_output)
723 if outputs_length > 0:
724 for j, output in enumerate(proc[KHR_TEXTURE_PROCEDURALS_OUTPUTS_BLOCK]):
725 if output[KHR_TEXTURE_PROCEDURALS_NAME] == nodegraph_output:
726 output_index = j
727 break
728 break
729
730 # Make the connection to the input on the material if the graph is already converted
731 if graph_index >= 0:
732 shader_input_texture = parent[shader_node_output] = {}
733 shader_input_texture[KHR_TEXTURE_PROCEDURALS_INDEX] = fallback_texture_index
734 ext = shader_input_texture[KHR_EXTENSIONS_BLOCK] = {}
735
736 # Set up graph index and output index. Only set output index if
737 # the graph has more than one output
738 lookup = ext[KHR_TEXTURE_PROCEDURALS] = {}
739 lookup[KHR_TEXTURE_PROCEDURALS_INDEX] = graph_index
740 if output_index >= 0 and outputs_length > 1:
741 lookup[KHR_TEXTURE_PROCEDURALS_OUTPUT] = output_index
742
743 # Convert the graph
744 else:
745 graph = mtlx_doc.getNodeGraph(nodegraph_name)
746 export_graph_names.append(nodegraph_name)
747
748 gltf_info = self.materialX_graph_to_glTF(graph, json_data)
749 procs = gltf_info[0]
750 output_nodes = gltf_info[1]
751
752 # Add a fallback texture
753 shader_input_texture = parent[shader_node_output] = {}
754 shader_input_texture[KHR_TEXTURE_PROCEDURALS_INDEX] = fallback_texture_index
755 ext = shader_input_texture[KHR_EXTENSIONS_BLOCK] = {}
756 lookup = ext[KHR_TEXTURE_PROCEDURALS] = {}
757 lookup[KHR_TEXTURE_PROCEDURALS_INDEX] = len(procs) - 1
758 output_index = -1
759
760 # Assign an output index if the graph has more than one output
761 if len(nodegraph_output) > 0:
762 nodegraph_outputPath = f"{nodegraph_name}/{nodegraph_output}"
763 if nodegraph_outputPath in output_nodes:
764 output_index = output_nodes[nodegraph_outputPath]
765 else:
766 self.logger.error(f'> Failed to find output: {nodegraph_output} '
767 ' in: {output_nodes}')
768 lookup[KHR_TEXTURE_PROCEDURALS_OUTPUT] = output_index
769 else:
770 lookup[KHR_TEXTURE_PROCEDURALS_OUTPUT] = 0
771
772 if KHR_TEXTURE_PROCEDURALS_NAME in material:
773 materials.append(material)
774
775 # Scan for unconnected graphs
776 unconnected_graphs = []
777 for ng in mtlx_doc.getNodeGraphs():
778 ng_name = ng.getName()
779 if ng.getAttribute(MTLX_NODEDEF_NAME_ATTRIBUTE) or ng.hasSourceUri():
780 continue
781 if ng_name not in export_graph_names:
782 unconnected_graphs.append(ng_name)
783 gltf_info = self.materialX_graph_to_glTF(ng, json_data)
784 procs = gltf_info[0]
785 output_nodes = gltf_info[1]
786
787 if len(materials) > 0:
788 json_data[KHR_MATERIALS_BLOCK] = materials
789 if len(unconnected_graphs) > 0:
790 status = 'Exported unconnected graphs: ' + ', '.join(unconnected_graphs)
791 else:
792 if len(unconnected_graphs) > 0:
793 status = 'Exported unconnected graphs: ' + ', '.join(unconnected_graphs)
794 else:
795 status = 'No appropriate glTF shader graphs found'
796
797 # if images is empty remove it
798 images_block = json_data.get(KHR_IMAGES_BLOCK, [])
799 if len(images_block) == 0:
800 json_data.pop(KHR_IMAGES_BLOCK, None)
801 # if textures is empty remove it
802 textures_block = json_data.get(KHR_TEXTURES_BLOCK, [])
803 if len(textures_block) == 0:
804 json_data.pop(KHR_TEXTURES_BLOCK, None)
805
806 # Add asset and extensions use blocks
807 if procs and len(procs) > 0:
808 json_data[KHR_ASSET_BLOCK] = json_asset
809 json_data[KHR_EXTENTIONSUSED_BLOCK] = extensions_used
810
811 # Get the JSON string back
812 json_string = json.dumps(json_data, indent=2) if json_data else ''
813 if json_string == '{}':
814 json_string = ''
815 else:
816 json_string = ''
817 status = 'No procedural graphs converted'
818
819 return json_string, status
820
821
824
825 def set_add_asset_info(self, add_asset_info):
826 '''
827 Set the flag to add asset information from glTF to the generated MaterialX files.
828 @param add_asset_info: The flag to add asset information
829 '''
830 self.add_asset_info = add_asset_info
831
832 def scalar_to_string(self, value, type):
833 '''
834 Convert a scalar value to a string value that is supported by MaterialX.
835 @param value: The scalar value to convert.
836 @param type: The type of the value.
837 @return The converted string value, or None if the type is unsupported.
838 '''
839 return_value = None
840 if type in self.supported_types:
842 return_value = ', '.join(map(lambda x: ('{0:g}'.format(x) if isinstance(x, float) else str(x)), value))
843 else:
844 return_value = '{0:g}'.format(value) if isinstance(value, float) else str(value)
845 else:
846 self.logger.warning(f'> Unsupported type:"{type}" not found in supported list: {self.supported_types}')
847
848 return return_value
849
850 def get_glTF_texture_uri(self, texture, images):
851 '''
852 Get the URI of a glTF texture.
853
854 @param texture: The glTF texture.
855 @param images: The set of glTF images.
856 @return The URI of the texture.
857 '''
858 uri = ''
859 if texture and KHR_IMAGE_SOURCE in texture:
860 source = texture[KHR_IMAGE_SOURCE]
861 if source < len(images):
862 image = images[source]
863 if 'uri' in image:
864 uri = image['uri']
865 return uri
866
867 def add_inputs_from_nodedef(self, node, node_def):
868 '''
869 Add inputs to a node from a given MaterialX node definition.
870
871 @param node: The MaterialX node to add inputs to.
872 @param node_def: The MaterialX node definition to add inputs from.
873 @return The updated MaterialX node.
874 '''
875 if node_def:
876 for node_def_input in node_def.getActiveInputs():
877 input_name = node_def_input.getName()
878 node_input = node.getInput(input_name)
879 if not node_input:
880 node_input = node.addInput(input_name, node_def_input.getType())
881 if node_def_input.hasValueString():
882 node_input.setValueString(node_def_input.getValueString())
883 node_input.setType(node_def_input.getType())
884 return node
885
887 '''
888 Does the glTF document have the required procedural texture extensions.
889
890 @param gltf_doc: The glTF document to check.
891 @return The result of the check in the form [boolean, string], where "boolean" is true if the extensions are present,
892 and "string" is an error message if the extensions are not present.
893 '''
894 # Check extensionsUsed for KHR_texture_procedurals
895 extensions_used = gltf_doc.get('extensionsUsed', None)
896 if extensions_used is None:
897 return [None, 'No extension used']
898
899 found = [False, False]
900 for ext in extensions_used:
901 if ext == KHR_TEXTURE_PROCEDURALS:
902 found[0] = True
903 if ext == EXT_TEXTURE_PROCEDURALS_MX_1_39:
904 found[1] = True
905
906 if not found[0]:
907 return [None, f'Missing {KHR_TEXTURE_PROCEDURALS} extension']
908 if not found[1]:
909 return [None, f'Missing{EXT_TEXTURE_PROCEDURALS_MX_1_39} extension']
910
911 return [True, '']
912
913 def glTF_to_materialX(self, gltf_doc, stdlib):
914 '''
915 Convert a glTF document to a MaterialX document.
916 @param gltFDoc: The glTF document to import.
917 @param stdlib: The MateriaLX standard library to use for the conversion.
918 @return The MaterialX document if successful, otherwise None.
919 '''
920 if not gltf_doc:
921 self.logger.error('> No glTF document specified')
922 return None
923
924 extension_check = self.have_procedural_tex_extensions(gltf_doc)
925 if extension_check[0] is None:
926 return None
927
928 # Prepare the glTF to add names to the graph if not already present
929 self.glTF_graph_create_names(gltf_doc)
930
931 doc = mx.createDocument()
932 doc.setAttribute('colorspace', 'lin_rec709')
933
934 # Import the graph
935 self.glTF_graph_to_materialX(doc, gltf_doc)
936
937 global_extensions = gltf_doc.get('extensions', None)
938 procedurals = None
939 if global_extensions and KHR_TEXTURE_PROCEDURALS in global_extensions:
940 procedurals = global_extensions[KHR_TEXTURE_PROCEDURALS].get(KHR_TEXTURE_PROCEDURALS_PROCEDURALS_BLOCK, None)
941 #self.logger.debug(f'Imported all procedurals: {procedurals}')
942
943 # Import materials and connect to graphs as needed
944 gltf_materials = gltf_doc.get(KHR_MATERIALS_BLOCK, None)
945 if gltf_materials:
946
947 input_maps = {}
948 input_maps[MTLX_GLTF_PBR_CATEGORY] = [
949 ['base_color', 'baseColorTexture', 'pbrMetallicRoughness']
950 ]
951 input_maps[MTLX_UNLIT_CATEGORY_STRING] = [['emission_color', 'baseColorTexture', 'pbrMetallicRoughness']]
952
953 for gltf_material in gltf_materials:
954
955 mtlx_shader_name = gltf_material.get(KHR_TEXTURE_PROCEDURALS_NAME, MTLX_DEFAULT_SHADER_NAME)
956 mtlx_material_name = ''
957 if len(mtlx_shader_name) == 0:
958 mtlx_shader_name = MTLX_DEFAULT_SHADER_NAME
959 mtlx_material_name = MTLX_DEFAULT_MATERIAL_NAME
960 else:
961 mtlx_material_name = MTLX_MATERIAL_PREFIX + mtlx_shader_name
962
963 mtlx_shader_name = doc.createValidChildName(mtlx_shader_name)
964 mtlx_material_name = doc.createValidChildName(mtlx_material_name)
965
966 use_unlit = False
967 extensions = gltf_material.get('extensions', None)
968 if extensions and KHR_MATERIALX_UNLIT in extensions:
969 use_unlit = True
970
971 shader_category = MTLX_GLTF_PBR_CATEGORY
972 #nodedef_string = 'ND_gltf_pbr_surfaceshader'
973 if use_unlit:
974 shader_category = MTLX_UNLIT_CATEGORY_STRING
975 nodedef_string = 'ND_surface_unlit'
976
977 shader_node = doc.addNode(shader_category, mtlx_shader_name, mx.SURFACE_SHADER_TYPE_STRING)
978
979 # No need to add all inputs from nodedef.
980 # In fact if there is a defaultgeomprop then it will be added automatically
981 # without any value or connection which causes a validation error.
982 #if stdlib:
983 #odedef = stdlib.getNodeDef(nodedef_string)
984 #if nodedef:
985 # self.add_inputs_from_nodedef(shader_node, nodedef)
986
987 if procedurals:
988 current_map = input_maps[shader_category]
989 for map_item in current_map:
990 dest_input = map_item[0]
991 source_texture = map_item[1]
992 source_parent = map_item[2]
993
994 if source_parent:
995 if source_parent == 'pbrMetallicRoughness':
996 if 'pbrMetallicRoughness' in gltf_material:
997 source_texture = gltf_material['pbrMetallicRoughness'].get(source_texture, None)
998 else:
999 source_texture = None
1000 else:
1001 source_texture = gltf_material.get(source_texture, None)
1002
1003 base_color_texture = source_texture
1004
1005 if base_color_texture:
1006 extensions = base_color_texture.get('extensions', None)
1007 if extensions and KHR_TEXTURE_PROCEDURALS in extensions:
1008 KHR_texture_procedurals = extensions[KHR_TEXTURE_PROCEDURALS]
1009 procedural_index = KHR_texture_procedurals.get('index', None)
1010 output = KHR_texture_procedurals.get('output', None)
1011
1012 if procedural_index is not None and procedural_index < len(procedurals):
1013 proc = procedurals[procedural_index]
1014 if proc:
1015 nodegraph_name = proc.get('name')
1016 graph_outputs = proc.get(KHR_TEXTURE_PROCEDURALS_OUTPUTS_BLOCK, None)
1017 output_count = len(graph_outputs)
1018 if graph_outputs:
1019 proc_output = graph_outputs[0]
1020 if output is not None:
1021 proc_output = graph_outputs[output]
1022
1023 if proc_output:
1024 input_node = shader_node.getInput(dest_input)
1025 if not input_node:
1026 input_node = shader_node.addInput(dest_input, proc_output[KHR_TEXTURE_PROCEDURALS_TYPE])
1027
1028 if input_node:
1029 input_node.removeAttribute('value')
1030 input_node.setNodeGraphString(nodegraph_name)
1031 if output_count > 1:
1032 input_node.setAttribute('output', proc_output[KHR_TEXTURE_PROCEDURALS_NAME])
1033
1034
1035 material_node = doc.addNode(mx.SURFACE_MATERIAL_NODE_STRING, mtlx_material_name, mx.MATERIAL_TYPE_STRING)
1036 shader_input = material_node.addInput(mx.SURFACE_SHADER_TYPE_STRING, mx.SURFACE_SHADER_TYPE_STRING)
1037 shader_input.setAttribute(MTLX_NODE_NAME_ATTRIBUTE, mtlx_shader_name)
1038
1039 self.logger.info(f'> Import material: {material_node.getName()}. Shader: {shader_node.getName()}')
1040
1041 # Import asset information as a doc string
1042 if self.add_asset_info:
1043 asset = gltf_doc.get('asset', None)
1044 mtlx_doc_string = ''
1045 if asset:
1046 version = asset.get('version', None)
1047 if version:
1048 mtlx_doc_string += f'glTF version: {version}. '
1049
1050 generator = asset.get('generator', None)
1051 if generator:
1052 mtlx_doc_string += f'glTF generator: {generator}. '
1053
1054 copyRight = asset.get('copyright', None)
1055 if copyRight:
1056 mtlx_doc_string += f'Copyright: {copyRight}. '
1057
1058 if mtlx_doc_string:
1059 doc.setDocString(mtlx_doc_string)
1060
1061 return doc
1062
1063 def glTF_graph_clear_names(self, gltf_doc):
1064 '''
1065 Clear all the names for all procedural graphs and materials
1066 @param gltf_doc: The glTF document to clear the names in.
1067 '''
1068 extensions = gltf_doc.get('extensions', None)
1069 procedurals = None
1070 if extensions:
1071 procedurals = extensions.get(KHR_TEXTURE_PROCEDURALS, {}).get(KHR_TEXTURE_PROCEDURALS_PROCEDURALS_BLOCK, None)
1072
1073 if procedurals:
1074 for proc in procedurals:
1075 # Remove the name from the procedural graph
1076 proc.pop(KHR_TEXTURE_PROCEDURALS_NAME, None)
1077
1078 inputs = proc.get(KHR_TEXTURE_PROCEDURALS_INPUTS_BLOCK, [])
1079 outputs = proc.get(KHR_TEXTURE_PROCEDURALS_OUTPUTS_BLOCK, [])
1080 nodes = proc.get(KHR_TEXTURE_PROCEDURALS_NODES_BLOCK, [])
1081
1082 for input_item in inputs:
1083 input_item.pop(KHR_TEXTURE_PROCEDURALS_NAME, None)
1084 for output_item in outputs:
1085 output_item.pop(KHR_TEXTURE_PROCEDURALS_NAME, None)
1086 for node in nodes:
1087 node.pop(KHR_TEXTURE_PROCEDURALS_NAME, None)
1088
1089 materials = gltf_doc.get(KHR_MATERIALS_BLOCK, None)
1090 if materials:
1091 for material in materials:
1092 material.pop(KHR_TEXTURE_PROCEDURALS_NAME, None)
1093
1094 def glTF_graph_create_names(self, gltf_doc):
1095 '''
1096 Create names for all procedural graphs and materials if they don't already exist.
1097 This method should always be run before converting a glTF document to MaterialX to
1098 ensure that all connection references to elements are also handled.
1099 @param gltf_doc: The glTF document to clear the names in.
1100 '''
1101 # Use a dummy document to handle unique name generation
1102 dummy_doc = mx.createDocument()
1103
1104 extensions = gltf_doc.get('extensions', None)
1105 procedurals = None
1106 if extensions:
1107 procedurals = extensions.get(KHR_TEXTURE_PROCEDURALS, {}).get(KHR_TEXTURE_PROCEDURALS_PROCEDURALS_BLOCK, None)
1108
1109 if procedurals:
1110 # Scan all procedurals
1111 for proc in procedurals:
1112 # Generate a procedural graph name if not already set
1113 proc_name = proc.get(KHR_TEXTURE_PROCEDURALS_NAME, '')
1114 if len(proc_name) == 0:
1115 proc_name = MTLX_DEFAULT_GRAPH_NAME
1116 proc['name'] = dummy_doc.createValidChildName(proc_name)
1117 #self.logger.info('Add procedural:' + proc['name'])
1118 dummy_graph = dummy_doc.addNodeGraph(proc['name'])
1119
1120 inputs = proc.get(KHR_TEXTURE_PROCEDURALS_INPUTS_BLOCK, [])
1121 outputs = proc.get(KHR_TEXTURE_PROCEDURALS_OUTPUTS_BLOCK, [])
1122 nodes = proc.get(KHR_TEXTURE_PROCEDURALS_NODES_BLOCK, [])
1123
1124 # Generate input names if not already set
1125 for input_item in inputs:
1126 input_name = input_item.get(KHR_TEXTURE_PROCEDURALS_NAME, '')
1127 if len(input_name) == 0:
1128 input_name = MTLX_DEFAULT_INPUT_NAME
1129 input_item['name'] = dummy_graph.createValidChildName(input_name)
1130 self.logger.debug('Add input:' + input_item['name'])
1131 dummy_graph.addInput(input_item['name'])
1132
1133 # Generate output names if not already set
1134 for output_item in outputs:
1135 output_name = output_item.get(KHR_TEXTURE_PROCEDURALS_NAME, '')
1136 if len(output_name) == 0:
1137 output_name = MTLX_DEFAULT_OUTPUT_NAME
1138 output_item['name'] = dummy_graph.createValidChildName(output_name)
1139 self.logger.debug('Add output:' + output_item['name'])
1140 dummy_graph.addOutput(output_item['name'])
1141
1142 # Generate node names if not already set
1143 for node in nodes:
1144 node_name = node.get(KHR_TEXTURE_PROCEDURALS_NAME, '')
1145 if len(node_name) == 0:
1146 node_name = MTLX_DEFAULT_NODE_NAME
1147 node['name'] = dummy_graph.createValidChildName(node_name)
1148 #self.logger.info('Add node:' + node['name'])
1149 node_type = node.get('nodetype', None)
1150 dummy_graph.addChildOfCategory(node_type, node['name'])
1151
1152 # Generate shader names.
1153 materials = gltf_doc.get(KHR_MATERIALS_BLOCK, None)
1154 if materials:
1155 for material in materials:
1156 material_name = material.get(KHR_TEXTURE_PROCEDURALS_NAME, '')
1157 if len(material_name) == 0:
1158 material_name = MTLX_DEFAULT_SHADER_NAME
1159 material['name'] = dummy_doc.createValidChildName(material_name)
1160 dummy_doc.addNode(MTLX_GLTF_PBR_CATEGORY, material['name'], mx.SURFACE_SHADER_TYPE_STRING)
1161
1162 def glTF_graph_to_materialX(self, doc, gltf_doc):
1163 '''
1164 Import the procedural graphs from a glTF document into a MaterialX document.
1165 @param doc: The MaterialX document to import the graphs into.
1166 @param gltf_doc: The glTF document to import the graphs from.
1167 @return The root MaterialX nodegraph if successful, otherwise None.
1168 '''
1169 root_mtlx = None
1170
1171 # Look for the extension
1172 extensions = gltf_doc.get('extensions', None)
1173 procedurals = None
1174 if extensions:
1175 procedurals = extensions.get(KHR_TEXTURE_PROCEDURALS, {}).get(KHR_TEXTURE_PROCEDURALS_PROCEDURALS_BLOCK, None)
1176
1177 if procedurals is None:
1178 self.logger.error('> No procedurals array found')
1179 return None
1180
1181 metadata = self.get_metadata()
1182
1183 # Pre and postfix for automatic graph name generation
1184 graph_index = 0
1185
1186 self.logger.info(f'> Importing {len(procedurals)} procedural graphs')
1187 for proc in procedurals:
1188 self.logger.debug(f'> Scan procedural {graph_index} of {len(procedurals)} :')
1189 if not proc.get('nodetype'):
1190 self.logger.warning('> No nodetype found in procedural. Skipping node')
1191 continue
1192
1193 if proc[KHR_TEXTURE_PROCEDURALS_NODETYPE] != 'nodegraph':
1194 self.logger.warning(f'> Unsupported procedural nodetype found: {proc["nodetype"]}')
1195 continue
1196
1197 # Assign a name to the graph if not already set
1198 graph_name = proc.get('name', 'GRAPH_' + str(graph_index))
1199 if len(graph_name) == 0:
1200 graph_name = 'GRAPH_' + str(graph_index)
1201 graph_name = doc.createValidChildName(graph_name)
1202 proc['name'] = graph_name
1203
1204 # Create new nodegraph and add metadata
1205 self.logger.info(f'> Create new nodegraph: {graph_name}')
1206 mtlx_graph = doc.addNodeGraph(graph_name)
1207 graph_metadata= self.get_graph_metadata()
1208 for meta in graph_metadata:
1209 if meta in proc:
1210 proc_meta_data = proc[meta]
1211 self.logger.debug(f'> Add extra graph attribute: {meta}, {proc_meta_data}')
1212 mtlx_graph.setAttribute(meta, proc_meta_data)
1213
1214 root_mtlx = mtlx_graph
1215
1216 inputs = proc.get(KHR_TEXTURE_PROCEDURALS_INPUTS_BLOCK, [])
1217 outputs = proc.get(KHR_TEXTURE_PROCEDURALS_OUTPUTS_BLOCK, [])
1218 nodes = proc.get(KHR_TEXTURE_PROCEDURALS_NODES_BLOCK, [])
1219
1220 # - Prelabel nodes
1221 # Pre-label inputs
1222 for input_item in inputs:
1223 input_name = input_item.get('name', MTLX_DEFAULT_INPUT_NAME)
1224 if len(input_name) == 0:
1225 input_name = MTLX_DEFAULT_INPUT_NAME
1226 input_item['name'] = mtlx_graph.createValidChildName(input_name)
1227 for output_item in outputs:
1228 output_name = output_item.get('name', MTLX_DEFAULT_INPUT_NAME)
1229 if len(output_name) == 0:
1230 output_name = MTLX_DEFAULT_OUTPUT_NAME
1231 output_item['name'] = mtlx_graph.createValidChildName(output_name)
1232 for node in nodes:
1233 node_name = node.get(KHR_TEXTURE_PROCEDURALS_NAME, MTLX_DEFAULT_NODE_NAME)
1234 if len(node_name) == 0:
1235 node_name = MTLX_DEFAULT_NODE_NAME
1236 node['name'] = mtlx_graph.createValidChildName(node_name)
1237
1238 # Scan for input interfaces in the node graph
1239 self.logger.debug(f'> Scan {len(inputs)} inputs')
1240 for input_item in inputs:
1241 inputname = input_item.get('name', None)
1242
1243 # A type is required
1244 input_type = input_item.get(KHR_TEXTURE_PROCEDURALS_TYPE, None)
1245 if not input_type:
1246 self.logger.error(f'> Input type not found for graph input: {inputname}')
1247 continue
1248
1249 # Add new interface input
1250 mtlx_input = mtlx_graph.addInput(inputname, input_type)
1251 # Add extra metadata to the input
1252 for meta in metadata:
1253 if meta in input_item:
1254 self.logger.debug(f'> Add extra interface attribute: {meta}, {input_item[meta]}')
1255 mtlx_input.setAttribute(meta, input_item[meta])
1256
1257 # If input is a file reference, examines textures and images to retrieve the URI
1258 if input_type == 'filename':
1259 texture_index = input_item.get('texture', None)
1260 if texture_index is not None:
1261 gltf_textures = gltf_doc.get('textures', None)
1262 gltf_images = gltf_doc.get('images', None)
1263 if gltf_textures and gltf_images:
1264 gltf_texture = gltf_textures[texture_index] if texture_index < len(gltf_textures) else None
1265 if gltf_texture:
1266 uri = self.get_glTF_texture_uri(gltf_texture, gltf_images)
1267 mtlx_input.setValueString(uri, input_type)
1268
1269 # If input has a value, set the value
1270 input_value = input_item.get('value', None)
1271 if input_value is not None:
1272 mtlx_value = self.scalar_to_string(input_value, input_type)
1273 if mtlx_value is not None:
1274 mtlx_input.setValueString(mtlx_value)
1275 mtlx_input.setType(input_type)
1276 else:
1277 mtlx_input.setValueString(str(input_value))
1278 else:
1279 self.logger.error(f'> Interface input has no value specified: {inputname}')
1280
1281 # Scan for nodes in the nodegraph
1282 self.logger.debug(f'> Scan {len(nodes)} nodes')
1283 for node in nodes:
1284 node_name = node.get(KHR_TEXTURE_PROCEDURALS_NAME, None)
1285 node_type = node.get('nodetype', None)
1286 output_type = node.get(KHR_TEXTURE_PROCEDURALS_TYPE, None)
1287 node_outputs = node.get(KHR_TEXTURE_PROCEDURALS_OUTPUTS_BLOCK, [])
1288
1289 # Create a new node in the graph
1290 mtlx_node = mtlx_graph.addChildOfCategory(node_type, node_name)
1291
1292 # Check for multiple outputs on the node to use 'multioutput'
1293 if len(node_outputs) > 1:
1294 output_type = MULTI_OUTPUT_TYPE_STRING
1295 #mtlx_node.setType(output_type)
1296
1297 #print('Set node name:', node_name)
1298 #mtlx_node.setName(node_name)
1299 if output_type:
1300 mtlx_node.setType(output_type)
1301 else:
1302 self.logger.error(f'> No output type specified for node: {node_name}')
1303
1304 # Look for other name, value pair children under node. Such as "xpos": "0.086957",
1305 # For each add an attribute to the node
1306 for key, value in node.items():
1307 if key not in [KHR_TEXTURE_PROCEDURALS_NAME, 'nodetype', KHR_TEXTURE_PROCEDURALS_TYPE, KHR_TEXTURE_PROCEDURALS_INPUTS_BLOCK, KHR_TEXTURE_PROCEDURALS_OUTPUTS_BLOCK]:
1308 self.logger.debug(f'> Add extra node attribute: {key}, {value}')
1309 mtlx_node.setAttribute(key, value)
1310
1311 # Add node inputs
1312 node_inputs = node.get(KHR_TEXTURE_PROCEDURALS_INPUTS_BLOCK, [])
1313 for input_item in node_inputs:
1314 input_name = input_item.get('name', MTLX_DEFAULT_INPUT_NAME)
1315 input_type = input_item.get(KHR_TEXTURE_PROCEDURALS_TYPE, None)
1316
1317 # Add node input
1318 mtlx_input = mtlx_node.addInput(input_name, input_type)
1319
1320 # If input is a file reference, examines textures and images to retrieve the URI
1321 if input_type == 'filename':
1322 texture_index = input_item.get('texture', None)
1323 if texture_index is not None:
1324 gltf_textures = gltf_doc.get('textures', None)
1325 gltf_images = gltf_doc.get('images', None)
1326 if gltf_textures and gltf_images:
1327 gltftexture = gltf_textures[texture_index] if texture_index < len(gltf_textures) else None
1328 if gltftexture:
1329 uri = self.get_glTF_texture_uri(gltftexture, gltf_images)
1330 mtlx_input.setValueString(uri)
1331
1332 # If input has a value, set the value
1333 input_value = input_item.get('value', None)
1334 if input_value is not None:
1335 mtlx_value = self.scalar_to_string(input_value, input_type)
1336 if mtlx_value is not None:
1337 mtlx_input.setValueString(mtlx_value)
1338 mtlx_input.setType(input_type)
1339 else:
1340 self.logger.error(f'> Unsupported input type: {input_type}. Performing straight assignment.')
1341 mtlx_input.setValueString(str(input_value))
1342
1343 # Check for connections
1344 else:
1345 connectable = None
1346
1347 # Set any upstream interface input connection
1348 if 'input' in input_item:
1349 connectable = inputs[input_item[KHR_TEXTURE_PROCEDURALS_INPUT]] if input_item[KHR_TEXTURE_PROCEDURALS_INPUT] < len(inputs) else None
1350 mtlx_input.setAttribute(MTLX_INTERFACEINPUT_NAME_ATTRIBUTE, connectable[KHR_TEXTURE_PROCEDURALS_NAME])
1351
1352 # Set any upstream node output connection
1353 elif 'output' in input_item:
1354 if 'node' not in input_item:
1355 connectable = outputs[input_item[KHR_TEXTURE_PROCEDURALS_OUTPUT]] if input_item[KHR_TEXTURE_PROCEDURALS_OUTPUT] < len(outputs) else None
1356 mtlx_input.setAttribute('output', connectable[KHR_TEXTURE_PROCEDURALS_NAME])
1357
1358 # Set and node connection
1359 if 'node' in input_item:
1360 connectable = nodes[input_item[KHR_TEXTURE_PROCEDURALS_NODE]] if input_item[KHR_TEXTURE_PROCEDURALS_NODE] < len(nodes) else None
1361 mtlx_input.setAttribute(MTLX_NODE_NAME_ATTRIBUTE, connectable[KHR_TEXTURE_PROCEDURALS_NAME])
1362
1363 if 'output' in input_item:
1364 # Get the node to connect to
1365 connectable = nodes[input_item[KHR_TEXTURE_PROCEDURALS_NODE]] if input_item[KHR_TEXTURE_PROCEDURALS_NODE] < len(nodes) else None
1366 if connectable:
1367 # Get the output name to connect to
1368 connected_outputs = connectable.get(KHR_TEXTURE_PROCEDURALS_OUTPUTS_BLOCK, [])
1369 if connected_outputs:
1370 output_index = input_item[KHR_TEXTURE_PROCEDURALS_OUTPUT]
1371 output_string = connected_outputs[output_index][KHR_TEXTURE_PROCEDURALS_NAME]
1372 mtlx_input.setAttribute('output', output_string)
1373 self.logger.debug(f'Set output specifier on input: {mtlx_input.getNamePath()}. Value: {input_item[KHR_TEXTURE_PROCEDURALS_OUTPUT]}')
1374
1375 # Add extra metadata to the input
1376 for meta in metadata:
1377 if meta in input_item:
1378 self.logger.debug(f'> Add extra input attribute: {meta}, {input_item[meta]}')
1379 mtlx_input.setAttribute(meta, input_item[meta])
1380
1381 # Add outputs for multioutput nodes
1382 if len(node_outputs) > 1:
1383 for output in node_outputs:
1384 output_name = output.get('name')
1385 output_type = output.get(KHR_TEXTURE_PROCEDURALS_TYPE, None)
1386 mtlxoutput = mtlx_node.addOutput(output_name, output_type)
1387 self.logger.debug(f'Add multioutput output {mtlxoutput.getNamePath()} of type {output_type} to node {node_name}')
1388
1389
1390
1391 # Scan for output interfaces in the nodegraph
1392 self.logger.info(f'> Scan {len(outputs)} procedural outputs')
1393 for output in outputs:
1394 output_name = output.get('name', None)
1395 output_type = output.get(KHR_TEXTURE_PROCEDURALS_TYPE, None)
1396 mtlx_graph_output = mtlx_graph.addOutput(output_name, output_type)
1397
1398 connectable = None
1399
1400 # Check for connection to upstream input
1401 if 'input' in output:
1402 connectable = inputs[output[KHR_TEXTURE_PROCEDURALS_INPUT]] if output[KHR_TEXTURE_PROCEDURALS_INPUT] < len(inputs) else None
1403 if connectable:
1404 mtlx_graph_output.setAttribute(MTLX_INTERFACEINPUT_NAME_ATTRIBUTE, connectable[KHR_TEXTURE_PROCEDURALS_NAME])
1405 else:
1406 self.logger.error(f'Input not found: {output["input"]}, {inputs}')
1407
1408 # Check for connection to upstream node output
1409 elif 'node' in output:
1410 connectable = nodes[output[KHR_TEXTURE_PROCEDURALS_NODE]] if output[KHR_TEXTURE_PROCEDURALS_NODE] < len(nodes) else None
1411 if connectable:
1412 mtlx_graph_output.setAttribute(MTLX_NODE_NAME_ATTRIBUTE, connectable[KHR_TEXTURE_PROCEDURALS_NAME])
1413 if 'output' in output:
1414 mtlx_graph_output.setAttribute('output', output[KHR_TEXTURE_PROCEDURALS_OUTPUT])
1415 else:
1416 self.logger.error(f'> Output node not found: {output["node"]}, {nodes}')
1417
1418 # Add extra metadata to the output
1419 for key, value in output.items():
1420 if key not in [KHR_TEXTURE_PROCEDURALS_NAME, KHR_TEXTURE_PROCEDURALS_TYPE, 'nodetype', 'node', 'output']:
1421 self.logger.debug(f'> Add extra graph output attribute: {key}. Value: {value}')
1422 mtlx_graph_output.setAttribute(key, value)
1423
1424 return root_mtlx
1425
1426 def gltf_string_to_materialX(self, gltFDocString, stdlib):
1427 '''
1428 Convert a glTF document to a MaterialX document.
1429 @param gltFDocString: The glTF document to import.
1430 @param stdlib: The MateriaLX standard library to use for the conversion.
1431 @return The MaterialX document if successful, otherwise None.
1432 '''
1433 gltf_doc = json.loads(gltFDocString)
1434 return self.glTF_to_materialX(gltf_doc, stdlib)
1435
Class for converting to convert between glTF Texture Procedurals content and MaterialX.
Definition converter.py:174
set_add_asset_info(self, add_asset_info)
glTF to MaterialX methods
Definition converter.py:825
glTF_graph_create_names(self, gltf_doc)
Create names for all procedural graphs and materials if they don't already exist.
get_supported_source_type(self)
Get the source type thay can be converted to MaterialX.
Definition converter.py:264
have_procedural_tex_extensions(self, gltf_doc)
Does the glTF document have the required procedural texture extensions.
Definition converter.py:886
get_glTF_texture_uri(self, texture, images)
Get the URI of a glTF texture.
Definition converter.py:850
glTF_to_materialX(self, gltf_doc, stdlib)
Convert a glTF document to a MaterialX document.
Definition converter.py:913
materialX_graph_to_glTF(self, graph, json)
Export a MaterialX nodegraph to a glTF procedural graph.
Definition converter.py:358
set_metadata(self, metadata)
Set the supported metadata for the converter.
Definition converter.py:236
gltf_string_to_materialX(self, gltFDocString, stdlib)
Convert a glTF document to a MaterialX document.
add_fallback_texture(self, json, fallback)
Add a fallback texture to the glTF JSON object.
Definition converter.py:316
get_supported_target_type(self)
Get the target type that MaterialX can be converted to.
Definition converter.py:257
glTF_graph_to_materialX(self, doc, gltf_doc)
Import the procedural graphs from a glTF document into a MaterialX document.
get_standard_ui_metadata(self)
Get the standard UI metadata defined by MaterialX.
Definition converter.py:229
get_metadata(self)
Get the supported metadata for the converter.
Definition converter.py:243
string_to_scalar(self, value, type)
Convert a supported MaterialX value string to a JSON scalar value.
Definition converter.py:271
glTF_graph_clear_names(self, gltf_doc)
Clear all the names for all procedural graphs and materials.
add_inputs_from_nodedef(self, node, node_def)
Add inputs to a node from a given MaterialX node definition.
Definition converter.py:867
get_graph_metadata(self)
Get the supported graph metadata for the converter.
Definition converter.py:250
set_debug(self, debug)
Set the debug flag for the converter.
Definition converter.py:219
scalar_to_string(self, value, type)
Convert a scalar value to a string value that is supported by MaterialX.
Definition converter.py:832
materialX_to_glTF(self, mtlx_doc)
Convert a MaterialX document to glTF.
Definition converter.py:616
initialize_glTF_texture(self, texture, name, uri, images)
Initialize a new glTF image entry and texture entry which references the image entry.
Definition converter.py:294