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