MaterialXMaterials 0.0.1
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 The currently supported shading models are:
92 - standard_surface
93 - open_pbr_surface
94 - gltf_pbr
95 @return None
96 '''
97 # Remap keys for Autodesk Standard Surface shading model.
98 standard_surface_remapKeys = {
99 'color': 'base_color',
100 'specularColor': 'specular_color',
101 'roughness': 'specular_roughness',
102 #'metalness': 'metalness',
103 'ior': 'specular_IOR',
104 #'transmission': 'transmission',
105 'transmission_color': 'transmission_color',
106 'thinFilmIor' : 'thin_film_IOR',
107 'thinFilmThickness' : 'thin_film_thickness',
108 'transmissionDispersion' : 'transmission_dispersion',
109 }
110 # Remap keys for OpenPBR shading model.
111 # Q: When to set geometry_thin_walled to true?
112 openpbr_remapKeys = {
113 'color': 'base_color',
114 'specularColor': 'specular_color',
115 'roughness': 'specular_roughness', # 'base_diffuse_roughness',
116 'metalness': 'base_metalness',
117 'ior': 'specular_ior',
118 'transmission': 'transmission_weight',
119 'transmission_color': 'transmission_color',
120 'subsurfaceRadius': 'subsurface_radius',
121 'thinFilmIor' : 'thin_film_ior',
122 'thinFilmThickness' : 'thin_film_thickness',
123 'transmissionDispersion' : 'transmission_dispersion_scale',
124 }
125 # Remap keys for Khronos glTF shading model.
126 gltf_remapKeys = {
127 'color': 'base_color',
128 'specularColor': 'specular_color',
129 'roughness': 'roughness',
130 'metalness': 'metallic',
131 'transmission_color': 'attenuation_color',
132 #'ior': 'ior',
133 #'transmission': 'transmission',
134 }
135
136 self.remapMap = {}
137 self.remapMap['standard_surface'] = standard_surface_remapKeys;
138 self.remapMap['gltf_pbr'] = gltf_remapKeys;
139 if self.support_openpbr:
140 self.remapMap['open_pbr_surface'] = openpbr_remapKeys;
141
142 def getJSON(self) -> dict:
143 ''' Get the JSON object representing the Physically Based Materials '''
145
146 def getJSONMaterialNames(self) -> list:
147 '''
148 Get the list of material names from the JSON object
149 @return The list of material names
150 '''
151 return self.materialNames
152
153 def getMaterialXDocument(self) -> mx.Document:
154 '''
155 Get the MaterialX document
156 @return The MaterialX document
157 '''
158 return self.doc
159
160 def loadMaterialsFromFile(self, fileName) -> dict:
161 '''
162 @brief Load the Physically Based Materials from a JSON file
163 @param fileName The filename to load the JSON file from
164 @return The JSON object representing the Physically Based Materials
165 '''
166 self.materialsmaterialsmaterials.clear()
167 self.materialNames.clear()
168 if not os.path.exists(fileName):
169 self.logger.error(f'> File does not exist: {fileName}')
170 return {}
171
172 with open(fileName, 'r') as json_file:
173 self.materialsmaterialsmaterials = json.load(json_file)
174 for mat in self.materialsmaterialsmaterials:
175 self.materialNames.append(mat['name'])
176
178
179 def loadMaterialsFromString(self, matString) -> dict:
180 '''
181 @brief Load the Physically Based Materials from a JSON string
182 @param matString The JSON string to load the Physically Based Materials from
183 @return The JSON object representing the Physically Based Materials
184 '''
185 self.materialsmaterialsmaterials.clear()
186 self.materialNames.clear()
187 self.materialsmaterialsmaterials = json.loads(matString)
188 for mat in self.materialsmaterialsmaterials:
189 self.materialNames.append(mat['name'])
190
192
193 def getMaterialsFromURL(self) -> dict:
194 '''
195 @brief Get the Physically Based Materials from the PhysicallyBased site
196 @return The JSON object representing the Physically Based Materials
197 '''
198
199 self.materialsmaterialsmaterials.clear()
200 self.materialNames.clear()
201 url = self.uri
202 headers = {
203 'Accept': 'application/json'
204 }
205
206 response = requests.get(url, headers=headers)
207
208 if response.status_code == HTTPStatus.OK:
209 self.materialsmaterialsmaterials = response.json()
210 for mat in self.materialsmaterialsmaterials:
211 self.materialNames.append(mat['name'])
212
213 else:
214 self.logger.error(f'> Status: {response.status_code}, {response.text}')
215
217
218 def printMaterials(self):
219 '''
220 @brief Print the materials to the console
221 @return None
222 '''
223 for mat in self.materialsmaterialsmaterials:
224 self.logger.info('Material name: ' + mat['name'])
225 # Print out each key and value
226 for key, value in mat.items():
227 if (key != 'name' and value):
228 self.logger.info(f'> - {key}: {value}')
229
230 def writeJSONToFile(self, filename):
231 '''
232 @brief Write the materials to a JSON file
233 @param filename The filename to write the JSON file to
234 @return True if the file was written successfully, otherwise False
235 '''
236 if not self.materialsmaterialsmaterials:
237 self.logger.warning('No materials to write')
238 return False
239
240 with open(filename, 'w') as json_file:
241 json.dump(self.materialsmaterialsmaterials, json_file, indent=4)
242 return True
243
244 return False
245
246 @staticmethod
247 def skipLibraryElement(elem) -> bool:
248 '''
249 @brief Utility to skip library elements when iterating over elements in a document.
250 @return True if the element is not in a library, otherwise False.
251 '''
252 return not elem.hasSourceUri()
253
254 def _getMethodName(self):
255 frame = inspect.currentframe().f_back
256 method_name = frame.f_code.co_name
257 return method_name
258 #return inspect.currentframe().f_code.co_name
259
261 '''
262 @brief Validate the MaterialX document
263 @param doc The MaterialX document to validate
264 @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.
265 '''
266 if not self.mxmx:
267 self.logger.critical(f'> {self._getMethodName()}: MaterialX module is required')
268 return False, ''
269
270 if not doc:
271 self.logger.warning(f'> {self._getMethodName()}: MaterialX document is required')
272 return False, ''
273
274 valid, errors = doc.validate()
275 return valid, errors
276
277 def addComment(self, doc, commentString):
278 '''
279 @brief Add a comment to the MaterialX document
280 @param doc The MaterialX document to add the comment to
281 @param commentString The comment string to add
282 @return None
283 '''
284 comment = doc.addChildOfCategory('comment')
285 comment.setDocString(commentString)
286
287 def convertToMaterialX(self, materialNames = [], shaderCategory='standard_surface',
288 remapKeys = {}, shaderPreFix ='') -> mx.Document:
289 '''
290 @brief Convert the Physically Based Materials to MaterialX format for a given target shading model.
291 @param materialNames The list of material names to convert. If empty, all materials will be converted.
292 @param shaderCategory The target shading model to convert to. Default is 'standard_surface'.
293 @param remapKeys The remapping keys for the target shading model. If empty, the default remapping keys will be used.
294 @param shaderPreFix The prefix to add to the shader name. Default is an empty string.
295 @return The MaterialX document
296 '''
297 if not self.mxmx:
298 self.logger.critical(f'> {self._getMethodName()}: MaterialX module is required')
299 return None
300
301 if not self.support_openpbr and shaderCategory == 'open_pbr_surface':
302 self.logger.warning(f'> OpenPBR shading model not supported in MaterialX version {self.mx.getVersionString()}')
303 return None
304
305 if not self.materialsmaterialsmaterials:
306 self.logger.info('> No materials to convert')
307 return None
308
309 if len(remapKeys) == 0:
310 remapKeys = self.getInputRemapping(shaderCategory)
311 if len(remapKeys) == 0:
312 self.logger.warning(f'> No remapping keys found for shading model: {shaderCategory}')
313
314 # Create main document and import the library document
315 self.doc = self.mxmx.createDocument()
316 if not self.doc:
317 return None
318
319 self.doc.importLibrary(self.stdlib)
320
321 # Add header comments
322 self.addComment(self.doc, 'Physically Based Materials from https://api.physicallybased.info ')
323 self.addComment(self.doc, ' Processsed via API and converted to MaterialX ')
324 self.addComment(self.doc, ' Target Shading Model: ' + shaderCategory)
325 self.addComment(self.doc, ' Utility Author: Bernard Kwok. kwokcb@gmail.com ')
326
327 # Add properties to the material
328 for mat in self.materialsmaterialsmaterials:
329 matName = mat['name']
330
331 # Filter by material name(s)
332 if len(materialNames) > 0 and matName not in materialNames:
333 #self.logger.debug('Skip material: ' + matName)
334 continue
335
336 if (len(shaderPreFix) > 0):
337 matName = matName + '_' + shaderPreFix
338
339 shaderName = self.doc.createValidChildName(matName + '_SHD_PBM')
340 self.addComment(self.doc, ' Generated shader: ' + shaderName + ' ')
341 shaderNode = self.doc.addNode(shaderCategory, shaderName, self.mxmx.SURFACE_SHADER_TYPE_STRING)
342 docString = mat['description']
343 refString = mat['reference']
344 if len(refString) > 0:
345 if len(docString) > 0:
346 docString += '. '
347 docString += 'Reference: ' + refString[0]
348 if len(docString) > 0:
349 shaderNode.setDocString(docString)
350 #shaderNode.addInputsFromNodeDef()
351 #shaderNode.setAttribute(self.mx.InterfaceElement.NODE_DEF_ATTRIBUTE, nodedefString)
352
353 # Create a new material
354 materialName = self.doc.createValidChildName(matName + '_MAT_PBM')
355 self.addComment(self.doc, ' Generated material: ' + materialName + ' ')
356 materialNode = self.doc.addNode(self.mxmx.SURFACE_MATERIAL_NODE_STRING, materialName, self.mxmx.MATERIAL_TYPE_STRING)
357 shaderInput = materialNode.addInput(self.mxmx.SURFACE_SHADER_TYPE_STRING, self.mxmx.SURFACE_SHADER_TYPE_STRING)
358 shaderInput.setAttribute(self.MTLX_NODE_NAME_ATTRIBUTE, shaderNode.getName())
359
360 # Keys to skip.
361 skipKeys = ['name', "density", "category", "description", "sources", "tags", "reference"]
362
363 metallness = None
364 roughness = None
365 color = None
366 transmission = None
367 for key, value in mat.items():
368
369 if (key not in skipKeys):
370 if key == 'metalness':
371 metallness = value
372 if key == 'roughness':
373 roughness = value
374 if key == 'transmission':
375 transmission = value
376 if key == 'color':
377 color = value
378
379 if key in remapKeys:
380 key = remapKeys[key]
381 input = shaderNode.addInputFromNodeDef(key)
382 if input:
383 # Convert number vector to string
384 if isinstance(value, list):
385 value = ','.join([str(x) for x in value])
386 # Convert number to string:
387 elif isinstance(value, (int, float)):
388 value = str(value)
389 input.setValueString(value)
390 #else:
391 # self.logger.debug('Skip unsupported key: ' + key)
392
393 if (transmission != None) and (metallness != None) and (roughness != None) and (color != None):
394 if (metallness == 0) and (roughness == 0):
395 if 'transmission_color' in remapKeys:
396 key = remapKeys['transmission_color']
397 input = shaderNode.addInputFromNodeDef(key)
398 if input:
399 self.logger.debug(f'Set transmission color {key}: {color}')
400 value = ','.join([str(x) for x in color])
401 input.setValueString(value)
402
403 return self.doc
404
405 def writeMaterialXToFile(self, filename):
406 '''
407 @brief Write the MaterialX document to disk
408 @param filename The filename to write the MaterialX document to
409 @return None
410 '''
411 if not self.mxmx:
412 self.logger.critical(f'> {self._getMethodName()}: MaterialX module is required')
413 return
414
415 writeOptions = self.mxmx.XmlWriteOptions()
416 writeOptions.writeXIncludeEnable = False
417 writeOptions.elementPredicate = self.skipLibraryElement
418 self.mxmx.writeToXmlFile(self.doc, filename, writeOptions)
419
421 '''
422 @brief Convert the MaterialX document to a string
423 @return The MaterialX document as a string
424 '''
425 if not self.mxmx:
426 self.logger.critical(f'> {self._getMethodName()}: MaterialX module is required')
427 return
428
429 writeOptions = self.mxmx.XmlWriteOptions()
430 writeOptions.writeXIncludeEnable = False
431 writeOptions.elementPredicate = self.skipLibraryElement
432 mtlx = self.mxmx.writeToXmlString(self.doc, writeOptions)
433 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.
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.
addComment(self, doc, commentString)
Add a comment to the MaterialX document.