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 行)实现的:
简单来说:**`_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~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 中:
或者通过 _TransparentMode 切换(在同一 shader 内部): _TransparentMode = 0 → Opaque(默认) _TransparentMode = 1 → Cutout(常用溶解) _TransparentMode = 2 → Transparent(半透明溶解)
这个 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();
当 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帧)
故障案例(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 = 1和Cloud/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 手动录制步骤
- 在 Hierarchy 选中新套装的一个 SMR GameObject
- 按 Ctrl+6 打开 Animation 窗口
- 点击 Create,保存到
Assets/Cazalis/Animation/FX/,命名新套装名_ON.anim - 在 Animation 窗口点击 Add Property:
- GameObject → m_IsActive(打勾)
- SkinnedMeshRenderer → material._DissolveParams → 展开 xyzw(打勾全部)
- 在 t=0 的白色菱形关键帧上,设置 Inspector 中的值:
m_IsActive = true_DissolveParams.x = 2, y = 0, z = -0.5, w = 0.1- 复制该 SMR 的操作,为每个 SMR重复步骤 3-5
- 同理录制 OFF clip(z=1.7, m_IsActive=false)和 Hide clip(z=1.7, m_IsActive=false) 操作,为每个 SMR重复步骤 3-5
- 同理录制 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(恒定值,直接隐藏)。
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=全脱)
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();
⚠️ 关键 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 实际测试
- 在 Unity Editor 中保存场景
- 点击 Play 进入运行模式
- 打开 Animator 窗口(Window → Animator 或 Ctrl+6)
- 在 Animator 窗口左上角选择 Cazalis_FX_Modified.controller
- 在右侧 Layers 面板中找到
4S_新套装名层 - 在 VRChat SDK 的 Expression Menu 测试面板中点击新套装的按钮
- 观察 Animator 窗口中的状态流转:
- 点击后应从 A→B(绿色高亮过渡),1 秒后 B→C
- 切其他衣服时,应从 C→D,1 秒后 D→A
- 快速点击(1 秒内切走切回):B 应瞬间跳回 A,D 应瞬间跳回 C
- 视觉效果确认:
- 点击后衣服应从顶部开始溶解出现(1 秒渐变)
- 切走后衣服应溶解消失(1 秒渐变后 GameObject disable)
- 其他衣服不应受到影响
- 材质在溶解过程中不应有闪烁或锯齿
部开始溶解出现(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) |