2@brief Class to load Physically Based Materials from the PhysicallyBased site.
3and convert the materials to MaterialX format for given target shading models.
6import requests, json, os, inspect
8from http
import HTTPStatus
10from typing
import Optional
14 @brief Class to load Physically Based Materials from the PhysicallyBased site.
15 The class can convert the materials to MaterialX format for given target shading models.
17 def __init__(self, mx_module, mx_stdlib : Optional[mx.Document] =
None):
19 @brief Constructor for the PhysicallyBasedMaterialLoader class.
20 Will initialize shader mappings and load the MaterialX standard library
21 if it is not passed in as an argument.
22 @param mx_module The MaterialX module. Required.
23 @param mx_stdlib The MaterialX standard library. Optional.
26 self.
logger = lg.getLogger(
'PBMXLoader')
27 lg.basicConfig(level=lg.INFO)
34 self.
uri =
'https://api.physicallybased.info/materials'
47 self.
logger.critical(f
'> {self._getMethodName()}: MaterialX module not specified.')
51 version_major, version_minor, version_patch = self.
mxmx.getVersionIntegers()
52 self.
logger.debug(f
'> MaterialX version: {version_major}.{version_minor}.{version_patch}')
53 if (version_major >=1
and version_minor >= 39)
or version_major > 1:
54 self.
logger.debug(
'> OpenPBR shading model supported')
62 libFiles = self.
mxmx.loadLibraries(mx.getDefaultDataLibraryFolders(), mx.getDefaultDataSearchPath(), self.
stdlib)
63 self.
logger.debug(f
'> Loaded standard library: {libFiles}')
67 @brief Set the debugging level for the logger.
68 @param debug True to set the logger to debug level, otherwise False.
72 self.
logger.setLevel(lg.DEBUG)
74 self.
logger.setLevel(lg.INFO)
78 @brief Get the remapping keys for a given shading model.
79 @param shadingModel The shading model to get the remapping keys for.
80 @return A dictionary of remapping keys.
85 self.
logger.warn(f
'> No remapping keys found for shading model: {shadingModel}')
90 @brief Initialize remapping keys for different shading models.
91 See: https://api.physicallybased.info/operations/get-materials
92 for more information on material properties.
94 The currently supported shading models are:
101 standard_surface_remapKeys = {
102 'color':
'base_color',
103 'specularColor':
'specular_color',
104 'roughness':
'specular_roughness',
105 'metalness':
'metalness',
106 'ior':
'specular_IOR',
107 'subsurfaceRadius':
'subsurface_radius',
108 'transmission':
'transmission',
109 'transmission_color':
'transmission_color',
110 'transmissionDispersion' :
'transmission_dispersion',
111 'thinFilmThickness' :
'thin_film_thickness',
112 'thinFilmIor' :
'thin_film_IOR',
116 openpbr_remapKeys = {
117 'color':
'base_color',
118 'specularColor':
'specular_color',
119 'roughness':
'specular_roughness',
120 'metalness':
'base_metalness',
121 'ior':
'specular_ior',
122 'subsurfaceRadius':
'subsurface_radius',
123 'transmission':
'transmission_weight',
124 'transmission_color':
'transmission_color',
125 'transmissionDispersion':
'transmission_dispersion_abbe_number',
129 'thinFilmThickness' :
'thin_film_thickness',
130 'thinFilmIor' :
'thin_film_ior',
134 'color':
'base_color',
135 'specularColor':
'specular_color',
136 'roughness':
'roughness',
137 'metalness':
'metallic',
139 'transmission':
'transmission',
140 'transmission_color':
'attenuation_color',
141 'thinFilmThickness' :
'iridescence_thickness',
142 'thinFilmIor' :
'iridescence_ior',
146 self.
remapMap[
'standard_surface'] = standard_surface_remapKeys;
147 self.
remapMap[
'gltf_pbr'] = gltf_remapKeys;
149 self.
remapMap[
'open_pbr_surface'] = openpbr_remapKeys;
153 @brief Write the remapping keys to a JSON file.
154 @param filename The filename to write the remapping keys to.
158 self.
logger.warning(
'No remapping keys to write')
161 with open(filepath,
'w')
as json_file:
162 json.dump(self.
remapMap, json_file, indent=4)
166 @brief Read the remapping keys from a JSON file.
167 @param filename The filename to read the remapping keys from.
168 @return A dictionary of remapping keys.
170 if not os.path.exists(filepath):
171 self.
logger.error(f
'> File does not exist: {filepath}')
174 with open(filepath,
'r')
as json_file:
175 self.
remapMap = json.load(json_file)
180 ''' Get the JSON object representing the Physically Based Materials '''
185 Get the list of material names from the JSON object
186 @return The list of material names
192 Get the MaterialX document
193 @return The MaterialX document
199 @brief Load the Physically Based Materials from a JSON file
200 @param fileName The filename to load the JSON file from
201 @return The JSON object representing the Physically Based Materials
205 if not os.path.exists(fileName):
206 self.
logger.error(f
'> File does not exist: {fileName}')
209 with open(fileName,
'r')
as json_file:
218 @brief Load the Physically Based Materials from a JSON string
219 @param matString The JSON string to load the Physically Based Materials from
220 @return The JSON object representing the Physically Based Materials
232 @brief Get the Physically Based Materials from the PhysicallyBased site
233 @return The JSON object representing the Physically Based Materials
240 'Accept':
'application/json'
243 response = requests.get(url, headers=headers)
245 if response.status_code == HTTPStatus.OK:
251 self.
logger.error(f
'> Status: {response.status_code}, {response.text}')
257 @brief Print the materials to the console
261 self.
logger.info(
'Material name: ' + mat[
'name'])
263 for key, value
in mat.items():
264 if (key !=
'name' and value):
265 self.
logger.info(f
'> - {key}: {value}')
269 @brief Write the materials to a JSON file
270 @param filename The filename to write the JSON file to
271 @return True if the file was written successfully, otherwise False
274 self.
logger.warning(
'No materials to write')
277 with open(filename,
'w')
as json_file:
286 @brief Utility to skip library elements when iterating over elements in a document.
287 @return True if the element is not in a library, otherwise False.
289 return not elem.hasSourceUri()
291 def _getMethodName(self):
292 frame = inspect.currentframe().f_back
293 method_name = frame.f_code.co_name
299 @brief Validate the MaterialX document
300 @param doc The MaterialX document to validate
301 @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.
304 self.
logger.critical(f
'> {self._getMethodName()}: MaterialX module is required')
308 self.
logger.warning(f
'> {self._getMethodName()}: MaterialX document is required')
311 valid, errors = doc.validate()
316 @brief Add a comment to the MaterialX document
317 @param doc The MaterialX document to add the comment to
318 @param commentString The comment string to add
321 comment = doc.addChildOfCategory(
'comment')
322 comment.setDocString(commentString)
325 remapKeys = {}, shaderPreFix ='') -> mx.Document:
327 @brief Convert the Physically Based Materials to MaterialX format for a given target shading model.
328 @param materialNames The list of material names to convert. If empty, all materials will be converted.
329 @param shaderCategory The target shading model to convert to. Default is 'standard_surface'.
330 @param remapKeys The remapping keys for the target shading model. If empty, the default remapping keys will be used.
331 @param shaderPreFix The prefix to add to the shader name. Default is an empty string.
332 @return The MaterialX document
335 self.
logger.critical(f
'> {self._getMethodName()}: MaterialX module is required')
339 self.
logger.warning(f
'> OpenPBR shading model not supported in MaterialX version {self.mx.getVersionString()}')
343 self.
logger.info(
'> No materials to convert')
346 if len(remapKeys) == 0:
348 if len(remapKeys) == 0:
349 self.
logger.warning(f
'> No remapping keys found for shading model: {shaderCategory}')
352 self.
doc = self.
mxmx.createDocument()
359 self.
addComment(self.
doc,
'Physically Based Materials from https://api.physicallybased.info ')
360 self.
addComment(self.
doc,
' Content Author: Anton Palmqvist, https://antonpalmqvist.com/ ')
361 self.
addComment(self.
doc, f
' Content processsed via REST API and mapped to MaterialX V{self.mx.getVersionString()} ')
362 self.
addComment(self.
doc, f
' Target Shading Model: {shaderCategory} ')
363 self.
addComment(self.
doc,
' Utility Author: Bernard Kwok. kwokcb@gmail.com ')
367 matName = mat[
'name']
371 if len(materialNames) > 0
and matName
not in materialNames:
375 if (len(shaderPreFix) > 0):
376 matName = matName +
'_' + shaderPreFix
378 shaderName = self.
doc.createValidChildName(matName +
'_SHD_PBM')
379 self.
addComment(self.
doc,
' Generated shader: ' + shaderName +
' ')
380 shaderNode = self.
doc.addNode(shaderCategory, shaderName, self.
mxmx.SURFACE_SHADER_TYPE_STRING)
381 shaderNode.setAttribute(
'uiname', uiName)
384 if 'category' in mat:
385 folderString = mat[
'category'][0]
387 if len(folderString) > 0:
389 folderString += mat[
'group']
390 if len(folderString) > 0:
391 shaderNode.setAttribute(
"uifolder", folderString)
393 docString = mat[
'description']
394 refString = mat[
'reference']
395 if len(refString) > 0:
396 if len(docString) > 0:
398 docString +=
'Reference: ' + refString[0]
399 if len(docString) > 0:
400 shaderNode.setDocString(docString)
407 materialName = self.
doc.createValidChildName(matName +
'_MAT_PBM')
408 self.
addComment(self.
doc,
' Generated material: ' + materialName +
' ')
409 materialNode = self.
doc.addNode(self.
mxmx.SURFACE_MATERIAL_NODE_STRING, materialName, self.
mxmx.MATERIAL_TYPE_STRING)
410 shaderInput = materialNode.addInput(self.
mxmx.SURFACE_SHADER_TYPE_STRING, self.
mxmx.SURFACE_SHADER_TYPE_STRING)
414 skipKeys = [
'name',
"density",
"category",
"description",
"sources",
"tags",
"reference"]
420 for key, value
in mat.items():
422 if (key
not in skipKeys):
424 if key ==
'metalness':
426 if key ==
'roughness':
428 if key ==
'transmission':
435 input = shaderNode.addInputFromNodeDef(key)
438 if isinstance(value, list):
439 value =
','.join([str(x)
for x
in value])
441 elif isinstance(value, (int, float)):
443 input.setValueString(value)
445 self.
logger.debug(
'Skip unsupported key: ' + key)
448 if (transmission !=
None)
and (metallness !=
None)
and (roughness !=
None)
and (color !=
None):
449 if (metallness == 0)
and (roughness == 0):
450 if 'transmission_color' in remapKeys:
451 key = remapKeys[
'transmission_color']
452 input = shaderNode.addInputFromNodeDef(key)
454 self.
logger.debug(f
'Set transmission color {key}: {color}')
455 value =
','.join([str(x)
for x
in color])
456 input.setValueString(value)
462 @brief Write the MaterialX document to disk
463 @param filename The filename to write the MaterialX document to
467 self.
logger.critical(f
'> {self._getMethodName()}: MaterialX module is required')
470 writeOptions = self.
mxmx.XmlWriteOptions()
471 writeOptions.writeXIncludeEnable =
False
473 self.
mxmx.writeToXmlFile(self.
doc, filename, writeOptions)
477 @brief Convert the MaterialX document to a string
478 @return The MaterialX document as a string
481 self.
logger.critical(f
'> {self._getMethodName()}: MaterialX module is required')
484 writeOptions = self.
mxmx.XmlWriteOptions()
485 writeOptions.writeXIncludeEnable =
False
487 mtlx = self.
mxmx.writeToXmlString(self.
doc, writeOptions)
Class to load Physically Based Materials from the PhysicallyBased site.
dict getInputRemapping(self, shadingModel)
Get the remapping keys for a given shading model.
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.
writeMaterialXToFile(self, filename)
Write the MaterialX document to disk.
dict materials
Materials list.
bool support_openpbr
OpenPBR support flag.
validateMaterialXDocument(self, doc)
Validate the MaterialX document.
__init__(self, mx_module, Optional[mx.Document] mx_stdlib=None)
Constructor for the PhysicallyBasedMaterialLoader class.
convertToMaterialXString(self)
Convert the MaterialX document to a string.
setDebugging(self, debug=True)
Set the debugging level for the logger.
writeJSONToFile(self, filename)
Write the materials to a JSON file.
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.
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.
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.
addComment(self, doc, commentString)
Add a comment to the MaterialX document.
list materialNames
Material names.