결과물

이제 Hand 메시에 물리 효과를 적용해서 오브젝트를 통과하지 않게끔 구현했습니다. 결과물을 보시면 가상의 메시는 뚫고 있지만, 실제 Hand 메시는 오브젝트를 뚫지 않고 막히는 모습을 확인할 수 있습니다.
구현 내용
1. AVRHand.h / .cpp
새롭게 SceneComponent 를 추가해서 기존 Hand 메시를 MotionController 로부터 분리시켰습니다. 그리고 가상의 Hand 메시를 MotionController의 자식으로 추가해서 PhysicsConstraint 로 기존 Hand 메시가 가상의 Hand 메시를 따라다니도록 구현했습니다.
기존 Hand 메시에는 Physics Asset을 추가해서 물리 효과를 구현했고, **SetAllBodiesBelowSimulatePhysics**와 **SetAllBodiesBelowPhysicsBlendWeight**을 통해서 물리 효과와 애니메이션 사이의 적절한 보간 비율을 설정해주었습니다.
AVRHand.h
더보기
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "VRHand.generated.h"
class UPhysicsConstraintComponent;
class IGrabbable;
struct FInputActionValue;
class UVRHandAnimInstance;
class UInputAction;
class USphereComponent;
class UWidgetInteractionComponent;
class UMotionControllerComponent;
UCLASS()
class VIRTUALREALITY_API AVRHand : public AActor
{
GENERATED_BODY()
public:
AVRHand();
virtual void OnConstruction(const FTransform& Transform) override;
virtual void PostInitializeComponents() override;
virtual void BeginPlay() override;
virtual void Tick(float DeltaSeconds) override;
public:
void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent);
// Action Binding Function Section
protected:
// HandGraspAction Binding Function
void DoHandGrasp(const FInputActionValue& InValue);
void StopHandGrasp();
// HandIndexCurl Binding Function
void DoHandIndexCurl(const FInputActionValue& InValue);
void StopHandIndexCurl();
// HandPoint Binding Function
void DoHandPoint();
void StopHandPoint();
// HandThumbUp Binding Function
void DoHandThumbUp();
void StopHandThumbUp();
// GrabAction Binding Function
void GrabObject();
void ReleaseObject();
private:
/** 애니메이션과 물리 엔진의 비율을 설정하는 함수입니다. */
void UpdateHandPhysicsBelow(FName InBoneName, bool bNewSimulate, float InBlendWeight);
// Component Section
protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "변수|컴포넌트")
TObjectPtr<USceneComponent> Root;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "변수|컴포넌트")
TObjectPtr<UMotionControllerComponent> MotionController;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "변수|컴포넌트")
TObjectPtr<USkeletalMeshComponent> HandMesh;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "변수|컴포넌트")
TObjectPtr<USkeletalMeshComponent> VirtualHandMesh;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "변수|컴포넌트")
TObjectPtr<UPhysicsConstraintComponent> PhysicsConstraint;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "변수|컴포넌트")
TObjectPtr<UWidgetInteractionComponent> WidgetInteractionComponent;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "변수|컴포넌트")
TObjectPtr<USphereComponent> GrabCollision;
// Input Section
protected:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "변수|입력")
TObjectPtr<UInputAction> HandGraspAction;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "변수|입력")
TObjectPtr<UInputAction> HandIndexCurlAction;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "변수|입력")
TObjectPtr<UInputAction> HandPointAction;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "변수|입력")
TObjectPtr<UInputAction> HandThumbUpAction;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "변수|입력")
TObjectPtr<UInputAction> GrabAction;
// Variable Section
protected:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "변수|손")
EControllerHand HandType;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "변수|손")
uint8 bMirrorAnimation : 1 = false;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "변수|손")
FName BoneName;
// Cached Section
private:
UPROPERTY()
TObjectPtr<UVRHandAnimInstance> AnimInstance;
TScriptInterface<IGrabbable> CurrentlyGrabbedActor;
FVector LastLocation;
FVector CurrentCalculatedVelocity;
public:
FORCEINLINE FVector GetHandVelocity() const { return CurrentCalculatedVelocity; }
};
AVRHand.cpp
더보기
#include "VRHand.h"
#include "EnhancedInputComponent.h"
#include "InputActionValue.h"
#include "MotionControllerComponent.h"
#include "VirtualReality.h"
#include "Animation/VRHandAnimInstance.h"
#include "Components/SphereComponent.h"
#include "Components/WidgetInteractionComponent.h"
#include "Define/Define.h"
#include "Interface/Grabbable.h"
#include "PhysicsEngine/PhysicsConstraintComponent.h"
AVRHand::AVRHand()
{
PrimaryActorTick.bCanEverTick = true;
// SceneComponent 초기화
Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
SetRootComponent(Root);
// MotionController 초기화
MotionController = CreateDefaultSubobject<UMotionControllerComponent>(TEXT("MotionController"));
MotionController->SetupAttachment(Root);
// 가상 HandMesh 초기화
VirtualHandMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("VirtualHandMesh"));
VirtualHandMesh->SetupAttachment(MotionController);
VirtualHandMesh->SetGenerateOverlapEvents(false);
VirtualHandMesh->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
VirtualHandMesh->SetClothMaxDistanceScale(ECR_Overlap);
// PhysicsConstraint 초기화
PhysicsConstraint = CreateDefaultSubobject<UPhysicsConstraintComponent>(TEXT("PhysicsConstraint"));
PhysicsConstraint->SetupAttachment(MotionController);
// HandMesh 초기화
HandMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("HandMesh"));
HandMesh->SetupAttachment(Root);
HandMesh->SetSimulatePhysics(true);
HandMesh->SetCollisionProfileName(FName("PhysicsActor"));
// WidgetInteractionComponent 초기화
WidgetInteractionComponent = CreateDefaultSubobject<UWidgetInteractionComponent>(TEXT("WidgetInteractionComponent"));
WidgetInteractionComponent->SetupAttachment(HandMesh);
// GrabCollision 초기화
GrabCollision = CreateDefaultSubobject<USphereComponent>(TEXT("GrabCollision"));
GrabCollision->SetupAttachment(HandMesh);
GrabCollision->SetSphereRadius(15.0f);
GrabCollision->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
GrabCollision->SetCollisionResponseToAllChannels(ECR_Ignore);
GrabCollision->SetCollisionResponseToChannel(ECC_GRABBABLE, ECR_Overlap);
}
void AVRHand::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
if (UEnhancedInputComponent* EIC = Cast<UEnhancedInputComponent>(PlayerInputComponent))
{
if (HandGraspAction)
{
EIC->BindAction(HandGraspAction, ETriggerEvent::Triggered, this, &AVRHand::DoHandGrasp);
EIC->BindAction(HandGraspAction, ETriggerEvent::Canceled, this, &AVRHand::StopHandGrasp);
EIC->BindAction(HandGraspAction, ETriggerEvent::Completed, this, &AVRHand::StopHandGrasp);
}
if (HandIndexCurlAction)
{
EIC->BindAction(HandIndexCurlAction, ETriggerEvent::Triggered, this, &AVRHand::DoHandIndexCurl);
EIC->BindAction(HandIndexCurlAction, ETriggerEvent::Canceled, this, &AVRHand::StopHandIndexCurl);
EIC->BindAction(HandIndexCurlAction, ETriggerEvent::Completed, this, &AVRHand::StopHandIndexCurl);
}
if (HandPointAction)
{
EIC->BindAction(HandPointAction, ETriggerEvent::Started, this, &AVRHand::DoHandPoint);
EIC->BindAction(HandPointAction, ETriggerEvent::Canceled, this, &AVRHand::DoHandPoint);
EIC->BindAction(HandPointAction, ETriggerEvent::Completed, this, &AVRHand::StopHandPoint);
}
if (HandThumbUpAction)
{
EIC->BindAction(HandThumbUpAction, ETriggerEvent::Started, this, &AVRHand::DoHandThumbUp);
EIC->BindAction(HandThumbUpAction, ETriggerEvent::Canceled, this, &AVRHand::DoHandThumbUp);
EIC->BindAction(HandThumbUpAction, ETriggerEvent::Completed, this, &AVRHand::StopHandThumbUp);
}
if (GrabAction)
{
EIC->BindAction(GrabAction, ETriggerEvent::Started, this, &AVRHand::GrabObject);
EIC->BindAction(GrabAction, ETriggerEvent::Canceled, this, &AVRHand::ReleaseObject);
EIC->BindAction(GrabAction, ETriggerEvent::Completed, this, &AVRHand::ReleaseObject);
}
}
}
void AVRHand::OnConstruction(const FTransform& Transform)
{
Super::OnConstruction(Transform);
switch (HandType)
{
case EControllerHand::Left:
MotionController->MotionSource = FName("Left");
BoneName = FName("hand_l");
break;
case EControllerHand::Right:
MotionController->MotionSource = FName("Right");
BoneName = FName("hand_r");
break;
default:
break;
}
}
void AVRHand::PostInitializeComponents()
{
Super::PostInitializeComponents();
AnimInstance = Cast<UVRHandAnimInstance>(HandMesh->GetAnimInstance());
}
void AVRHand::BeginPlay()
{
Super::BeginPlay();
AnimInstance->bIsMirror = bMirrorAnimation;
UpdateHandPhysicsBelow(BoneName, true, 0.15f);
}
void AVRHand::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
FVector CurrentLocation = MotionController->GetComponentLocation();
CurrentCalculatedVelocity = (CurrentLocation - LastLocation) / DeltaSeconds;
LastLocation = CurrentLocation;
GEngine->AddOnScreenDebugMessage(-1, 0.0f, FColor::Green, FString::Printf(TEXT("Hand Speed: %.2f"), CurrentCalculatedVelocity.Size()));
}
void AVRHand::GrabObject()
{
TArray<AActor*> OverlappedActors;
GrabCollision->GetOverlappingActors(OverlappedActors);
if (OverlappedActors.IsEmpty()) return;
AActor* FirstActorUnderCollision = OverlappedActors[0];
if (!FirstActorUnderCollision) return;
CurrentlyGrabbedActor = TScriptInterface<IGrabbable>(FirstActorUnderCollision);
if (CurrentlyGrabbedActor)
{
CurrentlyGrabbedActor->OnGrab(HandMesh, GrabCollision->GetComponentLocation());
}
}
void AVRHand::ReleaseObject()
{
if (CurrentlyGrabbedActor)
{
CurrentlyGrabbedActor->OnRelease(HandMesh);
CurrentlyGrabbedActor = nullptr;
}
}
void AVRHand::UpdateHandPhysicsBelow(FName InBoneName, bool bNewSimulate, float InBlendWeight)
{
if (HandMesh)
{
HandMesh->SetAllBodiesBelowSimulatePhysics(InBoneName, bNewSimulate, true);
HandMesh->SetAllBodiesBelowPhysicsBlendWeight(InBoneName, InBlendWeight, false, true);
}
}
void AVRHand::DoHandGrasp(const FInputActionValue& InValue)
{
const float ActionValue = InValue.Get<float>();
AnimInstance->PoseAlphaGrasp = ActionValue;
}
void AVRHand::DoHandIndexCurl(const FInputActionValue& InValue)
{
const float ActionValue = InValue.Get<float>();
AnimInstance->PoseAlphaIndexCurl = ActionValue;
}
void AVRHand::DoHandPoint()
{
AnimInstance->PoseAlphaPoint = 0.0f;
}
void AVRHand::DoHandThumbUp()
{
AnimInstance->PoseAlphaThumbUp = 0.0f;
}
void AVRHand::StopHandGrasp()
{
AnimInstance->PoseAlphaGrasp = 0.0f;
}
void AVRHand::StopHandIndexCurl()
{
AnimInstance->PoseAlphaIndexCurl = 0.0f;
}
void AVRHand::StopHandPoint()
{
AnimInstance->PoseAlphaPoint = 1.0f;
}
void AVRHand::StopHandThumbUp()
{
AnimInstance->PoseAlphaThumbUp = 1.0f;
}
2. Physical Asset
손가락 관절마다의 Constraint 를 추가해서 정밀한 물리 효과를 구현하고자 했습니다.

3. PhysicsConstraintComponent
PhysicsConstraintComponent의 속성 값은 다음과 같이 설정해주었습니다.



마무리
손의 물리 엔진을 구현하고나니 오브젝트와의 상호작용이 훨씬 자연스러워졌습니다. 이제 남은 건 Procedural Grip 기능인데, 며칠동안 찾아봐도 C++을 활용해서 구현한 사례는 전혀 없고, 오직 블루프린트 강의만 있는 상태라 쉽지가 않네요 .. 그래도 완성될 결과물을 생각해서라도 열심히 해봐야겠습니다 ! ㅎㅎ 화이팅입니다 ~
'Unreal Engine 프로젝트 > VR 공포게임' 카테고리의 다른 글
| [언리얼엔진] 6. VR Hand 애니메이션 제작 (0) | 2026.04.09 |
|---|---|
| [언리얼엔진] 5. VR 최적화 (0) | 2026.04.09 |
| [언리얼엔진] 4. VR 레버 구현 (2) | 2026.04.08 |
| [언리얼엔진] 2. VR 버튼 구현 (0) | 2026.04.04 |
| [언리얼엔진] 1. VR Grab/Release 기능 구현 (C++) (0) | 2026.04.04 |