MaterialXglTF 1.39.5
Loading...
Searching...
No Matches
core.py
Go to the documentation of this file.
1# core.py
2
3'''
4@file
5This module contains the core definitions and utilities for MaterialX glTF conversion.
6'''
7
8# MaterialX support
9import MaterialX as mx # type: ignore
10import MaterialX.PyMaterialXGenShader as mx_gen_shader # type: ignore
11from MaterialX import PyMaterialXRender as mx_render
12from MaterialX import PyMaterialXRenderGlsl as mx_render_glsl
13from sys import platform
14if platform == 'darwin':
15 from MaterialX import PyMaterialXRenderMsl as mx_render_msl
16
17# JSON / glTF support
18import json
19from pygltflib import GLTF2, BufferFormat # type: ignore
20from pygltflib.utils import ImageFormat # type: ignore
21
22# Utilities
23import os, re, copy, math, datetime
24
25from materialxgltf.globals import *
26
27
30class Util:
31
32 @staticmethod
33 def createMaterialXDoc() -> tuple[mx.Document, list]:
34 '''
35 @brief Utility to create a MaterialX document with the default libraries loaded.
36 @return The created MaterialX document and the list of loaded library filenames.
37 '''
38 doc = mx.createDocument()
39 stdlib = mx.createDocument()
40 libFiles = []
41 searchPath = mx.getDefaultDataSearchPath()
42 libFiles = mx.loadLibraries(mx.getDefaultDataLibraryFolders(), searchPath, stdlib)
43 doc.importLibrary(stdlib)
44
45 return doc, libFiles
46
47 @staticmethod
48 def skipLibraryElement(elem) -> bool:
49 '''
50 @brief Utility to skip library elements when iterating over elements in a document.
51 @return True if the element is not in a library, otherwise False.
52 '''
53 return not elem.hasSourceUri()
54
55 @staticmethod
56 def writeMaterialXDoc(doc, filename, predicate=skipLibraryElement):
57 '''
58 @brief Utility to write a MaterialX document to a file.
59 @param doc The MaterialX document to write.
60 @param filename The name of the file to write to.
61 @param predicate A predicate function to determine if an element should be written. Default is to skip library elements.
62 '''
63 writeOptions = mx.XmlWriteOptions()
64 writeOptions.writeXIncludeEnable = False
65 writeOptions.elementPredicate = predicate
66
67 if filename:
68 mx.writeToXmlFile(doc, filename, writeOptions)
69
70 @staticmethod
71 def writeMaterialXDocString(doc, predicate=skipLibraryElement):
72 '''
73 @brief Utility to write a MaterialX document to string.
74 @param doc The MaterialX document to write.
75 @param predicate A predicate function to determine if an element should be written. Default is to skip library elements.
76 @return The XML string representation of the MaterialX document.
77 '''
78 writeOptions = mx.XmlWriteOptions()
79 writeOptions.writeXIncludeEnable = False
80 writeOptions.elementPredicate = predicate
81
82 result = mx.writeToXmlString(doc, writeOptions)
83 return result
84
85 @staticmethod
86 def makeFilePathsRelative(doc, docPath) -> list:
87 '''
88 @brief Utility to make file paths relative to a document path
89 @param doc The MaterialX document to update.
90 @param docPath The path to make file paths relapy -tive to.
91 @return List of tuples of unresolved and resolved file paths.
92 '''
93 result = []
94
95 for elem in doc.traverseTree():
96 valueElem = None
97 if elem.isA(mx.ValueElement):
98 valueElem = elem
99 if not valueElem or valueElem.getType() != mx.FILENAME_TYPE_STRING:
100 continue
101
102 unresolvedValue = mx.FilePath(valueElem.getValueString())
103 if unresolvedValue.isEmpty():
104 continue
105
106 elementResolver = valueElem.createStringResolver()
107 if unresolvedValue.isAbsolute():
108 elementResolver.setFilePrefix('')
109 resolvedValue = valueElem.getResolvedValueString(elementResolver)
110 resolvedValue = mx.FilePath(resolvedValue).getBaseName()
111 valueElem.setValueString(resolvedValue)
112
113 if unresolvedValue != resolvedValue:
114 result.append([unresolvedValue.asString(mx.FormatPosix), resolvedValue])
115
116 return result
117
118
122 '''
123 @brief Class to hold options for glTF to MaterialX conversion.
124 Available options:
125 - 'addAllInputs' : Add all inputs from the node definition. Default is False.
126 - 'createAssignments' : Create MaterialX assignments for each glTF primitive. Default is False.
127 - 'debugOutput' : Print debug output. Default is False.
128 - 'assignXform' : Assign materials to transform nodes instead of shape nodes. Default is False.
129 '''
130 def __init__(self, *args, **kwargs):
131 '''
132 @brief Constructor
133 '''
134 super().__init__(*args, **kwargs)
135
136 self['createAssignments'] = False
137 self['addAllInputs'] = False
138 self['debugOutput'] = True
139 self['assignXform'] = False
140
141class GLTF2MtlxReader:
142 """
143 @class GLTF2MtlxReader
144 @brief Class to read glTF and convert to MaterialX.
145 @var _log
146 @brief Log string for storing conversion status and error messages.
147 @details This string accumulates log entries during the glTF to MaterialX conversion process.
148 It can be cleared with clearLog() and retrieved with getLog().
149 @var _options
150 @brief Options for the glTF to MaterialX conversion process.
151 @details This is an instance of GLTF2MtlxOptions that holds various options for controlling the conversion process, such as whether to add all inputs from node definitions, whether to create material assignments, and whether to print debug output.
152 """
153 '''
154 Class to read glTF and convert to MaterialX.
155 '''
156 # Log string
157 _log = ''
158
159 # Conversion options
160 _options = GLTF2MtlxOptions()
161
162 def clearLog(self):
163 '''
164 @brief Clear the log string.
165 '''
166 self._log = ''
167
168 def getLog(self):
169 '''
170 @brief Return the log string.
171 @return The log string.
172 '''
173 return self._log
174
175 def log(self, string):
176 '''
177 @brief Add a string to the log.
178 @param string The string to add to the log.
179 '''
180 self._log += string + '\n'
181
182 def setOptions(self, options):
183 '''
184 @brief Set the options for the reader.
185 @param options The options to set.
186 '''
187 self._options = options
188
189 def getOptions(self) -> GLTF2MtlxOptions:
190 '''
191 @brief Get the options for the reader.
192 @return The options.
193 '''
194 return self._options
195
196 def addNodeDefOutputs(self, mx_node):
197
198 # Handle with node outputs are not explicitly specified on
199 # a multioutput node.
200 if mx_node.getType() == MULTI_OUTPUT_TYPE_STRING:
201 mx_node_def = mx_node.getNodeDef()
202 if mx_node_def:
203 for mx_output in mx_node_def.getActiveOutputs():
204 mx_output_name = mx_output.getName()
205 if not mx_node.getOutput(mx_output_name):
206 mx_output_type = mx_output.getType()
207 mx_node.addOutput(mx_output_name, mx_output_type)
208
209 def addMtlxImage(self, materials, nodeName, fileName, nodeCategory, nodeDefId, nodeType, colorspace='') -> mx.Node:
210 '''
211 Create a MaterialX image lookup.
212 @param materials MaterialX document to add the image node to.
213 @param nodeName Name of the image node.
214 @param fileName File name of the image.
215 @param nodeCategory Category of the image node.
216 @param nodeDefId Node definition id of the image node.
217 @param nodeType Type of the image node.
218 @param colorspace Color space of the image node. Default is empty string.
219 @return The created image node.
220 '''
221 nodeName = materials.createValidChildName(nodeName)
222 imageNode = materials.addNode(nodeCategory, nodeName, nodeType)
223 if imageNode:
224 if not imageNode.getNodeDef():
225 self.log('Failed to create image node. Category,name,type: %s %s %s' % (nodeCategory, nodeName, nodeType))
226 return imageNode
227
228 if len(nodeDefId):
229 imageNode.setAttribute(mx.InterfaceElement.NODE_DEF_ATTRIBUTE, nodeDefId)
230
231 fileInput = imageNode.addInputFromNodeDef(mx.Implementation.FILE_ATTRIBUTE)
232 if fileInput:
233 fileInput.setValue(fileName, mx.FILENAME_TYPE_STRING)
234
235 if len(colorspace):
236 colorspaceattr = MTLX_COLOR_SPACE_ATTRIBUTE
237 fileInput.setAttribute(colorspaceattr, colorspace)
238 else:
239 self.log('-- failed to create file input for name: %s' % fileName)
240
241 self.addNodeDefOutputs(imageNode)
242
243 return imageNode
244
245 def addMTLXTexCoordNode(self, image, uvindex) -> mx.Node:
246 '''
247 @brief Create a MaterialX texture coordinate lookup
248 @param image The image node to connect the texture coordinate node to.
249 @param uvindex The uv index to use for the texture coordinate lookup.
250 @return The created texture coordinate node.
251 '''
252 parent = image.getParent()
253 if not parent.isA(mx.GraphElement):
254 return None
255
256 texcoordNode = None
257 if parent:
258
259 texcoordName = parent.createValidChildName('texcoord')
260 texcoordNode = parent.addNode('texcoord', texcoordName, 'vector2')
261
262 if texcoordNode:
263
264 uvIndexInput = texcoordNode.addInputFromNodeDef('index')
265 if uvIndexInput:
266 uvIndexInput.setValue(uvindex)
267
268 # Connect to image node
269 texcoordInput = image.addInputFromNodeDef('texcoord')
270 if texcoordInput:
271 texcoordInput.setAttribute(MTLX_NODE_NAME_ATTRIBUTE, texcoordNode.getName())
272
273 return texcoordNode
274
275 def getGLTFTextureUri(self, texture, images) -> str:
276 '''
277 @brief Get the uri of a glTF texture.
278 @param texture The glTF texture.
279 @param images The set of glTF images.
280 @return The uri of the texture.
281 '''
282 uri = ''
283 if texture and 'source' in texture:
284 source = texture['source']
285 if source < len(images):
286 image = images[source]
287
288 if 'uri' in image:
289 uri = image['uri']
290 return uri
291
292 def readGLTFImageProperties(self, imageNode, gltfTexture, gltfSamplers):
293 '''
294 @brief Convert gltF to MaterialX image properties
295 @param imageNode The MaterialX image node to set properties on.
296 @param gltfTexture The glTF texture to read properties from.
297 @param gltfSamplers The set of glTF samplers to examine properties from.
298 '''
299 texcoord = gltfTexture['texCoord'] if 'texCoord' in gltfTexture else None
300
301 extensions = gltfTexture['extensions'] if 'extensions' in gltfTexture else None
302 transformExtension = extensions['KHR_texture_transform'] if extensions and 'KHR_texture_transform' in extensions else None
303 if transformExtension:
304 rotation = transformExtension['rotation'] if 'rotation' in transformExtension else None
305 if rotation:
306 input = imageNode.addInputFromNodeDef('rotate')
307 if input:
308 # Note: Rotation in glTF and MaterialX are opposite directions
309 # Direction is handled in the MaterialX implementation
310 input.setValueString (str(rotation * TO_DEGREE))
311 offset = transformExtension['offset'] if 'offset' in transformExtension else None
312 if offset:
313 input = imageNode.addInputFromNodeDef('offset')
314 if input:
315 input.setValueString ( str(offset).removeprefix('[').removesuffix(']'))
316 scale = transformExtension['scale'] if 'scale' in transformExtension else None
317 if scale:
318 input = imageNode.addInputFromNodeDef('scale')
319 if input:
320 input.setValueString (str(scale).removeprefix('[').removesuffix(']') )
321
322 # Override texcoord if found in extension
323 texcoordt = transformExtension['texCoord'] if 'texCoord' in transformExtension else None
324 if texcoordt:
325 texcoord = texcoordt
326
327 # Add texcoord node if specified
328 if texcoord:
329 self.addMTLXTexCoordNode(imageNode, texcoord)
330
331 # Read sampler info
332 samplerIndex = gltfTexture['sampler'] if 'sampler' in gltfTexture else None
333 if samplerIndex != None and samplerIndex >= 0:
334 sampler = gltfSamplers[samplerIndex] if samplerIndex < len(gltfSamplers) else None
335
336 filterMap = {}
337 filterMap[9728] = "closest"
338 filterMap[9729] = "linear"
339 filterMap[9984] = "cubic"
340 filterMap[9985] = "closest"
341 filterMap[9986] = "linear"
342 filterMap[9987] = "cubic"
343
344 # Filter. There is only one filter type so set based on the max filter if found, otherwise
345 # min filter if found
346 magFilter = sampler['magFilter'] if 'magFilter' in sampler else None
347 if magFilter:
348 input = imageNode.addInputFromNodeDef('filtertype')
349 if input:
350 filterString = filterMap[magFilter]
351 input.setValueString (filterString)
352 minFilter = sampler['minFilter'] if 'minFilter' in sampler else None
353 if minFilter:
354 input = imageNode.addInputFromNodeDef('filtertype')
355 if input:
356 filterString = filterMap[minFilter]
357 input.setValueString (filterString)
358
359 wrapMap = {}
360 wrapMap[33071] = "clamp"
361 wrapMap[33648] = "mirror"
362 wrapMap[10497] = "periodic"
363 wrapS = sampler['wrapS'] if 'wrapS' in sampler else None
364 if wrapS:
365 input = imageNode.addInputFromNodeDef('uaddressmode')
366 if input:
367 input.setValueString (wrapMap[wrapS])
368 else:
369 self.log('Failed to add uaddressmode input')
370 wrapT = sampler['wrapT'] if 'wrapT' in sampler else None
371 if wrapT:
372 input = imageNode.addInputFromNodeDef('vaddressmode')
373 if input:
374 input.setValueString (wrapMap[wrapT])
375 else:
376 self.log('*** failed to add vaddressmode input')
377
378
379 def readInput(self, materials, texture, values, imageNodeName, nodeCategory, nodeType, nodeDefId,
380 shaderNode, inputNames, gltf_textures, gltf_images, gltf_samplers) -> mx.Node:
381 '''
382 @brief Read glTF material input and set input values or add upstream connected nodes
383 @param materials MaterialX document to update
384 @param texture The glTF texture to read properties from.
385 @param values The values to set on the shader node inputs.
386 @param imageNodeName The name of the image node to create if mapped
387 @param nodeCategory The category of the image node to create if mapped
388 @param nodeType The type of the image node to create if mapped
389 @param nodeDefId The node definition id of the image node to create if mapped
390 @param shaderNode The shader node to update inputs on.
391 @param inputNames The names of the inputs to update on the shader node.
392 @param gltf_textures The set of glTF textures to examine
393 @param gltf_images The set of glTF images to examine
394 @param gltf_samplers The set of glTF samplers to examine
395 @return The created image node if mapped, otherwise None.
396 '''
397 imageNode = None
398
399 # Create and set mapped input
400 if texture:
401 textureIndex = texture['index']
402 texture = gltf_textures[textureIndex] if textureIndex < len(gltf_textures) else None
403 uri = self.getGLTFTextureUri(texture, gltf_images)
404 imageNodeName = materials.createValidChildName(imageNodeName)
405 imageNode = self.addMtlxImage(materials, imageNodeName, uri, nodeCategory, nodeDefId,
406 nodeType, EMPTY_STRING)
407 if imageNode:
408 self.readGLTFImageProperties(imageNode, texture, gltf_samplers)
409
410 for inputName in inputNames:
411 input = shaderNode.addInputFromNodeDef(inputName)
412 if input:
413 input.setAttribute(MTLX_NODE_NAME_ATTRIBUTE, imageNode.getName())
414 input.removeAttribute(MTLX_VALUE_ATTRIBUTE)
415
416 # Create and set unmapped input
417 if not imageNode:
418 if len(values) > 0 and (len(values) == len(inputNames)):
419 for i in range(0, len(values)):
420 inputName = inputNames[i]
421 value = values[i]
422 input = shaderNode.addInputFromNodeDef(inputName)
423 if input:
424 input.setValue(float(value))
425
426 return imageNode
427
428 def versionGreaterThan(self, major, minor, patch):
429 return False
430
431 mx_major, mx_minor, mx_patch = mx.getVersionIntegers()
432 print('-------------- version: ', mx_major, mx_minor, mx_patch, '. vs', major, minor, mx_patch)
433 if mx_major < major:
434 return False
435 if mx_minor < minor:
436 return False
437 if mx_patch < patch:
438 return False
439 return True
440
441 def readColorInput(self, materials, colorTexture, color, imageNodeName, nodeCategory, nodeType, nodeDefId,
442 shaderNode, colorInputName, alphaInputName,
443 gltf_textures, gltf_images, gltf_samplers, colorspace=MTLX_DEFAULT_COLORSPACE):
444 '''
445 @brief Read glTF material color input and set input values or add upstream connected nodes
446 @param materials MaterialX document to update
447 @param colorTexture The glTF texture to read properties from.
448 @param color The color to set on the shader node inputs if unmapped
449 @param imageNodeName The name of the image node to create if mapped
450 @param nodeCategory The category of the image node to create if mapped
451 @param nodeType The type of the image node to create if mapped
452 @param nodeDefId The node definition id of the image node to create if mapped
453 @param shaderNode The shader node to update inputs on.
454 @param colorInputName The name of the color input to update on the shader node.
455 @param alphaInputName The name of the alpha input to update on the shader node.
456 @param gltf_textures The set of glTF textures to examine
457 @param gltf_images The set of glTF images to examine
458 @param gltf_samplers The set of glTF samplers to examine
459 @param colorspace The colorspace to set on the image node if mapped. Default is assumed to be srgb_texture.
460 to match glTF convention.
461 '''
462
463 assignedColorTexture = False
464 assignedAlphaTexture = False
465
466 # Check to see if all inputs shoudl be added
467 addAllInputs = self._options['addAllInputs']
468 if addAllInputs:
469 shaderNode.addInputsFromNodeDef()
470 shaderNode.removeChild('tangent')
471 shaderNode.removeChild('normal')
472 shaderNode.removeChild('clearcoat_normal')
473 shaderNode.removeChild('attenuation_distance')
474
475 # Try to assign a texture (image node)
476 if colorTexture:
477 # Get the index of the texture
478 textureIndex = colorTexture['index']
479 texture = gltf_textures[textureIndex] if textureIndex < len(gltf_textures) else None
480 uri = self.getGLTFTextureUri(texture, gltf_images)
481 imageNodeName = materials.createValidChildName(imageNodeName)
482 imageNode = self.addMtlxImage(materials, imageNodeName, uri, nodeCategory, nodeDefId, nodeType, colorspace)
483
484 if imageNode:
485 self.readGLTFImageProperties(imageNode, colorTexture, gltf_samplers)
486
487 newTextureName = imageNode.getName()
488
489 # Connect texture to color input on shader
490 if len(colorInputName):
491 colorInput = shaderNode.addInputFromNodeDef(colorInputName)
492 if not colorInput:
493 self.log('Failed to add color input:' + colorInputName)
494 else:
495 colorInput.setAttribute(MTLX_NODE_NAME_ATTRIBUTE, newTextureName)
496 colorInput.setOutputString('outcolor')
497 colorInput.removeAttribute(MTLX_VALUE_ATTRIBUTE)
498 assignedColorTexture = True
499
500 # Connect texture to alpha input on shader
501 if len(alphaInputName) and self.versionGreaterThan(1, 38, 10):
502 alphaInput = shaderNode.addInputFromNodeDef(alphaInputName)
503 if not alphaInput:
504 self.log('Failed to add alpha input:' + alphaInputName)
505 else:
506 alphaInput.setAttribute(MTLX_NODE_NAME_ATTRIBUTE, newTextureName)
507 alphaInput.setOutputString('outa')
508 alphaInput.removeAttribute(MTLX_VALUE_ATTRIBUTE)
509
510 assignedAlphaTexture = True
511
512 # Assign constant color / alpha if no texture is assigned
513 if color:
514 if not assignedColorTexture and len(colorInputName):
515 colorInput = shaderNode.addInputFromNodeDef(colorInputName)
516 if not colorInput:
517 nd = shaderNode.getNodeDef()
518 self.log('Failed to add color input: %s' % colorInputName)
519 else:
520 colorInput.setValue(mx.Color3(color[0], color[1], color[2]))
521 if len(colorspace):
522 colorspaceattr = MTLX_COLOR_SPACE_ATTRIBUTE
523 colorInput.setAttribute(colorspaceattr, colorspace)
524 if not assignedAlphaTexture and len(alphaInputName):
525 alphaInput = shaderNode.addInputFromNodeDef(alphaInputName)
526 if not alphaInput:
527 self.log('Failed to add alpha input: %s' % alphaInputName)
528 else:
529 # Force this to be interepret as float vs integer
530 alphaInput.setValue(float(color[3]))
531
532 def glTF2MaterialX(self, doc, gltfDoc) -> bool:
533 '''
534 @brief Convert glTF document to a MaterialX document.
535 @param doc The MaterialX document to update.
536 @param gltfDoc The glTF document to read from.
537 @return True if successful, otherwise False.
538 '''
539
540 materials = gltfDoc['materials'] if 'materials' in gltfDoc else []
541 textures = gltfDoc['textures'] if 'textures' in gltfDoc else []
542 images = gltfDoc['images'] if 'images' in gltfDoc else []
543 samplers = gltfDoc['samplers'] if 'samplers' in gltfDoc else []
544
545 if not materials or len(materials) == 0:
546 self.log('No materials found to convert')
547 return False
548
549 # Remapper from glTF to MaterialX for alpha mode
550 alphaModeMap = {}
551 alphaModeMap['OPAQUE'] = 0
552 alphaModeMap['MASK'] = 1
553 alphaModeMap['BLEND'] = 2
554
555 for material in materials:
556
557 # Generate shader and material names
558 shaderName = MTLX_DEFAULT_SHADER_NAME
559 materialName = MTLX_DEFAULT_MATERIAL_NAME
560 if 'name' in material:
561 gltfMaterialName = material['name']
562 materialName = MTLX_MATERIAL_PREFIX + gltfMaterialName
563 shaderName = MTLX_SHADER_PREFIX + gltfMaterialName
564 shaderName = gltfMaterialName
565 shaderName = doc.createValidChildName(shaderName)
566 materialName = doc.createValidChildName(materialName)
567 # Overwrite name in glTF with generated name
568 material['name'] = materialName
569
570 # Create shader. Check if unlit shader is needed
571 use_unlit = True if 'unlit' in material else False
572 if 'extensions' in material:
573 mat_extensions = material['extensions']
574 if 'KHR_materials_unlit' in mat_extensions:
575 use_unlit = True
576
577 shaderCategory = MTLX_UNLIT_CATEGORY_STRING if use_unlit else MTLX_GLTF_PBR_CATEGORY
578 nodedefString = 'ND_surface_unlit' if use_unlit else 'ND_gltf_pbr_surfaceshader'
579 comment = doc.addChildOfCategory('comment')
580 comment.setDocString(' Generated shader: ' + shaderName + ' ')
581 shaderNode = doc.addNode(shaderCategory, shaderName, mx.SURFACE_SHADER_TYPE_STRING)
582 shaderNode.setAttribute(mx.InterfaceElement.NODE_DEF_ATTRIBUTE, nodedefString)
583
584 addInputsFromNodeDef = self._options['addAllInputs']
585 if addInputsFromNodeDef:
586 shaderNode.addInputsFromNodeDef()
587 shaderNode.removeChild('tangent')
588 shaderNode.removeChild('normal')
589
590 # Create a surface material for the shader node
591 comment = doc.addChildOfCategory('comment')
592 comment.setDocString(' Generated material: ' + materialName + ' ')
593 materialNode = doc.addNode(mx.SURFACE_MATERIAL_NODE_STRING, materialName, mx.MATERIAL_TYPE_STRING)
594 shaderInput = materialNode.addInput(mx.SURFACE_SHADER_TYPE_STRING, mx.SURFACE_SHADER_TYPE_STRING)
595 shaderInput.setAttribute(MTLX_NODE_NAME_ATTRIBUTE, shaderNode.getName())
596
597 if self._options['debugOutput']:
598 print('- Convert gLTF material to MateriaLX: %s' % materialName)
599
600 # Check for separate occlusion - TODO
601 haveSeparateOcclusion = False
602
603 # ----------------------------
604 # Read in pbrMetallicRoughness
605 # ----------------------------
606 if 'pbrMetallicRoughness' in material:
607 pbrMetallicRoughness = material['pbrMetallicRoughness']
608
609 # Parse base color factor
610 # -----------------------
611 baseColorTexture = None
612 if 'baseColorTexture' in pbrMetallicRoughness:
613 baseColorTexture = pbrMetallicRoughness['baseColorTexture']
614 baseColorFactor = None
615 if 'baseColorFactor' in pbrMetallicRoughness:
616 baseColorFactor = pbrMetallicRoughness['baseColorFactor']
617 if baseColorTexture or baseColorFactor:
618 colorInputName = 'base_color'
619 alphaInputName = 'alpha'
620 if use_unlit:
621 colorInputName = 'emission_color'
622 alphaInputName = 'opacity'
623 imagename = 'image_' + colorInputName
624 self.readColorInput(doc, baseColorTexture, baseColorFactor, imagename,
625 MTLX_GLTF_COLOR_IMAGE, MULTI_OUTPUT_TYPE_STRING,
626 '', shaderNode, colorInputName, alphaInputName,
627 textures, images, samplers, MTLX_DEFAULT_COLORSPACE)
628
629 # Parse metallic factor
630 # ---------------------
631 if 'metallicFactor' in pbrMetallicRoughness:
632 metallicFactor = pbrMetallicRoughness['metallicFactor']
633 metallicFactor = str(metallicFactor)
634 metallicInput = shaderNode.addInputFromNodeDef('metallic')
635 metallicInput.setValueString(metallicFactor)
636
637 # Parse roughness factor
638 # ---------------------
639 if 'roughnessFactor' in pbrMetallicRoughness:
640 roughnessFactor = pbrMetallicRoughness['roughnessFactor']
641 roughnessFactor = str(roughnessFactor)
642 roughnessInput = shaderNode.addInputFromNodeDef('roughness')
643 roughnessInput.setValueString(roughnessFactor)
644
645 # Parse texture for metalic, roughness, and occlusion (if not specified separately)
646 # ---------------------------------------------------------------------
647
648 # Check for occlusion/metallic/roughness texture
649 texture = None
650 if 'metallicRoughnessTexture' in pbrMetallicRoughness:
651 texture = pbrMetallicRoughness['metallicRoughnessTexture']
652 if texture:
653 imageNode = self.readInput(doc, texture, [], 'image_orm', MTLX_GLTF_IMAGE, MTLX_VEC3_STRING, '',
654 shaderNode, ['metallic', 'roughness', 'occlusion'], textures, images, samplers)
655 self.readGLTFImageProperties(imageNode, texture, samplers)
656
657 # Route individual channels on ORM image to the appropriate inputs on the shader
658 indexName = [ 'x', 'y', 'z' ]
659 outputName = [ 'outx', 'outy', 'outz' ]
660 metallicInput = shaderNode.addInputFromNodeDef('metallic')
661 roughnessInput = shaderNode.addInputFromNodeDef('roughness')
662 occlusionInput = None if haveSeparateOcclusion else shaderNode.addInputFromNodeDef('occlusion')
663 inputs = [ occlusionInput, roughnessInput, metallicInput ]
664 addSeparateNode = False # TODO: This options is not supported on write parsing yet.
665 addExtractNode = True
666 separateNode = None
667 if addSeparateNode:
668 # Add a separate node to route the channels
669 separateNodeName = doc.createValidChildName('separate_orm')
670 separateNode = doc.addNode('separate3', separateNodeName, MULTI_OUTPUT_TYPE_STRING)
671 seperateInput = separateNode.addInputFromNodeDef(MTLX_IN_STRING)
672 seperateInput.setType(MTLX_VEC3_STRING)
673 seperateInput.setAttribute(MTLX_NODE_NAME_ATTRIBUTE, imageNode.getName())
674 for i in range(0,3):
675 input = inputs[i]
676 if input:
677 input.setType(MTLX_FLOAT_STRING)
678 if addExtractNode:
679 extractNodeName = doc.createValidChildName('extract_orm')
680 extractNode = doc.addNode('extract', extractNodeName, MTLX_FLOAT_STRING)
681 extractNode.addInputsFromNodeDef()
682 extractNodeInput = extractNode.getInput(MTLX_IN_STRING)
683 extractNodeInput.setType(MTLX_VEC3_STRING)
684 extractNodeInput.removeAttribute(MTLX_VALUE_STRING)
685 extractNodeInput.setAttribute(MTLX_NODE_NAME_ATTRIBUTE, imageNode.getName())
686 extractNodeInput = extractNode.getInput('index')
687 extractNodeInput.setValue(i)
688
689 input.setAttribute(MTLX_NODE_NAME_ATTRIBUTE, extractNode.getName())
690 elif separateNode:
691 input.setAttribute(MTLX_NODE_NAME_ATTRIBUTE, separateNode.getName())
692 input.setOutputString(outputName[i])
693
694 # Parse normal input
695 # ------------------
696 if 'normalTexture' in material:
697 normalTexture = material['normalTexture']
698 self.readInput(doc, normalTexture, [], 'image_normal', MTLX_GLTF_NORMALMAP_IMAGE, MTLX_VEC3_STRING, '',
699 shaderNode, ['normal'], textures, images, samplers)
700
701 # Parse occlusion input
702 # ---------------------
703 occlusionTexture = None
704 if 'occlusionTexture' in material:
705 occlusionTexture = material['occlusionTexture']
706 self.readInput(doc, occlusionTexture, [], 'image_occlusion', MTLX_GLTF_IMAGE, MTLX_FLOAT_STRING, '',
707 shaderNode, ['occlusion'], textures, images, samplers)
708
709 # Parse emissive inputs
710 # ----------------------
711 emissiveTexture = None
712 if 'emissiveTexture' in material:
713 emissiveTexture = material['emissiveTexture']
714 emissiveFactor = [0.0, 0.0, 0.0]
715 if 'emissiveFactor' in material:
716 emissiveFactor = material['emissiveFactor']
717 self.readColorInput(doc, emissiveTexture, emissiveFactor, 'image_emissive',
718 MTLX_GLTF_COLOR_IMAGE, MULTI_OUTPUT_TYPE_STRING,
719 '', shaderNode, 'emissive', '', textures, images, samplers, MTLX_DEFAULT_COLORSPACE)
720
721 # Parse and remap alpha mode
722 # --------------------------
723 if 'alphaMode' in material:
724 alphaModeString = material['alphaMode']
725 alphaMode = 0
726 if alphaModeString in alphaModeMap:
727 alphaMode = alphaModeMap[alphaModeString]
728 if alphaMode != 0:
729 alphaModeInput = shaderNode.addInputFromNodeDef('alpha_mode')
730 alphaModeInput.setValue(alphaMode)
731
732 # Parse alpha cutoff
733 # ------------------
734 if 'alphaCutoff' in material:
735 alphaCutOff = material['alphaCutoff']
736 if alphaCutOff != 0.5:
737 alphaCutOffInput = shaderNode.addInputFromNodeDef('alpha_cutoff')
738 alphaCutOffInput.setValue(float(alphaCutOff))
739
740 # Parse extensions
741 # --------------------------------
742 if 'extensions' in material:
743 extensions = material['extensions']
744
745 # Parse untextured ior extension
746 # ------------------------------
747 if 'KHR_materials_ior' in extensions:
748 iorExtension = extensions['KHR_materials_ior']
749 if 'ior' in iorExtension:
750 ior = iorExtension['ior']
751 iorInput = shaderNode.addInputFromNodeDef('ior')
752 iorInput.setValue(float(ior))
753
754 # Parse specular and specular color extension
755 if 'KHR_materials_specular' in extensions:
756 specularExtension = extensions['KHR_materials_specular']
757 specularColorFactor = None
758 if 'specularColorFactor' in specularExtension:
759 specularColorFactor = specularExtension['specularColorFactor']
760 specularColorTexture = None
761 if 'specularColorTexture' in specularExtension:
762 specularColorTexture = specularExtension['specularColorTexture']
763 if specularColorFactor or specularColorTexture:
764 self.readColorInput(doc, specularColorTexture, specularColorFactor, 'image_specularcolor',
765 MTLX_GLTF_COLOR_IMAGE, MULTI_OUTPUT_TYPE_STRING,
766 '', shaderNode, 'specular_color', '', textures, images, MTLX_DEFAULT_COLORSPACE)
767
768 specularTexture = specularFactor = None
769 if 'specularFactor' in specularExtension:
770 specularFactor = specularExtension['specularFactor']
771 if 'specularTexture' in specularExtension:
772 specularTexture = specularExtension['specularTexture']
773 if specularFactor or specularTexture:
774 self.readInput(doc, specularTexture, [specularFactor], 'image_specular', MTLX_GLTF_IMAGE,
775 MTLX_FLOAT_STRING, '', shaderNode, ['specular'], textures, images, samplers)
776
777 # Parse transmission extension
778 if 'KHR_materials_transmission' in extensions:
779 transmissionExtension = extensions['KHR_materials_transmission']
780 if 'transmissionFactor' in transmissionExtension:
781 transmissionFactor = transmissionExtension['transmissionFactor']
782 transmissionInput = shaderNode.addInputFromNodeDef('transmission')
783 transmissionInput.setValue(float(transmissionFactor))
784
785 # Parse iridescence extension - TODO
786 if 'KHR_materials_iridescence' in extensions:
787 iridescenceExtension = extensions['KHR_materials_iridescence']
788 if iridescenceExtension:
789
790 # Parse unmapped or mapped iridescence
791 iridescenceFactor = iridescenceTexture = None
792 if 'iridescenceFactor' in iridescenceExtension:
793 iridescenceFactor = iridescenceExtension['iridescenceFactor']
794 if 'iridescenceTexture' in iridescenceExtension:
795 iridescenceTexture = iridescenceExtension['iridescenceTexture']
796 if iridescenceFactor or iridescenceTexture:
797 self.readInput(doc, iridescenceTexture, [iridescenceFactor], 'image_iridescence', MTLX_GLTF_IMAGE,
798 MTLX_FLOAT_STRING, '', shaderNode, ['iridescence'], textures, images, samplers)
799
800 # Parse mapped or unmapped iridescence IOR
801 if 'iridescenceIor' in iridescenceExtension:
802 iridescenceIor = iridescenceExtension['iridescenceIor']
803 self.readInput(doc, None, [iridescenceIor], '', '',
804 MTLX_FLOAT_STRING, '', shaderNode, ['iridescence_ior'], textures, images, samplers)
805
806 # Parse iridescence texture
807 iridescenceThicknessMinimum = iridescenceExtension['iridescenceThicknessMinimum'] if 'iridescenceThicknessMinimum' in iridescenceExtension else None
808 iridescenceThicknessMaximum = iridescenceExtension['iridescenceThicknessMaximum'] if 'iridescenceThicknessMaximum' in iridescenceExtension else None
809 iridescenceThicknessTexture = iridescenceExtension['iridescenceThicknessTexture'] if 'iridescenceThicknessTexture' in iridescenceExtension else None
810 if iridescenceThicknessMinimum or iridescenceThicknessMaximum or iridescenceThicknessTexture:
811 floatInput = shaderNode.addInputFromNodeDef("iridescence_thickness")
812 if floatInput:
813 uri = ''
814 if iridescenceThicknessTexture:
815 textureIndex = iridescenceThicknessTexture['index']
816 texture = textures[textureIndex] if textureIndex < len(textures) else None
817 uri = self.getGLTFTextureUri(texture, images)
818
819 imageNodeName = doc.createValidChildName("image_iridescence_thickness")
820 newTexture = self.addMtlxImage(doc, imageNodeName, uri, 'gltf_iridescence_thickness', '', MTLX_FLOAT_STRING, '')
821 if newTexture:
822 floatInput.setAttribute(MTLX_NODE_NAME_ATTRIBUTE, newTexture.getName())
823 floatInput.removeAttribute(MTLX_VALUE_STRING)
824
825 if iridescenceThicknessMinimum:
826 minInput = newTexture.addInputFromNodeDef("thicknessMin")
827 if minInput:
828 minInput.setValue(float(iridescenceThicknessMinimum))
829 if iridescenceThicknessMaximum:
830 maxInput = newTexture.addInputFromNodeDef("thicknessMax")
831 if maxInput:
832 maxInput.setValue(float(iridescenceThicknessMaximum))
833
834 if 'KHR_materials_emissive_strength' in extensions:
835 emissiveStrengthExtension = extensions['KHR_materials_emissive_strength']
836 if 'emissiveStrength' in emissiveStrengthExtension:
837 emissiveStrength = emissiveStrengthExtension['emissiveStrength']
838 self.readInput(doc, None, [emissiveStrength], '', '', '', '',
839 shaderNode, ['emissive_strength'], textures, images, samplers)
840
841 # Parse volume Inputs:
842 if 'KHR_materials_volume' in extensions:
843 volumeExtension = extensions['KHR_materials_volume']
844
845 # Textured or untexture thickness
846 thicknessFactor = thicknessTexture = None
847 if 'thicknessFactor' in volumeExtension:
848 thicknessFactor = volumeExtension['thicknessFactor']
849 if 'thicknessTexture' in volumeExtension:
850 thicknessTexture = volumeExtension['thicknessTexture']
851 if thicknessFactor or thicknessTexture:
852 self.readInput(doc, thicknessTexture, [thicknessFactor], 'image_thickness', MTLX_GLTF_IMAGE, MTLX_FLOAT_STRING, '',
853 shaderNode, ['thickness'], textures, images, samplers)
854
855 # Untextured attenuation color
856 if 'attenuationColor' in volumeExtension:
857 attenuationColor = volumeExtension['attenuationColor']
858 attenuationInput = shaderNode.addInputFromNodeDef('attenuation_color')
859 attenuationInput.setValue(mx.Color3(attenuationColor[0], attenuationColor[1], attenuationColor[2]))
860
861 # Untextured attenuation distance
862 if 'attenuationDistance' in volumeExtension:
863 attenuationDistance = volumeExtension['attenuationDistance']
864 attenuationDistance = str(attenuationDistance)
865 attenuationInput = shaderNode.addInputFromNodeDef('attenuation_distance')
866 attenuationInput.setValueString(attenuationDistance)
867
868 # Parse clearcoat
869 if 'KHR_materials_clearcoat' in extensions:
870 clearcoat = extensions['KHR_materials_clearcoat']
871
872 clearcoatFactor = clearcoatTexture = None
873 if 'clearcoatFactor' in clearcoat:
874 clearcoatFactor = clearcoat['clearcoatFactor']
875 if 'clearcoatTexture' in clearcoat:
876 clearcoatTexture = clearcoat['clearcoatTexture']
877 if clearcoatFactor or clearcoatTexture:
878 self.readInput(doc, clearcoatTexture, [clearcoatFactor], 'image_clearcoat',
879 MTLX_GLTF_IMAGE, MTLX_FLOAT_STRING, '', shaderNode, ['clearcoat'],
880 textures, images, samplers)
881
882 clearcoatRoughnessFactor = clearcoatRoughnessTexture = None
883 if 'clearcoatRoughnessFactor' in clearcoat:
884 clearcoatRoughnessFactor = clearcoat['clearcoatRoughnessFactor']
885 if 'clearcoatRoughnessTexture' in clearcoat:
886 clearcoatRoughnessTexture = clearcoat['clearcoatRoughnessTexture']
887 if clearcoatRoughnessFactor or clearcoatRoughnessTexture:
888 self.readInput(doc, clearcoatRoughnessTexture, [clearcoatRoughnessFactor],
889 'image_clearcoat_roughness',
890 MTLX_GLTF_IMAGE, MTLX_FLOAT_STRING, '', shaderNode, ['clearcoat_roughness'],
891 textures, images, samplers)
892
893 if 'clearcoatNormalTexture' in clearcoat:
894 clearcoatNormalTexture = clearcoat['clearcoatNormalTexture']
895 self.readInput(doc, clearcoatNormalTexture, [None], 'image_clearcoat_normal',
896 MTLX_GLTF_NORMALMAP_IMAGE, MTLX_VEC3_STRING, '',
897 shaderNode, ['clearcoat_normal'], textures, images, samplers)
898
899 # Parse sheen
900 if 'KHR_materials_sheen' in extensions:
901 sheen = extensions['KHR_materials_sheen']
902
903 sheenColorFactor = sheenColorTexture = None
904 if 'sheenColorFactor' in sheen:
905 sheenColorFactor = sheen['sheenColorFactor']
906 if 'sheenColorTexture' in sheen:
907 sheenColorTexture = sheen['sheenColorTexture']
908 if sheenColorFactor or sheenColorTexture:
909 self.readColorInput(doc, sheenColorTexture, sheenColorFactor, 'image_sheen',
910 MTLX_GLTF_COLOR_IMAGE, MULTI_OUTPUT_TYPE_STRING,
911 '', shaderNode, 'sheen_color', '', textures, images, MTLX_DEFAULT_COLORSPACE)
912
913 sheenRoughnessFactor = sheenRoughnessTexture = None
914 if 'sheenRoughnessFactor' in sheen:
915 sheenRoughnessFactor = sheen['sheenRoughnessFactor']
916 if 'sheenRoughnessTexture' in sheen:
917 sheenRoughnessTexture = sheen['sheenRoughnessTexture']
918 if sheenRoughnessFactor or sheenRoughnessTexture:
919 self.readInput(doc, sheenRoughnessTexture, [sheenRoughnessFactor], 'image_sheen_roughness', MTLX_GLTF_IMAGE, MTLX_FLOAT_STRING, '',
920 shaderNode, ['sheen_roughness'], textures, images, samplers)
921
922 return True
923
924 def computeMeshMaterials(self, materialMeshList, materialCPVList, cnode, path, nodeCount, meshCount, meshes, nodes, materials):
925 '''
926 @brief Recursively computes mesh to material assignments.
927 @param materialMeshList The dictionary of material to mesh assignments to update.
928 @param materialCPVList The list of materials that require CPV.
929 @param cnode The current node to examine.
930 @param path The current string path to the node. If a node, mesh has no name then a default name is used. Primtives are named by index.
931 @param nodeCount The current node count.
932 @param meshCount The current mesh count.
933 @param meshes The set of meshes to examine.
934 @param nodes The set of nodes to examine.
935 @param materials The set of materials to examine.
936 '''
937
938 # Push node name on to path
939 prevPath = path
940 cnodeName = ''
941 if 'name' in cnode:
942 cnodeName = cnode['name']
943 else:
944 cnodeName = GLTF_DEFAULT_NODE_PREFIX + str(nodeCount)
945 nodeCount = nodeCount + 1
946 path = path + '/' + ( mx.createValidName(cnodeName) )
947
948 # Check if this node is associated with a mesh
949 if 'mesh' in cnode:
950 meshIndex = cnode['mesh']
951 cmesh = meshes[meshIndex]
952 if cmesh:
953
954 # Append mesh name on to path
955 meshName = ''
956 if 'name' in cmesh:
957 meshName = cmesh['name']
958 else:
959 meshName = self.GLTF_DEFAULT_MESH_PREFIX + str(meshCount)
960 meshCount = meshCount + 1
961 path = path + '/' + mx.createValidName(meshName)
962
963 if 'primitives' in cmesh:
964 primitives = cmesh['primitives']
965
966 # Check for material CPV attribute
967 requiresCPV = False
968 primitiveIndex = 0
969 for primitive in primitives:
970
971 material = None
972 if 'material' in primitive:
973 materialIndex = primitive['material']
974 material = materials[materialIndex]
975
976 # Add reference to mesh (by name) to material
977 if material:
978 materialName = material['name']
979
980 if materialName not in materialMeshList:
981 materialMeshList[materialName] = []
982 if len(primitives) == 1:
983 materialMeshList[materialName].append(path)
984 #materialMeshList[materialName].append(path + '/' + GLTF_DEFAULT_PRIMITIVE_PREFIX + str(primitiveIndex))
985 else:
986 materialMeshList[materialName].append(path + '/' + GLTF_DEFAULT_PRIMITIVE_PREFIX + str(primitiveIndex))
987
988 if 'attributes' in primitive:
989 attributes = primitive['attributes']
990 if 'COLOR' in attributes:
991 if self._options['debugOutput']:
992 print('CPV attribute found')
993 requiresCPV = True
994 break
995 if requiresCPV:
996 materialCPVList.append(materialName)
997
998 primitiveIndex = primitiveIndex + 1
999
1000 if 'children' in cnode:
1001 children = cnode['children']
1002 for childIndex in children:
1003 child = nodes[childIndex]
1004 self.computeMeshMaterials(materialMeshList, materialCPVList, child, path, nodeCount, meshCount, meshes, nodes, materials)
1005
1006 # Pop path name
1007 path = prevPath
1008
1009 def buildMaterialAssociations(self, gltfDoc) -> dict:
1010 '''
1011 @brief Build a dictionary of material assignments.
1012 @param gltfDoc The glTF document to read from.
1013 @return A dictionary of material assignments if successful, otherwise None.
1014 '''
1015 materials = gltfDoc['materials'] if 'materials' in gltfDoc else []
1016 meshes = gltfDoc['meshes'] if 'meshes' in gltfDoc else []
1017
1018 if not materials or not meshes:
1019 return {}
1020
1021 meshNameTemplate = "mesh"
1022 meshCount = 0
1023 materialAssignments : dict = {}
1024 for mesh in meshes:
1025 if 'primitives' in mesh:
1026 meshName = mesh['name'] if 'name' in mesh else meshNameTemplate + str(meshCount)
1027 primitives = mesh['primitives']
1028 primitiveCount = 0
1029 for primitive in primitives:
1030 if 'material' in primitive:
1031 materialIndex = primitive['material']
1032 if materialIndex < len(materials):
1033 material = materials[materialIndex]
1034 if 'name' in material:
1035 materialName = material['name']
1036 primitiveName = GLTF_DEFAULT_PRIMITIVE_PREFIX + str(primitiveCount)
1037 if materialName not in materialAssignments:
1038 materialAssignments[materialName] = []
1039 if len(primitives) == 1:
1040 materialAssignments[materialName].append(meshName)
1041 else:
1042 materialAssignments[materialName].append(meshName + '/' + primitiveName)
1043 primitiveCount += 1
1044 meshCount += 1
1045
1046 return materialAssignments
1047
1048 def convert(self, gltfFileName) -> mx.Document:
1049 '''
1050 @brief Convert a glTF file to a MaterialX document.
1051 @param gltfFileName The glTF file to convert.
1052 @return A MaterialX document if successful, otherwise None.
1053 '''
1054
1055 if not os.path.exists(gltfFileName):
1056 if self._options['debugOutput']:
1057 print('File not found:', gltfFileName)
1058 self.log('File not found:' + gltfFileName)
1059 return None
1060
1061 gltfJson = None
1062
1063 self.log('Read glTF file:' + gltfFileName)
1064 gltfFile = open(gltfFileName, 'r')
1065 gltfJson = None
1066 if gltfFile:
1067 gltfJson = json.load(gltfFile)
1068 gltfString = json.dumps(gltfJson, indent=2)
1069 self.log('GLTF JSON' + gltfString)
1070 if gltfJson:
1071 doc, libFiles = Util.createMaterialXDoc()
1072 self.glTF2MaterialX(doc, gltfJson)
1073
1074 # Create a look and assign materials if found
1075 # TODO: Handle variants
1076 #ssignments = None # buildMaterialAssociations(gltfJson)
1077 #if False: #assignments:
1078 # look = doc.addLook('look')
1079 # for assignMaterial in assignments:
1080 # matassign = look.addMaterialAssign(MTLX_MATERIAL_PREFIX + assignMaterial)
1081 # matassign.setMaterial(MTLX_MATERIAL_PREFIX + assignMaterial)
1082 # matassign.setGeom(','.join(assignments[assignMaterial]))
1083
1084 assignments : dict = {}
1085 materialCPVList : dict = {}
1086 meshes = gltfJson['meshes'] if 'meshes' in gltfJson else []
1087 nodes = gltfJson['nodes'] if 'nodes' in gltfJson else []
1088 scenes = gltfJson['scenes'] if 'scenes' in gltfJson else []
1089 if meshes and nodes and scenes:
1090 for scene in gltfJson['scenes']:
1091 self.log('Scan scene for materials: ' + str(scene))
1092 nodeCount = 0
1093 meshCount = 0
1094 path = ''
1095 for nodeIndex in scene['nodes']:
1096 node = nodes[nodeIndex]
1097 self.computeMeshMaterials(assignments, materialCPVList, node, path, nodeCount, meshCount, meshes, nodes, gltfJson['materials'])
1098
1099 # Add a CPV node if any assigned geometry has a color stream.
1100 for materialName in materialCPVList:
1101 materialNode = doc.getNode(materialName)
1102 shaderInput = materialNode.getInput('surfaceshader') if materialNode else None
1103 shaderNode = shaderInput.getConnectedNode() if shaderInput else None
1104 baseColorInput = shaderNode.getInput('base_color') if shaderNode else None
1105 baseColorNode = baseColorInput.getConnectedNode() if baseColorInput else None
1106 if baseColorNode:
1107 geomcolorInput = baseColorNode.addInputFromNodeDef('geomcolor')
1108 if geomcolorInput:
1109 geomcolor = doc.addNode('geomcolor', EMPTY_STRING, 'color4')
1110 geomcolorInput.setNodeName(geomcolor.getName())
1111
1112 # Create a look and material assignments.
1113 assign_xform = self._options['assignXform'] if 'assignXform' in self._options else False
1114 if self._options['createAssignments'] and len(assignments) > 0:
1115 comment = doc.addChildOfCategory('comment')
1116 comment.setDocString(' Generated material assignments ')
1117 look = doc.addLook('look')
1118 for assignMaterial in assignments:
1119 matassign = look.addMaterialAssign(assignMaterial)
1120 matassign.setMaterial(assignMaterial)
1121 if assign_xform:
1122 # remove last path = mesh geometry path
1123 geoms = assignments[assignMaterial]
1124 xformedGeom = []
1125 for geom in geoms:
1126 pathParts = geom.split('/')
1127 if len(pathParts) > 1:
1128 xformedGeom.append('/'.join(pathParts[:-1]))
1129 else:
1130 xformedGeom.append(geom)
1131 matassign.setGeom(','.join(xformedGeom))
1132 else:
1133 matassign.setGeom(','.join(assignments[assignMaterial]))
1134
1135 return doc
1136
1137
1140
1142 '''
1143 @brief Class to hold options for MaterialX to glTF conversion.
1144
1145 Available options:
1146 - 'translateShaders' : Translate MaterialX shaders to glTF PBR shader. Default is False.
1147 - 'bakeTextures' : Bake pattern graphs in MaterialX. Default is False.
1148 - 'bakeResolution' : Baked texture resolution if 'bakeTextures' is enabled. Default is 256.
1149 - 'packageBinary' : Package binary data in glTF. Default is False.
1150 - 'geometryFile' : Path to geometry file to use for glTF. Default is ''.
1151 - 'primsPerMaterial' : Create a new primitive per material in the MaterialX file and assign the material. Default is False.
1152 - 'searchPath' : Search path for files. Default is empty.
1153 - 'writeDefaultInputs' : Emit inputs even if they have default values. Default is False.
1154 - 'debugOutput' : Print debug output. Default is True.
1155 '''
1156 def __init__(self, *args, **kwargs):
1157 '''
1158 @brief Constructor.
1159 '''
1160 super().__init__(*args, **kwargs)
1161
1162 self['translateShaders'] = False
1163 self['bakeTextures'] = False
1164 self['bakeResolution'] = 256
1165 self['packageBinary'] = False
1166 self['geometryFile'] = ''
1167 self['primsPerMaterial'] = True
1168 self['debugOutput'] = True
1169 self['createProceduralTextures'] = False
1170 self['searchPath'] = mx.FileSearchPath()
1171 self['writeDefaultInputs'] = False
1172
1174 '''
1175 @brief Class to read in a MaterialX document and write to a glTF document.
1176 '''
1177
1178 # Log string
1179 _log = ''
1180 # Options
1181 _options = MTLX2GLTFOptions()
1182
1183 def clearLog(self):
1184 '''
1185 @brief Clear the log.
1186 '''
1187 self._log = ''
1188
1189 def getLog(self):
1190 '''
1191 @brief Get the log.
1192 '''
1193 return self._log
1194
1195 def log(self, string):
1196 '''
1197 @brief Log a string.
1198 '''
1199 self._log += string + '\n'
1200
1201 def setOptions(self, options):
1202 '''
1203 @brief Set options.
1204 @param options: The options to set.
1205 '''
1206 self._options = options
1207
1208 def getOptions(self) -> MTLX2GLTFOptions:
1209 '''
1210 @brief Get the options for the reader.
1211 @return The options.
1212 '''
1213 return self._options
1214
1215 def initialize_gtlf_texture(self, texture, name, uri, images) -> None:
1216 '''
1217 @brief Initialize a new gltF image entry and texture entry which references
1218 the image entry.
1219
1220 @param texture: The glTF texture entry to initialize.
1221 @param name: The name of the texture entry.
1222 @param uri: The URI of the image entry.
1223 @param images: The list of images to append the new image entry to.
1224 '''
1225 image = {}
1226 image['name'] = name
1227 uriPath = mx.FilePath(uri)
1228 image['uri'] = uriPath.asString(mx.FormatPosix)
1229 images.append(image)
1230
1231 texture['name'] = name
1232 texture['source'] = len(images) - 1
1233
1234 def getShaderNodes(self, graphElement):
1235 '''
1236 @brief Find all surface shaders in a GraphElement.
1237 @param graphElement: The GraphElement to search for surface shaders.
1238 '''
1239 shaderNodes = set()
1240 for material in graphElement.getMaterialNodes():
1241 for shader in mx.getShaderNodes(material):
1242 shaderNodes.add(shader.getNamePath())
1243 for shader in graphElement.getNodes():
1244 if shader.getType() == 'surfaceshader':
1245 shaderNodes.add(shader.getNamePath())
1246 return shaderNodes
1247
1248
1249 def getRenderableGraphs(self, doc, graphElement):
1250 '''
1251 @brief Find all renderable graphs in a GraphElement.
1252 @param doc: The MaterialX document.
1253 @param graphElement: The GraphElement to search for renderable graphs.
1254 '''
1255 ngnamepaths = set()
1256 graphs = []
1257 shaderNodes = self.getShaderNodes(graphElement)
1258 for shaderPath in shaderNodes:
1259 shader = doc.getDescendant(shaderPath)
1260 for input in shader.getInputs():
1261 ngString = input.getNodeGraphString()
1262 if ngString and ngString not in ngnamepaths:
1263 graphs.append(graphElement.getNodeGraph(ngString))
1264 ngnamepaths.add(ngString)
1265 return graphs
1266
1267 def copyGraphInterfaces(self, dest, ng, removeInternals):
1268 '''
1269 @brief Copy the interface of a nodegraph to a new nodegraph under a specified parent `dest`.
1270 @param dest: The parent nodegraph to copy the interface to.
1271 @param ng: The nodegraph to copy the interface from.
1272 @param removeInternals: Whether to remove internal nodes from the interface.
1273 '''
1274 copyMethod = 'add_remove'
1275 ng1 = dest.addNodeGraph(ng.getName())
1276 ng1.copyContentFrom(ng)
1277 removeInternals = False
1278 if removeInternals:
1279 for child in ng1.getChildren():
1280 if child.isA(mx.Input) or child.isA(mx.Output):
1281 for attr in ['nodegraph', MTLX_NODE_NAME_ATTRIBUTE, 'defaultinput']:
1282 child.removeAttribute(attr)
1283 continue
1284 ng1.removeChild(child.getName())
1285
1286 # Convert MaterialX element to JSON
1287 def elementToJSON(self, elem, jsonParent):
1288 '''
1289 @brief Convert an MaterialX XML element to JSON.
1290 Will recursively traverse the parent/child Element hierarchy.
1291
1292 @param elem: The MaterialX element to convert.
1293 @param jsonParent: The parent JSON element to add the converted element to.
1294 '''
1295 if (elem.getSourceUri() != ''):
1296 return
1297
1298 # Create a new JSON element for the MaterialX element
1299 jsonElem = {}
1300
1301 # Add attributes
1302 for attrName in elem.getAttributeNames():
1303 jsonElem[attrName] = elem.getAttribute(attrName)
1304
1305 # Add children
1306 for child in elem.getChildren():
1307 jsonElem = self.elementToJSON(child, jsonElem)
1308
1309 # Add element to parent
1310 jsonParent[elem.getCategory() + self.JSON_CATEGORY_NAME_SEPARATOR + elem.getName()] = jsonElem
1311 return jsonParent
1312
1313 # Convert MaterialX document to JSON
1314 def documentToJSON(self, doc):
1315 '''
1316 @brief Convert an MaterialX XML document to JSON
1317 @param doc: The MaterialX document to convert.
1318 '''
1319 root = {}
1320 root['materialx'] = {}
1321
1322 for attrName in doc.getAttributeNames():
1323 root[attrName] = doc.getAttribute(attrName)
1324
1325 for elem in doc.getChildren():
1326 self.elementToJSON(elem, root[self.MATERIALX_DOCUMENT_ROOT])
1327
1328 return root
1329
1330 def stringToScalar(self, value, type) -> None:
1331 '''
1332 @brief Convert a string to a scalar value.
1333 @param value: The string value to convert.
1334 @param type: The type of the value.
1335 '''
1336 returnvalue = value
1337
1338 if type in ['integer', 'matrix33', 'matrix44', 'vector2', MTLX_VEC3_STRING, MTLX_VEC3_STRING, MTLX_FLOAT_STRING, 'color3', 'color4']:
1339 splitvalue = value.split(',')
1340 if len(splitvalue) > 1:
1341 returnvalue = [float(x) for x in splitvalue]
1342 else:
1343 if type == 'integer':
1344 returnvalue = int(value)
1345 else:
1346 returnvalue = float(value)
1347
1348 return returnvalue
1349
1350 def graphToJson(self, graph, json, materials, textures, images, samplers):
1351 '''
1352 @brief Convert a MaterialX graph to JSON.
1353 @param graph: The MaterialX graph to convert.
1354 @param json: The JSON object to add the converted graph to.
1355 @param materials: The list of materials to append the new material to.
1356 @param textures: The list of textures to append new textures to.
1357 @param images: The list of images to append new images to.
1358 @param samplers: The list of samplers to append new samplers to.
1359 '''
1360
1361 debug = False
1362
1363 skipAttr = ['uiname', 'xpos', 'ypos' ] # etc
1364 procDict = dict()
1365
1366 graphOutputs = []
1367 procs = None
1368 extensions = None
1369 if 'extensions' not in json:
1370 extensions = json['extensions'] = {}
1371 extensions = json['extensions']
1372 khr_procedurals = None
1373 if 'KHR_procedurals' not in extensions:
1374 extensions['KHR_procedurals'] = {}
1375 khr_procedurals = extensions['KHR_procedurals']
1376 if 'procedurals' not in khr_procedurals:
1377 khr_procedurals['procedurals'] = []
1378 procs = khr_procedurals['procedurals']
1379
1380 for node in graph.getNodes():
1381 jsonNode = {}
1382 jsonNode['name'] = node.getNamePath()
1383 procs.append(jsonNode)
1384 procDict[node.getNamePath()] = len(procs) - 1
1385
1386 for input in graph.getInputs():
1387 jsonNode = {}
1388 jsonNode['name'] = input.getNamePath()
1389 jsonNode['nodetype'] = input.getCategory()
1390 if input.getValue() != None:
1391
1392 # If it's a file name then create a texture
1393 inputType = input.getAttribute(mx.TypedElement.TYPE_ATTRIBUTE)
1394 jsonNode['type'] = inputType
1395 if inputType == mx.FILENAME_TYPE_STRING:
1396 texture = {}
1397 filename = input.getValueString()
1398 self.initialize_gtlf_texture(texture, input.getNamePath(), filename, images)
1399 textures.append(texture)
1400 self.writeImageProperties(texture, samplers, node)
1401 jsonNode['texture'] = len(textures) -1
1402 # Otherwise just set the value
1403 else:
1404 value = input.getValueString()
1405 value = self.stringToScalar(value, inputType)
1406 jsonNode[MTLX_VALUE_STRING] = value
1407 procs.append(jsonNode)
1408 procDict[jsonNode['name']] = len(procs) - 1
1409
1410 for output in graph.getOutputs():
1411 jsonNode = {}
1412 jsonNode['name'] = output.getNamePath()
1413 procs.append(jsonNode)
1414 graphOutputs.append(jsonNode['name'])
1415 procDict[jsonNode['name']] = len(procs) - 1
1416
1417 for output in graph.getOutputs():
1418 jsonNode = None
1419 index = procDict[output.getNamePath()]
1420 jsonNode = procs[index]
1421 jsonNode['nodetype'] = output.getCategory()
1422
1423 outputString = connection = output.getAttribute('output')
1424 if len(connection) == 0:
1425 connection = output.getAttribute('interfacename')
1426 if len(connection) == 0:
1427 connection = output.getAttribute(MTLX_NODE_NAME_ATTRIBUTE)
1428 connectionNode = graph.getChild(connection)
1429 if connectionNode:
1430 #if self._options['debugOutput']:
1431 # jsonNode['proceduralName'] = connectionNode.getNamePath()
1432 jsonNode['procedural'] = procDict[connectionNode.getNamePath()]
1433 if len(outputString) > 0:
1434 jsonNode['output'] = outputString
1435
1436 #procs.append(jsonNode)
1437 #graphOutputs.append(jsonNode['name'])
1438 #procDict[jsonNode['name']] = len(procs) - 1
1439
1440 for node in graph.getNodes():
1441 jsonNode = None
1442 index = procDict[node.getNamePath()]
1443 jsonNode = procs[index]
1444 jsonNode['nodetype'] = node.getCategory()
1445 nodedef = node.getNodeDef()
1446 if debug and nodedef and len(nodedef.getNodeGroup()):
1447 jsonNode['nodegroup'] = nodedef.getNodeGroup()
1448 for attrName in node.getAttributeNames():
1449 if attrName not in skipAttr:
1450 jsonNode[attrName] = node.getAttribute(attrName)
1451
1452 inputs = []
1453 for input in node.getInputs():
1454
1455 inputItem = {}
1456 inputItem['name'] = input.getName()
1457
1458 if input.getValue() != None:
1459 # If it's a file name then create a texture
1460 inputType = input.getAttribute(mx.TypedElement.TYPE_ATTRIBUTE)
1461 inputItem['type'] = inputType
1462 if inputType == mx.FILENAME_TYPE_STRING:
1463 texture = {}
1464 filename = input.getValueString()
1465 self.initialize_gtlf_texture(texture, input.getNamePath(), filename, images)
1466 textures.append(texture)
1467 inputItem['texture'] = len(textures) -1
1468 self.writeImageProperties(texture, samplers, node)
1469 # Otherwise just set the value
1470 else:
1471 inputType = input.getAttribute(mx.TypedElement.TYPE_ATTRIBUTE)
1472 value = input.getValueString()
1473 value = self.stringToScalar(value, inputType)
1474 inputItem[MTLX_VALUE_STRING] = value
1475 else:
1476 connection = input.getAttribute('interfacename')
1477 if len(connection) == 0:
1478 connection = input.getAttribute(MTLX_NODE_NAME_ATTRIBUTE)
1479 connectionNode = graph.getChild(connection)
1480 if connectionNode:
1481 inputType = input.getAttribute(mx.TypedElement.TYPE_ATTRIBUTE)
1482 inputItem['type'] = inputType
1483 if debug:
1484 inputItem['proceduralName'] = connectionNode.getNamePath()
1485 inputItem['procedural'] = procDict[connectionNode.getNamePath()]
1486 outputString = input.getAttribute('output')
1487 if len(outputString) > 0:
1488 inputItem['output'] = outputString
1489
1490 inputs.append(inputItem)
1491
1492 if len(inputs) > 0:
1493 jsonNode['inputs'] = inputs
1494
1495 #procs.append(jsonNode)
1496
1497 addGraph = False
1498 if addGraph:
1499 jsonNode = {}
1500 jsonNode['name'] = graph.getNamePath()
1501 jsonNode['nodetype'] = 'graph'
1502 if False:
1503 inputs = {}
1504 for input in graph.getInputs():
1505 if input.getValue() != None:
1506 inputs[input.getName()] = input.getValueString()
1507 else:
1508 connection = input.getAttribute('output')
1509 if len(connection) == 0:
1510 connection = input.getAttribute('interfacename')
1511 if len(connection) == 0:
1512 connection = input.getAttribute('node')
1513 #connectionNode = graph.getChild(connection)
1514 #if connectionNode:
1515 if len(connection) > 0:
1516 inputs[input.getName()] = connection
1517 if len(inputs) > 0:
1518 jsonGraph['inputs'] = inputs
1519
1520 procs.append(jsonNode)
1521 procDict[jsonNode['name']] = len(procs) - 1
1522
1523 return graphOutputs, procDict
1524
1525 def translateShader(self, shader, destCategory) -> tuple[bool, str]:
1526 '''
1527 @brief Translate a MaterialX shader to a different category.
1528 @param shader: The MaterialX shader to translate.
1529 @param destCategory: The shader category to translate to.
1530 @return True if the shader was translated successfully, otherwise False.
1531 '''
1532
1533 shaderTranslator = mx_gen_shader.ShaderTranslator.create()
1534 try:
1535 if shader.getCategory() == destCategory:
1536 return True, ''
1537 shaderTranslator.translateShader(shader, MTLX_GLTF_PBR_CATEGORY)
1538 except mx.Exception as err:
1539 return False, err.__str__()
1540 except LookupError as err:
1541 return False, err.__str__()
1542
1543 return True, ''
1544
1545 def bakeTextures(self, doc, hdr, width, height, useGlslBackend, average, writeDocumentPerMaterial, outputFilename) -> bool:
1546 '''
1547 @brief Bake all textures in a MaterialX document.
1548 @param doc: The MaterialX document to bake textures from.
1549 @param hdr: Whether to bake textures as HDR.
1550 @param width: The width of the baked textures.
1551 @param height: The height of the baked textures.
1552 @param useGlslBackend: Whether to use the GLSL backend for baking.
1553 @param average: Whether to average the baked textures.
1554 @param writeDocumentPerMaterial: Whether to write a separate MaterialX document per material.
1555 @param outputFilename: The output filename to write the baked document to.
1556 '''
1557 baseType = mx_render.BaseType.FLOAT if hdr else mx_render.BaseType.UINT8
1558
1559 if platform == 'darwin' and not useGlslBackend:
1560 baker = mx_render_msl.TextureBaker.create(width, height, baseType)
1561 else:
1562 baker = mx_render_glsl.TextureBaker.create(width, height, baseType)
1563
1564 if not baker:
1565 return False
1566
1567 if average:
1568 baker.setAverageImages(True)
1569 baker.writeDocumentPerMaterial(writeDocumentPerMaterial)
1570 baker.bakeAllMaterials(doc, self._options['searchPath'], outputFilename)
1571
1572 return True
1573
1574 def buildPrimPaths(self, primPaths, cnode, path, nodeCount, meshCount, meshes, nodes):
1575 '''
1576 @brief Recurse through a node hierarcht to build a dictionary of paths to primitives.
1577 @param primPaths: The dictionary of primitive paths to build.
1578 @param cnode: The current node to examine.
1579 @param path: The parent path to the node.
1580 @param nodeCount: The current node count.
1581 @param meshCount: The current mesh count.
1582 @param meshes: The list of meshes to examine.
1583 @param nodes: The list of nodes to examine.
1584 '''
1585
1586 # Push node name on to path
1587 prevPath = path
1588 cnodeName = ''
1589 if 'name' in cnode:
1590 cnodeName = cnode['name']
1591 else:
1592 cnodeName = self.GLTF_DEFAULT_NODE_PREFIX + str(nodeCount)
1593 nodeCount = nodeCount + 1
1594 path = path + '/' + ( mx.createValidName(cnodeName) )
1595
1596 # Check if this node is associated with a mesh
1597 if 'mesh' in cnode:
1598 meshIndex = cnode['mesh']
1599 cmesh = meshes[meshIndex]
1600 if cmesh:
1601
1602 # Append mesh name on to path
1603 meshName = ''
1604 if 'name' in cmesh:
1605 meshName = cmesh['name']
1606 else:
1607 meshName = self.GLTF_DEFAULT_MESH_PREFIX + str(meshCount)
1608 meshCount = meshCount + 1
1609 path = path + '/' + mx.createValidName(meshName)
1610
1611 if 'primitives' in cmesh:
1612 primitives = cmesh['primitives']
1613
1614 # Check for material CPV attribute
1615 primitiveIndex = 0
1616 for primitive in primitives:
1617
1618 primpath = path
1619 if len(primitives) > 1:
1620 primpath = + '/' + self.GLTF_DEFAULT_PRIMITIVE_PREFIX + str(primitiveIndex)
1621 primPaths[primpath] = primitive
1622
1623 primitiveIndex = primitiveIndex + 1
1624
1625 if 'children' in cnode:
1626 children = cnode['children']
1627 for childIndex in children:
1628 child = nodes[childIndex]
1629 self.buildPrimPaths(primPaths, child, path, nodeCount, meshCount, meshes, nodes)
1630
1631 # Pop path name
1632 path = prevPath
1633
1634 def createPrimsForMaterials(self, gltfJson, rowCount=10) -> None:
1635 '''
1636 @brief Create new meshes and nodes for each material in a glTF document.
1637 @param gltfJson: The glTF document to create new meshes and nodes for.
1638 @param rowCount: The number of meshes per row to create.
1639 @return None
1640 '''
1641 if not 'materials' in gltfJson:
1642 return
1643 nodes = None
1644 if 'nodes' in gltfJson:
1645 nodes = gltfJson['nodes']
1646 if not nodes:
1647 return
1648 if 'scenes' in gltfJson:
1649 scenes = gltfJson['scenes']
1650 if not scenes:
1651 return
1652
1653 materials = gltfJson['materials']
1654 materialCount = len(materials)
1655 if materialCount == 1:
1656 return
1657
1658 if 'meshes' in gltfJson:
1659 meshes = gltfJson['meshes']
1660
1661 MESH_POSTFIX = '_material_'
1662 translationX = 2.5
1663 translationY = 0.0
1664
1665 meshCopies = []
1666 meshIndex = len(meshes) - 1
1667 nodeCopies = []
1668 for materialId in range(1, materialCount):
1669 for mesh in meshes:
1670 if 'primitives' in mesh:
1671 meshCopy = copy.deepcopy(mesh)
1672 primitivesCopy = meshCopy['primitives']
1673 for primitiveCopy in primitivesCopy:
1674 # Overwrite the material for each prim
1675 primitiveCopy['material'] = materialId
1676
1677 meshCopy['name'] = mesh['name'] + MESH_POSTFIX + str(materialId)
1678 meshCopies.append(meshCopy)
1679 meshIndex = meshIndex + 1
1680
1681 newNode = {}
1682 newNode['name'] = meshCopy['name']
1683 newNode['mesh'] = meshIndex
1684 newNode['translation'] = [translationX, translationY, 0]
1685 nodeCopies.append(newNode)
1686
1687 materialId = materialId + 1
1688 translationX = translationX + 2.5
1689 if materialId % rowCount == 0:
1690 translationX = 0.0
1691 translationY = translationY + 2.5
1692
1693 meshes.extend(meshCopies)
1694 nodeCount = len(nodes)
1695 nodes.extend(nodeCopies)
1696 scenenodes = scenes[0]['nodes']
1697 for i in range(nodeCount, len(nodes)):
1698 scenenodes.append(i)
1699
1700 def assignMaterial(self, doc, gltfJson) -> None:
1701 '''
1702 @brief Assign materials to meshes based on MaterialX looks (material assginments).
1703 The assignment is performed based on matching the MaterialX geometry paths to the glTF mesh primitive paths.
1704 @param doc: The MaterialX document containing looks.
1705 @param gltfJson: The glTF document to assign materials to.
1706 '''
1707
1708 gltfmaterials = gltfJson['materials']
1709 if len(gltfmaterials) == 0:
1710 self.log('No materials in gltfJson')
1711 return
1712
1713 meshes = gltfJson['meshes'] if 'meshes' in gltfJson else None
1714 if not meshes:
1715 self.log('No meshes in gltfJson')
1716 return
1717 nodes = gltfJson['nodes'] if 'nodes' in gltfJson else None
1718 if not nodes:
1719 self.log('No nodes in gltfJson')
1720 return
1721 scenes = gltfJson['scenes'] if 'scenes' in gltfJson else None
1722 if not scenes:
1723 self.log('No scenes in gltfJson')
1724 return
1725
1726 primPaths : dict = {}
1727 for scene in gltfJson['scenes']:
1728 nodeCount = 0
1729 meshCount = 0
1730 path = ''
1731 for nodeIndex in scene['nodes']:
1732 node = nodes[nodeIndex]
1733 self.buildPrimPaths(primPaths, node, path, nodeCount, meshCount, meshes, nodes)
1734 if not primPaths:
1735 return
1736
1737 # Build a dictionary of material name to material array index
1738 materialIndexes = {}
1739 for index in range(len(gltfmaterials)):
1740 gltfmaterial = gltfmaterials[index]
1741 if 'name' in gltfmaterial:
1742 matName = MTLX_MATERIAL_PREFIX + gltfmaterial['name']
1743 materialIndexes[matName] = index
1744
1745 # Scan through assignments in looks in order
1746 # if the materialname matches a meshhes' material name, then assign the material index to the mesh
1747 for look in doc.getLooks():
1748 materialAssigns = look.getMaterialAssigns()
1749 for materialAssign in materialAssigns:
1750 materialName = materialAssign.getMaterial()
1751 if materialName in materialIndexes:
1752 materialIndex = materialIndexes[materialName]
1753 geomString = materialAssign.getGeom()
1754 geomlist = geomString.split(',')
1755
1756 # Scan geometry list with primitive paths
1757 for geomlistitem in geomlist:
1758 if geomlistitem in primPaths:
1759 prim = primPaths[geomlistitem]
1760 if prim:
1761 self.log('assign material: ' + materialName + ' to mesh path: ' + geomlistitem)
1762 prim['material'] = materialIndex
1763 break
1764 else:
1765 self.log('Cannot find material: ' + materialName + 'in scene materials' + materialIndexes)
1766
1767
1768 def writeImageProperties(self, texture, samplers, imageNode) -> None:
1769 '''
1770 @brief Write image properties of a MaterialX gltF image node to a glTF texture and sampler entry.
1771 @param texture: The glTF texture entry to write to.
1772 @param samplers: The list of samplers to append new samplers to.
1773 @param imageNode: The MaterialX glTF image node to examine.
1774 '''
1775 TO_RADIAN = 3.1415926535 / 180.0
1776
1777 # Handle uvset index
1778 uvindex = None
1779 texcoordInput = imageNode.getInput('texcoord')
1780 if texcoordInput:
1781 texcoordNode = texcoordInput.getConnectedNode()
1782 if texcoordNode:
1783 uvindexInput = texcoordNode.getInput('index')
1784 if uvindexInput:
1785 value = uvindexInput.getValue()
1786 if value:
1787 uvindex = value
1788
1789 if uvindex:
1790 texture['texCoord'] = uvindex
1791
1792 # Handle transform extension
1793 # See: https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_texture_transform/README.md
1794
1795 offsetInputValue = None
1796 offsetInput = imageNode.getInput('offset')
1797 if offsetInput:
1798 offsetInputValue = offsetInput.getValue()
1799
1800 rotationInputValue = None
1801 rotationInput = imageNode.getInput('rotate')
1802 if rotationInput:
1803 rotationInputValue = rotationInput.getValue()
1804
1805 scaleInputValue = None
1806 scaleInput = imageNode.getInput('scale')
1807 if scaleInput:
1808 scaleInputValue = scaleInput.getValue()
1809
1810 if uvindex or offsetInputValue or rotationInputValue or scaleInputValue:
1811
1812 if not 'extensions' in texture:
1813 texture['extensions'] = {}
1814 extensions = texture['extensions']
1815 transformExtension : dict = extensions['KHR_texture_transform']
1816 transformExtension = {}
1817
1818 if offsetInputValue:
1819 transformExtension['offset'] = [offsetInputValue[0], offsetInputValue[1]]
1820 if rotationInputValue:
1821 val = float(rotationInputValue)
1822 # Note: Rotation in glTF is in radians and degrees in MaterialX
1823 transformExtension['rotation'] = val * TO_RADIAN
1824 if scaleInputValue:
1825 transformExtension['scale'] = [scaleInputValue[0], scaleInputValue[1]]
1826 if uvindex:
1827 transformExtension['texCoord'] = uvindex
1828
1829 # Handle sampler.
1830 # Based on: https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/schema/sampler.schema.json
1831 uaddressInput = imageNode.addInputFromNodeDef('uaddressmode')
1832 uaddressInputValue = uaddressInput.getValue() if uaddressInput else None
1833 vaddressInput = imageNode.addInputFromNodeDef('vaddressmode')
1834 vaddressInputValue = vaddressInput.getValue() if vaddressInput else None
1835 filterInput = imageNode.addInputFromNodeDef('filtertype')
1836 filterInputValue = filterInput.getValue() if filterInput else None
1837
1838 sampler = {}
1839 if uaddressInputValue or vaddressInputValue or filterInputValue:
1840
1841 wrapMap = {}
1842 wrapMap['clamp'] = 33071
1843 wrapMap['mirror'] = 33648
1844 wrapMap['periodic'] = 10497
1845
1846 if uaddressInputValue and uaddressInputValue in wrapMap:
1847 sampler['wrapS'] = wrapMap[uaddressInputValue]
1848 else:
1849 sampler['wrapS'] = 10497
1850 if vaddressInputValue and vaddressInputValue in wrapMap:
1851 sampler['wrapT'] = wrapMap[vaddressInputValue]
1852 else:
1853 sampler['wrapT'] = 10497
1854
1855 filterMap = {}
1856 filterMap['closest'] = 9728
1857 filterMap['linear'] = 9986
1858 filterMap['cubic'] = 9729
1859 if filterInputValue and filterInputValue in filterMap:
1860 sampler['magFilter'] = 9729 # No real value so make this fixed.
1861 sampler['minFilter'] = filterMap[filterInputValue]
1862
1863 # Add sampler to samplers list if an existing sampler with the same
1864 # parameters does not exist. Otherwise append a new one.
1865 # Set the 'sampler' index on the texture.
1866 reuseSampler = False
1867 for i in range(len(samplers)):
1868 if sampler == samplers[i]:
1869 texture['sampler'] = i
1870 reuseSampler = True
1871 break
1872 if not reuseSampler:
1873 samplers.append(sampler)
1874 texture['sampler'] = len(samplers) - 1
1875
1876 def writeFloatInput(self, pbrNode, inputName, gltfTextureName, gltfValueName, material, textures, images, samplers, remapper=None):
1877 '''
1878 @brief Write a float input from a MaterialX pbr node to a glTF material entry.
1879 @param pbrNode: The MaterialX pbr node to examine.
1880 @param inputName: The name of the input on the pbr node
1881 @param gltfTextureName: The name of the texture entry to write to.
1882 @param gltfValueName: The name of the value entry to write
1883 @param material: The glTF material entry to write to.
1884 @param textures: The list of textures to append new textures to.
1885 @param images: The list of images to append new images to.
1886 @param samplers: The list of samplers to append new samplers to.
1887 @param remapper: A optional remapping map to remap MaterialX values to glTF values
1888 '''
1889 filename = EMPTY_STRING
1890
1891 imageNode = pbrNode.getConnectedNode(inputName)
1892 if imageNode:
1893 fileInput = imageNode.getInput(mx.Implementation.FILE_ATTRIBUTE)
1894 filename = ''
1895 if fileInput and fileInput.getAttribute(mx.TypedElement.TYPE_ATTRIBUTE) == mx.FILENAME_TYPE_STRING:
1896 filename = fileInput.getResolvedValueString()
1897 if len(filename) == 0:
1898 imageNode = None
1899
1900 if imageNode:
1901 texture = {}
1902 self.initialize_gtlf_texture(texture, imageNode.getNamePath(), filename, images)
1903 textures.append(texture)
1904
1905 material[gltfTextureName] = {}
1906 material[gltfTextureName]['index'] = len(textures) - 1
1907
1908 self.writeImageProperties(texture, samplers, imageNode)
1909
1910 else:
1911 value = pbrNode.getInputValue(inputName)
1912 if value != None:
1913 nodedef = pbrNode.getNodeDef()
1914 # Don't write default values, unless specified
1915 writeDefaultInputs= self._options['writeDefaultInputs']
1916 if nodedef and (writeDefaultInputs or nodedef.getInputValue(inputName) != value):
1917 if remapper:
1918 if value in remapper:
1919 material[gltfValueName] = remapper[value]
1920 else:
1921 material[gltfValueName] = value
1922 #else:
1923 # print('INFO: skip writing default value for:', inputName, value)
1924 #else:
1925 # print('WARNING: Did not write value for:', inputName )
1926
1927 def writeColor3Input(self, pbrNode, inputName, gltfTextureName, gltfValueName, material, textures, images, samplers):
1928 '''
1929 @brief Write a color3 input from a MaterialX pbr node to a glTF material entry.
1930 @param pbrNode: The MaterialX pbr node to examine.
1931 @param inputName: The name of the input on the pbr node
1932 @param gltfTextureName: The name of the texture entry to write to.
1933 @param gltfValueName: The name of the value entry to write
1934 @param material: The glTF material entry to write to.
1935 @param textures: The list of textures to append new textures to.
1936 @param images: The list of images to append new images to.
1937 @param samplers: The list of samplers to append new samplers to.
1938 '''
1939
1940 filename = EMPTY_STRING
1941
1942 imageNode = pbrNode.getConnectedNode(inputName)
1943 if imageNode:
1944 fileInput = imageNode.getInput(mx.Implementation.FILE_ATTRIBUTE)
1945 filename = ''
1946 if fileInput and fileInput.getAttribute(mx.TypedElement.TYPE_ATTRIBUTE) == mx.FILENAME_TYPE_STRING:
1947 filename = fileInput.getResolvedValueString()
1948 if len(filename) == 0:
1949 imageNode = None
1950
1951 if imageNode:
1952 texture = {}
1953 self.initialize_gtlf_texture(texture, imageNode.getNamePath(), filename, images)
1954 textures.append(texture)
1955
1956 material[gltfTextureName] = {}
1957 material[gltfTextureName]['index'] = len(textures) - 1
1958
1959 material[gltfValueName] = [1.0, 1.0, 1.0]
1960
1961 self.writeImageProperties(texture, samplers, imageNode)
1962 else:
1963 value = pbrNode.getInputValue(inputName)
1964 if value:
1965 nodedef = pbrNode.getNodeDef()
1966 # Don't write default values
1967 writeDefaultInputs= self._options['writeDefaultInputs']
1968 if nodedef and (writeDefaultInputs or nodedef.getInputValue(inputName) != value):
1969 material[gltfValueName] = [ value[0], value[1], value[2] ]
1970
1971
1972 def writeCopyright(self, doc, gltfJson):
1973 '''
1974 @brief Write a glTF document copyright information.
1975 @param doc: The MaterialX document to examine.
1976 @param gltfJson: The glTF document to write to.
1977 '''
1978 asset = None
1979 if 'asset' in gltfJson:
1980 asset = gltfJson['asset']
1981 else:
1982 asset = gltfJson['asset'] = {}
1983 current_year = datetime.datetime.now().year
1984 asset['copyright'] = f'Copyright 2022-{current_year}: Bernard Kwok.'
1985 asset['generator'] = f'MaterialX {doc.getVersionString()} to glTF 2.0 generator. https://github.com/kwokcb/materialxgltf'
1986 asset['version'] = '2.0'
1987
1988 def materialX2glTF(self, doc, gltfJson, resetMaterials):
1989 '''
1990 @brief Convert a MaterialX document to glTF.
1991 @param doc: The MaterialX document to convert.
1992 @param gltfJson: The glTF document to write to.
1993 @param resetMaterials: Whether to clear any existing glTF materials.
1994 '''
1995 pbrNodes = {}
1996 unlitNodes = {}
1997
1998 addInputsFromNodeDef = self._options['writeDefaultInputs']
1999
2000 for material in doc.getMaterialNodes():
2001 shaderNodes = mx.getShaderNodes(material)
2002 for shaderNode in shaderNodes:
2003 category = shaderNode.getCategory()
2004 path = shaderNode.getNamePath()
2005 if category == MTLX_GLTF_PBR_CATEGORY and path not in pbrNodes:
2006 if addInputsFromNodeDef:
2007 hasNormal = shaderNode.getInput('normal')
2008 hasTangent = shaderNode.getInput('tangent')
2009 shaderNode.addInputsFromNodeDef()
2010 if not hasNormal:
2011 shaderNode.removeChild('tangent')
2012 if not hasTangent:
2013 shaderNode.removeChild('normal')
2014 pbrNodes[path] = shaderNode
2015 elif category == MTLX_UNLIT_CATEGORY_STRING and path not in unlitNodes:
2016 if addInputsFromNodeDef:
2017 hasNormal = shaderNode.getInput('normal')
2018 hasTangent = shaderNode.getInput('tangent')
2019 shaderNode.addInputsFromNodeDef()
2020 if not hasNormal:
2021 shaderNode.removeChild('tangent')
2022 if not hasTangent:
2023 shaderNode.removeChild('normal')
2024 unlitNodes[path] = shaderNode
2025
2026 materials_count = len(pbrNodes) + len(unlitNodes)
2027 if materials_count == 0:
2028 return
2029
2030 self.writeCopyright(doc, gltfJson)
2031 materials = None
2032 if 'materials' in gltfJson and not resetMaterials:
2033 materials = gltfJson['materials']
2034 else:
2035 materials = gltfJson['materials'] = list()
2036
2037 textures = None
2038 if 'textures' in gltfJson:
2039 textures = gltfJson['textures']
2040 else:
2041 textures = gltfJson['textures'] = list()
2042
2043 images = None
2044 if 'images' in gltfJson:
2045 images = gltfJson['images']
2046 else:
2047 images = gltfJson['images'] = list()
2048
2049 samplers = None
2050 if 'samplers' in gltfJson:
2051 samplers = gltfJson['samplers']
2052 else:
2053 samplers = gltfJson['samplers'] = list()
2054
2055 # Get 'extensions used' element to fill in if extensions are used
2056 extensionsUsed = None
2057 if not 'extensionsUsed' in gltfJson:
2058 gltfJson['extensionsUsed'] = []
2059 extensionsUsed = gltfJson['extensionsUsed']
2060
2061 # Write materials
2062 #
2063 COLOR_SEMANTIC = 'color'
2064
2065 for name in unlitNodes:
2066 material = {}
2067
2068 unlitExtension = 'KHR_materials_unlit'
2069 if not unlitExtension in extensionsUsed:
2070 extensionsUsed.append(unlitExtension)
2071 mat_extensions = material['extensions'] = {}
2072 mat_extensions[unlitExtension] = {}
2073
2074 unlitNode = unlitNodes[name]
2075 material['name'] = name
2076 roughness = material['pbrMetallicRoughness'] = {}
2077
2078 base_color_set = False
2079 base_color = [ 1.0, 1.0, 1.0, 1.0 ]
2080
2081 imageNode = unlitNode.getConnectedNode('emission_color')
2082 filename = ''
2083 if imageNode:
2084 fileInput = imageNode.getInput(mx.Implementation.FILE_ATTRIBUTE)
2085 if fileInput and fileInput.getAttribute(mx.TypedElement.TYPE_ATTRIBUTE) == mx.FILENAME_TYPE_STRING:
2086 filename = fileInput.getResolvedValueString()
2087
2088 if len(filename) == 0:
2089 imageNode = None
2090
2091 if imageNode:
2092 texture = {}
2093 self.initialize_gtlf_texture(texture, imageNode.getNamePath(), filename, images)
2094 textures.append(texture)
2095
2096 roughness['baseColorTexture'] = {}
2097 roughness['baseColorTexture']['index'] = len(textures) - 1
2098
2099 self.writeImageProperties(texture, samplers, imageNode)
2100
2101 # Pull off color from gltf_colorImage node
2102 color = unlitNode.getInputValue('emission_color')
2103 if color:
2104 base_color[0] = color[0]
2105 base_color[1] = color[1]
2106 base_color[2] = color[2]
2107 base_color_set = True
2108
2109 else:
2110 color = unlitNode.getInputValue('emission_color')
2111 if color:
2112 base_color[0] = color[0]
2113 base_color[1] = color[1]
2114 base_color[2] = color[2]
2115 base_color_set = True
2116
2117 value = unlitNode.getInputValue('opacity')
2118 if value:
2119 base_color[3] = value
2120 base_color_set = True
2121
2122 if base_color_set:
2123 roughness['baseColorFactor'] = base_color
2124
2125 if base_color_set:
2126 roughness['baseColorFactor'] = base_color
2127
2128 materials.append(material)
2129
2130 for name in pbrNodes:
2131 material = {}
2132
2133 # Setup extensions list
2134 extensions = None
2135 if not 'extensions' in material:
2136 material['extensions'] = {}
2137 extensions = material['extensions']
2138
2139 pbrNode = pbrNodes[name]
2140 if self._options['debugOutput']:
2141 print('- Convert MaterialX node to glTF:', pbrNode.getNamePath())
2142
2143 # Set material name
2144 material['name'] = name
2145
2146 # Handle PBR metallic roughness
2147 # -----------------------------
2148 roughness = material['pbrMetallicRoughness'] = {}
2149
2150 # Handle base color
2151 base_color = [ 1.0, 1.0, 1.0, 1.0 ]
2152 base_color_set = False
2153
2154 filename = ''
2155 imageNode = pbrNode.getConnectedNode('base_color')
2156 imageGraph = None
2157 if self._options['createProceduralTextures']:
2158 if imageNode:
2159 if imageNode.getParent().isA(mx.NodeGraph):
2160 imageGraph = imageNode.getParent()
2161
2162 if imageGraph:
2163 if self._options['debugOutput']:
2164 print('- Generate KHR procedurals for graph: ' + imageGraph.getName())
2165 self.log('- Generate KHR procedurals for graph: ' + imageGraph.getName())
2166 #dest = mx.createDocument()
2167 #removeInternals = False
2168 #copyGraphInterfaces(dest, imageGraph, removeInternals)
2169
2170 extensionName = 'KHR_procedurals'
2171 if extensionName not in extensionsUsed:
2172 extensionsUsed.append(extensionName)
2173 #if extensionName not in extensions:
2174 # extensions[extensionName] = {}
2175 #outputExtension = extensions[extensionName]
2176
2177 #jsonGraph = documentToJSON(imageGraph)
2178 graphOutputs, procDict = self.graphToJson(imageGraph, gltfJson, materials, textures, images, samplers)
2179 outputs = imageGraph.getOutputs()
2180 if len(outputs) > 0:
2181 connectionName = outputs[0].getNamePath()
2182 inputBaseColor = pbrNode.getInput('base_color')
2183 outputSpecifier = inputBaseColor.getAttribute('output')
2184 if len(outputSpecifier) > 0:
2185 connectionName = imageGraph.getNamePath() + '/' + outputSpecifier
2186
2187 # Add extension to texture entry
2188 baseColorEntry = roughness['baseColorTexture'] = {}
2189 baseColorEntry['index'] = 0 # Q: What should this be ?
2190 if 'extensions' not in baseColorEntry:
2191 baseColorEntry['extensions'] = {}
2192 extensions = baseColorEntry['extensions']
2193 if extensionName not in extensions:
2194 extensions[extensionName] = {}
2195 procExtensions = extensions[extensionName]
2196 procExtensions['index'] = procDict[connectionName]
2197
2198 else:
2199 if imageNode:
2200 fileInput = imageNode.getInput(mx.Implementation.FILE_ATTRIBUTE)
2201 if fileInput and fileInput.getAttribute(mx.TypedElement.TYPE_ATTRIBUTE) == mx.FILENAME_TYPE_STRING:
2202 filename = fileInput.getResolvedValueString()
2203
2204 if len(filename) == 0:
2205 imageNode = None
2206
2207 if imageNode:
2208 texture = {}
2209 self.initialize_gtlf_texture(texture, imageNode.getNamePath(), filename, images)
2210 textures.append(texture)
2211
2212 roughness['baseColorTexture'] = {}
2213 roughness['baseColorTexture']['index'] = len(textures) - 1
2214
2215 self.writeImageProperties(texture, samplers, imageNode)
2216
2217 # Pull off color from gltf_colorImage node
2218 color = pbrNode.getInputValue('base_color')
2219 if color:
2220 base_color[0] = color[0]
2221 base_color[1] = color[1]
2222 base_color[2] = color[2]
2223 base_color_set = True
2224
2225 else:
2226 color = pbrNode.getInputValue('base_color')
2227 if color:
2228 base_color[0] = color[0]
2229 base_color[1] = color[1]
2230 base_color[2] = color[2]
2231 base_color_set = True
2232
2233 value = pbrNode.getInputValue('alpha')
2234 if value:
2235 base_color[3] = value
2236 base_color_set = True
2237
2238 if base_color_set:
2239 roughness['baseColorFactor'] = base_color
2240
2241 # Handle metallic, roughness, occlusion
2242 # Handle partially mapped or when different channels map to different images
2243 # by merging into a single ORM image. Note that we save as BGR 24-bit fixed images
2244 # thus we scan by that order which results in an MRO image being written to disk.
2245 #roughness['metallicRoughnessTexture'] = {}
2246 metallicFactor = pbrNode.getInputValue('metallic')
2247 #if metallicFactor:
2248 # roughness['metallicFactor'] = metallicFactor
2249 # print('------------------------ set metallic', roughness['metallicFactor'],
2250 # ' node:', pbrNode.getNamePath())
2251 #roughnessFactor = pbrNode.getInputValue('roughness')
2252 #if roughnessFactor:
2253 # roughness['roughnessFactor'] = roughnessFactor
2254 extractInputs = [ 'metallic', 'roughness', 'occlusion' ]
2255 filenames = [ EMPTY_STRING, EMPTY_STRING, EMPTY_STRING ]
2256 imageNamePaths = [ EMPTY_STRING, EMPTY_STRING, EMPTY_STRING ]
2257 roughnessInputs = [ 'metallicFactor', 'roughnessFactor', '' ]
2258
2259 IN_STRING = MTLX_IN_STRING
2260 ormNode= None
2261 imageNode = None
2262 extractCategory = 'extract'
2263 for e in range(0, 3):
2264 inputName = extractInputs[e]
2265 pbrInput = pbrNode.getInput(inputName)
2266 if pbrInput:
2267 # Read past any extract node
2268 connectedNode = pbrNode.getConnectedNode(inputName)
2269 if connectedNode:
2270 if connectedNode.getCategory() == extractCategory:
2271 imageNode = connectedNode.getConnectedNode(IN_STRING)
2272 else:
2273 imageNode = connectedNode
2274
2275 if imageNode:
2276 fileInput = imageNode.getInput(mx.Implementation.FILE_ATTRIBUTE)
2277 filename = EMPTY_STRING
2278 if fileInput and fileInput.getAttribute(mx.TypedElement.TYPE_ATTRIBUTE) == mx.FILENAME_TYPE_STRING:
2279 filename = fileInput.getResolvedValueString()
2280 filenames[e] = filename
2281 imageNamePaths[e] = imageNode.getNamePath()
2282
2283 # Write out constant factors. If there is an image node
2284 # then ignore any value stored as the image takes precedence.
2285 if len(roughnessInputs[e]):
2286 value = pbrInput.getValue()
2287 if value != None:
2288 roughness[roughnessInputs[e]] = value
2289 #else:
2290 # roughness[roughnessInputs[e]] = 1.0
2291
2292 # Set to default 1.0
2293 #else:
2294 # if len(roughnessInputs[e]):
2295 # roughnessInputs[e] = 1.0
2296
2297 # Determine how many images to export and if merging is required
2298 metallicFilename = mx.FilePath(filenames[0])
2299 roughnessFilename = mx.FilePath(filenames[1])
2300 occlusionFilename = mx.FilePath(filenames[2])
2301 metallicFilename = self._options['searchPath'].find(metallicFilename)
2302 roughnessFilename = self._options['searchPath'].find(roughnessFilename)
2303 occlusionFilename = self._options['searchPath'].find(occlusionFilename)
2304
2305 # if metallic and roughness match but occlusion differs, Then export 2 textures if found
2306 if metallicFilename == roughnessFilename:
2307 if roughnessFilename == occlusionFilename:
2308 # All 3 are the same:
2309 if not roughnessFilename.isEmpty():
2310 print('- Append single ORM texture', roughnessFilename.asString())
2311 texture = {}
2312 self.initialize_gtlf_texture(texture, imageNamePaths[0], roughnessFilename.asString(mx.FormatPosix), images)
2313 self.writeImageProperties(texture, samplers, imageNode)
2314 textures.append(texture)
2315
2316 roughness['metallicRoughnessTexture'] = {}
2317 roughness['metallicRoughnessTexture']['index'] = len(textures) - 1
2318 else:
2319 # Metallic and roughness are the same
2320 if not metallicFilename.isEmpty():
2321 print('- Append single metallic-roughness texture', metallicFilename.asString())
2322 texture = {}
2323 self.initialize_gtlf_texture(texture, imageNamePaths[0], metallicFilename.asString(mx.FormatPosix), images)
2324 self.writeImageProperties(texture, samplers, imageNode)
2325 textures.append(texture)
2326
2327 roughness['metallicRoughnessTexture'] = {}
2328 roughness['metallicRoughnessTexture']['index'] = len(textures) - 1
2329
2330 # Append separate occlusion texture
2331 if not occlusionFilename.isEmpty():
2332 print('- Append single occlusion texture', metallicFilename.asString())
2333 texture = {}
2334 self.initialize_gtlf_texture(texture, imageNamePaths[2], occlusionFilename.asString(mx.FormatPosix), images)
2335 self.writeImageProperties(texture, samplers, imageNode)
2336 textures.append(texture)
2337
2338 material['occlusionTexture'] = {}
2339 material['occlusionTexture']['index'] = len(textures) - 1
2340
2341 # Metallic and roughness do no match and one or both are images. Merge as necessary
2342 elif not metallicFilename.isEmpty() or not (roughnessFilename).isEmpty():
2343 loader = mx_render.StbImageLoader.create()
2344 handler = mx_render.ImageHandler.create(loader)
2345 handler.setSearchPath(self._options['searchPath'])
2346 if handler:
2347 ormFilename = roughnessFilename if metallicFilename.isEmpty() else metallicFilename
2348
2349 imageWidth = 0
2350 imageHeight = 0
2351
2352 roughnessImage = handler.acquireImage(roughnessFilename) if not roughnessFilename.isEmpty() else None
2353 if roughnessImage:
2354 imageWidth = max(roughnessImage.getWidth(), imageWidth)
2355 imageHeight = max(roughnessImage.getHeight(), imageHeight)
2356
2357 metallicImage = handler.acquireImage(metallicFilename) if not metallicFilename.isEmpty() else None
2358 if metallicImage:
2359 imageWidth = max(metallicImage.getWidth(), imageWidth)
2360 imageHeight = max(metallicImage.getHeight(), imageHeight)
2361
2362 outputImage = None
2363 if (imageWidth * imageHeight) != 0:
2364 color = mx.Color4(0.0)
2365 outputImage = mx_render.createUniformImage(imageWidth, imageHeight,
2366 3, mx_render.BaseType.UINT8, color)
2367
2368 uniformRoughnessColor = 1.0
2369 if 'roughnessFactor' in roughness:
2370 uniformRoughnessColor = roughness['roughnessFactor']
2371 if roughnessImage:
2372 roughness['roughnessFactor'] = 1.0
2373
2374 uniformMetallicColor = 1.0
2375 if 'metallicFactor' in roughness:
2376 uniformMetallicColor = roughness['metallicFactor']
2377 if metallicImage:
2378 roughness['metallicFactor'] = 1.0
2379
2380 for y in range(0, imageHeight):
2381 for x in range(0, imageWidth):
2382 finalColor = outputImage.getTexelColor(x, y)
2383 finalColor[1] = roughnessImage.getTexelColor(x, y)[0] if roughnessImage else uniformRoughnessColor
2384 finalColor[2] = metallicImage.getTexelColor(x, y)[0] if metallicImage else uniformMetallicColor
2385 outputImage.setTexelColor(x, y, finalColor)
2386
2387 ormFilename.removeExtension()
2388 ormfilePath = ormFilename.asString(mx.FormatPosix) + '_combined.png'
2389 flipImage = False
2390 saved = loader.saveImage(ormfilePath, outputImage, flipImage)
2391
2392 uri = mx.FilePath(ormfilePath).getBaseName()
2393 print('- Merged metallic-roughness to single texture:', uri, 'Saved: ', saved)
2394 texture = {}
2395 self.initialize_gtlf_texture(texture, imageNode.getNamePath(), uri, images)
2396 self.writeImageProperties(texture, samplers, imageNode)
2397 textures.append(texture)
2398
2399 roughness['metallicRoughnessTexture'] = {}
2400 roughness['metallicRoughnessTexture']['index'] = len(textures) - 1
2401
2402 # Handle normal
2403 filename = EMPTY_STRING
2404 imageNode = pbrNode.getConnectedNode('normal')
2405 if imageNode:
2406 # Read past normalmap node
2407 if imageNode.getCategory() == 'normalmap':
2408 imageNode = imageNode.getConnectedNode(IN_STRING)
2409 if imageNode:
2410 fileInput = imageNode.getInput(mx.Implementation.FILE_ATTRIBUTE)
2411 filename = EMPTY_STRING
2412 if fileInput and fileInput.getAttribute(mx.TypedElement.TYPE_ATTRIBUTE) == mx.FILENAME_TYPE_STRING:
2413 filename = fileInput.getResolvedValueString()
2414 if len(filename) == 0:
2415 imageNode = None
2416 if imageNode:
2417 texture = {}
2418 self.initialize_gtlf_texture(texture, imageNode.getNamePath(), filename, images)
2419 textures.append(texture)
2420
2421 material['normalTexture'] = {}
2422 material['normalTexture']['index'] = len(textures) - 1
2423
2424 self.writeImageProperties(texture, samplers, imageNode)
2425
2426 # Handle transmission extension
2427 outputExtension = {}
2428 self.writeFloatInput(pbrNode, 'transmission',
2429 'transmissionTexture', 'transmissionFactor', outputExtension, textures, images, samplers)
2430 if len(outputExtension) > 0:
2431 extensionName = 'KHR_materials_transmission'
2432 extensions[extensionName] = outputExtension
2433 if extensionName not in extensionsUsed:
2434 extensionsUsed.append(extensionName)
2435
2436 # Handle specular color and specular extension
2437 imageNode = pbrNode.getConnectedNode('specular_color')
2438 imageGraph = None
2439 if imageNode:
2440 if imageNode.getParent().isA(mx.NodeGraph):
2441 imageGraph = imageNode.getParent()
2442
2443 # Procedural extension. WIP
2444 if imageGraph:
2445 extensionName = 'KHR_procedurals'
2446 if extensionName not in extensionsUsed:
2447 extensionsUsed.append(extensionName)
2448 if extensionName not in extensions:
2449 extensions[extensionName] = {}
2450 outputExtension = extensions[extensionName]
2451
2452 graphOutputs, procDict = self.graphToJson(imageGraph, gltfJson, materials, textures, images, samplers)
2453 outputs = imageGraph.getOutputs()
2454 if len(outputs) > 0:
2455 connectionName = outputs[0].getNamePath()
2456 inputBaseColor = pbrNode.getInput('specular_color')
2457 outputSpecifier = inputBaseColor.getAttribute('output')
2458 if len(outputSpecifier) > 0:
2459 connectionName = imageGraph.getNamePath() + '/' + outputSpecifier
2460 outputExtension['specularColorTexture'] = procDict[connectionName]
2461
2462 else:
2463 outputExtension = {}
2464 self.writeColor3Input(pbrNode, 'specular_color',
2465 'specularColorTexture', 'specularColorFactor', outputExtension, textures, images, samplers)
2466 self.writeFloatInput(pbrNode, 'specular',
2467 'specularTexture', 'specularFactor', outputExtension, textures, images, samplers)
2468 if len(outputExtension) > 0:
2469 extensionName = 'KHR_materials_specular'
2470 if extensionName not in extensionsUsed:
2471 extensionsUsed.append(extensionName)
2472 extensions[extensionName] = outputExtension
2473
2474 # Handle emission
2475 self.writeColor3Input(pbrNode, 'emissive',
2476 'emissiveTexture', 'emissiveFactor', material, textures, images, samplers)
2477
2478 # Handle emissive strength extension
2479 outputExtension = {}
2480 self.writeFloatInput(pbrNode, 'emissive_strength',
2481 '', 'emissiveStrength', outputExtension, textures, images, samplers)
2482 if len(outputExtension) > 0:
2483 extensionName = 'KHR_materials_emissive_strength'
2484 if extensionName not in extensionsUsed:
2485 extensionsUsed.append(extensionName)
2486 extensions[extensionName] = outputExtension
2487
2488 # Handle ior extension
2489 outputExtension = {}
2490 self.writeFloatInput(pbrNode, 'ior',
2491 '', 'ior', outputExtension, textures, images, samplers)
2492 if len(outputExtension) > 0:
2493 extensionName = 'KHR_materials_ior'
2494 if extensionName not in extensionsUsed:
2495 extensionsUsed.append(extensionName)
2496 extensions[extensionName] = outputExtension
2497
2498 # Handle sheen color
2499 outputExtension = {}
2500 self.writeColor3Input(pbrNode, 'sheen_color',
2501 'sheenColorTexture', 'sheenColorFactor', outputExtension, textures, images, samplers)
2502 self.writeFloatInput(pbrNode, 'sheen_roughness',
2503 'sheenRoughnessTexture', 'sheenRoughnessFactor', outputExtension, textures, images, samplers)
2504 if len(outputExtension) > 0:
2505 extensionName = 'KHR_materials_sheen'
2506 if extensionName not in extensionsUsed:
2507 extensionsUsed.append(extensionName)
2508 extensions[extensionName] = outputExtension
2509
2510 # Handle clearcloat
2511 outputExtension = {}
2512 self.writeFloatInput(pbrNode, 'clearcoat',
2513 'clearcoatTexture', 'clearcoatFactor', outputExtension, textures, images, samplers)
2514 self.writeFloatInput(pbrNode, 'clearcoat_roughness',
2515 'clearcoatRoughnessTexture', 'clearcoatRoughnessFactor', outputExtension, textures, images, samplers)
2516 if len(outputExtension) > 0:
2517 extensionName = 'KHR_materials_clearcoat'
2518 if extensionName not in extensionsUsed:
2519 extensionsUsed.append(extensionName)
2520 extensions[extensionName] = outputExtension
2521
2522 # Handle KHR_materials_volume extension
2523 # - Parse thickness
2524 outputExtension = {}
2525 thicknessInput = pbrNode.getInput('thickness')
2526 if thicknessInput:
2527
2528 thicknessNode = thicknessInput.getConnectedNode()
2529 thicknessFileName = EMPTY_STRING
2530 if thicknessNode:
2531 fileInput = thicknessNode.getInput(mx.Implementation.FILE_ATTRIBUTE)
2532 if fileInput and fileInput.getAttribute(mx.TypedElement.TYPE_ATTRIBUTE) == mx.FILENAME_TYPE_STRING:
2533 thicknessFileName = fileInput.getResolvedValueString()
2534
2535 if len(thicknessFileName) > 0:
2536 texture = {}
2537 self.initialize_gtlf_texture(texture, thicknessNode.getNamePath(), thicknessFileName, images)
2538 textures.append(texture)
2539
2540 outputExtension['thicknessTexture'] = {}
2541 outputExtension['thicknessTexture']['index'] = len(textures) - 1
2542 else:
2543 thicknessValue = thicknessInput.getValue()
2544 if thicknessValue:
2545 outputExtension['thicknessFactor'] = thicknessValue
2546
2547 # Parse attenuation and attenuation distance
2548 attenuationInput = pbrNode.getInput('attenuation_color')
2549 if attenuationInput:
2550 attenuationValue = attenuationInput.getValue()
2551 if attenuationValue:
2552 inputType = attenuationInput.getAttribute(mx.TypedElement.TYPE_ATTRIBUTE)
2553 outputExtension['attenuationColor'] = self.stringToScalar(attenuationInput.getValueString(), inputType)
2554 attenuationInput = pbrNode.getInput('attenuation_distance')
2555 if attenuationInput:
2556 attenuationValue = attenuationInput.getValue()
2557 if attenuationValue:
2558 outputExtension['attenuationDistance'] = attenuationValue
2559
2560 if len(outputExtension) > 0:
2561 extensionName = 'KHR_materials_volume'
2562 if extensionName not in extensionsUsed:
2563 extensionsUsed.append(extensionName)
2564 extensions[extensionName] = outputExtension
2565
2566 # Handle clearcoat normal
2567 filename = EMPTY_STRING
2568 imageNode = pbrNode.getConnectedNode('clearcoat_normal')
2569 if imageNode:
2570 # Read past normalmap node
2571 if imageNode.getCategory() == 'normalmap':
2572 imageNode = imageNode.getConnectedNode(IN_STRING)
2573 if imageNode:
2574 fileInput = imageNode.getInput(mx.Implementation.FILE_ATTRIBUTE)
2575 filename = EMPTY_STRING
2576 if fileInput and fileInput.getAttribute(mx.TypedElement.TYPE_ATTRIBUTE) == mx.FILENAME_TYPE_STRING:
2577 filename = fileInput.getResolvedValueString()
2578 if filename == EMPTY_STRING:
2579 imageNode = None
2580 if imageNode:
2581 texture = {}
2582 self.initialize_gtlf_texture(texture, imageNode.getNamePath(), filename, images)
2583 self.writeImageProperties(texture, samplers, imageNode)
2584 textures.append(texture)
2585
2586 outputExtension['clearcoatNormalTexture'] = {}
2587 outputExtension['clearcoatNormalTexture']['index'] = len(textures) - 1
2588
2589 # Handle alphA mode, cutoff
2590 alphModeInput = pbrNode.getInput('alpha_mode')
2591 if alphModeInput:
2592 value = alphModeInput.getValue()
2593
2594 alphaModeMap = {}
2595 alphaModeMap[0] = 'OPAQUE'
2596 alphaModeMap[1] = 'MASK'
2597 alphaModeMap[2] = 'BLEND'
2598 self.writeFloatInput(pbrNode, 'alpha_mode', '', 'alphaMode', material, textures, images, samplers, alphaModeMap)
2599 if value == 'MASK':
2600 self.writeFloatInput(pbrNode, 'alpha_cutoff', '', 'alphaCutoff', material, textures, images, samplers)
2601
2602
2603 # Handle iridescence
2604 outputExtension = {}
2605 self.writeFloatInput(pbrNode, 'iridescence',
2606 'iridescenceTexture', 'iridescenceFactor', outputExtension, textures, images, samplers)
2607 self.writeFloatInput(pbrNode, 'iridescence_ior',
2608 'iridescenceTexture', 'iridescenceIor', outputExtension, textures, images, samplers)
2609 if len(outputExtension) > 0:
2610 extensionName = 'KHR_materials_iridescence'
2611 if extensionName not in extensionsUsed:
2612 extensionsUsed.append(extensionName)
2613 extensions[extensionName] = outputExtension
2614
2615 # Scan for upstream <gltf_iridescence_thickness> node.
2616 # Note: This is the agreed upon upstream node to create to map
2617 # to gltf_pbr as part of the core implementation. It basically
2618 # represents this structure:
2619 # https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_iridescence/README.md
2620 thicknessInput = pbrNode.getInput('iridescence_thickness')
2621 if thicknessInput:
2622
2623 thicknessNode = thicknessInput.getConnectedNode()
2624 thicknessFileName = mx.FilePath()
2625 if thicknessNode:
2626 fileInput = thicknessNode.getInput(mx.Implementation.FILE_ATTRIBUTE)
2627 thicknessFileName = EMPTY_STRING
2628 if fileInput and fileInput.getAttribute(mx.TypedElement.TYPE_ATTRIBUTE) == mx.FILENAME_TYPE_STRING:
2629 thicknessFileName = fileInput.getResolvedValueString()
2630
2631 texture = {}
2632 self.initialize_gtlf_texture(texture, thicknessNode.getNamePath(), thicknessFileName, images)
2633 textures.append(texture)
2634
2635 outputExtension['iridescenceThicknessTexture'] = {}
2636 outputExtension['iridescenceThicknessTexture']['index'] = len(textures) - 1
2637
2638 thickessInput = thicknessNode.getInput('thicknessMin')
2639 thicknessValue = thickessInput.getValue() if thickessInput else None
2640 if thicknessValue:
2641 outputExtension['iridescenceThicknessMinimum'] = thicknessValue
2642 thickessInput = thicknessNode.getInput('thicknessMax')
2643 thicknessValue = thickessInput.getValue() if thickessInput else None
2644 if thicknessValue:
2645 outputExtension['iridescenceThicknessMaximum'] = thicknessValue
2646
2647 if len(material['extensions']) == 0:
2648 del material['extensions']
2649
2650 materials.append(material)
2651
2652 # Remove any empty items to avoid validation errors
2653 if len(gltfJson['extensionsUsed']) == 0:
2654 del gltfJson['extensionsUsed']
2655 if len(gltfJson['samplers']) == 0:
2656 del gltfJson['samplers']
2657 if len(gltfJson['images']) == 0:
2658 del gltfJson['images']
2659 if len(gltfJson['textures']) == 0:
2660 del gltfJson['textures']
2661
2662 def packageGLTF(self, inputFile, outputFile):
2663 '''
2664 @brief Package gltf file into a glb file
2665 @param inputFile gltf file to package
2666 @param outputFile Packaged glb file
2667 @return status, image list, buffer list.
2668 '''
2669 images = []
2670 buffers = []
2671
2672 # Load the gltf file
2673 gltfb = GLTF2()
2674 gltf = gltfb.load(inputFile)
2675 if not gltf:
2676 return False, images
2677
2678 # Embed images
2679 if len(gltf.images):
2680 for im in gltf.images:
2681 self.log('- Embedding image: ' + im.uri)
2682 searchPath = self._options['searchPath']
2683 path = searchPath.find(im.uri)
2684 path = searchPath.find(im.uri)
2685 if path:
2686 im.uri = path.asString(mx.FormatPosix)
2687 self.log('- Remapped buffer URI to: ' + im.uri)
2688 images.append(im.uri)
2689 gltf.convert_images(ImageFormat.DATAURI)
2690
2691 # Embed buffer references.
2692 # If the input file is not in the same folder as the current working directory,
2693 # then modify the buffer uri to be an absolute path.
2694 if len(gltf.buffers):
2695 absinputFile = os.path.abspath(inputFile)
2696 parentFolder = os.path.dirname(absinputFile)
2697 for buf in gltf.buffers:
2698 searchPath = self._options['searchPath']
2699 path = searchPath.find(buf.uri)
2700 if path:
2701 buf.uri = path.asString(mx.FormatPosix)
2702 self.log('- Remapped buffer URI to: ' + buf.uri)
2703 buffers.append(buf.uri)
2704 gltfb.convert_buffers(BufferFormat.BINARYBLOB)
2705
2706 saved = gltf.save(outputFile)
2707 return saved, images, buffers
2708
2709 def translateShaders(self, doc):
2710 '''
2711 @brief Translate shaders to gltf pbr
2712 @param doc MaterialX document to examine
2713 @return Number of shaders translated
2714 '''
2715 shadersTranslated = 0
2716 if not doc:
2717 return shadersTranslated
2718
2719 materialNodes = doc.getMaterialNodes()
2720 for material in materialNodes:
2721 shaderNodes = mx.getShaderNodes(material)
2722 for shaderNode in shaderNodes:
2723 category = shaderNode.getCategory()
2724 path = shaderNode.getNamePath()
2725 if category != MTLX_GLTF_PBR_CATEGORY and category != MTLX_UNLIT_CATEGORY_STRING:
2726 status, error = self.translateShader(shaderNode, MTLX_GLTF_PBR_CATEGORY)
2727 if not status:
2728 self.log('Failed to translate shader:' + shaderNode.getNamePath() +
2729 ' of type ' + shaderNode.getCategory())
2730 # + '\nError: ' + error.error)
2731 else:
2732 shadersTranslated = shadersTranslated + 1
2733
2734 return shadersTranslated
2735
2736 def convert(self, doc):
2737 '''
2738 @brief Convert MaterialX document to glTF
2739 @param doc MaterialX document to convert
2740 @return glTF JSON string
2741 '''
2742 # Resulting glTF JSON string
2743 gltfJson = {}
2744
2745 # Check for glTF geometry file inclusion
2746 gltfGeometryFile = self._options['geometryFile']
2747 if len(gltfGeometryFile):
2748 print('- glTF geometry file:' + gltfGeometryFile)
2749 if os.path.exists(gltfGeometryFile):
2750 gltfFile = open(gltfGeometryFile, 'r')
2751 if gltfFile:
2752 gltfJson = json.load(gltfFile)
2753 if self._options['debugOutput']:
2754 print('- Embedding glTF geometry file:' + gltfGeometryFile)
2755 self.log('- Embedding glTF geometry file:' + gltfGeometryFile)
2756
2757 # If no materials, add a default material
2758 for mesh in gltfJson['meshes']:
2759 for primitive in mesh['primitives']:
2760 if 'material' not in primitive:
2761 primitive['material'] = 0
2762 else:
2763 if self._options['debugOutput']:
2764 print('- glTF geometry file not found:' + gltfGeometryFile)
2765 self.log('- glTF geometry file not found:' + gltfGeometryFile)
2766
2767 # Clear and convert materials
2768 resetMaterials = True
2769 self.materialX2glTF(doc, gltfJson, resetMaterials)
2770
2771 # If geometry specified, create new primitives for each material
2772 materialCount = len(gltfJson['materials']) if 'materials' in gltfJson else 0
2773 if self._options['primsPerMaterial'] and (materialCount > 0):
2774 if self._options['debugOutput']:
2775 print('- Generating a new primitive for each of %d materials' % materialCount)
2776 # Compute sqrt of materialCount
2777 rowCount = int(math.sqrt(materialCount))
2778 self.createPrimsForMaterials(gltfJson, rowCount)
2779
2780 # Get resulting glTF JSON string
2781 gltfString = json.dumps(gltfJson, indent=2)
2782 self.log('- Output glTF with new MaterialX materials' + str(gltfString))
2783
2784 return gltfString
2785
Class to read glTF and convert to MaterialX.
Definition core.py:141
Class to hold options for glTF to MaterialX conversion.
Definition core.py:121
__init__(self, *args, **kwargs)
Constructor.
Definition core.py:130
Class to hold options for MaterialX to glTF conversion.
Definition core.py:1141
__init__(self, *args, **kwargs)
Constructor.
Definition core.py:1156
Class to read in a MaterialX document and write to a glTF document.
Definition core.py:1173
elementToJSON(self, elem, jsonParent)
Convert an MaterialX XML element to JSON.
Definition core.py:1287
writeColor3Input(self, pbrNode, inputName, gltfTextureName, gltfValueName, material, textures, images, samplers)
Write a color3 input from a MaterialX pbr node to a glTF material entry.
Definition core.py:1927
packageGLTF(self, inputFile, outputFile)
Package gltf file into a glb file.
Definition core.py:2662
getShaderNodes(self, graphElement)
Find all surface shaders in a GraphElement.
Definition core.py:1234
graphToJson(self, graph, json, materials, textures, images, samplers)
Convert a MaterialX graph to JSON.
Definition core.py:1350
bool bakeTextures(self, doc, hdr, width, height, useGlslBackend, average, writeDocumentPerMaterial, outputFilename)
Bake all textures in a MaterialX document.
Definition core.py:1545
None stringToScalar(self, value, type)
Convert a string to a scalar value.
Definition core.py:1330
getRenderableGraphs(self, doc, graphElement)
Find all renderable graphs in a GraphElement.
Definition core.py:1249
copyGraphInterfaces(self, dest, ng, removeInternals)
Copy the interface of a nodegraph to a new nodegraph under a specified parent dest.
Definition core.py:1267
MTLX2GLTFOptions getOptions(self)
Get the options for the reader.
Definition core.py:1208
None initialize_gtlf_texture(self, texture, name, uri, images)
Initialize a new gltF image entry and texture entry which references the image entry.
Definition core.py:1215
None assignMaterial(self, doc, gltfJson)
Assign materials to meshes based on MaterialX looks (material assginments).
Definition core.py:1700
documentToJSON(self, doc)
Convert an MaterialX XML document to JSON.
Definition core.py:1314
None writeImageProperties(self, texture, samplers, imageNode)
Write image properties of a MaterialX gltF image node to a glTF texture and sampler entry.
Definition core.py:1768
log(self, string)
Log a string.
Definition core.py:1195
tuple[bool, str] translateShader(self, shader, destCategory)
Translate a MaterialX shader to a different category.
Definition core.py:1525
None createPrimsForMaterials(self, gltfJson, rowCount=10)
Create new meshes and nodes for each material in a glTF document.
Definition core.py:1634
materialX2glTF(self, doc, gltfJson, resetMaterials)
Convert a MaterialX document to glTF.
Definition core.py:1988
clearLog(self)
Clear the log.
Definition core.py:1183
buildPrimPaths(self, primPaths, cnode, path, nodeCount, meshCount, meshes, nodes)
Recurse through a node hierarcht to build a dictionary of paths to primitives.
Definition core.py:1574
setOptions(self, options)
Set options.
Definition core.py:1201
writeCopyright(self, doc, gltfJson)
Write a glTF document copyright information.
Definition core.py:1972
writeFloatInput(self, pbrNode, inputName, gltfTextureName, gltfValueName, material, textures, images, samplers, remapper=None)
Write a float input from a MaterialX pbr node to a glTF material entry.
Definition core.py:1876
convert(self, doc)
Convert MaterialX document to glTF.
Definition core.py:2736
translateShaders(self, doc)
Translate shaders to gltf pbr.
Definition core.py:2709
Basic I/O Utilities.
Definition core.py:30
writeMaterialXDoc(doc, filename, predicate=skipLibraryElement)
Utility to write a MaterialX document to a file.
Definition core.py:56
writeMaterialXDocString(doc, predicate=skipLibraryElement)
Utility to write a MaterialX document to string.
Definition core.py:71
tuple[mx.Document, list] createMaterialXDoc()
Utility to create a MaterialX document with the default libraries loaded.
Definition core.py:33
bool skipLibraryElement(elem)
Utility to skip library elements when iterating over elements in a document.
Definition core.py:48
list makeFilePathsRelative(doc, docPath)
Utility to make file paths relative to a document path.
Definition core.py:86