Skip to content

VRM 1.0 spring bones ignore wind — VRM1SpringManager::update() has no wind handling (windScale / WindDirectionalSource have no effect) #579

Description

@franadoriv

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.xVRMSpringBone::VRMSpring::Update() — reads wind via Scene->GetWindParameters_GameThread(...) and adds it to external. ✅
  • VRM 1.0VRM1Spring::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! 🙏

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions