MaterialXLab API  0.0.1
APIs For MaterialXLab Libraries
Loading...
Searching...
No Matches
JsMaterialXglTF.js
Go to the documentation of this file.
1
17let MTLX_MATERIAL_PREFIX = 'MAT_'
23let MTLX_DEFAULT_SHADER_NAME = 'SHD_0'
29let MTLX_SHADER_PREFIX = 'SHD_'
30
36let MTLX_INTERFACEINPUT_NAME_ATTRIBUTE = 'interfacename';
42let MTLX_NODE_NAME_ATTRIBUTE = 'nodename';
54let MTLX_GLTF_PBR_CATEGORY = 'gltf_pbr'
60let MTLX_UNLIT_CATEGORY_STRING = 'surface_unlit'
66let MULTI_OUTPUT_TYPE_STRING = 'multioutput'
67
72class MxConverter {
78 export(ne_mx, doc) {
79 return (() => { throw new Error("Method not implemented. This is a required base class method."); })();
80 }
81
87 exportType() {
88 return (() => { throw new Error("Method not implemented. This is a required base class method."); })();
89 }
90
99 import(ne_mx, gltfDoc, stdlib) {
100 return (() => { throw new Error("Method not implemented. This is a required base class method."); })();
101 }
102
108 importType() {
109 return (() => { throw new Error("Method not implemented. This is a required base class method."); })();
110 }
111}
112
118class glTFMaterialX extends MxConverter {
119 constructor() {
120 super();
121 }
122
128 exportType() {
129 return 'gltf';
130 }
131
132
139 importType() {
140 return 'gltf';
141 }
142
150 scalarToString(value, type) {
151 let returnvalue = null;
152
153 const supportedTypes = ['string', 'integer', 'matrix33', 'matrix44', 'vector2', 'vector3', 'vector4', 'float', 'color3', 'color4'];
154 const arrayTypes = ['matrix33', 'matrix44', 'vector2', 'vector3', 'vector4', 'color3', 'color4'];
155
156 if (supportedTypes.includes(type)) {
157 if (arrayTypes.includes(type)) {
158 returnvalue = value.map(x => String(x)).join(', ');
159 } else {
160 returnvalue = String(value);
161 }
162 }
163
164 return returnvalue;
165 }
166
174 stringToScalar(value, type) {
175 let returnvalue = value;
176
177 const scalarTypes = ['integer', 'matrix33', 'matrix44', 'vector2', 'vector3', 'vector4', 'float', 'color3', 'color4'];
178
179 if (scalarTypes.includes(type)) {
180 const splitvalue = value.split(',');
181
182 if (splitvalue.length > 1) {
183 returnvalue = splitvalue.map(x => parseFloat(x));
184 } else {
185 if (type === 'integer') {
186 returnvalue = parseInt(value, 10);
187 } else {
188 returnvalue = parseFloat(value);
189 }
190 }
191 }
192
193 return returnvalue;
194 }
195
206 initialize_gtlf_texture(texture, name, uri, images) {
207 const image = {};
208 image.name = name;
209
210 // Assuming mx.FilePath and mx.FormatPosix equivalents exist in JavaScript context
211 //const uriPath = ne_mx.createFilePath(uri); // Assuming mx.FilePath is a class in the context
212 //image.uri = uriPath.asString(ne_mx.FormatPosix); // Assuming asString method and FormatPosix constant exist
213 image.uri = uri;
214
215 images.push(image);
216
217 texture.name = name;
218 texture.source = images.length - 1;
219 }
220
228 getGLTFTextureUri(texture, images) {
229 let uri = '';
230 if (texture && 'source' in texture) {
231 const source = texture.source;
232 if (source < images.length) {
233 const image = images[source];
234 if ('uri' in image) {
235 uri = image.uri;
236 }
237 }
238 }
239 return uri;
240 }
241
249 haveExtensions(glTFDoc) {
250 // check extensionsUsed for KHR_texture_procedurals
251 let extensionsUsed = glTFDoc.extensionsUsed || null;
252 if (extensionsUsed === null) {
253 return [null, 'No extension used'];
254 }
255 let found = [false, false];
256 for (let ext of extensionsUsed) {
257 if (ext === 'KHR_texture_procedurals') {
258 found[0] = true;
259 }
260 if (ext === 'EXT_texture_procedurals_mx_1_39') {
261 found[1] = true;
262 }
263 }
264 if (!found[0])
265 return [null, 'Missing KHR_texture_procedurals extension'];
266 if (!found[1])
267 return [null, 'Missing EXT_texture_procedurals_mx_1_39 extension'];
268
269 return [true, ''];
270 }
271
279 addInputsFromNodeDef(node, nodeDef) {
280 if (nodeDef) {
281 for (let nodeDefInput of nodeDef.getActiveInputs()) {
282 let inputName = nodeDefInput.getName();
283 let nodeInput = node.getInput(inputName);
284 if (!nodeInput) {
285 //console.log('-------------> add input:', inputName);
286 nodeInput = node.addInput(inputName, nodeDefInput.getType());
287 if (nodeDefInput.hasValueString()) {
288 nodeInput.setValueString(nodeDefInput.getValueString(), nodeDefInput.getType());
289 }
290 }
291 }
292 }
293 }
294
302 import(ne_mx, glTFDocString, stdlib) {
303 if (!ne_mx) {
304 return [null, 'MaterialX runtime not passed in'];
305 }
306 let glTFDoc = JSON.parse(glTFDocString);
307
308 let extensionCheck = this.haveExtensions(glTFDoc);
309 if (extensionCheck[0] === null) {
310 return extensionCheck;
311 }
312
313 let doc = ne_mx.createDocument();
314
315 // Import the graph
316 this.importGraphs(doc, glTFDoc);
317
318 let global_extensions = glTFDoc.extensions || null;
319 let procedurals = null;
320 if (global_extensions && global_extensions.KHR_texture_procedurals) {
321 procedurals = global_extensions.KHR_texture_procedurals.procedurals || null;
322 console.log('Imported all procedurals:', procedurals);
323 }
324
325 // Import materials and connect to graphs as needed
326 let shaderName = MTLX_DEFAULT_SHADER_NAME;
327 let materialName = MTLX_DEFAULT_MATERIAL_NAME;
328 let materialIndex = 1;
329 let glTFmaterials = glTFDoc.materials || null;
330 if (glTFmaterials) {
331
332 // TODO iterator over all the mappings.
333 let inputMaps = [];
334 inputMaps[MTLX_GLTF_PBR_CATEGORY] = [
335 ['base_color', 'baseColorTexture', 'pbrMetallicRoughness'],
336 ['metallic', 'metallicRoughnessTexture', 'pbrMetallicRoughness'],
337 ['roughness', 'metallicRoughnessTexture', 'pbrMetallicRoughness'],
338 ['occlusion', 'occlusionTexture', ''],
339 ['normal', 'normalTexture', ''],
340 ['emissive', 'emissiveTexture', '']
341 ];
342 inputMaps[MTLX_UNLIT_CATEGORY_STRING] = [['emission_color', 'baseColorTexture', 'pbrMetallicRoughness']]
343
344 for (let glTFmaterial of glTFmaterials) {
345
346 //console.log('reading material:', glTFmaterial.name || 'no name');
347 let mtlxShaderName = glTFmaterial.name;
348 let mtlxMaterialName = '';
349 if (mtlxShaderName.length == 0) {
350 mtlxShaderName = shaderName + String(materialIndex);
351 materialIndex++;
352 mtlxMaterialName = materialName + String(materialIndex);
353 }
354 else {
355 mtlxMaterialName = 'MAT_' + mtlxShaderName;
356 }
357 mtlxShaderName = doc.createValidChildName(mtlxShaderName);
358 mtlxMaterialName = doc.createValidChildName(mtlxMaterialName);
359
360 //console.log('create valid names:', mtlxShaderName, mtlxMaterialName);
361
362 let use_unlit = false;
363 let extensions = glTFmaterial.extensions || null;
364 if (extensions) {
365 let KHR_materials_unlit = extensions.KHR_materials_unlit || null;
366 if (KHR_materials_unlit) {
367 use_unlit = true;
368 }
369 }
370
371 let shaderCategory = MTLX_GLTF_PBR_CATEGORY;
372 let nodedefString = 'ND_gltf_pbr_surfaceshader';
373 if (use_unlit) {
374 shaderCategory = MTLX_UNLIT_CATEGORY_STRING;
375 nodedefString = 'ND_surface_unlit';
376 }
377 let comment = doc.addChildOfCategory('comment')
378 comment.setDocString(' Generated shader: ' + mtlxShaderName + ' ')
379 let shaderNode = doc.addNode(shaderCategory, mtlxShaderName, ne_mx.SURFACE_SHADER_TYPE_STRING);
380 //console.log(ne_mx.prettyPrint(shaderNode))
381 shaderNode.setAttribute(ne_mx.InterfaceElement.NODE_DEF_ATTRIBUTE, nodedefString)
382 if (stdlib) {
383 let nodedef = stdlib.getNodeDef(nodedefString);
384 if (nodedef)
385 this.addInputsFromNodeDef(shaderNode, nodedef);
386 }
387
388 // Connect inputs to nodegraph outputs. Ugly bespoke code...
389 let defaultGraphName = 'nodegraph';
390 if (procedurals) {
391 let currentMap = inputMaps[shaderCategory];
392 for (let map of currentMap) {
393 let destInput = map[0];
394 let sourceTexture = map[1];
395 let sourceParent = map[2];
396 if (sourceParent.length > 0) {
397 if (sourceParent == 'pbrMetallicRoughness') {
398 let newSource = glTFmaterial.pbrMetallicRoughness[sourceTexture];
399 //console.log('remap from parent:', sourceParent, 'sourceTexture:', sourceTexture, 'to:', newSource);
400 sourceTexture = newSource;
401 }
402 }
403 else {
404 sourceTexture = glTFmaterial[sourceTexture];
405 }
406
407 let baseColorTexture = sourceTexture;
408
409 if (baseColorTexture) {
410 //console.log('-- sourceTexture:', sourceTexture);
411 if (baseColorTexture) {
412 let extensions = baseColorTexture.extensions || null;
413 if (extensions) {
414 //console.log('----- extensions:', extensions);
415 let KHR_texture_procedurals = extensions.KHR_texture_procedurals || null;
416 if (KHR_texture_procedurals) {
417 let pindex = KHR_texture_procedurals.index;
418 let output = KHR_texture_procedurals.output;
419 //console.log('----- proc index:', pindex, 'output:', output);
420 if (pindex != null) {
421 if (pindex < procedurals.length) {
422 //console.log('------ connect to proc: ', procedurals[pindex]);
423 let proc = procedurals[pindex];
424 if (proc) {
425 let nodegraphName = defaultGraphName;
426 if (proc.name)
427 nodegraphName = proc.name;
428
429 let graphOutputs = proc.outputs || null;
430 if (graphOutputs && graphOutputs.length > 0) {
431 let proc_output = graphOutputs[0];
432 if (output != null) {
433 proc_output = graphOutputs[output];
434 }
435 if (proc_output) {
436 let input = shaderNode.getInput(destInput);
437 if (!input)
438 input = shaderNode.addInput(destInput, proc_output.type);
439 if (input) {
440 //console.log('------ connect to output:', proc_output.name, 'type:', proc_output.type);
441 input.setNodeGraphString(nodegraphName);
442 input.setAttribute('output', proc_output.name);
443 }
444 }
445 }
446 }
447 }
448 }
449 }
450 }
451 }
452 }
453 }
454 }
455
456 comment = doc.addChildOfCategory('comment')
457 comment.setDocString(' Generated material: ' + mtlxMaterialName + ' ')
458 let materialNode = doc.addNode(ne_mx.SURFACE_MATERIAL_NODE_STRING, mtlxMaterialName, ne_mx.MATERIAL_TYPE_STRING)
459 let shaderInput = materialNode.addInput(ne_mx.SURFACE_SHADER_TYPE_STRING, ne_mx.SURFACE_SHADER_TYPE_STRING)
460 shaderInput.setAttribute(MTLX_NODE_NAME_ATTRIBUTE, mtlxShaderName)
461 //console.log(ne_mx.prettyPrint(materialNode))
462
463 console.log('>> Import material:', materialNode.getName(), 'Shader:', shaderNode.getName());
464 }
465 }
466
467 // Import asset information
468 let asset = glTFDoc.asset || null;
469 let docDoc = ''
470 if (asset) {
471 //console.log('import asset info')
472 let version = asset.version || null;
473 if (version) {
474 let comment = doc.addChildOfCategory('comment');
475 docDoc += 'glTF version: ' + version + '. ';
476 comment.setDocString('glTF version: ' + version);
477 }
478 let generator = asset.generator || null;
479 if (generator) {
480 let comment = doc.addChildOfCategory('comment');
481 docDoc += 'glTF generator: ' + generator + '. ';
482 comment.setDocString('glTF generator: ' + generator);
483 }
484 let copyRight = asset.copyright || null;
485 if (copyRight) {
486 let comment = doc.addChildOfCategory('comment');
487 docDoc += 'Copyright: ' + copyRight + '. ';
488 comment.setDocString('Copyright: ' + copyRight);
489 }
490
491 if (docDoc.length > 0) {
492 doc.setAttribute('doc', docDoc);
493 }
494 }
495
496 // Validate
497 let errors = {}
498 let errorString = ''
499 var valid = doc.validate(errors);
500 if (!valid)
501 {
502 errorString = errors.message
503 }
504
505 let docString = ne_mx.writeToXmlString(doc);
506 //console.log('*** gltF -> MaterialX document:', docString);
507 return [docString, errorString];
508 }
509
517 importGraphs(doc, gltfDoc) {
518 let root_mtlx = null;
519
520 // Look for the extension
521 let extensions = gltfDoc.extensions || null;
522 let procedurals = null;
523 if (extensions) {
524 procedurals = extensions.KHR_texture_procedurals.procedurals || null;
525 }
526
527 if (procedurals === null) {
528 console.log('> Error - no procedurals array found');
529 return null;
530 }
531
532 let graph_index = 0;
533
534 // Use index to auto-generate dummy nodegraph names
535 let procindex = 1;
536 for (let proc of procedurals) {
537 console.log(`> Scan procedural ${procindex} of ${procedurals.length} :`);
538 if (!proc.nodetype) {
539 console.log('>> Warning: No nodetype found in procedural. SKipping');
540 continue;
541 }
542
543 if (proc.nodetype !== 'nodegraph') {
544 console.log('>> Skip unsupported rocedural nodetype:', proc.nodetype);
545 continue;
546 }
547
548 let graphname = 'graph_' + graph_index;
549 if (proc.name) {
550 graphname = proc.name;
551 } else {
552 graph_index++;
553 }
554 let mtlxgraph = doc.addNodeGraph(graphname);
555 root_mtlx = mtlxgraph;
556
557 let input_index = 0;
558 let output_index = 0;
559 let node_index = 0;
560
561 let inputs = proc.inputs || [];
562 let outputs = proc.outputs || [];
563 let nodes = proc.nodes || [];
564
565 // Scan for input interfaces in the nodegraph
566 for (let input of inputs) {
567 let inputname = input.name || 'input_' + input_index;
568 if (!input.name) {
569 input_index++;
570 }
571
572 let inputtype = input.type || null;
573 if (!inputtype) {
574 console.log('>> Error - Input type not found for graph input:', inputname);
575 continue;
576 }
577
578 let mtlxinput = mtlxgraph.addInput(inputname, inputtype);
579
580 if (input.colorspace) {
581 mtlxinput.setAttribute('colorspace', input.colorspace);
582 }
583
584 if (inputtype === 'filename') {
585 let textureIndex = input.texture || null;
586 if (textureIndex !== null) {
587 let gltftextures = gltfDoc.textures || null;
588 let gltfimages = gltfDoc.images || null;
589 if (gltftextures && gltfimages) {
590 let gltftexture = gltftextures[textureIndex] || null;
591 if (gltftexture) {
592 let uri = this.getGLTFTextureUri(gltftexture, gltfimages);
593 mtlxinput.setValueString(uri, inputtype);
594 }
595 }
596 }
597 }
598
599 let inputvalue = input.value || null;
600 if (inputvalue !== null) {
601 let mtlxvalue = this.scalarToString(inputvalue, inputtype);
602 if (mtlxvalue !== null) {
603 mtlxinput.setValueString(mtlxvalue, inputtype);
604 } else {
605 console.log('>> Error - Unsupported handle input type:', inputtype, '. Performing straight assignment.');
606 mtlxinput.setValueString(String(inputvalue, inputtype));
607 }
608 } else {
609 console.log('>> Invalid usage of top level graph connections:', inputname);
610 if (input.procedural) {
611 //console.log('*** Connect input to procedural', proc.procedural);
612 }
613 }
614 }
615
616 // Scan for output interfaces in the nodegraph
617 for (let output of outputs) {
618 let outputname = output.name || 'output_' + output_index;
619 let outputtype = output.type || null;
620 let mtlxgraph_output = mtlxgraph.addOutput(outputname, outputtype);
621
622 let connectable = null;
623 if (output.input !== undefined) {
624 connectable = inputs[output.input];
625 if (connectable)
626 mtlxgraph_output.setAttribute(MTLX_INTERFACEINPUT_NAME_ATTRIBUTE, connectable.name);
627 else
628 console.log('>> Error - Input not found:', output.input, inputs);
629 }
630 /* } else if (output.output !== undefined) {
631 connectable = outputs[output.output];
632 if (connectable)
633 mtlxgraph_output.setAttribute('output', connectable.name);
634 else
635 console.log('*** ERROR: Output not found:', output.output, outputs);
636 } */
637 else if (output.node !== undefined) {
638 connectable = nodes[output.node];
639 if (connectable) {
640 mtlxgraph_output.setAttribute(MTLX_NODE_NAME_ATTRIBUTE, connectable.name);
641 // Check for any output qualifier
642 if (output.output !== undefined) {
643 mtlxgraph_output.setAttribute('output', output.output);
644 }
645 }
646 else
647 console.log('>> Error - Output node not found:', output.node, nodes);
648 }
649 }
650
651 // Scan for nodes in the nodegraph
652 for (let node of nodes) {
653 let nodename = node.name || 'node_' + node_index;
654 let nodetype = node.nodetype || null;
655 let outputType = node.type || null;
656 let node_outputs = node.outputs || [];
657 if (node_outputs.length > 1) {
658 outputType = MULTI_OUTPUT_TYPE_STRING;
659 }
660 let mtlxnode = mtlxgraph.addChildOfCategory(nodetype);
661 mtlxnode.setName(nodename);
662 if (outputType) {
663 mtlxnode.setType(outputType);
664 } else {
665 console.log('>> Error - no output type for node:', nodename);
666 }
667
668 input_index = 0;
669 let node_inputs = node.inputs || [];
670 for (let input of node_inputs) {
671 let inputname = input.name || 'input_' + input_index;
672 let inputtype = input.type || null;
673 let mtlxinput = mtlxnode.addInput(inputname, inputtype);
674
675 if (input.colorspace) {
676 mtlxinput.setAttribute('colorspace', input.colorspace);
677 }
678
679 if (inputtype === 'filename') {
680 let textureIndex = input.texture || null;
681 if (textureIndex !== null) {
682 let gltftextures = gltfDoc.textures || null;
683 let gltfimages = gltfDoc.images || null;
684 if (gltftextures && gltfimages) {
685 let gltftexture = gltftextures[textureIndex] || null;
686 if (gltftexture) {
687 let uri = this.getGLTFTextureUri(gltftexture, gltfimages);
688 mtlxinput.setValueString(uri, inputtype);
689 }
690 }
691 }
692 }
693
694 let inputvalue = input.value || null;
695 if (inputvalue !== null) {
696 let mtlxvalue = this.scalarToString(inputvalue, inputtype);
697 if (mtlxvalue !== null) {
698 mtlxinput.setValueString(mtlxvalue, inputtype);
699 } else {
700 console.log('>> Error - Unsupported input type:', inputtype, '. Performing straight assignment.');
701 mtlxinput.setValueString(String(inputvalue), inputtype);
702 }
703 } else {
704 let connectable = null;
705 if (input.input !== undefined) {
706 connectable = inputs[input.input];
707 mtlxinput.setAttribute(MTLX_INTERFACEINPUT_NAME_ATTRIBUTE, connectable.name);
708 } else if (input.output !== undefined) {
709 if (input.node !== undefined) {
710 mtlxinput.setAttribute('output', input.output);
711 } else {
712 connectable = outputs[input.output];
713 mtlxinput.setAttribute('output', connectable.name);
714 }
715 }
716 if (input.node !== undefined) {
717 connectable = nodes[input.node];
718 mtlxinput.setAttribute(MTLX_NODE_NAME_ATTRIBUTE, connectable.name);
719 }
720 }
721 }
722
723 output_index = 0;
724 for (let output of node_outputs) {
725 let outputname = output.name || 'output_' + output_index;
726 let outputtype = output.type || null;
727 let mtlxoutput = mtlxnode.addOutput(outputname, outputtype);
728
729 let connectable = null;
730 if (output.input !== undefined) {
731 connectable = inputs[output.input];
732 mtlxoutput.setAttribute(MTLX_INTERFACEINPUT_NAME_ATTRIBUTE, connectable.name);
733 } else if (output.output !== undefined) {
734 connectable = outputs[output.output];
735 mtlxoutput.setAttribute('output', connectable.name);
736 }
737 if (output.node !== undefined) {
738 connectable = nodes[output.node];
739 mtlxoutput.setAttribute(MTLX_NODE_NAME_ATTRIBUTE, connectable.name);
740 }
741 }
742 }
743
744 procindex++;
745 }
746
747 return root_mtlx;
748 }
749
757 exportGraph(ne_mx, graph, json, materials) {
758 let no_result = [null, null, null]
759
760 if (!ne_mx) {
761 console.log('MaterialX runtime not passed in')
762 return no_result;
763 }
764
765 let graphOutputs = graph.getOutputs();
766 if (graphOutputs.length == 0) {
767 console.log('No graph outputs found on graph: ', graph.getNamePath())
768 return no_result;
769 }
770
771 const debug = false;
772 const usePaths = false;
773
774 // Create fallback texture:
775 const fallback = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4z/AfAAQAAf/zKSWvAAAAAElFTkSuQmCC';
776 let fallbackIndex = -1;
777
778 let imageArray = json['images'] || [];
779 if (!json['images']) json['images'] = imageArray;
780
781 for (let i = 0; i < imageArray.length; i++) {
782 const image = imageArray[i];
783 if (image['uri'] === fallback) {
784 fallbackIndex = i;
785 break;
786 }
787 }
788
789 let fallbackTextureIndex = -1;
790 if (fallbackIndex === -1) {
791 const image = {
792 'uri': fallback,
793 'name': 'KHR_texture_procedural_fallback'
794 };
795 imageArray.push(image);
796 fallbackIndex = imageArray.length - 1;
797 }
798
799 let textureArray = json['textures'] || [];
800 if (!json['textures']) json['textures'] = textureArray;
801
802 for (let i = 0; i < textureArray.length; i++) {
803 const texture = textureArray[i];
804 if (texture['source'] === fallbackIndex) {
805 fallbackTextureIndex = i;
806 break;
807 }
808 }
809
810 if (fallbackTextureIndex === -1) {
811 textureArray.push({ 'source': fallbackIndex });
812 fallbackTextureIndex = textureArray.length - 1;
813 }
814
815 const skipAttr = ['uiname', 'xpos', 'ypos'];
816 const procDictNodes = {};
817 const procDictInputs = {};
818 const procDictOutputs = {};
819
820 let extensions = json['extensions'] || {};
821 if (!json['extensions']) json['extensions'] = extensions;
822
823 let KHR_texture_procedurals = extensions['KHR_texture_procedurals'] || {};
824 if (!extensions['KHR_texture_procedurals']) extensions['KHR_texture_procedurals'] = KHR_texture_procedurals;
825
826 if (!KHR_texture_procedurals['procedurals']) {
827 KHR_texture_procedurals['procedurals'] = [];
828 }
829
830 const procs = KHR_texture_procedurals['procedurals'];
831 const nodegraph = {
832 'name': usePaths ? graph.getNamePath() : graph.getName(),
833 'nodetype': graph.getCategory()
834 };
835
836 nodegraph['type'] = graphOutputs.length > 1 ? 'multioutput' : graphOutputs[0].getType();
837 const nodegraphInputs = nodegraph['inputs'] = [];
838 const nodegraphOutputs = nodegraph['outputs'] = [];
839 const nodegraphNodes = nodegraph['nodes'] = [];
840 procs.push(nodegraph);
841
842 const metadata = ['colorspace', 'unit', 'unittype', 'uiname', 'uimin', 'uimax', 'uifolder', 'doc'];
843
844 graph.getNodes().forEach((node) => {
845 const jsonNode = {
846 'name': usePaths ? node.getNamePath() : node.getName()
847 };
848 nodegraphNodes.push(jsonNode);
849 procDictNodes[node.getNamePath()] = nodegraphNodes.length - 1;
850 });
851
852 graph.getInputs().forEach((input) => {
853 const jsonNode = {
854 'name': usePaths ? input.getNamePath() : input.getName(),
855 'nodetype': input.getCategory()
856 };
857
858 metadata.forEach((meta) => {
859 if (input.getAttribute(meta)) {
860 jsonNode[meta] = input.getAttribute(meta);
861 }
862 });
863
864 if (input.getValue() !== null) {
865 const inputType = input.getAttribute(ne_mx.TypedElement.TYPE_ATTRIBUTE);
866 jsonNode['type'] = inputType;
867 if (inputType === ne_mx.FILENAME_TYPE_STRING) {
868 const texture = {};
869 const filename = input.getResolvedValueString();
870 //console.log('initialize file texture: ', input.getNamePath(), filename, '. Skip image properties');
871 this.initialize_gtlf_texture(texture, input.getNamePath(), filename, imageArray);
872 textureArray.push(texture);
873 jsonNode['texture'] = textureArray.length - 1;
874 } else {
875 let value = input.getValueString();
876 value = this.stringToScalar(value, inputType);
877 jsonNode['value'] = value;
878 }
879 nodegraphInputs.push(jsonNode);
880 procDictInputs[input.getNamePath()] = nodegraphInputs.length - 1;
881 } else {
882 //if (input.getAttribute("interfacename") || input.getAttrbute("nodename") ||
883 // input.getAttribute("nodegraph"))
884 //{
885 // console.error('Error: Graph input connections to upstream nodes are invalid. Input skipped:', input.getNamePath());
886 //}
887 //else
888 {
889 console.error('Error: no value or invalid connection specified for input. Input skipped:', input.getNamePath());
890 }
891 }
892 });
893
894 graphOutputs.forEach((output) => {
895 const jsonNode = {
896 'name': usePaths ? output.getNamePath() : output.getName()
897 };
898 nodegraphOutputs.push(jsonNode);
899 procDictOutputs[output.getNamePath()] = nodegraphOutputs.length - 1;
900
901 jsonNode['nodetype'] = output.getCategory();
902 jsonNode['type'] = output.getType();
903
904 let connection = output.getAttribute(MTLX_INTERFACEINPUT_NAME_ATTRIBUTE);
905 if (connection.length == 0)
906 connection = output.getAttribute(MTLX_NODE_NAME_ATTRIBUTE);
907
908 const connectionNode = graph.getChild(connection);
909 if (connectionNode) {
910 let connectionPath = connectionNode.getNamePath()
911 if (debug)
912 jsonNode['debug_connection_path'] = connectionPath;
913
914 if (procDictInputs[connectionPath] != null) {
915 jsonNode['input'] = procDictInputs[connectionPath];
916 } else if (procDictNodes[connectionPath] != null) {
917 jsonNode['node'] = procDictNodes[connectionPath];
918 } else {
919 console.error('Invalid output connection to:', connectionPath);
920 }
921
922 const outputString = output.getAttribute('output');
923 if (outputString.length > 0) {
924 jsonNode['output'] = outputString;
925 }
926 }
927 });
928
929 graph.getNodes().forEach((node) => {
930 let jsonNode = null;
931 const index = procDictNodes[node.getNamePath()];
932 jsonNode = nodegraphNodes[index];
933 jsonNode['nodetype'] = node.getCategory();
934 const nodedef = node.getNodeDef();
935
936 if (debug && nodedef && nodedef.getNodeGroup().length) {
937 jsonNode['nodegroup'] = nodedef.getNodeGroup();
938 }
939
940 node.getAttributeNames().forEach((attrName) => {
941 if (!skipAttr.includes(attrName)) {
942 jsonNode[attrName] = node.getAttribute(attrName);
943 }
944 });
945
946 const inputs = [];
947 node.getInputs().forEach((input) => {
948 const inputItem = {
949 'name': input.getName(),
950 'nodetype': 'input'
951 };
952
953 metadata.forEach((meta) => {
954 if (input.getAttribute(meta)) {
955 inputItem[meta] = input.getAttribute(meta);
956 }
957 });
958
959 const inputType = input.getAttribute(ne_mx.TypedElement.TYPE_ATTRIBUTE);
960 inputItem['type'] = inputType;
961
962 if (input.getValue() !== null) {
963 if (inputType === ne_mx.FILENAME_TYPE_STRING) {
964 const texture = {};
965 const filename = input.getResolvedValueString();
966 this.initialize_gtlf_texture(texture, input.getNamePath(), filename, imageArray);
967 textureArray.push(texture);
968 inputItem['texture'] = textureArray.length - 1;
969 } else {
970 let value = input.getValueString();
971 value = this.stringToScalar(value, inputType);
972 inputItem['value'] = value;
973 }
974 } else {
975 let isInterface = true;
976 let connection = input.getAttribute(MTLX_INTERFACEINPUT_NAME_ATTRIBUTE);
977 if (!connection.length) {
978 isInterface = false;
979 connection = input.getAttribute(MTLX_NODE_NAME_ATTRIBUTE);
980 }
981
982 if (connection.length > 0) {
983 const connectionNode = graph.getChild(connection);
984 if (connectionNode) {
985 const inputType = input.getAttribute(ne_mx.TypedElement.TYPE_ATTRIBUTE);
986 inputItem['type'] = inputType;
987 let connectionPath = connectionNode.getNamePath();
988 if (debug) {
989 inputItem['debug_connection_path'] = connectionPath;
990 }
991
992 if (isInterface && procDictInputs[connectionPath] != null) {
993 inputItem['input'] = procDictInputs[connectionPath];
994 } else if (procDictNodes[connectionPath] != null) {
995 inputItem['node'] = procDictNodes[connectionPath];
996 }
997
998 const outputString = input.getAttribute('output');
999 if (outputString.length > 0) {
1000 const connectedNodeOutputs = connectionNode.getOutputs();
1001 for (let i = 0; i < connectedNodeOutputs.length; i++) {
1002 if (connectedNodeOutputs[i].getName() === outputString) {
1003 inputItem['output'] = i;
1004 break;
1005 }
1006 }
1007 }
1008 } else {
1009 console.error('Error: Invalid input connection to:', connection, ' from: input:', input.getNamePath(), ' node:', node.getNamePath());
1010 }
1011 }
1012
1013 if (input.getAttribute(MTLX_NODEGRAPH_NAME_ATTRIBUTE)) {
1014 console.error('Error: Invalid input connection to:', connection, ' from: input:', input.getNamePath(), ' node:', node.getNamePath());
1015 }
1016 }
1017
1018 inputs.push(inputItem);
1019 });
1020
1021 if (inputs.length > 0) {
1022 jsonNode['inputs'] = inputs;
1023 }
1024
1025 const outputs = [];
1026 node.getOutputs().forEach((output) => {
1027 const outputItem = {
1028 'nodetype': 'output',
1029 'name': output.getName(),
1030 'type': output.getType()
1031 };
1032 outputs.push(outputItem);
1033 });
1034
1035 if (nodedef) {
1036 nodedef.getOutputs().forEach((output) => {
1037 let exists = false;
1038 outputs.forEach((outputItem) => {
1039 if (outputItem['name'] === output.getName()) {
1040 exists = true;
1041 }
1042 });
1043
1044 if (!exists) {
1045 const outputItem = {
1046 'nodetype': 'output',
1047 'name': output.getName(),
1048 'type': output.getType()
1049 };
1050 outputs.push(outputItem);
1051 }
1052 });
1053 } else {
1054 console.error('Missing nodedef for node:', node.getNamePath());
1055 }
1056
1057 if (outputs.length > 0) {
1058 jsonNode['outputs'] = outputs;
1059 }
1060 });
1061
1062 return [procs, procDictOutputs, procDictNodes, fallbackTextureIndex];
1063 }
1064
1071 export(ne_mx, glTFDoc) {
1072
1073 if (!ne_mx) {
1074 status = 'MaterialX runtime not passed in'
1075 return [null, status];
1076 }
1077
1078 let status = '';
1079 if (!glTFDoc) {
1080 status = 'Invalid document to convert';
1081 return [null, status];
1082 }
1083
1084 let materials = [];
1085 let mxMaterials = glTFDoc.getMaterialNodes();
1086 if (mxMaterials.length == 0) {
1087 //status = 'No MaterialX materials found in document';
1088 //return [null, status];
1089 }
1090
1091 let json = {};
1092 let json_asset = {
1093 "version": "2.0",
1094 "generator": "MaterialX 1.39 to glTF 2.0 procedural textures converter",
1095 "copyright": "Copyright (c) 2024, Bernard Kwok"
1096 }
1097
1098 let inputMaps = [];
1099 inputMaps[MTLX_GLTF_PBR_CATEGORY] = [
1100 ['base_color', 'baseColorTexture', 'pbrMetallicRoughness'],
1101 ['metallic', 'metallicRoughnessTexture', 'pbrMetallicRoughness'],
1102 ['roughness', 'metallicRoughnessTexture', 'pbrMetallicRoughness'],
1103 ['occlusion', 'occlusionTexture', ''],
1104 ['normal', 'normalTexture', ''],
1105 ['emissive', 'emissiveTexture', '']
1106 ];
1107 inputMaps[MTLX_UNLIT_CATEGORY_STRING] = [['emission_color', 'baseColorTexture', 'pbrMetallicRoughness']]
1108
1109 let pbrNodes = [];
1110 let fallbackTextureIndex = -1;
1111 let procs = [];
1112 let exportGraphNames = [];
1113
1114 for (let mxMaterial of mxMaterials) {
1115 let mxshaders = ne_mx.getShaderNodes(mxMaterial);
1116 for (let shaderNode of mxshaders) {
1117 let category = shaderNode.getCategory();
1118 let path = shaderNode.getNamePath()
1119 let isUnlit = (category == MTLX_UNLIT_CATEGORY_STRING);
1120 let isPBR = (category == MTLX_GLTF_PBR_CATEGORY);
1121 if ((isPBR || isUnlit) && pbrNodes[path] == null) {
1122 console.log('> Convert shader to glTF:', shaderNode.getNamePath(), 'Category:', category);
1123 // Add to pbrNodes if not exists
1124 pbrNodes[path] = shaderNode;
1125
1126 let material = {};
1127 {
1128 let base_color_input = null;
1129 let base_color_output = '';
1130 let inputPairs = inputMaps[category];
1131 for (let inputPair of inputPairs) {
1132 //console.log('Input map:', inputPair[0], ' maps to', inputPair[1]);
1133 base_color_input = shaderNode.getInput(inputPair[0]);
1134 base_color_output = inputPair[1];
1135
1136 if (!base_color_input) {
1137 continue;
1138 }
1139
1140 let nodeGraphName = base_color_input.getNodeGraphString();
1141 if (nodeGraphName.length == 0) {
1142 continue;
1143 }
1144
1145 let nodeGraphOutput = base_color_input.getOutputString();
1146 material['name'] = path;
1147
1148 let parent = material;
1149 if (inputPair[2].length > 0) {
1150 if (!material[inputPair[2]]) {
1151 material[inputPair[2]] = {};
1152 }
1153 parent = material[inputPair[2]]
1154 }
1155
1156 // Look for existing graph
1157 let graphIndex = -1;
1158 let outputIndex = -1;
1159 if (procs) {
1160 let i = 0;
1161 for (let proc of procs) {
1162 if (proc['name'] == nodeGraphName) {
1163 graphIndex = i;
1164 if (nodeGraphOutput.length > 0) {
1165 let outputs = proc['outputs'];
1166 let j = 0;
1167 for (let output of outputs) {
1168 if (output['name'] == nodeGraphOutput) {
1169 outputIndex = j;
1170 break;
1171 }
1172 j++;
1173 }
1174 }
1175 break;
1176 }
1177 i++;
1178 }
1179 }
1180
1181 if (graphIndex >= 0) {
1182 let baseColorTexture = parent[base_color_output] = {}
1183 baseColorTexture['index'] = fallbackTextureIndex;
1184 let ext = baseColorTexture['extensions'] = {}
1185 {
1186 if (isUnlit) {
1187 ext['KHR_materials_unlit'] = {};
1188 }
1189 let lookup = ext['KHR_texture_procedurals'] = {}
1190 lookup['index'] = graphIndex;
1191 if (outputIndex >= 0) {
1192 lookup['output'] = outputIndex;
1193 }
1194 }
1195 }
1196 else {
1197 let graph = glTFDoc.getNodeGraph(nodeGraphName);
1198 exportGraphNames.push(nodeGraphName);
1199
1200 let gltfInfo = this.exportGraph(ne_mx, graph, json, materials)
1201 //console.log('gltfInfo:', gltfInfo);
1202 procs = gltfInfo[0]
1203 let outputNodes = gltfInfo[1];
1204 let proceduralNodes = gltfInfo[2];
1205 fallbackTextureIndex = gltfInfo[3];
1206
1207 //console.log('procs', procs, 'gltfInfo:', gltfInfo, 'fallbackTextureIndex:', fallbackTextureIndex)
1208
1209 //visitedGraphs.push([nodeGraphName, nodeGraphOutput]);
1210
1211 let baseColorTexture = parent[base_color_output] = {}
1212 baseColorTexture['index'] = fallbackTextureIndex;
1213 let ext = baseColorTexture['extensions'] = {}
1214 {
1215 if (isUnlit) {
1216 ext['KHR_materials_unlit'] = {};
1217 }
1218 let lookup = ext['KHR_texture_procedurals'] = {}
1219 lookup['index'] = procs.length - 1;
1220 let outputIndex = -1;
1221
1222 if (nodeGraphOutput.length > 0) {
1223 for (let outputNodeName in outputNodes) {
1224 let nodeGraphOutputPath = (nodeGraphName + '/' + nodeGraphOutput);
1225 if (outputNodeName == nodeGraphOutputPath) {
1226 outputIndex = outputNodes[outputNodeName];
1227 break;
1228 }
1229 }
1230 if (outputIndex == -1) {
1231 console.error('Failed to find output:', nodeGraphOutput, ' in:', outputNodes)
1232 }
1233 lookup['output'] = outputIndex;
1234 }
1235 // Default is first procedural output
1236 else {
1237 lookup['output'] = 0;
1238 }
1239 }
1240 }
1241 }
1242 }
1243
1244 if (material['name']) {
1245 materials.push(material);
1246 }
1247 }
1248 }
1249 }
1250
1251 let unconnectedGraphs = [];
1252 for (let ng of glTFDoc.getNodeGraphs()) {
1253 let ng_name = ng.getName();
1254 if (ng.getAttribute('nodedef') || ng.hasSourceUri()) {
1255 //console.log('Skip nodegraph for nodedef', ng_name, ng.getAttribute('nodedef'));
1256 continue;
1257 }
1258 if (!exportGraphNames.includes(ng_name)) {
1259 unconnectedGraphs.push(ng_name);
1260
1261 let gltfInfo = this.exportGraph(ne_mx, ng, json, materials)
1262 procs = gltfInfo[0]
1263 let outputNodes = gltfInfo[1];
1264 let proceduralNodes = gltfInfo[2];
1265 fallbackTextureIndex = gltfInfo[3];
1266 }
1267 }
1268
1269 if (materials.length > 0) {
1270 json['materials'] = materials;
1271 if (unconnectedGraphs.length > 0) {
1272 status = 'Exported unconnected graphs: ' + unconnectedGraphs.join(', ');
1273 }
1274 }
1275 else {
1276 if (unconnectedGraphs.length > 0) {
1277 status = 'Exported unconnected graphs: ' + unconnectedGraphs.join(', ');
1278 }
1279 else {
1280 status = 'No appropriate glTF shader graphs found';
1281 }
1282 }
1283
1284 //console.log('pbrNodes:', pbrNodes, 'unlitNodes:', unlitNodes, 'materials:', json['materials'])
1285
1286 if (procs.length > 0) {
1287 json['asset'] = json_asset;
1288 json['extensionsUsed'] = [];
1289 json['extensionsUsed'].push('KHR_texture_procedurals');
1290 json['extensionsUsed'].push('EXT_texture_procedurals_mx_1_39');
1291 }
1292
1293 // Convert JSON to string
1294 let jsonString = json ? JSON.stringify(json, null, 2) : '';
1295 if (jsonString == '{}') {
1296 jsonString = '';
1297 }
1298
1299 return [jsonString, status];
1300 }
1301}
let MTLX_NODEGRAPH_NAME_ATTRIBUTE
MTLX_NODEGRAPH_NAME_ATTRIBUTE.
let MTLX_NODE_NAME_ATTRIBUTE
MTLX_NODE_NAME_ATTRIBUTE.
let MTLX_DEFAULT_MATERIAL_NAME
jsMaterialXglTF