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