(이하는 A trip through the Graphics Pipeline 2011, part 1 를 날림번역한 것임다.)

Part 1 - 소프트웨어 계층


응용 프로그램

니가 짠 코드ㅇㅇ. 니가 싼 버그(...)도 포함. (주:D3D나 OpenGL API 를 호출하는 코드가 포함된 프로그램을 얘기한다고 보면 될듯)


API 런타임

API 런타임은 다음과 같은 요청들을 니가 짠 코드로부터 받게 된다.
  • 리소스 생성
  • 스테이트 설정
  • 드로우콜 등
API 런타임은 넘겨받은 요청에 다음과 같은 처리를 한 뒤 작업들을 묶어서(batch) 유저 모드 드라이버(UMD)에게 넘겨준다. UMD 에 대한 설명은 바로 다음 섹션임.
  • 설정된 스테이트를 유지, 관리
  • 파라미터 유효성이나 그 외 에러나 일관성 검사 수행
  • 유저가 접근 가능한 리소스 관리
  • 셰이더 유효성 검사나 링키지(linkage)처리(D3D 는 이 단계에서 처리하지만, OpenGL 은 드라이버단에서 처리함)
  • 그 외 몇가지 작업들


유저 모드 그래픽 드라이버(user mode graphics driver, UMD)

CPU단의 마법들이 일어나는 곳. 니가 짠 코드가 호출한 API 에서 크래시가 발생할 때 운영체제가 "여기서 디졌어염 뿌우"하고 일러바치는 곳. 엔비디아는 nvd3dum.dll, AMD는 atiumd*.dll 뭐 그런 거.

유저모드라는 이름에서 알 수 있듯 응용 프로그램 코드와 같은 컨텍스트, 주소공간에서 돌아가고 권한 상승도 없다.

D3D가 호출하는 저수준 API(DDI)를 구현한 곳. DDI는 D3D API 와 유사한 형태인데, 메모리 관리와 같은 부분이 좀 더 명시적으로 처리되는 등의 차이가 있음.

(주 : 이 부분 부터는 순서도 좀 다르게 편집했고 의역도 엄청 들어감미다..)

셰이더 컴파일 과정의 일부가 처리되는 곳. 물론 셰이더는 HLSL컴파일러나 D3D API 런타임에서 처리되는 부분도 있기는 한데, 이러한 과정을 거친 것은 shader token stream 이며 UMD는 이를 받아 하드웨어와 밀접한 저수준의 컴파일단계를 처리한다.

좀 더 상세히 설명하자면, UMD 이전 단계에서의 셰이더 코드는 다음과 같은 검사를 거치게 된다.
  • 문법적으로 올바른지
  • D3D 제한사항들을 올바르게 지키고 있는지. 제한사항은 다음과 같다.
    • 올바른 타입을 사용하였는가
    • 텍스처/샘플 갯수 제한을 넘지 않았는가
    • 상수 버퍼 갯수 제한을 넘지 않았는가 등.
또한 여러 고수준 최적화가 적용된다. 이는 다음과 같다.
  • 다양한 루프 최적화
  • 사용하지 않는 코드 제거
  • constant propagation (주 : 어떤 수식이 컴파일타임에 상수로 계산될 수 있으면 그 식을 상수로 치환하는 것)
  • 분기 예측 등
UMD는 이러한 비싼 최적화들이 적용된 결과물을 넘겨받기 때문에 그만큼 부담을 덜게 된다.

UMD에서 수행되는 여러가지 저수준 최적화도 있는데, 이는 다음과 같다.
  • 레지스터 할당
  • loop unrolling (주 : 루프를 반복된 코드로 확장하는 것) 등
간단히 얘기하자면, 중간코드(Intermediate Representation, IR) 로 변환된 것을 뭔가 조금 더 컴파일해주는 정도라고 할 수 있는데, 이는 (저수준)컴파일러가 뭔가 좋은 결과를 내기 위해 엄청난 일을 할 필요가 없을만큼 D3D 바이트코드가 셰이더 하드웨어에 충분히 가깝기 때문이다. 하지만 그럼에도 불구하고 D3D가 알 필요도 없고 신경쓰지도 않는 하드웨어와 관련된 세부사항들이 있기에, 이 단계는 결코 사소한 단계라고 볼 수 없다. 이러한 세부사항들은 다음과 같다.
  • 하드웨어 자원 제한
  • 스케줄링 제약

그리고 유명한 게임들에 대해서는 엔비댜와 암드의 프로그래머들이 그 셰이더를 디벼보고 직접 새로 최적화해 작성한 셰이더가 대신 돌아가게 하는 경우가 있는데, 그러한 해당 게임의 검출과 셰이더 교체 또한 이 UMD 단에서 이뤄진다.

그리고 또한 몇몇 API state 들은 최종적으로 셰이더에 컴파일되어 적용된다. 예를 들어 드물게 사용되는 기능인 텍스처 경계 상태(texture border state)는 샘플러 코드에 구현되는 것이 아니라 셰이더에 추가 코드로서 에뮬레이션된다.(혹은 쌩까고 아예 동작하지 않도록 무시되기도 함). 이 말인 즉슨 한 셰이더가 대해 각각의 state 조합들이 적용된 여러 버전의 셰이더들이 존재할 수 있다는 얘기임.

덧붙여 말하자면 이게 바로 어떠한 셰이더나 리소스를 새로이 사용할 때에 지연이 발생하는 이유임ㅇㅇ. 왜냐면 수많은 생성/컴파일 작업이 드라이버에 의해 가능한한 늦춰지거든. 언제까지? 걔가 필요할 때 까지. 그래픽스 프로그래머들은 이 이야기를 다른 방향으로 접해서 알고 있는데, 그건 바로 뭔가가 그저 메모리만 공간만 확보받은 채로 있는 게 아니라 확실하게 생성되었음을 보장받으려면 그 뭔가를 사용하는 더미 드로우콜을 날려주는 것으로 "워밍업"을 하라는 것이다. 지저분하고 번거롭지만 이건 글쓴이가 1999년에 3D 하드웨어를 처음 시작했을 때 부터 주욱 그래왔으니 어쩌겠음? 그냥 익숙해지셈.

UMD는 또한 D3D9 "legacy" 셰이더들과 fixed pipeline 들도 처리한다. 물론 그들은 새로운 버전의 셰이더로 변환되어 사용된다. (그리고 그렇게 처리한지도 좀 되었음)

텍스처 생성 명령 처리 같은 메모리 관리도 UMD 에서 처리된다. 실질적으로는 KMD(kernel mode driver) 로 부터 얻은 커다란 메모리 영역을 다시 작은 영역으로 나눠 할당하는 것이다. 비디오 메모리의 어느 영역을 UMD가 볼 수 있는지, 그리고 시스템 메모리의 어느 영역에 GPU 가 접근할 수 있는지 매핑하는 것은 KMD의 권한이므로 UMD가 처리할 수 없다.

UMD는 텍스처의 swizzling 처리(주:원문 글쓴이의 다른 글로 링크함 shader 에서 쓰는 swizzling 과는 다른 개념이며 텍스처 처리시 캐히 히트율을 높이기 위해 연속된 공간에 배치되는 픽셀의 순서를 바꾸어 저장한 텍스처 데이터를 어드레싱하기 위해 주소(오프셋)값의 비트 순서를 바꾸는 것을 말함..인데 언젠가는 원문도 각잡고 번역하든 요약정리를 하든 할 것임. 일단 그 전에는 원문 링크로...아. 이런 건 각주 시스템이 있으면 각주로 다는 게 좋을 거 같네ㅋ)를 하고 시스템 메모리와 (매핑된) 비디오 메모리간의 전송을 스케줄링하는 것과 같은 일을 처리하기도 한다. 물론 swizzling 의 경우 GPU 가 3D 파이프라인이 아닌 2D blitting 유닛을 통해 처리할 수도 있는데, 이렇게 할 수 없는 경우 UMD 에 의해서 CPU단에서 처리되는 것이지만. 가장 중요한 사실은 UMD 가 커맨드 버퍼(혹은 DMA 버퍼. 원문 글쓴이께서 커맨드버퍼와 DMA버퍼라는 단어를 섞어서 쓰시겠답니다ㅋ)의 내용을 작성한다는 것인데, 이 커맨드 버퍼는 KMD가 할당한 것이다. API런타임을 통해 요청한 상태 전환, 그리기 작업 등은 UMD에 의해 하드웨어가 알아먹는 커맨드로 변환된다. 텍스처나 셰이더를 비디오 메모리로 업로드하는 작업 등과 같은 많은 작업들이 이러한 변환에 의해 알아서 호출되게 된다.

일반적으로 드라이버는 UMD로 최대한 많은 처리 요청을 밀어넣으려 할 것인데, 왜냐면 UMD는 유저모드 코드이기 때문이다. 그래서 UMD에 의해 처리되는 작업들은 비싼 커널모드 전환이 필요치 않고, 자유롭게 메모리를 할당할 수 있으며, 작업들을 멀티스레드로 돌릴 수 있고, 등등등.. 왜냐면 UMD는 보통의 DLL로 만들어져 있으니까. (물론 애플리케이션에 의해 바로 로딩되는 것은 아니고 API에 의해 로딩되는 것이긴 하지만.) 이는 드라이버 개발도중 만약 UMD가 크래시하면 이를 사용하는 앱 역시 크래시하지만 시스템은 크래시하지 않고, 시스템이 돌아가는 동안 간단히 교체할 수 있으며(왜냐면 그냥 DLL이니까!), 일반적인 디버거로 디버깅할 수 있는 등의 장점이 있다. 편리하고 효율적이져.


"유저 모드 드라이버" 가 아니라 "유저모드 드라이버들"

UMD는 이미 말했던바와 같이 단지 DLL이다. D3D의 가호를 받고 KMD에 곧바로 연결되어있기는 해도, 여전히 보통의 DLL이다. 그래서 호출한 프로세스와 같은 주소공간에서 돌아간다.

하지만 요즘의 OS들은 멀티태스킹이다. 그리고 사실 그런지 이미 좀 되었지.

계속해서 얘기하고 있는 이 "GPU"라는 물건은 공유 자원이다. 메인 디스플레이를 구동하는 GPU는 단 하나만 존재한다(SLI 나 크로스파이어 구성을 했더라도 말이지). (주 : 이러한 하나 뿐인 GPU를 멀티태스킹 OS에서 공유하여 쓸 수 있게 만드는 것은) 자동으로 되지 않는다. 옛날에는 3D를 사용하는 애플리케이션 하나가 활성화되면 다른 모든 애플리케이션들은 접근을 하지 못하게 하는 식으로 처리했다. 하지만 윈도우 시스템을 렌더링하는 데 GPU를 쓴다면 그렇게 할 수는 없다. 이것이 바로 GPU로의 접근을 중재하여 시분할로 처리해주는 같은 작업들을 행하는 구성요소가 필요한 이유이다.


스케줄러

스케줄러는 시스템 구성요소이다. 하지만 여기서 얘기하는 것은 CPU스케줄러나 IO스케줄러에 대해서가 아니라 그래픽스 스케줄러임을 명심하자. 스케줄러가 행하는 것은 니가 생각하는 바로 그것이다. 3D 파이프라인을 사용하려는 여러 앱들의 접근을 시분할로 중재해주는 것이져. 컨텍스트 스위칭이 발생하면, 최소한 GPU 의 상태 변환은 일어나고(이를 처리하기 위해 커맨드 버퍼에 추가 커맨드가 들어감), 비디오 메모리로 리소스가 올라가거나 내려오는 스와핑이 일어날 수도 있다. 그리고 당연히 어떤 한 시점에서 3D 파이프라인에 커맨드를 주는 것은 단 하나의 프로세스이다.

콘솔 프로그래머들은 PC 3D API 의 상당히 고수준이며 직접 제어할 수 없고 알아서 처리되는(hand-off) 특성에 의해 야기되는 성능 비용에 대해 종종 불만을 표시한다. 하지만 PC 의 3D API 나 드라이버가  해결해야 하는 문제들은 콘솔 게임에 비해 더 복잡하다. 예를 들자면 PC 에서는 모든 스테이트의 변화를 관리하여야 하는데, 이는 다른 앱이 언제 상태를 바꿔버릴지 알 수 없기 때문이다. 또한 PC 에서는 망가진 애플리케이션을 우회하고 이로 인해 발생하는 보이지 않는 성능 문제를 해결해야 한다. 이는 드라이버 개발자 자신을 포함해서 누구에게도 즐겁지 않은 상당히 성가신 관행이지만, 현실 세계에서는 비즈니스 관점이라는 게 있으니까여. 사람들은 잘 돌아가고 있는 것은 계속해서 (아주 부드럽게) 잘 돌아가기를 바라져 넵. 애플리케이션에다 대고 "그건 틀렸어 임마!"라고 외치고는 부루퉁한 표정으로 엄청 느린 길을 택해서 간다면 친구를 아무도 사귈 수 없을겁니다.


커널 모드 드라이버(kernel-mode drver, KMD)

KMD 는 하드웨어를 다루는 부분이다. UMD는 동시에 여러 인스턴스가 실행중일 수 있지만, KMD는 항상 오직 하나만 실행될 수 있다. 그리고 만약 그게 크래쉬된다면 잦되는거져ㅋ. 예전에는 블루스크린이었지만, 요즘 윈도는 크래쉬된 드라이버를 죽였다 다시 살리는 게 가능하다. 물론 이건 최소한 커널 메모리 손상이 아닌 단순한 크래시일때만 가능하다. 그렇지 않다면 끝장인거임ㅋ

커널 모드 드라이버는 모든 '단 하나 뿐인 것들'을 다룬다. 여러 앱이 GPU 메모리를 두고 다투지만, 실제 GPU 메모리는 하나밖에 없다. 누군가는 명령을 내리고 실제로 메모리를 할당하며 이를 매핑해야한다. 이와 유사하게 누군가는 시스템이 시작될 때 GPU를 초기화하거나, 디스플레이 모드를 설정하거나 디스플레이로부터 모드 정보를 얻어온다던가 하드웨어 마우스 커서를 관리하는 일, 하드웨어 타이머를 설정하여 GPU가 일정 시간 응답이 없으면 리셋을 하는 일, 인터럽트에 응답하는 일 등을 해야 한다. 이것이 바로 KMD가 하는 일이다.

또한 컨텐츠 보호 / DRM과 관련하여 비디오 플레이어와 GPU간에 보호/DRM 된 패스를 설정하여, 지저분한 유저 모드 코드들 통해 디코딩된 소중한 픽셀들이 드러나는 일이 없도록 해서, 이를 디스크에 덤프한다던가 하는 등의 끔찍한 일이 일어나지 않도록 해야 한다. KMD 는 이에 일부 관여한다.

가장 중요한 사실은 KMD가 관리하는 커맨드 버퍼가 하드웨어가 실제로 처리하는 바로 그 커맨드 버퍼라는 것이다. UMD 가 생성하는 커맨드 버퍼는 실제 커맨드 버퍼가 아니다. GPU 가 접근 가능한 랜덤한 메모리 조각에 불과하다. 실제로는 다음과 같은 일이 일어난다. UMD 가 커맨드 버퍼 작업을 마치면 이를 스케줄러에 접수시킨다. 스케줄러는 프로세스가 깨어날 때 까지 기다렸다가 UMD 커맨드버퍼를 KMD 커맨드버퍼에 넘겨준다. 그러면 KMD는 (UMD) 커맨드 버퍼를 호출하는 명령을 주 커맨드 버퍼에 써넣는데, 만약 GPU 가 메인 메모리를 읽을 수 없을 경우 (UMD) 커맨드 버퍼의 내용을 비디오 메모리로 먼저 DMA 한 다음에 이 작업을 수행한다. 메인 커맨드 버퍼는 보통 (아주 작은) 링 버퍼로, 거기에 실제 쓰여지는 것은 시스템/초기화 명령과 실제 3D 커맨드 버퍼를 호출하는 명령밖에 없다.

어쨌든 이 커맨드 버퍼는 메모리상에 존재하는 버퍼일 뿐이다. 그래픽 카드에게 이 버퍼의 위치는 보통 읽기 포인터와 쓰기 포인터를 통해 알려진다. 읽기 포인터는 주 커맨드 버퍼의 어디를 GPU 처리하고 있는지를 표시하며, 쓰기 포인터는 KMD 가 명령을 써 놓은 위치가 어디까지인지를(좀 더 정확히 얘기하자면 KMD 가 GPU 에게 어디까지 명령을 작성했는지 알려줬는지를) 나타낸다. 이들은 하드웨어 레지스터로, 메모리에 매핑되어 있으며 KMD 가 이를 주기적으로 업데이트한다(보통 KMD 가 새로운 작업 덩어리를 접수시킬 때 업데이트한다).


버스(bus)

KMD 가 이렇게 써 놨다고 해서 이게 바로 그래픽 카드로 가는 것은 아니다(그래픽 카드가 CPU 다이에 통합되어 있지 않은 다음에야). 먼저 버스를 통과해야 한다. 요즈음은 PCI 익스프레이스이다. DMA 전송 등도 같은 경로를 거친다. 이는 오래 걸리는 것은 아니지만, 어쨌든 이 여행의 또다른 단계이다. 그리고 마침내...


커맨드 프로세서(command processor)!

커맨드 프로세서는 GPU 의 프론트엔드이다. KMD 가 작성한 커맨드들을 실제로 읽어들이는 것이 이곳이다. 이미 글이 충분히 길어졌기에, 나머지는 다음 포스팅에서 계속.


OpenGL 의 몇가지 차이점들

OpenGL 은 이 글에서 설명한 내용과 매우 비슷하다. 하지만 API 와 UMD 가 딱 잘라 나뉘어지지 않는다. 그리고 D3D 와는 달리, (GLSL)셰이더 컴파일은 API가 아니라 드라이버단에서 전부 처리된다. 따라서 3D 하드웨어 제조사들마다 하나씩 GLSL 프론트엔드를 구현하게 되는데, 이는 기본적으로는 같은 스펙을 구현한 것이지만 각자의 버그나 특이성을 갖게 되는 부작용이 발생하게 된다. 노잼이져. 그리고 이는 드라이버가 셰이더를 만날 때 마다 최적화 - 그 '비싼' 최적화들을 포함해서 - 를 처리해 한다는 것을 의미한다. D3D 의 바이트코드는 이런 문제에 대한 매우 깔끔한 해결책이다. 컴파일러가 하나밖에 없으니까. (따라서 제조사간에 미묘하게 달라서 호환되지 않는 방언들이 존재하는 문제도 발생하지 않게 된다.) 그리고 이는 니가 보통 수행하는 비싼 데이터 흐름 분석을 가능하게 한다. <- 원문은 'and it allows for some costlier data-flow analysis than you would normally do' 인데 costlier data-flow analysis 가 뭘 의미하는지 감이 안 잡히네? 뭔가 특정한 걸 의미하는 거 같은데...


생략되고 간소화된 부분들

이 글은 개요이기에 엄청나게 많은 미묘한 사항들을 적당히 얼버무리고 넘어갔다. 예를 들자면 스케줄러의 경우 딱 하나만 있는 것이 아니고 여러 가지 구현이 존재할 수 있다(드라이버가 이를 선택할 수 있음). CPU 와 GPU 간의 동기화에 대해서도 아직까지 전혀 설명하지 않았으며, 기타 등등 여러가지가 있다. 그리고 내가 무언가 중요한 것을 완전히 까먹고 있을 수도 있는데, 이러한 것은 고칠 수 있도록 알려 달라. 하지만 지금은 안녕. 담에보아~
2016/09/13 00:18 2016/09/13 00:18
2013년 즈음이었는지 그보다 더 전이었는지 정확히 기억이 나지 않는데(원래 써놨는데 망할 텍큐 버그 때문에 한 문단을 날리면서...) 어쨌든 A trip through the Graphics Pipeline 2011 를 발견하고 읽으려고 시도했으나 몇 번 시도 끝에 엄청 더디게 진도는 나갔으나 곧 포기.. 또 읽다가 포기. 그리고 몇 년을 묵혀놨었는데...

마침 DirectX9 이후로 팽개쳐둔 그래픽스 API들에 대한 공부도 좀 해둬야겠단 생각이 든 차에 이 글이 다시 생각나서, 이번엔 좀 더 active 하게 읽기 위해 요약을 하면서 기록으로 남기고자 이 글을 시작.

...했으나 쓰다보니 요약이 아니라 전문번역에 가까워졌네-_-

어쨌거나 일단 이 글은 index 파트에 대한 요약임.

(이하는 A trip through the Graphics Pipeline 2011: Index 를 요약한 내용임다)

GPU에 실제로 구현된 D3D/OpenGL 그래픽스 파이프라인에 관한 글임.

GPU에 관해 넓은 범위에서의 개요를 다루거나 각 컴포넌트를 자세히 다루는 논문은 많지만 그 중간을 다루는 것이 없는 게 계속 신경쓰였다. (주:그래서 필자가 저 블로그 포스팅 시리즈들을 썼겠져.)

적어도 D3D9 이상, OpenGL 2.0 이상의 3D API 에 대한 지식을 가지고 있는 프로그래머를 대상으로 함.

초보용 아님.

레지스터라던가, FIFO 라던가, 캐시, 파이프라인이 뭐고 어떻게 동작하지와 같은 하드웨어에 대한 최소한의 지식도 필요함ㅇㅇ.

병렬 프로그래밍 메커니즘에 대한 최소한의 지식. 왜냐면 GPU 자체가 엄청 병렬 처리를 하는 컴퓨터니까.

(원래 여기에 한 문단이 더 있는데, 처음엔 요약으로 시작해서 빼버려도 될 거 같아서 생략했다가 Part 1을 진행하면서 번역 비슷하게 되어버려 좀 애매해졌는데.. 추후에 봐서 번역해 추가하던지 하겠슴ㅋ)

Part 2 - GPU memory architecture and the Command Processor.
Part 3 - 3D pipeline overview, vertex processing.
Part 4 - Texture samplers.
Part 5 - Primitive Assembly, Clip/Cull, Projection, and Viewport transform.
Part 6 - (Triangle) rasterization and setup.
Part 7 - Z/Stencil processing, 3 different ways.
Part 8 - Pixel processing – “fork phase”.
Part 9 - Pixel processing – “join phase”.
Part 10 - Geometry Shaders.
Part 11 - Stream-Out.
Part 12 - Tessellation.
Part 13 - Compute Shaders
2016/08/25 16:09 2016/08/25 16:09
작년 이맘때쯤엔 한창 신규 프로젝트의 세일즈 빌드 작업을 하고 있었다. 작업을 하다 보니 셰이더 코드를 좀 건드릴 일이 있었는데 그때 눈에 밟히는 부분이 있긴 했지만 딱히 파고 들 시간이 없어서 냅두다가 최근에서야 약간 여유가 생겨 다시 파보게 됨.

사실 딱히 복잡하다거나 뭔가 엄청난 신기술이거나 그런 거 아니고, 어찌보면 기본의 기본에 해당하는 부분인데 매번 '일단 돌아가니까 그걸로 ㅇㅋ'로 대충 뭉개고 넘어가던 부분인지라.. 이왕 삽질하면서 알게 된 거 정리라도 해 둬야 나중에 까먹고 또 삽질할때 시간이라도 좀 아끼지 하는 마음으로 써봄.

어쨌거나 문제의 발단이 된 코드는 이건데,

http://wiki.unity3d.com/index.php/MatCap

얼핏 봤을 땐 시선 방향을 범프를 적용해 어찌저찌 틀어서 이걸 이용해 matcap 텍스처의 픽셀을 가져다 쓰는..뭐 그런 코드인 거는 내가 잘 알겠다.

근데 구체적으로 뜯어보려니까 음? 어라라?? 스러운 부분이 한두군데가 아닌 것이었다. 정확히 얘기하자면 한두군데가 이상한 정도가 아니라 아니라 전체를 대충은 알아도 각 코드 한 줄 한 줄이 구체적으로 어떤 의미인지 정확히는 모르고 있었던 것이지라.

여튼 코드에서 뜯어봐야 할 핵심인 부분을 보자면,

vert 함수(버텍스 셰이더 코드)의
TANGENT_SPACE_ROTATION;
o.TtoV0 = mul(rotation, UNITY_MATRIX_IT_MV[0].xyz);
o.TtoV1 = mul(rotation, UNITY_MATRIX_IT_MV[1].xyz);


그리고 frag 함수(픽셀 셰이더 코드)의
vn.x = dot(i.TtoV0, normal);
vn.y = dot(i.TtoV1, normal);

float4 matcapLookup = tex2D(_MatCap, vn*0.5 + 0.5);


각각 세 줄 씩.

일단 버텍스 셰이더 코드를 보자니.. 첨부터 못 보던 매크로 같은 물건이 보인다. TANGENT_SPACE_ROTATION

이건 뭔가 유니티 Shaderlab 에서 제공하는 변수 같은건가 하고 찾아보니까.. 딱히 매뉴얼에 정의가 된 건 안보임(내가 못찾았을수도),

그래서 혹시나 싶어 빌트인 셰이더 소스코드의 UnityCG.cginc 를 까보니 맞네... 거기 정의되어있는 매크로였음.

// Declares 3x3 matrix 'rotation', filled with tangent space basis
#define TANGENT_SPACE_ROTATION \
  float3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz) ) * v.tangent.w; \
  float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal )

보니까 버텍스 셰이더 입력으로 노멀과 탄젠트가 들어오는데, 이걸로 바이노멀을 계산해서 탄젠트 스페이스상의 값으로 회전시켜주는(..것으로 보이는) rotation 이라는 변수명의 3x3 행렬을 만들어주는 매크로.

그리고 바로 다음줄과 그 다음줄에 등장하는 UNITY_MATRIX_IT_MV 라는 역시나 매크로스러워 보이는 이름의 무언가가 있는데.. 처음엔 이름으로 대충 때려잡기로 IT 는 inverse transform 일테고, MV 는 model 이랑 view 행렬을 얘기하는 거겠지..라고 생각.
근데 아니었음. 제대로 찾아보면 나오는데.. "Inverse transpose of model * view matrix" 임ㅋ. 그리고 매크로가 아니라 저거 자체가 변수이기도 하고.

어쨌든 여기까지 해서 모르던 변수나 매크로는 대충 확인이 되었으니.. 이걸 가지고 전체적인 흐름을 다시 한 번 보자면 버텍스 셰이더에서 탄젠트 스페이스로의 변환 행렬과 모델뷰 행렬을 곱해서 픽셀 셰이더로 넘기고, 이걸 픽셀 셰이더에서 노멀맵에서 언팩한 노멀 벡터랑 쿵짝해서 만든 실수값 두 개를 uv좌표로 써서 matcap 텍스처의 픽셀을 가져다 씀..인 것. 뭐 얼핏 봤을때의 그것이랑 같은 얘기. 근데 당장 제일 처음에 버텍스 셰이더에서 탄젠트 스페이스로 보내주는 벡터부터 뭐인지부터 모르겠다는거.

그래서 우선, 어떤 걸 모르고/헷갈리고 있었는지부터 적어보고자 함.

제일 처음이 바로 위에서 얘기한 탄젠트 스페이스로 보내지는..벡터가 뭔지 모르겠는데.. 이건 사실 코드상에서 뭘 곱해주는지가 명시적으로 드러나지 않는 상황. 그래서 나머지 코드들을 해석하고 나서 역으로 끼워맞춰야 하더라. 다 풀고 보니 결과적으로 이렇게 되긴 했는데, 모를 땐 처음부터 막혀서 막 헤매게 된 원인이기도 함.

그 다음이 TANGENT_SPACE_ROTATION 인데..
얘는 오브젝트의 로컬 스페이스에서 한 점의 탄젠트, 바이노멀, 노멀 벡터가 어느 방향인지를 나타내는 값을 가져다 이를 차례대로 행렬의 1, 2, 3행으로 쌓아서 행렬을 만든것.

근데 일단 이게 내가 생각한 것(탄젠트 스페이스로의 변환)이 맞는지 확인해보려고 하니 행렬과 벡터를 곱하는 순서가 어떻게 되는지부터 알아야겠단 생각이 들었음.

지금 회사에서 유니티 쓰기 전까지 일하면서는 주욱 D3D로만 작업을 하다보니 알고있고 익숙한 게 대부분 D3D기준인데, 예를 들자면 D3D 는 행렬에 벡터를 곱할 때 벡터가 먼저 오고 행렬이 뒤에 오는 형태라는 거. 이건 4x4 행렬로 트랜스폼을 한 방에 처리할 때 translation 에 해당하는 성분이 4행의 1, 2, 3열에 오게 된다.

근데 유니티의 셰이더코드는 Cg 기반이고, Cg는 예전에 얼핏 듣기로 행렬과 벡터의 곱셈이 행렬이 먼저 오고 벡터가 나중에 오는 형태라고 알고 있었는데.. 찾아보니 맞게 기억하고 있는거였음. 확인은 위키책의 해당 항목(이쪽이 좀 더 자세한데..위키책 사이트가 종종 터지는듯?) 이나 좀 덜 자세하지만 Cg 의 mul 함수 설명에서.

따라서 유니티 셰이더 코드의 4x4 행렬에서는 translation 에 해당하는 성분은 4열의 1, 2, 3행에 위치함. 사실 이 글에서 translation 위치 자체가 중요한 건 아닌데.. 내가 4x4 트랜스폼 행렬이 어느 방향을 향하는지 기억하는 기준이 translation 행 혹은 열의 위치인지라.

어쨌거나 다시 TANGENT_SPACE_ROTATION 로 돌아가서 보면, 매크로 코드에 의해 이 3x3 행렬의 1행은 탄젠트 벡터, 2행은 바이노멀 벡터, 3행이 노멀 벡터인데,

회전변환행렬을 구성할 때 회전된 결과인 세 축을 알면 그 축을 각각 회전행렬의 1, 2, 3행으로 쓸 수도 있다.. 정도의 아련한(...)기억이 있는지라, 일단 이게 맞는지 확인부터 해야겠다는 생각이 들었음. 근데 검색을 해도 찾기가 어려워서.. 그럼 이 행렬에다 x, y, z축의 방향벡터를 의미하는 (1, 0, 0), (0, 1, 0), (0, 0, 1) 을 곱해준 결과가 뭔지를 확인해보자로 넘어가게 됨.

즉, x축의 방향벡터인 (1, 0, 0)을 TANGENT_SPACE_ROTATION 의 결과물인 rotation 변수에 곱해주면 탄젠트 방향벡터가 나오겠지... 했는데 아니네? 탄젠트 벡터는 rotation 행렬의 1행인데 rotation 에 (1, 0, 0)을 곱해주면 1열 성분의 벡터가 나오자네. 즉 rotation 행렬을 transpose 한 것에 (1, 0, 0)을 곱해줘야 탄젠트벡터가 나오게 되니, 여기서 나는 rotation 변수를 계산할 때 단순히 1, 2, 3행에 탄젠트, 바이노멀, 노멀을 넣고 땡이 아니라 이걸 한 번 transpose 해 줘야 하는 코드에선 이걸 빼먹고 잘못 쓰고 있는 게 아닌가.. 라고 엉뚱한 생각을 했다-_-. 혹은 코드가 잘못되지 않았다면rotation 변수의 의미가 탄젠트 스페이스로의 회전이 아니라 탄젠트 스페이스에서 오브젝트의 로컬 스페이스로의 회전이 아닌가..라고 의심을 하게 됨(역시나 틀린 생각임). 이렇게 첫 부분도 그 다음도 계속 헷갈리고 있으니 나머지 부분도 엄청 삽질하게 될 것은 불을 보듯 뻔하고..

일단 그렇게 아리송다리송@_@한 상태에서.. 나머지 부분은 어케 문맥이랑 끼워맞춰보면 될라나 하는 심정으로 그 다음줄로 넘어가니...
어?
행렬이랑 행렬을 곱하는 게 나올 줄 알았는데 그게 아니라 행렬이랑 행렬의 1, 2 행 성분을 각각 곱해서 그걸 픽셀 셰이더로 넘겨주네?

이걸 보고서 노멀 언패킹때처럼 서로 직교하는 세 축의 경우 두 축만 값을 남기고 나머지 하나는 계산으로 만들어 쓰는건가? 아니면 행렬끼리 계산한 결과와 마지막에 곱해주는 벡터에 x, y 성분만 사용하는건가 하는 등의 생각이 들었는데.. 일단은 여기도 어떤 것인지 확실하게 정하지 못한 상태로 또 넘어감(...제대로 하는 게 뭐냐ㅋ)


뭔지는 몰라도 어쨌든 행렬과 행렬을 곱하는거 같긴 한데.. 그렇다면 앞의 행렬(rotation 변수)과 뒤의 행렬(UNITY_MATRIX_IT_MV 변수) 의 을 곱해줘야 할텐데.. 왜 곱해주는 게 열이 아니라 행(UNITY_MATRIX_IT_MV[0] 과 UNITY_MATRIX_IT_MV[1])일까-_-. 여기서 혹시나, 정말 혹시나 행렬에 대해 [] 연산자를 쓸 경우 행이 아니라 열에 접근하는건가 하고 다시 아까 Cg 레퍼런스를 확인해봤는데 아님. [] 연산자로 접근했을 때 나오는 벡터는 행 하나가 맞음ㅋ.

그렇다면 저 식의 의미는 UNITY_MATRIX_IT_MV 가 아니라 UNITY_MATRIX_IT_MV 의 transpose 행렬을 곱해주는 것임. 그런데 여기서 UNITY_MATRIX_IT_MV를 직교행렬이라고 착각하고, 이것의 transpose 이니 역행렬이구나..라고 또 착각. 저 식이 UNITY_MATRIX_IT_MV 의 역행렬을 곱해주는 게 되려면 UNITY_MATRIX_IT_MV 가 직교행렬이어야 하는데, 정답편에서 얘기하지만 얘는 직교행렬이 아니라 역행렬이 아니라 그냥 transpose 인거. 왜 이런 식을 쓰는지 정확한 의미는 요 아래에서. 어쨌든 식에서 저 부분을 view space 서 model space 로 변환시켜주는 행렬, 다른 말로는 카메라 공간에서 모델의 로컬 공간으로 변환시켜주는 행렬이 된다고 생각했는데... 이 부분은 대략적으로는 맞긴 하지만 역시나 100% 정확한 표현은 아니고. 뭐 어쨌든 이런 혼돈의 카오스 상태에서 픽셀 셰이더 코드로 넘어갔음.

픽셀 셰이더 코드 조각에서는 첫 번째 줄과 두 번째 줄에서 내적을 해 주는 것을.. 버텍스 셰이더에서 픽셀 셰이더까지 이어지는 변환의 일부(마지막) 즉, 버텍스 셰이더에서 넘겨받은 행렬에 해당하는 것에 픽셀 셰이더가 노멀맵에서 뽑아낸 normal 벡터를 곱해서 변환을 하는 것으로 다시 한 번 거하게 착각을 해 버림. 근데 그거 아니구여... 어쨌거나 착각을 하고 있으니 결과가 어떤 의미인지도 감이 안 잡히고 어디서부터 손을 대야할지도 모르겠다 싶은 상황인거. 일단 이렇게 한 이틀 헤매다(...) 3일째에 갑자기 실마리가 떠올랐다. 이제 정답편으로 넘어가보겠심다.

정답편

일단 첫빠따는 TANGENT_SPACE_ROTATION.
얘의 진정한 의미는 모델의 로컬 좌표상의 벡터값을 곱하면 탄젠트 스페이스상의 값이 튀어나오는 행렬, 즉, 모델 공간에서 탄젠트 공간으로 변환시켜주는 행렬임. 변수 이름과 설명에 rotation 이라고 되어 있어서 '회전을 시킨다' 라고 생각을 하게 되어 내 경우엔 더 헷갈렸는데, 사실 이건 방향을 나타내는 벡터가 있는데 이것의 값을 구하기 위해 사용하는 기준 좌표축을 바꿔줌에 따라 값이 변하는 것을 계산하기 위한 행렬임. 자세한 내용은 선형대수에서 기저변환(change of basis) 부분을 참조..해야 할 거임. 근데 지금 참고하는 책이랑 자료에서 학술적으루다가 정확하고 이쁘게 설명해 놓은 게 없음. 그래서 그냥 무식하게 확인한 방법을 설명해보겠심다.

TANGENT_SPACE_ROTATION 의 결과로 나온 rotation 변수에 x축의 방향벡터 (1, 0, 0)을 곱해주는 상황을 다시 한 번 생각해보면, 그 결과 벡터는... 음. 당장은 뭔지 모르겠네-_-? 탄젠트, 바이노멀, 노멀(TBN) 벡터의 각 x 성분으로 만든 벡터인데.. 이것만 봐선 뭔 의민지...

뭔가 알 수 있는게 나오려면 결과가 TBN 벡터 셋 중에 하나여야 하는데, 그러려면 rotation 이 아니라 rotation 의 transpose 에다가 (1, 0, 0) 을 곱해야겠져? 그래야 rotation 의 1행 성분인 탄젠트 벡터가 나오니께.

근데 이 탄젠트 벡터는 어느 공간상의 값이다? 넹. 모델 공간상의 값임다. 왜냐면 이걸 계산하는 데 쓰인 버텍스의 값이 모델 공간상의 값이니까여. 물론 이건 버텍스 셰이더에 넘어오기 이전에 이미 모델 데이터 레벨에서 정해지는 값이라.. 그러니까 애초에 모델링 툴에서 익스포트하거나 익스포트한 걸 엔진으로 임포트 할 때 정해지는(계산되는) 값이긴 한데.. 음.. 더 자세히 설명하면 여기서 너무 늘어질 수 있으니 일단 여기서는 그냥 모델공간상의 값입니다.. 정도로 넘어가는 걸로ㅋ

어쨌거나 x, y, z좌표축에 해당하는 (1, 0, 0), (0, 1, 0), (0, 0, 1)을 rotation 의 transpose 에다 각각 곱해주면 그 결과값은 모델공간에서의 TBN 벡터에 해당하는 방향값을 뱉아냄미당.

그런데 rotation 은 직교행렬(=행렬을 구성하는 서로 다른 행벡터끼리 내적하면 0. 왜냐면 탄젠트, 바이노멀, 노멀은 서로가 서로에게 수직인 벡터)이니까, rotation 의 transpose 는 rotation 의 역행렬이져.

바로 위 두 문장을 조합하면, rotation 의 역행렬은 좌표축 벡터를 모델공간상의 TBN(탄젠트, 바이노멀, 노멀) 벡터로 회전을 시켜주는 행렬이네여. 그럼 rotaion 은 뭐다? 모델공간상의 TBN 벡터를 좌표축 벡터가 되도록 회전시켜주겠져. 즉, 모델 공간에서 TBN벡터에 해당하는 벡터들을 rotation 행렬에 의해 변환시키면 결과값은 좌표축 벡터가 된다는 말임다. 탄젠트 바이노멀 노멀이 좌표축이 되는 공간이 뭐다? 뭐긴뭐야 탄젠트 공간이지.

즉, rotation 을 모델공간상의 값에다 곱해주면 결과값은 탄젠트 공간상의 값이 된다는 거. (워낙 말장난같긴 한데.. 좌표축 변환이라는 게 기준이 되는 축을 바꾸고 그 결과에 의해 값의 의미가 달라지는거라.. 아.. 나도 설명 좀 더 잘하고 싶다....)

한참을 장황하게 설명했는데, 어쨌든 결론은 TANGENT_SPACE_ROTATION 은 모델 공간의 값을 탄젠트 공간의 값으로 변화시켜주는 행렬을 계산해 이걸 rotation 이라는 이름의 변수에 넣어주는 매크로 라는 게 확실해졌슴다. 그리고 아직 끝나지 않았져. 결론은 결론인데 코드 첫 줄에 대한 결론이잖아. 나머지도 해석해야지ㅋ.

그 다음으로 UNITY_MATRIX_IT_MV 의 진정한 의미를 확인해보도록 하겠슴다.
유니티 문서에 따르면 UNITY_MATRIX_MV 는 Inverse transpose of model * view matrix 임다(아까도 한 얘기지만). 근데 말로 써놔서 좀 헷갈리긴 하는데, 저 말의 정확한 의미는 model * view matrix 를 inverse 한 것을 다시 transpose 한 거란 얘기져. transpose 한 것을 inverse 한 게 아님. (중요)

아우.. 뭘 inverse 랑 transpose 랑 구분을 하고 앉았냐. 둘이 같은거니 어느 게 먼저 와도 상관없이 그냥 상쇄되는거 아녀? 라고 생각하신다면.. 아녀. 그거 틀렸슴미다. model * view matrix 가 일단 직교행렬이 아닐 수 있으니까요. 언제 직교행렬이 아니냐? 며는.. 일단 translation 성분 값이 (0, 0, 0) 이 아닐 때도 그렇고, 스케일이 non-uniform 해도 그렇고.

근데 어차피 이 코드에선 4x4 행렬 중에 좌상단 3x3 값만 써서 노멀 회전시키는거니까, 그렇게되면 translation 성분은 사라지니 노멀이 좀 틀어져도 트랜스폼된 결과값만 노멀라이즈 해서 적당히 쓰면 상관없지 않음? 이라는 시도를 해 볼 수도 있겠지만.. 뭐 다른 수가 없다면 그렇게라도 할텐데 수학적으로 정확한 결과를 낼 수 있는 방법이 엄연히 있져. 바로 그게 inverse transpose of MV 라는 해괴한(...) 물건임. 구체적인 사항은 이 게시물에서 언급하는 이 게시물에서, 혹은 Eric Lengyel 저 3D 게임 프로그래밍&컴퓨터 그래픽을 위한 수학3.5장 법선벡터의 변환에서 확인 가능함다. (2판 한글판 기준으로 119쪽 부근). 그냥 간단히는 non-uniform scale 이 적용된 트랜스폼일때에도 노멀을 제대로 변환하는 용도로 사용..정도로만 정리해둡시다. 어쨌든 UNITY_MATRIX_IT_MV 에 의한 변환은 모델 공간의 노멀을 뷰 공간(카메라 공간)의 노멀로 바꾸는 용도임.

그럼 UNITY_MATRIX_IT_MV 의 1열과 2열이 아니라 1행과 2행을 가져다 rotation 에 곱하는 의미는 무엇인가... 우선 버텍스 셰이더 코드 두 번째 줄 우변의 코드를 식으로 옮겨 써 보겠슴다.

rotation x UNITY_MATRIX_IT_MV[0]

그리고 UNITY_MATRIX_IT_MV 를 네 문단 앞에서 얘기한대로  transpose(inverse(model x view)) 로 풀어서 치환해보져.

  rotation x UNITY_MATRIX_IT_MV[0]

= rotation x (transpose(inverse(model x view)))[0]

연산자를 보통은 이렇게 쓰지 않지만, 편의상 행렬의 열벡터를 가져오는 연산을 {} 이라 정의하면, 3x3행렬 M 에 대해서 다음과 같이 쓸 수 있겠져.

M[0] = (transpose(M)){0}

(transpose(M))[0] = M{0}

마지막 하늘색 박스에서 transpose(inverse(model x view)) 부분의 계산 결과를 하나의 행렬로 보고 바로 위 변환(두번째 줄)을 적용하면 다음과 같아집니다.

  rotation x (transpose(inverse(model x view)))[0]

= rotation x (inverse(model x view)){0}

그런데 행렬의 첫번째 열을 구하는 연산 M{0} 은 행렬 M 에 벡터 (1, 0, 0) 을 곱해주는 것과 같습니다. 따라서 다음과 같이 쓸 수 있게 되져.

  rotation x (inverse(model x view)){0}

= rotation x inverse(model x view)) x (1, 0, 0)

즉, 버텍스 셰이더 두 번째 줄 코드의 내용은 rotation x inverse(model x view) x (1, 0, 0) 이라는 식과 같다고 할 수 있겠네여. 이 식을 풀어 써 보면 벡터 (1, 0, 0) 를 model x view 행렬의 역행렬에 곱하고, 이걸 다시 rotation 에 곱해주는 식 되겠슴다.

model x view 행렬은 모델 로컬 공간에서 뷰(카메라) 공간으로 변환시켜주는 행렬인데.. 이것의 역행렬이니까 카메라 공간에서 모델 공간으로 변환시켜주는 행렬이져? 따라서 여기에 곱해주는 벡터 (1, 0, 0) 은 카메라 공간상의 벡터로 봐야 함다. 카메라 공간상에서 벡터 (1, 0, 0) 는? 화면상의 가로축이네여. 즉 식의 뒷부분은 화면상의 가로축을 모델 공간의 값으로 변환합니다.

rotation 은 저으기 위에서 얘기한대로 모델 공간에서 탄젠트 공간으로 변환시켜주는 행렬입니다. 바로 위에서 화면상의 가로축을 모델 공간상의 값으로 변환했는데, 여기에 rotation 을 곱해주면 다시 이 모델 공간상의 값이 탄젠트 공간의 값으로 바뀌는거죠. 결과적으로 이 식은 화면상의 가로축을 탄젠트 공간의 값으로 변환하게 됩니다.

같은 방식으로, 버텍스 셰이더의 세 번째 줄은 화면상의 세로축을 탄젠트 공간의 값으로 변환하게 되죠. 이렇게 구한 두 벡터는 버텍스 셰이더 출력값에 저장되어 픽셀 셰이더로 넘겨지게 됩니다.

픽셀 셰이더 코드의 해석은.. 일단 픽셀 셰이더가 넘겨받은 두 값 i.TtoV0 와 i.TtoV1 부터 봅시다. 얘네들은 바로 위 버텍스 셰이더에서 넘겨준 카메라공간의 가로와 세로축 방향을 나타내는 벡터입니다. 카메라 공간의 가로 세로 이러니 장황한데 좀 더 간단히 얘기하자면 화면의 가로와 세로 방향을 나타내는 벡터라는거져. 근데 이 값은 어느 공간상의 값이다? 넵. 탄젠트 공간.

이 탄젠트 공간상의 값과 내적을 해 주는 normal 벡터는 어디서 온 값이냐.. 이건 가져온 코드에서는 안 보이지만 노멀맵에서 언팩한 값이져(frag 함수 몸체 첫 줄의 UnpackNormal 호출 결과로 받는 값). 얘는 애초에 탄젠트 공간의 값임다. 왜 탄젠트 공간이냐면 탄젠트 공간이니까 탄젠트 공간이라 한 것인데 왜냐고 물으시면.. 이 아니라 이 부분에 대해서 풀어 쓰려니 분량이 너무 늘어날 거 같아 일단 UnpackNormal 의 결과값은 탄젠트 공간이다..라고만 해 두고 넘어가져. 탄젠트 공간상의 무슨 값? 노멀맵에서 가져온거니 노멀값이겠져? 정확히는 픽셀 셰이더에 의해서 그려지고 있는 해당 점의 노멀 방향값이져.

결국 이건 화면의 가로와 세로 방향 벡터랑 해당 점의 노멀 방향 벡터를 내적하는 게 되는데, 둘 다 탄젠트 공간의 값입니다. 따라서 애초에 착각했던것처럼 뭔가 변환은 아니게 되는거져. (굳이 결과에 끼워 맞추자면 텍스처의 uv 공간으로 변환하는거라고 할 수 있을지도 모르겠지만 이러면 더 헷갈리니까 이건 제쳐두고.)

두 벡터의 노멀을 어떤 의미로 봐야하는지를 알기 위해 두 벡터의 기하학적 정의를 한 번 살펴보도록 하져.


'두 벡터의 내적'의 기하학적 정의는 두 벡터의 각각의 길이 값을 곱한 것에 두 벡터 사이각의 cosine 값을 곱한 값..입니다. 이 정의에 따라 픽셀 셰이더의 식을 해석해 보면, 버텍스 셰이더 코드의 첫째 줄은 화면의 가로방향 벡터와 노멀맵에서 가져온 노멀 벡터를 내적한 값을 계산하는 거져.

그런데 화면의 가로 방향 벡터는 식 사이에 숨어 있는 값 (1, 0, 0) 로 등장한 이후 픽셀셰이더로 넘어오기 전까지 계속해서 회전 변환만 적용된 상태입니다. 따라서 최초에 1이었던 길이는 픽셀 셰이더에서 내적 할 때 까지도 여전히 유지되고 있져. 방향 벡터는 보통 길이 1을 유지하도록 하는 경우가 많은데, 특히나 내적을 계산할 때 두 벡터 중 하나가 가 길이 1인 방향 벡터일 경우, 위에서 언급한 벡터 내적의 기하학적 정의에 대입해보면 나머지 한 벡터의 길이값에 두 벡터 사이각의 cosine 값을 곱한 것과 같은 값이 됩니다. 길이가 1인 벡터를 어떤 기준이 되는 벡터라고 보면, 이 기준 벡터와 다른 벡터를 내적한 값은 기준 벡터를 축으로 두었을 때 다른 벡터의 해당 축에 대한 좌표 성분값을 얻는 연산으로 볼 수 있는 것이져. 이런 관점에서 보면, 픽셀 셰이더의 첫째 줄과 둘째 줄 코드는 노멀 벡터를 화면상의 가로축과 세로축을 기준 축으로 하는 좌표계의 좌표값으로 변환하는 코드라 할 수 있슴다. 같은 말이긴 한데 좀 현실세계스러운 비유를 들어 설명하자면, 화면의 가로축과 세로축을 자(ruler)라고 생각하고, 노멀 벡터의 끝이 가르키는 점의 위치가 가로축과 세로축 방향으로 얼마나 되는지를 재어서 그 값을 vn.x 와 vn.y 에 저장하는 것이 픽셀셰이더 첫번째와 두번째 줄이 하는 일인 것이져.

그런데 노멀 벡터 역시 방향벡터로, 길이는 1을 유지하고 있습니다. 혹은 연산 도중 1이 아닌 값이 되게 되더라도 마지막이나 사용할 때에 1이 되도록 조정해주져. 방향 벡터이니 시작점을 원점에 두고, 길이가 1인 모든 벡터의 끝점에 점을 찍어보면... 넵. 아시다시피 이 점들의 집합은 원점을 중심으로 하는 반지름 1인 구면을 이루게 되져. 노멀 벡터들은 이 구면상의 값이라면 어떤 값이든 될 수 있는데, 픽셀 셰이더의 첫째 줄과 둘째 줄에 의한 연산을 하고 나온 결과값 (vn.x, vn.y)은 구면상의 각 점의 가로 세로 좌표값이니 이 값들을 2D 평면에 찍어보면 원점을 중심으로 반지름 1인 원을 가득 채우게 되져.

픽셀셰이더 두번째 줄 까지 계산한 결과를 좌표로 왜 2D평면상에 다시 점을 찍어보느냐는.. 이런 가정을 해 보져. 점을 찍을 평면에 그림이 그려져있다고 말이져. 그리고 해당 좌표에 점을 찍는 대신 그 점을 찍을 위치의 색깔을 가져다 다른 데 쓴다면? 넵. 그게 바로 픽셀셰이더 세번째 줄에서 tex2D 함수가 하는 일임돠.

그런데 tex2D 함수를 자세히 보면 파라미터로 넘겨주는 vx 를 바로 사용하지 않고 가공을 해 주고 있져? 이건 vx 의 x 와 y 값이 -1 에서 1 사이의 값이기 때문이져. 원점을 중심으로 반지름 1 짜리 원 내부의 점들이니까여. -1 에서 1 사이의 값에 0.5 를 곱해주면 -0.5 에서 0.5 사이의 값이 되고, 여기에 다시 0.5 를 더해주면 0 에서 1 사이의 값이 되져. 이렇게 하는 이유는 바로 텍스처 좌표가 0 에서 1 사이의 값을 가져야 하기 때문인 것이라는 거.

이렇게 구한 값을 픽셀셰이더의 출력값으로 사용하게 됩니다. 픽셀 셰이더라는 게 한 점을 그릴 때 그 점의 색깔을 무슨 값으로 하느냐를 계산하는 물건이져. 이 MatCap 셰이더의 경우는 해당 점의 노멀 방향벡터를 화면의 가로세로축이 만드는 평면상으로 옮겨서, 그 평면상에 붙어(...)있는 _MatCap 텍스처의 그 위치의 색깔로 화면에 그려주는 셰이더가 되는 겁니다. 그림을 그리면 좀 이해가 편할텐데 귀찮아서(...) 말로 설명을 하자면 노멀이 화면의 가로세로축과 완전히 수직한 경우, 즉 화면을 뚫고 나오거나 뚫고 들어가거나 하는 경우는 화면의 가로세로축에 의해 만들어지는 평면의 중점, 다시 말해 _MatCap 텍스처의 한 가운데 점의 색깔로 그려집니다. 그걸 기준으로 좌우로 기울어지든 상하로 기울어지든, 중점으로부터 좌/우측 혹은 아래/위쪽 위치에서 픽셀을 얻어 그 색깔로 그려지게 되는 것이져. 그리고 이쯤 되면 눈치 챌 수 있겠지만, _MatCap 텍스처에서 사용되지 않는 부분이 있는데 그건 바로 텍스처의 사각형 영역에서 이 영영을 꽉 채우는 원의 바깥부분, 즉 네 귀퉁이 인근 부분의 픽셀들이져. 얘네들을 가리키려면 해당 노멀의 길이가 1이 넘어야 하는데, 정상적인 노멀은 항상 길이가 1이므로 애는 절대 참조될 수 없기 때문에 영향을 미치지 않는 텍셀들이 되는거져.

어째 막판으로 갈수록 설명은 상세해지면서 내용은 더 날림이 된 거 같긴 한데, 일단 한 번 끊어야겠기에 여기까지 쓰고 일단락해야겠슴다. 나중에 좀 더 알아먹기 쉽게 고칠 수 있는 방법이 생각나면 그때 고치져 뭐. 끗.
2016/07/31 23:59 2016/07/31 23:59

애자일

일관련 2006/07/16 03:13 posted by 윤뿌쮸
http://agile.egloos.com/2093309

애자일 방법론의 접근 방법은 고리타분하지 않고 일면 파격적이지만 한편으론 본질적 원리에 벗어나지 않는 느낌이라.. 좋아한다.
2006/07/16 03:13 2006/07/16 03:13

요근래 고민하던것 중 하나

일관련 2006/06/09 02:15 posted by 윤뿌쮸
우리는 팀인가요?
2006/06/09 02:15 2006/06/09 02:15

맥스 플러그인 디버깅 그 두번째 시간

일관련 2006/04/26 02:16 posted by 윤뿌쮸
(...사실은 한번에 진행되었던거지만 -ㅂ-)

지난 글에 이은 야그.

대략적인 상황 설명을 세원옹에게 마친 뒤, 디버거를 돌리는 작업으로 돌입.

일단 컴터상엔 작업중인 FaceMarker 와 비교 대상인 Mesh Select 프로젝트를 켜 놓은 비졀스튜됴 외에도, 참고가 될만한 다른 프로젝트 파일들도 수없이 불러놓은 상태. 비졀스튜됴 인스턴스만 한 여섯개는 되었던듯하다-ㅂ-;;

여튼 처음 작업으론, 정상적인 동작을 보이고 있는 Mesh Select 의 코드를 최대한 FaceMarker 와 비슷하게 맞추어, 비정상 동작이 하는 순간을 잡아내자는 것.

..하지만 몇번 코드를 옮겨보아도 잘 동작하고 있는 Mesh Select 이놈을 엉뚱하게 움직이도록 만드는 것은 어렵다고 판단.

그렇다면 이번에는 FaceMarker 의 코드에다 Mesh Select 의 코드를 '복사'해 넣어서, 바뀌는 시점을 캐치하자는 방향으로 접근.

우선은 코드의 배열, MAX system API 호출 순서 등을 맞춰보았으나 역시나 문제의 부분은 오리무중..

그래서 이번엔, 아예 FaceMarker 특유의 기능 부분은 주석 처리하고, Mesh Select 의 코드를 갖다가 복사해 넣는 식으로 작업이 진행되었심..

...허나... 역시나 FaceMarker 는 face select 모드에서 선택된 face 를 출력하지 못하고 있는 상황이 반복되고..

어차피 플러그인이라는게 메인 루틴이 되는 MAX 측에서 필요할때 호출하는 함수들로 이뤄진 놈이니 코드의 전체 흐름을 디버거로 보기는 어려운 상황..

혹시 '이 함수는 아닐거야..'라고 생각해 제쳐두고 있을지 모르는 함수가 있을까 하여 제안된 방안은 'FaceMarker 의 모든 메소드 시작점에 브레이크포인트를 걸어보자!'

.....그렇지만 역시 이 방법도 좌절.. 예상했던 함수 외에 다른 용도로 호출되는 녀석은 없더라는 것.

이에 비장의 카드로 어셈블리 레벨 디버깅을 제안하는 세원옹!
옆에서 덜덜덜 떨면서 그러하시지요라고 대답하는 본인. (...어렵잖아!)

하지만 알지못할 MOV ADD JMP LEA CALL 들 사이를 수십수백번은 헤매야할거라 지레 겁먹었던것과는 달리..

트레이싱을 몇번 거치다 보니.. 어라? FaceMarker 코드를 호출하는 부분이 나오네..?

어랏..근데.. CPP 파일이 아니라 H 파일에 포함된 메소드!!
Modifier::ChannelsChanged() 였던 것이었던 것이었다.

이 메소드의 역할은 modifier 가 변화시키는 데이터 채널을 맥스에게 알려주는 것이다.

순간 '띵~'하면서 머리속을 지나가는 무언가 '...범인은 이녀석인가!'
하지만 이미 수십..까진 아니고 몇번을 그런 느낌을 받고 실패했던터라 아직까진 반신반의 상태..

여하튼 百見이 不如一打라.. 코드를 고치고 컴파일.. 맥스 띄우고 박스 만들고 edit mesh 건 다음 FaceMarker 적용.. face select..

"된다!!!!!!!!!!!!!!!! ;ㅁ;"

...... FaceMarker 의 역할이 mesh 데이터 자체에 '변경'을 가하는 것은 아니기에 이 함수를 들여다 볼 생각은 까맣게 잊고 있었던것.(PART_SELECT 라는 채널이 엄연히 존재하는데도 말이지)

바로 위에 오는 Modifier::ChannelsUsed() 은 작업 중에 손을 본 적도 있었는데.. 이 함수를 건드릴 생각을 못했던건.. 뭔가 씌었거나.. 그랴. 나의 불찰이다 ;ㅁ;

하지만.. face selection 은 일시적인 변경값인데 이것을 mesh data channel 의 하나로서 geometry 정보나 topology 정보와 동급으로 취급된다는건.. 어찌보면 설계할때 개념을 잘못 잡은거라고!! (..라는 식으로 책임회피.. 하지만 소용없다 lllorz )

여하간에.. 바로 위에 모든 메소드에 브레이크포인트를 찍어보자고 할 때에 걸릴번도 했던 이 녀석은..

거주지가 CPP 파일이 아니라 H 파일인지라 깜빡하고 넘어갔던 터에.. 디스어셈블까지 걸어보는 상황에서야 겨우 잡혀주시고 만 것.

테스트하느라 파엎은 코드들을 정리하는 작업까지만 가뿐히 마치고 갈까 했으나..

이미 야심한 시각. 숙소까지 느린 걸음으로도 10분이면 되는 나는 그렇다 쳐도 집까지 30분 차까지 끊긴 시간의 세원옹을 위해서라도 1분이라도 일찍 일어나야 한다!!

.....네..사실은 고치고나니 맥이 빠져서 귀찮아진거예요 ;ㅁ;

여튼 그렇게 그날의 디버깅세션은 막을 내리고..

집에가던 중 갈림길에서 본인은 세원옹에게 감사의 한마디를 건넨 것으로 오늘의 얘기는 끝이납니다.

.
.
.
앗!! 생각해보니 세원옹한테 맛난것 대접하기로 해놓고는 깜빡하고 있었다!! 쏘리 세원옹.


ps. 디버깅작업과는 직접적인 관계없지만, MAX 플러그인 관련하여 메모 몇가지.

1. modifier 에 의해 변경되는 mesh 의 출력은 각 modifier context 에 속한 mesh 가 아니라, 제일 밑단의 object 가 들고 있는 mesh 이다. modifier 들은 이 mesh 를 출력에 맞도록 변화시켜 줄 의무가 있다.

2. 하지만 각 modifier data 클래스는 이 mesh 들에 대한 사본을 가지고 유지할 필요가 있다. 이건.. 왜그랬더라? ;ㅁ; 까먹었네.. 일찍 좀 정리할걸..
여하튼 modifier data 클래스의 mesh 사본의 동기화는 Modifier::ModifyObject() 메소드에서 해 주면 된다.

3. face selection 의 출력 역시 원본 mesh 상의 selection data 에 값을 반영해주면 될 듯 하지만.. 이게 심히 fake 임. 원본 mesh 의 face selection bit array 에 값을 백날 때려넣어도 출력 안됨. 이는 Mesh::render() 메소드에는 face selection 을 출력하는 부분이 구현되어있지 않기 때문이다.(Modifier 에서 구현된건가 그럼..? 정확히 모르겠네 지금으로선) 여하튼, Modifier::ChannelsChanged() 의 반환값 중에 PART_SELECT 플래그가 확실히 세팅이 되어있어야 적용이 된다.

4. Mesh 클래스의 인스턴스를 마련하고, Mesh::render() 함수를 호출하는것만으로 뷰포트상에 임의 mesh 를 출력할 수 있다. 그러나, Mesh 클래스 인스턴스를 생성하는것만으로 무조건 뷰포트상에 렌더링이 걸리게 되는 것은 아니다. Display() 메소드 를 가진 녀석 내부에서 Mesh::render() 를 호출한다던가 하는 식으로 렌더링 메소드를 명시적으로 호출해줘야한다.


날림으로 적었더니 내용은 개발새발.. 다른사람들은 알아먹기도 힘들 말을 적어놨고..

괜찮아. 나중에 나만 알아볼수 있음 돼....
(...과연 알아 볼수나 있겠니? -ㅂ-; )
2006/04/26 02:16 2006/04/26 02:16
3DS MAX SDK 로 modifier 플러그인을 작성하던 윤모씨.

작업도 막바지에 이르러.. 자체 최종 테스트를 진행하던 도중,
standard primitive 에 edit mesh 를 적용하고 그 위에 작성하던 플러그인(FaceMarker) 를 적용시켰을때 선택된 face 가 표시되지 않는 문제를 발견한다.

standard primitive->edit poly->FaceMarker : OK
standard primitive->convert to Editable Poly->FaceMarker : OK
standard primitive->convert to Editable Mesh->FaceMarker : OK

...이렇게 문제가 되는 상황과 유사한 조합 세가지는 멀쩡히 되던 터라, 한편으로는 별 문제 없겠지 라고 생각하고, 한편으로는 '이거 맥스의 버그 아녀?' 라고 택도 없는 의심을 품던 차..

FaceMarker 대신 유사품 Mesh Select(맥스의 내장 플러그인)을 적용했을때는 전혀 문제가 없는것을 확인!! 본격적인 디버깅에 착수하게 된다.

...허나 반나절이면 끝날 줄 알았던 이 디버깅 작업이 하루가 되고 이틀이 되고 주말이 지나고..이러다가 또 일주일이 훌쩍 지나버릴지도 모르겠단 위기감이 뒷골을 타고 올라오면서(참고 : 다른 작업으로 이미 일주일을 말아먹은 상황-_-; )...
2006년 4월 19일 19시, 업무시간 종료시점에 다음과 같이 결심하게 된다. 당일 23시 59분까지 디버깅 작업을 진행해보고, 딱히 해결방안이 나오지 않으면 일단 제끼고 다음 작업을 진행하자고.

하지만 해결책이 보일듯말듯 상황은 점점 꼬여만 가던 22시 30분경..
작업을 마치고 귀가하려던 세원옹이 윤모씨의 작업에 눈을 돌리게 되었으니..

...to be continued
2006/04/19 02:00 2006/04/19 02:00

"소프트웨어 설계는

일관련 2006/04/02 00:32 posted by 윤뿌쮸
모든 상황에서 완벽한 해결법은 없지만 특정 상황에 더 나은 해결책은 있기 마련입니다."

출처는 http://pds2.egloos.com/pds/1/200601/03/63/Inheritance_vs_Composition.pdf
2006/04/02 00:32 2006/04/02 00:32