다수의 애니메이션에 일괄 적용 가능한 Animation Modifier로 Footstep 기능구현 성공기
요약
- 주요 문제 :
캐릭터 모션 중 발바닥이 지면에 닿는 지점에 줄 수 있는Footstep Sound등 효과적용을 위해 대량의 애니메이션에 수작업으로 Notify를 하나하나 추가하는 비생산적 단순반복 작업 진행 - 해결 방안 :
Animation Modifier로 애니메이션의 Foot본의 최저 X값 산출, curve와 AnimNotifier 자동생성 - 시도한 방법 :
- 발바닥의 위치정보를 얻기 위해 Root-중앙 위치 통과 지점 산출
- 발바닥이 지면에 닿은 상태에만
FootIK적용
- 이루어낸 성과 :
- 다수의 애니메이션에 일괄적으로 Footstep Notify 추가가능하여 목표한 효과 단시간에 설정
- 애니메이션과 본 트랜스폼에 대한 이해
- 주어진 에셋에 제한된 결과물 보단 내 입맛에 맞게 커스터마이징하고 싶은 개발욕심 충족
FootIK 기술구현 상세내용은 관련 Post를 참고해주세요
⚙️ Animation Modifier
발바닥이 지면에 닿는 지점을 true값으로 하여 발이 지면에 닿는 순간 FootIK와 각종 발소리 효과 등을 적용하고자 Animation Sequence에서 Curve를 추가해주었다
다수의 Animation작업을 진행하고나니 단순하지만 반복적이고 양적으로 너무 많아 비효율적인 작업으로 판단되어 방법을 검색을 해보았고, Animation Modifier 1 를 통해 일괄적으로 Animation에 Curve와 Notify를 추가해주는 기능을 알게되었다
Giuseppe Portelli님의 포스트 2 를 통해 Animation Modifier의 개념과 활용을 배웠고, TechAnim Studios님의 튜토리얼 YouTube 영상 3 을 통해 적용사례를 참고 했다
Setting
- 필요한 객체들 추가
| 구분 | 객체명 | 내용 |
|---|---|---|
| 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 구분 |
- 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 가능
NotifiesToRemove에 입력한 삭제하고 싶은 Notify Track 삭제- 입력된 NotifyTrack 이름을
IsValidAnimNotifyTrackName4 를 통해 유효성 검증 - 유효성확인 후
RemoveAnimationNotifyTrack5 을 통해 NotifyTrack 삭제
Track내의 모든 Notify Event들을 일괄 삭제해주기 때문에 편리하다
- 입력된 NotifyTrack 이름을
- 입력된
Footsteps배열을 loop하며 각 요소마다AddCurvesAndNotifies함수 호출
Add Curve & Notify Track
*Fullscreen또는 Ctrl+마우스휠로 ZoomIn/Out 가능
- Footsteps에 입력된 본이름의 마지막 문자에 따라 왼발/오른발을 bool로 구분
- Footsteps에 입력한 Curve이름과 NotifyTrack
AddCurve6 과AddAnimationNotifyTrack7 함수를 통해 생성
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 순으로 저장했다
- 필요한 로컬변수들 초기화, ForLoop내부에서 AccumulatedTransform를 초기값으로 Set
- FootBone의 Local Transform을 Component로 계산하기 위한 2차 Loop 진행 (상세내용 하단 내용 참고)
- 2차 Loop 완료 후 Direction으로 입력한 본의 축정보에 따라 필요한 축의 값 Switch로 선택 및 최종 높이 값 배열에 저장
- 모든 프레임 수 만큼 Loop완료 후 BoneHeightsByFrame배열의 최소값을 MinFootHeight로 Set
👟Local → Component
본의 Transform을 Local, Component, 또는 World스페이스로 변환하는 함수는 Animation그룹에 있는 함수라 Animation Modifier 블루프린트에선 호출을 할 수 없다
따라서 본 Transform을 Local 에서 Component 스페이스로 변환하는 GetComponentSpaceTransform의 구현 방식을 참고하여 직접 계산하는 방법을 알아낼 수 있었다
- 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의 경우 오류를 확인 할 수 있다AddFloatCurveKey10 를 통해 이미 생성된 Curve에 지정된 시간에 float값을 Key로 생성할 수 있다- 시간을 입력해 줘야 하지만 Frame을 기준으로 위치값을 구했기 때문에
GetTimeatFrame11 를 통해 Loop로 돌고 있는CurrentFrameIndex값을 시간으로 계산해AddFloatCurveKey에 입력 BoneHeightsByFrame배열의 해당 순서의 값을 Key value로 입력
- Add Key & Notify Event
Test Mode를 통해 오류여부를 확인한 뒤 특이사항이 없으면 실제 필요한 Key값과 Notify Event 추가- bool값과 같이 발바닥이 지면에 닿은 시점으로 판단 될 때 true값을 반환받을 수 있도록 산출한 최저높이 값과
ToleranceRange내 근사값들을 1로 치환하고 그 외를 0으로 입력 - 발자국 소리를 생성해주는 Notify Event를 첫번째 true 값 입력 시 생성해주기 위해 bool Updated값을 true로 Set 발바닥이 지면에 닿아있는 동안 발자국 소리 이벤트가 지속적으로 호출되어 중복되지 않도록 근사값범위를 벗어난 Key값 입력이 되기 전까지 true로 유지
- bool값과 같이 발바닥이 지면에 닿은 시점으로 판단 될 때 true값을 반환받을 수 있도록 산출한 최저높이 값과
FootIK 활용 사례
지면에 닿은 상태를 1(true)로 반환받는 Curve를 FootIK 적용 시 본IK 트랜스폼의 Alpha값에 입력되도록 구성해보았다
- 장점 : 모든 모션에 FootIK를 적용한 뒤 일부 상태에 따라 bool적용을 할 필요 없이 실제 발바닥이 지면에 닿는 시점에만 FootIK가 적용되도록 할 수 있음
- 단점 : 발바닥이 지면에 닿는 시점과 아닌 시점의 변환이 급격하여 보간이 적용되지 않아 무릎이 구부러지는 상황에 덜그럭 거리는 것과 같이 부자연스러워 보임
보간적용이 되지 않아 움직임이 매끄럽지 못한 모습이 만족스럽지 않아 Curve값을 FootIK에 활용하지 않기로 했다
🔊 Footstep Sound
*Fullscreen또는 Ctrl+마우스휠로 ZoomIn/Out 가능
Footstep Notify Event 호출 시 SpawnSoundatLocation 12 을 통해 소리를 낼 수 있다
음향 효과를 조금 더 자연스럽게 연출할 수 있도록 지면의 Physical Material에 따라 적절한 소리를 낼 수 있게 해주는 Sound Cue와 Sound Attenuation의 기능을 사용해보았다
- Character Mesh의 World Location을 Start지점으로, Mesh의 하단으로 100만큼 떨어진 지점을 End로 LineTrace를 추적해 충돌한 개체의 Physical Material 정보 확인
- Notify 로컬 변수로 지정한 지면 Material이름 string 배열의 값을 조회해 해당 이름과 일치할 경우 해당 정보를 param으로하여 SoundCue에서 해당되는 소리를 출력
*음원파일은 음원 판매 사이트 Sonniss 13 에서 GDC개최기념으로 무료 배포중인 파일들을 사용했습니다
Setting
- 필요한 객체들 추가
| 구분 | 객체명 | 내용 |
|---|---|---|
| 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
- 자연스러움을 위해 조금씩 다른 소리를 Random으로 출력하도록 같은 Physical Material이더라도 여러 Sound파일을 추가했다
- 해당되는 Physical Material을 param을 받았을 때 Switch로 해당 값 출력
Sound Attenuation
Sound Attenuation 14을 통해 음원의 발생지점에서부터의 거리에 따라 소리가 크고 작게 들리는 효과를 넣어주었다
cmd` 호출 Audio3dVisualize를 통해 런타임 중 Sound가 발생되는 지점의 감쇠적용을 시각적으로 확인할 수 있다
생각정리
나에게 가장 필요한 것은 더 많은 경험이고 가장 큰 자산이 될 것이다
초기 FootIK를 적용하기 위해 Animation Modifier 연구했으나, Curve값을 통한 FootIK를 적용이 부자연스러운 결과로 Animation Modifier를 활용하지 못하게 되어 아쉬웠다
이후 캐릭터 모션에 있어 발바닥이 지면에 닿는 지점을 활용해 다양한 효과를 넣을 수 있다는 것을 알게되었고, 여러 게임개발 블로그 글과 유튜브 튜토리얼을 검색해 다듬어 나가 원하는 효과를 만들 수 있게되어 매우 뿌듯하다
Reference
-
Unreal Engine. Unreal Engine Documentation. Animation Modifiers ↩
-
Giuseppe Portelli(2017.11.1). A clockwork berry. Automated foot sync markers using animation modifiers in Unreal Engine ↩
-
TechAnim Studios(2020.08.15). YouTube. UE4 Animation Modifier-Automated Foot Sync Markers-#UE4#UE4Tuts ↩
-
Unreal Engine. Unreal Engine Documentation. IsValidAnimNotifyTrackName ↩
-
Unreal Engine. Unreal Engine Documentation. RemoveAnimationNotifyTrack ↩
-
Unreal Engine. Unreal Engine Documentation. AddAnimationNotifyTrack ↩
-
Unreal Engine. Unreal Engine Documentation. Compose Transforms ↩
-
Unreal Engine. Unreal Engine Documentation. Get Bone Pose for Frame ↩
-
Unreal Engine. Unreal Engine Documentation. AddFloatCurveKey ↩
-
Unreal Engine. Unreal Engine Documentation. GetTimeatFrame ↩
-
Unreal Engine. Unreal Engine Documentation. SpawnSoundatLocation ↩
-
Sonniss. Sonniss. Royalty Free Sound Effects Archive: GameAudioGDC ↩
-
Unreal Engine. Unreal Engine Documentation. SoundAttenuation ↩