Skip to content

Instantly share code, notes, and snippets.

@unitycoder
Created September 12, 2026 15:25
Show Gist options
  • Select an option

  • Save unitycoder/72ba2f996f0ff6e8a0c5ce0eb866f203 to your computer and use it in GitHub Desktop.

Select an option

Save unitycoder/72ba2f996f0ff6e8a0c5ce0eb866f203 to your computer and use it in GitHub Desktop.
Unity Editor script to generate models in locally running Hunyuan3D
// 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;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment