Merge Lab — de-duplicate the .001/.002 duplicate materials, images/textures & node groups Blender spawns on object copy. Unity AI Lab Blender suite. Verified Blender 2.93.0 -> 5.2.0.
Find a file
2026-09-04 20:55:31 -07:00
.gitignore Mass Data Save 2026-09-04 20:55:31 -07:00
LICENSE Merge Lab v1.0.0 — datablock de-duplication (materials / images / node groups) 2026-07-20 19:33:09 -07:00
merge_lab.py Merge Lab v1.4.0 — look-safe merging + fake-user merge fix 2026-07-20 21:10:58 -07:00
README.md Merge Lab v1.4.0 — look-safe merging + fake-user merge fix 2026-07-20 21:10:58 -07:00

Merge Lab

version blender python license

A Blender add-on that de-duplicates the .001 / .002 datablocks Blender spawns when you copy objects — merging them back onto a single canonical block so your file stays lean and one edit updates every copy.

Part of the Unity AI Lab Blender suite (Iris Lab, Skin Lab, Anatomy Lab, Blockout Lab, Jiggle Lab, Deform Doctor, Texturly). Where those tools build, Merge Lab tidies.

Look-safe by design. By default Merge Lab only ever merges datablocks that are genuinely identical, so it will not change how your scene renders — in Blender or after a normal FBX/glTF export to Unity / VRChat. See Look safety & Unity/VRChat export.


Table of contents


The problem it solves

Blender never merges datablocks on its own. Every time you duplicate an object (Shift+D), append from another file, or import, Blender makes a fresh suffixed copy of every material, image, texture and node group that object referenced:

Skin        Skin.001    Skin.002    Skin.003 …
Metal       Metal.001   Metal.002 …
brick.png   brick.001   brick.002 …

After a busy scene — or the classic "I made a city of skyscrapers, joined them with Ctrl+J, and now I have 300 duplicate materials" situation — the file is bloated with hundreds of near-identical datablocks. That breaks texture atlasing, wastes memory, and scatters your references so recolouring "all the glass" means editing 300 materials by hand instead of one.

Merge Lab collapses those duplicates back onto a single canonical datablock, in one click, without changing how anything looks.


What it de-duplicates

Merge Lab handles four distinct datablock types, each independently toggleable. They are genuinely different things — a material is not a texture:

Type Blender collection What it is
Materials bpy.data.materials The shader assigned to faces.
Images bpy.data.images The image files that shader-node Image Texture nodes plug in.
Textures bpy.data.textures The legacy Texture datablocks used by brushes, modifiers (Displace/Warp) and particle systems.
Node Groups bpy.data.node_groups Reusable node networks.

It does not de-duplicate mesh (geometry) datablocks. That is deliberate — merging meshes by name could silently change an object's shape if two same-named copies had been edited apart. Mesh de-duplication is intentionally out of scope.


Supported Blender versions

Merge Lab targets the full Blender 2.93 LTS → 5.x range from a single file, and is verified on the two ends of that range:

Build Blender Python
Oldest supported (LTS) 2.93.0 3.9.2
Current (LTS) 5.2.0 3.13.13

bl_info["blender"] is (2, 93, 0) — the minimum. There is no upper bound; the add-on uses only API that is present and behaves identically across the whole range, and feature-detects the few things that differ (see Cross-version compatibility). Behaviour has been proven byte-for-byte identical on 2.93.0 and 5.2.0 by an automated test suite.

On Blender 4.2 and newer, Merge Lab installs as a legacy add-on (it ships a classic bl_info block rather than an extension blender_manifest.toml) — this is intentional so the one file also drops into 2.93. It registers and runs identically either way.


Installation

Merge Lab is a single-file add-on — no build step, no dependencies.

  1. Download merge_lab.py.
  2. In Blender: Edit ▸ Preferences ▸ Add-ons ▸ Install… (in Blender 4.2+, the button is Install from Disk… under the ▾ menu).
  3. Select merge_lab.py.
  4. Tick "Merge Lab" in the add-on list to enable it.
  5. Open the N-panel in the 3D Viewport (press N) and select the Merge Lab tab.

That's it. The add-on runs standalone; if it detects a sibling suite plugin it shows a small cosmetic tip and nothing more — there are no hard dependencies on any other add-on.


Quick start

The safe, recommended first run:

  1. Save your file (good practice before any batch cleanup).
  2. Open the Merge Lab panel (N-panel → Merge Lab tab).
  3. Leave the type toggles on, mode on Name.
  4. Tick Dry Run (report only) and press Merge All Enabled.
  5. Read the report line — e.g. "DRY RUN — would merge 42 (mats 18, imgs 12, texs 4, groups 8) into 9 canonicals". Nothing has changed yet.
  6. Happy with the numbers? Untick Dry Run and press Merge All Enabled again.

For the Ctrl+J-joined-mesh case, also tick Collapse Duplicate Slots before the real run (see that section).


The interface

All controls live in the Merge Lab tab of the 3D Viewport N-panel.

Datablock type togglesMaterials, Images, Textures, Node Groups. These choose which types the all-in-one pass processes.

Buttons:

Button What it runs
Merge All Enabled The all-in-one pass — de-duplicates every ticked type in one go (plus slot-collapse, if that toggle is on).
Materials / Images / Textures / Node Groups Per-type buttons — run just that one type, ignoring the toggles. Use when you only want to touch one kind.
Collapse Duplicate Slots Runs only the material-slot collapse (see below), independent of any datablock de-duplication.

All buttons obey the shared settings (mode, fake-user protection, dry-run, selected-only). A footer shows the detected Blender / Python version so you can confirm what you're running on.


Settings reference

Setting Default Effect
Materials / Images / Textures / Node Groups all on Which datablock types the all-in-one pass processes.
Match By Name Name (base-name + identical) vs Content (fingerprint) — see Match modes.
Force Merge by Name off (Name mode only, advanced.) Merge same-base-name blocks without the identity check — the classic name-only merge. Faster, but can change looks. Leave off for look-safe merges.
Protect Fake-User on Keeps pinned (shield-icon) materials safe: the pinned block is chosen as the surviving canonical and stays pinned, so a merge never deletes the last copy of a material you pinned. Duplicates still merge onto it — this does not block merging.
Selected Objects Only off Only datablocks used by the selected objects are eligible to be removed (and only slots on selected objects are collapsed). The canonical they remap onto may live outside the selection.
Collapse Duplicate Slots on After merging datablocks, also collapse redundant material slots on mesh objects — see that section.
Dry Run (report only) off Non-destructive. Counts what would merge / collapse and changes nothing.

The report line (panel readout and operator status bar) looks like:

Merged 9 (mats 4, imgs 2, texs 2, groups 1) into 4 canonicals; removed 12 duplicate slots

The ; removed N duplicate slots tail only appears when Collapse Duplicate Slots ran. In dry-run everything is prefixed DRY RUN — would merge ….


Match modes — Name vs Content

Mode What it groups Use it when
Name (default) Groups by base name (strip .NNN) and then only merges the ones that are genuinely identical. Wall, Wall.001, Wall.002 merge if they're true duplicates; a recoloured Wall.001, or a brick.001 that points at a different image file, is left alone. The everyday "I duplicated / joined objects" cleanup. Safe — never changes a look.
Content Fingerprints the datablock and merges genuinely-identical data even under different names (Metal + Chrome with identical node graphs merge). You want to consolidate identical materials that were given unrelated names.
Name + Force Merge by Name (advanced, opt-in) Classic name-only merge — merges every same-base-name block without checking they're identical. You specifically want same-named copies unified even if they diverged — can change looks.

Both default modes are look-safe: they compare content before merging. The .NNN matcher requires 3 or more digits, so a real name ending in .42 or v1.5 is never mistakenly stripped.

Legacy textures in Content mode: only image textures are content-merged (identical iff they point at the same image and share the basic wrap flags). Procedural textures (CLOUDS, NOISE, VORONOI, …) are never content-merged — their parameters are too varied to hash safely — so in Content mode they are left untouched; they still merge by Name when the .NNN says they are duplicates.

Packed / generated images are skipped in Content mode (each treated as unique); they still merge in Name mode when the name says they are duplicates.


Collapse Duplicate Slots

De-duplicating the material datablocks fixes the look — every slot that pointed at Glass.001 now points at the one Glass. But a mesh joined from N copies (e.g. via Ctrl+J) still carries N redundant material slots, all pointing at the same couple of materials. Collapse Duplicate Slots does the second half of the cleanup:

  1. Repoints every face off a duplicate slot onto its material's first slot.
  2. Removes the duplicate slots (highest index first, letting Blender re-index the faces).

So [Glass, Glass.001, Wall, Wall.001, Glass.002, Wall.002] (6 slots) becomes [Glass, Wall] (2 slots) with the per-face look preserved — an alternating glass/wall pattern stays glass/wall, it does not flatten.

It is done by editing mesh.materials directly rather than via bpy.ops.object.material_slot_remove, so it is reliable in scripts and headless runs and behaves identically across all supported Blender versions.

Scope and limits: mesh objects only. Object-linked slots (per-object material overrides) and linked/library data are skipped wholesale rather than risk corrupting an override. Non-mesh objects (curves, text, metaballs) are not slot-collapsed.


How it works

For each duplicate group, Merge Lab re-points every reference onto one canonical datablock and then frees the orphan — the only mechanism that is safe headless and stable across Blender 2.93 → 5.x:

duplicate.user_remap(canonical)     # 1. redirect ALL references onto the survivor
bpy.data.<collection>.remove(dup)   # 2. free it, once users == 0
  • Remap before remove — nothing ever ends up pointing at a freed block; a mesh that showed Skin.001 now shows the shared Skin, not an empty slot.
  • Leaves-first order — images → textures → node groups → materials, so a content-fingerprinted parent already references the canonical shared children.
  • Canonical selection — the lowest suffix wins; a bare, un-suffixed name (Foo) beats Foo.001 beats Foo.002. If a group has no un-suffixed member (the original was deleted), the lowest-suffix survivor is promoted and renamed to the stripped base — but only in Name mode, only on a real run, and only if that base name is free.
  • It does not call bpy.ops.outliner.orphans_purge — that needs an Outliner context and is fragile/unreliable headless. Merge Lab removes only the orphans it itself creates, so anything you intentionally orphaned elsewhere is never touched.

Look safety & Unity/VRChat export

This is the core guarantee: with default settings, Merge Lab never changes how your scene looks — in Blender or after export.

  • Only identical datablocks merge. In the default Name and Content modes a block is merged onto another only when they are genuinely identical (same node graph, same parameters, and — crucially — the same referenced image file). A recoloured Glass.009, or a brick.001 that points at a different texture file, is not merged. This is what prevents the "a merge swapped my textures" problem.
  • References are re-pointed, geometry is untouched. Merging only redirects references onto the surviving datablock; it never edits meshes, UVs, normals, vertex colours, shape keys, armatures or weights.
  • Slot-collapse preserves the per-face material. It rewrites the material slot list and per-face material indices together, so every face keeps the exact material it had — the look is identical, there are just fewer slots (which is better for a game engine).

For Unity / VRChat: Merge Lab is not an exporter and changes nothing about how you export. When you export normally (FBX / glTF, or your usual avatar/world pipeline), the engine reads per-face material assignments, the materials' textures, and the mesh UVs — all of which Merge Lab leaves visually unchanged. The result is that a mesh looks the same in Unity/VRChat after a merge as before, and typically exports more cleanly (fewer duplicate materials and slots = fewer submeshes and materials to wrangle). This is verified by an automated test that captures each face's material colour, referenced image file path, and UV coordinates before and after a merge and asserts they are unchanged, on both Blender 2.93.0 and 5.2.0, and confirms a real glTF export still succeeds with the expected reduced material count.

The only way to make a merge change a look is to explicitly enable Force Merge by Name (or run Content mode on materials you want consolidated). The safe defaults never do.


⚠️ Cautions — read before your first real run

  • Save first, and use Dry Run. The operator supports Undo, but a Dry Run (which changes nothing and just reports counts) is the reliable way to preview a batch cleanup before committing.
  • Only "Force Merge by Name" can change a look. The default Name and Content modes merge only genuinely-identical data, so they never alter appearance. Force Merge by Name is the one setting that will flatten same-named-but-different blocks (e.g. a recoloured Wall.001) onto the canonical — enable it only when that's what you want.
  • Merging frees the duplicate datablocks. This is the point — the redundant copies' data is gone after the run (references are always preserved; only the duplicate blocks are removed). Pinned duplicates are merged too, but the surviving canonical keeps the pin, so you never lose a material you pinned.
  • Slot-collapse edits mesh data. It rewrites mesh.materials and per-face material indices (never geometry/UVs). It preserves the look, but on shared/multi-user meshes the edit affects every object using that mesh (usually what you want). It is on by default — untick it if you want to touch only datablocks, not slot lists.
  • "Selected Objects Only" + Textures is modifier-scoped. Under Selected Objects Only, legacy textures are gathered from the selected objects' modifiers. Textures referenced only by particle systems or brushes are not found in that mode — run without the restriction to catch everything.
  • Unverifiable images are left alone. An image with no file path and no generation parameters can't be proven identical to another, so it is not merged in the safe modes (use Force Merge by Name if you really want to merge such images by name).

Cross-version compatibility

Merge Lab is not merely assumed to work across versions — its API surface was runtime-probed on both supported builds and its behaviour proven identical by an automated 4-scenario + slot + texture test suite (every non-version field matched).

Every ID / bpy.data call it makes is present with an identical signature on both 2.93.0 and 5.2.0: ID.user_remap(), ID.use_fake_user / .users, bpy.data.<coll>.remove(...), Material.diffuse_color / .metallic / .roughness, the Image.source enum, bpy.data.textures, and bpy.data.node_groups. There are therefore no behavioural version forks. What genuinely differs is handled by feature detection, never version-number guessing:

  • bpy.app.version + sys.version_info are captured at import and shown in the panel footer.
  • hashlib.md5(usedforsecurity=…) (a keyword added in CPython 3.9) is probed once and used through a wrapper, so hashing is FIPS-safe from Python 3.9 through 3.13+.
  • The Principled BSDF socket set differs by version, but content fingerprints are only ever compared within one session, so that difference cannot affect a result.

Deliberately avoided to hold the 2.93 floor: match/case (Python 3.10), PEP-604 X | Y annotation unions, color_attributes (3.2+), temp_override (3.2+), FileHandler (4.1+), and orphans_purge.


Troubleshooting / FAQ

"I pressed Merge and nothing happened." Check that Dry Run is off for a real run, that the relevant type toggle is on (for the all-in-one button), and that the duplicates actually match your mode (Name needs a shared base name; Content needs identical data).

"After merging, an object looks different." You almost certainly ran Name mode on a same-named copy that had been edited to differ. Undo, switch to Content mode, and re-run. Content mode never merges blocks whose data differs.

"My object still has hundreds of material slots." De-duplicating datablocks fixes the materials, not the slot list. Tick Collapse Duplicate Slots (or press its dedicated button) to compact the slots on joined meshes.

"A pinned material didn't get merged." That's Protect Fake-User doing its job. Turn it off if you want pinned blocks merged.

"Does it clean up all orphan data?" No — by design it only removes the duplicates it merges. For a general orphan sweep use Blender's File ▸ Clean Up ▸ Purge.


Uninstalling

Edit ▸ Preferences ▸ Add-ons, find Merge Lab, expand it, and press Remove (or just untick it to disable). It stores no files outside the add-on itself and leaves no residual data in your .blend.


Version history

Version Highlights
1.4.0 Look-safe by default: Name mode now merges only genuinely-identical blocks (verifies content — including the referenced image file — before merging), so merges no longer change how anything looks or exports to Unity/VRChat. Fake-User protection now merges pinned duplicates onto a surviving pinned canonical (instead of blocking the merge entirely). Collapse Duplicate Slots defaults on. Added an advanced Force Merge by Name opt-in for the old name-only behaviour. Identical generated images now merge safely.
1.3.1 Documentation overhaul; staged release.
1.3.0 Added legacy Texture (bpy.data.textures) de-duplication as a fourth type; per-type buttons alongside the all-in-one pass; report gained a texs field.
1.2.0 Collapse Duplicate Slots — compacts redundant material slots on Ctrl+J-joined meshes while preserving the per-face look.
1.1.0 Cross-version hardening; capability layer; behaviour proven byte-for-byte identical on Blender 2.93.0 and 5.2.0.
1.0.0 Initial release — Name/Content material, image and node-group de-duplication with fake-user protection, dry-run and restrict-to-selected.

License · author · source