A calibrated color-mixing model for FDM 3D printers that interleave filaments at the layer level. Predicts the visible color of a multi-filament print from the source filament hexes and their ratios, calibrated against measured Prusa XL prints.
No runtime dataset, ~50 lines of math, MIT-licensed — vendor it freely.
Live results, distributions, and per-recipe breakdown are published at prusa3d.github.io/prusa-fdm-mixer/apps/harness/. The harness re-renders on every commit, so the numbers there are always current — toggle between the training set, the held-out set, or all measurements. This README intentionally avoids hard-coding sample counts and ΔE figures, since the dataset grows over time.
This work started while integrating the OrcaSlicer-FullSpectrum multi-color FDM fork into Prusa EasyPrint and PrusaSlicer. Once the toolpath was producing real multi-filament prints, the slicer's preview colors visibly disagreed with the parts coming off the bed — bright complementary mixes in particular looked nothing like reality.
Digging in surfaced two compounding errors that any naive per-channel blend has:
- sRGB is gamma-encoded, so averaging in it adds spurious brightness.
- Real FDM prints darken by ~5–10% from inter-layer shadows that pure-RGB math doesn't see at all.
The result: previews come out brighter and more washed-out than the print actually does, especially for saturated complementary mixes (cyan + magenta, etc.) where the prediction is laughably wrong.
In parallel, BambuStudio shipped their own per-channel linear RGB mixing for multi-filament previews — same family of approach, same underlying issues. The gap is industry-wide, not a Prusa-specific quirk.
This repo ships a model that fixes both errors — empirically tuned against real measurements rather than from physics first-principles — and the infrastructure to keep extending it.
Requires Node 20+ (matches the version pinned in
.github/workflows/deploy.yml).
git clone https://github.com/prusa3d/prusa-fdm-mixer.git
cd prusa-fdm-mixer
npm install
npm run dev # vite dev server with hot reload
npm test # vitest
npm run build # production build to dist/For the drop-in C++17 implementation and its build instructions, see
cpp/README.md.
| App | What it does |
|---|---|
| Playground | Interactive palette generator: pick extruders, set ratios, see predicted colors sorted by hue |
| Harness | Score the prusa-fdm-mixer model against measured prints; compare against linear RGB, Kubelka-Munk, PolyMixer. Toggle between the training set, the held-out set, or all measurements |
| Gatherer | Standalone tool to enter your own LAB measurements and export JSONL |
| Calibration | NR200 colorimeter calibration — Tier 1 (3-point neutral linear fit), Tier 2 (24-patch ColorChecker 3×3 XYZ matrix), Tier 3 (batch Lab → display hex with three rendering variants) |
All four apps are served from one static GitHub Pages build at
prusa3d.github.io/prusa-fdm-mixer/;
the apps/ source directories live under this repo.
The playground browses real spools from two sources:
- OpenPrintTag database — the open NFC standard from Prusa Research that ships hex codes for every spool that follows the spec. Synced into
data/filament-library-openprinttag.json. - HueForge affiliate libraries — per-vendor JSONs from HueForge's official affiliate page (Polymaker, Sunlu, BambuLab, 3D Fuel, IIIDMax, Prusament, Protopasta, Numakers, Overture). Each entry includes a Transmission Distance (TD) value. Synced into
data/filament-library-hueforge.json.
A daily GitHub Action refreshes both files and commits any changes.
To trigger a sync manually:
npm run sync # both libraries
npm run sync:openprinttag # OpenPrintTag only
npm run sync:hueforge # HueForge onlyOr run the workflow from the Actions tab on GitHub. If your spool isn't in either library, the playground's "+ Custom hex" button still works for any hex you paste — the libraries are convenience, not a hard dependency.
import { mixFilaments } from 'prusa-fdm-mixer';
const result = mixFilaments([
{ hex: '#009bc3', ratio: 0.5 }, // cyan
{ hex: '#f6b921', ratio: 0.5 }, // yellow
]);
console.log(result.hex); // '#519e5f'
console.log(result.lab); // { L, a, b }The package also exports comparison models (mixLinearRgb,
mixKubelkaMunk, mixPolyMixer) and color helpers (hexToLab,
deltaE2000, chroma, hueDegrees).
A drop-in C++17 implementation lives in cpp/. Single header +
single source file, no external dependencies:
#include "prusa_fdm_mixer.hpp"
const std::vector<prusa_fdm_mixer::Part> parts = {
{"#009bc3", 0.5},
{"#f6b921", 0.5},
};
const auto result = prusa_fdm_mixer::mix(parts);
// result.hex, result.lab, result.rgbSee cpp/README.md for vendoring instructions and the
33-test suite.
- Yule-Nielsen base — gamma-decode each filament to linear-light RGB,
raise to
1/n(n = 3.0), ratio-average, raise back ton. Standard halftone math. - Lightness correction — measured prints are darker than YN predicts,
especially when the input filaments span a wide L* range. Apply
ΔL = -0.0477·L_gap - 2.112, plus an extra-0.060·(L_gap - 15)knee whenL_gap > 15. - Chroma correction — bright mixes lose saturation faster than dark
mixes. Apply
ΔC = 0.2780·predicted_L - 15.580to scale(a, b). - Cyan-band hue rotation — predictions in the cyan band drift slightly
warm. Rotate by up to
+10.38°at hue 210°, with linear fall-off ±30°. - Bell-curve weighting — corrections scaled by
w = N^N · ∏ratios(peaks at uniform mixing, zero at pure components) so pure colors are returned unchanged and gradients stay smooth.
All constants were fitted on the published fitting set
(data/fitting-set.jsonl). The full methodology,
including which alternatives were tried and rejected, is in
docs/methodology.md.
The harness scores prusa-fdm-mixer side-by-side with Kubelka-Munk, the PolyMixer port (BambuStudio's current model, ex-OrcaSlicer-FullSpectrum), HueForge-style TD blending, and the legacy linear-sRGB blend that most slicers ship today. Live numbers — median ΔE2000, hit-rate at ΔE 5/8/10, distributions, and per-recipe breakdowns — are at prusa3d.github.io/prusa-fdm-mixer/apps/harness/.
prusa-fdm-mixer is the only model in the comparison where 3-color performance doesn't collapse versus 2-color. Linear sRGB is on the table because it's what slicers actually use today, not because it's a serious physics candidate.
prusa-fdm-mixer/
├── src/ TypeScript model + comparison models
├── data/ Fitting set + held-out validation set
├── apps/ Three browser apps (playground, harness, gatherer)
├── cpp/ Drop-in C++17 implementation + tests
├── tests/ Vitest unit tests
└── docs/ Methodology, results
- A held-out batch in
data/holdout-set.jsonlwas never seen during calibration; out-of-sample ΔE there is the honest performance number. The harness toggles between training, held-out, and all measurements. - Calibrated against Prusament PLA. Other brands and materials may differ.
- 3-color predictions are extrapolated from 2-color fits with limited validation data. Treat as directional, not precise.
- Bronze/galaxy/glitter "special effect" filaments mix less predictably than solid-color ones and are slightly over-represented in the harder tail of the error distribution.
MIT — vendor freely. See LICENSE.
- justinh-rahb/filament-mixer — the PolyMixer comparison model and the original C++ scaffolding inspiration
- ratdoux/OrcaSlicer-FullSpectrum — multi-color FDM workflow that surfaced the need for better mixing math