iOS 앱 첫 심사 요청 후,

하루도 채 안되서 시원하게 리젝 먹었다.


리젝 메일로 iTunes Connect > Resolution Center 통해서 자세한 사항을 읽어보니..


"ipv6 환경에서 테스트 하는데 네 앱에서 에러 메시지가 뜨더라."

라고 하며 ipv6 환경에 대한 가이드 라인을 blah-blah 적어놨더라.


심지어 친절하게 스크린샷까지 첨부해줬다.


 <- 전달받은 스크린샷의 일부.



1. 애플의 함정

 - ipv6 환경이라니. iOS 9부터 ipv6 환경은 필수적으로 지원해야 한다

애플에서 공식적으로 발표한 것은 알고 있었다. 하지만 막상 이렇게 닥치니 멘붕.


 - 정말 이 오류가 재현이 되는지 확인해보고 싶었다.


 - 그래서 리젝당한 메일을 따라 ipv6 환경 셋팅 방법 문서을 읽어내려갔다.

https://developer.apple.com/library/content/documentation/NetworkingInternetWeb/Conceptual/NetworkingOverview/UnderstandingandPreparingfortheIPv6Transition/UnderstandingandPreparingfortheIPv6Transition.html#//apple_ref/doc/uid/TP40010220-CH213-SW16 )


- 읽다보면 wifi 없이 네트워크에 연결된, 맥이 설치된 장비가 필요한데 나는 맥북밖에 없었고

랜선을 직접적으로 꽂을 수가 없었다.


- usb 이더넷 젠더를 따로 구매해야 하나 심각하게 고민하던 중에...


- 유니티 포럼에서 나와 비슷한 상황에 처한 사람들이 많은 것을 발견.

https://forum.unity.com/threads/solved-unity-5-6-1-iaps-cannot-initialize-on-ipv6-network.477122/ )


- 위 url 에서 unity 개발자가 마지막에 남긴 코멘트를 자세히 읽어보면..

"unity는 이미 ipv6를 지원하고 있으므로 애플이 던진 모든 리젝이 ipv6와 꼭 관련이 있는 건 아니다."


- 이 코멘트에 따르면 확실히 ipv6 문제가 아니다. 애플이 던진 함정에 빠진 것이었다.

문제를 다시 생각해보니.. 내가 전달받은 문제의 스크린샷이 실마리였다.



2. 실마리

- "구매 가능한 상품이 없습니다." 이게 해결의 실마리였다.

유니티 공식 문서에 따르면 다음과 같은 문구가 있다.

https://docs.unity3d.com/kr/current/Manual/UnityIAPiOSMAS.html )

NoProductsAvailable 초기화 오류가 발생하는 경우 다음의 사항을 확인해야 합니다.

  • - iTunes Connect 제품 식별자가 Unity IAP에 제공된 제품 식별자와 정확하게 일치해야 합니다.
  • - iTunes Connect 애플리케이션에 대해 애플리케이션 내 구매가 활성화되어 있어야 합니다.
  • - 해당 상품이 iTunes Connect에서 판매 중이어야 합니다.
  • - iTunes Connect 제품을 새로 생성한 경우 구매가 가능하려면 몇 시간이 소요될 수 있습니다.
  • - 최신 iTunes Connect 개발자 약관에 동의하고 유효한 계좌 정보를 등록해야 합니다.


- iTunes Connect 에서 '앱 내 구매' 메뉴를 들어가보니 이게 웬일...

첫 심사 때 같이 심사 받았던 인앱상품으로 등록해둔 상품들이 전부 리젝 먹으면서 "개발자의 조치가 필요함"

이라는 에러 메시지를 남기고 있었다.


- 문제가 되는 상품을 클릭해보니 현지화 부분에서 에러 표시가 남겨졌지만

정확히 어떤 부분이 문제가 되는지는 알 수가 없었다. 그래서 관련 검색 시작.

( 1 : http://mmzzuu.tistory.com/67 )

( 2 : https://swifteyes.blogspot.kr/2017/04/itunes-connect.html )


- 미리 경험해 본 선배님들의 글에 따르면 표시 이름(display name)에 문제가 있다고 한다.

그래서 나도 수정했다. 어떻게 수정해야 될지 몰라서 일단 기존에 표시 이름과 설명이 동일하게 해둔 것을

설명은 그대로 두고 표시 이름만 짤막한 단어 형식으로 변경했다.


- 그 외는 건드리지 말라고 해서 인앱 상품 에러만 수정해서 다시 재심사 요청.

(display name 수정 + 혹시 몰라서 수정한 display name이 출력되는 스크린샷 재첨부)



3. 승인

- 하루가 지나고 애플의 답변이 왔다. 24시간 이내로 앱이 출시될 거라는 메일이..

남들은 3차 이상 리젝 먹고 출시하는 경우도 허다했지만 그래도 그동안 고생한 것 생각하면 무척 기뻤다..


- 이 일을 계기로 2가지의 교훈을 얻었다.

(1) 문제를 해결하려고 하기 전에 문제가 정확히 무엇인지 파악하자.

(2) 화면에 에러 메시지 출력하는 것을 절대 귀찮아 하지 말자.


- 만약 내가 에러 핸들링 제대로 안 하고(대충 로그만 박는다든가) 심사 요청했으면 

과연 어떤 리젝이 날아왔을지.. 실마리 못 찾고 하마터면 ipv6 연결해서 테스트 해보고 좌절하고 

온갖 쓴맛을 다 봤을지도.. 생각만 해도 소름..


by kelicia 2018. 2. 19. 01:41

https://developer.apple.com/support/itunes-connect/kr/


대부분 iOS 앱 출시 준비하다가 발생하는 에러들을 보면..

위 가이드 대로 충실히(?) 이행하지 않아서 발생하는 경우가 대부분이다.

'~카더라' 통신 보다는 공식 가이드 문서를 잘 읽도록 하자.. (영어라 하더라도 orz...)


- 앱 거절 : 앱 아이콘 투명 라운딩 처리 

  => 투명 제거 아이콘을 유니티에서 올리고 자동으로 리사이징 맡김

  => iOS 7 이후 부터(?) '.png, 알파없음' 포맷만 허용하는 듯 (xcode 업로딩 실패 메시지로 뜬다)


- 앱 거절 : 앱스토어 아이콘 파일을 찾을 수 없다 

  => 1024x1024 앱스토어용 아이콘 이미지 파일 준비 (.png, 알파없음)

  => xcode 에서 프로젝트 Resources 우클릭 후 이미지 파일 추가

  => images.xcassets 에서 앱 아이콘에 추가한 파일 등록


- 앱 경고 : Missing push notification xx => 별도 포스팅 (http://secretroute.tistory.com/entry/1802101737)

- 인앱 결제 : 

  => 아이튠즈 커넥트 - 계약, 세금 및 금융 정보 등록 및 승인

  => app id : 앱내구입 활성화

  => 프로젝트에서 결제 구현

  => xcode9 빌드 설정 - Capabilities - 인앱 결제 활성화 - 빌드 후 업로드

  => 아이튠즈 커넥트 - 나의 앱 - 앱내 추가기능 - 앱내구입 - 상품 등록

  => 아이튠즈 커넥트 - 사용자 및 역할 - 테스터로 초대할 사람 추가 (iTunes connect 사용자(관리자급) / sand box 테스터)

  => test flight 에서 업로드 된 빌드에 대해 테스터 추가 (iTunes connect 그룹은 빌드 업로드시 자동 메일 알림 옴)

  => 테스터는 test flight 앱 설치 후 수신한 테스터 메일을 통해 초대 승인, 설치, 테스트 개시

  => 테스터가 결제하는 경우 iOS 팝업 상에 '베타 테스터에게는..(어쩌구저쩌구)' 가 추가로 기입되어있다.

   (Development 버전의 경우에는 [Environment: SandBox] 로, Production은 위와 같이 알림 표시 확인 -> 실제 결제x)


  => 구글 플레이는 가짜 영수증 발행해주는데.. 애플 스토어는 결제 안 될 거라고 표시만 해주고 아무런 알림도, 영수증도 없었다.


by kelicia 2018. 2. 10. 20:20


xcode 에서 빌드하고 앱스토어에 앱을 업로드할 때..

업로드는 됐긴 했어도 "Missing Push Notification Entitlement" 라는

경고 메일이 애플로부터 날아올 때가 있다.


이건 대체 뭔 소리인지..


- 요약 이슈 : 당신의 앱은 push 를 사용하지만 app id 상에서는 push가 활성화되어있지 않다.

이를 체크해서 검사한 뒤 다시 업로드 해라. (업로드 거절까지는 아니고 경고 메시지임)


- 경고 메시지이나 무시하면 앱 심사 거절 사유 + 업로드할 때마다 매번 경고 메일 날아옴.


- 해결 : 아래 절차대로 수행

(1) 애플 개발자 로그인 - app id 수정 - push 활성화 - push 용 개발자 인증서 생성 및 빌드머신에 인증서 설치

  : provisioning 상에서 entitlement: aps-environment 표시 확인 가능

(2) xcode 에서 프로젝트 열고 설정 창 - Capabilities 탭에서 Push Notifications 활성화

  : 아래 이미지처럼 앱 업로드 할 때 확인 가능


- 참고 링크 : http://blog.themuser.xyz/ios-missing-push-notification-entitlement-이슈-해결/


- (1)만 했더니 계속 경고 메일 날아와서 완전 멘붕했었는데.. xcode 8 부터 푸시 인증서 따로 요구 했다고 한다..

애플 진짜 짜증.. 이메일에서 참고하라는 링크 따라가면 xcode 설정 얘긴 없어서 헤맬 뻔했다.. 검색/공유 만세.


by kelicia 2018. 2. 10. 17:37

# =============== #

# Unity generated #

# =============== #

Temp/

Obj/

UnityGenerated/

Library/


# ===================================== #

# Visual Studio / MonoDevelop generated #

# ===================================== #

ExportedObj/

*.svd

*.userprefs

*.csproj

*.pidb

*.suo

*.sln

*.user

*.unityproj

*.booproj


# ============ #

# OS generated #

# ============ #

.DS_Store

.DS_Store?

._*

.Spotlight-V100

.Trashes

Icon?

ehthumbs.db

Thumbs.db


by kelicia 2017. 8. 7. 17:53

오랜만에 포스팅!



C 공부하다가 문득 printf()로 format에 맞춰 정수를 출력할 때,

"%ul"과 "%lu"의 차이가 뭔지 궁금해졌다.


내가 알기로는 보통 "%ul"이라는 format을 많이 쓰는 걸로 알고 있는데

찾아보니 이는 틀린 format이라고 한다. - 실제 일부 컴파일러는 에러를 내뿜는다고 함.



그런 의미로 평소에 별 생각없이 사용했던 printf()가 어떤 녀석인지 정확히 알 필요가 있을 것 같다.


int printf(char *format, arg1, arg2, ...)


일반적으로 위의 인터페이스에 따라 사용하고는 한다. (이 외의 인터페이스는 귀찮으니 생략)


다들 알다시피 printfformat에 적힌 내용 중에 argument들을 변환하고 formatting 해준 다음,

standard output에 그 결과를 출력하는 일을 한다. return value로는 출력된 문자들의 갯수를 반환한다.


format에 입력할 수 있는 내용으로는 2가지가 있다. 

하나는 standard output stream에 그대로 복사되는 일반 문자들,

다른 하나는 argument들을 어떻게 변환/formatting할 것인지에 대한 conversion specification이다.


conversion specification 서식 = %[flag][width][.precision][length]specifier


'[]'으로 묶인 내용은 선택적인 값을 의미한다. (명시해도 그만, 안 해도 그만. 필요하다면 명시해야겠죠?)

서식을 하나씩 살펴보면,


flag : 왼쪽 or 오른쪽 정렬할 것인지에 대한 표시. 

명시를 안하면 기본은 오른쪽이고, 왼쪽 정렬하려면 '-'을 붙이면 된다.


width : 출력물(?)이 차지할 너비로 숫자 값이다. 

오른쪽 정렬 상태에서 너비가 출력할 문자열 길이보다 크다면, 왼쪽이 텅 비겠죠?


.precision : 앞에 '.'은 width 값이랑 precision 값을 구분하기 위해서 넣어준다. 

precision은 숫자 값으로 무엇을 출력하느냐에 따라 값의 의미가 다르다. 문자열 또는 실수를 출력한다면, 문자나 소수점 이하 숫자의 최대 길이를 의미한다. 정수를 출력한다면 숫자의 최소 길이를 의미한다.


length : 정수를 short로 출력하려면 'h', long으로 출력하려면 'l'(소문자 ell).


specifier : argument를 어떻게 해석할 것인지에 대한 문자 값.

구체적인 conversion table이 있으나 이 포스팅의 제목에 초점을 맞춰서 'u'만 언급한다.

'u' = int; unsigned decimal number.



"%ul"과 "%lu"의 차이를 알기 위해 길게 돌아왔는데, 아마 감이 왔을 것이다.


서식의 순서를 보면 length 한정자가 먼저 나오기때문에 "%lu"가 맞고, "%ul"은 틀리다.


예제 코드들을 보면 "%ul"이라고 쓰는 경우가 굉장히 많은데 (심지어 책에서도 보인다)

바로 알고 쓰도록 하자~!


* Reference

http://stackoverflow.com/questions/23852073/whats-the-difference-between-ul-and-lu-c-format-specifiers

- The C Programming Language 2nd edition (K&R) : 7.2 절


by kelicia 2015. 7. 5. 20:36


나는 여태까지 C++ 은 정적 컴파일이다보니

동적으로 뭔가를 한다는 게 불가능할 줄 알았는데,


그냥 내가 무식했나보다...ㅎㅎㅎ..



C++ 에서 4 가지 캐스팅 연산자 (dynamic_cast, static_cast, const_cast, reinterpret_cast) 가운데

dynamic_cast 에 대해 알아보고자 한다.



 dynamic_cast 상속 관계 안에서 포인터나 참조자의 타입


기본 클래스 → 파생 클래스 로의 다운캐스팅


다중 상속에서 기본 클래스 간의 안전한 타입 캐스팅에 사용된다.

(안전한 타입 캐스팅 : 런타임에 타입 검사를 하겠다는 의미)


const_cast 와 같이 용도가 명확하기 때문에 다른 용도로는 사용하지 못한다.



+추가로

객체가 위치한 메모리의 시작부분을 찾는 데도 사용된다고 한다.

아래와 같이 p 에 시작주소가 담겨진다.

void* p = dynamic_cast<void*> (ObjectPointer);



* dynamic_cast 의 제약사항

1) 상속 관계 안에서만 사용할 수 있다.

2) 하나 이상의 가상 함수를 가져야 한다.

3) 컴파일러의 RTTI (Runtime Type Information) 설정이 켜져있어야 한다.



[예제1]

class Base {
public:
    virtual void print() { cout << "This is Base" << endl; }
};

class Derived : public Base {
public:
    void print() { cout << "This is Derived" << endl; }
};

void main()
{
    Base* pBase1 = new Base;
    Base* pDerived1 = new Derived;

    Derived* pDerived2 = new Derived;
    Derived* pDerived = nullptr;

    // 컴파일 실패 : 부모는 자식이 될 수 없음
    pDerived = pBase;

    // 컴파일 성공 : but 부모는 자식이 될 수 없으므로 널 포인터를 리턴한다!
    pDerived = dynamic_cast<Derived*> (pBase);
    if ( pDerived == nullptr )
        cout << "failed to casting pBase->pDerived" << endl;


    // 컴파일 실패 : pDerived1 의 타입이 Base* 이므로, 부모는 자식이 될 수 없음
    pDerived = pDerived1

    // 컴파일 성공 : 런타임에 타입 변환이 성공하여 Derived 타입의 포인터를 리턴한다!
    pDerived = dynamic_cast<Derived*> (pDerived1);
    if ( pDerived )
        pDerived->print();


    // 컴파일 성공 : 이런 경우에는 캐스팅이 필요없다.
    pDervied = pDerived2;
}


위 예제에서의 키포인트는 

'포인터가 실제로 가르키는 대상이 캐스팅이 될 수 있는 녀석인가' 이다.



dynamic_cast 는 캐스팅에 실패할 경우,

대상이 pointer 면 nullptr 를 반환하고

대상이 참조자이면 bad_cast 예외를 던진다.


이 점이 위에서 언급한 안전한 타입 캐스팅의 의미이다.

캐스팅의 가능 여부를 런타임에 검사하여 처리할 수있다.




다음은 다중 상속일 때, 기본 클래스 간의 타입 캐스팅(Cross-Casting)을 다룬다.


[예제2]

class BaseOne {
public:
    virtual void print() { cout << "This is BaseOne" << endl; }
};

class BaseTwo {
public:
    virtual void print() { cout << "This is BaseTwo" << endl; }
};

class Derived : public BaseOne, public BaseTwo {
public:
    void print() { cout << "This is Derived" << endl; }
};

void main()
{
    BaseOne* pBaseOne = nullptr;
    BaseTwo* pBaseTwo = new Derived;

    // 컴파일 실패 : BaseOne 과 BaseTwo 타입은 호환 불가
    pBaseOne = pBaseTwo;

    // 컴파일 성공 : 기본 클래스 간의 타입 캐스팅 가능
    pBaseOne = dynamic_cast<BaseOne*> (pBaseTwo);

    if ( pBaseOne )
        pBaseOne->print();
}


[예제2] 를 정리하자면 아래의 그림과 같다.



크로스 캐스팅의 한 사용 예로는 

각 기본 클래스 포인터(or 참조자) 타입의 컨테이너 혹은 배열을 운용하면서 

서로 간의 요소를 교환할 때 사용할 수 있다.




여기까지 dynamic_cast 의 용도를 알아보았다.

( [예제1] : 다운 캐스팅[예제2] : 크로스 캐스팅 )



참고로 다중 상속은 권장하지 않는 분위기(?)라서 설계를 재검토 하도록 하자.




※ 포스팅 참고 자료:

http://prostars.net/55

http://msdn.microsoft.com/ko-kr/library/cby9kycs.aspx



포스팅의 엄청난 도움을 주신 (+게으른 저를 포스팅하게 만들어 주셨습니다)

http://prostars.net/55 님께 감사의 인사를 덧붙입니다. (__)


by kelicia 2014. 10. 26. 23:12


언리얼 엔진 4 문서에서 '데이터 주도형 게임 플레이 요소' 라는 글을 읽고

따라해보다가 설명된 것 치고는 은근히 어물쩡(?) 설명되있어서 

사람을 몇 시간씩 구글링하게 만들게 한다ㅡㅡ+...


열받아서 나처럼 헤매는 이가 없도록 포스팅을 하게 됐다.



그래서 오늘 할 것은 

".csv 파일의 데이터를 언리얼 엔진4 에서 사용할 수 있도록 DataTable 오브젝트를 생성하는 가이드 라인" 을 

작성해 보겠다. 

+ 팁으로 일부 버그에 대한 핸들링도 언급하겠다.


다소 영문 문서를 그대로 번역한 느낌의 말투가 있더라도 양해를(__)

제가 직접 해본 것을 기반으로 포스팅한 거라 "반드시 이렇다!" 라는 건 아닙니다!




언리얼 엔진4에서 사용되는 DataTable 의 유형은 크게 2 가지이다.


1) Data Table

어떤 데이터 타입이든 자유롭게 정의할 수 있다. 

ex ) int, string, TAssetPtr<UTexture>, 기타 등등.


2) Curve Table

부동소수점 실수 타입의 데이터만 정의할 수 있고 곡선 상의 보간에 특화된 테이블이다.


- Curve 는 게임 내의 밸런스나 기술/능력에 대한 특성 조절에 유용하게 사용될 수 있다고 한다.

예를 들어 게임 난이도 설정에 따라 HP 가 달라지는 몬스터가 있는 경우

100 ~ 10,000 범위의 HP Curve 를 선형이든 큐브형이든 어떤 방식으로 정의한 후에,

어려움의 난이도 에서 몬스터의 HP 값을 그 Curve 의 최소 / 최대 값 의 75% 정도에서

따오면 된다.

(Curve Table 의 경우, 자세한 내용을 다루는 것은 못봐서 이 정도로만 언급하겠다)




그러면 아까 위에서 말한 가이드 라인으로 돌아와서

크게 5가지 스텝으로 나누어 설명하도록 하겠다.


1. .csv 파일 생성

2. FTableRowBase 를 상속받는 구조체를 생성

3. 코드 빌드 후 언리얼 엔진 재실행

4. 컨텐츠 브라우저에 .csv 파일을 드래그앤드롭 하면 DataTable 오브젝트 생성

5. 블루 프린트나 코드에서 4. 의 오브젝트를 이용해 데이터에 접근 가능




1. 언리얼 엔진에 import 할 .csv 파일을 생성한다.


다음과 같이 엑셀에서 데이터를 만든 후, 저장할 때 .csv 파일을 생성해도 된다.

A1 은 비워두라고 되어있지만 굳이 쓰고 싶다면 써도 큰 문제는 없다.


왜냐하면 첫 번째 컬럼 값들은 Row ID 를 나타냄으로서 

데이터 테이블에서 Row 를 구분하기 위해 반드시 있어야 하는 기본적인 아이라 

언리얼 엔진4에서 이 파일을 import 할 때, 첫 번째 컬럼은 건너뛰어 버리기 때문이다.

(관련 링크 : CreateTableFromCSVString

https://docs.unrealengine.com/latest/INT/API/Runtime/Engine/Engine/UDataTable/CreateTableFromCSVString/index.html )


내가 만든 샘플 데이터(.csv)들은 다음과 같이 생겼다.




2. FTableRowBase 를 상속받는 구조체를 생성한다.



언리얼 엔진에서 프로젝트를 열고, 코드를 추가해준다. 

이 때 주의해야 할 점은 None 이 아니라 언리얼 엔진에서 제공하는 클래스를 반드시 상속받도록 하자.

안 그러면 차후 빌드할 때 꽤나 골치 아파진다.. 그냥 Dummy 클래스 하나 만든다고 생각하자.


저 같은 경우에는 'Show All Classes' 에 체크하고 Engine 클래스를 상속 받았습니다.

별 뜻 없습니다. 단지 None 으로 하고 구조체만 달랑 정의했다가 -매우 - 피봤기 때문에ㅠ_ㅠ

누군가가 "이렇게 하니까 되던데요" 라고 하는 코드를 보고 저 클래스를 상속 받았을 뿐.


Engine 클래스가 아니더라도 Actor 를 받아도 되고 상관 없습니다.

구조체 정의하기 전에 언리얼 엔진의 클래스를 반드시 상속 받는 Dummy 클래스를 만드는 게 포인트.



저 같은 경우 빈 프로젝트에서 데이터 테이블을 생성하고 있었는데,

찾아보니 이 문제(?)를 해결 하기 위해서는 프로젝트에 반드시 언리얼 엔진에서 

상속받은 클래스가 적어도 1개 이상 있어야 한다고 한다.

(관련 링크 : https://answers.unrealengine.com/questions/71182/can-not-derivate-from-struct-ftablerowbase.html )



클래스를 만들었으면 구조체를 정의하는데 그 안에

1.에서 만든 데이터 필드들의 이름과 동일하게 변수들을 작성해주면 된다.


내가 만든 데이터들을 기반으로 구조체를 작성해 본 결과는 다음과 같다.

언리얼 엔진에서 Engine 클래스를 상속받은 MyDataAsset 이라는 클래스를 생성했고,

아래 코드는 MyDataAsset.h 에 작성하였다. LevelUpdata 라는 구조체를 만들었다.



아까 위에서 보여준 .csv 파일에서 첫번째 셀의 값에 Name 이라는 값을 넣어주었지만

코드에서는 적어주지 않았다. 적어도 되고 안 적어도 되는 듯 싶지만...


언리얼 엔진4 문서에서는 생략하길래 그냥 나도 생략해버렸다. 귀찮아서.


여기서 포인트는 구조체가 FTableRowBase 를 상속받았다는 점.

아, 참고로 언리얼 엔진 구조체 내에는 함수를 정의할 수 없다고 한다. 

(상당히 내겐 충격적이었음_-_ .......... 구조체 내에 함수 정의를 못 해서

밑에 다룰 버그 관련 내용에서 살짝 귀찮게 됨)



코드 따라서 입력하기 귀찮으신 분들은 포스팅 맨 아래 참고 링크들

따라가셔서 ctrl + c,v 하시면 됩니다~




3. 빌드를 하고 언리얼 엔진을 재실행 시킨다.


주의해야 할 점은 빌드 하기 전에는 반드시 언리얼 엔진을 종료시켜야 한다.

안 그러면 LNK 에러를 미친 듯이 뿜어낼 수 있다. (정확한 이유는 잘 모르겠음)


엔진 종료 -> 빌드 -> 엔진 재실행


순서로 가자.




4. 엔진의 컨텐츠 브라우저에서 1. 에서 만든 .csv 파일을 드래그앤드롭 한다.



이 때, 다음과 같은 다이얼로그 박스가 하나 나올 것이다.



3. 에서 정의한 구조체인 LevelUpData 가 보일 것이다.

만약 2.에서 언리얼 엔진으로부터 상속 받은 클래스가 없다면 저 망할 Row Type 에 

내가 정의한 구조체가 뜨지 않는, 짜증나는 현상의 연속을 보게 될 것이다. -> 이것때문에 삽질 겁나 함.



아무튼 여기까지 Import 과정을 모두 마쳤으면 위의 스샷처럼 콘텐츠 브라우저에

'DataTable' 이라는 초록색 오브젝트가 생긴 것을 볼 수 있다 :-)


오브젝트를 더블 클릭하면,


요렇게 이쁘게 나오는 것을 볼 수 있다.


참고로 중간에 .csv 파일의 데이터가 수정되었다면 

엔진 쪽에서 만든 오브젝트를 우클릭 -> Reimport 하면 변경된 데이터를 볼 수 있다.



그런데 ! 주의해야할 점을 알려준다면 

위와 같이 어떤 파일의 상대주소를 데이터로 넣었을 때, 제대로 안 뜨는 경우가 있다.


나의 경우 어떤 삽질을 했냐면...


엑셀 파일에서 "Texture2d'/Game/Textures/AchievementIcon2'" 라고 입력을 한 뒤에

.csv 파일로 변환해서 그 파일을 열어보면 어처구니 없게도 ㅡ_ㅡ

양쪽에 큰 따옴표가 2개씩 더 붙어서 나오는 현상이 있었다.

이렇게. -> """Texture2d'/Game/Textures/AchievementIcon2'"""


저런 경우에는 언리얼 엔진에서 import 한 데이터가 None 으로 뜨게 되버린다.


또는 큰 따옴표를 빼먹고 Texture2d'/Game/Textures/AchievementIcon2' 라는 데이터가

.csv 파일에 저장되어있을 때, import 하면 데이터가

Texture2d' 까지 밖에 안 보이는 현상이 있었다.


위 스샷 처럼 정확한 주소를 보이게 하려면 .csv 파일을 열어봤을 때, 데이터가

"Texture2d'/Game/Textures/AchievementIcon2'" 와 같이 정확히 한쌍의 큰 따옴표만 감싸고 있어야 한다는 점.


그러니 엑셀에서 큰 따옴표를 사용해 데이터를 저장할 때는 주의하자 ! !



참고로 아래는 언리얼 엔진 문서에서 발췌한 내용이다.

The double quotes around the asset type are important for the property importing pipeline. Without them, the text is imported as Texture2d'.





5. 블루프린트나 코드 상에서 4.에서 생성한 오브젝트에 접근해보자.


여기서 중요한 건, 블루프린트에서 데이터에 접근할 때 짜증나는 버그가 존재한다.

차후 엔진 측에서 고쳐줄 지는 모르겠지만 지금 언리얼 엔진 4.4 버전 상에서는 아직도 존재한다.


1) 블루프린트에서 DataTable 오브젝트에 접근 하기


무슨 버그가 있는지 일단 살펴보겠다.

아래에 보면 개발자가 정의한 데이터 테이블에 있는 데이터에 접근하기 위해 

"Get Data Table Row (DataTable Object 이름)" 라는 노드가 존재한다.

이게 문제다.



빨간색으로 'X' 표시 되어있는 부분이 문제의 버그다.

막상 블루프린트 작성할 때는 멀쩡히 잘 연결되다가 언리얼 엔진을 종료하고

다시 이 프로젝트를 열면 저기 표시되어 있는 링크 부분이 사라지게 되는 버그가 발생한다.

그래서 데이터에 접근이 안 되서 컴파일 할 수 없다고 난리다. 


(데이터 테이블의 초기화 문제였나? 암튼 그런 쪽으로 뭔가 버그가 있는 듯 했다.

구글링했을 때 영문으로 뭐라고 나와있었는데 이해할 수 없었다. 아시는 분은 방명록에 남겨주시면 ㄳㄳ (__)

자세한 링크는

https://answers.unrealengine.com/questions/67457/get-data-table-row-bug.html )


결국, 문제의 'Get Data Table Row' 노드를 사용할 수 없다.

그러니 우회하는 방법으로 데이터에 접근하자. 꽤 간단하게 해결되는 문제다.



여기서부터 내가 구글링의 도움을 받아 실험한 결과를 토대로 작성한 가이드 라인이다.

정답은 아니고 '이런 식으로 해결 할 수 있다고 하네요' 정도로만 읽어주시면 감사하겠습니다 :-)



① 언리얼 엔진에서 코드를 추가한다.

내 경우에는 Actor 를 부모 클래스로 상속받아 'DummyActor' 클래스를 생성했다.



② DummyActor.h 에 다음과 같이 작성해준다.



사실 나는 아까 선언했던 LevelUpData 구조체 안에 getTableRow() 함수를 넣으려고 했지만 

위에서 이미 언급했듯이 구조체 안에 함수 선언을 할 수가 없다...ㄱ-....아놔..


그래서 따로 울며 겨자먹기로 새로 클래스를 만들어 위와 같이 코드를 작성해주었다.



③ DummyActor.cpp 에 다음과 같이 작성해준다.



빨간색 네모 박스에 있는 코드가 핵심이다.



④ 언리얼 엔진을 종료하고 빌드한 후, 엔진을 재실행하자.



⑤ 내 경우에는 게임이 Play 될 때 좌측 상단에 데이터를 출력하는 걸 테스트하고 싶었다.

그래서 잘은 모르지만(?) : 참고로 필자는 언리얼 엔진4 써 본지 얼마 안 된 조금 멍청한 초보다.

GameMode 를 상속받아 블루프린트를 생성하였다.



⑥ 블루프린트에서 DataTable Row Handler 타입의 변수를 하나 생성해 

Default value 에서는 4.에서 생성한 Data Table 오브젝트에 연결 할 수 있다.

Row Name 은 데이터 테이블에서 행을 구분해주는 첫번째 컬럼을 말한다. 가져오길 원하는 RowID 값을 선택하면 된다.


아까 정의한 getTableRow 함수를 가져와야 하니 DummyActor 변수도 생성해주자.



⑦ 다음과 같이 데이터에 접근할 수 있다.

원하는 데이터에 접근해 마음대로 지지고 볶고 하시면 됩니다. 잘 안 보이시면 이미지 클릭.





여기까지 블루프린트에서 DataTable 에 있는 데이터에 접근하는 방법이었습니다.


코드 부분은 제가 따로 테스트를 하려고 시도했지만...

언리얼 엔진4의 최소 메모리 스펙이 8GB 인데... 제 컴이 그 절반 밖에 안되는 녀석이라

여기까지 도달하는 데도 수많은 시간이 걸렸습니다 흑흑 ㅠ_ㅠ


그래서 코드로 데이터 접근하는 부분인 제가 직접 해보지는 못 했구요,

관련 자료 링크만 정리 해서 남겨봅니다.




* 엑셀 데이터로 게임 플레이 구동시키기(한글) :

https://www.unrealengine.com/ko/blog/driving-gameplay-with-data-from-excel


* 데이터 주도형 게임플레이 요소(한글, 영문은 주소에서 KOR 를 INT 로 바꿔주세요) :

https://docs.unrealengine.com/latest/KOR/Gameplay/DataDriven/index.html


2개 링크 모두 비슷한 설명 수준이구요, 자세한 설명은 아닙니다.

그냥 '이러한 흐름을 거치면 .csv 파일이 import  됩니다. 하하:D' 정도?


* 제가 방금 언급했던 미처 확인하지 못한 코드에 대한 자료는 언리얼 엔진4 위키에 있습니다. 아래 링크 참조.

https://wiki.unrealengine.com/Using_excel_to_store_gameplay_data_-_DataTables



되는 것만으로도 감지덕지한 기분으로 하다보니 미처 '이 부분은 왜 이렇게 되는거임?' 이라는 궁금증이

있었음에도 불구하고, 일단 되게끔 만드는 데 집중하다보니 부족한 부분이 많이 있습니다 ㅠ_ㅠ


혹시 지적해주실 내용이 있으시다면 조금 귀찮으시더라도

방명록에 꼭 남겨주세요. 수정해야할 부분은 수정하도록 하겠습니다 :-)



'Programming' 카테고리의 다른 글

유니티로 개발할 때 많이 쓰이는 .gitignore  (0) 2017.08.07
[C]printf()에서 "%ul"와 "%lu"의 차이?  (0) 2015.07.05
자료구조 : Linked List  (0) 2014.04.25
Delegate (대리자)  (1) 2013.12.20
[Python, C#]Lambda Form  (0) 2013.12.19
by kelicia 2014. 9. 22. 14:39

중복 컴파일 방지를 위해 자주 쓰이는 전처리기 지시자들에 대해 비교해 보고자 한다.



1) #ifndef


먼저 다음과 같은 코드가 있다고 하자.


First.h

#ifndef _FIRST
#define _FIRST

class First
{
};
#endif

Second.h

#ifndef _SECOND
#define _SECOND

#include "First.h"

class Second
{
};
#endif

Main.cpp

#include "First.h"
#include "Second.h"

void Main()
{
}


Main.cpp 를 컴파일 해보면 #include 의 작동방식에 따라

그 파일의 코드가 그대로 복사될 것이다.


#ifndef _FIRST
#define _FIRST

class First
{
};
#endif

////////////////////////////////

#ifndef _SECOND
#define _SECOND

#include "First.h"

class Second
{
};
#endif

////////////////////////////////

void Main()
{
}


위와 같이 되고 Second 에서 또 한번 #include "First.h" 를 읽게 될 것이다.

그 결과로 ,


#ifndef _FIRST
#define _FIRST

class First
{
};
#endif

////////////////////////////////

#ifndef _SECOND
#define _SECOND

#ifndef _FIRST
#define _FIRST

class First
{
};
#endif

class Second
{
};
#endif

////////////////////////////////

void Main()
{
}


위의 코드와 같이 Main.cpp 에는 First.h 이 2번 복사되는 결과를 볼 수 있다.


결론을 내리자면, #ifndef 의 방식으로 중복 컴파일을 피하는 것은

#include 명령어를 사용한 횟수 만큼 전처리기가 그 파일을 복사해오고,

복사해 온 코드 상단에 작성된 #ifndef 구문을 읽고 컴파일 여부를 확인 한다는 것이다.


이렇게 결론을 지으면 

'그런데도 왜 이 방식을 써서 중복 컴파일 방지를 하냐 ?!'

라고 할 수 있다. 물론 장점도 존재하고 포스팅 마무리할 때쯤 언급하겠다.




2) #pragma once


그런데 나는 - pragma - 라는 단어가 대체 무슨 뜻인지 궁금해서 찾아보았다.



짜증. 괜히 맛있는 치킨만 먹고 싶어졌다.

그냥 컴파일러 지시자? 정도로만 이해해야겠다.


구글에 검색해도 pragma 가 뭐 하는 녀석인지만 나와있지,

정작 pragma 라는 단어 자체의 뜻에 대해서는 잘 안 나오는 듯_-_



어쨌든 잡소리였고 다시 중복 컴파일 방지를 위한 #pragma once 명령어로 돌아가자.


이 지시자가 의미하는 바는 '1번만 컴파일 하겠다' 라는 뜻으로 알고 있다.

msdn 에 따르면 (http://msdn.microsoft.com/en-us/library/4141z1cx(v=vs.80).aspx)


Pragma 지시자의 토큰 값으로 들어갈 수 있는 once 키워드 : 
Specifies that the file will be included (opened) only once by the compiler when compiling a source code file.

Remark : This can reduce build times as the compiler will not open and read the file after the first #include of the module.


아까 말한 것과 다를 바 없고, 이게 전부다. 

#ifndef 과 다르게 include 로 인해 여러 번 파일을 열어서 읽는 것이 아니라서 빌드 타임을 감소시킬 수 있다는 점.




3) 정리


#ifndef

- 앞서 보았듯이 헤더 파일을 여러 번 include 하게 되면 define 여부를 계속 체크해야 되기 때문에

컴파일 단계 중에서 파일 해석 단계의 속도가 #pragma once 에 비해서 다소 느리다고 할 수 있다.

하지만 장점을 꼽자면 전처리기 지시자(Preprocessor directive)라서 모든 컴파일러에서 동작한다는 점이다.


#pragma once :

- 1번만 컴파일 하고 그 뒤로부터는 동일한 파일의 경우, 읽기조차 하지 않는다. 그래서 파일 해석 단계의 속도가

#ifndef 보다는 빠르다. 하지만 아까 사전적 의미를 찾아봤을 때, 자세히 보면 'A compiler directive' 라고 적혀있다.

컴파일러 지시자로 특정 컴파일러에서만 동작하는 지시자이며 Visual C++ 5.0 이상에서만 동작한다고 한다.



얼핏 속도면에서 보면 #pragma once 가 좋아보일 수 있으나 

거대 프로젝트가 아닌 이상 속도에서 큰 차이를 볼 수 있을까? 하는 의문이 든다.


그리고 안정성(호환성)과 범용성을 고려한다면 #ifndef 방식을 택해야 할 것이다.




4) Reference


http://ace01kr.tistory.com/entry/%ED%97%A4%EB%8D%94%ED%8C%8C%EC%9D%BC-%EC%A4%91%EB%B3%B5%EB%B0%A9%EC%A7%80-pragma-once-vs-ifndefendif


http://neodreamer-dev.tistory.com/310


http://msdn.microsoft.com/en-us/library/t22e924w(v=vs.80).aspx


'Programming > C++' 카테고리의 다른 글

dynamic_cast 연산자  (0) 2014.10.26
정적 바인딩(Static binding) vs. 동적 바인딩(Dynamic binding)  (0) 2014.08.19
by kelicia 2014. 8. 25. 19:21


* Binding 

- 프로그램 구성 요소의 성격을 결정해주는 것

ex ) 변수의 데이터 타입이 무엇인지 정해지는 것




 종류

정적 바인딩(Static binding)

동적 바인딩(Dynamic binding)

 정의

 컴파일 시간에 성격이 결정되는 것

 실행 시간(runtime)에 성격이 결정되는 것

 예시

C언어 컴파일 시간에 변수의 데이터 타입이 결정

Python(Interpreter 언어) 런타임에 값에 따라 

 변수의 데이터 타입이 결정

 장단점

 컴파일 시간에 많은 정보가 결정되므로 실행 효율↑

 런타임에 자유롭게 성격이 바뀌므로 적응성↑




* 함수의 바인딩

- 함수를 만들어 컴파일을 하면 각각의 코드가 메모리 어딘가에 저장된다.

그리고 함수를 호출하는 부분에는 그 함수가 저장된 메모리 번지수(주소값)이 저장된다.


프로그램 실행 → 함수 호출 → 함수가 저장된 주소로 점프 → 함수 실행 → 원래 위치


위 과정에서 함수를 호출하는 부분에 함수가 위치한 메모리 번지로 연결시켜 주는 것을 바인딩(Binding) 이라고 한다.



- 함수를 바인딩하는 2가지 방법

(1) 정적 바인딩 (일반 함수)

컴파일 시간에 호출될 함수로 점프할 주소가 결정되어 바인딩 되는 것.

(2) 동적 바인딩 (가상 함수)

실행 파일을 만들 때 바인딩 되지 않고 보류 상태 둔다.

점프할 메모리 번지를 저장하기 위한 메모리 공간(4 byte)을 가지고 있다가 런타임에 결정.

=> 단점 : 타입 체킹으로 인한 수행 속도 저하 / 메모리 공간 낭비

=> 가급적 정적 바인딩 사용


?? 2 가지의 단점이 있음에도 불구하고 동적 바인딩을 하는 이유 ??

- 어떤 포인터에 의해 접근되었는 지에 상관없이 참조된 인스턴스의 실제 클래스형에 따라 재정의된 함수 호출이 가능!




이 포스팅의 원본 출처이자 C++ 에서

Template, 정적 바인딩 vs. Virtual Function, 동적 바인딩 으로 실험한 결과를 볼 수 있는 블로그 :

http://blog.daum.net/sox25/2


위 출처에서 정적 바인딩이 항상 동적 바인딩 보다 빠르다고 할 수 없다는 결과를 볼 수 있다.





* 프로그래밍 언어에서의 2가지 Type System


(1) 정적 타입 (Static Type)

- 컴파일 시에 타입이 결정

- 변수를 선언할 때, 반드시 앞에 타입을 명시해야 하는 언어들은 모두 정적 타입 시스템에 속한다.

ex ) C, C++, Java ...


- 결국 장점에 대해 언급을 하자면,

. 컴파일 시에 타입에 대한 정보를 결정하기 때문에 속도↑ (효율성↑)

타입 에러로 인한 문제점을 초기에 발견할 수 있어 타입의 안정성↑


(2) 동적 타입 (Dynamic Type)

- 런타임 시에 타입이 결정

- 코드를 작성할 때, 변수 타입을 명시하지 않고 런타임에 변수의 값에 따라 타입이 결정되는 시스템.

ex ) Python, Ruby, SmallTalk ...


- 장점 : 유연성 (혹은 적응성)

런타임까지 타입에 대한 결정을 끌고 갈 수 있기 때문에 많은 선택의 여지가 있다.

- 단점 : 안정성

인터프리터 언어는 배우기는 쉬우나 실행 도중에 변수에 예상치 못한 타입이 들어와 Type Error 를 뿜는 경우가 생긴다.

그러니 너무 편리함에만 의존하면 안정성 저하를 유발할 수 있다.




* Type System 에 따른 상속의 의미 차이


객체지향에서 중요한 개념 중에 하나가 다형성(Polymorphism) 이고,

일반적으로 다형성은 함수의 오버라이딩, 즉 동적 바인딩에 의한 것이다.


(1) 정적 타입(Static Type) 의 상속

: 코드 재사용 + 타입의 호환성 유지 를 목적으로 한다.

- 컴파일 시에 상속의 관계를 파악하여 상속관계에 있는 객체들 간의 타입 호환성을 유지해

타입은 컴파일 시에 결정되나 메소드 호출 시에 값에 따라 그 객체의 메소드를 호출하게 된다.

이와 같은 방법으로 동적 바인딩에 대한 문제를 해결했다고 할 수 있다.


(2) 동적 타입(Dynamic Type) 의 상속

: 코드 재사용 을 목적으로 한다.

- 타입 속성에 따라 메소드의 형태만 같으면 동적 바인딩을 유도할 수 있다.



간단하게 예시를 보면,

a.call();


정적 타입의 경우 :

컴파일 시에 a 객체의 타입에 호환될 수 있는 타입을 결정하고, 

런타임 시에 객체의 값에 따른 호환성 있는 객체의 메소드를 호출한다.

=> 즉, 상위 클래스의 코드 재사용과 동시에 타입의 호환성을 유지하는 목적을 가진다.


동적 타입의 경우 :

동적 타입 시스템에 의해 동적 바인딩이 자동적으로 유도되어 단지 상위 클래스의 코드를 상속받는 의미가 전부다.




바인딩이 일어나는 타이밍에 관련 포스팅 :

http://destiny738.tistory.com/178



휴.. 팔수록 어렵다 ㅠ_ㅠ


'Programming > C++' 카테고리의 다른 글

dynamic_cast 연산자  (0) 2014.10.26
.h 컴파일 : #ifndef vs. #pragma once  (0) 2014.08.25
by kelicia 2014. 8. 19. 11:25


01. 3D 그래픽의 이해



1. 3D 그래픽 파이프라인



1.1 게임 엔진을 구성하는 소스코드 모듈들

- User Input (사용자 입력)

- Resource Management (자원 관리)

- Loading and Rendering Graphics (그래픽 로딩과 렌더링)

- Interpreting and Executing Scripts (스크립트 해석과 실행)

- Playing Sound Effects (음향 처리)

- Artificial Intelligence (인공 지능)



1.4 좌표계

- Mesh (3차원 공간상의 객체) 를 기하학적으로 표현하기 위해선 좌표계가 필요하다.

- 2차원 좌표계(2차원 직교 좌표계, 화면(스크린) 좌표계), 3차원 좌표계(왼손 좌표계, 오른손 좌표계)

(출처 : http://msdn.microsoft.com/en-us/library/windows/apps/ff729721.aspx)


(출처 : http://blog.daum.net/jty71/15645437)



- 모델(or 지역) 좌표계와 월드 좌표계

- 모델 좌표계 : 객체의 지역적 공간(Local space)을 표현하는 좌표계(모델마다 별도의 좌표계를 갖고 있다고 가정)

- 월드 좌표계 : 게임 세계 전체를 하나의 통일된 좌표계로 표현하기 위한 좌표계(전역 좌표계)

(출처 : http://msdn.microsoft.com/en-us/library/windows/desktop/bb206365(v=vs.85).aspx)



1.5 와인딩 순서(Winding Order)

- Mesh 를 구성하는 다각형의 정점들을 나열하는 순서. 

즉, 다각형의 정점 or 모서리가 어떤 순서로 연결되는지를 나타냄.

- 은면(Back Face Culling : 카메라에서 보이지 않는 면) 제거를 수행하기 위해 사용한다.

- 시계 방향, 반시계 방향 순서가 있는데 Direct3D는 기본적으로 시계방향. (보통 반시계가 기본임)

Direct3D 기준으로 눈에 보이는 면들이 시계방향으로 와인딩 되기 때문에 
보이지 않는 면의 경우 반시계방향으로 와인딩 된다고 할 수 있다.

(출처 : http://cookiejeon.tistory.com/3)



1.6 화면 렌더링

- 화가의 알고리즘(Painter's Algorithm) : 3차원 세상을 2차원 평면에 그리는 과정이나 비효율적.

 자세한 내용은 위키백과.

http://ko.wikipedia.org/wiki/%ED%99%94%EA%B0%80_%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98 )





2. 변환 파이프라인


① 정점(지역 좌표계)

월드 변환 (World Transformation)

② 월드 좌표계

카메라 변환 (View, Camera Transformation)

③ 카메라 좌표계

------------------------------------------------ ↑ 3D, ↓2D 좌표

투영 변환 (Projection Transformation)

④ 투영 좌표계

화면 변환 (Screen Transformation)

⑤ 화면 좌표계



* 참고로 Primitive Transformation 에는 3가지 종류가 있다.

1) Translation (이동)   2) Rotation (회전)   3) Scaling (크기)

(관련 링크 : http://msdn.microsoft.com/en-us/library/windows/apps/ff729722.aspx)



# 월드 변환(World Transformation)

- 회전 변환 + 평행이동 변환을 통해 지역 좌표계로 표현된 객체의 Mesh 가 월드 좌표계로 변환되는 과정

(회전 변환 계산법 생략*), (회전을 먼저 하냐 평행을 먼저 하냐에 따라 결과가 다르다는 점 주의)

(출처 : http://goldface.tistory.com/archive/20120205)


# 카메라 변환(View, Camera Transformation)

- 월드 좌표계로 변환된 정점을 카메라 좌표계로 변환하는 것을 말한다.

- 3차원 공간이 투영되어 가상 카메라에 나타나는 2차원 평면을 카메라 평면이라 한다.

- 가상 카메라는 위치, 방향, FOV(Field of View) 와 같은 속성을 가진다.

※ FOV : 특정 방향과 특정 위치에서 어떤 물체가 보이는 지를 말한다. 

   사람의 경우 앞(Forward)을 향해 FOV를 가지고 있다. 그래서 자신의 뒤에 무엇이 있는지 볼 수 없다.

※ 화각(Viewing Angle) : 카메라에서 볼 수 있는 각도, 화각에 따라 FOV 공간의 형태가 달라진다.

아래 그림에서 Θ에 해당한다.


사람은 너무 가깝거나 or 너무 멀거나 하는 물체는 제대로 볼 수 없다.

그래서 컴퓨터 그래픽에서는 이 FOV 를 다음과 같이 View Frustum 으로 표현한다.


View Frustum 은 총 6개의 면으로 이루어져 있고 그 중 2 개는 XY 평면과 평행하다.

아래 그림에서 a 면이 Near-Z, b 면이 Far-Z 평면이라 불린다.

FOV 가 넓을수록 View Frustum 의 부피도 커지고 그만큼 시야가 넓어지는 것을 의미한다.


보통 좌표계에서 Z 축이 Forward 방향을 나타낸다. (X 축이 좌우, Y 축이 상하, Z 축이 앞뒤)

(출처 : http://msdn.microsoft.com/en-us/library/windows/apps/ff729721.aspx)

- 카메라 변환 과정

1) 카메라를 월드 좌표계의 원점으로 평행이동

2) 1)과 같은 변환을 게임 세계의 모든 객체에 적용

3) 카메라 좌표계 축들이 월드 좌표계 축들과 일치하도록 회전 변환

4) 3)과 같은 변환을 게임 세계의 모든 객체에 적용

(출처 : http://msdn.microsoft.com/en-us/library/windows/apps/ff729721.aspx)



# 투영 변환(Projection Transformation)

- 카메라 좌표계에서 카메라에 보이는 객체에 대하여 카메라와 객체까지의 거리는 기본적으로 Z 좌표로 근사할 수 있다.

- 카메라와 Mesh 사이의 Z 좌표가 증가할 때 메시의 각 정점 X, Y 좌표를 Z 좌표(ViewSpaceZ)의 z 값에 반비례하도록

변환하면 원근 효과를 나타낼 수 있다.

- 카메라 FOV 공간에 있는 정점들이 투영되는 공간은 투영 윈도우(Projection Window) 라고 한다.

-1 <= X <= 1, -1 <= Y <= 1 :: 카메라 FOV 공간에 포함된 정점들을 투영 변환했을 때 값들의 범위

(그외의 값을 갖는 점들은 카메라 FOV 공간에 포함되지 않아 보이지 않음)

- 투영 변환 :

X(projected) = x / ( z / d )

Y(projected) = y / ( z / d )

d 는 카메라에서 투영 평면까지의 거리를 의미한다.

d = 1 인 경우는 FOV 의 화각이 90도인 경우를 말한다. Direct3D의 경우 항상 1.0 의 값으로 설정.

- 쉽게 설명하자면,

투영 변환이란 3D 좌표 → 2D 좌표로 바꾸는 작업이다. 그러면 어떻게 바꾸냐?

FOV 의 화각이 90도 라고 했을 때(d=1),

(x, y, z) → (x/z, y/z, z/z) → (x/z, y,z, 1) 으로 바꿔버린다.

그러면 모든 정점들의 Z 좌표 값이 모두 1이 될 것이다. 

즉, 한 평면에 모든 정점들이 표현되고 이는 2D 좌표와 다를 것이 없다.


여기서 중요한 건 d=1 이라는 가정이다. 정확히는 z / (z/d) = 1 / d 가 되어

투영 변환된 Z좌표의 값은 0 <= Z <= 1 값을 갖게 될 것이다.

'그러면 2D가 아니지 않느냐?' 라고 생각할 수 있는데 왜 이렇게 되어야 하는 지는

Z-Buffer 얘기를 해야하는데 위에 '화가의 알고리즘' 관련 링크를 따라가 보면 알 수 있다 :-)



# 화면 변환(Screen Transformation)

- 투영 변환을 거친 정점은 2차원 좌표라고 생각할 수 있다.

이러한 2차원 좌표를 화면 좌표계로 변환하여 2차원 화면의 Pixel 로 그리는 단계.

- 렌더링할 대상의 화면 영역을 뷰포트(Viewport) 라고 한다.

(출처 : http://www.asksatyam.com/2011/01/window-to-viewporttransformation.html)


아무리 비율 좋은 Mesh 라고 한다고 해도 화면 비율에 맞게 렌더링 하다보면

위와 같은 현상이 나타날 수 있다. 그러니 다음과 같은 식을 이용한다.


- 픽셀 위치 좌표계 :

ScreenX = projVertex.x * (ViewportWidth / 2) + ViewportLeft + (ViewportWidth / 2)

ScreenY = - projVertex.y * (ViewportHeight / 2) + ViewportTop + (ViewportHeight /2)


ViewportLeft, ViewportTop : 뷰포트의 원점 위치

ViewportWidth, ViewportHeight : 뷰포트 가로, 세로 크기

※ 투영 좌표계는 좌표계 한가운데가 원점인 직교 좌표계이고, 

뷰포트는 왼쪽 상단이 원점인 좌표계이므로 Y 축의 방향이 서로 반대 방향이라 (-) 부호를 붙인다.




Transformation Pipeline 의 흐름을 잘 이해할 수 있는 이미지를 가져와봤다.

(출처 : https://forum.libcinder.org/topic/finding-an-object-s-screen-coordinates)



* 그 외 참조 :

http://code.msdn.microsoft.com/windowsapps/Direct3D-Tutorial-Win32-829979ef


by kelicia 2014. 7. 21. 21:11
| 1 2 3 4 |