Skip to content

Commit 36fcefb

Browse files
committed
Merge remote-tracking branch 'upstream/beta' into revamp/brand-distribution-analytics
# Conflicts: # README.md
2 parents 69eb51c + 819d7d3 commit 36fcefb

147 files changed

Lines changed: 8350 additions & 7 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
---
2+
name: blender-to-unity
3+
description: Hand off a model from Blender (via BlenderMCP) into Unity (via MCP for Unity) — export the current Blender model, import it through import_model_file, and place it in the open scene. Use when the user has BlenderMCP and MCP for Unity both connected and wants to bring a Blender model into Unity. Does NOT drive Blender's own generators; BlenderMCP owns how the model got into Blender.
4+
---
5+
6+
# Blender → Unity Model Handoff
7+
8+
Bring whatever model is currently in Blender into the open Unity scene. The seam is the
9+
local filesystem: Blender exports a file, Unity imports it. The two servers never talk
10+
directly.
11+
12+
## Preconditions
13+
- Both `mcp__blender__*` tools and MCP for Unity tools are connected.
14+
- A model exists in the Blender scene (confirm with `mcp__blender__get_scene_info` /
15+
`mcp__blender__get_object_info`). If empty, stop and tell the user — this skill does not generate models.
16+
17+
## Steps
18+
1. **Resolve the Unity project path.** Read `mcpforunity://editor/state` for the project
19+
root (the editor dataPath's parent). Decide the export format: **FBX by default**
20+
(built-in importer, zero extra dependencies). Use glTF/.glb only if glTFast is installed
21+
and PBR fidelity matters.
22+
2. **Export from Blender to a temp path** via `mcp__blender__execute_blender_code`:
23+
```python
24+
import bpy, os, tempfile
25+
out = os.path.join(tempfile.gettempdir(), "blender_to_unity.fbx")
26+
# Export the selection if any, else the whole scene:
27+
bpy.ops.export_scene.fbx(filepath=out, use_selection=bool(bpy.context.selected_objects),
28+
apply_unit_scale=True, bake_space_transform=True)
29+
print(out)
30+
```
31+
(glTF branch: `bpy.ops.export_scene.gltf(filepath=out_glb, export_format='GLB')`.)
32+
3. **Import into Unity** with `import_model_file`:
33+
`import_model_file(source_path=<temp path>, name=<asset name>, target_size=<final size in meters>)`.
34+
It returns `{ asset_path, asset_guid }`. Pass `target_size` as the intended final size, but treat
35+
it only as a hint: it rescales at import solely when the project's **Auto-normalize** pref is on,
36+
and even then is unreliable for Blender FBX (see the **Scale** note). Step 4 does the reliable
37+
normalization.
38+
4. **Place it in the scene, normalized to size.** Ensure the scene has a camera + directional
39+
light (`manage_scene` / `manage_gameobject`). Instantiate the model at the chosen position via
40+
`manage_gameobject(action="create", prefab_path=<asset_path>, name=<asset name>, position=[x,y,z])`.
41+
Then normalize its size deterministically — Blender FBX commonly imports ~100× too large — by
42+
measuring the placed model's world bounds and scaling so its largest dimension equals your target
43+
size. Run via `execute_code` (substitute your object name and target meters):
44+
```csharp
45+
var go = GameObject.Find("<asset name>");
46+
var rs = go.GetComponentsInChildren<Renderer>();
47+
var b = rs[0].bounds; for (int i = 1; i < rs.Length; i++) b.Encapsulate(rs[i].bounds);
48+
float maxDim = Mathf.Max(b.size.x, Mathf.Max(b.size.y, b.size.z));
49+
float target = 2f; // intended size in meters
50+
if (maxDim > 0.0001f) go.transform.localScale *= target / maxDim;
51+
```
52+
5. **Verify** with `manage_camera(action="screenshot", include_image=true)` and report the
53+
asset path + a screenshot.
54+
55+
## Notes
56+
- **Scale.** Models from Blender almost always arrive at the wrong scale — its FBX unit handling
57+
makes them land ~100× too large in Unity. `import_model_file`'s `target_size` only rescales at
58+
import when the project's Auto-normalize pref is enabled, and its importer-level normalization is
59+
unreliable for Blender FBX (it can over- or under-shoot, e.g. a `target_size=2` model measured 200 m).
60+
The robust fix is the Step 4 measure-bounds-then-set-`localScale` routine, which hits the target
61+
size deterministically regardless of the import scale or the Auto-normalize pref.
62+
- FBX is the default because glTFast is optional in MCP for Unity. If the import errors with
63+
"GLB import requires glTFast", re-export as FBX (or install glTFast from the Dependencies tab).
64+
- Keep one model per handoff; for batches, repeat the loop with distinct names.
65+
- This skill never sends API keys or file bytes over the MCP bridge — Unity reads the file from disk.
66+
- `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.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Manual Verify — blender-to-unity
2+
3+
Run against a live Blender (BlenderMCP) + Unity (MCP for Unity) pair.
4+
5+
- [ ] A cube/model exists in Blender (`get_scene_info` shows it).
6+
- [ ] FBX export writes a non-empty file to the temp path (printed path exists, size > 0).
7+
- [ ] `import_model_file` returns `success: true` with `asset_path` under `Assets/` and a non-empty `asset_guid`.
8+
- [ ] The imported model appears in the Project window at `asset_path`.
9+
- [ ] The model is instantiated in the open scene and visible in a `manage_camera` screenshot.
10+
- [ ] Scale: after the Step 4 measure-bounds-then-`localScale` routine, the placed model's largest
11+
world dimension ≈ the target size (Blender FBX imports ~100× too large until normalized).
12+
- [ ] glTF path: with glTFast installed, a `.glb` export imports successfully; without it,
13+
the error names glTFast/the Dependencies tab (and FBX still works).
14+
- [ ] No API keys or file bytes appear in any bridge payload (handoff is filesystem-only).

MCPForUnity/Editor/Constants/EditorPrefKeys.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,5 +73,14 @@ internal static class EditorPrefKeys
7373
internal const string LogRecordEnabled = "MCPForUnity.LogRecordEnabled";
7474

7575
internal const string ExecuteCodeCompiler = "MCPForUnity.ExecuteCode.Compiler";
76+
77+
// AI Asset Generation — NON-SECRET config only. Provider API keys live in the OS
78+
// secure store (MCPForUnity.Editor.Security.SecureKeyStore), never in EditorPrefs.
79+
internal const string AssetGenSelectedModelProvider = "MCPForUnity.AssetGen.ModelProvider";
80+
internal const string AssetGenSelectedImageProvider = "MCPForUnity.AssetGen.ImageProvider";
81+
internal const string AssetGenDefaultFormat = "MCPForUnity.AssetGen.Format";
82+
internal const string AssetGenOutputRoot = "MCPForUnity.AssetGen.OutputRoot";
83+
internal const string AssetGenAutoNormalize = "MCPForUnity.AssetGen.AutoNormalize";
84+
internal const string AssetGenProviderEnabledPrefix = "MCPForUnity.AssetGen.Enabled.";
7685
}
7786
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using System;
2+
using System.IO;
3+
using UnityEngine;
4+
5+
namespace MCPForUnity.Editor.Helpers
6+
{
7+
/// <summary>
8+
/// Project-path conversions shared by the asset-gen import/write code: project-relative
9+
/// ("Assets/...") ↔ absolute on-disk paths, with forward-slash normalization for
10+
/// cross-platform consistency.
11+
/// </summary>
12+
public static class AssetGenPaths
13+
{
14+
/// <summary>Resolve a project-relative ("Assets/...") path to an absolute, forward-slashed path.</summary>
15+
public static string ToAbsolute(string projectRelative)
16+
{
17+
if (string.IsNullOrWhiteSpace(projectRelative)) return projectRelative;
18+
string p = projectRelative.Replace('\\', '/');
19+
string abs = Path.IsPathRooted(p) ? p : Path.Combine(ProjectRoot(), p);
20+
return Path.GetFullPath(abs).Replace('\\', '/');
21+
}
22+
23+
/// <summary>Convert an absolute (or already-relative) path to a project-relative ("Assets/...") path.</summary>
24+
public static string ToProjectRelative(string path)
25+
{
26+
if (TryGetAssetsRelativePath(path, out string rel)) return rel;
27+
return path?.Replace('\\', '/');
28+
}
29+
30+
/// <summary>
31+
/// Normalize an absolute or "Assets/..." path and verify it resolves inside the project's
32+
/// Assets directory. Rejects traversal such as "Assets/../ProjectSettings".
33+
/// </summary>
34+
public static bool TryGetAssetsRelativePath(string path, out string projectRelative)
35+
{
36+
projectRelative = null;
37+
if (string.IsNullOrWhiteSpace(path)) return false;
38+
39+
try
40+
{
41+
string p = path.Replace('\\', '/');
42+
string abs;
43+
if (p == "Assets" || p.StartsWith("Assets/", StringComparison.Ordinal))
44+
{
45+
abs = Path.Combine(ProjectRoot(), p);
46+
}
47+
else if (Path.IsPathRooted(p))
48+
{
49+
abs = p;
50+
}
51+
else
52+
{
53+
return false;
54+
}
55+
56+
string full = Path.GetFullPath(abs).Replace('\\', '/');
57+
string dataPath = Path.GetFullPath(Application.dataPath).Replace('\\', '/').TrimEnd('/');
58+
if (string.Equals(full, dataPath, StringComparison.Ordinal))
59+
{
60+
projectRelative = "Assets";
61+
return true;
62+
}
63+
64+
string prefix = dataPath + "/";
65+
if (!full.StartsWith(prefix, StringComparison.Ordinal)) return false;
66+
67+
projectRelative = "Assets/" + full.Substring(prefix.Length);
68+
return true;
69+
}
70+
catch
71+
{
72+
return false;
73+
}
74+
}
75+
76+
/// <summary>Normalize an Assets folder path, trimming trailing slashes.</summary>
77+
public static bool TryGetAssetsFolder(string path, out string projectRelative)
78+
{
79+
if (!TryGetAssetsRelativePath(path, out projectRelative)) return false;
80+
projectRelative = projectRelative.TrimEnd('/');
81+
return true;
82+
}
83+
84+
private static string ProjectRoot()
85+
{
86+
string dataPath = Path.GetFullPath(Application.dataPath).Replace('\\', '/');
87+
return dataPath.Substring(0, dataPath.Length - "Assets".Length);
88+
}
89+
}
90+
}

MCPForUnity/Editor/Helpers/AssetGenPaths.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using MCPForUnity.Editor.Constants;
2+
using UnityEditor;
3+
4+
namespace MCPForUnity.Editor.Helpers
5+
{
6+
/// <summary>
7+
/// Per-user, NON-SECRET configuration for the AI Asset Generation feature.
8+
///
9+
/// Provider API keys are never stored here — they live in the OS secure store
10+
/// (<see cref="MCPForUnity.Editor.Security.SecureKeyStore"/>). Only UI/behavior
11+
/// preferences (selected provider, default format, output folder, normalize toggle,
12+
/// per-provider enable flags) are kept in EditorPrefs.
13+
/// </summary>
14+
public static class AssetGenPrefs
15+
{
16+
public const string DefaultOutputRoot = "Assets/Generated";
17+
public const string DefaultModelProvider = "tripo";
18+
public const string DefaultImageProvider = "fal";
19+
public const string DefaultFormatValue = "glb";
20+
21+
public static string ModelProvider
22+
{
23+
get => EditorPrefs.GetString(EditorPrefKeys.AssetGenSelectedModelProvider, DefaultModelProvider);
24+
set => SetOrDelete(EditorPrefKeys.AssetGenSelectedModelProvider, value);
25+
}
26+
27+
public static string ImageProvider
28+
{
29+
get => EditorPrefs.GetString(EditorPrefKeys.AssetGenSelectedImageProvider, DefaultImageProvider);
30+
set => SetOrDelete(EditorPrefKeys.AssetGenSelectedImageProvider, value);
31+
}
32+
33+
public static string DefaultFormat
34+
{
35+
get => EditorPrefs.GetString(EditorPrefKeys.AssetGenDefaultFormat, DefaultFormatValue);
36+
set => SetOrDelete(EditorPrefKeys.AssetGenDefaultFormat, value);
37+
}
38+
39+
/// <summary>Project-relative root under which generated assets are written. Defaults to Assets/Generated.</summary>
40+
public static string OutputRoot
41+
{
42+
get
43+
{
44+
string v = EditorPrefs.GetString(EditorPrefKeys.AssetGenOutputRoot, string.Empty);
45+
return string.IsNullOrWhiteSpace(v) ? DefaultOutputRoot : v;
46+
}
47+
set => SetOrDelete(EditorPrefKeys.AssetGenOutputRoot, value);
48+
}
49+
50+
/// <summary>When true, imported models are uniformly scaled to a target size on import.</summary>
51+
public static bool AutoNormalize
52+
{
53+
get => EditorPrefs.GetBool(EditorPrefKeys.AssetGenAutoNormalize, true);
54+
set => EditorPrefs.SetBool(EditorPrefKeys.AssetGenAutoNormalize, value);
55+
}
56+
57+
public static bool IsProviderEnabled(string providerId) =>
58+
!string.IsNullOrEmpty(providerId)
59+
&& EditorPrefs.GetBool(EditorPrefKeys.AssetGenProviderEnabledPrefix + providerId, false);
60+
61+
public static void SetProviderEnabled(string providerId, bool enabled)
62+
{
63+
if (string.IsNullOrEmpty(providerId)) return;
64+
EditorPrefs.SetBool(EditorPrefKeys.AssetGenProviderEnabledPrefix + providerId, enabled);
65+
}
66+
67+
private static void SetOrDelete(string key, string value)
68+
{
69+
if (string.IsNullOrWhiteSpace(value)) EditorPrefs.DeleteKey(key);
70+
else EditorPrefs.SetString(key, value.Trim());
71+
}
72+
}
73+
}

MCPForUnity/Editor/Helpers/AssetGenPrefs.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using UnityEngine;
5+
6+
namespace MCPForUnity.Editor.Helpers
7+
{
8+
/// <summary>
9+
/// Best-effort detection of a locally installed Blender application, for the Asset Gen tab's
10+
/// "Blender → Unity handoff" hint. This finds the Blender APP only — it cannot tell whether the
11+
/// BlenderMCP server is configured in the user's AI client (that lives outside Unity).
12+
/// </summary>
13+
internal static class BlenderDetection
14+
{
15+
/// <summary>True if a Blender executable is found in a well-known location or on PATH.</summary>
16+
public static bool IsInstalled()
17+
{
18+
try { return DetectIn(CandidatePaths(), File.Exists); }
19+
catch { return false; }
20+
}
21+
22+
/// <summary>Pure core: true if <paramref name="exists"/> reports any candidate present. Testable.</summary>
23+
internal static bool DetectIn(IEnumerable<string> candidates, Func<string, bool> exists)
24+
{
25+
if (candidates == null || exists == null) return false;
26+
foreach (string c in candidates)
27+
if (!string.IsNullOrEmpty(c) && exists(c)) return true;
28+
return false;
29+
}
30+
31+
/// <summary>Well-known Blender executable paths for the current platform, plus PATH entries.</summary>
32+
internal static IEnumerable<string> CandidatePaths()
33+
{
34+
var list = new List<string>();
35+
bool win = Application.platform == RuntimePlatform.WindowsEditor;
36+
string exeName = win ? "blender.exe" : "blender";
37+
38+
// PATH entries: <dir>/blender(.exe)
39+
string pathVar = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
40+
foreach (string dir in pathVar.Split(win ? ';' : ':'))
41+
if (!string.IsNullOrWhiteSpace(dir)) list.Add(Path.Combine(dir.Trim(), exeName));
42+
43+
switch (Application.platform)
44+
{
45+
case RuntimePlatform.OSXEditor:
46+
list.Add("/Applications/Blender.app/Contents/MacOS/Blender");
47+
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
48+
if (!string.IsNullOrEmpty(home))
49+
list.Add(Path.Combine(home, "Applications/Blender.app/Contents/MacOS/Blender"));
50+
break;
51+
case RuntimePlatform.WindowsEditor:
52+
foreach (string pf in new[]
53+
{
54+
Environment.GetEnvironmentVariable("ProgramFiles"),
55+
Environment.GetEnvironmentVariable("ProgramFiles(x86)")
56+
})
57+
{
58+
if (string.IsNullOrEmpty(pf)) continue;
59+
string foundation = Path.Combine(pf, "Blender Foundation");
60+
// Blender installs under a version subdir (Blender X.Y); enumerate them.
61+
try
62+
{
63+
if (Directory.Exists(foundation))
64+
foreach (string d in Directory.GetDirectories(foundation))
65+
list.Add(Path.Combine(d, "blender.exe"));
66+
}
67+
catch { /* unreadable dir; ignore */ }
68+
}
69+
break;
70+
case RuntimePlatform.LinuxEditor:
71+
list.Add("/usr/bin/blender");
72+
list.Add("/usr/local/bin/blender");
73+
list.Add("/snap/bin/blender");
74+
list.Add("/var/lib/flatpak/exports/bin/org.blender.Blender");
75+
break;
76+
}
77+
return list;
78+
}
79+
}
80+
}

MCPForUnity/Editor/Helpers/BlenderDetection.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

MCPForUnity/Editor/Security.meta

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)