Using The Core Library¶
This notebook provides a set of sample code which demonstrates the workflow to convert between glTF and MaterialX.
The sample input file is the "BoomBox with Axes" file from the glTF sample repository found here is used to demonstrate glTF to MaterialX conversion. The resulting MaterialX file after conversion is used to demonstrate conversion to glTF.
The following are some examples from conversion of Substance3D materials to glTF
import materialxgltf
import materialxgltf.core as core
This is import not required and is only added used here to improve output display
from IPython.display import display_markdown
def displaySource(title, string, language='xml', open=True):
text = '<details '
text = text + (' open' if open else '')
text = text + '><summary><b>' + title + '</b></summary>\n\n' + '```' + language + '\n' + string + '\n```\n' + '</details>\n'
display_markdown(text, raw=True)
Packaged Sample Data
For convenience a few sample files are included as part of the Python package and are used in this notebook.
import pkg_resources
import os
directory_name = "data"
files = pkg_resources.resource_listdir('materialxgltf', directory_name)
result = ''
for file in files:
if file == 'shaderball.gltf' or file.find('baked') != -1:
continue
file_extension = os.path.splitext(file)[1]
if file_extension in ['.mtlx', '.gltf']:
result = result + file + '\n'
displaySource('Available data files', result, 'text', True)
Available data files
BoomBoxWithAxes.gltf
BoomBoxWithAxes.mtlx
BoomBoxWithAxes.mtlx.gltf
BoomBoxWithAxes_primMaterials.gltf
gltf_test_nondefault_pbr.mtlx
gltf_test_nondefault_pbr.mtlx.gltf
standard_surface_marble_solid.mtlx
import pkg_resources
import MaterialX as mx
print(f'Using ( MaterialX version: {mx.getVersionString()} materialxgltf version: {materialxgltf.__version__} )\n')
gltfFileName = pkg_resources.resource_filename('materialxgltf', 'data/BoomBoxWithAxes.gltf')
print('Converting: %s' % mx.FilePath(gltfFileName).getBaseName())
# Instantiate a the reader class. Read in sample glTF file
# and output a MaterialX document
gltf2MtlxReader = core.GLTF2MtlxReader()
doc = gltf2MtlxReader.convert(gltfFileName)
if not doc:
print('Exiting due to error')
else:
status, err = doc.validate()
if not status:
print('- Generated MaterialX document has validation errors: ', err)
else:
print('- Generated MaterialX document is valid')
# Examine the document output
result = core.Util.writeMaterialXDocString(doc)
displaySource('Resulting MaterialX document', result, 'xml', True)
Using ( MaterialX version: 1.39.2 materialxgltf version: 1.39.2 ) Converting: BoomBoxWithAxes.gltf - Convert gLTF material to MateriaLX: MAT_M_BoomBox - Convert gLTF material to MateriaLX: MAT_M_Coordinates
- Generated MaterialX document is valid
Resulting MaterialX document
<?xml version="1.0"?>
<materialx version="1.39">
<!-- Generated shader: M_BoomBox -->
<gltf_pbr name="M_BoomBox" type="surfaceshader" nodedef="ND_gltf_pbr_surfaceshader">
<input name="base_color" type="color3" nodename="image_base_color" output="outcolor" />
<input name="metallic" type="float" nodename="extract_orm3" />
<input name="roughness" type="float" nodename="extract_orm2" />
<input name="occlusion" type="float" nodename="extract_orm" />
<input name="normal" type="vector3" nodename="image_normal" />
<input name="emissive" type="color3" nodename="image_emissive" output="outcolor" />
</gltf_pbr>
<!-- Generated material: MAT_M_BoomBox -->
<surfacematerial name="MAT_M_BoomBox" type="material">
<input name="surfaceshader" type="surfaceshader" nodename="M_BoomBox" />
</surfacematerial>
<gltf_colorimage name="image_base_color" type="multioutput">
<input name="file" type="filename" value="BoomBoxWithAxes_baseColor.png" colorspace="srgb_texture" />
<output name="outcolor" type="color3" />
<output name="outa" type="float" />
</gltf_colorimage>
<gltf_image name="image_orm" type="vector3">
<input name="file" type="filename" value="BoomBoxWithAxes_roughnessMetallic.png" />
</gltf_image>
<extract name="extract_orm" type="float">
<input name="in" type="vector3" nodename="image_orm" />
<input name="index" type="integer" value="0" />
</extract>
<extract name="extract_orm2" type="float">
<input name="in" type="vector3" nodename="image_orm" />
<input name="index" type="integer" value="1" />
</extract>
<extract name="extract_orm3" type="float">
<input name="in" type="vector3" nodename="image_orm" />
<input name="index" type="integer" value="2" />
</extract>
<gltf_normalmap name="image_normal" type="vector3">
<input name="file" type="filename" value="BoomBoxWithAxes_normal.png" />
</gltf_normalmap>
<gltf_colorimage name="image_emissive" type="multioutput">
<input name="file" type="filename" value="BoomBoxWithAxes_emissive.png" colorspace="srgb_texture" />
<output name="outcolor" type="color3" />
<output name="outa" type="float" />
</gltf_colorimage>
<!-- Generated shader: M_Coordinates -->
<gltf_pbr name="M_Coordinates" type="surfaceshader" nodedef="ND_gltf_pbr_surfaceshader">
<input name="base_color" type="color3" nodename="image_base_color2" output="outcolor" />
<input name="alpha" type="float" value="1" />
<input name="metallic" type="float" value="0.0" />
<input name="roughness" type="float" value="0.735" />
<input name="emissive" type="color3" value="0, 0, 0" colorspace="srgb_texture" />
</gltf_pbr>
<!-- Generated material: MAT_M_Coordinates -->
<surfacematerial name="MAT_M_Coordinates" type="material">
<input name="surfaceshader" type="surfaceshader" nodename="M_Coordinates" />
</surfacematerial>
<gltf_colorimage name="image_base_color2" type="multioutput">
<input name="file" type="filename" value="BoomBoxWithAxes_baseColor1.png" colorspace="srgb_texture" />
<output name="outcolor" type="color3" />
<output name="outa" type="float" />
</gltf_colorimage>
</materialx>
Using glTF to MaterialX Options¶
The option to create material assignments is enabled and the MaterialX file is regenerated.
# Set option to write material assignments
options = core.GLTF2MtlxOptions()
options['createAssignments'] = True
gltf2MtlxReader.setOptions(options)
print('Converting with Look: %s' % mx.FilePath(gltfFileName).getBaseName())
doc = gltf2MtlxReader.convert(gltfFileName)
if not doc:
print('Existing due to error')
else:
status, err = doc.validate()
if not status:
print('Generated MaterialX document has validation errors: ', err)
else:
print('Generated MaterialX document is valid')
# Display the resulting document
result = core.Util.writeMaterialXDocString(doc)
displaySource('Resulting MaterialX document', result, 'xml', True)
Converting with Look: BoomBoxWithAxes.gltf - Convert gLTF material to MateriaLX: MAT_M_BoomBox - Convert gLTF material to MateriaLX: MAT_M_Coordinates
Generated MaterialX document is valid
Resulting MaterialX document
<?xml version="1.0"?>
<materialx version="1.39">
<!-- Generated shader: M_BoomBox -->
<gltf_pbr name="M_BoomBox" type="surfaceshader" nodedef="ND_gltf_pbr_surfaceshader">
<input name="base_color" type="color3" nodename="image_base_color" output="outcolor" />
<input name="metallic" type="float" nodename="extract_orm3" />
<input name="roughness" type="float" nodename="extract_orm2" />
<input name="occlusion" type="float" nodename="extract_orm" />
<input name="normal" type="vector3" nodename="image_normal" />
<input name="emissive" type="color3" nodename="image_emissive" output="outcolor" />
</gltf_pbr>
<!-- Generated material: MAT_M_BoomBox -->
<surfacematerial name="MAT_M_BoomBox" type="material">
<input name="surfaceshader" type="surfaceshader" nodename="M_BoomBox" />
</surfacematerial>
<gltf_colorimage name="image_base_color" type="multioutput">
<input name="file" type="filename" value="BoomBoxWithAxes_baseColor.png" colorspace="srgb_texture" />
<output name="outcolor" type="color3" />
<output name="outa" type="float" />
</gltf_colorimage>
<gltf_image name="image_orm" type="vector3">
<input name="file" type="filename" value="BoomBoxWithAxes_roughnessMetallic.png" />
</gltf_image>
<extract name="extract_orm" type="float">
<input name="in" type="vector3" nodename="image_orm" />
<input name="index" type="integer" value="0" />
</extract>
<extract name="extract_orm2" type="float">
<input name="in" type="vector3" nodename="image_orm" />
<input name="index" type="integer" value="1" />
</extract>
<extract name="extract_orm3" type="float">
<input name="in" type="vector3" nodename="image_orm" />
<input name="index" type="integer" value="2" />
</extract>
<gltf_normalmap name="image_normal" type="vector3">
<input name="file" type="filename" value="BoomBoxWithAxes_normal.png" />
</gltf_normalmap>
<gltf_colorimage name="image_emissive" type="multioutput">
<input name="file" type="filename" value="BoomBoxWithAxes_emissive.png" colorspace="srgb_texture" />
<output name="outcolor" type="color3" />
<output name="outa" type="float" />
</gltf_colorimage>
<!-- Generated shader: M_Coordinates -->
<gltf_pbr name="M_Coordinates" type="surfaceshader" nodedef="ND_gltf_pbr_surfaceshader">
<input name="base_color" type="color3" nodename="image_base_color2" output="outcolor" />
<input name="alpha" type="float" value="1" />
<input name="metallic" type="float" value="0.0" />
<input name="roughness" type="float" value="0.735" />
<input name="emissive" type="color3" value="0, 0, 0" colorspace="srgb_texture" />
</gltf_pbr>
<!-- Generated material: MAT_M_Coordinates -->
<surfacematerial name="MAT_M_Coordinates" type="material">
<input name="surfaceshader" type="surfaceshader" nodename="M_Coordinates" />
</surfacematerial>
<gltf_colorimage name="image_base_color2" type="multioutput">
<input name="file" type="filename" value="BoomBoxWithAxes_baseColor1.png" colorspace="srgb_texture" />
<output name="outcolor" type="color3" />
<output name="outa" type="float" />
</gltf_colorimage>
<!-- Generated material assignments -->
<look name="look">
<materialassign name="MAT_M_BoomBox" material="MAT_M_BoomBox" geom="/BoomBox_Coordinates/BoomBox/BoomBox" />
<materialassign name="MAT_M_Coordinates" material="MAT_M_Coordinates" geom="/BoomBox_Coordinates/CoordinateSystem/CoordinateSystem,/BoomBox_Coordinates/X_axis/X_axis,/BoomBox_Coordinates/Y_axis/Y_axis,/BoomBox_Coordinates/Z_axis/Z_axis" />
</look>
</materialx>
Conversion from MaterialX to glTF¶
This file is then converted back to glTF.
materialXFileName = pkg_resources.resource_filename('materialxgltf', 'data/BoomBoxWithAxes.mtlx')
print('> Load MaterialX document: %s' % materialXFileName)
mtlx2glTFWriter = core.MTLX2GLTFWriter()
doc, libFiles = core.Util.createMaterialXDoc()
mx.readFromXmlFile(doc, materialXFileName, mx.FileSearchPath())
options = core.MTLX2GLTFOptions()
options['debugOutput'] = True
mtlx2glTFWriter.setOptions(options)
gltfString = mtlx2glTFWriter.convert(doc)
if len(gltfString) > 0:
displaySource('Resulting glTF', gltfString, 'json', True)
else:
print('> Failed to convert MaterialX document to glTF')
> Load MaterialX document: C:\Users\home\AppData\Local\Programs\Python\Python311\Lib\site-packages\materialxgltf\data\BoomBoxWithAxes.mtlx - Convert MaterialX node to glTF: M_BoomBox - Append single ORM texture BoomBoxWithAxes_roughnessMetallic.png - Convert MaterialX node to glTF: M_Coordinates - Generating a new primitive for each of 2 materials
Resulting glTF
{
"asset": {
"copyright": "Copyright 2022-2025: Bernard Kwok.",
"generator": "MaterialX 1.39 to glTF 2.0 generator. https://github.com/kwokcb/materialxgltf",
"version": "2.0"
},
"materials": [
{
"name": "M_BoomBox",
"pbrMetallicRoughness": {
"baseColorTexture": {
"index": 0
},
"metallicRoughnessTexture": {
"index": 1
}
},
"normalTexture": {
"index": 2
},
"emissiveTexture": {
"index": 3
},
"emissiveFactor": [
1.0,
1.0,
1.0
]
},
{
"name": "M_Coordinates",
"pbrMetallicRoughness": {
"baseColorTexture": {
"index": 4
},
"metallicFactor": 0.0,
"roughnessFactor": 0.7350000143051147
}
}
],
"textures": [
{
"name": "image_base_color",
"source": 0,
"sampler": 0
},
{
"name": "image_orm",
"source": 1,
"sampler": 0
},
{
"name": "image_normal",
"source": 2,
"sampler": 0
},
{
"name": "image_emissive",
"source": 3,
"sampler": 0
},
{
"name": "image_base_color2",
"source": 4,
"sampler": 0
}
],
"images": [
{
"name": "image_base_color",
"uri": "BoomBoxWithAxes_baseColor.png"
},
{
"name": "image_orm",
"uri": "BoomBoxWithAxes_roughnessMetallic.png"
},
{
"name": "image_normal",
"uri": "BoomBoxWithAxes_normal.png"
},
{
"name": "image_emissive",
"uri": "BoomBoxWithAxes_emissive.png"
},
{
"name": "image_base_color2",
"uri": "BoomBoxWithAxes_baseColor1.png"
}
],
"samplers": [
{
"wrapS": 10497,
"wrapT": 10497,
"magFilter": 9729,
"minFilter": 9986
}
]
}
Embedding Geometry¶
To view the material on sample geometry the sample "shader ball" geometry is imported. The first MaterialX material will be assigned to all of the geometric primitives.
gltfGeometryFile = pkg_resources.resource_filename('materialxgltf', 'data/shaderBall.gltf')
print('> Load glTF geometry file: %s' % mx.FilePath(gltfGeometryFile).getBaseName())
options = core.MTLX2GLTFOptions()
# Set the geometry file to use
options['geometryFile'] = gltfGeometryFile
mtlx2glTFWriter.setOptions(options)
# Perform the conversion
gltfString = mtlx2glTFWriter.convert(doc)
if len(gltfString) > 0:
displaySource('Resulting glTF', gltfString, 'json', False)
else:
print('> Failed to convert MaterialX document to glTF')
> Load glTF geometry file: shaderBall.gltf - glTF geometry file:C:\Users\home\AppData\Local\Programs\Python\Python311\Lib\site-packages\materialxgltf\data\shaderBall.gltf - Embedding glTF geometry file:C:\Users\home\AppData\Local\Programs\Python\Python311\Lib\site-packages\materialxgltf\data\shaderBall.gltf - Convert MaterialX node to glTF: M_BoomBox - Append single ORM texture BoomBoxWithAxes_roughnessMetallic.png - Convert MaterialX node to glTF: M_Coordinates - Generating a new primitive for each of 2 materials
Resulting glTF
{
"asset": {
"version": "2.0",
"copyright": "Copyright 2022-2025: Bernard Kwok.",
"generator": "MaterialX 1.39 to glTF 2.0 generator. https://github.com/kwokcb/materialxgltf"
},
"accessors": [
{
"bufferView": 0,
"byteOffset": 0,
"componentType": 5126,
"count": 36228,
"type": "VEC3",
"max": [
1.0168780088424683,
2.091491937637329,
0.9763060212135315
],
"min": [
-1.1265790462493896,
0.07684300094842911,
-1.1052850484848022
]
},
{
"bufferView": 1,
"byteOffset": 0,
"componentType": 5126,
"count": 36228,
"type": "VEC3",
"max": [
0.9999819993972778,
0.9999993443489075,
0.9999726414680481
],
"min": [
-0.9999819993972778,
-0.9999997615814209,
-0.9999726414680481
]
},
{
"bufferView": 2,
"byteOffset": 0,
"componentType": 5126,
"count": 36228,
"type": "VEC2",
"max": [
0.9962999820709229,
0.9973000288009644
],
"min": [
0.002199999988079071,
0.004199981689453125
]
},
{
"bufferView": 3,
"byteOffset": 0,
"componentType": 5125,
"count": 212424,
"type": "SCALAR",
"max": [
36227
],
"min": [
0
]
},
{
"bufferView": 4,
"byteOffset": 0,
"componentType": 5126,
"count": 9140,
"type": "VEC3",
"max": [
0.6733869910240173,
1.8961529731750488,
0.7821810245513916
],
"min": [
-0.7826089859008789,
0.004230000078678131,
-0.763903021812439
]
},
{
"bufferView": 5,
"byteOffset": 0,
"componentType": 5126,
"count": 9140,
"type": "VEC3",
"max": [
1,
1,
1
],
"min": [
-1,
-1,
-1
]
},
{
"bufferView": 6,
"byteOffset": 0,
"componentType": 5126,
"count": 9140,
"type": "VEC2",
"max": [
0.9962000250816345,
0.7520999908447266
],
"min": [
0.0027000000700354576,
0.0037999749183654785
]
},
{
"bufferView": 7,
"byteOffset": 0,
"componentType": 5125,
"count": 52368,
"type": "SCALAR",
"max": [
9139
],
"min": [
0
]
}
],
"buffers": [
{
"uri": "shaderball_data.bin",
"byteLength": 2510944
}
],
"bufferViews": [
{
"buffer": 0,
"byteOffset": 0,
"byteLength": 434736,
"target": 34962
},
{
"buffer": 0,
"byteOffset": 434736,
"byteLength": 434736,
"target": 34962
},
{
"buffer": 0,
"byteOffset": 869472,
"byteLength": 289824,
"target": 34962
},
{
"buffer": 0,
"byteOffset": 1159296,
"byteLength": 849696,
"target": 34963
},
{
"buffer": 0,
"byteOffset": 2008992,
"byteLength": 109680,
"target": 34962
},
{
"buffer": 0,
"byteOffset": 2118672,
"byteLength": 109680,
"target": 34962
},
{
"buffer": 0,
"byteOffset": 2228352,
"byteLength": 73120,
"target": 34962
},
{
"buffer": 0,
"byteOffset": 2301472,
"byteLength": 209472,
"target": 34963
}
],
"materials": [
{
"name": "M_BoomBox",
"pbrMetallicRoughness": {
"baseColorTexture": {
"index": 0
},
"metallicRoughnessTexture": {
"index": 1
}
},
"normalTexture": {
"index": 2
},
"emissiveTexture": {
"index": 3
},
"emissiveFactor": [
1.0,
1.0,
1.0
]
},
{
"name": "M_Coordinates",
"pbrMetallicRoughness": {
"baseColorTexture": {
"index": 4
},
"metallicFactor": 0.0,
"roughnessFactor": 0.7350000143051147
}
}
],
"meshes": [
{
"name": "Preview_Mesh",
"primitives": [
{
"mode": 4,
"material": 0,
"indices": 3,
"attributes": {
"POSITION": 0,
"NORMAL": 1,
"TEXCOORD_0": 2
}
}
]
},
{
"name": "Calibration_Mesh",
"primitives": [
{
"mode": 4,
"material": 0,
"indices": 7,
"attributes": {
"POSITION": 4,
"NORMAL": 5,
"TEXCOORD_0": 6
}
}
]
},
{
"name": "Preview_Mesh_material_1",
"primitives": [
{
"mode": 4,
"material": 1,
"indices": 3,
"attributes": {
"POSITION": 0,
"NORMAL": 1,
"TEXCOORD_0": 2
}
}
]
},
{
"name": "Calibration_Mesh_material_1",
"primitives": [
{
"mode": 4,
"material": 1,
"indices": 7,
"attributes": {
"POSITION": 4,
"NORMAL": 5,
"TEXCOORD_0": 6
}
}
]
}
],
"nodes": [
{
"name": "Preview_Mesh",
"mesh": 0
},
{
"name": "Calibration_Mesh",
"mesh": 1
},
{
"name": "Preview_Mesh_material_1",
"mesh": 2,
"translation": [
2.5,
0.0,
0
]
},
{
"name": "Calibration_Mesh_material_1",
"mesh": 3,
"translation": [
2.5,
0.0,
0
]
}
],
"scenes": [
{
"nodes": [
0,
1,
2,
3
]
}
],
"scene": 0,
"textures": [
{
"name": "image_base_color",
"source": 0,
"sampler": 0
},
{
"name": "image_orm",
"source": 1,
"sampler": 0
},
{
"name": "image_normal",
"source": 2,
"sampler": 0
},
{
"name": "image_emissive",
"source": 3,
"sampler": 0
},
{
"name": "image_base_color2",
"source": 4,
"sampler": 0
}
],
"images": [
{
"name": "image_base_color",
"uri": "BoomBoxWithAxes_baseColor.png"
},
{
"name": "image_orm",
"uri": "BoomBoxWithAxes_roughnessMetallic.png"
},
{
"name": "image_normal",
"uri": "BoomBoxWithAxes_normal.png"
},
{
"name": "image_emissive",
"uri": "BoomBoxWithAxes_emissive.png"
},
{
"name": "image_base_color2",
"uri": "BoomBoxWithAxes_baseColor1.png"
}
],
"samplers": [
{
"wrapS": 10497,
"wrapT": 10497,
"magFilter": 9729,
"minFilter": 9986
}
]
}
Creating Primitives Per Material¶
This geometry is (transform) instanced for each of the MaterialX materials and then assigned that material.
In this case there are 2 materials.
gltfGeometryFile = pkg_resources.resource_filename('materialxgltf', 'data/shaderBall.gltf')
print('> Load glTF geometry file: %s' % gltfGeometryFile)
options = core.MTLX2GLTFOptions()
options['geometryFile'] = gltfGeometryFile
# Set to create an transform instance for each material and assign the material
options['primsPerMaterial'] = True
mtlx2glTFWriter.setOptions(options)
gltfString = mtlx2glTFWriter.convert(doc)
if len(gltfString) > 0:
displaySource('Resulting glTF', gltfString, 'json', False)
else:
print('> Failed to convert MaterialX document to glTF')
> Load glTF geometry file: C:\Users\home\AppData\Local\Programs\Python\Python311\Lib\site-packages\materialxgltf\data\shaderBall.gltf - glTF geometry file:C:\Users\home\AppData\Local\Programs\Python\Python311\Lib\site-packages\materialxgltf\data\shaderBall.gltf - Embedding glTF geometry file:C:\Users\home\AppData\Local\Programs\Python\Python311\Lib\site-packages\materialxgltf\data\shaderBall.gltf - Convert MaterialX node to glTF: M_BoomBox - Append single ORM texture BoomBoxWithAxes_roughnessMetallic.png - Convert MaterialX node to glTF: M_Coordinates - Generating a new primitive for each of 2 materials
Resulting glTF
{
"asset": {
"version": "2.0",
"copyright": "Copyright 2022-2025: Bernard Kwok.",
"generator": "MaterialX 1.39 to glTF 2.0 generator. https://github.com/kwokcb/materialxgltf"
},
"accessors": [
{
"bufferView": 0,
"byteOffset": 0,
"componentType": 5126,
"count": 36228,
"type": "VEC3",
"max": [
1.0168780088424683,
2.091491937637329,
0.9763060212135315
],
"min": [
-1.1265790462493896,
0.07684300094842911,
-1.1052850484848022
]
},
{
"bufferView": 1,
"byteOffset": 0,
"componentType": 5126,
"count": 36228,
"type": "VEC3",
"max": [
0.9999819993972778,
0.9999993443489075,
0.9999726414680481
],
"min": [
-0.9999819993972778,
-0.9999997615814209,
-0.9999726414680481
]
},
{
"bufferView": 2,
"byteOffset": 0,
"componentType": 5126,
"count": 36228,
"type": "VEC2",
"max": [
0.9962999820709229,
0.9973000288009644
],
"min": [
0.002199999988079071,
0.004199981689453125
]
},
{
"bufferView": 3,
"byteOffset": 0,
"componentType": 5125,
"count": 212424,
"type": "SCALAR",
"max": [
36227
],
"min": [
0
]
},
{
"bufferView": 4,
"byteOffset": 0,
"componentType": 5126,
"count": 9140,
"type": "VEC3",
"max": [
0.6733869910240173,
1.8961529731750488,
0.7821810245513916
],
"min": [
-0.7826089859008789,
0.004230000078678131,
-0.763903021812439
]
},
{
"bufferView": 5,
"byteOffset": 0,
"componentType": 5126,
"count": 9140,
"type": "VEC3",
"max": [
1,
1,
1
],
"min": [
-1,
-1,
-1
]
},
{
"bufferView": 6,
"byteOffset": 0,
"componentType": 5126,
"count": 9140,
"type": "VEC2",
"max": [
0.9962000250816345,
0.7520999908447266
],
"min": [
0.0027000000700354576,
0.0037999749183654785
]
},
{
"bufferView": 7,
"byteOffset": 0,
"componentType": 5125,
"count": 52368,
"type": "SCALAR",
"max": [
9139
],
"min": [
0
]
}
],
"buffers": [
{
"uri": "shaderball_data.bin",
"byteLength": 2510944
}
],
"bufferViews": [
{
"buffer": 0,
"byteOffset": 0,
"byteLength": 434736,
"target": 34962
},
{
"buffer": 0,
"byteOffset": 434736,
"byteLength": 434736,
"target": 34962
},
{
"buffer": 0,
"byteOffset": 869472,
"byteLength": 289824,
"target": 34962
},
{
"buffer": 0,
"byteOffset": 1159296,
"byteLength": 849696,
"target": 34963
},
{
"buffer": 0,
"byteOffset": 2008992,
"byteLength": 109680,
"target": 34962
},
{
"buffer": 0,
"byteOffset": 2118672,
"byteLength": 109680,
"target": 34962
},
{
"buffer": 0,
"byteOffset": 2228352,
"byteLength": 73120,
"target": 34962
},
{
"buffer": 0,
"byteOffset": 2301472,
"byteLength": 209472,
"target": 34963
}
],
"materials": [
{
"name": "M_BoomBox",
"pbrMetallicRoughness": {
"baseColorTexture": {
"index": 0
},
"metallicRoughnessTexture": {
"index": 1
}
},
"normalTexture": {
"index": 2
},
"emissiveTexture": {
"index": 3
},
"emissiveFactor": [
1.0,
1.0,
1.0
]
},
{
"name": "M_Coordinates",
"pbrMetallicRoughness": {
"baseColorTexture": {
"index": 4
},
"metallicFactor": 0.0,
"roughnessFactor": 0.7350000143051147
}
}
],
"meshes": [
{
"name": "Preview_Mesh",
"primitives": [
{
"mode": 4,
"material": 0,
"indices": 3,
"attributes": {
"POSITION": 0,
"NORMAL": 1,
"TEXCOORD_0": 2
}
}
]
},
{
"name": "Calibration_Mesh",
"primitives": [
{
"mode": 4,
"material": 0,
"indices": 7,
"attributes": {
"POSITION": 4,
"NORMAL": 5,
"TEXCOORD_0": 6
}
}
]
},
{
"name": "Preview_Mesh_material_1",
"primitives": [
{
"mode": 4,
"material": 1,
"indices": 3,
"attributes": {
"POSITION": 0,
"NORMAL": 1,
"TEXCOORD_0": 2
}
}
]
},
{
"name": "Calibration_Mesh_material_1",
"primitives": [
{
"mode": 4,
"material": 1,
"indices": 7,
"attributes": {
"POSITION": 4,
"NORMAL": 5,
"TEXCOORD_0": 6
}
}
]
}
],
"nodes": [
{
"name": "Preview_Mesh",
"mesh": 0
},
{
"name": "Calibration_Mesh",
"mesh": 1
},
{
"name": "Preview_Mesh_material_1",
"mesh": 2,
"translation": [
2.5,
0.0,
0
]
},
{
"name": "Calibration_Mesh_material_1",
"mesh": 3,
"translation": [
2.5,
0.0,
0
]
}
],
"scenes": [
{
"nodes": [
0,
1,
2,
3
]
}
],
"scene": 0,
"textures": [
{
"name": "image_base_color",
"source": 0,
"sampler": 0
},
{
"name": "image_orm",
"source": 1,
"sampler": 0
},
{
"name": "image_normal",
"source": 2,
"sampler": 0
},
{
"name": "image_emissive",
"source": 3,
"sampler": 0
},
{
"name": "image_base_color2",
"source": 4,
"sampler": 0
}
],
"images": [
{
"name": "image_base_color",
"uri": "BoomBoxWithAxes_baseColor.png"
},
{
"name": "image_orm",
"uri": "BoomBoxWithAxes_roughnessMetallic.png"
},
{
"name": "image_normal",
"uri": "BoomBoxWithAxes_normal.png"
},
{
"name": "image_emissive",
"uri": "BoomBoxWithAxes_emissive.png"
},
{
"name": "image_base_color2",
"uri": "BoomBoxWithAxes_baseColor1.png"
}
],
"samplers": [
{
"wrapS": 10497,
"wrapT": 10497,
"magFilter": 9729,
"minFilter": 9986
}
]
}
Creating Binary Package¶
For the purposesd of data interop and preview the glTF file is packaged to produce a single glb file.
Result after conversion to glTF as show in the ThreeJS editor
# Load in sample gltf file
gltfFileName = pkg_resources.resource_filename('materialxgltf', 'data/BoomBoxWithAxes_primMaterials.gltf')
gltfFileNameBase = mx.FilePath(gltfGeometryFile).getBaseName()
log = 'Packaging GLB...\n'
log = log + '- Load glTF geometry file: %s' % gltfFileNameBase + '\n'
# Package the gltf file's dependents into a glb file
binaryFileName = str()
binaryFileName = gltfFileName .replace('.gltf', '.glb')
gltfFileNameBase = mx.FilePath(binaryFileName).getBaseName()
try:
saved, images, buffers = mtlx2glTFWriter.packageGLTF(gltfFileName , binaryFileName)
log = log + '- Save GLB file:' + gltfFileNameBase + '. Status: ' + str(saved)
log = log + '\n'
for image in images:
log = log + ' - Embedded image: %s' % image + '\n'
for buffer in buffers:
log = log + ' - Embedded buffer: %s' % buffer + '\n'
log = log + 'Packaging completed.\n'
except Exception as err:
log = log + '- Failed to package GLB file: %s' % err + '\n'
displaySource('Packaging Log', log, 'text', True)
Packaging Log
Packaging GLB...
- Load glTF geometry file: shaderBall.gltf
- Save GLB file:BoomBoxWithAxes_primMaterials.glb. Status: True
- Embedded image: BoomBoxWithAxes_baseColor.png
- Embedded image: BoomBoxWithAxes_roughnessMetallic.png
- Embedded image: BoomBoxWithAxes_normal.png
- Embedded image: BoomBoxWithAxes_emissive.png
- Embedded image: BoomBoxWithAxes_baseColor1.png
- Embedded buffer: shaderball_data.bin
Packaging completed.
Translate Shader and Bake Textures¶
Shader translation and and baking generally are paired together as shader translation inserts an additional MaterialX "translation" node graph which needs to be "baked" out to get direct mappings from the upstream pattern graphs to the target shading model.
The built in baking for MaterialX is used. This baking:
- Requires a GPU which can render GLSL or Metal (for Mac platforms).
- Always writes new files and images to disk.
- Can may halt or not run inside a Jupyter notebook due to it's usage of GPU rendering.
These steps can be performed as an independent pre-process with only a dependency on the core MaterialX distribution -- thus does not require this package to be used.
Marble Example¶
The following assumes access to the "marble" example available from MaterialX github.
Figure: Original shader graph in MaterialX Graph Editor. The marble is a procedural shader.The first step executes shader translation.
# Set search path to defaut so that MaterialX libraries can be found
searchPath = mx.getDefaultDataSearchPath()
# Translate shaders
materialXFileName = pkg_resources.resource_filename('materialxgltf', 'data/standard_surface_marble_solid.mtlx')
materialXFileNameBase = mx.FilePath(materialXFileName).getBaseName()
print('> Load MaterialX file: %s' % materialXFileNameBase)
mtlx2glTFWriter = core.MTLX2GLTFWriter()
doc, libFiles = core.Util.createMaterialXDoc()
mx.readFromXmlFile(doc, materialXFileName, mx.FileSearchPath())
# Perform shader translation and baking if necessary
translatedCount = mtlx2glTFWriter.translateShaders(doc)
title = ' Translated ' + str(translatedCount) + ' shader(s).'
displaySource(title, core.Util.writeMaterialXDocString(doc), 'xml', True)
> Load MaterialX file: standard_surface_marble_solid.mtlx
Translated 1 shader(s).
<?xml version="1.0"?>
<materialx version="1.39" colorspace="lin_rec709">
<nodegraph name="NG_marble1">
<input name="base_color_1" type="color3" value="0.8, 0.8, 0.8" uiname="Color 1" uifolder="Marble Color" />
<input name="base_color_2" type="color3" value="0.1, 0.1, 0.3" uiname="Color 2" uifolder="Marble Color" />
<input name="noise_scale_1" type="float" value="6.0" uisoftmin="1.0" uisoftmax="10.0" uiname="Scale 1" uifolder="Marble Noise" />
<input name="noise_scale_2" type="float" value="4.0" uisoftmin="1.0" uisoftmax="10.0" uiname="Scale 2" uifolder="Marble Noise" />
<input name="noise_power" type="float" value="3.0" uisoftmin="1.0" uisoftmax="10.0" uiname="Power" uifolder="Marble Noise" />
<input name="noise_octaves" type="integer" value="3" uisoftmin="1" uisoftmax="8" uiname="Octaves" uifolder="Marble Noise" />
<position name="obj_pos" type="vector3" />
<dotproduct name="add_xyz" type="float">
<input name="in1" type="vector3" nodename="obj_pos" />
<input name="in2" type="vector3" value="1, 1, 1" />
</dotproduct>
<multiply name="scale_xyz" type="float">
<input name="in1" type="float" nodename="add_xyz" />
<input name="in2" type="float" interfacename="noise_scale_1" />
</multiply>
<multiply name="scale_pos" type="vector3">
<input name="in1" type="vector3" nodename="obj_pos" />
<input name="in2" type="float" interfacename="noise_scale_2" />
</multiply>
<fractal3d name="noise" type="float">
<input name="octaves" type="integer" interfacename="noise_octaves" />
<input name="position" type="vector3" nodename="scale_pos" />
</fractal3d>
<multiply name="scale_noise" type="float">
<input name="in1" type="float" nodename="noise" />
<input name="in2" type="float" value="3.0" />
</multiply>
<add name="sum" type="float">
<input name="in1" type="float" nodename="scale_xyz" />
<input name="in2" type="float" nodename="scale_noise" />
</add>
<sin name="sin" type="float">
<input name="in" type="float" nodename="sum" />
</sin>
<multiply name="scale" type="float">
<input name="in1" type="float" nodename="sin" />
<input name="in2" type="float" value="0.5" />
</multiply>
<add name="bias" type="float">
<input name="in1" type="float" nodename="scale" />
<input name="in2" type="float" value="0.5" />
</add>
<power name="power" type="float">
<input name="in1" type="float" nodename="bias" />
<input name="in2" type="float" interfacename="noise_power" />
</power>
<mix name="color_mix" type="color3">
<input name="bg" type="color3" interfacename="base_color_1" />
<input name="fg" type="color3" interfacename="base_color_2" />
<input name="mix" type="float" nodename="power" />
</mix>
<standard_surface_to_gltf_pbr name="node1" type="multioutput" nodedef="ND_standard_surface_to_gltf_pbr">
<input name="base" type="float" value="1" colorspace="lin_rec709" />
<input name="base_color" type="color3" nodename="color_mix" colorspace="lin_rec709" />
<input name="specular_roughness" type="float" value="0.1" colorspace="lin_rec709" />
</standard_surface_to_gltf_pbr>
<output name="base_color_out" type="color3" nodename="node1" output="base_color_out" />
<output name="metallic_out" type="float" nodename="node1" output="metallic_out" />
<output name="roughness_out" type="float" nodename="node1" output="roughness_out" />
<output name="transmission_out" type="float" nodename="node1" output="transmission_out" />
<output name="thickness_out" type="float" nodename="node1" output="thickness_out" />
<output name="attenuation_color_out" type="color3" nodename="node1" output="attenuation_color_out" />
<output name="sheen_color_out" type="color3" nodename="node1" output="sheen_color_out" />
<output name="sheen_roughness_out" type="float" nodename="node1" output="sheen_roughness_out" />
<output name="clearcoat_out" type="float" nodename="node1" output="clearcoat_out" />
<output name="clearcoat_roughness_out" type="float" nodename="node1" output="clearcoat_roughness_out" />
<output name="emissive_out" type="color3" nodename="node1" output="emissive_out" />
</nodegraph>
<gltf_pbr name="SR_marble1" type="surfaceshader">
<input name="base_color" type="color3" output="base_color_out" nodegraph="NG_marble1" />
<input name="metallic" type="float" output="metallic_out" nodegraph="NG_marble1" />
<input name="roughness" type="float" output="roughness_out" nodegraph="NG_marble1" />
<input name="transmission" type="float" output="transmission_out" nodegraph="NG_marble1" />
<input name="thickness" type="float" output="thickness_out" nodegraph="NG_marble1" />
<input name="attenuation_color" type="color3" output="attenuation_color_out" nodegraph="NG_marble1" />
<input name="sheen_color" type="color3" output="sheen_color_out" nodegraph="NG_marble1" />
<input name="sheen_roughness" type="float" output="sheen_roughness_out" nodegraph="NG_marble1" />
<input name="clearcoat" type="float" output="clearcoat_out" nodegraph="NG_marble1" />
<input name="clearcoat_roughness" type="float" output="clearcoat_roughness_out" nodegraph="NG_marble1" />
<input name="emissive" type="color3" output="emissive_out" nodegraph="NG_marble1" />
</gltf_pbr>
<surfacematerial name="Marble_3D" type="material">
<input name="surfaceshader" type="surfaceshader" nodename="SR_marble1" />
</surfacematerial>
</materialx>
This is followed by texture baking to obtain this result:
MaterialX Graph Editor | Baked Image(s) |
---|---|
Figure: Baked shader graph in MaterialX Graph Editor (lef). Baked images (right)
Baking itself will write out a new MaterialX document and a set of baked images.
There are two issues to be aware of when using baking:
Baking requires that the shader implementation code be accessible as it's either using
GLSL`` or
Metal`` code generation (at time of writing). Thus the appropriate search paths must be set to find the shader code. Additionally any file image references must be resolved taking into any document file name paths qualifiers (such asfileprefix
and tokens.Baking embeds baked image file name references with absolute paths. This is not a problem for the MaterialX document itself, but is a problem for the glTF file which is being generated. To handle this all absolute paths are converted to relative paths, given the assumption that baking will write the files into the same folder location as the baked MaterialX document.
import os
materialXFileName = materialXFileName + '_baked.mtlx'
bakeResolution = 256
# Set the search options properly to ensure the MaterialX definition library can be found
# as well as set search paths for filename resolving.
options = core.MTLX2GLTFOptions()
searchPath = mx.getDefaultDataSearchPath()
if not mx.FilePath(materialXFileName).isAbsolute():
materialXFileName = os.path.abspath(materialXFileName)
searchPath.append(mx.FilePath(materialXFileName).getParentPath())
searchPath.append(mx.FilePath.getCurrentPath())
options['searchPath'] = searchPath
mtlx2glTFWriter.setOptions(options)
# Perform baking
mtlx2glTFWriter.bakeTextures(doc, False, bakeResolution, bakeResolution, False,
False, False, materialXFileName)
doc, libFiles = core.Util.createMaterialXDoc()
mx.readFromXmlFile(doc, materialXFileName, searchPath)
title = ' Baked document: '
displaySource(title, core.Util.writeMaterialXDocString(doc), 'xml', True)
Baked document:
<?xml version="1.0"?>
<materialx version="1.39" colorspace="lin_rec709">
<nodegraph name="NG_baked" colorspace="srgb_texture">
<image name="base_color" type="color3">
<input name="file" type="filename" value="C:\Users\home\AppData\Local\Programs\Python\Python311\Lib\site-packages\materialxgltf\data\Marble_3D_gltf_pbr_base_color.png" />
</image>
<output name="base_color_output" type="color3" nodename="base_color" />
</nodegraph>
<gltf_pbr name="SR_marble1" type="surfaceshader">
<input name="base_color" type="color3" output="base_color_output" nodegraph="NG_baked" />
<input name="metallic" type="float" value="0" />
<input name="roughness" type="float" value="0.0980392" />
<input name="clearcoat_roughness" type="float" value="0.0980392" />
</gltf_pbr>
<surfacematerial name="Marble_3D" type="material">
<input name="surfaceshader" type="surfaceshader" nodename="SR_marble1" />
</surfacematerial>
</materialx>
After baking we perform a final pass to make these image paths relative to that folder as platform specific absolute paths are not valid for glTF.
remappedUris = core.Util.makeFilePathsRelative(doc, materialXFileName)
for uri in remappedUris:
print('- Remapped URI: "%s" to "%s"' % (uri[0], uri[1]))
title = ' Baked document with resolved URIs: '
displaySource(title, core.Util.writeMaterialXDocString(doc), 'xml', True)
- Remapped URI: "C:/Users/home/AppData/Local/Programs/Python/Python311/Lib/site-packages/materialxgltf/data/Marble_3D_gltf_pbr_base_color.png" to "Marble_3D_gltf_pbr_base_color.png"
Baked document with resolved URIs:
<?xml version="1.0"?>
<materialx version="1.39" colorspace="lin_rec709">
<nodegraph name="NG_baked" colorspace="srgb_texture">
<image name="base_color" type="color3">
<input name="file" type="filename" value="Marble_3D_gltf_pbr_base_color.png" />
</image>
<output name="base_color_output" type="color3" nodename="base_color" />
</nodegraph>
<gltf_pbr name="SR_marble1" type="surfaceshader">
<input name="base_color" type="color3" output="base_color_output" nodegraph="NG_baked" />
<input name="metallic" type="float" value="0" />
<input name="roughness" type="float" value="0.0980392" />
<input name="clearcoat_roughness" type="float" value="0.0980392" />
</gltf_pbr>
<surfacematerial name="Marble_3D" type="material">
<input name="surfaceshader" type="surfaceshader" nodename="SR_marble1" />
</surfacematerial>
</materialx>
This final document can then be used as input for conversion to glTF:
# Create a convert and convert write only materials to a glTF file
mtlx2glTFWriter = core.MTLX2GLTFWriter()
options = core.MTLX2GLTFOptions()
options['debugOutput'] = True
mtlx2glTFWriter.setOptions(options)
gltfString = mtlx2glTFWriter.convert(doc)
if len(gltfString) > 0:
displaySource('Translate and Baked Result to glTF', gltfString, 'json', True)
else:
print('> Failed to convert MaterialX document to glTF')
- Convert MaterialX node to glTF: SR_marble1 - Generating a new primitive for each of 1 materials
Translate and Baked Result to glTF
{
"asset": {
"copyright": "Copyright 2022-2025: Bernard Kwok.",
"generator": "MaterialX 1.39 to glTF 2.0 generator. https://github.com/kwokcb/materialxgltf",
"version": "2.0"
},
"materials": [
{
"extensions": {
"KHR_materials_clearcoat": {
"clearcoatRoughnessFactor": 0.09803920239210129
}
},
"name": "SR_marble1",
"pbrMetallicRoughness": {
"baseColorTexture": {
"index": 0
},
"metallicFactor": 0.0,
"roughnessFactor": 0.09803920239210129
}
}
],
"textures": [
{
"name": "NG_baked/base_color",
"source": 0,
"sampler": 0
}
],
"images": [
{
"name": "NG_baked/base_color",
"uri": "Marble_3D_gltf_pbr_base_color.png"
}
],
"samplers": [
{
"wrapS": 10497,
"wrapT": 10497,
"magFilter": 9729,
"minFilter": 9986
}
],
"extensionsUsed": [
"KHR_materials_clearcoat"
]
}
The final result can then be viewed using a viewer such as the ThreeJS editor:
Translation and baking are useful to just convert shading models. Below is a snapshot of a few materials which were downloaded from the Physical Based Site which is maintained by Anton Palmqvist. The materials were converted from MaterialX with the preview using the ThreeJS editor:
Figure: From left to right: "Aluminum", "Sapphire", "Whiteboard", and "Tire" examplesConvenience Functions and Command Line Tools¶
The following are convenience functions and command line tools which are provided as part of this package.
- The file
mtlx2gltf.py`` contains a command line tool that uses a utility function
mtlx2gltfto convert from MaterialX to glTF. Various command line options are mapped to conversion options (
MTLX2GLTFOptions`).
usage: mtlx2gltf.py [-h] [--gltfFileName GLTFFILENAME] [--gltfGeomFileName GLTFGEOMFILENAME] [--primsPerMaterial PRIMSPERMATERIAL]
[--packageBinary PACKAGEBINARY] [--translateShaders TRANSLATESHADERS] [--bakeTextures BAKETEXTURES]
[--bakeResolution BAKERESOLUTION]
mtlxFileName
Utility to convert a MaterialX file to a glTF file
positional arguments:
mtlxFileName Path containing MaterialX file to convert.
options:
-h, --help show this help message and exit
--gltfFileName GLTFFILENAME
Name of MaterialX output file. If not specified the glTF name with "_tomtlx.mtlx" suffix will be used
--gltfGeomFileName GLTFGEOMFILENAME
Name of MaterialX output file. If not specified the glTF name with "_tomtlx.mtlx" suffix will be used
--primsPerMaterial PRIMSPERMATERIAL
Create a new primitive per material and assign the material. Default is False
--packageBinary PACKAGEBINARY
Create a biary packaged GLB file. Default is False
--translateShaders TRANSLATESHADERS
Translate shaders to glTF. Default is False
--bakeTextures BAKETEXTURES
Bake pattern graphs as textures. Default is False
--bakeResolution BAKERESOLUTION
Bake image resolution. Default is 256
- The file gltf2mtlx.py contains a command line tool that uses a utility function to convert from glTF to MaterialX.
Various command line options are mapped to conversion options (
GLTF2MTLXOptions
).
usage: gltf2mtlx.py [-h] [--mtlxFileName MTLXFILENAME] [--createAssignments CREATEASSIGNMENTS] gltfFileName
Utility to convert a glTF file to MaterialX file
positional arguments:
gltfFileName Path containing glTF file to convert.
options:
-h, --help show this help message and exit
--mtlxFileName MTLXFILENAME
Name of MaterialX output file. If not specified the glTF name with "_tomtlx.mtlx" suffix will be used
--createAssignments CREATEASSIGNMENTS
Create material assignments. Default is True
Image and Geometry Pathing¶
glTF does not allow for any pathing to be specified on resources such as image and geometry uris. As such the proper pathing must be set before packaging to glb files. This is handled during the packaing process which uses the pygltflib
Python package.
By default mtlx2gltf
adds search paths (mx.FileSearchPath) to attempt to find the absoluate location of the uri resources, sets the uri and hen performs binary packing. Thus it is possible for instance to specify embedding geometry which is not in the same folder as the root glTF file.
The examples at the beginning of this document show an example Adobe Substance 3D material which was exported and mapped to a MaterialX material. Then it was converted to glTF with different geometry paths.