Skip to content

Commit 8864e57

Browse files
committed
Feature added: Hyper3D Rodin support
1 parent f51e574 commit 8864e57

13 files changed

+750
-6
lines changed

Editor/Commands/CommandRegistry.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,13 @@ public static class CommandRegistry
3535
{ "GET_SELECTED_OBJECT", _ => ObjectCommandHandler.GetSelectedObject() },
3636

3737
// Editor control commands
38-
{ "EDITOR_CONTROL", parameters => EditorControlHandler.HandleEditorControl(parameters) }
38+
{ "EDITOR_CONTROL", parameters => EditorControlHandler.HandleEditorControl(parameters) },
39+
40+
// Hyper3D Rodin commands
41+
{ "GET_HYPER3D_STATUS", _ => Hyper3DRodin.Hyper3DRodinCommandHandler.GetHyper3DStatus() },
42+
{ "CREATE_RODIN_JOB", parameters => Hyper3DRodin.Hyper3DRodinCommandHandler.CreateRodinJob(parameters) },
43+
{ "POLL_RODIN_JOB_STATUS", parameters => Hyper3DRodin.Hyper3DRodinCommandHandler.PollRodinJobStatus(parameters) },
44+
{ "DOWNLOAD_RODIN_JOB_RESULT", parameters => Hyper3DRodin.Hyper3DRodinCommandHandler.DownloadRodinJobResult(parameters) },
3945
};
4046

4147
/// <summary>

Editor/Commands/CommandRegistry.cs.meta

Lines changed: 10 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 359 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
using System.IO;
5+
using System.Linq;
6+
using System.Net.Http;
7+
using System.Text;
8+
using GluonGui.Dialog;
9+
using MCPServer.Editor.Commands;
10+
using Newtonsoft.Json.Linq;
11+
using UnityEditor;
12+
using UnityEngine;
13+
using UnityEngine.Rendering;
14+
using UnityEngine.Rendering.Universal;
15+
16+
#if UNITY_EDITOR
17+
namespace Hyper3DRodin{
18+
public static class Hyper3DRodinCommandHandler
19+
{
20+
public static object GetHyper3DStatus()
21+
{
22+
// Access settings globally
23+
bool enabled = SettingsManager.enabled;
24+
string apiKey = SettingsManager.apiKey;
25+
ServiceProvider mode = SettingsManager.serviceProvider;
26+
27+
if (enabled)
28+
{
29+
if (string.IsNullOrEmpty(apiKey))
30+
{
31+
return new
32+
{
33+
enabled = false,
34+
message = @"Hyper3D Rodin integration is currently enabled, but no API key is provided. To enable it:
35+
1. Open Unity's menu and go to **Window > Unity MCP > Hyper3D Rodin**.
36+
2. Ensure the **'Enable Hyper3D Rodin Service'** checkbox is checked.
37+
3. Select the appropriate **service provider**.
38+
4. Enter your **API Key** in the provided input field.
39+
5. Restart the connection for changes to take effect."
40+
};
41+
}
42+
43+
string keyType = apiKey == Constants.GetFreeTrialKey() ? "free_trial" : "private";
44+
string message = $"Hyper3D Rodin integration is enabled and ready to use. Provider: {mode}. " +
45+
$"Key type: {keyType}";
46+
47+
return new
48+
{
49+
enabled = true,
50+
message = message
51+
};
52+
}
53+
else
54+
{
55+
return new
56+
{
57+
enabled = false,
58+
message = @"Hyper3D Rodin integration is currently disabled. To enable it:
59+
1. Open Unity's menu and go to **Window > Unity MCP > Hyper3D Rodin**.
60+
2. Check the **'Enable Hyper3D Rodin Service'** option.
61+
3. Restart the connection for changes to take effect."
62+
};
63+
}
64+
}
65+
66+
public static object CreateRodinJob(JObject @params)
67+
{
68+
switch (SettingsManager.serviceProvider)
69+
{
70+
case ServiceProvider.MAIN_SITE:
71+
return CreateRodinJobMainSite(@params);
72+
case ServiceProvider.FAL_AI:
73+
return CreateRodinJobFalAi(@params);
74+
default:
75+
return new { error = "Error: Unknown Hyper3D Rodin mode!" };
76+
}
77+
}
78+
79+
private static object CreateRodinJobMainSite(JObject @params)
80+
{
81+
HttpClient client = new HttpClient();
82+
try
83+
{
84+
var formData = new MultipartFormDataContent();
85+
if (@params["images"] is JArray imagesArray)
86+
{
87+
int i = 0;
88+
foreach (var img in imagesArray)
89+
{
90+
string imgSuffix = img["suffix"]?.ToString();
91+
string imgPath = img["path"]?.ToString();
92+
if (!string.IsNullOrEmpty(imgPath) && File.Exists(imgPath))
93+
{
94+
formData.Add(new ByteArrayContent(File.ReadAllBytes(imgPath)), "images", $"{i:D4}{imgSuffix}");
95+
i++;
96+
}
97+
}
98+
}
99+
100+
formData.Add(new StringContent("Sketch"), "tier");
101+
formData.Add(new StringContent("Raw"), "mesh_mode");
102+
103+
104+
if (@params["text_prompt"] != null)
105+
formData.Add(new StringContent(@params["text_prompt"].ToString()), "prompt");
106+
107+
if (@params["bbox_condition"] != null)
108+
formData.Add(new StringContent(@params["bbox_condition"].ToString()), "bbox_condition");
109+
110+
var request = new HttpRequestMessage(HttpMethod.Post, "https://hyperhuman.deemos.com/api/v2/rodin")
111+
{
112+
Headers = { { "Authorization", $"Bearer {SettingsManager.apiKey}" } },
113+
Content = formData
114+
};
115+
116+
HttpResponseMessage response = client.SendAsync(request).Result;
117+
string responseBody = response.Content.ReadAsStringAsync().Result;
118+
return JObject.Parse(responseBody);
119+
}
120+
catch (Exception e)
121+
{
122+
return new JObject { ["error"] = e.Message };
123+
}
124+
}
125+
126+
private static object CreateRodinJobFalAi(JObject @params)
127+
{
128+
HttpClient client = new HttpClient();
129+
try
130+
{
131+
var requestData = new JObject
132+
{
133+
["tier"] = "Sketch",
134+
};
135+
136+
if (@params["images"] is JArray imagesArray)
137+
requestData["input_image_urls"] = imagesArray;
138+
139+
if (@params["text_prompt"] != null)
140+
requestData["prompt"] = @params["text_prompt"].ToString();
141+
142+
if (@params["bbox_condition"] != null)
143+
requestData["bbox_condition"] = @params["bbox_condition"];
144+
145+
var request = new HttpRequestMessage(HttpMethod.Post, "https://queue.fal.run/fal-ai/hyper3d/rodin")
146+
{
147+
Headers = { { "Authorization", $"Key {SettingsManager.apiKey}" } },
148+
Content = new StringContent(requestData.ToString(), Encoding.UTF8, "application/json")
149+
};
150+
151+
HttpResponseMessage response = client.SendAsync(request).Result;
152+
string responseBody = response.Content.ReadAsStringAsync().Result;
153+
return JObject.Parse(responseBody);
154+
}
155+
catch (Exception e)
156+
{
157+
return new JObject { ["error"] = e.Message };
158+
}
159+
}
160+
161+
public static object PollRodinJobStatus(JObject @params)
162+
{
163+
switch (SettingsManager.serviceProvider)
164+
{
165+
case ServiceProvider.MAIN_SITE:
166+
return PollRodinJobStatusMainSite(@params);
167+
case ServiceProvider.FAL_AI:
168+
return PollRodinJobStatusFalAi(@params);
169+
default:
170+
return new JObject { ["error"] = "Error: Unknown Hyper3D Rodin mode!" };
171+
}
172+
}
173+
174+
private static object PollRodinJobStatusMainSite(JObject @params)
175+
{
176+
HttpClient client = new HttpClient();
177+
try
178+
{
179+
var requestData = new JObject
180+
{
181+
["subscription_key"] = @params["subscription_key"]
182+
};
183+
184+
var request = new HttpRequestMessage(HttpMethod.Post, "https://hyperhuman.deemos.com/api/v2/status")
185+
{
186+
Headers = { { "Authorization", $"Bearer {SettingsManager.apiKey}" } },
187+
Content = new StringContent(requestData.ToString(), Encoding.UTF8, "application/json"),
188+
};
189+
190+
HttpResponseMessage response = client.SendAsync(request).Result;
191+
string responseBody = response.Content.ReadAsStringAsync().Result;
192+
return JObject.Parse(responseBody);
193+
}
194+
catch (Exception e)
195+
{
196+
return new JObject { ["error"] = e.Message };
197+
}
198+
}
199+
200+
private static object PollRodinJobStatusFalAi(JObject @params)
201+
{
202+
HttpClient client = new HttpClient();
203+
try
204+
{
205+
string requestId = @params["request_id"]?.ToString();
206+
if (string.IsNullOrEmpty(requestId))
207+
return new JObject { ["error"] = "Invalid request ID" };
208+
209+
var request = new HttpRequestMessage(HttpMethod.Get, $"https://queue.fal.run/fal-ai/hyper3d/requests/{requestId}/status")
210+
{
211+
Headers = { { "Authorization", $"Key {SettingsManager.apiKey}" } }
212+
};
213+
214+
HttpResponseMessage response = client.SendAsync(request).Result;
215+
string responseBody = response.Content.ReadAsStringAsync().Result;
216+
return JObject.Parse(responseBody);
217+
}
218+
catch (Exception e)
219+
{
220+
return new JObject { ["error"] = e.Message };
221+
}
222+
}
223+
224+
public static object DownloadRodinJobResult(JObject @params)
225+
{
226+
switch (SettingsManager.serviceProvider)
227+
{
228+
case ServiceProvider.MAIN_SITE:
229+
return DownloadRodinJobResultMainSite(@params);
230+
case ServiceProvider.FAL_AI:
231+
return DownloadRodinJobResultFalAi(@params);
232+
default:
233+
return new JObject { ["error"] = "Error: Unknown Hyper3D Rodin mode!" };
234+
}
235+
}
236+
237+
private static object DownloadRodinJobResultMainSite(JObject @params)
238+
{
239+
HttpClient client = new HttpClient();
240+
try
241+
{
242+
// Extract parameters
243+
string taskUuid = @params["task_uuid"]?.ToString();
244+
string savePath = @params["path"]?.ToString();
245+
246+
if (string.IsNullOrEmpty(taskUuid) || string.IsNullOrEmpty(savePath))
247+
return new JObject { ["error"] = "Missing required parameters: task_uuid or path" };
248+
249+
// Prepare API request
250+
var request = new HttpRequestMessage(HttpMethod.Post, "https://hyperhuman.deemos.com/api/v2/download")
251+
{
252+
Headers = { { "Authorization", $"Bearer {SettingsManager.apiKey}" } },
253+
Content = new StringContent(new JObject { ["task_uuid"] = taskUuid }.ToString(), Encoding.UTF8, "application/json")
254+
};
255+
256+
// Send request
257+
HttpResponseMessage response = client.SendAsync(request).Result;
258+
string responseBody = response.Content.ReadAsStringAsync().Result;
259+
JObject data = JObject.Parse(responseBody);
260+
261+
// Find GLB file URL
262+
foreach (var item in data["list"])
263+
{
264+
if (item["name"].ToString().EndsWith(".glb"))
265+
{
266+
JObject @result = JObject.FromObject(
267+
DownloadFile(item["url"].ToString(), savePath + "/" + item["name"])
268+
);
269+
if (@result["error"] != null){
270+
return result;
271+
}
272+
}
273+
}
274+
275+
return new JObject { ["succeed"] = true };
276+
}
277+
catch (Exception e)
278+
{
279+
return new JObject { ["error"] = e.Message };
280+
}
281+
}
282+
283+
private static object DownloadRodinJobResultFalAi(JObject @params)
284+
{
285+
HttpClient client = new HttpClient();
286+
try
287+
{
288+
// Extract parameters
289+
string requestId = @params["request_id"]?.ToString();
290+
string savePath = @params["path"]?.ToString();
291+
292+
if (string.IsNullOrEmpty(requestId) || string.IsNullOrEmpty(savePath))
293+
return new JObject { ["error"] = "Missing required parameters: request_id or path" };
294+
295+
// Prepare API request
296+
var request = new HttpRequestMessage(HttpMethod.Get, $"https://queue.fal.run/fal-ai/hyper3d/requests/{requestId}")
297+
{
298+
Headers = { { "Authorization", $"Key {SettingsManager.apiKey}" } }
299+
};
300+
301+
// Send request
302+
HttpResponseMessage response = client.SendAsync(request).Result;
303+
string responseBody = response.Content.ReadAsStringAsync().Result;
304+
JObject data = JObject.Parse(responseBody);
305+
306+
// Find GLB file URL
307+
string fileUrl = data["model_mesh"]?["url"]?.ToString();
308+
if (string.IsNullOrEmpty(fileUrl))
309+
return new JObject { ["error"] = "No .glb file found in response" };
310+
311+
return DownloadFile(fileUrl, savePath);
312+
}
313+
catch (Exception e)
314+
{
315+
return new JObject { ["error"] = e.Message };
316+
}
317+
}
318+
319+
private static object DownloadFile(string fileUrl, string filePath)
320+
{
321+
HttpClient client = new HttpClient();
322+
try
323+
{
324+
// Ensure filePath starts with "Assets/"
325+
if (!filePath.StartsWith("Assets/"))
326+
return new JObject { ["error"] = "Invalid file path. Path must start with 'Assets/'" };
327+
328+
// Convert Unity-relative path to absolute system path
329+
string absolutePath = Path.Combine(Application.dataPath, filePath.Substring(7)); // Remove "Assets/" prefix
330+
331+
// Ensure directory exists
332+
string directory = Path.GetDirectoryName(absolutePath);
333+
if (!Directory.Exists(directory))
334+
Directory.CreateDirectory(directory);
335+
336+
// Prepare download request
337+
var request = new HttpRequestMessage(HttpMethod.Get, fileUrl);
338+
HttpResponseMessage response = client.SendAsync(request).Result;
339+
340+
if (!response.IsSuccessStatusCode)
341+
return new JObject { ["error"] = $"Failed to download file. HTTP Status: {response.StatusCode}" };
342+
343+
// Save file to path
344+
using (var fs = new FileStream(absolutePath, FileMode.Create, FileAccess.Write))
345+
{
346+
response.Content.CopyToAsync(fs).Wait();
347+
}
348+
349+
// Return Unity-relative path
350+
return new JObject { ["succeed"] = true, ["path"] = filePath };
351+
}
352+
catch (Exception e)
353+
{
354+
return new JObject { ["error"] = e.Message };
355+
}
356+
}
357+
}
358+
}
359+
#endif

0 commit comments

Comments
 (0)