티스토리 뷰

유니티 웨이브 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?

 

[Spine] 게임 제작을 위한 최고의 애니메이션 툴! “스파인 (SPINE)”

[Spine] 게임 제작을 위한 최고의 애니메이션 툴! “스파인 (SPINE)” [홈페이지] http://ko.esotericsoftware.com/ 오늘은 게임에서 사용되는 2D 애니메이션을 조금 더 쉽게 만들 수 있는 방법에 대해 알아보

blueasa.tistory.com

* 게임 크리에이터를 위한 2D 스켈레톤 애니메이션 제작 도구

* 애니메이션과 물리 시뮬레이션을 혼합하여 지원하며 편집에 용이

* 머리카락, 엉덩이 등 반동으로 인한 흔들림 애니메이션을 별도 제작하지 않아도 ok (물리 시뮬레이션)

 

[장점/특성]

 

  1. 동작을 만들기 위해 이미지를 추가할 필요가 없습니다.
  2. 용량 절감 효과가 탁월합니다. 파츠 애니메이션으로 동작하기 때문에 수백가지 동작을 구현해도 적은 데이터만을 사용합니다.
  3. 애니메이션을 만들고 구성하기가 편합니다.
  4. 같은 스켈레톤을 사용하면 이미지를 바꿔도 같은 동작을 하게 만들 수 있습니다.
  5. 게임의 상황에 맞게 동작의 속도를 조절할 수 있습니다.
  6. 기존 프레임 애니메이션보다 동작이 매우 부드럽습니다.
  7. 새로운 애니메이션을 만들때  기존의 애니메이션을 다시 사용 할 수 있습니다.

 

[시연 영상]

 

 

Reference

 

'니케'는 머리카락을 프레임마다 계산한다

기본적으로 '니케' 캐릭터 움직임은 유니티 스파인 물리 시뮬레이션을 활용해 제작됐다. 이는 애니메이션과 물리를 혼합해 지원하고, 편집이 편리하다는 장점이 있다. 또한, '니케'에 중요한 머

www.inven.co.kr

 

이제민 개발자(retr0) 저서 <레트로의 유니티 게임 프로그래밍 에센스>

 

레트로의 유니티 게임 프로그래밍 에센스 - YES24

소문난 명강사 ‘레트로’가 게임 개발 입문자에게 보내는 선물 같은 책 게임을 만드는 ‘완벽한 준비’를 위해 시간을 낭비하지 말자. 이 책은 기본을 빠르게 익히고 나서 게임을 직접 만들며

www.yes24.com

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/01   »
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
글 보관함