티스토리 뷰
[유니티 웨이브 2022] 스파인 캐릭터 헤어 & 엉덩이 흔들림 물리구현 세션
dancefirst 2022. 5. 20. 17:01유니티 웨이브 2022에서 시프트업 이제민 클라이언트 프로그래머가 발표한 물리 시뮬레이션 세션을 구현 코드와 함께 정리.
기본 구현: 훅의 법칙 / 부모-자식 회전값 상속 / 고스트 스켈레톤
* 기본적으로 흔들림 애니메이션을 위한 머리카락 움직임, 엉덩이 움직임 계산은 병렬 연산으로 수행
* 머리카락 및 엉덩이 애니메이션에 '훅의 법칙' 적용 -> "물체가 원 위치에서 벗어난 만큼 반대방향으로 힘이 생긴다" (=스프링)
* 머리카락에 앞서 머리 움직임이 선행 -> "자식 본"은 "부모 본"의 움직임을 참조한다(따라간다)
스켈레톤 본 & 가상 본
* '스켈레톤 본' -> 애니메이션에 따른 최신 본 위치를 가상 본에 전달한다
* '가상 본' -> 훅의 법칙을 반영한 새로운 위칫값을 스켈레톤 본으로 반환한다
* 가상 본이 최종 계산한 값은 스켈레톤 본에 전달한 뒤 별도 저장되지 않고 삭제
병렬 연산에 따른 연산순서 지정의 어려움?
* 연산의 병렬처리로 어떤 본이 먼저 계산될 지는 제어할 수 없음 -> 오차 발생 가능
* 그러나 병렬처리로 얻어지는 성능향상을 위해 일부 오차를 감수
* 물리 연산을 자주 실행함으로써 오차 감소 효과를 얻을 수 있었다고 함
Problem & Solution
[계단 현상]
* 머리카락(자식 본)이 물리에 의해 위치가 변했음에도, 부모 본의 rotation은 그대로 유지되는 문제
* 부모 본에 회전 옵션을 추가하여, 부모 본이 자식 본의 변화를 참고하여 회전하도록 하여 문제 해결
[물리효과와 원본 애니메이션의 결합(혼합)]
* 최종적으로 '스켈레톤 본'에 반환되는 위치 값은 물리효과 + 원본 애니메이션 + 부가 변환 연산이 뒤섞인 값
* 단일 스켈레톤으로는 정보량 부족으로 원본 애니메이션과 물리효과 등을 충분히 구현하기 어려움
* 고스트 스켈레톤을 구현, (1) 애니메이션만 반영한 위치와 (2) 물리만 반영했을 때의 위치를 나누어 관리함으로써 문제 해결
* 매 프레임 렌더링 직전에 2개 정보를 혼합하여 처리함
[오버 슈팅]
* 현실 시간과 게임 시간의 차이로 인해 발생
* 현실 시간은 무한하게 쪼개지지만(analogue), 게임 시간은 델타 타임으로 샘플링된 이산 시간(digital)이라는 차이
* 오버슈팅으로 인해 스프링 본이 과도하게 움직이거나, 의도된 이동 경로를 벗어나 튕겨나가는 문제가 발생함
* 완전한 해결법은 존재하지 않으나, 최종적으로 연산 주기를 타이트하게 줄여 더 자주 실행함으로써 보완
* 스프링 질량, 강성 값을 자주 업데이트함으로써 안정성 강화
* 직접적 해결 방법으로는 '오일러-크로머 방법'이 있음
어떤 방법으로도 완벽하게 (오버슈팅) 해결은 불가능하지만,
'니케'의 경우 연산을 더 자주 실행하는 것으로 보완했다
Summary
- '승리의 여신: 니케'의 머리카락과 엉덩이 흔들림을 스프링 물리 시뮬레이션으로 구현
- 캐릭터 모델의 본과 물리 계산용 본을 나누어서 사용
- 성능을 위해 버스트(Burst) 컴파일러와 C# Job 시스템 활용
- 물리-애니메이션 혼합을 위해 고스트 스켈레톤 사용
Code Implementations
SpringChainJob
스프링 본 체인에 대한 물리 연산 Job
public struct SpringChainJob : IJobParallelFor
{
public SpringChainJob(NativeArray<BoneSpringAccess> boneSpringAccesses, float deltaTime)
{
BoneSpringAccesses = boneSpringAccesses;
_deltaTime = deltaTime;
}
private readonly float _deltaTime; // 시간 간격
private NativeArray<BoneSpringAccess> BoneSpringAccesses; // 연산에 사용할 스프링 본 액세스 변수들
public void Execute(int index)
{
var boneSpringAccess = BoneSpringAccesses[index];
// 연산_
BoneSpringAccesses[index] = boneSpringAccess;
}
}
SpringChainJob.Execute(int Index)
각 본에 대한 물리 연산
public void Execute(int index)
{
var boneSpringAccess = BoneSpringAccesses[index]; // 계산할 본 가져오기
var bonePositionInSkeletonSpace = boneSpringAccess.PositionInSkeletonSpace; // 계산할 본의 world location 가져오기
// 현재 시점에서 부모 본과의 거리 측정
var currentDistanceFromPArentBone =
bonePositionInSkeletonSpace - boneSpringAccess.ParentPositionInSkeletonSpace;
// 부모 본과의 "원래 거리"와 현재 거리의 차이 측정
var distanceDifference = currentDistanceFromParentBone -
boneSpringAccess.StartPositionFromParentBoneSkeletonSpace;
// 힘 = -1 * 강성 * 거리차이
var springForce =
-1f * boneSpringAccess.Stiffness * distanceDifference;
// 감쇄(dampening) 힘 계산
var dampingForce = boneSpringAccess.Damping * boneSpringAccess.Velocity;
// 최종적으로 본에게 가해지는 힘 = 스프링 장력 - 감쇄력
var force = springForce - dampingForce;
// 힘으로부터 가속도를 구한다. 가속도 = 힘 / 질량
var acceleration = force * boneSpringAccess.MassReversed;
// 본의 현재 속도를 갱신한다
boneSpringAccess.Velocity += acceleration;
// 본의 현재 위치를 갱신한다: deltaTime 만큼의 이동량을 현재 위치에 더해주기.
boneSpringAccess.PositionInSkeletonSpace += boneSpringAccess.Velocity * _deltaTime;
// 계산된 사항을 반영한다
BoneSpringAccesses[index] = boneSpringAccess;
// +분량 상 보조 스프링을 포함한 기타 보조연산 생략
}
FixedUpdate()
물리 갱신 실행 주기 + 직전 FixedUpdate()에서 예약한 물리연산 Job 강제 완료처리
protected void FixedUpdate()
{
// 직전 FixedUpdate()에서 예약한 물리 연산 Job 강제완료
_updateSkeletonJobHandle.Complete();
// 먼저 액세스 -> 원본에 반영
// 계산이 끝난 스프링 본 액세스 -> 스프링 본 방향으로 결과(위치, 속도) 전달
for (var i = 0; i < _boneSpringAccesses.Length; i++)
{
var boneSpringAccess = _boneSpringAccesses[i];
var boneSpringOrigin = _boneSpringOrigins[i];
boneSpringOrigin.Velocity = boneSpringAccess.Velocity;
boneSpringOrigin.PositionInSkeletonSpace = boneSpringAccess.PositionInSkeletonSpace;
}
// 원본 -> 액세스로 갱신
// 스프링 본 -> 스프링 본 액세스 방향으로 다음 연산에 사용할 값들을 전달
for (var i = 0; i < _boneSpringAccesses.Length; ++i)
{
var boneSpringAccess = _boneSpringAccesses[i];
var boneSpringOrigin = _boneSpringOrigins[i];
// 스켈레톤 본의 최신 위치를 스프링 본 액세스에 반영하는 연산도 포함되어 있음
boneSpringAccess.UpdateFromOrigin(boneSpringOrigin);
_boneSpringAccesses[i] = boneSpringAccess;
}
// 배치 크기 8 단위로 새로운 물리연산 Job 예약
_updateSkeletonJobHandle = new SpringChainJob(_boneSpringAccesses, Time.deltaTime)
.Schedule(_boneSpringAccesses.Length, 8);
}
Unity Spine?
* 게임 크리에이터를 위한 2D 스켈레톤 애니메이션 제작 도구
* 애니메이션과 물리 시뮬레이션을 혼합하여 지원하며 편집에 용이
* 머리카락, 엉덩이 등 반동으로 인한 흔들림 애니메이션을 별도 제작하지 않아도 ok (물리 시뮬레이션)
[장점/특성]
- 동작을 만들기 위해 이미지를 추가할 필요가 없습니다.
- 용량 절감 효과가 탁월합니다. 파츠 애니메이션으로 동작하기 때문에 수백가지 동작을 구현해도 적은 데이터만을 사용합니다.
- 애니메이션을 만들고 구성하기가 편합니다.
- 같은 스켈레톤을 사용하면 이미지를 바꿔도 같은 동작을 하게 만들 수 있습니다.
- 게임의 상황에 맞게 동작의 속도를 조절할 수 있습니다.
- 기존 프레임 애니메이션보다 동작이 매우 부드럽습니다.
- 새로운 애니메이션을 만들때 기존의 애니메이션을 다시 사용 할 수 있습니다.
[시연 영상]
Reference
이제민 개발자(retr0) 저서 <레트로의 유니티 게임 프로그래밍 에센스>
- Total
- Today
- Yesterday
- 원유로필터
- PoseNet
- 컴퓨터그래픽스 좌표계와 변환
- vertex shader
- 메타버스
- 컴퓨터그래픽스 강의
- 컴퓨터그래픽스
- 고려대학교 한정현
- tensorflow.js
- 3d affine transform
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |