원래 Tangent라는 건 대충 표면 평면 벡터인 Tangent와 Bitangent 이고 이제 Normal과 Tangent, Bitangent는 모두 수직이고 이 3개 축을 가지고 만든 Space가 Tangent Space 이다... 이정도로만 알고 있었다. 하지만 이걸 이제 실제로 쓰는 상황이 오니 Tangent의 실제 쓰임이 어떻게 되는 지 이런 부분에 있어서 부족함을 느꼈다.
일단 Tangent Space로의 Transform에 대해서이다. 유니티 셰이더에서 Normal은 반드시 Tangent Space 노멀을 사용한다. 즉, Camera를 가지고 World Normal을 렌더링한 Render Texture를 가지고 Normal Blending을 하려고 했더니 World Nornal을 Tangent Space로 변환해서 SurfaceData에 저장을 해주어야 하는 상황이 발생했다.
그러면 Tangent Space로 변환하는 Matrix는 어떻게 되는 것이고, 왜 그런 형태를 갖고 있는 가를 알아보아야 한다.
다음으로, 셰이더가 아닌 스크립트에서의 사용이다. RaycastHit 구조체에는 Normal이라는 멤버가 존재한다.
https://docs.unity3d.com/ScriptReference/RaycastHit-normal.html
Unity - Scripting API: RaycastHit.normal
Success! Thank you for helping us improve the quality of Unity Documentation. Although we cannot accept all submissions, we do read each suggested change from our users and will make updates where applicable. Close
docs.unity3d.com
충돌한 충돌체의 표면이 바라보는 벡터를 나타내는 것인데, Tangent를 Right 방향을 구하는 용도로 사용하기도 한다.
예를 들면 다음과 같은 방식이다.
물체(정육면체)로 부터 Ray(녹색)이 Plane과 충돌하였을 때의 Normal(파랑)이다. 여기서 빨강이 내가 알고자 하는 것이다.
빨강 벡터는 hit.normal과 Vector3.up을 외적한 값인 Right 벡터이다.
Vector3 right = Vector3.Cross(Vector3.up, hit.normal); 이다.
이 값은 y축 (수직 축)과 반드시 수직이며, hit.normal이 정면이라고 하였을 때 right 방향 벡터가 된다.
...왜? 진짜 수학 안배운 티가 난다. 딴 사람들은 이미 알고 있는 부분일텐데 나는 항상 이런 벡터나 행렬 미적분 관련된 거에서 막힌다...ㅠ
Cross Product가 뭐였는가... 두 벡터에 동시에 수직인 벡터가 나오는 벡터 곱셈연산이다. 그리고 어떤 벡터가 앞에 오느냐에 따라 다른 값의 벡터가 나온다. 오른손 감기 법칙인가... 그거에 의해 Cross(A,B)가 있으면 A를 기준으로 B를 향해 오른손을 감을 때 엄지손가락이 가리키는 벡터가 연산 결과 벡터가 된다.
그러니까.. 에.... Vector3.up이라는 건 (0,1,0) 이고, hit.normal로 오른손을 감으면.... 왼쪽인데? 반대로 나오는데? 뭐지?
일단 이 부분은 좀 더 찾아보도록 하고... 일단 두 벡터를 Cross한다는 것은 대충 두 벡터가 한 평면 위에 존재하는 선이라고 보았을 때 해당 평면에 수직인 벡터를 구하는 계산이라고 대략 이해했다.
음...그리고 오른손이 아니라 왼손으로 하는 게 맞나보다... 뭐지... 영상마다 내용이 다 다르네. 일단 다음 영상에서는 왼손으로 엄지랑 검지를 A,B라 했을 때 뻐큐손가락이 결과 방향 벡터가 된다고 한다... 좌표계 차이인가? 왜 어디서는 오른손이라고 하고 어디선 왼손이라 하냐 헷갈리게;
https://www.youtube.com/watch?v=kz92vvioeng
뭐... 그러면 오른쪽 방향이 나오는 이유는 해결했다.
근데 왼손인지 오른손인지 차이나는 이유를 이제 밝혀보자.
유니티는 왼손 좌표계를 사용한다. 뭐 왼손이고 오른손이고 걍 Z축이 화면쪽으로 +가 되면 오른손이고 그 반대면 왼손 좌표계라고 하면된다.
혹시 임의로 값에 부호를 반전시켜주는 것인가? 한번 Vector3.Cross가 아니라 수학에서 배운 공식대로 Cross함수를 만들어서 써보자.
결과는 그냥 똑같다. 유니티 함수나 내 함수나 별다른 차이가 없다.
계산한 값에는 아무런 차이가 없다. 그렇다면 결국 좌표계 차이로 생기는 문제가 맞는 것 같다.
Cross(Vector3.up, Vector3.right)를 해보자. 그러면 결과는 Vector3.back이 될 것이다.
여기서 차이가 생기는 게 맞는 것 같다.
똑같은 Vector3.back, 즉 (0,0,-1)이지만 유니티에서는 화면에 가까운 쪽에 해당 값이 있을 것이고 오른손 좌표계를 쓰는 곳에서는 화면 먼쪽에 자리를 잡고 있을 것이다. 그러니까 결과 값은 똑같지만 좌표계 차이로 인해 왼손 좌표계에서는 왼손 법칙에 따라 벡터의 방향이 정해지고 오른손 좌표계에서는 오른손 법칙에 따라 벡터의 방향이 정해지는 것 같다.
보통 수학에서는 가로가 X, 세로가 Y, 높이가 Z축인데, Z축은 높을수록 숫자가 올라간다. 그러니까 게임 엔진에서 쓰는 것처럼 Z축을 높이가 아니라 세로 축으로 돌려보면 수학에서 나오는 좌표계는 오른손 좌표계가 된다. 그래서 오른손 법칙을 가지고 Cross Product를 설명했던 것 같다.
어.. 그러면 Tangent와 간접적으로 상관이 있는 Cross에 대해서 일단 이해를 했다.
자... 그러면 이제 World Space에서 Tangent Space 로 좌표계를 변환을 어떻게 하는 거고, 왜 이런 식이 나오는 것인지 한번 알아보도록 하자..
일단 검색을 해보니까 왜 tangent 공간으로 변환하는 행렬이
float3x3(world_tangent, world_bitangent, world_normal)
이 되는 지 나와있는 곳이 없다. 이렇게 되면 아예 더 앞으로 가는 수 밖에 없다... 물론 벡터에 대해 제대로 배웠다면 이럴 일도 없었겠지만...
일단 그래서 공간 변환이라는 것부터 알아보기로 하였다.
https://www.youtube.com/watch?v=kYB8IZa5AuE
이 영상을 보니 조금 감이 온다. 이 영상 내용에 따라서 생각을 해보면
(-1,2)라는 어떤 벡터 v가 있고, x축을 의미하는 i-hat과 y축을 의미하는 j-hat이 있다고 하자. 이러면 v를 i와 j로 표현한다면 다음과 같이 표현할 수 있다.
v = -1 * i-hat+ 2 * j-hat
햇이라는 단어는 대학물리 배우면서 처음 알게 되었다. 난 처음들어보는 단어였는데 걍 (1,0), (0,1)같이 축을 나타내는 벡터를 i-hat j-hat 하면서 ^ <- 이걸 모자라고 헷이라고 하는 건지 뭐 그렇게 쓰더라.
그러니까 v라는 벡터는 x축을 의미하는 1,0 벡터에 대해 -1을 한 (-1,0) 벡터와 y축을 의미하는 (0,1)벡터에 2배를 한 (0,2)벡터의 합, 즉 v = (-1, 0) + (0, 2) = (-1, 2) 로 나타낼 수 있다.
그러므로 이 축이 되는 벡터가 다른 값으로 변경되어도 Transformed V의 값을 축 벡터를 이용해서 추측할 수 있다는 것이다.
이를 3D 공간으로 변환시켜도 똑같이 적용된다.
World Space 상에서 원점은 (0,0) 이고 x축은 (1, 0, 0), y축은 (0, 1, 0), z축은 (0, 0, 1) 이다.
그러면 이 공간 상에 어떠한 벡터 v ( a, b, c)는 다음과 같이 나타낼 수 있을 것이다.
x : (1, 0, 0),
y : (0, 1, 0),
z : (0, 0, 1)
v = ax + by + cz
자, 그러면 Tangent Space의 정의를 생각해보자. Tangent Space는 Normal이 Z축, Tangent가 X축, Bitangent가 Y축이 되는 공간이다. 참고로 다 정규화 되어있어야 한다.
그러면 Tangent Space 상의 벡터 v는 다음과 같이 나타낼 수 있을 것이다.
v = a * tangent-vector + b * bitangent-vector + c * normal-vector
그러면 된거다. 이걸 이제 행렬로 바꾸기만 하면 된다.
조금 앞으로 돌아가서, v를 행렬로 표현을 한다면
요렇게 된다. 즉 다시 말해
이다.
그러면 이제 Tangent Space 상의 벡터 V는 다음과 같이 나타낼 수 있다!
흠.... 뭔가 논리 비약이 있었던 것 같다. 지금 맹점은 Tangent Space 상에서 존재하는 Vector V에 World Space로 변환된 Tangent, Bitangent, Normal을 가지고 변환행렬을 만들어서 곱해주면 World Space 상의 Vector V가 나온다. 근데 World Space 상의 Vector V에 World Space 상의 TBN(다 쓰기 귀찮으니 앞으로 이렇게 줄여서 쓰겠다. Tangent, Bitangent, Normal을 줄여서 TBN이라 하겠다) 을 다시 곱해주면 Tangent Space가 되는 게 맞나?
다음은 Unity에 있는 SpaceTransforms.hlsl에 있는 탄젠트 공간 변환 함수들이다.
TransformTangentToWorld 함수는 내가 생각한 대로이다. Tangent Space 상의 방향 벡터 dirTS에 대해 tangentToWorld 행렬인 TBN을 곱해주면 World Space 상의 방향 벡터가 된다. 하지만 내가 생각한 것과 달리 WorldSpace에서 Tangent Space로 변환하는 함수는 뭔가... 복잡하다. 그냥 TBN을 곱해주면 되는 문제가 아닌 것 같다. TBN의 역행렬을 구해서 곱해주는 것일까? 일단 주석으로 달려있는 scalar triple product랑 mikktspace.h 헤더를 한번 참조해보자.
일단 mikktspace.h 헤더는 Unity에서는 어디있는지 몰라서 검색해보니 다음과 같은 깃헙 주소가 나왔다.
https://github.com/mmikk/MikkTSpace/blob/master/mikktspace.h
GitHub - mmikk/MikkTSpace: A common standard for tangent space used in baking tools to produce normal maps.
A common standard for tangent space used in baking tools to produce normal maps. - GitHub - mmikk/MikkTSpace: A common standard for tangent space used in baking tools to produce normal maps.
github.com
아래 쪽에 보면 뭔가 설명이 적혀져 있는데, 잘 이해가 되진 않지만 한번 해석을 해보자.
노멀맵 쓸 때 오류가 없으려면 노멀맵 샘플러가 픽셀 셰이더 변환 행렬의 정확한 역행렬을 사용해야 한다.
픽셀 셰이더에서 우리가 얻을 수 있는 가장 효율적인 변환행렬은 정규화되지 않은 탄젠트, 바이탄젠트, 그리고 버텍스 노멀을 사용하는 것이다. 이를 vT, vB, vN이라고 칭하도록 한다.
픽셀셰이더에서
노멀 아웃풋 vNout = normalize( vNt.x * vT + vBt.y * vB + vNt.z *vN);
이때 vNt는 탄젠트 공간 노멀이다. (노멀맵에서 추출한 벡터 말하는 거인듯)
노멀맵 샘플러는 픽셀 셰이더랑 호환되기 위해 비정규화되고 보간처리된 탄젠트 바이탄젠트 버텍스 노멀을 사용해야 합니다.
float3 row0 = cross(vB, vN);
float3 row1 = cross(vN, vT);
float3 row2 = cross(vT, vB);
float fSign(flip sign 줄인 말인 듯) = dot(vT, row0)<0? -1 : 1; (이건 노멀맵에서 X축이 음의 방향으로 증가하는 좌표계인지 양의 방향으로 증가하는 좌표계인지에 따라 탄젠트의 부호를 변경해주기 위한 코드인듯하다. 일단 신경끄고 보자.)
vNt = normalize(fSign * float3(dot(vNout, row0), dot(vNout, row1), dot(vNout, row2)) );
음... 아래의 내용은 잘 모르겠고... 일단 위에서 벡터랑 Bitangent 이런 녀석들을 cross product 해줌으로써 TBN의 역행렬을 구하려는 것 같은데... 역행렬을 cross product를 이용해 구할 수도 있는 건가? 와 진짜 겁나 어렵네. 다른 것도 한번 찾아볼까....
https://youtu.be/0QhR7WSoF78?t=70
OpenGL 튜토리얼 영상인데, 여기 1:10 을 보면 light 나 eye/camera 벡터를 tangent space로 변환하는건 TBN 행렬의 전치 행렬에 벡터를 곱해주면 된다고 나와있다. 왜인지는 모르겠지만. 이건 이제 알아봐야지.
왜인지 위의 코드 내용을 알것 같다.
cross(vB, vN)을 했다면 그 결과는 비정규화된 Tangent가 된다. vT
cross(vN, vT)을 했다면 그 결과는 비정규화된 Bitangent이 된다. vB
cross(vT, vB)을 했다면 그 결과는 비정규화된 Normal이된다. vN
그러니까 결국 저 위의 연산은 TBN 행렬을 만드는 계산식이 맞다. 아직 부호 바뀌는 부분은 완전히 파악은 안했지만.
https://bbtarzan12.github.io/Noraml-Mapping/
Normal Mapping 과 TBN 행렬
World 공간에서 Tangent 공간으로!
bbtarzan12.github.io
위 글을 보면 회전행렬의 역행렬은 회전행렬의 전치행렬과 같다는 말이 나온다.
Normal 벡터는 위치는 의미없고 방향만 의미를 갖는 벡터이기 때문에 이 벡터의 변환행렬은 평행이동을 제외한 회전, 스케일 값만 들어가있는 어떠한 행렬이 될 것인데, 이때 Scale 행렬은 대각행렬이라 전치를 하든말든 상관이 없고 Rotation 행렬이 직교행렬이기 때문에 전치행렬이 역행렬이 된다고 한다. 이거 대학수학2에서 배운거 같긴 한데 잘 기억이... 안 난다.
그러니까 간단하게 생각하면 Tangent Space에서 World Space로 변환하는 행렬이 TBN 행렬이면 World Space에서 Tangent Space로 변환하는 행렬은 TBN의 역행렬을 구해서 곱해주면 되는데, 이때 역행렬이 TBN의 전치행렬과 같다는 말이고, 그러면 TBN의 역행렬을 구하는 건 간단하게 transpose 함수를 쓰면 될 줄 알았는데, transpose를 해서 구하는 것이 아니라 cross product를 해서 행렬을 만들고, 거기에 곱셈 연산을 하는 게 아니라 dot product를 해준 후에 이걸 normalize한 게 world space에서 tangent space로 변환한 벡터이다...?
뭐 dot product 쓰는 부분이 아마 scalar triple product와 관계된 식이겠지. 그러면 cross product로 구한 값이 determinant... 그러니까 뭐 행렬의 판별식 얘기를 하는 건가 싶은데 이거랑 대체 무슨 관계가 있는 지 알아보자.
https://www.youtube.com/watch?v=BaM7OCEm3G0
또 이분의 영상이다. 아무래도 저 시리즈 챕터를 모두 보면 이해를 할 수 있을 듯 하다. 행렬과 Dot Product, Cross Product가 어떤 관계가 있는 지 모두 설명이 되어 있다. 일단 이 글은 저 영상을 모두 보고 이해한 후 다시 적도록 하겠다.
'Programming' 카테고리의 다른 글
input parameter 'metainput' missing semantics (1) | 2022.10.11 |
---|---|
FSM 공부한 부분 (0) | 2022.04.20 |
구 & 광선 교차 판정 하기 (4) | 2020.02.12 |
[C++] 비트 연산 활용 연습 (1) | 2020.01.30 |
Template 상속 :: 부모의 변수 사용시 오류 (0) | 2020.01.25 |