걷기모션을 더욱 자연스럽게 해주는 방법


Tags : Animation Animation-Modifier AnimNotify Foot-Plant Footstep Sound-Cue Physical-Material


다수의 애니메이션에 일괄 적용 가능한 Animation ModifierFootstep 기능구현 성공기 AnimModifier_

요약

  • 주요 문제 :
    캐릭터 모션 중 발바닥이 지면에 닿는 지점에 줄 수 있는 Footstep Sound등 효과적용을 위해 대량의 애니메이션에 수작업으로 Notify를 하나하나 추가하는 비생산적 단순반복 작업 진행
  • 해결 방안 :
    Animation Modifier로 애니메이션의 Foot본의 최저 X값 산출, curve와 AnimNotifier 자동생성
  • 시도한 방법 :
    1. 발바닥의 위치정보를 얻기 위해 Root-중앙 위치 통과 지점 산출
    2. 발바닥이 지면에 닿은 상태에만 FootIK 적용
  • 이루어낸 성과 :
    1. 다수의 애니메이션에 일괄적으로 Footstep Notify 추가가능하여 목표한 효과 단시간에 설정
    2. 애니메이션과 본 트랜스폼에 대한 이해
    3. 주어진 에셋에 제한된 결과물 보단 내 입맛에 맞게 커스터마이징하고 싶은 개발욕심 충족


FootIK 기술구현 상세내용은 관련 Post를 참고해주세요

UE4(BP & C++) FootIK 적용



⚙️ Animation Modifier

발바닥이 지면에 닿는 지점을 true값으로 하여 발이 지면에 닿는 순간 FootIK와 각종 발소리 효과 등을 적용하고자 Animation Sequence에서 Curve를 추가해주었다
AM_Footsteps

다수의 Animation작업을 진행하고나니 단순하지만 반복적이고 양적으로 너무 많아 비효율적인 작업으로 판단되어 방법을 검색을 해보았고, Animation Modifier 1 를 통해 일괄적으로 Animation에 Curve와 Notify를 추가해주는 기능을 알게되었다
Giuseppe Portelli님의 포스트 2 를 통해 Animation Modifier의 개념과 활용을 배웠고, TechAnim Studios님의 튜토리얼 YouTube 영상 3 을 통해 적용사례를 참고 했다


Setting

Objects

  • 필요한 객체들 추가
구분 객체명 내용
Animation Modifier AM_Footstep 입력값에 따라 Anim Sequence에 Anim Curve와 Notify 추가
User Defined Struct FFootstep Animation Modifier 입력값
최저점을 산출할 본의 이름과 추가할 Notify, Curve명칭, Notify Event 클래스
정보를 전달할 구조체
(name) BoneName
(name) NotifyTrackName
(name) CurveName
(AnimNotifyClass) NotifyClass
User Defined Enum ERootMotionDirection Animation Modifier 입력값
RootMotion의 이동방향 설정
X-Pos를 기본값으로 본의 트랜스폼 축이 상이한 경우 별도 설정가능
X-Pos, Y-Pos, Z-Pos, X-Neg, Y-Neg, Z-Neg
Anim Notify AN_Footstep_L
AN_Footstep_R
지정된 본의 최저위치 도달 시 발동 될 효과를 호출 할 Notify 클래스
양발 별도적용을 위해 Left와 Right 구분

image

  • Inputs
    Animation Sequence에 AM_Footstep 적용 시 세부사항 입력 값
구분 변수명 Default 내용
Array (struct)
FFootstep
Footsteps 0 :
BoneName foot_l
NotifyTrackName FootFX_L
CurveName LeftFoot
NotifyClass AN_Footstep_L
1:
BoneName foot_r
NotifyTrackName FootFX_R
CurveName RightFoot
NotifyClass AN_Footstep_R
최저점을 산출할 본의 이름과 추가할 Notify,
Curve명칭, 호출할 Notify Event 클래스 정보
(enum)
ERootMotionDirection
Direction X-Pos 좌측기준 본의 축방향 설정
float ToleranceRange 5.0f 최저점으로 판단할 근사값 오차범위
Array
name
NotifiesToRemove null 이미 설정된 NotifyTrack이 있는 경우 삭제할
Track 이름
bool Test false Test모드여부 설정
*Test모드: Notify 설정 없이 Curve에 실제
본의 위치값 표기
  • Functions
함수명 내용
ApplyModifier Animation Modifier 적용시 OnApply 이벤트로 호출될 함수
제거할 NotifyToRemove가 있는 경우 제거 후
Footsteps배열의 각 본 정보마다 AddCurvesAndNotifies함수 호출
RevertModifier Animation Modifier 되돌리기 실행 시 OnRevert 이벤트로 호출될 함수
입력된 NotifyTrackName과 CurveName을 삭제하며 Modifier 적용 전 상태로 초기화
AddCurvesAndNotifies Footstep구조체의 정보기준으로 Curve와 Notify Track 생성 및
매 Frame마다 본트랜스폼의 높이값을 구해 본의 최저점을 산출하여 Curve Key 입력


ApplyModifier

*Fullscreen또는 Ctrl+마우스휠로 ZoomIn/Out 가능

  1. NotifiesToRemove에 입력한 삭제하고 싶은 Notify Track 삭제
  2. 입력된 Footsteps 배열을 loop하며 각 요소마다 AddCurvesAndNotifies함수 호출


Add Curve & Notify Track

*Fullscreen또는 Ctrl+마우스휠로 ZoomIn/Out 가능

  1. Footsteps에 입력된 본이름의 마지막 문자에 따라 왼발/오른발을 bool로 구분
  2. Footsteps에 입력한 Curve이름과 NotifyTrack AddCurve 6AddAnimationNotifyTrack 7 함수를 통해 생성


Set Minimum Foot Height

*Fullscreen또는 Ctrl+마우스휠로 ZoomIn/Out 가능

  • 로컬변수
구분 변수명 Default 내용
Array float BoneHeightsByFrame Clear 각 프레임 당 해당 본의 Component기준 위치(높이)값을 담을 배열
int32 CurrentFrameIndex 0 Loop를 통해 증가할 프레임 인덱스
transform AccumulatedTransform Location(0,0,0)
Rotation(0,0,0)
Scale(1,1,1)
ComposeTransforms 8 을 통해
Foot본의 트랜스폼에서 Root본 까지 병합된 트랜스폼 정보
float CurrentHeight 0.0f Direction값으로 입력한 본의 축정보에 따라
필요한 축의 값을 최종 높이로 저장
float MinFootHeight 0.0f BoneHeightsByFrame배열의 값들 중 최소값

GetBonePoseForFrame 9 을 통해 각 프레임 마다 본의 Local Transform 정보를 추출해 Foot본의 Location값을 얻을 수 있다
따라서 해당 AnimationSequence의 프레임 수 만큼 For Loop를 돌려 필요한 Location값을 얻어 BoneHeightsByFrame배열에 CurrentFrameIndex 순으로 저장했다

  1. 필요한 로컬변수들 초기화, ForLoop내부에서 AccumulatedTransform를 초기값으로 Set
  2. FootBone의 Local Transform을 Component로 계산하기 위한 2차 Loop 진행 (상세내용 하단 내용 참고)
  3. 2차 Loop 완료 후 Direction으로 입력한 본의 축정보에 따라 필요한 축의 값 Switch로 선택 및 최종 높이 값 배열에 저장
  4. 모든 프레임 수 만큼 Loop완료 후 BoneHeightsByFrame배열의 최소값을 MinFootHeight로 Set

👟Local → Component

본의 Transform을 Local, Component, 또는 World스페이스로 변환하는 함수는 Animation그룹에 있는 함수라 Animation Modifier 블루프린트에선 호출을 할 수 없다
따라서 본 Transform을 Local 에서 Component 스페이스로 변환하는 GetComponentSpaceTransform의 구현 방식을 참고하여 직접 계산하는 방법을 알아낼 수 있었다

LocalAndComponent

  • GetComponentSpaceTransform : \Engine\Source\Runtime\Engine\private\Animation\PoseAsset.cpp
  • ComposeTransforms 함수 위치 : \Engine\Source\Runtime\Engine\Classes\Kismet\KismetMathLibrary.h
  • Transorm Multiply 연산자 위치 : \Engine\Source\Runtime\Core\Public\Math\TransformVectorized.h

상위 본노드에 대한 상대적(Relative) Transform을 차례로 병합(Compose)시켜주면서,
Foot본을 최상위 본인 Root에 상대적인 Transform을 산출하며 본의 Component 스페이스 기준 Transform을 구할 수 있다

FTransform UPoseAsset::GetComponentSpaceTransform(FName BoneName, const TArray<FTransform>& LocalTransforms) const
{
   const FReferenceSkeleton& RefSkel = GetSkeleton()->GetReferenceSkeleton();
   // Init component space transform with identity
   FTransform ComponentSpaceTransform = FTransform::Identity;
   // Start to walk up parent chain until we reach root (ParentIndex == INDEX_NONE)
   int32 BoneIndex = RefSkel.FindBoneIndex(BoneName);
   while (BoneIndex != INDEX_NONE)
   {
      BoneName = RefSkel.GetBoneName(BoneIndex);
      int32 TrackIndex = GetTrackIndexByName(BoneName);
      // If a track for parent, get local space transform from that
      // If not, get from ref pose
      FTransform BoneLocalTM = (TrackIndex != INDEX_NONE) ? LocalTransforms[TrackIndex] : RefSkel.GetRefBonePose()[BoneIndex];
      // Continue to build component space transform
      ComponentSpaceTransform = ComponentSpaceTransform * BoneLocalTM;
      // Now move up to parent
      BoneIndex = RefSkel.GetParentIndex(BoneIndex);
   }
   return ComponentSpaceTransform;
}
ComposeTransforms (click!)
// FTransform * FTransform
//
// When Q = quaternion, S = single scalar scale, and T = translation
// QST(A) = Q(A), S(A), T(A), and QST(B) = Q(B), S(B), T(B)
// QST (AxB) 
//  QST(A) = Q(A)*S(A)*P*-Q(A) + T(A)
//  QST(AxB) = Q(B)*S(B)*QST(A)*-Q(B) + T(B)
//  QST(AxB) = Q(B)*S(B)*[Q(A)*S(A)*P*-Q(A) + T(A)]*-Q(B) + T(B)
//  QST(AxB) = Q(B)*S(B)*Q(A)*S(A)*P*-Q(A)*-Q(B) + Q(B)*S(B)*T(A)*-Q(B) + T(B)
//  QST(AxB) = [Q(B)*Q(A)]*[S(B)*S(A)]*P*-[Q(B)*Q(A)] + Q(B)*S(B)*T(A)*-Q(B) + T(B)
// Q(AxB) = Q(B)*Q(A)
// S(AxB) = S(A)*S(B)
// T(AxB) = Q(B)*S(B)*T(A)*-Q(B) + T(B)


Add Key & Notify Event

*Fullscreen또는 Ctrl+마우스휠로 ZoomIn/Out 가능

각 Frame 당 Curve Key가 입력되어야 하므로 Animation Sequence의 Frame수만큼 For Loop를 진행

  • Test Mode
    결과값 Test용으로 각 프레임 당 Foot본의 높이값을 Curve의 Key로 출력하여 육안으로 높이 변화 확인
    축이 바뀌어있는 Skeleton의 경우 오류를 확인 할 수 있다
    1. AddFloatCurveKey 10 를 통해 이미 생성된 Curve에 지정된 시간에 float값을 Key로 생성할 수 있다
    2. 시간을 입력해 줘야 하지만 Frame을 기준으로 위치값을 구했기 때문에GetTimeatFrame 11 를 통해 Loop로 돌고 있는 CurrentFrameIndex값을 시간으로 계산해 AddFloatCurveKey에 입력
    3. BoneHeightsByFrame 배열의 해당 순서의 값을 Key value로 입력

image

  • Add Key & Notify Event
    Test Mode를 통해 오류여부를 확인한 뒤 특이사항이 없으면 실제 필요한 Key값과 Notify Event 추가
    1. bool값과 같이 발바닥이 지면에 닿은 시점으로 판단 될 때 true값을 반환받을 수 있도록 산출한 최저높이 값과 ToleranceRange내 근사값들을 1로 치환하고 그 외를 0으로 입력
    2. 발자국 소리를 생성해주는 Notify Event를 첫번째 true 값 입력 시 생성해주기 위해 bool Updated값을 true로 Set 발바닥이 지면에 닿아있는 동안 발자국 소리 이벤트가 지속적으로 호출되어 중복되지 않도록 근사값범위를 벗어난 Key값 입력이 되기 전까지 true로 유지



FootIK 활용 사례

image 지면에 닿은 상태를 1(true)로 반환받는 Curve를 FootIK 적용 시 본IK 트랜스폼의 Alpha값에 입력되도록 구성해보았다

  • 장점 : 모든 모션에 FootIK를 적용한 뒤 일부 상태에 따라 bool적용을 할 필요 없이 실제 발바닥이 지면에 닿는 시점에만 FootIK가 적용되도록 할 수 있음
  • 단점 : 발바닥이 지면에 닿는 시점과 아닌 시점의 변환이 급격하여 보간이 적용되지 않아 무릎이 구부러지는 상황에 덜그럭 거리는 것과 같이 부자연스러워 보임

FootIK_Diff

보간적용이 되지 않아 움직임이 매끄럽지 못한 모습이 만족스럽지 않아 Curve값을 FootIK에 활용하지 않기로 했다



🔊 Footstep Sound

*Fullscreen또는 Ctrl+마우스휠로 ZoomIn/Out 가능

Footstep Notify Event 호출 시 SpawnSoundatLocation 12 을 통해 소리를 낼 수 있다
음향 효과를 조금 더 자연스럽게 연출할 수 있도록 지면의 Physical Material에 따라 적절한 소리를 낼 수 있게 해주는 Sound Cue와 Sound Attenuation의 기능을 사용해보았다

  1. Character Mesh의 World Location을 Start지점으로, Mesh의 하단으로 100만큼 떨어진 지점을 End로 LineTrace를 추적해 충돌한 개체의 Physical Material 정보 확인
  2. Notify 로컬 변수로 지정한 지면 Material이름 string 배열의 값을 조회해 해당 이름과 일치할 경우 해당 정보를 param으로하여 SoundCue에서 해당되는 소리를 출력

*음원파일은 음원 판매 사이트 Sonniss 13 에서 GDC개최기념으로 무료 배포중인 파일들을 사용했습니다

SoundAttenuation

Setting

FootstepSoundObjects

  • 필요한 객체들 추가
구분 객체명 내용
SoundCue SC_FootstepCue 다수의 Sound파일들을 관리
Physical Material 등과 같은 요소들을 param값으로 받을 수 있어
상황에 따라 적절한 Sound파일을 출력할 수 있게 해줌
SoundAttenuation SA_FootstepAttenuation 거리, 구간 등 상황에 따라 소리의 감쇠기능을 적용할 수 있음
PhysicalMaterial PM_Concrete
PM_Grass
PM_Ground
PM_Snow
PM_Wooden
지면의 질감에 따라 상이한 소리를 낼 수 있도록
지면 Mesh에 입힐 Physical Material 파일 생성하여 지면 Mesh에 Set

Sound Cue

image

  1. 자연스러움을 위해 조금씩 다른 소리를 Random으로 출력하도록 같은 Physical Material이더라도 여러 Sound파일을 추가했다
  2. 해당되는 Physical Material을 param을 받았을 때 Switch로 해당 값 출력

Sound Attenuation

AttenuationRange

Sound Attenuation 14을 통해 음원의 발생지점에서부터의 거리에 따라 소리가 크고 작게 들리는 효과를 넣어주었다
cmd` 호출 Audio3dVisualize를 통해 런타임 중 Sound가 발생되는 지점의 감쇠적용을 시각적으로 확인할 수 있다



생각정리

나에게 가장 필요한 것은 더 많은 경험이고 가장 큰 자산이 될 것이다

초기 FootIK를 적용하기 위해 Animation Modifier 연구했으나, Curve값을 통한 FootIK를 적용이 부자연스러운 결과로 Animation Modifier를 활용하지 못하게 되어 아쉬웠다
이후 캐릭터 모션에 있어 발바닥이 지면에 닿는 지점을 활용해 다양한 효과를 넣을 수 있다는 것을 알게되었고, 여러 게임개발 블로그 글과 유튜브 튜토리얼을 검색해 다듬어 나가 원하는 효과를 만들 수 있게되어 매우 뿌듯하다



Reference