跳转至

name: vrchat-clothing-dissolve-workflow category: gaming description: 完整的 VRChat 换装+溶解工作流 — 从录制 ON/OFF/Hide 动画、配置 lilToon 材质、搭建 4 状态 Animator Layer、到 Int 参数菜单联动。基于 Cazalis zigai 项目实战验证,包含每步的具体参数数值和关键帧数据。 trigger: 用户说"有新衣服要做溶解切换"、"加一套新衣服"、"做换装溶解"、"照着这个再做一套" load_order: | 1. 加载本 skill 2. 也加载 vrchat-toggle-parameter-linkage (菜单/参数链路) 3. 也加载 liltoon-dissolve-material-batch (顽固材质处理)


VRChat 换装 + 溶解完整工作流(AI 操作版)

已验证项目:Cazalis zigai,FX Controller 有 27 个层 现有 4 套衣服:原皮(Origin)、露肩短裙(Lujian)、休闲服(Casual/SilentSkinPeek)、开衫毛衣(Cardigan/Cloud) 核心参数taozhuang = Int, saved=true, default=0 值映射:0=原皮, 1=露肩短裙, 2=休闲服, 3=开衫毛衣, 4=娃花, 5=Blanchir, 6=HonmeiKnit(01_Black) 已有 4S_ 层:l18=4S_Origin, l19=4S_Lujian, l20=4S_Casual, l21=4S_Cardigan, l22=4S_Wahua, l25=4S_Blanchir, l26=4S_Underwear, l27=4S_HonmeiKnit

⚠️ 该项目同时存在两套 clip 系统: - 旧 Dissolve 层(两状态):clip 命名带 _Dissolve_,用方案 B(渐变 clip) - 4S 状态循环层:clip 命名纯英文(yuanpi_/lujian_/Casual_/Cardigan_),用方案 A(1 帧 clip + transition 渐变) - 新衣服统一用 4S 状态循环方案(方案 A)


lujian_/Casual_/Cardigan_),用方案 A(1 帧 clip + transition 渐变)

  • 新衣服统一用 4S 状态循环方案(方案 A)

---## 架构总览

每套衣服一个独立的 Animator Layer (Override, weight=1, writeDefaultValues=A=true/B/C/D=false)
每个 Layer 4 个状态:

  A_Closed (关闭/隐藏) ──taozhuang==N──→ B_Appearing (溶解出现)
       ↑                                      │
       │  taozhuang != N (打断)            HasExitTime 播完
       │                                      ↓
  D_Disappearing (溶解消失) ←──taozhuang!=N── C_Opened (开启/显示)
       │                                      │
       └── taozhuang==N (打断) ───────────────┘

参数链路: 菜单 Toggle(taozhuang) → VRCExpressionParameters → FX Controller 条件 → AnimationClip

完整操作流程一览

新衣服要加溶解切换
[Step 1] 找SMR路径 ─── 列出新衣服所有部件路径
[Step 2] 配材质 ─── 批量设lilToon溶解参数(含检查渲染模式)
[Step 3] 录动画 ─── 录制ON/OFF/Hide 三个AnimationClip
[Step 4] 建Layer ─── 用C#创建4状态Animator Layer
[Step 5] 更新taozhuang ─── 理解并行机制,检查值冲突
[Step 6] 加菜单 ─── 添加Button控件到ClothType_未分类
[Step 7] 验证 ─── 结构验证 + Play Mode 实际测试

Step 1: 收集新套装 SMR 路径

目标:确定新套装涉及哪些 SkinnedMeshRenderer(SMR)和 GameObject,获取它们的完整路径。

---

## Step 1: 收集新套装 SMR 路径

**目标**:确定新套装涉及哪些 SkinnedMeshRenderer(SMR)和 GameObject,获取它们的完整路径。### MCP 执行代码

```csharp
var avatar = GameObject.Find("Cazalis zigai");
if (avatar == null) { Debug.LogError("Avatar not found!"); return; }

var smrs = avatar.GetComponentsInChildren<SkinnedMeshRenderer>(true);
var sb = new System.Text.StringBuilder();
sb.AppendLine("=== All SMR paths ===");
foreach (var smr in smrs) {
    // 构建 avatar 相对路径
    var parts = new List<string>();
    var p = smr.transform;
    while (p != null && p != avatar.transform) { 
        parts.Insert(0, p.name); 
        p = p.parent; 
    }
    string path = string.Join("/", parts);

    // 列出每个SMR的材质信息
    string mats = "";
    foreach (var m in smr.sharedMaterials) {
        if (m != null) mats += (string.IsNullOrEmpty(mats) ? "" : ", ") + m.name;
    }

    // 按关键词过滤(用户替换"关键字"为套装名特征词)
    if (path.Contains("关键字")) {
        sb.AppendLine("  PATH: " + path);
        sb.AppendLine("  MATS: " + mats);
        sb.AppendLine("  SHADER: " + (smr.sharedMaterials.Length > 0 && smr.sharedMaterials[0] != null 
            ? smr.sharedMaterials[0].shader.name : "N/A"));
        sb.AppendLine();
    }
}
Debug.Log(sb.ToString());

手动替代:在 Hierarchy 中找新衣服的 GameObject,右键 → Copy Path Line(); } } Debug.Log(sb.ToString());

**手动替代**:在 Hierarchy 中找新衣服的 GameObject,右键 → Copy Path### 需要记录的信息

| 信息 | 说明 |
|:---|:----|
| SMR 路径列表 | 如 `Cloth_Dress`, `Cloth_Socks`, `Cazalis4 Variant/Skirt` 等 |
| 材质列表 | 每个 SMR 的 sharedMaterial 路径 |
| 材质 shader | 必须是 lilToon,且渲染模式不是 Opaque |

**参考:现有套装的实际 SMR 路径:**

| 套装 | SMR 路径(数量) |
|:----|:---------------|
| 原皮 (Origin) | `Cloth_Dress`, `Cloth_DressRibbon`, `Cloth_DressSkirt`, `Cloth_HairRibbon`, `Cloth_Pumps`, `Cloth_Socks`, `Cloth_Under_Shorts`, `Hair_Accessories`, `Hair_Earring`(8个SMR, 42条曲线)|
| 露肩短裙 (Lujian) | `Cloth_Under_Shorts`, `Hair_Earring`, `Cazalis4 Variant/Garter/Niso/Niso_Lace/Outer/Shoes/Shoulder/Skirt/SkirtUnder/Choker/Necklace`(12个SMR, 48条曲线)|
| 休闲服 (Casual) | `SilentSkinPeek_Cazalis_Stripe_White/Belt/blouse/earring/pants/sandal`(5个SMR, 10条曲线)|
| 开衫毛衣 (Cardigan) | `Cloth_Under_Bra`, `Cloud/Boots/Outer/Outer_Ribbon/Pants/Socks`, `Cloth_Dress`(8个SMR, 32条曲线)|
| Blanchir | `Blanchir_Cazalis/Blanchir_inner`, `Blanchir_Cazalis/Blanchir_skirt`, `Blanchir_Cazalis/Blanchir_top`(3个SMR)|

---

## Step 2: 配置 lilToon 材质溶解参数
_Cazalis/Blanchir_skirt`, `Blanchir_Cazalis/Blanchir_top`(3个SMR)|

---

## Step 2: 配置 lilToon 材质溶解参数### lilToon Dissolve 原理(必读)

lilToon 的溶解效果是通过 Fragment Shader 中的 `lilCalcDissolve()` 函数(`lil_common_functions.hlsl` 第 626-666 行)实现的:
dissolveAlpha = fwidth(...) 或 distance(uv, dissolvePos.xy) 或 positionOS // 计算出一个 0~1 的「溶解进度因子」 finalAlpha = dissolveAlpha > _DissolveParams.z ? 0 : 1 // 如果溶解因子大于阈值 z → 透明(隐藏) // 如果溶解因子小于阈值 z → 显示
简单来说:**`_DissolveParams.z` 是阈值门槛**,值越大需要越强的「溶解因子」才能跨过去并隐藏。

> ⚠️ **重要概念**:lilToon 溶解是「阈值比较」机制,不是透明度渐变。溶解边缘的渐变效果来自 `_DissolveParams.w`(边缘宽度)和噪点纹理的随机值。如果 `_TransparentMode=1(Cutout)`,溶解区域完全透明(硬切);如果 `_TransparentMode=2(Transparent)`,溶解区域半透明渐变。

### 完整参数详解
_TransparentMode=1(Cutout)`,溶解区域完全透明(硬切);如果 `_TransparentMode=2(Transparent)`,溶解区域半透明渐变。

### 完整参数详解#### `_DissolveParams` 四分量详解(源码级)

这个参数控制溶解的全部核心行为。是 Vector4 类型,4 个分量各有不同用途:

| 分量 | Shader 字段 | 用途 | 本项目值 | 调低效果 | 调高效果 | 可动画? | 备注 |
|:---:|:----------|:----|:--------|:--------|:--------|:-------:|:----|
| `.x` (r) | `dissolveParams.r` | **溶解形状模式** | **2** (点形) | 0=禁用(无溶解), 1=遮罩(基于贴图R通道) | 3=世界坐标(基于物体空间位置) | ❌ 只能设整数 | 被 `round()` 取整 |
| `.y` (g) | `dissolveParams.g` | **形状子模式** | **0** (径向) | — | 1=旋转线性(沿着轴方向扫描) | ❌ 只能设整数 | 被 `round()` 取整 |
| `.z` (b) | `dissolveParams.b` | **溶解进度阈值** | **-0.5~1.7** | 值越小越容易显示(-0.5=全显) | 值越大越容易隐藏(1.7=全隐) | ✅ **主动画目标** | 范围约 -0.5~2.0 |
| `.w` (a) | `dissolveParams.a` | **边缘宽度/模糊** | **0.1** | 值越小边缘越锐利(0=锯齿硬边) | 值越大边缘越宽(0.5=宽模糊边) | ✅ 可动画 | 不要设 0,不要超过 0.5 |

**形状模式详解(x 分量):**

| x 值 | 名称 | 计算公式 | 效果 | 适用场景 |
|:---:|:----|:--------|:----|:--------|
| **0** | 禁用 | — | 溶解模块完全跳过 | 不需要溶解的材质 |
| **1** | 遮罩 | `tex.r` | 基于 _DissolveMask 纹理的 R 通道 | 需要按贴图形状溶解 |
| **2** | **点(UV)** | `distance(uv, dissolvePos.xy)` | 从 _DissolvePos 位置开始往外溶解 | ⭐ 当前项目采用 | 
| **3** | 世界坐标 | `distance(positionOS, dissolvePos.xyz)` | 基于物体空间位置远近溶解 | 网格不均匀时有差异 |

**子模式详解(y 分量,仅对 x=2 点形有效):**

| y 值 | 名称 | 效果 |
|:---:|:----|:----|
| **0** | 径向 | 从中心点向外圆形扩散溶解(当前项目采用) |
| **1** | 旋转线性 | 沿着某个轴方向线性扫描溶解 |

> 调整 y=1 + 结合 _DissolvePos.w(旋转角度)可实现「从右到左扫描」效果。目前项目用 y=0(径向扩散),不调整。
**1** | 旋转线性 | 沿着某个轴方向线性扫描溶解 |

> 调整 y=1 + 结合 _DissolvePos.w(旋转角度)可实现「从右到左扫描」效果。目前项目用 y=0(径向扩散),不调整。#### `_DissolveParams.z` 的视觉效果(核心)

这是动画中最关键的参数。它的含义是「溶解进度阈值」:
z = -0.5 → 阈值极低,几乎所有像素都 < 阈值 → 全部显示 z = 0.0 → 部分像素达到阈值 → 开始出现溶解 z = 1.0 → 大多数像素达到阈值 → 大部分溶解 z = 1.7 → 阈值极高,几乎所有像素都 ≥ 阈值 → 完全溶解
**实际视觉感受:**
- z 在 -0.5~0.0:完全显示,无变化
- z 在 0.0~1.0:溶解从边缘/底部逐渐消失(渐变过渡区)
- z 在 1.0~1.7:继续溶解到完全消失
- **z > 2.0 是无效值**,shader 计算可能异常(部分残留或卡中间态)

#### `_DissolveParams.w` 的视觉效果(边缘控制)

这个值控制「溶解边缘」的软硬程度:

| w 值 | 效果 | 说明 |
|:---:|:----|:----|
| **0** | ❌ 锯齿硬边 | 溶解边缘每个像素直角切,有锯齿感 |
| **0.1** | ✅ 细软边 | 当前项目值,边缘约 1~2 像素过渡,效果自然 |
| **0.3** | 宽软边 | 边缘较宽,溶解感更柔和 |
| **0.5+** | 模糊边 | 边缘很宽,溶解效果不明显 |

> w=0.1 是当前项目的最佳实践值。噪声纹理的存在已经提供了自然的随机边缘,不需要加大 w。

#### `_DissolvePos` 详解

这个 Vector4 参数决定溶解起始点和方向(仅对 x=2 点形有效):

| 分量 | 用途 | 本项目值 | 效果 |
|:---:|:----|:--------|:----|
| `.x` | 溶解中心 X | **0** | 水平居中 |
| `.y` | 溶解中心 Y | **1** | 从顶部开始溶解(正数=上,负数=下) |
| `.z` | 溶解中心 Z | **0** | 2D UV 溶解用不到 |
| `.w` | 旋转角度 | **0** | y=1 旋转线性模式时有用 |

> **调整 y 的效果**(点形径向模式):溶解不是「一整块消失」,而是从 `_DissolvePos.y` 位置开始扩散
> - y=1: 从衣服顶部开始向下溶解(类似「从上往下消失」)
> - y=0: 从衣服中心统一溶解
> - y=-1: 从底部开始向上溶解
> 
> 当前项目 ON/OFF 都统一用 y=1,确保溶解方向一致(都是从顶部开始)。

#### `_DissolvePos.y` 在动画中的特殊作用

> ⚠️ **重要发现**:在 yuanpi_OFF.anim 中,`_DissolvePos.y` 在 t=0 时设为 -1,但在 ON clip 中没有动画化这个值。这意味着:
> - OFF 时 `_DissolvePos.y` 从默认的 1 变为 -1
> - 溶解方向发生变化:从「从上往下消失」变成「从下往上消失」
> - 这个细节会影响视觉体验,新套装录制时应保持一致
> - OFF 时 `_DissolvePos.y` 从默认的 1 变为 -1
> - 溶解方向发生变化:从「从上往下消失」变成「从下往上消失」
> - 这个细节会影响视觉体验,新套装录制时应保持一致#### 其他溶解参数

| 参数 | 用途 | 本项目值 | 调整效果 |
|:----|:----|:--------|:--------|
| `_DissolveNoiseMask` | 噪点纹理 | `Cazalis_Reflection_Noise.png` | 提供溶解边缘的随机变化。如果不设 → 溶解边缘太平滑/太规则 |
| `_DissolveNoiseStrength` | 噪点强度 | **0.3** | 值越大溶解边缘越不规则、越「破碎」。0=无噪点(平滑边缘),1=极度不规则 |
| `_DissolveColor` | 溶解边缘颜色 | **#387AE9** (蓝色) | 溶解过渡区会显示这个颜色。设为透明色或黑色会看不到过渡效果 |
| `_TransparentMode` | 渲染模式 | **1(Cutout) 或 2(Transparent)** | ❗详见下方说明 |
区会显示这个颜色。设为透明色或黑色会看不到过渡效果 |
| `_TransparentMode` | 渲染模式 | **1(Cutout) 或 2(Transparent)** | ❗详见下方说明 |#### Shader 变体和 Keywords 详解

lilToon 使用 Shader Variant 系统,不同的渲染模式和特性需要在不同的 Shader 中:
材质 Inspector 顶部 Shader 选择: lilToon → 不透明 (Opaque) → ❌ 溶解无效 Hidden/lilToonCutout → 镂空 (Cutout) → ✅ 溶解有效(硬边透明) Hidden/lilToonTransparent → 透明 (Transparent) → ✅ 溶解有效(半透明) Hidden/lilToonTwoPassTransparent → 透明双面 → ✅ 溶解有效(双面渲染)

或者通过 _TransparentMode 切换(在同一 shader 内部): _TransparentMode = 0 → Opaque(默认) _TransparentMode = 1 → Cutout(常用溶解) _TransparentMode = 2 → Transparent(半透明溶解)

**关键 Keyword:`GEOM_TYPE_BRANCH_DETAIL`**
这个 keyword 控制 lilToon 的「溶解模块」是否被编译到 shader variant 中。 如果不启用 → 即使 _DissolveParams.x > 0 也不生效。 如果材质不是使用 Cutout/Transparent shader variant → 即使 keyword 启用也不生效。 两者缺一不可!
**总结:溶解生效的 3 个必要条件(缺一不可!)**

| # | 条件 | 检查方法 |
|:-:|:----|:--------|
| ① | 渲染模式是 Cutout(1) 或 Transparent(2) | `mat.GetFloat("_TransparentMode") != 0` |
| ② | Shader Keyword 已启用 | `mat.IsKeywordEnabled("GEOM_TYPE_BRANCH_DETAIL")` |
| ③ | `_DissolveParams.x > 0` (典型值 2) | `mat.GetFloat("_DissolveParams.x") == 2f` |

> 最常见的故障:材质是 Opaque(条件①不满足)→ 溶解代码被 shader 编译器跳过 → 无论怎么动画 _DissolveParams.z 都没效果。
arams.x") == 2f` |

> 最常见的故障:材质是 Opaque(条件①不满足)→ 溶解代码被 shader 编译器跳过 → 无论怎么动画 _DissolveParams.z 都没效果。### 完整参数速查表

| 参数 | 用途 | 本项目值 | 调低效果 | 调高效果 |
|:----|:----|:--------|:--------|:--------|
| `_DissolveParams.x` | 溶解形状 | **2** (点) | 0=禁用, 1=遮罩 | 3=世界坐标 |
| `_DissolveParams.y` | 子模式 | **0** (径向) | — | 1=旋转线性 |
| `_DissolveParams.z` | 溶解进度 | **-0.5(显)~1.7(溶)** | 更容易显示 | 更容易隐藏 |
| `_DissolveParams.w` | 边缘宽度 | **0.1** | 0=锯齿硬边 | >0.5=过度模糊 |
| `_DissolvePos` | 溶解起点 | **X=0, Y=1** (Vector4) | Y负=从下往上溶 | Y正=从上往下溶 |
| `_DissolveNoiseMask` | 噪点纹理 | `Cazalis_Reflection_Noise.png` | 无=边缘平滑 | 不同纹理=不同随机样式 |
| `_DissolveNoiseStrength` | 噪点强度 | **0.3** | 0=无噪点(规则边缘) | 1=极不规则(破碎) |
| `_DissolveColor` | 边缘颜色 | **#387AE9** | — | — |
| `_TransparentMode` | 渲染模式 | **1(Cutout) 或 2(Transparent)** | 0=Opaque(❌不溶解) | — |
7AE9** | — | — |
| `_TransparentMode` | 渲染模式 | **1(Cutout) 或 2(Transparent)** | 0=Opaque(❌不溶解) | — |### 批量配置代码

```csharp
var noiseTex = AssetDatabase.LoadAssetAtPath<Texture2D>(
    "Assets/Cazalis/Cazalis_Reflection_Noise.png");
var color = new Color(0.2196f, 0.4784f, 0.9137f, 1.0f); // #387AE9
var avatar = GameObject.Find("Cazalis zigai");
var smrs = avatar.GetComponentsInChildren<SkinnedMeshRenderer>(true);
var done = new HashSet<Material>();

// ← 此处填 Step 1 得到的 SMR 路径列表
string[] clothPaths = { "Cloth_New_Dress", "Cloth_New_Shoes" };

foreach (var smr in smrs) {
    var path = GetAvatarPath(smr, avatar); // 需要实现
    if (!clothPaths.Contains(path)) continue;

    foreach (var mat in smr.sharedMaterials) {
        if (mat == null || done.Contains(mat)) continue;
        done.Add(mat);

        mat.SetFloat("_DissolveParams.x", 2f);
        mat.SetFloat("_DissolveParams.y", 0f);
        mat.SetFloat("_DissolveParams.z", 1.7f);  // 默认完全溶解
        mat.SetFloat("_DissolveParams.w", 0.1f);
        mat.SetVector("_DissolvePos", new Vector4(0f, 1f, 0f, 0f));
        mat.SetTexture("_DissolveNoiseMask", noiseTex);
        mat.SetFloat("_DissolveNoiseStrength", 0.3f);
        mat.SetColor("_DissolveColor", color);
        mat.EnableKeyword("GEOM_TYPE_BRANCH_DETAIL");
        EditorUtility.SetDirty(mat);
    }
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
EditorUtility.SetDirty(mat); } } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); ```### 顽固材质处理

当 SetFloat 不生效时(常见于 Assets/choco/*, Assets/AyuElla/*, Assets/Rest/Awakoi_Code/*):

⚠️ 文件编辑必须同时处理以下所有项(容易遗漏噪点图!): 1. _DissolveParams → {r: 2, g: 0, b: 1.7, a: 0.1} 2. m_Shader → 改为 Cutout/Transparent 变体 GUID 3. m_ValidKeywords + m_ShaderKeywords → 包含 GEOM_TYPE_BRANCH_DETAIL 4. _DissolveNoiseMask → 噪点图(Cazalis_Reflection_Noise, GUID: e21822f135616e54a8b13f8cd408a826) 5. _DissolveNoiseStrength → 0.3 6. _DissolvePos → {r: 0, g: 1, b: 0, a: 0} 7. _DissolveColor → {r: 0.22, g: 0.48, b: 0.91, a: 1}

没有噪点图时溶解边缘是直切的非常生硬!

var fullPath = Path.GetFullPath(Application.dataPath + "/../" + assetPath);
var content = File.ReadAllText(fullPath);
content = Regex.Replace(content,
    @"_DissolveParams: \\{r: [^,]+,\\s*g: [^,]+,\\s*b: [^,]+,\\s*a: [^}]+\\}",
    "_DissolveParams: {r: 2, g: 0, b: 1.7, a: 0.1}");
File.WriteAllText(fullPath, content);
AssetDatabase.Refresh();

Step 3: 录制 ON / OFF / Hide 动画

本项目采用方案 A(1帧 clip + Animator transition 实现渐变): - ON clip 只有 1 帧,设好目标值(z=-0.5, m_IsActive=true) - OFF clip 有 2 帧(t=0 设 z=1.7, t=0.0167 设 m_IsActive=false) - Hide clip 有 1 帧(z=1.7, m_IsActive=false,恒定值) - 真正的 1 秒渐变效果来自 Animator 的 Transition Duration=1.0s Hide clip 有 1 帧(z=1.7, m_IsActive=false,恒定值) - 真正的 1 秒渐变效果来自 Animator 的 Transition Duration=1.0s### 4.0 为什么要录这些参数?(原理说明)

每个 clip 录制的参数和关键帧都有明确作用:

参数 在 clip 中为什么要录 不录会怎样
m_IsActive 控制 GameObject 的启用/禁用。ON=显示, OFF=隐藏 衣服永远显示或永远隐藏
_DissolveParams.x 设为 2(点形),确保 shape 正确 可能被其他 clip 覆盖变成 0(禁用) 或 3
_DissolveParams.y 设为 0(径向),确保子模式正确 同上
_DissolveParams.z 核心参数,控制溶解进度 这是关键!必须动画
_DissolveParams.w 设为 0.1,确保边缘宽度稳定 可能被默认值覆盖导致锯齿
_DissolvePos 确保溶解起点坐标正确 OFF 时若 y 变成 0,溶解方向错误

为什么 ON clip 是 1 帧(不是渐变 clip)? - 1 帧的 ON clip 直接设好所有目标值(z=-0.5, m_IsActive=true) - 渐变效果来自 Animator 的 Transition Duration(1 秒) - 在 transition 期间,Animator 在两个状态的 clip 值之间进行 线性插值(crossfade) - 从 A_Closed(z=1.7)切换到 B_Appearing(z=-0.5),中间经过 1 秒 linear interpolation - 这就产生了「z 从 1.7 逐渐降到 -0.5」的过渡视觉效果 Closed(z=1.7)切换到 B_Appearing(z=-0.5),中间经过 1 秒 linear interpolation - 这就产生了「z 从 1.7 逐渐降到 -0.5」的过渡视觉效果### 4.1 ON clip 录制参数

命名规则新套装名_ON.anim(英文,如 NewOutfit_ON.anim

要录制的属性(每个 SMR 都要):

对 每个 SMR GameObject,添加以下曲线:

[GameObject]
  m_IsActive = true(1 帧,t=0)

[SkinnedMeshRenderer > material._DissolveParams]
  _DissolveParams.x = 2(恒定,1 帧)
  _DissolveParams.y = 0(恒定,1 帧)
  _DissolveParams.z = -0.5(显示,1 帧)  ← 关键!
  _DissolveParams.w = 0.1(恒定,1 帧)

clip 属性:长度 = 0s,帧率 = 60

参考:yuanpi_ON.anim 实际数据

长度: 0s, 帧率: 60, 曲线数: 42
SMR列表(8个):
  Hair_Accessories, Cloth_Under_Shorts, Cloth_Socks, Cloth_Pumps,
  Cloth_HairRibbon, Cloth_DressSkirt, Cloth_DressRibbon, Cloth_Dress, Hair_Earring
每个SMR有5条曲线: m_IsActive + _DissolveParams.x/y/z/w (各1帧)
th_DressRibbon, Cloth_Dress, Hair_Earring 每个SMR有5条曲线: m_IsActive + _DissolveParams.x/y/z/w (各1帧) ```> ⚠️ 重要:PhysBone 父节点也需要 m_IsActive!

故障案例(Cazalis zigai Cloud 套装已发现): 该套装的 VRCPhysBone 组件全部挂在 Cloud/PB/ 子节点下。OFF clip 中写了 Cloud/PB/m_IsActive = 0,但如果 ON clip 遗漏Cloud/PB/m_IsActive = 1,PhysBone 节点会保持关闭 → 衣服所有物理飘动失效。

已发现的缺失(Cazalis zigai,2026-05-17): - yuanpi_ON.anim 缺少 Cloud/PB/m_IsActive = 1Cloud/Armature/m_IsActive = 1 ❌ - xiuxian_ON.anim 缺少 PB/m_IsActive = 1 ❌ - ksmy_ON.anim 包含这些曲线 ✅(正常)

修复方案:在 ON clip 中必须额外录制:

Cloud/PB/m_IsActive = 1          (PhysBone 父节点)
Cloud/Armature/m_IsActive = 1     (骨骼父节点)
Cloud/Collider/m_IsActive = 1     (碰撞器父节点)

具体哪些父节点需要,取决于 OFF clip 中 disable 了哪些非 SMR 的 GameObject。规则:OFF clip 里加了 m_IsActive = 0所有非 SMR GameObject,ON clip 都必须有对应的 m_IsActive = 1。找出这些额外 GameObject 的方法是:对比 ON/OFF clip 的 curve 路径,ON 没有但 OFF 有的,逐一补上。 N clip 都必须有对应的 m_IsActive = 1。找出这些额外 GameObject 的方法是:对比 ON/OFF clip 的 curve 路径,ON 没有但 OFF 有的,逐一补上。### 4.2 OFF clip 录制参数

命名规则新套装名_OFF.anim

要录制的属性

对 每个 SMR GameObject,添加以下曲线(两种子方案):

方案 A1(带 m_IsActive + _DissolvePos — 如 yuanpi_OFF):
  2 帧:
    t=0:      _DissolveParams.z = 1.7(完全溶解)
              _DissolvePos.x = 0, .y = 1, .z = 0, .w = 0
    t=0.0167: m_IsActive = false (1/60秒后关闭)
    x,y,w 各 1 帧(恒定): x=2, y=0, w=0.1

方案 A2(只有 _DissolveParams — 如 Cardigan_Dissolve_OFF、Casual_Dissolve_OFF):
  无 m_IsActive,只有 _DissolveParams
  长度 = 1s(渐变 clip 方案,不在本项目范围)

⚠️ 本项目统一用方案 A1:所有 SMR 都要有 m_IsActive(否则衣服溶解后 PhysBone 还在运行)

4.3 Hide clip 录制参数(用于关闭状态 A)

命名规则新套装名_Hide.anim

所有 SMR:
  _DissolveParams.x = 2(1帧恒定)
  _DissolveParams.y = 0(1帧恒定)
  _DissolveParams.z = 1.7(完全溶解)
  _DissolveParams.w = 0.1(1帧恒定)
  m_IsActive = false

参考:Origin_Dissolve_Hide.anim 实际数据

长度: 0s, 曲线数: 32
8个SMR,每个4条 _DissolveParams 曲线(xyzw各1帧)
无 m_IsActive 曲线! ← ⚠️ 原 Hide clip 不控制 m_IsActive
    本项目的新 Hide clip 应该加 m_IsActive 曲线

4.4 手动录制步骤

  1. 在 Hierarchy 选中新套装的一个 SMR GameObject
  2. 按 Ctrl+6 打开 Animation 窗口
  3. 点击 Create,保存到 Assets/Cazalis/Animation/FX/,命名 新套装名_ON.anim
  4. 在 Animation 窗口点击 Add Property:
  5. GameObject → m_IsActive(打勾)
  6. SkinnedMeshRenderer → material._DissolveParams → 展开 xyzw(打勾全部)
  7. 在 t=0 的白色菱形关键帧上,设置 Inspector 中的值:
  8. m_IsActive = true
  9. _DissolveParams.x = 2, y = 0, z = -0.5, w = 0.1
  10. 复制该 SMR 的操作,为每个 SMR重复步骤 3-5
  11. 同理录制 OFF clip(z=1.7, m_IsActive=false)和 Hide clip(z=1.7, m_IsActive=false) 操作,为每个 SMR重复步骤 3-5
  12. 同理录制 OFF clip(z=1.7, m_IsActive=false)和 Hide clip(z=1.7, m_IsActive=false)### 4.5 录制注意事项
问题 解决方案
有的 SMR 录不进去 该 SMR 的材质不是 lilToon 或渲染模式是 Opaque
找不到 material._DissolveParams 检查 shader 是否 lilToon,渲染模式是否 Cutout/Transparent
录制完 _DissolveParams 只出现一个分量 手动添加 x,y,w(只展开了 z)
多个 SMR 共用一个材质 只需要控制一次(clip 按 SMR GameObject 绑定,不是按材质)

Step 4: 搭建 4 状态 Animator Layer(核心)

4.0 为什么是 4 状态?(原理说明)

为什么不用 2 状态(ON↔OFF)?
  2 状态无法处理「动画进行中再次点击」的场景。
  例如:用户点了休闲服(开始1秒溶解),0.5秒后又点了原皮。
  如果只有 ON↔OFF,旧的 dissolve 动画还没播完,新的又来了 → 状态混乱。

4 状态如何解决?
  A(关闭) ──选中──→ B(溶解出现) ──播完──→ C(开启/显示) ──切走──→ D(溶解消失) ──播完──→ A(关闭)
                          ↑                        ↓
                          └── 打断保护(切走了) ────┘
                          └── 打断保护(切回了) ────┘

  B 和 D 是「过渡状态」,播完自动进入稳定态(C 或 A)。
  B→A 和 D→C 的打断过渡(duration=0)保证快速点击不卡死。
  每个状态职责明确:
    A = 衣服隐藏(默认态)
    B = 衣服正在溶解出现(过渡态,1秒)
    C = 衣服显示(稳定态)
    D = 衣服正在溶解消失(过渡态,1秒)

为什么 B 和 C 可以用同一个 ON clip?
  因为 ON clip(z=-0.5, m_IsActive=true)就是「显示状态」的正确值。
  B 使用它作为 motion,配合 transition duration 实现渐变进入。
  C 使用它作为 motion,直接保持显示。

为什么 A 要用 Hide clip 而不是 OFF clip?
  Hide clip(z=1.7, m_IsActive=false)=> 完全隐藏,可安全进入。
  OFF clip(z=1.7→m_IsActive=false, 2帧)=> 是「从显示到隐藏」的过渡 clip。
  如果 A 用 OFF clip 作为 motion,写 DefaultValues=true 时 OFF clip的终态才生效 → 不正确。
  所以 A 用 Hide clip(恒定值,直接隐藏)。
用 OFF clip 作为 motion,写 DefaultValues=true 时 OFF clip的终态才生效 → 不正确。 所以 A 用 Hide clip(恒定值,直接隐藏)。 ```### 4.1 准备工作

clip 路径约定:
  Assets/Cazalis/Animation/FX/新套装名_ON.anim
  Assets/Cazalis/Animation/FX/新套装名_OFF.anim
  Assets/Cazalis/Animation/FX/新套装名_Hide.anim  (可选,A状态用)

taozhuang 值: 新套装用 5+(0=原皮,1=露肩短裙,2=休闲服,3=开衫毛衣,4=全脱)
alis/Animation/FX/新套装名_Hide.anim (可选,A状态用)

taozhuang 值: 新套装用 5+(0=原皮,1=露肩短裙,2=休闲服,3=开衫毛衣,4=全脱) ```### 4.2 MCP 创建完整 Layer 代码

var ctrl = AssetDatabase.LoadAssetAtPath<AnimatorController>(
    "Assets/Cazalis/Animation/Animator/Cazalis_FX_Modified.controller");

string outfitName = "NewOutfit";   // 新套装英文名
int tv = 5;                        // 新 taozhuang 值
float td = 1.0f;                   // 溶解过渡时长(秒)

// 加载 clip(需先生成)
var onClip = AssetDatabase.LoadAssetAtPath<AnimationClip>(
    "Assets/Cazalis/Animation/FX/" + outfitName + "_ON.anim");
var offClip = AssetDatabase.LoadAssetAtPath<AnimationClip>(
    "Assets/Cazalis/Animation/FX/" + outfitName + "_OFF.anim");
var hideClip = AssetDatabase.LoadAssetAtPath<AnimationClip>(
    "Assets/Cazalis/Animation/FX/" + outfitName + "_Hide.anim");

if (onClip == null || offClip == null) {
    Debug.LogError("Missing clips!"); return;
}

// --- 创建 Layer ---
var layer = new AnimatorControllerLayer();
layer.name = "4S_" + outfitName;
layer.defaultWeight = 1f;
layer.blendingMode = AnimatorLayerBlendingMode.Override;

var sm = new AnimatorStateMachine();
sm.name = layer.name + "_SM";
layer.stateMachine = sm;

// --- 创建 4 个状态 ---
var A = sm.AddState("A_Closed_" + outfitName);
A.motion = hideClip ?? offClip;  // Hide 优先(强烈建议使用 Hide clip,见 wiki Ch111)
A.writeDefaultValues = true;     // A=关闭状态 wd=true

var B = sm.AddState("B_Appearing_" + outfitName);
B.motion = onClip;
B.writeDefaultValues = false;    // 溶解过程中 wd=false
("B_Appearing_" + outfitName);
B.motion = onClip;
B.writeDefaultValues = false;    // 溶解过程中 wd=falsevar C = sm.AddState("C_Opened_" + outfitName);
C.motion = onClip;               // C 和 B 共用 ON clip
C.writeDefaultValues = false;    // 显示状态 wd=false(不能用 true,否则 C→D 时可能 flash)

var D = sm.AddState("D_Disappearing_" + outfitName);
D.motion = offClip;
D.writeDefaultValues = false;    // 溶解过程中 wd=false

// --- Entry → A ---
sm.AddEntryTransition(A);

// --- A → B: taozhuang == N ---
var a2b = A.AddTransition(B);
a2b.hasExitTime = false;
a2b.hasFixedDuration = true;
a2b.duration = td;
a2b.interruptionSource = TransitionInterruptionSource.CurrentStateThenNextState; // ⚠️ 关键!否则打断保护不工作
a2b.AddCondition(AnimatorConditionMode.Equals, (float)tv, "taozhuang");

// --- B → C: 播完自动进入开启 ---
var b2c = B.AddTransition(C);
b2c.hasExitTime = true;
b2c.exitTime = 1f;
b2c.hasFixedDuration = true;
b2c.duration = 0f;  // 瞬间切

// --- C → D: taozhuang != N ---
var c2d = C.AddTransition(D);
c2d.hasExitTime = false;
c2d.hasFixedDuration = true;
c2d.duration = td;
c2d.interruptionSource = TransitionInterruptionSource.CurrentStateThenNextState; // ⚠️ 关键!否则打断保护不工作
c2d.AddCondition(AnimatorConditionMode.NotEqual, (float)tv, "taozhuang");

// --- D → A: 播完自动回到关闭(无条件,播完就回) ---
var d2a = D.AddTransition(A);
d2a.hasExitTime = true;
d2a.exitTime = 1f;
d2a.hasFixedDuration = true;
d2a.duration = 0f;
// ⚠️ 没有条件!HasExitTime 播完自动回 A
// 如果加条件会卡在 D:taozhuang != N 时条件不满足 → D 播完卡住
true;
d2a.duration = 0f;
// ⚠️ 没有条件!HasExitTime 播完自动回 A
// 如果加条件会卡在 D:taozhuang != N 时条件不满足 → D 播完卡住// --- [打断保护] B → A: 溶解中途切走了 ---
var b2a = B.AddTransition(A);
b2a.hasExitTime = false;
b2a.hasFixedDuration = true;
b2a.duration = 0f;  // 瞬间
b2a.AddCondition(AnimatorConditionMode.NotEqual, (float)tv, "taozhuang");

// --- [打断保护] D → C: 消失中途切回了 ---
var d2c = D.AddTransition(C);
d2c.hasExitTime = false;
d2c.hasFixedDuration = true;
d2c.duration = 0f;  // 瞬间
d2c.AddCondition(AnimatorConditionMode.Equals, (float)tv, "taozhuang");
// ⚠️ 这个条件和 D→A 的 HasExitTime 并行存在:
//   - 如果用户切回(taozhuang==N):打断保护触发,瞬间回 C
//   - 如果用户切走(taozhuang!=N):D 播完通过 HasExitTime(无条件)自动回 A
//   - 两者不冲突,各自独立

// --- 添加到 Controller ---
ctrl.AddLayer(layer);
EditorUtility.SetDirty(ctrl);
EditorUtility.SetDirty(sm);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
SetDirty(ctrl); EditorUtility.SetDirty(sm); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); ```### 4.3 过渡配置总结表

⚠️ 关键 Bug 发现:A→B 和 C→D 的 transition 必须设置 interruptionSource = CurrentStateThenNextState。默认值是 None,意味着即使打断条件(taozhuang != N)已满足,transition 也会播完才检查,导致打断保护(B→A, D→C)完全无效。参见 wiki Ch43 详细分析。

代号 起点 终点 触发条件 hasExitTime exitTime duration interruptionSource 说明
A→B A_Closed B_Appearing taozhuang == N 1.0s CurrentStateThenNextState 开始溶解出现(必须设 interruptionSource 使打断保护生效)
B→C B_Appearing C_Opened 播完自动 1.0 0s 进入稳定的显示态
C→D C_Opened D_Disappearing taozhuang != N 1.0s CurrentStateThenNextState 开始溶解消失(必须设 interruptionSource 使打断保护生效)
D→A D_Disappearing A_Closed 无条件(播完自动回) 1.0 0s ⭐回到关闭态
B→A B_Appearing A_Closed taozhuang != N 0s ⭐打断:切走了
D→C D_Disappearing C_Opened taozhuang == N 0s ⭐打断:切回了
⭐打断:切走了
D→C D_Disappearing C_Opened taozhuang == N 0s ⭐打断:切回了
状态 wd 原因
A_Closed true 稳定关闭态,wd=true 保证所有属性归零。A 必须使用 Hide clip(恒定值 clip),不能用 OFF clip,否则 wd=true 会播 clip 的过渡动画导致闪烁
B_Appearing false 溶解中,wd=false 让 clip 的动画值保持(不归零 z 值)。配合 ON clip 的 t=0 值(z=-0.5)通过 crossfade 从 A 的 z=1.7 渐变到 -0.5
C_Opened false 显示态,wd=false 让 ON clip 的 z=-0.5 和 m_IsActive=true 保持。如果 wd=true,在 C→D 过渡时 C 退出前会写 ON clip t=0 值可能触发 flash
D_Disappearing false 溶解中,wd=false 让 OFF clip 的动画值保持。通过 crossfade 从 C 的 z=-0.5 渐变到 1.7

不推荐全 true(方案 B):C→D 过渡时 C 退出前 wd=true 写入 ON clip t=0 值(z=-0.5, m_IsActive=1),下一帧 D 要写 z=1.7 和 m_IsActive=0,可能在极短时间内产生值冲突。

不推荐全 false(方案 C):首次进入(Entry→A)时值来源不可预测,在多层并行时调试困难。


Step 5: 更新 taozhuang 参数范围

时间内产生值冲突。

不推荐全 false(方案 C):首次进入(Entry→A)时值来源不可预测,在多层并行时调试困难。


Step 5: 更新 taozhuang 参数范围### taozhuang 并行控制原理(必读)

所有 4S_ 层使用同一个 taozhuang Int 参数。 这是系统能「一键切换所有衣服」的关键设计:

假设当前 taozhuang = 2(休闲服):
  4S_Origin    (taozhuang==0): taozhuang != 0 → 条件成立 → C→D 溶解消失
  4S_Lujian    (taozhuang==1): taozhuang != 1 → 条件成立 → C→D 溶解消失
  4S_Casual    (taozhuang==2): taozhuang == 2 → 条件成立 → A→B 溶解出现 ✅
  4S_Cardigan  (taozhuang==3): taozhuang != 3 → 条件成立 → C→D 溶解消失
  • Animator 层是并行执行的:所有层的状态机同时运行
  • 一个时间点只有一个层taozhuang == N 为 true -> 那层进入 B(出现) → C(显示)
  • 其他所有层taozhuang != N 为 true -> 它们从 C→D(消失)→A(关闭)
  • 这就是为什么「点一套衣服,其他衣服自动消失」

⚠️ 重要:当用户点击 Toggle 设 taozhuang=0(全脱/原皮)时: - 4S_Origin(taozhuang==0)条件成立 → 显示原皮 - 其他层(!=0)→ 全部消失 - 这就是原皮和其他衣服互斥的原理

如果新套装需要新值(如 5),Int 参数自然支持,不需要修改 VRCExpressionParameters 定义:

// 不需要做任何事!taozhuang 已经是 Int, saved=true, default=0
// Int 自然支持 0~N 任意整数值
Debug.Log("taozhuang supports value 5 automatically");

Step 6: 更新菜单

Int 自然支持 0~N 任意整数值 Debug.Log("taozhuang supports value 5 automatically");

---

## Step 6: 更新菜单### MCP 执行代码

```csharp
var menu = AssetDatabase.LoadAssetAtPath<VRCExpressionsMenu>(
    "Assets/Cazalis/Animation/EXMenu/ClothType_未分类.asset");

var newControl = new VRCExpressionsMenu.Control();
newControl.name = "新套装名";  // 显示文本
newControl.type = (VRCExpressionsMenu.Control.ControlType)101; // Button
newControl.value = (float)tv; // 设新 taozhuang 值
newControl.parameter = new VRCExpressionsMenu.Control.Parameter();
newControl.parameter.name = "taozhuang";  // 必须设置!

var list = new List<VRCExpressionsMenu.Control>(menu.controls);
list.Add(newControl);
menu.controls = list.ToArray();
EditorUtility.SetDirty(menu);
AssetDatabase.SaveAssets();

⚠️ 菜单注意事项: - Toggle(type=102)的 parameter.name 必须设置 - Button(type=101)直接设固定的 Int 值,适合互斥切换 - 如果用 Toggle,ON 时设 taozhuang=N,OFF 时设 taozhuang=0 → 会触发 4S_Origin 层! - 推荐用 Button 设固定 Int 值,不用 Toggle


Step 7: 验证清单

,OFF 时设taozhuang=0` → 会触发 4S_Origin 层! - 推荐用 Button 设固定 Int 值,不用 Toggle


Step 7: 验证清单### 7.1 结构验证

  • 所有 SMR 的材质是 lilToon + Cutout/Transparent
  • 材质 _DissolveParams.x=2, z=1.7(default), _DissolveNoiseMask 不为空
  • GEOM_TYPE_BRANCH_DETAIL 关键字已启用
  • ON clip 每个 SMR 有 m_IsActive + _DissolveParams 4 分量(各 1 帧)
  • OFF clip 每个 SMR 有 m_IsActive + _DissolveParams + _DissolvePos(2 帧)
  • Hide clip(可选)每个 SMR 有 m_IsActive + _DissolveParams(1 帧)
  • 3 个 clip 控制相同数量的 SMR(路径数量一致)
  • Layer 4 个状态都有非 null motion(用下方代码验证)
  • writeDefaultValues: A=true, B/C/D=false
  • 6 条过渡全部配置(4 主线 + 2 打断)
  • B→C 和 D→A 有 HasExitTime=true, exitTime=1,D→A 无条件
  • 打断过渡 duration=0(瞬间切换)
  • taozhuang 值不冲突(已有 0/1/2/3/4,新套装用 5+)
  • 菜单添加了对应的 Button 控件
  • Button 的 parameter.name="taozhuang",value 设为正确值 已有 0/1/2/3/4,新套装用 5+)
  • 菜单添加了对应的 Button 控件
  • Button 的 parameter.name="taozhuang",value 设为正确值### 7.2 motion 加载验证代码

执行以下 C# 代码确认 clip 已正确绑定到状态:

var ctrl = AssetDatabase.LoadAssetAtPath<AnimatorController>(
    "Assets/Cazalis/Animation/Animator/Cazalis_FX_Modified.controller");
string targetLayer = "4S_NewOutfit"; // 改为实际层名

for (int i = 0; i < ctrl.layers.Length; i++) {
    var l = ctrl.layers[i];
    if (l.name == targetLayer) {
        var sm = l.stateMachine;
        foreach (var st in sm.states) {
            bool hasMotion = st.state.motion != null;
            string clipName = hasMotion ? st.state.motion.name : "NULL";
            Debug.Log($"  [{st.state.name}] motion={hasMotion}, clip={clipName}, wd={st.state.writeDefaultValues}");
        }

        // 验证过渡数量
        foreach (var st in sm.states) {
            int tCount = st.state.transitions.Length;
            Debug.Log($"  Transitions from [{st.state.name}]: {tCount}");
        }
        break;
    }
}

7.3 Play Mode 实际测试

  1. 在 Unity Editor 中保存场景
  2. 点击 Play 进入运行模式
  3. 打开 Animator 窗口(Window → Animator 或 Ctrl+6)
  4. 在 Animator 窗口左上角选择 Cazalis_FX_Modified.controller
  5. 在右侧 Layers 面板中找到 4S_新套装名
  6. 在 VRChat SDK 的 Expression Menu 测试面板中点击新套装的按钮
  7. 观察 Animator 窗口中的状态流转:
  8. 点击后应从 A→B(绿色高亮过渡),1 秒后 B→C
  9. 切其他衣服时,应从 C→D,1 秒后 D→A
  10. 快速点击(1 秒内切走切回):B 应瞬间跳回 A,D 应瞬间跳回 C
  11. 视觉效果确认:
  12. 点击后衣服应从顶部开始溶解出现(1 秒渐变)
  13. 切走后衣服应溶解消失(1 秒渐变后 GameObject disable)
  14. 其他衣服不应受到影响
  15. 材质在溶解过程中不应有闪烁或锯齿

部开始溶解出现(1 秒渐变) - 切走后衣服应溶解消失(1 秒渐变后 GameObject disable) - 其他衣服不应受到影响 - 材质在溶解过程中不应有闪烁或锯齿

---### 7. 上传前 YAML 验证(关键!)

上传前必须从 Linux 端验证 .controller YAML 文件中的过渡配置是否正确持久化:

# 检查所有 Transition 的 interruptionSource 是否非零
grep -A2 "m_InterruptionSource" Cazalis_FX_Modified.controller | grep -v "^--$"

# 检查是否存在目标状态为 null 的过渡
grep -B1 "m_DstState: {fileID: 0}" Cazalis_FX_Modified.controller | head -20

# 检查 WriteDefaultValues 配置
grep -B2 "m_WriteDefaultValues:" Cazalis_FX_Modified.controller

8. ⚠️ 上传时的镜像克隆风险

当通过 C# API 创建 Layer 后直接上传到 VRChat: - Editor 中 Play Mode 测试正常 ≠ 上传后正常运行 - VRChat SDK 在构建时会进行镜像克隆(Mirror Clone) — 为镜像场景创建 Controller 副本 - 以下情况会导致镜像克隆失败: - 参数类型无效(通过 RemoveInvalidParameters()RemoveWrongParamTypes() 检查) - 状态机为 null - 状态没有 motion(null motion) - Always 在上传前用 Play Mode 测试 + YAML 验证

9. VRCFury 构建时自动改写 Controller

如果项目使用 VRCFury,构建时会发生以下修改(Editor 中的结构与运行时不同): - LayerToTree:所有 FX Layer 被展开为 Direct BlendTree - FixWriteDefaults:自动修复 wd 设置 - 参数重写:VRCFury 可能重写参数名(添加前缀),导致手动添加的菜单控件找不到参数 - Tracking Control Actor:自动添加 Tracking 控制层

上传前验证清单

  • Play Mode 测试通过
  • .controller YAML 检查 interruptionSource ≠ 0(有 duration 的过渡上)
  • .controller YAML 检查无 null target transitions
  • 参数预算 ≤ 256 bits(Bool=1bit, Int=8bits, Float=8bits)
  • 所有 Layer 的状態都有非 null motion
  • 无无效参数类型
  • VRCAvatarDescriptor 引用了正确的副本(非原版)
  • 如果使用 VRCFury,确认参数名不会被重写影响

已知陷阱

1. 录制 ON/OFF clip 时 SMR 不全

  • 症状:有的部件溶解了有的没溶解
  • 解决:录完后用 clip_get_info 检查 curve 数量,核对 SMR 路径是否全部覆盖

1. 录制 ON/OFF clip 时 SMR 不全

  • 症状:有的部件溶解了有的没溶解
  • 解决:录完后用 clip_get_info 检查 curve 数量,核对 SMR 路径是否全部覆盖### 2. SetFloat 不生效(外部包材质)
  • 症状:DissolveParams 改了但保存后恢复
  • 解决:直接编辑 .mat 文件 + AssetDatabase.Refresh()

3. Opaque 模式不溶解

  • 症状:_DissolveParams.z 动画了但画面无变化
  • 根因:lilToon 源码 #if LIL_RENDER != 0 — Opaque 模式跳过溶解
  • 解决:切到 Cutout 或 Transparent 渲染模式

4. 快速点击导致动画不播放

  • 根因 1(已修复):D→A 过渡既有 HasExitTime=true 又有条件 taozhuang==N。当用户切走时 taozhuang!=N,D 播完但条件不满足 → 卡在 D 状态。修复:D→A 无条件,仅靠 HasExitTime 自动回 A。
  • 根因 2:B→C 和 D→A 的 HasExitTime=1 必须等 clip 播完(1秒)。快速连点时打断保护(B→A, D→C)虽然存在,但其 duration=0 的瞬间切换 会打断 HasExitTime 过渡,所以不会卡死。但是如果打断优先级更低(低 index 层先计算),打断过渡可能被主过渡覆盖。
  • 如果菜单用 Toggle + taozhuang=0 作为 OFF 值,打断条件 taozhuang != N 会匹配到 4S_Origin 层
  • 解决方案:用 Button(type=101)设固定值,不用 Toggle

5. writeDefaultValues 的选择

  • A=true:进入关闭态时归零所有属性
  • B/C/D=false:溶解/显示过程中保持 clip 动画值

6. Int 参数 + Toggle 菜单的冲突

  • Toggle ON 设 taozhuang=N,Toggle OFF 设 taozhuang=0
  • taozhuang=0 触发 4S_Origin 层 → 原皮意外显示
  • 解决方案:用 Button(type=101)设固定值;把"全脱"作为 taozhuang=4 单独处理

taozhuang=0` 触发 4S_Origin 层 → 原皮意外显示 - 解决方案:用 Button(type=101)设固定值;把"全脱"作为 taozhuang=4 单独处理

---## 快速引用

项目路径:
  FX Controller: Assets/Cazalis/Animation/Animator/Cazalis_FX_Modified.controller
  ON/OFF/Hide clip目录: Assets/Cazalis/Animation/FX/
  菜单资产: Assets/Cazalis/Animation/EXMenu/Cazalis_Menu_Modified.asset
  子菜单: ClothType_未分类.asset
  参数: Cazalis_Parameter_Modified.asset
  噪点纹理: Assets/Cazalis/Cazalis_Reflection_Noise.png
  C# API: 使用 UnityEditor.AnimationUtility / UnityEngine.AnimationClip

**⚠️ 新衣服绑定必检项**:安装后必须对比 Armature.1 骨骼旋转与主 Armature 是否一致!
Blanchir 踩坑:原模型绑定姿势(A-Pose)与 Cazalis(T-Pose)不同,导致 39 个骨骼旋转偏移(UpperArm 48.6°, LowerArm 80°, UpperLeg 30°, LowerLeg 80°)。
MA MergeArmature **不会自动修正骨骼旋转差异**。其他4套衣服的 Armature.1 旋转全部与主骨骼一致所以没问题。
修复方法:递归遍历 Armature.1,将所有与主 Armature 同名骨骼的 localPosition + localRotation 复制为主骨骼值。Blanchir 专属骨骼(衣服额外骨骼如飘带)保持不动。

taozhuang 值映射:
  0=原皮(Origin), 1=露肩短裙(Lujian), 2=休闲服(Casual), 3=开衫毛衣(Cardigan), 4=娃花(Wahua), 5=Blanchir, 6=HonmeiKnit(01_Black)
  新套装建议用: 7+

已有 4S_ 层索引 (l0起算):
  l18=4S_Origin, l19=4S_Lujian, l20=4S_Casual, l21=4S_Cardigan, l22=4S_Wahua, l25=4S_Blanchir, l26=4S_Underwear, l27=4S_HonmeiKnit

溶解动画核心参数:
  _DissolveParams.z: -0.5(显示) → 1.7(完全溶解)
  Transition Duration: **1.2s** (2026-05-27 更新;从1s→1.5s→1.2s)
  ON clip: 长度 0s, 1帧 (设 z=-0.5 + m_IsActive=true)
  OFF clip: 长度 0.0167s, 2帧 (t=0时 z=1.7, t=0.0167时 m_IsActive=false)

-0.5 + m_IsActive=true) OFF clip: 长度 0.0167s, 2帧 (t=0时 z=1.7, t=0.0167时 m_IsActive=false) ```

---## C# API 速查

操作 代码
添加状态 var st = sm.AddState("name")
设 motion st.state.motion = clip
设 wd st.state.writeDefaultValues = true/false
添加过渡 var t = stateA.AddTransition(stateB)
设 exitTime t.hasExitTime = true; t.exitTime = 1f
设条件 t.AddCondition(AnimatorConditionMode.Equals, val, "param")
添加 Entry sm.AddEntryTransition(state)
添加层 ctrl.AddLayer(layer)
添加参数 ctrl.AddParameter("name", AnimatorControllerParameterType.Float)
读 clip AnimationUtility.GetCurveBindings(clip)
读 clip 关键帧 AnimationUtility.GetEditorCurve(clip, binding)
创建 clip new AnimationClip() + AnimationUtility.SetEditorCurve(clip, binding, curve)