40. DirectX11 3D 입문_Normal
Normal 벡터에 대해 알아본다.
표면이랑 정확히 수직인 벡터를 Normal 벡터라고 한다.
쿼드는 정점들이랑 수직인 벡터
큐브도 각 평면마다 수직인 애를 찾으면 된다.
구형의 경우는 원점에서 도넛의 각 점으로 가는 방향벡터가 Normal 벡터이다.
그리드 같은 경우는 평면이니까 위로 가는 거 하나다.
이걸 구해서 무얼 하느냐
나중에 다양한 형태의 조명연산을 해줄 것이다.
빛이 정확히 수직으로 오면 쎈 거고, 빛이 거의 90도면 빛이 약한 거.
물체의 색상을 보여주고 싶으면 물체에 빛이 반사되는 걸로 물체의 색을 보는 것이다. 노멀 벡터를 이용해서 하고 싶은 건 각도가 작으면 작을수록 빛을 세게, 90도에 갈수록 약해지는 걸 원한다.
내적을 하는데 -L과 노멀벡터의 내적을 구하면
0도일 때 1, 90도일 때 0이 된다.
0~1사이로 나온다는 걸 이용해서 많은 작업을 할 수 있다.
조명 연산이란게 이런 식이다.
가상의 조명에 대한 식을 정의하고 어느 순간에 세게 먹힐지 등등을 예상해서 만들면 된다.
순차적으로 normal 벡터부터 하나씩 구해줘야 한다.
normalMap이라 해서 복잡한 메쉬는 값을 구하기 어렵기 때문에 각기 정점마다 그 아이가 해당하는 normal 방향을 구해서 하나의 맵을 만들어 주는 경우가 있다.
거기까지 가지는 않고 기본적으로 사용하고 있는 기본 도형들을 대상으로 실습을 할 것이다.
1. struct VertexTextureNormalData
Normal을 이용하기 위해서 VertexData.h에
struct VertexTextureNormalData
{
Vec3 position = { 0, 0, 0 };
Vec2 uv = { 0, 0 };
Vec3 normal = { 0, 0, 0 };
};
이런 애가 추가가 되었다 .
Vec3로 normal이라는 값을 받게 될 것이다.
이걸 이용해서 빛을 연산할 때 얼마만큼 얘가 세게 먹어야 하는지 등등을 관리하게 된다고 볼 수 있다.
2. GeometryHelper에서 VertexTextureNormalData버전 Create 함수들 정의하기
GeometryHelper.h에 가서 보면 원래는 VertexTextureData가 들어가는 4개를 만들어 놨었는데
static void CreateQuad(shared_ptr<Geometry<VertexTextureData>> geometry);
static void CreateCube(shared_ptr<Geometry<VertexTextureData>> geometry);
static void CreateSphere(shared_ptr<Geometry<VertexTextureData>> geometry);
static void CreateGrid(shared_ptr<Geometry<VertexTextureData>> geometry, int32 sizeX, int32 sizeZ);
여기에 추가적으로 VertexTextureNormalData에 해당하는 걸 만들어야 한다.
static void CreateQuad(shared_ptr<Geometry<VertexTextureNormalData>> geometry);
static void CreateCube(shared_ptr<Geometry<VertexTextureNormalData>> geometry);
static void CreateGrid(shared_ptr<Geometry<VertexTextureNormalData>> geometry, int32 sizeX, int32 sizeZ);
static void CreateSphere(shared_ptr<Geometry<VertexTextureNormalData>> geometry);
이렇게 VertexTextureNormalData가 추가가 되어야 한다.
1) CreateQuad
void GeometryHelper::CreateQuad(shared_ptr<Geometry<VertexTextureNormalData>> geometry)
{
vector<VertexTextureNormalData> 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[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[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[3].position = Vec3(0.5f, 0.5f, 0.f);
vtx[3].uv = Vec2(1.f, 0.f);
vtx[2].normal = Vec3(0.f, 0.f, -1.f);
geometry->SetVertices(vtx);
vector<uint32> idx = { 0, 1, 2, 2, 1, 3 };
geometry->SetIndices(idx);
}
Quad는 단순하다 평면이기 때문에 0, 0, -1
룩벡터의 반대 방향이다.
2) CreateCube
큐브 같은 경우에는
가져온 코드가 C++ 20 버전으로 되어 있으니 Engine의 Properties의 C/C++에서 C++언어 표준을 C++ 20으로 해준다.
그럼 에러가 사라진다.
void GeometryHelper::CreateCube(shared_ptr<Geometry<VertexTextureNormalData>> geometry)
{
float w2 = 0.5f;
float h2 = 0.5f;
float d2 = 0.5f;
vector<VertexTextureNormalData> vtx(24);
// 앞면
vtx[0] = VertexTextureNormalData(Vec3(-w2, -h2, -d2), Vec2(0.0f, 1.0f), Vec3(0.0f, 0.0f, -1.0f));
vtx[1] = VertexTextureNormalData(Vec3(-w2, +h2, -d2), Vec2(0.0f, 0.0f), Vec3(0.0f, 0.0f, -1.0f));
vtx[2] = VertexTextureNormalData(Vec3(+w2, +h2, -d2), Vec2(1.0f, 0.0f), Vec3(0.0f, 0.0f, -1.0f));
vtx[3] = VertexTextureNormalData(Vec3(+w2, -h2, -d2), Vec2(1.0f, 1.0f), Vec3(0.0f, 0.0f, -1.0f));
// 뒷면
vtx[4] = VertexTextureNormalData(Vec3(-w2, -h2, +d2), Vec2(1.0f, 1.0f), Vec3(0.0f, 0.0f, 1.0f));
vtx[5] = VertexTextureNormalData(Vec3(+w2, -h2, +d2), Vec2(0.0f, 1.0f), Vec3(0.0f, 0.0f, 1.0f));
vtx[6] = VertexTextureNormalData(Vec3(+w2, +h2, +d2), Vec2(0.0f, 0.0f), Vec3(0.0f, 0.0f, 1.0f));
vtx[7] = VertexTextureNormalData(Vec3(-w2, +h2, +d2), Vec2(1.0f, 0.0f), Vec3(0.0f, 0.0f, 1.0f));
// 윗면
vtx[8] = VertexTextureNormalData(Vec3(-w2, +h2, -d2), Vec2(0.0f, 1.0f), Vec3(0.0f, 1.0f, 0.0f));
vtx[9] = VertexTextureNormalData(Vec3(-w2, +h2, +d2), Vec2(0.0f, 0.0f), Vec3(0.0f, 1.0f, 0.0f));
vtx[10] = VertexTextureNormalData(Vec3(+w2, +h2, +d2), Vec2(1.0f, 0.0f), Vec3(0.0f, 1.0f, 0.0f));
vtx[11] = VertexTextureNormalData(Vec3(+w2, +h2, -d2), Vec2(1.0f, 1.0f), Vec3(0.0f, 1.0f, 0.0f));
// 아랫면
vtx[12] = VertexTextureNormalData(Vec3(-w2, -h2, -d2), Vec2(1.0f, 1.0f), Vec3(0.0f, -1.0f, 0.0f));
vtx[13] = VertexTextureNormalData(Vec3(+w2, -h2, -d2), Vec2(0.0f, 1.0f), Vec3(0.0f, -1.0f, 0.0f));
vtx[14] = VertexTextureNormalData(Vec3(+w2, -h2, +d2), Vec2(0.0f, 0.0f), Vec3(0.0f, -1.0f, 0.0f));
vtx[15] = VertexTextureNormalData(Vec3(-w2, -h2, +d2), Vec2(1.0f, 0.0f), Vec3(0.0f, -1.0f, 0.0f));
// 왼쪽면
vtx[16] = VertexTextureNormalData(Vec3(-w2, -h2, +d2), Vec2(0.0f, 1.0f), Vec3(-1.0f, 0.0f, 0.0f));
vtx[17] = VertexTextureNormalData(Vec3(-w2, +h2, +d2), Vec2(0.0f, 0.0f), Vec3(-1.0f, 0.0f, 0.0f));
vtx[18] = VertexTextureNormalData(Vec3(-w2, +h2, -d2), Vec2(1.0f, 0.0f), Vec3(-1.0f, 0.0f, 0.0f));
vtx[19] = VertexTextureNormalData(Vec3(-w2, -h2, -d2), Vec2(1.0f, 1.0f), Vec3(-1.0f, 0.0f, 0.0f));
// 오른쪽면
vtx[20] = VertexTextureNormalData(Vec3(+w2, -h2, -d2), Vec2(0.0f, 1.0f), Vec3(1.0f, 0.0f, 0.0f));
vtx[21] = VertexTextureNormalData(Vec3(+w2, +h2, -d2), Vec2(0.0f, 0.0f), Vec3(1.0f, 0.0f, 0.0f));
vtx[22] = VertexTextureNormalData(Vec3(+w2, +h2, +d2), Vec2(1.0f, 0.0f), Vec3(1.0f, 0.0f, 0.0f));
vtx[23] = VertexTextureNormalData(Vec3(+w2, -h2, +d2), Vec2(1.0f, 1.0f), Vec3(1.0f, 0.0f, 0.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);
}
각 면마다 고정된 노멀값이 있다는 게 특징이다.
3) CreateSphere
Sphere 구 같은 경우에는
void GeometryHelper::CreateSphere(shared_ptr<Geometry<VertexTextureNormalData>> geometry)
{
float radius = 0.5f; // 구의 반지름
uint32 stackCount = 20; // 가로 분할
uint32 sliceCount = 20; // 세로 분할
vector<VertexTextureNormalData> vtx;
VertexTextureNormalData v;
// 북극
v.position = Vec3(0.0f, radius, 0.0f);
v.uv = Vec2(0.5f, 0.0f);
v.normal = v.position;
v.normal.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();
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();
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);
}
각 위치마다 자신의 포지션 자체가 normal 벡터 방향이 된다.
Grid는 높이맵을 적용시킨 다음에 다시 구해야 하기 때문에 높이맵이 없는 상태에서는 그냥 단일 방향으로 일반적인 코드랑 다를 바가 없다. 중요하지 않으니까 나중에 따로 챙기도록 한다.
Engine을 빌드한다.
Cube랑 Sphere로 테스트를 할 것이다.
3. 08. NormalDemo 클래스 생성하고 Main에 세팅하기
늘 하던 데로 탐색기로 가서 07. HeightMapDemo클래스를 복제해서 이름을 08. NormalDemo라고 한다. 그리고 Clienet/Game필터에 넣어준다. 코드도 NormalDemo에 맞게 수정한다.
Normal vector를 테스트하는 단순한 의미라고 보면 된다.
Main에 가서
#include "08. NormalDemo.h"
desc.app = make_shared<NormalDemo>(); // 실행 단위
이렇게 추가한다.
4. 07. Normal.fx 생성하고 세팅하기
1) 07. Normal.fx 생성하고 NormalDemo::Init에서 _shader 만들 때 사용하기
탐색기의 Shaders 폴더에 가서 06. Terrain.fx를 복제해서 이름을 07. Normal.fx로 한다.
그리고 NormalDemo::Init에서
_shader = make_shared<Shader>(L"07. Normal.fx");
이렇게 세팅한다.
2) VertexInput, VertexOutput에 NORMAL 추가하기
07. Normal.fx 쉐이더부터 작업을 해보도록 하자.
float3 LightDir;
를 선언한다. 조명을 쏘는 방향이다.
float3 normal : NORMAL;
을 VertexInput, VertexOutput에 추가했다.
struct VertexInput
{
float4 position : POSITION; // POSITION을 찾아서 연결해 줄 것이다.
float2 uv : TEXCOORD;
float3 normal : NORMAL;
};
struct VertexOutput
{
float4 position : SV_POSITION; // System Value 라고 예약된 이름이라 명시
float2 uv : TEXCOORD;
float3 normal : NORMAL;
};
3) 3x3 world를 normal에 곱해줘서 normal에 회전값만 적용해주기
VS에서 uv는 그대로 토스했지만, normal의 경우는 그대로 토스하면 안 된다.
회전하거나 하면 방향이 유지되는 것이 아니라 회전값에 맞춰서 바뀔 수가 있다.
Vector에 translation은 무시하고 회전값만 적용시키고 싶을 때 사용하던 문법은 x, y, z, w에서 w에 0을 넣어주면 방향을 무시하고, 1을 넣어주면 translation까지 적용이 된다고 했었다.
그거를 위해서 transformCoord나 transformNormal 두 개 중에서 하나를 사용했다.
행렬을 적용시키는데 translation을 할 것인가 회전만 할 것인가에 따라서
input.normal에 곱해줄 때 Vector4에다가 0을 넣어서 해도 되고, 아니면 3x3 world로 바꿔서 곱하는 것도 하나의 방법이다. 그러면 애당초 마지막 네 번째 줄은 무시가 되기 때문에 translform normal을 한 효과를 이렇게 낼 수 있다.
VertexOutput VS( VertexInput input)
{
VertexOutput output;
output.position = mul(input.position, World);
output.position = mul(output.position, View);
output.position = mul(output.position, Projection);
output.uv = input.uv;
output.normal = mul(input.normal, (float3x3) World);
return output;
}
4) SampleState 디폴트 값으로 돌리기
SampelerState Sampler0 묘사부를 삭제하고 이것만 남긴다.
SamplerState Sampler0;
5. NormalDemo::Render에서 07. Normal.fx의 float3 LightDir를 세팅하기
PS에서
빛이 하얀색이라 가정을 하고,
float4 PS(VertexOutput input) : SV_TARGET
{
float3 normal = normalize(input.normal);
float3 light = -LightDir;
return float4(1, 1, 1, 1) * dot(light, normal);
// return Texture0.Sample(Sampler0, input.uv);
}
이렇게 했을 때 빛이 정확하게 수직으로 떨어진다면 하얀색으로 뜨겠지만
그게 아니라면 dot의 결과가 1보다 작은 값이 될 테니까 점점 희미해질 것이다.
1) NormalDemo::Init() 코드 정리 하기
지금 _heightMap은 사용 안 할 것이니 08. NormalDemo클래스에서 관련 코드 삭제한다.
_geometry도 VertexTextureData에서 VertexTextureNormalData로 바꿔준다.
void NormalDemo::Init()
{
_shader = make_shared<Shader>(L"07. Normal.fx");
// Object
_geometry = make_shared<Geometry<VertexTextureNormalData>>();
GeometryHelper::CreateCube(_geometry);
// GeometryHelper::CreateSphere(_geometry);
_vertexBuffer = make_shared<VertexBuffer>();
_vertexBuffer->Create(_geometry->GetVertices());
_indexBuffer = make_shared<IndexBuffer>();
_indexBuffer->Create(_geometry->GetIndices());
// Camera
_camera = make_shared<GameObject>();
_camera->GetOrAddTransform();
_camera->AddComponent(make_shared<Camera>());
_camera->AddComponent(make_shared<CameraScript>());
// Texture
_texture = RESOURCES->Load<Texture>(L"Veigar", L"..\\\\Resources\\\\Textures\\\\veigar.jpg");
}
이렇게 HeightMap 부분을 삭제했다.
2) NormalDemo.h에서 빛의 방향을 조절하는 변수 _lightDir를 선언하고 초기화 하기
NormalDemo.h에
Vec3 _lightDir = Vec3(0.f, -1.f, 0.f);
지금은 빛이 아래 방향으로 가게 해 줬다. 이런 식으로 빛을 조절할 수 있다.
Vec3 _lightDir = Vec3(1.f, 0.f, 0.f);
이렇게 하면 빛이 오른쪽으로 가게 된다.
3) NormalDemo::Render에서 빛의 방향 변수 _lightDir를 07. Normal.fx의 float3 LightDir에 세팅하기
이걸 NormalDemo::Render에서
_shader->GetVector("LightDir")->SetFloatVector((float*)&_lightDir);
이렇게 07. Normal.fx의 float3 LightDir에 _lightDir를 넣어 줬다.
6. 테스트하기
1) PS에서 return float4(1, 1, 1, 1) * dot(light, normal); 로 한 경우
빌드 후 실행해 보면
왼쪽 면만 하얀색이다.
세팅할 때 빛이 오른쪽으로 가게 해서 그렇다.
왼쪽 면은 사이각이 0도가 되어서 가장 강한 빛이 된 것이고,
나머지 면은 빛을 못 받고 있는 것이다.
만약 바라보는 정면을 하얀색으로 바꾸고 싶다면
Vec3 _lightDir = Vec3(0.f, 0.f, 1.f);
이렇게 하면 빛이 look 방향으로 쏘고 있는 거니까
정면만 밝고 나머지는 어두워진 것을 볼 수 있다.
2) PS에서 return Texture0.Sample(Sampler0, input.uv) * dot(light, normal);로 한 경우
이번엔 PS에서
float4 PS(VertexOutput input) : SV_TARGET
{
float3 normal = normalize(input.normal);
float3 light = -LightDir;
// return float4(1, 1, 1, 1) * dot(light, normal);
return Texture0.Sample(Sampler0, input.uv) * dot(light, normal);
}
다시 Sampler0을 사용하는 버전으로 바꿔본다.
옆면은 안 보이는 것을 알 수 있다.
3) NormalDemo::Init에서 CreateCube가 아닌 CreateSphere로 한 경우
큐브는 normal 벡터가 단조롭다.
Shpere로 바꿔 보자
void NormalDemo::Init()
{
_shader = make_shared<Shader>(L"07. Normal.fx");
// Object
_geometry = make_shared<Geometry<VertexTextureNormalData>>();
//GeometryHelper::CreateCube(_geometry);
GeometryHelper::CreateSphere(_geometry);
빛도 위에서 아래로 내리는 걸로 바꿔보자.
Vec3 _lightDir = Vec3(0.f, -1.f, 0.f);
이렇게 뜬다.
나중엔 뭔가 연산을 더해서 그라디에이션이 들어가게 해야겠지만
기본적으로 쉐이더를 다루면서 빛 연산을 한다는 건 이런 느낌이다.
7. 정리
각 지점마다 이런 식으로 normal 벡터를 만들어 줬고,
빛은 일정하게 내리꽂는데 그 방향에 따라가지고 영향을 얻을 수 있다.
사이각이 0도가 될 때 가장 세고
90도가 되면 사실상 빛이 없는 것처럼 되게 식을 만들었다.
단위 백터끼리 내적을 할 때 cos각도 가 되는데, 0도일 때 가 1, 90도 일 때 가 0이 되기 때문이다.
PS에서 어떤 색상으로 나타내야 한다는 걸 나타내고 있는 건데
만약
float4(1, 1, 1, 1) * 0.3f
이렇게 되면 그만큼 어두워지게 되는 것이고,
0도일 경우는
float4(1, 1, 1, 1) * 1
이렇게 되는 것이다.
나중에는 여기에 power를 이용해서 5 제곱을 한다거나 더 명확하게 효과가 나게 만들어 줄 수 있다.
결국 픽셀 셰이더에서 조명에 대한 방향벡터와 노멀 벡터의 방향벡터를 2개를 얻어 온 다음에 그걸 이용해서 연산을 해주는 게 핵심이다.
float3 LightDir;
은 World 기준으로 한 좌표일까, 물체 기준으로 한 좌표일까?
기본적으로 월드 좌표를 기반으로 연산을 한 것이다.
return float4(1, 1, 1, 1) * dot(light, normal);
PS에서 첫 번째 버전에서는 빛의 밝기를 잘 먹고 있는지 테스트할 수 있고,
return Texture0.Sample(Sampler0, input.uv) * dot(light, normal);
두 번째 버전에서는 그것에 따라 세기 강도를 조절하는 부분을 뒤에다 붙여서 넣어 놨다.
앞으로는 이런 식으로 작업을 하게 될 것이다.
여러 빛을 조금씩 섞어서 최종 결과물을 나타내게 될 것이다.
여러 가지 기법들을 이용하게 될 건데 이런 수학이론은 기본으로 알고 있어야 한다.
물체가 복잡해지면 heightMap처럼 normalMap이라는 개념이 생겨서 그 normalMap을 기반으로 하는 연산을 하게 될 것이다.
그 normalMap을 넘기려면
Texture2D Texture0;
이런 텍스쳐를 하나 더 넘겨야 한다.