MaterialXMaterials 1.39.5
Utilities for retrieving materials from remote servers
Loading...
Searching...
No Matches
jsPolyHavenLoader.js
1
10 this.baseUrl = "https://api.polyhaven.com";
11 this.userAgent = "MTLX_Polyhaven_Loader/1.0";
12 }
13
19 try {
20 const response = await fetch(`${this.baseUrl}/assets?t=textures`, {
21 headers: { "User-Agent": this.userAgent }
22 });
23
24 if (!response.ok) {
25 throw new Error(`HTTP error! status: ${response.status}`);
26 }
27
28 const data = await response.json();
29 return this.processMaterialsData(data);
30 } catch (error) {
31 console.error('Error fetching materials:', error);
32 throw error;
33 }
34 }
35
42 const materials = [];
43 const categories = new Set();
44
45 for (const [id, materialData] of Object.entries(rawData)) {
46 // Skip materials without required data
47 if (!materialData.name || !materialData.categories) continue;
48
49 // Process authors
50 let authorsList = 'Author(s): ';
51 const authors = materialData.authors || {};
52 const authorNames = Object.keys(authors);
53 authorsList += authorNames.join(', ');
54
55 let max_resolution = materialData.max_resolution
56 let maxresolutionString = '';
57 if (max_resolution) {
58 maxresolutionString = `${max_resolution[0]} x ${max_resolution[1]}`
59 }
60
61 const material = {
62 id,
63 name: materialData.name,
64 description: authorsList,
65 categories: materialData.categories,
66 tags: materialData.tags || [],
67 thumb_url: `https://cdn.polyhaven.com/asset_img/thumbs/${id}.png?width=512`,
68 maps: materialData.maxresolutionString || {}
69 };
70
71 materials.push(material);
72
73 // Collect categories
74 material.categories.forEach(cat => categories.add(cat));
75 }
76
77 return { materials, categories };
78 }
79
85 async fetchMaterialFiles(materialId) {
86 try {
87 const response = await fetch(`${this.baseUrl}/files/${materialId}`, {
88 headers: { "User-Agent": this.userAgent }
89 });
90
91 if (!response.ok) {
92 throw new Error(`Failed to fetch MaterialX data for ${materialId}`);
93 }
94
95 const result = await response.json();
96 console.log(`Fetched MaterialX files for ${materialId}:`, result);
97 return result;
98 } catch (error) {
99 console.error(`Error fetching material files for ${materialId}:`, error);
100 throw error;
101 }
102 }
103
110 try {
111 const response = await fetch(url, {
112 headers: { "User-Agent": this.userAgent }
113 });
114
115 if (!response.ok) {
116 throw new Error('Failed to download MaterialX file');
117 }
118
119 const result = await response.text();
120 // Find
121 return result;
122 } catch (error) {
123 console.error('Error downloading MaterialX content:', error);
124 throw error;
125 }
126 }
127
133 async downloadTexture(url) {
134 try {
135 const response = await fetch(url, {
136 headers: { "User-Agent": this.userAgent }
137 });
138
139 if (!response.ok) {
140 throw new Error(`Failed to download texture from ${url}`);
141 }
142 else {
143 //console.log(`> Successfully downloaded texture from ${url}`);
144 }
145
146 return await response.blob();
147 } catch (error) {
148 console.error(`Error downloading texture from ${url}:`, error);
149 throw error;
150 }
151 }
152
158 async downloadThumbnail(thumbnailUrl) {
159 try {
160 const response = await fetch(thumbnailUrl, {
161 headers: { "User-Agent": this.userAgent }
162 });
163
164 if (!response.ok) {
165 throw new Error('Failed to download thumbnail');
166 }
167
168 return await response.blob();
169 } catch (error) {
170 console.error('Error downloading thumbnail:', error);
171 throw error;
172 }
173 }
174
182 async createMaterialXPackage(material, resolution, preFetchedData = null) {
183
184 async function downloadWithConcurrency(tasks, concurrency = 3) {
185 const results = [];
186 const queue = tasks.slice();
187 async function worker() {
188 while (queue.length) {
189 const task = queue.shift();
190 results.push(await task());
191 }
192 }
193 await Promise.all(Array(concurrency).fill().map(worker));
194 return results;
195 }
196
197 async function blobToUint8Array(blob) {
198 return new Uint8Array(await blob.arrayBuffer());
199 }
200
201 try {
202 let filesData, mtlxData, mtlxContent, textureFiles;
203 if (preFetchedData) {
204 mtlxContent = preFetchedData.mtlxContent;
205 textureFiles = preFetchedData.textureFiles;
206 } else {
207 filesData = await this.fetchMaterialFiles(material.id);
208 mtlxData = filesData.mtlx?.[resolution]?.mtlx;
209 if (!mtlxData) throw new Error(`No MaterialX files for ${resolution}`);
210 mtlxContent = await this.downloadMaterialXContent(mtlxData.url);
211 textureFiles = mtlxData.include || {};
212 }
213
214 // Fetch MaterialX files data
215 console.log('> createMaterialXPackage - fetched MaterialX data:', mtlxData);
216
217 // Conatiner for Zip contents
218 const zip = {}
219
220 // Download all referenced textures
221 let texturePaths = [];
222 let blobs = {};
223
224 const texturePromises = Object.entries(textureFiles).map(([path, fileData]) => async () =>
225 {
226 try {
227 console.log(`Processing texture: ${path} from URL: ${fileData.url}`);
228 const textureBlob = await this.downloadTexture(fileData.url);
229 texturePaths.push(path);
230
231 // Build the local path for the zip (preserving folder structure)
232 const pathParts = path.split('/');
233 const localPath = pathParts.join('/');
234
235 blobs[localPath] = await blobToUint8Array(textureBlob);
236
237 } catch (error) {
238 console.error(`Error downloading texture from URI ${path}:`, error);
239 const pathParts = path.split('/');
240 const localPath = pathParts.join('/');
241 blobs[localPath] = fflate.strToU8(`Error downloading texture: ${error.message}`);
242 }
243 });
244 await downloadWithConcurrency(texturePromises, 3);
245
246 for (const [localPath, blobData] of Object.entries(blobs)) {
247 zip[localPath] = blobData;
248 //console.log(`Added texture to ZIP: ${localPath}`);
249 }
250
251 // Download and add thumbnail image
252 if (material.thumb_url) {
253 try {
254 const thumbBlob = await this.downloadThumbnail(material.thumb_url);
255 const thumbUrl = new URL(material.thumb_url);
256 const thumbPath = thumbUrl.pathname.split('/').pop();
257 const thumbExt = thumbPath.split('.').pop();
258 //console.log('Add thumbnail to ZIP:', thumbPath);
259 zip[`${material.id}_thumbnail.${thumbExt}`] = await blobToUint8Array(thumbBlob);
260 } catch (error) {
261 console.error('Error downloading thumbnail:', error);
262 }
263 }
264
265 // Wait for all downloads to complete
266 await Promise.all(texturePromises);
267
268 // Add Materialx document to ZIP.
269 //console.log(`Adding MaterialX file to ZIP: ${material.id}.mtlx`);
270 zip[`${material.id}.mtlx`] = fflate.strToU8(mtlxContent);
271
272 // Add README file to root of ZIP
273 //console.log('Adding README.txt to ZIP with material metadata and file list');
274 zip["README.txt"] = fflate.strToU8(
275 `Material: ${material.name}\n` +
276 `Resolution: ${resolution}\n` +
277 `Source: https://polyhaven.com/a/${material.id}\n` +
278 `Downloaded: ${new Date().toISOString()}\n\n` +
279 `Contains the following files:\n` +
280 `- ${material.id}.mtlx\n` +
281 (material.thumb_url ? `- ${material.id}_thumbnail.png\n` : '') +
282 (texturePaths.length > 0 ? texturePaths.map(t => `- ${t}`).join('\n') + '\n' : '')
283 );
284
285 for (const [k, v] of Object.entries(zip)) {
286 console.log(`ZIP entry: ${k}, size: ${v.length || v.size || 'unknown'}`);
287 }
288
289 // Compress the ZIP file
290 //console.log('Compressing ZIP file with fflate...');
291 const zipped = fflate.zipSync(zip);
292
293 // Create a Blob from the zipped data
294 //console.log('Creating Blob from zipped data...');
295 const blob = new Blob([zipped], { type: 'application/zip' });
296
297 console.log('MaterialX package created successfully:', blob);
298 return blob;
299
300 } catch (error) {
301 console.error('Error creating MaterialX package:', error);
302 throw error;
303 }
304 }
305
313 async getMaterialContent(materialId, resolution, textureFormat = '') {
314 try {
315 const filesData = await this.fetchMaterialFiles(materialId);
316 const mtlxData = filesData.mtlx?.[resolution]?.mtlx;
317
318 if (!mtlxData) {
319 throw new Error(`No MaterialX files found for ${resolution} resolution`);
320 }
321
322 // Get MaterialX content
323 let mtlxContent = await this.downloadMaterialXContent(mtlxData.url);
324
325 let textureFileData = mtlxData.include || {};
326
327 // Preprocess MTLX content and file data to remap texture references to .<textureFormat>.
328 if (textureFormat.length > 0)
329 {
330 if (textureFileData && Object.keys(textureFileData).length > 0) {
331 const textureExtension = "." + textureFormat.toLowerCase();
332
333 for (const [path, fileData] of Object.entries(textureFileData)) {
334 const baseName = path.replace(/\\/g, '/').split('/').pop().replace(/\.[^.]+$/, '');
335
336 // Replace all references to baseName.<ext> with baseName.<textureFormat>
337 const extRegex = new RegExp(baseName + '\\.[a-zA-Z0-9]+', 'g');
338 console.log(`Remapping texture references in MTLX content for ${baseName}: ${extRegex}`);
339 let prevMtlxContent = mtlxContent;
340 mtlxContent = mtlxContent.replace(extRegex, baseName + textureExtension);
341 if (prevMtlxContent == mtlxContent) {
342 console.warn(`No references found in MTLX content for texture ${baseName}. There is a mismatch between the MTLX content and the texture file data !`);
343 }
344
345 // Remap path and url to .<textureFormat>
346 let extension = path.split('.').pop().toLowerCase();
347 if (extension !== textureFormat) {
348 const prevPath = path;
349 const newPath = path.replace(/\.[^.]+$/i, textureExtension);
350
351 // Remap fileData as well
352 fileData.url = fileData.url.replace(/\.[^.]+$/i, textureExtension);
353 // Split the path and replace any extension folder with textureExtension
354 const urlParts = fileData.url.split('/');
355 const extFromPath = extension;
356 fileData.url = urlParts.map(part => part.toLowerCase() === extFromPath ? textureFormat : part).join('/');
357
358 delete textureFileData[path];
359 textureFileData[newPath] = fileData;
360 console.log(`>> Remapped texture path: ${prevPath} -> ${newPath}. File data: ${fileData.url}`);
361 } else {
362 console.log(`>> Texture path is already ${textureFormat}: ${path}. File data: ${fileData.url}`);
363 }
364 }
365
366 //console.log('Final remapped MTLX content:', mtlxContent);
367 //console.log('Final remapped texture file data:', textureFileData);
368 }
369 }
370
371 return {
372 mtlxContent,
373 textureFiles: textureFileData
374 };
375 } catch (error) {
376 console.error('Error getting material content:', error);
377 throw error;
378 }
379 }
380}
381
382// Export for use in other scripts
383if (typeof module !== 'undefined' && module.exports) {
384 module.exports = JsPolyHavenAPILoader;
385} else if (typeof window !== 'undefined') {
386 window.JsPolyHavenAPILoader = JsPolyHavenAPILoader;
387}
JsPolyHavenAPILoader - A JavaScript class for interacting with the Poly Haven API Handles fetching ma...
async downloadMaterialXContent(url)
Download MaterialX content from URL.
async downloadThumbnail(thumbnailUrl)
Download thumbnail image.
processMaterialsData(rawData)
Process raw materials data from API into structured format.
async getMaterialContent(materialId, resolution, textureFormat='')
Get MaterialX content and texture files for preview.
constructor()
Constructor for JsPolyHavenAPILoader.
async fetchMaterials()
Fetch all materials from Poly Haven API.
async createMaterialXPackage(material, resolution, preFetchedData=null)
Create a complete MaterialX package with all textures.
async fetchMaterialFiles(materialId)
Fetch MaterialX files data for a specific material.
async downloadTexture(url)
Download a texture file from URL.