MaterialXWeb 1.39.5
Utilities for using MaterialX Packages with Web clients
Loading...
Searching...
No Matches
JsAmbientCGLoader.js
1
2// Use global fetch if available (Node.js v18+), otherwise use node-fetch
3const fetch =
4 typeof globalThis.fetch === 'function'
5 ? globalThis.fetch
6 : (...args) => import('node-fetch').then(({ default: fetch }) => fetch(...args));
7const fs = require('fs');
8const { parse } = require('csv-parse/sync');
9
18 if (AmbientCGLoader.instance) {
19 return AmbientCGLoader.instance;
20 }
21
22 this.logger = console;
23 this.database = {};
24 this.materials = null;
25 this.materialNames = [];
26 this.csvMaterials = null;
27 this.downloadMaterial = null;
28 this.downloadMaterialFileName = '';
29
30 // Cache the instance
31 AmbientCGLoader.instance = this;
32 }
33
34 loadMaterialsFromCache() {
35 const MATERIALS_CACHE_FILE = 'ambientcg_materials.json';
36 if (fs.existsSync(MATERIALS_CACHE_FILE)) {
37 try {
38 const data = fs.readFileSync(MATERIALS_CACHE_FILE, 'utf8');
39 this.materials = JSON.parse(data);
40 this.logger.info(`Loaded AmbientCG materials from cache: ${MATERIALS_CACHE_FILE}`);
41 } catch (e) {
42 this.logger.warn(`Failed to load AmbientCG materials cache: ${e.message}`);
43 }
44 }
45 }
46
47 setDebugging(debug = true) {
52 this.logger.level = debug ? 'debug' : 'info';
53 }
54
55 getMaterialNames(key = 'assetId') {
61 this.materialNames = [];
62 const uniqueNames = new Set();
63 if (this.materials) {
64 this.materials.forEach(item => uniqueNames.add(item[key]));
65 }
66 this.materialNames = Array.from(uniqueNames).sort();
67 return this.materialNames;
68 }
69
70 writeMaterialList(materialList, filename) {
76 this.logger.info(`Writing material list to file: ${filename}`);
77 fs.writeFileSync(filename, JSON.stringify(materialList, null, 4));
78 }
79
80 buildDownloadAttribute(imageFormat = 'PNG', imageResolution = '1') {
87 return `${imageResolution}K-${imageFormat}`;
88 }
89
90 splitDownloadAttribute(downloadAttribute) {
97 const parts = downloadAttribute.split('-');
98 }
99
105 return {
106 filename: this.downloadMaterialFileName,
107 content: this.downloadMaterial
108 };
109 }
110
115 if (this.downloadMaterial) {
116 this.downloadMaterial = null;
117 }
118 this.downloadMaterialFileName = '';
119 }
120
126 const haveDownload = this.downloadMaterialFileName && this.downloadMaterial;
127 if (!haveDownload) {
128 this.logger.warning('No current material downloaded');
129 return;
130 }
131
132 const filename = `${path}/${this.downloadMaterialFileName}`;
133 fs.writeFileSync(filename, this.downloadMaterial);
134 this.logger.info(`Saved downloaded material to: ${filename}`);
135 }
136
137 async downloadMaterialAsset(assetId, imageFormat = 'PNG', imageResolution = '1', downloadAttributeKey = 'downloadAttribute', downloadLinkKey = 'downloadLink') {
148
149 const items = this.findMaterial(assetId);
150 const target = this.buildDownloadAttribute(imageFormat, imageResolution);
151 let url = '';
152 let downloadAttribute = '';
153
154 items.forEach(item => {
155 downloadAttribute = item[downloadAttributeKey];
156 if (downloadAttribute === target) {
157 url = item[downloadLinkKey];
158 this.logger.info(`Found Asset: ${assetId}. Download Attribute: ${downloadAttribute} -> ${url}`);
159 }
160 });
161
162 if (!url) {
163 this.logger.error(`No download link found for asset: ${assetId}, attribute: ${target}`);
164 return '';
165 }
166
167 this.downloadMaterialFileName = url.split('file=')[1];
168
169 try {
170 const response = await fetch(url);
171 if (!response.ok) {
172 throw new Error(`HTTP error! Status: ${response.status}`);
173 }
174
175 // Get the response as an ArrayBuffer
176 const arrayBuffer = await response.arrayBuffer();
177
178 // Convert ArrayBuffer to Buffer
179 this.downloadMaterial = Buffer.from(arrayBuffer);
180 this.logger.info(`Material file downloaded: ${this.downloadMaterialFileName}`);
181
182 } catch (error) {
183 this.downloadMaterialFileName = '';
184 this.logger.error(`Error occurred while downloading the file: ${error}`);
185 }
186
187 return this.downloadMaterialFileName;
188 }
189
190 findMaterial(assetId, key = 'assetId') {
197 if (this.materials) {
198 return this.materials.filter(item => item[key] === assetId);
199 }
200 return [];
201 }
202
209 this.materials = JSON.parse(fs.readFileSync(fileName, 'utf8'));
210 this.logger.info(`Loaded materials list from: ${fileName}`);
211 return this.materials;
212 }
213
219 let haveMaterials = false;
220 const MATERIALS_CACHE_FILE = 'ambientcg_materials.json';
221 // 1. Try to load from cache file
222 if (fs.existsSync(MATERIALS_CACHE_FILE)) {
223 try {
224 const data = fs.readFileSync(MATERIALS_CACHE_FILE, 'utf8');
225 this.materials = JSON.parse(data);
226 this.logger.info(`Loaded AmbientCG materials from cache: ${MATERIALS_CACHE_FILE}`);
227 haveMaterials = true;
228 } catch (e) {
229 this.logger.warn(`Failed to load AmbientCG materials cache: ${e.message}`);
230 }
231 }
232
233 // 2. If not in cache, fetch from network
234 if (!haveMaterials)
235 {
236 const headers = { Accept: 'application/csv' };
237 const url = new URL('https://ambientCG.com/api/v2/downloads_csv');
238 url.searchParams.append('method', 'PBRPhotogrammetry');
239 url.searchParams.append('type', 'Material');
240 url.searchParams.append('sort', 'Alphabet');
241
242 this.logger.info('Downloading materials CSV list from network...');
243 try {
244 const response = await fetch(url, { headers });
245 if (response.status === 200) {
246 const csvContent = await response.text();
247 this.csvMaterials = csvContent;
248 this.materials = parse(csvContent, { columns: true });
249 this.logger.info('Downloaded CSV material list as JSON.');
250 } else {
251 this.materials = null;
252 this.logger.warning(`Failed to fetch the CSV material content. HTTP status code: ${response.status}`);
253 }
254 } catch (error) {
255 this.materials = null;
256 this.logger.error(`Error downloading materials list: ${error}`);
257 }
258
259 // Read database list from file.
260 this.readDatabaseFromFile('ambientcg_database.json');
261 const have_database = this.database && Object.keys(this.database).length > 0;
262 if (have_database) {
263 for (let material of this.materials) {
264 const assetId = material.assetId;
265 const databaseEntry = this.database.find(entry => entry.assetId === assetId);
266 if (databaseEntry) {
267 // Get "256-PNG" from previewImage (object, not Map)
268 let preview = '';
269 if (databaseEntry.previewImage && typeof databaseEntry.previewImage === 'object') {
270 preview = databaseEntry.previewImage['256-PNG'] || '';
271 }
272 material.displayCategory = databaseEntry.displayCategory;
273 material.previewImage = preview;
274 material.tags = databaseEntry.tags;
275 }
276 else {
277 this.logger.warn('No database entry found for assetId:', assetId);
278 }
279
280 }
281 }
282
283 // Save to cache file after fetching
284 try {
285 fs.writeFileSync(MATERIALS_CACHE_FILE, JSON.stringify(this.materials, null, 2));
286 this.logger.info(`Saved AmbientCG materials to cache: ${MATERIALS_CACHE_FILE}`);
287 } catch (e) {
288 this.logger.warn(`Failed to write AmbientCG materials cache: ${e.message}`);
289 }
290 }
291
292 //console.log('>>> WRITE MATERIALS TO FILE:', this.materials.length);
293 //this.writeMaterialList(this.materials, 'ambientcg_materials.json');
294
295 return this.materials;
296 }
297
303 return this.database;
304 }
305
311 this.database = {};
312
313 haveDatabase = false;
314 const MATERIALS_DATABASE_FILE = 'ambientcg_database.json';
315 if (fs.existsSync(MATERIALS_DATABASE_FILE)) {
316 try {
317 const data = fs.readFileSync(MATERIALS_DATABASE_FILE, 'utf8');
318 this.database = JSON.parse(data);
319 this.logger.info(`Loaded AmbientCG database from file: ${MATERIALS_DATABASE_FILE}`);
320 haveDatabase = true;
321 } catch (e) {
322 this.logger.warn(`Failed to load AmbientCG database from file: ${e.message}`);
323 }
324 }
325
326 if (!haveDatabase) {
327
328 const limit = 500
329 //https://ambientcg.com/api/v2/full_json?type=material
330 const headers = { Accept: 'application/json' };
331 let url = new URL('https://ambientcg.com/api/v2/full_json');
332 url.searchParams.append('method', 'PBRPhotogrammetry');
333 url.searchParams.append('type', 'Material');
334 url.searchParams.append('sort', 'Alphabet');
335 url.searchParams.append('limit', limit);
336 url.searchParams.append('offset', 0);
337 url.searchParams.append('include', 'tagData,previewData')
338
339 let numberOfResults = -1;
340 let offset = 0
341
342 //let data_list = []
343 let asset_list = []
344 while (numberOfResults === -1 || offset < numberOfResults) {
345 this.logger.info(`Downloading asset database from: ${url.toString()}`);
346
347 try {
348 const response = await fetch(url, { headers });
349 if (response.status === 200) {
350 const data = await response.json();
351 this.logger.info(`Downloaded data at offset ${offset}. ${data.foundAssets.length} assets found.`);
352 //data_list.push(data);
353
354 // Only keep desired fiels: displayCategory, previewImage, tags and assetId from each entry
355 let reduced_assets = data.foundAssets.map(asset => {
356 return {
357 assetId: asset.assetId,
358 displayCategory: asset.displayCategory,
359 previewImage: asset.previewImage,
360 tags: asset.tags
361 }
362 });
363 asset_list = asset_list.concat(reduced_assets);
364
365 if (numberOfResults === -1) {
366 numberOfResults = data.numberOfResults;
367 }
368 // Update offset for next page
369 if (!data.nextPageHttp)
370 {
371 break;
372 }
373 offset = offset + limit;
374
375 url.searchParams.set('offset', offset);
376 } else {
377 this.logger.error(`Status: ${response.status}, ${response.data}`);
378 }
379 }
380 catch (error) {
381 this.logger.error(`Error downloading asset database: ${error}`);
382 }
383 }
384
385 this.database = asset_list;
386
387 // Pull out the preview image link from database and put it
388 // into the materials.
389 }
390
391 return this.database;
392 }
393
400 if (!this.database) {
401 this.logger.info('No database to write');
402 return false;
403 }
404
405 this.logger.info(`Writing database to file: ${filename}`);
406 fs.writeFileSync(filename, JSON.stringify(this.database, null, 4));
407 return true;
408 }
409
416 try {
417 const data = fs.readFileSync(filename, 'utf8');
418 this.database = JSON.parse(data);
419 return this.database;
420 } catch (e) {
421 this.logger.error(`Failed to read database from file: ${e.message}`);
422 return null;
423 }
424 }
425
432 if (!this.mx) {
433 this.logger.error('MaterialX module is required');
434 return [false, ''];
435 }
436
437 if (!doc) {
438 this.logger.warning('MaterialX document is required');
439 return [false, ''];
440 }
441
442 const valid = doc.validate();
443 return [valid, valid ? '' : 'Validation failed'];
444 }
445
446 addComment(doc, commentString) {
452 const comment = doc.addChildOfCategory('comment');
453 comment.setDocString(commentString);
454 }
455
462 if (!this.mx) {
463 this.logger.error('MaterialX module is required');
464 return;
465 }
466
467 const writeOptions = this.mx.XmlWriteOptions();
468 writeOptions.writeXIncludeEnable = false;
469 writeOptions.elementPredicate = this.skipLibraryElement;
470 return this.mx.writeToXmlString(doc, writeOptions);
471 }
472}
473
474module.exports = new AmbientCGLoader();
buildDownloadAttribute(imageFormat='PNG', imageResolution='1')
setDebugging(debug=true)
constructor()
Class to load materials from the AmbientCG site.
async downloadMaterialAsset(assetId, imageFormat='PNG', imageResolution='1', downloadAttributeKey='downloadAttribute', downloadLinkKey='downloadLink')
writeDownloadedMaterialToFile(path='')
addComment(doc, commentString)
findMaterial(assetId, key='assetId')
getMaterialNames(key='assetId')
readDatabaseFromFile(filename)
writeDatabaseToFile(filename)
splitDownloadAttribute(downloadAttribute)
writeMaterialList(materialList, filename)