Summary
Spring bones on VRM 1.0 avatars do not react to wind at all. WindDirectionalSource actors, the windScale / randomWindRange properties on the VrmSpringBone anim node, and bIgnoreWindDirectionalSource have zero effect on VRM 1.0 models. The same setup works correctly on VRM 0.x avatars.
Environment
- UE 5.7 / 5.8
- VRM4U (current
master)
- VRM 1.0 model with spring bones (e.g. VRoid hair)
Root cause
VRM4U has two separate spring simulation paths in Source/VRM4U/Private/VrmSpringBone.cpp, selected in AnimNode_VrmSpringBone.cpp by VrmMetaObject_Internal->GetVRMVersion():
- VRM 0.x →
VRMSpringBone::VRMSpring::Update() — reads wind via Scene->GetWindParameters_GameThread(...) and adds it to external. ✅
- VRM 1.0 →
VRM1Spring::VRM1SpringManager::update() — computes external from gravity + gravityAdd only. There is no wind code at all. ❌
So for VRM 1.0 the wind force is simply never applied, regardless of windScale or any WindDirectionalSource in the level.
Suggested fix
Mirror the VRM 0.x wind block inside VRM1SpringManager::update(). Compute the wind force once per update (after ComponentToLocal is set) and add it to each joint's external.
⚠️ Important: VRM1SpringManager::update() runs on the anim worker thread (FAnimNode_VrmSpringBone::EvaluateSkeletalControl_AnyThread). TActorIterator / GetAllActorsOfClass cannot be used there — they assert IsInGameThread() and crash. Use Scene->GetWindParameters_GameThread(...), which is worker-thread-safe (exactly as the VRM 0.x path already does).
// once per update, after: const FTransform ComponentToLocal = ComponentTransform.Inverse();
FVector WindExternal = FVector::ZeroVector;
if (animNode->bIgnoreWindDirectionalSource == false) {
const USkeletalMeshComponent* SkelComp = Output.AnimInstanceProxy->GetSkelMeshComponent();
const UWorld* World = SkelComp ? SkelComp->GetWorld() : nullptr;
FSceneInterface* Scene = World ? World->Scene : nullptr;
if (Scene) {
FVector WindDirection = FVector::ZeroVector;
float WindSpeed = 0.f, MinGust = 0.f, MaxGust = 0.f;
Scene->GetWindParameters_GameThread(SkelComp->GetComponentTransform().GetLocation(),
WindDirection, WindSpeed, MinGust, MaxGust);
WindDirection = ComponentToLocal.TransformVector(WindDirection);
const float WindUnitScale = 0.5f * 250.0f
* FMath::FRandRange(1.f - animNode->randomWindRange, 1.f + animNode->randomWindRange)
* animNode->windScale;
// VRM 0.x does `external *= 100` later; VRM 1.0 uses `external` directly, so /100 to match scale.
WindExternal = WindDirection * WindSpeed * WindUnitScale * DeltaTime / 100.f;
}
}
// then per joint:
FVector external = ComponentToLocal.TransformVector(ue4grav) * (j1.gravityPower * DeltaTime) * animNode->gravityScale
+ ComponentToLocal.TransformVector(animNode->gravityAdd) * DeltaTime
+ WindExternal;
I applied this locally on UE 5.8 and VRM 1.0 hair now reacts to WindDirectionalSource with proper gusting (via randomWindRange). Thanks for the great plugin! 🙏
Summary
Spring bones on VRM 1.0 avatars do not react to wind at all.
WindDirectionalSourceactors, thewindScale/randomWindRangeproperties on theVrmSpringBoneanim node, andbIgnoreWindDirectionalSourcehave zero effect on VRM 1.0 models. The same setup works correctly on VRM 0.x avatars.Environment
master)Root cause
VRM4U has two separate spring simulation paths in
Source/VRM4U/Private/VrmSpringBone.cpp, selected inAnimNode_VrmSpringBone.cppbyVrmMetaObject_Internal->GetVRMVersion():VRMSpringBone::VRMSpring::Update()— reads wind viaScene->GetWindParameters_GameThread(...)and adds it toexternal. ✅VRM1Spring::VRM1SpringManager::update()— computesexternalfrom gravity +gravityAddonly. There is no wind code at all. ❌So for VRM 1.0 the wind force is simply never applied, regardless of
windScaleor anyWindDirectionalSourcein the level.Suggested fix
Mirror the VRM 0.x wind block inside
VRM1SpringManager::update(). Compute the wind force once per update (afterComponentToLocalis set) and add it to each joint'sexternal.VRM1SpringManager::update()runs on the anim worker thread (FAnimNode_VrmSpringBone::EvaluateSkeletalControl_AnyThread).TActorIterator/GetAllActorsOfClasscannot be used there — they assertIsInGameThread()and crash. UseScene->GetWindParameters_GameThread(...), which is worker-thread-safe (exactly as the VRM 0.x path already does).I applied this locally on UE 5.8 and VRM 1.0 hair now reacts to
WindDirectionalSourcewith proper gusting (viarandomWindRange). Thanks for the great plugin! 🙏