2@brief Class to load Physically Based Materials from the PhysicallyBased site.
3and convert the materials to MaterialX format for given target shading models.
7import requests, json, os, inspect
9from http
import HTTPStatus
11from typing
import Optional
12import importlib.resources
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.
21 def __init__(self, mx_module, mx_stdlib : Optional[mx.Document] =
None, materials_file : str =
''):
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.
30 self.
logger = lg.getLogger(
'PBMXLoader')
31 lg.basicConfig(level=lg.INFO)
38 self.
uri =
'https://api.physicallybased.info/materials'
66 self.
remapFile =
'PhysicallyBasedMaterialX/PhysicallyBasedToMtlxMappings.json'
69 self.
logger.critical(f
'> {self._getMethodName()}: MaterialX module not specified.')
73 version_major, version_minor, version_patch = self.
mx.getVersionIntegers()
74 self.
logger.debug(f
'> MaterialX version: {version_major}.{version_minor}.{version_patch}')
75 if (version_major >=1
and version_minor >= 39)
or version_major > 1:
76 self.
logger.debug(
'> OpenPBR shading model supported')
81 self.
stdlib = self.
mx.createDocument()
82 libFiles = self.
mx.loadLibraries(mx.getDefaultDataLibraryFolders(), mx.getDefaultDataSearchPath(), self.
stdlib)
83 self.
logger.debug(f
'> Loaded standard library: {libFiles}')
90 @brief Initialize Physically Based MaterialX definitions, materials, remappings, and translators.
94 if materials_file
and os.path.exists(materials_file):
102 self.
logger.info(
'> Created Physically Based MaterialX definition library...')
103 status, error = self.
physlib.validate()
106 self.
logger.error(
'> Error validating NodeDef document:')
109 self.
logger.info(
'> Definition documents passed validation.')
121 @brief Set the debugging level for the logger.
122 @param debug True to set the logger to debug level, otherwise False.
126 self.
logger.setLevel(lg.DEBUG)
128 self.
logger.setLevel(lg.INFO)
132 @brief Get the MaterialX standard library document.
133 @return The MaterialX standard library document.
139 @brief Get the Physically Based MaterialX definition library.
140 @return The Physically Based MaterialX definition library.
146 @brief Get the Physically Based MaterialX definition NodeDef.
147 @return The Physically Based MaterialX definition NodeDef.
155 @brief Get the Physically Based MaterialX surface category.
156 @return The Physically Based MaterialX surface category.
162 @brief Get the Physically Based MaterialX definition name.
163 @return The Physically Based MaterialX definition name.
169 @brief Get the Physically Based MaterialX implementation (nodegraph) name.
170 @return The Physically Based MaterialX implementation (nodegraph) name.
176 @brief Get the Physically Based MaterialX materials document.
177 @return The Physically Based MaterialX materials document.
183 @brief Get a combined MaterialX document containing the standard library and Physically Based MaterialX definition and translators.
184 @return The combined MaterialX document.
197 @brief Get the Physically Based MaterialX translators document.
198 @return The Physically Based MaterialX translators document.
204 @brief Get the remapping keys for a given shading model.
205 @param shadingModel The shading model to get the remapping keys for.
206 @return A dictionary of remapping keys.
216 @brief Initialize remapping keys for different shading models.
217 See: https://api.physicallybased.info/operations/get-materials
218 for more information on material properties.
220 The JSON file PhysicallyBasedToMtlxMappings.json which is part of the package
221 will be used if it exists. Otherwise, default remapping keys will be used.
223 The currently supported shading models are:
233 with importlib.resources.files(
"materialxMaterials.data").joinpath(self.
remapFile).open(
"r", encoding=
"utf-8")
as json_file:
234 self.
logger.info(f
'> Load remapping from installed package: {self.remapFile}')
235 self.
remapMap = json.load(json_file)
236 except FileNotFoundError:
237 self.
logger.warning(
'> No remapping file found in installed package. Using default remapping keys.')
243 standard_surface_remapKeys = {
244 'color':
'base_color',
245 'specularColor':
'specular_color',
246 'roughness':
'specular_roughness',
247 'metalness':
'metalness',
248 'ior':
'specular_IOR',
249 'subsurfaceRadius':
'subsurface_radius',
250 'transmission':
'transmission',
251 'transmission_color':
'transmission_color',
252 'transmissionDispersion' :
'transmission_dispersion',
253 'thinFilmThickness' :
'thin_film_thickness',
254 'thinFilmIor' :
'thin_film_IOR',
258 openpbr_remapKeys = {
259 'color':
'base_color',
260 'specularColor':
'specular_color',
261 'roughness':
'specular_roughness',
262 'metalness':
'base_metalness',
263 'ior':
'specular_ior',
264 'subsurfaceRadius':
'subsurface_radius',
265 'transmission':
'transmission_weight',
266 'transmission_color':
'transmission_color',
267 'transmissionDispersion':
'transmission_dispersion_abbe_number',
271 'thinFilmThickness' :
'thin_film_thickness',
272 'thinFilmIor' :
'thin_film_ior',
276 'color':
'base_color',
277 'specularColor':
'specular_color',
278 'roughness':
'roughness',
279 'metalness':
'metallic',
281 'transmission':
'transmission',
282 'transmission_color':
'attenuation_color',
283 'thinFilmThickness' :
'iridescence_thickness',
284 'thinFilmIor' :
'iridescence_ior',
288 self.
remapMap[
'standard_surface'] = standard_surface_remapKeys;
289 self.
remapMap[
'gltf_pbr'] = gltf_remapKeys;
291 self.
remapMap[
'open_pbr_surface'] = openpbr_remapKeys;
295 @brief Write the remapping keys to a JSON file.
296 @param filepath The filename to write the remapping keys to.
300 self.
logger.warning(
'No remapping keys to write')
303 with open(filepath,
'w')
as json_file:
304 json.dump(self.
remapMap, json_file, indent=4)
308 @brief Read the remapping keys from a JSON file.
309 @param filepath The filename to read the remapping keys from.
310 @return A dictionary of remapping keys.
312 if not os.path.exists(filepath):
313 self.
logger.error(f
'> File does not exist: {filepath}')
316 with open(filepath,
'r')
as json_file:
317 self.
remapMap = json.load(json_file)
322 ''' Get the JSON object representing the Physically Based Materials '''
327 Get the list of material names from the JSON object
328 @return The list of material names
334 Get the MaterialX document
335 @return The MaterialX document
341 @brief Load the Physically Based Materials from a JSON file
342 @param fileName The filename to load the JSON file from
343 @return The JSON object representing the Physically Based Materials
347 if not os.path.exists(fileName):
348 self.
logger.error(f
'> File does not exist: {fileName}')
351 with open(fileName,
'r')
as json_file:
360 @brief Load the Physically Based Materials from a JSON string
361 @param matString The JSON string to load the Physically Based Materials from
362 @return The JSON object representing the Physically Based Materials
374 @brief Get the Physically Based Materials from the PhysicallyBased site
375 @return The JSON object representing the Physically Based Materials
382 'Accept':
'application/json'
385 response = requests.get(url, headers=headers)
387 if response.status_code == HTTPStatus.OK:
393 self.
logger.error(f
'> Status: {response.status_code}, {response.text}')
399 @brief Print the materials to the console
403 self.
logger.info(
'Material name: ' + mat[
'name'])
405 for key, value
in mat.items():
406 if (key !=
'name' and value):
407 self.
logger.info(f
'> - {key}: {value}')
411 @brief Write the materials to a JSON file
412 @param filename The filename to write the JSON file to
413 @return True if the file was written successfully, otherwise False
416 self.
logger.warning(
'No materials to write')
419 with open(filename,
'w')
as json_file:
420 json.dump(self.
materials, json_file, indent=4)
428 @brief Utility to skip library elements when iterating over elements in a document.
429 @return True if the element is not in a library, otherwise False.
431 return not elem.hasSourceUri()
435 @brief Get the name of the calling method for logging purposes.
436 @return The name of the calling method.
438 frame = inspect.currentframe().f_back
439 method_name = frame.f_code.co_name
445 @brief Validate the MaterialX document
446 @param doc The MaterialX document to validate
447 @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.
450 self.
logger.critical(f
'> {self._getMethodName()}: MaterialX module is required')
454 self.
logger.warning(f
'> {self._getMethodName()}: MaterialX document is required')
457 valid, errors = doc.validate()
462 @brief Add a comment to the MaterialX document
463 @param doc The MaterialX document to add the comment to
464 @param commentString The comment string to add
467 comment = doc.addChildOfCategory(
'comment')
468 comment.setDocString(commentString)
472 @brief Map a key to a NodeDef input
473 @param mat Material to map keys from
474 @param ndef The definition to map the key to
475 @return The input name if the key was mapped, otherwise None
477 for key, value
in mat.items():
479 if ndef.getInput(key)
is None:
487 self.
logger.debug(f
'> Add key as input: {key}')
489 input_type =
"string"
490 if 'color' in key.lower():
491 input_type =
"color3"
493 elif isinstance(value, float):
496 elif isinstance(value, int):
499 elif key ==
'category':
506 elif key
in [
'sources',
'reference',
'tags',
'group']:
509 input = ndef.addInput(key, input_type)
511 if value
is not None:
512 if isinstance(value, list):
514 value_list = [str(x)
for x
in value]
516 is_number_list = all(isinstance(x, (int, float))
for x
in value)
518 if len(value_list) > 4 :
519 input.setType(
'string')
520 elif len(value_list) > 3 :
521 input.setType(
'vector4')
522 elif len(value_list) > 2 :
523 input.setType(
'vector3')
524 elif len(value_list) > 1 :
525 input.setType(
'vector2')
527 input.setType(
'float')
529 value_list = [
'0.0' for x
in value_list]
530 value =
', '.join(value_list)
533 input.setValueString(str(value))
539 uiname =
''.join([
' ' + c
if c.isupper()
else c
for c
in key]).strip().title()
540 input.setAttribute(
"uiname", uiname)
543 if key
in [
'description',
'sources',
'reference',
'tags']:
544 uifolder =
'Metadata'
545 input.setAttribute(
"uifolder", uifolder)
554 if uifolder
is not None:
555 input.setAttribute(
"uifolder", uifolder)
559 @brief Scan all nodedefs with output type of "surfaceshader"
560 doc : The MaterialX document to scan
561 @return A list of nodedefs found
563 bxdfs : list[mx.NodeDef] = []
564 for nodedef
in doc.getNodeDefs():
565 if nodedef.getType() ==
"surfaceshader":
566 if nodedef.getNodeString()
not in [
"convert",
"surface"]
and nodedef.getNodeGroup() ==
"pbr":
567 bxdfs.append(nodedef)
570 def derive_translator_name_from_targets(self, source : str, target : str) -> str:
571 return f
"ND_{source}_to_{target}"
574 source : str, target : str,
575 source_version =
"", target_version =
"",
577 output_doc : mx.Document |
None =
None):
579 @brief Create a translator nodedef and nodegraph from source to target definitions.
580 @param doc The source document containing the definition.
581 @param source The source definition category.
582 @param target The target definition category.
583 @param source_version The source version string. If empty, use the first source definition version found.
584 @param target_version The target version string. If empty, use the first target definition version found.
585 @param mappings A dictionary mapping source input names to target input names.
586 @param output_doc The document to add the translator to. If None, use the source doc.
587 @return The created translator definition.
599 source_nodedef =
None
600 target_nodedef =
None
601 for nodedef
in nodedefs:
602 if nodedef.getNodeString() == source:
603 if source_version ==
"" or nodedef.getVersionString() == source_version:
604 source_nodedef = nodedef
605 if nodedef.getNodeString() == target:
606 if target_version ==
"" or nodedef.getVersionString() == target_version:
607 target_nodedef = nodedef
609 if not source_nodedef
or not target_nodedef:
611 if not source_nodedef:
612 print(f
"Source nodedef not found for '{source}' with version '{source_version}'")
613 if not target_nodedef:
614 print(f
"Target nodedef not found for '{target}' with version '{target_version}'")
622 nodename = derived_name[3:]
if derived_name.startswith(
"ND_")
else derived_name
623 translator_nodedef : mx.NodeDef = output_doc.getNodeDef(derived_name)
624 if translator_nodedef:
630 if translator_nodedef:
631 self.
logger.warning(f
'> Translator NodeDef already exists: {target_nodedef.getName()}')
633 return translator_nodedef
635 translator_nodedef = output_doc.addNodeDef(derived_name)
636 translator_nodedef.removeOutput(
"out")
637 translator_nodedef.setNodeString(nodename)
638 translator_nodedef.setNodeGroup(
"translation")
639 translator_nodedef.setDocString(f
"Translator from '{source}' to '{target}'")
641 version1 = source_nodedef.getVersionString()
644 translator_nodedef.setAttribute(
'source_version', version1)
645 translator_nodedef.setAttribute(
'source', source)
646 version2 = target_nodedef.getVersionString()
649 translator_nodedef.setAttribute(
'target_version', version2)
650 translator_nodedef.setAttribute(
'target', target)
653 comment = translator_nodedef.addChildOfCategory(
"comment")
654 comment.setDocString(f
"Inputs (inputs from source '{source}')")
655 for input
in source_nodedef.getActiveInputs():
657 nodedef_input = translator_nodedef.addInput(input.getName(), input.getType())
658 if input.hasValueString():
659 nodedef_input.setValueString(input.getValueString())
662 comment = translator_nodedef.addChildOfCategory(
"comment")
663 comment.setDocString(f
"Outputs (inputs from target '{target}' with '_out' suffix)")
664 for input
in target_nodedef.getActiveInputs():
665 output_name = input.getName() +
"_out"
667 translator_nodedef.addOutput(output_name, input.getType())
670 comment = output_doc.addChildOfCategory(
"comment")
671 comment.setDocString(f
"NodeGraph implementation for translator '{nodename}'")
672 nodegraph_id =
'NG_' + nodename
673 nodegraph : mx.NodeGraph = output_doc.addNodeGraph(nodegraph_id)
674 nodegraph.setNodeDefString(derived_name)
675 nodegraph.setDocString(f
"NodeGraph implementation of translator from '{source}' to '{target}'")
676 nodegraph.setAttribute(
'source_version', version1)
677 nodegraph.setAttribute(
'source', source)
678 nodegraph.setAttribute(
'target_version', version2)
679 nodegraph.setAttribute(
'target', target)
680 for output
in translator_nodedef.getActiveOutputs():
681 nodegraph.addOutput(output.getName(), output.getType())
683 for source_input_name, target_input_name
in mappings.items():
684 source_input = translator_nodedef.getInput(source_input_name)
685 output_name = target_input_name +
"_out"
686 target_output = nodegraph.getOutput(output_name)
687 if source_input
and target_output:
688 dot_name = nodegraph.createValidChildName(target_input_name)
689 comment = nodegraph.addChildOfCategory(
"comment")
690 comment.setDocString(f
"Routing source input: '{source_input_name}' to target input: '{target_input_name}'")
691 dot_node = nodegraph.addNode(
'dot', dot_name)
692 dot_inpput = dot_node.addInput(
'in', source_input.getType())
693 dot_inpput.setInterfaceName(source_input.getName())
694 target_output.setNodeName(dot_node.getName())
697 return translator_nodedef, output_doc
702 @brief Create translators for all supported shading models.
703 @param definitions The source document containing Physically Based MaterialX definitions.
704 @param output_doc The document to add the translators to. If None, use the source doc.
705 @return A list of created translator definitions.
711 trans_doc = result[
"doc"]
714 trans_doc.copyContentFrom(definitions)
719 self.
logger.error(
'No output document specified for translators')
728 bsdf_name = bsdf.getNodeString()
732 target_bsdf = bsdf.getNodeString()
735 if len(remapping.items()) > 0:
737 source_bsdf, target_bsdf,
739 remapping, output_doc)
741 self.
logger.info(f
'> Created translator to BSDF: {bsdf_name}')
742 trans_nodedefs.append(trans_nodedef)
744 return trans_nodedefs
748 @brief Create a NodeDef for the Physically Based Material inputs
749 @param doc The MaterialX document to add the NodeDef to. If None, a new document will be created.
750 @return A tuple of the MaterialX document and the created definition.
752 @details The NodeDef will contain inputs for all the keys in the Physically Based Material JSON object.
754 The nodegraph is a placeholder with a simple diffuse shader accepting color as followe:
756 <nodegraph name="NG_PhysicallyBasedMaterial" nodedef="ND_PhysicallyBasedMaterial">
757 <oren_nayar_diffuse_bsdf name="oren_nayar_diffuse_bsdf" type="BSDF" >
758 <output name="out" type="BSDF" />
759 <input name="color" type="color3" interfacename="color" />
760 <input name="roughness" type="color3" interfacename="roughness" />
761 </oren_nayar_diffuse_bsdf>
763 <surface name="surface" type="surfaceshader">
764 <input name="bsdf" type="BSDF" output="out" nodename="oren_nayar_diffuse_bsdf" />
767 <output name="out" type="surfaceshader" nodename="surface"/>
772 doc = mx.createDocument()
779 node = graph.addNode(
'oren_nayar_diffuse_bsdf',
'oren_nayar_diffuse_bsdf',
'BSDF')
780 node_in = node.addInput(
'color',
'color3')
781 node_in.setInterfaceName(
'color')
782 node_in_rough = node.addInput(
'roughness',
'float')
783 node_in_rough.setInterfaceName(
'roughness')
784 node.addOutput(
'out',
'BSDF')
786 node = graph.addNode(
'surface',
'surface',
'surfaceshader')
787 node_in = node.addInput(
'bsdf',
'BSDF')
788 node_in.setAttribute(
'out',
'out')
789 node_in.setNodeName(
'oren_nayar_diffuse_bsdf')
791 node_out = graph.addOutput(
'out',
'surfaceshader')
792 node_out.setNodeName(
'surface')
798 ndef.setNodeGroup(
"pbr")
799 ndef.setDocString(
"Node definitions for PhysicallyBased Material")
800 ndef.setVersionString(
"1.0")
801 ndef.setAttribute(
"isdefaultversion",
"true")
812 @brief Create a MaterialX document containing Physically Based MaterialX materials
813 @param doc_mat The MaterialX document to add the materials to
814 @param filter_list A list of material names to filter. If None, all materials will be processed.
815 @return The MaterialX document containing the materials
820 doc_mat = mx.createDocument()
824 doc_mat.setDataLibrary(definitions)
828 matName = mat[
'name']
829 if filter_list
and matName
not in filter_list:
835 shaderName = doc_mat.createValidChildName(matName +
'_SHD_PBM')
836 shaderNode = doc_mat.addNode(self.
physlib_category, shaderName, mx.SURFACE_SHADER_TYPE_STRING)
837 for key, value
in mat.items():
839 new_name = doc_mat.createValidChildName(str(value))
840 shaderNode.setName(new_name)
842 input = shaderNode.addInputFromNodeDef(key)
843 if value
is not None:
844 if isinstance(value, list):
846 value_list = [str(x)
for x
in value]
850 value =
', '.join(value_list)
851 input.setValueString(str(value))
855 if key ==
'description':
856 doc_string = str(value)
857 if len(doc_string) > 0:
858 shaderNode.setDocString(doc_string)
860 shaderNode.setAttribute(
'uiname', matName)
863 materialName = doc_mat.createValidChildName(matName +
'_MAT_PBM')
864 materialNode = doc_mat.addNode(mx.SURFACE_MATERIAL_NODE_STRING, materialName, mx.MATERIAL_TYPE_STRING)
865 shaderInput = materialNode.addInput(mx.SURFACE_SHADER_TYPE_STRING, mx.SURFACE_SHADER_TYPE_STRING)
870 def find_translator(self, doc : mx.Document, source : str, target : str) -> mx.NodeDef |
None:
872 @brief Find a translator nodedef from source to target in the document.
873 @param doc The MaterialX document to search.
874 @param source The source definition category.
875 @param target The target definition category.
876 @return The translator nodedef if found, otherwise None.
880 translator_nodedef : mx.NodeDef = doc.getNodeDef(derived_name)
881 return translator_nodedef
883 def translate_node(self, doc : mx.Document, source_bxdf : str, target_bxdf : str, node : mx.Node) -> dict |
None:
885 @brief Translate a shader node of source_bxdf to target_bxdf using ungrouped nodes.
886 @detail This function creates a target node and a translation node based on the translator nodedef, then
887 makes upstream and downstream connections.
888 @param doc The document to operate on.
889 @param source_bxdf The source BXDF shading model name.
890 @param target_bxdf The target BXDF shading model name.
891 @param node The source shader node to translate.
892 @return A dictionary with 'translationNode' and 'targetNode' if successful, None otherwise.
896 nodedef : mx.NodeDef |
None = self.
find_translator(doc, source_bxdf, target_bxdf)
898 print(f
"- No translator found from '{source_bxdf}' to '{target_bxdf}' for node '{node.getName()}'")
903 replace_name = node.getName()
904 target_node_name = doc.createValidChildName(f
'{replace_name}_{target_bxdf}_SPB');
907 downstream_ports = node.getDownstreamPorts()
908 for port
in downstream_ports:
910 downstream_node = port.getParent()
911 downstream_input = downstream_node.getInput(port.getName())
914 downstream_input.setNodeName(target_node_name);
916 targetNode = doc.addChildOfCategory(target_bxdf, target_node_name)
918 print(f
"- Failed to create target node of category '{target_bxdf}' for node '{node.getName()}'")
920 targetNode.setType(
"surfaceshader")
924 translationNode = doc.addNodeInstance(nodedef,
925 targetNode.getName() +
"_translator")
931 for input
in node.getActiveInputs():
932 translationInput = translationNode.addInputFromNodeDef(input.getName())
935 translationInput.copyContentFrom(input)
940 impl = nodedef.getImplementation();
941 for output
in nodedef.getActiveOutputs():
944 impl_output = impl.getOutput(output.getName())
945 if not impl_output.getConnectedNode():
948 target_input_name = output.getName()
950 target_input_name = target_input_name[:-4]
if target_input_name.endswith(
'_out')
else target_input_name
952 translationOutput = translationNode.addOutput(output.getName(), output.getType())
953 translationOutput.copyContentFrom(output)
955 target_input = targetNode.addInputFromNodeDef(target_input_name);
957 print(f
" - Warning: Target node '{targetNode.getName()}' has no input named '{target_input_name}' for output '{output.getName()}'")
961 target_input.setNodeName(translationNode.getName())
962 target_input.setOutputString(translationOutput.getName())
963 target_input.removeAttribute(
'value')
967 doc.removeNode(node.getName())
969 return {
'translationNode' : translationNode,
'targetNode' : targetNode }
971 def add_copyright_comment(self, doc, shaderCategory, embedDate=False):
973 self.addComment(doc,
'Physically Based Materials from https://api.physicallybased.info ')
974 self.addComment(doc,
' Content Author: Anton Palmqvist, https://antonpalmqvist.com/ ')
975 self.addComment(doc, f
' Content processsed via REST API and mapped to MaterialX V{self.mx.getVersionString()} ')
977 self.addComment(doc, f
' Target Shading Model: {shaderCategory} ')
978 self.addComment(doc,
' Utility Author: Bernard Kwok. kwokcb@gmail.com ')
980 now = datetime.datetime.now()
981 dt_string = now.strftime(
"%Y-%m-%d %H:%M:%S")
982 self.addComment(doc, f
' Generated on: {dt_string} ')
985 remapKeys = {}, shaderPreFix ='') -> mx.Document:
987 @brief Convert the Physically Based Materials to MaterialX format for a given target shading model.
988 @param materialNames The list of material names to convert. If empty, all materials will be converted.
989 @param shaderCategory The target shading model to convert to. Default is 'standard_surface'.
990 @param remapKeys The remapping keys for the target shading model. If empty, the default remapping keys will be used.
991 @param shaderPreFix The prefix to add to the shader name. Default is an empty string.
992 @return The MaterialX document
995 self.
logger.critical(f
'> {self._getMethodName()}: MaterialX module is required')
999 self.
logger.warning(f
'> OpenPBR shading model not supported in MaterialX version {self.mx.getVersionString()}')
1003 self.
logger.info(
'> No materials to convert')
1006 if len(remapKeys) == 0:
1012 self.
doc = self.
mx.createDocument()
1022 matName = mat[
'name']
1026 if len(materialNames) > 0
and matName
not in materialNames:
1030 if (len(shaderPreFix) > 0):
1031 matName = matName +
'_' + shaderPreFix
1033 shaderName = self.
doc.createValidChildName(matName +
'_SHD_PBM')
1034 self.
addComment(self.
doc,
' Generated shader: ' + shaderName +
' ')
1035 shaderNode = self.
doc.addNode(shaderCategory, shaderName, self.
mx.SURFACE_SHADER_TYPE_STRING)
1036 shaderNode.setAttribute(
'uiname', uiName)
1039 if 'category' in mat:
1040 folderString = mat[
'category'][0]
1042 if len(folderString) > 0:
1044 folderString += mat[
'group']
1045 if len(folderString) > 0:
1046 shaderNode.setAttribute(
"uifolder", folderString)
1048 docString = mat[
'description']
1049 refString = mat[
'reference']
1050 if len(refString) > 0:
1051 if len(docString) > 0:
1053 docString +=
'Reference: ' + refString[0]
1054 if len(docString) > 0:
1055 shaderNode.setDocString(docString)
1062 materialName = self.
doc.createValidChildName(matName +
'_MAT_PBM')
1063 self.
addComment(self.
doc,
' Generated material: ' + materialName +
' ')
1064 materialNode = self.
doc.addNode(self.
mx.SURFACE_MATERIAL_NODE_STRING, materialName, self.
mx.MATERIAL_TYPE_STRING)
1065 shaderInput = materialNode.addInput(self.
mx.SURFACE_SHADER_TYPE_STRING, self.
mx.SURFACE_SHADER_TYPE_STRING)
1069 skipKeys = [
'name',
"density",
"category",
"description",
"sources",
"tags",
"reference"]
1075 for key, value
in mat.items():
1077 if (key
not in skipKeys):
1079 if key ==
'metalness':
1081 if key ==
'roughness':
1083 if key ==
'transmission':
1084 transmission = value
1088 if key
in remapKeys:
1089 key = remapKeys[key]
1090 input = shaderNode.addInputFromNodeDef(key)
1093 if isinstance(value, list):
1094 value =
','.join([str(x)
for x
in value])
1096 elif isinstance(value, (int, float)):
1098 input.setValueString(value)
1100 self.
logger.debug(
'Skip unsupported key: ' + key)
1103 if (transmission !=
None)
and (metallness !=
None)
and (roughness !=
None)
and (color !=
None):
1104 if (metallness == 0)
and (roughness == 0):
1105 if 'transmission_color' in remapKeys:
1106 key = remapKeys[
'transmission_color']
1107 input = shaderNode.addInputFromNodeDef(key)
1109 self.
logger.debug(f
'Set transmission color {key}: {color}')
1110 value =
','.join([str(x)
for x
in color])
1111 input.setValueString(value)
1117 @brief Write the MaterialX document to disk
1118 @param filename The filename to write the MaterialX document to
1122 self.
logger.critical(f
'> {self._getMethodName()}: MaterialX module is required')
1125 output_doc = doc
if doc
else self.
doc
1127 self.
logger.critical(f
'> {self._getMethodName()}: No MaterialX document to write')
1130 writeOptions = self.
mx.XmlWriteOptions()
1131 writeOptions.writeXIncludeEnable =
False
1133 self.
mx.writeToXmlFile(output_doc, filename, writeOptions)
1137 @brief Convert the MaterialX document to a string
1138 @return The MaterialX document as a string
1141 self.
logger.critical(f
'> {self._getMethodName()}: MaterialX module is required')
1144 writeOptions = self.
mx.XmlWriteOptions()
1145 writeOptions.writeXIncludeEnable =
False
1147 mtlx = self.
mx.writeToXmlString(self.
doc, writeOptions)
1151 def create_working_document() -> dict[str, mx.Document]:
1152 doc : mx.Document = mx.createDocument()
1153 stdlib : mx.Document = mx.createDocument()
1155 searchPath : mx.FileSearchPath = mx.getDefaultDataSearchPath()
1156 libraryFolders : list[mx.FilePath]= mx.getDefaultDataLibraryFolders()
1157 libraryFiles : set[str] = mx.loadLibraries(libraryFolders, searchPath, stdlib)
1158 doc.setDataLibrary(stdlib)
1159 nodedefs : list[mx.NodeDef] = doc.getNodeDefs()
1162 result = {
"doc": doc,
"stdlib": stdlib }
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.
__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.
doc
MaterialX document used for conversion.
dict loadMaterialsFromFile(self, fileName)
Load the Physically Based Materials from a JSON file.
str uri
Root URI for the PhysicallyBased site.
dict[str, mx.Document] create_working_document()
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.
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.
bool support_openpbr
OpenPBR support flag.
map_keys_to_definition(self, mat, ndef)
Map a key to a NodeDef input.
mx.NodeDef|None get_physlib_definition(self)
Get the Physically Based MaterialX definition NodeDef.
dict materials
Materials list.
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.
validateMaterialXDocument(self, doc)
Validate the MaterialX document.
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...
add_copyright_comment(self, doc, shaderCategory, embedDate=False)
mx.Document get_physlib_materials(self)
Get the Physically Based MaterialX materials document.
dict remapMap
Remapping keys for different shading models.
writeMaterialXToFile(self, filename, doc=None)
Write the MaterialX document to disk.
convertToMaterialXString(self)
Convert the MaterialX document to a string.
str derive_translator_name_from_targets(self, str source, str target)
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.
writeJSONToFile(self, filename)
Write the materials to a JSON file.
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.
stdlib
MaterialX standard library.
str MTLX_NODE_NAME_ATTRIBUTE
MaterialX node name attribute.
dict getJSON(self)
Get the JSON object representing the Physically Based Materials.
mx.Document getMaterialXDocument(self)
Get the MaterialX document.
printMaterials(self)
Print the materials to the console.
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.
readRemappingFile(self, filepath)
Read the remapping keys from a JSON file.
str remapFile
Default remapping file (part of installed package).
addComment(self, doc, commentString)
Add a comment to the MaterialX document.
mx.Document physlib
Document containing PhysicallyBased definition library.
str physlib_definition_name
PhysicallyBased MaterialX surface definition name.
str physlib_category
PhysicallyBased MaterialX surface category.
list materialNames
Material names.