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.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
directory_name = "data"
files = pkg_resources.resource_listdir('materialxgltf', directory_name)
result = ''
for file in files:
result = result + file + '\n'
displaySource('Available data files', result, 'text', True)
BoomBoxWithAxes.gltf
BoomBoxWithAxes.mtlx
BoomBoxWithAxes.mtlx.gltf
BoomBoxWithAxes.mtlx.gltf_stripped.mtlx
BoomBoxWithAxes_baseColor.png
BoomBoxWithAxes_baseColor1.png
BoomBoxWithAxes_emissive.png
BoomBoxWithAxes_normal.png
BoomBoxWithAxes_primMaterials.glb
BoomBoxWithAxes_primMaterials.gltf
BoomBoxWithAxes_roughnessMetallic.png
gltf_test_nondefault_pbr.mtlx
gltf_test_nondefault_pbr.mtlx.glb
gltf_test_nondefault_pbr.mtlx.gltf
Marble_3D_gltf_pbr_base_color.png
shaderball.gltf
shaderball_data.bin
standard_surface_marble_solid.mtlx
standard_surface_marble_solid.mtlx_baked.mtlx
standard_surface_marble_solid_translated.mtlx
import pkg_resources
import MaterialX as mx
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)
Converting: BoomBoxWithAxes.gltf - Convert gLTF material to MateriaLX: MAT_M_BoomBox - Convert gLTF material to MateriaLX: MAT_M_Coordinates - Generated MaterialX document is valid
<?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>
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
<?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>
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
{
"asset": {
"copyright": "Copyright 2022-2024: 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
}
]
}
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
{
"asset": {
"version": "2.0",
"copyright": "Copyright 2022-2024: 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
]
},
{
"name": "Calibration_Mesh_material_1",
"mesh": 3,
"translation": [
2.5,
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
}
]
}
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
{
"asset": {
"version": "2.0",
"copyright": "Copyright 2022-2024: 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
]
},
{
"name": "Calibration_Mesh_material_1",
"mesh": 3,
"translation": [
2.5,
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
}
]
}
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 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.
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:
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.
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
<?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 as fileprefix
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)
<?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"
<?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
{
"asset": {
"copyright": "Copyright 2022-2024: 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" examplesThe following are convenience functions and command line tools which are provided as part of this package.
mtlx2gltf.py`` contains a command line tool that uses a utility function
mtlx2gltf to 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
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
glTF does not allow for any pathing specifieds 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.