Skip to main content

Core library

mtlx-core is the document and package layer. The root API works with text and bytes in browsers and Node. Filesystem helpers and native texture processing are separate Node-only subpaths.

npm install mtlx-core
import { parseMaterialX, serializeMaterialX } from 'mtlx-core';
import { createEditorSession } from 'mtlx-core/session';

const session = createEditorSession({
document: parseMaterialX('<materialx version="1.39"/>'),
});
const graph = session.graph('');
const node = graph.addNode({ definition: 'ND_constant_color3' });
graph.setInputValue(node, 'value', [0.8, 0.2, 0.1], { type: 'color3' });
console.log(serializeMaterialX(session.getDocument()));

Concepts​

mtlx works with two on-disk forms of the same thing, and one in-memory model for both.

FormatWhat it is
.mtlxA loose XML document. Textures and libraries are referenced by relative path.
.mtlx.zipA relaxed container: any ordinary zip with a .mtlx inside.

A MaterialXPackage is a document plus every resource it references, held in memory:

interface MaterialXPackage {
rootPath: string; // 'material.mtlx'
document: MaterialXDocument;
resources: MaterialXResource[]; // { archivePath, sourcePath, data }
}

Once you hold a package the format it came from no longer matters — it can be written back out as either. A Transform is a function from a package to nothing ((pkg) => void | Promise<void>); transforms mutate the package in place and compose with transform(pkg, ...transforms).

document.elements is the canonical mutable element tree. document.nodes and document.nodeGraphs are derived readonly snapshots for inspection: edit the canonical elements and read a new snapshot afterward. The snapshots are frozen at runtime as well as readonly in TypeScript. Use createMaterialXDocument(attributes, elements) to construct a document and cloneMaterialXDocument(document) to copy it; do not spread a document to make an editable copy.

Parsing malformed XML throws. Validation of a parsed document returns issues; use checkMaterialXText when you want parsing failures reported as issues as well. Validation covers selected rules, not complete MaterialX conformance or shader compilation.

Headless editing​

mtlx-core/session is the primary manipulation interface. It has no React, DOM, or Three.js dependency, and works in JavaScript as well as TypeScript. The UI uses the same operations as scripts; the session owns the current document, validation, subscriptions, and undo/redo. Low-level immutable graph transforms are also exported from this subpath. They do not provide the session's complete validation and history contract.

import { parseMaterialX, serializeMaterialX } from 'mtlx-core';
import { createEditorSession, EditorError } from 'mtlx-core/session';

const editor = createEditorSession({ document: parseMaterialX('<materialx version="1.39"/>') });
const graph = editor.graph(''); // Use 'group/nested' for a nested graph.

try {
editor.transaction('Build multiplier', () => {
const color = graph.addNode({ definition: 'ND_constant_color3' });
const multiply = graph.addNode({ definition: 'ND_multiply_color3' });
graph.setInputValue(color, 'value', [0.8, 0.2, 0.1], { type: 'color3' });
graph.connect({ node: color, output: 'out' }, { node: multiply, input: 'in1' });
});
} catch (error) {
if (error instanceof EditorError) console.error(error.code, error.message);
else throw error;
}

const xml = serializeMaterialX(editor.getDocument());

Use graph.addNode, setInputValue, connect, disconnectInput, and removeNodes to edit a graph. Invalid operations throw EditorError; use checkConnection to validate a proposed connection without changing the document. Graph scopes use names such as group/nested.

Each successful edit creates an immutable document snapshot. Use editor.subscribe(listener) for updates and call the returned function to unsubscribe. editor.undo() and editor.redo() navigate history; historyLimit defaults to 50 entries. replaceDocument(document) loads a document and clears history.

Transactions are synchronous and group changes into one undo entry. An uncaught error rolls back the transaction. Load resources before starting a transaction. Use cloneMaterialXDocument(editor.getDocument()) when an API needs a mutable document.

editor.toXml() serializes the current document. dirty tracks changes since loading or markClean(). Set node positions through editor.layout.moveNodes({ [id]: { x, y } }, scope) when your application needs persisted layout.

Complete inspection​

inspectMaterialX runs all five rule groups. ZIP inputs supply their own resource inventory; for loose XML, pass a readResource(path) callback to check recursive dependencies. Missing resources are collected independently. resourcesChecked records whether dependency checks could run, and resourcePaths includes failed reads. Pass supportedCategories from your renderer for category coverage; shader compilation remains the renderer's job.

import { inspectMaterialX } from 'mtlx-core';

const bytes = new TextEncoder().encode('<materialx version="1.39"/>');
const report = await inspectMaterialX(bytes, 'material.mtlx', { supportedCategories: [] });
if (report.issues.some((issue) => issue.level === 'error')) throw new Error('Material checks failed');
console.log(report.resourcesChecked); // true: no dependencies to resolve

mtlx-core/node (filesystem helpers)​

Load a loose document with its referenced resources, or read a ZIP package, then write either format. The output extension selects the format:

import { loadMaterialXPackage, writeMaterialXPackage } from 'mtlx-core/node';

const pkg = await loadMaterialXPackage('material.mtlx');
await writeMaterialXPackage(pkg, 'material.mtlx.zip');

For shared textures with loose output, pass { textureLibrary: '../textures' } to writeMaterialXPackage. ZIP output keeps resources inside the archive. Use readMaterialX / writeMaterialX when you only need XML, or checkMaterialX to validate a file on disk.

mtlx-core/textures (Node-only, needs sharp)​

Backed by sharp; never pulled into a browser bundle.

import { transform } from 'mtlx-core';
import { resizeTextures } from 'mtlx-core/textures';

await transform(pkg, resizeTextures({ maxImageSize: 2048, targets: [{ format: 'webp' }], imageQuality: 90 }));

This standalone Node example converts a one-pixel PNG to WebP:

import { transformImage } from 'mtlx-core/textures';

const pngBytes = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAADUlEQVQImWP4////fwAJ+wP9CNHoHgAAAABJRU5ErkJggg==',
'base64',
);
const { data, extension } = await transformImage(pngBytes, '.png', { targets: [{ format: 'webp' }] });
if (extension !== '.webp' || data.byteLength === 0) throw new Error('Image conversion failed');

See Processing pipelines for processMaterialX, staged results, and dry-run planning.

Node helpers require Node.js 22+. Installing mtlx-core also installs Sharp. See read limits for input budgets.

Generated API reference