2020년 9월 27일 일요일

3D Graphic 에서 TBN + TBN 구하기


 TBN 은 노말맵 할 때 주로 쓰는데 Tangent, Bi-Tangent, Normal 의 줄임말이다.

 벡터가 각각 수직으로, 이것이 모여서 하나의 공간인 Tangent 공간을 만든다.

 하지만 또 다른 뜻도 있다.

 Tangent 공간은 Texture Mapping 과 밀접히 연관이 있다.

 Normal Map 은 uv 로 각 픽셀의 노말값을 들고 오는데 이때의 값이 TBN 공간에 있다.

 Normal Map 의 값은 Z 가 우리가 텍스쳐를 보는 방향, x, y 는 각각 uv 축을 의미한다.

 이런 값을 그냥 사용할 순 없다.

 우리는 이러한 Tangent 공간에서 Local, World 공간으로 변환해 사용해야 한다.

 왜냐하면 world 에서 빛, 카메라, 사물 등의 위치가 공유되기 때문이다.


 그러면 T, B, N 은 각각 무엇이어야 하겠는가? 당연히 Local 기준의 Tangent 공간 축이다. 

 Normal Map 의 x 축, y 축, z 축이 Local 의 x, y, z 축으로 매핑되어야 하기 때문이다.

 우리는 노말맵 내의 (0,0,1) 이 표면과 수직이란 약속이란 것을 배우지 않았나. 

 

 우리는 서로 수직이고 Unit Vector 인 공간 축만 알면 공간 변환, 역변환 모두 할 수 있다.

 이때의 축이 T,B,N 인 것이다.

 그런데 Normal 은 vertex 에 필수요소인데, Tangent, Bi-Tangent 는 종종 없다.

 이를 어떻게 구할까?


 Polygon 은 삼각형으로 구성되어 있다. 이를 이용해서 구하면 된다.

 사실 이렇게 구하면 엄밀하겐 삼각형 하나를 기준으로 만든 Tangent 공간이 되는데,

 점 하나는 여러 삼각형과의 교점이라서 오차가 심할 수도 있다.

 하지만 TBN 안주는데 어쩌겠나. 어차피 그렇게 오차는 안심하다.


 하는 법은 다음과 같다.

 삼각형 정점, p0, p1, p2 가 있으면 p2 - p0, p1 - p0 의 공간좌표와

 그 점에서의 uv0, uv1, uv2 가 있어서 uv2 - uv0, uv1 - uv0 이 있을 것이다.

 그럼 후자 * TBN = 전자. 이렇게 될 것이다.

 이는 TBN = 후자역행렬 * 전자 이렇게 될 것이다.


$\begin{bmatrix}\combi{u}_0&\combi{v}_0\\\combi{u}_1&\combi{u}_1\end{bmatrix}\begin{bmatrix}T_x&T_y&T_z\\B_x&B_y&B_z\end{bmatrix}=\begin{bmatrix}e0_x&e0_y&e0_z\\e1_x&e1_y&e1_z\end{bmatrix}$[
u0v0
u1v1
]
[
TxTyTz
BxByBz
]
=[
e0xe0ye0z
e1xe1ye1z
]
$\begin{bmatrix}T_x&T_y&T_z\\B_x&B_y&B_z\end{bmatrix}=\frac{1}{u_0\combi{v}_1\ -u_1\combi{v}_0}\begin{bmatrix}\combi{v}_1&-\combi{v}_0\\\combi{-u}_1&\combi{u}_0\end{bmatrix}\begin{bmatrix}e0_x&e0_y&e0_z\\e1_x&e1_y&e1_z\end{bmatrix}$[
TxTyTz
BxByBz
]
=1u0v1 − u1v0[
v1v0
u1u0
]
[
e0xe0ye0z
e1xe1ye1z
]


그럼 위처럼 식을 만들 수 있다.

u0 은 p1 - p0 의 uv 값 중 u, u1 은 p2 - p0 의 값 중 u 이다. e, v 도 비슷하다.

p1 이 먼전지 p2 가 먼저인지 순서는 별로 중요하지 않다.

하지만 만약 p1 - p0, p2 - p0 으로 새로 normal 을 계산한다면 callback 이 시계방향인지 아닌지 꼭 살펴보자.


하지만 프로그래밍은 부동소수점 오류가 많아서 저대론 쓰지 않는다.

우리는 위 식을 이용해 T 만 구한다.

그리고 이미 주어진 값인 Normal 을 이용해 T 와 Normal 이 수직하게 만든다.

마지막으로 Tangent 를 Normalize 를 시킨다.

Binormal 은 Normal 과 위에서 구한 Tangent 를 Cross 시키면 자연스럽게 구할 수 있다.

물론 위의 역행렬을 써도 비슷하겐 얻을 수 있는데 필요한 조건을 맞추려면,

즉 서로 수직 + 유닛벡터 를 만족하려면 이게 젤 낫다.


Tangent = (Tangent - dot(Normal, Tangent) * Normal );

Tangent = Normalize(Tangent);

Binormal = Normalize(Cross(Normal, Tangent));


위처럼 말이다.

참고로 Cross 순서는 저게 맞다. 

Cross(x,y)=>z, Cross(y,z) =>x, Cross(z,x)=>y 이렇고 순서바꾸면 음수로 나온다.


이렇게 구한 TBN을 이용해 Tangent Space 내의 값인 Normal Map 의 값을

Local Space로 그리고 World 를 곱해서 Specular, Diffuse 등에 쓰일 수 있다.  


for (uint i = 0; i < indices.size() / 3; i++)
{
	uint index0 = indices[i * 3 + 0];
	uint index1 = indices[i * 3 + 1];
	uint index2 = indices[i * 3 + 2];

	Vector3 p0 = vertices[index0].pos;
	Vector3 p1 = vertices[index1].pos;
	Vector3 p2 = vertices[index2].pos;

	Vector3 e0 = p1 - p0;
	Vector3 e1 = p2 - p0;

	Vector2 uv0 = vertices[index0].uv;
	Vector2 uv1 = vertices[index1].uv;
	Vector2 uv2 = vertices[index2].uv;

	float u0 = uv1.x - uv0.x;
	float u1 = uv2.x - uv0.x;

	float v0 = uv1.y - uv0.y;
	float v1 = uv2.y - uv0.y;

	float r = 1.0f / (u0 * v1 - v0 * u1);  // 역행렬용

	Vector3 normal = vertices[index0].normal;

	normal = Vector3::Cross(e0, e1).Normalize(); 

	Vector3 tangent;
	tangent.x = r * (v1 * e0.x - v0 * e1.x);
	tangent.y = r * (v1 * e0.y - v0 * e1.y);
	tangent.z = r * (v1 * e0.z - v0 * e1.z);
	if (isinf(r))
	{
		tangent = normal;
		tangent.x += 0.01f;
	}

	tangent = (tangent - normal * Vector3::Dot(tangent, normal)).Normalize();
	Vector3 binormal = Vector3::Cross(normal, tangent).Normalize();;

	vertices[index0].tangent = tangent;
	vertices[index1].tangent = tangent;
	vertices[index2].tangent = tangent;
	vertices[index0].binormal = binormal;
	vertices[index1].binormal = binormal;
	vertices[index2].binormal = binormal;
	vertices[index0].normal = normal;
	vertices[index1].normal = normal;
	vertices[index2].normal = normal;
}


List