Using the materialxMaterials API¶
To use the API the package can be installed by cloning the repository and then installing it using pip
# pip install .
Each loader is encapsulated in a different class. Currently there are two loaders:
GPUOpenMaterialLoaderwhich can downlad materials from the AMD GPUOpen databasePhysicallyBasedMaterialXLoaderwhich can download materials from the PhysicallyBased database
1. GPUOpenMaterialLoader¶
from materialxMaterials import GPUOpenLoader
loader = GPUOpenLoader.GPUOpenMaterialLoader()
# Download materials
materials = loader.getMaterials()
materialNames = loader.getMaterialNames()
materialCount = len(materialNames)
print(f'Available number of materials: {materialCount}')
Available number of materials: 454
Downloading Material Packages¶
A regular expression can be used to search for materials. The search is case insensitive and the regular expression is applied to the material name.
The downloadPackageByExpression() method is used below to download all materials that contain the word "Canyon Maple Dark Wood" in their name.
The second argument specifies the package number. Different packages have different resolution images. We use 0 to get the first package.
searchExpr = 'Canyon Maple Dark Wood'
dataItems = loader.downloadPackageByExpression(searchExpr, 0)
for dataItem in dataItems:
print('Found material: ', dataItem[1])
data = dataItem[0]
title = dataItem[1]
Found material: Canyon Maple Dark Wood
The downloadPackageByExpression() method returns a list of material data. The data is in the form of a zip file from which data can be extracted.
In the sample code, the MaterialX file and the images are extracted from the zip file. Image extraction requires passing over the pillow module.
import io
import zipfile
from PIL import Image
from IPython.display import display
extracted_data = loader.extractPackageData(data, Image)
if extracted_data:
for item in extracted_data:
if item['type'] == 'mtlx':
print(f'- MaterialX file {item["file_name"]}')
elif item["type"] == 'image':
print(f'- Image file {item["file_name"]}')
image = item["data"]
small_image = image.resize((256, 256)) # Resize to 256x256 pixels
display(small_image)
#display(image)
- MaterialX file Canyon_Maple_Dark_Wood.mtlx - Image file textures/Canyon_Maple_Dark_Wood_Mask.png
- Image file textures/Canyon_Maple_Dark_Wood_Normal.png
- Image file textures/Canyon_Maple_Dark_Wood_baseColor.png
Materials packages can also be downloaded by index into the material lists. The downloadPackageByIndex() method is used to download the the 3rd material in the list. The last argument is the package number.
indices = [0, 2, 0]
materialList = int(indices[0])
materialIndex = int(indices[1])
materialPackage = int(indices[2])
[data, title] = loader.downloadPackage(materialList, materialIndex, materialPackage)
extracted_data = loader.extractPackageData(data, None)
if extracted_data:
for item in extracted_data:
if item['type'] == 'mtlx':
print(f'- MaterialX file {item["file_name"]}')
elif item["type"] == 'image':
print(f'- Image file {item["file_name"]}')
- MaterialX file Art_Deco_Gatsby_Wallpaper.mtlx - Image file textures/Art_Deco_Gatsby_Wallpaper_Mask.png - Image file textures/Art_Deco_Gatsby_Wallpaper_baseColor.png - Image file textures/Art_Deco_Gatsby_Wallpaper_Normal.png
2. PhysicallyBasedMaterialXLoader¶
The PhysicallyBasedMaterialXLoader has a dependence on MaterialX to generate MaterialX documents from the material
descriptions downloaded from the PhysicallyBased database.
The material information can first be downloaded using getMaterialsFromURL() method. The method returns a list of material data in JSON format
from materialxMaterials import physicallyBasedMaterialX as pbmx
import MaterialX as mx
jsonMat = None
pb_loader = pbmx.PhysicallyBasedMaterialLoader(mx, None)
jsonMat = pb_loader.getMaterialsFromURL()
print(f'Found {len(jsonMat)} materials in PhysicallyBased MaterialX library.')
# Print JSON formatted
#import json
#print(json.dumps(jsonMat, indent=4))
Found 84 materials in PhysicallyBased MaterialX library.
This data can be parsed to create MaterialX documents using the convertToMaterialX() method. One of the supported shading models can be used. In the example below we convert to the OpenPBR model.
We set a postfix string to make a unique id.
# Convert to OpenPBR material
pb_loader.convertToMaterialX(['Banana'], 'open_pbr_surface', {}, 'OpenPBR')
matdoc_string = pb_loader.convertToMaterialXString()
print(matdoc_string)
<?xml version="1.0"?>
<materialx version="1.39">
<!--Physically Based Materials from https://api.physicallybased.info -->
<!-- Content Author: Anton Palmqvist, https://antonpalmqvist.com/ -->
<!-- Content processsed via REST API and mapped to MaterialX V1.39.5 -->
<!-- Target Shading Model: open_pbr_surface -->
<!-- Utility Author: Bernard Kwok. kwokcb@gmail.com -->
<!-- Generated shader: Banana_OpenPBR_SHD_PBM -->
<open_pbr_surface name="Banana_OpenPBR_SHD_PBM" type="surfaceshader" uiname="Banana" uifolder="Organic" doc="Reference: https://raw.githubusercontent.com/AntonPalmqvist/physically-based-api/main/images/renders/cycles/600/banana.jpeg">
<input name="base_color" type="color3" value="0.634,0.532,0.111" />
<input name="base_metalness" type="float" value="0" />
<input name="specular_roughness" type="float" value="0.5" />
<input name="specular_ior" type="float" value="1.5" />
</open_pbr_surface>
<!-- Generated material: Banana_OpenPBR_MAT_PBM -->
<surfacematerial name="Banana_OpenPBR_MAT_PBM" type="material">
<input name="surfaceshader" type="surfaceshader" nodename="Banana_OpenPBR_SHD_PBM" />
</surfacematerial>
</materialx>
In this example we generate glTF PBR materials instead.
pb_loader.convertToMaterialX(['Banana'], 'gltf_pbr', {}, 'glTF')
matdoc_string = pb_loader.convertToMaterialXString()
print(matdoc_string)
<?xml version="1.0"?>
<materialx version="1.39">
<!--Physically Based Materials from https://api.physicallybased.info -->
<!-- Content Author: Anton Palmqvist, https://antonpalmqvist.com/ -->
<!-- Content processsed via REST API and mapped to MaterialX V1.39.5 -->
<!-- Target Shading Model: gltf_pbr -->
<!-- Utility Author: Bernard Kwok. kwokcb@gmail.com -->
<!-- Generated shader: Banana_glTF_SHD_PBM -->
<gltf_pbr name="Banana_glTF_SHD_PBM" type="surfaceshader" uiname="Banana" uifolder="Organic" doc="Reference: https://raw.githubusercontent.com/AntonPalmqvist/physically-based-api/main/images/renders/cycles/600/banana.jpeg">
<input name="base_color" type="color3" value="0.634,0.532,0.111" />
<input name="metallic" type="float" value="0" />
<input name="roughness" type="float" value="0.5" />
<input name="ior" type="float" value="1.5" />
</gltf_pbr>
<!-- Generated material: Banana_glTF_MAT_PBM -->
<surfacematerial name="Banana_glTF_MAT_PBM" type="material">
<input name="surfaceshader" type="surfaceshader" nodename="Banana_glTF_SHD_PBM" />
</surfacematerial>
</materialx>
from materialxMaterials import ambientCGLoader
import MaterialX as mx
# Create the loader
loader = ambientCGLoader.AmbientCGLoader(mx, None)
# Download the list of available materials
materials = loader.downloadMaterialsList()
materialNames = loader.getMaterialNames()
materialCount = len(materialNames)
print(f'Found {materialCount} materials in AmbientCG library')
Found 1959 materials in AmbientCG library
Downloading Material Packages¶
Once we have a list we can search for a specific material name. In this case we look for the first
occurance of WoodFloor038. We specify the format to download and the image resolition.
"1" resolution means 1K image files.
materialName = 'WoodFloor038'
result = loader.findMaterial(materialName)
if result:
fileName = loader.downloadMaterialAsset(materialName) #, opts.downloadmageFormat, opts.downloadResolution)
if len(fileName) > 0:
loader.writeDownloadedMaterialToFile('.')
import io
import zipfile
from PIL import Image
from IPython.display import display
def extractPackageData(data, pilImage):
'''
Extract the package data from a zip file.
@param data: The data to extract.
@param pilImage: The PIL image module.
@return: A list of extracted data of the form:
[ { 'file_name': file_name, 'data': data, 'type': type } ]
'''
if not pilImage:
pilImage = PILImage
if not pilImage:
self.logger.debug('Pillow (PIL) image module provided. Image data will not be extracted.')
#zip_object = io.BytesIO(data)
extracted_data_list = []
with zipfile.ZipFile(data, 'r') as zip_file:
# Iterate through the files in the zip archive
for file_name in zip_file.namelist():
# Extract each file into memory
extracted_data = zip_file.read(file_name)
if file_name.endswith('.mtlx'):
mtlx_string = extracted_data.decode('utf-8')
extracted_data_list.append( {'file_name': file_name, 'data': mtlx_string, 'type': 'mtlx'} )
# If the data is a image, create a image in Python
elif file_name.endswith('.png'):
if pilImage:
image = pilImage.open(io.BytesIO(extracted_data))
else:
image = None
extracted_data_list.append( {'file_name': file_name, 'data': image, 'type': 'image'} )
return extracted_data_list
# Extract and display MaterialX and image files from the downloaded zip
if fileName:
with open(fileName, 'rb') as f:
data = io.BytesIO(f.read())
if data:
print(f'Extracting data from zip file: {fileName}')
extracted_data = extractPackageData(data, Image)
if extracted_data:
for item in extracted_data:
if item['type'] == 'mtlx':
print(f'- MaterialX file {item["file_name"]}')
elif item['type'] == 'image':
print(f'- Image file {item["file_name"]}')
image = item['data']
small_image = image.resize((256, 256)) # Resize to 256x256 pixels
display(small_image)
# Delete the unzipped folder and the zip file
import shutil
import os
#shutil.rmtree(os.path.splitext(fileName)[0], ignore_errors=True)
os.remove(fileName)
Extracting data from zip file: WoodFloor038_1K-PNG.zip - Image file WoodFloor038_1K-PNG_AmbientOcclusion.png
- Image file WoodFloor038_1K-PNG_Color.png
- Image file WoodFloor038_1K-PNG_Displacement.png
- Image file WoodFloor038_1K-PNG_NormalDX.png
- Image file WoodFloor038_1K-PNG_NormalGL.png
- Image file WoodFloor038_1K-PNG_Roughness.png
- MaterialX file WoodFloor038_1K-PNG.mtlx - Image file WoodFloor038.png
4. PolyHavenLoader¶
The PolyHavenLoader allows you to download and extract materials from the Poly Haven database. This loader provides access to a wide variety of high-quality, free textures and materials.
Below is an example of how to use the loader to search for and download a material package.
# PolyHavenLoader usage example (following polyHavenLoaderCmd.py logic)
from pathlib import Path
import json
from materialxMaterials import polyHavenLoader
import os
import zipfile
import io
from PIL import Image
from IPython.display import display
import shutil
# Set download parameters
download_id = "polystyrene"
download_id = "aerial_asphalt_01"
resolution = "1k" # Specify resolution: '1k', '2k', '4k', or '8k'
data_folder = "data/PolyHavenMaterialX"
os.makedirs(data_folder, exist_ok=True)
# Fetch MaterialX assets list
loader = polyHavenLoader.PolyHavenLoader()
if loader:
materialx_assets, all_assets, filtered_polyhaven_assets = loader.fetch_materialx_assets(download_id=download_id)
print(f'Found {len(all_assets)} materials in PolyHaven library.')
# Save list to file
#data_file = Path(data_folder) / "polyhaven_materialx_assets.json"
#with open(data_file, "w") as f:
# json.dump(materialx_assets, f, indent=4)
# Download the material (zip file)
entry_id = download_id + '___' + resolution
entry = materialx_assets.get(entry_id)
if entry:
asset_list = {entry_id: entry, resolution: resolution}
if asset_list:
id, mtlx_string, texture_binaries = loader.download_asset(asset_list)
loader.save_materialx_with_textures(id, mtlx_string, texture_binaries, data_folder)
print(f'Downloaded "{download_id}" material to: {data_folder}/{id}_materialx.zip')
Found 735 materials in PolyHaven library. Downloaded "aerial_asphalt_01" material to: data/PolyHavenMaterialX/aerial_asphalt_01___1k_materialx.zip
After downloading, we can again unzip the contents and display them here.
if entry:
zip_path = Path(data_folder) / f"{id}_materialx.zip"
print(f'Extract contents of zip file: {zip_path}\n')
with zipfile.ZipFile(zip_path, 'r') as zip_file:
for file_name in zip_file.namelist():
if file_name.endswith('.mtlx'):
mtlx_doc = zip_file.read(file_name).decode('utf-8')
print(f'- MaterialX file: {file_name}')
doc = mx.createDocument()
mx.readFromXmlString(doc, mtlx_doc)
mtlx_doc = mx.writeToXmlString(doc)
print(mtlx_doc)
elif file_name.endswith('.png') or file_name.endswith('.jpg') or file_name.endswith('.jpeg'):
image = Image.open(io.BytesIO(zip_file.read(file_name)))
print(f'- Image file: {file_name}')
small_image = image.resize((256, 256))
display(small_image)
# Clean up
shutil.rmtree(zip_path.parent, ignore_errors=True)
else:
print(f"No asset found for ID '{entry_id}'")
Extract contents of zip file: data/PolyHavenMaterialX/aerial_asphalt_01___1k_materialx.zip
- MaterialX file: aerial_asphalt_01___1k.mtlx
<?xml version="1.0"?>
<materialx version="1.39">
<nodegraph name="NG_aerial_asphalt_01">
<texcoord name="Texcoord" type="vector2">
<input name="index" type="integer" value="0" />
</texcoord>
<image name="diff" type="color3">
<input name="file" type="filename" value="textures/aerial_asphalt_01_diff_1k.jpg" colorspace="srgb_texture" />
<input name="default" type="color3" value="0.8, 0.6, 0.4" />
<input name="texcoord" type="vector2" nodename="Texcoord" />
</image>
<image name="rough" type="float">
<input name="file" type="filename" value="textures/aerial_asphalt_01_rough_1k.exr" />
<input name="default" type="float" value="0.3" />
<input name="texcoord" type="vector2" nodename="Texcoord" />
</image>
<image name="normal" type="vector3">
<input name="file" type="filename" value="textures/aerial_asphalt_01_nor_gl_1k.exr" />
<input name="default" type="vector3" value="0.5, 0.5, 1" />
<input name="texcoord" type="vector2" nodename="Texcoord" />
</image>
<normalmap name="Normalmap" type="vector3">
<input name="in" type="vector3" nodename="normal" />
<input name="scale" type="float" value="1" />
</normalmap>
<tangent name="Tangent" type="vector3">
<input name="space" type="string" value="world" />
<input name="index" type="integer" value="0" />
</tangent>
<image name="disp" type="float">
<input name="file" type="filename" value="textures/aerial_asphalt_01_disp_1k.png" />
<input name="default" type="float" value="0" />
<input name="texcoord" type="vector2" nodename="Texcoord" />
</image>
<output name="output_diff_out" type="color3" nodename="diff" />
<output name="output_rough_out" type="float" nodename="rough" />
<output name="output_Normalmap_out" type="vector3" nodename="Normalmap" />
<output name="output_Tangent_out" type="vector3" nodename="Tangent" />
<output name="output_disp_out" type="float" nodename="disp" />
</nodegraph>
<displacement name="Displacement" type="displacementshader">
<input name="displacement" type="float" output="output_disp_out" nodegraph="NG_aerial_asphalt_01" />
<input name="scale" type="float" value="0.01" />
</displacement>
<standard_surface name="SR_aerial_asphalt_01" type="surfaceshader">
<input name="base" type="float" value="1" />
<input name="base_color" type="color3" output="output_diff_out" nodegraph="NG_aerial_asphalt_01" />
<input name="diffuse_roughness" type="float" value="0" />
<input name="metalness" type="float" value="0" />
<input name="specular" type="float" value="1" />
<input name="specular_color" type="color3" value="1, 1, 1" />
<input name="specular_roughness" type="float" output="output_rough_out" nodegraph="NG_aerial_asphalt_01" />
<input name="specular_IOR" type="float" value="1.5" />
<input name="specular_anisotropy" type="float" value="0" />
<input name="specular_rotation" type="float" value="0" />
<input name="transmission" type="float" value="0" />
<input name="transmission_color" type="color3" value="1, 1, 1" />
<input name="transmission_depth" type="float" value="0" />
<input name="transmission_scatter" type="color3" value="0, 0, 0" />
<input name="transmission_scatter_anisotropy" type="float" value="0" />
<input name="transmission_dispersion" type="float" value="0" />
<input name="transmission_extra_roughness" type="float" value="0" />
<input name="subsurface" type="float" value="0" />
<input name="subsurface_color" type="color3" value="1, 1, 1" />
<input name="subsurface_radius" type="color3" value="1, 1, 1" />
<input name="subsurface_scale" type="float" value="1" />
<input name="subsurface_anisotropy" type="float" value="0" />
<input name="sheen" type="float" value="0" />
<input name="sheen_color" type="color3" value="1, 1, 1" />
<input name="sheen_roughness" type="float" value="0.3" />
<input name="emission" type="float" value="0" />
<input name="emission_color" type="color3" value="1, 1, 1" />
<input name="normal" type="vector3" output="output_Normalmap_out" nodegraph="NG_aerial_asphalt_01" />
<input name="tangent" type="vector3" output="output_Tangent_out" nodegraph="NG_aerial_asphalt_01" />
</standard_surface>
<surfacematerial name="aerial_asphalt_01" type="material">
<input name="surfaceshader" type="surfaceshader" nodename="SR_aerial_asphalt_01" />
<input name="displacementshader" type="displacementshader" nodename="Displacement" />
</surfacematerial>
</materialx>
- Image file: textures/aerial_asphalt_01_rough_1k.jpg
- Image file: textures/aerial_asphalt_01_disp_1k.png
- Image file: textures/aerial_asphalt_01_diff_1k.jpg
- Image file: aerial_asphalt_01___1k_thumbnail..png