diff --git a/Editor/Commands/CommandRegistry.cs b/Editor/Commands/CommandRegistry.cs index 19983864..ba817a37 100644 --- a/Editor/Commands/CommandRegistry.cs +++ b/Editor/Commands/CommandRegistry.cs @@ -35,7 +35,13 @@ public static class CommandRegistry { "GET_SELECTED_OBJECT", _ => ObjectCommandHandler.GetSelectedObject() }, // Editor control commands - { "EDITOR_CONTROL", parameters => EditorControlHandler.HandleEditorControl(parameters) } + { "EDITOR_CONTROL", parameters => EditorControlHandler.HandleEditorControl(parameters) }, + + // Hyper3D Rodin commands + { "GET_HYPER3D_STATUS", _ => Hyper3DRodin.Hyper3DRodinCommandHandler.GetHyper3DStatus() }, + { "CREATE_RODIN_JOB", parameters => Hyper3DRodin.Hyper3DRodinCommandHandler.CreateRodinJob(parameters) }, + { "POLL_RODIN_JOB_STATUS", parameters => Hyper3DRodin.Hyper3DRodinCommandHandler.PollRodinJobStatus(parameters) }, + { "DOWNLOAD_RODIN_JOB_RESULT", parameters => Hyper3DRodin.Hyper3DRodinCommandHandler.DownloadRodinJobResult(parameters) }, }; /// diff --git a/Editor/Commands/CommandRegistry.cs.meta b/Editor/Commands/CommandRegistry.cs.meta index 55b68298..15ec884b 100644 --- a/Editor/Commands/CommandRegistry.cs.meta +++ b/Editor/Commands/CommandRegistry.cs.meta @@ -1,2 +1,11 @@ fileFormatVersion: 2 -guid: 5b61b5a84813b5749a5c64422694a0fa \ No newline at end of file +guid: 5b61b5a84813b5749a5c64422694a0fa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Commands/Hyper3DRodinCommandHandler.cs b/Editor/Commands/Hyper3DRodinCommandHandler.cs new file mode 100644 index 00000000..bab7ee62 --- /dev/null +++ b/Editor/Commands/Hyper3DRodinCommandHandler.cs @@ -0,0 +1,359 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text; +using GluonGui.Dialog; +using MCPServer.Editor.Commands; +using Newtonsoft.Json.Linq; +using UnityEditor; +using UnityEngine; +using UnityEngine.Rendering; +using UnityEngine.Rendering.Universal; + +#if UNITY_EDITOR +namespace Hyper3DRodin{ + public static class Hyper3DRodinCommandHandler + { + public static object GetHyper3DStatus() + { + // Access settings globally + bool enabled = SettingsManager.enabled; + string apiKey = SettingsManager.apiKey; + ServiceProvider mode = SettingsManager.serviceProvider; + + if (enabled) + { + if (string.IsNullOrEmpty(apiKey)) + { + return new + { + enabled = false, + message = @"Hyper3D Rodin integration is currently enabled, but no API key is provided. To enable it: +1. Open Unity's menu and go to **Window > Unity MCP > Hyper3D Rodin**. +2. Ensure the **'Enable Hyper3D Rodin Service'** checkbox is checked. +3. Select the appropriate **service provider**. +4. Enter your **API Key** in the provided input field. +5. Restart the connection for changes to take effect." + }; + } + + string keyType = apiKey == Constants.GetFreeTrialKey() ? "free_trial" : "private"; + string message = $"Hyper3D Rodin integration is enabled and ready to use. Provider: {mode}. " + + $"Key type: {keyType}"; + + return new + { + enabled = true, + message = message + }; + } + else + { + return new + { + enabled = false, + message = @"Hyper3D Rodin integration is currently disabled. To enable it: +1. Open Unity's menu and go to **Window > Unity MCP > Hyper3D Rodin**. +2. Check the **'Enable Hyper3D Rodin Service'** option. +3. Restart the connection for changes to take effect." + }; + } + } + + public static object CreateRodinJob(JObject @params) + { + switch (SettingsManager.serviceProvider) + { + case ServiceProvider.MAIN_SITE: + return CreateRodinJobMainSite(@params); + case ServiceProvider.FAL_AI: + return CreateRodinJobFalAi(@params); + default: + return new { error = "Error: Unknown Hyper3D Rodin mode!" }; + } + } + + private static object CreateRodinJobMainSite(JObject @params) + { + HttpClient client = new HttpClient(); + try + { + var formData = new MultipartFormDataContent(); + if (@params["images"] is JArray imagesArray) + { + int i = 0; + foreach (var img in imagesArray) + { + string imgSuffix = img["suffix"]?.ToString(); + string imgPath = img["path"]?.ToString(); + if (!string.IsNullOrEmpty(imgPath) && File.Exists(imgPath)) + { + formData.Add(new ByteArrayContent(File.ReadAllBytes(imgPath)), "images", $"{i:D4}{imgSuffix}"); + i++; + } + } + } + + formData.Add(new StringContent("Sketch"), "tier"); + formData.Add(new StringContent("Raw"), "mesh_mode"); + + + if (@params["text_prompt"] != null) + formData.Add(new StringContent(@params["text_prompt"].ToString()), "prompt"); + + if (@params["bbox_condition"] != null) + formData.Add(new StringContent(@params["bbox_condition"].ToString()), "bbox_condition"); + + var request = new HttpRequestMessage(HttpMethod.Post, "https://hyperhuman.deemos.com/api/v2/rodin") + { + Headers = { { "Authorization", $"Bearer {SettingsManager.apiKey}" } }, + Content = formData + }; + + HttpResponseMessage response = client.SendAsync(request).Result; + string responseBody = response.Content.ReadAsStringAsync().Result; + return JObject.Parse(responseBody); + } + catch (Exception e) + { + return new JObject { ["error"] = e.Message }; + } + } + + private static object CreateRodinJobFalAi(JObject @params) + { + HttpClient client = new HttpClient(); + try + { + var requestData = new JObject + { + ["tier"] = "Sketch", + }; + + if (@params["images"] is JArray imagesArray) + requestData["input_image_urls"] = imagesArray; + + if (@params["text_prompt"] != null) + requestData["prompt"] = @params["text_prompt"].ToString(); + + if (@params["bbox_condition"] != null) + requestData["bbox_condition"] = @params["bbox_condition"]; + + var request = new HttpRequestMessage(HttpMethod.Post, "https://queue.fal.run/fal-ai/hyper3d/rodin") + { + Headers = { { "Authorization", $"Key {SettingsManager.apiKey}" } }, + Content = new StringContent(requestData.ToString(), Encoding.UTF8, "application/json") + }; + + HttpResponseMessage response = client.SendAsync(request).Result; + string responseBody = response.Content.ReadAsStringAsync().Result; + return JObject.Parse(responseBody); + } + catch (Exception e) + { + return new JObject { ["error"] = e.Message }; + } + } + + public static object PollRodinJobStatus(JObject @params) + { + switch (SettingsManager.serviceProvider) + { + case ServiceProvider.MAIN_SITE: + return PollRodinJobStatusMainSite(@params); + case ServiceProvider.FAL_AI: + return PollRodinJobStatusFalAi(@params); + default: + return new JObject { ["error"] = "Error: Unknown Hyper3D Rodin mode!" }; + } + } + + private static object PollRodinJobStatusMainSite(JObject @params) + { + HttpClient client = new HttpClient(); + try + { + var requestData = new JObject + { + ["subscription_key"] = @params["subscription_key"] + }; + + var request = new HttpRequestMessage(HttpMethod.Post, "https://hyperhuman.deemos.com/api/v2/status") + { + Headers = { { "Authorization", $"Bearer {SettingsManager.apiKey}" } }, + Content = new StringContent(requestData.ToString(), Encoding.UTF8, "application/json"), + }; + + HttpResponseMessage response = client.SendAsync(request).Result; + string responseBody = response.Content.ReadAsStringAsync().Result; + return JObject.Parse(responseBody); + } + catch (Exception e) + { + return new JObject { ["error"] = e.Message }; + } + } + + private static object PollRodinJobStatusFalAi(JObject @params) + { + HttpClient client = new HttpClient(); + try + { + string requestId = @params["request_id"]?.ToString(); + if (string.IsNullOrEmpty(requestId)) + return new JObject { ["error"] = "Invalid request ID" }; + + var request = new HttpRequestMessage(HttpMethod.Get, $"https://queue.fal.run/fal-ai/hyper3d/requests/{requestId}/status") + { + Headers = { { "Authorization", $"Key {SettingsManager.apiKey}" } } + }; + + HttpResponseMessage response = client.SendAsync(request).Result; + string responseBody = response.Content.ReadAsStringAsync().Result; + return JObject.Parse(responseBody); + } + catch (Exception e) + { + return new JObject { ["error"] = e.Message }; + } + } + + public static object DownloadRodinJobResult(JObject @params) + { + switch (SettingsManager.serviceProvider) + { + case ServiceProvider.MAIN_SITE: + return DownloadRodinJobResultMainSite(@params); + case ServiceProvider.FAL_AI: + return DownloadRodinJobResultFalAi(@params); + default: + return new JObject { ["error"] = "Error: Unknown Hyper3D Rodin mode!" }; + } + } + + private static object DownloadRodinJobResultMainSite(JObject @params) + { + HttpClient client = new HttpClient(); + try + { + // Extract parameters + string taskUuid = @params["task_uuid"]?.ToString(); + string savePath = @params["path"]?.ToString(); + + if (string.IsNullOrEmpty(taskUuid) || string.IsNullOrEmpty(savePath)) + return new JObject { ["error"] = "Missing required parameters: task_uuid or path" }; + + // Prepare API request + var request = new HttpRequestMessage(HttpMethod.Post, "https://hyperhuman.deemos.com/api/v2/download") + { + Headers = { { "Authorization", $"Bearer {SettingsManager.apiKey}" } }, + Content = new StringContent(new JObject { ["task_uuid"] = taskUuid }.ToString(), Encoding.UTF8, "application/json") + }; + + // Send request + HttpResponseMessage response = client.SendAsync(request).Result; + string responseBody = response.Content.ReadAsStringAsync().Result; + JObject data = JObject.Parse(responseBody); + + // Find GLB file URL + foreach (var item in data["list"]) + { + if (item["name"].ToString().EndsWith(".glb")) + { + JObject @result = JObject.FromObject( + DownloadFile(item["url"].ToString(), savePath + "/" + item["name"]) + ); + if (@result["error"] != null){ + return result; + } + } + } + + return new JObject { ["succeed"] = true }; + } + catch (Exception e) + { + return new JObject { ["error"] = e.Message }; + } + } + + private static object DownloadRodinJobResultFalAi(JObject @params) + { + HttpClient client = new HttpClient(); + try + { + // Extract parameters + string requestId = @params["request_id"]?.ToString(); + string savePath = @params["path"]?.ToString(); + + if (string.IsNullOrEmpty(requestId) || string.IsNullOrEmpty(savePath)) + return new JObject { ["error"] = "Missing required parameters: request_id or path" }; + + // Prepare API request + var request = new HttpRequestMessage(HttpMethod.Get, $"https://queue.fal.run/fal-ai/hyper3d/requests/{requestId}") + { + Headers = { { "Authorization", $"Key {SettingsManager.apiKey}" } } + }; + + // Send request + HttpResponseMessage response = client.SendAsync(request).Result; + string responseBody = response.Content.ReadAsStringAsync().Result; + JObject data = JObject.Parse(responseBody); + + // Find GLB file URL + string fileUrl = data["model_mesh"]?["url"]?.ToString(); + if (string.IsNullOrEmpty(fileUrl)) + return new JObject { ["error"] = "No .glb file found in response" }; + + return DownloadFile(fileUrl, savePath); + } + catch (Exception e) + { + return new JObject { ["error"] = e.Message }; + } + } + + private static object DownloadFile(string fileUrl, string filePath) + { + HttpClient client = new HttpClient(); + try + { + // Ensure filePath starts with "Assets/" + if (!filePath.StartsWith("Assets/")) + return new JObject { ["error"] = "Invalid file path. Path must start with 'Assets/'" }; + + // Convert Unity-relative path to absolute system path + string absolutePath = Path.Combine(Application.dataPath, filePath.Substring(7)); // Remove "Assets/" prefix + + // Ensure directory exists + string directory = Path.GetDirectoryName(absolutePath); + if (!Directory.Exists(directory)) + Directory.CreateDirectory(directory); + + // Prepare download request + var request = new HttpRequestMessage(HttpMethod.Get, fileUrl); + HttpResponseMessage response = client.SendAsync(request).Result; + + if (!response.IsSuccessStatusCode) + return new JObject { ["error"] = $"Failed to download file. HTTP Status: {response.StatusCode}" }; + + // Save file to path + using (var fs = new FileStream(absolutePath, FileMode.Create, FileAccess.Write)) + { + response.Content.CopyToAsync(fs).Wait(); + } + + // Return Unity-relative path + return new JObject { ["succeed"] = true, ["path"] = filePath }; + } + catch (Exception e) + { + return new JObject { ["error"] = e.Message }; + } + } + } +} +#endif \ No newline at end of file diff --git a/Editor/Commands/Hyper3DRodinCommandHandler.cs.meta b/Editor/Commands/Hyper3DRodinCommandHandler.cs.meta new file mode 100644 index 00000000..4068cf7c --- /dev/null +++ b/Editor/Commands/Hyper3DRodinCommandHandler.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4928373c2d8454a22941d76a62329c3b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/EditorWindows/Hyper3DEditorWindow.cs b/Editor/EditorWindows/Hyper3DEditorWindow.cs new file mode 100644 index 00000000..4720faa3 --- /dev/null +++ b/Editor/EditorWindows/Hyper3DEditorWindow.cs @@ -0,0 +1,108 @@ +using UnityEngine; +using UnityEditor; +using System.Collections.Generic; +using System; +using System.Runtime.CompilerServices; +using System.ComponentModel; +using Newtonsoft.Json.Linq; + +#if UNITY_EDITOR +namespace Hyper3DRodin{ + // Enum definition + public enum ServiceProvider + { + MAIN_SITE, + FAL_AI + } + + static class Constants + { + private const string FREE_TRIAL_KEY = "k9TcfFoEhNd9cCPP2guHAHHHkctZHIRhZDywZ1euGUXwihbYLpOjQhofby80NJez"; + + public static string GetFreeTrialKey() + { + return FREE_TRIAL_KEY; + } + } + + [Serializable] + public class Settings + { + public bool enabled = false; + public string apiKey = ""; + + // Enum for service status + public ServiceProvider serviceProvider = ServiceProvider.MAIN_SITE; + } + + public static class SettingsManager + { + private const string EnableServiceKey = "UnityMCP.Hyper3D.EnableService"; + private const string ApiKeyKey = "UnityMCP.Hyper3D.ApiKey"; + private const string ServiceProviderKey = "UnityMCP.Hyper3D.ServiceProvider"; + + public static bool enabled + { + get => EditorPrefs.GetBool(EnableServiceKey, false); + set => EditorPrefs.SetBool(EnableServiceKey, value); + } + + public static string apiKey + { + get => EditorPrefs.GetString(ApiKeyKey, ""); + set => EditorPrefs.SetString(ApiKeyKey, value); + } + + public static ServiceProvider serviceProvider + { + get => (ServiceProvider)EditorPrefs.GetInt(ServiceProviderKey, (int)ServiceProvider.MAIN_SITE); + set => EditorPrefs.SetInt(ServiceProviderKey, (int)value); + } + } + + public class SettingsEditorWindow : EditorWindow + { + [MenuItem("Window/Unity MCP Modules/Hyper3D Rodin")] + public static void ShowWindow() + { + GetWindow("Hyper3D Rodin Settings"); + } + + private void OnGUI() + { + SettingsManager.enabled = EditorGUILayout.Toggle("Enable Hyper3D Rodin Service", SettingsManager.enabled); + SettingsManager.apiKey = EditorGUILayout.PasswordField("API Key", SettingsManager.apiKey); + // "Set Free Trial Key" button + if (GUILayout.Button("Set Free Trial Key")) + { + SettingsManager.apiKey = Constants.GetFreeTrialKey(); + SettingsManager.serviceProvider = ServiceProvider.MAIN_SITE; + } + + // Custom Enum Popup with Friendly Names + SettingsManager.serviceProvider = (ServiceProvider)EditorGUILayout.Popup( + "Service Provider", + (int)SettingsManager.serviceProvider, + GetEnumDisplayNames() + ); + } + + private string[] GetEnumDisplayNames() + { + Dictionary enumDisplayNames = new Dictionary + { + { ServiceProvider.MAIN_SITE, "hyper3d.ai" }, + { ServiceProvider.FAL_AI, "fal.ai" }, + }; + + string[] displayNames = new string[enumDisplayNames.Count]; + int i = 0; + foreach (var value in Enum.GetValues(typeof(ServiceProvider))) + { + displayNames[i++] = enumDisplayNames[(ServiceProvider)value]; + } + return displayNames; + } + } +} +#endif diff --git a/Editor/EditorWindows/Hyper3DEditorWindow.cs.meta b/Editor/EditorWindows/Hyper3DEditorWindow.cs.meta new file mode 100644 index 00000000..982743dd --- /dev/null +++ b/Editor/EditorWindows/Hyper3DEditorWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d0389c6ede7d34477a65103dbb5b3230 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/UnityMCPBridge.cs b/Editor/UnityMCPBridge.cs index 4b36e819..be615f16 100644 --- a/Editor/UnityMCPBridge.cs +++ b/Editor/UnityMCPBridge.cs @@ -306,6 +306,10 @@ private static string ExecuteCommand(Command command) "APPLY_PREFAB" => AssetCommandHandler.ApplyPrefab(command.@params), "GET_ASSET_LIST" => AssetCommandHandler.GetAssetList(command.@params), "EDITOR_CONTROL" => EditorControlHandler.HandleEditorControl(command.@params), + "GET_HYPER3D_STATUS" => Hyper3DRodin.Hyper3DRodinCommandHandler.GetHyper3DStatus(), + "CREATE_RODIN_JOB" => Hyper3DRodin.Hyper3DRodinCommandHandler.CreateRodinJob(command.@params), + "POLL_RODIN_JOB_STATUS" => Hyper3DRodin.Hyper3DRodinCommandHandler.PollRodinJobStatus(command.@params), + "DOWNLOAD_RODIN_JOB_RESULT" => Hyper3DRodin.Hyper3DRodinCommandHandler.DownloadRodinJobResult(command.@params), _ => throw new Exception($"Unknown command type: {command.type}") }; diff --git a/Editor/UnityMCPBridge.cs.meta b/Editor/UnityMCPBridge.cs.meta index 39156984..dcaa7616 100644 --- a/Editor/UnityMCPBridge.cs.meta +++ b/Editor/UnityMCPBridge.cs.meta @@ -1,2 +1,11 @@ fileFormatVersion: 2 -guid: 1e0fb0e418dd19345a8236c44078972b \ No newline at end of file +guid: 1e0fb0e418dd19345a8236c44078972b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Python/server.py b/Python/server.py index 17d3b4e5..37ae5fce 100644 --- a/Python/server.py +++ b/Python/server.py @@ -100,6 +100,28 @@ def asset_creation_strategy() -> str: " - Regularly apply prefab changes\n" " - Monitor console logs for errors and warnings\n" " - Use search terms to filter console output when debugging\n" + "\n" + "8. Hyper3D Rodin Tools\n" + "Hyper3D Rodin is an online platform that can generate 3D assets. It is good at generating 3D models for single item.\n" + "But don't try to:\n" + "1. Generate the whole scene with one shot\n" + "2. Generate ground using Rodin\n" + "3. Generate parts of the items separately and put them together afterwards\n" + "If you want to generate assets using Hyper3D Rodin, follow the following steps:\n" + "0. Check rodin status using get_hyper3d_status(), only use Rodin if it is enabled and available\n" + "1. Create the model generation task\n" + " - Use generate_hyper3d_model_via_images() if image(s) is/are given\n" + " - Use generate_hyper3d_model_via_text() if generating 3D asset using text prompt\n" + " If key type is free_trial and insufficient balance error returned, tell the user that the free trial key can only generated limited models everyday, they can choose to:\n" + " - Wait for another day and try again\n" + " - Go to hyper3d.ai to find out how to get their own API key\n" + " - Go to fal.ai to get their own private API key\n" + "2. Poll the status\n" + " - Use poll_rodin_job_status() to check if the generation task has completed or failed\n" + "3. Download the asset\n" + " - Use download_generated_asset() to download the generated GLB model to the path given\n" + "4. Import the asset into scene\n" + " - Use other tools to handle the rest of the things.\n" ) # Run the server diff --git a/Python/tools/__init__.py b/Python/tools/__init__.py index a7787e0e..e326e031 100644 --- a/Python/tools/__init__.py +++ b/Python/tools/__init__.py @@ -4,6 +4,7 @@ from .editor_tools import register_editor_tools from .asset_tools import register_asset_tools from .object_tools import register_object_tools +from .hyper3d_rodin_tools import register_hyper3d_tools def register_all_tools(mcp): """Register all tools with the MCP server.""" @@ -12,4 +13,5 @@ def register_all_tools(mcp): register_material_tools(mcp) register_editor_tools(mcp) register_asset_tools(mcp) - register_object_tools(mcp) \ No newline at end of file + register_object_tools(mcp) + register_hyper3d_tools(mcp) diff --git a/Python/tools/asset_tools.py b/Python/tools/asset_tools.py index 1a5bdbf3..139b4bd4 100644 --- a/Python/tools/asset_tools.py +++ b/Python/tools/asset_tools.py @@ -70,18 +70,23 @@ def import_asset( def instantiate_prefab( ctx: Context, prefab_path: str, + prefab_suffix: str = ".prefab", position_x: float = 0.0, position_y: float = 0.0, position_z: float = 0.0, rotation_x: float = 0.0, rotation_y: float = 0.0, - rotation_z: float = 0.0 + rotation_z: float = 0.0, ) -> str: """Instantiate a prefab into the current scene at a specified location. Args: ctx: The MCP context prefab_path: Path to the prefab asset (relative to Assets folder) + prefab_suffix: The suffix of the prefab file.Must to be included if + trying to instantiate something other than ".prefab" + are trying to get instantiated, like a model file + (".fbx", ".gltf" and others). (default: ".prefab") position_x: X position in world space (default: 0.0) position_y: Y position in world space (default: 0.0) position_z: Z position in world space (default: 0.0) @@ -94,6 +99,7 @@ def instantiate_prefab( """ try: unity = get_unity_connection() + prefab_suffix = prefab_suffix.lower() # Parameter validation if not prefab_path or not isinstance(prefab_path, str): @@ -118,13 +124,13 @@ def instantiate_prefab( prefab_name = prefab_path.split('/')[-1] # Ensure prefab has .prefab extension for searching - if not prefab_name.lower().endswith('.prefab'): - prefab_name = f"{prefab_name}.prefab" - prefab_path = f"{prefab_path}.prefab" + if not prefab_name.lower().endswith(prefab_suffix): + prefab_name = f"{prefab_name}{prefab_suffix}" + prefab_path = f"{prefab_path}{prefab_suffix}" prefab_assets = unity.send_command("GET_ASSET_LIST", { - "type": "Prefab", - "search_pattern": prefab_name, + "type": "Prefab" if prefab_suffix == ".prefab" else "GameObject", + "search_pattern": prefab_name.removesuffix(prefab_suffix), "folder": prefab_dir }).get("assets", []) diff --git a/Python/tools/hyper3d_rodin_tools.py b/Python/tools/hyper3d_rodin_tools.py new file mode 100644 index 00000000..a83eb0f6 --- /dev/null +++ b/Python/tools/hyper3d_rodin_tools.py @@ -0,0 +1,193 @@ +"""Tools for gererating assets from Hyper3D.""" + +from typing import Optional, List, Dict, Any +from mcp.server.fastmcp import FastMCP, Context +from unity_connection import get_unity_connection +import json + +def register_hyper3d_tools(mcp: FastMCP): + @mcp.tool() + def get_hyper3d_status(ctx: Context) -> str: + """ + Check if Hyper3D Rodin integration is enabled in Unity. + Returns a message indicating whether Hyper3D Rodin features are available. + + Don't emphasize the key type in the returned message, but sliently remember it. + """ + try: + unity = get_unity_connection() + result = unity.send_command("GET_HYPER3D_STATUS") + enabled = result.get("enabled", False) + message = result.get("message", "") + if enabled: + message += "" + return message + except Exception as e: + return f"Error checking Hyper3D status: {str(e)}" + + @mcp.tool() + def generate_hyper3d_model_via_text( + ctx: Context, + text_prompt: str, + bbox_condition: list[float]=None + ) -> str: + """ + Generate 3D asset using Hyper3D by giving description of the desired asset, and import the asset into Unity. + The 3D asset has built-in materials. + The generated model has a normalized size, so re-scaling after generation can be useful. + + Parameters: + - text_prompt: A short description of the desired model in **English**. + - bbox_condition: Optional. If given, it has to be a list of floats of length 3. Controls the ratio between [Length, Width, Height] of the model. The final size of the model is normalized. + + Returns a message indicating success or failure. + """ + try: + unity = get_unity_connection() + result = unity.send_command("CREATE_RODIN_JOB", { + "text_prompt": text_prompt, + "images": None, + "bbox_condition": bbox_condition, + }) + succeed = result.get("submit_time", False) + if succeed: + return json.dumps({ + "task_uuid": result["uuid"], + "subscription_key": result["jobs"]["subscription_key"], + }) + else: + return json.dumps(result) + except Exception as e: + return f"Error generating Hyper3D task: {str(e)}" + + @mcp.tool() + def generate_hyper3d_model_via_images( + ctx: Context, + input_image_paths: list[str]=None, + input_image_urls: list[str]=None, + bbox_condition: list[float]=None + ) -> str: + """ + Generate 3D asset using Hyper3D by giving images of the wanted asset, and import the generated asset into Unity. + The 3D asset has built-in materials. + The generated model has a normalized size, so re-scaling after generation can be useful. + + Parameters: + - input_image_paths: The **absolute** paths of input images. Even if only one image is provided, wrap it into a list. Required if Hyper3D Rodin using provider MAIN_SITE. + - input_image_urls: The URLs of input images. Even if only one image is provided, wrap it into a list. Required if Hyper3D Rodin using provider FAL_AI. + - bbox_condition: Optional. If given, it has to be a list of ints of length 3. Controls the ratio between [Length, Width, Height] of the model. The final size of the model is normalized. + + Only one of {input_image_paths, input_image_urls} should be given at a time, depending on the Hyper3D Rodin's current provider. + Returns a message indicating success or failure. + """ + if input_image_paths is not None and input_image_urls is not None: + return f"Error: Conflict parameters given!" + if input_image_paths is None and input_image_urls is None: + return f"Error: No image given!" + if input_image_paths is not None: + if not all(os.path.exists(i) for i in input_image_paths): + return "Error: not all image paths are valid!" + images = [] + for path in input_image_paths: + with open(path, "rb") as f: + images.append( + (Path(path).suffix, base64.b64encode(f.read()).decode("ascii")) + ) + elif input_image_urls is not None: + if not all(urlparse(i) for i in input_image_paths): + return "Error: not all image URLs are valid!" + images = input_image_urls.copy() + try: + unity = get_unity_connection() + result = unity.send_command("CREATE_RODIN_JOB", { + "text_prompt": None, + "images": images, + "bbox_condition": bbox_condition, + }) + succeed = result.get("submit_time", False) + if succeed: + return json.dumps({ + "task_uuid": result["uuid"], + "subscription_key": result["jobs"]["subscription_key"], + }) + else: + return json.dumps(result) + except Exception as e: + logger.error(f"Error generating Hyper3D task: {str(e)}") + return f"Error generating Hyper3D task: {str(e)}" + + @mcp.tool() + def poll_rodin_job_status( + ctx: Context, + subscription_key: str=None, + request_id: str=None, + ): + """ + Check if the Hyper3D Rodin generation task is completed. + + For Hyper3D Rodin provider MAIN_SITE: + Parameters: + - subscription_key: The subscription_key given in the generate model step. + + Returns a list of status. The task is done if all status are "Done". + If "Failed" showed up, the generating process failed. + This is a polling API, so only proceed if the status are finally determined ("Done" or "Canceled"). + + For Hyper3D Rodin provider FAL_AI: + Parameters: + - request_id: The request_id given in the generate model step. + + Returns the generation task status. The task is done if status is "COMPLETED". + The task is in progress if status is "IN_PROGRESS". + If status other than "COMPLETED", "IN_PROGRESS", "IN_QUEUE" showed up, the generating process might be failed. + This is a polling API, so only proceed if the status are finally determined ("COMPLETED" or some failed state). + """ + try: + unity = get_unity_connection() + kwargs = {} + if subscription_key: + kwargs = { + "subscription_key": subscription_key, + } + elif request_id: + kwargs = { + "request_id": request_id, + } + result = unity.send_command("POLL_RODIN_JOB_STATUS", kwargs) + return result + except Exception as e: + return f"Error generating Hyper3D task: {str(e)}" + + @mcp.tool() + def download_generated_asset( + ctx: Context, + path: str, + task_uuid: str=None, + request_id: str=None, + ): + """ + Download the assets generated by Hyper3D Rodin after the generation task is completed. + + Parameters: + - path: The path to download the asset. Starts with "Assets/". + - task_uuid: For Hyper3D Rodin provider MAIN_SITE: The task_uuid given in the generate model step. + - request_id: For Hyper3D Rodin provider FAL_AI: The request_id given in the generate model step. + + Only give one of {task_uuid, request_id} based on the Hyper3D Rodin Mode! + Return if the asset has been downloaded to the given path successfully. + """ + try: + unity = get_unity_connection() + kwargs = { + "path": path + } + if not path.startswith("Assets/"): + return "Error with path: not starting with Assets/" + if task_uuid: + kwargs["task_uuid"] = task_uuid + elif request_id: + kwargs["request_id"] = request_id + result = unity.send_command("DOWNLOAD_RODIN_JOB_RESULT", kwargs) + return result + except Exception as e: + return f"Error generating Hyper3D task: {str(e)}" diff --git a/Python/tools/hyper3d_rodin_tools.py.meta b/Python/tools/hyper3d_rodin_tools.py.meta new file mode 100644 index 00000000..46649a3a --- /dev/null +++ b/Python/tools/hyper3d_rodin_tools.py.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1293af8d6f652484ba0cc10a759d60dc +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/package.json b/package.json index 58e19796..d585b640 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "unity": "2020.3", "dependencies": { "com.unity.nuget.newtonsoft-json": "3.0.2", - "com.unity.render-pipelines.universal": "12.1.7" + "com.unity.render-pipelines.universal": "12.1.7", + "com.unity.cloud.gltfast": "6.10.3" } }