MaterialXMaterials 0.0.3
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
6import requests, json, os, inspect # type: ignore
7import logging as lg
8from http import HTTPStatus
9import MaterialX as mx # type: ignore
10from typing import Optional
11
13 '''
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.
16 '''
17 def __init__(self, mx_module, mx_stdlib : Optional[mx.Document] = None):
18 '''
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.
24 '''
25
26 self.logger = lg.getLogger('PBMXLoader')
27 lg.basicConfig(level=lg.INFO)
28
29
31
32 self.materialNames : list[str]= []
33
34 self.uri = 'https://api.physicallybased.info/materials'
35
36 self.doc = None
37
38 self.mxmx = mx_module
39
40 self.stdlib = mx_stdlib
41
42 self.MTLX_NODE_NAME_ATTRIBUTE = 'nodename'
43
44 self.support_openpbr = False
45
46 if not mx_module:
47 self.logger.critical(f'> {self._getMethodName()}: MaterialX module not specified.')
48 return
49
50 # Check for OpenPBR support which is only available in 1.39 and above
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')
55 self.support_openpbr = True
56
58
59 # Load the MaterialX standard library if not provided
60 if not self.stdlib:
61 self.stdlib = self.mxmx.createDocument()
62 libFiles = self.mxmx.loadLibraries(mx.getDefaultDataLibraryFolders(), mx.getDefaultDataSearchPath(), self.stdlib)
63 self.logger.debug(f'> Loaded standard library: {libFiles}')
64
65 def setDebugging(self, debug=True):
66 '''
67 @brief Set the debugging level for the logger.
68 @param debug True to set the logger to debug level, otherwise False.
69 @return None
70 '''
71 if debug:
72 self.logger.setLevel(lg.DEBUG)
73 else:
74 self.logger.setLevel(lg.INFO)
75
76 def getInputRemapping(self, shadingModel) -> dict:
77 '''
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.
81 '''
82 if (shadingModel in self.remapMap):
83 return self.remapMap[shadingModel]
84
85 self.logger.warn(f'> No remapping keys found for shading model: {shadingModel}')
86 return {}
87
89 '''
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.
93
94 The currently supported shading models are:
95 - standard_surface
96 - open_pbr_surface
97 - gltf_pbr
98 @return None
99 '''
100 # Remap keys for Autodesk Standard Surface shading model.
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', # 'color' remapping as needed
110 'transmissionDispersion' : 'transmission_dispersion',
111 'thinFilmThickness' : 'thin_film_thickness',
112 'thinFilmIor' : 'thin_film_IOR',
113 }
114 # Remap keys for OpenPBR shading model.
115 # Q: When to set geometry_thin_walled to true?
116 openpbr_remapKeys = {
117 'color': 'base_color',
118 'specularColor': 'specular_color',
119 'roughness': 'specular_roughness', # 'base_diffuse_roughness',
120 'metalness': 'base_metalness',
121 'ior': 'specular_ior',
122 'subsurfaceRadius': 'subsurface_radius',
123 'transmission': 'transmission_weight',
124 'transmission_color': 'transmission_color', # 'color' remapping as needed
125 'transmissionDispersion': 'transmission_dispersion_abbe_number',
126 #'complexIor' TODO : add in array remap
127 # Complex IOR values, n (refractive index), and k (extinction coefficient), for each color channel, in the following order:
128 # nR, kR, nG, kG, nB, kB.
129 'thinFilmThickness' : 'thin_film_thickness',
130 'thinFilmIor' : 'thin_film_ior',
131 }
132 # Remap keys for Khronos glTF shading model.
133 gltf_remapKeys = {
134 'color': 'base_color',
135 'specularColor': 'specular_color',
136 'roughness': 'roughness',
137 'metalness': 'metallic',
138 'ior': 'ior',
139 'transmission': 'transmission',
140 'transmission_color': 'attenuation_color', # Remap transmission color to attenuation color
141 'thinFilmThickness' : 'iridescence_thickness',
142 'thinFilmIor' : 'iridescence_ior',
143 }
144
145 self.remapMap = {}
146 self.remapMap['standard_surface'] = standard_surface_remapKeys;
147 self.remapMap['gltf_pbr'] = gltf_remapKeys;
148 if self.support_openpbr:
149 self.remapMap['open_pbr_surface'] = openpbr_remapKeys;
150
151 def writeRemappingFile(self, filepath):
152 '''
153 @brief Write the remapping keys to a JSON file.
154 @param filename The filename to write the remapping keys to.
155 @return None
156 '''
157 if not self.remapMap:
158 self.logger.warning('No remapping keys to write')
159 return
160
161 with open(filepath, 'w') as json_file:
162 json.dump(self.remapMap, json_file, indent=4)
163
164 def readRemappingFile(self, filepath):
165 '''
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.
169 '''
170 if not os.path.exists(filepath):
171 self.logger.error(f'> File does not exist: {filepath}')
172 return {}
173
174 with open(filepath, 'r') as json_file:
175 self.remapMap = json.load(json_file)
176
177 return self.remapMap
178
179 def getJSON(self) -> dict:
180 ''' Get the JSON object representing the Physically Based Materials '''
182
183 def getJSONMaterialNames(self) -> list:
184 '''
185 Get the list of material names from the JSON object
186 @return The list of material names
187 '''
188 return self.materialNames
189
190 def getMaterialXDocument(self) -> mx.Document:
191 '''
192 Get the MaterialX document
193 @return The MaterialX document
194 '''
195 return self.doc
196
197 def loadMaterialsFromFile(self, fileName) -> dict:
198 '''
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
202 '''
203 self.materialsmaterialsmaterials.clear()
204 self.materialNames.clear()
205 if not os.path.exists(fileName):
206 self.logger.error(f'> File does not exist: {fileName}')
207 return {}
208
209 with open(fileName, 'r') as json_file:
210 self.materialsmaterialsmaterials = json.load(json_file)
211 for mat in self.materialsmaterialsmaterials:
212 self.materialNames.append(mat['name'])
213
215
216 def loadMaterialsFromString(self, matString) -> dict:
217 '''
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
221 '''
222 self.materialsmaterialsmaterials.clear()
223 self.materialNames.clear()
224 self.materialsmaterialsmaterials = json.loads(matString)
225 for mat in self.materialsmaterialsmaterials:
226 self.materialNames.append(mat['name'])
227
229
230 def getMaterialsFromURL(self) -> dict:
231 '''
232 @brief Get the Physically Based Materials from the PhysicallyBased site
233 @return The JSON object representing the Physically Based Materials
234 '''
235
236 self.materialsmaterialsmaterials.clear()
237 self.materialNames.clear()
238 url = self.uri
239 headers = {
240 'Accept': 'application/json'
241 }
242
243 response = requests.get(url, headers=headers)
244
245 if response.status_code == HTTPStatus.OK:
246 self.materialsmaterialsmaterials = response.json()
247 for mat in self.materialsmaterialsmaterials:
248 self.materialNames.append(mat['name'])
249
250 else:
251 self.logger.error(f'> Status: {response.status_code}, {response.text}')
252
254
255 def printMaterials(self):
256 '''
257 @brief Print the materials to the console
258 @return None
259 '''
260 for mat in self.materialsmaterialsmaterials:
261 self.logger.info('Material name: ' + mat['name'])
262 # Print out each key and value
263 for key, value in mat.items():
264 if (key != 'name' and value):
265 self.logger.info(f'> - {key}: {value}')
266
267 def writeJSONToFile(self, filename):
268 '''
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
272 '''
273 if not self.materialsmaterialsmaterials:
274 self.logger.warning('No materials to write')
275 return False
276
277 with open(filename, 'w') as json_file:
278 json.dump(self.materialsmaterialsmaterials, json_file, indent=4)
279 return True
280
281 return False
282
283 @staticmethod
284 def skipLibraryElement(elem) -> bool:
285 '''
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.
288 '''
289 return not elem.hasSourceUri()
290
291 def _getMethodName(self):
292 frame = inspect.currentframe().f_back
293 method_name = frame.f_code.co_name
294 return method_name
295 #return inspect.currentframe().f_code.co_name
296
298 '''
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.
302 '''
303 if not self.mxmx:
304 self.logger.critical(f'> {self._getMethodName()}: MaterialX module is required')
305 return False, ''
306
307 if not doc:
308 self.logger.warning(f'> {self._getMethodName()}: MaterialX document is required')
309 return False, ''
310
311 valid, errors = doc.validate()
312 return valid, errors
313
314 def addComment(self, doc, commentString):
315 '''
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
319 @return None
320 '''
321 comment = doc.addChildOfCategory('comment')
322 comment.setDocString(commentString)
323
324 def convertToMaterialX(self, materialNames = [], shaderCategory='standard_surface',
325 remapKeys = {}, shaderPreFix ='') -> mx.Document:
326 '''
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
333 '''
334 if not self.mxmx:
335 self.logger.critical(f'> {self._getMethodName()}: MaterialX module is required')
336 return None
337
338 if not self.support_openpbr and shaderCategory == 'open_pbr_surface':
339 self.logger.warning(f'> OpenPBR shading model not supported in MaterialX version {self.mx.getVersionString()}')
340 return None
341
342 if not self.materialsmaterialsmaterials:
343 self.logger.info('> No materials to convert')
344 return None
345
346 if len(remapKeys) == 0:
347 remapKeys = self.getInputRemapping(shaderCategory)
348 if len(remapKeys) == 0:
349 self.logger.warning(f'> No remapping keys found for shading model: {shaderCategory}')
350
351 # Create main document and import the library document
352 self.doc = self.mxmx.createDocument()
353 if not self.doc:
354 return None
355
356 self.doc.importLibrary(self.stdlib)
357
358 # Add header comments
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 ')
364
365 # Add properties to the material
366 for mat in self.materialsmaterialsmaterials:
367 matName = mat['name']
368 uiName = matName
369
370 # Filter by material name(s)
371 if len(materialNames) > 0 and matName not in materialNames:
372 #self.logger.debug('Skip material: ' + matName)
373 continue
374
375 if (len(shaderPreFix) > 0):
376 matName = matName + '_' + shaderPreFix
377
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)
382
383 folderString = ''
384 if 'category' in mat:
385 folderString = mat['category'][0]
386 if 'group' in mat:
387 if len(folderString) > 0:
388 folderString += '/'
389 folderString += mat['group']
390 if len(folderString) > 0:
391 shaderNode.setAttribute("uifolder", folderString)
392
393 docString = mat['description']
394 refString = mat['reference']
395 if len(refString) > 0:
396 if len(docString) > 0:
397 docString += '. '
398 docString += 'Reference: ' + refString[0]
399 if len(docString) > 0:
400 shaderNode.setDocString(docString)
401
402 # TODO: Add in option to add all inputs + add nodedef string
403 #shaderNode.addInputsFromNodeDef()
404 #shaderNode.setAttribute(self.mx.InterfaceElement.NODE_DEF_ATTRIBUTE, nodedefString)
405
406 # Create a new material
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)
411 shaderInput.setAttribute(self.MTLX_NODE_NAME_ATTRIBUTE, shaderNode.getName())
412
413 # Keys to skip.
414 skipKeys = ['name', "density", "category", "description", "sources", "tags", "reference"]
415
416 metallness = None
417 roughness = None
418 color = None
419 transmission = None
420 for key, value in mat.items():
421
422 if (key not in skipKeys):
423 # Keep track of these for possible transmission color remapping
424 if key == 'metalness':
425 metallness = value
426 if key == 'roughness':
427 roughness = value
428 if key == 'transmission':
429 transmission = value
430 if key == 'color':
431 color = value
432
433 if key in remapKeys:
434 key = remapKeys[key]
435 input = shaderNode.addInputFromNodeDef(key)
436 if input:
437 # Convert number vector to string
438 if isinstance(value, list):
439 value = ','.join([str(x) for x in value])
440 # Convert number to string:
441 elif isinstance(value, (int, float)):
442 value = str(value)
443 input.setValueString(value)
444 else:
445 self.logger.debug('Skip unsupported key: ' + key)
446
447 # Re-route color to mapped transmission_color if needed
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)
453 if input:
454 self.logger.debug(f'Set transmission color {key}: {color}')
455 value = ','.join([str(x) for x in color])
456 input.setValueString(value)
457
458 return self.doc
459
460 def writeMaterialXToFile(self, filename):
461 '''
462 @brief Write the MaterialX document to disk
463 @param filename The filename to write the MaterialX document to
464 @return None
465 '''
466 if not self.mxmx:
467 self.logger.critical(f'> {self._getMethodName()}: MaterialX module is required')
468 return
469
470 writeOptions = self.mxmx.XmlWriteOptions()
471 writeOptions.writeXIncludeEnable = False
472 writeOptions.elementPredicate = self.skipLibraryElement
473 self.mxmx.writeToXmlFile(self.doc, filename, writeOptions)
474
476 '''
477 @brief Convert the MaterialX document to a string
478 @return The MaterialX document as a string
479 '''
480 if not self.mxmx:
481 self.logger.critical(f'> {self._getMethodName()}: MaterialX module is required')
482 return
483
484 writeOptions = self.mxmx.XmlWriteOptions()
485 writeOptions.writeXIncludeEnable = False
486 writeOptions.elementPredicate = self.skipLibraryElement
487 mtlx = self.mxmx.writeToXmlString(self.doc, writeOptions)
488 return mtlx
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.
dict loadMaterialsFromFile(self, fileName)
Load the Physically Based Materials from a JSON file.
writeMaterialXToFile(self, filename)
Write the MaterialX document to disk.
__init__(self, mx_module, Optional[mx.Document] mx_stdlib=None)
Constructor for the PhysicallyBasedMaterialLoader class.
setDebugging(self, debug=True)
Set the debugging level for the logger.
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.
dict getJSON(self)
Get the JSON object representing the Physically Based Materials.
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.