
평면의 텍스쳐에 normal map이라는걸 이용해서 입체감을 주는 방식임

왼쪽의 텍스쳐를 normal map을 이용하면 오른쪽 텍스쳐가 되는거임
normal map은 간단하게 설명하면
개별의 fragment의 normal을 임의로 수정한 텍스쳐를 이용해서 빛 반사를 다르게 만드는거임

또한 normal mapping을 bump mapping이라고도 함
기본적으로 normal vector, 법선 벡터는 -1~1 사이의 값을 가짐
왜 -1~1사이의 값이냐면... 정규화된 길이의 벡터이기 때문ㅇㅇ
하지만 우리가 제공받는 normal map텍스쳐는 0~1사이의 값임
따라서 이를 사용하려면 다시 -1~1사이의 값을 가지도록 값을 바꿔줘야함
uniform sampler2D normalMap;
void main()
{
// [0,1] 범위의 노멀 맵에서 법선 얻기
normal = texture(normalMap, fs_in.TexCoords).rgb;
// 법선 벡터를 [-1,1] 범위로 변환
normal = normalize(normal * 2.0 - 1.0);
[...]
// 정상적으로 조명 처리 진행
}

기본적으로 normal map텍스쳐는 이렇게 생김
이런 색상인 이유는 대부분 텍셀에 대해 z축이 사용자를 바라보는, 1의 값이므로 파란색상을 띄는거임
상단은 양의 y축을 바라보고 있으니, y가 1인 녹색에 가까움ㅇㅇ
벽돌 사이사이는 어두운색인 이유도 이거임ㅇㅇ
근데 문제가 잇음


이 두 이미지의 차이가 보임?
아래의 텍스쳐는 제대로 normal mapping이 된것처럼 보이지 않음
그 이유는 normal map텍스쳐는 위에서 설명했듯이, 양의 z축으로 normal이 이루어져 있음
근데 z축방향에서 빛이 오는게 아닌 y축이나 x축의 비스듬한 각도에서 빛이 오면??
normal은 z축을 바라보고 있는데, 빛은 그 축이 아니니 이상하게 normal mapping이 된거임
그럼 이걸 어떻게 해결하냐??
1번은 고해상도 텍스쳐의 경우 그걸 다 만들기에는 무리가 있음
고해상도 텍스쳐가 아니라면 변환행렬 이용해서 계산하는것보다는 효율이 괜찮을지도??
보통은 2번을 사용함
2번과 같은 방식을 행하는 공간이 탄젠트 공간임
오브젝트는 삼각형들로 이루어져 있음
이러한 오브젝트는 월드공간에서 문제가 없음
다만 오브젝트의 텍스쳐가 문제가 있는거임
위에서 설명했다싶이
x,y,z가 고정된 상태로 어떤 면은 아래, 어떤면은 위, 어떤면은 오른쪽,,,
이렇게 바라보는데 빛의 방향이 바뀔때도 이게 유지가 된다면...
빛 반사에 오류가 생김
이때 텍스쳐의 x,y,z는 각 표면의 삼각형에 국한된,
표면에 relate한 좌표라는거임
이 좌표공간이 tangent space임
따라서 이 각 표면 삼각형에 relate한 탄젠트 공간 좌표를 밖으로 꺼내서,
월드 공간 좌표로 변환해, 빛이 어느방향에서 오든 알맞은 방향으로 빛이 발산되도록 하는게 목표인거임
이때 탄젠트 공간 -> 월드 공간으로의 변환을 수행하는 행렬을
TBN행렬이라고 함
Tangent, BitTangent, Normal이라는 3개의 벡터로 이루어진 변환행렬임

사실 uv에 normal을 합쳐서 재명칭을 붙였다고 봐야함
x축이 tangent벡터, y축이 bittangent벡터, z축이 normal벡터가 되는거임

내부 검은 삼각형을 라고 해보자
검은 삼각형은 해당 텍스쳐가 매핑하게 될 오브젝트의 특정 삼각형이라는 가정임
을 빗변으로 가지는 삼각형을
을 빗변으로 가지는 삼각형을
라고 함
우리는 에서 이 혹은 의 일부분이라는걸 알고 있음
마찬가지로 은 혹은 의 일부분이라는것도 알고 있음
벡터의 삼각형법에 의해 이 됨
1번과 마찬가지로 식에 대입하면
가 되는걸 할 수 있음
라는 식은
으로 분해가 가능
라는 식은
으로 분해가 가능
먼저 월드 좌표로의 행렬 변환이 되어야 하는 이유가 있음
텍스쳐는 UV라는 좌표계를 이용함
ㅤ
단순히 텍스쳐 로컬에서만 살펴본다면 , 임
ㅤ
하지만 이 좌표는 말했듯, 텍스쳐 로컬 좌표고, 오브젝트에 매핑되는 순간 우리는 해당 텍스쳐의 좌표를 알지 못함
ㅤ
우리는 정확한 normal mapping을 위해 로컬 텍스쳐 좌표가 아닌, 월드 텍스쳐 좌표
즉, 오브젝트 위에서 사용된 텍스쳐의 월드 좌표가 필요한거임
ㅤ
따라서 오브젝트는 삼각형으로 만들어져있고,
매핑된 텍스쳐의 삼각형 부분을 사용해서 텍스쳐를 월드 좌표로 변환시키는거임
위 2개의 분해된 연립일차방정식을 행렬로 표현하면 아래와 같음
우리는 가 필요하므로, 행렬의 이항정리를 통해 식을 약간 바꿔줌
역행렬은 행렬식을 사용해 표현할 수 있음
역행렬
ㅤ
라는 행렬이 있음
ㅤ
임
- 는 행렬 A에 대한 행렬식임
행렬식 공식은 쉬움
ㅤ- 는 행렬 A의 수반행렬임
역행렬 구하기 - 수학하는 수영을 참고해서 구하면 됨
2*2크기의 수반행렬은 그냥 대각성분 바꾸고 부호만 제대로 넣어주면 됨
ㅤ
따라서 식을 전개해보면
이 된다는걸 알 수 있음
// positions
glm::vec3 pos1(-1.0f, 1.0f, 0.0f); //좌상단
glm::vec3 pos2(-1.0f, -1.0f, 0.0f); //좌하단
glm::vec3 pos3( 1.0f, -1.0f, 0.0f); //우하단
glm::vec3 pos4( 1.0f, 1.0f, 0.0f); //우상단
// texture coordinates
glm::vec2 uv1(0.0f, 1.0f); //좌상단
glm::vec2 uv2(0.0f, 0.0f); //좌하단
glm::vec2 uv3(1.0f, 0.0f); //우하단
glm::vec2 uv4(1.0f, 1.0f); //우상단
// normal vector
glm::vec3 nm(0.0f, 0.0f, 1.0f);
먼저 위 코드처럼 좌표계에 맞춰
순서를 맞춰 position과 texture좌표와 normal을 선언함
사실 이부분은 모델을 쓰면 자동으로 들어와있음
그리고 우리는 , 를 먼저 벡터 뺄셈을 통해 구할 수 있음
마찬가지로 uv의 도 구할 수 있음
glm::vec3 edge1 = pos2 - pos1;
glm::vec3 edge2 = pos3 - pos1;
glm::vec2 deltaUV1 = uv2 - uv1;
glm::vec2 deltaUV2 = uv3 - uv1;
그리고 이제 이걸 이용해서 위에서 유도한 공식에 대입해서
tangent space -> world space변환행렬을 구하면 됨
glm::vec3 tangent1, bitangent1;
glm::vec3 tangent2, bitangent2;
float f = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV2.x * deltaUV1.y); //행렬식
// triangle 1
// ----------
glm::vec3 edge1 = pos2 - pos1;
glm::vec3 edge2 = pos3 - pos1;
glm::vec2 deltaUV1 = uv2 - uv1;
glm::vec2 deltaUV2 = uv3 - uv1;
tangent1.x = f * (deltaUV2.y * edge1.x - deltaUV1.y * edge2.x);
tangent1.y = f * (deltaUV2.y * edge1.y - deltaUV1.y * edge2.y);
tangent1.z = f * (deltaUV2.y * edge1.z - deltaUV1.y * edge2.z);
bitangent1.x = f * (-deltaUV2.x * edge1.x + deltaUV1.x * edge2.x);
bitangent1.y = f * (-deltaUV2.x * edge1.y + deltaUV1.x * edge2.y);
bitangent1.z = f * (-deltaUV2.x * edge1.z + deltaUV1.x * edge2.z);
// triangle 2
// ----------
edge1 = pos3 - pos1;
edge2 = pos4 - pos1;
deltaUV1 = uv3 - uv1;
deltaUV2 = uv4 - uv1;
f = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV2.x * deltaUV1.y);
tangent2.x = f * (deltaUV2.y * edge1.x - deltaUV1.y * edge2.x);
tangent2.y = f * (deltaUV2.y * edge1.y - deltaUV1.y * edge2.y);
tangent2.z = f * (deltaUV2.y * edge1.z - deltaUV1.y * edge2.z);
bitangent2.y = f * (-deltaUV2.x * edge1.y + deltaUV1.x * edge2.y);
bitangent2.z = f * (-deltaUV2.x * edge1.z + deltaUV1.x * edge2.z);
bitangent2.x = f * (-deltaUV2.x * edge1.x + deltaUV1.x * edge2.x);
각각의 tangent를 구하는게 왜이런건지 모르겠다면
이렇게 전개되는 행렬곱을 생각해보쟈~
먼저 vertex shader
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;
layout (location = 3) in vec3 aTangent;
layout (location = 4) in vec3 aBitangent;
out VS_OUT {
vec3 FragPos;
vec2 TexCoords;
vec3 TangentLightPos;
vec3 TangentViewPos;
vec3 TangentFragPos;
} vs_out;
uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;
uniform vec3 lightPos;
uniform vec3 viewPos;
void main()
{
//1
vs_out.FragPos = vec3(model * vec4(aPos, 1.0));
vs_out.TexCoords = aTexCoords;
//2
mat3 normalMatrix = transpose(inverse(mat3(model)));
//3
vec3 T = normalize(normalMatrix * aTangent);
vec3 N = normalize(normalMatrix * aNormal);
T = normalize(T - dot(T, N) * N);
vec3 B = cross(N, T);
//4
mat3 TBN = transpose(mat3(T, B, N));
vs_out.TangentLightPos = TBN * lightPos;
vs_out.TangentViewPos = TBN * viewPos;
vs_out.TangentFragPos = TBN * vs_out.FragPos;
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
천천히 보면 쉬움
1~4까지 설명해줌
정점들에 model행렬을 곱해서 world공간으로 정점을 이동
normal벡터의 변환행렬로 Normal Matrix, 법선 행렬로 불림
tangent, bittangent, normal을 탄젠트 공간에서 world공간으로 넘겨주기 위해 구한 NormalMatrix를 곱해줌
T = normalize(T - dot(T, N) * N);는 그람 슈미트 과정이라고 부름
그람 슈미트 과정은 하나의 벡터를 이용해 직교(90도)벡터를 만드는 공식임
T,N을 구했을때, 두 벡터는 완전한 직교상태가 아닐 수 있으므로, 이를 완벽하게 정규직교상태로 만들어주기 위해 그람 슈미트를 이용함
vec3 B = cross(N, T);는 입력으로 받은 bitTangent를 사용하지 않고 있음
이유는 1번에서 T,N을 정규직교화 했기때문에, 기존의 aTangent, aNormal을 변환했을때와 약간의 차이가 있을 수 있음
따라서 우리는 새롭게 구한 T,N으로 bitTangent를 새롭게 구해줌
여기서 4번이 조금 문제임
우리는 2가지 경우를 생각할 수 있음
1번이 가장 직관적임
그냥 TBN구한걸로 행렬 만들어서 넘기면 됨
2번은 조금 복잡함
mat3 normalMatrix = transpose(inverse(mat3(model)));
vec3 T = normalize(normalMatrix * aTangent);
vec3 N = normalize(normalMatrix * aNormal);
T = normalize(T - dot(T, N) * N);
vec3 B = cross(N, T);
우리는 벡터 T와 N을 그람 슈미트 과정을 통해 정규 직교가 되도록 만들었음
그리고 서로 수직인 두 벡터를 외적하면 외적된 벡터또한 두 벡터에 대해 수직임
그리고 직교기저의 역행렬 = 전치행렬임
월드공간의 TBN행렬을 다시 접선 공간으로 바꿔 계산해도 연산결과는 동일하다는거임ㄷㄷ
따라서 월드공간의 TBN행렬을 접선공간(전치)으로 다시 바꾼 후,
할 일이 많은 fragment shader가 아닌, vertex shader에서 미리 빛, view, FragPos를 계산해서 사용하는거임
그럼 자연스럽게 normal을 따로 TBN행렬을 이용해 연산할 필요도 없어지는겨~
| 1번 | 2번 |
|---|---|
| 접선 공간의 벡터들을 월드 공간으로 바꿔 계산 | 월드 공간에 존재하는 TBN행렬을 접선 공간으로 바꿔서 light,view,FragPos만 먼저 계산후 빛 반사만 fragment에서 연산 |
| fragment shader에서 연산 | vertex shader에서 연산 |
따라서 4번의
mat3 TBN = transpose(mat3(T, B, N));
vs_out.TangentLightPos = TBN * lightPos;
vs_out.TangentViewPos = TBN * viewPos;
vs_out.TangentFragPos = TBN * vs_out.FragPos;
라고
TBN의 전치를 통해 접선공간으로 바꾼후...
왜 접선공간인지 모르겠다면, AI에게 TBN행렬의 전치행렬이 왜 접선공간으로 바꾸는 행렬이 되는건지 물어보도록! 그냥 수식 계산이라 귀찮음...
ㅤ
단순하게 접근하면 TBN행렬은 탄젠트->월드 공간으로의 변환행렬이므로,
월드->탄젠트 공간으로 변환하려면 TBN의 역행렬을 사용하면 됨
그리고 직교기저의 전치행렬 = 역행렬이어서 연산비용이 적은 전치행렬로 같은 효과를 낼 수 있어 사용하는거
TBN행렬의 전치행렬이 왜 접선공간으로의 행렬이 되는지 알려줘
light, view, FragPos를 계산해서 그걸 fragment shader로 옮긴 후 사용함
최적화도 됨!
그냥 입력된 값들로 blinn phong을 쓰면 됨
#version 330 core
out vec4 FragColor;
in VS_OUT {
vec3 FragPos;
vec2 TexCoords;
vec3 TangentLightPos;
vec3 TangentViewPos;
vec3 TangentFragPos;
} fs_in;
uniform sampler2D diffuseMap;
uniform sampler2D normalMap;
uniform vec3 lightPos;
uniform vec3 viewPos;
void main()
{
// obtain normal from normal map in range [0,1]
vec3 normal = texture(normalMap, fs_in.TexCoords).rgb;
// normal벡터를 0-1범위에서 -1~1범위로 바꿈
normal = normalize(normal * 2.0 - 1.0); // 탄젠트공간
// get diffuse color
vec3 color = texture(diffuseMap, fs_in.TexCoords).rgb;
// ambient
vec3 ambient = 0.1 * color;
// diffuse
vec3 lightDir = normalize(fs_in.TangentLightPos - fs_in.TangentFragPos);
float diff = max(dot(lightDir, normal), 0.0);
vec3 diffuse = diff * color;
// specular
vec3 viewDir = normalize(fs_in.TangentViewPos - fs_in.TangentFragPos);
vec3 reflectDir = reflect(-lightDir, normal);
vec3 halfwayDir = normalize(lightDir + viewDir);
float spec = pow(max(dot(normal, halfwayDir), 0.0), 32.0);
vec3 specular = vec3(0.2) * spec;
FragColor = vec4(ambient + diffuse + specular, 1.0);
}

평면의 texture인데도 불구하고 빛의 위치, 반사가 바로바로 적용되는걸 볼 수 이뜸~