학습 페이지
www.inflearn.com
오늘은 Normal Mapping에 대해서 배워보자
이론
먼저 Normal Mapping이 왜 필요한지에 대해 생각해보자 큐브를 생각해보자
지금의 큐브는 normal Vector가 한면에서는 모두 동일하다. 이 방식에서 정밀한 음영효과가 들어간 큐브의 모양을 만들려면 물체를 구성하는 삼각형의 개수를 많이 한다. 이렇게 되면 부하가 너무 심해진다.
그렇기 때문에 등장한 것이 Normal Mapping이다. Normal Mapping은 텍스처에 각 부분에 Normal Vevtor를 저장하여 이것을 활용하여 정밀한 음영효과를 효율적으로 표현할 수 있다.
움직일 때 각 부분의 벡테 방향이 달라지기 때문에 Normal Vector가 어디를 기준으로 하는 좌표인지가 문제이다.
이때 등장하는 것이 탄젠트 공간이다.
탄젠트 공간에서 t b n 3개의 벡터가 만들어진다. x y z와 비슷하다고 보면 된다. 결국에는 이 t b n을 구해주면 된다.
텍스처에는 이 탄첸트 공간 좌표를 가지고 있고 우리는 이를 통해 연산을 해주면 된다.
이걸 계산해줄 때 이 탄젠트 공간이 어느 영역과 만나서 연산이 되는지는 쉐이더에서 결정해주게 된다.
탄젠트 공간-> 로컬공간->World space 로 넘어가는 단계로 넘아가야한다. t b n 이라는 방향벡터 월드좌표에서는 어떤 방향을 가지고 있는지 구해야한다.
좌표계 변환은 up look right vector를 행렬연산을 통해 변환해주면 된다. 이때 평행이동을 적용시키려면 x y z w에서 w
값을 1로 설정해주면 된다. 0이라면 회전만 적용된다.
이때 텍스처에는 rgb 0~255의 값이 들어가있는데 이 값을 -1 ~ 1 사이의 값으로 바꿔주어야한다.
코드
이제 실제 코드에 적용해보자 일단 Tangent Space의 값을 받을 수 있도록 구조체를 하나 새로만들어주자
VertexData.h
struct VertexTextureNormalTangentData
{
Vec3 position = { 0, 0, 0 };
Vec2 uv = { 0, 0 };
Vec3 normal = { 0, 0, 0 };
Vec3 tangent = { 0,0,0 };
};
그리고 기본 도형을 생성해주는 부분에서 도형정보를 가지고 있는 Mesh 클래스에서 Geometry를 이 구조체를 사용하도록 바꿔주자
Mesh.h
private:
shared_ptr<Geometry<VertexTextureNormalTangentData>> _geometry;
Mesh.cpp
#include "pch.h"
#include "Mesh.h"
#include "GeometryHelper.h"
Mesh::Mesh() : Super(ResourceType::Mesh)
{
}
Mesh::~Mesh()
{
}
void Mesh::CreateQuad()
{
_geometry = make_shared<Geometry<VertexTextureNormalTangentData>>();
GeometryHelper::CreateQuad(_geometry);
CreateBuffers();
}
void Mesh::CreateCube()
{
_geometry = make_shared<Geometry<VertexTextureNormalTangentData>>();
GeometryHelper::CreateCube(_geometry);
CreateBuffers();
}
void Mesh::CreateGrid(int32 sizeX, int32 sizeZ)
{
_geometry = make_shared<Geometry<VertexTextureNormalTangentData>>();
GeometryHelper::CreateGrid(_geometry, sizeX, sizeZ);
CreateBuffers();
}
void Mesh::CreateSphere()
{
_geometry = make_shared<Geometry<VertexTextureNormalTangentData>>();
GeometryHelper::CreateSphere(_geometry);
CreateBuffers();
}
void Mesh::CreateBuffers()
{
_vertexBuffer = make_shared<VertexBuffer>();
_vertexBuffer->Create(_geometry->GetVertices());
_indexBuffer = make_shared<IndexBuffer>();
_indexBuffer->Create(_geometry->GetIndices());
}
또한 실제 도형을 만드는 함수를 가지고있는 GeometryHelper 클래스에도 이 구조체 Geometry를 통해 물체를 만드는 함수를 추가해주자.
GeometryHelper.h
public:
static void CreateQuad(shared_ptr<Geometry<VertexTextureNormalTangentData>> geometry);
static void CreateCube(shared_ptr<Geometry<VertexTextureNormalTangentData>> geometry);
static void CreateGrid(shared_ptr<Geometry<VertexTextureNormalTangentData>> geometry, int32 sizeX, int32 sizeZ);
static void CreateSphere(shared_ptr<Geometry<VertexTextureNormalTangentData>> geometry);
GeometryHelper.cpp
void GeometryHelper::CreateQuad(shared_ptr<Geometry<VertexTextureNormalTangentData>> geometry)
{
vector<VertexTextureNormalTangentData> vtx;
vtx.resize(4);
vtx[0].position = Vec3(-0.5f, -0.5f, 0.f);
vtx[0].uv = Vec2(0.f, 1.f);
vtx[0].normal = Vec3(0.f, 0.f, -1.f);
vtx[0].tangent = Vec3(1.0f, 0.0f, 0.0f);
vtx[1].position = Vec3(-0.5f, 0.5f, 0.f);
vtx[1].uv = Vec2(0.f, 0.f);
vtx[1].normal = Vec3(0.f, 0.f, -1.f);
vtx[1].tangent = Vec3(1.0f, 0.0f, 0.0f);
vtx[2].position = Vec3(0.5f, -0.5f, 0.f);
vtx[2].uv = Vec2(1.f, 1.f);
vtx[2].normal = Vec3(0.f, 0.f, -1.f);
vtx[2].tangent = Vec3(1.0f, 0.0f, 0.0f);
vtx[3].position = Vec3(0.5f, 0.5f, 0.f);
vtx[3].uv = Vec2(1.f, 0.f);
vtx[3].normal = Vec3(0.f, 0.f, -1.f);
vtx[3].tangent = Vec3(1.0f, 0.0f, 0.0f);
geometry->SetVertices(vtx);
vector<uint32> idx = { 0, 1, 2, 2, 1, 3 };
geometry->SetIndices(idx);
}
void GeometryHelper::CreateCube(shared_ptr<Geometry<VertexTextureNormalTangentData>> geometry)
{
float w2 = 0.5f;
float h2 = 0.5f;
float d2 = 0.5f;
vector<VertexTextureNormalTangentData> vtx(24);
// 앞면
vtx[0] = VertexTextureNormalTangentData(Vec3(-w2, -h2, -d2), Vec2(0.0f, 1.0f), Vec3(0.0f, 0.0f, -1.0f), Vec3(1.0f, 0.0f, 0.0f));
vtx[1] = VertexTextureNormalTangentData(Vec3(-w2, +h2, -d2), Vec2(0.0f, 0.0f), Vec3(0.0f, 0.0f, -1.0f), Vec3(1.0f, 0.0f, 0.0f));
vtx[2] = VertexTextureNormalTangentData(Vec3(+w2, +h2, -d2), Vec2(1.0f, 0.0f), Vec3(0.0f, 0.0f, -1.0f), Vec3(1.0f, 0.0f, 0.0f));
vtx[3] = VertexTextureNormalTangentData(Vec3(+w2, -h2, -d2), Vec2(1.0f, 1.0f), Vec3(0.0f, 0.0f, -1.0f), Vec3(1.0f, 0.0f, 0.0f));
// 뒷면
vtx[4] = VertexTextureNormalTangentData(Vec3(-w2, -h2, +d2), Vec2(1.0f, 1.0f), Vec3(0.0f, 0.0f, 1.0f), Vec3(-1.0f, 0.0f, 0.0f));
vtx[5] = VertexTextureNormalTangentData(Vec3(+w2, -h2, +d2), Vec2(0.0f, 1.0f), Vec3(0.0f, 0.0f, 1.0f), Vec3(-1.0f, 0.0f, 0.0f));
vtx[6] = VertexTextureNormalTangentData(Vec3(+w2, +h2, +d2), Vec2(0.0f, 0.0f), Vec3(0.0f, 0.0f, 1.0f), Vec3(-1.0f, 0.0f, 0.0f));
vtx[7] = VertexTextureNormalTangentData(Vec3(-w2, +h2, +d2), Vec2(1.0f, 0.0f), Vec3(0.0f, 0.0f, 1.0f), Vec3(-1.0f, 0.0f, 0.0f));
// 윗면
vtx[8] = VertexTextureNormalTangentData(Vec3(-w2, +h2, -d2), Vec2(0.0f, 1.0f), Vec3(0.0f, 1.0f, 0.0f), Vec3(1.0f, 0.0f, 0.0f));
vtx[9] = VertexTextureNormalTangentData(Vec3(-w2, +h2, +d2), Vec2(0.0f, 0.0f), Vec3(0.0f, 1.0f, 0.0f), Vec3(1.0f, 0.0f, 0.0f));
vtx[10] = VertexTextureNormalTangentData(Vec3(+w2, +h2, +d2), Vec2(1.0f, 0.0f), Vec3(0.0f, 1.0f, 0.0f), Vec3(1.0f, 0.0f, 0.0f));
vtx[11] = VertexTextureNormalTangentData(Vec3(+w2, +h2, -d2), Vec2(1.0f, 1.0f), Vec3(0.0f, 1.0f, 0.0f), Vec3(1.0f, 0.0f, 0.0f));
// 아랫면
vtx[12] = VertexTextureNormalTangentData(Vec3(-w2, -h2, -d2), Vec2(1.0f, 1.0f), Vec3(0.0f, -1.0f, 0.0f), Vec3(-1.0f, 0.0f, 0.0f));
vtx[13] = VertexTextureNormalTangentData(Vec3(+w2, -h2, -d2), Vec2(0.0f, 1.0f), Vec3(0.0f, -1.0f, 0.0f), Vec3(-1.0f, 0.0f, 0.0f));
vtx[14] = VertexTextureNormalTangentData(Vec3(+w2, -h2, +d2), Vec2(0.0f, 0.0f), Vec3(0.0f, -1.0f, 0.0f), Vec3(-1.0f, 0.0f, 0.0f));
vtx[15] = VertexTextureNormalTangentData(Vec3(-w2, -h2, +d2), Vec2(1.0f, 0.0f), Vec3(0.0f, -1.0f, 0.0f), Vec3(-1.0f, 0.0f, 0.0f));
// 왼쪽면
vtx[16] = VertexTextureNormalTangentData(Vec3(-w2, -h2, +d2), Vec2(0.0f, 1.0f), Vec3(-1.0f, 0.0f, 0.0f), Vec3(0.0f, 0.0f, -1.0f));
vtx[17] = VertexTextureNormalTangentData(Vec3(-w2, +h2, +d2), Vec2(0.0f, 0.0f), Vec3(-1.0f, 0.0f, 0.0f), Vec3(0.0f, 0.0f, -1.0f));
vtx[18] = VertexTextureNormalTangentData(Vec3(-w2, +h2, -d2), Vec2(1.0f, 0.0f), Vec3(-1.0f, 0.0f, 0.0f), Vec3(0.0f, 0.0f, -1.0f));
vtx[19] = VertexTextureNormalTangentData(Vec3(-w2, -h2, -d2), Vec2(1.0f, 1.0f), Vec3(-1.0f, 0.0f, 0.0f), Vec3(0.0f, 0.0f, -1.0f));
// 오른쪽면
vtx[20] = VertexTextureNormalTangentData(Vec3(+w2, -h2, -d2), Vec2(0.0f, 1.0f), Vec3(1.0f, 0.0f, 0.0f), Vec3(0.0f, 0.0f, 1.0f));
vtx[21] = VertexTextureNormalTangentData(Vec3(+w2, +h2, -d2), Vec2(0.0f, 0.0f), Vec3(1.0f, 0.0f, 0.0f), Vec3(0.0f, 0.0f, 1.0f));
vtx[22] = VertexTextureNormalTangentData(Vec3(+w2, +h2, +d2), Vec2(1.0f, 0.0f), Vec3(1.0f, 0.0f, 0.0f), Vec3(0.0f, 0.0f, 1.0f));
vtx[23] = VertexTextureNormalTangentData(Vec3(+w2, -h2, +d2), Vec2(1.0f, 1.0f), Vec3(1.0f, 0.0f, 0.0f), Vec3(0.0f, 0.0f, 1.0f));
geometry->SetVertices(vtx);
vector<uint32> idx(36);
// 앞면
idx[0] = 0; idx[1] = 1; idx[2] = 2;
idx[3] = 0; idx[4] = 2; idx[5] = 3;
// 뒷면
idx[6] = 4; idx[7] = 5; idx[8] = 6;
idx[9] = 4; idx[10] = 6; idx[11] = 7;
// 윗면
idx[12] = 8; idx[13] = 9; idx[14] = 10;
idx[15] = 8; idx[16] = 10; idx[17] = 11;
// 아랫면
idx[18] = 12; idx[19] = 13; idx[20] = 14;
idx[21] = 12; idx[22] = 14; idx[23] = 15;
// 왼쪽면
idx[24] = 16; idx[25] = 17; idx[26] = 18;
idx[27] = 16; idx[28] = 18; idx[29] = 19;
// 오른쪽면
idx[30] = 20; idx[31] = 21; idx[32] = 22;
idx[33] = 20; idx[34] = 22; idx[35] = 23;
geometry->SetIndices(idx);
}
void GeometryHelper::CreateGrid(shared_ptr<Geometry<VertexTextureNormalTangentData>> geometry, int32 sizeX, int32 sizeZ)
{
vector<VertexTextureNormalTangentData> vtx;
for (int32 z = 0; z < sizeZ + 1; z++)
{
for (int32 x = 0; x < sizeX + 1; x++)
{
VertexTextureNormalTangentData v;
v.position = Vec3(static_cast<float>(x), 0, static_cast<float>(z));
v.uv = Vec2(static_cast<float>(x), static_cast<float>(sizeZ - z));
v.normal = Vec3(0.f, 1.f, 0.f);
v.tangent = Vec3(1.f, 0.f, 0.f);
vtx.push_back(v);
}
}
geometry->SetVertices(vtx);
vector<uint32> idx;
for (int32 z = 0; z < sizeZ; z++)
{
for (int32 x = 0; x < sizeX; x++)
{
// [0]
// | \
// [2] - [1]
idx.push_back((sizeX + 1) * (z + 1) + (x));
idx.push_back((sizeX + 1) * (z)+(x + 1));
idx.push_back((sizeX + 1) * (z)+(x));
// [1] - [2]
// \ |
// [0]
idx.push_back((sizeX + 1) * (z)+(x + 1));
idx.push_back((sizeX + 1) * (z + 1) + (x));
idx.push_back((sizeX + 1) * (z + 1) + (x + 1));
}
}
geometry->SetIndices(idx);
}
void GeometryHelper::CreateSphere(shared_ptr<Geometry<VertexTextureNormalTangentData>> geometry)
{
float radius = 0.5f; // 구의 반지름
uint32 stackCount = 20; // 가로 분할
uint32 sliceCount = 20; // 세로 분할
vector<VertexTextureNormalTangentData> vtx;
VertexTextureNormalTangentData v;
// 북극
v.position = Vec3(0.0f, radius, 0.0f);
v.uv = Vec2(0.5f, 0.0f);
v.normal = v.position;
v.normal.Normalize();
v.tangent = Vec3(1.0f, 0.0f, 0.0f);
v.tangent.Normalize();
vtx.push_back(v);
float stackAngle = XM_PI / stackCount;
float sliceAngle = XM_2PI / sliceCount;
float deltaU = 1.f / static_cast<float>(sliceCount);
float deltaV = 1.f / static_cast<float>(stackCount);
// 고리마다 돌면서 정점을 계산한다 (북극/남극 단일점은 고리가 X)
for (uint32 y = 1; y <= stackCount - 1; ++y)
{
float phi = y * stackAngle;
// 고리에 위치한 정점
for (uint32 x = 0; x <= sliceCount; ++x)
{
float theta = x * sliceAngle;
v.position.x = radius * sinf(phi) * cosf(theta);
v.position.y = radius * cosf(phi);
v.position.z = radius * sinf(phi) * sinf(theta);
v.uv = Vec2(deltaU * x, deltaV * y);
v.normal = v.position;
v.normal.Normalize();
v.tangent.x = -radius * sinf(phi) * sinf(theta);
v.tangent.y = 0.0f;
v.tangent.z = radius * sinf(phi) * cosf(theta);
v.tangent.Normalize();
vtx.push_back(v);
}
}
// 남극
v.position = Vec3(0.0f, -radius, 0.0f);
v.uv = Vec2(0.5f, 1.0f);
v.normal = v.position;
v.normal.Normalize();
v.tangent = Vec3(1.0f, 0.0f, 0.0f);
v.tangent.Normalize();
vtx.push_back(v);
geometry->SetVertices(vtx);
vector<uint32> idx(36);
// 북극 인덱스
for (uint32 i = 0; i <= sliceCount; ++i)
{
// [0]
// | \
// [i+1]-[i+2]
idx.push_back(0);
idx.push_back(i + 2);
idx.push_back(i + 1);
}
// 몸통 인덱스
uint32 ringVertexCount = sliceCount + 1;
for (uint32 y = 0; y < stackCount - 2; ++y)
{
for (uint32 x = 0; x < sliceCount; ++x)
{
// [y, x]-[y, x+1]
// | /
// [y+1, x]
idx.push_back(1 + (y)*ringVertexCount + (x));
idx.push_back(1 + (y)*ringVertexCount + (x + 1));
idx.push_back(1 + (y + 1) * ringVertexCount + (x));
// [y, x+1]
// / |
// [y+1, x]-[y+1, x+1]
idx.push_back(1 + (y + 1) * ringVertexCount + (x));
idx.push_back(1 + (y)*ringVertexCount + (x + 1));
idx.push_back(1 + (y + 1) * ringVertexCount + (x + 1));
}
}
// 남극 인덱스
uint32 bottomIndex = static_cast<uint32>(vtx.size()) - 1;
uint32 lastRingStartIndex = bottomIndex - ringVertexCount;
for (uint32 i = 0; i < sliceCount; ++i)
{
// [last+i]-[last+i+1]
// | /
// [bottom]
idx.push_back(bottomIndex);
idx.push_back(lastRingStartIndex + i);
idx.push_back(lastRingStartIndex + i + 1);
}
geometry->SetIndices(idx);
}
이제 쉐이더 코드를 수정해서 geometry부분의 정보가 잘 전달될 수 있도록 VertexBuffer 부분과 VertexOutput부분을 수정해주자
Global.fx
/////////////////
// VertexBuffer //
/////////////////
struct VertexTextureNormalTangent
{
float4 position : POSITION;
float2 uv : TEXCOORD;
float3 normal : NORMAL;
float3 tangent : TANGENT;
};
/////////////////
// Vertexoutput //
/////////////////
struct MeshOutput
{
float4 position : SV_POSITION;
float3 worldPosition : POSITION1;
float2 uv : TEXCOORD;
float3 normal : NORMAL;
float3 tangent : TANGENT;
};
이제 Client 부분에서 쉐이더를 새로 만들어주자 이때 새로 만들어준 Tangent 데이터까지 받을 수 있게 바꿔주고 PS부분에서 함수를 통해 normal 값이 바뀌도록 해주자
이때 함수는 공통 fx 파일인 Light에서 Tangent space -> World Space 행렬을 구한다음 TangentNormal에 곱해주면 된다.
매개변수에 inout을 앞에 선언해주면 들어온 변수를 변환할 수 있다.
Light.fx
//inout-> 들어온값을 수정해준다
void ComputeNormalMapping(inout float3 normal,float3 tangent,float2 uv)
{
//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
float3 B = normalize(cross(N, T)); //y
//Tan->world - 행렬
float3x3 TBN = float3x3(T, B, N);
//[0,1] -> [-1,1]
float3 tangentSpaceNormal = (map.rgb * 2.0f - 1.0f);
float3 worldNormal = mul(tangentSpaceNormal, TBN);
normal = worldNormal;
}
NormalMapping.fx
#include "00. Global.fx"
#include "00. Light.fx"
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);
float4 color = ComputeLight(input.normal, input.uv, input.worldPosition);
return color;
}
technique11 T0
{
PASS_VP(P0, VS, PS)
};
이렇게 해두고 메인코드에서 노멀맵을 파일경로를 통해 로드하고 Material로 저장한다음 가져와서 오브젝트에 지정해주면된다.
NormalMappingDmeo.cpp
#include "pch.h"
#include "18. NormalMappingDemo.h"
#include "GeometryHelper.h"
#include "Camera.h"
#include "GameObject.h"
#include "CameraScript.h"
#include "MeshRenderer.h"
#include "Mesh.h"
#include "Material.h"
void NormalMappingDemo::Init()
{
RESOURCES->Init();
_shader = make_shared<Shader>(L"14. NormalMapping.fx");
// Material
{
shared_ptr<Material> material = make_shared<Material>();
{
material->SetShader(_shader);
}
{
auto texture = RESOURCES->Load<Texture>(L"Leather", L"..\\Resources\\Textures\\Leather.jpg");
material->SetDiffuseMap(texture);
}
{
auto texture = RESOURCES->Load<Texture>(L"LeatherNormal", L"..\\Resources\\Textures\\Leather_Normal.jpg");
material->SetNormalMap(texture);
}
MaterialDesc& desc = material->GetMaterialDesc();
desc.ambient = Vec4(1.f);
desc.diffuse = Vec4(1.f);
desc.specular = Vec4(1.f);
RESOURCES->Add(L"Leather", material);
}
// Camera
_camera = make_shared<GameObject>();
_camera->GetOrAddTransform()->SetPosition(Vec3{ 0.f, 0.f, -10.f });
_camera->AddComponent(make_shared<Camera>());
_camera->AddComponent(make_shared<CameraScript>());
// Object
_obj = make_shared<GameObject>();
_obj->GetOrAddTransform();
_obj->AddComponent(make_shared<MeshRenderer>());
{
auto mesh = RESOURCES->Get<Mesh>(L"Sphere");
_obj->GetMeshRenderer()->SetMesh(mesh);
}
{
auto material = RESOURCES->Get<Material>(L"Leather");
_obj->GetMeshRenderer()->SetMaterial(material);
}
// Object2
_obj2 = make_shared<GameObject>();
_obj2->GetOrAddTransform()->SetPosition(Vec3{ 0.5f, 0.f, 2.f });
_obj2->AddComponent(make_shared<MeshRenderer>());
{
auto mesh = RESOURCES->Get<Mesh>(L"Cube");
_obj2->GetMeshRenderer()->SetMesh(mesh);
}
{
auto material = RESOURCES->Get<Material>(L"Leather");
_obj2->GetMeshRenderer()->SetMaterial(material);
}
RENDER->Init(_shader);
}
void NormalMappingDemo::Update()
{
_camera->Update();
RENDER->Update();
{
LightDesc lightDesc;
lightDesc.ambient = Vec4(0.5f);
lightDesc.diffuse = Vec4(1.f);
lightDesc.specular = Vec4(1.f, 1.f, 1.f, 1.f);
lightDesc.direction = Vec3(1.f, 0.f, 1.f);
RENDER->PushLightData(lightDesc);
}
{
_obj->Update();
}
{
_obj2->Update();
}
}
void NormalMappingDemo::Render()
{
}
이렇게 해주면 정교한 가죽 모양의 구와 큐브가 나온다.
'게임공부 > Directx11' 카테고리의 다른 글
[Directx11][C++][3D]8. Assimp(Material) (0) | 2024.09.14 |
---|---|
[Directx11][C++][3D]7. Assimp(이론 및 설정) (0) | 2024.09.09 |
[Directx11][C++][3D]5. 빛 통합,Material (0) | 2024.09.06 |
[Directx11][C++][3D]5. Light (0) | 2024.09.04 |
[Directx11][C++][3D]4. Global Shader (0) | 2024.09.02 |