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