Using the materialxMaterials API¶

To use the API the package can be installed by cloning the repository and then installing it using pip

In [157]:
# pip install . 

Each loader is encapsulated in a different class. Currently there are two loaders:

  • GPUOpenMaterialLoader which can downlad materials from the AMD GPUOpen database
  • PhysicallyBasedMaterialXLoader which can download materials from the PhysicallyBased database

1. GPUOpenMaterialLoader¶

In [158]:
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.

In [159]:
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.

In [160]:
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
No description has been provided for this image
- Image file textures/Canyon_Maple_Dark_Wood_Normal.png
No description has been provided for this image
- Image file textures/Canyon_Maple_Dark_Wood_baseColor.png
No description has been provided for this image

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.

In [161]:
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

In [162]:
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.

In [163]:
# 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.

In [164]:
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>

3. AmbientCGLoader¶

The AmbientCGLoader allows you to download and extract materials from the ambientCG database.

In the first example we create a loader and download the list of available materials.

In [165]:
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.

In [166]:
materialName = 'WoodFloor038'
result = loader.findMaterial(materialName)
if result:
    fileName = loader.downloadMaterialAsset(materialName) #, opts.downloadmageFormat, opts.downloadResolution)
    if len(fileName) > 0:
        loader.writeDownloadedMaterialToFile('.')
In [167]:
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
No description has been provided for this image
- Image file WoodFloor038_1K-PNG_Color.png
No description has been provided for this image
- Image file WoodFloor038_1K-PNG_Displacement.png
No description has been provided for this image
- Image file WoodFloor038_1K-PNG_NormalDX.png
No description has been provided for this image
- Image file WoodFloor038_1K-PNG_NormalGL.png
No description has been provided for this image
- Image file WoodFloor038_1K-PNG_Roughness.png
No description has been provided for this image
- MaterialX file WoodFloor038_1K-PNG.mtlx
- Image file WoodFloor038.png
No description has been provided for this image

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.

In [168]:
# 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.

In [169]:
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
No description has been provided for this image
- Image file: textures/aerial_asphalt_01_disp_1k.png
No description has been provided for this image
- Image file: textures/aerial_asphalt_01_diff_1k.jpg
No description has been provided for this image
- Image file: aerial_asphalt_01___1k_thumbnail..png
No description has been provided for this image