MaterialXMaterials 1.39.5
Utilities for retrieving materials from remote servers
Loading...
Searching...
No Matches
polyHavenLoader.py
1'''
2@brief Module to fetch MaterialX assets from PolyHaven API and download them.
3'''
4import requests
5import json
6from pathlib import Path
7import zipfile
8import logging
9import io
10import tempfile
11import os
12import MaterialX as mx
13
14HAVE_OPENIMAGEIO = False
15try:
16 import OpenImageIO as oiio
17 import numpy as np
18 HAVE_OPENIMAGEIO = True
19except ImportError:
20 print("OpenImageIO or numpy not installed. EXR image conversion not supported.")
21
23 """
24 @brief Fetch MaterialX assets from PolyHaven API and download them.
25
26 This class provides methods to fetch and download MaterialX assets and textures from PolyHaven.
27 """
28 def __init__(self):
29 '''
30 Initialize the PolyHavenLoader with API endpoints and headers.
31 '''
32 self.BASE_API = "https://api.polyhaven.com"
33 self.ASSET_API = "https://api.polyhaven.com/assets"
34 self.INFO_API = "https://api.polyhaven.com/info"
35 self.FILES_API = "https://api.polyhaven.com/files"
36 self.HEADERS = {
37 "User-Agent": "MTLX_Polyaven_Loader/1.0", # Required by PolyHaven API
38 }
39
40 # Keys for supported asset types
41 self.MTLX_KEY = 'mtlx'
42 self.GLTF_KEY = 'gltf'
43 self.BLEND_KEY = 'blend'
44
45 self.logger = logging.getLogger('PolyH')
46 logging.basicConfig(level=self.logger.info)
47
48 def fetch_materialx_assets(self, max_items=1, download_id=None, download_type=None):
49 '''
50 Fetch MaterialX assets from PolyHaven API and filter them by resolution.
51
52 @param max_items Maximum number of assets to fetch. If None, fetch all.
53 @param download_id If set, only fetch asset with this ID.
54 @param download_type Type of asset to download (e.g. 'mtlx', 'blend', 'gltf'). Default is None which means to fetch MaterialX
55 @return Tuple (materialx_assets, all_assets, filtered_polyhaven_assets):
56 - materialx_assets: Dictionary of MaterialX assets with URLs and texture files.
57 - all_assets: All assets returned by PolyHaven API.
58 - filtered_polyhaven_assets: Filtered assets containing only MaterialX files.
59 '''
60 parameters = {
61 "type": "textures"
62 }
63
64 resp = requests.get(self.ASSET_API, headers=self.HEADERS, params=parameters)
65 resp.raise_for_status()
66 all_assets = resp.json()
67
68 materialx_assets = {}
69 gltf_assets = {}
70 blender_assets = {}
71 filtered_polyhaven_assets = {}
72
73 # Look for 1K, 2K , 4K, and 8K versions
74 resolutions = {
75 "1k": None,
76 "2k": None,
77 "4k": None,
78 "8k": None
79 }
80
81 # Default to fetching MaterialX assets if no download type specified
82 if not download_type:
83 download_type = self.MTLX_KEY
84
85 item_count = 0
86 for id, data in all_assets.items():
87
88 found_gltf = False
89 found_blend = False
90 found_mtlx = False
91
92 if download_id and id != download_id:
93 #self.logger.info(f"Skipping asset id: '{id}' (not matching {download_id})")
94 continue
95
96 # Get the thumbnail
97 thumbnail_url = data.get("thumbnail_url")
98
99 resp = requests.get(f"{self.FILES_API}/{id}", headers=self.HEADERS)
100 resp.raise_for_status()
101 files_data = resp.json()
102 #json_string = json.dumps(files_data, indent=4)
103 #print(f"Files data for asset '{id}': {json_string}")
104
105 blend_files = files_data.get(self.BLEND_KEY, [])
106 if blend_files:
107 found_blend = True
108
109 for resolution_key in resolutions.keys():
110 res = blend_files.get(resolution_key, None)
111 if not res:
112 continue
113
114 n_k_mtlx = res.get(self.BLEND_KEY)
115 texture_struct = {}
116 if n_k_mtlx:
117 include_files = n_k_mtlx.get("include", {})
118 for path, data in include_files.items():
119 texture_url = data.get("url")
120 texture_struct[path] = texture_url
121 url = n_k_mtlx.get("url")
122 res_id = id + '_' + resolution_key
123 if url:
124 blender_assets[res_id] = {
125 "url": url,
126 "texture_files": texture_struct,
127 "thumbnail_url": thumbnail_url
128 }
129
130 if download_id == id and download_type == self.BLEND_KEY:
131 break
132
133 gltf_files = files_data.get(self.GLTF_KEY, [])
134 if gltf_files:
135 found_gltf = True
136
137 for resolution_key in resolutions.keys():
138 res = gltf_files.get(resolution_key, None)
139 if not res:
140 continue
141
142 n_k_mtlx = res.get(self.GLTF_KEY)
143 texture_struct = {}
144 if n_k_mtlx:
145 include_files = n_k_mtlx.get("include", {})
146 for path, data in include_files.items():
147 texture_url = data.get("url")
148 texture_struct[path] = texture_url
149 url = n_k_mtlx.get("url")
150 res_id = id + '_' + resolution_key
151 if url:
152 gltf_assets[res_id] = {
153 "url": url,
154 "texture_files": texture_struct,
155 "thumbnail_url": thumbnail_url
156 }
157
158 # Halt if download_id is specified and matches the current asset ID and download type
159 if download_id == id and download_type == self.GLTF_KEY:
160 break
161
162
163 # Remove all keys other than "mtlx"
164 #files_data = {k: v for k, v in files_data.items() if k == "mtlx"}
165 mtlx_files = files_data.get(self.MTLX_KEY, [])
166 if mtlx_files:
167 found_mtlx = True
168
169 for resolution_key in resolutions.keys():
170 res = mtlx_files.get(resolution_key, None)
171 if not res:
172 continue
173
174 n_k_mtlx = res.get(self.MTLX_KEY)
175 texture_struct = {}
176 if n_k_mtlx:
177 include_files = n_k_mtlx.get("include", {})
178 for path, data in include_files.items():
179 texture_url = data.get("url")
180 texture_struct[path] = texture_url
181 mtlx_url = n_k_mtlx.get("url")
182 res_id = id + '_' + resolution_key
183 if mtlx_url:
184 materialx_assets[res_id] = {
185 "url": mtlx_url,
186 "texture_files": texture_struct,
187 "thumbnail_url": thumbnail_url
188 }
189
190 filtered_polyhaven_assets[id] = files_data
191
192 self.logger.info(f"Id: '{id}' has blender: {found_blend}, glTF: {found_gltf}, mtlx: {found_mtlx}")
193
194 # Halt if download_id is specified and matches the current asset ID and download type
195 if download_id == id and download_type == self.MTLX_KEY:
196 break
197
198 # Halt if max_items is specified and reached
199 if not download_id and max_items:
200 item_count += 1
201 if item_count >= max_items:
202 break
203
204 #if "materialx" in formats:
205 # materialx_assets[slug] = formats["materialx"]
206
207 return materialx_assets, all_assets, filtered_polyhaven_assets, blender_assets, gltf_assets
208
209 def download_gltf_asset(self, asset_list):
210 '''
211 Download glTF asset from PolyHaven.
212
213 @param asset_list Dictionary of glTF assets with URLs and texture files.
214 @return Tuple (id, gltf_ascii, texture_binaries):
215 - id: ID of the downloaded asset.
216 - gltf_ascii: The glTF file as ASCII string.
217 - texture_binaries: List of tuples (path, binary content) for textures and thumbnails
218 '''
219 for id, asset in asset_list.items():
220 url = asset.get("url")
221 if not url:
222 self.logger.info(f"No glTF URL found for '{id}'")
223 continue
224 if not url.endswith(".gltf") and not url.endswith(".glb"):
225 self.logger.info(f"Invalid glTF URL for '{id}': {url}")
226 continue
227
228 resp = requests.get(url, headers=self.HEADERS)
229 resp.raise_for_status()
230 if url.endswith(".glb"):
231 self.logger.info(f"Downloaded glTF binary file {url}, size: {len(resp.content)} bytes")
232 gltf_content = resp.content
233 elif url.endswith(".gltf"):
234 gltf_content = resp.text
235 self.logger.info(f"Download glTF file {url}, length: {len(gltf_content)} characters")
236
237 texture_binaries = []
238 for path, texture_url in asset.get("texture_files", {}).items():
239 self.logger.info(f"Download texture from {texture_url} ...")
240 texture_resp = requests.get(texture_url, headers=self.HEADERS)
241 texture_resp.raise_for_status()
242 texture_content = texture_resp.content
243 texture_binaries.append((path, texture_content))
244
245 thumbnail_url = asset.get("thumbnail_url")
246 if thumbnail_url:
247 self.logger.info(f"Download thumbnail from {thumbnail_url} ...")
248 thumbnail_resp = requests.get(thumbnail_url, headers=self.HEADERS)
249 thumbnail_resp.raise_for_status()
250
251 clean_url = thumbnail_url
252 # Strip any ? or # from the URL
253 clean_url = clean_url.split('?')[0].split('#')[0]
254 clean_url = clean_url.split('/')[-1] # Get the last part of the URL
255 extension = Path(clean_url).suffix.lower()
256 texture_binaries.append((f"{id}_thumbnail{extension}", thumbnail_resp.content))
257 return id, gltf_content, texture_binaries
258
259 def download_blender_asset(self, asset_list):
260 '''
261 Download Blender asset from PolyHaven.
262
263 @param asset_list Dictionary of Blender assets with URLs and texture files.
264 @return Tuple (id, blend_binary, texture_binaries):
265 - id: ID of the downloaded asset.
266 - blend_binary: The Blender file as binary content.
267 - texture_binaries: List of tuples (path, binary content) for textures and thumbnails.
268 '''
269 for id, asset in asset_list.items():
270 url = asset.get("url")
271 if not url:
272 self.logger.info(f"No Blender URL found for '{id}'")
273 continue
274
275 resp = requests.get(url, headers=self.HEADERS)
276 resp.raise_for_status()
277 blend_binary = resp.content
278 self.logger.info(f"Download Blender file {url}, size: {len(blend_binary)} bytes")
279
280 texture_binaries = []
281 for path, texture_url in asset.get("texture_files", {}).items():
282 self.logger.info(f"Download texture from {texture_url} ...")
283 texture_resp = requests.get(texture_url, headers=self.HEADERS)
284 texture_resp.raise_for_status()
285 texture_content = texture_resp.content
286 texture_binaries.append((path, texture_content))
287
288 thumbnail_url = asset.get("thumbnail_url")
289 if thumbnail_url:
290 self.logger.info(f"Download thumbnail from {thumbnail_url} ...")
291 thumbnail_resp = requests.get(thumbnail_url, headers=self.HEADERS)
292 thumbnail_resp.raise_for_status()
293
294 clean_url = thumbnail_url
295 # Strip any ? or # from the URL
296 clean_url = clean_url.split('?')[0].split('#')[0]
297 clean_url = clean_url.split('/')[-1] # Get the last part of the URL
298 extension = Path(clean_url).suffix.lower()
299 texture_binaries.append((f"{id}_thumbnail{extension}", thumbnail_resp.content))
300 return id, blend_binary, texture_binaries
301
302 def download_mtlx_asset(self, asset_list, convert_exr_to_png=True):
303 '''
304 Download MaterialX asset and its textures from PolyHaven.
305
306 @param asset_list Dictionary of MaterialX assets with URLs and texture files.
307 @param convert_exr_to_png If True, attempt to use PNG images instead of EXR if available other attempt
308 to convert using OpenImageIO if installed. Default is to use PNG if possible, and convert EXR to PNG if OpenImageIO is available.
309 @return Tuple (id, mtlx_string, texture_binaries):
310 - id: ID of the downloaded asset.
311 - mtlx_string: The MaterialX document as a string.
312 - texture_binaries: List of tuples (path, binary content) for textures and thumbnails.
313 '''
314 for id, asset in asset_list.items():
315 url = asset.get("url")
316 if not url:
317 self.logger.info(f"No MaterialX URL found for '{id}'")
318 continue
319
320 resp = requests.get(url, headers=self.HEADERS)
321 resp.raise_for_status()
322 mtlx_string = resp.text
323 self.logger.info(f"Download MaterialX document {url}, length: {len(mtlx_string)} characters")
324
325 texture_binaries = []
326 download_texture_names = []
327 for path, texture_url in asset.get("texture_files", {}).items():
328 # Get texture files
329 ext = Path(path).suffix.lower()
330 texture_content = None
331
332 if ext == ".exr" and convert_exr_to_png:
333 # Replace /exr and .exr with /png and .png in the URL to check if a PNG version is available
334 old_texture_url = texture_url
335 texture_url = texture_url.replace("/exr/", "/png/").replace(".exr", ".png")
336 texture_resp = requests.get(texture_url, headers=self.HEADERS)
337 texture_resp.raise_for_status()
338 texture_content = texture_resp.content
339 if texture_content:
340 self.logger.info(f"Download {texture_url} instead of {old_texture_url} SUCCESSFUL")
341 ext = ".png"
342 # Update extension in the path to .png
343 old_path = path
344 path = str(Path(path).with_suffix(ext))
345 self.logger.info(f"- Updating texture path from {old_path} to {path}")
346
347 else:
348 self.logger.info(f"Download {texture_url} instead of {old_texture_url} FAILED")
349
350 if not texture_content:
351 self.logger.info(f"Download texture from {texture_url} ...")
352 texture_resp = requests.get(texture_url, headers=self.HEADERS)
353 texture_resp.raise_for_status()
354 texture_content = texture_resp.content
355
356 name = Path(path).stem
357
358 if ext == ".exr":
359 if HAVE_OPENIMAGEIO and convert_exr_to_png:
360 self.logger.info(f"Converting EXR to PNG for texture: {path}")
361 # Write EXR bytes to a temporary file
362 with tempfile.NamedTemporaryFile(suffix=".exr", delete=False) as tmp_exr:
363 tmp_exr.write(texture_content)
364 tmp_exr_path = tmp_exr.name
365 try:
366 inbuf = oiio.ImageInput.open(tmp_exr_path)
367 if inbuf:
368 spec = inbuf.spec()
369 pixels = inbuf.read_image(format=oiio.UINT8)
370 inbuf.close()
371 # Write PNG to another temporary file
372 with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_png:
373 outbuf = oiio.ImageOutput.create(tmp_png.name)
374 if outbuf:
375 outbuf.open(tmp_png.name, spec)
376 outbuf.write_image(pixels)
377 outbuf.close()
378 tmp_png.seek(0)
379 png_bytes = tmp_png.read()
380 png_name = f"{name}.png"
381 texture_binaries.append((png_name, png_bytes))
382
383 continue # Skip adding the original EXR
384 else:
385 self.logger.info("Failed to read EXR with OpenImageIO")
386 finally:
387 os.remove(tmp_exr_path)
388 if 'tmp_png' in locals():
389 os.remove(tmp_png.name)
390 self.logger.info(f" WARNING: EXR file present which may not be supported by MaterialX texture loader: {path}")
391
392 # Get file name from path
393 download_texture_name = path.split('/')[-1]
394 download_texture_names.append(download_texture_name)
395
396 texture_binaries.append((path, texture_content))
397
398 thumbnail_url = asset.get("thumbnail_url")
399 if thumbnail_url:
400 self.logger.info(f"Download thumbnail from {thumbnail_url} ...")
401 thumbnail_resp = requests.get(thumbnail_url, headers=self.HEADERS)
402 thumbnail_resp.raise_for_status()
403
404 clean_url = thumbnail_url
405 # Strip any ? or # from the URL
406 clean_url = clean_url.split('?')[0].split('#')[0]
407 clean_url = clean_url.split('/')[-1] # Get the last part of the URL
408 extension = Path(clean_url).suffix.lower()
409 texture_binaries.append((f"{id}_thumbnail{extension}", thumbnail_resp.content))
410
411 # Replace .exr with .png in mtlx_string
412 for name in download_texture_names:
413 extension = Path(name).suffix.lower()
414 exr_name = name.replace(extension, ".exr")
415 # Replace exr_name with name in the mtlx_string
416 mtlx_string = mtlx_string.replace(exr_name, name)
417
418 before_mtlx_string = mtlx_string
419 mtlx_string = mtlx_string.replace(".exr", ".png")
420 if (before_mtlx_string != mtlx_string):
421 self.logger.info(f"Updated MaterialX string to reference PNG texture instead of EXR for {path}")
422 #self.logger.info(mtlx_string)
423
424 return id, mtlx_string, texture_binaries
425
426 def save_blender_with_textures(self, id, blend_binary, texture_binaries, data_folder, extract_zip=False):
427 '''
428 Save Blender file and texture binaries to a zip file.
429
430 @param id The ID of the Blender asset.
431 @param blend_binary The Blender file as binary content.
432 @param texture_binaries List of tuples (path, binary content) for textures and thumbnails.
433 @param data_folder Folder to save the zip file.
434 @param extract_zip If True, extract the zip file after saving zip.
435 @return None
436 '''
437 # Create a zip file with Blender file and textures
438 filename = f"{id}_blender.zip"
439 filename = Path(data_folder) / filename
440 with zipfile.ZipFile(filename, "w") as zipf:
441 # Write Blender file
442 zipf.writestr(f"{id}.blend", blend_binary)
443 # Write texture files
444 for path, content in texture_binaries:
445 zipf.writestr(path, content)
446 self.logger.info(f"Saved zip: {filename}")
447
448 # Save zip contents to folder
449 extract_folder = Path(data_folder) / f"{id}_blender"
450 if extract_zip:
451 with zipfile.ZipFile(filename, "r") as zipf:
452 zipf.extractall(extract_folder)
453 self.logger.info(f"Extracted zip contents to folder: {extract_folder}")
454
455 def save_gltf_with_textures(self, id, gltf_ascii, texture_binaries, data_folder, extract_zip=False):
456 '''
457 Save glTF file and texture binaries to a zip file.
458
459 @param id The ID of the glTF asset.
460 @param gltf_ascii The glTF file as ASCII string.
461 @param texture_binaries List of tuples (path, binary content) for textures and thumbnails.
462 @param data_folder Folder to save the zip file.
463 @param extract_zip If True, extract the zip file after saving zip.
464 @return None
465 '''
466 # Create a zip file with glTF file and textures
467 filename = f"{id}_gltf.zip"
468 filename = Path(data_folder) / filename
469 with zipfile.ZipFile(filename, "w") as zipf:
470 # Write glTF file
471 zipf.writestr(f"{id}.gltf", gltf_ascii)
472 # Write texture files
473 for path, content in texture_binaries:
474 zipf.writestr(path, content)
475 self.logger.info(f"Saved zip: {filename}")
476
477 # Save zip contents to folder
478 extract_folder = Path(data_folder) / f"{id}_gltf"
479 if extract_zip:
480 with zipfile.ZipFile(filename, "r") as zipf:
481 zipf.extractall(extract_folder)
482 self.logger.info(f"Extracted zip contents to folder: {extract_folder}")
483
484 def save_materialx_with_textures(self, id, mtlx_string, texture_binaries, data_folder, extract_zip=False):
485 '''
486 Save MaterialX string and texture binaries to a zip file.
487
488 @param id The ID of the MaterialX asset.
489 @param mtlx_string The MaterialX string content.
490 @param texture_binaries List of tuples (path, binary content) for textures and thumbnails.
491 @param data_folder Folder to save the zip file.
492 @param extract_zip If True, extract the zip file after saving zip.
493 @return None
494 '''
495 # Create a zip file with MaterialX and textures
496 filename = f"{id}_materialx.zip"
497 filename = Path(data_folder) / filename
498 with zipfile.ZipFile(filename, "w") as zipf:
499 # Write MaterialX file
500 zipf.writestr(f"{id}.mtlx", mtlx_string)
501 # Write texture files
502 for path, content in texture_binaries:
503 zipf.writestr(path, content)
504 self.logger.info(f"Saved zip: {filename}")
505
506 # Save zip contents to folder
507 extract_folder = Path(data_folder) / f"{id}_materialx"
508 if extract_zip:
509 with zipfile.ZipFile(filename, "r") as zipf:
510 zipf.extractall(extract_folder)
511 self.logger.info(f"Extracted zip contents to folder: {extract_folder}")
512
Fetch MaterialX assets from PolyHaven API and download them.
save_blender_with_textures(self, id, blend_binary, texture_binaries, data_folder, extract_zip=False)
Save Blender file and texture binaries to a zip file.
__init__(self)
Initialize the PolyHavenLoader with API endpoints and headers.
save_materialx_with_textures(self, id, mtlx_string, texture_binaries, data_folder, extract_zip=False)
Save MaterialX string and texture binaries to a zip file.
download_mtlx_asset(self, asset_list, convert_exr_to_png=True)
Download MaterialX asset and its textures from PolyHaven.
download_gltf_asset(self, asset_list)
Download glTF asset from PolyHaven.
save_gltf_with_textures(self, id, gltf_ascii, texture_binaries, data_folder, extract_zip=False)
Save glTF file and texture binaries to a zip file.
download_blender_asset(self, asset_list)
Download Blender asset from PolyHaven.
fetch_materialx_assets(self, max_items=1, download_id=None, download_type=None)
Fetch MaterialX assets from PolyHaven API and filter them by resolution.