diff --git a/.claude/skills/blender-to-unity/SKILL.md b/.claude/skills/blender-to-unity/SKILL.md index 46fc3dc05..6ea4a3df2 100644 --- a/.claude/skills/blender-to-unity/SKILL.md +++ b/.claude/skills/blender-to-unity/SKILL.md @@ -13,12 +13,22 @@ directly. - Both `mcp__blender__*` tools and MCP for Unity tools are connected. - A model exists in the Blender scene (confirm with `mcp__blender__get_scene_info` / `mcp__blender__get_object_info`). If empty, stop and tell the user — this skill does not generate models. +- **`import_model_file` is in the `asset_gen` tool group, which is off by default** (only `core` + loads). Enable it first with `manage_tools` (enable the `asset_gen` group), **or** call the C# + handler straight through `batch_execute` — + `{"tool":"import_model_file","params":{"sourcePath":...,"name":...,"outputFolder":...}}` (camelCase) + — which dispatches by name regardless of group gating. ## Steps 1. **Resolve the Unity project path.** Read `mcpforunity://editor/state` for the project - root (the editor dataPath's parent). Decide the export format: **FBX by default** - (built-in importer, zero extra dependencies). Use glTF/.glb only if glTFast is installed - and PBR fidelity matters. + root (the editor dataPath's parent). Decide the export format: + - **GLB (glTFast) when the model has a rig, animation, PBR (metallic/roughness), emission, + or transparency** — glTFast carries all of these automatically, no post-processing + (see [references/bridge-fidelity.md](references/bridge-fidelity.md)). Multi-material zones + survive either format, so they alone don't force GLB. + - **FBX otherwise** — when glTFast isn't installed, the model is plain geometry, or you + specifically need the built-in importer's humanoid-avatar pipeline. FBX drops emission/metallic + (Step 5 restores emission) and surfaces animation only with `animation_type` set (Step 3). 2. **Export from Blender to a temp path** via `mcp__blender__execute_blender_code`: ```python import bpy, os, tempfile @@ -28,9 +38,15 @@ directly. apply_unit_scale=True, bake_space_transform=True) print(out) ``` - (glTF branch: `bpy.ops.export_scene.gltf(filepath=out_glb, export_format='GLB')`.) + (glTF branch: `out_glb = os.path.join(tempfile.gettempdir(), "blender_to_unity.glb")`, then + `bpy.ops.export_scene.gltf(filepath=out_glb, export_format='GLB', use_active_scene=True)` + — its default `use_active_scene=False` can silently export a *different* open scene.) 3. **Import into Unity** with `import_model_file`: `import_model_file(source_path=, name=, target_size=)`. + For a **rigged/animated FBX**, also pass `animation_type="generic"` (or `"humanoid"`; `"legacy"` + targets the old Animation-component system) — the importer defaults to `"none"`, which + deliberately imports the mesh with **zero animation clips**. GLB ignores this (glTFast imports + animation itself), so it's an FBX-only knob. It returns `{ asset_path, asset_guid }`. Pass `target_size` as the intended final size, but treat it only as a hint: it rescales at import solely when the project's **Auto-normalize** pref is on, and even then is unreliable for Blender FBX (see the **Scale** note). Step 4 does the reliable @@ -49,7 +65,37 @@ directly. float target = 2f; // intended size in meters if (maxDim > 0.0001f) go.transform.localScale *= target / maxDim; ``` -5. **Verify** with `manage_camera(action="screenshot", include_image=true)` and report the +5. **Restore emission FBX dropped (FBX path).** Blender scenes commonly store their color/glow + in *material emission* (and other Principled-node inputs). FBX carries base/diffuse color but + **not emission**, so neon / "Tron" scenes import as dark bodies with black accents. If the + import looks flat vs. Blender, restore it: + a. Dump the emissive materials from Blender via `execute_blender_code`: + ```python + import bpy, json + out = {} + for m in bpy.data.materials: + if not (m.use_nodes and m.node_tree): continue + col, s = (0, 0, 0), 0.0 + p = next((n for n in m.node_tree.nodes if n.type == 'BSDF_PRINCIPLED'), None) + es = next((k for k in ('Emission Color', 'Emission') if p and k in p.inputs), None) # 4.x / 3.x name + if es: + col = tuple(p.inputs[es].default_value)[:3] + s = float(p.inputs['Emission Strength'].default_value) + e = next((n for n in m.node_tree.nodes if n.type == 'EMISSION'), None) + if e and s == 0: + col = tuple(e.inputs['Color'].default_value)[:3]; s = float(e.inputs['Strength'].default_value) + if s > 0 and sum(col) > 0.01: + out[m.name] = [round(col[0], 3), round(col[1], 3), round(col[2], 3), round(s, 3)] + print(json.dumps(out)) + ``` + b. In Unity (`execute_code`): extract the FBX's materials so they're editable + (`AssetDatabase.ExtractAsset` per `Material`, then `ImportAsset(fbx, ForceUpdate)`), then for + each dumped name set `_EmissionColor = color * Mathf.Clamp(strength*0.45f, 1.5f, 5f)`, + `EnableKeyword("_EMISSION")`, and `globalIlluminationFlags = RealtimeEmissive`. Match names + **case-insensitively** — FBX mangles case and can split one material into variants + (`EdgeCyan` → `EdgeCyan` + `EDGE_CYAN`); set every match. Add a global Bloom volume and enable + the camera's `renderPostProcessing` so the emission actually glows. +6. **Verify** with `manage_camera(action="screenshot", include_image=true)` and report the asset path + a screenshot. ## Notes @@ -59,8 +105,30 @@ directly. unreliable for Blender FBX (it can over- or under-shoot, e.g. a `target_size=2` model measured 200 m). The robust fix is the Step 4 measure-bounds-then-set-`localScale` routine, which hits the target size deterministically regardless of the import scale or the Auto-normalize pref. +- **Materials & emission.** FBX carries base/diffuse color and transforms fine, but **drops + emission and any node-based color** — so Blender neon / "Tron" scenes (color stored in + emission) import as dark bodies with black accents. Two fixes: **(a) prefer glTF/GLB when glTFast + is installed** — glTF's PBR model carries `emissiveFactor` + `KHR_materials_emissive_strength` + natively, so emission survives with no post-step (`bpy.ops.export_scene.gltf(filepath=out, export_format='GLB', use_active_scene=True)`); + **(b) with FBX, run Step 5** to dump Blender's emission and reapply it. Also check the mesh for a + `color_attributes` (vertex color) layer — the *data* survives FBX, but URP Lit won't *display* it; + that needs a vertex-color-reading shader, not `_EmissionColor`. - FBX is the default because glTFast is optional in MCP for Unity. If the import errors with "GLB import requires glTFast", re-export as FBX (or install glTFast from the Dependencies tab). +- **Format fidelity.** [references/bridge-fidelity.md](references/bridge-fidelity.md) is a tested + matrix of what each format carries. Summary: **GLB (glTFast)** keeps textures, metallic/roughness, + emission, transparency, and animation automatically; **FBX** drops metallic + emission, needs + `ModelImporter.animationType = Generic` to surface transform animation, and needs material/texture + extraction to assign an embedded texture. Both bake modifiers and carry geometry + vertex-color + *data* (URP Lit won't *display* vertex colors). Neither carries procedural/node materials — bake + them to image textures in Blender first. +- **Animation doesn't auto-play.** An imported clip won't move the model until an `AnimatorController` + drives it — the placed model has an `Animator` with a **null controller**, so it looks frozen. Build + one with `manage_animation` (`controller_create` → add the clip as a looping state → `controller_assign` + onto the instance) rather than hand-rolling `execute_code`. See bridge-fidelity gotcha #7. +- **Multi-material zones survive.** A mesh split into material slots in Blender (e.g. skin/shirt/pants + regions) imports as submeshes with one material each — base colors carry over GLB natively — so you + can "dress" a model with material zones and it arrives intact. - Keep one model per handoff; for batches, repeat the loop with distinct names. - This skill never sends API keys or file bytes over the MCP bridge — Unity reads the file from disk. - `import_model_file` copies only the single source file; for multi-file exports (a text `.gltf` with an external `.bin`, or an `.obj` with a sibling `.mtl`/textures), zip them first and pass the `.zip` — a bare `.gltf`/`.obj` will lose its sidecars. diff --git a/.claude/skills/blender-to-unity/references/bridge-fidelity.md b/.claude/skills/blender-to-unity/references/bridge-fidelity.md new file mode 100644 index 000000000..1c3bffed2 --- /dev/null +++ b/.claude/skills/blender-to-unity/references/bridge-fidelity.md @@ -0,0 +1,106 @@ +# Blender → Unity Bridge — Fidelity Test Results + +Empirical results from probing what survives the Blender→Unity file handoff. The bridge is a +file export/import seam (Blender writes FBX/glTF, Unity reads it), so fidelity = whatever those +interchange formats + Unity's importers support — not a BlenderMCP limitation. + +**Test env:** Blender (BlenderMCP) → Unity 6000.4.11f1, URP, glTFast installed. FBX via the +built-in model importer; GLB via glTFast. Method: one controlled object per feature, exported +**both** ways (`export_scene.fbx` and `export_scene.gltf` GLB), imported both, inspected the +resulting mesh/material/animation assets. + +## Results + +| Probe | FBX | GLB (glTFast) | +|---|---|---| +| Mesh geometry (verts, normals, UVs) | ✅ | ✅ | +| Object hierarchy + transforms | ✅ | ✅ | +| Base / diffuse color | ✅ | ✅ (values shift sRGB↔linear, e.g. `0.20`→`0.48`; visually correct) | +| **Procedural / node material** (noise→color) | ❌ → flat default grey | ❌ → flat default white — **but bake it to a texture first (below) → ✅ arrives intact** | +| **Image texture** (checker on base color) | ❌ embedded but **not assigned** to `_BaseMap` | ✅ `baseColorTexture` = embedded image | +| **Vertex colors — data** | ✅ `mesh.colors` present | ✅ present | +| Vertex colors — *display* | ❌ URP Lit ignores them | ✅ glTFast shader displays them (confirmed — white-base mesh rendered its vertex colors) | +| **Alpha / transparency** | ❌ stays **Opaque** (alpha kept in color, surface not switched, rq 2000) | ✅ **Transparent** (Surface=1, rq 3000) | +| **Metallic / roughness** | ❌ metallic reset to 0 on URP conversion | ✅ `metal=1.0`, `rough=0.15` | +| **Modifier** (Subsurf, unapplied) | ✅ baked on export (8→384 verts) | ✅ baked (8→384 verts) | +| **Animation** (object transform) | ⚠️ in file, but importer default `animationType=None` → 0 clips; pass `animation_type="generic"` → 1 clip | ✅ auto-imported (1 clip) | +| **Armature + skinning** | ✅ skinned mesh, 3 bind poses | ✅ skinned mesh, 3 bind poses | +| **Skeletal animation** | ✅ (pass `animation_type="generic"`/`"humanoid"`) | ✅ auto (Animator + clip) | +| **Multi-material zones** (submeshes) | ✅ submeshes + a material each | ✅ submeshes + a material each (base colors native) | +| **Shape keys → blend shapes** | ✅ (Bulge, Spike) | ✅ (Bulge, Spike) | +| **Custom properties** | ➖ FBX user-props (not surfaced by default) | ➖ glTF `extras` (needs `export_extras`; not auto-exposed) | +| **Emission** (from the city scene) | ❌ dropped | ✅ native (`emissiveFactor` + `KHR_materials_emissive_strength`) | +| **Scale** | oversized, needs normalize | oversized (larger), needs normalize | + +Legend: ✅ transfers · ❌ lost · ⚠️ needs an import setting · ➖ partial/shader-dependent. + +## Verdict + +**GLB via glTFast is materially higher-fidelity than FBX** for this pipeline: textures, +transparency, metallic/roughness, emission, and animation all transfer automatically. FBX only +ties on geometry, vertex-color *data*, and modifier baking, and it wins only where you need the +built-in importer's rig/humanoid pipeline or want URP-Lit-native materials (e.g. for URP fog). + +Neither format carries **procedural/node materials** — those must be **baked to image textures in +Blender first**. Neither shows **vertex colors** without a vertex-color-reading shader. + +## Gotchas discovered (worth automating around) + +1. **`export_scene.gltf` defaults `use_active_scene=False`.** It exported the *wrong* scene (a + different open scene) — 4.7 MB instead of 65 KB. Always pass **`use_active_scene=True`**. +2. **FBX `embed_textures=True` embeds but doesn't assign.** The image lands inside the FBX but + Unity's URP material conversion leaves `_BaseMap` empty. Extract materials/textures and assign, + or just use GLB. +3. **FBX animation needs a non-`None` rig mode.** The clip data is in the file, but the importer's + default `animationType = None` yields **zero clips**. `import_model_file` takes + `animation_type="generic" | "humanoid" | "legacy"` to set this at import time (previously you had + to hand-edit the `ModelImporter` after the fact, which is what surfaced this whole gotcha). GLB is + unaffected — glTFast imports animation itself. +4. **FBX drops metallic and emission** in the built-in→URP material conversion; GLB keeps both. +5. **Both need a scale normalize** (measure world bounds → set `localScale`); GLB tends to land + even larger than FBX. +6. **Rigs & morphs need "no-apply" export.** For skinned / shape-key meshes, skip + `bake_space_transform` (FBX) and `export_apply` (glTF) — applying modifiers bakes away the + armature deform and morph targets. Use `use_mesh_modifiers=False` (FBX) / `export_apply=False` + (glTF). Static-geometry exports still want the apply (it bakes Subsurf etc.). +7. **Imported animation doesn't auto-play.** The clip imports fine but the placed model just has an + `Animator` with a **null controller**, so it looks frozen in edit mode. To *see* it: assign an + `AnimatorController` referencing the clip (set the clip's `loopTime` for continuous playback) and + enter **Play mode** (or drive it via the Animation window / Timeline / legacy `Animation`). This + is a Unity playback detail, not a transfer failure — verified the bone rotated 37°→6° while + playing and looped 15×. + +## Recovering procedural / node materials (bake) + +Neither format carries node graphs, but you can **bake them to an image texture in Blender**, then +they export as an ordinary texture. Verified: a `Voronoi → ColorRamp` material baked to 512px and +arrived in Unity with `baseColorTexture` assigned and the pattern visible. + +Recipe (Cycles bake): +1. Give the object a real UV unwrap — `bpy.ops.uv.smart_project(...)`. +2. Add an empty Image + Image Texture node to the material and set it **active** (the bake target): + `nt.nodes.active = texnode`. +3. `scene.render.engine='CYCLES'`; select the object as active; then + `bpy.ops.object.bake(type='DIFFUSE', pass_filter={'COLOR'}, margin=4, use_clear=True)`. +4. Rewire the baked texture node into **Base Color** (replacing the procedural chain); `image.pack()`. +5. Export GLB as usual. (Bake `EMIT` for glow-driven looks; `COMBINED` to bake full lighting in.) + +## Capture more in one export (GLB flags) + +Enable the extra channels you want in `export_scene.gltf(..., export_format='GLB', use_active_scene=True, ...)`: +- `export_apply=True` — bakes modifiers (Subsurf etc.) — **static geometry only**; skinned / shape-key + meshes need `export_apply=False` or the armature deform and morph targets bake away (gotcha 6) +- `export_animations=True` — **active or NLA-stashed** actions → clips; unassociated actions are + dropped (Stash / Push Down them onto the object they animate first) +- `export_morph=True` — **shape keys → Unity blend shapes** +- `export_skins=True` — **armature / skinning** +- `export_tangents=True` — normal-map-ready meshes +- `export_extras=True` — Blender **custom properties** +- `export_cameras=True`, `export_lights=True` — cameras/lights come across (units differ — treat as reference, retune in Unity) + +## Practical rules + +- **Materials/textures/PBR/emission/animation matter → GLB** (glTFast installed). +- **Rigs/humanoid/URP-Lit-native materials matter → FBX** (then extract materials, set animation type). +- **Anything procedural/simulated (node materials, geometry nodes, particles, physics) → bake in + Blender first** (bake to textures, apply/realize modifiers, convert particles to mesh). diff --git a/MCPForUnity/Editor/Services/AssetGen/AssetGenJobManager.cs b/MCPForUnity/Editor/Services/AssetGen/AssetGenJobManager.cs index f784a9300..1f23fa578 100644 --- a/MCPForUnity/Editor/Services/AssetGen/AssetGenJobManager.cs +++ b/MCPForUnity/Editor/Services/AssetGen/AssetGenJobManager.cs @@ -30,6 +30,7 @@ public sealed class AssetGenJob public float Progress; public string Format; public float TargetSize = 1f; + public string AnimationType; // FBX/OBJ rig mode: null/none | generic | humanoid | legacy public string AssetPath; public string AssetGuid; public string Error; diff --git a/MCPForUnity/Editor/Services/AssetGen/Import/ModelImportPipeline.cs b/MCPForUnity/Editor/Services/AssetGen/Import/ModelImportPipeline.cs index fa3fe8c69..02f1ab035 100644 --- a/MCPForUnity/Editor/Services/AssetGen/Import/ModelImportPipeline.cs +++ b/MCPForUnity/Editor/Services/AssetGen/Import/ModelImportPipeline.cs @@ -150,7 +150,7 @@ private static void ApplyModelImporterSettings(string rel, AssetGenJob job) if (!(AssetImporter.GetAtPath(rel) is ModelImporter importer)) return; importer.useFileScale = true; importer.materialImportMode = ModelImporterMaterialImportMode.ImportStandard; - importer.animationType = ModelImporterAnimationType.None; + importer.animationType = ParseAnimationType(job?.AnimationType); if (AssetGenPrefs.AutoNormalize && job.TargetSize > 0f) { @@ -168,6 +168,24 @@ private static void ApplyModelImporterSettings(string rel, AssetGenJob job) importer.SaveAndReimport(); } + /// + /// Map the caller's rig mode to a . Defaults to + /// None (no rig) when unset — pass "generic"/"humanoid" to surface a rigged FBX/OBJ's + /// AnimationClips, which the None default otherwise hides. "legacy" selects Unity's + /// legacy Animation system — rarely needed. + /// + internal static ModelImporterAnimationType ParseAnimationType(string value) + { + switch ((value ?? string.Empty).Trim().ToLowerInvariant()) + { + case "generic": return ModelImporterAnimationType.Generic; + case "humanoid": + case "human": return ModelImporterAnimationType.Human; + case "legacy": return ModelImporterAnimationType.Legacy; + default: return ModelImporterAnimationType.None; + } + } + private static float ComputeMaxDimension(string rel) { try diff --git a/MCPForUnity/Editor/Tools/AssetGen/ImportModelFile.cs b/MCPForUnity/Editor/Tools/AssetGen/ImportModelFile.cs index 134c58957..cb9ff21a1 100644 --- a/MCPForUnity/Editor/Tools/AssetGen/ImportModelFile.cs +++ b/MCPForUnity/Editor/Tools/AssetGen/ImportModelFile.cs @@ -46,7 +46,11 @@ public static object HandleCommand(JObject @params) string destRel = StageUnderAssets(srcAbs, baseName, ext, p.Get("outputFolder")); AssetDatabase.Refresh(); - var job = new AssetGenJob { TargetSize = p.GetFloat("targetSize", 1f) ?? 1f }; + var job = new AssetGenJob + { + TargetSize = p.GetFloat("targetSize", 1f) ?? 1f, + AnimationType = p.Get("animationType"), + }; AssetGenJob result = ModelImportPipeline.ImportInto(job, destRel); if (result == null || result.State == AssetGenJobState.Failed) diff --git a/Server/src/cli/commands/asset_gen.py b/Server/src/cli/commands/asset_gen.py index fdee7c8d0..c2d093248 100644 --- a/Server/src/cli/commands/asset_gen.py +++ b/Server/src/cli/commands/asset_gen.py @@ -122,8 +122,12 @@ def import_model( @click.option("--name", default=None, help="Base name for the imported asset.") @click.option("--output-folder", default=None, help="Destination folder under Assets/.") @click.option("--target-size", default=None, type=float, help="Normalize largest dimension (meters).") +@click.option("--animation-type", "animation_type", default=None, + type=click.Choice(["none", "generic", "humanoid", "legacy"]), + help="FBX/OBJ rig mode: generic/humanoid surface animation clips; " + "legacy selects Unity's legacy Animation system (glTF ignores this).") @handle_unity_errors -def import_model_file(source_path, name, output_folder, target_size): +def import_model_file(source_path, name, output_folder, target_size, animation_type): """Import a local 3D model file (e.g. a Blender export) into the Unity project.""" config = get_config() params = { @@ -131,6 +135,7 @@ def import_model_file(source_path, name, output_folder, target_size): "name": name, "outputFolder": output_folder, "targetSize": target_size, + "animationType": animation_type, } params = {k: v for k, v in params.items() if v is not None} result = run_command("import_model_file", params, config) diff --git a/Server/src/services/tools/import_model_file.py b/Server/src/services/tools/import_model_file.py index f9b62595b..692781882 100644 --- a/Server/src/services/tools/import_model_file.py +++ b/Server/src/services/tools/import_model_file.py @@ -5,7 +5,7 @@ Thin pass-through: NO API keys and NO file bytes cross the bridge. The C# side copies the file under Assets/ and runs the shared model-import pipeline. """ -from typing import Annotated, Any +from typing import Annotated, Any, Literal from fastmcp import Context from mcp.types import ToolAnnotations @@ -24,7 +24,12 @@ "under Assets/ and run through Unity's model-import pipeline (scale-normalize, material " "settings; glTF requires glTFast). Carries no API keys and no file bytes over the bridge.\n\n" "Params: source_path (absolute or Assets-relative path to a .fbx/.obj/.glb/.gltf/.zip), " - "name, output_folder (under Assets/), target_size. Returns { asset_path, asset_guid }.\n\n" + "name, output_folder (under Assets/), target_size, animation_type. " + "Returns { asset_path, asset_guid }.\n\n" + "animation_type (FBX/OBJ only): pass 'generic' or 'humanoid' for a rigged/animated mesh so " + "Unity surfaces its AnimationClips; omitted or 'none' imports no rig (this is the usual " + "cause of a rigged FBX importing with zero clips); 'legacy' selects Unity's legacy Animation " + "system (rarely needed). glTF/GLB ignore it — glTFast imports animation itself.\n\n" "For multi-file exports (a text .gltf with an external .bin, or an .obj with a sibling " ".mtl/textures), zip them and pass the .zip — a bare .gltf/.obj is copied without its sidecars." ), @@ -39,6 +44,12 @@ async def import_model_file( name: Annotated[str, "Base name for the imported asset."] | None = None, output_folder: Annotated[str, "Destination folder under Assets/ for the import."] | None = None, target_size: Annotated[float, "Normalize the largest dimension to this size (meters)."] | None = None, + animation_type: Annotated[ + Literal["none", "generic", "humanoid", "legacy"], + "FBX/OBJ only: rig/animation import mode. 'generic' or 'humanoid' surface the model's " + "AnimationClips; 'legacy' selects Unity's legacy Animation system (rarely needed); " + "omitted or 'none' imports no rig. Ignored for glTF/GLB.", + ] | None = None, ) -> dict[str, Any]: unity_instance = await get_unity_instance_from_context(ctx) @@ -47,6 +58,7 @@ async def import_model_file( "name": name, "outputFolder": output_folder, "targetSize": target_size, + "animationType": animation_type, } params_dict = {k: v for k, v in params_dict.items() if v is not None} diff --git a/Server/tests/test_asset_gen_import_file.py b/Server/tests/test_asset_gen_import_file.py index 22b17af48..5f48dd28f 100644 --- a/Server/tests/test_asset_gen_import_file.py +++ b/Server/tests/test_asset_gen_import_file.py @@ -17,7 +17,7 @@ COMMAND = "import_model_file" -ALLOWED_KEYS = {"sourcePath", "name", "outputFolder", "targetSize"} +ALLOWED_KEYS = {"sourcePath", "name", "outputFolder", "targetSize", "animationType"} def _call_tool(**kwargs): @@ -85,6 +85,13 @@ def test_none_values_stripped(self): _, sent = _call_tool(source_path="/tmp/x.obj") assert _sent_params(sent) == {"sourcePath": "/tmp/x.obj"} + def test_animation_type_mapped(self): + _, sent = _call_tool(source_path="/tmp/rig.fbx", animation_type="generic") + params = _sent_params(sent) + assert params["animationType"] == "generic" + assert "animation_type" not in params + assert set(params.keys()).issubset(ALLOWED_KEYS) + def test_no_secret_keys_in_payload(self): _, sent = _call_tool( source_path="/tmp/a.glb", name="N", @@ -121,3 +128,12 @@ def test_import_model_file_cli(self, cli_runner): assert params["outputFolder"] == "Assets/Props" assert params["targetSize"] == 1.5 assert set(params.keys()).issubset(ALLOWED_KEYS) + + def test_import_model_file_cli_animation_type(self, cli_runner): + result, mock_run = cli_runner([ + "import-model-file", "--source-path", "/tmp/rig.fbx", "--animation-type", "humanoid", + ]) + assert result.exit_code == 0 + params = mock_run.call_args.args[1] + assert params["animationType"] == "humanoid" + assert set(params.keys()).issubset(ALLOWED_KEYS) diff --git a/Server/uv.lock b/Server/uv.lock index 3eaa003ee..a7e843476 100644 --- a/Server/uv.lock +++ b/Server/uv.lock @@ -858,7 +858,7 @@ wheels = [ [[package]] name = "mcpforunityserver" -version = "9.7.3" +version = "10.0.0" source = { editable = "." } dependencies = [ { name = "click" }, diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/ModelImportPipelineTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/ModelImportPipelineTests.cs index 349495c2a..b04c66022 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/ModelImportPipelineTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/ModelImportPipelineTests.cs @@ -1,6 +1,7 @@ using MCPForUnity.Editor.Services.AssetGen; using MCPForUnity.Editor.Services.AssetGen.Import; using NUnit.Framework; +using UnityEditor; namespace MCPForUnityTests.Editor.AssetGen { @@ -43,5 +44,19 @@ public void NullPath_Fails() AssetGenJob result = ModelImportPipeline.ImportInto(Job("fbx"), null); Assert.AreEqual(AssetGenJobState.Failed, result.State); } + + [TestCase("generic", ModelImporterAnimationType.Generic)] + [TestCase("Generic", ModelImporterAnimationType.Generic)] + [TestCase("humanoid", ModelImporterAnimationType.Human)] + [TestCase("human", ModelImporterAnimationType.Human)] + [TestCase(" LEGACY ", ModelImporterAnimationType.Legacy)] + [TestCase("none", ModelImporterAnimationType.None)] + [TestCase("", ModelImporterAnimationType.None)] + [TestCase(null, ModelImporterAnimationType.None)] + [TestCase("nonsense", ModelImporterAnimationType.None)] + public void ParseAnimationType_MapsRigMode(string input, ModelImporterAnimationType expected) + { + Assert.AreEqual(expected, ModelImportPipeline.ParseAnimationType(input)); + } } } diff --git a/website/docs/reference/tools/asset_gen/import_model_file.md b/website/docs/reference/tools/asset_gen/import_model_file.md index 0948fed4f..8014cc51b 100644 --- a/website/docs/reference/tools/asset_gen/import_model_file.md +++ b/website/docs/reference/tools/asset_gen/import_model_file.md @@ -14,7 +14,9 @@ description: "Import a local 3D model file that already exists on disk (e.g. an Import a local 3D model file that already exists on disk (e.g. an FBX/OBJ/glTF exported from Blender or another DCC tool) into the Unity project. The file is copied under Assets/ and run through Unity's model-import pipeline (scale-normalize, material settings; glTF requires glTFast). Carries no API keys and no file bytes over the bridge. -Params: source_path (absolute or Assets-relative path to a .fbx/.obj/.glb/.gltf/.zip), name, output_folder (under Assets/), target_size. Returns { asset_path, asset_guid }. +Params: source_path (absolute or Assets-relative path to a .fbx/.obj/.glb/.gltf/.zip), name, output_folder (under Assets/), target_size, animation_type. Returns { asset_path, asset_guid }. + +animation_type (FBX/OBJ only): pass 'generic' or 'humanoid' for a rigged/animated mesh so Unity surfaces its AnimationClips; omitted or 'none' imports no rig (this is the usual cause of a rigged FBX importing with zero clips); 'legacy' selects Unity's legacy Animation system (rarely needed). glTF/GLB ignore it — glTFast imports animation itself. For multi-file exports (a text .gltf with an external .bin, or an .obj with a sibling .mtl/textures), zip them and pass the .zip — a bare .gltf/.obj is copied without its sidecars. @@ -26,6 +28,7 @@ For multi-file exports (a text .gltf with an external .bin, or an .obj with a si | `name` | `str \| None` | — | Base name for the imported asset. | | `output_folder` | `str \| None` | — | Destination folder under Assets/ for the import. | | `target_size` | `float \| None` | — | Normalize the largest dimension to this size (meters). | +| `animation_type` | `Literal['none', 'generic', 'humanoid', 'legacy'] \| None` | — | FBX/OBJ only: rig/animation import mode. 'generic' or 'humanoid' surface the model's AnimationClips; 'legacy' selects Unity's legacy Animation system (rarely needed); omitted or 'none' imports no rig. Ignored for glTF/GLB. | ## Returns