DirectX11

DirectX11 학습 - Normal Mapping

영춘권의달인 2024. 3. 30. 15:31

 

지금까지 작성한 코드와 쉐이더로 가죽 무늬 텍스쳐를 구와 큐브 메시에 적용한 모습이다. 

분명 잘 적용된 모습이지만 굴곡이 표현되지 않아서 뭔가 아쉬운 모습이다.

이것은 큐브로 예를 들자면 큐브의 한 면에 있는 4개의 꼭짓점의 정점의 normal값이 모두 같아서 보간 작업을 해도 모든 픽셀이 같은 normal값을 갖기 때문이다.

그렇다면 이 텍스쳐를 더 입체적으로 굴곡이 있는 모습을 표현하려면 어떻게 해야할까?

제일 직관적인 방법은 굴곡이 있는 부분마다 정점을 추가하는 방법이 있을 것이다. 하지만 이 방법은 정점의 개수가 너무 많아지고 텍스쳐마다 다르게 적용해야 하기 때문에 좋은 방법은 아닐 것이다. 

그렇다면 정점의 개수를 늘리지 않고 물체를 더욱 생동감 있게 표현하려면 어떻게 해야할까?

 

 

이럴때 사용하는 기법을 Normal Mapping이라고 한다.

물체에 적용할 텍스쳐는 물체에 적용될 색상들의 픽셀을 모아놓은 것이고, 해당 픽셀들의 빛 연산을 더욱 정밀하게 하기 위해 텍스쳐의 각 픽셀의 normal값을 따로 모아서 관리하는 텍스쳐를 normal map이라고 한다.

 

 

물체에 적용할 텍스쳐(Diffuse Map)와 노말 맵(Normal Map)

일단 Normal Map이 Normal 정보를 저장하고 있는 정보라는 것은 알았는데, 이 Normal 정보들은 무슨 좌표를 기준으로 하고 있는 것일까?

적용할 물체의 Local 좌표를 기준으로 하고 있는 것일까? 적용할 물체가 그냥 사각형이라면 그럴 수 있겠지만 만약 구 라고 한다면 노말 벡터들도 사방으로 퍼지면서 제각각 일것이고, 다른 물체들에 적용한다고 했을때도 물체들마다 전부 다를 것이다. 하지만 노말 맵은 하나의 텍스쳐이기때문에 Local 좌표를 기준으로 하지는 않을 것이다.

그렇다면 대체 무슨 좌표를 기준으로 작성된 것일까?

 

 

정답은 지금까지는 등장하지 않았던 Tangent Space를 기준으로 작성된 벡터이다.

각 정점마다 접하는 평면이 있고 해당 평면을 기준으로 T,B,N 3개의 벡터가 만들어지게 된다.

T(Tangent), B(Binormal) : 접하는 평면을 구했다면 특정 수학식을 이용해 구할 수 있다고 한다. 구하는 방법은 나도 잘 모르고 복잡하기 때문에 생략

N(Normal) : 정점의 노말 벡터

 

TBN좌표계에서 N이 위쪽 방향이고 노말 벡터는 대체로 위를 향하기 때문에 N이 차지하는 비중이 크다. 이것을 rgb 포멧으로 텍스쳐로 저장하면 N의 값이 b(blue)에 들어가게 되기 때문에 Normal Map은 대체로 푸르게 보이는 것이다.

 

struct VertexTextureNormalTangentData
{
	Vec3 position = { 0, 0, 0 };
	Vec2 uv = { 0, 0 };
	Vec3 normal = { 0, 0, 0 };
	Vec3 tangent = { 0, 0, 0 };
};

 

cpp 내용은 크게 달라지지 않았다. 정점 정보에 tangent 정보를 추가해서 쉐이더에 넘겨주고, Normal Map도 넘겨주었다.

Binormal의 값은 쉐이더에서 tangent와 normal을 외적하여 구할 수 있다.

 

float4 PS(MeshOutput input) : SV_TARGET
{	
	ComputeNormalMapping(input.normal, input.tangent, input.uv);
	float4 color = ComputeLight(input.normal, input.uv, input.worldPosition);
	return color;
}

void ComputeNormalMapping(inout float3 normal, float3 tangent, float2 uv)
{
	// [0,255] 범위에서 [0,1]로 변환
	float4 map = NormalMap.Sample(LinearSampler, uv);
	if (any(map.rgb) == false)
		return;

	float3 N = normalize(normal); // z

	//float3 T = normalize(tangent); // x
	// T를 그냥 사용하지 않는 이유 : 처음 입력받을때는 N과 T가 수직이었지만
	// 보간 작업을 거치면서 수직이 아니게 될수도 있기 때문에 수직이 되도록 해준다.
	float3 T = normalize(tangent - dot(tangent, N) * N);
	float3 B = normalize(cross(N, T)); // y
	float3x3 TBN = float3x3(T, B, N); // TS -> WS

	// [0,1] 범위에서 [-1,1] 범위로 변환
	float3 tangentSpaceNormal = (map.rgb * 2.0f - 1.0f);
	float3 worldNormal = mul(tangentSpaceNormal, TBN);

	normal = worldNormal;
}

 

버텍스 쉐이더는 tangent가 추가된 것 말고는 동일하다. tangent도 동일하게 월드 변환을 해주면 된다.

그리고 픽셀 쉐이더 단계에서 Normal Mapping 작업을 해준다.

우선 노말 맵의 픽셀값을 uv매핑을 통해 추출해서 map에 저장해둔다.

ComputeNormalMapping에 들어오는 normal, tangent벡터는 각 정점에서 World를 기준으로 한 좌표로 변환된 벡터이고 normal, tangent의 외적을 이용해 binormal을 구하고 이 3개의 값을 이용해 World 기준의 TBN 행렬을 얻을 수  있다.

처음 구한 map에 TBN 행렬을 곱하면 tangent space를 기준으로 하는 map을 World를 기준으로 하는 map으로 변환할 수 있고, 이것이 World를 기준으로 하는 노말값이다.

 

여기서 구한 노말값으로 전에 했던 빛 연산을 해주면 해당 픽셀의 최종 색상이 결정된다.

 

 

Normal Mapping을 적용한 모습이다. 처음 모습보다 입체감이 있는것을 확인할 수 있다.

Normal Mapping의 핵심은 정점을 추가하지 않고 픽셀 단위로 다른 Normal값을 가질 수 있도록 만드는 것이다.