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 self.logger = logging.getLogger('PolyH')
41 logging.basicConfig(level=self.logger.info)
42
43 def fetch_materialx_assets(self, max_items=1, download_id=None):
44 '''
45 Fetch MaterialX assets from PolyHaven API and filter them by resolution.
46
47 @param max_items Maximum number of assets to fetch. If None, fetch all.
48 @param download_id If set, only fetch asset with this ID.
49 @return Tuple (materialx_assets, all_assets, filtered_polyhaven_assets):
50 - materialx_assets: Dictionary of MaterialX assets with URLs and texture files.
51 - all_assets: All assets returned by PolyHaven API.
52 - filtered_polyhaven_assets: Filtered assets containing only MaterialX files.
53 '''
54 parameters = {
55 "type": "textures"
56 }
57
58 resp = requests.get(self.ASSET_API, headers=self.HEADERS, params=parameters)
59 resp.raise_for_status()
60 all_assets = resp.json()
61
62 materialx_assets = {}
63 filtered_polyhaven_assets = {}
64
65 item_count = 0;
66 for id, data in all_assets.items():
67
68 if download_id and id != download_id:
69 #self.logger.info(f"Skipping asset id: '{id}' (not matching {download_id})")
70 continue
71
72 # Get the thumbnail
73 thumbnail_url = data.get("thumbnail_url")
74
75 resp = requests.get(f"{self.FILES_API}/{id}", headers=self.HEADERS)
76 resp.raise_for_status()
77 files_data = resp.json()
78 #json_string = json.dumps(files_data, indent=4)
79
80 # Remove all keys other than "mtlx"
81 files_data = {k: v for k, v in files_data.items() if k == "mtlx"}
82
83 mtlx_files = files_data.get("mtlx", [])
84 if mtlx_files:
85
86 self.logger.info(f"Found MaterialX data for '{id}'")
87
88 # Look for 1K, 2K , 4K, and 8K versions
89 resolutions = {
90 "1k": None,
91 "2k": None,
92 "4k": None,
93 "8k": None
94 }
95 for resolution_key in resolutions.keys():
96 res = mtlx_files.get(resolution_key, None)
97 if not res:
98 continue
99
100 n_k_mtlx = res.get("mtlx")
101 texture_struct = {}
102 if n_k_mtlx:
103 #self.logger.info(f"Found MaterialX files for '{one_k}'")
104 include_files = n_k_mtlx.get("include", {})
105 #self.logger.info(f"Found include files for '{id}': {one_k}")
106 for path, data in include_files.items():
107 texture_url = data.get("url")
108 #self.logger.info("Texture path:", path, "URL:", texture_url)
109 texture_struct[path] = texture_url
110 mtlx_url = n_k_mtlx.get("url")
111 res_id = id + '_' + resolution_key
112 if mtlx_url:
113 materialx_assets[res_id] = {
114 "url": mtlx_url,
115 "texture_files": texture_struct,
116 "thumbnail_url": thumbnail_url
117 }
118 json_string = json.dumps(materialx_assets[res_id], indent=4)
119 #self.logger.info(f"Found MaterialX for '{res_id}': {json_string}")
120 #self.logger.info(f"Found MaterialX for '{res_id}'")
121 # Create folder poly_have_data
122
123 # Save asset data to JSON file
124 #with open(f"polyhaven_data/{id}_data.json", "w") as f:
125 # json.dump(asset_data, f, indent=4)
126 # self.logger.info(f"Saved asset data for '{id}' to polyhaven_data/{id}_data.json")
127
128 filtered_polyhaven_assets[id] = files_data
129
130 # Halt if download_id is specified and matches the current asset ID
131 if download_id == id:
132 break
133
134 # Halt if max_items is specified and reached
135 if not download_id and max_items:
136 item_count += 1
137 if item_count >= max_items:
138 break
139
140 #if "materialx" in formats:
141 # materialx_assets[slug] = formats["materialx"]
142
143 return materialx_assets, all_assets, filtered_polyhaven_assets
144
145 def download_asset(self, asset_list, convert_exr_to_png=True):
146 '''
147 Download MaterialX asset and its textures from PolyHaven.
148
149 @param asset_list Dictionary of MaterialX assets with URLs and texture files.
150 @param convert_exr_to_png If True, attempt to use PNG images instead of EXR if available other attempt
151 to convert using OpenImageIO if installed. Default is to use PNG if possible, and convert EXR to PNG if OpenImageIO is available.
152 @return Tuple (id, mtlx_string, texture_binaries):
153 - id: ID of the downloaded asset.
154 - mtlx_string: The MaterialX document as a string.
155 - texture_binaries: List of tuples (path, binary content) for textures and thumbnails.
156 '''
157 for id, asset in asset_list.items():
158 url = asset.get("url")
159 if not url:
160 self.logger.info(f"No MaterialX URL found for '{id}'")
161 continue
162
163 resp = requests.get(url, headers=self.HEADERS)
164 resp.raise_for_status()
165 mtlx_string = resp.text
166 self.logger.info(f"Download MaterialX document {url}, length: {len(mtlx_string)} characters")
167
168 texture_binaries = []
169 download_texture_names = []
170 for path, texture_url in asset.get("texture_files", {}).items():
171 # Get texture files
172 ext = Path(path).suffix.lower()
173 texture_content = None
174
175 if ext == ".exr" and convert_exr_to_png:
176 # Replace /exr and .exr with /png and .png in the URL to check if a PNG version is available
177 old_texture_url = texture_url
178 texture_url = texture_url.replace("/exr/", "/png/").replace(".exr", ".png")
179 texture_resp = requests.get(texture_url, headers=self.HEADERS)
180 texture_resp.raise_for_status()
181 texture_content = texture_resp.content
182 if texture_content:
183 self.logger.info(f"Download {texture_url} instead of {old_texture_url} SUCCESSFUL")
184 ext = ".png"
185 # Update extension in the path to .png
186 old_path = path
187 path = str(Path(path).with_suffix(ext))
188 self.logger.info(f"- Updating texture path from {old_path} to {path}")
189
190 else:
191 self.logger.info(f"Download {texture_url} instead of {old_texture_url} FAILED")
192
193 if not texture_content:
194 self.logger.info(f"Download texture from {texture_url} ...")
195 texture_resp = requests.get(texture_url, headers=self.HEADERS)
196 texture_resp.raise_for_status()
197 texture_content = texture_resp.content
198
199 name = Path(path).stem
200
201 if ext == ".exr":
202 if HAVE_OPENIMAGEIO and convert_exr_to_png:
203 self.logger.info(f"Converting EXR to PNG for texture: {path}")
204 # Write EXR bytes to a temporary file
205 with tempfile.NamedTemporaryFile(suffix=".exr", delete=False) as tmp_exr:
206 tmp_exr.write(texture_content)
207 tmp_exr_path = tmp_exr.name
208 try:
209 inbuf = oiio.ImageInput.open(tmp_exr_path)
210 if inbuf:
211 spec = inbuf.spec()
212 pixels = inbuf.read_image(format=oiio.UINT8)
213 inbuf.close()
214 # Write PNG to another temporary file
215 with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_png:
216 outbuf = oiio.ImageOutput.create(tmp_png.name)
217 if outbuf:
218 outbuf.open(tmp_png.name, spec)
219 outbuf.write_image(pixels)
220 outbuf.close()
221 tmp_png.seek(0)
222 png_bytes = tmp_png.read()
223 png_name = f"{name}.png"
224 texture_binaries.append((png_name, png_bytes))
225
226 continue # Skip adding the original EXR
227 else:
228 self.logger.info("Failed to read EXR with OpenImageIO")
229 finally:
230 os.remove(tmp_exr_path)
231 if 'tmp_png' in locals():
232 os.remove(tmp_png.name)
233 self.logger.info(f" WARNING: EXR file present which may not be supported by MaterialX texture loader: {path}")
234
235 # Get file name from path
236 download_texture_name = path.split('/')[-1]
237 download_texture_names.append(download_texture_name)
238
239 texture_binaries.append((path, texture_content))
240
241 thumbnail_url = asset.get("thumbnail_url")
242 if thumbnail_url:
243 self.logger.info(f"Download thumbnail from {thumbnail_url} ...")
244 thumbnail_resp = requests.get(thumbnail_url, headers=self.HEADERS)
245 thumbnail_resp.raise_for_status()
246
247 clean_url = thumbnail_url
248 # Strip any ? or # from the URL
249 clean_url = clean_url.split('?')[0].split('#')[0]
250 clean_url = clean_url.split('/')[-1] # Get the last part of the URL
251 extension = Path(clean_url).suffix.lower()
252 texture_binaries.append((f"{id}_thumbnail{extension}", thumbnail_resp.content))
253
254 # Replace .exr with .png in mtlx_string
255 for name in download_texture_names:
256 extension = Path(name).suffix.lower()
257 exr_name = name.replace(extension, ".exr")
258 # Replace exr_name with name in the mtlx_string
259 mtlx_string = mtlx_string.replace(exr_name, name)
260
261 before_mtlx_string = mtlx_string
262 mtlx_string = mtlx_string.replace(".exr", ".png")
263 if (before_mtlx_string != mtlx_string):
264 self.logger.info(f"Updated MaterialX string to reference PNG texture instead of EXR for {path}")
265 #self.logger.info(mtlx_string)
266
267 return id, mtlx_string, texture_binaries
268
269 def save_materialx_with_textures(self, id, mtlx_string, texture_binaries, data_folder, extract_zip=False):
270 '''
271 Save MaterialX string and texture binaries to a zip file.
272
273 @param id The ID of the MaterialX asset.
274 @param mtlx_string The MaterialX string content.
275 @param texture_binaries List of tuples (path, binary content) for textures and thumbnails.
276 @param data_folder Folder to save the zip file.
277 @param extract_zip If True, extract the zip file after saving zip.
278 @return None
279 '''
280 # Create a zip file with MaterialX and textures
281 filename = f"{id}_materialx.zip"
282 filename = Path(data_folder) / filename
283 with zipfile.ZipFile(filename, "w") as zipf:
284 # Write MaterialX file
285 zipf.writestr(f"{id}.mtlx", mtlx_string)
286 # Write texture files
287 for path, content in texture_binaries:
288 zipf.writestr(path, content)
289 self.logger.info(f"Saved zip: {filename}")
290
291 # Save zip contents to folder
292 extract_folder = Path(data_folder) / f"{id}_materialx"
293 if extract_zip:
294 with zipfile.ZipFile(filename, "r") as zipf:
295 zipf.extractall(extract_folder)
296 self.logger.info(f"Extracted zip contents to folder: {extract_folder}")
297
Fetch MaterialX assets from PolyHaven API and download them.
__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_asset(self, asset_list, convert_exr_to_png=True)
Download MaterialX asset and its textures from PolyHaven.
fetch_materialx_assets(self, max_items=1, download_id=None)
Fetch MaterialX assets from PolyHaven API and filter them by resolution.