이번에는 Normal Map을 사용해 바닥 표면을 표현했다.
알베도 텍스처와 Normal Map을 적용하여 표면의 질감과 조명이 자연스럽게 반응하도록 만들었다. Tangent-Bitangent-Normal (TBN) 행렬을 활용해 조명 계산을 위한 텍스처 공간(Tangent Space) 변환을 적용하여 표면의 기복에 따라 조명이 반사되도록 구현하였다.
바닥 텍스처 및 모델 초기화 코드
먼저 바닥에 사용할 알베도 텍스처와 Normal Map을 로드하고, 바닥의 크기와 회전 정보를 설정했다.
// Normal Map 셰이더 프로그램 로드
m_normalProgram = Program::Create("./shader/normal.vs", "./shader/normal.fs");
// 바닥 알베도 텍스처와 Normal Map 텍스처 로드
m_groundAlbedo = Texture::CreateFromImage(Image::Load("./image/Old_Plastered_Stone_Wall_1_Diffuse.png").get());
m_groundNormal = Texture::CreateFromImage(Image::Load("./image/Old_Plastered_Stone_Wall_1_Normal.png").get());
렌더링 코드
이 코드는 Normal Map을 포함한 알베도 텍스처를 바닥에 적용하여 렌더링하는 부분이다. 조명 위치와 카메라 위치, 모델 변환 행렬 등을 셰이더로 전달하고, 바닥에 알맞은 크기와 회전을 적용했다.
// Normal Map 셰이더 프로그램 사용
m_normalProgram->Use();
// 카메라 위치와 조명 위치 유니폼 설정
m_normalProgram->SetUniform("viewPos", m_cameraPos);
m_normalProgram->SetUniform("lightPos", m_lightPos);
// 알베도 텍스처 바인딩 및 유니폼 설정
glActiveTexture(GL_TEXTURE0);
m_groundAlbedo->Bind();
m_normalProgram->SetUniform("diffuse", 0);
// Normal Map 텍스처 바인딩 및 유니폼 설정
glActiveTexture(GL_TEXTURE1);
m_groundNormal->Bind();
m_normalProgram->SetUniform("normalMap", 1);
// 기본 텍스처 유닛으로 복귀
glActiveTexture(GL_TEXTURE0);
// 모델 변환 행렬 설정: 크기와 회전 조정
auto model = glm::mat4(1.0f);
model = glm::scale(model, glm::vec3(30.0f)); // 바닥의 크기 조정
model = glm::rotate(model, glm::radians(-90.0f), glm::vec3(1.0f, 0.0f, 0.0f)); // 바닥을 -90도 회전시켜 평면 방향 조정
m_normalProgram->SetUniform("transform", projection * view * model); // 최종 변환 행렬
m_normalProgram->SetUniform("modelTransform", model); // 모델 좌표계 변환 행렬
// 바닥 평면 렌더링
m_plane->Draw(m_normalProgram.get());
Vertex Shader (normal.vs
)
Vertex Shader에서는 정점의 위치와 텍스처 좌표를 계산한 뒤, 조명 처리를 위해 필요한 Normal과 Tangent를 변환하여 Fragment Shader로 전달한다. 이때 Tangent Space
변환을 위해 모델의 modelTransform
행렬을 기반으로 normal과 tangent 벡터를 변환하여 Fragment Shader에 전달한다.
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoord;
layout (location = 3) in vec3 aTangent;
uniform mat4 transform;
uniform mat4 modelTransform;
out vec2 texCoord;
out vec3 position;
out vec3 normal;
out vec3 tangent;
void main() {
gl_Position = transform * vec4(aPos, 1.0); // 최종 화면 공간에서의 위치 계산
texCoord = aTexCoord; // 텍스처 좌표 전달
// 모델 좌표에서 월드 좌표계로 변환된 정점 위치
position = (modelTransform * vec4(aPos, 1.0)).xyz;
// Normal과 Tangent를 모델 변환 행렬에 맞게 변환 (역행렬과 전치 사용)
mat4 invTransModelTransform = transpose(inverse(modelTransform));
normal = (invTransModelTransform * vec4(aNormal, 0.0)).xyz; // 변환된 Normal
tangent = (invTransModelTransform * vec4(aTangent, 0.0)).xyz; // 변환된 Tangent
}
- gl_Position 계산:
transform
행렬을 사용해 최종 위치를 계산하고gl_Position
에 전달한다. 이 행렬에는 카메라의 투영 및 뷰 행렬이 포함되어 있어, 모델 좌표에서 화면 좌표로 변환하는 역할을 한다. - 텍스처 좌표 전달:
aTexCoord
를texCoord
에 전달하여 Fragment Shader에서 텍스처를 참조할 수 있게 한다. - 월드 좌표계에서의 정점 위치 계산:
modelTransform
을 통해 정점의 위치를 모델 좌표에서 월드 좌표로 변환한다. 변환된 정점의 위치를position
으로 전달하여 Fragment Shader에서 조명 계산에 사용할 수 있게 한다. - Normal과 Tangent 변환:
Normal
과Tangent
벡터를 모델 변환에 맞게 변환하기 위해modelTransform
의 전치된 역행렬(transpose(inverse(modelTransform))
)을 사용했다. 이 과정을 통해 각 정점의 Normal과 Tangent가 월드 좌표계에 맞춰 변환되어 Fragment Shader로 전달된다.
Fragment Shader (normal.fs
)
Fragment Shader에서는 조명 계산을 수행하는데, 여기서 가장 중요한 작업은 Normal Map을 기반으로 조명을 조정하는 것이다. TBN 행렬을 이용해 텍스처의 Normal 정보를 Tangent Space로 변환하여 조명 방향을 동적으로 반영한다. 또한 Ambient, Diffuse, Specular 조명을 적용해 사실적인 조명 효과를 구현했다.
#version 330 core
in vec2 texCoord;
in vec3 position;
in vec3 normal;
in vec3 tangent;
out vec4 fragColor;
uniform vec3 viewPos;
uniform vec3 lightPos;
uniform sampler2D diffuse; // 알베도 텍스처
uniform sampler2D normalMap; // 노멀 맵 텍스처
void main() {
// 알베도 텍스처에서 색상 가져오기
vec3 texColor = texture(diffuse, texCoord).xyz;
// Normal Map에서 노멀 벡터를 샘플링하고 [-1,1] 범위로 변환
vec3 texNorm = texture(normalMap, texCoord).xyz * 2.0 - 1.0;
// Tangent Space 변환을 위한 TBN 행렬 생성
vec3 N = normalize(normal); // 정규화된 Normal 벡터
vec3 T = normalize(tangent); // 정규화된 Tangent 벡터
vec3 B = cross(N, T); // Normal과 Tangent의 외적을 통해 Bitangent 계산
mat3 TBN = mat3(T, B, N); // TBN 행렬 생성
// Normal Map의 텍스처 노멀을 Tangent Space에서 조명에 맞게 변환
vec3 pixelNorm = normalize(TBN * texNorm);
// Ambient 조명 계산
vec3 ambient = texColor * 0.2;
// Diffuse 조명 계산 (조명과 Normal 간의 각도에 비례한 조명)
vec3 lightDir = normalize(lightPos - position);
float diff = max(dot(pixelNorm, lightDir), 0.0);
vec3 diffuse = diff * texColor * 0.8;
// Specular 조명 계산 (하이라이트 반사 계산)
vec3 viewDir = normalize(viewPos - position);
vec3 reflectDir = reflect(-lightDir, pixelNorm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = spec * vec3(0.5);
// 최종 색상 계산
fragColor = vec4(ambient + diffuse + specular, 1.0);
}
- 텍스처에서 색상 및 Normal 정보 가져오기:
texColor
는 알베도 텍스처에서 샘플링된 색상 정보이다.texNorm
은 Normal Map에서 샘플링된 노멀 벡터를 [0, 1]에서 [-1, 1]로 조정한 것이다. Normal Map은 일반적으로 RGB로 노멀 정보를 저장하므로, 이를 조명 계산에 사용하기 위해서는 좌표를 변환해야 한다.
- TBN 행렬 생성:
- Normal (N), Tangent (T), Bitangent (B) 벡터를 조합해
TBN
행렬을 구성한다. 이 행렬은 텍스처의 Tangent Space에서 조명을 반영하기 위한 변환 행렬로 사용된다. B
는N
과T
의 외적을 통해 계산한다. 이렇게 하면 각 픽셀의 방향이 TBN 행렬에 의해 카메라와 조명에 맞춰 변환된다.
- Normal (N), Tangent (T), Bitangent (B) 벡터를 조합해
- Normal Map 기반 조명 계산:
pixelNorm
은 TBN 행렬을 통해 변환된 최종 노멀 벡터로, Normal Map을 기반으로 Tangent Space에서 조명 방향을 반영한다. 이 벡터는 조명 계산에 사용되는 최종 노멀 벡터이다.
- Ambient 조명:
ambient
는 기본적인 주변광 효과로,texColor
에 0.2의 값을 곱하여 설정하였다. 이는 장면의 전반적인 조명 수준을 결정하는 역할을 한다.
- Diffuse 조명:
- 조명 방향(
lightDir
)과 변환된 노멀 벡터(pixelNorm
) 사이의 내적을 통해 확산 조명(Diffuse)을 계산한다. 이 값이 높을수록 픽셀의 조명이 밝아진다. diffuse
는texColor
에diff
를 곱한 값으로, 조명과 표면의 각도에 비례한 밝기를 표현한다.
- 조명 방향(
- Specular 조명:
- 카메라 방향(
viewDir
)과 반사
reflectDir
) 사이의 내적을 이용해 반사광을 계산한다. 내적 값이 클수록 빛의 하이라이트가 강해진다.spec
은 하이라이트 정도를 조절하는shininess
값으로 설정하며, 여기서는 32를 사용했다.
- 카메라 방향(
- 최종 색상 계산:
fragColor
는 ambient, diffuse, specular 조명을 합쳐 최종 색상을 계산한다. 이 값이 최종적으로 픽셀에 반영되어 화면에 출력된다.
결과
'Computer Graphics > ShaderPixel' 카테고리의 다른 글
ShaderPixel - 5. 색 구슬 구현 (0) | 2024.11.17 |
---|---|
ShaderPixel - 4. 버텍스 셰이더에서 계산한 노말 값과 월드 좌표는 실제로 프래그먼트에 그려지는 픽셀의 월드 좌표와는 다르다 (2) | 2024.11.15 |
ShaderPixel - 3. 렌더링 전략 고민.. (1) | 2024.11.13 |
ShaderPixel - 1. 배경 구성 (Skybox) (0) | 2024.11.11 |
ShaderPixel - 0. 과제 해석 (0) | 2024.10.30 |