수업


✅ 주제

  • 3D 오브젝트에 픽셀 단위로 정밀한 Normal 방향을 부여하여, 표면 질감 표현을 향상시키는 기술인 Normal Mapping을 구현하는 것이 핵심 목표다.
  • 이 기술을 통해 삼각형 수를 늘리지 않고도 세밀한 조명 처리가 가능하며, 실제 구현을 위해:
    • 정점 구조체 확장 (Tangent 포함),
    • TBN 행렬 구성,
    • NormalMap 처리 셰이더 구현,
    • World Space 변환 처리를 실습한다.

📘 개념

🔹 왜 Normal Mapping이 필요한가?

  • 일반적인 3D 오브젝트는 정점 단위의 Normal 벡터만을 기반으로 음영을 계산한다.
  • 하지만 이렇게 하면 면 단위로 동일한 조명이 적용되므로 곡면처럼 보이게 하려면 삼각형 개수를 늘려야 한다 → 이는 성능 저하로 이어짐.
  • 해결책으로 Normal Map이라는 텍스처를 사용하여, 각 픽셀마다 Normal 방향을 따로 정의함으로써, 정밀한 표면 질감 표현이 가능해진다.

🔹 탄젠트 공간(Tangent Space)이란?

  • Tangent Space는 각 픽셀의 기준 좌표계로, 정점 기준으로 다음 세 축을 정의한다:
    • Tangent (T) → 텍스처의 U 방향 (x축, 빨강)
    • Bitangent (B) → 텍스처의 V 방향 (y축, 초록)
    • Normal (N) → 표면 수직 방향 (z축, 파랑)
  • Normal Map에 저장된 RGB 값은 사실상 이 Tangent Space 기준의 Normal 벡터 정보를 의미한다.

🔹 TBN 행렬과 좌표계 변환

  • Tangent Space에서 조명 계산을 수행하려면, 해당 공간의 Normal을 World Space로 변환해야 한다.
  • 이를 위해 T, B, N을 월드 기준 방향으로 정규화한 후, 아래와 같이 3×3 변환 행렬을 구성한다:
float3x3 TBN = float3x3(Tangent, Bitangent, Normal)
  • 이후, NormalMap에서 가져온 RGB를 [-1.0 ~ 1.0] 범위로 변환한 후 이 행렬을 곱하면 → World 기준의 Normal 벡터가 완성된다.

🧾 용어 정리

용어설명
Normal Mapping텍스처 기반의 픽셀 단위 노멀 처리 기술
Tangent SpaceT, B, N 벡터로 구성된 픽셀 기준 좌표계
TBN 행렬Tangent, Bitangent, Normal로 구성된 변환 행렬
VertexTextureNormalTangentDataTangent 벡터까지 포함한 정점 구조체
ComputeNormalMapping()NormalMap 기반으로 World Normal을 계산하는 함수

🧠 코드 분석


🔸 구조체 확장 – VertexTextureNormalTangentData

struct VertexTextureNormalTangentData
{
	Vec3 position;
	Vec2 uv;
	Vec3 normal;
	Vec3 tangent;
};
  • Tangent 추가로 인해 기존 VertexTextureNormalData와는 다름.
  • Bitangent는 Shader에서 cross(N, T)로 계산되므로 저장하지 않아도 된다.

🔸 GeometryHelper 수정 예시

vtx[0].normal = Vec3(0.f, 0.f, -1.f);  // 정면
vtx[0].tangent = Vec3(1.f, 0.f, 0.f);  // 오른쪽 방향
  • 각 정점에서 Normal과 Tangent를 명시적으로 설정해야 한다.
  • GeometryHelper 내부의 CreateCube, CreateSphere, CreateGrid 등 모든 도형 생성 함수에서 적용됨.

🔸 Global.fx – 정점 및 출력 구조

struct VertexTextureNormalTangent
{
	float4 position : POSITION;
	float2 uv       : TEXCOORD;
	float3 normal   : NORMAL;
	float3 tangent  : TANGENT;
};

struct MeshOutput
{
	float4 position : SV_POSITION;
	float3 worldPosition : POSITION1;
	float2 uv : TEXCOORD;
	float3 normal : NORMAL;
	float3 tangent : TANGENT;
};
  • Vertex 구조에서 Tangent를 반드시 포함해야 Shader에서 전달 가능하다.
  • POSITION1은 World 좌표 전달용 커스텀 시맨틱이다.

🔸 ComputeNormalMapping – Light.fx 함수 구현

void ComputeNormalMapping(inout float3 normal, float3 tangent, float2 uv)
{
	float4 map = NormalMap.Sample(LinearSampler, uv);
	if (any(map.rgb) == false)
		return;

	float3 N = normalize(normal);
	float3 T = normalize(tangent);
	float3 B = normalize(cross(N, T));

	float3x3 TBN = float3x3(T, B, N);

	float3 tangentSpaceNormal = (map.rgb * 2.0f - 1.0f);
	float3 worldNormal = mul(tangentSpaceNormal, TBN);

	normal = worldNormal;
}
  • inout으로 받은 normal은 내부에서 덮어쓰게 되며, 픽셀 기준 조명 벡터로 교체된다.
  • RGB는 [0~1] 범위이므로 *2 - 1 연산을 통해 [-1~1]로 보정한다.
  • 최종적으로 TBN 행렬을 곱해 World 기준 Normal로 변환.

🔸 Shader – VS/PS 구현

MeshOutput VS(VertexTextureNormalTangent input)
{
	MeshOutput output;
	output.position = mul(input.position, W);
	output.worldPosition = input.position.xyz;
	output.position = mul(output.position, VP);
	output.uv = input.uv;
	output.normal = mul(input.normal, (float3x3)W); // 회전만
	output.tangent = mul(input.tangent, (float3x3)W);

	return output;
}

float4 PS(MeshOutput input) : SV_TARGET
{
	ComputeNormalMapping(input.normal, input.tangent, input.uv);
	return ComputeLight(input.normal, input.uv, input.worldPosition);
}
  • VS에서 Local 기준 Normal, Tangent를 World 좌표계 기준으로 회전만 적용.
  • PS에서 ComputeNormalMapping()을 통해 픽셀 기준 정밀 조명 처리를 수행.

🔸 적용 예시 – NormalMappingDemo.cpp

auto material = make_shared<Material>();
material->SetShader(_shader);

material->SetDiffuseMap(RESOURCES->Load<Texture>(L"Leather", L"..\\Textures\\Leather.jpg"));
material->SetNormalMap(RESOURCES->Load<Texture>(L"LeatherNormal", L"..\\Textures\\Leather_Normal.jpg"));
auto mesh = RESOURCES->Get<Mesh>(L"Cube");
auto mat = RESOURCES->Get<Material>(L"Leather");

_obj->GetMeshRenderer()->SetMesh(mesh);
_obj->GetMeshRenderer()->SetMaterial(mat);
  • 오브젝트(Sphere, Cube)에 동일한 NormalMap 적용 가능.
  • 클론 없이 단일 Material을 공유해도 적용됨.

🧪 실험 및 테스트

✔ NormalMap 주석 처리 시 차이점

// material->SetNormalMap(texture);  // ← 주석 처리 시
  • 픽셀마다 동일한 Normal 적용 → 밋밋한 조명 결과
  • Normal Mapping 활성화 시 각 점마다 다른 조명이 적용되어 입체감 향상

✅ 핵심 정리

항목내용
🎯 구현 목표픽셀 단위로 정밀한 조명 효과를 부여하기 위한 Normal Mapping
🧱 구조체VertexTextureNormalTangentData에 Tangent 포함
🧠 좌표계 전환Tangent Space Normal → TBN → World Space 변환
🎨 Shader 처리ComputeNormalMapping에서 RGB를 Normal로 변환
💡 결과정점을 늘리지 않고도 고해상도 조명 디테일 확보 가능

📌 시각 요약

  • Tangent: 표면의 가로(U) 방향 (빨강)
  • Bitangent: 세로(V) 방향 (초록, N×T)
  • Normal: 표면 수직 방향 (파랑)
TBN = float3x3(Tangent, Bitangent, Normal)

NormalMap의 RGB 값은 TangentSpace 기준 → [-1,1] 변환 → TBN으로 월드 좌표계로 변환.


profile
李家네_공부방

0개의 댓글