跳转至

name: vrchat-dress-bra-link-layer description: Correct implementation of Dress-Bra/Shorts linkage in VRChat FX Controller — higher layer overrides underwear when Dress is worn tags: [vrchat, unity, fx-controller, animator, underwear, dress-toggle, blendtree]


VRChat Dress ↔ Bra/Shorts Link Layer (FX Controller)

Problem

When adding underwear toggle (Bra/Shorts ON/OFF) to a VRChat avatar's FX Controller via Layer 14's Cloth_Option BlendTree (Direct type, parameter=Voice), the underwear stays visible when the user wears a Dress.

The naive approach — putting the Link layer before Layer 14 — doesn't work because Override layers with higher index numbers override lower ones. Layer 14 would still set m_IsActive=1 for Bra/Shorts on top of whatever a lower layer does.

Correct Architecture

Layer 13: Dress_ON         (m_IsActive control for Cloth_Dress, Cloth_DressRibbon)
Layer 14: Cloth_Option     (BlendTree: Bra_ON, Shorts_ON, Socks_ON, etc. — underwear toggle)
Layer 18: Dress_Bra_Link   (OVERRIDE: force Bra/Shorts hidden when Dress_ON=true)

Layer 18 must be AFTER Layer 14 (higher index) so it overrides the underwear toggle.

Implementation Steps

st be AFTER Layer 14** (higher index) so it overrides the underwear toggle.

Implementation Steps### 1. Create the Hide/Restore Animation Clips

Two clips in Assets/Cazalis/Animation/FX/:

BraShorts_Hide.anim — sets both underwear items to hidden: - Cloth_Under_Bra / m_IsActive = 0 (key: 0s→0, 0.016s→0) - Cloth_Under_Shorts / m_IsActive = 0 (key: 0s→0, 0.016s→0)

BraShorts_Restore.anim — sets both underwear items to visible: - Cloth_Under_Bra / m_IsActive = 1 (key: 0s→1, 0.016s→1) - Cloth_Under_Shorts / m_IsActive = 1 (key: 0s→1, 0.016s→1)

Use C# to create these via new AnimationClip() + SetCurve() + AssetDatabase.CreateAsset().

2. Add Shorts_ON Sub-BlendTree to Layer 14

The main BlendTree (Direct type, param=Voice) needs a Shorts child entry:

var shortsBt = new UnityEditor.Animations.BlendTree();
shortsBt.name = "Shorts_ON";
shortsBt.blendParameter = "Shorts_ON";
shortsBt.blendType = BlendTreeType.Simple1D;
shortsBt.useAutomaticThresholds = true;
shortsBt.AddChild(Shorts_OFF_clip, 0f);
shortsBt.AddChild(Shorts_ON_clip, 1f);

// Insert into parent BlendTree at correct position
var newChildren = new ChildMotion[currentChildren.Length + 1];
// Copy existing, insert at desired index
newChildren[index].motion = shortsBt;
newChildren[index].threshold = 0f;
newChildren[index].directBlendParameter = "Weight";

Don't forget to remove any old/broken Shorts entries that may have directBlendParameter="Blend" — this causes SDK "parameter does not exist" errors. ** that may have directBlendParameter="Blend" — this causes SDK "parameter does not exist" errors.### 3. Create Layer 18 (Dress_Bra_Link) — Correctly

This is the critical part. The layer must use Override blending, writeDefaultValues=false, and have two states:

// Remove old Layer 18 if it exists
var layers = ctrl.layers;
var without18 = new AnimatorControllerLayer[layers.Length - 1];
for (int i = 0; i < layers.Length - 1; i++) without18[i] = layers[i];
ctrl.layers = without18;

// Create new layer
var newLayer = new UnityEditor.Animations.AnimatorControllerLayer();
newLayer.name = "Dress_Bra_Link";
newLayer.defaultWeight = 1f;
newLayer.blendingMode = UnityEditor.Animations.AnimatorLayerBlendingMode.Override;

var sm = new UnityEditor.Animations.AnimatorStateMachine();
newLayer.stateMachine = sm;

// State 1: Idle — NO clip (let Layer 14 control underwear)
var idleState = sm.AddState("Idle");
idleState.motion = null;
idleState.writeDefaultValues = false; // CRITICAL!

// State 2: BraShorts_Hide — force hidden
var hideClip = AssetDatabase.LoadAssetAtPath<AnimationClip>("Assets/Cazalis/Animation/FX/BraShorts_Hide.anim");
var hideState = sm.AddState("BraShorts_Hide");
hideState.motion = hideClip;
hideState.writeDefaultValues = false; // CRITICAL!

// Transition: Idle → Hide when Dress_ON = true
var t1 = idleState.AddTransition(hideState);
t1.hasExitTime = false;
t1.duration = 0;
t1.conditions = new AnimatorCondition[] {
    new AnimatorCondition { parameter = "Dress_ON", mode = AnimatorConditionMode.If, threshold = 0 }
};
 new AnimatorCondition { parameter = "Dress_ON", mode = AnimatorConditionMode.If, threshold = 0 }
};// Transition: Hide → Idle when Dress_ON = false
var t2 = hideState.AddTransition(idleState);
t2.hasExitTime = false;
t2.duration = 0;
t2.conditions = new AnimatorCondition[] {
    new AnimatorCondition { parameter = "Dress_ON", mode = AnimatorConditionMode.IfNot, threshold = 0 }
};

sm.defaultState = idleState;

// Append to controller
var newLayers = new AnimatorControllerLayer[ctrl.layers.Length + 1];
for (int i = 0; i < ctrl.layers.Length; i++) newLayers[i] = ctrl.layers[i];
newLayers[ctrl.layers.Length] = newLayer;
ctrl.layers = newLayers;
ewLayers[i] = ctrl.layers[i]; newLayers[ctrl.layers.Length] = newLayer; ctrl.layers = newLayers; ```### 4. Set Underwear Default State

The Cloth_Under_Bra GameObject should start active=false (hidden by default):

GameObject.Find("Cloth_Under_Bra").SetActive(false);
This matches the original avatar design where underwear doesn't show until toggled on by the menu.

Why writeDefaultValues=false?

  • Idle state: When exiting the hide state back to idle, writeDefaultValues=false means m_IsActive is NOT reset to the GameObject's default value. Instead, the Animation system leaves it at whatever value the BraShorts_Hide clip set it to… but since Idle has no clip (motion=null), Layer 14's animation takes over immediately.
  • If writeDefaultValues=true, exiting the hide state would snap Bra/Shorts to their scene default (active=false), breaking the menu toggle logic. e state would snap Bra/Shorts to their scene default (active=false), breaking the menu toggle logic.## Verification Checklist

  • Layer 18 exists at index 18 (after Layer 14 at index 14)

  • Layer 18 blending = Override, weight = 1
  • Idle state has motion=null, writeDefaultValues=false
  • BraShorts_Hide state has the hide clip, writeDefaultValues=false
  • Transition conditions: Idle→Hide = Dress_ON If 0; Hide→Idle = Dress_ON IfNot 0
  • Cloth_Under_Bra is active=false by default in scene
  • Layer 14 BlendTree has Shorts_ON sub-entry (not a duplicate with "Blend" parameter)
  • Shorts_ON parameter exists in VRCExpressionParameters (Bool, saved=true, default=1)
  • No stale Shorts_Sub entries with directBlendParameter="Blend" in the BlendTree
  • Layer 13 (Dress_ON) exists and works correctly — this is a separate layer with two states (Dress_ON with Dress_ON.anim, Dress_OFF with Dress_OFF.anim), transitions using Dress_ON IfNot 0→ON and Dress_ON If 0→OFF
  • Verify which controller the avatar actually uses via VRCAvatarDescriptor.baseAnimationLayers[2] (FX layer) — the Animator component's runtimeAnimatorController may be null while the descriptor references a different controller ent's runtimeAnimatorController may be null while the descriptor references a different controller## Pitfalls

  • Layer ordering matters: If you try to put the link layer BEFORE Layer 14, it gets overridden. Must be AFTER.

  • writeDefaultValues=true on the hide state causes the underwear to reset to inactive when Dress is removed, even if the menu says ON.
  • Duplicate Shorts entries in BlendTree: Adding Shorts to the BlendTree via bt.children manipulation can leave behind old entries. Always check for and clean up duplicates.
  • Animated Scene Scale conflict: Make sure no other AnimatorController on the avatar writes to Cloth_Under_Bra.m_IsActive.
  • GameObject.Find("Cloth_Under_Bra") only finds root-level objects. If the model has multiple instances (Cazalis + Cazalis zigai), specify the full path or use FindObjectsOfType and filter.