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