跳转至

VRChat 服装穿脱开关 + 溶解动画 + 多套换装系统

整合版本: 2026-05-09
适用对象: 有 VRChat 模体修改需求的用户
核心功能: 内衣/内裤/整套衣服溶解穿脱 + 多套服装切换 + 第三方发型/衣服整合


目录

  1. 系统架构总览
  2. 溶解动画核心原理
  3. 材质隔离(必须!)
  4. 内衣/内裤 Toggle 实现
  5. 整套衣服穿透 Toggle
  6. 多套服装切换系统
  7. 非本素体发型/衣服整合
  8. Expression Menu 配置
  9. Animator 状态机设计
  10. 常见坑点与排错
  11. MCP 操作流程

1. 系统架构总览

1.1 目标功能

功能 实现方式 关键技术
内衣穿脱 lilToon 溶解动画 _DissolveParams.z (Progress)
内裤穿脱 lilToon 溶解动画 _DissolveParams.z (Progress)
整套衣服穿透 lilToon 溶解动画 _DissolveParams.z (Progress)
多套服装切换 MA MenuItem Toggle + 独立材质 材质隔离 + 参数唯一命名
第三方发型 Parent to Bone + 独立 PhysBone 骨骼挂载 + PhysBone 配置
第三方衣服 Parent to Bone + 独立材质 骨骼挂载 + 材质隔离

1.2 核心原则

🔴 黄金法则:ObjectToggle 与溶解动画不能共存!

ObjectToggle 会瞬间设置 GameObject.active = false,溶解动画来不及播放。必须仅用 Animator 参数驱动 _DissolveParams.z

1.3 系统流程图

用户点击 VRChat 菜单按钮
MA MenuItem (Toggle, Bool 参数: "Cazalis_UW_Bra")
NDMF 构建阶段注册参数
Animator FX Layer 接收参数变化
Transition (Duration=0.3s) 触发
AnimationClip 播放: _DissolveParams.z: 0 → 1 (或反向)
lilToon Shader 溶解: 噪点纹理驱动边缘溶解
视觉效果: 粒子状消散 (约 0.3 秒)

2. 溶解动画核心原理

反向) ↓ lilToon Shader 溶解: 噪点纹理驱动边缘溶解 ↓ 视觉效果: 粒子状消散 (约 0.3 秒)

---

## 2. 溶解动画核心原理### 2.1 lilToon 溶解参数解析(源码验证)

lilToon 的溶解系统由 `_DissolveParams` (Vector4) 控制:

| 分量 | 字段 | 含义 | 正确值 | 动画目标? |
|------|------|------|--------|-----------|
| `.x` (R) | Mode | 溶解模式 | **必须为 1**(启用 2D Mask) | ❌ 固定 |
| `.y` (G) | SubMode | 子模式 | 0 | ❌ 固定 |
| `.z` (B) | **Progress** | **溶解进度** | **0 → 1** | ✅ **动画此值** |
| `.w` (A) | Softness | 边缘柔和度 | 0.1 | ❌ 固定 |

> ⚠️ **历史教训**:之前动画 `.x` 分量导致溶解模式被设为 0(关闭),动画静默失败!Progress 才是溶解进度。

### 2.2 溶解公式(lilToon 源码)

```hlsl
// 核心公式 (lil_common_functions.hlsl)
dissolveAlpha = 1.0 - saturate(abs(dissolveMaskVal + dissolveNoise - dissolveParams.b) / dissolveParams.a);

// 其中:
// dissolveParams.b = _DissolveParams.z (Progress/Threshold)
// dissolveParams.a = _DissolveParams.w (Softness)
// dissolveMaskVal = 噪点纹理值
// dissolveNoise = _DissolveNoiseStrength

2.3 材质必须配置

// ✅ 正确配置
mat.SetFloat("_TransparentMode", 1);        // Cutout 模式
mat.SetVector("_DissolveParams", new Vector4(1, 0, 0, 0.1f)); // Mode=1, Progress=0
mat.SetTexture("_DissolveNoiseMask", dissolveNoiseTex);  // 噪点纹理

// ❌ 错误配置
mat.SetFloat("_DissolveParams.x", 0);  // Mode=0 = 关闭溶解!

2.4 噪点纹理要求

参数 推荐值 说明
尺寸 256x256 或 512x512 足够覆盖 UV
格式 RGBA32 需要多通道支持边缘颜色
类型 Perlin/Voronoi Noise 避免规则图案
边缘 Seamless 循环 防止接缝突兀

3. 材质隔离(必须!)

道支持边缘颜色 | | 类型 | Perlin/Voronoi Noise | 避免规则图案 | | 边缘 | Seamless 循环 | 防止接缝突兀 |


3. 材质隔离(必须!)### 3.1 为什么必须隔离

原版模型材质通常被多个部件共用。例如 Cazalis 的 Cazalis_Cloth.mat 被以下 7 个部件共享:

Cazalis_Cloth.mat (共享材质)
  ├─ Cloth_Dress (连衣裙)
  ├─ Cloth_DressRibbon (裙蝴蝶结)
  ├─ Cloth_DressSkirt (裙摆)
  ├─ Cloth_HairRibbon (头发蝴蝶结)
  ├─ Cloth_Socks (袜子)
  ├─ Cloth_Under_Bra (内衣) ← 需要独立溶解
  └─ Cloth_Under_Shorts (内裤) ← 需要独立溶解

不隔离的后果: - 修改内衣溶解 → 裙子一起消失 - 修改裙子颜色 → 袜子也变色

3.2 隔离流程

// 1. 创建独立材质副本
var origMat = AssetDatabase.LoadAssetAtPath<Material>("Assets/.../Cazalis_Cloth.mat");
var braMat = Object.Instantiate(origMat);  // 实例化(不是复制文件)
AssetDatabase.CreateAsset(braMat, "Assets/.../Cazalis_Underwear_Bra.mat");

var shortsMat = Object.Instantiate(origMat);
AssetDatabase.CreateAsset(shortsMat, "Assets/.../Cazalis_Underwear_Shorts.mat");

// 2. 分配到目标网格
var braMesh = root.transform.Find("Cloth_Under_Bra");
braMesh.GetComponent<SkinnedMeshRenderer>().sharedMaterial = braMat;

var shortsMesh = root.transform.Find("Cloth_Under_Shorts");
shortsMesh.GetComponent<SkinnedMeshRenderer>().sharedMaterial = shortsMat;

// 3. 配置溶解参数(仅影响独立副本)
braMat.SetFloat("_TransparentMode", 1);
braMat.SetVector("_DissolveParams", new Vector4(1, 0, 0, 0.1f));
braMat.SetTexture("_DissolveNoiseMask", dissolveNoiseTex);

4. 内衣/内裤 Toggle 实现

4.1 参数命名规范

✅ 推荐命名:
   Cazalis_UW_Bra     (内衣)
   Cazalis_UW_Shorts  (内裤)
   Cazalis_Cloth_Dress (整套裙子)

❌ 禁止命名(与原版冲突):
   Bra / Shorts / Dress / Toggle / IsToggled
(内裤) Cazalis_Cloth_Dress (整套裙子)

❌ 禁止命名(与原版冲突): Bra / Shorts / Dress / Toggle / IsToggled ```### 4.2 AnimationClip 创建

需要 4 个 AnimationClip:

文件名 属性 含义
Bra_ON.anim _DissolveParams.z 0 穿着可见
Bra_OFF.anim _DissolveParams.z 1 完全溶解
Shorts_ON.anim _DissolveParams.z 0 穿着可见
Shorts_OFF.anim _DissolveParams.z 1 完全溶解
// 创建 Bra_ON.anim
var clip = new AnimationClip();
clip.name = "Bra_ON";
clip.legacy = false;
clip.frameRate = 60;

var curve = new AnimationCurve();
curve.AddKey(0f, 0f);  // _DissolveParams.z = 0 (穿着)
curve.AddKey(0.3f, 0f); // 保持 0.3 秒

// 关键!绑定到 SkinnedMeshRenderer.material
var binding = new EditorCurveBinding {
    path = "Cloth_Under_Bra",           // GameObject 路径
    type = typeof(SkinnedMeshRenderer), // 组件类型
    propertyName = "material._DissolveParams.z"  // 材质属性 + 分量
};
AnimationUtility.SetEditorCurve(clip, binding, curve);

AssetDatabase.CreateAsset(clip, "Assets/.../Dissolve/Bra_ON.anim");

4.3 Animator 状态机

Underwear_Dissolve Layer (weight=1.0, Additive)
├── Bra_ON ──[Bra=true, 0.3s]──▶ Bra_OFF
├── Bra_OFF ──[Bra=false, 0.3s]──▶ Bra_ON
├── Shorts_ON ──[Shorts=true, 0.3s]──▶ Shorts_OFF
└── Shorts_OFF ──[Shorts=false, 0.3s]──▶ Shorts_ON
Shorts_ON ──[Shorts=true, 0.3s]──▶ Shorts_OFF └── Shorts_OFF ──[Shorts=false, 0.3s]──▶ Shorts_ON ```### 4.4 MA MergeAnimator 配置

// 创建独立 AnimatorController
var ctrl = new AnimatorController();
ctrl.AddParameter("Cazalis_UW_Bra", AnimatorControllerParameterType.Bool);
ctrl.AddParameter("Cazalis_UW_Shorts", AnimatorControllerParameterType.Bool);

// 添加 Layer (weight=1, Additive)
var layer = new AnimatorControllerLayer();
layer.name = "Underwear_Dissolve";
layer.defaultWeight = 1.0f;
layer.stateMachine = new AnimatorStateMachine();

// 添加状态和过渡...
ctrl.AddLayer(layer);

AssetDatabase.CreateAsset(ctrl, "Assets/.../UnderwearDissolve_Merge.controller");

// 添加 MA MergeAnimator 组件
var maType = System.Type.GetType("nadena.dev.modular_avatar.core.ModularAvatarMergeAnimator");
var mergeAnimator = root.AddComponent(maType);
var so = new UnityEditor.SerializedObject(mergeAnimator);
so.FindProperty("animator").objectReferenceValue = ctrl;
so.FindProperty("layerType").intValue = 4;   // FX
so.FindProperty("type").intValue = 0;         // Additive
so.FindProperty("pathMode").intValue = 0;     // AvatarRoot
so.ApplyModifiedProperties();

5. 整套衣服穿透 Toggle

5.1 与内衣的区别

特性 内衣/内裤 整套衣服
目标对象 单一部件 多个部件组合
溶解时机 同时溶解 可选择同时或分组
切换服装 不涉及 涉及换装
--- ---------- ----------
目标对象 单一部件 多个部件组合
溶解时机 同时溶解 可选择同时或分组
切换服装 不涉及 涉及换装

整套衣服的穿透开关实际上是一个"快速卸装"按钮,可以同时溶解多个部件:

// AnimationClip: AllCloth_OFF.anim
// 同时动画多个对象的 _DissolveParams.z
var binding1 = new EditorCurveBinding {
    path = "Cloth_Dress",
    type = typeof(SkinnedMeshRenderer),
    propertyName = "material._DissolveParams.z"
};
AnimationUtility.SetEditorCurve(clip, binding1, curve); // 值从 0 → 1

var binding2 = new EditorCurveBinding {
    path = "Cloth_DressRibbon",
    type = typeof(SkinnedMeshRenderer),
    propertyName = "material._DissolveParams.z"
};
AnimationUtility.SetEditorCurve(clip, binding2, curve);

6. 多套服装切换系统

6.1 设计思路

多套服装切换的关键是:同一时间只有一套服装的溶解值为 0(可见),其他套为 1(溶解)

服装套数命名示例:
   Outfit_School (校服)     → 参数: Outfit_School
   Outfit_Cafe   (咖啡装)   → 参数: Outfit_Cafe
   Outfit_Swimsuit (泳装)   → 参数: Outfit_Swimsuit

6.2 状态机设计

Outfit_Selector Layer (weight=1.0)
├── School_Visible ──[School=true]──▶ School_Visible
├── School_Hidden ──[School=false]──▶ School_Hidden
├── Cafe_Visible ──[Cafe=true]──▶ Cafe_Visible  
├── Cafe_Hidden ──[Cafe=false]──▶ Cafe_Hidden
└── Swimsuit_Visible ──[Swimsuit=true]──▶ Swimsuit_Visible
    Swimsuit_Hidden ──[Swimsuit=false]──▶ Swimsuit_Hidden

切换逻辑: 点击 "校服" → 动画所有其他套溶解(1→1) + 校服显现(1→0)

6.3 材质管理

每套服装需要独立的材质实例:

材质结构:
   Outfit_School_Mat    (仅给校服部件用)
   Outfit_Cafe_Mat      (仅给咖啡装部件用)
   Outfit_Swimsuit_Mat  (仅给泳装部件用)

7. 非本素体发型/衣服整合

件用) Outfit_Cafe_Mat (仅给咖啡装部件用) Outfit_Swimsuit_Mat (仅给泳装部件用)

---

## 7. 非本素体发型/衣服整合### 7.1 整合流程总览
第三方模型包 (FBX/VRM) ↓ 在 Unity 中导入 ↓ 找到发型/衣服网格 ↓ Parent to 目标骨骼 (例如 "J_Head" 或 "J_Spine") ↓ 调整位置/旋转/缩放 ↓ 配置 PhysBone (如果是头发/裙子) ↓ 配置材质 (独立副本) ↓ 集成到 Toggle 系统
### 7.2 骨骼挂载

#### 发型挂载点

| 发型类型 | 挂载骨骼 | 说明 |
|----------|----------|------|
| 短发 | J_Head | 直接在头上 |
| 中长发 | J_Head 或 J_Sec_Fcl_HairBack1 | 考虑头发重量 |
| 长发 | J_Spine 或专用 Hair Root | 需要 PhysBone |

```csharp
// 找到目标骨骼
var targetBone = root.transform.Find("J_Head");

// 加载第三方发型
var hairPrefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/.../Hair_Pack/Hair_Long.prefab");
var hairInstance = Object.Instantiate(hairPrefab);

// 挂载到骨骼
hairInstance.transform.SetParent(targetBone);

// 调整本地位置(根据模型调整)
hairInstance.transform.localPosition = new Vector3(0, 0.05f, 0);
hairInstance.transform.localRotation = Quaternion.identity;
hairInstance.transform.localScale = Vector3.one;
.transform.localRotation = Quaternion.identity; hairInstance.transform.localScale = Vector3.one; ```#### 衣服挂载点

衣服类型 挂载骨骼 说明
上衣 J_Spine 或 J_Chest 跟随躯干
裙子 J_Hips 或专用 Skirt Root 可能需要 PhysBone
裤子 J_Hips 或 J_UpperLeg 跟随髋部
// 找到衣服挂载点
var clothRoot = root.transform.Find("J_Spine"); // 或专用 "Cloth_Root"

// 加载第三方衣服
var clothPrefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/.../Cloth_Pack/Dress.prefab");
var clothInstance = Object.Instantiate(clothPrefab);

clothInstance.transform.SetParent(clothRoot);
clothInstance.transform.localPosition = Vector3.zero;
clothInstance.transform.localRotation = Quaternion.identity;

7.3 PhysBone 配置(发型/裙子)

第三方发型/裙子通常没有 PhysBone,需要手动添加:

// 为头发添加 PhysBone
var hairRoot = hairInstance.transform.Find("Hair_Root"); // 或根对象
var pb = hairRoot.gameObject.AddComponent<VRCPhysBone>();
pb.rootBone = "Hair_Root";
pb.pull = 0.4f;
pb.spring = 0.35f;
pb.stiffness = 0.15f;
pb.gravity = 0.08f;
pb.immobileType = VRCPhysBoneBase.ImmobileType.None;

// 配置碰撞
pb.collisionFilter = new VRCPhysBoneBase.CollisionFilter {
    colliders = new List<VRCPhysBoneColliderBase>(),
    collisionCheck = true
};
lisionFilter { colliders = new List(), collisionCheck = true }; ```### 7.4 整合到 Toggle 系统

整合后的发型/衣服需要加入现有的溶解 Toggle 系统:

// 1. 创建独立材质
var hairMat = Object.Instantiate(originalHairMat);
AssetDatabase.CreateAsset(hairMat, "Assets/.../Hair_Long_Mat.mat");
hairInstance.GetComponent<SkinnedMeshRenderer>().sharedMaterial = hairMat;

// 2. 配置溶解参数
hairMat.SetFloat("_TransparentMode", 1);
hairMat.SetVector("_DissolveParams", new Vector4(1, 0, 0, 0.1f));
hairMat.SetTexture("_DissolveNoiseMask", dissolveNoiseTex);

// 3. 添加到 Animator 层(复用现有层或新建)
// 动画绑定路径 = 相对于 AvatarRoot 的路径
// 例如: "Hair_Long/Hair Strand 1"

7.5 整合后验证清单

检查项 验证方法
骨骼绑定正确 移动头部,头发跟随
权重正常 不应有穿模或脱节
PhysBone 物理 头发/裙子应自然摆动
溶解 Toggle 点击按钮后正确溶解/显现

8. Expression Menu 配置

8.1 MA MenuItem 配置

通过 MCP 或手动添加 MA MenuItem:

// 创建菜单项
var menuItem = root.transform.Find("Outfit_Menu").gameObject;
var mi = menuItem.AddComponent<maMenuItemType);

// Toggle 配置
var so = new UnityEditor.SerializedObject(mi);
so.FindProperty("m_control.type").enumValueIndex = 1; // Toggle
so.FindProperty("m_parameter.name").stringValue = "Cazalis_UW_Bra";
so.FindProperty("label").stringValue = "内衣";
so.FindProperty("isDefault").boolValue = true;  // 默认穿着
so.ApplyModifiedProperties();
ue = "内衣"; so.FindProperty("isDefault").boolValue = true; // 默认穿着 so.ApplyModifiedProperties(); ```### 8.2 Menu 结构

根菜单
├── 内衣 (Toggle, Param=Cazalis_UW_Bra)
├── 内裤 (Toggle, Param=Cazalis_UW_Shorts)
├── 整套 (Toggle, Param=Cazalis_Cloth_All)
└── 服装切换 (SubMenu)
    ├── 校服 (Toggle, Param=Outfit_School)
    ├── 咖啡装 (Toggle, Param=Outfit_Cafe)
    └── 泳装 (Toggle, Param=Outfit_Swimsuit)

8.3 参数容量

VRC Expression Parameters 上限 256 bits:

类型 大小
Bool 1 bit
Int 8 bits
Float 8 bits

多套服装 + 内衣内裤需要计算好参数数量。


9. Animator 状态机设计

9.1 层结构

FX Controller
├── Base Layer (原版)
├── Underwear_Dissolve Layer (MA MergeAnimator, weight=1, Additive)
│   ├── Bra_ON / Bra_OFF
│   └── Shorts_ON / Shorts_OFF
└── Outfit_Selector Layer (MA MergeAnimator, weight=1, Additive)
    ├── School_ON / School_OFF
    ├── Cafe_ON / Cafe_OFF
    └── Swimsuit_ON / Swimsuit_OFF

9.2 Transition 配置

参数 推荐值 说明
Has Exit Time False 点击立即响应
Transition Duration 0.3s 溶解速度
Fixed Duration True 恒定过渡时间

9.3 Default State

默认状态必须是 ON(穿着/可见)

Layer defaultState = Bra_ON  (穿着内衣)
Layer defaultState = Shorts_ON (穿着内裤)

10. 常见坑点与排错

10.1 溶解不生效

可能原因 检查方法 解决方案
_TransparentMode 不是 1 检查材质 Inspector mat.SetFloat("_TransparentMode", 1)
_DissolveParams.x 不是 1 检查材质 Inspector _DissolveParams.x 必须为 1
动画绑定到错误的分量 检查 AnimationClip 曲线 绑定 _DissolveParams.z
材质被其他对象共享 检查 SMR sharedMaterial 材质隔离
到错误的分量 检查 AnimationClip 曲线 绑定 _DissolveParams.z
材质被其他对象共享 检查 SMR sharedMaterial 材质隔离
可能原因 解决方案
参数名与原版冲突 使用唯一前缀(如 Cazalis_
MA MergeAnimator 未添加 正确添加组件并配置
FX 层未启用 在 AvatarDescriptor 中启用 FX 层
ObjectToggle 与溶解冲突 移除 ObjectToggle,仅用 Animator

10.3 溶解方向反了

现象 原因 解决
点击后反而脱掉 ON 动画的 _DissolveParams.z = 1 ON 应该是 0,OFF 应该是 1
默认状态是脱掉的 Default State 设成了 OFF Default State 设为 ON

10.4 第三方发型/衣服位置错

问题 解决方案
位置偏移 调整 localPosition
旋转错误 调整 localRotation
缩放不对 调整 localScale 或重新导入
穿模 添加/调整 PhysBone Collider

11. MCP 操作流程

11.1 前提条件

  1. Unity MCP 服务器已配置
  2. 会话已建立
  3. 目标 Avatar 已导入 Unity

11.2 标准操作顺序

1. 材质隔离
   └── 创建材质副本 → 分配到目标网格 → 配置溶解参数

2. 创建噪点纹理
   └── 生成 Perlin Noise PNG → 导入 Unity → 分配到材质

3. 创建 AnimationClip
   └── 4个Clip (Bra/Shorts ON/OFF) → 绑定 _DissolveParams.z

4. 配置 Animator
   └── 创建独立 Controller → 添加参数/层/状态/过渡

5. 添加 MA MergeAnimator
   └── 指向独立 Controller → layerType=FX, type=Additive

6. 配置 Menu
   └── 添加 MA MenuItem → 设置 Toggle 参数 → 设置 isDefault=true

7. 构建测试
   └── NDMF Build → 上传 VRChat → 游戏内测试

11.3 MCP 关键代码片段

oggle 参数 → 设置 isDefault=true

  1. 构建测试 └── NDMF Build → 上传 VRChat → 游戏内测试
    ### 11.3 MCP 关键代码片段#### 材质隔离
    ```csharp
    var orig = AssetDatabase.LoadAssetAtPath<Material>("Assets/.../Cazalis_Cloth.mat");
    var braMat = Object.Instantiate(orig);
    AssetDatabase.CreateAsset(braMat, "Assets/.../Cazalis_Underwear_Bra.mat");
    braMesh.GetComponent<SkinnedMeshRenderer>().sharedMaterial = braMat;
    braMat.SetFloat("_TransparentMode", 1);
    braMat.SetVector("_DissolveParams", new Vector4(1, 0, 0, 0.1f));
    braMat.SetTexture("_DissolveNoiseMask", dissolveNoiseTex);
    

创建动画

var clip = new AnimationClip();
clip.name = "Bra_ON"; clip.legacy = false;
var curve = new AnimationCurve();
curve.AddKey(0f, 0f);
var binding = EditorCurveBinding.FloatCurve("Cloth_Under_Bra", typeof(SkinnedMeshRenderer), "material._DissolveParams.z");
AnimationUtility.SetEditorCurve(clip, binding, curve);
AssetDatabase.CreateAsset(clip, "Assets/.../Bra_ON.anim");

添加 MA MergeAnimator

var maType = System.Type.GetType("nadena.dev.modular_avatar.core.ModularAvatarMergeAnimator");
var merge = root.AddComponent(maType);
var so = new UnityEditor.SerializedObject(merge);
so.FindProperty("animator").objectReferenceValue = ctrl;
so.FindProperty("layerType").intValue = 4;
so.FindProperty("type").intValue = 0;
so.FindProperty("pathMode").intValue = 0;
so.ApplyModifiedProperties();

ype").intValue = 0; so.FindProperty("pathMode").intValue = 0; so.ApplyModifiedProperties(); ```

---## 附录 A:参考文档

附录 B:关键参数速查

用途 属性
启用溶解 _DissolveParams.x 1
溶解进度 _DissolveParams.z 0→1
边缘柔和 _DissolveParams.w 0.1
Cutout 模式 _TransparentMode 1
噪点纹理 _DissolveNoiseMask Texture
噪点强度 _DissolveNoiseStrength 0.05~0.1

本文档整合自 VRChat 模体修改实战知识库 | 最后更新: 2026-05-09