MaterialXMaterials 1.39.5
Utilities for retrieving materials from remote servers
Loading...
Searching...
No Matches
physicallyBasedMaterialX.py
1'''
2@brief Class to load Physically Based Materials from the PhysicallyBased site.
3and convert the materials to MaterialX format for given target shading models.
4'''
5
6#from numpy import source
7import requests, json, os, inspect # type: ignore
8import logging as lg
9from http import HTTPStatus
10import MaterialX as mx # type: ignore
11from typing import Optional
12import importlib.resources
13import json
14import datetime
15
17 '''
18 @brief Class to load Physically Based Materials from the PhysicallyBased site.
19 The class can convert the materials to MaterialX format for given target shading models.
20 '''
21 def __init__(self, mx_module, mx_stdlib : Optional[mx.Document] = None, materials_file : str = ''):
22 '''
23 @brief Constructor for the PhysicallyBasedMaterialLoader class.
24 Will initialize shader mappings and load the MaterialX standard library
25 if it is not passed in as an argument.
26 @param mx_module The MaterialX module. Required.
27 @param mx_stdlib The MaterialX standard library. Optional.
28 '''
29
30 self.logger = lg.getLogger('PBMXLoader')
31 lg.basicConfig(level=lg.INFO)
32
33
34 self.materials : dict = {}
35
36 self.materialNames : list[str]= []
37
38 self.uri = 'https://api.physicallybased.info/v2/materials'
39
40 self.doc = None
41
42 self.mx = mx_module
43
44 self.stdlib = mx_stdlib
45
46 self.physlib = None
47
48 self.physlib_definition_name = "ND_PhysicallyBasedMaterial"
49
50 self.physlib_implementation_name = "NG_PhysicallyBasedMaterial"
51
52 self.physlib_category = "physbased_pbr_surface"
53
55
57
58 self.all_lib = None
59
60 self.MTLX_NODE_NAME_ATTRIBUTE = 'nodename'
61
62 self.support_openpbr = False
63
64 self.remapMap = {}
65
66 self.remapFile = 'PhysicallyBasedMaterialX/PhysicallyBasedToMtlxMappings.json'
67
68 self.desired_color_space = 'srgb-linear'
69
70 if not mx_module:
71 self.logger.critical(f'> {self._getMethodName()}: MaterialX module not specified.')
72 return
73
74 # Check for OpenPBR support which is only available in 1.39 and above
75 version_major, version_minor, version_patch = self.mx.getVersionIntegers()
76 self.logger.debug(f'> MaterialX version: {version_major}.{version_minor}.{version_patch}')
77 if (version_major >=1 and version_minor >= 39) or version_major > 1:
78 self.logger.debug('> OpenPBR shading model supported')
79 self.support_openpbr = True
80
81 # Load the MaterialX standard library if not provided
82 if not self.stdlib:
83 self.stdlib = self.mx.createDocument()
84 libFiles = self.mx.loadLibraries(mx.getDefaultDataLibraryFolders(), mx.getDefaultDataSearchPath(), self.stdlib)
85 self.logger.debug(f'> Loaded standard library: {libFiles}')
86
87 # Initialize Physically Based MaterialX definitions, materials, remappings, and translators
89
90 def set_desired_color_space(self, color_space : str):
91 '''
92 @brief Set the desired color space to use for writing colors. Default is 'srgb-linear' which remaps to 'link_rec709'.
93 @param color_space The desired color space to use for writing colors.
94 @return None
95 '''
96 self.desired_color_space = color_space
97
98 def initialize_definitions_and_materials(self, materials_file : str = ''):
99 '''
100 @brief Initialize Physically Based MaterialX definitions, materials, remappings, and translators.
101 @return None
102 '''
103 # Load information from PhysicallyBased site, and initialize remappings
104 if materials_file and os.path.exists(materials_file):
105 self.loadMaterialsFromFile(materials_file)
106 else:
109
110 # Create Physically Based MaterialX definition library
111 self.physlib = self.create_definition(None)
112 self.logger.info('> Created Physically Based MaterialX definition library...')
113 status, error = self.physlib.validate()
114 if not status:
115 self.logger.info(mx.prettyPrint(self.physlib))
116 self.logger.error('> Error validating NodeDef document:')
117 self.logger.error(error)
118 else:
119 self.logger.info('> Definition documents passed validation.')
120
121 # Create all translators
122 self.physlib_translators = mx.createDocument()
124
125 if self.physlib:
126 filter_list = []
127 self.physlib_materials = self.create_definition_materials(None, filter_list)
128
129 def setDebugging(self, debug=True):
130 '''
131 @brief Set the debugging level for the logger.
132 @param debug True to set the logger to debug level, otherwise False.
133 @return None
134 '''
135 if debug:
136 self.logger.setLevel(lg.DEBUG)
137 else:
138 self.logger.setLevel(lg.INFO)
139
140 def get_stdlib(self) -> mx.Document:
141 '''
142 @brief Get the MaterialX standard library document.
143 @return The MaterialX standard library document.
144 '''
145 return self.stdlib
146
147 def get_physlib(self) -> mx.Document:
148 '''
149 @brief Get the Physically Based MaterialX definition library.
150 @return The Physically Based MaterialX definition library.
151 '''
152 return self.physlib
153
154 def get_physlib_definition(self) -> mx.NodeDef | None:
155 '''
156 @brief Get the Physically Based MaterialX definition NodeDef.
157 @return The Physically Based MaterialX definition NodeDef.
158 '''
159 if self.physlib:
160 return self.physlib.getNodeDef(self.get_physlib_definition_name())
161 return None
162
163 def get_physlib_category(self) -> str:
164 '''
165 @brief Get the Physically Based MaterialX surface category.
166 @return The Physically Based MaterialX surface category.
167 '''
168 return self.physlib_category
169
171 '''
172 @brief Get the Physically Based MaterialX definition name.
173 @return The Physically Based MaterialX definition name.
174 '''
175 return self.physlib_definition_name
176
178 '''
179 @brief Get the Physically Based MaterialX implementation (nodegraph) name.
180 @return The Physically Based MaterialX implementation (nodegraph) name.
181 '''
183
184 def get_physlib_materials(self) -> mx.Document:
185 '''
186 @brief Get the Physically Based MaterialX materials document.
187 @return The Physically Based MaterialX materials document.
188 '''
189 return self.physlib_materials
190
191 def get_definitions(self) -> mx.Document:
192 '''
193 @brief Get a combined MaterialX document containing the standard library and Physically Based MaterialX definition and translators.
194 @return The combined MaterialX document.
195 '''
196 if not self.all_lib:
197 self.all_lib = self.mx.createDocument()
198 self.all_lib.copyContentFrom(self.stdlib)
199 if self.physlib:
200 self.all_lib.copyContentFrom(self.physlib)
201 if self.physlib_translators:
202 self.all_lib.copyContentFrom(self.physlib_translators)
203 return self.all_lib
204
205 def get_translators(self) -> mx.Document:
206 '''
207 @brief Get the Physically Based MaterialX translators document.
208 @return The Physically Based MaterialX translators document.
209 '''
210 return self.physlib_translators
211
212 def getInputRemapping(self, shadingModel) -> dict:
213 '''
214 @brief Get the remapping keys for a given shading model.
215 @param shadingModel The shading model to get the remapping keys for.
216 @return A dictionary of remapping keys.
217 '''
218 if (shadingModel in self.remapMap):
219 return self.remapMap[shadingModel]
220
221 #self.logger.warning(f'> No remapping keys found for shading model: {shadingModel}')
222 return {}
223
225 '''
226 @brief Initialize remapping keys for different shading models.
227 See: https://api.physicallybased.info/v2/#tag/materials/GET/materials
228 for more information on material properties.
229
230 The JSON file PhysicallyBasedToMtlxMappings.json which is part of the package
231 will be used if it exists. Otherwise, default remapping keys will be used.
232
233 The currently supported shading models are:
234 - standard_surface
235 - open_pbr_surface
236 - gltf_pbr
237 @return None
238 '''
239 # Read PhysicallyBasedToMtlxMappings.json installed package
240 self.remapMap = {}
241
242 try:
243 with importlib.resources.files("materialxMaterials.data").joinpath(self.remapFile).open("r", encoding="utf-8") as json_file:
244 self.logger.info(f'> Load remapping from installed package: {self.remapFile}')
245 self.remapMap = json.load(json_file)
246 except FileNotFoundError:
247 self.logger.warning('> No remapping file found in installed package. Using default remapping keys.')
248
249 if self.remapMap:
250 return
251
252 # Remap keys for Autodesk Standard Surface shading model.
253 standard_surface_remapKeys = {
254 'color': 'base_color',
255 'specularColor': 'specular_color',
256 'roughness': 'specular_roughness',
257 'metalness': 'metalness',
258 'ior': 'specular_IOR',
259 'subsurfaceRadius': 'subsurface_radius',
260 'transmission': 'transmission',
261 'transmission_color': 'transmission_color', # 'color' remapping as needed
262 'transmissionDispersion' : 'transmission_dispersion',
263 'thinFilmThickness' : 'thin_film_thickness',
264 'thinFilmIor' : 'thin_film_IOR',
265 }
266 # Remap keys for OpenPBR shading model.
267 # Q: When to set geometry_thin_walled to true?
268 openpbr_remapKeys = {
269 'color': 'base_color',
270 'specularColor': 'specular_color',
271 'roughness': 'specular_roughness', # 'base_diffuse_roughness',
272 'metalness': 'base_metalness',
273 'ior': 'specular_ior',
274 'subsurfaceRadius': 'subsurface_radius',
275 'transmission': 'transmission_weight',
276 'transmission_color': 'transmission_color', # 'color' remapping as needed
277 'transmissionDispersion': 'transmission_dispersion_abbe_number',
278 #'complexIor' TODO : add in array remap
279 # Complex IOR values, n (refractive index), and k (extinction coefficient), for each color channel, in the following order:
280 # nR, kR, nG, kG, nB, kB.
281 'thinFilmThickness' : 'thin_film_thickness',
282 'thinFilmIor' : 'thin_film_ior',
283 }
284 # Remap keys for Khronos glTF shading model.
285 gltf_remapKeys = {
286 'color': 'base_color',
287 'specularColor': 'specular_color',
288 'roughness': 'roughness',
289 'metalness': 'metallic',
290 'ior': 'ior',
291 'transmission': 'transmission',
292 'transmission_color': 'attenuation_color', # Remap transmission color to attenuation color
293 'thinFilmThickness' : 'iridescence_thickness',
294 'thinFilmIor' : 'iridescence_ior',
295 }
296
297 self.remapMap = {}
298 self.remapMap['standard_surface'] = standard_surface_remapKeys;
299 self.remapMap['gltf_pbr'] = gltf_remapKeys;
300 if self.support_openpbr:
301 self.remapMap['open_pbr_surface'] = openpbr_remapKeys;
302
303 def writeRemappingFile(self, filepath):
304 '''
305 @brief Write the remapping keys to a JSON file.
306 @param filepath The filename to write the remapping keys to.
307 @return None
308 '''
309 if not self.remapMap:
310 self.logger.warning('No remapping keys to write')
311 return
312
313 with open(filepath, 'w') as json_file:
314 json.dump(self.remapMap, json_file, indent=4)
315
316 def readRemappingFile(self, filepath):
317 '''
318 @brief Read the remapping keys from a JSON file.
319 @param filepath The filename to read the remapping keys from.
320 @return A dictionary of remapping keys.
321 '''
322 if not os.path.exists(filepath):
323 self.logger.error(f'> File does not exist: {filepath}')
324 return {}
325
326 with open(filepath, 'r') as json_file:
327 self.remapMap = json.load(json_file)
328
329 return self.remapMap
330
331 def getJSON(self) -> dict:
332 ''' Get the JSON object representing the Physically Based Materials '''
333 return self.materials
334
335 def getJSONMaterialNames(self) -> list:
336 '''
337 Get the list of material names from the JSON object
338 @return The list of material names
339 '''
340 return self.materialNames
341
342 def getMaterialXDocument(self) -> mx.Document:
343 '''
344 Get the MaterialX document
345 @return The MaterialX document
346 '''
347 return self.doc
348
349 def loadMaterialsFromFile(self, fileName) -> dict:
350 '''
351 @brief Load the Physically Based Materials from a JSON file
352 @param fileName The filename to load the JSON file from
353 @return The JSON object representing the Physically Based Materials
354 '''
355 self.materials.clear()
356 self.materialNames.clear()
357 if not os.path.exists(fileName):
358 self.logger.error(f'> File does not exist: {fileName}')
359 return {}
360
361 with open(fileName, 'r') as json_file:
362 self.materials = json.load(json_file)
363 for mat in self.materials:
364 self.materialNames.append(mat['name'])
365
366 return self.materials
367
368 def loadMaterialsFromString(self, matString) -> dict:
369 '''
370 @brief Load the Physically Based Materials from a JSON string
371 @param matString The JSON string to load the Physically Based Materials from
372 @return The JSON object representing the Physically Based Materials
373 '''
374 self.materials.clear()
375 self.materialNames.clear()
376 self.materials = json.loads(matString)
377 for mat in self.materials:
378 self.materialNames.append(mat['name'])
379
380 return self.materials
381
382 def getMaterialsFromURL(self) -> dict:
383 '''
384 @brief Get the Physically Based Materials from the PhysicallyBased site
385 @return The JSON object representing the Physically Based Materials
386 '''
387
388 self.materials.clear()
389 self.materialNames.clear()
390 url = self.uri
391 headers = {
392 'Accept': 'application/json'
393 }
394
395 response = requests.get(url, headers=headers)
396
397 if response.status_code == HTTPStatus.OK:
398 self.materials = response.json()["data"]
399 for mat in self.materials:
400 self.materialNames.append(mat['name'])
401
402 else:
403 self.logger.error(f'> Status: {response.status_code}, {response.text}')
404
405 return self.materials
406
407 def printMaterials(self):
408 '''
409 @brief Print the materials to the console
410 @return None
411 '''
412 for mat in self.materials:
413 self.logger.info('Material name: ' + mat['name'])
414 # Print out each key and value
415 for key, value in mat.items():
416 if (key != 'name' and value):
417 self.logger.info(f'> - {key}: {value}')
418
419 def writeJSONToFile(self, filename):
420 '''
421 @brief Write the materials to a JSON file
422 @param filename The filename to write the JSON file to
423 @return True if the file was written successfully, otherwise False
424 '''
425 if not self.materials:
426 self.logger.warning('No materials to write')
427 return False
428
429 with open(filename, 'w') as json_file:
430 json.dump(self.materials, json_file, indent=4)
431 return True
432
433 return False
434
435 @staticmethod
436 def skipLibraryElement(elem) -> bool:
437 '''
438 @brief Utility to skip library elements when iterating over elements in a document.
439 @return True if the element is not in a library, otherwise False.
440 '''
441 return not elem.hasSourceUri()
442
443 def _getMethodName(self):
444 '''
445 @brief Get the name of the calling method for logging purposes.
446 @return The name of the calling method.
447 '''
448 frame = inspect.currentframe().f_back
449 method_name = frame.f_code.co_name
450 return method_name
451 #return inspect.currentframe().f_code.co_name
452
454 '''
455 @brief Validate the MaterialX document
456 @param doc The MaterialX document to validate
457 @return A tuple of (valid, errors) where valid is True if the document is valid, and errors is a list of errors if the document is invalid.
458 '''
459 if not self.mx:
460 self.logger.critical(f'> {self._getMethodName()}: MaterialX module is required')
461 return False, ''
462
463 if not doc:
464 self.logger.warning(f'> {self._getMethodName()}: MaterialX document is required')
465 return False, ''
466
467 valid, errors = doc.validate()
468 return valid, errors
469
470 def addComment(self, doc, commentString):
471 '''
472 @brief Add a comment to the MaterialX document
473 @param doc The MaterialX document to add the comment to
474 @param commentString The comment string to add
475 @return None
476 '''
477 comment = doc.addChildOfCategory('comment')
478 comment.setDocString(commentString)
479
480 def map_keys_to_definition(self, mat, ndef):
481 '''
482 @brief Map a key to a NodeDef input
483 @param mat Material to map keys from
484 @param ndef The definition to map the key to
485 @return The input name if the key was mapped, otherwise None
486 '''
487 for key, value in mat.items():
488 uifolder = None
489 if ndef.getInput(key) is None:
490 if key == 'name':
491 # Skip as these wil be node instance names
492 pass
493 #continue
494
495 #print('Add key to nodedef:', key)
496
497 self.logger.debug(f'> Add key as input: {key}')
498
499 input_type = "string"
500 if 'color' in key.lower():
501 input_type = "color3"
502 value = "1,1,1"
503 elif isinstance(value, float):
504 input_type = "float"
505 value = "0.0"
506 elif isinstance(value, int):
507 input_type = "float"
508 value = "0.0"
509 elif key == 'category':
510 #if isinstance(value, list) and len(value) > 0:
511 # uifolder = str(value[0])
512 #else:
513 # uifolder = str(value)
514 #value = None
515 pass
516 elif key in ['images', 'references', 'tags', 'group']:
517 value = ''
518
519 input = ndef.addInput(key, input_type)
520 if input:
521 if value is not None:
522 if isinstance(value, list):
523 # Split list into array
524 value_list = [str(x) for x in value]
525 # Check if values are numbers
526 is_number_list = all(isinstance(x, (int, float)) for x in value)
527 if is_number_list:
528 if len(value_list) > 4 :
529 input.setType('string') # floatarray will cause errors in shader generation !
530 elif len(value_list) > 3 :
531 input.setType('vector4')
532 elif len(value_list) > 2 :
533 input.setType('vector3')
534 elif len(value_list) > 1 :
535 input.setType('vector2')
536 else:
537 input.setType('float')
538 # Replace all numbers with 0.0
539 value_list = ['0.0' for x in value_list]
540 value = ', '.join(value_list)
541
542 #value = ','.join([str(x) for x in value])
543 input.setValueString(str(value))
544
545
546 uiname = key
547 # Split camel case names and separate by space
548 # e.g. specularColor -> Specular Color
549 uiname = ''.join([' ' + c if c.isupper() else c for c in key]).strip().title()
550 input.setAttribute("uiname", uiname)
551
552 uifolder = 'Base'
553 if key in ['description', 'images', 'references', 'tags']:
554 uifolder = 'Metadata'
555 input.setAttribute("uifolder", uifolder)
556
557 # Add doc string
558 #doc_string = ''
559 #if key == 'description':
560 # doc_string = str(value)
561 #if len(doc_string) > 0:
562 # input.setDocString(doc_string)
563
564 if uifolder is not None:
565 input.setAttribute("uifolder", uifolder)
566
567 def find_all_bxdf(self, doc : mx.Document) -> list[mx.NodeDef]:
568 '''
569 @brief Scan all nodedefs with output type of "surfaceshader"
570 doc : The MaterialX document to scan
571 @return A list of nodedefs found
572 '''
573 bxdfs : list[mx.NodeDef] = []
574 for nodedef in doc.getNodeDefs():
575 if nodedef.getType() == "surfaceshader":
576 if nodedef.getNodeString() not in ["convert", "surface"] and nodedef.getNodeGroup() == "pbr":
577 bxdfs.append(nodedef)
578 return bxdfs
579
580 def derive_translator_name_from_targets(self, source : str, target : str) -> str:
581 return f"ND_{source}_to_{target}"
582
583 def create_translator(self, doc : mx.Document,
584 source : str, target : str,
585 source_version = "", target_version = "",
586 mappings = None,
587 output_doc : mx.Document | None = None):
588 '''
589 @brief Create a translator nodedef and nodegraph from source to target definitions.
590 @param doc The source document containing the definition.
591 @param source The source definition category.
592 @param target The target definition category.
593 @param source_version The source version string. If empty, use the first source definition version found.
594 @param target_version The target version string. If empty, use the first target definition version found.
595 @param mappings A dictionary mapping source input names to target input names.
596 @param output_doc The document to add the translator to. If None, use the source doc.
597 @return The created translator definition.
598 '''
599 if not output_doc:
600 return None
601
602 # Get source and target nodedefs
603 nodedefs = self.find_all_bxdf(doc)
604
605 #nodedefs : list[mx.NodeDef] = doc.getNodeDefs()
606 #nodedefs_set = dict((nd.getNodeString() + nd.getVersionString(), nd) for nd in nodedefs)
607 #for key, value in nodedefs_set.items():
608 # print(f"Key: '{key}' -> Nodedef: '{value.getNodeString()}'")
609 source_nodedef = None
610 target_nodedef = None
611 for nodedef in nodedefs:
612 if nodedef.getNodeString() == source:
613 if source_version == "" or nodedef.getVersionString() == source_version:
614 source_nodedef = nodedef
615 if nodedef.getNodeString() == target:
616 if target_version == "" or nodedef.getVersionString() == target_version:
617 target_nodedef = nodedef
618
619 if not source_nodedef or not target_nodedef:
620 #raise ValueError(f"Source or target nodedef not found for '{source}' to '{target}'")
621 if not source_nodedef:
622 print(f"Source nodedef not found for '{source}' with version '{source_version}'")
623 if not target_nodedef:
624 print(f"Target nodedef not found for '{target}' with version '{target_version}'")
625 return None
626 #else:
627 # print("Found source nodedef:", source_nodedef.getNodeString(), "version:", source_nodedef.getVersionString())
628 # print("Found target nodedef:", target_nodedef.getNodeString(), "version:", target_nodedef.getVersionString())
629
630 # 1. Add a new nodedef for the translator
631 derived_name = self.derive_translator_name_from_targets(source, target)
632 nodename = derived_name[3:] if derived_name.startswith("ND_") else derived_name
633 translator_nodedef : mx.NodeDef = output_doc.getNodeDef(derived_name)
634 if translator_nodedef:
635 # Try to append the version string to make unique. Note that translators
636 # do not support versioning so this will allow creation but not usage.
637 # Thus we skip doing this for now...
638 #derived_name += mx.createValidName(target_nodedef.getVersionString())
639 #translator_nodedef = output_doc.getNodeDef(derived_name)
640 if translator_nodedef:
641 self.logger.warning(f'> Translator NodeDef already exists: {target_nodedef.getName()}')
642 #mx.prettyPrint(translator_nodedef)
643 return translator_nodedef
644
645 translator_nodedef = output_doc.addNodeDef(derived_name)
646 translator_nodedef.removeOutput("out")
647 translator_nodedef.setNodeString(nodename)
648 translator_nodedef.setNodeGroup("translation")
649 translator_nodedef.setDocString(f"Translator from '{source}' to '{target}'")
650
651 version1 = source_nodedef.getVersionString()
652 if not version1:
653 version1 = "1.0"
654 translator_nodedef.setAttribute('source_version', version1)
655 translator_nodedef.setAttribute('source', source)
656 version2 = target_nodedef.getVersionString()
657 if not version2:
658 version2 = "1.0"
659 translator_nodedef.setAttribute('target_version', version2)
660 translator_nodedef.setAttribute('target', target)
661
662 # Add inputs from source as inputs to the translator
663 comment = translator_nodedef.addChildOfCategory("comment")
664 comment.setDocString(f"Inputs (inputs from source '{source}')")
665 for input in source_nodedef.getActiveInputs():
666 #print('add input:', input.getName(), input.getType())
667 nodedef_input = translator_nodedef.addInput(input.getName(), input.getType())
668 if input.hasValueString():
669 nodedef_input.setValueString(input.getValueString())
670
671 # Add inputs from target as outputs to the translator
672 comment = translator_nodedef.addChildOfCategory("comment")
673 comment.setDocString(f"Outputs (inputs from target '{target}' with '_out' suffix)")
674 for input in target_nodedef.getActiveInputs():
675 output_name = input.getName() + "_out"
676 #print('add output:', output_name, input.getType())
677 translator_nodedef.addOutput(output_name, input.getType())
678
679 # 2 Create a new functional nodegraph
680 comment = output_doc.addChildOfCategory("comment")
681 comment.setDocString(f"NodeGraph implementation for translator '{nodename}'")
682 nodegraph_id = 'NG_' + nodename
683 nodegraph : mx.NodeGraph = output_doc.addNodeGraph(nodegraph_id)
684 nodegraph.setNodeDefString(derived_name)
685 nodegraph.setDocString(f"NodeGraph implementation of translator from '{source}' to '{target}'")
686 nodegraph.setAttribute('source_version', version1)
687 nodegraph.setAttribute('source', source)
688 nodegraph.setAttribute('target_version', version2)
689 nodegraph.setAttribute('target', target)
690 for output in translator_nodedef.getActiveOutputs():
691 nodegraph.addOutput(output.getName(), output.getType())
692
693 for source_input_name, target_input_name in mappings.items():
694 source_input = translator_nodedef.getInput(source_input_name)
695 output_name = target_input_name + "_out"
696 target_output = nodegraph.getOutput(output_name)
697 if source_input and target_output:
698 dot_name = nodegraph.createValidChildName(target_input_name)
699 comment = nodegraph.addChildOfCategory("comment")
700 comment.setDocString(f"Routing source input: '{source_input_name}' to target input: '{target_input_name}'")
701 dot_node = nodegraph.addNode('dot', dot_name)
702 dot_inpput = dot_node.addInput('in', source_input.getType())
703 dot_inpput.setInterfaceName(source_input.getName())
704 target_output.setNodeName(dot_node.getName())
705 #print(f" - Added connection from input '{source_input.getName()}' to output '{target_output.getName()}'")
706
707 return translator_nodedef, output_doc
708
709
710 def create_all_translators(self, definitions : mx.Document, output_doc : mx.Document | None = None) -> list[mx.NodeDef]:
711 '''
712 @brief Create translators for all supported shading models.
713 @param definitions The source document containing Physically Based MaterialX definitions.
714 @param output_doc The document to add the translators to. If None, use the source doc.
715 @return A list of created translator definitions.
716 '''
717 trans_nodedefs = []
718
719 # Create temporary doc with all standard library definitions
720 result = self.create_working_document()
721 trans_doc = result["doc"]
722
723 # Add Physically Based Material definitions to the temporary document
724 trans_doc.copyContentFrom(definitions)
725
726 self.add_copyright_comment(output_doc, None)
727
728 if not output_doc:
729 self.logger.error('No output document specified for translators')
730 return trans_nodedef
731
732 # Source BSDF is always physbased_pbr_surface
733 source_bsdf = self.physlib_category
734
735 # Iterate over all target BSDFs
736 bsdfs = self.find_all_bxdf(trans_doc)
737 for bsdf in bsdfs:
738 bsdf_name = bsdf.getNodeString()
739 if bsdf_name == self.physlib_category:
740 continue
741
742 target_bsdf = bsdf.getNodeString()
743 remapping = self.getInputRemapping(target_bsdf)
744 #print('Remapping:', remapping)
745 if len(remapping.items()) > 0:
746 trans_nodedef = self.create_translator(trans_doc,
747 source_bsdf, target_bsdf,
748 "", "",
749 remapping, output_doc)
750 if trans_nodedef:
751 self.logger.info(f'> Created translator to BSDF: {bsdf_name}')
752 trans_nodedefs.append(trans_nodedef)
753
754 return trans_nodedefs
755
756 def create_definition(self, doc : mx.Document | None) -> mx.Document:
757 '''
758 @brief Create a NodeDef for the Physically Based Material inputs
759 @param doc The MaterialX document to add the NodeDef to. If None, a new document will be created.
760 @return A tuple of the MaterialX document and the created definition.
761
762 @details The NodeDef will contain inputs for all the keys in the Physically Based Material JSON object.
763
764 The nodegraph is a placeholder with a simple diffuse shader accepting color as followe:
765 <pre>
766 <nodegraph name="NG_PhysicallyBasedMaterial" nodedef="ND_PhysicallyBasedMaterial">
767 <oren_nayar_diffuse_bsdf name="oren_nayar_diffuse_bsdf" type="BSDF" >
768 <output name="out" type="BSDF" />
769 <input name="color" type="color3" interfacename="color" />
770 <input name="roughness" type="color3" interfacename="roughness" />
771 </oren_nayar_diffuse_bsdf>
772
773 <surface name="surface" type="surfaceshader">
774 <input name="bsdf" type="BSDF" output="out" nodename="oren_nayar_diffuse_bsdf" />
775 </surface>
776
777 <output name="out" type="surfaceshader" nodename="surface"/>
778 </nodegraph>
779 </pre>
780 '''
781 if not doc:
782 doc = mx.createDocument()
783 self.add_copyright_comment(doc, None)
784
785 # Create placeholder nodegraph
786 graph = doc.addNodeGraph(self.get_physlib_implementation_name())
787 graph.setNodeDefString(self.get_physlib_definition_name())
788
789 node = graph.addNode('oren_nayar_diffuse_bsdf', 'oren_nayar_diffuse_bsdf', 'BSDF')
790 node_in = node.addInput('color', 'color3')
791 node_in.setInterfaceName('color')
792 node_in_rough = node.addInput('roughness', 'float')
793 node_in_rough.setInterfaceName('roughness')
794 node.addOutput('out', 'BSDF')
795
796 node = graph.addNode('surface', 'surface', 'surfaceshader')
797 node_in = node.addInput('bsdf', 'BSDF')
798 node_in.setAttribute('out', 'out')
799 node_in.setNodeName('oren_nayar_diffuse_bsdf')
800
801 node_out = graph.addOutput('out', 'surfaceshader')
802 node_out.setNodeName('surface')
803
804 # Create definition template
805 ndef = doc.addNodeDef(self.get_physlib_definition_name(), 'surfaceshader')
806 #graph.removeOutput('out')
807 ndef.setNodeString(self.physlib_category)
808 ndef.setNodeGroup("pbr")
809 ndef.setDocString("Node definitions for PhysicallyBased Material")
810 ndef.setVersionString("1.0")
811 ndef.setAttribute("isdefaultversion", "true")
812
813 for mat in self.materials:
814 # Map keys to definition inputs
815 self.map_keys_to_definition(mat, ndef)
816
817 return doc
818
819
820 def create_definition_materials(self, doc_mat, filter_list = None):
821 '''
822 @brief Create a MaterialX document containing Physically Based MaterialX materials
823 @param doc_mat The MaterialX document to add the materials to
824 @param filter_list A list of material names to filter. If None, all materials will be processed.
825 @return The MaterialX document containing the materials
826 '''
827 definitions = self.get_definitions()
828
829 if not doc_mat:
830 doc_mat = mx.createDocument()
831 self.add_copyright_comment(doc_mat, None)
832
833 # Reference the library definitions into the material document
834 doc_mat.setDataLibrary(definitions)
835
836 for mat in self.materials:
837
838 matName = mat['name']
839 if filter_list and matName not in filter_list:
840 #self.logger.info(f'> Skipping material: {matName}')
841 continue
842 #else:
843 # self.logger.info(f'> Creating material: {matName}')
844
845 shaderName = doc_mat.createValidChildName(matName + '_SHD_PBM')
846 shaderNode = doc_mat.addNode(self.physlib_category, shaderName, mx.SURFACE_SHADER_TYPE_STRING)
847 for key, value in mat.items():
848 if key == 'name':
849 new_name = doc_mat.createValidChildName(str(value))
850 shaderNode.setName(new_name)
851
852 colorspace = ''
853 input_doc = ''
854
855 if key in ['viscosity', 'density', 'thinFilmThickness']:
856 # These are not defined as the same type fo all materials. TBD what to do.
857 continue
858
859 input = shaderNode.addInputFromNodeDef(key)
860 if value is not None:
861
862 if key == 'color':
863 result = self.extract_color(value)
864 color = result[0]
865 colorspace = result[1]
866 value = color
867
868 elif key == 'specularColor':
869 result = self.extract_specular_color(value, self.physlib_category)
870 color = result[0]
871 colorspace = result[1]
872 input_doc = result[2]
873 value = color
874
875 if isinstance(value, list):
876 # TBD what to do if data does not match type....
877 value_list = [str(x) for x in value]
878 value = ', '.join(value_list)
879
880 input.setValueString(str(value))
881
882 if colorspace:
883 input.setColorSpace(colorspace)
884 if input_doc:
885 input.setDocString(input_doc)
886
887 # Add doc string
888 doc_string = ''
889 if key == 'description':
890 doc_string = str(value)
891 if len(doc_string) > 0:
892 shaderNode.setDocString(doc_string)
893
894 shaderNode.setAttribute('uiname', matName)
895
896 # Create a new material
897 materialName = doc_mat.createValidChildName(matName + '_MAT_PBM')
898 materialNode = doc_mat.addNode(mx.SURFACE_MATERIAL_NODE_STRING, materialName, mx.MATERIAL_TYPE_STRING)
899 shaderInput = materialNode.addInput(mx.SURFACE_SHADER_TYPE_STRING, mx.SURFACE_SHADER_TYPE_STRING)
900 shaderInput.setAttribute(self.MTLX_NODE_NAME_ATTRIBUTE, shaderNode.getName())
901
902 return doc_mat
903
904 def find_translator(self, doc : mx.Document, source : str, target : str) -> mx.NodeDef | None:
905 '''
906 @brief Find a translator nodedef from source to target in the document.
907 @param doc The MaterialX document to search.
908 @param source The source definition category.
909 @param target The target definition category.
910 @return The translator nodedef if found, otherwise None.
911 '''
912 derived_name = self.derive_translator_name_from_targets(source, target)
913 # Look for the translator in the document
914 translator_nodedef : mx.NodeDef = doc.getNodeDef(derived_name)
915 return translator_nodedef
916
917 def translate_node(self, doc : mx.Document, source_bxdf : str, target_bxdf : str, node : mx.Node) -> dict | None:
918 '''
919 @brief Translate a shader node of source_bxdf to target_bxdf using ungrouped nodes.
920 @detail This function creates a target node and a translation node based on the translator nodedef, then
921 makes upstream and downstream connections.
922 @param doc The document to operate on.
923 @param source_bxdf The source BXDF shading model name.
924 @param target_bxdf The target BXDF shading model name.
925 @param node The source shader node to translate.
926 @return A dictionary with 'translationNode' and 'targetNode' if successful, None otherwise.
927 '''
928
929 # Look for a translator if one exists.
930 nodedef : mx.NodeDef | None = self.find_translator(doc, source_bxdf, target_bxdf)
931 if not nodedef:
932 print(f"- No translator found from '{source_bxdf}' to '{target_bxdf}' for node '{node.getName()}'")
933 return None
934
935 # Create a target node of the target_bxdf category.
936 #print('> Add target node of category:', target_bxdf)
937 replace_name = node.getName()
938 target_node_name = doc.createValidChildName(f'{replace_name}_{target_bxdf}_SPB');
939
940 # Cleanup dowstream connections
941 downstream_ports = node.getDownstreamPorts()
942 for port in downstream_ports:
943 # print('Scan downstream port:', port.getName(), 'of node:', port.getParent().getName());
944 downstream_node = port.getParent()
945 downstream_input = downstream_node.getInput(port.getName())
946 if downstream_input:
947 #print(` - Reconnecting downstream node '${downstream_node.getName()}' input '${downstream_input.getName()}' from '${node.getName()}' to target node '${target_node_name}'`);
948 downstream_input.setNodeName(target_node_name);
949
950 targetNode = doc.addChildOfCategory(target_bxdf, target_node_name)
951 if not targetNode:
952 print(f"- Failed to create target node of category '{target_bxdf}' for node '{node.getName()}'")
953 return None
954 targetNode.setType("surfaceshader")
955 #targetNodeDef = targetNode.getNodeDef()
956
957 # Create a translation node based on the translator nodedef.
958 translationNode = doc.addNodeInstance(nodedef,
959 targetNode.getName() + "_translator")
960
961 # Copy over inputs from the source node to the translation node.
962 # Note that this will copy over all attributes including upstream connections.
963 #print('> Add translation inputs.')
964 num_overrides = 0
965 for input in node.getActiveInputs():
966 translationInput = translationNode.addInputFromNodeDef(input.getName()) #translationNode.getInput(input.getName())
967 #print('>> Overwrite input:', translationInput.getName())
968 if translationInput:
969 translationInput.copyContentFrom(input)
970 num_overrides += 1
971 #print(f'>> Overwrote {num_overrides} inputs on translation node.')
972
973 # Connect translation outputs to target inputs.
974 impl = nodedef.getImplementation();
975 for output in nodedef.getActiveOutputs():
976
977 # Avoid adding ports which do not route an input data
978 impl_output = impl.getOutput(output.getName())
979 if not impl_output.getConnectedNode():
980 continue
981
982 target_input_name = output.getName()
983 # Remove trailing '_out' from name
984 target_input_name = target_input_name[:-4] if target_input_name.endswith('_out') else target_input_name
985
986 translationOutput = translationNode.addOutput(output.getName(), output.getType())
987 translationOutput.copyContentFrom(output)
988
989 target_input = targetNode.addInputFromNodeDef(target_input_name);
990 if not target_input:
991 print(f" - Warning: Target node '{targetNode.getName()}' has no input named '{target_input_name}' for output '{output.getName()}'")
992 continue
993 else:
994 #print('Target input name:', target_input_name)
995 target_input.setNodeName(translationNode.getName())
996 target_input.setOutputString(translationOutput.getName())
997 target_input.removeAttribute('value')
998
999
1000 # Remove original node
1001 doc.removeNode(node.getName())
1002
1003 return {'translationNode' : translationNode, 'targetNode' : targetNode }
1004
1005 def add_copyright_comment(self, doc, shaderCategory, embedDate=False):
1006 # Add header comments
1007 self.addComment(doc, 'Physically Based Materials from https://api.physicallybased.info ')
1008 self.addComment(doc, ' Content Author: Anton Palmqvist, https://antonpalmqvist.com/ ')
1009 self.addComment(doc, f' Content processsed via REST API and mapped to MaterialX V{self.mx.getVersionString()} ')
1010 if shaderCategory:
1011 self.addComment(doc, f' Target Shading Model: {shaderCategory} ')
1012 self.addComment(doc, ' Utility Author: Bernard Kwok. kwokcb@gmail.com ')
1013 if embedDate:
1014 now = datetime.datetime.now()
1015 dt_string = now.strftime("%Y-%m-%d %H:%M:%S")
1016 self.addComment(doc, f' Generated on: {dt_string} ')
1017
1018 def remap_color_space(self, pb_colorspace) -> str:
1019 '''
1020 @brief Remap a color space name use in Physically Based to the name used in MaterialX
1021 @param pb_colorspace The color space name used in Physically Based
1022 @return The remapped color space name used in MaterialX
1023 '''
1024 map = {}
1025
1026 map['srgb-linear'] = 'lin_rec709'
1027 map['acescg'] = 'acescg'
1028
1029 # Add more as needed
1030 if pb_colorspace in map:
1031 return map[pb_colorspace]
1032 # Return nothing as we don't know how to map which is equivalent to linear.
1033 return ''
1034
1035 def get_color_entry_index(self, color_block):
1036 '''
1037 @brief Find entry where colorSpace == colorspace.
1038 If not found use the first entry
1039 [
1040 {
1041 "colorSpace": "srgb-linear",
1042 "color": []
1043 },
1044 {
1045 "colorSpace": "acescg",
1046 "color": []
1047 }
1048 ]
1049 '''
1050 entry_index = 0
1051 for entry in color_block:
1052 if 'colorSpace' in entry:
1053 #print('test color space:', entry['colorSpace'], 'desired:', self.desired_color_space)
1054 if entry['colorSpace'] == self.desired_color_space:
1055 return entry_index
1056 entry_index += 1
1057
1058 return 0
1059
1060 def extract_color(self, value) -> list:
1061 '''
1062 @brief Extract the color and colorspace from the value list based on the format entry.
1063 @param value The value list to extract the color from
1064 @return A list [color, colorspace]
1065 '''
1066 color_entry_color = self.get_color_entry_index(value)
1067 color = value[color_entry_color]["color"]
1068 colorspace = value[color_entry_color]["colorSpace"] if "colorSpace" in value[color_entry_color] else ''
1069 if colorspace:
1070 colorspace = self.remap_color_space(colorspace)
1071 #value = color
1072 return [color, colorspace]
1073
1074 def extract_specular_color(self, value, shaderCategory) -> list:
1075 '''
1076 @brief Extract the specular color from the value list based on the format entry.
1077 @detail Expected format is:
1078
1079 "specularColor": [
1080 {
1081 "format": [
1082 "Gulbrandsen"
1083 ],
1084 "color": [ // per colorspace list
1085 ]
1086 @param value The value list to extract the specular color from
1087 @return A list [color, colorspace, input_doc].
1088 '''
1089 # Assume specular color uses Gulbrandsen (standard surface, usdpreview, disney). For OpenPBR
1090 # and glTF PBR use the F82. Note: <artistic_ior> MTLX node usage entails using Gulbrandsen
1091 # This could be scanned for but makes it too implementation dependent.
1092 color_entry_specular = "Gulbrandsen"
1093 if shaderCategory in ['open_pbr_surface', 'gltf_pbr']:
1094 color_entry_specular = "F82"
1095
1096 color = None
1097 colorspace = ''
1098 input_doc = ''
1099
1100 if len(value) > 0:
1101 # Scan each value and find the one where "format" == color_entry_specular
1102 for format_entry in value:
1103 if "format" in format_entry and color_entry_specular in format_entry["format"]:
1104 # Pull of "color" entry for the desired colorspace for the format entry
1105 color_entry_color = self.get_color_entry_index(format_entry["color"])
1106 color_entry = format_entry["color"][color_entry_color]
1107
1108 # Get the color and colorspace values
1109 color = color_entry["color"]
1110 colorspace = color_entry["colorSpace"] if "colorSpace" in color_entry else ''
1111 if colorspace:
1112 colorspace = self.remap_color_space(colorspace)
1113 # Add format as doc string for the input
1114 format_string = "Format: "
1115 for format in format_entry["format"]:
1116 format_string += format + " "
1117 input_doc = format_string.strip()
1118
1119 value = color
1120 break
1121
1122 return [color, colorspace, input_doc]
1123
1124
1125 def convertToMaterialX(self, materialNames = [], shaderCategory='standard_surface',
1126 remapKeys = {}, shaderPreFix ='') -> mx.Document:
1127 '''
1128 @brief Convert the Physically Based Materials to MaterialX format for a given target shading model.
1129 @param materialNames The list of material names to convert. If empty, all materials will be converted.
1130 @param shaderCategory The target shading model to convert to. Default is 'standard_surface'.
1131 @param remapKeys The remapping keys for the target shading model. If empty, the default remapping keys will be used.
1132 @param shaderPreFix The prefix to add to the shader name. Default is an empty string.
1133 @return The MaterialX document
1134 '''
1135 if not self.mx:
1136 self.logger.critical(f'> {self._getMethodName()}: MaterialX module is required')
1137 return None
1138
1139 if not self.support_openpbr and shaderCategory == 'open_pbr_surface':
1140 self.logger.warning(f'> OpenPBR shading model not supported in MaterialX version {self.mx.getVersionString()}')
1141 return None
1142
1143 if not self.materials:
1144 self.logger.info('> No materials to convert')
1145 return None
1146
1147 if len(remapKeys) == 0:
1148 remapKeys = self.getInputRemapping(shaderCategory)
1149 #if len(remapKeys) == 0:
1150 # self.logger.warning(f'> No remapping keys found for shading model: {shaderCategory}')
1151
1152 # Create main document and import the library document
1153 self.doc = self.mx.createDocument()
1154 if not self.doc:
1155 return None
1156
1157 self.doc.importLibrary(self.stdlib)
1158
1159 self.add_copyright_comment(self.doc, shaderCategory)
1160
1161 # Add properties to the material
1162 for mat in self.materials:
1163 matName = mat['name']
1164 uiName = matName
1165
1166 # Filter by material name(s)
1167 if len(materialNames) > 0 and matName not in materialNames:
1168 #self.logger.debug('Skip material: ' + matName)
1169 continue
1170
1171 if (len(shaderPreFix) > 0):
1172 matName = matName + '_' + shaderPreFix
1173
1174 shaderName = self.doc.createValidChildName(matName + '_SHD_PBM')
1175 self.addComment(self.doc, ' Generated shader: ' + shaderName + ' ')
1176 shaderNode = self.doc.addNode(shaderCategory, shaderName, self.mx.SURFACE_SHADER_TYPE_STRING)
1177 shaderNode.setAttribute('uiname', uiName)
1178
1179 folderString = ''
1180 docString = ''
1181 if 'category' in mat:
1182 folderString = mat['category'][0]
1183 if 'group' in mat:
1184 if len(folderString) > 0:
1185 folderString += '/'
1186 folderString += mat['group']
1187 if len(folderString) > 0:
1188 shaderNode.setAttribute("uifolder", folderString)
1189
1190 if 'description' in mat:
1191 docString = mat['description']
1192 refString = mat['images'][1]["300"]
1193 if len(refString) > 0:
1194 if len(docString) > 0:
1195 docString += '. '
1196 docString += 'Reference: ' + refString
1197 if len(docString) > 0:
1198 shaderNode.setDocString(docString)
1199
1200 # TODO: Add in option to add all inputs + add nodedef string
1201 #shaderNode.addInputsFromNodeDef()
1202 #shaderNode.setAttribute(self.mx.InterfaceElement.NODE_DEF_ATTRIBUTE, nodedefString)
1203
1204 # Create a new material
1205 materialName = self.doc.createValidChildName(matName + '_MAT_PBM')
1206 self.addComment(self.doc, ' Generated material: ' + materialName + ' ')
1207 materialNode = self.doc.addNode(self.mx.SURFACE_MATERIAL_NODE_STRING, materialName, self.mx.MATERIAL_TYPE_STRING)
1208 shaderInput = materialNode.addInput(self.mx.SURFACE_SHADER_TYPE_STRING, self.mx.SURFACE_SHADER_TYPE_STRING)
1209 shaderInput.setAttribute(self.MTLX_NODE_NAME_ATTRIBUTE, shaderNode.getName())
1210
1211 # Keys to skip.
1212 skipKeys = ['name', "density", "category", "description", "images", "tags", "references"]
1213
1214 metallness = None
1215 roughness = None
1216 color = None
1217 transmission = None
1218
1219 # Used to track "color" input's colorspace for possible
1220 # remapping to transmission color if needed.
1221 transmission_colorspace = ''
1222
1223 for key, value in mat.items():
1224
1225 colorspace = ''
1226 input_doc = ''
1227
1228 if (key not in skipKeys):
1229 # Keep track of these for possible transmission color remapping
1230 if key == 'metalness':
1231 metallness = value
1232 elif key == 'roughness':
1233 roughness = value
1234 elif key == 'transmission':
1235 transmission = value
1236
1237 # Get color based on desired color space
1238 elif key == 'color':
1239 result = self.extract_color(value)
1240 color = result[0]
1241 colorspace = result[1]
1242 value = color
1243 transmission_colorspace = colorspace
1244
1245 # Specular color format is chosen above based on shader category.
1246 # Again we only choose one format and one color space.
1247 #
1248 elif key == 'specularColor':
1249 result = self.extract_specular_color(value, shaderCategory)
1250 color = result[0]
1251 colorspace = result[1]
1252 input_doc = result[2]
1253 value = color
1254
1255 if key in remapKeys:
1256 key = remapKeys[key]
1257 input = shaderNode.addInputFromNodeDef(key)
1258 if input:
1259 # Convert number vector to string
1260 if isinstance(value, list):
1261 value = ', '.join([str(x) for x in value])
1262 # Convert number to string:
1263 elif isinstance(value, (int, float)):
1264 value = str(value)
1265 input.setValueString(value)
1266 if colorspace:
1267 #print('>>> Set color space for key:', key, 'to:', colorspace)
1268 input.setAttribute('colorspace', colorspace)
1269 if len(input_doc) > 0:
1270 input.setDocString(input_doc)
1271 else:
1272 self.logger.debug('Skip unsupported key: ' + key)
1273
1274 # Re-route color to mapped transmission_color if needed
1275 if (transmission != None) and (metallness != None) and (roughness != None) and (color != None):
1276 if (metallness == 0) and (roughness == 0):
1277 if 'transmission_color' in remapKeys:
1278 key = remapKeys['transmission_color']
1279 input = shaderNode.addInputFromNodeDef(key)
1280 if input:
1281 value = ','.join([str(x) for x in color])
1282 input.setValueString(value)
1283 if transmission_colorspace:
1284 input.setAttribute('colorspace', transmission_colorspace)
1285 self.logger.debug(f'Set transmission color {key}: {color}, colorspace: {transmission_colorspace}')
1286
1287 return self.doc
1288
1289 def writeMaterialXToFile(self, filename, doc = None):
1290 '''
1291 @brief Write the MaterialX document to disk
1292 @param filename The filename to write the MaterialX document to
1293 @return None
1294 '''
1295 if not self.mx:
1296 self.logger.critical(f'> {self._getMethodName()}: MaterialX module is required')
1297 return
1298
1299 output_doc = doc if doc else self.doc
1300 if not output_doc:
1301 self.logger.critical(f'> {self._getMethodName()}: No MaterialX document to write')
1302 return
1303
1304 writeOptions = self.mx.XmlWriteOptions()
1305 writeOptions.writeXIncludeEnable = False
1306 writeOptions.elementPredicate = self.skipLibraryElement
1307 self.mx.writeToXmlFile(output_doc, filename, writeOptions)
1308
1310 '''
1311 @brief Convert the MaterialX document to a string
1312 @return The MaterialX document as a string
1313 '''
1314 if not self.mx:
1315 self.logger.critical(f'> {self._getMethodName()}: MaterialX module is required')
1316 return
1317
1318 writeOptions = self.mx.XmlWriteOptions()
1319 writeOptions.writeXIncludeEnable = False
1320 writeOptions.elementPredicate = self.skipLibraryElement
1321 mtlx = self.mx.writeToXmlString(self.doc, writeOptions)
1322 return mtlx
1323
1324 @staticmethod
1325 def create_working_document() -> dict[str, mx.Document]:
1326 doc : mx.Document = mx.createDocument()
1327 stdlib : mx.Document = mx.createDocument()
1328
1329 searchPath : mx.FileSearchPath = mx.getDefaultDataSearchPath()
1330 libraryFolders : list[mx.FilePath]= mx.getDefaultDataLibraryFolders()
1331 libraryFiles : set[str] = mx.loadLibraries(libraryFolders, searchPath, stdlib)
1332 doc.setDataLibrary(stdlib)
1333 nodedefs : list[mx.NodeDef] = doc.getNodeDefs()
1334 #print(f"Created working doc with {len(nodedefs)} nodedefs from standard library.")
1335
1336 result = { "doc": doc, "stdlib": stdlib }
1337 return result
Class to load Physically Based Materials from the PhysicallyBased site.
dict|None translate_node(self, mx.Document doc, str source_bxdf, str target_bxdf, mx.Node node)
Translate a shader node of source_bxdf to target_bxdf using ungrouped nodes.
dict getInputRemapping(self, shadingModel)
Get the remapping keys for a given shading model.
list extract_color(self, value)
Extract the color and colorspace from the value list based on the format entry.
__init__(self, mx_module, Optional[mx.Document] mx_stdlib=None, str materials_file='')
Constructor for the PhysicallyBasedMaterialLoader class.
list getJSONMaterialNames(self)
Get the list of material names from the JSON object.
dict loadMaterialsFromString(self, matString)
Load the Physically Based Materials from a JSON string.
dict loadMaterialsFromFile(self, fileName)
Load the Physically Based Materials from a JSON file.
physlib_materials
Document containing PhysicallyBased materials using PhysicallyBasedMaterial definition.
mx.Document get_physlib(self)
Get the Physically Based MaterialX definition library.
physlib_translators
Document containing PhysicallyBased MaterialX translators.
get_color_entry_index(self, color_block)
Find entry where colorSpace == colorspace.
create_translator(self, mx.Document doc, str source, str target, source_version="", target_version="", mappings=None, mx.Document|None output_doc=None)
Create a translator nodedef and nodegraph from source to target definitions.
mx.NodeDef|None get_physlib_definition(self)
Get the Physically Based MaterialX definition NodeDef.
str get_physlib_definition_name(self)
Get the Physically Based MaterialX definition name.
initialize_definitions_and_materials(self, str materials_file='')
Initialize Physically Based MaterialX definitions, materials, remappings, and translators.
mx.Document create_definition(self, mx.Document|None doc)
Create a NodeDef for the Physically Based Material inputs.
list[mx.NodeDef] create_all_translators(self, mx.Document definitions, mx.Document|None output_doc=None)
Create translators for all supported shading models.
str get_physlib_implementation_name(self)
Get the Physically Based MaterialX implementation (nodegraph) name.
mx.NodeDef|None find_translator(self, mx.Document doc, str source, str target)
Find a translator nodedef from source to target in the document.
mx.Document get_definitions(self)
Get a combined MaterialX document containing the standard library and Physically Based MaterialX defi...
mx.Document get_physlib_materials(self)
Get the Physically Based MaterialX materials document.
writeMaterialXToFile(self, filename, doc=None)
Write the MaterialX document to disk.
list[mx.NodeDef] find_all_bxdf(self, mx.Document doc)
Scan all nodedefs with output type of "surfaceshader" doc : The MaterialX document to scan.
setDebugging(self, debug=True)
Set the debugging level for the logger.
str physlib_implementation_name
PhysicallyBased MaterialX surface implementation (nodegraph) name.
create_definition_materials(self, doc_mat, filter_list=None)
Create a MaterialX document containing Physically Based MaterialX materials.
_getMethodName(self)
Get the name of the calling method for logging purposes.
dict getMaterialsFromURL(self)
Get the Physically Based Materials from the PhysicallyBased site.
initializeInputRemapping(self)
Initialize remapping keys for different shading models.
writeRemappingFile(self, filepath)
Write the remapping keys to a JSON file.
str get_physlib_category(self)
Get the Physically Based MaterialX surface category.
mx.Document get_stdlib(self)
Get the MaterialX standard library document.
dict getJSON(self)
Get the JSON object representing the Physically Based Materials.
all_lib
All MaterialX definitions (standard library + PhysicallyBased definition + translators).
mx.Document get_translators(self)
Get the Physically Based MaterialX translators document.
bool skipLibraryElement(elem)
Utility to skip library elements when iterating over elements in a document.
mx.Document convertToMaterialX(self, materialNames=[], shaderCategory='standard_surface', remapKeys={}, shaderPreFix='')
Convert the Physically Based Materials to MaterialX format for a given target shading model.
list extract_specular_color(self, value, shaderCategory)
Extract the specular color from the value list based on the format entry.
readRemappingFile(self, filepath)
Read the remapping keys from a JSON file.
str remap_color_space(self, pb_colorspace)
Remap a color space name use in Physically Based to the name used in MaterialX.
addComment(self, doc, commentString)
Add a comment to the MaterialX document.
set_desired_color_space(self, str color_space)
Set the desired color space to use for writing colors.
mx.Document physlib
Document containing PhysicallyBased definition library.
str physlib_definition_name
PhysicallyBased MaterialX surface definition name.