Unity动态创建材质球:TaoToken统一Key接入AI材质生成工作流
1. Unity运行时动态创建材质球到底难在哪Unity 动态创建材质球说白了就是在游戏跑起来之后用代码 new 一个 Material把贴图、颜色、光滑度这些参数塞进去再挂到某个模型的 MeshRenderer 上。听起来三行代码的事但真到项目里坑一个接一个贴图从哪来、Shader 找不到、材质球在编辑器里看不见、打包后 Resources 路径失效、参数名写错导致 SetFloat 静默失败。尤其是做 AR 涂涂乐、换装、DIY 家具这类功能时用户上传一张图你得实时把它变成材质贴到模型上还要保证不同平台表现一致。我最近在做一个类似的项目需求是让用户输入一段文字描述比如“磨砂质感的深蓝色金属”然后 AI 返回一组材质参数颜色、金属度、光滑度、法线强度Unity 端拿到参数后动态生成材质球并替换。这样用户不用懂 Shader也能快速看到效果。问题在于AI 调用需要一个稳定的 API 通道而国内直接调各家大模型 API 经常遇到网络和鉴权问题。后来我用了 TaoToken 的统一 Key 接入方案一个 Key 走通多个模型省去了分别注册和管理的麻烦。这篇文章就围绕这个场景展开先讲清楚 Unity 动态创建材质球的核心 API 和常见坑再给出用 TaoToken 统一 Key 接入 AI 生成材质参数的完整 C# 脚本和配置最后在 Unity Editor 里一步步验证结果。适合有一定 Unity 基础、想接入 AI 辅助材质生成的开发者。你不需要事先了解 TaoToken跟着配就行。核心检索词先明确Unity 动态创建材质球指的是在运行时通过new Material()创建材质实例配合Material.SetColor、SetFloat、SetTexture等方法设置属性再赋值给MeshRenderer.material。它和编辑器里手动创建材质球的区别在于运行时创建的材质不会保存到 Assets只存在于内存中场景里看不到只有运行时才生效。这一点后面会反复提到。2. TaoToken 统一 Key 接入前置准备在写 Unity 脚本之前先把 API 通道准备好。TaoToken 是一个统一的大模型 API 接入平台你注册后拿到一个 Key就能调用它支持的多个模型不用为每个模型单独申请账号。对于 Unity 项目来说这意味着你只需要在 C# 里维护一个 Base URL 和一个 Key切换模型只改 Model ID 就行。先访问官网注册账号https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。注册流程不复杂邮箱验证后进入控制台。然后在控制台里创建 API Key路径是 console 页面https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsole 。创建时建议给 Key 起个名字比如 “unity-material-gen”方便后面区分用途。Key 只显示一次复制后先存到安全的地方。接下来确认 API 端点。TaoToken 的 API 基础地址是https://taotoken.net/api 。注意这个地址不带 UTM 参数直接用于代码里的请求。模型对话的接口路径是/v1/chat/completions和 OpenAI 兼容格式一致。你可以在模型对话页面先手动测试一下https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 。在页面里选一个模型输入“返回一个 JSON包含 color、metallic、smoothness 三个字段值分别是深蓝色、0.8、0.3”看返回格式是否符合预期。这一步能帮你确认 Key 有效、模型可用。如果你打算长期在 Unity 里做 AI 辅助开发比如批量生成材质、自动生成 Shader 参数可以考虑 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。它适合需要频繁调用、长期编码的场景比按次调用更划算。不过对于本文的材质生成 demo按次调用就够用了。拿到 Key 后在 Unity 项目里建一个配置文件。我习惯用Resources文件夹放一个TaoTokenConfig.json但注意 Key 不要硬编码在会打包进客户端的脚本里。更安全的做法是运行时从本地读取或者用环境变量。本文为了演示方便先放在一个 ScriptableObject 里实际项目请做加密或服务端转发。配置内容如下你可以直接复制成TaoTokenConfig.json放在Assets/Resources/下{ baseUrl: https://taotoken.net/api, apiKey: sk-你的Key, modelId: gpt-4o-mini, timeoutSeconds: 30 }Model ID 根据你在模型对话页面选的模型填。TaoToken 支持多个模型具体列表在文档里https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。注意 Base URL 末尾不要加斜杠代码里拼接路径时统一处理。这里有个前置检查在 Unity 里用UnityWebRequest发 POST 请求时Header 要设Content-Type: application/json和Authorization: Bearer Key。如果你在 Editor 里测试确保网络能访问taotoken.net。如果公司网络有限制换手机热点试试。不要用任何代理工具直接访问即可。3. 可复制的 Unity C# 脚本与配置这一节给出完整的可复制代码。整个流程分三块读取配置、调用 TaoToken API 获取材质参数、动态创建材质球并替换。我会把每一步拆开讲你可以直接拷到项目里改。先建一个MaterialParam类用来反序列化 AI 返回的 JSON[System.Serializable] public class MaterialParam { public string color; // 十六进制颜色如 #1A2B3C public float metallic; // 金属度 0~1 public float smoothness; // 光滑度 0~1 public float normalScale; // 法线强度 0~1 }再建一个TaoTokenConfig类对应上面的 JSON 文件[System.Serializable] public class TaoTokenConfig { public string baseUrl; public string apiKey; public string modelId; public int timeoutSeconds; }然后是核心的AIMaterialGenerator脚本。它挂在场景里任意 GameObject 上暴露一个targetRenderer字段指向你要替换材质的模型。调用GenerateAndApply(string prompt)就会走完整流程。using System; using System.Collections; using System.Text; using UnityEngine; using UnityEngine.Networking; public class AIMaterialGenerator : MonoBehaviour { public MeshRenderer targetRenderer; public string configResourcePath TaoTokenConfig; private TaoTokenConfig _config; void Awake() { var json Resources.LoadTextAsset(configResourcePath); if (json null) { Debug.LogError(找不到 TaoTokenConfig.json请放在 Assets/Resources/ 下); return; } _config JsonUtility.FromJsonTaoTokenConfig(json.text); } public void GenerateAndApply(string userPrompt) { StartCoroutine(GenerateRoutine(userPrompt)); } private IEnumerator GenerateRoutine(string userPrompt) { string systemPrompt 你是一个材质参数生成器。根据用户描述返回严格 JSON不要多余文字。 字段color(十六进制字符串), metallic(0~1), smoothness(0~1), normalScale(0~1)。; var requestBody new ChatRequest { model _config.modelId, messages new[] { new ChatMessage { role system, content systemPrompt }, new ChatMessage { role user, content userPrompt } }, temperature 0.3f }; string jsonBody JsonUtility.ToJson(requestBody); string url _config.baseUrl.TrimEnd(/) /v1/chat/completions; using (var req new UnityWebRequest(url, POST)) { byte[] bodyRaw Encoding.UTF8.GetBytes(jsonBody); req.uploadHandler new UploadHandlerRaw(bodyRaw); req.downloadHandler new DownloadHandlerBuffer(); req.SetRequestHeader(Content-Type, application/json); req.SetRequestHeader(Authorization, Bearer _config.apiKey); req.timeout _config.timeoutSeconds; yield return req.SendWebRequest(); if (req.result ! UnityWebRequest.Result.Success) { Debug.LogError($请求失败: {req.error} | 响应: {req.downloadHandler.text}); yield break; } string responseText req.downloadHandler.text; string content ParseContent(responseText); if (string.IsNullOrEmpty(content)) { Debug.LogError(解析 choices 失败: responseText); yield break; } MaterialParam param; try { param JsonUtility.FromJsonMaterialParam(content); } catch (Exception e) { Debug.LogError(材质参数 JSON 解析失败: content | e.Message); yield break; } ApplyMaterial(param); } } private string ParseContent(string responseJson) { var resp JsonUtility.FromJsonChatResponse(responseJson); if (resp?.choices null || resp.choices.Length 0) return null; return resp.choices[0].message.content; } private void ApplyMaterial(MaterialParam param) { if (targetRenderer null) { Debug.LogError(targetRenderer 未赋值); return; } Material mat new Material(Shader.Find(Standard)); if (ColorUtility.TryParseHtmlString(param.color, out Color c)) mat.SetColor(_Color, c); mat.SetFloat(_Metallic, Mathf.Clamp01(param.metallic)); mat.SetFloat(_Glossiness, Mathf.Clamp01(param.smoothness)); if (mat.HasProperty(_BumpScale)) mat.SetFloat(_BumpScale, Mathf.Clamp01(param.normalScale)); targetRenderer.material mat; Debug.Log($材质已应用: color{param.color}, metallic{param.metallic}, smoothness{param.smoothness}); } [Serializable] private class ChatRequest { public string model; public ChatMessage[] messages; public float temperature; } [Serializable] private class ChatMessage { public string role; public string content; } [Serializable] private class ChatResponse { public Choice[] choices; } [Serializable] private class Choice { public ChatMessage message; } }这段代码的关键点Shader.Find(Standard)在 Editor 里没问题但打包到某些平台可能被裁剪需要在 Graphics Settings 的 Always Included Shaders 里加上 Standard。JsonUtility不支持字典所以返回结构要简单。ParseContent里如果 choices 为空说明 API 返回了错误打印完整响应方便排查。配置片段再强调一次TaoTokenConfig.json放在Assets/Resources/下内容{ baseUrl: https://taotoken.net/api, apiKey: sk-你的Key, modelId: gpt-4o-mini, timeoutSeconds: 30 }如果你用 Claude Code 做辅助开发想让它帮你生成更多材质变体可以配置 ClaudeCodeAnthropic 的接入https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_codeutm_campaignrewrite 。不过本文的 Unity 脚本不依赖它按需使用。4. 在 Unity Editor 中验证请求与材质结果代码写好了接下来在 Editor 里跑一遍。步骤很具体跟着做就行。第一步准备场景。新建一个 3D 场景放一个 Cube 或 Sphere确保它有 MeshRenderer。把AIMaterialGenerator脚本挂到场景里一个空 GameObject 上比如叫 “MaterialGen”。在 Inspector 里把 Cube 的 MeshRenderer 拖到targetRenderer字段。第二步确认配置文件。在Assets/Resources/下放TaoTokenConfig.json填好 Key 和 Model ID。注意 Resources 文件夹名字拼写正确大小写敏感。如果Resources.Load返回 null检查路径和文件名。第三步写一个临时测试脚本或者直接在AIMaterialGenerator里加一个 ContextMenu 方法方便在 Inspector 里右键触发。加在类里[ContextMenu(测试生成材质)] private void TestGenerate() { GenerateAndApply(磨砂质感的深蓝色金属略带反光); }保存后回到 Inspector点右上角三个点选“测试生成材质”。这时 Console 会打印请求过程。如果成功你会看到材质已应用: color#1A2B3C, metallic0.8, smoothness0.3这样的日志同时 Scene 视图里的 Cube 材质变了。第四步验证结果。选中 Cube在 Inspector 的 MeshRenderer 里能看到材质实例名字是 “Standard (Instance)”说明是运行时创建的。展开材质属性_Color、_Metallic、_Glossiness应该和 AI 返回的一致。注意这个材质不会出现在 Project 窗口的 Assets 里因为它是内存对象。停止运行后Cube 恢复原材质这是正常的。第五步测试不同描述。改TestGenerate里的 prompt比如“红色塑料光滑”再点一次。观察颜色和光滑度变化。如果 AI 返回的 JSON 格式不对Console 会打印解析失败的内容你可以据此调整 system prompt。实测下来从点击到材质更新大概 2~5 秒取决于模型响应速度。如果超过 timeout检查timeoutSeconds是否够大或者换一个更快的模型。TaoToken 的模型对话页面可以提前测响应速度。这里给一个成功响应的示例方便你对照{ choices: [ { message: { role: assistant, content: {\color\:\#1A2B3C\,\metallic\:0.8,\smoothness\:0.3,\normalScale\:0.5} } } ] }如果你的返回里 content 带了 markdown 代码块标记比如 json需要在ParseContent里去掉。可以在解析前做一次字符串清洗content content.Replace(json, ).Replace(, ).Trim();这个坑很常见AI 有时会自作主张加格式。加上这行就稳了。5. 常见报错排查与真实错误对照这一节列出我实际遇到的报错和解决办法你对照 Console 输出定位。401 Unauthorized最常见。检查AuthorizationHeader 是不是Bearer sk-xxx注意 Bearer 后面有空格。Key 是否复制完整有没有多余换行。如果 Key 正确还报 401去 console 页面确认 Key 状态是否启用https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。另外确认 Base URL 是https://taotoken.net/api不要写成别的路径。local proxy failed / Cannot connect to destination host这个报错说明 Unity 发出的请求没到达服务器。先检查网络用浏览器打开https://taotoken.net/api看是否可达。如果浏览器能开但 Unity 不行可能是 Unity 的代理设置问题在 Edit Preferences General 里把代理设为 None。不要用任何第三方代理工具直接连即可。reading choices 失败 / choices 为 null说明返回的 JSON 结构不是预期的。打印完整responseText看是不是返回了 error 字段。常见原因是 Model ID 写错或者请求体里 model 字段和账号权限不匹配。去模型对话页面确认可用模型列表。另外检查ChatResponse类的字段名是否和返回一致JsonUtility 对大小写敏感。OAuth 相关报错如果你在别处配置过 OAuth 或者用了 Claude Code 的认证方式可能和 API Key 冲突。本文用的是 Bearer Token不需要 OAuth。如果 Console 出现 OAuth 字样检查是不是误用了其他接入方式。Claude Code 的接入是独立的参考文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。Shader.Find(Standard) 返回 null在 Editor 里一般不会但打包后可能。解决办法是在 Project Settings Graphics Always Included Shaders 里添加 Standard。或者改用Shader.Find(Standard (Specular setup))看是否存在。材质球在 Editor 里看不见这是正常现象。运行时创建的 Material 不在 Assets 里只有运行时生效。如果你想在 Editor 里预览可以临时用AssetDatabase.CreateAsset保存但正式项目不要这么做会污染资源。SetFloat 无效检查属性名。Standard Shader 的光滑度属性是_Glossiness金属度是_Metallic颜色是_Color。如果你用的自定义 Shader属性名可能不同用mat.HasProperty(_Glossiness)先判断。写错属性名不会报错只是静默失败所以一定要加判断。JSON 解析失败AI 返回的 content 可能包含换行或转义字符。用JsonUtility.FromJson前先Trim()并去掉可能的 markdown 标记。如果字段类型不匹配比如 metallic 返回了字符串 0.8需要先转 float。可以在 system prompt 里强调“数值字段必须是数字不要加引号”。请求超时默认 30 秒如果模型响应慢调大到 60。但更建议换一个响应快的模型。在模型对话页面测试不同模型的延迟。排查时养成习惯先看 Console 完整报错再看req.downloadHandler.text的原始响应。大部分问题都能从原始响应里找到线索。如果还是搞不定去文档页搜错误关键词https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。6. 把 AI 材质生成接进你的工作流到这里Unity 动态创建材质球和 TaoToken 接入的完整链路就通了。你可以把GenerateAndApply接到 UI 按钮上让用户输入描述后点击生成。也可以批量调用比如给一组模型分别生成不同材质。如果要做更复杂的控制比如根据 AI 返回的参数生成自定义 Shader 变体可以在ApplyMaterial里扩展。对于长期在 Unity 里做 AI 辅助开发的场景建议把 API 调用封装成一个独立的服务类加上缓存和重试。TaoToken 的 Coding Plan 适合这种高频调用https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。如果只是偶尔用按次调用即可。最后提醒几个实际项目中的注意点。Key 不要提交到 Git用.gitignore排除配置文件或者运行时从服务端获取。打包前测试目标平台尤其是 WebGLUnityWebRequest的跨域和 Header 限制不同。AI 返回的参数要做范围校验Mathf.Clamp01已经做了但颜色格式要容错。如果 AI 返回的颜色不是十六进制加一个 fallback 默认色。我试过把生成结果保存成 Material 资产方便下次直接加载但要注意AssetDatabase只在 Editor 可用运行时用Resources.Save或自己序列化。这些扩展就留给你按需实现了。核心的动态创建和 API 调用已经跑通剩下的就是业务逻辑。