references/godot-web.md
# Godot 4.x and Web (glTF 2.0)
## glTF basics (presets `godot`, `web`, `gltf`)
- `.glb` (binary, embedded textures) for delivery, `.gltf` + bin + images for debugging. Y-up, meters, right-handed. The exporter converts Blender Z-up automatically.
- Supported material: Principled BSDF → `pbrMetallicRoughness` (metallicRoughness texture: **G = roughness, B = metallic**; occlusion R via the "glTF Material Output" group → ORM in one texture), normal (OpenGL), emissive (+ `KHR_materials_emissive_strength`), alpha (OPAQUE/MASK/BLEND).
- Extensions the Blender exporter can write: `KHR_materials_transmission`, `_volume`, `_ior`, `_specular`, `_clearcoat`, `_sheen`, `_iridescence`, `_anisotropy`, `_dispersion`, `_emissive_strength`, `KHR_texture_transform`, `KHR_lights_punctual`, `KHR_mesh_quantization`, `EXT_mesh_gpu_instancing`, `EXT_texture_webp`, `KHR_draco_mesh_compression`, `EXT/KHR_meshopt_compression`.
- Custom properties → glTF `extras` (`export_extras=True`), which Godot exposes as metadata.
## Godot 4 import
- Drop `.glb` (or `.blend`, if the Blender path is set in Godot's settings; it converts via glTF) into the project.
- Name suffixes on objects (Godot import hints): `-col` (mesh + static collision), `-colonly` (collision only, mesh removed), `-convcolonly` (convex), `-rigid` (RigidBody), `-navmesh`, `-noimp` (skip), `-loop`/`-cycle` on animation names (loop). Use them on Blender object names, e.g. `Crate-colonly`.
- Animations: each action → AnimationPlayer clip. Godot retargets humanoids via SkeletonProfileHumanoid (Bone Map in import dock).
- LOD: Godot auto-generates mesh LODs on import (`meshes/generate_lods`). No manual LODs needed.
- Lightmaps: enable "Light Baking: Static Lightmaps" on import to auto-unwrap UV2, or provide one.
## Web (three.js, Babylon.js, model-viewer, R3F)
- Preset `web`: GLB + **meshopt** compression (`EXT_meshopt_compression`, or `KHR_meshopt_compression` via `--set export_meshopt_extension=KHR_meshopt_compression`) + **WebP** textures (q85). `--draco` switches to Draco (smaller for static, slower decode; needs the DRACOLoader).
- Budgets: hero model ≤ 2–5 MB GLB, total scene ≤ 10–20 MB, textures 1–2k, ≤ 50–100 draw calls on mobile web.
- Further compression outside Blender (recommended): `gltf-transform optimize in.glb out.glb --texture-compress ktx2` (KTX2/Basis textures: big GPU memory savings), `gltfpack -cc -tc` (the Blender exporter can call gltfpack: `export_use_gltfpack`).
- three.js: `GLTFLoader` + `MeshoptDecoder`/`DRACOLoader`/`KTX2Loader`. Set `renderer.outputColorSpace = SRGBColorSpace` and tone mapping (`AgX`/`Neutral`) to match Blender's view transform (Khronos PBR Neutral ↔ `NeutralToneMapping`).
- Validate with the Khronos glTF Validator and view in https://gltf-viewer.donmccurdy.com or model-viewer.
references/roblox.md
# Roblox (Creator Hub specs, verified 2026-10)
## Units and export
- 1 stud ≈ 0.28 m. Default R15 character ≈ 5 studs tall.
- Blender FBX: **Apply Scalings = FBX Unit Scale**, every other scale 1.0, Path Mode **Copy** + **Embed Textures**, **Add Leaf Bones off**, Bake Animation **off** unless exporting a clip. That's preset `roblox`.
- Alternatives from the docs: Transform Scale 0.01, or scene Unit Scale 0.01 (the docs warn this causes Blender issues).
- glTF (`.gltf/.glb`) is supported and needs no scale tricks: preset `roblox_gltf`.
- The Roblox docs recommend Unit System None + Rotation Degrees in Blender to "match studs". Optional; keep Metric if you rely on the presets.
- **One animation per exported file.** Export each clip separately.
## Mesh rules (all MeshParts)
- ≤ **20,000 triangles per mesh**. Watertight, no holes/backfaces, no zero thickness, quads preferred, no n-gons.
- **One UV set**, all UVs inside 0–1 (overlaps allowed).
- Use `blender-asset-qa --profile roblox`.
- `RenderFidelity=Automatic` creates LODs, so don't ship LOD meshes. Set `CollisionFidelity` (Box/Hull/Default/PreciseConvexDecomposition) on simple parts.
## Textures / SurfaceAppearance
- Maps: `ColorMap` (RGB + alpha), `NormalMap` (**OpenGL, tangent space**, flat = 127,127,255), `MetalnessMap` (8-bit gray), `RoughnessMap` (8-bit gray), `EmissiveMaskContent` (8-bit gray) + `EmissiveTint`/`EmissiveStrength`. No packed ORM, so export separate grayscale maps.
- Max 4096²; PBR maps are commonly limited to 1024². Size guidance: 5×5 studs → 256², 10×10 → 512², 20×20 → 1024². Marketplace max 2048².
- `AlphaMode`: Overlay (default; alpha shows the part color), Transparency (cutout/blend), TintMask (alpha = where `Color` tints), Opaque.
- Files: PNG/JPG/TGA/BMP. Upload via Studio (`AssetService:CreateAssetAsync` for batch).
## Rigging (any skinned mesh)
- Freeze bones: scale 1, rotation 0. Root bone at 0,0,0. ≤ **4 influences per vertex**, **no weights on Root**, symmetric weights.
- `blender-character-rigging` → `rig_check.py --profile roblox`.
## Avatar character bodies (R15)
- 15 meshes named `<Part>_Geo`: Head, UpperTorso, LowerTorso, Left/Right UpperArm, LowerArm, Hand, UpperLeg, LowerLeg, Foot. Each is watertight and capped at the joints.
- Facing **+Z, up +Y** in Roblox space (Blender preset conversion: front = -Y in Blender). Pose: I/A/T. Pivots at 0,0,0.
- Tri budgets: DynamicHead 4,000, Torso 1,750, each Arm/Leg 1,248, total **10,742**.
- Body scale (studs, X/Y/Z): min 1.35/3.6/0.7. Max Normal 8.6/9.5/2.25, Slender 6/9.5/2, Classic 8/9.1/2.
- Skeleton: `Root > HumanoidRootNode > LowerTorso > UpperTorso > (Head, arms)`, legs from LowerTorso. Root and LowerTorso at the origin. Up to 37 optional bones (Spine, Chest, Clavicles, fingers...) need `HumanoidRigDescription`.
- Attachments (empties) with `_Att` suffix: FaceCenter, FaceFront, Hat, Hair (head); Left/RightCollar, Neck, BodyBack, BodyFront (UpperTorso); Root (0,0,0), WaistFront/Back/Center (LowerTorso); Left/RightShoulder, Left/RightGrip; Left/RightFoot.
- Each body part needs an **outer cage** `<Part>_OuterCage`. Don't delete cage verts or change cage UVs.
- Marketplace: opaque body parts, ≥50% bounding-box fill, Material Plastic, VertexColor 1,1,1. Skin-tone areas transparent in the ColorMap.
- Dynamic heads: FACS poses as animation/shape data (see the Creator Hub "dynamic heads" page). Map ARKit-style shapes with `blender-facial-shapekeys`.
## Accessories and layered clothing
- Rigid accessories: ~4,000 tris max. Layered clothing: 4,000 tris max. Texture ≤ 1024² recommended.
- Layered clothing needs `<Name>_InnerCage` + `<Name>_OuterCage`, built from the Roblox template cages (same vert count/order/UVs as the template). The inner cage hugs the body, the outer cage hugs the clothing surface.
- Skin the clothing to the R15 rig (same bone names). Cages stay unskinned or follow the rig per template.
- Validate in Studio with the Accessory Fitting Tool.
references/unity.md
# Unity (2022 LTS / Unity 6)
## FBX settings (presets `unity`, `unity_skeletal`)
- Apply Scalings **FBX All**, Forward **-Z**, Up **Y**. Static meshes: **Apply Transform** (`bake_space_transform=True`), which removes the -90° X rotation and the 100 scale on root objects. Never use it with armatures. Skeletal: Apply Transform off, and in Unity enable **Bake Axis Conversion** in the Model importer if the root shows a -90° X rotation.
- Smoothing: Face. Triangulate on (stable tangents). Leaf bones off. Deform bones only.
- Unity importer: Model tab Scale Factor 1, Convert Units on. Normals = Import, Tangents = Import (or Calculate Mikktspace).
## Humanoid (Mecanim) rigs
- Minimum 15 bones: Hips, Spine, Head, Upper/Lower legs, Feet, Upper/Lower arms, Hands. Chest, Neck, Shoulders, Toes, fingers and eyes are optional.
- Bind pose in **T-pose** (Unity can enforce it in Configure Avatar). Hips is the root of the deform hierarchy. An extra `root` bone above Hips is fine for root motion.
- Clean names (`.L/.R` map fine, or Rigify `DEF-*` after export with deform-only). Avoid twist bones parented out of chain; Unity ignores them for retargeting but they still skin.
- Root motion: animate the `root`/Hips; in the clip import settings, set Root Transform Rotation/Position "Based upon" Original/Body Orientation.
## Animation clips
- Export all actions in one FBX (`--set bake_anim_use_all_actions=true`) or one FBX per clip (`Hero@Run.fbx` naming lets Unity share the avatar). Loop: match first/last frames, set Loop Time in Unity.
- Blendshapes export as FBX shape keys (Model tab: Import BlendShapes; Blend Shape Normals: Calculate).
## Materials
- Unity doesn't build PBR materials from FBX usefully. Recreate them in Unity, or use extract + remap.
- URP Lit: BaseMap (sRGB), NormalMap (OpenGL), Metallic map: **R = Metallic, A = Smoothness** (smoothness = 1 − roughness). Occlusion separate.
- HDRP Lit: MaskMap **R = Metallic, G = AO, B = Detail mask, A = Smoothness**.
- Build the packed maps in Blender (`blender-texture-baking` → numpy channel pack) or in Unity with a packing tool.
## Other
- Lightmap UV2: ship one (`uv_pack.py --lightmap`) or tick "Generate Lightmap UVs".
- LOD: name meshes `X_LOD0/1/2`. Unity auto-creates a LOD Group from the suffixes.
- glTF in Unity needs a package (glTFast). FBX remains the default.
references/unreal.md
# Unreal Engine 5.x
## FBX settings (presets `unreal`, `unreal_skeletal`)
- Scene unit scale 1.0 (meters). Apply Scalings **FBX Units Scale**: the unit goes into the FBX header, so UE converts to cm **without** a 100× scale on the armature/root bone. A root bone that imports with scale 100 means the wrong apply-scale mode was used.
- Forward -Z / Up Y (UE converts the axis system). Smoothing **Face** (avoids the "no smoothing groups" warning). Triangulate on. Tangents on.
- Skeletal: **Add Leaf Bones off**, **Only Deform Bones on**, armature node type Null. If the armature object is named `Armature`, UE drops it and the first bone becomes the root. Otherwise the object becomes an extra root bone, so name the object `root` if you want that explicitly.
- UE 5.4+ also imports glTF/USD through Interchange. FBX remains the most battle-tested path for skeletal meshes.
## Skeleton conventions
- Mannequin (UE5 Manny/Quinn) hierarchy: `root > pelvis > spine_01..05 > neck_01..02 > head`, `clavicle_l/r > upperarm > lowerarm > hand`, `thigh > calf > foot > ball`, plus twist/IK bones. Matching the names and bone orientation lets the IK Retargeter auto-map them.
- The UE Mannequin's skeletal mesh faces **+Y** in its asset space (the Character Blueprint rotates the mesh -90° to face +X). After the first import, compare your mesh with the Mannequin in the Skeletal Mesh editor. If they differ, rotate the armature ±90° around Z in Blender, apply, and re-export, so retargeting and root motion line up.
- Bone orientation: Blender bones are Y-along. If you need UE-style X-along bones for engine-side tools, set `primary_bone_axis="X"`, `secondary_bone_axis="-Y"` with `--set`.
## Static meshes
- Collision: `UCX_<MeshName>_00` (convex, ≤ 255 verts, not overlapping), `UBX_` boxes, `USP_` spheres, `UCP_` capsules, exported in the same FBX as the mesh.
- LODs: `<Mesh>_LOD0..N` in one FBX, with "Import Mesh LODs" enabled, or let UE auto-generate them.
- Lightmap UV: UE can generate one (Generate Lightmap UVs). Ship UV2 only for hand-made lightmaps.
- Sockets: empties named `SOCKET_<Name>` parented to the mesh.
## Nanite (static, UE 5.0+; skinned meshes experimental in 5.5+)
- Ship dense geometry directly (millions of tris). No LODs, no normal-map bake needed for geometric detail. Keep UV seams sensible (Nanite's error metric respects them).
- Opaque and masked materials are supported. Translucency isn't. Small/thin meshes (foliage) need care (Preserve Area option).
- Enable on import ("Build Nanite") or per asset.
## Materials and textures
- BaseColor (sRGB), Normal (**DirectX**: bake with `--directx`, or tick "Flip Green Channel" on the texture), **ORM** (R=AO, G=Roughness, B=Metallic; uncheck sRGB, compression Masks).
- Name textures `T_<Asset>_BC/_N/_ORM`. Materials become `M_` and instances `MI_`.
## Animation
- One action per clip. FBX exports the active action, NLA strips, or all actions (`bake_anim_use_all_actions`). Import the animation FBX against the existing Skeleton asset.
- Root motion: animate the `root` bone (not the armature object). Enable Root Motion on the anim sequence.
- Morph targets come from shape keys. Enable "Import Morph Targets".
scripts/_bpyx.py
"""_bpyx - tiny shared helpers for blender-agent-skills scripts (Blender 5.2+).
Source of truth: shared/_bpyx.py. Copies in skills/*/scripts/ are synced by
tools/sync_shared.py - never edit a copy.
Every skill script follows one contract:
run(**kwargs) -> dict # call from MCP / Python (import the module)
blender -b f.blend -P x.py -- --arg v # CLI: prints one line "@@JSON@@{...}"
"""
import argparse
import json
import sys
import bpy
MARK = "@@JSON@@"
def argv():
"""Arguments after '--' (Blender ignores them)."""
return sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
def _default(o):
if hasattr(o, "__len__") and not isinstance(o, str):
try:
return [round(v, 5) if isinstance(v, float) else v for v in o]
except TypeError:
pass
if isinstance(o, (set, frozenset)):
return sorted(o)
return str(o)
def emit(data):
"""Print compact JSON on one marked line (parsed by run_in_blender.py)."""
print(MARK + json.dumps(data, separators=(",", ":"), default=_default), flush=True)
return data
def parser(doc):
"""ArgumentParser whose help is the script docstring's first line."""
return argparse.ArgumentParser(description=(doc or "").strip().splitlines()[0])
def cli(run, p):
"""CLI entry: parse args after '--', run, emit JSON. Exit 1 on failure."""
args = p.parse_args(argv())
try:
result = run(**vars(args))
save = getattr(args, "save", None)
if save:
bpy.ops.wm.save_as_mainfile(filepath=bpy.path.abspath(save))
result["saved"] = save
emit(result)
except Exception as e: # report, don't dump a traceback into agent context
emit({"ok": False, "error": f"{type(e).__name__}: {e}"})
if bpy.app.background:
sys.exit(1)
def add_common(p, save=True):
"""--objects / --save flags shared by most scripts."""
p.add_argument("--objects", nargs="*", default=None,
help="object names (default: selected, else all meshes)")
if save:
p.add_argument("--save", default=None, help="save .blend to this path after run")
return p
def objects(names=None, types=("MESH",)):
"""Resolve names -> objects. No names: selected of `types`, else all visible of `types`."""
if names:
missing = [n for n in names if n not in bpy.data.objects]
if missing:
raise KeyError(f"objects not found: {missing}")
return [bpy.data.objects[n] for n in names]
pool = [o for o in bpy.context.scene.objects if not types or o.type in types]
sel = [o for o in pool if o.select_get()]
return sel or [o for o in pool if o.visible_get()]
def object_mode():
"""Leave edit/sculpt/pose modes; most data API work needs OBJECT mode."""
ob = bpy.context.view_layer.objects.active
if ob and ob.mode != "OBJECT":
bpy.ops.object.mode_set(mode="OBJECT")
def select_only(objs, active=None):
"""Select exactly `objs` and make `active` (default first) active."""
for o in bpy.context.view_layer.objects:
o.select_set(False)
for o in objs:
o.select_set(True)
bpy.context.view_layer.objects.active = active or (objs[0] if objs else None)
def node(tree, type_):
"""First node of `type_` (e.g. 'BSDF_PRINCIPLED'). Names are localized - never look up by name."""
return next((n for n in tree.nodes if n.type == type_), None)
def enum_ids(owner, prop):
"""Valid identifiers of an enum property (works on types or instances)."""
return [i.identifier for i in owner.bl_rna.properties[prop].enum_items]
def set_enum(owner, prop, value):
"""Assign an enum; on failure raise listing valid values (dynamic enums under-report)."""
try:
setattr(owner, prop, value)
except TypeError as e:
raise ValueError(f"{prop}={value!r} invalid: {e}") from None
def mesh_tris(ob, evaluated=True):
"""Triangle count (evaluated = with modifiers)."""
if evaluated:
eo = ob.evaluated_get(bpy.context.evaluated_depsgraph_get())
me = eo.to_mesh()
try:
me.calc_loop_triangles()
return len(me.loop_triangles)
finally:
eo.to_mesh_clear()
me = ob.data
me.calc_loop_triangles()
return len(me.loop_triangles)
def r(v, n=4):
"""Round a float or a vector for compact JSON."""
if isinstance(v, float):
return round(v, n)
return [round(x, n) for x in v]
scripts/export_asset.py
"""Export objects/collections with engine presets, then verify the file.
Presets: roblox (FBX), roblox_gltf, unity (FBX), unity_skeletal, unreal (FBX), unreal_skeletal,
godot (GLB), web (GLB + meshopt + WebP), gltf (plain GLB), usd (USDZ/USD).
Selection: --objects (children included) or --collection; default = selected/visible meshes+armatures.
--batch exports each top-level object to its own file at the world origin.
Usage:
blender -b f.blend -P export_asset.py -- --preset unreal --objects SM_Crate --out //export/SM_Crate.fbx
[--collection Export] [--batch] [--no-anim] [--draco] [--set key=value ...]
MCP: import export_asset; print(export_asset.run(preset="godot", objects=["Hero"], out="//export/Hero.glb"))
GLB results include a parsed summary (meshes, materials, images, animations, skins, extensions).
"""
import json
import os
import struct
import sys
import bpy
from mathutils import Vector
sys.path.insert(0, os.path.dirname(__file__))
import _bpyx as bx # noqa: E402
FBX_BASE = dict(use_selection=True, object_types={"MESH", "ARMATURE", "EMPTY"}, use_mesh_modifiers=True,
mesh_smooth_type="FACE", add_leaf_bones=False, use_armature_deform_only=True,
armature_nodetype="NULL", use_triangles=True, use_tspace=True, use_custom_props=False,
bake_anim=True, bake_anim_use_all_actions=False, bake_anim_use_nla_strips=False,
bake_anim_force_startend_keying=True, bake_anim_simplify_factor=0.0, path_mode="AUTO")
GLTF_BASE = dict(use_selection=True, export_format="GLB", export_yup=True, export_apply=True,
export_texcoords=True, export_normals=True, export_tangents=False,
export_materials="EXPORT", export_image_format="AUTO", export_animations=True,
export_animation_mode="ACTIONS", export_skins=True, export_morph=True,
export_influence_nb=4, export_def_bones=True, export_leaf_bone=False,
export_extras=False, export_cameras=False, export_lights=False)
PRESETS = {
# Roblox: FBX Unit Scale imports at the same scale (Creator Hub); embed textures; no leaf bones
"roblox": ("fbx", dict(apply_scale_options="FBX_SCALE_UNITS", path_mode="COPY", embed_textures=True,
use_tspace=False)),
"roblox_gltf": ("gltf", dict(export_influence_nb=4)),
# Unity: FBX All + Apply Transform -> scale 1, no -90 X rotation on import
"unity": ("fbx", dict(apply_scale_options="FBX_SCALE_ALL", bake_space_transform=True,
axis_forward="-Z", axis_up="Y")),
"unity_skeletal": ("fbx", dict(apply_scale_options="FBX_SCALE_ALL", bake_space_transform=False,
axis_forward="-Z", axis_up="Y")),
# Unreal: unit scale in the FBX header (no 100x root scale); smoothing groups; deform bones only
"unreal": ("fbx", dict(apply_scale_options="FBX_SCALE_UNITS", axis_forward="-Z", axis_up="Y")),
"unreal_skeletal": ("fbx", dict(apply_scale_options="FBX_SCALE_UNITS", axis_forward="-Z", axis_up="Y",
primary_bone_axis="Y", secondary_bone_axis="X")),
"godot": ("gltf", dict(export_extras=True)),
"web": ("gltf", dict(export_image_format="WEBP", export_image_quality=85,
export_meshopt_compression_enable=True)),
"gltf": ("gltf", {}),
"usd": ("usd", dict(selected_objects_only=True, export_materials=True, generate_preview_surface=True,
export_armatures=True, only_deform_bones=True, export_shapekeys=True,
export_textures_mode="NEW", relative_paths=True, convert_orientation=False)),
}
EXT = {"fbx": ".fbx", "gltf": ".glb", "usd": ".usdz"}
def glb_summary(path):
"""Parse the JSON chunk of a .glb (no import needed)."""
with open(path, "rb") as f:
magic, _, _ = struct.unpack("<4sII", f.read(12))
if magic != b"glTF":
return {"error": "not a GLB"}
clen, ctype = struct.unpack("<II", f.read(8))
doc = json.loads(f.read(clen))
tris = 0
for m in doc.get("meshes", []):
for p in m.get("primitives", []):
acc = doc["accessors"][p["indices"]] if "indices" in p else None
tris += (acc["count"] // 3) if acc else 0
return {k: len(doc.get(k, [])) for k in ("nodes", "meshes", "materials", "images", "animations", "skins")} | {
"tris": tris, "extensions": doc.get("extensionsUsed", []),
"animation_names": [a.get("name") for a in doc.get("animations", [])][:20]}
def _gather(objects, collection):
if collection:
col = bpy.data.collections[collection]
return [o for o in col.all_objects if o.type in {"MESH", "ARMATURE", "EMPTY", "CURVE"}]
obs = bx.objects(objects, types=("MESH", "ARMATURE", "EMPTY"))
out = []
for o in obs:
out.append(o)
out += [c for c in o.children_recursive if c.type in {"MESH", "ARMATURE", "EMPTY"}]
if o.parent and o.parent.type == "ARMATURE":
out.append(o.parent)
return list(dict.fromkeys(out))
def _export(fmt, path, kw):
if fmt == "fbx":
return bpy.ops.export_scene.fbx(filepath=path, **kw)
if fmt == "gltf":
return bpy.ops.export_scene.gltf(filepath=path, **kw)
return bpy.ops.wm.usd_export(filepath=path, **kw)
def _coerce(v):
for t in (int, float):
try:
return t(v)
except ValueError:
pass
return {"true": True, "false": False}.get(v.lower(), v)
def run(preset="gltf", objects=None, collection=None, out="", batch=False, no_anim=False,
draco=False, set=None, **_): # noqa: A002
bx.object_mode()
fmt, extra = PRESETS[preset]
kw = dict(FBX_BASE if fmt == "fbx" else GLTF_BASE if fmt == "gltf" else {})
kw.update(extra)
if no_anim:
kw.update({"bake_anim": False} if fmt == "fbx" else {"export_animations": False} if fmt == "gltf"
else {"export_animation": False})
if draco and fmt == "gltf":
kw.update(export_draco_mesh_compression_enable=True, export_meshopt_compression_enable=False)
for item in set or []:
k, v = item.split("=", 1)
kw[k] = _coerce(v)
obs = _gather(objects, collection)
if not obs:
raise ValueError("nothing to export")
warn = [f"{o.name}: unapplied scale {tuple(round(s, 3) for s in o.scale)}" for o in obs
if o.type == "MESH" and any(abs(s - 1) > 1e-4 for s in o.scale)]
warn += [f"{o.name}: n-gons present; tangents need tris/quads" for o in obs
if o.type == "MESH" and fmt == "fbx" and kw.get("use_tspace") and not kw.get("use_triangles")
and any(len(p.vertices) > 4 for p in o.data.polygons)]
out = bpy.path.abspath(out) if out else os.path.join(
os.path.dirname(bpy.data.filepath) or os.getcwd(), "export", f"{obs[0].name}{EXT[fmt]}")
if fmt == "usd" and not out.endswith((".usd", ".usda", ".usdc", ".usdz")):
out += ".usdz"
os.makedirs(os.path.dirname(out), exist_ok=True)
jobs = []
if batch:
roots = [o for o in obs if o.parent is None or o.parent not in obs]
for r in roots:
members = [r] + [c for c in r.children_recursive if c in obs]
jobs.append((os.path.join(os.path.dirname(out), f"{r.name}{EXT[fmt]}"), members, r))
else:
jobs.append((out, obs, None))
files = []
for path, members, root in jobs:
bx.select_only(members, active=members[0])
loc = None
if root is not None:
loc = root.location.copy()
root.location = Vector()
try:
res = _export(fmt, path, kw)
finally:
if loc is not None:
root.location = loc
rec = {"file": path, "ok": res == {"FINISHED"} and os.path.exists(path),
"kb": round(os.path.getsize(path) / 1024, 1) if os.path.exists(path) else 0,
"objects": [m.name for m in members]}
if path.endswith(".glb") and rec["ok"]:
rec["summary"] = glb_summary(path)
files.append(rec)
return {"ok": all(f["ok"] for f in files), "preset": preset, "format": fmt, "files": files,
"warnings": warn}
if __name__ == "__main__":
p = bx.add_common(bx.parser(__doc__), save=False)
p.add_argument("--preset", default="gltf", choices=list(PRESETS))
p.add_argument("--collection", default=None)
p.add_argument("--out", default="")
p.add_argument("--batch", action="store_true")
p.add_argument("--no-anim", action="store_true")
p.add_argument("--draco", action="store_true")
p.add_argument("--set", nargs="*", default=None, help="extra exporter kwargs key=value")
bx.cli(run, p)
SKILL.md
---
name: blender-export-pipelines
description: Export from Blender 5.2 with verified presets - Roblox (FBX/glTF, cages, R15, SurfaceAppearance), Unity (FBX, Humanoid), Unreal (FBX skeletal, Nanite, UCX), Godot/web (GLB, meshopt/Draco, WebP), USD. Scale/axes, smoothing, tangents, leaf bones, clips, batch export, post-export verification. Use whenever an asset leaves Blender.
license: MIT
metadata:
author: blender-agent-skills
version: "1.2.0"
---
# Export Pipelines
The execution model is in `blender-core`. Run `blender-asset-qa` (and `blender-game-optimization` for budgets) **before** exporting.
## One call
```
scripts/export_asset.py -- --preset unreal_skeletal --objects SK_Hero --out //export/SK_Hero.fbx
scripts/export_asset.py -- --preset web --collection Export --out //export/scene.glb
scripts/export_asset.py -- --preset unity --objects SM_A SM_B SM_C --batch --out //export/x.fbx # one file each, at origin
```
Presets: `roblox`, `roblox_gltf`, `unity`, `unity_skeletal`, `unreal`, `unreal_skeletal`, `godot`, `web`, `gltf`, `usd`. Pass any exporter kwarg with `--set key=value`. Every preset is tested with a round-trip that keeps scale and avoids a 100× armature scale.
## Engine reference (load only the one you need)
- [references/roblox.md](references/roblox.md): MeshParts, avatar bodies (R15), accessories, layered clothing cages, SurfaceAppearance, limits
- [references/unity.md](references/unity.md): FBX settings, Humanoid rigs, URP/HDRP maps, animation clips
- [references/unreal.md](references/unreal.md): static/skeletal FBX, root bone, collision UCX, LODs, Nanite, ORM/DirectX
- [references/godot-web.md](references/godot-web.md): glTF/GLB, import suffixes, three.js/Babylon, compression, KHR extensions
## Universal pre-export checklist
1. **Scale**: scene unit scale 1.0, objects at scale 1, rotation applied (except intentional object transforms). The presets handle unit conversion.
2. **Pivot/origin** where the engine expects it: props at the bottom-center, characters at the world origin between the feet, facing **-Y** (Blender front). Every preset converts that to the engine's forward.
3. **Modifiers**: the presets export the evaluated mesh. Mirror, bevel and weighted normals are kept; booleans become real geometry.
4. **Normals**: custom/weighted normals export. Smooth shading, plus hard edges marked sharp.
5. **Triangulation**: FBX presets triangulate (`use_triangles`) so the bake and engine tangents match. glTF always triangulates.
6. **Materials**: image-texture → Principled chains only, with names `M_*` and textures `T_*`. Procedural nodes don't export, so bake them first.
7. **Armature**: deform bones only, no leaf bones, rest pose = bind pose, 4 weights per vertex max, and the root at the origin.
8. **Animation**: one action per clip, named `Rig_Clip`. Bake constraints/IK to keys (`blender-animation`). FBX exports the active/NLA action per preset settings; glTF `ACTIONS` mode exports every action that has a slot for the armature.
9. **Naming**: unique, ASCII, no spaces. Engines key materials/meshes by name across reimports.
## Format choice
| Need | Format |
|---|---|
| Unity, Unreal (most stable for skeletal) | FBX |
| Godot, web, Roblox (supported), Unreal 5.x Interchange, Blender↔Blender | glTF 2.0 (`.glb`) |
| Film/VFX, Omniverse, Apple AR (`.usdz`), layout interchange | USD |
| Static geometry for printing/CAD | OBJ / STL (`wm.obj_export`, `wm.stl_export`) |
| Point caches / deforming sims | Alembic (`wm.alembic_export`) |
## Verify after export (required)
- The script returns `ok`, the file size, and for `.glb` a parsed summary (meshes, materials, images, animations, skins, tris, extensions). Compare it with what you expected.
- For FBX/USD: re-import into an empty scene (`import_scene.fbx` / `wm.usd_import`) and check `dimensions`, the armature scale (must be 1, not 100) and the action count.
- In the engine: scale against a reference (UE mannequin 180 cm, Unity capsule 2 m, Roblox character 5 studs), normals/smoothing, the normal map's green channel, and animation root motion.
## Collection exporters (re-export in one click, 4.2+)
See `blender-scene-setup`: `collection.exporters` stores the format + settings per collection, and `bpy.ops.wm.collection_export_all()` re-exports everything.
## Gotchas
- FBX `use_tspace` fails silently on n-gons. Triangulate first (the presets do).
- `bake_space_transform` (Apply Transform) is experimental. Use it for Unity static meshes only, never with armatures.
- glTF exports **actions** linked through slots/NLA. An action with no user on the armature is skipped (`export_animation_mode` options: `ACTIONS`, `ACTIVE_ACTIONS`, `NLA_TRACKS`, `SCENE`, `BROADCAST`).
- glTF needs image textures in PNG/JPEG/WebP. EXR/TIFF get converted (`export_image_format`).
- Linked library data exports fine, but make it local or override it if you need to modify it first.
- Shape keys: FBX exports them as blendshapes, glTF as morph targets (`export_morph`). Modifiers before the Armature modifier can block shape-key export in FBX (apply them first).