| | // first run the launcher and texturing mode enabled. | | | // https://github.com/YanWenKun/Hunyuan3D-2-WinPortable | | | using System; | | | using System.Collections.Generic; | | | using System.Globalization; | | | using System.IO; | | | using System.Text; | | | using System.Text.RegularExpressions; | | | using System.Threading.Tasks; | | | using UnityEditor; | | | using UnityEngine; | | | using UnityEngine.Networking; | | | namespace UnityLibrary.AI | | | { | | | public class Hunyuan3DEditor : EditorWindow | | | { | | | [SerializeField] private string serverUrl = "http://127.0.0.1:8080"; | | | [SerializeField] private string outputFolder = "Assets/Generated/Hunyuan3D"; | | | [TextArea(3, 8)] | | | [SerializeField] private string prompt = "A wooden treasure chest, game asset, white background"; |
| | [SerializeField] private bool generateTexture = true; |
| | [SerializeField] private bool removeBackground = true; |
| | [SerializeField] private bool randomizeSeed = true; |
| | [SerializeField] private int seed = 1234; |
| | [SerializeField] private int inferenceSteps = 5; |
| | [SerializeField] private int octreeResolution = 256; |
| | [SerializeField] private int numChunks = 8000; |
| | [SerializeField] private float guidanceScale = 5f; |
| | private bool isGenerating; | | | private string status = "Ready"; |
| | [MenuItem("Tools/Hunyuan3D/Text to 3D")] |
| | public static void ShowWindow() |
| | { |
| | Hunyuan3DEditor window = GetWindow<Hunyuan3DEditor>(); |
| | window.titleContent = new GUIContent("Hunyuan3D"); |
| | window.minSize = new Vector2(460f, 520f); |
| | } | | | private void OnGUI() | | | { | | | EditorGUILayout.LabelField("Hunyuan3D Text to 3D", EditorStyles.boldLabel); |
| | EditorGUILayout.Space(); |
| | serverUrl = EditorGUILayout.TextField("Server URL", serverUrl); |
| | outputFolder = EditorGUILayout.TextField("Output Folder", outputFolder); |
| | EditorGUILayout.Space(); |
| | EditorGUILayout.LabelField("Prompt"); |
| | prompt = EditorGUILayout.TextArea(prompt, GUILayout.Height(80f)); |
| | EditorGUILayout.Space(); |
| | generateTexture = EditorGUILayout.Toggle("Generate Texture", generateTexture); | | | removeBackground = EditorGUILayout.Toggle("Remove Background", removeBackground); | | | randomizeSeed = EditorGUILayout.Toggle("Randomize Seed", randomizeSeed); |
| | EditorGUILayout.Space(); |
| | using (new EditorGUI.DisabledScope(randomizeSeed)) |
| | { | | | seed = EditorGUILayout.IntField("Seed", seed); | | | } | | | inferenceSteps = EditorGUILayout.IntSlider("Inference Steps", inferenceSteps, 1, 100); | | | octreeResolution = EditorGUILayout.IntSlider("Octree Resolution", octreeResolution, 16, 512); | | | numChunks = EditorGUILayout.IntField("Number Of Chunks", numChunks); | | | guidanceScale = EditorGUILayout.FloatField("Guidance Scale", guidanceScale); | | | EditorGUILayout.Space(); | | | EditorGUILayout.HelpBox(status, isGenerating ? MessageType.Info : MessageType.None); |
| | EditorGUILayout.Space(); |
| | using (new EditorGUI.DisabledScope(isGenerating \|\| string.IsNullOrWhiteSpace(prompt))) |
| | { | | | if (GUILayout.Button(generateTexture ? "Generate Textured Model" : "Generate Model", GUILayout.Height(40f))) | | | { | | | Generate(); | | | } | | | } | | | } | | | private async void Generate() | | | { | | | if (isGenerating) | | | return; | | | if (!outputFolder.StartsWith("Assets", StringComparison.OrdinalIgnoreCase)) | | | { | | | EditorUtility.DisplayDialog("Hunyuan3D", "Output folder must be inside the Unity Assets folder.", "OK"); | | | return; | | | } | | | isGenerating = true; | | | status = "Submitting generation request..."; | | | Repaint(); | | | try | | | { | | | string apiName = generateTexture ? "generation_all" : "shape_generation"; | | | string requestJson = BuildRequestJson(); | | | string eventId; | | | string apiPrefix; | | | try | | | { | | | apiPrefix = "/gradio_api/call/"; | | | eventId = await SubmitRequest(apiPrefix, apiName, requestJson); | | | } | | | catch (Exception firstException) | | | { | | | Debug.LogWarning("Hunyuan3D: /gradio_api/call failed, trying /call. " + firstException.Message); |
| | apiPrefix = "/call/"; |
| | eventId = await SubmitRequest(apiPrefix, apiName, requestJson); |
| | } |
| | status = "Generating model..."; |
| | Repaint(); |
| | string result = await WaitForResult(apiPrefix, apiName, eventId); |
| | status = "Finding generated GLB..."; |
| | Repaint(); |
| | string modelUrl = FindModelUrl(result, generateTexture); |
| | if (string.IsNullOrEmpty(modelUrl)) |
| | throw new Exception("Generation finished but no GLB download URL was found in the Gradio response.\n\nResponse:\n" + result); |
| | modelUrl = MakeAbsoluteUrl(modelUrl); |
| | status = "Down GLB..."; |
| | Repaint(); |
| | byte[] glbData = await DownloadFile(modelUrl); |
| | status = "Importing into Unity..."; |
| | Repaint(); |
| | string assetPath = SaveToAssets(glbData); |
| | AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.ForceUpdate); | | | AssetDatabase.Refresh(); | | | UnityEngine.Object importedObject = AssetDatabase.LoadMainAssetAtPath(assetPath); | | | if (importedObject != null) | | | { | | | Selection.activeObject = importedObject; | | | EditorGUIUtility.PingObject(importedObject); | | | } |
| | status = "Finished: " + assetPath; |
| | Debug.Log("Hunyuan3D model imported: " + assetPath); |
| | } | | | catch (Exception e) | | | { |
| | status = "Failed: " + e.Message; |
| | Debug.LogException(e); |
| | EditorUtility.DisplayDialog("Hunyuan3D generation failed", e.Message, "OK"); | | | } | | | finally | | | { |
| | isGenerating = false; |
| | Repaint(); |
| | } | | | } | | | private string BuildRequestJson() | | | { |
| | StringBuilder json = new StringBuilder(); |
| | json.Append("{\"data\":["); |
| | json.Append(JsonString(prompt)); |
| | json.Append(",null"); |
| | json.Append(",null"); |
| | json.Append(",null"); |
| | json.Append(",null"); |
| | json.Append(",null"); |
| | json.Append(","); |
| | json.Append(inferenceSteps); |
| | json.Append(","); |
| | json.Append(guidanceScale.ToString(CultureInfo.InvariantCulture)); |
| | json.Append(","); |
| | json.Append(seed); |
| | json.Append(","); |
| | json.Append(octreeResolution); |
| | json.Append(","); |
| | json.Append(removeBackground ? "true" : "false"); |
| | json.Append(","); |
| | json.Append(numChunks); |
| | json.Append(","); |
| | json.Append(randomizeSeed ? "true" : "false"); |
| | json.Append("]}"); |
| | return json.ToString(); |
| | } | | | private async Task<string> SubmitRequest(string apiPrefix, string apiName, string json) | | | { |
| | string url = serverUrl.TrimEnd('/') + apiPrefix + apiName; |
| | using (UnityWebRequest request = new UnityWebRequest(url, UnityWebRequest.kHttpVerbPOST)) |
| | { |
| | byte[] body = Encoding.UTF8.GetBytes(json); |
| | request.uploadHandler = new UploadHandlerRaw(body); |
| | request.downloadHandler = new DownloadHandlerBuffer(); |
| | request.SetRequestHeader("Content-Type", "application/json"); |
| | request.timeout = 0; |
| | await SendRequest(request); |
| | if (request.result != UnityWebRequest.Result.Success) | | | throw new Exception("Request failed: HTTP " + request.responseCode + "\n" + request.error + "\n" + request.downloadHandler.text); | | | string response = request.downloadHandler.text; |
| | Match match = Regex.Match(response, "\"event_id\"\\s*:\\s*\"([^\"]+)\""); |
| | if (!match.Success) |
| | throw new Exception("Gradio did not return an event_id.\n\nResponse:\n" + response); | | | return match.Groups[1].Value; | | | } | | | } | | | private async Task<string> WaitForResult(string apiPrefix, string apiName, string eventId) | | | { | | | string url = serverUrl.TrimEnd('/') + apiPrefix + apiName + "/" + eventId; | | | using (UnityWebRequest request = UnityWebRequest.Get(url)) | | | { |
| | request.downloadHandler = new DownloadHandlerBuffer(); |
| | request.timeout = 0; |
| | await SendRequest(request); |
| | if (request.result != UnityWebRequest.Result.Success) | | | throw new Exception("Generation request failed: HTTP " + request.responseCode + "\n" + request.error + "\n" + request.downloadHandler.text); | | | string response = request.downloadHandler.text; | | | if (response.Contains("event: error")) | | | throw new Exception("Hunyuan3D reported a generation error.\n\n" + response); | | | if (!response.Contains("event: complete")) | | | Debug.LogWarning("Hunyuan3D response did not contain an explicit complete event."); | | | return response; | | | } | | | } | | | private string FindModelUrl(string response, bool textured) | | | { |
| | MatchCollection matches = Regex.Matches(response, "\"url\"\\s*:\\s*\"([^\"]+)\""); |
| | List<string> urls = new List<string>(); |
| | foreach (Match match in matches) | | | { |
| | string url = JsonUnescape(match.Groups[1].Value); |
| | if (!string.IsNullOrEmpty(url) && !urls.Contains(url)) |
| | urls.Add(url); |
| | } | | | string expectedName = textured ? "textured_mesh" : "white_mesh"; | | | foreach (string url in urls) | | | { | | | if (url.IndexOf(expectedName, StringComparison.OrdinalIgnoreCase) >= 0) | | | return url; | | | } |
| | if (textured && urls.Count >= 2) |
| | return urls[1]; |
| | if (urls.Count >= 1) |
| | return urls[0]; |
| | return null; | | | } | | | private async Task<byte[]> DownloadFile(string url) | | | { | | | using (UnityWebRequest request = UnityWebRequest.Get(url)) | | | { |
| | request.downloadHandler = new DownloadHandlerBuffer(); |
| | request.timeout = 0; |
| | await SendRequest(request); |
| | if (request.result != UnityWebRequest.Result.Success) | | | throw new Exception("Failed to download GLB: HTTP " + request.responseCode + "\n" + request.error); |
| | byte[] data = request.downloadHandler.data; |
| | if (data == null \|\| data.Length == 0) |
| | throw new Exception("Downloaded GLB file is empty."); | | | return data; | | | } | | | } | | | private string SaveToAssets(byte[] data) | | | { |
| | string folder = outputFolder.Replace("\\", "/").TrimEnd('/'); |
| | if (!Directory.Exists(folder)) |
| | Directory.CreateDirectory(folder); |
| | string safePrompt = MakeSafeFileName(prompt); |
| | if (safePrompt.Length > 50) |
| | safePrompt = safePrompt.Substring(0, 50); |
| | if (string.IsNullOrWhiteSpace(safePrompt)) |
| | safePrompt = "hunyuan_model"; |
| | string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); |
| | string assetPath = folder + "/" + safePrompt + "_" + timestamp + ".glb"; | | | File.WriteAllBytes(assetPath, data); | | | return assetPath; | | | } | | | private string MakeAbsoluteUrl(string url) | | | { | | | if (url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) | | | return url; |
| | if (!url.StartsWith("/")) |
| | url = "/" + url; |
| | return serverUrl.TrimEnd('/') + url; |
| | } | | | private static async Task SendRequest(UnityWebRequest request) | | | { |
| | UnityWebRequestAsyncOperation operation = request.SendWebRequest(); |
| | while (!operation.isDone) |
| | await Task.Yield(); |
| | } | | | private static string JsonString(string value) | | | { | | | if (value == null) | | | return "null"; |
| | StringBuilder builder = new StringBuilder(); |
| | builder.Append('"'); |
| | foreach (char c in value) |
| | { | | | switch (c) | | | { | | | case '"': | | | builder.Append("\""); | | | break; | | | case '\': | | | builder.Append("\\"); | | | break; | | | case '\b': | | | builder.Append("\b"); | | | break; | | | case '\f': | | | builder.Append("\f"); | | | break; | | | case '\n': | | | builder.Append("\n"); | | | break; | | | case '\r': | | | builder.Append("\r"); | | | break; | | | case '\t': | | | builder.Append("\t"); | | | break; | | | default: |
| | if (c < 32) |
| | builder.Append("\\u" + ((int)c).ToString("x4")); |
| | else | | | builder.Append(c); | | | break; | | | } | | | } |
| | builder.Append('"'); |
| | return builder.ToString(); |
| | } | | | private static string JsonUnescape(string value) | | | { | | | value = value.Replace("\/", "/"); | | | try | | | { | | | return Regex.Unescape(value); | | | } | | | catch | | | { | | | return value; | | | } | | | } | | | private static string MakeSafeFileName(string value) | | | { |
| | string result = Regex.Replace(value, @"[^a-zA-Z0-9_-]+", "_"); |
| | result = result.Trim('_'); |
| | return result; | | | } | | | } | | | } |