Unity MatCap shader 분석(하려다 삽질한) 기록

작년 이맘때쯤엔 한창 신규 프로젝트의 세일즈 빌드 작업을 하고 있었다. 작업을 하다 보니 셰이더 코드를 좀 건드릴 일이 있었는데 그때 눈에 밟히는 부분이 있긴 했지만 딱히 파고 들 시간이 없어서 냅두다가 최근에서야 약간 여유가 생겨 다시 파보게 됨.

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

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

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 3×3 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 이라는 변수명의 3×3 행렬을 만들어주는 매크로.

그리고 바로 다음줄과 그 다음줄에 등장하는 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 는 행렬에 벡터를 곱할 때 벡터가 먼저 오고 행렬이 뒤에 오는 형태라는 거. 이건 4×4 행렬로 트랜스폼을 한 방에 처리할 때 translation 에 해당하는 성분이 4행의 1, 2, 3열에 오게 된다.

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

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

어쨌거나 다시 TANGENT_SPACE_ROTATION 로 돌아가서 보면, 매크로 코드에 의해 이 3×3 행렬의 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 이라고 되어 있어서 ‘회전을 시킨다’ 라고 생각을 하게 되어 내 경우엔 더 헷갈렸는데, 사실 A라는 좌표계(좌표축)상에서 어떤 방향을 나타내는 벡터가 있을 때 이걸 B라는 좌표축상에서는 어떤 값의 방향벡터로 나타내어지는지, 즉 사용하는 기준 좌표축이 바뀜에 따라 값이 변하는 것을 계산하기 위한 행렬임. 자세한 내용은 선형대수에서 기저변환(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 해도 그렇고.

근데 어차피 이 코드에선 4×4 행렬 중에 좌상단 3×3 값만 써서 노멀 회전시키는거니까, 그렇게되면 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]

연산자를 보통은 이렇게 쓰지 않지만, 편의상 행렬의 열벡터를 가져오는 연산을 {} 이라 정의하면, 3×3행렬 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이므로 애는 절대 참조될 수 없기 때문에 영향을 미치지 않는 텍셀들이 되는거져.

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

Leave a Reply

Your email address will not be published. Required fields are marked *