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;
The Cloth_Under_Bra GameObject should start active=false (hidden by default):
Why writeDefaultValues=false?
- Idle state: When exiting the hide state back to idle,
writeDefaultValues=falsemeansm_IsActiveis 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_Subentries withdirectBlendParameter="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 andDress_ON If 0→OFF -
Verify which controller the avatar actually uses via
VRCAvatarDescriptor.baseAnimationLayers[2](FX layer) — the Animator component'sruntimeAnimatorControllermay be null while the descriptor references a different controller ent'sruntimeAnimatorControllermay 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.childrenmanipulation 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
FindObjectsOfTypeand filter.