MaterialXMaterials 0.0.1
Utilities for retrieving materials from remote servers
Loading...
Searching...
No Matches
ambientCGLoader.py
1'''
2@brief Utilities to extract materials from the ambientCG material database.
3
4See: https://docs.ambientcg.com/api/ for information on available API calls.
5'''
6import logging as lg
7
8from http import HTTPStatus
9import requests # type: ignore
10import os # type: ignore
11#import inspect # type: ignore
12
13import csv # type: ignore
14import json # type: ignore
15import io # type: ignore
16import zipfile # type: ignore
17
18import MaterialX as mx # type: ignore
19from typing import Optional
20
22 '''
23 @brief Class to load materials from the AmbientCG site.
24 The class can convert the materials to MaterialX format for given target shading models.
25 '''
26 def __init__(self, mx_module, mx_stdlib : Optional[mx.Document] = None):
27 '''
28 @brief Constructor for the AmbientCGLoader class.
29 Will initialize shader mappings and load the MaterialX standard library
30 if it is not passed in as an argument.
31 @param mx_module The MaterialX module. Required.
32 @param mx_stdlib The MaterialX standard library. Optional.
33 '''
34
35
36 self.logger = lg.getLogger('ACGLoader')
37 lg.basicConfig(level=lg.INFO)
38
39
40 self.database : dict = {}
41
42 self.assetsassets : dict = {}
43
44 # Material download information
45
46 self.materials = None
47
48 self.materialNames : list[str]= []
49
50 self.csv_materials = None
51
52 # Downloaded material information
53
55
57
58
59 self.mx = mx_module
60
61 self.stdlib = mx_stdlib
62
63 self.support_openpbr = False
64
65 if not mx_module:
66 self.logger.critical(f'> {self._getMethodName()}: MaterialX module not specified.')
67 return
68
69 # Check for OpenPBR support which is only available in 1.39 and above
70 version_major, version_minor, version_patch = self.mx.getVersionIntegers()
71 self.logger.info(f'Using MaterialX version: {version_major}.{version_minor}.{version_patch}')
72 if (version_major >=1 and version_minor >= 39) or version_major > 1:
73 self.logger.debug('> OpenPBR shading model supported')
74 self.support_openpbr = True
75
76 # Load the MaterialX standard library if not provided
77 if not self.stdlib:
78 self.stdlib = self.mx.createDocument()
79 libFiles = self.mx.loadLibraries(mx.getDefaultDataLibraryFolders(), mx.getDefaultDataSearchPath(), self.stdlib)
80 self.logger.debug(f'> Loaded standard library: {libFiles}')
81
82 def setDebugging(self, debug : Optional[bool]=True):
83 '''
84 @brief Set the debugging level for the logger.
85 @param debug True to set the logger to debug level, otherwise False.
86 @return None
87 '''
88 if debug:
89 self.logger.setLevel(lg.DEBUG)
90 else:
91 self.logger.setLevel(lg.INFO)
92
93 def getMaterialNames(self, key='assetId') -> list:
94 '''
95 Get the list of material names.
96 @param key The key to use for the material name. Default is 'assetId' based
97 on the version 2 ambientCG API.
98 @return The list of material names
99 '''
100 self.materialNames.clear()
101 unique_names = set()
102 if self.materials:
103 for item in self.materials:
104 unique_names.add(item.get(key) )
105 self.materialNames = list(sorted(unique_names))
106 return self.materialNames
107
108 def writeMaterialList(self, materialList, filename):
109 '''
110 @brief Write the material list in JSON format to a file
111 @param materialList The list of materials to write
112 @param filename The file path to write the list to
113 @return None
114 '''
115 self.logger.info(f'Writing material list to file: {filename}')
116 with open(filename, mode='w', encoding='utf-8') as json_file:
117 json.dump(materialList, json_file, indent=4)
118
119 def buildDownLoadAttribute(self, imageFormat='PNG', imageResolution='1'):
120 '''
121 @brief Build the download attribute string for a given image format and resolution
122 Note: This is a hard-coded string format used by ambientCG. If this changes then this
123 must be updated !
124 @param imageFormat The image format to download
125 @param imageResolution The image resolution to download
126 @return The download attribute string
127 '''
128 target = f"{imageResolution}K-{imageFormat}"
129 return target
130
132 '''
133 @brief Get the current downloaded material information
134 '''
135 return { 'filename': self.downloadMaterialFileName,
136 'content': self.downloadMaterialdownloadMaterial }
137
139 '''
140 @brief Clear any cached current material asset
141 '''
143 self.downloadMaterialdownloadMaterial.seek(0) # Reset position
144 self.downloadMaterialdownloadMaterial.truncate(0) # Clear the buffer
147
149 '''
150 @brief Write the currently downloaded file to file
151 @param path The output path for the material. Default is empty.
152 '''
153 haveDownload = len(self.downloadMaterialFileName) > 0 and self.downloadMaterialdownloadMaterial
154 if not haveDownload:
155 self.logger.warning('No current material downloaded')
156
157 # Write the file in chunks to avoid memory issues with large files
158 # TBD: What is the "ideal" chunk size.
159 filename = self.downloadMaterialFileName
160 filename = os.path.join(path, filename)
161
162 # Write the file in chunks to avoid memory issues
163 CHUNK_SIZE = 8192
165 with open(filename, "wb") as file:
166 while True:
167 chunk = self.downloadMaterialdownloadMaterial.read(CHUNK_SIZE)
168 if not chunk:
169 break # End of file
170 file.write(chunk)
171 #with open(filename, "wb") as file:
172 # file.write(self.downloadMaterial.read())
173
174 self.logger.info(f"Saved downloaded material to: {filename}")
175
176 def downloadMaterialAsset(self, assetId, imageFormat='PNG', imageResolution='1',
177 downloadAttributeKey = 'downloadAttribute', downloadLinkKey = 'downloadLink'):
178 '''
179 @brief Download a material with a given id and format + resolution for images.
180 Default is to look for a 1K PNG variant.
181 @param assetId The string id of the material
182 @param imageFormat The image format to download. Default is PNG.
183 @param imageResolution The image resolution to download. Default is 1.
184 @param downloadAttributeKey The download attribute key. Default is 'downloadAttribute' based on the V2 ambientCG API.
185 @param downloadLinkKey The download link key. Default is 'downloadLink' based on the V2 ambientCG API.
186 @return File name of downloaded content
187 '''
188 # Clear previous data
190
191 # Look item with the given assetId, imageFormat and imageResolution
192 url = ''
193 downloadAttribute = ''
194 items = self.findMaterial(assetId)
195 target = self.buildDownLoadAttribute(imageFormat, imageResolution)
196 for item in items:
197 downloadAttribute = item[downloadAttributeKey]
198 if downloadAttribute == target:
199 url = item[downloadLinkKey]
200 self.logger.info(f'Found Asset: {assetId}. Download Attribute: {downloadAttribute} -> {url}')
201
202 if len(url) == 0:
203 self.logger.error(f'No download link found for asset: {assetId}, attribute: {target}')
204 return ''
205
206 # Extract filename for save
207 self.downloadMaterialFileName = url.split("file=")[-1]
208
209 try:
210 # Send a GET request to the URL
211 response = requests.get(url, stream=True)
212 response.raise_for_status() # Raise an exception for HTTP errors
213
214 # Create an in-memory binary stream
215 self.downloadMaterialdownloadMaterial = io.BytesIO()
216
217 # Write the file in chunks to avoid memory issues with large files
218 CHUNK_SIZE = 8192
219 for chunk in response.iter_content(chunk_size=CHUNK_SIZE):
220 self.downloadMaterialdownloadMaterial.write(chunk)
221
222 self.logger.info(f"Material file downloaded: {self.downloadMaterialFileName}")
223
224 except requests.exceptions.RequestException as e:
226
227 self.logger.info(f"Error occurred while downloading the file: {e}")
228
229 return self.downloadMaterialFileName
230
231 def findMaterial(self, assetId, key='assetId'):
232 '''
233 @brief Get the list of materials matching a material identifier
234 @param assetId Material string identifier
235 @param key The key to lookup asset identifiers. Default is 'assetId' based on the version 2 ambientCG API.
236 @return List of materials, or None if not found
237 '''
238 if self.materials:
239 materialList = [item for item in self.materials if item.get(key) == assetId]
240 return materialList
241 return None
242
243 def loadMaterialsList(self, fileName):
244 '''
245 @brief Load in the list of downloadable materials from file.
246 @param fileName Name of JSON containing list
247 @return Materials list
248 '''
249 with open(fileName, 'r') as json_file:
250 self.materials = json.load(json_file)
251 self.logger.info(f'Loaded materials list from: {fileName}')
252 return self.materials
253
255 '''
256 @brief Download the list of materials from the ambientCG site: "ttps://ambientCG.com/api/v2/downloads_csv"
257 Takes the origina CSV file and remaps this into JSON for runtime.
258 @return Materials list
259 '''
260 # URL of the CSV file
261 url = "https://ambientCG.com/api/v2/downloads_csv"
262 headers = {
263 'Accept': 'application/csv'
264 }
265 parameters = {
266 'method': 'PBRPhotogrammetry', # TODO: Allow user filtering options
267 'type': 'Material',
268 'sort': 'Alphabet',
269 }
270
271 self.logger.info('Downloading materials CSV list...')
272 response = requests.get(url, headers=headers, params=parameters)
273
274 # Check if the request was successful
275 if response.status_code == HTTPStatus.OK:
276 # Decode the CSV content from the response
277 self.csv_materials = response.content.decode("utf-8")
278
279 # Parse the CSV content
280 if self.csv_materials:
281 csv_reader = csv.DictReader(self.csv_materials.splitlines())
282
283 # Convert the CSV rows to a JSON object (list of dictionaries)
284 self.materials = [row for row in csv_reader]
285
286 self.logger.info("Downloaded CSV material list as JSON.")
287 else:
288 self.materials = None
289 self.logger.warning("Failed to parse the CSV material content")
290
291 else:
292 self.materials = None
293 self.logger.warning(f"Failed to fetch the CSV material content. HTTP status code: {response.status_code}")
294
295 return self.materials
296
297 def getDataBase(self):
298 '''
299 @brief Get asset database
300 @return Asset database
301 '''
302 return self.database
303
305 '''
306 @brief Get asset database material list
307 @return Material list
308 '''
309 return self.assetsassets
310
311 def downloadAssetDatabase(self) -> dict:
312 '''
313 @brief Download the asset database for materials from the ambientCG site.
314 @return None
315 '''
316 self.database.clear()
317 self.assetsassets = None
318
319 url = 'https://ambientcg.com/api/v2/full_json'
320 headers = {
321 'Accept': 'application/json'
322 }
323 parameters = {
324 'method': 'PBRPhotogrammetry', # TODO: Allow user filtering options
325 'type': 'Material',
326 'sort': 'Alphabet',
327 }
328
329 response = requests.get(url, headers=headers, params=parameters)
330
331 if response.status_code == HTTPStatus.OK:
332 self.database = response.json()
333 self.assetsassets = self.database['foundAssets']
334 else:
335 self.logger.error(f'> Status: {response.status_code}, {response.text}')
336
337 def writeDatabaseToFile(self, filename):
338 '''
339 @brief Write the database file
340 @param filename The filename to write the JSON file to
341 @return True if the file was written successfully, otherwise False
342 '''
343 if not self.database:
344 self.logger.warning('No database to write')
345 return False
346
347 with open(filename, 'w') as json_file:
348 json.dump(self.database, json_file, indent=4)
349 return True
350
351 return False
352
353 @staticmethod
355 '''
356 @brief Validate the MaterialX document
357 @param doc The MaterialX document to validate
358 @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.
359 '''
360 if not self.mx:
361 self.logger.critical(f'> {self._getMethodName()}: MaterialX module is required')
362 return False, ''
363
364 if not doc:
365 self.logger.warning(f'> {self._getMethodName()}: MaterialX document is required')
366 return False, ''
367
368 valid, errors = doc.validate()
369 return valid, errors
370
371 @staticmethod
372 def addComment(self, doc, commentString):
373 '''
374 @brief Add a comment to the MaterialX document
375 @param doc The MaterialX document to add the comment to
376 @param commentString The comment string to add
377 @return None
378 '''
379 comment = doc.addChildOfCategory('comment')
380 comment.setDocString(commentString)
381
382 @staticmethod
383 def getMaterialXString(self, doc):
384 '''
385 @brief Convert the MaterialX document to a string
386 @return The MaterialX document as a string
387 '''
388 if not self.mx:
389 self.logger.critical(f'> {self._getMethodName()}: MaterialX module is required')
390 return
391
392 writeOptions = self.mx.XmlWriteOptions()
393 writeOptions.writeXIncludeEnable = False
394 writeOptions.elementPredicate = self.skipLibraryElement
395 mtlx = self.mx.writeToXmlString(doc, writeOptions)
396 return mtlx
Class to load materials from the AmbientCG site.
logger
logger is the logging object for the class
validateMaterialXDocument(self, doc)
Validate the MaterialX document.
getMaterialXString(self, doc)
Convert the MaterialX document to a string.
getDownloadedMaterialInformation(self)
Get the current downloaded material information.
downloadMaterialAsset(self, assetId, imageFormat='PNG', imageResolution='1', downloadAttributeKey='downloadAttribute', downloadLinkKey='downloadLink')
Download a material with a given id and format + resolution for images.
bool support_openpbr
Flag to indicate OpenPBR shader support.
dict database
Database of asset information.
list materials
List of materials in JSON format.
findMaterial(self, assetId, key='assetId')
Get the list of materials matching a material identifier.
addComment(self, doc, commentString)
Add a comment to the MaterialX document.
dict downloadAssetDatabase(self)
Download the asset database for materials from the ambientCG site.
downloadMaterialsList(self)
Download the list of materials from the ambientCG site: "ttps://ambientCG.com/api/v2/downloads_csv" T...
writeDatabaseToFile(self, filename)
Write the database file.
buildDownLoadAttribute(self, imageFormat='PNG', imageResolution='1')
Build the download attribute string for a given image format and resolution Note: This is a hard-code...
__init__(self, mx_module, Optional[mx.Document] mx_stdlib=None)
Constructor for the AmbientCGLoader class.
clearDownloadMaterial(self)
Clear any cached current material asset.
writeDownloadedMaterialToFile(self, path='')
Write the currently downloaded file to file.
str downloadMaterialFileName
Current downlaoded material file name.
setDebugging(self, Optional[bool] debug=True)
Set the debugging level for the logger.
writeMaterialList(self, materialList, filename)
Write the material list in JSON format to a file.
list getMaterialNames(self, key='assetId')
Get the list of material names.
getDataBaseMaterialList(self)
Get asset database material list.
dict assets
Reference to database found assets.
loadMaterialsList(self, fileName)
Load in the list of downloadable materials from file.