MaterialX Needs a Single-File Binary Container

Back to Blog Listing

MaterialX Needs a Single-File Binary Container

MaterialX and OpenPBR need a first-class single-file package format for collaboration and delivery. The right answer is not a new format — it is a .pbr file, a GLB containing a single OpenPBR material and its textures.

Ben HoustonApril 15, 202617 min read

At Land of Assets, one of the pillars of our pipeline is standardizing on MaterialX and OpenPBR for material interchange.

That decision has been great for collaboration. But in production, it exposes a practical gap: how do we deliver a MaterialX material as one portable, atomic file that includes all required textures?

For local authoring, loose files are fine. For real collaboration, versioning, publishing, review, marketplace distribution, and API-driven delivery, loose files are friction.

The Problem

A typical MaterialX asset is:

  • material.mtlx (XML graph + parameters)
  • multiple texture files (base color, normal, roughness, metalness, etc.)
  • relative file paths that can easily break when moved

That is manageable in a DCC project folder. It is painful in a repository, a CDN, a database object store, or a collaborative material library where materials are constantly being copied, renamed, and shared.

Without a single-file container, you get:

  • broken relative paths after moves or renames
  • texture mismatch bugs between environments
  • more complex upload/download APIs
  • harder permissioning and audit trails
  • brittle cache invalidation and CDN behavior

With a single-file container, you get:

  • deterministic portability
  • simpler automation pipelines
  • cleaner artifact management
  • easier integrity checks and provenance tracking

What we need in practice is simple:

  1. One file to upload/download.
  2. One hash for deduplication and content addressing.
  3. One object to version and permission.
  4. One package that loads everywhere the same way.

For us at Land of Assets, this is directly tied to making materials truly reusable — across DCC tools like Maya, Blender, and Houdini; game engines like Unity and Unreal Engine; and web renderers like Three.js and Babylon.js.

Shader Balls

Existing Approaches (and Their Trade-offs)

There are a few ways to get close today, but each has drawbacks.

1) zipfile.mtlx.zip

This is the obvious workaround: put the .mtlx and textures into a zip.

It works. But it is not a format-level standard for MaterialX itself. Tooling support is inconsistent, metadata conventions are undefined, and load behavior is usually custom per application.

Most importantly, a generic zip does not describe material semantics. It is just an archive.

It has seen real-world adoption in online MaterialX repositories (including Poly Haven and AMD's Material Library), which reinforces how strong the demand is for single-file delivery. But it remains a convention, not a purpose-built MaterialX container standard.

2) Inline textures in XML (for example, base64 payloads)

This gives you one text file, but it is a poor fit for production:

  • larger file size overhead (~33% inflation)
  • extra encode/decode work
  • awkward diffing and debugging
  • not friendly to streaming binary payloads

Great for a quick prototype. Not great for a standard interchange container.

Requirements

After working through this in a real material repository, I think a practical MaterialX packaging standard needs the following:

  1. Single-file, self-contained delivery so every required texture ships with the material.
  2. Binary texture payloads without base64 inflation.
  3. Deterministic packaging so two tools can generate byte-stable or semantically equivalent artifacts.
  4. Web-friendly parsing and tooling with minimal dependencies.
  5. A JSON representation of the MaterialX graph that avoids forcing XML libraries into every integration point.
  6. Forward-compatible extensibility for metadata, signatures, provenance, and future extensions.
  7. No mandatory whole-file compression since texture payloads are usually already compressed.
  8. Existing ecosystem support so adoption does not start from zero.

That last requirement is the one that changes everything.

Proposed Solution: Just Use GLB

When I first approached this problem, I assumed we needed a new binary container format — something purpose-built for MaterialX, inspired by GLB but with its own magic number, its own chunk semantics, its own parsers. I even drafted a full specification for it.

Then I asked the obvious question: what is actually different between that new format and a GLB file containing a single material?

The honest answer: almost nothing. The binary layout is the same. The texture storage mechanism is the same. The chunk structure is the same. The only differences were a different magic number and a slightly different JSON schema — and those differences bought us nothing except an empty ecosystem on day one.

A single-file MaterialX material should simply be a GLB file — with the .pbr extension.

What this looks like

A MaterialX material package is a valid GLB file, saved with the .pbr extension, that contains:

  • Exactly one material, defined entirely through the MaterialX extension for glTF (e.g. KHR_materials_materialx)
  • OpenPBR as the sole surface shading model. The material graph must use open_pbr_surface — not standard_surface, not gltf_pbr, not UsdPreviewSurface. OpenPBR is actively evolving as the industry-standard surface model, and tying the packaging convention to it means .pbr files stay current as the standard advances. One surface model also means every consuming tool needs exactly one translation path into its native shading representation.
  • All required textures, stored through glTF's standard images / textures / bufferViews mechanism
  • Minimal scene scaffolding — an empty scene and nodes array, as the glTF spec requires — which adds negligible overhead
  • No geometry, no animations, no cameras — just the material and its textures

The .pbr extension is deliberate. The file is byte-for-byte a valid GLB — you can rename it to .glb and open it in any viewer — but the distinct extension communicates that this is a material package, not a 3D scene. It enables tools, file browsers, and APIs to route material files differently from scene files without inspecting the contents.

This convention is deliberately opinionated. It does not use glTF 2.0's built-in flat material model (pbrMetallicRoughness, KHR_materials_unlit, etc.) and it does not allow alternative MaterialX surface models. The old glTF flat model is archaic relative to what OpenPBR can express, and any flat PBR material can be trivially represented as an OpenPBR graph anyway. Similarly, standard_surface, gltf_pbr, and other MaterialX surface models can all be converted to OpenPBR — but requiring every consumer to handle every possible surface model recreates the very compatibility problem we are trying to solve. One surface model, one translation path, no ambiguity.

The minimal JSON payload

Here is what the complete glTF JSON looks like inside the GLB for a single MaterialX material with three texture maps:

{
  "asset": {
    "version": "2.0",
    "generator": "LandOfAssets-MaterialX-Packager"
  },
  "extensionsUsed": ["KHR_materials_materialx"],
  "extensionsRequired": ["KHR_materials_materialx"],
  "scene": 0,
  "scenes": [{ "nodes": [] }],
  "buffers": [{ "byteLength": 2883584 }],
  "bufferViews": [
    { "buffer": 0, "byteOffset": 0, "byteLength": 1048576 },
    { "buffer": 0, "byteOffset": 1048576, "byteLength": 1048576 },
    { "buffer": 0, "byteOffset": 2097152, "byteLength": 786432 }
  ],
  "images": [
    { "bufferView": 0, "mimeType": "image/png", "name": "base_color" },
    { "bufferView": 1, "mimeType": "image/png", "name": "normal" },
    { "bufferView": 2, "mimeType": "image/png", "name": "roughness_metalness" }
  ],
  "textures": [{ "source": 0 }, { "source": 1 }, { "source": 2 }],
  "materials": [
    {
      "name": "BrushedCopper",
      "extensions": {
        "KHR_materials_materialx": {
          "graph": "... MaterialX graph referencing textures by index ..."
        }
      }
    }
  ]
}

The overhead of glTF scaffolding — scene, scenes, asset — is a few dozen bytes. Every GLB parser on the planet already handles it. The textures are stored exactly the way GLB has always stored textures: bufferViews define byte ranges in the binary chunk, images reference those views by index, and textures reference images. No new mechanisms to specify, no new parsers to write.

Why this is better than a new format

The benefits of reusing GLB rather than inventing a new container are substantial:

  • Instant ecosystem. Every GLB-capable viewer, engine, and tool can open the file on day one. They may not understand the MaterialX extension yet, but they can inspect the structure, read the textures, and validate the container.
  • No new spec for the container. The only new specification work is the MaterialX extension within glTF — the container problem is already solved.
  • Battle-tested binary layout. GLB ships in every major browser and 3D engine. Its chunk structure, 4-byte alignment, padding rules, and bufferView indirection are proven at scale.
  • Accelerates MaterialX in glTF. Every tool that learns to read a MaterialX material package also learns to read MaterialX materials inside full glTF scenes. Adoption of the packaging convention directly drives adoption of the extension. A rising tide lifts all boats.

Why not zip?

Why not simply package .mtlx and textures in zip? Because for this use case zip usually adds little value:

  • texture payloads dominate file size and are already compressed (PNG, JPEG, WebP, KTX2)
  • MaterialX graph text is typically small compared to image payloads
  • archive-level compression adds format complexity for marginal practical gains

And on the web, transport compression is already handled by the delivery layer (gzip, zstd, and similar encodings). That optimization belongs at HTTP/CDN boundaries, not baked into the container format.

The MaterialX Extension for glTF

The packaging convention depends on a MaterialX extension for glTF — likely KHR_materials_materialx — that defines how a MaterialX graph is represented as a JSON payload within the glTF material object.

This extension needs a canonical JSON representation of MaterialX graphs. MaterialX today is defined as XML, so the extension effectively defines a MaterialX-JSON encoding: a JSON structure that maps deterministically and losslessly to and from MaterialX XML.

This is not a radical departure — it is a notation change. The graph semantics, node types, input/output connections, and parameter values remain identical. Only the serialization changes.

For example, a simple OpenPBR material in XML:

<materialx version="1.39">
  <open_pbr_surface name="BrushedCopper" type="surfaceshader">
    <input name="base_color" type="color3" nodename="base_color_tex" />
    <input name="specular_roughness" type="float" value="0.35" />
    <input name="base_metalness" type="float" value="1.0" />
  </open_pbr_surface>
  <tiledimage name="base_color_tex" type="color3">
    <input name="file" type="filename" value="base_color.png" />
  </tiledimage>
</materialx>

Inside a .pbr file, this becomes part of the extension payload, with texture file references replaced by glTF texture indices:

{
  "KHR_materials_materialx": {
    "version": "1.39",
    "nodes": [
      {
        "type": "open_pbr_surface",
        "name": "BrushedCopper",
        "outputs": "surfaceshader",
        "inputs": [
          { "name": "base_color", "type": "color3", "nodename": "base_color_tex" },
          { "name": "specular_roughness", "type": "float", "value": 0.35 },
          { "name": "base_metalness", "type": "float", "value": 1.0 }
        ]
      },
      {
        "type": "tiledimage",
        "name": "base_color_tex",
        "outputs": "color3",
        "inputs": [{ "name": "file", "type": "filename", "textureIndex": 0 }]
      }
    ]
  }
}

Note how the file input uses textureIndex instead of a file path, referencing the glTF textures array. Inside a .pbr file, texture references are always resolved through glTF's standard textures -> images -> bufferViews chain — there are no file paths to break.

This JSON encoding of MaterialX graphs does not exist yet as a formal standard. Defining it as part of the glTF extension is the real specification work. It would also have value beyond material packaging — any JavaScript or Python tool that works with MaterialX would benefit from a standard JSON representation.

FAQ

Why not a new dedicated container format?

When I started thinking about this problem, I designed a purpose-built binary format called MTLB with its own magic number, chunk types, and JSON schema. Then I realized the binary layout was byte-for-byte identical to GLB.

The only real differences were the magic number and a slightly different JSON schema — and those differences came at a steep cost: zero parsers on day one, a new specification to maintain, and a fragmented ecosystem where MaterialX materials lived in their own container that no existing tool could open.

GLB solves the container problem. The real work is the MaterialX extension for glTF, and that work is needed regardless of whether the container is GLB or something new.

Can existing GLB viewers open .pbr files?

Yes. A .pbr file is a fully valid GLB file. Renaming it to .glb is enough for any viewer that does not recognize the .pbr extension. Tools that support glTF/GLB can also be configured to associate .pbr with their GLB parser — no code changes needed, just a file association.

Viewers that do not yet support the KHR_materials_materialx extension will not be able to render the material, but they can still inspect the file structure, read metadata, extract textures, and validate the container. This is strictly better than a truly new format, where unsupported tools would not be able to open the file at all.

Because the file declares extensionsRequired: ["KHR_materials_materialx"], compliant viewers will clearly signal that they need the MaterialX extension to fully display the material, rather than falling back to a blank or incorrect material.

Why .pbr instead of .glb?

The file is a valid GLB either way. The distinct extension serves a practical purpose: it tells humans, tools, and APIs at a glance that this file is a material, not a 3D scene. In a material library with thousands of files, in a CDN routing layer, or in an upload API that needs to distinguish material packages from scene assets, the extension is the first and fastest signal.

It also sets expectations. A .glb file might contain anything — meshes, animations, cameras, multiple materials. A .pbr file contains exactly one OpenPBR material and its textures. The extension encodes the convention.

What about glTF's built-in PBR material model?

The material package deliberately omits glTF 2.0's flat material properties (pbrMetallicRoughness, KHR_materials_unlit, etc.). The OpenPBR graph is the sole material definition.

This is intentional. The flat PBR model served the ecosystem well, but OpenPBR represents a generational leap in expressiveness. Any material that can be described with pbrMetallicRoughness can be trivially represented as an OpenPBR graph. The reverse is not true — OpenPBR can express layered materials, subsurface scattering, thin-film coatings, and shading models that the flat model cannot.

For material packaging, we want a clean, forward-looking format. Carrying legacy material properties alongside the OpenPBR graph would create ambiguity about which definition is authoritative.

Why mandate OpenPBR instead of allowing any MaterialX surface model?

MaterialX supports many surface shading models: standard_surface (Autodesk Standard Surface), gltf_pbr, UsdPreviewSurface, and others. If we allowed any of them in material packages, we would just move the compatibility problem from file formats into shading models. Every consuming tool would need to handle every possible surface model, or fail silently.

Mandating open_pbr_surface eliminates that problem. OpenPBR is the newest and most expressive surface model in the MaterialX ecosystem, developed collaboratively by Adobe, Autodesk, and others through ASWF specifically to be the industry-standard surface model. It subsumes what standard_surface and gltf_pbr can express.

With one mandated surface model, every tool needs exactly one translation path: OpenPBR to its native shading representation. The translation happens at the consumption boundary, not inside the interchange format. Consider the breadth of tools that can share a single .pbr file:

  • DCC tools — Maya, Blender, Houdini each translate OpenPBR into their native shading models (e.g. standard_surface for Arnold, Principled BSDF for Cycles, MaterialX for Karma)
  • Game engines — Unity and Unreal Engine map OpenPBR parameters onto their runtime shader pipelines
  • Web renderers — Three.js and Babylon.js translate OpenPBR to their WebGL/WebGPU PBR shaders

One .pbr file, understood everywhere. Each tool handles its own translation from OpenPBR — but the interchange format is universal.

This is an opinionated choice. OpenPBR is not a static target — it is actively evolving as the interoperable surface standard, with ongoing development through ASWF. By standardizing the .pbr format on OpenPBR, we tie the packaging convention to that evolution. As OpenPBR gains new capabilities — new lobes, new parameters, new physical models — .pbr files gain them automatically. We are not betting on a snapshot; we are aligning with a living standard that the industry is converging on.

How does this relate to using MaterialX materials in full glTF scenes?

The same KHR_materials_materialx extension that enables the packaging convention also enables MaterialX materials in complete glTF scenes with meshes, animations, and everything else.

The material package is just a restricted subset: a GLB with one material and no geometry. Any tool that learns to read the MaterialX extension for packaging also gains the ability to read MaterialX materials in full scenes. The two use cases share the same extension and the same tooling.

Should we use these material packages inside USDZ?

In general, no.

USDZ already supports MaterialX workflows and already has a robust way to package textures into a single file. There is no need to embed .pbr files inside USDZ.

When creating a USDZ from one or more .pbr files, the better path is:

  • extract each material's graph representation and convert it to MaterialX XML
  • collect and deduplicate texture assets across all materials
  • write the resulting MaterialX/USD data plus shared textures into USDZ

This avoids duplicate image payloads and preserves the existing USDZ format and tooling model.

Path Forward

A packaging convention is only useful if it leads to real adoption. Here is what I think the path looks like:

  1. Finalize the MaterialX extension for glTF. The core enabling work is a KHR_materials_materialx extension that defines how MaterialX graphs are represented in JSON within glTF materials. This should be driven through Khronos, working alongside the Academy Software Foundation (ASWF) which maintains MaterialX.

  2. Define the packaging convention. Document the specific constraints that make a .pbr file: a valid GLB containing exactly one material, open_pbr_surface only, MaterialX extension only (no legacy PBR), no geometry. This can be a lightweight profile or best-practices document, including a recommended MIME type registration.

  3. Build reference tooling. An open-source CLI and library — ideally in TypeScript and Python first, since those are the environments where XML friction is highest — that packs loose .mtlx + textures into a .pbr file and unpacks back to loose files.

  4. Prove it in production. At Land of Assets, we intend to adopt this packaging convention as the primary format for our MaterialX material library. Real-world use will surface edge cases and validate the design before broader standardization.

The beauty of this approach is that every step also advances the broader goal of MaterialX support in glTF. There is no throwaway work.

We welcome collaboration from the MaterialX and OpenPBR communities. If you are building material repositories, content pipelines, or DCC integrations and this resonates, we would love to hear from you.

Closing

MaterialX and OpenPBR are exactly the direction the industry needs: open, renderer-agnostic, and semantically rich.

But to make them operationally excellent in modern pipelines, we need to close the packaging gap. The good news is that the answer was already in front of us: GLB. It is a proven, web-friendly, universally supported binary container with battle-tested texture storage. A .pbr file — a single-material GLB using OpenPBR through the MaterialX extension — gives us everything we need: single-file delivery, binary textures, deterministic structure, instant ecosystem support, a single surface model that every tool can target, and an extension that makes the file's purpose unmistakable.

The hard part is not the container. It is the MaterialX extension for glTF — a canonical JSON representation of MaterialX graphs, integrated into the glTF material model — and the continued community investment in OpenPBR as the evolving standard for physically based surface shading. As OpenPBR matures, so does every .pbr file in every material library. That alignment is the real power of this approach. The container is already solved.


This post is also published on Land of Assets.