Home 13. 스키닝 애니메이션
Post
Cancel

13. 스키닝 애니메이션

갑자기 이게 수학(?)에 나오네요 하하;;

만약 메쉬를 수정할 때 모든 버텍스를 직접 조작하면 좋겠지만,

오래걸리고 번거로우니 트랜스폼의 부모 개념인 본(Bone) 기준으로 이동하도록 설정하는 것입니다.

하지만 그렇게 되면 관절부분이 이동하거나 회전할때 찢어지게 되겠죠?

그걸 해결하기 위해 스키닝을 사용하게 됩니다.

버텍스마다 어떤 본들의 이동에 더 초점을 두는지를 계산하게 되는데요,

무게중심좌표를 임의로 설정하여 어느쪽 본에 더 가까운지(영향을 받는지)를 나타낸다고 생각하면 편합니다.

그렇게 구현된 skinned Mesh는 약간 고무풍선과 유사하다고 생각하시면 됩니다.

늘리면 늘어나고, 돌리면 꺾입니다. 모두 이어진 상태로요.

이때 설정하는 무게중심좌표를 Weight, 가중치라고 부릅니다.

안녕하세요, 리깅입니다. (Hello, Rig)

안녕하세요, 리깅입니다. (Hello, Rig)

스키닝 애니메이션을 위한 메시에 추가적으로 사용되는 자료와 그 원리


일단 위에서 기술하였듯, 본이라는 개념이 필요합니다.

본(Bone)은 일단 위치정보를 들고 있어야 하므로 트랜스폼을 가지고있어야하며,

본과 본을 구분할 수 있는 구분자가 필요합니다. 이때 키로 이름을 사용하게 됩니다.

또한 어디에 있었는지 최초의 값이 필요합니다. 이를 BindPose라 합니다.

버텍스관점에서 생각해봅시다.

버텍스는 어떤본의 이동에 대해 얼마만큼 이동을 할지 결정해야합니다.

즉 1개의 본에 대해서는 해당 본을 알고있어야하며, 얼마나 영향을 받는지를,

2개의 본에 대해서는 각 본을 알고 있어야하며,

각 본에 대한 영향치(결국 1이 되어야 하는 아핀조합을 생각하면 편할 것 같습니다. )

를 가지고 있어야 할 것 입니다.

따라서 각 버텍스는 추가적으로

연결된 본의 이름(구분자)와 해당 본에 대응하는 weight값을 가진 ‘리스트’가 필요합니다.

본에 설정하는 BindPose는 무엇인지, 왜 필요한지에 대해 정리하시오.


BindPose는 본의 원점이라고 생각하면 편합니다.

자주 접하는 그래픽 관련 프로그램 회사인 오토데스크에서 설명하는 BindPose는 다음과 같습니다.

바인드 포즈는 스킨을 바인드 할 때 골격이있는 포즈이므로 캐릭터의 기본 (또는 휴식) 포즈입니다. 스키닝 후 캐릭터의 골격을 포즈를 취하면 골격의 동작이 피부를 변형시킵니다. 피부에 변형을 일으키지 않는 유일한 포즈는 바인드 포즈입니다.

결국 변환을 이어질수록 본의 위치는 제각각이 될텐데, 이게 현재 얼마나 이동하였는지, 얼마나 회전하였는지에 대한 값을 알 수 없으므로 원위치를 저장해 두자는 것이죠.

마치 어떤 크기를 기준으로 +-1의 사이즈 변환을 할 때, 원래의 값을 모른다면 계산할 수 없듯이요.

그렇게 설정한 BindPose를 기준으로 본의 로컬공간을 계산한 다음,

BindPose 공간으로 정점을 변환하여 연산을 처리하여 다시 모델링공간으로 변환하고,

각 점을 처리합니다.

더 자세하게 들어가봅시다.

BindPose 공간에서 정점을 변환한 후 가중치를 적용하는 스키닝 연산의 과정


BindPose 공간에서 정점을 변환한 후,

BindPose공간을 기준으로 한 본의 로컬공간을 계산해야합니다.

그렇게 만들어진 본의 로컬 공간에서 아까 구해뒀던 바뀐 정점을 변환한 값을

최종적으로 다시 모델링공간으로 다시 변환합니다.

이렇게 구한 모델링공간에서의 최종 좌표에 가중치를 곱한것을 모두 더하면

버텍스 하나의 스키닝 연산이 완료되게 됩니다.

간단한 의사코드와 함께 보시면 편할 것 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
var totalPos = Vector.zero;
foreach(var bone in currentVertex.Bones)
{
	var bindPose = bone.BindPose;

	var localPos = bindPose.WorldToLocal(currentVertex.position);
	var boneLocal = bone.worldTransform.WorldToLocal(bindPose);
	var convertedLocalPos = boneLocal.Matrix * localPosition;
	var convertedWorldPos = bindPose.Matrix * convertedLocalPos;
	totalPos += convertedWorldPos * bone.Weight
}
...
currentVertex.Position = totalPos;

그러면 이제 구현으로 넘어갑시다.

Implementation

2D 캐릭터와 본을 설계


하다보니 엔진기초에서 다룬 Unity의 2D Sprite가 생각났습니다.

어짜피 같은 2D에, 본을 사용하고, 버텍스가 있고, 스프라이트가 있다면..

저장된 파일에서 긁어오면 타이핑도 적게하고 빠르지 않을까? 라는 헛된 생각에서 시작하였습니다.

일단 너무 많은 버텍스와 본을 가진 파일을 대강 정리해서 머리카락에만 스키닝 애니메이션을 해보도록 도전하였습니다.

따라서 일단 unity를 켜서 2D Bone과 Weight, 스프라이트를 설정하고, psb를 통해 아틀라스를 한장 구워줍니다.

14%20Skinning%20Animation%2038b8ec827fed419eb6730052937385dd/Untitled.png

14%20Skinning%20Animation%2038b8ec827fed419eb6730052937385dd/Untitled%201.png

14%20Skinning%20Animation%2038b8ec827fed419eb6730052937385dd/Untitled%202.png

그리고 해당 파일의 위치로 찾아갑니다.

일단은 해당 파일의 메타를 까보면 이곳에 데이터가 들어있음을 알 수 있습니다.

770줄. 단순 쿼드 하나와 로우폴리급 데이터, 각 스프라이트당 4개의 본, 총 9개의 본이 존재할 뿐이지만... 770줄.

770줄. 단순 쿼드 하나와 로우폴리급 데이터, 각 스프라이트당 4개의 본, 총 9개의 본이 존재할 뿐이지만… 770줄.

사실 여기서 방대한 데이터를 보며

포기하고 그냥 하려는 것이나 했어야 했습니다.

하지만 저는 할 수 있다고 생각했고,

그렇게 간단하게 시작한 이짓은 정말

오래….오래…..힘겹고 힘든 사투를

이어나가게 되었습니다.

함부로 뱉으면 안되는말

함부로 뱉으면 안되는말

그렇게 진행되었습니다.

버텍스를 따오고

우측 스크롤에 보이는 저 긴 스크롤 다 버텍스입니다

우측 스크롤에 보이는 저 긴 스크롤 다 버텍스입니다

14%20Skinning%20Animation%2038b8ec827fed419eb6730052937385dd/Untitled%206.png

삼각형 인덱스를 작성합니다.

1
2
3
4
5
for (size_t part = 0; part < totalCharacterParts; part++)
	{
		std::transform(cubeMeshPositions.begin(), cubeMeshPositions.end(), std::back_inserter(v), [&](auto& p) { return p * cubeMeshSize[part] + cubeMeshOffset[part]; });
		std::transform(cubeMeshIndice.begin(), cubeMeshIndice.end(), std::back_inserter(i), [&](auto& p) { return p + 24 * part; });
	}

기존에는 쿼드를 그리기때문에 쉽게 그냥 메모리에 일반화하여 반복적으로 박은 저 인덱스버퍼를

저는 하나하나 버텍스를 손으로 작성하며, 기존 버텍스위치와 대조하여 버텍스 인덱스를 확인한 후,

순서를 매겨 삼각형을 만들어줍니다.

14%20Skinning%20Animation%2038b8ec827fed419eb6730052937385dd/Untitled%207.png

인덱스버퍼와 본 가중치를 작성하며 .. 테스트하는데 뭔가 이상합니다.

이때 깨달은건 유니티는 바이트를 저장할때 엔디안변환이 발생합니다.

마치 물리 피직스 매트릭스를 저장하는 방식과 같이요.

그래서 바이트로 저장된 데이터를 그대로 읽으면 꼬입니다. 하하

아마 읽을때 리틀엔디안 변환을 해야 제대로 읽을 수 있다고 기억합니다.

유니티 물리 매트릭스는 그렇게 읽기 때문이죠.

아무튼 다시 돌아가 제대로 다시 인덱스버퍼와 가중치를 (직접)적어줍니다.

위에 적어둔 버텍스 순서를 따라가며 가중치를 눈으로 확인하면서요

압도적인 indices

압도적인 indices

14%20Skinning%20Animation%2038b8ec827fed419eb6730052937385dd/Untitled%209.png

어찌어찌 두장의 텍스쳐를 띄우는데까지는 성공합니다.

(소프트렌더러 PNG나 TARGA 파일 투명도 넣어주세요 ㅜㅜㅜ)

(소프트렌더러 PNG나 TARGA 파일 투명도 넣어주세요 ㅜㅜㅜ)

본의 포지션도 넣어줬는데 정상적으로 작동하지 않습니다.

왜일까요?

14%20Skinning%20Animation%2038b8ec827fed419eb6730052937385dd/Untitled%2011.png

14%20Skinning%20Animation%2038b8ec827fed419eb6730052937385dd/Untitled%2012.png

유니티2D 리깅의 경우 Position의 x는 회전한 본의 진행방향, y는 진행방향의 좌측을 사용하기 때문입니다. (열심히 읽으면서 분석했습니다.)

심지어 회전은 쿼터니언(사원수)를 사용하네요.

그냥 대강 깡으로 박아줍니다.

14%20Skinning%20Animation%2038b8ec827fed419eb6730052937385dd/Untitled%2013.png

아주 좋습니다.

아주 좋습니다.

원래 캐릭터 헤어가 두갈래인데..

어짜피 투명도 안되어있어서 하나더하면 또 가릴텐데

그냥 하나만 넣읍시다.

노가다를 한번 더 할려 하니 척추가 거절합니다.

캐릭터 애니메이션


과제에 썻던 애니메이션을 한번 켜봅니다

14%20Skinning%20Animation%2038b8ec827fed419eb6730052937385dd/Untitled%2015.png

음… 지저분하군요.

다시 끕니다.

14%20Skinning%20Animation%2038b8ec827fed419eb6730052937385dd/Untitled%2016.png

어짜피 쓸 값은 이것 밖에 없습니다.

이 커브도 넣을려면 넣을 순 있지만…

베지어커브를 구현하고 Evaluate함수를 만들어서

키마다의 값의 보간값을 구해내면 됩니다.

언젠가는 구현해보면 좋을 것 같습니다. 지금은 말고요.

지금은 대충 sin 비슷하게해서 약간 오프셋을 주고 진행합니다.

어떻게요? 로테이션으로요.

14%20Skinning%20Animation%2038b8ec827fed419eb6730052937385dd/assignment_GIF_14_1.gif

14%20Skinning%20Animation%2038b8ec827fed419eb6730052937385dd/assignment_GIF_14_2.gif

짠.

정리되지 않은 잡다한게 많은 제 소스코드도 첨부합니다.

감사합니다!

SoftRenderer2D.CPP

https://gist.github.com/ashuatz/48c0184618f2d45babdec4bbd8d82732

GameEngine.cpp

https://gist.github.com/ashuatz/15f448b30bef255b9e1819dfb3f370e4

… 제 이쁜 사각형 주석이 깨져서 슬프네요

14%20Skinning%20Animation%2038b8ec827fed419eb6730052937385dd/Untitled%2017.png

This post is licensed under CC BY 4.0 by the author.
Contents