Skip to content
The Technical ArtistArt. Code. Pipelines. Games.
Home/Blender/bpy Cookbook
blender - reference
bpy recipes
problem -> code -> gotcha

The bpy Cookbook

Workingtutorial~3 min read

These recipes cover common Blender pipeline tasks, with the code and the limitations to check before using it. Run them from the Scripting tab first; an add-on is a useful next step when the team needs a UI. There are more examples in the Python Snippet Library.

On this page Batch Export Collections Auto-LOD Chain Scene Audit Report Metadata -> engine Fix Common Sins Headless Blender The Context Gotcha

Recipe 1 - Batch-Export Every Collection To FBX

Problem: twenty props in one .blend file need to become twenty engine assets. Exporting them by hand is repetitive and easy to get wrong. Use collections as export groups so the script can handle each asset separately.

bpy - batch exporter
import bpy, os

OUT = bpy.path.abspath("//exports") # folder next to the .blend
os.makedirs(OUT, exist_ok=True)

for col in bpy.data.collections:
 if col.name.startswith("_") or not col.objects:
 continue # _source, _reference etc. stay home
 bpy.ops.object.select_all(action="DESELECT")
 for ob in col.objects:
 ob.select_set(True)
 bpy.ops.export_scene.fbx(
 filepath=os.path.join(OUT, col.name + ".fbx"),
 use_selection=True,
 apply_scale_options="FBX_SCALE_ALL",
 bake_space_transform=True,
 add_leaf_bones=False)
 print("exported", col.name)

Gotcha: objects in collections excluded from the view layer can't be selected - and silently export as empty files. Either keep exportables enabled, or link objects into an export view layer first. The _ prefix convention gives artists an obvious "not for export" switch.

Recipe 2 - Auto-Generate An LOD Chain

Problem: the engine wants LOD1-LOD3 and nobody budgeted artist time for making them. Decimate can produce useful distance LODs for props. Check the silhouette and UVs, particularly on hero assets.

bpy - LOD chain from selected
import bpy

RATIOS = {1: 0.5, 2: 0.25, 3: 0.1} # LOD index -> triangle ratio

for src in list(bpy.context.selected_objects):
 if src.type != "MESH": continue
 for lod, ratio in RATIOS.items():
 dup = src.copy()
 dup.data = src.data.copy()
 dup.name = f"{src.name}_LOD{lod}"
 bpy.context.scene.collection.objects.link(dup)

 mod = dup.modifiers.new("decimate", "DECIMATE")
 mod.ratio = ratio
 bpy.context.view_layer.objects.active = dup
 bpy.ops.object.modifier_apply(modifier=mod.name)
 src.name = f"{src.name}_LOD0"

Gotcha: decimate scrambles UV seams at aggressive ratios - spot-check LOD2/3 with our UV grid texture before shipping. Naming matters: Unreal auto-imports _LOD0...3 suffixed meshes as one mesh with LODs if "Import LODs" is on. That filename convention is the pipeline.

Recipe 3 - The Scene Audit Report

Problem: "is this file export-ready?" shouldn't require opening every object. One script, one verdict - the scripted version of the pre-flight checklist.

bpy - audit before export
import bpy

problems = []
for ob in bpy.data.objects:
 if ob.type != "MESH": continue
 tris = sum(len(p.vertices) - 2 for p in ob.data.polygons)
 if not ob.data.uv_layers: problems.append((ob.name, "no UVs"))
 if tris > 10000: problems.append((ob.name, f"{tris} tris"))
 if any(abs(s - 1.0) > 1e-4 for s in ob.scale):
 problems.append((ob.name, f"unapplied scale {tuple(ob.scale)}"))
 if not ob.data.materials: problems.append((ob.name, "no material"))
 if "." in ob.name: problems.append((ob.name, "dupe-suffix name (.001)"))

if problems:
 print(f"- {len(problems)} problem(s) -")
 for name, what in problems: print(f" {name}: {what}")
else:
 print("clean. export away.")

Gotcha: duplicate-suffixed names can become duplicate assets in the engine. Flag them before export so the artist can check whether they’re intentional.

Recipe 4 - Custom Properties -> Engine Metadata

Problem: the engine importer needs to know things per-asset (LOD group, collision type) and filenames can only carry so much.

bpy - author metadata
import bpy

META = {"lod_group": "prop_small", "collision": "convex", "author": "ta_pipeline"}

for ob in bpy.context.selected_objects:
 for k, v in META.items():
 ob[k] = v
# Export with "Custom Properties" ticked. In Unreal, read via the FBX import
# pipeline or asset metadata; in Unity, via an AssetPostprocessor's userData.

Gotcha: properties added via UI sliders can carry min/max UI metadata that confuses some importers - plain ob["key"] = value assignments export cleanest. The full three-DCC metadata story is Rosetta task 5.

Recipe 5 - Fix Common Scene Issues In One Pass

Problem: outsourced or jam-week files: unapplied transforms, stray parenting, orphaned data bloating the file.

bpy - the janitor
import bpy

# apply transforms on all meshes
bpy.ops.object.select_all(action="DESELECT")
for ob in bpy.data.objects:
 if ob.type == "MESH":
 ob.select_set(True)
if bpy.context.selected_objects:
 bpy.context.view_layer.objects.active = bpy.context.selected_objects[0]
 bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)

# purge orphaned meshes/materials/images (the file-size mystery, solved)
bpy.ops.outliner.orphans_purge(do_recursive=True)
print("janitor done.")

Gotcha: don't apply location on modular kit pieces - their offset from origin is often intentional pivot placement. Rotation and scale, always; location, look first.

Recipe 6 - Run Blender Without The UI

Problem: exporting 40 .blend files shouldn't require opening 40 .blend files.

shell + bpy - batch over files
# export_one.py - runs INSIDE blender
import bpy, sys
out = sys.argv[sys.argv.index("--") + 1]
bpy.ops.export_scene.gltf(filepath=out)

# then from any shell / CI job / watch folder:
# blender -b props/crate.blend -P export_one.py -- D:/out/crate.glb

Gotcha: everything after -- is yours; everything before belongs to Blender. Wrap the shell loop in Python with subprocess (see the snippet library) and you've built a content build farm - pair it with a watch folder and it runs itself.

Why Operator Context Matters

When a script that works in the Scripting tab fails from an add-on or headless run with context is incorrect - you've met bpy's signature quirk. bpy.ops.* operators are UI actions; they assume an active object, a mode, sometimes literally a mouse-over-viewport. Three escapes, in order: prefer data access over operators (ob.name = "x" needs no context; bpy.ops.object.rename does); set the context explicitly (view_layer.objects.active = ob) before operators you must use; and for the stubborn ones, bpy.context.temp_override(...). Test the same script from the UI, an add-on, and a headless run. Context assumptions often explain why only one of those works.