2016.12.07 09:19

제 1 조 (목적)

이 약관은 TeamDB(이하 “회사”)에서 제공하는 유•무선을 이용한 인터넷 서비스 또는 온•오프라인 상의 게임 서비스(이하 “서비스”,접속 가능한 유•무선 단말기의 종류와는 상관없이“회사”가 제공하는 이용 가능한 모든 “서비스”를 의미합니다.)와 관련하여 서비스 내 개인정보 항목, 수집방법, 공유, 보유, 파기, 보호, 운영 등취급방법에 대한 사항을 공개 할 목적으로 작성 되었습니다.


제 2 조 (수집하는 개인정보의 항목 및 수집방법)

서비스는 회원가입, 원활한 고객상담, 회원간의 교류, 각종 서비스의 제공 등을 위해 아래와 같은 항목과방법으로 개인정보를 수집하고 있습니다.

1. 회원의 이메일 주소, 비밀번호, 닉네임을 회원 가입 시 수집 합니다.

2. 회원가입 후 성별, 연령, 사진, 인사말등 추가로 입력된 개인 정보도 수집을 합니다.

3. 1항과 2항에대한 별도의 동의 절차가 없는 서비스는 1항과 2항의 정보를수집하지 않습니다.

4. 서비스 이용에 따른 PC Device 정보, 서비스 접속 Application, 자동 생성 정보, IP Address, 최근접속시간, 위치정보, 회원의 휴대폰 번호, 디바이스 정보(종류 및 UDID 또는 IMEI), 서비스 이용 기록, 불량 이용 기록 등이 수집 될 수있습니다.

5. 서비스 이용 또는 사용함으로써 자동으로 수집될 수 있습니다.

6. 서비스 가입이나 사용 중 회원의 자발적 제공을통해 수집 될 수 있습니다.

7. 유료 서비스 이용 과정에서 카드, 휴대전화, 구매내역 등의 결제 정보들이 수집 될 수 있습니다.


제 3 조 (개인정보의 수집목적 및 이용목적)

1. 회원가입 시에 기기종류, 기기고유번호 (디바이스 아이디 또는 IMEI)를 수집하고 저장하여 회원의 전화번호와 조합하여 개인식별을 위한 정보로 사용할 수 있습니다.

2. 회원 상태정보(인사말), 회원 닉네임, 사진은회원이 다른 회원과의 소통과정에서 자신을 설명하기 위해 등록하는 정보로써 회원간에는위 정보가 서로에게 공개될 수 있습니다.

3. 본인확인,개인식별, 불만처리 등 민원처리, 고지사항 전달및 불량회원의 부정이용방지와 차단에 사용 합니다.

4. 신규 서비스 개발 및 마케팅, 광고에 활용됩니다.

5. 맞춤 서비스 제공, 통계학적 특성에 따른 서비스 제공 및 광고 게재, 서비스의 유효성확인, 이벤트 및 광고성 정보 제공 및 참여기회 제공, 접속빈도파악, 회원의 서비스 이용에 대한 통계자료로 사용됩니다.

6. 유료정보 이용에 대한 문의처리 및 계약이행분쟁처리, 결제환불 등 고객 서비스 제공을 위해 사용됩니다.


제 4 조 (개인정보의 공유 및 제공)

서비스는 회원들의 개인정보를 ‘3. 개인정보의 수집목적 및 이용목적’에서고지한 범위 내에서 사용하며 회원의 사전 동의 없이는 지정한 범위를 초과하여 이용하거나 원칙적으로 회원의 개인정보를 외부에 공개하지 않습니다. 다만 아래의 경우에는 예외로 합니다.

      1. 회원이 공개에 동의한 정보

      2. 서비스 제공에 관한 계약의 이행을 위해 필요한개인정보로서 경제적/기술적인 사유로 통상의 동의를 받는 것이 

         현저히 곤란한 경우

      3. 서비스 제공에 따른 요금정산을 위하여 필요한경우

      4. 법령의 규정에 의거하거나, 수사의 목적으로 법령에 정해진 절차와 방법에 따라 수사기관의 요구가 있는 경우

      5. 금융실명거래및비밀보정에관한법률, 신용정보의이용및보호에관한법률, 전기통신기본법, 전기통신사업법, 지방세            법, 소비자보호법, 한국은행법, 형사소송법 등 법률에 특별한 규정이 있는 경우


제 5 조 (개인정보의 보유 및 이용기간)

원칙적으로 개인정보는 원칙적으로 이용자의 회원가입 후 탈퇴 전까지 이용자의 개인정보를 보관하며, 이용자가 탈퇴를 하거나 개인정보수집 및 이용목적이 달성된 후에는 해당 정보를 지체 없이 파기합니다. 하지만 아래의 정보는 회사의 정책에 따라 백업 사본이 아래 기간까지 보관될 수도 있습니다.

     1. 회원탈퇴 시 보존 개인정보

        - 보유근거:부정 이용 방지 (회사내부 방침) | 보존기간: 회원탈퇴 후 1년

     2. 본인확인 관련기록 보존 개인정보

        - 보유근거:정보통신망 이용촉진 및 정보보호 등에 관한 법률 | 보존기간: 6개월

     3. 서비스 접속기록

       - 보유근거:통신비밀보호법 | 보존기간: 3개월

     4. 소비자의 불만 또는 분쟁처리에 관한 기록

       - 보유근거:전자상거래 등에서의 소비자 보호에 관한 법률 | 보존기간:3년

     5. 계약 또는 청약 철회 등에 관한 기록

       - 보유근거:상법, 전자상거래 등에서의 소비자보호에 관한 법률 | 보존기간: 5년

     6. 대금결제 및 재화 등의 공급에 관한 기록

       - 보유근거:상법, 전자상거래 등에서의 소비자보호에 관한 법률 | 보존기간: 5년


제 6 조 (개인정보 파기 절차 및 방법)

개인정보 파기절차 및 방법은 다음과 같습니다.

1. 회원이 회원가입 및 서비스 이용을 위해 입력한정보는 목적이 달성된 후 별도의 DB로 옮겨져 (종이의 경우별도의 서류함) 내부 방침 및기타 관련 법령의 정보 보호 사유에 따라(보유 및 이용기간 참조) 일정 기간 저장된 후 파기됩니다.

2. 개인정보는 법률에 의한 경우가 아닌 한 보유되는이외의 다른 목적으로 이용/제공되지 않습니다.

3. 종이에 출력된 개인정보는 분쇄기로 분쇄하거나소각을 통하여 파기합니다.

4. 전자적 파일 형태로 저장된 개인정보는 기록을재생할 수 없는 기술적 방법을 사용하여 삭제합니다.

5. 회원탈퇴를 아니하고 앱을 삭제했을경우 서비스 제공사에게 개인정보가 남아있을 수 있습니다.

이때 회원께서는 서비스 제공자에게 별도의 탈퇴신청을 요청해 주셔야 합니다.

제 7 조 (개인정보 자동 수집 장치의 설치/운영 및 거부에 관한 사항)

1. 회원은 원칙적으로 개인정보 보호법에 의해개인정보 수집 동의를 거부할 권리가 있으며, 수집동의 거부 시에는 서비스를 이용할 수 없습니다.

2. 계정정보를 생성하기 위해 회원이 서비스 실행시 기기식별번호 (디바이스 아이디 또는 IMEI)를 자동으로수집하게 됩니다.

3. 서비스 내에서 회원간의 거리를 표시하기 위해회원의 위치정보를 기기에 내장된 위치정보 제공 장치로부터 받아 수집할 수 있습니다.


제 8 조 (개인정보의 기술적/관리적 보호 대책)

“회사”는 회원들의 개인정보를 취급함에 있어 개인정보가 분실, 도난, 누출, 변조 또는 훼손되지 않도록 안전성 확보를 위하여 다음과 같은

기술적/관리적 대책을 강구하고 최선의 노력을 다하고 있습니다.

1. 회원의 개인정보는 비밀번호에 의해 보호되며, ID와 비밀번호는 암호화되어 저장 및 관리되고 있어 본인만이 알고 있으며, 아이디정보는변경기능을 제공하고 있지 않습니다.

2. 해킹이나 컴퓨터 바이러스 등에 의해 회원의개인정보가 유출되거나 훼손되는 것을 막기 위해 최선을 다하고 있습니다.

3. 개인정보의 훼손에 대비하여 방화벽 등을 이용하여회원들의 개인정보나 자료가 누출되거나 손상되지 않도록 방지하고 있습니다. 기타시스템적으로 보안성을확보하기 위한 가능한 모든 기술적 장치를 갖추도록 노력하고 있습니다.

4. 개인정보관련 취급 직원은 담당자에 한정시키고있고 이를 위한 접근에는 별도의 비밀번호를 부여하고 있으며, 담당자에 대한 수시 교육을 통하여 개인정보취급방침의준수를 항상 강조하고 있습니다.


제 9 조 (제3자 사이트로의 이동)

“회사”는 이용자에게 제 3자의 웹사이트 또는 자료에 대한 링크를제공할 수 있습니다. 이 경우 “회사”는 제 3자의 외부사이트 및 자료에 대한 아무런 통제권이 없으며, 해당 사이트의 개인정보보호정책 및 약관사항은 “회사”와무관하므로 방문한 사이트의 정책을 확인하여 불이익및 물질적 손해가 일어나지 않도록 주의하여야 합니다. 이로인해 발생한 책임은 전적으로 이용자에게 있습니다.


제 10 조 (개인정보관리책임자 및 담당자의 연락처)

회원께서는 회사의 서비스를 이용하면서 발생하는 모든 개인정보보호 관련 민원을 개인정보관리책임자 혹은 담당부서로신고하실 수 있습니다. 회사는 이용자들의 신고사항에 대해 신속히 충분한 답변을 드릴 것입니다.

1. 개인정보 관리 책임자

이 름: 황대웅

전화번호: 010-3787-1676

메 일: kayserdragon@naver.com

기타 개인정보침해에 대한신고나 상담이 필요하신 경우에는 아래 기관에 문의하시기 바랍니다.

- 개인정보침해신고센터 (privacy.kisa.or.kr/ / 118)

- 정보보호마크인증위원회 (www.eprivacy.or.kr / 02-550-9531~2)

- 대검찰청 첨단범죄수사과 (www.spo.go.kr /02-3480-2000)

- 경찰청 사이버테러대응센터 (www.ctrc.go.kr / 182)

부칙

공고일자: 2017년 11월 20일

시행일자: 2017년 11월 20일

신고
Posted by 우엉 여왕님!! ghostkyow
2015.08.06 22:46

데코레이터 패턴 : 

 - 객체에 추가 요소를 동적으로 더할 수 있다.

 - 데코레이터 패턴을 사용하면 서브 클래스를 만드는 경우에 비해 훨씬 유연하게 기능을 확장할 수 있다.

 - 특정 요소의 형식을 알아내서 그 결과를 바탕으로 작업해야 하는 코드에는 적합하지 않다.

 - 데코레이터는 팩토리나 빌더 같은 다른 패턴을 써서 사용되는 경우가 많다.


급속도로 성장한 초대형 커피 전문점인 스타버즈 커피!

처음 사업을 시작할 무렵에 만들어진 클래스들은 아마 다음과 같은 식으로 구성되었을 것이다.



[그림 1. 초기 스타버즈의 상품 클래스]


커피를 주문할 때는 스팀 우유나 두유, 모카, 휘핑 크림등을 얹기도 하는데 각각을 추가할 때마다 커피 가격을 올리기 때문에 주문 시스템 역시 그런 점들을 고려해야한다.




[그림 2. 엄청나게 늘어난 클래스들]

 

위는 정말 황당한 구조이다. 그냥 인스턴스 변수와 슈퍼 클래스를 상속해서 추가사항을 관리하면 어떨까?

 

 

 

Beverage 클래스에 두유, 우유, 모카, 휘핑 크림이 들어가는지 여부를 판단하는 인스턴스 변수를 추가해보자

또한 cost()역시 메서드를 오버라이드 할 때 그 기능을 확장하여 음료의 가격을 정하자

 

Colored By Color Scripter

1
2
3
4
5
6
7
8
9
10
11
float Beverage::cost()
{
    float const = 0.0f;
 
    if(hasMilk())  cost += milkCost;
    if(hasSoy())   cost += soyCost;
    
    ...
    
    return cost
 }

 

1
2
3
4
float DarkRoast::cost()
{
    return Beverage::cost() + 0.99;
}

 

 

 

이제 클래스는 다섯 개로 줄어들었지만 문제점이 아직 남아있다.

첨가물 가격이 바뀌면 기존 코드를 수정해야하고 첨가물의 종류가 많아지면 메서드도 추가해야한다.

또한 녹차같은 새로운 음료가 추가된다고 가정할 때 사용하지도 않는 hasSoy()같은 메서드를 같이 상속받는 문제가 생길 것이다.


 

만약 특정 음료 객체를 생성하고, 첨가물을 장식(Decorate)하는 방식으로 구현할 수 있다면 어떨까?

예를 들면 DarkRoast 객체를 가져오고

Mocha 객체로 장식하고

Whip 객체로 장식하고

cost() 메서드를 호출하면 가격을 계산하는 일이 첨가된 객체들에게 위임되어 처리된다면 멋질 것이다.

 

 

 

객체에 추가적인 요건을 동적으로 첨가하고 서브클래스를 만드는 것으로 기능을 유연하게 확장할 수 있는 데코레이터 패턴을 이용하면 이러한 일이 가능하다!

 

 

 

 

커피 종류마다 구성요소를 나타내는 구상 클래스를 하나씩 만들고, 각각의 첨가물을 나타내는 데코레이터 클래스들을 정의한다.

데코레이터 패턴에서는 상속을 이용해서 형식을 맞추는데 데코레이터 객체들은 자신이 감싸고 있는 객체와 같은 인터페이스를 가져야하기 때문이다.

 

C++ 코드로 나타내면 대략 다음과 같다.

 

 

Beverage는 추상 클래스이며 음료를 설명하는 메서드와 가격을 나타내는 메서드를 가지고 있다.

 

우선 각각의 음료들은 cost() 메서드만을 오버라이딩하여 가격을 정의하고 음료를 설명하는 문자열을 설정하고 있다.

 

이제 첨가물을 나타낼 추상 클래스를 보자.

자신이 감싸야 하는 객체와 같은 인터페이스를 가져야 하므로 Beverage를 상속받고있다.


 

각각의 첨가물은 감싸고자 하는 음료를 저장하기 위한 인스턴스 변수를 가지고, 생성자에서 음료 객체를 전달하고 있다.

설명 메서드와 가격을 나타내는 메서드는 각각 원래 음료의 가격에 더하여 계산되어야한다.

 

위에서 구현한 데코레이터 패턴을 이용한 테스트 코드이다.

음료 객체를 생성하고 여러 첨가물을 동적으로 붙이는 것이 가능해졌다!

보이는 바와 같이 데코레이터 패턴을 이용하면 서브 클래스를 만드는 경우에 비해 훨씬 더 유연한 기능 확장이 가능하다.

다만 데코레이터로 감싸는 과정이 복잡해 보일 수 있는데 팩토리와 빌더 패턴을 이용하면 이러한 문제를 보완할 수 있다.

 

 

 

자료에 첨부된 소스 코드는 디자인 패턴의 개념을 참고하여 C++로 구현한 것이므로 잘못된 점이 있을 수 있습니다.

문제점이 발견될 시 피드백 부탁드립니다.

 

[참고 문헌 : 에릭 프리먼, 서환수 역, Head First Design Patterns ] 

[참고 그림 URL :

 http://wiki.gurubee.net/pages/viewpage.action?pageId=1507398,

http://cfile25.uf.tistory.com/image/117C804850C595DD21B4B7​​ ]


출처 : http://blog.naver.com/jidon333


신고
Posted by 우엉 여왕님!! ghostkyow
2015.08.06 22:46

옵저버 패턴(Observer Pattern)은

한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들에게 연락이 가고 자동으로 내용이 갱신되는 일대다(one-to-many)방식의 관계이다.

 

옵저버 패턴(Observer Pattern)을 설명하기에 앞서 신문사과 구독자와의 관계를 생각해보자.

 

구독자가 신문을 보고 싶으면 신문사에 구독 신청을 한다.

그러면 구독자로 남아 있는 동안에는 매일 신문을 받아 볼 수 있다.

그리고 신문을 더 이상 보고 싶지 않으면 구독 해지 신청을 하면 된다.

신문사가 영업을 하는 동안에는 개인 독자, 호텔, 항공사, 학교 등등 여러 곳에서 꾸준히 구독 신청 및 해지를 할 것이다.

 

이러한 신문사와 구독자와의 관계는 옵저버 패턴과 매우 유사하다.

여기서 신문사를 주제(Subject), 구독자를 옵저버(Observer)라고 부른다는 것을 일단 기억하자.

 

 

 

[그림1 : 옵저버 패턴의 작동방식 , URL : http://hewiki.heroengine.com/wiki/Observer_Pattern]

 

그림과 같이 Subject에서 정보가 갱신되면 Subject에 등록된 모든 Observer에게 데이터를 전송한다.

옵저버 패턴에서 상태를 저장하고 지배하는 것은 주제객체(Subject) 이므로 상태를 갖는 객체는 하나만 있으면 된다.

또한 데이터의 주인은 사실상 Subject 객체이고 옵저버 객체는 사실상 데이터가 갱신되기를 기다리는 입장이라는 특징이 있는데 이러한 구조는 여러 객체에서 동일한 데이터에 접근하는 것 보다 좀 더 깔끔한 객체지향 디자인을 만든다.

 

이러한 디자인은 두 객체 사이의 결합도를 느슨하게 만들 수 있고 이 말은 주제와 옵저버가 서로 상호작용을 하기는 하지만 서로에 대해 잘 모른다는 것을 의미한다.

따라서 새로운 형식의 옵저버를 추가하더라도 Subject를 전혀 변경할 필요도 없고, Subject와 Observer는 서로 독립적으로 재사용이 가능하다.

 

그럼 이러한 특징을 가진 옵저버 패턴을 구현하기 위해서는 어떻게 디자인해야 할까?

옵저버로 등록될 객체들의 항목은 모두 다를 수 있다.

DOG도 옵저버가 되길 원할 수 있고, Cat도 Duck도 모두 다른 객체이지만 Observer가 되기를 희망할 수 있다.

따라서 C++에서 옵저버는 공통된 인터페이스를 갖도록 구현해야하는데 나는 Observer라는 인터페이스 클래스를 만들고 이것을 상속받도록 하여 구현하였다.

 

 

 

 

 

 

Subject는 제각각 다른 속성을 가질 수 있는 옵저버에 대해서 알 필요가 없다.

오직 옵저버들이 공통된 인터페이스를 갖는다는 사실만 알면 충분하다.

그렇기 때문에 Subject 클래스는 공통된 인터페이스를 사용하는 부분과 새로운 옵저버를 등록, 해제하고 정보를 갱신하는 인터페이스만을 가지면 된다.


아래의 코드에서는 registerObserver(), removeObserver() 메서드를 통해 옵저버(구독자)를 등록, 해제하고 notifyObserver()를 통해 갱신된 정보를 전달한다.



 


 

 

코드를 보면 알겠지만 옵저버 패턴은 마치 신문사와 구독자의 비유처럼 등록, 해지할 수 있도록 짜여진 구조라는 것을 알 수 있다.

옵저버 패턴의 구조를 좀 더 쉽게 이해하기 위한 Dog, Cat, Duck의 이야기를 보며 포스팅을 마치겠다.

 

 

[출력 결과]



 

 

[출력 결과]

 

 

참고로 옵저버 패턴에는 위와 같이 Subject가 Observer에게 데이터를 전송하는 방식과 

각각의 Observer가 Subject의 포인터를 갖고 Observer가 데이터를 가져오는 방식​ 두 가지가 있다.

후자의 방식으로 사용하면 옵저버가 원할 때 좀더 유연하게 정보를 처리할 수 있다는 장점이 있지만 상대적으로 전자의 방식보다 결합도는 높아지게 된다.

 

 

 

 

 

자료에 첨부된 소스 코드는 디자인 패턴의 개념을 참고하여 C++로 구현한 것이므로 잘못된 점이 있을 수 있습니다.

문제점이 발견될 시 피드백 부탁드립니다.

 

[참고 문헌 : 에릭 프리먼, 서환수 역, Head First Design Patterns ]


출처 : http://blog.naver.com/jidon333


신고
Posted by 우엉 여왕님!! ghostkyow
2015.08.06 22:45

스트레티지 패턴(Strategy Pattern)이란 알고리즘군을 정의하고 각각을 캡슐화하여 교환해서 사용할 수 있도록 만든다.

스트래티지를 활용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.

라고 Head Firest Design Pattern 책에서 정의한다.

 

알고리즘군을 정의한다는 말이 조금 어려운데 그냥 행동들을 정의하는 클래스를 만든다고 생각하면 된다.

일단 아래의 자료들을 보며 스트레티지 패턴(Strategy Pattern)이 어떨때 사용되는 것인지 생각해보자.

 

개들이 짖고 뛰어다니고 수영하고 하는 시뮬레이션 게임이 있다고 생각해보자.

개를 정의하는 클래스는 간략하게 아래와같이 디자인 할 수 있을 것이다. 

그런데 이 개들에게 날아다니는 기능이 추가되었다고 생각해보자. 어떻게 기능을 추가할 수 있을까?


 

가장 쉽게 생각할 수 있는 방법이 Dog 클래스에 Fly()메서드를 추가하는 것이다.

이렇게 하면 Dog를 상속받은 모든 클래스가 나는 기능을 가질 것이다.

하지만 모든 개가 날아다니는 것이 아니라 어떤 개는 날고 어떤 개는 못난다면 어떨까?

또는 어떤 개는 날개를 달아 날지만 어떤 개는 프로펠러나 로켓을 통해 날아다닌다면??



물론 오버라이딩을 통해 해결할 수 있고 이러한 방법도 나쁘다고 할 수 없다.

 

하지만 프로젝트의 규모가 점점 커지면서 많은 기능이 추가된다면 어떨까?

우선적으로 Dog클래스의 코드가 점점 많아질 것이고 추가된 기능을 사용하지 않는 자식들도 있을 것이다.


 

 

여기서 뭔가 잘못되었다는 것을 깨달았으니 리펙토링을 시도해보자.

 

Dog의 속성을 다르게 하는 요소들을 분리하여 캡슐화해야 한다는 것은 떠오르는데 막상 클래스를 디자인하려니 이것 저것 거슬리는 것이 한 두가지가 아니다.

여러 기능을 갖기 위해 다중 상속을 하는 것도 생각해보았지만 코드 중복 문제도 심각하고 절대 빠져나올 수 없는 구렁텅이로 빠지는 느낌이다.

 

일단 어떻게든 캡슐화를 해야한다. 어떻게 해야 할까?

 

방법은 바로 행동(알고리즘군)에 따라 분리하는 것이다.

클래스를 일반적으로 어떤 대상을 나타내기 위해서만 디자인해왔다면 이것이 굉장히 어색하게 느껴질 수 있지만 그렇게 못할 것도 없다.

 

행동에 따라 분리하고 인터페이스를 중점으로 클래스를 디자인해보자.

 

분리된 클래스의 디자인은 다음과 같이 나타낼 수 있다.

 

상속만을 생각할 때는 막막했지만 Has A 관계로 눈을 돌리면 코드는 매우 유연해진다.

 

위와 같은 디자인의 큰 장점 중 하나는 setter를 이용하여 실행 타임에 행동을 변경하는 것이 가능하다는 것이다.

처음에 디자인한 것처럼 Super 클래스에 매서드를 몰아 넣고 모든 경우를 오버라이딩할 때에는 이런 유연함을 가질 수 없었다.

 

지금까지 설명한 내용을 기반으로 작성한 C++ 소스 코드이다.

Duck 클래스는 이제 각종 행동을 가상 함수의 형태로 갖는 대신 분리된 인터페이스를 Has하는 형태로 바뀌었다.

 

인터페이스는 다음과 같이 분리하였기 때문에 구현부가 아무리 바뀌고 확장된다고 하더라도 Duck 클래스에는 영향을 미치지 않는다.

 

또한 메인문에서 보는 바와 같이 실행시간에 행동의 속성을 바꾸는 것도 가능하다..




 

 

 

자료에 첨부된 소스 코드는 디자인 패턴의 개념을 참고하여 C++로 구현한 것이므로 잘못된 점이 있을 수 있습니다.

문제점이 발견될 시 피드백 부탁드립니다.

 

[참고 문헌 : 에릭 프리먼, 서환수 역, Head First Design Patterns ]


출처 : http://blog.naver.com/jidon333

신고
Posted by 우엉 여왕님!! ghostkyow
2015.08.06 22:40


 

[그림 : 랜더링 파이프라인]


랜더링 파이프라인은 크게 정점 처리, 래스터화, 프레그먼트 처리, 출력 병합의 단계로 구분할 수 있다.


(vertex processing) : 정점처리는 모든 정점에 대해 수행되며 변환을 비롯한 다양한 정점별 연산을 처리한다.

(rasterization) : 래스터화는 이제 변환된 정점으로 정의되는 폴리곤 내부영역을 채우는 프래그먼트를 생성하는 단계를 말한다.

(fragment processing) : 프래그먼트 처리는 각각의 프래그먼트에 대하여 텍스처링같은 작업을 거쳐 색상을 결정하는 단계이고

(output merger) : 마지막으로 출력병합단계는 프래그먼트와 현재 컬러버퍼의 색상을 병합하거나 하나를 선택하여 컬러버퍼를 수정하는 과정을 말한다.

 


월드변환 : 

게임세상에서 각각의 3차원 매쉬들은 모두 각각의 오브젝트 공간에서 정의되어있으므로 이들을 하나의 공통된 공간으로 사상시킬 필요가 있다.

이 공통된 하나의 공간을 월드 공간이라 부르며 이 공간으로의 변환을 월드변환이라 부른다.


뷰 변환 : 

월드변환을 통해 모든 오브젝트를 하나의 공간으로 사상시킨 이후에는 관찰자(카메라)의 위치에서 월드공간을 관찰함으로써 랜더링과정을 쉽게 만들 필요가 있는데 이런 역할을 하는 공간을 카메라 공간 혹은 뷰 공간이라 부른다.


절두체(View Frustum)

: 시야각( Field Of View)과 종횡비(aspect)에 의해 정의되는 하나의 볼륨을 근평면과 원평면으로 잘라낸 공간.


클리핑(clipping) 

: 하나의 폴리곤이 절두체와 교차할 경우 폴리곤의 경계를 잘라 내는 것.

( 하지만 클리핑은 카메라공간에서 절두체를 이용해 수행되지 않는다. 카메라 공간의 모든 물체는 "투영변환"을 통해 clip-space로 변환되는데 이곳에서 클리핑이 수행된다.


투영변환(projection transform)

: 절두체를 축에 나란한 직육면체 볼륨으로 변경하여 카메라공간의 모든 3차원 물체를 3차원 클립공간으로 변환시키는 것.

(이름과 다르게 2차원으로 투영하는 것이 아니라는 것을 주의하자. 실제 투영은 래스터화 단계에서 *원근투영나눗셈이 일어날 때 이뤄진다.)



래스터화 단계

: 보통 하드웨어로 고정된 단계로 프로그래밍이 불가능하다.

클리핑, 원근 나눗셈, 뒷면제거, 뷰포트변환, 스캔변환 등의 요소로 구성



*원근나눗셈

: 투영변환이후 w에 저장된 값으로 좌표를 나누는 연산.

 원근법의 구현에 해당하며 (x,y,z,w)의 동차좌표계에서 (x,y,z)의 카테시안 좌표로 변하게되는데 이를 NDC(normalized device coordinates)라 부름.


뒷면제거

: 카메라를 바라보지 않는 폴리곤을 제거하는 것


스캔변환

: 삼각형 내부를 채우는 프래그먼트를 생성한다. 즉 삼각형이 차지하는 스크린 공간의 픽셀 위치를 정하고, 삼각형의 정점별 속성을 보간하여 이를 각 픽셀 위치에 할당한다.


출처 : http://blog.naver.com/jidon333


신고
Posted by 우엉 여왕님!! ghostkyow
2015.08.06 22:39

지금까지 골격을 애니메이션하는 방법을 살펴보았다.

이제 골격을 둘러싸는 스킨 단일 매쉬를 애니메이션하는 방법에 대해 논의할 차례이다.

이는 정점 블랜딩 (Vertex Blending)으로 불린다.

정점 블랜딩을 설명하기에 앞서 스킨 매쉬는 각각의 뼈대마다 존재하는 것이 아닌 bind-space상에 존재하는 단일 매쉬라는 것을 기억하자.


스키닝 애니메이션에서 정점의 최종 위치는 정점에 영향을 주는 뼈대들의 가중치 평균에 따라 결정된다.

( 해당 가중치는 대부분 모델링 과정에서 아티스트에 의해 결정된다.)


지구상에 존재하는 대부분의 실제 생물학적 구조에서 하나의 정점에 영향을 주는 뼈대는 최대 4개이다.

따라서 스키닝 애니메이션에서는 관습적으로 최대 influence bone의 개수를 4로 지정하는 것이 관례이다.


정점의 최종 위치가 영향을 주는 뼈들의 가중치 평균에 의해 결정된다고 했으므로 각각의 정점은 자신에게 영향을 주는 뼈대의 인덱스들(최대 4개)를 가진다.

이 인덱스는 골격의 최종 변환 행렬의 한 원소를 가리키며 이를 bone matrix palette라고 흔히 부른다.

또한 각 정점에는 각각의 영향을 주는 뼈대가 그 정점에 얼마나 영향을 미치는지를 결정하는 가중치 4개도 갖는다.


이런 형식의 정점들로 이루어져있는, 뼈대들과 가중치가 적절히 배정되어있는 매쉬를 스키닝된 매쉬라고 부른다.


임의의 정점 v에 대해서 정점 블랜딩을 통해 얻는 정점의 root-space기준 위치는 다음과 같은 가중치 평균 공식으로 얻는다.

V ' = W0vF0 + W1vF1 + W2vF2 + W3vF3

여기서 가중치 w0 + w1 + w2 + w3 = 1이다. 즉 가중치의 합은 1이어야한다.


공식을 말로 설명하자면 정점 V에 영향을 주는 모든 뼈대의 최종 변환을 정점에 적용하고, 그 결과들의 가중치 평균을 구해서 최종 위치 V'을 구하는 것이다.

이와 같은 방법으로 법선 또한 구한다.


다음은 위의 과정을 수행하는 정점 셰이더 코드의 예이다.



 








[참고문헌 : 프랭크 D. 루나 저 류광 역, DirectX 11을 이용한 3D 게임 프로그래밍 입문 ,

원제 : Introduction to 3d game programming with Directx 11 ]


[참고 URL : http://www.gamedev.net/page/resources/_/technical/graphics-programming-and-theory/skinned-mesh-animation-using-matrices-r3577

]

출처 : http://blog.naver.com/jidon333

신고
Posted by 우엉 여왕님!! ghostkyow
2015.08.06 22:39

애니메이션과 애니메이션 자료구조


​애니메이션이라는 것은 한 순간에서의 물체의 position, rotation, scale을 지정하는 키 프레임들의 시간순의 목록으로 정의된다.

게임 어플리케이션에서 매 프레임 시간은 원하는대로 정확하게 끊어지지 않으므로 키 프레임과 키 프레임 사이에 보간이 필요하다.



키 프레임의 구조는 위 코드와 비슷하게 구성할 수 있을 것이다.

( 여기서 rotation정보를 표현하는데 기하학적으로 4차원 벡터의 형태를 갖는 쿼터니온을 사용하고 있는 것을 볼 수 있는데 이것은 구면선형보간이 가능하다.)


​골격을 애니메이션 하는 것은 그냥 연결된 여러 개의 뼈대를 변환하는 것에 불과하다.

즉 골격을 애니메이션 하기 위해서는 각 bone에 대해 local 애니메이션을 마친 뒤, 그 bone의 조상을 고려하여 Root-space로 변환하면 된다. 

또한 캐릭터는 아마 다양한 동작을 할 수 있을 텐데 ( 걷기, 뛰기, 사격 등등) 이러한 하나의 동작을 정의하는 bone들의 애니메이션의 집합을 "Animation Clip" or "Animation Set"이라 부른다.


 


일반적으로 하나의 캐릭터는 다양한 동작을 하기 때문에 이러한 동작 하나를 정의하는 애니메이션 클립이 여러개 필요하다.

그런데 모든 애니메이션은 같은 골격에 대해 작동하므로 사용하는 뼈대 개수가 모두 동일하다( 일부 뼈대가 움직이지 않는 경우도 있겠지만...)

마지막으로 앞에서 언급한 것처럼 각 뼈대마다 정점들을 bind-space에서 bone-space로 변환하는 offset matrix가 필요하다.

또한 골격계층구조를 표현하기 위한 방법도 필요하다. ( 아래에 나올 예제 코드에서는 배열을 사용하여 표현한다.)

다음은 이러한 것들을 종합하여 만든 골격 애니메이션 자료 구조이다.


 



최종 변환의 계산


​캐릭터의 계층 구조는 일반적으로 트리로 구성된 이런식의 계층구조를 갖는다.

 


앞으로 기술할 Frank.D.Luna의 예제 코드는

이러한 계층구조를 하나의 정수 배열로 표현하며, 배열의 i번쨰 원소가 i번째 뼈대의 부모의 인덱스가 되게 한다.

비슷하게 뼈대의 인덱스 i는 해당 애니메이션 클립에서 그 뼈대의 BoneAnimation의 인덱스와 일치하며,

offset 변환 배열에서 그 뼈대의 offset 변환의 인덱스와도 일치한다.

Root 뼈대는 항상 0번이며, 부모는 없다.


다음은 이러한 관례에 기초한 각 뼈대의 최종 변환을 구하는 코드이다.

 


[참고문헌 : 프랭크 D. 루나 저 류광 역, DirectX 11을 이용한 3D 게임 프로그래밍 입문 ,

원제 : Introduction to 3d game programming with Directx 11 ]


[참고URL : http://www.gamedev.net/page/resources/_/technical/graphics-programming-and-theory/skinned-mesh-animation-using-matrices-r3577

]

출처 : http://blog.naver.com/jidon333

신고
Posted by 우엉 여왕님!! ghostkyow
2015.08.06 22:38

스키닝 애니메이션


​스키닝 애니메이션은 스킨(skin)을 을 이루는 단일 매쉬와 뼈대(bone, joint)로 이루어진 구조를 갖고있다.

bone은 캐릭터 애니메이션을 위한 계층구조를 제공하는데 이 bone이 움직일 때 스킨 매쉬가 따라 움직이며 신축성있는 움직임을 보여준다.


애니메이션 초기에 이 스킨 매쉬는 bind-space를 기준으로 한다.

bind-space는 이 스킨 매쉬가 정의된 로컬 좌표계로써 보통은 계충구조에서의 root-space를 사용한다.

bone이 움직일 떄 스킨 매쉬의 정점들이 부여된 가중치에 따라 움직이며 캐릭터의 애니메이션을 표현하는 방법을 스키닝 애니메이션이라 한다.


To-Root 행렬


이 스키닝 애니메이션에서 중요한 특징들이 있는데 우선 To-Root 행렬이 있다.

일반적으로 단일 매쉬로 이루어진 스킨 매쉬는 위에서 언급한 것처럼 root-space에 저장된다.

따라서 각 프레임을 world-space가 아닌 root-frame-space로 변환하기 위한 행렬이 필요한데 이를 To-root 행렬이라 부른다.


하향식 운행


​두 번째로는 계층구조의 bone들을 운행하는 방식에 있다. 

이전의 계층구조를 설명할 때에는 하나의 bone에서 시작하여 그 부모로, 계속해서 조상을 거슬러올라가 root까지 도달했었다.

하지만 실제로는 위에서 아래로 내려오며 계산하는 방식이 더 효율적이다.


골격을 구성하는 n개의 bone에 정수 번호 0,1,2, ... n-1을 부여할 때 i번 뼈대의 root 변환 공식은 다음과 같다.

toRoot(i) = toParent(i) * toRoot(p) // p는 parent의 번호

 

여기서 toRoot(p)는 bone(p)의 좌표계에서 root의 좌표계로 변경한다.

트리를 하향식으로 위에서 아래로 내려오며 계산하면 목표 노드 i번에 도달할 때 이미 toRoot(p)는 계산된 상태가 될 수 있다.

만약에 상향식으로 계산한다고 가정하면 각 bone마다 모든 조상을 훑어야 하고, 또한 같은 부모를 공유하는 bone에 대해서도 동일한 연산이 중복수행되는 문제가 생긴다.



 

[그림 URL : http://hybridego.net/entry/skindparsingmesh-xfile ]

​offset 변환 


​스키닝에서 움직이고 싶은 bone의 좌표계와 정점을 가지고 있는 스킨 매쉬의 좌표계는 동일하지 않다.

따라서 원하는 뼈대를 Root-space로 변환하기 이전에 스킨 매쉬를 bind-space(대부분의 경우 root-space를 사용)에서 변환하고자하는 bone-space로 변환하는 것이 선행되어야한다.

이러한 변환을 offset 변환이라 부르고 이 변환을 수행하는 행렬을 offset matrix라 부른다.


이 offset 행렬의 정의부터 말하자면 Reference-pose( 또는 T-pose, rest-pose로 불린다)에서의 To-Root행렬의 역행렬이 되겠다.

조금 복잡해보일 수 있으나 차분하게 생각해보면 당연한 말인 것을 알 수 있다.


우리에게 우선적으로 필요한 것은 Reference-pose에서의 움직이고자하는 bone의 좌표계와 스킨 매쉬의 좌표계를 일치시키는 것이다.

스킨 매쉬가 존재하는 bind-space는 대부분  root-space이라고 했던 것을 상기해보자.

우리는 특정한 bone에서의 To-Root의 행렬은 조상을 거슬러 올라가며 To-parent를 곱하는 것으로 구할 수 있는데

지금 알고 싶은 것은 Root-Space인 스킨 매쉬의 좌표계에서 특정한 bone-space로의 변환 행렬을 구하고자 하는 것이다.

bone이 가지고 있는 To-Root 행렬의 역의 의미를 생각해보면 Root-space에서 bone로 변환하는 행렬의 의미가 된다.

이것은 offset 행렬이 추구하는 바와 같은 것이고 따라서 To-Root행렬의 역행렬이 Offset 행렬이 될 수 있는 것이다.


final 변환 

 

​이제 특정 뼈대를 Root로 변환시키기 위한 행렬(To-Root Matrix)도 알고 있고, 스킨 매쉬의 정점을 원하는 bone-space로 변환하는 행렬(Offset Matrix)도 알고 있다. 

남은 일은 움직이고자 하는 bone의 좌표계와 스킨 매쉬의 좌표계를 일치시킨 뒤 그것에 애니메이션 변환 행렬을 곱해 Root-space로 변환하는 것 뿐이다.


수학적으로 i번 뼈대의 최종 변환 행렬은 다음과 같이 표기할 수 있다.

F(i) = offset(i) * toRoot(i);













[참고문헌 : 프랭크 D. 루나 저 류광 역, DirectX 11을 이용한 3D 게임 프로그래밍 입문 ,

원제 : Introduction to 3d game programming with Directx 11 ]


[참고 URL : http://www.gamedev.net/page/resources/_/technical/graphics-programming-and-theory/skinned-mesh-animation-using-matrices-r3577

]

출처 : http://blog.naver.com/jidon333

신고
Posted by 우엉 여왕님!! ghostkyow
2015.08.06 22:38

프레임 계층 구조에서 사용되는 수학적 내용


 


세상에는 ​여러 부분이 부모-자식 관계로 연결되어 있는 것들이 많다.

사람의 관절을 예를 들 수 있는데 사람의 관절은 부모가 움직일 때 자식이 함께 움직여야한다.

사람의 손은 손, 아래팔, 위팔과 같이 세 부품으로 나눌 수 있다.

손은 손목 관절을 중심으로 독자적으로 회전하지만 팔뚝이 팔꿈치 관절에 대해 회전하면 손도 반드시 그에 따라 움직여야한다.

마찬가지로 위팔이 어깨 관절에 대해 회전하면 팔뚝도 그에 따라 회전한다.


앞으로 설명을 위해 위팔 - 팔뚝 - 손 계층구조를 계속해서 언급할 것이고, 이들을 각각 뼈대0, 뼈대1, 뼈대2로 칭할 것이며 root는 뼈대0을 의미한다.

우리는 캐릭터 애니메이션의 움직임이 world-space에서 표현되기를 원하므로 생각해야할 문제는 바로 이것이다.

" 계층구조의 한 부품을 움직일 때, 이것을 어떻게 world-space로 변환해야하는가? "

​여기서 한 부품만을 변환해서는 안된다.

계층구조의 위치와 방향은 조상들로부터 영향을 받으므로 조상들의 변환도 고려해야한다.


계층구조에서 한 부품은 local-space를 가지며 pivot joint(관절)을 가져야한다.

물체를 편하게 회전할 수 있도록 pivot은 local-space의 원점에 위치하도록 하자.


계층 구조에서 각각의 부품의 local-spcae간의 관계를 정의하는 것이 가능하다.

특히 임의의 순간(한 순간 찰나의 시간)에서의 local과 local간의 관계를 정의하는 것이 유용한데 애니메이션이라는 것이 시간에 따라 움직이기 때문이다.


각 local-space를 parent-space를 기준으로 하여 서술할 수 있다.

이 때 계층구조 최상위에 있는 root의 부모 좌표계는 world-space이다.

이것을 행렬로 서술하면 자식의 공간에서 부모의 공간으로 변환하는 행렬이라 할 수 있고 이를 To-parent행렬이라 부를 수 있다.


손-팔뚝-위팔의 예제를 통해 설명하면 손은 팔뚝 공간으로의 행렬, 팔뚝은 위팔 공간으로의 행렬, 위팔은 월드 공간으로의 행렬을 갖는다.

각각의 행렬을 A0, A1, A2 라고 할 때

손을 world-space로 변환하는 행렬은 M = A0*A1*A2로 정의할 수 있다.

결과적으로 계층구조의 한 물체를 변환하기위해서는 그 물체가 world-spcae에 도달할 때까지 계속해서 To-parent 변환을 진행하면 된다.


이러한 발상은 일반적인 Tree 구조에서도 적용된다.




[참고문헌 : 프랭크 D. 루나 저 류광 역, DirectX 11을 이용한 3D 게임 프로그래밍 입문 ,

원제 : Introduction to 3d game programming with Directx 11 ]


[참고 URL : http://www.gamedev.net/page/resources/_/technical/graphics-programming-and-theory/skinned-mesh-animation-using-matrices-r3577

]


출처 : http://blog.naver.com/jidon333

신고
Posted by 우엉 여왕님!! ghostkyow
2015.08.06 22:37

원문 출처 : http://www.gamedev.net/page/resources/_/technical/graphics-programming-and-theory/skinned-mesh-animation-using-matrices-r3577


번역 및 개인 정리 (오역의 여지가 있으니 혹시나 이 글을 참고할 분들은 원문을 같이 살펴보는 것을 권장합니다 ㅜㅜ)


이 기사는 스키닝 매쉬 애니메이션의 방법을 설명한다.


게임은 애니메이션을 갖는 캐릭터를 자주 사용한다. 이를테면 걷기, 달리기, 사격등등...

이러한 애니메이션을 갖는 캐릭터에 자주 쓰이는 것이 스키닝 매쉬 애니메이션 기법(Skinned Character Animation)이다.

이런 스키닝 메쉬와 애니메이션 동작들은 몇몇 모델링 프로그램으로 만들어진다 ( 예를 들면 Blender)

이러한 모델들은 다양한 API를 통해 적절한 포멧으로 임포트/익스포트된다.

지금부터 이 기사는 데이터가 "임포트된 이후"의 애니메이션 과정에 대해 살펴보려한다.

모델링 과정, rigging, 애니메이션에 대해 언급할 것이고 임포트나 익스포팅 과정은 제외되니 알아두길 바란다.


Notes

------------------------

기사내 등장하는 SRT ​라는 용어는 변환과정의 Scale, Rotation, Translation 연산을 말한다.

대부분의 어플리케이션에서는 원하는 결과를 위해서는 SRT의 순서로 연산을 수행한다.


많은 API에서의 벡터-행렬 곱셈과 행렬-행렬 곱셈은 아래의 방법을 따르고 대표적으로 DIrectX가 다음과 같은 방법을 사용한다.

[ 역자 : 행행렬 또는 열행렬은 벡터를 표현하기 위해서 종종 사용되는데 어떤 것을 사용하는 것이 표준인지는 따로 정해진 것이 없다.

 다만 대부분의 수학책과 openGL에서는 주로 열벡터를 사용하고 DirectX 에서는 행 벡터를 사용한다. ]


// order of multiplication in DirectX

FinalVector = vector * ScaleMat * RotationMat * TranslationMat



OPENGL에서는 이런 곱셈 연산의 순서가 반대이다.

openGL 어플리케이션에서는 보통 "열 벡터" 행렬을 사용하기 때문이다.


// order of multiplication in OpenGL FinalVector = TranslationMat * RotationMat * ScaleMat * vector



* 수학적으로 위, 아래 식의 결과는 같다. openGL과 DirectX 모두 Scale, Rotation, Translation 순서로 연산된다.


스키닝 애니메이션은 이러한 SRT-변환이 행렬의 형태로 표현되는 것을 전제로 이루어진다.

행렬 곱의 특성에 의해 SRT는 하나의 행렬로 표현 가능하며 이것은 각각 SRT를 계산한 결과와 같다.

final-matrix = SRTmatrix1(rot1 followed by trans1) * SRTmatrix2( rot2 followed by trans2).



The Components of an Animated Skinned Mesh


"Animated"는 움직이거나 혹은 움직이는 것처럼 보이는 것을 뜻한다.
"Skinned"는 프레임 또는 뼈 계층구조의 매쉬를 뜻하는데 이 매쉬의 정점들은 다양한 위치에 그려질 수 있어 뛰거나 울렁이는 것처럼 나타낼 수 있다.

"Mesh"는 점들의 집합이며 그리기를 위한 면(삼각형, 사각형)을 생성하고 이는 그 위치에 제한받지 않는다(SRTs).


What's a "bone?"


"Frame"(프레임)이라는 단어가 사용되는데 이는 "Frame of Reference"(참조 프레임) 또는 월드에서의 좌표축의 방향을 의미한다.
[역자 : 참조 프레임이라는 것은 물리학/수학에서 방향을 가진 점을 표현하기 위하여 사용하는 좌표계이다. 이것은 상대적인 개념을 표현하기 위해 사용될 수 있다. ]
"bone"(본) 이라는 단어는 "frame"대신에 자주 사용되는데 왜냐하면 bone 주변의 스킨이 어떻게 움직이는지에 대한 개념으로 사용되기 때문이다.
만약 팔을 들어올리면 팔의 뼈는 주변의 피부가 위로 늘어나도록 영향을 줄 것이다.

여기서 "bone"이라는 단어는 뼈의 길이조차도 암시하지만 이 기사의 애니메이션에서 설명하는 "bone frame"은 뼈의 길이의 의미와는 관련이 없다.
뼈의 길이라는 것이 뼈에서 자식 뼈 사이의 거리( 예를들면 손뼈와 팔뼈는 13인치정도 떨어져있는 것처럼)에 대한 생각일지라도 이 스키닝 매쉬에서의 뼈는 하나 이상의 자식뼈를 갖고 자식 뼈는 부모로부터 같은 거리가 떨어져있어야 할 이유가 없다.
예를들면 인간의 캐릭터가 하나의 목 뼈를 갖고, 왼쪽 오른쪽 어께가 등 뼈의 자식이 되는 것은 이상하게 여겨지지 않는다.
자식 뼈는 관절 뼈로부터 같은 거리를 갖는 것이 필요하지 않다.
[ 위 문단이 잘 이해가 되지 않아 원문을 같이 올림]
 However, the term "bone" implies a length associated with it. Bone frames used in the type of animation described in this article do not have an associated length. Though the "length" of a bone might be thought of as the distance between a bone and one of its child bones (lower arm bone with a hand bone 13 inches away), a bone as used for skinning meshes may have more than one child bone, and those child bones need not be at the same distance from the parent. For instance, it is not unusual for a human character to have a neck bone, and right and left shoulder bones, all being children of a spine bone. Those child bones need not be at the same distance to the spine bone.

 

 


일반적인 프레임 계층구조에서 루트 프레임을 제외한 모든 프레임은 부모 프레임을 갖는다.

이것은 구조 전체가 단일 행렬만으로 비례확대, 회전, 평행이동을 모두 가능하게한다.

아래의 계층구조를 보자.

프레임이름의 바로 아래에서 프레임의 자식-부모 관계를 가리킨다.

eg) 엉덩이 프레임은 루트 프레임의 자식이고, 왼쪽 넓적다리는 엉덩이의 자식이다. 좀 더 살펴보면 등뼈는 엉덩이와 같은 레벨의 프레임인걸 알 수 있다.

이것은 엉덩이와 등뼈가 서로 형제관계이며 루트의 자식이라는 것을 보여준다.

만약 루트프레임에 SRT-변환이 적용되면 SRT-변환은 자식에서 자식으로 트리 전체에 전파될 것이다.

eg) 만약 루트가 회전하면 엉덩이는 루트에 의해 회전되고 왼쪽 넓적다리는 엉덩이에 의해 회전된다.

비슷하게 만약 루트에 평행이동변환이 적용되면 전체에 평행이동 변환이 적용될 것이다.


스키닝 애니메이션에서 SRT-행렬은 계층구조의 어떤 프레임이나 적용 가능한데 이것은 자식에서 그들의 자식들을 통해 오직 아래로만 전파된다.

부모에게는 아무런 영향을 주지 않는데 만약 Left Upper Arm을 위쪽으로 회전시키면 Left Lower Arm과 Left Hand만이 같이 위로 회전하고

Right Clavicle이나 Spine, root에는 영향을 주지 않기 때문에 우리가 원하는 대로의 움직임이 보여질 것이다.



Root frame 

Hip

Left Thigh

Left Shin

Left Foot

Right Thigh

Right Shin

Right Foot

Spine

Neck

Head

Left Clavicle

Left Upper Arm

Left Lower Arm

Left Hand

Right Clavicle

Right Upper Arm

Right Lower Arm

Right Hand


이러한 계층 구조는 크레인이나 의자, 탱크와 같은 물체에서도 볼 수 있지만 스키닝 애니메이션의 진정한 장점은 이런 물체들이 아닌 피부를 가진 사람이나 동물의 매쉬에서

적절한 가중치를 가진 뼈를 움직일 때 피부가 자연스럽게 늘어나며 마치 비선형적인 움직임처럼 보이게 하는 것이다.

예를들면 팔을 들 때 가슴과 어께 사이의 피부가 쭉 늘어나는 것.


이 기사에서 논의하는 프레임(뼈)는 행렬들과 스키닝 매쉬 애니메이션으로 구현된다.

각각의 프레임들은 애니메이션을 수행하는 동안 싱글 랜더 사이클에 관련된 몇몇 행렬들을 갖는다.

각각의 뼈들은 예를들면 bone의 To-root SRT행렬과 관련있는 행렬인 "offset matrix"를 갖는다. 

각각의 뼈들은 "key matrix"를 갖고 : 요건 애니메이션 동안에 각각의 뼈의 To-Parent 변환 행렬임

또 "animation matrix"와 "final matrix"를 갖는다.


지금 위에서 설명한 것이 꽤나 복잡해보이지만 하나씩 차근차근 접근하면 이해할 수 있다.



The Frame Structure


계속해서 진행하기 전에 "Frame"이 어떻게 실제 코드상에서 표현되는지 예제를 살펴보자.
슈도코드는 C,C++ 처럼 보이지만 이 글을 읽는 프로그래머라면 자신에게 맞는 언어로 표현할 수 있으리라 생각한다.
struct Frame { string Name; // the frame or "bone" name Matrix TransformationMatrix; // to be used for local animation matrix MeshContainer MeshData; // perhaps only one or two frames will have mesh data FrameArray Children; // pointers or references to each child frame of this frame Matrix ToParent; // the local transform from bone-space to bone's parent-space Matrix ToRoot; // from bone-space to root-frame space };

보통 각각의 프레임은 이름을 갖는다. (위의 계층구조 예에서는 root, hip, spine, etc ... ) 이 이름은 계층 구조상의 이름과 같을 것이다.

다른 프레임의 구조체 멤버의 용도에 대해서는 좀 더 나중에 자세하게 설명할 예정이다.


Animation Data and How It's Used


아래에 보여지는 데이터는 스키닝 애니메이션 메쉬에 필요하다.
물론 이것이 프레임 계층 구조에서 사용되는 모든 데이터는 아니다.

일반적으로 프레임 계층구조는 

 - 뼈의 구조에 대한 설명,

 - 매쉬 구조체,

 - 그리고 매쉬의 정점들과 뼈들간의 관계를 나타내는 데이터를 갖는다.

 이러한 모든 데이터가 스키닝 매쉬에서의 "rest pose"를 나타낸다.

[ 역자 : "rest pose"는 "T pose" 또는 "reference pose"등으로도 불려지는데 그냥 "기본포즈"라고 부르겠다. ]


애니메이션 데이터는 흔히 별도로 저장되고 접근된다.

이것은 매쉬의 단일 액션 표현을 위해서인데 예를들면 "걷기", "뛰기" 같은거...

이러한 동작은 아마도 하나 이상 설정될테지만 하나의 계층구조 위에서 일어난다는 점을 기억하자.


스키닝 애니메이션에 필요한 전체 데이터는 다음과 같이 구성된다.


 * 기본 포즈에서의 매쉬 (아마 프레임이 포함될 것임)

 * 프레임 계층구조. 계층구조는 다음으로 구성됨

 - 루트 프레임

 - (모든 프레임마다) 자식 프레임을 가리킬 리스트나 포인터 배열

 - (모든 프레임마다) 프레임이 매쉬를 갖는다면 그 매쉬에 대한 포인터

 - (모든 프레임마다) SRT-변환을 위한 하나 혹은 그 이상의 행렬들

 * 뼈 데이터에 영향을 주는 다양한 배열들

- 각각 뼈 데이터에 영향을 미치는 오프셋 행렬

- 각각 뼈 데이터에 영향을 미치는 정점의 인덱스들과 가중치들

* 애니매이션들의 배열, 각각의 애니메이션은 다음으로 구성됨

- 각각 애니메이션이 적용되는 뼈에대한 포인터

- "Key"들의 배열, 각각의 key는 다음으로 구성됨 (시간+프레임)

- 애니메이션의 시간을 가리킬 tick 카운트

- 각 tick 카운트에 적용될 행렬( 또는 각각의 SRT-변환들)


 * ticks-per-second (초당 몇 tick이 진행되어야 하는지에 대한 데이터)


Note : 모든 데이타 파일에서 위의 정보를 항상 갖는 것은 아니다. 예를 들어 어떤 데이터 포멧에서는 오프셋 행렬을 갖지 않을 수도 있는데 이렇게되면 직접 만들어야한다.

오프셋 행렬에 대해서는 아래에서 설명하겠다.


The Mesh


매쉬 데이터는 점들의 배열, 루트 프레임에 대한 상대적인 위치, 그리고 노말벡터나 텍스처 UV, 기타 등등으로 구성된다.

이 때 정점의 위치는 보통 기본 자세에서의 위치를 뜻한다 ( 애니메이션이 진행되는 동안의 위치가 아님!)

만약 매쉬 데이터가 루트 프레임보다 자식 프레임이면 스키닝 과정에서 반드시 그것에 대한 설명이 필요한데 이는 아래에 설명될 오프셋 행렬에서 논의될 것이다.


애니메이션이 적용된 매쉬는 셰이더나 이펙트 안에서 가장 많이 랜더링된다.

이러한 셰이더는 랜더링하는 동안 정점 입력으로 특별한 순서를 기대하는데데이터를 임포트하여 호환되는 포멧으로 바꾸는 과정은 이 기사에서 다루지 않는다.


Initializing The Data


스키닝 매쉬 애니메이션을 위해 필요한 데이터, 애니메이션을 가능하게 하는 과정의 개요가 여기 있다.

요구되는 많은 데이터들은 외부 파일에서 로드하게 된다.

그 후에 프레임 계층 구조를 생성하고, 프레임 구조체를 참조하여 최소 Name, Mesh Data, children, To-parent matrix 을 채워야한다.


각각의 프레임을 위한 To-root 행렬은 다음과 같이 초기화한다.

// given this function ...
function CalcToRootMatrix( Frame frame, Matrix parentMatrix )
{
    // transform from frame-space to root-frame-space through the parent's ToRoot matrix
    frame.ToRoot = frame.ToParent * parentMatrix;

    for each Child in frame: CalcToRootMatrix( Child, frame.ToRoot );
}

// ... calculate all the Frame ToRoot matrices 

CalcToRootMatrix( RootFrame, IdentityMatrix ); // the root frame has no parent 


위 함수와 같은 재귀함수는 처음에 이해하는 것이 조금 어려울 테지만 다음의 과정을 보면 무슨 일이 일어나는지 좀 더 이해가 될 것이다.


frame.ToRoot = frame.ToParent * frame-parent.ToParent * frame-parent-parent.ToParent * ... * RootFrame.ToRoot


애니메이션을 위한 몇몇 데이터는 오직 매쉬 정점에 영향을 미치는 뼈들에게만 영향을 미칠 것이다.

따라서 랜더링 과정중에 데이터는 뼈의 이름이 아닌 뼈의 인덱스에 의해 접근한다.

뼈가 영향을 주는 데이터의 배열을 사용하는 스킨에대한 정보 객체는 뼈 정보를 인덱스 배열로 제공하고 또한 그 인덱스를 통해 뼈의 이름도 얻을 수 있다.


또한 초기화 과정에서 오프셋 행렬들의 배열을 생성하는데 이 행렬은 뼈의 각각의 영향을 위한 것이고, 만약 모든 계층구조 프레임이 뼈에 영향을 주지 않는다고 하면

이 행렬은 프레임들의 수보다 적을 것이다.

 

A Slight Diversion from the Initialization Process


이건 초기화 과정은 아니지만 "프레임의 변환 행렬(Frame TransformationMatrix)"에 대해 이해하는 것은 왜 애니메이션에서 오프셋 행렬들이 필요한가에 대해 이해하는 것에 도움을 줄 것이다.

프레임의 변환 행렬은 애니메이션을 사용하는 각각의 랜더링 사이클마다 채워진다.

애니메이션 관리 객체 또는 함수는 이 변환을 계산하기 위해 애니메이션 데이터를 사용하고 그 계산된 결과를 프레임 변환 행렬에 저장한다.

이 행렬은 정점들을 "bone's reference frame"에서 "bone's parent's animated reference frame"으로 변환한다.

이것은 To-parent 행렬과 비슷한데 이것은 "pose position"으로의 적용이다.

이 행렬은 "animated position"에 적용된다.(애니메이션동안에 매쉬가 어떻게 보이게하는지)

이 행렬에 대해 할 수 있는 생각중 하나는 [어떻게 뼈가 "pose position"에서 "animated position"으로 바꾸는지이다.] 


아래의 상황에 대해 생각해보자.

애니메이션이 일어나는 동안 캐릭터의 손이 뼈의 영향을 받아 살짝 회전한다.

 

Bone-frame Rotation



만약 bone의 변환행렬이 영향을 미치는 모든 정점에 대해서 적용된다고 하면, 정점은 root-frame 공간으로 변환되고, root-frame 공간에서 회전하기 때문에 원하지 않은 결과를 보이게 될 것이다.

 


따라서 아직 올바른 변환을 위해서는 정점을 bone-space로 변환시킬 행렬이 필요한데...


Back into Initialization - The Offset Matrix


Note:  오프셋 행렬은 이미 많은 파일 포멧에서 제공되기 때문에 뼈의 영향에 따른 오프셋 행렬의 정확한 계산은 필요하지 않을 수 있다.

하지만 오프셋 행렬을 이해하기 위한 목적으로 정보를 제공한다.


애니메이션이 일어나는 동안에 뼈에 대한 영향을 고려하는 점에 대한 적절한 변환에서 

반드시 root-frame에서의 "pose"  position이 bone에서의 pose frame으로 변환되어야한다.

일단 한 번 변환되면 bone의 변환행렬은 위에서 bone-frame Rotation 그림에서처럼

"어떻게 pose position에서 animated position으로 정점을 바꿀지"에 대한 결과를 만드는 적용을 할 수 있게된다.


정점(root-frame에서의)을 bone-space에서의 "pose" position으로 변환하는 이 행렬은 오프셋 행렬(offset matrix)이라고 불린다.

만약 매쉬가 루트-프레임이 아닌 경우, 매쉬가 부모 프레임에 있다면, 이전 단락에서 언급했던 root-frame에서의 "pose" position으로의 변환 이전에

이 부모-프레임은 루트-프레임 공간으로 변환되어야한다.

운좋게도 이것은 매쉬의 부모-프레임의 To-Root 행렬을 사용하므로써 쉽게 이루어진다.


각각의 영향을 주는 뼈 프레임은 To-Root행렬을 가지고 있다.

하지만 이것은 bone-space에서 root-frame-space로의 변환이다.

따라서 역변환이 필요하다.

행렬 수학에서 역행렬이라는 이 간단한 개념을 통해 정점에 어떤 변환을 곱하든지 상관없이 그것을 그 반대로 만들어 버릴 수 있다.


오프셋-행렬들은 다음과 같이 계산된다.

// A function to search the hierarchy for a frame named "frameName" and return a reference to that frame
Frame FindFrame( Frame frame, string frameName )
{
    Frame tmpFrame;

    if ( frame.Name == frameName ) return frame;
    for each Child in frame {
        if ( (tmpFrame = FindFrame( Child, frameName )) != NULL ) return tmpFrame;
    }
    return NULL;
}

// Note: MeshFrame.ToRoot is the transform for moving the mesh into root-frame space.
function CalculateOffsetMatrix( Index boneIndex )
{
    string boneName = SkinInfo.GetBoneName( boneIndex );
    Frame boneFrame = FindFrame( root_frame, boneName );
    // error check for boneFrame == NULL if desired
    offsetMatrix[ boneIndex ] = MeshFrame.ToRoot * MatrixInverse( boneFrame.ToRoot );
}

// generate all the offset matrices
for( int i = 0; i < SkinInfo.NumBones(); i++ ) CalculateOffsetMatrix( i );



A pseudo-expansion of an offset matrix is as follows:
 

offsetMatrix = MeshFrame.ToRoot * Inverse( bone.ToParent * parent.ToParent * ... * root.ToParent )


A further pseudo-expansion including the Inverse would be:
 

offsetMatrix = MeshFrame.ToRoot * root.ToSomeChild * Child.ToAnotherChild * ... * boneParent.ToInfluenceBone


이 오프셋 행렬은 "pose" position 데이터로부터 단 한 번 계산되지만 이 계산은 계속되는 랜더 사이클 내내 사용된다.

  



The Root Frame


"root"는 계층 구조에서 다른 모든 뼈들의 부모가 되는 프레임이다.

모든 뼈는 부모 또는 부모의 부모를 거슬러 올라감으로써  root를 부모로 갖는다.


만약 루트-프레임에 SRT변환이 적용되면 모든 계층구조 프레임에 SRT변환이 적용된다.

루트-프레임은 매쉬에서 어느 곳이나 위치할 수 있지만 애니메이션 캐릭터 매쉬에서 캐릭터의 중앙에 위치하는 것이 편리하다.

예를 들어 땅 위에서 움직이는 캐릭터의 경우 발과 발 사이에 위치하면 좋을 것이고, 만약 날아다니는 캐릭터라면 캐릭터의 무게중심에 위치하면 편리할 것이다.

이러한 사항은 모델링 과정에서 결정된다.

루트-프레임은 가장 최상단 프레임이기 때문에 부모를 갖지 않고 오직 자식만이 존재한다.


A Bone and It's Children


몇몇의 bone들은 그들이 영향("influence")을 주는 정점들과 연관되어있다.

"influence"의 의미는 bone이 움직일 떄 정점 또한 같이 움직인다는 것을 의미한다.

예를 들어 팔의 bone은 팔꿈치와 손목 부분의 매쉬에 영향을 줄 것이다.

만약 팔을 회전시키면 이 팔이 영향을 주는 점들도 같이 회전의 영향을 받는다.

또한 팔의 움직임은 손목 -> 손가락으로 이어지며 움직이게 만든다.

그렇게 되면 손목과 손가락 bone에 영향을 받는 매쉬의 정점들 또한 영향을 받아 움직인다.

하지만 팔보다 위에 있는 위쪽 팔 또는 다른 부위는 영향을 받지 않는다.


이러한 것들은 어떤 프레임이 움직일 때 이 프레임의 움직임은 아무 프레임에게나 영향을 주는 것이 아니라 영향을 주는 프레임이 정해져 있다는 것을 의미한다.

이러한 프레임들은 여전히 "bone" 이라고 불리지만 "influence bone"이라고 불리지는 않는다.

이러한 프레임들의 동작은 여전히 애니메이션이 일어나는 동안 자식쪽을 향하고, 변환 행렬의 계산은 여전히 계층 구조를 따른다.

 


The mesh and bone hierarchy in pose position 


Bone Influences and Bone Weights


이 데이터는 bone이 영향을 미치는 정점의 배열 + 가중치이다.

각각의 bone에 대하여 몇몇의 쌍을 이루는 데이터인데, 정점의 인덱스 번호와 0~1사이의 float 값 이라고 할 수 있다.

정점의 인덱스는 매쉬 정점 배열에서의 위치를 나타내는 인덱스를 말하고, 가중치라고 불리우는 0~1사이의 실수는 bone이 움직일 떄 거기에 영향을받는 

정점의 위치가 얼마나 많이 움직일지에 대한 상수이다.

만약 하나의 정점에 대해서 여러개의 bone이 영향을 미친다면 이 각각의 bone의 가중치의 합은 항상 1이된다.

예를 들어 하나의 정점에 두 개의 bone이 영향을 미치는데 하나의 가중치가 0.3이라면 다른 하나의 bone이 갖는 가중치는 자연스럽게 0.7이라는 것을 유추할 수 있다.


이러한 데이터는 주로 SkinInfo 객체에서 갖고 있다.

SkinInfo 객체가 어떻게 이러한 데이터를 다루는지에 대해 설명하는 것은 이 기사의 범위를 벗어나므로 설명하지 않겠다.


The Animation Process


"pose" position에서의 계층 구조에 대한 설명을 위에서 다루었었다.

이 매쉬를 실시간으로 애니메이션하기 위한 정보는 보통 계층구조 데이터들과 분리되어 존재한다.

이것은 의도적인데, 캐릭터의 단일 액션을 표현하기 위한 애니메이션 데이터( 걷기, 뛰기등등) 는 "pose" position에 적용될 수 있고,

이것은 캐릭터가 달리거나 걷는 상태에서 다른 액션으로 바뀔 수 있게 한다.

예를 들면 뛰면서 사격 모션을 취하기.


이 애니메이션 데이터는 보통 단순한 함수의 집합에 불과한 AnimationController  객체에서 저장되고 다루어진다.

역시 이 AnimationController 객체에 대해서 자세히 설명하는 것은 이 기사의 범위를 넘는 것이므로 생략하지만

그래도 AnimationController 객체가 하는 몇몇 기능은 아래에서 설명했다.


단일 캐릭터 액션을 위한 애니메이션 데이터는 "animation set"으로 불려지고 이것은 보통 frame-animation의 배열로 구성된다.

아래의 슈도 코드는 이해를 돕기위하여 첨부하였는데 아마 대부분의 animation set은 다음과 같은 형태로 이루어져 있을 것이다.


struct AnimationSet {
    string animSetName; // for multiple sets, allows selection of actions
    AnimationArray animations;
}

struct Animation {
    string frameName; // look familiar?
    AnimationKeysArray keyFrames;
}

struct AnimationKey {
    TimeCode keyTime;
    Vector Scale, Translation;
    Quaternion Rotation;
}


Animation Keys


각각의 프레임은 애니메이션 키의 집합과 연관되어있다.

이 애니메이션 키는 To-parent 변환과 애니메이션 과정중의 특정한 시간의 쌍으로 정의된다.

이 "시간"은 아마 정수형 타입으로 표현될텐데 count of ticks를 의미한다.( 애니메이션의 시작 시간을 표현하기 위해 실수형 타입이 추가되기도 한다. )

그러면 보통 최소 2개의 "time key"를 갖는데 하나는 애니메이션의 시작을 나타내기 위한 것이고 또 다른 하나는 애니메이션의 종료를 나타내기 위한 것이다.

여기서는 여러개의 키가 들어갈 수도 있다.

예를 들면 애니메이션의 틱 카운트 구간이 100이라고 가정할 때 0에서 팔을 들기 시작하고 50에서 팔이 모두 올라갔다가 내려오기 시작하고 100에서 다시 원상복귀되는 식으로 말이다.


키는 변환과 시간의 쌍으로 정의된다고 헀는데 여기서 변환을 표현하기위해 행렬이 사용되기도 하지만

개별적인 벡터의 집합으로 표현되기도 한다. ( 이동을 위한 벡터, 비례확대를 위한 벡터, 회전을 위한 쿼터니온 벡터 )

이렇게 벡터의 집합으로 표현하는 것은 보간(Interpolation)과정을 쉽게 하기 위해서이다.

아까 카운트가 100인 애니메이션의 예로 설명하자면 카운트가 25일때의 행동을 위해서는 0일때의 행동과 50일때의 행동을 보간하여 계산할 수 있다.

만약 키가 이렇게 벡터와 쿼터니온의 형태로 저장된다면 변환행렬은 이전 키와 다음 키의 translation, scaling, rotation)에 대한 보간으로 계산된다. ( 0과 50을 보간하여 25 카운트에서의 위치를 계산했던 예처럼 )


이러한 보간은 보통 다음과 같은 방법으로 계산된다 :

 NLERP (Normalized Linear IntERPolation) or SLERP (Spherical Linear intERPolation) of the quaternions and LERP (Linear intERPolation) of the vectors.


연속적인 틱에서 회전의 변화는 그렇게 크지 않기 때문에 대부분 NLERP를 사용하는 것이 결과도 꽤나 만족스러우며 빠르다.

만약 키가 행렬의 형태로 저장된다면 행렬에서 벡터부분과 쿼터니온 부분을 분리하여 보간 연산을 처리해야하는데 만약 scaling 변환이 적용된 경우 잘못된 결과를 산출할 수 있다.


프레임을 위한 행렬이 계산되면(애니메이션 과정에서 특정한 카운트에서 사용될) 

이 행렬은 프레임의 프레임 구조체에서 변환행렬로써 저장된다. ( frame's Frame structure as theTranformationMatrix.)


아까 언급했듯이 애니메이션 데이터는 프레임 계층구조와는 개별적으로 저장된다.

따라서 각각의 프레임 행렬을 알맞은 장소에 저장하는 것은 아래 예제에서의 FindFunction()과 같은 함수를 통해 이루어진다.

function CalulateTransformationMatrices( TimeCode deltaTime ) { TimeCode keyFrameTime = startTime + deltaTime; for each animation in AnimationSet: { Matrix frameTransform = CalculateFromAnimationKeys( keyFrameTime, animation.frameName ); Frame frame = FindFrame( rootFrame, animation.frameName ); frame.TransformationMatrix = frameTransform; } }




Ticks Per Second


스키닝 매쉬는 유저를 표현하기 위해 실시간으로 표현된다.

여기서 "실시간"은 초단위이지 틱 단위까지는 아니다.

만약에 100 tick count 애니메이션이 3초 걸린다면, 초당 틱 카운트는 100/3 tick가 될 것이다.

애니메이션을 시작하기 위해 처음 tick count는 0으로 설정되어야한다.

씬을 랜더하면서 마지막 랜더로부터 지난 delta time은 여기에 계속 더해져 증가해야한다.

만약 순환되는 애니메이션인 경우, 예를들면 count가 100까지인 애니메이션.

계속 틱 카운트를 더하다가 100이 넘어 120이 됬다고 치면 100을 빼서 20으로 만들고 계속해서 순환되게 해야할 것이다.



Yet More Matrices Must Be Calculated


하... 정말? 진짜?

그렇다. 아직 계산되야할 행렬이 더 남았다 ㅜㅜ

정점을 frame-space로 변환시키는 offset 행렬이 아까 언급되었었다.

그 부분에서 프레임의 TransformationMatrix  가 frame-space에서 적용되어야 했었다.

이 작업은 이제 정점을 frame-animated-space에서 root-frame-animated-space로 변환하고, 그러면 이제 랜더링 될 것이다.

이 변환을 계산하는 것은 "pose" frame-space에서 "pose" root-space로 변환하는 CalcToRootMatrix  함수와 비슷하다.

아래는 frame-animated-space를 root-frame-animated-space로 변환하는 계산이다.

모든 프레임에 root-frame-animated-space 변환에 대한 배열을 생성하여 갖는 것 보다는 TransformationMatrix  를 사용하여 간단하게

frame-space에서 root-space로 변환하자. 


// given this function ...
function CalcCombinedMatrix( Frame frame, Matrix parentMatrix )
{
    // transform from frame-space to root-frame-space through the parent's ToRoot matrix
    frame.TransformationMatrix = frame.TransformationMatrix * parentMatrix;

    for each Child in frame: CalcCombinedMatrix( Child, frame.TransformationMatrix );
}

// ... calculate all the Frame to-root animation matrices
CalcCombinedMatrix( RootFrame, IdentityMatrix );



Are We There Yet?


이제 거의 다왔다.

하지만 아직 빠진 정보가 있다.


정점의 위치는 offset 행렬을 통해 frame-pose-space로 변환되어야 하고,

정점의 위치는 TransformationMatrix를 이용하여 frame-animated-space에서 root-animated-space로 변환되어야 한다.

셰이더 프로그램 또는 랜더링 루틴에서 우리는 위 두개의 연산을 위한 행렬의 배열이 필요하다.

하지만 정말 필요한 것은 영향을 주는 bone의 행렬 뿐이다.


자 이제 FinalMatrix 로 불리우는 또다른 행렬들의 배열, SkinInfo.NumBones() 의 사이즈에 딱 맞는 이것이 생성되었다.

하지만 이 행렬은 이 행렬이 사용되는 매 랜더 사이클마다 다시 생성되어야 한다.

이 FinalMatrix의 계산은 아래와 같다.

// Given a FinalMatrix array..
function CalculateFinalMatrix( int boneIndex )
{
    string boneName = SkinInfo.GetBoneName( boneIndex );
    Frame boneFrame = FindFrame( root_frame, boneName );
    // error check for boneFrame == NULL if desired
    FinalMatrix[ boneIndex ] = OffsetMatrix[ boneIndex ] * boneFrame.TransformationMatrix;
}

// generate all the final matrices
for( int i = 0; i < SkinInfo.NumBones(); i++ ) CalculateFinalMatrix( i );


How It All Works Together


드디어 우리는 스키닝 애니메이션을 랜더링하기 위한 준비가 다 되었다.

매 랜더 사이클마다 아래의 작업을 수행하자.


1. The animation "time" is incremented. That delta-time is converted to a tick count. 


2. For each frame, a timed-key-matrix is calculated from the frame's keys. If the tick count is "between" two keys, the matrix calculated is an interpolation of the key with the next lowest tick count, and the key with the next higher tick count. Those matrices are stored in a key array.


3. When all the frame hierarchy timed-key-matrices have been calculated, the timed-key-matrix for each frame is combined with the timed-key-matrix for its parent.


4. Final transforms are calculated and stored in an array. 


5. The next operation is commonly performed in a vertex shader as GPU hardware is more efficient at performing the required calculations, though it can be done in system memory by the application.


The shader is initialized by copying the FinalMatrix array to the GPU, as well as other needed data such as world, view and projection transforms, lighting positions and paramaters, texture data, etc.


Each mesh vertex is multiplied by the FinalMatrix of a bone that influences that vertex, then by the bone's weight. The results of those calculations are summed, resulting in a weight-blended position. If the vertex has an associated normal vector, a similar calculation and summation is done. The weighted-position (see below) is then multiplied by world, view and projection matrices to convert it from root-frame space to world space to homogeneous clip space.

As mentioned above, proper rendering requires that the sum of the blend weights (weights for the influence bones) for a vertex sum to 1. The only way to enforce that assumption is to ensure that the model is created correctly before it is imported into the animation application. However, a simple bit of code can help and reduces by 1 the number of bone weights that must be passed to the vertex position calculations.

Calculating A Weighted Position
 

// numInfluenceBones is the number of bones which influence the vertex
// Depending on the vertex structure passed to the shader, it may passed in the vertex structure
// or be set as a shader constant
float fLastWeight = 1;
float fWeight;
vector vertexPos( 0 ); // start empty
for (int i=0; i < numInfluenceBones-1; i++) // N.B., the last boneweight is not need!
{
   fWeight = boneWeight[ i ];
   vertexPos += inputVertexPos * final_transform[ i ] * fWeight;
   fLastWeight -= fWeight;
}
vertexPos += inputVertexPos * final_transform [ numInfluenceBones - 1 ] * fLastWeight;


Summary


Data for a skinned mesh is loaded into or calculated by an application. That data is comprised of:
- mesh vertex data. For each vertex: positions relative to a frame of the bone hierarchy
- frame hierarchy data. For each frame: the frame's children, offset matrix, animation key frame data
- bone influence data - usually in the form of an array for each bone listing the index and weight for each vertex the bone influences.
 

Note:  Many of the operations described above can be combined into fewer steps and otherwise simplified. The intent of this article is to provide descriptions of the processes involved in animating skinned meshes using matrices, not necessarily in an efficient fashion.


신고
Posted by 우엉 여왕님!! ghostkyow

티스토리 툴바