MaterialXMaterials 1.39.5
Utilities for retrieving materials from remote servers
Loading...
Searching...
No Matches
JsMaterialXPhysicallyBased.js
1let MTLX_NODE_NAME_ATTRIBUTE = 'nodename';
2
13 url = '';
18 headers = {};
23 materials = null;
38 mx = null;
43 doc = null;
48 stdlib = null;
54
60 constructor(mtlx_module = null, mtlx_stdlib = null)
61 {
62 this.url = 'https://api.physicallybased.info/materials';
63 this.headers = { 'Accept': 'application/json' };
64
65 this.materials = null;
66 this.materialNames = [];
67
68 this.mxMaterialNames = [];
69 this.mx = null;
70 if (mtlx_module) {
71 this.mx = mtlx_module;
72 }
73 this.stdlib = null;
74 if (mtlx_stdlib) {
75 this.stdlib = mtlx_stdlib;
76 }
77 this.doc = null;
78
80
81 this.physlib = null;
82 // PhysicallyBased MaterialX surface definition name
83 this.physlib_definition_name = "ND_PhysicallyBasedMaterial";
84 // PhysicallyBased MaterialX surface implementation (nodegraph) name
85 this.physlib_implementation_name = "NG_PhysicallyBasedMaterial";
86 // PhysicallyBased MaterialX surface category
87 this.physlib_category = "physbased_pbr_surface"
88 // Document containing PhysicallyBased materials using PhysicallyBasedMaterial definition
89 this.physlib_materials = null
90 // Document containing PhysicallyBased MaterialX translators
91 this.physlib_translators = null
92 // All MaterialX definitions (standard library + PhysicallyBased definition + translators)
93 this.all_lib = null;
94 // Translated materials keyed by shading model
95 this.translated_materials = {};
96 }
97
103 {
104 return this.materials
105 }
106
111 {
112 return this.materialNames
113 }
114
119 {
120 return this.doc;
121 }
122
128 {
129 if (this.doc) {
130 let errors = {}
131 let errorString = ''
132 var valid = this.doc.validate(errors);
133 if (!valid) {
134 errorString = errors.message;
135 }
136 return [valid, errorString]
137 }
138 return [false, 'No MaterialX document'];
139 }
140
146 getInputRemapping(shadingModel)
147 {
148 if (shadingModel in this.remapMap) {
149 return this.remapMap[shadingModel];
150 }
151 else
152 {
153 console.log('>> No remap keys for shading model:', shadingModel);
154 }
155 return {};
156 }
157
163 {
164 return this.remapMap;
165 }
166
167 get_physlib() {
168 // Get the Physically Based MaterialX definition library.
169 return this.physlib;
170 }
171
172 get_physlib_definition() {
173 // Get the Physically Based MaterialX definition NodeDef.
174 if (this.physlib) {
175 return this.physlib.getNodeDef(this.get_physlib_definition_name());
176 }
177 return null;
178 }
179
180 get_physlib_category() {
181 // Get the Physically Based MaterialX surface category.
182 return this.physlib_category;
183 }
184
185 get_physlib_definition_name() {
186 // Get the Physically Based MaterialX definition name.
187 return this.physlib_definition_name;
188 }
189
190 get_physlib_implementation_name() {
191 // Get the Physically Based MaterialX implementation (nodegraph) name.
192 return this.physlib_implementation_name;
193 }
194
195 get_physlib_materials() {
196 // Get the Physically Based MaterialX materials document.
197 return this.physlib_materials;
198 }
199
200 get_definitions() {
201 // Get a combined MaterialX document containing the standard library and Physically Based MaterialX definition and translators.
202 if (!this.all_lib) {
203 this.all_lib = this.mx.createDocument();
204 this.all_lib.copyContentFrom(this.stdlib);
205 if (this.physlib) {
206 this.all_lib.copyContentFrom(this.physlib);
207 }
208 if (this.physlib_translators) {
209 this.all_lib.copyContentFrom(this.physlib_translators);
210 }
211 }
212 return this.all_lib;
213 }
214
215 map_keys_to_definition(mat, ndef) {
216 // Map a key to a NodeDef input
217 for (let [key, value] of Object.entries(mat)) {
218 let uifolder = null;
219 if (!ndef.getInput(key)) {
220 //if (key === 'name') {
221 // Skip as these will be node instance names
222 // continue;
223 //}
224
225 let input_type = "string";
226 if (key.toLowerCase().includes('color')) {
227 input_type = "color3";
228 value = "1,1,1";
229 } else if (typeof value === 'number' && !Number.isInteger(value)) {
230 input_type = "float";
231 value = "0.0";
232 } else if (typeof value === 'number' && Number.isInteger(value)) {
233 input_type = "float";
234 value = "0.0";
235 } else if (key === 'category') {
236 // category handling (optional)
237 } else if (["sources", "reference", "tags", "group"].includes(key)) {
238 value = '';
239 }
240 //console.log(`> Add key as input: ${key}. Type: ${typeof value}. input_type: ${input_type}, value: ${value}`);
241 let input = ndef.addInput(key, input_type);
242 if (input) {
243 if (value !== undefined && value !== null) {
244 if (Array.isArray(value)) {
245 let value_list = value.map(x => String(x));
246 let is_number_list = value.every(x => typeof x === 'number');
247 if (is_number_list) {
248 if (value_list.length > 4) {
249 input_type = 'string'; // floatarray will cause errors in shader generation!
250 } else if (value_list.length > 3) {
251 input_type = 'vector4';
252 } else if (value_list.length > 2) {
253 input_type = 'vector3';
254 } else if (value_list.length > 1) {
255 input_type = 'vector2';
256 } else {
257 input_type = 'float';
258 }
259 value_list = value_list.map(() => '0.0');
260 }
261 value = value_list.join(', ');
262 }
263 input.setValueString(String(value));
264 input.setType(input_type);
265 }
266 // Split camel case names and separate by space, e.g. specularColor -> Specular Color
267 let uiname = key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase()).trim();
268 input.setAttribute("uiname", uiname);
269 uifolder = 'Base';
270 if (["description", "sources", "reference", "tags"].includes(key)) {
271 uifolder = 'Metadata';
272 }
273 input.setAttribute("uifolder", uifolder);
274 }
275 }
276 }
277 }
278
279 find_all_bxdf(doc) {
280 // Scan all nodedefs with output type of "surfaceshader"
281 let bxdfs = [];
282 for (let nodedef of doc.getNodeDefs()) {
283 if (nodedef.getType() === "surfaceshader") {
284 if (nodedef.getNodeString() !== "convert" && nodedef.getNodeString() !== "surface" && nodedef.getNodeGroup() === "pbr") {
285 bxdfs.push(nodedef);
286 }
287 }
288 }
289 return bxdfs;
290 }
291
292 derive_translator_name_from_targets(source, target )
293 {
294 return `ND_${source}_to_${target}`;
295 }
296
297 create_translator(doc, source, target, source_version = "", target_version = "", mappings = null, output_doc) {
309 if (!output_doc) {
310 return null;
311 }
312 // Get source and target nodedefs
313 let nodedefs = this.find_all_bxdf(doc);
314 let source_nodedef = null;
315 let target_nodedef = null;
316 for (let nodedef of nodedefs) {
317 if (nodedef.getNodeString() === source) {
318 if (source_version === "" || nodedef.getVersionString() === source_version) {
319 source_nodedef = nodedef;
320 }
321 }
322 if (nodedef.getNodeString() === target) {
323 if (target_version === "" || nodedef.getVersionString() === target_version) {
324 target_nodedef = nodedef;
325 }
326 }
327 }
328 if (!source_nodedef || !target_nodedef) {
329 if (!source_nodedef) {
330 console.warn(`Source nodedef not found for '${source}' with version '${source_version}'`);
331 }
332 if (!target_nodedef) {
333 console.warn(`Target nodedef not found for '${target}' with version '${target_version}'`);
334 }
335 return null;
336 }
337 //console.log('******* Source nodedef:\n', this.mx.prettyPrint(source_nodedef));
338
339 // 1. Add a new nodedef for the translator
340 let derived_name = this.derive_translator_name_from_targets(source, target);
341 let nodename = derived_name.startsWith("ND_") ? derived_name.substring(3) : derived_name;
342 let translator_nodedef = output_doc.getNodeDef(derived_name);
343 if (translator_nodedef) {
344 console.log(`> Translator NodeDef already exists: ${target_nodedef.getName()}`);
345 return translator_nodedef;
346 }
347 else {
348 console.log(`> Creating Translator NodeDef: ${nodename} from '${source}' to '${target}'`);
349 }
350
351 translator_nodedef = output_doc.addNodeDef(derived_name);
352 translator_nodedef.removeOutput("out");
353 translator_nodedef.setNodeString(nodename);
354 translator_nodedef.setNodeGroup("translation");
355 translator_nodedef.setDocString(`Translator from '${source}' to '${target}'`);
356 let version1 = source_nodedef.getVersionString();
357 if (!version1) version1 = "1.0";
358 translator_nodedef.setAttribute('source_version', version1);
359 translator_nodedef.setAttribute('source', source);
360 let version2 = target_nodedef.getVersionString();
361 if (!version2) version2 = "1.0";
362 translator_nodedef.setAttribute('target_version', version2);
363 translator_nodedef.setAttribute('target', target);
364
365 // Add inputs from source as inputs to the translator
366 let comment = translator_nodedef.addChildOfCategory("comment");
367 comment.setDocString(`Inputs (inputs from source '${source}')`);
368 let inputs = source_nodedef.getActiveInputs();
369 //console.log('>>>>>>>>>>>>>> ADD Inputs from source nodedef:', inputs.length);
370 for (let input of inputs) {
371 //console.log('-------------------------- Input:', input.getName(), input.getType());
372 let nodedef_input = translator_nodedef.addInput(input.getName(), input.getType());
373 if (input.hasValueString()) {
374 nodedef_input.setValueString(input.getValueString(), input.getType());
375 }
376 }
377
378 // Add inputs from target as outputs to the translator
379 comment = translator_nodedef.addChildOfCategory("comment");
380 comment.setDocString(`Outputs (inputs from target '${target}' with '_out' suffix)`);
381 for (let input of target_nodedef.getActiveInputs()) {
382 let output_name = input.getName() + "_out";
383 translator_nodedef.addOutput(output_name, input.getType());
384 }
385 // 2. Create a new functional nodegraph
386 comment = output_doc.addChildOfCategory("comment");
387 comment.setDocString(`NodeGraph implementation for translator '${nodename}'`);
388 let nodegraph_id = 'NG_' + nodename;
389 let nodegraph = output_doc.addNodeGraph(nodegraph_id);
390 nodegraph.setNodeDefString(derived_name);
391 nodegraph.setDocString(`NodeGraph implementation of translator from '${source}' to '${target}'`);
392 nodegraph.setAttribute('source_version', version1);
393 nodegraph.setAttribute('source', source);
394 nodegraph.setAttribute('target_version', version2);
395 nodegraph.setAttribute('target', target);
396 for (let output of translator_nodedef.getActiveOutputs()) {
397 nodegraph.addOutput(output.getName(), output.getType());
398 }
399 if (mappings) {
400 for (let [source_input_name, target_input_name] of Object.entries(mappings)) {
401 let source_input = translator_nodedef.getInput(source_input_name);
402 let output_name = target_input_name + "_out";
403 let target_output = nodegraph.getOutput(output_name);
404 if (source_input && target_output) {
405 let dot_name = nodegraph.createValidChildName(target_input_name);
406 let comment = nodegraph.addChildOfCategory("comment");
407 comment.setDocString(`Routing source input: '${source_input_name}' to target input: '${target_input_name}'`);
408 let dot_node = nodegraph.addNode('dot', dot_name);
409 let dot_input = dot_node.addInput('in', source_input.getType());
410 dot_input.setInterfaceName(source_input.getName());
411 target_output.setNodeName(dot_node.getName());
412 }
413 }
414 }
415 return translator_nodedef;
416 }
417
418 create_all_translators(definitions, output_doc = null) {
425 let trans_nodedefs = [];
426 // Create temporary doc with all standard library definitions
427 //let result = this.create_working_document();
428 let trans_doc = this.mx.createDocument();
429 let stdlib_defs = this.stdlib.getNodeDefs();
430 console.log('> --------------------->>>>>>>>>> Use stdlib', stdlib_defs.length, 'definitions');
431 trans_doc.setDataLibrary(this.stdlib);
432
433 // Add Physically Based Material definitions to the temporary document
434 trans_doc.copyContentFrom(definitions);
435 this.add_copyright_docstring(output_doc, '');
436 if (!output_doc) {
437 console.log('No output document specified for translators');
438 return trans_nodedefs;
439 }
440 // Source BSDF is always physbased_pbr_surface
441 let source_bsdf = this.physlib_category;
442 // Iterate over all target BSDFs
443 let bsdfs = this.find_all_bxdf(trans_doc);
444 for (let bsdf of bsdfs) {
445 let bsdf_name = bsdf.getNodeString();
446 if (bsdf_name === this.physlib_category) {
447 continue;
448 }
449 let target_bsdf = bsdf.getNodeString();
450 let remapping = this.getInputRemapping(target_bsdf);
451 if (remapping && Object.keys(remapping).length > 0) {
452 let trans_nodedef = this.create_translator(trans_doc, source_bsdf, target_bsdf, "", "", remapping, output_doc);
453 if (trans_nodedef) {
454 console.log(`> Created translator to BSDF: ${bsdf_name}`);
455 trans_nodedefs.push(trans_nodedef);
456 }
457 }
458 }
459 return trans_nodedefs;
460 }
461
462 add_copyright_comment(doc, shaderCategory, embedDate = false) {
463
464 // Add header comments
465 this.addComment(doc, 'Physically Based Materials from https://api.physicallybased.info ');
466 this.addComment(doc, ' Content Author: Anton Palmqvist, https://antonpalmqvist.com/ ');
467 this.addComment(doc, ` Content processsed via REST API and mapped to MaterialX V${this.mx.getVersionString()} `);
468 if (shaderCategory) {
469 this.addComment(doc, ` Target Shading Model: ${shaderCategory} `);
470 }
471 this.addComment(doc, ' Utility Author: Bernard Kwok. kwokcb@gmail.com ');
472 if (embedDate) {
473 const now = new Date();
474 const dt_string = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0') + '-' + String(now.getDate()).padStart(2, '0') + ' ' + String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0') + ':' + String(now.getSeconds()).padStart(2, '0');
475 this.addComment(doc, ` Generated on: ${dt_string} `);
476 }
477 }
478
485 if (!doc) {
486 doc = this.mx.createDocument();
488 }
489
490 // Create placeholder nodegraph
491 let graph = doc.addNodeGraph(this.get_physlib_implementation_name());
492 graph.setNodeDefString(this.get_physlib_definition_name());
493 let node = graph.addNode('oren_nayar_diffuse_bsdf', 'oren_nayar_diffuse_bsdf', 'BSDF');
494 let node_in = node.addInput('color', 'color3');
495 node_in.setInterfaceName('color');
496 let node_in_rough = node.addInput('roughness', 'float');
497 node_in_rough.setInterfaceName('roughness');
498 node.addOutput('out', 'BSDF');
499 node = graph.addNode('surface', 'surface', 'surfaceshader');
500 node_in = node.addInput('bsdf', 'BSDF');
501 node_in.setAttribute('out', 'out');
502 node_in.setNodeName('oren_nayar_diffuse_bsdf');
503 let node_out = graph.addOutput('out', 'surfaceshader');
504 node_out.setNodeName('surface');
505
506 // Create definition template
507 let ndef = doc.addNodeDef(this.get_physlib_definition_name(), 'surfaceshader');
508 ndef.setNodeString(this.physlib_category);
509 ndef.setNodeGroup("pbr");
510 ndef.setDocString("Node definitions for PhysicallyBased Material");
511 ndef.setVersionString("1.0");
512 ndef.setAttribute("isdefaultversion", "true");
513
514 if (this.materials) {
515 console.log('> Map keys to definition for materials:', this.materials.length);
516 for (let mat of this.materials) {
517 this.map_keys_to_definition(mat, ndef);
518 }
519 }
520 else {
521 console.log('> No materials to map keys to definition');
522 }
523 return doc;
524 }
525
526 create_definition_materials(doc_mat, filter_list = null) {
533 let definitions = this.get_definitions();
534 if (!doc_mat) {
535 doc_mat = this.mx.createDocument();
536 this.add_copyright_docstring(doc_mat, '');
537 }
538 // Reference the library definitions into the material document
539 doc_mat.setDataLibrary(definitions);
540 for (let mat of this.materials) {
541
542 let matName = mat['name'];
543 if (filter_list && !filter_list.includes(matName)) {
544 continue;
545 }
546
547 let shaderName = doc_mat.createValidChildName(matName + '_SHD_PBM');
548 //console.log('************* Create material for:', mat['name'], '-> shader name:', shaderName, filter_list);
549
550 let shaderNode = doc_mat.addNode(this.physlib_category, shaderName, this.mx.SURFACE_SHADER_TYPE_STRING);
551 for (let [key, value] of Object.entries(mat)) {
552 if (!value){
553 continue;
554 }
555
556 if (key === 'name') {
557 let new_name = doc_mat.createValidChildName(String(value));
558 shaderNode.setName(new_name);
559 }
560 let input = shaderNode.addInputFromNodeDef(key);
561 if (input) {
562 //console.log('> Add input for key:', key, 'value:', value, 'input:', input.getName());
563 if (Array.isArray(value)) {
564 value = value.map(x => String(x)).join(', ');
565 }
566 input.setValueString(String(value), input.getType());
567 // Add doc string
568 let doc_string = '';
569 if (key === 'description') {
570 doc_string = String(value);
571 }
572 if (doc_string.length > 0) {
573 shaderNode.setDocString(doc_string);
574 }
575 }
576 }
577 shaderNode.setAttribute('uiname', matName);
578
579 // Create a new material
580 let materialName = doc_mat.createValidChildName(matName + '_MAT_PBM');
581 let materialNode = doc_mat.addNode(this.mx.SURFACE_MATERIAL_NODE_STRING, materialName, this.mx.MATERIAL_TYPE_STRING);
582 let shaderInput = materialNode.addInput(this.mx.SURFACE_SHADER_TYPE_STRING, this.mx.SURFACE_SHADER_TYPE_STRING);
583 shaderInput.setAttribute(MTLX_NODE_NAME_ATTRIBUTE, shaderNode.getName());
584 }
585 return doc_mat;
586 }
587
588 find_translator(doc, source, target) {
596 let derived_name = this.derive_translator_name_from_targets(source, target);
597 let translator_nodedef = doc.getNodeDef(derived_name);
598 return translator_nodedef;
599 }
600
601 translate_node(doc, source_bxdf, target_bxdf, node) {
611 let nodedef = this.find_translator(doc, source_bxdf, target_bxdf);
612 if (!nodedef) {
613 console.warn(`- No translator found from '${source_bxdf}' to '${target_bxdf}' for node '${node.getName()}'`);
614 return null;
615 }
616 // Create a target node of the target_bxdf category.
617 let replace_name = node.getName();
618 let target_node_name = doc.createValidChildName(`${replace_name}_${target_bxdf}_SPB`);
619
620 // Cleanup dowstream connections
621 let downstream_ports = node.getDownstreamPorts()
622 for (let port of downstream_ports) {
623 //console.log('Scan downstream port:', port.getName(), 'of node:', port.getParent().getName());
624 let downstream_node = port.getParent();
625 let downstream_input = downstream_node.getInput(port.getName());
626 if (downstream_input) {
627 //console.log(` - Reconnecting downstream node '${downstream_node.getName()}' input '${downstream_input.getName()}' from '${node.getName()}' to target node '${target_node_name}'`);
628 downstream_input.setNodeName(target_node_name);
629 }
630 }
631
632 let targetNode = doc.addChildOfCategory(target_bxdf, target_node_name);
633 if (!targetNode) {
634 console.warn(`- Failed to create target node of category '${target_bxdf}' for node '${node.getName()}'`);
635 return null;
636 }
637 targetNode.setType("surfaceshader");
638 // WARNING: addInputsFromNodeDef() is missing from MaterialX JS API as of 1.39.5 !
639 //targetNode.addInputsFromNodeDef();
640 //const targetNodeDef = targetNode.getNodeDef();
641 //for (let input of targetNodeDef.getActiveInputs()) {
642 // let targetInput = targetNode.addInputFromNodeDef(input.getName());
643 //}
644
645 // Create a translation node based on the translator nodedef.
646 let translationNode = doc.addNodeInstance(nodedef,
647 targetNode.getName() + "_translator");
648
649 // Copy over inputs from the source node to the translation node.
650 let num_overrides = 0;
651 for (let input of node.getActiveInputs()) {
652 let translationInput = translationNode.addInputFromNodeDef(input.getName());
653 if (translationInput) {
654 translationInput.copyContentFrom(input);
655 num_overrides += 1;
656 }
657 }
658
659 // Connect translation outputs to target inputs.
660 const impl = nodedef.getImplementation();
661 for (let output of nodedef.getActiveOutputs()) {
662
663 // Avoid adding ports which do not route an input data
664 const impl_output = impl.getOutput(output.getName());
665 if (!impl_output.getConnectedNode()) {
666 continue;
667 }
668
669 let target_input_name = output.getName();
670 // Remove trailing '_out' from name
671 if (target_input_name.endsWith('_out')) {
672 target_input_name = target_input_name.slice(0, -4);
673 }
674
675 let translationOutput = translationNode.addOutput(output.getName(), output.getType());
676 translationOutput.copyContentFrom(output);
677
678 let target_input = targetNode.addInputFromNodeDef(target_input_name);
679 if (!target_input) {
680 console.info(` - Warning: Target node '${targetNode.getName()}' has no input named '${target_input_name}' for output '${output.getName()}'`);
681 continue;
682 } else {
683 target_input.setNodeName(translationNode.getName());
684 target_input.setOutputString(translationOutput.getName());
685 target_input.removeAttribute('value');
686 }
687 }
688
689
690 // Remove original node
691 doc.removeNode(replace_name);
692
693 return { 'translationNode': translationNode, 'targetNode': targetNode };
694 }
695
696
697 initialize_definitions_and_materials(shadingModel = 'standard_surface', materialNames = [], force=false) {
702
703 console.log('> Generate for shading model:', shadingModel, 'materials filer:', materialNames);
704
705 if (!this.physlib || force) {
706
707 // Create Physically Based MaterialX definition library
708 this.physlib = this.create_definition(null);
709 console.log('> Created Physically Based MaterialX definition library...');
710 }
711
712 let translated_doc = null;
713 let untranslated_doc = null;
714 if (!this.physlib_materials || !this.physlib_translators || force) {
715 // Create all translators
716 this.physlib_translators = this.mx.createDocument();
717 this.create_all_translators(this.physlib, this.physlib_translators);
718
719 // Create Physically Based MaterialX materials library
720 let filter_list = null;
721 this.physlib_materials = this.create_definition_materials(null, filter_list);
722 }
723
724 untranslated_doc = this.physlib_materials;
725
726 if (materialNames.length > 0) {
727
728 // Translate specified materials
729 //
730 untranslated_doc = this.create_definition_materials(null, materialNames);
731 translated_doc = this.mx.createDocument();
732 translated_doc.copyContentFrom(untranslated_doc);
733 translated_doc.setDataLibrary(this.get_definitions());
734 const category = this.get_physlib_category()
735 for (const node of translated_doc.getNodes()) {
736 if (materialNames.length > 0 && !materialNames.includes(node.getName())) {
737 continue;
738 }
739 if (node.getCategory() == 'physbased_pbr_surface')
740 {
741 console.log('> Translate specified material:', node.getName());
742 let result = this.translate_node(translated_doc, 'physbased_pbr_surface', shadingModel, node)
743 }
744 }
745
746 }
747 else
748 {
749 // Translate all material
750 //
751 if (!(shadingModel in this.translated_materials) || force) {
752
753 const category = this.get_physlib_category()
754 console.log('> Translating all materials from', category, 'to', shadingModel);
755
756 // Create translated materials for specified shader model
757 translated_doc = this.mx.createDocument();
758 translated_doc.copyContentFrom(this.physlib_materials);
759 translated_doc.setDataLibrary(this.get_definitions());
760 for (const node of translated_doc.getNodes()) {
761 if (node.getCategory() == 'physbased_pbr_surface')
762 {
763 let result = this.translate_node(translated_doc, 'physbased_pbr_surface', shadingModel, node)
764 }
765 }
766 this.translated_materials[shadingModel] = translated_doc;
767 }
768 else {
769 translated_doc = this.translated_materials[shadingModel];
770 }
771 }
772
773 let def_string = this.mx.writeToXmlString(this.physlib);
774 //console.log('> Physically Based MaterialX Definition Library:\n', def_string);
775 let trans_string = this.mx.writeToXmlString(this.physlib_translators);
776 //console.log('> Physically Based MaterialX Translators Library:\n', trans_string);
777 let mat_string = this.mx.writeToXmlString(untranslated_doc);
778 //console.log('> Physically Based MaterialX Materials Library:\n', mat_string);
779 let mat_trans_string = this.mx.writeToXmlString(translated_doc);
780
781 return { 'bsdf': def_string,
782 'bsdf_trans': trans_string,
783 'bsdf_materials': mat_string,
784 'bsdf_trans_materials': mat_trans_string };
785 }
786
787
793 {
794 const standard_surface_remapKeys = {
795 "color": "base_color",
796 "specularColor": "specular_color",
797 "roughness": "specular_roughness",
798 "metalness": "metalness",
799 "ior": "specular_IOR",
800 "subsurfaceRadius": "subsurface_radius",
801 "transmission": "transmission",
802 "transmission_color": "transmission_color",
803 "transmissionDispersion": "transmission_dispersion",
804 "thinFilmThickness": "thin_film_thickness",
805 "thinFilmIor": "thin_film_IOR"
806 };
807
808 const openpbr_remapKeys = {
809 "color": "base_color",
810 "specularColor": "specular_color",
811 "roughness": "specular_roughness",
812 "metalness": "base_metalness",
813 "ior": "specular_ior",
814 "subsurfaceRadius": "subsurface_radius",
815 "transmission": "transmission_weight",
816 "transmission_color": "transmission_color",
817 "transmissionDispersion": "transmission_dispersion_abbe_number",
818 "thinFilmThickness": "thin_film_thickness",
819 "thinFilmIor": "thin_film_ior"
820 };
821
822 const gltf_remapKeys = {
823 "color": "base_color",
824 "specularColor": "specular_color",
825 "roughness": "roughness",
826 "metalness": "metallic",
827 "ior": "ior",
828 "transmission": "transmission",
829 "transmission_color": "attenuation_color",
830 "thinFilmThickness": "iridescence_thickness",
831 "thinFilmIor": "iridescence_ior"
832 };
833
834 this.remapMap = {
835 'standard_surface': standard_surface_remapKeys,
836 'gltf_pbr': gltf_remapKeys,
837 'open_pbr_surface': openpbr_remapKeys
838 };
839 }
840
841
847 {
848 console.log('Initializing input remapping for Physically Based Materials...');
849 this.remapMap = null;
850
851 const remapKeyURL = 'https://raw.githubusercontent.com/kwokcb/materialxMaterials/refs/heads/main/src/materialxMaterials/data/PhysicallyBasedMaterialX/PhysicallyBasedToMtlxMappings.json';
852
853 fetch(remapKeyURL)
854 .then((response) =>
855 {
856 if (!response.ok) {
857 console.warn(`HTTP error! Status: ${response.status}`);
858 return null;
859 }
860 return response.json();
861 })
862 .then((data) => {
863 if (data) {
864 this.remapMap = data;
865 console.log('- Remap keys loaded from repo:', this.remapMap);
866 } else {
867 console.log('- No remap keys from repo. Using default remap keys.');
868 this.setDefaultRemapKeys();
869 }
870 })
871 .catch((error) => {
872 console.log('- Error loading remap keys:', error);
873 this.setDefaultRemapKeys();
874 console.warn('- Using default remap keys.', this.remapMap);
875 });
876 }
877
883 {
884 return new Promise((resolve, reject) => {
885 MaterialX().then((mtlx) => {
886 this.mx = mtlx;
887 resolve();
888 }).catch((error) => {
889 reject(error);
890 });
891 });
892 }
893
899 {
900 try {
901 // Re-initialize cached information
902 this.materials = null
903 this.materialNames = [];
904 this.physlib = null;
905 this.physlib_materials = null;
906 this.physlib_translators = null;
907 this.translated_materials = {};
908
909 const response = await fetch(this.url, {
910 method: 'GET',
911 headers: this.headers
912 });
913
914 if (!response.ok) {
915 throw new Error('Network response was not ok ' + response.statusText);
916 }
917
918 this.materials = await response.json();
919 for (let i = 0; i < this.materials.length; i++) {
920 this.materialNames.push(this.materials[i]['name']);
921 }
922 return this.materials;
923 } catch (error) {
924 console.error('There has been a problem with your fetch operation:', error);
925 }
926
927 return null;
928 }
929
935 {
936 if (!this.mx) {
937 // Call the asynchronous function and then perform additional logic
938 this.loadMaterialX().then(() => {
939
940 this.esslgenerator = new this.mx.EsslShaderGenerator.create();
941 this.esslgenContext = new this.mx.GenContext(this.esslgenerator);
942 this.stdlib = this.mx.loadStandardLibraries(this.esslgenContext);
943 let children = this.stdlib.getChildren();
944 for (let i = 0; i < children.length; i++) {
945 let child = children[i];
946 child.setSourceUri('STDLIB_ELEMENT');
947 }
948
949 let nodedefs = this.stdlib.getNodeDefs();
950 console.log("MaterialX is loaded. With ", nodedefs.length, "definitions.");
951 }).catch((error) => {
952 console.error("Error loading MaterialX:", error);
953 });
954 }
955 }
956
963 {
964 return !elem.hasSourceUri()
965 }
966
972 {
973 if (!this.doc) {
974 console.error('No MaterialX document to convert');
975 return '';
976 }
977
978 // Create write options
979 const writeOptions = new this.mx.XmlWriteOptions();
980 writeOptions.writeXIncludeEnable = false;
981 //writeOptions.writeXIncludes = false;
982 writeOptions.elementPredicate = this.skipLibraryElement;
983
984 // Convert the MaterialX document to a string
985 const mtlx = this.mx.writeToXmlString(this.doc, writeOptions);
986 return mtlx;
987 }
988
994 addComment(doc, commentString)
995 {
996 let comment = doc.addChildOfCategory('comment')
997 comment.setDocString(commentString)
998 }
999
1000
1006 {
1007 let references = [];
1008 if (this.materials) {
1009 for (let i = 0; i < this.materials.length; i++) {
1010 const mat = this.materials[i];
1011 const matName = mat['name'];
1012 const refString = mat['reference'];
1013 const tags = mat['tags'];
1014 const category = mat['category'];
1015 if (refString.length > 0)
1016 {
1017 let referenceItem = { name: matName, reference: refString[0], tags: tags, category: category };
1018 //console.log('Add Reference:', referenceItem);
1019 references.push(referenceItem);
1020 }
1021 }
1022 // Sort references by name
1023 references.sort((a, b) => a.name.localeCompare(b.name));
1024 }
1025 return references;
1026 }
1027
1033 add_copyright_docstring(doc, shaderCategory = '')
1034 {
1035 // Add document level accreditation
1036 let docString = 'Physically Based Materials from https://api.physicallybased.info.\n'
1037 docString += ' Content Author: Anton Palmqvist, https://antonpalmqvist.com/ \n'
1038 docString += ` Content processsed via REST API and mapped to MaterialX V${this.mx.getVersionString()} \n`
1039 if (shaderCategory.length > 0) {
1040 docString += ` Target Shading Model: ${shaderCategory} \n`
1041 }
1042 docString += ' Utility Author: Bernard Kwok. kwokcb@gmail.com '
1043 doc.setDocString(docString);
1044 }
1045
1056 convertToMaterialX(shaderCategory, references, addAllInputs = false, materialNames = [], remapKeys = {}, shaderPreFix = '')
1057 {
1058 if (!this.mx) {
1059 console.error('MaterialX module is not loaded');
1060 return false;
1061 }
1062
1063 if (!this.materials) {
1064 console.warn('No Physically Based Materials to convert');
1065 return false;
1066 }
1067
1068 if (Object.keys(remapKeys).length === 0)
1069 {
1070 remapKeys = this.getInputRemapping(shaderCategory);
1071 }
1072 //console.log('Using remap keys for shading model:', shaderCategory, remapKeys);
1073
1074 // Create a dummy doc with the surface shader with all inputs
1075 // as reference
1076 let refDoc = this.mx.createDocument();
1077 refDoc.importLibrary(this.stdlib);
1078 const refNode = refDoc.addNode(shaderCategory, 'refShader', this.mx.SURFACE_SHADER_TYPE_STRING);
1079 if (addAllInputs) {
1080 console.warn('MaterialX JS API missing addInputsFromNodeDef()');
1081 //refNode.addInputsFromNodeDef() -- This is missing from the JS API.
1082 }
1083 this.doc = this.mx.createDocument();
1084
1085 // Add document level accreditation
1086 this.add_copyright_docstring(this.doc, shaderCategory);
1087
1088 // Add properties to the material
1089 for (let i = 0; i < this.materials.length; i++) {
1090 const mat = this.materials[i];
1091 let matName = mat['name'];
1092
1093 // Filter by material name(s)
1094 let skipGeneration = materialNames.length > 0 && !materialNames.includes(matName);
1095
1096 let shaderNode = null;
1097 let docString = ''
1098 if (!skipGeneration)
1099 {
1100 if (shaderPreFix.length > 0) {
1101 matName = shaderPreFix + '_' + matName;
1102 }
1103
1104 const shaderName = this.doc.createValidChildName(matName + '_' + shaderCategory + '_SPB');
1105 this.addComment(this.doc, ' Generated shader: ' + matName + ' ');
1106 shaderNode = this.doc.addNode(shaderCategory, shaderName, this.mx.SURFACE_SHADER_TYPE_STRING);
1107
1108 const category = mat['category'];
1109 const group = mat['group'];
1110 let uifolder = '';
1111 if (category && category.length > 0) {
1112 uifolder = category[0];
1113 }
1114 if (group && group.length > 0) {
1115 if (uifolder.length > 0) {
1116 uifolder += '/';
1117 }
1118 uifolder += group[0];
1119 }
1120 if (uifolder.length > 0) {
1121 shaderNode.setAttribute('uifolder', uifolder);
1122 }
1123
1124 if (mat['description'].length > 0) {
1125 docString += 'Description: ' + mat['description'];
1126 }
1127 }
1128
1129 // Always want to build the reference
1130 const refString = mat['reference'];
1131 if (refString.length > 0) {
1132 if (docString.length > 0) {
1133 docString += '. ';
1134 }
1135 docString += 'Reference: ' + refString[0];
1136
1137 let referenceItem = { name: matName, reference: refString[0] };
1138 //console.log('Add Reference:', referenceItem);
1139 references.push(referenceItem);
1140 }
1141 // Sort references by name
1142 references.sort((a, b) => a.name.localeCompare(b.name));
1143
1144 if (!shaderNode) {
1145 continue;
1146 }
1147 if (docString.length > 0) {
1148 shaderNode.setDocString(docString);
1149 }
1150
1151 // Create a new material
1152 const materialName = this.doc.createValidChildName(matName + '_' + shaderCategory + '_MPB');
1153 this.addComment(this.doc, ' Generated material: ' + matName + ' ');
1154 const materialNode = this.doc.addNode(this.mx.SURFACE_MATERIAL_NODE_STRING, materialName, this.mx.MATERIAL_TYPE_STRING);
1155 const shaderInput = materialNode.addInput(this.mx.SURFACE_SHADER_TYPE_STRING, this.mx.SURFACE_SHADER_TYPE_STRING);
1156 shaderInput.setAttribute(MTLX_NODE_NAME_ATTRIBUTE, shaderNode.getName());
1157
1158 // Warning this is a bit bespoke for remapping keys
1159 // to Autodesk Standard Surface shader inputs
1160 const skipKeys = ['name', "density", "category", "description", "sources", "tags", "reference"];
1161
1162 let metallness = null;
1163 let roughness = null;
1164 let transmission_color = null;
1165 let transmission = null;
1166 Object.entries(mat).forEach(([key, value]) => {
1167
1168 if (!skipKeys.includes(key)) {
1169
1170 //console.log(`-- Processing key: "${key}" with value:`, value);
1171
1172 if (key == 'metalness') {
1173 metallness = value;
1174 //console.log('Metalness:', metallness);
1175 }
1176 if (key == 'roughness') {
1177 roughness = value;
1178 //console.log('Roughness:', roughness);
1179 }
1180 if (key == 'transmission') {
1181 transmission = value;
1182 //console.log('Transmission:', transmission);
1183 }
1184 if (key == 'color') {
1185 transmission_color = value;
1186 //console.log('Color:', color);
1187 }
1188
1189 //console.log(`-- Remapping key "${key}" to "${remapKeys[key]}"`);
1190 if (remapKeys[key]) {
1191 key = remapKeys[key];
1192 }
1193
1194 let refInput = refNode.getInput(key);
1195 if (!refInput)
1196 refInput = refNode.addInputFromNodeDef(key);
1197 if (refInput) {
1198 const input = shaderNode.addInput(key);
1199 input.copyContentFrom(refInput);
1200 if (input) {
1201 // Convert number vector to string
1202 if (Array.isArray(value)) {
1203 value = value.join(',');
1204 }
1205 // Convert number to string
1206 else if (typeof value === 'number') {
1207 value = value.toString();
1208 }
1209 // Note: This API has side-effects as the
1210 // type is set to "string" when the value is set. Thus
1211 // we must explicitly set the type here.
1212 input.setValueString(value, refInput.getType());
1213 }
1214 }
1215 else {
1216 //console.log('>> Could not map input:', key, 'to node definition')
1217 }
1218 }
1219 });
1220
1221 if (transmission !== null && metallness !== null && roughness !== null && transmission_color !== null)
1222 {
1223 if (metallness == 0 && roughness == 0)
1224 {
1225 if (remapKeys['transmission_color']) {
1226 let inputName = remapKeys['transmission_color'];
1227 let input = shaderNode.addInput(inputName);
1228 if (input) {
1229 let value = transmission_color.join(',');
1230 //console.log(`Add "${inputName}": "${value}"`);
1231 input.setValueString(value, 'color3');
1232 }
1233 }
1234 }
1235 };
1236 }
1237 return true;
1238 }
1239
1240}
Javascript class for querying materials from the Physically Based database and creating MaterialX mat...
skipLibraryElement(element)
Predicate to skip library elements.
create_translator(doc, source, target, source_version="", target_version="", mappings=null, output_doc)
initialize_definitions_and_materials(shadingModel='standard_surface', materialNames=[], force=false)
getJSONMaterialNames()
Get list of the Physically Based Material names.
initializeInputRemapping()
Initialize the input remapping for different shading models.
getInputRemapping(shadingModel)
Get the remapping keys for a given shading model.
getJSON()
Get the Physically Based Materials as JSON.
getMaterialXDocument()
Get the MaterialX document.
remapMap
Remap keys for input values for different shading models.
materials
List of Physically Based Materials.
loadStandardLibraries()
Load the MaterialX standard libraries.
constructor(mtlx_module=null, mtlx_stdlib=null)
Constructor for the PhysicallyBasedMaterialLoader.
add_copyright_docstring(doc, shaderCategory='')
Add copyright docstring to the MaterialX document.
getMaterialXString()
Get the MaterialX document as a string.
create_definition_materials(doc_mat, filter_list=null)
setDefaultRemapKeys()
Set the default remapping keys for different shading models : glTF, OpenPBR, and Autodesk Standard Su...
create_all_translators(definitions, output_doc=null)
async getPhysicallyBasedMaterials()
Get the Physically Based Materials from the API.
materialNames
List of Physically Based Material names.
mxMaterialNames
List of MaterialX Material names.
getReferenceList()
Return a sorted list reference names mapped reference images.
headers
Headers for the fetch operation.
url
URL to fetch the Physically Based Materials.
translate_node(doc, source_bxdf, target_bxdf, node)
addComment(doc, commentString)
Add a comment to the MaterialX document.
validateDocument()
Validate the MaterialX document.
convertToMaterialX(shaderCategory, references, addAllInputs=false, materialNames=[], remapKeys={}, shaderPreFix='')
Convert the Physically Based Materials to MaterialX.