跳转至

name: vrchat-ma-blendshape-sync-fix description: 修复 ModularAvatarBlendshapeSync 组件的 MA-1005 警告(referencePath / Blendshape 字段错填),含 Unity MCP 反射批量修复脚本和 Cazalis 服装包已知 bug


VRChat ModularAvatar BlendshapeSync 修复

适用场景

Unity Console 出现 MA-1005 警告:

[ModularAvatarBlendshapeSync] Reference mesh not found...

最常见原因(按发生率排序): 1. referencePath 错填(指向不存在的节点,例 Cazalis 服装包写成 "Kikyo_Body_Base") 2. Blendshape 名字大小写错误(例 "Breasts_big" 实际是 "Breasts_Big") 3. 目标节点已被删除/重命名 4. AvatarObjectReference 缓存字段过期(path 已改但 Get() 返回旧值)

已知 bug 名单

Cazalis 服装包(zigai 系列三件套)

部件 错填 referencePath Blendshape 大小写错误
01_Black/Bag Kikyo_Body_Base Breasts_big / Breasts_small
01_Black/Blouse Kikyo_Body_Base Breasts_big / Breasts_small
01_Black/Knit Kikyo_Body_Base Breasts_big / Breasts_small

每个组件 2 个 binding,每个 Avatar 6 个 binding。修复方式: - referencePath: Kikyo_Body_BaseBody_Base - Blendshape: Breasts_big/smallBreasts_Big/Small ferencePath: Kikyo_Body_BaseBody_Base - Blendshape: Breasts_big/smallBreasts_Big/Small## 数据结构关键点(容易踩坑)

// 组件: ModularAvatarBlendshapeSync
// Bindings: List<BlendshapeBinding>  ← struct,值类型!

public struct BlendshapeBinding {
    public AvatarObjectReference ReferenceMesh;  // class(引用类型)
    public string Blendshape;                    // 源 blendshape 名
    public string LocalBlendshape;               // 目标 blendshape 名(空=同名)
}

public class AvatarObjectReference {
    public string referencePath;
    // 内部缓存(私有字段,要清掉)
    private bool _cacheValid;
    private string _cachedPath;
    private Object _cachedReference;
}

坑 1:BlendshapeBinding 是 struct,从 list 取出后修改不写回会丢失:

// ❌ 错的
bindings[i].Blendshape = "Breasts_Big";  // 编译报错或修改的是临时副本

// ✅ 对的
var b = bindings[i];
b.GetType().GetField("Blendshape").SetValue(b, "Breasts_Big");
bindings[i] = b;  // 必须写回

坑 2:AvatarObjectReference 是 class,可以直接修改字段,但要清缓存

refMesh.referencePath = "Body_Base";
// 还要清 _cacheValid / _cachedPath / _cachedReference 否则 Get() 仍返回旧值
rmt.GetField("_cacheValid").SetValue(refMesh, false);
rmt.GetField("_cachedPath").SetValue(refMesh, "");
rmt.GetField("_cachedReference").SetValue(refMesh, null);

批量修复脚本(Unity MCP execute_code)

完整脚本:见 scripts/fix-ma-blendshape-sync.cs

核心流程: 1. 遍历指定 Avatar 列表的所有子 Transform 2. 找所有 ModularAvatarBlendshapeSync 组件 3. 反射读取 Bindings,逐个检查 referencePath / Blendshape 4. 匹配已知错误模式时修正,必须 bindings[i] = binding 写回 5. 清 AvatarObjectReference 缓存 6. EditorUtility.SetDirty + SaveScene ,必须 bindings[i] = binding 写回 5. 清 AvatarObjectReference 缓存 6. EditorUtility.SetDirty + SaveScene## 验证步骤(极其重要!)

⚠️ 2026-05-31 教训:Get() 假阳性陷阱

AvatarObjectReference 序列化的真实字段不止 referencePath 一个,至少有 7 个

- referencePath              (public string)
- targetObject               (private GameObject)  ← 关键!
- ReferencesLockedAtFrame    (long)
- _cacheSeq                  (private)
- _cacheValid                (private)
- _cachedPath                (private)
- _cachedReference           (private)

修复时只改 referencePath 会出现: - ✅ Get() 调用动态解析新路径,返回正确 GameObject(看起来修好了) - ❌ .unity 序列化时仍写旧的 targetObject 引用 - ❌ Avatar 被复制 / Prefab 重生成后,新 Avatar 的 targetObject 仍指向源 Avatar 的 GameObject,跨 Avatar 引用 → MA-1005 仍报

完整修复必须同时改两个字段

// 方法 A(推荐):调用官方 Set 方法
refMesh.Set(targetGameObject);  // 内部会同时设置 path + targetObject + IsConsistent 校验

// 方法 B:反射裸改(必须三件套)
refMesh.referencePath = "Body_Base";
var targetField = typeof(AvatarObjectReference)
    .GetField("targetObject", BindingFlags.Instance | BindingFlags.NonPublic);
targetField.SetValue(refMesh, targetGameObject);
// 然后清缓存
typeof(AvatarObjectReference)
    .GetField("_cacheValid", BindingFlags.Instance | BindingFlags.NonPublic)
    .SetValue(refMesh, false);

反射读 private 字段必须用 NonPublic BindingFlags(否则 GetField 返回 null):

// ❌ 默认只拿 public,private targetObject 返回 null
var f = type.GetField("targetObject");

// ✅ 必须显式
var f = type.GetField("targetObject",
    BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

修复后三重验证: bject", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

修复后**三重验证**:```csharp
// 1. 扫描确认所有 binding referencePath / Blendshape 都不在错误名单
// 2. Get() 能 resolve 到 GameObject
var resolved = refMesh.Get(component);
// 3. ★必须★ 反射读 targetObject 字段,确认指向当前 Avatar 而非源 Avatar
var targetField = typeof(AvatarObjectReference)
    .GetField("targetObject", BindingFlags.Instance | BindingFlags.NonPublic);
var t = targetField.GetValue(refMesh) as GameObject;
// t != null && t.transform.root == component.transform.root 才算成功

⚠️ fake null 陷阱:UnityEngine.Object 被销毁后 == null 是 true 但反射 GetValue 后强转 as GameObject 可能拿到 destroyed 引用,访问 .name / .transform 会抛 NullReferenceException。先 == null 检查再访问。 转 as GameObject 可能拿到 destroyed 引用,访问 .name / .transform 会抛 NullReferenceException。先 == null 检查再访问。## 安全规范(参照改模硬纪律)

  • 修复前必备份AssetDatabase.CopyAsset("Assets/Scene.unity", "Assets/_Backup_xxx/Scene.unity.bak")
  • 样本先行:先修 1 个 binding,用户验证 OK 再批量
  • 修 zigai02 等"原版"对象的破例条件:纯字符串字段 + 可逆 + 修复 bug 而非新增风险
  • 修复完整可逆:保留 .bak 文件至少一周

Cazalis 案例完整执行流(2026-05-31)

Phase 范围 数量 验证方式
0 备份场景 1 file (8.5MB) size 校验
1 修 1 样本 zigai02/01_Black/Bag 2 binding Get() resolved != null
2 批量修剩余 16 binding 扫描确认 0 残留 + Get() 54/54 resolved
3 ⚠️ 复制 Avatar 后用户报 ZG_02 仍报错 targetObject 指向源 Avatar

最终:8 components / 18 bindings 全部修复,0 MA-1005 警告。

⚠️ 助手翻车记录与教训(2026-05-31)

事件复盘: 1. 助手 Phase 2 报告"54/54 全部 resolved"是假阳性 — 只验 Get() 没验 targetObject 字段 2. 用户拆分 Avatar 后 ZG_01/ZG_02 仍报 MA-1005,因为复制时拷贝了源 Avatar 的 targetObject 引用 3. 助手第二次扫描以为"all OK",实际是反射读 private 字段没加 BindingFlags.NonPublic 全返 null 走 EX 分支 4. 用户三次回过头说"你只换了源没换目标"才意识到 targetObject 是独立字段

根本教训: - ❗ 修复任何 Unity Component 字段前,必须先用 SerializedObject 看完整字段列表,不要假设 "看 Inspector 上的字段就够了" - ❗ Get() / 静态 API 解析正常 ≠ 序列化正确。验证一定要看 .unity 文件实际写了什么(或反射读所有可序列化字段) - ❗ 修 MA 字段优先用官方 Set/Clone/IsConsistent API,反射裸改属于最后手段 - ❗ 当 user 报错与 verify 结果矛盾时,立即怀疑 verify 而非 user

常见问题:复制 Avatar 后上传被覆盖

原因

从已有 Avatar 复制出来的新 Avatar,PipelineManager 组件会保留原 Avatar 的 Blueprint IDavtr_xxx-xxxx),VRChat SDK 会认为是同一个 Avatar 的新版本,后上传覆盖前面的。 eManager组件会保留原 Avatar 的 **Blueprint ID**(avtr_xxx-xxxx),VRChat SDK 会认为是同一个 Avatar 的新版本,后上传覆盖前面的。### 解决方法(每个新 Avatar 操作一次) 1. 选中 Avatar 根节点 → 找到Pipeline Manager` 组件 2. 点击 Blueprint ID 右侧 Detach (Optional) 按钮,ID 变空白 3. VRCSDK 控制面板 → Builder → 确认 Avatar 名称正确 → Build & Publish 4. 第一次上传勾选 New Avatar,生成新的独立 Blueprint ID

⚠️ 原 Avatar 不要 detach,保留原 ID 保持原有上传记录。