Skip to content

Commit 456508f

Browse files
authored
Poly haven optimizations (#35)
* opt 1 * opt 2 * Swith to using fflate vs JsZip. * Move preprocessing to immediately after teching data to do it one regardless of where it's used: load, preview, download. Set default to jpg. With "desiredTextureFormat" URI argument to change. * Disable grid until get a loaded message from viewer page. * Cleanup.
1 parent fdde5af commit 456508f

5 files changed

Lines changed: 287 additions & 175 deletions

File tree

README.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,8 @@ <h4>Examples</h4>
430430
</tr>
431431
</table>
432432
</td>
433+
</tr>
434+
<tr>
433435
<td>
434436
<b>PhysicallyBased</b>
435437
<table>
@@ -470,6 +472,8 @@ <h4>Examples</h4>
470472
</tr>
471473
</table>
472474
</td>
475+
</tr>
476+
<tr>
473477
<td>
474478
<b>PolyHaven</b>
475479
<table>

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ See the <a href="https://kwokcb.github.io/materialxMaterials/examples/index.html
7878
</tr>
7979
</table>
8080
</td>
81+
</tr>
82+
<tr>
8183
<td>
8284
<b>PhysicallyBased</b>
8385
<table>
@@ -118,6 +120,8 @@ See the <a href="https://kwokcb.github.io/materialxMaterials/examples/index.html
118120
</tr>
119121
</table>
120122
</td>
123+
</tr>
124+
<tr>
121125
<td>
122126
<b>PolyHaven</b>
123127
<table>

javascript/JsPolyHaven/index.html

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@
7979
}
8080
}
8181
</style>
82-
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
82+
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script> -->
83+
<!-- <script src="https://cdn.jsdelivr.net/npm/fflate@0.7.3/umd/index.min.js"></script> -->
84+
<script src="https://cdn.jsdelivr.net/npm/fflate@0.8.2/umd/index.min.js"></script>
8385
<!-- Code mirror -->
8486
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.css">
8587
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/theme/material.min.css">
@@ -142,7 +144,7 @@
142144
</div>
143145
</div>
144146

145-
<div class="row" id="materialsContainer">
147+
<div class="row" id="materialsContainer" style="height: 512px; overflow-y: auto;">
146148
<!-- Materials will be loaded here -->
147149
</div>
148150
</div>

javascript/JsPolyHaven/jsPolyHavenLoader.js

Lines changed: 126 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ class JsPolyHavenAPILoader {
140140
throw new Error(`Failed to download texture from ${url}`);
141141
}
142142
else {
143-
console.log(`> Successfully downloaded texture from ${url}`);
143+
//console.log(`> Successfully downloaded texture from ${url}`);
144144
}
145145

146146
return await response.blob();
@@ -176,93 +176,87 @@ class JsPolyHavenAPILoader {
176176
* Create a complete MaterialX package with all textures
177177
* @param material Material object
178178
* @param resolution Resolution (1k, 2k, 4k, 8k)
179+
* @param preFetchedData Data if already fetched.
179180
* @returns ZIP file blob containing the complete package
180181
*/
181-
async createMaterialXPackage(material, resolution) {
182-
try {
183-
// Fetch MaterialX files data
184-
const filesData = await this.fetchMaterialFiles(material.id);
185-
const mtlxData = filesData.mtlx?.[resolution]?.mtlx;
186-
console.log('> createMaterialXPackage - fetched MaterialX data:', mtlxData);
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+
}
187196

188-
if (!mtlxData) {
189-
throw new Error(`No MaterialX files found for ${resolution} resolution`);
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 || {};
190212
}
191213

192-
// Create ZIP file
193-
const zip = new JSZip();
214+
// Fetch MaterialX files data
215+
console.log('> createMaterialXPackage - fetched MaterialX data:', mtlxData);
194216

195-
// 1. Download and add the main MaterialX file
196-
let mtlxContent = await this.downloadMaterialXContent(mtlxData.url);
217+
// Conatiner for Zip contents
218+
const zip = {}
197219

198-
// 2. Download and add all included texture files
199-
const textureFiles = mtlxData.include || {};
220+
// Download all referenced textures
200221
let texturePaths = [];
222+
let blobs = {};
201223

202-
const texturePromises = Object.entries(textureFiles).map(async ([path, fileData]) => {
224+
const texturePromises = Object.entries(textureFiles).map(([path, fileData]) => async () =>
225+
{
203226
try {
204227
console.log(`Processing texture: ${path} from URL: ${fileData.url}`);
205-
206-
let isEXR = path.toLowerCase().endsWith('.exr')
207-
// Try changing extension to .png.
208-
if (isEXR) {
209-
const prevPath = path;
210-
path = path.replace(/\.exr$/i, '.png');
211-
// Replace /exr/ with /png/
212-
fileData.url = fileData.url.replace(/\/exr\//i, '/png/');
213-
// Replace .exr with .png extension
214-
fileData.url = fileData.url.replace('.exr', '.png');
215-
// Replace .exr with .png in mtlxContent if present
216-
const previousMtlxContent = mtlxContent;
217-
mtlxContent = previousMtlxContent.replace(prevPath, path);
218-
219-
console.log(`EXR file detected. Path: ${prevPath} -> ${path}, URL: ${fileData.url}`);
220-
if (previousMtlxContent !== mtlxContent) {
221-
console.log(`Updated MaterialX content to replace .exr with .png for texture: ${path}`);
222-
}
223-
224-
}
225-
226228
const textureBlob = await this.downloadTexture(fileData.url);
227-
228-
// Maintain the folder structure from the include paths
229-
const pathParts = path.split('/');
230-
let currentFolder = zip;
231-
232-
// Handle nested folder structure
233-
for (let i = 0; i < pathParts.length - 1; i++) {
234-
const folderName = pathParts[i];
235-
currentFolder = currentFolder.folder(folderName);
236-
}
237-
238-
currentFolder.file(pathParts[pathParts.length - 1], textureBlob);
229+
texturePaths.push(path);
239230

240-
if (path.toLowerCase().endsWith('.exr')) {
241-
console.warn(`EXR file present which may not be supported by MaterialX texture loader: ${path}`);
242-
}
243-
else {
244-
console.log(`Added texture ${path} to ZIP from URL: ${fileData.url}`);
245-
zip.file(path, textureBlob);
246-
}
231+
// Build the local path for the zip (preserving folder structure)
232+
const pathParts = path.split('/');
233+
const localPath = pathParts.join('/');
247234

248-
texturePaths.push(path);
235+
blobs[localPath] = await blobToUint8Array(textureBlob);
249236

250237
} catch (error) {
251-
console.error(`Error downloading texture ${path}:`, error);
252-
// Add placeholder file if download fails
253-
zip.file(path, `Failed to download: ${fileData.url}`);
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}`);
254242
}
255243
});
256-
244+
await downloadWithConcurrency(texturePromises, 3);
257245

258-
// 3. Download and add thumbnail
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
259252
if (material.thumb_url) {
260253
try {
261254
const thumbBlob = await this.downloadThumbnail(material.thumb_url);
262255
const thumbUrl = new URL(material.thumb_url);
263256
const thumbPath = thumbUrl.pathname.split('/').pop();
264257
const thumbExt = thumbPath.split('.').pop();
265-
zip.file(`${material.id}_thumbnail.${thumbExt}`, thumbBlob);
258+
//console.log('Add thumbnail to ZIP:', thumbPath);
259+
zip[`${material.id}_thumbnail.${thumbExt}`] = await blobToUint8Array(thumbBlob);
266260
} catch (error) {
267261
console.error('Error downloading thumbnail:', error);
268262
}
@@ -271,24 +265,13 @@ class JsPolyHavenAPILoader {
271265
// Wait for all downloads to complete
272266
await Promise.all(texturePromises);
273267

274-
for (const textureName of texturePaths) {
275-
// Patch bad MTLX references in original file
276-
const extenson = textureName.split('.').pop();
277-
const exrName = textureName.replace(`.${extenson}`, `.exr`);
278-
if (mtlxContent.includes(exrName)) {
279-
console.log(`Replace ${exrName} with ${textureName} in MaterialX content for preview`);
280-
mtlxContent = mtlxContent.replace(exrName, textureName);
281-
}
282-
}
283-
284268
// Add Materialx document to ZIP.
285-
// This must be done after texture processing which may
286-
// modify the MTLX image references.
287-
zip.file(`${material.id}.mtlx`, mtlxContent);
288-
console.log(`Added MaterialX file to ZIP: ${material.id}.mtlx`); //, ${mtlxContent}`);
269+
//console.log(`Adding MaterialX file to ZIP: ${material.id}.mtlx`);
270+
zip[`${material.id}.mtlx`] = fflate.strToU8(mtlxContent);
289271

290-
// Add README file, and thumbnail to root of ZIP
291-
zip.file("README.txt",
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(
292275
`Material: ${material.name}\n` +
293276
`Resolution: ${resolution}\n` +
294277
`Source: https://polyhaven.com/a/${material.id}\n` +
@@ -299,8 +282,20 @@ class JsPolyHavenAPILoader {
299282
(texturePaths.length > 0 ? texturePaths.map(t => `- ${t}`).join('\n') + '\n' : '')
300283
);
301284

302-
// Generate the ZIP file
303-
return await zip.generateAsync({ type: 'blob' });
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;
304299

305300
} catch (error) {
306301
console.error('Error creating MaterialX package:', error);
@@ -312,9 +307,10 @@ class JsPolyHavenAPILoader {
312307
* Get MaterialX content and texture files for preview
313308
* @param materialId Material ID
314309
* @param resolution Resolution (1k, 2k, 4k, 8k)
310+
* @param textureFormat Optional texture format to remap references to (e.g. 'png'). If empty, original formats are used.
315311
* @returns Object containing MaterialX content and texture files
316312
*/
317-
async getMaterialContent(materialId, resolution) {
313+
async getMaterialContent(materialId, resolution, textureFormat = '') {
318314
try {
319315
const filesData = await this.fetchMaterialFiles(materialId);
320316
const mtlxData = filesData.mtlx?.[resolution]?.mtlx;
@@ -324,11 +320,57 @@ class JsPolyHavenAPILoader {
324320
}
325321

326322
// Get MaterialX content
327-
const mtlxContent = await this.downloadMaterialXContent(mtlxData.url);
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+
}
328370

329371
return {
330372
mtlxContent,
331-
textureFiles: mtlxData.include || {}
373+
textureFiles: textureFileData
332374
};
333375
} catch (error) {
334376
console.error('Error getting material content:', error);

0 commit comments

Comments
 (0)