// Place in Assets/Editor. Install com.unity.cloud.gltfast in Package Manager first. // Credentials stay in this window's memory or BAKE3D_API_KEY, never in project assets. using System; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using UnityEditor; using UnityEngine; using UnityEngine.Networking; public sealed class Bake3DWindow : EditorWindow { const string Origin = "https://bake3d.ai"; string apiKey = "", prompt = "A ceramic fox standing on four legs"; string projectId = "", jobId = "", motion = "Look around curiously, then settle"; string animationId = "", presetId = "", status = "Create a Bake3D API key to connect."; bool busy, restPose; [MenuItem("Tools/Bake3D/Character studio")] public static void Open() => GetWindow("Bake3D"); void OnGUI() { GUILayout.Label("Bake3D characters", EditorStyles.boldLabel); apiKey = EditorGUILayout.PasswordField("API key", apiKey); if (GUILayout.Button("Open API key settings")) Application.OpenURL(Origin + "/account/api"); EditorGUILayout.Space(); using (new EditorGUI.DisabledScope(busy)) { prompt = EditorGUILayout.TextField("Character", prompt); if (GUILayout.Button("Generate character (35 credits)")) Run(Generate); jobId = EditorGUILayout.TextField("Job ID", jobId); if (GUILayout.Button("Check job")) Run(CheckJob); EditorGUILayout.Space(); projectId = EditorGUILayout.TextField("Project ID", projectId); if (GUILayout.Button("Create / retry included animation (free)")) Run(InitialAnimation); motion = EditorGUILayout.TextField("Motion", motion); if (GUILayout.Button("Compose motion (5 credits)")) Run(Animate); if (GUILayout.Button("Use latest saved animation (free)")) Run(LatestAnimation); animationId = EditorGUILayout.TextField("Animation ID", animationId); presetId = EditorGUILayout.TextField("Preset ID (optional)", presetId); restPose = EditorGUILayout.Toggle("Rest pose only", restPose); if (GUILayout.Button("Import character and motion")) Run(Import); } EditorGUILayout.Space(); EditorGUILayout.HelpBox(status, MessageType.Info); EditorGUILayout.HelpBox("Imports add a new prefab instance. GLB animation requires Unity glTFast. Use its Animation settings and the clip preview to inspect motion.", MessageType.None); } async void Run(Func action) { busy = true; status = "Working…"; Repaint(); try { await action(); } catch (Exception error) { status = error.Message; Debug.LogWarning("Bake3D: " + error.Message); } finally { busy = false; if (this) Repaint(); } } string Token() { var key = string.IsNullOrWhiteSpace(apiKey) ? Environment.GetEnvironmentVariable("BAKE3D_API_KEY") : apiKey; if (string.IsNullOrEmpty(key) || !key.StartsWith("bk_live_")) throw new Exception("Enter a Bake3D API key."); return key; } static string Quote(string value) { var json = JsonUtility.ToJson(new StringValue { value = value }); return json.Substring(9, json.Length - 10); } async Task Request(string path, string body = null) { using (var request = new UnityWebRequest(Origin + "/api/v1" + path, body == null ? "GET" : "POST")) { request.downloadHandler = new DownloadHandlerBuffer(); request.SetRequestHeader("Authorization", "Bearer " + Token()); if (body != null) { request.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(body)); request.SetRequestHeader("Content-Type", "application/json"); } request.timeout = 180; var operation = request.SendWebRequest(); while (!operation.isDone) await Task.Yield(); if (request.result != UnityWebRequest.Result.Success) { var error = JsonUtility.FromJson(request.downloadHandler.text); throw new Exception(error?.error ?? ("HTTP " + request.responseCode)); } return JsonUtility.FromJson(request.downloadHandler.text); } } async Task Generate() { var result = await Request("/generate", "{\"prompt\":" + Quote(prompt) + ",\"options\":{\"rig\":true,\"quality\":\"standard\",\"initialAnimation\":" + (restPose ? "false" : "true") + "}}"); projectId = result.project.id; jobId = result.job.id; animationId = ""; status = "Generation started. Check the job until it succeeds, then import."; } async Task CheckJob() { var job = await Request("/jobs/" + Uri.EscapeDataString(jobId)); status = job.status + " · " + job.progress + "% · " + (string.IsNullOrEmpty(job.error) ? job.stage : job.error); if (!string.IsNullOrEmpty(job.projectId)) projectId = job.projectId; } async Task Animate() { var result = await Request("/projects/" + Uri.EscapeDataString(projectId) + "/animate", "{\"prompt\":" + Quote(motion) + ",\"durationSeconds\":3,\"loop\":true}"); animationId = result.animation.id; restPose = false; status = "Motion saved. Import it to preview in Unity."; } async Task InitialAnimation() { var result = await Request("/projects/" + Uri.EscapeDataString(projectId) + "/initial-animation", "{}"); animationId = result.animationId ?? ""; presetId = ""; restPose = false; if (result.job != null && !string.IsNullOrEmpty(result.job.id)) { jobId = result.job.id; status = "Initial animation started. Check its job, then import."; } else status = "Initial animation is ready to import."; } async Task LatestAnimation() { var project = await Request("/projects/" + Uri.EscapeDataString(projectId)); if (project.animations == null || project.animations.Length == 0) { status = "No saved animations yet. Compose motion or create the included animation."; return; } var latest = project.animations[0]; animationId = latest.id; presetId = ""; restPose = false; status = "Selected " + latest.name + ". " + project.animations.Length + " saved animation(s); newest first."; } async Task Import() { var body = "{\"fps\":30,\"rootMotion\":false"; if (restPose) body += ",\"restPose\":true"; else if (!string.IsNullOrWhiteSpace(animationId)) body += ",\"animationId\":" + Quote(animationId); else if (!string.IsNullOrWhiteSpace(presetId)) body += ",\"presetId\":" + Quote(presetId); body += "}"; var export = await Request("/projects/" + Uri.EscapeDataString(projectId) + "/export", body); var url = new Uri(export.downloadUrl); if (url.Scheme != "https") throw new Exception("Bake3D returned an invalid download URL."); Directory.CreateDirectory("Assets/Bake3D"); var path = AssetDatabase.GenerateUniqueAssetPath("Assets/Bake3D/Character.glb"); using (var download = UnityWebRequest.Get(url)) { download.downloadHandler = new DownloadHandlerFile(path) { removeFileOnAbort = true }; download.timeout = 120; var operation = download.SendWebRequest(); while (!operation.isDone) await Task.Yield(); if (download.result != UnityWebRequest.Result.Success) { File.Delete(path); throw new Exception("Download failed. Export again to refresh the link."); } } AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceSynchronousImport); var prefab = AssetDatabase.LoadAssetAtPath(path); if (!prefab) throw new Exception("GLB saved at " + path + ". Install Unity glTFast, then reimport this asset."); var instance = PrefabUtility.InstantiatePrefab(prefab) as GameObject; Undo.RegisterCreatedObjectUndo(instance, "Import Bake3D character"); Selection.activeGameObject = instance; SceneView.lastActiveSceneView?.FrameSelected(); var clips = AssetDatabase.LoadAllAssetsAtPath(path).OfType().Where(c => !c.name.StartsWith("__preview__")).ToArray(); status = "Imported " + path + " with " + clips.Length + " animation clips. Select a clip to preview."; } [Serializable] class StringValue { public string value; } [Serializable] class ApiError { public string error; } [Serializable] class Project { public string id; } [Serializable] class Job { public string id, status, stage, error, projectId; public float progress; } [Serializable] class Generation { public Project project; public Job job; } [Serializable] class MotionResult { public Project animation; } [Serializable] class SavedAnimation { public string id, name; } [Serializable] class ProjectDetail { public SavedAnimation[] animations; } [Serializable] class InitialResult { public Job job; public string animationId; } [Serializable] class ExportResult { public string downloadUrl; } }