# Unity Editor script to generate models in locally running Hunyuan3D

> Source: <https://gist.github.com/unitycoder/72ba2f996f0ff6e8a0c5ce0eb866f203>
> Published: 2026-09-12 15:25:33+00:00

|  | // 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 = "Downloading 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; | 
|  | } | 
|  | } | 
|  | } |
