수업
✅ 주제
- 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 Space | T, B, N 벡터로 구성된 픽셀 기준 좌표계 |
| TBN 행렬 | Tangent, Bitangent, Normal로 구성된 변환 행렬 |
VertexTextureNormalTangentData | Tangent 벡터까지 포함한 정점 구조체 |
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 주석 처리 시 차이점
- 픽셀마다 동일한 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으로 월드 좌표계로 변환.