scenario-blender-export
About scenario-blender-export
scenario-blender-export is a skill for preparing Blender assets for game engines, web formats, USD pipelines, and 3D printing. It combines engine-specific export presets with delivery audits and re-import comparisons to catch differences beyond a successful export.
Key features
- 01Export with engine-specific presets
- 02Audit assets before export
- 03Re-import and compare exported files
- 04Summarize GLB contents and extensions
- 05Generate LOD and collision meshes
- 06Batch-convert supported model formats
Use cases
- 01Prepare a character FBX for Unity or Unreal Engine
- 02Export a GLB for Godot or web use
- 03Check model differences through a round trip
- 04Convert a folder of models and review its report
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ scenario-labs/ skills --skill scenario-blender-exportAfter installing, type/scenario-blender-exportin Claude Code to confirm it loaded.
Bundled filesreferences/engines.mdscripts/bx_export.py
Details
From the project READMEBlender export (engine-ready files)
An export is done when the file has been re-imported and measured, not when the exporter returned
FINISHED. Gate the asset first, export with a preset, then round-trip the file. This skill builds
on scenario-blender-expert (execution channels, the delivery gate) and imports its scripts, so
install the two side by side. If a sibling skill named here is missing from your available skills,
ask the user to install it (npx skills add scenario-labs/skills --skill <name>); unattended,
proceed from tool schemas and flag the gap.
Toolkit: scripts/bx_export.py. Per-engine facts with their sources:
references/engines.md.
Quick reference
| Target | Preset | File | Engine side |
|---|---|---|---|
| Unity, static mesh | unity | FBX | import defaults |
| Unity, character | unity_skeletal | FBX | leave Bake Axis Conversion off |
| Unreal Engine | unreal | FBX | Convert Scene Unit (to centimeters) |
| Unreal Engine, character | unreal_skeletal | FBX | one root bone |
| Godot 4 | godot | GLB | import defaults; Generate LODs in the dock |
| Roblox | roblox | FBX | 20,000 triangles per mesh |
| three.js and the web | web | GLB, meshopt, WebP | MeshoptDecoder; not for Godot |
| any glTF reader | gltf | GLB | |
| USD pipelines | usd | USDC | |
| Apple AR Quick Look | usdz | USDZ, 2048 textures | |
| 3D printing | print | STL in millimeters | slicer reads millimeters |
The Unity and Godot presets were imported into Unity 6000.3 and Godot 4.7.2 and measured; unreal,
unreal_skeletal and roblox follow Epic's and Roblox's documentation and pass the Blender round
trip only. The measurements live in references/engines.md.
Procedure
- Gate. Run
bx_audit.deliverable(parts, profile)fromscenario-blender-expertand fix every error. Scale left on an object travels into the engine as the node's transform. - Pick the preset from the table. Pass every object the file needs: a skinned mesh AND its armature, LOD meshes and their group, collision meshes beside their render mesh.
- Export:
bxe.export(path, preset, objects, **overrides). Any exporter keyword overrides the preset, such asbake_anim=Trueonrobloxfor an animated file. The result records the settings used. - Round-trip:
bxe.round_trip(path, objects, preset=...)re-imports the file in a background Blender, leaving the open scene alone. It compares size (1 mm), triangles, deforming bones, root bones (one expected), armature scale, shape keys and every action stored for the objects, by name. Then it lists every difference. Fix each one, or say why it is expected. - glTF contents:
bxe.glb_summary(path)listsextensions_required, morph targets, skins, clips and image types without Blender. A loader refuses a file that requires an extension it lacks. - Engine check: import into the engine and look, with
scenario-unity-pipeline-automation,scenario-unreal-pipeline-automationorscenario-godot-pipeline-automation.
Scale, axes and units
- Blender works in meters, Z-up, with the asset's front facing -Y. glTF is meters, Y-up
(
export_yup). FBX stores its axes as metadata; the presets write -Z forward, Y up. - Keep
scene.unit_settings.scale_lengthat 1.0. Any other value rescales every export. - Apply object scale before export:
obj.data.transform(Matrix.Diagonal(obj.scale).to_4x4()), thenobj.scale = (1, 1, 1), so the engine node imports at scale 1. - FBX scale:
unitywritesFBX_SCALE_ALL(Unity reads file scale 1).unrealkeepsFBX_SCALE_NONE(Unity reads it as file scale 0.01, a centimeter file).robloxusesFBX_SCALE_UNITS, as Roblox's export requirements ask. - STL has no unit and slicers read millimeters, so
printmultiplies by 1000.
Characters
- Export the armature with its meshes.
use_armature_deform_onlyleaves control bones out, so keep every deforming bone in one hierarchy under one root bone. Unreal's importer reports "Found multiple roots. The importer only supports one root per mesh." add_leaf_bonesis off in every preset. Left on, each chain gains an extra end bone that breaks retargeting.unity_skeletalandunreal_skeletalbake every action (bake_anim_use_all_actions), one clip per action. Unity names each clipArmature|Action(measured:Rig|Wave), so name actions as the clips should read. glTF exports each action as an animation under its own name.robloxexports no animation. For an animated file, passbake_anim=Truetoexport()and toround_trip(), which then compares the actions too.- Shape keys travel in FBX (blend shapes), glTF (morph targets, names in
extras.targetNames) and USD. Building ARKit or viseme sets isscenario-blender-rigging's work. - Roblox: at most 4 influences per vertex, bones frozen (scale 1, rotation 0), root at the origin
with no influences, one animation track per mesh. Check influences with
max(len(v.groups) for v in obj.data.vertices).
LODs and collision
bxe.lods(obj, ratios=(0.5, 0.25))renames the meshName_LOD0and adds decimatedName_LOD1..nunder an empty calledName, which takes the mesh's place under its parent. Static meshes only: it refuses a skinned mesh. Unity builds the LOD Group from those names. Godot generates LODs at import (Generate LODs). Unreal documents "Import LODs", all LODs sharing one pivot. The group exports as a plain FBX Null, and Unreal's import of it is unmeasured.bxe.collision(obj, engine, shape)adds a collision mesh named for the importer:- Unreal:
UCX_Name_00(convex hull) orUBX_Name_00(box), exported in the same FBX. - Godot:
Name-convcolonlyforconvex(one hull) andbox(the bounds, a box collider), orName-colonlyformesh(the mesh as a trimesh, static bodies only). - Unity and Roblox: returns None. Colliders are set in the editor.
- Unreal:
- Decimation can wreck UV seams and silhouettes. Review every LOD with
bx_reviewbefore export.
Batch conversion
blender -b --factory-startup --python-exit-code 1 -P bx_export.py -- \
--batch /abs/in /abs/out --preset godot --profile game
For every model in the folder (GLB, glTF, FBX, OBJ, USD, STL, PLY): import into an empty scene, run
the delivery gate, export with the preset, re-import and compare. It writes /abs/out/report.json
with each file's gate errors, size and round-trip differences. One failing file never stops the
folder: it gets a failed entry. Two inputs sharing a name keep their format in the output name
(hero_fbx.glb, hero_glb.glb). Open the report before calling the batch done.
Worked example: a generated GLB to Unity and Godot
import sys
sys.path.append("/abs/skills/scenario-blender-expert/scripts")
sys.path.append("/abs/skills/scenario-blender-export/scripts")
import bpy, bx_audit, bx_export as bxe
bpy.ops.wm.read_factory_settings(use_empty=True)
bpy.ops.import_scene.gltf(filepath="/abs/in/robot.glb")
parts = [o.name for o in bpy.context.scene.objects if o.type == "MESH"]
gate = bx_audit.deliverable(parts, profile="game")
print(gate["errors"]) # fix each one, then gate again until "pass" is True
bxe.export("/abs/out/robot.fbx", "unity", parts)
print(bxe.round_trip("/abs/out/robot.fbx", parts, preset="unity")["diffs"]) # []
bxe.export("/abs/out/robot.glb", "godot", parts)
print(bxe.glb_summary("/abs/out/robot.glb")["extensions_required"]) # []
A generated GLB often holds one object per part, all sharing the asset's pivot. Keep them as
separate objects when the engine should animate or swap parts. Join them (bpy.ops.object.join with
the parts selected) for a single static prop, then gate again.
Common mistakes
- Calling an export done on
FINISHED. The exporter succeeds on files the engine imports at 100x scale or without the skin. - Exporting a skinned mesh without its armature, or the armature without its mesh.
- Setting Bake Axis Conversion or Apply Transform to remove the armature node's rotation: neither removes it, and the first turns the character around.
- The
webpreset for Godot, or any compressed GLB for a loader without its decoder. Checkextensions_requiredfirst. - FBX textures left as external paths: the presets copy and embed them (
path_mode="COPY",embed_textures=True), so the file travels alone.export()gives packed images without an extension (as a GLB import leaves them) a.jpgor.pngname first. An image never saved (a new bake target) or edited since travels as a packed copy of its pixels (packed_unsaved_images). - A textured FBX that shows untextured in Unity: the default import keeps embedded textures inside the file. Extract Textures (Materials tab) into the model's folder assigns them.
- An STL at Blender scale: a 10 cm part prints at 0.1 mm.
- LODs exported without a look: decimation can collapse thin parts.