MaterialX / glTF Texture Procedurals  0.0.2
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 input_name = input.getNamePath() if use_paths else input.getName()
433 json_node = {
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][input_name] = json_node
457
458 # Add input to dictionary
459 nodegraph_inputs[input.getNamePath()] = input_name
460 else:
461 self.logger.error(f'> 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 for output in graph_outputs:
467 output_name = output.getNamePath() if use_paths else output.getName()
468 json_node = {}
469 json_node[KHR_TEXTURE_PROCEDURALS_NODETYPE] = output.getCategory()
470 json_node[KHR_TEXTURE_PROCEDURALS_TYPE] = output.getType()
471
472 # Add additional attributes to the output
473 for meta in metadata:
474 if output.getAttribute(meta):
475 json_node[meta] = output.getAttribute(meta)
476
477 # Add connection if any. Only interfacename and nodename
478 # are supported.
479 connection = output.getAttribute(MTLX_INTERFACEINPUT_NAME_ATTRIBUTE)
480 if len(connection) == 0:
481 connection = output.getAttribute(MTLX_NODE_NAME_ATTRIBUTE)
482
483 connection_node = graph.getChild(connection)
484 if connection_node:
485 connection_path = connection_node.getNamePath()
486 if debug:
487 json_node['debug_connection_path'] = connection_path
488
489 # Add an input or node connection
490 if nodegraph_inputs.get(connection_path) is not None:
491 json_node[KHR_TEXTURE_PROCEDURALS_INPUT] = nodegraph_inputs[connection_path]
492 elif nodegraph_nodes.get(connection_path) is not None:
493 json_node[KHR_TEXTURE_PROCEDURALS_NODE] = nodegraph_nodes[connection_path]
494 else:
495 self.logger.error(f'> Invalid output connection to: {connection_path}')
496
497 # Add output qualifier if any
498 output_string = output.getAttribute(MTLX_OUTPUT_ATTRIBUTE)
499 if len(output_string) > 0:
500 json_node[KHR_TEXTURE_PROCEDURALS_OUTPUT] = output_string
501
502 nodegraph[KHR_TEXTURE_PROCEDURALS_OUTPUTS_BLOCK][output_name] = json_node
503
504 # Add output to dictionary
505 nodegraph_outputs[output.getNamePath()] = output_name
506
507 # Add nodes to the graph
508 for node in graph.getNodes():
509 json_node = nodegraph[KHR_TEXTURE_PROCEDURALS_NODES_BLOCK][nodegraph_nodes[node.getNamePath()]]
510 json_node[KHR_TEXTURE_PROCEDURALS_NODETYPE] = node.getCategory()
511 nodedef = node.getNodeDef()
512
513 # Skip unsupported nodes
514 if not nodedef:
515 self.logger.error(f'> Missing nodedef for node: {node.getNamePath()}')
516 continue
517
518 if debug and nodedef and nodedef.getNodeGroup():
519 json_node[KHR_TEXTURE_PROCEDURALS_NODEGROUP] = nodedef.getNodeGroup()
520
521 for attr_name in node.getAttributeNames():
522 json_node[attr_name] = node.getAttribute(attr_name)
523
524 # Add node inputs
525 #
526 inputs = {}
527 for input in node.getInputs():
528 input_name = input.getNamePath() if use_paths else input.getName()
529 input_item = {
530 'nodetype': 'input'
531 }
532
533 for meta in metadata:
534 if input.getAttribute(meta):
535 input_item[meta] = input.getAttribute(meta)
536
537 input_type = input.getAttribute(mx.TypedElement.TYPE_ATTRIBUTE)
538 input_item[KHR_TEXTURE_PROCEDURALS_TYPE] = input_type
539
540 # Add connection. Connections superscede values.
541 # Only interfacename and nodename are supported.
542 is_interface = True
543 connection = input.getAttribute(MTLX_INTERFACEINPUT_NAME_ATTRIBUTE)
544 if not connection:
545 is_interface = False
546 connection = input.getAttribute(MTLX_NODE_NAME_ATTRIBUTE)
547
548 if connection:
549 connection_node = graph.getChild(connection)
550 if connection_node:
551 connection_path = connection_node.getNamePath()
552 if debug:
553 input_item['debug_connection_path'] = connection_path
554
555 if is_interface and nodegraph_inputs.get(connection_path) is not None:
556 input_item[KHR_TEXTURE_PROCEDURALS_INPUT] = nodegraph_inputs[connection_path]
557 elif nodegraph_nodes.get(connection_path) is not None:
558 input_item[KHR_TEXTURE_PROCEDURALS_NODE] = nodegraph_nodes[connection_path]
559
560 output_string = input.getAttribute(MTLX_OUTPUT_ATTRIBUTE)
561 if output_string:
562 input_item[KHR_TEXTURE_PROCEDURALS_OUTPUT] = output_string
563 #connected_node_outputs = connection_node.getOutputs()
564 #for i, connected_output in enumerate(connected_node_outputs):
565 # if connected_output.getName() == output_string:
566 # input_item[KHR_TEXTURE_PROCEDURALS_OUTPUT] = i
567 # break
568 else:
569 self.logger.error(f'> Invalid input connection to: '
570 '{connection} from input: {input.getNamePath()} '
571 'node: {node.getNamePath()}')
572
573 # Node input value if any
574 elif input.getValue() is not None:
575 if input_type == mx.FILENAME_TYPE_STRING:
576 texture = {}
577 filename = input.getResolvedValueString()
578 self.initialize_glTF_texture(texture, input.getNamePath(), filename, images_block)
579 texture_array.append(texture)
580 input_item[KHR_TEXTURE_PROCEDURALS_TEXTURE] = len(texture_array) - 1
581 else:
582 value = input.getValueString()
583 value = self.string_to_scalar(value, input_type)
584 input_item[KHR_TEXTURE_PROCEDURALS_VALUE] = value
585
586 inputs[input_name] = input_item
587
588 # Add node inputs list
589 if inputs:
590 json_node[KHR_TEXTURE_PROCEDURALS_INPUTS_BLOCK] = inputs
591
592 # Find explicit node outputs
593 outputs = {}
594 for output in node.getOutputs():
595 output_name = output.getName()
596 output_item = {
597 'nodetype': KHR_TEXTURE_PROCEDURALS_OUTPUT,
598 KHR_TEXTURE_PROCEDURALS_TYPE: output.getType()
599 }
600 outputs[output_name] = output_item
601
602 # Add implicit outputs (based on nodedef)
603 if nodedef:
604 for output in nodedef.getOutputs():
605 if not any(output_item != output.getName() for output_item in outputs):
606 output_item = {
607 'nodetype': KHR_TEXTURE_PROCEDURALS_OUTPUT,
608 KHR_TEXTURE_PROCEDURALS_TYPE: output.getType()
609 }
610 outputs[output.getName()] = output_item
611 else:
612 self.logger.warning(f'> Missing nodedef for node: {node.getNamePath()}')
613
614 # Add to node outputs list
615 if outputs:
616 json_node[KHR_TEXTURE_PROCEDURALS_OUTPUTS_BLOCK] = outputs
617
618 return [procs, nodegraph_outputs, nodegraph_nodes]
619
620 def materialX_to_glTF(self, mtlx_doc):
621 '''
622 @brief Convert a MaterialX document to glTF.
623 @param mtlx_doc: The MaterialX document to convert.
624 @return glTF JSON string and status message.
625 '''
626
627 status = ''
628 if not mtlx_doc:
629 status = 'Invalid document to convert'
630 return None, status
631
632 materials = []
633 mx_materials = mtlx_doc.getMaterialNodes()
634 if len(mx_materials) == 0:
635 self.logger.warning('> No materials found in document')
636 #return None, status # No MaterialX materials found in the document
637
638 json_data = {}
639 json_asset = {
640 "version": "2.0",
641 "generator": "MaterialX 1.39 / glTF 2.0 Texture Procedural Converter"
642 }
643
644 # Supported input mappings
645 input_maps = {}
646 input_maps[MTLX_GLTF_PBR_CATEGORY] = [
647 # Contains:
648 # <MaterialX input name>, <gltf input name>, [<gltf parent block>]
649 # Note that this ordering must match the order of the inputs in the MaterialX shader
650 # in order produce an instance which matches the definition.
651 ['base_color', 'baseColorTexture', 'pbrMetallicRoughness'],
652 ['metallic', 'metallicRoughnessTexture', 'pbrMetallicRoughness'],
653 ['roughness', 'metallicRoughnessTexture', 'pbrMetallicRoughness'],
654 ['normal', 'normalTexture', ''],
655 ['occlusion', 'occlusionTexture', ''],
656 ['emissive', 'emissiveTexture', '']
657 ]
658 input_maps[MTLX_UNLIT_CATEGORY_STRING] = [['emission_color', 'baseColorTexture', 'pbrMetallicRoughness']]
659
660 pbr_nodes = {}
661 fallback_texture_index = -1
662 fallback_image_data = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4z/AfAAQAAf/zKSWvAAAAAElFTkSuQmCC'
663 procs = []
664 export_graph_names = []
665
666 extensions_used = [KHR_TEXTURE_PROCEDURALS, EXT_TEXTURE_PROCEDURALS_MX_1_39]
667
668 # Scan for materials
669 for mxMaterial in mx_materials:
670 mtlx_shaders = mx.getShaderNodes(mxMaterial)
671
672 # Scan for shaders for the material
673 for shader_node in mtlx_shaders:
674 category = shader_node.getCategory()
675 path = shader_node.getNamePath()
676 is_pbr = (category == MTLX_GLTF_PBR_CATEGORY)
677 is_unlit = (category == MTLX_UNLIT_CATEGORY_STRING)
678
679 if (is_pbr or is_unlit) and pbr_nodes.get(path) is None:
680 # Add fallback if not already added
681 if fallback_texture_index == -1:
682 fallback_texture_index = self.add_fallback_texture(json_data, fallback_image_data)
683
684 self.logger.info(f'> Convert shader to glTF: {shader_node.getNamePath()}. Category: {category}')
685 pbr_nodes[path] = shader_node
686
687 material = {}
688
689 material[KHR_TEXTURE_PROCEDURALS_NAME] = path
690 if is_unlit:
691 material[KHR_EXTENSIONS_BLOCK] = {}
692 material[KHR_EXTENSIONS_BLOCK][KHR_MATERIALX_UNLIT] = {}
693 # Append if not found
694 if KHR_MATERIALX_UNLIT not in extensions_used:
695 extensions_used.append(KHR_MATERIALX_UNLIT)
696
697 shader_node_input = None
698 shader_node_output = ''
699 input_pairs = input_maps[category]
700
701 # Scan through support inupt channels
702 for input_pair in input_pairs:
703 shader_node_input = shader_node.getInput(input_pair[0])
704 shader_node_output = input_pair[1]
705
706 if not shader_node_input:
707 continue
708
709 # Check for upstream nodegraph connection. Skip if not found
710 nodegraph_name = shader_node_input.getNodeGraphString()
711 if len(nodegraph_name) == 0:
712 continue
713
714 # Check for upstream nodegraph output connection.
715 nodegraph_output = shader_node_input.getOutputString()
716
717 # Determine the parent of the input
718 parent = material
719 if len(input_pair[2]) > 0:
720 if input_pair[2] not in material:
721 material[input_pair[2]] = {}
722 parent = material[input_pair[2]]
723
724 # Check for an existing converted graph and / or output index
725 # in the "procedurals" list
726 graph_index = -1
727 output_name = ""
728 outputs_length = 0
729 if procs:
730 for i, proc in enumerate(procs):
731 if proc[KHR_TEXTURE_PROCEDURALS_NAME] == nodegraph_name:
732 graph_index = i
733 outputs_length = len(nodegraph_output)
734 if outputs_length > 0:
735 outputs_list = proc[KHR_TEXTURE_PROCEDURALS_OUTPUTS_BLOCK]
736 for test_name, item in outputs_list.items():
737 if test_name == nodegraph_output:
738 output_name = test_name
739 break
740 break
741
742 # Make the connection to the input on the material if the graph is already converted
743 if graph_index >= 0:
744 shader_input_texture = parent[shader_node_output] = {}
745 shader_input_texture[KHR_TEXTURE_PROCEDURALS_INDEX] = fallback_texture_index
746 ext = shader_input_texture[KHR_EXTENSIONS_BLOCK] = {}
747
748 # Set up graph index and output index. Only set output index if
749 # the graph has more than one output
750 lookup = ext[KHR_TEXTURE_PROCEDURALS] = {}
751 lookup[KHR_TEXTURE_PROCEDURALS_INDEX] = graph_index
752 if len(output_name):
753 lookup[KHR_TEXTURE_PROCEDURALS_OUTPUT] = output_name
754
755 # Convert the graph
756 else:
757 graph = mtlx_doc.getNodeGraph(nodegraph_name)
758 export_graph_names.append(nodegraph_name)
759
760 gltf_info = self.materialX_graph_to_glTF(graph, json_data)
761 procs = gltf_info[0]
762 output_nodes = gltf_info[1]
763
764 # Add a fallback texture
765 shader_input_texture = parent[shader_node_output] = {}
766 shader_input_texture[KHR_TEXTURE_PROCEDURALS_INDEX] = fallback_texture_index
767 ext = shader_input_texture[KHR_EXTENSIONS_BLOCK] = {}
768 lookup = ext[KHR_TEXTURE_PROCEDURALS] = {}
769 lookup[KHR_TEXTURE_PROCEDURALS_INDEX] = len(procs) - 1
770 output_name = ""
771
772 # Assign an output index if the graph has more than one output
773 if len(nodegraph_output) > 0:
774 nodegraph_outputPath = f"{nodegraph_name}/{nodegraph_output}"
775 if nodegraph_outputPath in output_nodes:
776 output_name = output_nodes[nodegraph_outputPath]
777 lookup[KHR_TEXTURE_PROCEDURALS_OUTPUT] = output_name
778 else:
779 self.logger.error(f'> Failed to find output: {nodegraph_output} '
780 ' in: {output_nodes}')
781 else:
782 # Set to first key in output_nodes
783 lookup[KHR_TEXTURE_PROCEDURALS_OUTPUT] = next(iter(output_nodes.values()))
784
785 if KHR_TEXTURE_PROCEDURALS_NAME in material:
786 materials.append(material)
787
788 # Scan for unconnected graphs
789 unconnected_graphs = []
790 for ng in mtlx_doc.getNodeGraphs():
791 ng_name = ng.getName()
792 if ng.getAttribute(MTLX_NODEDEF_NAME_ATTRIBUTE) or ng.hasSourceUri():
793 continue
794 if ng_name not in export_graph_names:
795 unconnected_graphs.append(ng_name)
796 gltf_info = self.materialX_graph_to_glTF(ng, json_data)
797 procs = gltf_info[0]
798 output_nodes = gltf_info[1]
799
800 if len(materials) > 0:
801 json_data[KHR_MATERIALS_BLOCK] = materials
802 if len(unconnected_graphs) > 0:
803 status = 'Exported unconnected graphs: ' + ', '.join(unconnected_graphs)
804 else:
805 if len(unconnected_graphs) > 0:
806 status = 'Exported unconnected graphs: ' + ', '.join(unconnected_graphs)
807 else:
808 status = 'No appropriate glTF shader graphs found'
809
810 # if images is empty remove it
811 images_block = json_data.get(KHR_IMAGES_BLOCK, [])
812 if len(images_block) == 0:
813 json_data.pop(KHR_IMAGES_BLOCK, None)
814 # if textures is empty remove it
815 textures_block = json_data.get(KHR_TEXTURES_BLOCK, [])
816 if len(textures_block) == 0:
817 json_data.pop(KHR_TEXTURES_BLOCK, None)
818
819 # Add asset and extensions use blocks
820 if procs and len(procs) > 0:
821 json_data[KHR_ASSET_BLOCK] = json_asset
822 json_data[KHR_EXTENTIONSUSED_BLOCK] = extensions_used
823
824 # Get the JSON string back
825 json_string = json.dumps(json_data, indent=2) if json_data else ''
826 if json_string == '{}':
827 json_string = ''
828 else:
829 json_string = ''
830 status = 'No procedural graphs converted'
831
832 return json_string, status
833
834
837
838 def set_add_asset_info(self, add_asset_info):
839 '''
840 Set the flag to add asset information from glTF to the generated MaterialX files.
841 @param add_asset_info: The flag to add asset information
842 '''
843 self.add_asset_info = add_asset_info
844
845 def scalar_to_string(self, value, type):
846 '''
847 Convert a scalar value to a string value that is supported by MaterialX.
848 @param value: The scalar value to convert.
849 @param type: The type of the value.
850 @return The converted string value, or None if the type is unsupported.
851 '''
852 return_value = None
853 if type in self.supported_types:
855 return_value = ', '.join(map(lambda x: ('{0:g}'.format(x) if isinstance(x, float) else str(x)), value))
856 elif type in ['integer', 'float']:
857 # If it'of type 'float' or 'integer', extract value from array
858 return_value = '{0:g}'.format(value[0]) if isinstance(value, list) else '{0:g}'.format(value) if isinstance(value, float) else str(value)
859 else:
860 return_value = '{0:g}'.format(value) if isinstance(value, float) else str(value)
861 else:
862 self.logger.warning(f'> Unsupported type:"{type}" not found in supported list: {self.supported_types}')
863
864 return return_value
865
866 def get_glTF_texture_uri(self, texture, images):
867 '''
868 Get the URI of a glTF texture.
869
870 @param texture: The glTF texture.
871 @param images: The set of glTF images.
872 @return The URI of the texture.
873 '''
874 uri = ''
875 if texture and KHR_IMAGE_SOURCE in texture:
876 source = texture[KHR_IMAGE_SOURCE]
877 if source < len(images):
878 image = images[source]
879 if 'uri' in image:
880 uri = image['uri']
881 return uri
882
883 def add_inputs_from_nodedef(self, node, node_def):
884 '''
885 Add inputs to a node from a given MaterialX node definition.
886
887 @param node: The MaterialX node to add inputs to.
888 @param node_def: The MaterialX node definition to add inputs from.
889 @return The updated MaterialX node.
890 '''
891 if node_def:
892 for node_def_input in node_def.getActiveInputs():
893 input_name = node_def_input.getName()
894 node_input = node.getInput(input_name)
895 if not node_input:
896 node_input = node.addInput(input_name, node_def_input.getType())
897 if node_def_input.hasValueString():
898 node_input.setValueString(node_def_input.getValueString())
899 node_input.setType(node_def_input.getType())
900 return node
901
903 '''
904 Does the glTF document have the required procedural texture extensions.
905
906 @param gltf_doc: The glTF document to check.
907 @return The result of the check in the form [boolean, string], where "boolean" is true if the extensions are present,
908 and "string" is an error message if the extensions are not present.
909 '''
910 # Check extensionsUsed for KHR_texture_procedurals
911 extensions_used = gltf_doc.get('extensionsUsed', None)
912 if extensions_used is None:
913 return [None, 'No extension used']
914
915 found = [False, False]
916 for ext in extensions_used:
917 if ext == KHR_TEXTURE_PROCEDURALS:
918 found[0] = True
919 if ext == EXT_TEXTURE_PROCEDURALS_MX_1_39:
920 found[1] = True
921
922 if not found[0]:
923 return [None, f'Missing {KHR_TEXTURE_PROCEDURALS} extension']
924 if not found[1]:
925 return [None, f'Missing{EXT_TEXTURE_PROCEDURALS_MX_1_39} extension']
926
927 return [True, '']
928
929 def glTF_to_materialX(self, gltf_doc, stdlib):
930 '''
931 Convert a glTF document to a MaterialX document.
932 @param gltFDoc: The glTF document to import.
933 @param stdlib: The MateriaLX standard library to use for the conversion.
934 @return The MaterialX document if successful, otherwise None.
935 '''
936 if not gltf_doc:
937 self.logger.error('> No glTF document specified')
938 return None
939
940 extension_check = self.have_procedural_tex_extensions(gltf_doc)
941 if extension_check[0] is None:
942 return None
943
944 # Prepare the glTF to add names to the graph if not already present
945 self.glTF_graph_create_names(gltf_doc)
946
947 doc = mx.createDocument()
948 doc.setAttribute('colorspace', 'lin_rec709')
949
950 # Import the graph
951 self.glTF_graph_to_materialX(doc, gltf_doc)
952
953 global_extensions = gltf_doc.get('extensions', None)
954 procedurals = None
955 if global_extensions and KHR_TEXTURE_PROCEDURALS in global_extensions:
956 procedurals = global_extensions[KHR_TEXTURE_PROCEDURALS].get(KHR_TEXTURE_PROCEDURALS_PROCEDURALS_BLOCK, None)
957 #self.logger.debug(f'Imported all procedurals: {procedurals}')
958
959 # Import materials and connect to graphs as needed
960 gltf_materials = gltf_doc.get(KHR_MATERIALS_BLOCK, None)
961 if gltf_materials:
962
963 input_maps = {}
964 input_maps[MTLX_GLTF_PBR_CATEGORY] = [
965 ['base_color', 'baseColorTexture', 'pbrMetallicRoughness']
966 ]
967 input_maps[MTLX_UNLIT_CATEGORY_STRING] = [['emission_color', 'baseColorTexture', 'pbrMetallicRoughness']]
968
969 for gltf_material in gltf_materials:
970
971 mtlx_shader_name = gltf_material.get(KHR_TEXTURE_PROCEDURALS_NAME, MTLX_DEFAULT_SHADER_NAME)
972 mtlx_material_name = ''
973 if len(mtlx_shader_name) == 0:
974 mtlx_shader_name = MTLX_DEFAULT_SHADER_NAME
975 mtlx_material_name = MTLX_DEFAULT_MATERIAL_NAME
976 else:
977 mtlx_material_name = MTLX_MATERIAL_PREFIX + mtlx_shader_name
978
979 mtlx_shader_name = doc.createValidChildName(mtlx_shader_name)
980 mtlx_material_name = doc.createValidChildName(mtlx_material_name)
981
982 use_unlit = False
983 extensions = gltf_material.get('extensions', None)
984 if extensions and KHR_MATERIALX_UNLIT in extensions:
985 use_unlit = True
986
987 shader_category = MTLX_GLTF_PBR_CATEGORY
988 #nodedef_string = 'ND_gltf_pbr_surfaceshader'
989 if use_unlit:
990 shader_category = MTLX_UNLIT_CATEGORY_STRING
991 nodedef_string = 'ND_surface_unlit'
992
993 shader_node = doc.addNode(shader_category, mtlx_shader_name, mx.SURFACE_SHADER_TYPE_STRING)
994
995 # No need to add all inputs from nodedef.
996 # In fact if there is a defaultgeomprop then it will be added automatically
997 # without any value or connection which causes a validation error.
998 #if stdlib:
999 #odedef = stdlib.getNodeDef(nodedef_string)
1000 #if nodedef:
1001 # self.add_inputs_from_nodedef(shader_node, nodedef)
1002
1003 if procedurals:
1004 current_map = input_maps[shader_category]
1005 for map_item in current_map:
1006 dest_input = map_item[0]
1007 source_texture = map_item[1]
1008 source_parent = map_item[2]
1009
1010 if source_parent:
1011 if source_parent == 'pbrMetallicRoughness':
1012 if 'pbrMetallicRoughness' in gltf_material:
1013 source_texture = gltf_material['pbrMetallicRoughness'].get(source_texture, None)
1014 else:
1015 source_texture = None
1016 else:
1017 source_texture = gltf_material.get(source_texture, None)
1018
1019 base_color_texture = source_texture
1020
1021 if base_color_texture:
1022 extensions = base_color_texture.get('extensions', None)
1023 if extensions and KHR_TEXTURE_PROCEDURALS in extensions:
1024 KHR_texture_procedurals = extensions[KHR_TEXTURE_PROCEDURALS]
1025 procedural_index = KHR_texture_procedurals.get('index', None)
1026 output = KHR_texture_procedurals.get('output', None)
1027
1028 if procedural_index is not None and procedural_index < len(procedurals):
1029 proc = procedurals[procedural_index]
1030 if proc:
1031 nodegraph_name = proc.get('name')
1032 graph_outputs = proc.get(KHR_TEXTURE_PROCEDURALS_OUTPUTS_BLOCK, None)
1033 output_count = len(graph_outputs)
1034 if graph_outputs:
1035 if output is not None:
1036 proc_output = None
1037 for key, value in graph_outputs.items():
1038 if key == output:
1039 proc_output = value
1040 break
1041
1042 if proc_output:
1043 input_node = shader_node.getInput(dest_input)
1044 if not input_node:
1045 input_node = shader_node.addInput(dest_input, proc_output[KHR_TEXTURE_PROCEDURALS_TYPE])
1046
1047 if input_node:
1048 input_node.removeAttribute('value')
1049 input_node.setNodeGraphString(nodegraph_name)
1050 if output_count > 1:
1051 input_node.setAttribute('output', proc_output[KHR_TEXTURE_PROCEDURALS_NAME])
1052
1053
1054 material_node = doc.addNode(mx.SURFACE_MATERIAL_NODE_STRING, mtlx_material_name, mx.MATERIAL_TYPE_STRING)
1055 shader_input = material_node.addInput(mx.SURFACE_SHADER_TYPE_STRING, mx.SURFACE_SHADER_TYPE_STRING)
1056 shader_input.setAttribute(MTLX_NODE_NAME_ATTRIBUTE, mtlx_shader_name)
1057
1058 self.logger.info(f'> Import material: {material_node.getName()}. Shader: {shader_node.getName()}')
1059
1060 # Import asset information as a doc string
1061 if self.add_asset_info:
1062 asset = gltf_doc.get('asset', None)
1063 mtlx_doc_string = ''
1064 if asset:
1065 version = asset.get('version', None)
1066 if version:
1067 mtlx_doc_string += f'glTF version: {version}. '
1068
1069 generator = asset.get('generator', None)
1070 if generator:
1071 mtlx_doc_string += f'glTF generator: {generator}. '
1072
1073 copyRight = asset.get('copyright', None)
1074 if copyRight:
1075 mtlx_doc_string += f'Copyright: {copyRight}. '
1076
1077 if mtlx_doc_string:
1078 doc.setDocString(mtlx_doc_string)
1079
1080 return doc
1081
1082 def glTF_graph_clear_names(self, gltf_doc):
1083 '''
1084 Clear all the names for all procedural graphs and materials
1085 @param gltf_doc: The glTF document to clear the names in.
1086 '''
1087 extensions = gltf_doc.get('extensions', None)
1088 procedurals = None
1089 if extensions:
1090 procedurals = extensions.get(KHR_TEXTURE_PROCEDURALS, {}).get(KHR_TEXTURE_PROCEDURALS_PROCEDURALS_BLOCK, None)
1091
1092 if procedurals:
1093 for proc in procedurals:
1094 # Remove the name from the procedural graph
1095 proc.pop(KHR_TEXTURE_PROCEDURALS_NAME, None)
1096
1097 inputs = proc.get(KHR_TEXTURE_PROCEDURALS_INPUTS_BLOCK, {})
1098 outputs = proc.get(KHR_TEXTURE_PROCEDURALS_OUTPUTS_BLOCK, {})
1099 nodes = proc.get(KHR_TEXTURE_PROCEDURALS_NODES_BLOCK, [])
1100
1101 # Cannot clear key names
1102 #for input_item in inputs:
1103 # input_item.pop(KHR_TEXTURE_PROCEDURALS_NAME, None)
1104 #for output_item in outputs:
1105 # output_item.pop(KHR_TEXTURE_PROCEDURALS_NAME, None)
1106 for node in nodes:
1107 node.pop(KHR_TEXTURE_PROCEDURALS_NAME, None)
1108
1109 materials = gltf_doc.get(KHR_MATERIALS_BLOCK, None)
1110 if materials:
1111 for material in materials:
1112 material.pop(KHR_TEXTURE_PROCEDURALS_NAME, None)
1113
1114 def glTF_graph_create_names(self, gltf_doc):
1115 '''
1116 Create names for all procedural graphs and materials if they don't already exist.
1117 This method should always be run before converting a glTF document to MaterialX to
1118 ensure that all connection references to elements are also handled.
1119 @param gltf_doc: The glTF document to clear the names in.
1120 '''
1121 # Use a dummy document to handle unique name generation
1122 dummy_doc = mx.createDocument()
1123
1124 extensions = gltf_doc.get('extensions', None)
1125 procedurals = None
1126 if extensions:
1127 procedurals = extensions.get(KHR_TEXTURE_PROCEDURALS, {}).get(KHR_TEXTURE_PROCEDURALS_PROCEDURALS_BLOCK, None)
1128
1129 if procedurals:
1130 # Scan all procedurals
1131 for proc in procedurals:
1132 # Generate a procedural graph name if not already set
1133 proc_name = proc.get(KHR_TEXTURE_PROCEDURALS_NAME, '')
1134 if len(proc_name) == 0:
1135 proc_name = MTLX_DEFAULT_GRAPH_NAME
1136 proc['name'] = dummy_doc.createValidChildName(proc_name)
1137 #self.logger.info('Add procedural:' + proc['name'])
1138 dummy_graph = dummy_doc.addNodeGraph(proc['name'])
1139
1140 inputs = proc.get(KHR_TEXTURE_PROCEDURALS_INPUTS_BLOCK, {})
1141 outputs = proc.get(KHR_TEXTURE_PROCEDURALS_OUTPUTS_BLOCK, {})
1142 nodes = proc.get(KHR_TEXTURE_PROCEDURALS_NODES_BLOCK, [])
1143
1144 # Generate input names if not already set
1145 if 0:
1146 for input_item in inputs:
1147 input_name = input_item.get(KHR_TEXTURE_PROCEDURALS_NAME, '')
1148 if len(input_name) == 0:
1149 input_name = MTLX_DEFAULT_INPUT_NAME
1150 input_item['name'] = dummy_graph.createValidChildName(input_name)
1151 self.logger.debug('Add input:' + input_item['name'])
1152 dummy_graph.addInput(input_item['name'])
1153
1154 # Generate output names if not already set
1155 for output_item in outputs:
1156 output_name = output_item.get(KHR_TEXTURE_PROCEDURALS_NAME, '')
1157 if len(output_name) == 0:
1158 output_name = MTLX_DEFAULT_OUTPUT_NAME
1159 output_item['name'] = dummy_graph.createValidChildName(output_name)
1160 self.logger.debug('Add output:' + output_item['name'])
1161 dummy_graph.addOutput(output_item['name'])
1162
1163 # Generate node names if not already set
1164 for node in nodes:
1165 node_name = node.get(KHR_TEXTURE_PROCEDURALS_NAME, '')
1166 if len(node_name) == 0:
1167 node_name = MTLX_DEFAULT_NODE_NAME
1168 node['name'] = dummy_graph.createValidChildName(node_name)
1169 #self.logger.info('Add node:' + node['name'])
1170 node_type = node.get('nodetype', None)
1171 dummy_graph.addChildOfCategory(node_type, node['name'])
1172
1173 # Generate shader names.
1174 materials = gltf_doc.get(KHR_MATERIALS_BLOCK, None)
1175 if materials:
1176 for material in materials:
1177 material_name = material.get(KHR_TEXTURE_PROCEDURALS_NAME, '')
1178 if len(material_name) == 0:
1179 material_name = MTLX_DEFAULT_SHADER_NAME
1180 material['name'] = dummy_doc.createValidChildName(material_name)
1181 dummy_doc.addNode(MTLX_GLTF_PBR_CATEGORY, material['name'], mx.SURFACE_SHADER_TYPE_STRING)
1182
1183 def glTF_graph_to_materialX(self, doc, gltf_doc):
1184 '''
1185 Import the procedural graphs from a glTF document into a MaterialX document.
1186 @param doc: The MaterialX document to import the graphs into.
1187 @param gltf_doc: The glTF document to import the graphs from.
1188 @return The root MaterialX nodegraph if successful, otherwise None.
1189 '''
1190 root_mtlx = None
1191
1192 # Look for the extension
1193 extensions = gltf_doc.get('extensions', None)
1194 procedurals = None
1195 if extensions:
1196 procedurals = extensions.get(KHR_TEXTURE_PROCEDURALS, {}).get(KHR_TEXTURE_PROCEDURALS_PROCEDURALS_BLOCK, None)
1197
1198 if procedurals is None:
1199 self.logger.error('> No procedurals array found')
1200 return None
1201
1202 metadata = self.get_metadata()
1203
1204 # Pre and postfix for automatic graph name generation
1205 graph_index = 0
1206
1207 self.logger.info(f'> Importing {len(procedurals)} procedural graphs')
1208 for proc in procedurals:
1209 self.logger.debug(f'> Scan procedural {graph_index} of {len(procedurals)} :')
1210 if not proc.get('nodetype'):
1211 self.logger.warning('> No nodetype found in procedural. Skipping node')
1212 continue
1213
1214 if proc[KHR_TEXTURE_PROCEDURALS_NODETYPE] != 'nodegraph':
1215 self.logger.warning(f'> Unsupported procedural nodetype found: {proc["nodetype"]}')
1216 continue
1217
1218 # Assign a name to the graph if not already set
1219 graph_name = proc.get('name', 'GRAPH_' + str(graph_index))
1220 if len(graph_name) == 0:
1221 graph_name = 'GRAPH_' + str(graph_index)
1222 graph_name = doc.createValidChildName(graph_name)
1223 proc['name'] = graph_name
1224
1225 # Create new nodegraph and add metadata
1226 self.logger.info(f'> Create new nodegraph: {graph_name}')
1227 mtlx_graph = doc.addNodeGraph(graph_name)
1228 graph_metadata= self.get_graph_metadata()
1229 for meta in graph_metadata:
1230 if meta in proc:
1231 proc_meta_data = proc[meta]
1232 self.logger.debug(f'> Add extra graph attribute: {meta}, {proc_meta_data}')
1233 mtlx_graph.setAttribute(meta, proc_meta_data)
1234
1235 root_mtlx = mtlx_graph
1236
1237 inputs = proc.get(KHR_TEXTURE_PROCEDURALS_INPUTS_BLOCK, {})
1238 outputs = proc.get(KHR_TEXTURE_PROCEDURALS_OUTPUTS_BLOCK, {})
1239 nodes = proc.get(KHR_TEXTURE_PROCEDURALS_NODES_BLOCK, [])
1240
1241 # - Prelabel nodes
1242 # Pre-label inputs
1243 for input_name, input_item in inputs.items():
1244 if len(input_name) == 0:
1245 input_name = MTLX_DEFAULT_INPUT_NAME
1246 input_item['name'] = mtlx_graph.createValidChildName(input_name)
1247 if len(input_name) == 0:
1248 input_name = MTLX_DEFAULT_INPUT_NAME
1249 input_item['name'] = mtlx_graph.createValidChildName(input_name)
1250 for output_name, output_item in outputs.items():
1251 if len(output_name) == 0:
1252 output_name = MTLX_DEFAULT_OUTPUT_NAME
1253 output_item['name'] = mtlx_graph.createValidChildName(output_name)
1254 for node in nodes:
1255 node_name = node.get(KHR_TEXTURE_PROCEDURALS_NAME, MTLX_DEFAULT_NODE_NAME)
1256 if len(node_name) == 0:
1257 node_name = MTLX_DEFAULT_NODE_NAME
1258 node['name'] = mtlx_graph.createValidChildName(node_name)
1259
1260 # Scan for input interfaces in the node graph
1261 self.logger.debug(f'> Scan {len(inputs)} inputs')
1262 for inputname, input_item in inputs.items():
1263 #inputname = input_item.get('name', None)
1264
1265 # A type is required
1266 input_type = input_item.get(KHR_TEXTURE_PROCEDURALS_TYPE, None)
1267 if not input_type:
1268 self.logger.error(f'> Input type not found for graph input: {inputname}')
1269 continue
1270
1271 # Add new interface input
1272 mtlx_input = mtlx_graph.addInput(inputname, input_type)
1273 # Add extra metadata to the input
1274 for meta in metadata:
1275 if meta in input_item:
1276 self.logger.debug(f'> Add extra interface attribute: {meta}, {input_item[meta]}')
1277 mtlx_input.setAttribute(meta, input_item[meta])
1278
1279 # If input is a file reference, examines textures and images to retrieve the URI
1280 if input_type == 'filename':
1281 texture_index = input_item.get('texture', None)
1282 if texture_index is not None:
1283 gltf_textures = gltf_doc.get('textures', None)
1284 gltf_images = gltf_doc.get('images', None)
1285 if gltf_textures and gltf_images:
1286 gltf_texture = gltf_textures[texture_index] if texture_index < len(gltf_textures) else None
1287 if gltf_texture:
1288 uri = self.get_glTF_texture_uri(gltf_texture, gltf_images)
1289 mtlx_input.setValueString(uri, input_type)
1290
1291 # If input has a value, set the value
1292 input_value = input_item.get('value', None)
1293 if input_value is not None:
1294 mtlx_value = self.scalar_to_string(input_value, input_type)
1295 if mtlx_value is not None:
1296 mtlx_input.setValueString(mtlx_value)
1297 mtlx_input.setType(input_type)
1298 else:
1299 mtlx_input.setValueString(str(input_value))
1300 else:
1301 self.logger.error(f'> Interface input has no value specified: {inputname}')
1302
1303 # Scan for nodes in the nodegraph
1304 self.logger.debug(f'> Scan {len(nodes)} nodes')
1305 for node in nodes:
1306 node_name = node.get(KHR_TEXTURE_PROCEDURALS_NAME, None)
1307 node_type = node.get('nodetype', None)
1308 output_type = node.get(KHR_TEXTURE_PROCEDURALS_TYPE, None)
1309 node_outputs = node.get(KHR_TEXTURE_PROCEDURALS_OUTPUTS_BLOCK, [])
1310
1311 # Create a new node in the graph
1312 mtlx_node = mtlx_graph.addChildOfCategory(node_type, node_name)
1313
1314 # Check for multiple outputs on the node to use 'multioutput'
1315 if len(node_outputs) > 1:
1316 output_type = MULTI_OUTPUT_TYPE_STRING
1317 #mtlx_node.setType(output_type)
1318
1319 if output_type:
1320 mtlx_node.setType(output_type)
1321 else:
1322 self.logger.error(f'> No output type specified for node: {node_name}')
1323
1324 # Look for other name, value pair children under node. Such as "xpos": "0.086957",
1325 # For each add an attribute to the node
1326 for key, value in node.items():
1327 if key not in [KHR_TEXTURE_PROCEDURALS_NAME, 'nodetype', KHR_TEXTURE_PROCEDURALS_TYPE, KHR_TEXTURE_PROCEDURALS_INPUTS_BLOCK, KHR_TEXTURE_PROCEDURALS_OUTPUTS_BLOCK]:
1328 self.logger.debug(f'> Add extra node attribute: {key}, {value}')
1329 mtlx_node.setAttribute(key, value)
1330
1331 # Add node inputs
1332 node_inputs = node.get(KHR_TEXTURE_PROCEDURALS_INPUTS_BLOCK, {})
1333 for input_name, input_item in node_inputs.items():
1334 if not input_name:
1335 input_name = MTLX_DEFAULT_INPUT_NAME
1336 input_type = input_item.get(KHR_TEXTURE_PROCEDURALS_TYPE, None)
1337
1338 # Add node input
1339 mtlx_input = mtlx_node.addInput(input_name, input_type)
1340
1341 # If input is a file reference, examines textures and images to retrieve the URI
1342 if input_type == 'filename':
1343 texture_index = input_item.get('texture', None)
1344 if texture_index is not None:
1345 gltf_textures = gltf_doc.get('textures', None)
1346 gltf_images = gltf_doc.get('images', None)
1347 if gltf_textures and gltf_images:
1348 gltftexture = gltf_textures[texture_index] if texture_index < len(gltf_textures) else None
1349 if gltftexture:
1350 uri = self.get_glTF_texture_uri(gltftexture, gltf_images)
1351 mtlx_input.setValueString(uri)
1352
1353 # If input has a value, set the value
1354 input_value = input_item.get('value', None)
1355 if input_value is not None:
1356 mtlx_value = self.scalar_to_string(input_value, input_type)
1357 if mtlx_value is not None:
1358 mtlx_input.setValueString(mtlx_value)
1359 mtlx_input.setType(input_type)
1360 else:
1361 self.logger.error(f'> Unsupported input type: {input_type}. Performing straight assignment.')
1362 mtlx_input.setValueString(str(input_value))
1363
1364 # Check for connections
1365 else:
1366 connectable = None
1367
1368 # Set any upstream interface input connection
1369 if 'input' in input_item:
1370 # Get 'input' value
1371 input_key = input_item['input']
1372 if input_key in inputs:
1373 connectable = inputs[input_key]
1374 mtlx_input.setAttribute(MTLX_INTERFACEINPUT_NAME_ATTRIBUTE, connectable[KHR_TEXTURE_PROCEDURALS_NAME])
1375 else:
1376 self.logger.error(f'Input key not found: {input_key}')
1377
1378 # Set any upstream node output connection
1379 elif 'output' in input_item:
1380 if 'node' not in input_item:
1381 connectable = outputs[input_item[KHR_TEXTURE_PROCEDURALS_OUTPUT]] if input_item[KHR_TEXTURE_PROCEDURALS_OUTPUT] < len(outputs) else None
1382 mtlx_input.setAttribute('output', connectable[KHR_TEXTURE_PROCEDURALS_NAME])
1383
1384 # Set and node connection
1385 if 'node' in input_item:
1386 connectable = nodes[input_item[KHR_TEXTURE_PROCEDURALS_NODE]] if input_item[KHR_TEXTURE_PROCEDURALS_NODE] < len(nodes) else None
1387 mtlx_input.setAttribute(MTLX_NODE_NAME_ATTRIBUTE, connectable[KHR_TEXTURE_PROCEDURALS_NAME])
1388
1389 if 'output' in input_item:
1390 # Get the node to connect to
1391 connectable = nodes[input_item[KHR_TEXTURE_PROCEDURALS_NODE]] if input_item[KHR_TEXTURE_PROCEDURALS_NODE] < len(nodes) else None
1392 if connectable:
1393 # Get the output name to connect to
1394 #connected_outputs = connectable.get(KHR_TEXTURE_PROCEDURALS_OUTPUTS_BLOCK, {})
1395 #if connected_outputs:
1396 # output_name = list(connected_outputs.keys())[list(connected_outputs.values()).index(output)]
1397 # print(">>> Scan connected outputs:", connected_outputs, "for output:", output_name)
1398 # output_string = connected_outputs.get(output_name, "")
1399 mtlx_input.setAttribute('output', input_item['output'])
1400 self.logger.debug(f'Set output specifier on input: {mtlx_input.getNamePath()}. Value: {input_item[KHR_TEXTURE_PROCEDURALS_OUTPUT]}')
1401
1402 # Add extra metadata to the input
1403 for meta in metadata:
1404 if meta in input_item:
1405 self.logger.debug(f'> Add extra input attribute: {meta}, {input_item[meta]}')
1406 mtlx_input.setAttribute(meta, input_item[meta])
1407
1408 # Add outputs for multioutput nodes
1409 if len(node_outputs) > 1:
1410 for output_name, output in node_outputs.items():
1411 output_type = output.get(KHR_TEXTURE_PROCEDURALS_TYPE, None)
1412 mtlxoutput = mtlx_node.addOutput(output_name, output_type)
1413 self.logger.debug(f'Add multioutput output {mtlxoutput.getNamePath()} of type {output_type} to node {node_name}')
1414
1415
1416
1417 # Scan for output interfaces in the nodegraph
1418 self.logger.info(f'> Scan {len(outputs)} procedural outputs')
1419 for output_name, output in outputs.items():
1420 #output_name = output.get('name', None)
1421 output_type = output.get(KHR_TEXTURE_PROCEDURALS_TYPE, None)
1422 mtlx_graph_output = mtlx_graph.addOutput(output_name, output_type)
1423
1424 connectable = None
1425
1426 # Check for connection to upstream input
1427 if 'input' in output:
1428 connectable = inputs[output[KHR_TEXTURE_PROCEDURALS_INPUT]] if output[KHR_TEXTURE_PROCEDURALS_INPUT] < len(inputs) else None
1429 if connectable:
1430 mtlx_graph_output.setAttribute(MTLX_INTERFACEINPUT_NAME_ATTRIBUTE, connectable[KHR_TEXTURE_PROCEDURALS_NAME])
1431 else:
1432 self.logger.error(f'Input not found: {output["input"]}, {inputs}')
1433
1434 # Check for connection to upstream node output
1435 elif 'node' in output:
1436 connectable = nodes[output[KHR_TEXTURE_PROCEDURALS_NODE]] if output[KHR_TEXTURE_PROCEDURALS_NODE] < len(nodes) else None
1437 if connectable:
1438 mtlx_graph_output.setAttribute(MTLX_NODE_NAME_ATTRIBUTE, connectable[KHR_TEXTURE_PROCEDURALS_NAME])
1439 if 'output' in output:
1440 mtlx_graph_output.setAttribute('output', output[KHR_TEXTURE_PROCEDURALS_OUTPUT])
1441 else:
1442 self.logger.error(f'> Output node not found: {output["node"]}, {nodes}')
1443
1444 # Add extra metadata to the output
1445 for key, value in output.items():
1446 if key not in [KHR_TEXTURE_PROCEDURALS_NAME, KHR_TEXTURE_PROCEDURALS_TYPE, 'nodetype', 'node', 'output']:
1447 self.logger.debug(f'> Add extra graph output attribute: {key}. Value: {value}')
1448 mtlx_graph_output.setAttribute(key, value)
1449
1450 return root_mtlx
1451
1452 def gltf_string_to_materialX(self, gltFDocString, stdlib):
1453 '''
1454 Convert a glTF document to a MaterialX document.
1455 @param gltFDocString: The glTF document to import.
1456 @param stdlib: The MateriaLX standard library to use for the conversion.
1457 @return The MaterialX document if successful, otherwise None.
1458 '''
1459 gltf_doc = json.loads(gltFDocString)
1460 return self.glTF_to_materialX(gltf_doc, stdlib)
1461
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:838
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:902
get_glTF_texture_uri(self, texture, images)
Get the URI of a glTF texture.
Definition converter.py:866
glTF_to_materialX(self, gltf_doc, stdlib)
Convert a glTF document to a MaterialX document.
Definition converter.py:929
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:883
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:845
materialX_to_glTF(self, mtlx_doc)
Convert a MaterialX document to glTF.
Definition converter.py:620
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