Pixel-Perfect MaterialX in Blender and Three.js
How I built the MaterialX Fidelity Suite, then used it to drive pixel-perfect MaterialX support in both Three.js and Blender — including custom noise node implementations that finally close the gap between reference renders and open-source tools.
Ben Houston • May 1, 2026 • 10 min read
One of the long-standing frustrations with MaterialX is that the format is well-specified on paper but wildly inconsistent in practice. Open a .mtlx file in the reference viewer, then open the same file in Three.js or Blender, and you get something that looks vaguely related — but not the same. Colors shift. Noise patterns break. Procedural textures come out completely wrong.
I wanted to fix that. This is the story of how I did it — twice, in parallel, for two very different rendering environments.
The Inspiration: The Khronos glTF Fidelity Suite#
The catalyst was the Khronos glTF Render Fidelity suite. That project compared glTF rendering across multiple real-time and photorealistic renderers — including V-Ray and Blender — using a shared set of sample assets. It was remarkably effective. Renderer authors could see exactly where they diverged from the reference, and having objective evidence drove real improvements. It raised the baseline quality of glTF support across the entire ecosystem.
I wanted to repeat that for MaterialX. No comparable suite existed. You could look at isolated examples, but there was no systematic, side-by-side comparison across renderers that made regressions and gaps immediately visible. Without that, you are guessing.
Building the MaterialX Fidelity Suite#
So I built one: the MaterialX Fidelity Suite.
It started with a couple hundred test materials and has grown to over 400. Each material is designed to test specific aspects of the format in relative isolation — individual node types, surface models, parameter combinations, procedural patterns — rather than complex, real-world scenes that make it hard to identify what broke. The suite covers:
- Surface models:
standard_surface,gltf_pbr, andopen_pbr_surface - Node categories: math, vector math, compositing, color, ramps, logic, matrix transforms, image sampling, and geometry accessors (position, normal, texcoord)
- Procedural noise: Perlin, fractal, cell noise, Worley noise, and unified noise variants — both 2D and 3D
Every material is rendered through the reference materialxview tool from the MaterialX project itself. That render is the ground truth. Every other renderer is compared against it, side by side, and a visual difference metric is computed automatically so regressions are immediately visible as numbers — not just impressions.
The Renderer Type Problem#
One complication became clear early: the renderers in the suite use fundamentally different techniques, and those differences produce unavoidable visual divergence that has nothing to do with MaterialX correctness.
materialxview— the reference — is a ray tracer- Blender Eevee — is a rasterizer (though it can approximate some ray-traced effects)
- Blender Cycles — is a path tracer, which adds global illumination and self-reflections that a ray tracer may not capture the same way
- Three.js — is a rasterizer, with no path-traced global illumination at all
This means some visual differences between renderers are expected and correct — a path tracer will show inter-reflections and indirect lighting that a rasterizer simply cannot produce. The fidelity suite compares everything against materialxview, so those structural differences show up in the metrics. The goal was never to eliminate them; it was to minimize the avoidable differences — the ones caused by incorrect node implementations, wrong coordinate spaces, or broken noise functions — so that what remains is only the fundamental rendering-technique gap.
This distinction matters when reading the metrics. A Three.js result that scores slightly lower than a Blender Cycles result is not necessarily worse — it is partly a consequence of rasterization versus path tracing. The suite makes this visible and lets you distinguish the two categories of difference.
The fidelity suite is what made everything else possible. Without it, I would not have known what was broken or when I had actually fixed it.
Starting with Three.js#
My familiarity with Three.js made it the natural starting point. The library already had a MaterialX loader, but it had accumulated significant divergence from the reference. I worked through the test suite systematically, using the visual comparisons to guide fixes.
The number of issues was larger than expected:
- Most noise implementations were incorrect. Hash functions, octave accumulation, parameter mappings — any one of those being slightly off produces a completely different pattern.
- The
mx_ifgreater,mx_ifgreatereq, andmx_ifequalcondition nodes were producing the opposite of the intended result. rotate2d,rotate3d, andplace2dwere all reimplemented to match reference behavior.- Dozens of smaller corrections to
mx_*functions and their default values.
I also introduced new features in the same PR: support for the open_pbr_surface and gltf_pbr surface models, archive loading (.mtlx.zip files from AMD Material Library and Poly Haven work directly), a modular node registry design, and proper handling of default values.
The key challenge in Three.js, as it turned out, was the noise functions. Three.js had similar noise primitives to MaterialX, but "similar" is not the same as "identical." A slightly different hash, a slightly different parameter space, or a different approach to octave accumulation, and the whole output changes. I imported the noise implementations from the MaterialX reference library and ported them faithfully into the Three.js GLSL shaders. That is what closed the gap.
The result: near-perfect MaterialX reproducibility across all 400+ samples, with the sole remaining outlier being hextiledimage, which requires a more specialized solution.
The PR is open here: Three.js PR #33485 — WIP: MaterialX upgrade
Moving to Blender#
Blender was a harder problem — and a more interesting one.
The approach had to be fundamentally different. Three.js uses GLSL shaders, so fixing it meant writing better shader code. Blender uses a visual node graph, and a MaterialX graph does not map one-to-one onto a Blender node graph. The importer has to be a compiler: it reads the MaterialX document and constructs an equivalent Blender material that reproduces the visual result, even if the graph structure looks nothing like the original.
The Python Importer#
I wrote a Python importer — blender-materialx-importer — that uses the MaterialX Python library already compiled into Blender to read .mtlx files, then constructs equivalent Blender node graphs.
A few things make this non-trivial:
- MaterialX and Blender use different coordinate spaces for texture coordinates, normals, and tangents. Conversions have to be inserted at the right points.
- Some MaterialX nodes require one-to-many mappings into Blender nodes. A single
place2dnode, for example, requires a chain of Blender math and vector nodes to reproduce correctly. - The importer emits warnings when it falls back to approximations, rather than silently producing an incorrect result. That makes it useful in a production context where you need to know what you can trust.
After getting the importer working and running it through the fidelity suite, I found that the vast majority of nodes reproduced well. The exception was the noise functions — the same problem I had hit in Three.js, but harder to fix.
The Noise Problem#
MaterialX's procedural noise library is central to a huge range of materials: marble, wood, brick, stone, fabric, cloud. If the noise is wrong, an entire category of materials breaks. And Blender's built-in noise nodes, while powerful, are not the same implementations as MaterialX's.
The differences are subtle but consequential:
- Different hash functions produce different spatial patterns at identical parameter values
- Different default frequencies, amplitudes, and octave counts
- Different parameter spaces (e.g., how "scale" is defined and applied)
- Some MaterialX noise types have no Blender equivalent at all
The only way to get correct results was to implement the MaterialX noise functions natively in Blender's shader languages.
Custom Blender MaterialX Noise Nodes#
I wrote pixel-perfect implementations of the MaterialX noise functions for both Cycles (in OSL and GLSL) and submitted them as a Blender core PR: Blender PR #158054 — MaterialX Noise Nodes.
This adds native Blender shader nodes that exactly match the MaterialX reference:
ShaderNodeMxNoise2D/ShaderNodeMxNoise3DShaderNodeMxFractal2D/ShaderNodeMxFractal3DShaderNodeMxCellNoise2D/ShaderNodeMxCellNoise3DShaderNodeMxWorleyNoise2D/ShaderNodeMxWorleyNoise3DShaderNodeMxUnifiedNoise2D/ShaderNodeMxUnifiedNoise3D
The Python importer detects whether these nodes are available at runtime. If they are, it uses them and gets exact results. If they are not (i.e., on a stock Blender build), it falls back to Blender-native approximations and records a warning.
This design means the importer is useful right now, and becomes progressively better as the custom nodes are available — in the meantime, users can patch their own Blender build from the PR.
The Parallel Between Three.js and Blender#
Looking back, both implementations converged on the same root cause and the same fix: the noise functions.
MaterialX's noise implementations are specific. They are not interchangeable with "similar" noise in another system. The hash functions, the domain transformations, the way octaves are accumulated — all of it has to match the reference exactly, or the output diverges. The visual difference between a correct and an incorrect noise implementation can range from subtle to completely unrecognizable.
The efficiency of working on both simultaneously was real. The insights from debugging noise in GLSL for Three.js transferred directly to the OSL/GLSL implementations for Blender. The fidelity suite made both efforts measurable and kept regressions visible at every step.
Where Things Stand#
Across 400+ MaterialX samples:
- Three.js PR #33485 achieves near-perfect fidelity. The only remaining outlier is
hextiledimage. - Blender MaterialX Noise Nodes PR #158054 + blender-materialx-importer achieves pixel-perfect results on noise and near-perfect results across the full sample suite. The fallback path on stock Blender handles everything except the noise-dependent materials.
This is a qualitative leap from where things were. Previously, MaterialX in Three.js or Blender meant a handful of textures and a few base parameters wired up. Now it means the full graph — procedural textures, coordinate transforms, compositing, surface model variants — reproduced faithfully.
What This Enables#
For Three.js users: Drop in a .mtlx or .mtlx.zip file and get a real-time render that matches what the reference viewer produces. This works today, pending the PR merge.
For Blender users: Import any MaterialX material — from AMD Material Library, Poly Haven, or your own pipeline — and get a Blender Cycles material that reproduces the reference render. If you build Blender with PR #158054, the noise-based procedural materials also match.
For the ecosystem: The MaterialX Fidelity Suite is open for other renderers to add their results. If you are building a MaterialX importer, you can benchmark against the same reference and see exactly where you stand.
MaterialX is a powerful, expressive standard for graph-based materials. It deserves tooling that actually delivers what the format promises. This is a step toward that.
This work is sponsored by Land of Assets.