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/v2/materials'
66 self.
remapFile =
'PhysicallyBasedMaterialX/PhysicallyBasedToMtlxMappings.json'
71 self.
logger.critical(f
'> {self._getMethodName()}: MaterialX module not specified.')
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')
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}')
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.
100 @brief Initialize Physically Based MaterialX definitions, materials, remappings, and translators.
104 if materials_file
and os.path.exists(materials_file):
112 self.
logger.info(
'> Created Physically Based MaterialX definition library...')
113 status, error = self.
physlib.validate()
116 self.
logger.error(
'> Error validating NodeDef document:')
119 self.
logger.info(
'> Definition documents passed validation.')
131 @brief Set the debugging level for the logger.
132 @param debug True to set the logger to debug level, otherwise False.
136 self.
logger.setLevel(lg.DEBUG)
138 self.
logger.setLevel(lg.INFO)
142 @brief Get the MaterialX standard library document.
143 @return The MaterialX standard library document.
149 @brief Get the Physically Based MaterialX definition library.
150 @return The Physically Based MaterialX definition library.
156 @brief Get the Physically Based MaterialX definition NodeDef.
157 @return The Physically Based MaterialX definition NodeDef.
165 @brief Get the Physically Based MaterialX surface category.
166 @return The Physically Based MaterialX surface category.
172 @brief Get the Physically Based MaterialX definition name.
173 @return The Physically Based MaterialX definition name.
179 @brief Get the Physically Based MaterialX implementation (nodegraph) name.
180 @return The Physically Based MaterialX implementation (nodegraph) name.
186 @brief Get the Physically Based MaterialX materials document.
187 @return The Physically Based MaterialX materials document.
193 @brief Get a combined MaterialX document containing the standard library and Physically Based MaterialX definition and translators.
194 @return The combined MaterialX document.
207 @brief Get the Physically Based MaterialX translators document.
208 @return The Physically Based MaterialX translators document.
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.
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.
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.
233 The currently supported shading models are:
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.')
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',
262 'transmissionDispersion' :
'transmission_dispersion',
263 'thinFilmThickness' :
'thin_film_thickness',
264 'thinFilmIor' :
'thin_film_IOR',
268 openpbr_remapKeys = {
269 'color':
'base_color',
270 'specularColor':
'specular_color',
271 'roughness':
'specular_roughness',
272 'metalness':
'base_metalness',
273 'ior':
'specular_ior',
274 'subsurfaceRadius':
'subsurface_radius',
275 'transmission':
'transmission_weight',
276 'transmission_color':
'transmission_color',
277 'transmissionDispersion':
'transmission_dispersion_abbe_number',
281 'thinFilmThickness' :
'thin_film_thickness',
282 'thinFilmIor' :
'thin_film_ior',
286 'color':
'base_color',
287 'specularColor':
'specular_color',
288 'roughness':
'roughness',
289 'metalness':
'metallic',
291 'transmission':
'transmission',
292 'transmission_color':
'attenuation_color',
293 'thinFilmThickness' :
'iridescence_thickness',
294 'thinFilmIor' :
'iridescence_ior',
298 self.
remapMap[
'standard_surface'] = standard_surface_remapKeys;
299 self.
remapMap[
'gltf_pbr'] = gltf_remapKeys;
301 self.
remapMap[
'open_pbr_surface'] = openpbr_remapKeys;
305 @brief Write the remapping keys to a JSON file.
306 @param filepath The filename to write the remapping keys to.
310 self.
logger.warning(
'No remapping keys to write')
313 with open(filepath,
'w')
as json_file:
314 json.dump(self.
remapMap, json_file, indent=4)
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.
322 if not os.path.exists(filepath):
323 self.
logger.error(f
'> File does not exist: {filepath}')
326 with open(filepath,
'r')
as json_file:
327 self.
remapMap = json.load(json_file)
332 ''' Get the JSON object representing the Physically Based Materials '''
337 Get the list of material names from the JSON object
338 @return The list of material names
344 Get the MaterialX document
345 @return The MaterialX document
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
357 if not os.path.exists(fileName):
358 self.
logger.error(f
'> File does not exist: {fileName}')
361 with open(fileName,
'r')
as json_file:
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
384 @brief Get the Physically Based Materials from the PhysicallyBased site
385 @return The JSON object representing the Physically Based Materials
392 'Accept':
'application/json'
395 response = requests.get(url, headers=headers)
397 if response.status_code == HTTPStatus.OK:
403 self.
logger.error(f
'> Status: {response.status_code}, {response.text}')
409 @brief Print the materials to the console
413 self.
logger.info(
'Material name: ' + mat[
'name'])
415 for key, value
in mat.items():
416 if (key !=
'name' and value):
417 self.
logger.info(f
'> - {key}: {value}')
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
426 self.
logger.warning(
'No materials to write')
429 with open(filename,
'w')
as json_file:
430 json.dump(self.
materials, json_file, indent=4)
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.
441 return not elem.hasSourceUri()
445 @brief Get the name of the calling method for logging purposes.
446 @return The name of the calling method.
448 frame = inspect.currentframe().f_back
449 method_name = frame.f_code.co_name
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.
460 self.
logger.critical(f
'> {self._getMethodName()}: MaterialX module is required')
464 self.
logger.warning(f
'> {self._getMethodName()}: MaterialX document is required')
467 valid, errors = doc.validate()
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
477 comment = doc.addChildOfCategory(
'comment')
478 comment.setDocString(commentString)
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
487 for key, value
in mat.items():
489 if ndef.getInput(key)
is None:
497 self.
logger.debug(f
'> Add key as input: {key}')
499 input_type =
"string"
500 if 'color' in key.lower():
501 input_type =
"color3"
503 elif isinstance(value, float):
506 elif isinstance(value, int):
509 elif key ==
'category':
516 elif key
in [
'images',
'references',
'tags',
'group']:
519 input = ndef.addInput(key, input_type)
521 if value
is not None:
522 if isinstance(value, list):
524 value_list = [str(x)
for x
in value]
526 is_number_list = all(isinstance(x, (int, float))
for x
in value)
528 if len(value_list) > 4 :
529 input.setType(
'string')
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')
537 input.setType(
'float')
539 value_list = [
'0.0' for x
in value_list]
540 value =
', '.join(value_list)
543 input.setValueString(str(value))
549 uiname =
''.join([
' ' + c
if c.isupper()
else c
for c
in key]).strip().title()
550 input.setAttribute(
"uiname", uiname)
553 if key
in [
'description',
'images',
'references',
'tags']:
554 uifolder =
'Metadata'
555 input.setAttribute(
"uifolder", uifolder)
564 if uifolder
is not None:
565 input.setAttribute(
"uifolder", uifolder)
569 @brief Scan all nodedefs with output type of "surfaceshader"
570 doc : The MaterialX document to scan
571 @return A list of nodedefs found
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)
580 def derive_translator_name_from_targets(self, source : str, target : str) -> str:
581 return f
"ND_{source}_to_{target}"
584 source : str, target : str,
585 source_version =
"", target_version =
"",
587 output_doc : mx.Document |
None =
None):
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.
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
619 if not source_nodedef
or not target_nodedef:
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}'")
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:
640 if translator_nodedef:
641 self.
logger.warning(f
'> Translator NodeDef already exists: {target_nodedef.getName()}')
643 return translator_nodedef
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}'")
651 version1 = source_nodedef.getVersionString()
654 translator_nodedef.setAttribute(
'source_version', version1)
655 translator_nodedef.setAttribute(
'source', source)
656 version2 = target_nodedef.getVersionString()
659 translator_nodedef.setAttribute(
'target_version', version2)
660 translator_nodedef.setAttribute(
'target', target)
663 comment = translator_nodedef.addChildOfCategory(
"comment")
664 comment.setDocString(f
"Inputs (inputs from source '{source}')")
665 for input
in source_nodedef.getActiveInputs():
667 nodedef_input = translator_nodedef.addInput(input.getName(), input.getType())
668 if input.hasValueString():
669 nodedef_input.setValueString(input.getValueString())
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"
677 translator_nodedef.addOutput(output_name, input.getType())
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())
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())
707 return translator_nodedef, output_doc
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.
721 trans_doc = result[
"doc"]
724 trans_doc.copyContentFrom(definitions)
729 self.
logger.error(
'No output document specified for translators')
738 bsdf_name = bsdf.getNodeString()
742 target_bsdf = bsdf.getNodeString()
745 if len(remapping.items()) > 0:
747 source_bsdf, target_bsdf,
749 remapping, output_doc)
751 self.
logger.info(f
'> Created translator to BSDF: {bsdf_name}')
752 trans_nodedefs.append(trans_nodedef)
754 return trans_nodedefs
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.
762 @details The NodeDef will contain inputs for all the keys in the Physically Based Material JSON object.
764 The nodegraph is a placeholder with a simple diffuse shader accepting color as followe:
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>
773 <surface name="surface" type="surfaceshader">
774 <input name="bsdf" type="BSDF" output="out" nodename="oren_nayar_diffuse_bsdf" />
777 <output name="out" type="surfaceshader" nodename="surface"/>
782 doc = mx.createDocument()
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')
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')
801 node_out = graph.addOutput(
'out',
'surfaceshader')
802 node_out.setNodeName(
'surface')
808 ndef.setNodeGroup(
"pbr")
809 ndef.setDocString(
"Node definitions for PhysicallyBased Material")
810 ndef.setVersionString(
"1.0")
811 ndef.setAttribute(
"isdefaultversion",
"true")
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
830 doc_mat = mx.createDocument()
834 doc_mat.setDataLibrary(definitions)
838 matName = mat[
'name']
839 if filter_list
and matName
not in filter_list:
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():
849 new_name = doc_mat.createValidChildName(str(value))
850 shaderNode.setName(new_name)
855 if key
in [
'viscosity',
'density',
'thinFilmThickness']:
859 input = shaderNode.addInputFromNodeDef(key)
860 if value
is not None:
865 colorspace = result[1]
868 elif key ==
'specularColor':
871 colorspace = result[1]
872 input_doc = result[2]
875 if isinstance(value, list):
877 value_list = [str(x)
for x
in value]
878 value =
', '.join(value_list)
880 input.setValueString(str(value))
883 input.setColorSpace(colorspace)
885 input.setDocString(input_doc)
889 if key ==
'description':
890 doc_string = str(value)
891 if len(doc_string) > 0:
892 shaderNode.setDocString(doc_string)
894 shaderNode.setAttribute(
'uiname', matName)
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)
904 def find_translator(self, doc : mx.Document, source : str, target : str) -> mx.NodeDef |
None:
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.
914 translator_nodedef : mx.NodeDef = doc.getNodeDef(derived_name)
915 return translator_nodedef
917 def translate_node(self, doc : mx.Document, source_bxdf : str, target_bxdf : str, node : mx.Node) -> dict |
None:
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.
930 nodedef : mx.NodeDef |
None = self.
find_translator(doc, source_bxdf, target_bxdf)
932 print(f
"- No translator found from '{source_bxdf}' to '{target_bxdf}' for node '{node.getName()}'")
937 replace_name = node.getName()
938 target_node_name = doc.createValidChildName(f
'{replace_name}_{target_bxdf}_SPB');
941 downstream_ports = node.getDownstreamPorts()
942 for port
in downstream_ports:
944 downstream_node = port.getParent()
945 downstream_input = downstream_node.getInput(port.getName())
948 downstream_input.setNodeName(target_node_name);
950 targetNode = doc.addChildOfCategory(target_bxdf, target_node_name)
952 print(f
"- Failed to create target node of category '{target_bxdf}' for node '{node.getName()}'")
954 targetNode.setType(
"surfaceshader")
958 translationNode = doc.addNodeInstance(nodedef,
959 targetNode.getName() +
"_translator")
965 for input
in node.getActiveInputs():
966 translationInput = translationNode.addInputFromNodeDef(input.getName())
969 translationInput.copyContentFrom(input)
974 impl = nodedef.getImplementation();
975 for output
in nodedef.getActiveOutputs():
978 impl_output = impl.getOutput(output.getName())
979 if not impl_output.getConnectedNode():
982 target_input_name = output.getName()
984 target_input_name = target_input_name[:-4]
if target_input_name.endswith(
'_out')
else target_input_name
986 translationOutput = translationNode.addOutput(output.getName(), output.getType())
987 translationOutput.copyContentFrom(output)
989 target_input = targetNode.addInputFromNodeDef(target_input_name);
991 print(f
" - Warning: Target node '{targetNode.getName()}' has no input named '{target_input_name}' for output '{output.getName()}'")
995 target_input.setNodeName(translationNode.getName())
996 target_input.setOutputString(translationOutput.getName())
997 target_input.removeAttribute(
'value')
1001 doc.removeNode(node.getName())
1003 return {
'translationNode' : translationNode,
'targetNode' : targetNode }
1005 def add_copyright_comment(self, doc, shaderCategory, embedDate=False):
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()} ')
1011 self.addComment(doc, f
' Target Shading Model: {shaderCategory} ')
1012 self.addComment(doc,
' Utility Author: Bernard Kwok. kwokcb@gmail.com ')
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} ')
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
1026 map[
'srgb-linear'] =
'lin_rec709'
1027 map[
'acescg'] =
'acescg'
1030 if pb_colorspace
in map:
1031 return map[pb_colorspace]
1037 @brief Find entry where colorSpace == colorspace.
1038 If not found use the first entry
1041 "colorSpace": "srgb-linear",
1045 "colorSpace": "acescg",
1051 for entry
in color_block:
1052 if 'colorSpace' in entry:
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]
1067 color = value[color_entry_color][
"color"]
1068 colorspace = value[color_entry_color][
"colorSpace"]
if "colorSpace" in value[color_entry_color]
else ''
1072 return [color, colorspace]
1076 @brief Extract the specular color from the value list based on the format entry.
1077 @detail Expected format is:
1084 "color": [ // per colorspace list
1086 @param value The value list to extract the specular color from
1087 @return A list [color, colorspace, input_doc].
1092 color_entry_specular =
"Gulbrandsen"
1093 if shaderCategory
in [
'open_pbr_surface',
'gltf_pbr']:
1094 color_entry_specular =
"F82"
1102 for format_entry
in value:
1103 if "format" in format_entry
and color_entry_specular
in format_entry[
"format"]:
1106 color_entry = format_entry[
"color"][color_entry_color]
1109 color = color_entry[
"color"]
1110 colorspace = color_entry[
"colorSpace"]
if "colorSpace" in color_entry
else ''
1114 format_string =
"Format: "
1115 for format
in format_entry[
"format"]:
1116 format_string += format +
" "
1117 input_doc = format_string.strip()
1122 return [color, colorspace, input_doc]
1126 remapKeys = {}, shaderPreFix ='') -> mx.Document:
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
1136 self.
logger.critical(f
'> {self._getMethodName()}: MaterialX module is required')
1140 self.
logger.warning(f
'> OpenPBR shading model not supported in MaterialX version {self.mx.getVersionString()}')
1144 self.
logger.info(
'> No materials to convert')
1147 if len(remapKeys) == 0:
1153 self.
doc = self.
mx.createDocument()
1163 matName = mat[
'name']
1167 if len(materialNames) > 0
and matName
not in materialNames:
1171 if (len(shaderPreFix) > 0):
1172 matName = matName +
'_' + shaderPreFix
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)
1181 if 'category' in mat:
1182 folderString = mat[
'category'][0]
1184 if len(folderString) > 0:
1186 folderString += mat[
'group']
1187 if len(folderString) > 0:
1188 shaderNode.setAttribute(
"uifolder", folderString)
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:
1196 docString +=
'Reference: ' + refString
1197 if len(docString) > 0:
1198 shaderNode.setDocString(docString)
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)
1212 skipKeys = [
'name',
"density",
"category",
"description",
"images",
"tags",
"references"]
1221 transmission_colorspace =
''
1223 for key, value
in mat.items():
1228 if (key
not in skipKeys):
1230 if key ==
'metalness':
1232 elif key ==
'roughness':
1234 elif key ==
'transmission':
1235 transmission = value
1238 elif key ==
'color':
1241 colorspace = result[1]
1243 transmission_colorspace = colorspace
1248 elif key ==
'specularColor':
1251 colorspace = result[1]
1252 input_doc = result[2]
1255 if key
in remapKeys:
1256 key = remapKeys[key]
1257 input = shaderNode.addInputFromNodeDef(key)
1260 if isinstance(value, list):
1261 value =
', '.join([str(x)
for x
in value])
1263 elif isinstance(value, (int, float)):
1265 input.setValueString(value)
1268 input.setAttribute(
'colorspace', colorspace)
1269 if len(input_doc) > 0:
1270 input.setDocString(input_doc)
1272 self.
logger.debug(
'Skip unsupported key: ' + key)
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)
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}')
1291 @brief Write the MaterialX document to disk
1292 @param filename The filename to write the MaterialX document to
1296 self.
logger.critical(f
'> {self._getMethodName()}: MaterialX module is required')
1299 output_doc = doc
if doc
else self.
doc
1301 self.
logger.critical(f
'> {self._getMethodName()}: No MaterialX document to write')
1304 writeOptions = self.
mx.XmlWriteOptions()
1305 writeOptions.writeXIncludeEnable =
False
1307 self.
mx.writeToXmlFile(output_doc, filename, writeOptions)
1311 @brief Convert the MaterialX document to a string
1312 @return The MaterialX document as a string
1315 self.
logger.critical(f
'> {self._getMethodName()}: MaterialX module is required')
1318 writeOptions = self.
mx.XmlWriteOptions()
1319 writeOptions.writeXIncludeEnable =
False
1321 mtlx = self.
mx.writeToXmlString(self.
doc, writeOptions)
1325 def create_working_document() -> dict[str, mx.Document]:
1326 doc : mx.Document = mx.createDocument()
1327 stdlib : mx.Document = mx.createDocument()
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()
1336 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.
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.
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()
str desired_color_space
Color space to use for writing colors.
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.
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.
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 remapFile
Default remapping file (part of installed package).
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.
str physlib_category
PhysicallyBased MaterialX surface category.
list materialNames
Material names.